275

I currently have an app displaying the build number in its title window. That's well and good except it means nothing to most of the users, who want to know if they have the latest build - they tend to refer to it as "last Thursday's" rather than build 1.0.8.4321.

The plan is to put the build date there instead - So "App built on 21/10/2009" for example.

I'm struggling to find a programmatic way to pull the build date out as a text string for use like this.

For the build number, I used:

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

after defining how those came up.

I'd like something like that for the compile date (and time, for bonus points).

Pointers here much appreciated (excuse pun if appropriate), or neater solutions...

Luke Girvin
  • 12,672
  • 8
  • 57
  • 79
Mark Mayo
  • 10,833
  • 11
  • 49
  • 82
  • 2
    I tried the supplied ways to get the build data of assemblies which works in simple scenarios but if two assemblies are merged together i get not the correct build time, it is one hour in the future.. any suggestions? –  Mar 16 '11 at 09:01

26 Answers26

364

Jeff Atwood had a few things to say about this issue in Determining Build Date the hard way.

The most reliable method turns out to be retrieving the linker timestamp from the PE header embedded in the executable file -- some C# code (by Joe Spivey) for that from the comments to Jeff's article:

public static DateTime GetLinkerTime(this Assembly assembly, TimeZoneInfo target = null)
{
    var filePath = assembly.Location;
    const int c_PeHeaderOffset = 60;
    const int c_LinkerTimestampOffset = 8;

    var buffer = new byte[2048];

    using (var stream = new FileStream(filePath, FileMode.Open, FileAccess.Read))
        stream.Read(buffer, 0, 2048);

    var offset = BitConverter.ToInt32(buffer, c_PeHeaderOffset);
    var secondsSince1970 = BitConverter.ToInt32(buffer, offset + c_LinkerTimestampOffset);
    var epoch = new DateTime(1970, 1, 1, 0, 0, 0, DateTimeKind.Utc);

    var linkTimeUtc = epoch.AddSeconds(secondsSince1970);

    var tz = target ?? TimeZoneInfo.Local;
    var localTime = TimeZoneInfo.ConvertTimeFromUtc(linkTimeUtc, tz);

    return localTime;
}

Usage example:

var linkTimeLocal = Assembly.GetExecutingAssembly().GetLinkerTime();

UPDATE: The method was working for .Net Core 1.0, but stopped working after .Net Core 1.1 release(gives random years in 1900-2020 range)

Michael Freidgeim
  • 21,559
  • 15
  • 127
  • 153
