Getting the date of build of a .NET assembly at runtime

 
 
  • Gérald Barré

Knowing the compilation date of an assembly can be useful in several scenarios. For instance, you can display it alongside the version number: v1.2.3 built one day ago.

#Method 1: Linker timestamp

You can retrieve the embedded linker timestamp from the IMAGE_FILE_HEADER section of the Portable Executable header:

C#
public static DateTime GetLinkerTimestampUtc(Assembly assembly)
{
    var location = assembly.Location;
    return GetLinkerTimestampUtc(location);
}

public static DateTime GetLinkerTimestampUtc(string filePath)
{
    const int peHeaderOffset = 60;
    const int linkerTimestampOffset = 8;
    var bytes = new byte[2048];

    using (var file = new FileStream(filePath, FileMode.Open, FileAccess.Read, FileShare.ReadWrite))
    {
        file.Read(bytes, 0, bytes.Length);
    }

    var headerPos = BitConverter.ToInt32(bytes, peHeaderOffset);
    var secondsSince1970 = BitConverter.ToInt32(bytes, headerPos + linkerTimestampOffset);
    var dt = new DateTime(1970, 1, 1, 0, 0, 0, DateTimeKind.Utc);
    return dt.AddSeconds(secondsSince1970);
}

You can then get the build date using:

C#
GetLinkerTimestampUtc(Assembly.GetExecutingAssembly());

#Method 2: AssemblyInformationalVersionAttribute

With the .NET SDK, you can use an MSBuild property to define the value of AssemblyInformationalVersionAttribute. This lets you inject the value of DateTime.UtcNow into that attribute and parse it at runtime to retrieve the build date.

Open the csproj and add the following line:

csproj (MSBuild project file)
<Project Sdk="Microsoft.NET.Sdk.Web">
  ...
  <PropertyGroup>
    <SourceRevisionId>build$([System.DateTime]::UtcNow.ToString("yyyyMMddHHmmss"))</SourceRevisionId>
  </PropertyGroup>
  ...
</Project>

The value of SourceRevisionId is added to the metadata section of the version (after the +). The value is of the following form: 1.2.3+build20180101120000

C#
private static DateTime GetBuildDate(Assembly assembly)
{
    const string BuildVersionMetadataPrefix = "+build";

    var attribute = assembly.GetCustomAttribute<AssemblyInformationalVersionAttribute>();
    if (attribute?.InformationalVersion != null)
    {
        var value = attribute.InformationalVersion;
        var index = value.IndexOf(BuildVersionMetadataPrefix);
        if (index > 0)
        {
            value = value.Substring(index + BuildVersionMetadataPrefix.Length);
            if (DateTime.TryParseExact(value, "yyyyMMddHHmmss", CultureInfo.InvariantCulture, DateTimeStyles.None, out var result))
            {
                return result;
            }
        }
    }

    return default;
}

You can then get the build date using:

C#
GetBuildDate(Assembly.GetExecutingAssembly());

#Method 3: Using a custom Attribute

By inspecting the target file that generates AssemblyInfo.cs (C:\Program Files\dotnet\sdk\2.1.401\Sdks\Microsoft.NET.Sdk\targets\Microsoft.NET.GenerateAssemblyInfo.targets), you can see that adding other custom attributes is straightforward.

The target takes a parameter named AssemblyAttribute:

csproj (MSBuild project file)
<WriteCodeFragment AssemblyAttributes="@(AssemblyAttribute)" Language="$(Language)" OutputFile="$(GeneratedAssemblyInfoFile)">
    <Output TaskParameter="OutputFile" ItemName="Compile" />
    <Output TaskParameter="OutputFile" ItemName="FileWrites" />
</WriteCodeFragment>

If you look at the file, you can see how they are declared:

csproj (MSBuild project file)
<Target Name="GetAssemblyAttributes" DependsOnTargets="GetAssemblyVersion;AddSourceRevisionToInformationalVersion">
  <ItemGroup>
    <AssemblyAttribute Include="System.Reflection.AssemblyCompanyAttribute" Condition="'$(Company)' != '' and '$(GenerateAssemblyCompanyAttribute)' == 'true'">
      <_Parameter1>$(Company)</_Parameter1>
    </AssemblyAttribute>
    <AssemblyAttribute Include="System.Reflection.AssemblyConfigurationAttribute" Condition="'$(Configuration)' != '' and '$(GenerateAssemblyConfigurationAttribute)' == 'true'">
      <_Parameter1>$(Configuration)</_Parameter1>
    </AssemblyAttribute>
    ...
  </ItemGroup>
</Target>

So, you can easily add a custom attribute. First, declare the attribute in your code:

C#
[AttributeUsage(AttributeTargets.Assembly)]
internal class BuildDateAttribute : Attribute
{
    public BuildDateAttribute(string value)
    {
        DateTime = DateTime.ParseExact(value, "yyyyMMddHHmmss", CultureInfo.InvariantCulture, DateTimeStyles.None);
    }

    public DateTime DateTime { get; }
}

Then, add the attribute declaration in the .csproj:

csproj (MSBuild project file)
<Project Sdk="Microsoft.NET.Sdk">

  <PropertyGroup>
    <OutputType>Exe</OutputType>
    <TargetFramework>netcoreapp2.1</TargetFramework>
  </PropertyGroup>

  <ItemGroup>
    <AssemblyAttribute Include="BuildDateAttribute">
      <_Parameter1>$([System.DateTime]::UtcNow.ToString("yyyyMMddHHmmss"))</_Parameter1>
    </AssemblyAttribute>
  </ItemGroup>
</Project>

It will generate the following file under obj\Debug\netcoreapp2.1\BuildTime.AssemblyInfo.cs:

C#
//------------------------------------------------------------------------------
// <auto-generated>
//     This code was generated by a tool.
//     Runtime Version:4.0.30319.42000
//
//     Changes to this file may cause incorrect behavior and will be lost if
//     the code is regenerated.
// </auto-generated>
//------------------------------------------------------------------------------

using System;
using System.Reflection;

[assembly: BuildDateAttribute("20180901204042")] 👈
[assembly: System.Reflection.AssemblyCompanyAttribute("Test")]
[assembly: System.Reflection.AssemblyConfigurationAttribute("Debug")]
[assembly: System.Reflection.AssemblyFileVersionAttribute("1.0.0.0")]
[assembly: System.Reflection.AssemblyInformationalVersionAttribute("1.0.0")]
[assembly: System.Reflection.AssemblyProductAttribute("Test")]
[assembly: System.Reflection.AssemblyTitleAttribute("Test")]
[assembly: System.Reflection.AssemblyVersionAttribute("1.0.0.0")]

// Generated by the MSBuild WriteCodeFragment class.

Finally, you can get the build date using:

C#
private static DateTime GetBuildDate(Assembly assembly)
{
    var attribute = assembly.GetCustomAttribute<BuildDateAttribute>();
    return attribute != null ? attribute.DateTime : default(DateTime);
}

#Conclusion

MSBuild is very powerful. The binary log helps you understand how it works and customize the build to your needs. Of the three approaches shown, Method 2 is the one I prefer for my own projects, since the SourceRevisionId property may also be set by other targets such as SourceLink.

Do you have a question or a suggestion about this post? Contact me!

Follow me:
Enjoy this blog?