One of the key diagnostic data points for any .NET assembly is "when was it built"? Until recently, I thought there were only two ways to suss this out:
The filesystem method has obvious limitations:
Function AssemblyLastWriteTime(ByVal a As Reflection.Assembly) As DateTime
Try
Return File.GetLastWriteTime(a.Location)
Catch ex As Exception
Return DateTime.MaxValue
End Try
End Function
The version method, however, works quite well-- as long as developers don't deviate too far from the default .NET version string of <Assembly: AssemblyVersion("1.0.*")>
When specifying a version, you have to at least specify major. If you specify major and minor, you can specify an asterisk (*) for build. This will cause build to be equal to the number of days since January 1, 2000 local time, and for revision to be equal to the number of seconds since midnight local time, divided by 2.If you specify major, minor, and build, you can specify an asterisk for revision. This will cause revision to be equal to the number of seconds since midnight local time, divided by 2.
Function AssemblyBuildDate(ByVal a As Reflection.Assembly, _
Optional ByVal forceFileDate As Boolean = False) As DateTime
Dim v As System.Version = a.GetName.Version
Dim dt As DateTime
If forceFileDate OrElse (v.Build < 730 Or v.Revision = 0) Then
dt = AssemblyLastWriteTime(a)
Else
dt = New DateTime(2000, 1, 1, 0, 0, 0). _
AddDays(v.Build). _
AddSeconds(v.Revision * 2)
If TimeZone.IsDaylightSavingTime(dt, _
TimeZone.CurrentTimeZone.GetDaylightChanges(dt.Year)) Then
dt = dt.AddHours(1)
End If
'-- sanity check
If dt > DateTime.Now Or dt < New DateTime(2000, 1, 1, 0, 0, 0) Then
dt = AssemblyLastWriteTime(a)
End If
End If
Return dt
End Function
Be careful when relying on version to predict build date in Visual Studio .NET. For some reason, the IDE does not update the build number every time you build a solution. Visual Studio only increments the build and revision number when the solution is closed and reopened. If you build fifty times throughout the day in the same solution, every single one of your builds will have the same version. Close and reopen that solution, though, and you'll get a new version immediately. Go figure.
Luckily, we don't have to settle for those two options. There's a third way to calculate build date that's much more reliable. Dustin Aleksiuk recently posted a clever blog entry describing how to retrieve the embedded linker timestamp from the IMAGE_FILE_HEADER section of the Portable Executable header:
Function RetrieveLinkerTimestamp(ByVal filePath As String) As DateTime
Const PeHeaderOffset As Integer = 60
Const LinkerTimestampOffset As Integer = 8
Dim b(2047) As Byte
Dim s As Stream
Try
s = New FileStream(filePath, FileMode.Open, FileAccess.Read)
s.Read(b, 0, 2048)
Finally
If Not s Is Nothing Then s.Close()
End Try
Dim i As Integer = BitConverter.ToInt32(b, PeHeaderOffset)
Dim SecondsSince1970 As Integer = BitConverter.ToInt32(b, i + LinkerTimestampOffset)
Dim dt As New DateTime(1970, 1, 1, 0, 0, 0)
dt = dt.AddSeconds(SecondsSince1970)
dt = dt.AddHours(TimeZone.CurrentTimeZone.GetUtcOffset(dt).Hours)
Return dt
End Function
When I ran Dustin's code for the first time, I wondered why the dates and minutes were correct, but the hours were consistently off by four. Even I can figure out GMT/UTC issues when they practically slap me in the face. I emailed Dustin to ask him what he thought, and as it turns out, Dustin lives in GMT-- that's the ultimate "it runs on my machine"! Sure does make those pesky mental IIS logfile date conversions easier, too.. ;)
Posted by Jeff Atwood View blog reactions
« ASP.NET NTLM Authentication - is it worth it? Good Test / Bad Test »
Thanks for that! I'll correct and update that post over the next few days.
I've read your blog before. Nice to see stuff about VB.NET. I'm not the biggest VB.NET fan, but I seem to end up using it a lot and it's not going away. :)
Regards,
Dustin
That whole "number of days" and "number of seconds" thing made me nervous, even without having to open and close the IDE. Even though some people passionately explained to me why it's a good thing, those numbers seem basically random and make it hard to know what version you are in fact looking at. So I (and lots of others) wrote a little VS macro to update the build number every time I do a build, and I update the other three according to the bigness of the change. Then the file version is pretty useful, I think.
Great blog, btw.
Josh on April 15, 2005 05:02 PMThough, upon considering your post, I realize that my method doesn't tell you anything about when the file was actually built. I bet another macro could be used to take this string and write it as a resource in the executable. Probably not as sexy as Dustin's approach though.
Josh on April 15, 2005 05:04 PM> So I (and lots of others) wrote a little VS macro to update the build number every time I do a build
Yes, that's certainly logical. But it begs the question: why didn't Microsoft do it this way? Oversight? Or did they just want to ensure the build and revision number are different every time in the absence of any real metadata provided by the developer?
> My method doesn't tell you anything about when the file was actually built.
Right, and build # is kind of a meaningless number anyway. Does anyone remember the build number of the first release of NT 4.0? of XP? The *date* of the build is much more useful information than how many times the developers on the project happened to press the F5 key before it was packaged into a box and shipped..
Jeff Atwood on April 15, 2005 09:49 PMGiven that a 32 bit int from 1970 will expire in 2038, which is coming up soon, I think I'd prefer the assembly version method instead.
Marc
Marc Clifton on August 25, 2006 02:12 PMThanks for that great solution. I converted it to c# (with minor mod), listed here if anyone wants it (sorry the formatting looks so bad :)
private DateTime RetrieveLinkerTimestamp()
{
string filePath = System.Reflection.Assembly.GetCallingAssembly().Location;
const int c_PeHeaderOffset = 60;
const int c_LinkerTimestampOffset = 8;
byte[] b = new byte[2048];
System.IO.Stream s = null;
try
{
s = new System.IO.FileStream(filePath, System.IO.FileMode.Open, System.IO.FileAccess.Read);
s.Read(b, 0, 2048);
}
finally
{
if (s != null)
{
s.Close();
}
}
int i = System.BitConverter.ToInt32(b, c_PeHeaderOffset);
int secondsSince1970 = System.BitConverter.ToInt32(b, i + c_LinkerTimestampOffset);
DateTime dt = new DateTime(1970, 1, 1, 0, 0, 0);
dt = dt.AddSeconds(secondsSince1970);
dt = dt.AddHours(TimeZone.CurrentTimeZone.GetUtcOffset(dt).Hours);
return dt;
}
Thanks for the Code
Regards
Joseph
txs that helped!
fmeri on July 5, 2007 05:39 AMBest site on that topic I found in the web.
Thanks for the code.
Thanks for this code. It helped me a lot!!! Regards, me
Halcyon on November 25, 2007 11:14 AMThe build hour and minutes not changing on each release .NET build compile caught me out. Norton AV prompted every time the binary changed, so I'd expected version number to change. Open/Close IDE – nice tip!
Andy on January 14, 2008 09:51 AMThanks Joe, your code worked perfectly.
Echilon on April 21, 2008 06:24 AM1.0.* doesn't work it show 1.0.* or error. Article is useless.
Andy on June 13, 2008 10:27 AMAndy (above) thinks he's being clever, but in fact his curt little 'error report' has robbed him of any chance of help. Well done you. It works perfectly for me, although it did take a few tries with asterisks and blanks around different places to get it going. (I'm on C# Express 2008) This is what patience and being polite gets you.
Thanks, Joe.
Andrew on July 8, 2008 03:10 AMThanks Jeff too -- seems only fair!
Andrew on July 8, 2008 03:10 AM| Content (c) 2008 Jeff Atwood. Logo image used with permission of the author. (c) 1993 Steven C. McConnell. All Rights Reserved. |