mdb
  • 49,388
  • 10
  • 62
  • 62
  • very impressive, but I tried John's 3 line one below, and certainly for my system and versions, it appears to work fine. However as per the article there are some limitations in that method, so I'm voting for both of yours and accepting yours so that future readers can benefit from its soundness. Thanks! – Mark Mayo Oct 21 '09 at 14:54
  • 3
    I would never dig into the PE header in that way, just to get the assembly version information. I've never had an issue with the build number not being update to date, that problem is a thing of the past. Since you're looking at the executable as raw bytes you have no guarantees that the PE header won't change in the future or be a Windows PE header at all (does this work in mono? probably yes). And that's the only reason you should ever need. Besides the format there's an probable issue with endian on the XBOX360 that you'll run into when someone tries to port this code. – John Leidegren Feb 20 '10 at 09:47
  • 9
    I've changed my tone about this somewhat, I'd still be very careful when digging into the acutal PE header. But as far as I can tell, this PE stuff is a lot more reliable than using the versioning numbers, besides I wan't to assign the version numbers seperate from the build date. – John Leidegren Sep 30 '10 at 20:17
  • 6
    I like this and am using it, but that second to last line with the `.AddHours()` is rather hackish and (I think) won't take DST into account. If you want it in local time, you should use the cleaner `dt.ToLocalTime();` instead. The middle part could also be greatly simplified with a `using()` block. – JLRishe May 07 '13 at 04:06
  • 1
    @JLRishe Agreed. Using .AddHours() doesn't take into account timezones with fractions of an hour either. – Daniel Curtis Oct 28 '13 at 22:59
  • 1
    Wow. Strange how you can't request that through any framework classes. The UTC issue is annoying though, especially since I want to use it for logging purposes. Customers often send a partial log file, on tools that can't really use version numbers (since it's a services host that does the logging, but the services on it that get updated) so I want the build date in it, but it seems keeping it UTC is the best option then. – Nyerguds Jun 10 '14 at 13:54
  • more opaque than @JohnLeidegren's answer – JJS Jul 29 '15 at 19:36
  • 2
    Modernized this answer – Chris Marisic Jan 22 '16 at 18:58
  • 3
    I don't know if it's intentional or not, but as of .net core 1.1.1 this seems to not be valid (worked in previous versions of .net core though). That is assuming I didn't get time travelled to the 1960s (getting negative epochs). – KallDrexx Mar 13 '17 at 23:10
  • 7
    Yeah, this stopped working for me with .net core as well (1940s, 1960s, etc) – eoleary Mar 28 '17 at 13:33
  • 2
    Since this is no longer working in latest .net core I am now using MSBUILD tasks to write the current UTCNow into a builddate.txt. My code then loads the text from this file – KallDrexx Apr 25 '17 at 14:50
  • Works again with .net core 2.x – djunod Nov 02 '17 at 14:30
  • 7
    While usage of PE header might seem a good option today, it's worth to note that MS is experimenting with deterministic builds (which would render this header useless) and perhaps even making it default in future compiler versions of C# (for good reasons). Good read: http://blog.paranoidcoding.com/2016/04/05/deterministic-builds-in-roslyn.html and here's answer related to .NET Core (TLDR: "it's by design"): https://developercommunity.visualstudio.com/content/problem/35873/invalid-timestamp-in-pe-header-of-compiled-net-cor.html – Paweł Bulwan Dec 12 '17 at 09:57
  • It seems like this method is an implementation detail, based on Jared Par's blog: "The practice of validating the time stamp is questionable but given tools were doing it there was a significant back compat risk. Hence we moved to the current computed value and haven’t seen any issues since then.". Admittedly it looks like this method will only cause harm in .NET Core. – jrh Dec 18 '17 at 13:16
  • This is the version I could make work in ASP.NET 4.7 – MikeJ Jan 17 '18 at 14:54
  • 14
    For those who find this no longer works, the issue is not a .NET Core issue. See my answer below about new build parameter defaults starting with Visual Studio 15.4. – Tom Feb 14 '18 at 17:34
  • Note that the size passed to `FileStream.Read` is a maximum, it does not guarantee anything was read and must be called in a loop to ensure correct behavior. – Tim Sylvester Jun 19 '18 at 17:31
  • Given that it's not added and this is the accepted answer. This is also updated when a JIT compilation occurs on the DLL. So if you are using this on a website to show the build timestamp it will most likely be incorrect. – Dave3of5 Nov 23 '18 at 11:30
  • PLEASE NOTE: Using commands in the pre-build will cause Visual Studio to create a random named .cmd file that will be blocked by AVAST. Something like c:\users\username\AppData\Local\Temp\tmpbafc948d2ea741e3.exec.cmd. This file is randomly named, and CANNOT be whitelisted by wildcard. – Some_Yahoo Feb 03 '21 at 20:05
  • This not work for dot net core > 1.1, which is standard today. Please consider the solution of Otis Bourman – Christian Müller Apr 06 '21 at 11:39
124

Add below to pre-build event command line:

echo %date% %time% > "$(ProjectDir)\Resources\BuildDate.txt"

Add this file as resource, now you have 'BuildDate' string in your resources.

To create resources, see How to create and use resources in .NET.

Community
  • 1
  • 1
Abdurrahim
  • 1,832
  • 1
  • 15
  • 15
  • Clever way to get a reliable build date. I executed a (python) script in the pre-build event so I could better control the formatting of the build date. – bj0 Oct 21 '13 at 17:40
  • i think this is the best way to go however if assembly is not your own then we might have to use @mdb's solution. I'll definitely go with your clever solution. – Mubashar Jan 10 '14 at 05:04
  • 6
    +1 from me, simple and effective. I even managed to get the value from the file with a line of code like this: String buildDate = .Properties.Resources.BuildDate – davidfrancis Feb 14 '14 at 10:18
  • 1
    +1 To this BTW. So elegant and simple. However I used a different format for the date time (YYYY-MM-DD HH:MM:SS.MS) to guarantee parsing in any culture. – Jason D May 09 '14 at 13:56
  • 15
    another option is make a class: (have to include in project after first time that you compile it) --> echo namespace My.app.namespace { public static class Build { public static string Timestamp = "%DATE% %TIME%".Substring(0,16);}} > "$(ProjectDir)\BuildTimestamp.cs" - - - --> then can call it with Build.Timestamp – FabianSilva Jun 02 '14 at 16:10
  • 10
    This is an excellent solution. The only problem is that %date% and %time% command line variables are localized, so the output will vary depending on the Windows language of the user. – V.S. Jul 23 '14 at 15:00
  • 2
    +1, this is a better method than reading PE headers - because there are several scenarios where that won't work at all (Windows Phone App for example) – Matt Whitfield Aug 01 '14 at 20:57
  • Tried that as well. Though output differs from environment to environment (within windows, don't get me started on building on Mac or Linux). – Yves Schelpe Feb 18 '15 at 15:38
  • 22
    Clever. You can also use powershell to get more precise control over the format, e.g. to get a the UTC datetime formatted as ISO8601: powershell -Command "((Get-Date).ToUniversalTime()).ToString(\"s\") | Out-File '$(ProjectDir)Resources\BuildDate.txt'" – dbruning Oct 08 '15 at 22:12
  • To use this from razor in ASP.NET, make sure the resource visibility is `public`. – galdin Nov 14 '15 at 17:21
  • For a non-localized (so, stable) version without powershell, have a look at http://ss64.com/nt/syntax-getdate.html – MKesper Jun 09 '16 at 10:36
  • This should really be UTC in a non-localized variant. – Joey Feb 21 '18 at 11:25
  • I did that but used Assets because it's easier to handle simple txt files (while resources file must be in a specific format) – Jimbot Apr 24 '18 at 13:13
  • 1
    I had to do echo %25date%25 %25time%25 > "$(ProjectDir)Resources\BuildDate.txt" to get it to work - see https://developercommunity.visualstudio.com/content/problem/237752/prebuild-event-command-line-date-issue.html – David Christopher Reynolds Mar 20 '19 at 10:57
  • I am using the following code: `lblVersion.Text = String.Format("Version: {0} built on {1}", Assembly.GetEntryAssembly().GetName().Version, Properties.Resources.BuildDate);` The code works properly, but when changing the Enabled property of multiple selected buttons in design mode, I receive: Property value is not valid. Could not find file 'C:\Users\...\Resources\BuildDate.txt.txt'. (Notice double extension.) I am using Visual Studio Community 2015 Version 14.0.25431.01 Update 3 (.NET Framework Version 4.8.04084) – Derek Johnson Jan 07 '21 at 20:01
92

The way

As pointed out by @c00000fd in the comments. Microsoft is changing this. And while many people don't use the latest version of their compiler I suspect this change makes this approach unquestionably bad. And while it's a fun exercise I would recommend people to simply embed a build date into their binary through any other means necessary if it's important to track the build date of the binary itself.

This can be done with some trivial code generation which probably is the first step in your build script already. That, and the fact that ALM/Build/DevOps tools help a lot with this and should be preferred to anything else.

I leave the rest of this answer here for historical purposes only.

The new way

I changed my mind about this, and currently use this trick to get the correct build date.

#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

The old way

Well, how do you generate build numbers? Visual Studio (or the C# compiler) actually provides automatic build and revision numbers if you change the AssemblyVersion attribute to e.g. 1.0.*

What will happen is that is that the build will 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.

see Community Content, Automatic Build and Revision numbers

e.g. 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)
John Leidegren
  • 56,169
  • 16
  • 118
  • 148
  • 1
    Perfect, was just the limited short answer I needed and it works great. However I feel that for completeness, mdb's answer above will prevent others from shooting themselves in the foot, so accepting his. Gave you a vote tho - thanks! – Mark Mayo Oct 21 '09 at 14:56
  • The community content seem to be removed... How can we be sure that MS wont change the algorithm? :) Any official docs on this guys? – Alex from Jitbit Jun 29 '10 at 07:50
  • This method gives me a timestamp that is off by one hour. I guess it's because of daylight savings time or perhaps because my timezone i CET, i.e. +1. I have tried construction the new DateTime with DateTimeKind.Local and DateTimeKind.Utc, but that doesn't help. – Jan Aagaard Sep 30 '10 at 11:34
  • Yeah, I've started to think less of this approach after all. Digging into the PE header is just easier, and you get an UTC timestamp that you can do the correct stuff with. I use the version number for things other than determining build date now a days. However, I wrote my code a bit differently, I wouldn't assume that the timestamp is found at a specific offset every time. – John Leidegren Sep 30 '10 at 20:04
  • 3
    I just added one hour if `TimeZone.CurrentTimeZone.IsDaylightSavingTime(buildDateTime) == true` – e4rthdog Jun 21 '13 at 05:42
  • 2
    Unfortunately I used this approach without thoroughly vetting it, and it's biting us in production. The problem is that when the JIT compiler kicks in the PE header info is changed. Hence the downvote. Now I'm getting to do unneeded 'research' to explain why we see the install date as the build date. – Jason D May 09 '14 at 13:12
  • 11
    @JasonD In what universe does your problem somehow become my problem? How do you justify a downvote simply because you ran into an issue that this implementation didn't take into consideration. You got this for free and you tested it poorly. Also what makes you believe the the header is being rewritten by the JIT compiler? Are you reading this information from process memory or from file? – John Leidegren May 11 '14 at 14:39
  • 1
    Because the PE Header build date is not the same as the actual build date. The title of the question is "Displaying the build date." You had no caveats warning about the JIT. Hence the downvote. – Jason D Jun 10 '14 at 19:58
  • The "new" solution. Stupid question, but where does this go? In `Program.cs`? Also, what's the `CoffHeader` type? – gamut Apr 06 '16 at 16:10
  • 6
    I've noticed that if you are running in a web application, the .Codebase property appears to be a URL (file://c:/path/to/binary.dll). This causes the File.Exists call to fail. Using "assembly.Location" instead of the CodeBase property resolved the issue for me. – mdryden Dec 06 '17 at 20:51
  • 1
    @mdryden there has been various attempt to fix this by editing my answer. This eventually lead to a code sample that didn't even compile. I've rolled back this answer a couple of times because I think it's excessive editing, you are free to provide your own answer to the question, if so inclined. With that said, you may want to look at the change history for this answer as it might have a couple of clues in there and if you're really up to it, provide your own answer. – John Leidegren Dec 07 '17 at 07:47
  • 1
    @JohnLeidegren Thanks John. I don't think I'd suggest changing it, I'm sure my change wouldn't work for someone else, as is usually the case with these things. I just figured a follow up comment might help someone else if they ran into the same issue. – mdryden Dec 11 '17 at 01:33
  • The "new way" worked for me, I had only to build the path using URI. Tanks. – Thadeu Antonio Ferreira Melo Jan 24 '18 at 15:01
  • 3
    @JohnLeidegren: Don't rely on Windows PE header for that. [Since Windows 10](https://blogs.msdn.microsoft.com/oldnewthing/20180103-00/?p=97705) and [reproducible builds](https://reproducible-builds.org/), the `IMAGE_FILE_HEADER::TimeDateStamp` field is set to a random number and is no longer a time-stamp. – c00000fd Feb 13 '19 at 10:21
  • Fix for "path": string codeBase = assembly.CodeBase; UriBuilder uri = new UriBuilder(codeBase); string path = Uri.UnescapeDataString(uri.Path); – klm_ Mar 12 '19 at 06:40
  • Note that the 32-bit `TimeDateStamp` will overflow on Jan 19, 2037, just like all 32-bit Unix-epoch timestamps embedded within binary files. – David R Tribble Jul 08 '19 at 22:54
63

Add below to pre-build event command line:

echo %date% %time% > "$(ProjectDir)\Resources\BuildDate.txt"

Add this file as resource, now you have 'BuildDate' string in your resources.

After inserting the file into the Resource (as public text file), I accessed it via

string strCompTime = Properties.Resources.BuildDate;

To create resources, see How to create and use resources in .NET.

Community
  • 1
  • 1
brewmanz
  • 992
  • 8
  • 16
  • 1
    @DavidGorsline - the comment markdown was correct as it is quoting [this other answer](http://stackoverflow.com/a/15502932/1364007). I have insufficient reputation to rollback your change, otherwise I'd have done it myself. – Wai Ha Lee Jul 07 '15 at 13:09
  • 2
    @Wai Ha Lee - a) the answer that you quote does not give code to actually retrieve the compilation date/time. b) at the time I did not have enough reputation to add comment to that answer (which I would have done), only to post. so c) I posted giving full answer so people could get all details in one area.. – brewmanz Jul 08 '15 at 20:11
  • If you see Úte% instead of %date%, check here: https://developercommunity.visualstudio.com/content/problem/237752/prebuild-event-command-line-date-issue.html In a nutshell, do this: echo %25date%25 %25time%25 – Qodex Mar 24 '19 at 17:45
