我目前有一个应用程序显示构建号在其标题窗口。这很好,除了它对大多数用户没有任何意义,他们想知道他们是否有最新的版本-他们倾向于将其称为“上周四的”而不是1.0.8.4321版本。

计划是把构建日期放在那里,例如“App构建于21/10/2009”。

我正在努力寻找一种程序化的方法,将构建日期作为文本字符串提取出来,以便像这样使用。

对于版本号,我使用:

Assembly.GetExecutingAssembly().GetName().Version.ToString()

在定义了这些是怎么来的之后。

我希望编译日期(和时间,为了加分)也像这样。

非常感谢这里的指示(如果合适的话,借口双关语),或者更整洁的解决方案……


当前回答

对于. net Core(。NET 5+),可以这样做。它的优点在于不需要添加或嵌入文件,没有T4,也没有预构建脚本。

在你的项目中添加这样一个类:

namespace SuperDuper
{
    [AttributeUsage(AttributeTargets.Assembly)]
    public class BuildDateTimeAttribute : Attribute
    {
        public string Date { get; set; }
        public BuildDateTimeAttribute(string date)
        {
            Date = date;
        }
    }
}

更新项目的.csproj以包含如下内容:

<ItemGroup>
    <AssemblyAttribute Include="SuperDuper.BuildDateTime">
        <_Parameter1>$([System.DateTime]::Now.ToString("s"))</_Parameter1>
    </AssemblyAttribute>
</ItemGroup>

注意,_Parameter1是一个神奇的名字——它意味着BuildDateTime属性类的构造函数的第一个(也是唯一一个)参数。

这就是在程序集中记录构建日期时间所需的全部内容。

然后读取程序集的构建日期时间,执行如下操作:

private static DateTime? getAssemblyBuildDateTime()
{
    var assembly = System.Reflection.Assembly.GetExecutingAssembly();
    var attr = Attribute.GetCustomAttribute(assembly, typeof(BuildDateTimeAttribute)) as BuildDateTimeAttribute;
    if (DateTime.TryParse(attr?.Date, out DateTime dt))
        return dt;
    else
        return null;
}

注1(根据评论中的Flydog57):如果你的.csproj有属性GenerateAssemblyInfo在其中列出并设置为false,构建将不会生成程序集信息,你将不会在程序集中获得BuildDateTime信息。因此,要么不要在.csproj中提到GenerateAssemblyInfo(这是新项目的默认行为,如果没有特别设置为false, GenerateAssemblyInfo默认为true),要么显式地将其设置为true。

注2(根据Teddy在评论中的说法):在给出的_Parameter1示例中,我们使用::Now来使用DateTime。现在,这是您的计算机上的本地日期和时间,如果适用日光节约时间和您的本地时区。如果你想使用::UtcNow来使用DateTime,你可以这样做。UtcNow以便将构建日期和时间记录为UTC/GMT。

其他回答

对于。net 5,我已经成功地使用了这种方法。(在这里找到)。

把这个添加到.csproj文件中:

<SourceRevisionId>build$([System.DateTime]::UtcNow.ToString("yyyyMMddHHmmss"))</SourceRevisionId>

获取构建日期的方法:

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;
}

用法:

 var buildTime = GetBuildDate(Assembly.GetExecutingAssembly());
 buildTime = buildTime.ToLocalTime();

的方式

正如@c00000fd在评论中指出的那样。微软正在改变这一点。虽然很多人不使用他们的编译器的最新版本,但我怀疑这个变化使这种方法毫无疑问是糟糕的。虽然这是一个有趣的练习,但如果跟踪二进制文件本身的构建日期很重要,我建议人们通过任何其他必要的方法将构建日期嵌入到二进制文件中。

这可以通过一些简单的代码生成来完成,这可能是构建脚本中的第一步。事实上,ALM/Build/DevOps工具在这方面帮助很大,应该优先于其他任何工具。

我把这个答案的其余部分留在这里,仅用于历史目的。

新方式

我改变了主意,现在使用这个技巧来获得正确的构建日期。

#region Gets the build date and time (by reading the COFF header)

// http://msdn.microsoft.com/en-us/library/ms680313

