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