48

One approach which I'm amazed no-one has mentioned yet is to use T4 Text Templates for code generation.

<#@ template debug="false" hostspecific="true" language="C#" #>
<#@ assembly name="System.Core" #>
<#@ import namespace="System" #>
<#@ output extension=".g.cs" #>
using System;
namespace Foo.Bar
{
    public static partial class Constants
    {
        public static DateTime CompilationTimestampUtc { get { return new DateTime(<# Write(DateTime.UtcNow.Ticks.ToString()); #>L, DateTimeKind.Utc); } }
    }
}

Pros:

  • Locale-independent
  • Allows a lot more than just the time of compilation

Cons:

Peter Taylor
  • 4,560
  • 1
  • 32
  • 57
  • 1
    So, this is now the best answer. 324 points to go before it becomes the top voted answer :). Stackoverflow needs a way to show the fastest climber. – pauldendulk Apr 04 '18 at 14:54
  • 1
    @pauldendulk, wouldn't help much, because the most upvoted answer and the accepted answer nearly always pick up votes fastest. The accepted answer to this question has [+60/-2](http://data.stackexchange.com/stackoverflow/query/834729) since I posted this answer. – Peter Taylor Apr 04 '18 at 15:01
  • I believe you need to add a .ToString() to your Ticks (I get a compilation error otherwise). That said, I'm running into a steep learning curve here, could you show how to USE this in the main program too ? – Andy Jan 30 '19 at 19:17
  • @Andy, you're right about the ToString(). Usage is just `Constants.CompilationTimestampUtc`. If VS isn't generating a C# file with the class then you need to figure out how to get it to do that, but the answer depends on (at the very least) the version of VS and the type of csproj file, so it's too much detail for this post. – Peter Taylor Jan 30 '19 at 19:29
  • 2
    In case others are wondering, this is what it took to get it working on VS 2017: I had to make this a Design Time T4 template (Took me a while to figure out, I added a Preprocessor template first). I also had to include this assembly: Microsoft.VisualStudio.TextTemplating.Interfaces.10.0 as a reference to the project. Finally my template had to include "using System;" before the namespace, or else the reference to DateTime failed. – Andy Jan 30 '19 at 19:40
  • Sorry, I might have been wrong about the Design Template versus Preprocessor. So question: I'm using Custom Tool: TextTemplatingPreProcessor, yet the output file is not updated on each build. Am I missing something else to force it to update on each compilation? – Andy Jan 30 '19 at 20:24
  • Got it working. Make sure that you install the Clarius.TransformOnBuild NuGet package that is linked to in the answer (Duh), and the template should use the TextTemplatingPreProcessor custom tool after all... Works now. – Andy Jan 30 '19 at 20:48
  • I'm a big fan of T4, but it isn't appropriate for this use case. It's awkward to configure compile-time generation in framework projects and nigh impossible in .Net Core with the new "SDK" csproj system. I only want the build date, so the simpler and more reliable `WriteLinesToFile` MSBuild task approach better: https://stackoverflow.com/a/50905092/418362 – Artfunkel Apr 16 '20 at 11:48
27

Lots of great answers here but I feel like I can add my own because of simplicity, performance (comparing to resource-related solutions) cross platform (works with Net Core too) and avoidance of any 3rd party tool. Just add this msbuild target to the csproj.

<Target Name="Date" BeforeTargets="BeforeBuild">
    <WriteLinesToFile File="$(IntermediateOutputPath)gen.cs" Lines="static partial class Builtin { public static long CompileTime = $([System.DateTime]::UtcNow.Ticks) %3B }" Overwrite="true" />
    <ItemGroup>
        <Compile Include="$(IntermediateOutputPath)gen.cs" />
    </ItemGroup>
</Target>

and now you have Builtin.CompileTime in this project, e.g.:

var compileTime = new DateTime(Builtin.CompileTime, DateTimeKind.Utc);

ReSharper is not gonna like it. You can ignore him or add a partial class to the project too but it works anyway.

UPD: Nowadays ReSharper have an option in a first page of Options: "MSBuild access", "Obtain data from MSBuild after each compilation". This helps with visibility of generated code.

Dmitry Gusarov
  • 1,096
  • 10
  • 19
  • 1
    I can build with this and develop locally (run websites) in ASP.NET Core 2.1 but web deploy publishing from VS 2017 fails with the error "The name 'Builtin' does not exist in the current context". ADDITION: If I am accessing `Builtin.CompileTime` from a Razor view. – Jeremy Cook Nov 05 '18 at 18:16
  • In this case I think you just need ```BeforeTargets="RazorCoreCompile"``` but only while this is in the same project – Dmitry Gusarov Sep 04 '19 at 13:04
  • cool, but how do we refer to the generated object? it seems to me the answer is missing a key part... – Matteo Mar 12 '20 at 15:52
  • 1
    @Matteo, as mentioned in the answer, you can use "Builtin.CompileTime" or "new DateTime(Builtin.CompileTime, DateTimeKind.Utc)". Visual Studio IntelliSense is capable to see this right away. An old ReSharper can complain in design time, but looks like they fixed this in new versions. https://clip2net.com/s/46rgaaO – Dmitry Gusarov Mar 13 '20 at 22:34
  • 1
    I used this version so no need for additional code to get the date. Also resharper does not complain with its latest version. – Softlion May 11 '20 at 06:06
  • I had to change BeforeTargets="CoreCompile" to BeforeTargets="BeforeBuild" to resolve the "The name x does not exist" error. – John Thoits Sep 01 '20 at 01:52
22

Regarding the technique of pulling build date/version info from the bytes of an assembly PE header, Microsoft has changed the default build parameters beginning with Visual Studio 15.4. The new default includes deterministic compilation, which makes a valid timestamp and automatically incremented version numbers a thing of the past. The timestamp field is still present but it gets filled with a permanent value that is a hash of something or other, but not any indication of the build time.

Some detailed background here

For those who prioritize a useful timestamp over deterministic compilation, there is a way to override the new default. You can include a tag in the .csproj file of the assembly of interest as follows:

  <PropertyGroup>
      ...
      <Deterministic>false</Deterministic>
  </PropertyGroup>

Update: I endorse the T4 text template solution described in another answer here. I used it to solve my issue cleanly without losing the benefit of deterministic compilation. One caution about it is that Visual Studio only runs the T4 compiler when the .tt file is saved, not at build time. This can be awkward if you exclude the .cs result from source control (since you expect it to be generated) and another developer checks out the code. Without resaving, they won't have the .cs file. There is a package on nuget (I think called AutoT4) that makes T4 compilation part of every build. I have not yet confronted the solution to this during production deployment, but I expect something similar to make it right.

IKavanagh
  • 5,704
  • 11
  • 38
  • 44
Tom
  • 286
  • 2
  • 7
18

I am just C# newbie so maybe my answer sound silly - I display the build date from the date the executable file was last written to:

string w_file = "MyProgram.exe"; 
string w_directory = Directory.GetCurrentDirectory();

DateTime c3 =  File.GetLastWriteTime(System.IO.Path.Combine(w_directory, w_file));
RTB_info.AppendText("Program created at: " + c3.ToString());

I tried to use File.GetCreationTime method but got weird results: the date from the command was 2012-05-29, but the date from the Window Explorer showed 2012-05-23. After searching for this discrepancy I found that the file was probably created on 2012-05-23 (as shown by Windows Explorer), but copied to the current folder on 2012-05-29 (as shown by File.GetCreationTime command) - so to be on the safe side I am using File.GetLastWriteTime command.

Zalek

Zalek Bloom
  • 525
  • 4
  • 13
  • 4
    I'm not sure if this is bullet proof from copying the executable across drives / computers / networks. – Stealth Rabbi Nov 01 '13 at 17:24
  • this is the first thing comes in mind but you know its not reliable there are many software used to move the files over the network which do not update the attributes after downloading, i would go with @Abdurrahim's answer. – Mubashar Jan 10 '14 at 05:00
  • I know this is old, but I just found with some similar code that the INSTALL process (at least when using clickonce) updates the assembly file time. Not very useful. Not sure it would apply to this solution, though. – bobwki Aug 20 '18 at 23:56
  • You probably really want the `LastWriteTime`, since that accurately reflects the time that the executable file was actually updated. – David R Tribble Jul 08 '19 at 22:57
  • Sorry but an executable file write time is not a reliable indication of the build time. The file time stamp can be rewritten due to all kinds of things that are outside your sphere of influence. – Tom Sep 05 '19 at 19:36
18

For .NET Core projects, I adapted Postlagerkarte's answer to update the assembly Copyright field with the build date.

Directly Edit csproj

The following can be added directly to the first PropertyGroup in the csproj:

<Copyright>Copyright © $([System.DateTime]::UtcNow.Year) Travis Troyer ($([System.DateTime]::UtcNow.ToString("s")))</Copyright>

Alternative: Visual Studio Project Properties

Or paste the inner expression directly into the Copyright field in the Package section of the project properties in Visual Studio:

Copyright © $([System.DateTime]::UtcNow.Year) Travis Troyer ($([System.DateTime]::UtcNow.ToString("s")))

This can be a little confusing, because Visual Studio will evaluate the expression and display the current value in the window, but it will also update the project file appropriately behind the scenes.

Solution-wide via Directory.Build.props

You can plop the <Copyright> element above into a Directory.Build.props file in your solution root, and have it automatically applied to all projects within the directory, assuming each project does not supply its own Copyright value.

<Project>
 <PropertyGroup>
   <Copyright>Copyright © $([System.DateTime]::UtcNow.Year) Travis Troyer ($([System.DateTime]::UtcNow.ToString("s")))</Copyright>
 </PropertyGroup>
</Project>

Directory.Build.props: Customize your build

Output

The example expression will give you a copyright like this:

Copyright © 2018 Travis Troyer (2018-05-30T14:46:23)

Retrieval

You can view the copyright information from the file properties in Windows, or grab it at runtime:

var version = FileVersionInfo.GetVersionInfo(Assembly.GetEntryAssembly().Location);

Console.WriteLine(version.LegalCopyright);
Travis Troyer
  • 531
  • 4
  • 7
12

The above method can be tweaked for assemblies already loaded within the process by using the file's image in memory (as opposed to re-reading it from storage):

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;
    }
}
tcostin
  • 121
  • 1
  • 3
  • This one works great, even for framework 4.7 Usage: Utils.GetLinkerDateTime(Assembly.GetExecutingAssembly(), null)) – real_yggdrasil Jan 23 '18 at 10:47
  • This does work when building a Debug-release, but kills my application (no exception thrown) when building as Release. Unfortunately, I have no clue as to why. I am using Visual Studio 2019 in Windows 10 20H2. – Betaminos Oct 23 '20 at 17:35