struct _IMAGE_FILE_HEADER
{
    public ushort Machine;
    public ushort NumberOfSections;
    public uint TimeDateStamp;
    public uint PointerToSymbolTable;
    public uint NumberOfSymbols;
    public ushort SizeOfOptionalHeader;
    public ushort Characteristics;
};

static DateTime GetBuildDateTime(Assembly assembly)
{
    var path = assembly.GetName().CodeBase;
    if (File.Exists(path))
    {
        var buffer = new byte[Math.Max(Marshal.SizeOf(typeof(_IMAGE_FILE_HEADER)), 4)];
        using (var fileStream = new FileStream(path, FileMode.Open, FileAccess.Read))
        {
            fileStream.Position = 0x3C;
            fileStream.Read(buffer, 0, 4);
            fileStream.Position = BitConverter.ToUInt32(buffer, 0); // COFF header offset
            fileStream.Read(buffer, 0, 4); // "PE\0\0"
            fileStream.Read(buffer, 0, buffer.Length);
        }
        var pinnedBuffer = GCHandle.Alloc(buffer, GCHandleType.Pinned);
        try
        {
            var coffHeader = (_IMAGE_FILE_HEADER)Marshal.PtrToStructure(pinnedBuffer.AddrOfPinnedObject(), typeof(_IMAGE_FILE_HEADER));

            return TimeZone.CurrentTimeZone.ToLocalTime(new DateTime(1970, 1, 1) + new TimeSpan(coffHeader.TimeDateStamp * TimeSpan.TicksPerSecond));
        }
        finally
        {
            pinnedBuffer.Free();
        }
    }
    return new DateTime();
}

#endregion

老办法

那么,如何生成构建号呢?如果你将AssemblyVersion属性更改为1.0.*,Visual Studio(或c#编译器)实际上会提供自动构建和修订号

将要发生的情况是,构建将等于自当地时间2000年1月1日以来的天数,而对于修订,则等于自当地时间午夜以来的秒数,除以2。

请参阅社区内容、自动构建和修订编号

例如AssemblyInfo.cs

[assembly: AssemblyVersion("1.0.*")] // important: use wildcard for build and revision numbers!

SampleCode.cs

var version = Assembly.GetEntryAssembly().GetName().Version;
var buildDateTime = new DateTime(2000, 1, 1).Add(new TimeSpan(
TimeSpan.TicksPerDay * version.Build + // days since 1 January 2000
TimeSpan.TicksPerSecond * 2 * version.Revision)); // seconds since midnight, (multiply by 2 to get original)

我不确定,但也许Build Incrementer会有所帮助。

如果这是一个windows应用程序,你可以只使用应用程序的可执行路径: 新System.IO.FileInfo (Application.ExecutablePath) .LastWriteTime.ToString(“yyyy.MM.dd”)

上面的方法可以对进程中已经加载的程序集进行调整,使用内存中的文件映像(而不是从存储中重新读取它):

using System;
using System.Runtime.InteropServices;
using Assembly = System.Reflection.Assembly;

static class Utils
{
    public static DateTime GetLinkerDateTime(this Assembly assembly, TimeZoneInfo tzi = null)
    {
        // Constants related to the Windows PE file format.
        const int PE_HEADER_OFFSET = 60;
        const int LINKER_TIMESTAMP_OFFSET = 8;

        // Discover the base memory address where our assembly is loaded
        var entryModule = assembly.ManifestModule;
        var hMod = Marshal.GetHINSTANCE(entryModule);
        if (hMod == IntPtr.Zero - 1) throw new Exception("Failed to get HINSTANCE.");

        // Read the linker timestamp
        var offset = Marshal.ReadInt32(hMod, PE_HEADER_OFFSET);
        var secondsSince1970 = Marshal.ReadInt32(hMod, offset + LINKER_TIMESTAMP_OFFSET);

        // Convert the timestamp to a DateTime
        var epoch = new DateTime(1970, 1, 1, 0, 0, 0, DateTimeKind.Utc);
        var linkTimeUtc = epoch.AddSeconds(secondsSince1970);
        var dt = TimeZoneInfo.ConvertTimeFromUtc(linkTimeUtc, tzi ?? TimeZoneInfo.Local);
        return dt;
    }
}