Timestamping assemblies with Build date and time.






4.96/5 (21 votes)
It surprised me to find that there is no simple way to find out when a .NET assembly was built. You can work it out from the revision number (provided you only use the "standard" numbering scheme) but it's not obvious. This provides a simple way to timestamp individual assemblies at build time.
Introduction
Recently I was answering a QA question on build time stamping, and realised that it is a surprising omission from .NET - there is no simple way to get the date and time of a build from an assembly. This tip rectifies this, and introduces one of the more advanced Visual Studio features.
Background
Why would you want to timestamp a build? If for no other reason than you can easily check when it was produced! You date all your memos, emails and SMS messages, don't you? Or at least, the system does for you, and you almost certainly organise them by that time stamp. Why should assemblies be any different?
There are a couple of ways you can work out a build date:
- From the version number
- From the assembly file
CreatedOn
attribute - Embed it in your file at build time
Using the version number
The first of these options requires you to enable it:
- Open the project properties sheet (not the pane - double click "properties" under the project name)
- Under the
Application
tab, press theAdvanced
button. - In the version number field, replace the third digit with an asterisk character, and delete the final number.
Now, when you build your assembly, you will get versions such as "1.0.4500.17115" where "4500" is the days since Jan 1st, 2000, and the "17115" is the seconds since midnight (midnight, where I don't know - it isn't here!)
And yes, it is easy to work back to today's date. But there is a problem - this number is the "binding version" - if you are using Strongly Named Assemblies then any change to this number will cause the binding to fail. Needless to say, you can't use this format in the FileVersion, where it could be useful! I do not recommend this approach.
Using the File Date Information
You can pick up the File created date attribute, and it's not difficult to do:
Assembly asm = Assembly.GetExecutingAssembly();
FileInfo fi = new FileInfo(asm.Location);
Console.WriteLine(fi.CreationTime);
Console.WriteLine(fi.LastWriteTime);
Unfortunately, there are problems here as well:
- CreationTime is the first time the file was created - i.e. the first time you built the EXE, not the last.
- LastWriteTime is the last time the file was written - i.e. the last time you built it.
Embedding in your file
Embedding is surprisingly easy: Visual studio provides a PreBuild and a PostBuild step for each project which can execute any application. All we have to do is write an application to generate a time stamp as a source file:
- Open Visual Studio, and create a new C# Project: Call it GenerateTimeStampFile and make it a WinForms application.
- When the project is constructed, highlight the only Form in the project (normally called Form1) and delete it. We don't need it as we will not have a user interface. (I didn't construct it as a Console Application, because that would mean getting rid of the console itself, which is a little harder to do).
- Open the Program.cs file and replace it's content:
using System;
using System.IO;
namespace GenerateTimeStampFile
{
static class Program
{
/// <summary>
/// The main entry point for the application.
/// </summary>
[STAThread]
static void Main()
{
string[] args = Environment.GetCommandLineArgs();
if (args.Length > 1)
{
// Assume new file required
string fileName = null;
string nameSpace = "TimeStamp";
string[] TimestampFile = new string[]
{@"using System;",
@"// The namespace can be overidden by the /N option:",
@"// GenerateTimeStampFile file.cs /N:MyNameSpace",
@"// Such settings will override your value here.",
@"namespace TimeStamp",
@" {",
@" /// <summary>",
@" /// Static Timestamp related data.",
@" /// </summary>",
@" /// <remarks>",
@" /// THIS FILE IS CHANGED BY EXTERNAL PROGRAMS.",
@" /// Do not modify the namespace, as it may be overwritten. You can",
@" /// set the namespace with the /N option.",
@" /// Do not modify the definition of BuildAt as your changes will be discarded.",
@" /// Do not modify the definition of TimeStampedBy as your changes will be discarded.",
@" /// </remarks>",
@" public static class Timestamp",
@" {",
@" /// <summary>",
@" /// The time stamp",
@" /// </summary>",
@" /// <remarks>",
@" /// Do not modify the definition of BuildAt as your changes will be discarded.",
@" /// </remarks>",
@" public static DateTime BuildAt { get { return new DateTime(???); } } //--**",
@" /// <summary>",
@" /// The program that time stamped it.",
@" /// </summary>",
@" /// <remarks>",
@" /// Do not modify the definition of TimeStampedBy as your changes will be discarded.",
@" /// </remarks>",
@" public static string TimeStampedBy { get { return @""???""; } } //--**",
@" }",
@" }" };
for (int i = 1; i < args.Length; i++)
{
if (!args[i].StartsWith("/"))
{
// File name
fileName = args[i];
if (File.Exists(fileName))
{
TimestampFile = File.ReadAllLines(fileName);
}
}
else
{
// It's an option
if (args[i].StartsWith("/N:"))
{
// Set the namespace for the Timestamp class.
nameSpace = args[i].Substring(3);
}
}
}
if (!string.IsNullOrWhiteSpace(fileName))
{
// We have an output location.
// Replace the namespace and timestamp.
for(int i = 0; i < TimestampFile.Length; i++)
{
string line = TimestampFile[i].Trim();
if (line.StartsWith("namespace"))
{
TimestampFile[i] = "namespace " + nameSpace;
}
else if (line.EndsWith("//--**"))
{
// Special
if (line.Contains("DateTime BuildAt"))
{
TimestampFile[i] = @" public static DateTime BuildAt { get { return new DateTime(" +
DateTime.Now.Ticks.ToString() +
@"); } } //--**";
}
else if (line.Contains("string TimeStampedBy"))
{
TimestampFile[i] = @" public static string TimeStampedBy { get { return @""GenerateTimeStampFile V1.0""; } } //--**";
}
}
}
File.WriteAllLines(fileName, TimestampFile);
}
}
Environment.ExitCode = 0; //Set no error
}
}
}
You can now build the project - preferably as the Release version.
It is important that you locate the EXE file you just generated - it may help to copy it to the Program Files
folder so you can find it easily later.
Using the code
- In the assembly you want to time stamp double click on
Properties
in the Solution Explorer. - In the
Build Events
tab, fill in thePre-build event command line
text box: with a single line.
"C:\Program Files\GenerateTimeStampFile.exe" "$(ProjectDir)Timestamp.cs" "/N:MyNamespace"
The quote marks are important!
The /N:MyNamespace
part is an optional specifier to put the timestamp into your project namespace - if you do not use it, then it will go in the TimeStamp namespace. Using your own allows you to more easily have separate time stamps for each assembly in a project. Using the default allows them all to refer to the same one.
Build your application. Nothing will change!
In the solution explorer for your project, right click and select "Add...Existing Item..."
Select the TimeStamp.cs file, and press "Add".
You can now code to use the time stamp:
Console.WriteLine(Timestamp.BuildAt.ToString() + " by " + Timestamp.TimeStampedBy);
If you used the default namespace, then you will need to add the appropriate using
statement.
How does it work?
When you build your project, Visual Studio executes the commands in the pre-build step. In the example above, it looks in "C:\Program Files" for an executable called "GenerateTimeStampFile.exe" and runs it with the given parameters - having replaced "$(...)" values with Visual Studio project and / or solution related strings.
So, if in the example above your project was in the folder "C:\Users\Griff\Documents\My Projects\UtilityApplications\SetShares" then it is the equivalent of bringing up a command prompt and manually running GenerateTimeStampFile with two parameters:
- "C:\Users\griff\Documents\My Projects\UtilityApplications\SetShares\Timestamp.cs"
- "/N:MyNamespace"
- Generates a dummy file in case the target destination does not exist. This allows for an easy start up.
- Processes the application parameters: it sets the file name and path to output to, and optionally the namespace to use - this defaults to "TimeStamp". If the file exists, the default file is discarded and the new one loaded, allowing you to add items to the Timestamp class if you need to.
- It then checks that a file was specified, and if so it loops through each line in the current file content looking for elements to replace: the namespace and any line ending with a "--**" comment - these lines will be replaced completely with the stamped versions.
- The file is then written, and the program exits indicating no error back to VS.
Notes
This started life as a simple quick and dirty application, followed by a Tip/Trick. But as such things do, it "growed like Topsy" so I converted it to a small article - it was way too big for a tip!
History
Original version (27/04/2012 12:35:53 by GenerateTimeStampFile V1.0)