12

In 2018 some of the above solutions do not work anymore or do not work with .NET Core.

I use the following approach which is simple and works for my .NET Core 2.0 project.

Add the following to your .csproj inside the PropertyGroup :

    <Today>$([System.DateTime]::Now)</Today>

This defines a PropertyFunction which you can access in your pre build command.

Your pre-build looks like this

echo $(today) > $(ProjectDir)BuildTimeStamp.txt

Set the property of the BuildTimeStamp.txt to Embedded resource.

Now you can read the time stamp like this

public static class BuildTimeStamp
    {
        public static string GetTimestamp()
        {
            var assembly = Assembly.GetEntryAssembly(); 

            var stream = assembly.GetManifestResourceStream("NamespaceGoesHere.BuildTimeStamp.txt");

            using (var reader = new StreamReader(stream))
            {
                return reader.ReadToEnd();
            }
        }
    }
Postlagerkarte
  • 5,047
  • 2
  • 24
  • 45
  • 1
    Just generating that BuildTimeStamp.txt from the pre-build events [using batch script commands](https://stackoverflow.com/a/203116/395685) also works. Do note that you made a mistake there: you should surround your target in quotes (e.g. `"$(ProjectDir)BuildTimeStamp.txt"`) or it'll break when there are spaces in the folder names. – Nyerguds Jun 18 '18 at 08:05
  • 1
    Maybe it makes sense to use culture invariant time format. Like this: `$([System.DateTime]::Now.tostring("MM/dd/yyyy HH:mm:ss"))` instead of `$([System.DateTime]::Now)` – Ivan Kochurkin Aug 07 '18 at 13:06
  • See https://stackoverflow.com/a/11336754/4675770 to get rid of the newline character produced by the echo command, so that the txt file has one line instead of two lines. – Jinjinov Aug 04 '20 at 14:25
10

For anyone that needs to get the compile time in Windows 8 / Windows Phone 8:

    public static async Task<DateTimeOffset?> RetrieveLinkerTimestamp(Assembly assembly)
    {
        var pkg = Windows.ApplicationModel.Package.Current;
        if (null == pkg)
        {
            return null;
        }

        var assemblyFile = await pkg.InstalledLocation.GetFileAsync(assembly.ManifestModule.Name);
        if (null == assemblyFile)
        {
            return null;
        }

        using (var stream = await assemblyFile.OpenSequentialReadAsync())
        {
            using (var reader = new DataReader(stream))
            {
                const int PeHeaderOffset = 60;
                const int LinkerTimestampOffset = 8;

                //read first 2048 bytes from the assembly file.
                byte[] b = new byte[2048];
                await reader.LoadAsync((uint)b.Length);
                reader.ReadBytes(b);
                reader.DetachStream();

                //get the pe header offset
                int i = System.BitConverter.ToInt32(b, PeHeaderOffset);

                //read the linker timestamp from the PE header
                int secondsSince1970 = System.BitConverter.ToInt32(b, i + LinkerTimestampOffset);

                var dt = new DateTimeOffset(1970, 1, 1, 0, 0, 0, DateTimeOffset.Now.Offset) + DateTimeOffset.Now.Offset;
                return dt.AddSeconds(secondsSince1970);
            }
        }
    }

For anyone that needs to get the compile time in Windows Phone 7:

    public static async Task<DateTimeOffset?> RetrieveLinkerTimestampAsync(Assembly assembly)
    {
        const int PeHeaderOffset = 60;
        const int LinkerTimestampOffset = 8;            
        byte[] b = new byte[2048];

        try
        {
            var rs = Application.GetResourceStream(new Uri(assembly.ManifestModule.Name, UriKind.Relative));
            using (var s = rs.Stream)
            {
                var asyncResult = s.BeginRead(b, 0, b.Length, null, null);
                int bytesRead = await Task.Factory.FromAsync<int>(asyncResult, s.EndRead);
            }
        }
        catch (System.IO.IOException)
        {
            return null;
        }

        int i = System.BitConverter.ToInt32(b, PeHeaderOffset);
        int secondsSince1970 = System.BitConverter.ToInt32(b, i + LinkerTimestampOffset);
        var dt = new DateTimeOffset(1970, 1, 1, 0, 0, 0, DateTimeOffset.Now.Offset) + DateTimeOffset.Now.Offset;
        dt = dt.AddSeconds(secondsSince1970);
        return dt;
    }

NOTE: In all cases you're running in a sandbox, so you'll only be able to get the compile time of assemblies that you deploy with your app. (i.e. this won't work on anything in the GAC).

Matt Dotson
  • 5,522
  • 1
  • 21
  • 23
  • Here's how you get the Assembly in WP 8.1: `var assembly = typeof (AnyTypeInYourAssembly).GetTypeInfo().Assembly;` – André Fiedler Sep 01 '15 at 09:18
  • What if you want to run your code on both systems ? - is one of these methods applicable for both platforms ? – bvdb Feb 10 '17 at 10:14
9

I just do:

File.GetCreationTime(GetType().Assembly.Location)
Rui Santos
  • 91
  • 1
  • 2
  • 2
    Interestingly, if running from debug, the 'true' date is the GetLastAccessTime() – balint Feb 07 '19 at 14:13
  • Note, you add `using System.IO;` and put it in the constructor of a class so `GetType()` works on an instance. – phyatt Apr 21 '21 at 18:15
8

The option not discussed here is to insert your own data into AssemblyInfo.cs, the "AssemblyInformationalVersion" field seems appropriate - we have a couple of projects where we were doing something similar as a build step (however I'm not entirely happy with the way that works so don't really want to reproduce what we've got).

There's an article on the subject on codeproject: http://www.codeproject.com/KB/dotnet/Customizing_csproj_files.aspx

Murph
  • 9,675
  • 2
  • 22
  • 40
6

I needed a universal solution that worked with a NETStandard project on any platform (iOS, Android, and Windows.) To accomplish this, I decided to automatically generate a CS file via a PowerShell script. Here is the PowerShell script:

param($outputFile="BuildDate.cs")

$buildDate = Get-Date -date (Get-Date).ToUniversalTime() -Format o
$class = 
"using System;
using System.Globalization;

namespace MyNamespace
{
    public static class BuildDate
    {
        public const string BuildDateString = `"$buildDate`";
        public static readonly DateTime BuildDateUtc = DateTime.Parse(BuildDateString, null, DateTimeStyles.AssumeUniversal | DateTimeStyles.AdjustToUniversal);
    }
}"

Set-Content -Path $outputFile -Value $class

Save the PowerScript file as GenBuildDate.ps1 and add it your project. Finally, add the following line to your Pre-Build event:

powershell -File $(ProjectDir)GenBuildDate.ps1 -outputFile $(ProjectDir)BuildDate.cs

Make sure BuildDate.cs is included in your project. Works like a champ on any OS!

David Tosi
  • 61
  • 1
  • 3
  • 1
    You can also use this to get the SVN revision number using the svn command line tool. I've done something similar to this with that. – user169771 Apr 05 '18 at 16:13
5

A different, PCL-friendly approach would be to use an MSBuild inline task to substitute the build time into a string that is returned by a property on the app. We are using this approach successfully in an app that has Xamarin.Forms, Xamarin.Android, and Xamarin.iOS projects.

EDIT:

Simplified by moving all of the logic into the SetBuildDate.targets file, and using Regex instead of simple string replace so that the file can be modified by each build without a "reset".

The MSBuild inline task definition (saved in a SetBuildDate.targets file local to the Xamarin.Forms project for this example):

<Project xmlns='http://schemas.microsoft.com/developer/msbuild/2003' ToolsVersion="12.0">

  <UsingTask TaskName="SetBuildDate" TaskFactory="CodeTaskFactory" 
    AssemblyFile="$(MSBuildToolsPath)\Microsoft.Build.Tasks.v12.0.dll">
    <ParameterGroup>
      <FilePath ParameterType="System.String" Required="true" />
    </ParameterGroup>
    <Task>
      <Code Type="Fragment" Language="cs"><![CDATA[

        DateTime now = DateTime.UtcNow;
        string buildDate = now.ToString("F");
        string replacement = string.Format("BuildDate => \"{0}\"", buildDate);
        string pattern = @"BuildDate => ""([^""]*)""";
        string content = File.ReadAllText(FilePath);
        System.Text.RegularExpressions.Regex rgx = new System.Text.RegularExpressions.Regex(pattern);
        content = rgx.Replace(content, replacement);
        File.WriteAllText(FilePath, content);
        File.SetLastWriteTimeUtc(FilePath, now);

   ]]></Code>
    </Task>
  </UsingTask>

</Project>

Invoking the above inline task in the Xamarin.Forms csproj file in target BeforeBuild:

  <!-- To modify your build process, add your task inside one of the targets below and uncomment it. 
       Other similar extension points exist, see Microsoft.Common.targets.  -->
  <Import Project="SetBuildDate.targets" />
  <Target Name="BeforeBuild">
    <SetBuildDate FilePath="$(MSBuildProjectDirectory)\BuildMetadata.cs" />
  </Target>

The FilePath property is set to a BuildMetadata.cs file in the Xamarin.Forms project that contains a simple class with a string property BuildDate, into which the build time will be substituted:

public class BuildMetadata
{
    public static string BuildDate => "This can be any arbitrary string";
}

Add this file BuildMetadata.cs to project. It will be modified by every build, but in a manner that allows repeated builds (repeated replacements), so you may include or omit it in source control as desired.

Mark Larter
  • 2,295
  • 1
  • 23
  • 32
3

You can use this project: https://github.com/dwcullop/BuildInfo

It leverages T4 to automate the build date timestamp. There are several versions (different branches) including one that gives you the Git Hash of the currently checked out branch, if you're into that sort of thing.

Disclosure: I wrote the module.

Darrin Cullop
  • 1,121
  • 8
  • 13
2

A small update on the "New Way" answer from Jhon.

You need to build the path instead of using the CodeBase string when working with ASP.NET/MVC

    var codeBase = assembly.GetName().CodeBase;
    UriBuilder uri = new UriBuilder(codeBase);
    string path = Uri.UnescapeDataString(uri.Path);
Ole Albers
  • 7,645
  • 7
  • 56
  • 130
1

I'm not sure, but maybe the Build Incrementer helps.

Greg Sansom
  • 19,197
  • 6
  • 54
  • 72
Bobby
  • 10,998
  • 5
  • 42
  • 67
1

You could use a project post-build event to write a text file to your target directory with the current datetime. You could then read the value at run-time. It's a little hacky, but it should work.

MikeWyatt
  • 7,692
  • 10
  • 44
  • 69
1

I used Abdurrahim's suggestion. However, it seemed to give a weird time format and also added the abbreviation for the day as part of the build date; example: Sun 12/24/2017 13:21:05.43. I only needed just the date so I had to eliminate the rest using substring.

After adding the echo %date% %time% > "$(ProjectDir)\Resources\BuildDate.txt"to the pre-build event, I just did the following:

string strBuildDate = YourNamespace.Properties.Resources.BuildDate;
string strTrimBuildDate = strBuildDate.Substring(4).Remove(10);

The good news here is that it worked.

DemarcPoint
  • 137
  • 1
  • 6
  • Very simple solution. I like it. And if the format is a bother, there are [ways to get a better format](https://stackoverflow.com/a/203116/395685) from the command line. – Nyerguds Jun 18 '18 at 08:07
0

You could launch an extra step in the build process that writes a date stamp to a file which can then be displayed.

On the projects properties tab look at the build events tab. There is an option to execute a pre or post build command.

0

I just added pre-build event command:

powershell -Command Get-Date -Format 'yyyy-MM-ddTHH:mm:sszzz' > Resources\BuildDateTime.txt

in the project properties to generate a resource file that is then easy to read from the code.

Syr
  • 1
  • 2
0

I had difficulties with the suggested solutions with my project, a .Net Core 2.1 web application. I combined various suggestions from above and simplified, and also converted the date to my required format.

The echo command:

echo Build %DATE:~-4%/%DATE:~-10,2%/%DATE:~-7,2% %time% > "$(ProjectDir)\BuildDate.txt"

The code:

Logger.Info(File.ReadAllText(@"./BuildDate.txt").Trim());

It seems to work. The output:

2021-03-25 18:41:40,877 [1] INFO Config - Build 2021/03/25 18:41:37.58

Nothing very original, I just combined suggestions from here and other related questions, and simplified.

Jonathan Rosenne
  • 2,055
  • 15
  • 24
-1

If this is a windows app, you can just use the application executable path: new System.IO.FileInfo(Application.ExecutablePath).LastWriteTime.ToString("yyyy.MM.dd")

John Clark
  • 27
  • 2
-1

it could be Assembly execAssembly = Assembly.GetExecutingAssembly(); var creationTime = new FileInfo(execAssembly.Location).CreationTime; // "2019-09-08T14:29:12.2286642-04:00"

Sasha Bond
  • 680
  • 8
  • 8