用 StringBuilder 拼接 iCalendar 最直接:需严格遵循 RFC 5545,手动处理时区(UTC 加 Z 或带偏移)、软换行、UID 唯一性、BASE64 编码中文、UTF-8 无 BOM 保存及 DTSTAMP/SEQUENCE 规则。

用 StringBuilder 拼接标准 iCalendar 格式最直接
iCalendar(.ics)本质是纯文本,RFC 5545 规范定义了严格的字段顺序、换行和转义规则。C# 中不推荐用 XmlSerializer 或 JSON 库生成,也不必引入重型库(如 Ical.Net)——除非你需处理重复事件、时区转换或日历订阅等复杂逻辑。对单个会议、提醒类事件,StringBuilder 手动拼接更可控、无依赖、不易出编码问题。
关键点:
-
DTSTART和DTEND必须用 UTC 时间(末尾加Z)或带偏移的本地时间(如+0800),不能只写“20240520T140000”而不声明时区 - 每行不能超过 75 字节,超长需软换行(行末加
\r\n,注意空格)——但现代日历客户端(Outlook、Apple Calendar)大多容忍,可暂不处理 -
UID必须全局唯一,建议用Guid.NewGuid().ToString()生成,避免重复导入时被忽略 - 必须以
BEGIN:VCALENDAR开头、END:VCALENDAR结尾,中间嵌套BEGIN:VEVENT/END:VEVENT
System.Text.Encodings.Web 处理中文标题和描述的 URL 编码陷阱
如果事件标题含中文(如“项目评审会”),直接写入 .ics 文件会导致 Outlook 打开乱码或解析失败。iCalendar 要求非 ASCII 字符必须用 ENCODING=BASE64 + CHARSET=UTF-8 声明,不能靠文件保存编码“蒙混过关”。
正确做法是用 System.Text.Encodings.Web 的 WebEncoders.Base64Encode 编码字节:
string summary = "项目评审会"; byte[] utf8Bytes = Encoding.UTF8.GetBytes(summary); string encoded = WebEncoders.Base64Encode(utf8Bytes); // 写入 .ics 时: // SUMMARY;ENCODING=BASE64;CHARSET=UTF-8:UEBqZWN057yW56iL5L+h5oGv
常见错误:
- 用
Convert.ToBase64String(Encoding.UTF8.GetBytes(...))—— 结果一样,但WebEncoders是 .NET Core 3.0+ 官方推荐,语义更明确 - 漏写
CHARSET=UTF-8,部分安卓日历会默认当 ISO-8859-1 解码 - 对
DESCRIPTION或LOCATION忘记同样处理
用 File.WriteAllText 保存时必须指定 Encoding.UTF8
.ics 文件不是“随便保存就能用”的文本。如果调用 File.WriteAllText(path, content) 不传编码参数,.NET 默认用系统 ANSI(Windows 上常为 GBK),中文字段立刻变乱码,且 Apple Calendar 会直接拒绝导入。
务必显式指定 UTF-8:
File.WriteAllText(filePath, icsContent, Encoding.UTF8);
验证方法:用 VS Code 或 Notepad++ 打开生成的 .ics 文件,右下角确认编码显示为 “UTF-8”,而非 “UTF-8 with BOM” 或 “GBK”。iCalendar 规范禁止 BOM,BOM 会导致某些客户端解析失败。
其他注意事项:
- 路径中不要含中文或空格(尤其部署到 Linux 服务器时),用
Path.GetInvalidFileNameChars()过滤 - 文件扩展名必须是
.ics,不能是.txt或.ical,否则 macOS 双击不会关联日历应用 - HTTP 下载时,响应头应设
Content-Type: text/calendar; charset=utf-8
Outlook 和 iOS 对 DTSTAMP 和 SEQUENCE 的宽松与严格
简单事件可省略 SEQUENCE,但 DTSTAMP(时间戳)必须存在,且应设为当前 UTC 时间。Outlook 允许缺省,iOS 则可能拒绝导入无 DTSTAMP 的文件。
若后续需更新同一事件(比如改时间),必须同时更新:
-
DTSTAMP:设为更新时刻的 UTC 时间 -
SEQUENCE:从 0 开始递增(每次修改 +1) -
UID:保持不变
否则 iOS 会当作新事件重复添加,Outlook 可能无法识别更新意图。测试时可用同一 UID 连续生成两个版本,导入后观察是否覆盖而非新增。
容易被忽略的是:所有时间字段(DTSTART、DTEND、DTSTAMP)格式必须统一——要么全用 UTC(结尾 Z),要么全用本地时间+偏移(如 20240520T140000+0800)。混用会导致 iOS 解析出错,且错误不报具体提示,只静默失败。










