在C#开发中,我们经常需要在界面关于框、日志文件头或者调试信息中展示当前程序自身的编译时间,同时也会频繁读取程序集的内置属性,例如版本号、产品名称和描述等。这些信息都存放在程序集的清单里,通过反射即可在运行时获取。除此之外,编译时间本身并不是.NET默认提供的标准特性,需要借助生成事件或源代码生成器在编译阶段写入自定义特性,才能在后期读取。

一、读取程序集的标准属性信息
每一个C#项目在编译后都会生成包含元数据的程序集(DLL或EXE)。这些元数据中的大部分来自项目文件中的属性或者传统的 AssemblyInfo.cs 文件。我们可以通过 System.Reflection 命名空间下的 Assembly 类来获取当前执行程序集的实例,再访问其对应属性。
下面是一段读取常见程序集属性的示例代码。注意代码中的小于号和大于号都已经做了转义处理,以保证在HTML环境下也能正确展示。
using System;
using System.Reflection;
class Program
{
static void Main()
{
Assembly asm = Assembly.GetExecutingAssembly();
// 程序集标题
var titleAttr = (AssemblyTitleAttribute)Attribute.GetCustomAttribute(
asm, typeof(AssemblyTitleAttribute));
// 程序集版本
Version ver = asm.GetName().Version;
// 产品名称
var productAttr = (AssemblyProductAttribute)Attribute.GetCustomAttribute(
asm, typeof(AssemblyProductAttribute));
Console.WriteLine("标题: " + (titleAttr != null ? titleAttr.Title : "无"));
Console.WriteLine("版本: " + ver.ToString());
Console.WriteLine("产品: " + (productAttr != null ? productAttr.Product : "无"));
}
}
上面的代码使用了 Assembly.GetExecutingAssembly() 获取正在运行的程序集,然后通过 Attribute.GetCustomAttribute 方法取得具体的特性对象。对于版本号,直接调用 GetName().Version 即可,无需额外特性查询。
这种方式的优点是简单、稳定,且不需要额外配置,只要项目里设置了对应的程序集属性就能读到。缺点是它只能读取.NET已经定义好的那几个特性,像编译时间这种需求就必须自己扩展。
二、在编译时写入编译时间
由于程序集本身不自带编译时间,我们通常在构建阶段利用预生成事件或源生成器把当前时间写进一个自定义特性类。最简单实用的方法是在项目文件里添加生成事件命令行,让编译前自动生成一个包含当前UTC时间的C#源文件。
假设我们定义了一个名为 BuildTimeAttribute 的特性,放在项目的 Common 命名空间下,然后在 csproj 文件中使用 BeforeBuild 目标来写文件。示例 csproj 片段如下:
<Target Name="BeforeBuild">
<WriteLinesToFile
File="$(IntermediateOutputPath)BuildTime.g.cs"
Lines="using System; namespace Common { [AttributeUsage(AttributeTargets.Assembly)] public sealed class BuildTimeAttribute : Attribute { public BuildTimeAttribute(string time) { Time = time; } public string Time { get; } } }"
Overwrite="true" />
<WriteLinesToFile
File="$(IntermediateOutputPath)BuildTime.g.cs"
Lines="[assembly: Common.BuildTimeAttribute("$([System.DateTime]::UtcNow.ToString("yyyy-MM-dd HH:mm:ss"))")]"
Overwrite="false" />
</Target>
这段代码在每次生成前都会重新写出 BuildTime.g.cs,其中包含了当前UTC时间字符串,并以程序集特性形式附着到产物上。使用 $([System.DateTime]::UtcNow) 可以保证时间随编译动作自动刷新,不需要开发者手动改代码。
相比在代码里硬编码一个日期常量,这种做法完全自动化,也不会因为忘记修改而导致关于界面显示旧时间。要注意的是,如果团队使用确定性构建(Deterministic),某些场景下时间戳可能被忽略,此时应改用源生成器或在CI脚本中显式传入时间参数。
三、读取自定义编译时间特性
写入之后,读取就只是普通的反射操作。我们复用前面提到的 Attribute.GetCustomAttribute 方法,只是类型换成自己定义的 BuildTimeAttribute。
using System;
using System.Reflection;
using Common;
class InfoReader
{
public static string GetBuildTime()
{
Assembly asm = Assembly.GetExecutingAssembly();
var attr = (BuildTimeAttribute)Attribute.GetCustomAttribute(
asm, typeof(BuildTimeAttribute));
if (attr == null)
return "未知";
return attr.Time;
}
}
class Program
{
static void Main()
{
Console.WriteLine("本程序编译时间: " + InfoReader.GetBuildTime());
}
}
上述代码中,InfoReader.GetBuildTime 方法尝试从执行程序集中取出 BuildTimeAttribute,如果找不到就返回“未知”。找到后直接读取其 Time 属性即可得到生成时写入的时间文本。
为了兼容不同时区,建议在写入阶段统一使用UTC,在展示时再根据用户本地时区转换。例如可把字符串解析为 DateTime 后再调用 ToLocalTime。这样既能保证构建服务器与开发者机器时间一致,也能在客户端显示符合习惯的时间。
四、通过程序集元数据做更多事情
除了编译时间,程序集属性还能承载很多有用的信息,比如提交哈希、构建编号、运行环境要求等。我们可以把这些内容都做成自定义特性,然后在程序启动时统一收集,输出到日志或者系统信息接口里。
下面用一张表总结常见程序集属性及获取方式:
| 属性名称 | 对应特性类 | 获取方式 |
|---|---|---|
| 程序集标题 | AssemblyTitleAttribute | Attribute.GetCustomAttribute |
| 版本号 | 内置清单 | Assembly.GetName().Version |
| 公司名称 | AssemblyCompanyAttribute | Attribute.GetCustomAttribute |
| 编译时间 | 自定义BuildTimeAttribute | Attribute.GetCustomAttribute |
借助这套机制,我们不仅能回答“C#怎么获取程序的编译时间”的问题,还能把整个程序集的属性读取封装成一个通用工具类,在多个项目中复用。对于需要远程诊断的客户端软件,把编译时间与版本号随错误报告一起上报,可以大幅降低排查成本。
最后提醒一点,程序集特性在反射时会有轻微性能开销,若高频调用建议缓存结果。例如在静态构造函数里读取一次编译时间并保存为只读字段,后续直接返回字段值,既满足需求又不影响运行效率。