0

How to add build data to assembly and show it in About box to user in ASP.NET 5 MVC application ?

In .NET 4.7 MVC application uses

//http://www.geekproject.com/showtopic.aspx?ID=21
// When you specify that you want to have buildnumber and revision automatically generated then //the compiler (C#) will generate buildnumber as the number of days from the 1st of January //2000. The revision is generated as the number of seconds from midnight divided by two, but it //will NOT take daylight savings time into account.
[assembly: AssemblyVersion("1.0.*")]

and

public static DateTime BuildDate
{
    get
    {
        string version = Assembly.GetExecutingAssembly().FullName.Split(',')[1].Trim();
        DateTime start = new DateTime(2000, 1, 1);
        int buildNumber = Convert.ToInt32(version.Split('.')[2]);
        int revision = Convert.ToInt32(version.Split('.')[3]);
        return start.Add(new TimeSpan(buildNumber, 0, 0, 2 * revision, 0));
    }
}

In .NET 5 AssemblyVersion throws

Deterministics must be turned off and duplicate attribute compile errors.

Visual Studio 2019 IDE is used to public MVC application.

Andrus
  • 26,339
  • 60
  • 204
  • 378

1 Answers1

0

I was using this feature for many years. Universal solution is to retrieve and parse the VersionInfo block, which is always present, using interop functions. Your string parsing is correct; I would add the error handling in case Split returns less than 4 items or they are not numeric.

Initially, the date was not taking into account daylight saving time, but between Visual Studios 2008 and 2017 somewhere Microsoft added that. I do not know exactly when that happens.

Until Microsoft didn't break backwards compatibility:

DateTime buildDate = new DateTime(2000, 1, 1).ToUniversalTime().
    AddDays(build).AddSeconds(revision * 2).ToLocalTime();

For Visual Studio 2017 (maybe earlier) and up:

DateTime buildDate = new DateTime(2000, 1, 1).
    AddDays(build).AddSeconds(revision * 2));

I tried to remove whatever I could, leaving only search for "Assebmly Version". The code sample is still quite large, sorry about that. Please be aware, that the block with strings ("StringFileInfo") could appear several times depending on locale.

public string GetAssemblyVersion()
{
    string versionString = "";
    IntPtr temp;
    string fileSpec = System.Reflection.Assembly.GetExecutingAssembly().Location;
    uint size = GetFileVersionInfoSizeW(fileSpec, out temp);
    if (size > 0)
    {
        IntPtr ptr = Marshal.AllocHGlobal((int)size);
        if (GetFileVersionInfoW(fileSpec, out temp, size, ptr) != 0)
        {
            versionString = ScanVersionInfo(ptr, "Assembly Version");
        }
        Marshal.FreeHGlobal(ptr);
    }
    return versionString;
}

private string ScanVersionInfo(IntPtr ptr, string key)
{
    int size = Marshal.ReadInt16(ptr);
    byte[] buf = new byte[size];
    Marshal.Copy(ptr, buf, 0, size);

    int fixedFileInfoSize = BitConverter.ToInt16(buf, 2);
    int fixedFileInfoOffset = pad32(2 + 2 + 2 + ("VS_VERSION_INFO".Length + 1) * 2);
    int offset = pad32(fixedFileInfoOffset + fixedFileInfoSize);

    return FindStringValue(buf, offset, key);
}

private string FindStringValue(byte[] buf, int offset, string key)
{
    while (offset < buf.Length)
    {

        int wLength = BitConverter.ToUInt16(buf, offset);           // item length with padding
        string blockName = ASCIIEncoding.Unicode.GetString(buf, offset + 6, "StringFileInfo".Length * 2);
        if (blockName == "StringFileInfo")
        {
            int offs = pad32(offset + 2 + 2 + 2 + ("StringFileInfo".Length + 1) * 2);
            string unicodeKey = ASCIIEncoding.Unicode.GetString(buf, offs + 2 + 2 + 2, "12345678".Length * 2);
            int itemOffset = pad32(offs + 2 + 2 + 2 + (unicodeKey.Length + 1) * 2);
            int endOffset = BitConverter.ToInt16(buf, offs) + offs;

            while (itemOffset < endOffset)
            {
                string value;
                if (extractString(buf, ref itemOffset, out value) == key)
                {
                    return value;
                }
            }
        }
        offset = pad32(offset + wLength);                           // advance to the next item
    }
    return "";
}

private string extractString(byte[] buf, ref int offset, out string value)
{
    int wLength      = BitConverter.ToUInt16(buf, offset);      // item length with padding
    int wValueLength = BitConverter.ToInt16(buf, offset + 2);   // value length in words
            
    int start = offset + 2 + 2 + 2;                             // key starts here
    int size = wLength - 2 - 2 - 2 - wValueLength * 2;          // tentative key length in bytes
    string key = ASCIIEncoding.Unicode.GetString(buf, start, size); // extract key
    key = key.Substring(0, key.IndexOf('\0'));                  // clean key's tail

    start = pad32(start + (key.Length + 1) * 2);                // value starts here
    size = (wValueLength - 1) * 2;                              // exact value length in bytes
    value = ASCIIEncoding.Unicode.GetString(buf, start, size);   // extract value

    offset = pad32(offset + wLength);                           // advance to the next item
    return key;
}

private int pad32(int size)
{
    return ((size + 3) / 4) * 4;
}

[DllImport("version.dll", CharSet = CharSet.Unicode)]
private static extern uint GetFileVersionInfoSizeW(string lptstrFilename, out IntPtr lpdwHandle);

[DllImport("version.dll", CharSet = CharSet.Unicode)]
private static extern uint GetFileVersionInfoW(string lptstrFilename, out IntPtr lpdwHandle, uint dwLen, IntPtr lpData);