DEV Community

Cover image for An easy way to practice version embedding on .NET
Kouji Matsui
Kouji Matsui

Posted on • Edited on

An easy way to practice version embedding on .NET

Introduction

Have you ever wanted to visualize the version number of your application when you distribute it? There are many possible reasons:

  • To display the version of the application when it is launched.
  • To include it in diagnostic information for smooth support (and possibly output it to a log)
  • Other...

From my many years of experience in software support and maintenance, I have come to believe that self-diagnostic information is important when a problem occurs. One of those materials is the version number.

So, how should we manage version numbers in a .NET application? For example, consider the following console application:

// No command line arguments are given.
if (args.Length == 0)
{
  // Display help and exit
  Console.WriteLine("FooBarNet 1.0.0");  // <-- Version number
  return 0;
}
Enter fullscreen mode Exit fullscreen mode

For very small code base, this method of hard-coding and embedding the version code is a good idea. However, in general, it is easy to make mistakes when thinking about releasing the application:

  • Forgot to update the version number.
  • You gave the wrong version number.
  • etc...

The problem does not stop with released application binaries. Another headache is the difficulty in identifying the source code version that corresponds to the binary version number.

Cooperating partners I have seen have done things like "freezing" the source code at the time of product release and compressing it, along with the compiled binaries, into a ZIP file or saving it to external storage media and labeling it.

"No, no way, in this day and age?" You might think :)

So how do we solve this problem in a "modern" way?

For source code, it is probably common practice to use a source code management system such as Git or Subversion. Some teams may store binaries at the time of release together in these repositories.

That is one way to do it, but it has the disadvantage of increasing the size of the repository. Also, there remains the problem that the first diagnostic step is not resolved when an application problem occurs.


Automatically embed version

The RelaxVersioner project described in this article is one way to solve this problem. Simply install this NuGet package in your project and automatically embed the version number in your application. No big deal, just install. It should be enough for you to try it out and see what happens.

If you want, you can also easily reference the version number as follows:

// Refer to the version number:
Console.WriteLine($"FooBarNet {ThisAssembly.AssemblyVersion}");
Enter fullscreen mode Exit fullscreen mode

Looking at this code, the following questions may arise:

  • Doesn't the version number refer to the AssemblyVersionAttribute?
  • Doesn't the version number use the reflection API?
  • How is the version number determined in the first place?

There are several types of version numbers in .NET, and of course, RelaxVersioner applies these standard attribute classes as well. On top of that, it also defines literal strings, as shown above, for ease of use in programmatically referencing versions.

.NET standard attribute classes, so that information can be displayed even when the application binary is viewed from the Explorer:

Explorer image

More detailed information can be visualized by using a decompilation tool such as ILSpy:

ILSpy image

I will then explain the core of the last question:


How is the version number determined?

If you can apply a version number simply by installing the NuGet package of RelaxVersioner, how is that version number determined?

Let us reveal the seed. It refers to a tag in the Git repository. That is, RelaxVersioner refers to the local Git repository where the project is located and uses the tag with the version number applied. If there is a tag of the form "1.2.3" or "v1.2.3" separated by dots, as shown below, the commit is recognized as that version:

Version tagging and automatic calculation 1

  • Screenshots are from GitKraken.

Then, what happens in a commit without a tag is that the trailing value of the version number, separated by a dot, is automatically incremented. In the example above, a commit on a branch derived from 0.5.0 would be calculated to 0.5.1 or 0.5.3.

Let us show a slightly less simple example. Where a merge commit occurs, the version number increment is computed on the primary parent commit first, as in the following example:

Version tagging and automatic calculation 2

Therefore, it is 0.5.1, not 0.5.6. You may think, "This would result in duplicate version numbers!", but the question is whether the application has been released or not. Once you have released your application, add a tag to it. Then the calculation will be based on the tag number.

In this case, if you want the commit in the red box to be 0.5.6, add the tag "0.5.6" to this commit. There must be a reason why you want the version number to be 0.5.6 intentionally. Perhaps you want to release it, or have released it. The term "release" here is not limited to public, but private releases as well.

This specification in RelaxVersioner is based on the idea that Git's local repository exists for the developer, not for release management. We wanted developers to be able to manipulate the commits of their own free (freedom) will, unless they decide to "release this commit. This way, you can enjoy the flexibility of Git while at the same time managing releases.


How is the version number embedded?

Some of you may have questions about how this can be accomplished simply by installing the NuGet package of RelaxVersioner. In this section, we will briefly explain how RelaxVersioner actually embeds the version number.

  1. First, check if the directory where the .NET project exists is under a local repository in Git.
  2. then search the Git local repository for the current checkout commit. If no commits exist at all, set the version number to 0.0.1. (either a newly created Git repository or you didn't git init it in the first place)
  3. if the commit exists and a tag corresponding to the version number is found, use that version number. If the commit exists but does not have a tag, the tag corresponding to the version number is found by traversing the parent commits in order.
  4. based on the found version number and the MSBuild property information group, the template source code is generated based on the rule file. The default rule file is built in, so if it does not exist, use the default. The template source code generates C#, F#, VB.NET, and C++/CLI code in a temporary directory according to the language used in the project.
  5. ensure that the template source code is included in the MSBuild build target.

These steps are performed by MSBuild scripts and the RelaxVersioner command line tool. When the tool runs at build time, you will see a message similar to the following:

1>RelaxVersioner[2.5.5]: Generated versions code: Language=C#, Version=0.7.12
1>FlashCap -> C:\Projects\FlashCap\FlashCap\bin\Debug\net6.0\FlashCap.dll
Enter fullscreen mode Exit fullscreen mode

If your project is multi-platform, you will see many of the same messages in the build log, which is not unusual, because RelaxVersioner generates template source code for each platform.

And the template source code is as follows (in C#) :

using System.Reflection;
[assembly: AssemblyVersion("1.0.21")]
[assembly: AssemblyFileVersion("2020.12.20.33529")]
[assembly: AssemblyInformationalVersion("1.0.21-561387e2f6dc90046f56ef4c3ac501aad0d5ec0a")]
[assembly: AssemblyMetadata("Date","Sun, 20 Dec 2020 09:37:39 GMT")]
[assembly: AssemblyMetadata("Branch","master")]
[assembly: AssemblyMetadata("Tags","")]
[assembly: AssemblyMetadata("Author","Kouji Matsui <k@kekyo.net>")]
[assembly: AssemblyMetadata("Committer","Kouji Matsui <k@kekyo.net>")]
[assembly: AssemblyMetadata("Message","Merge branch 'devel'")]
[assembly: AssemblyMetadata("Build","")]
[assembly: AssemblyMetadata("Generated","Sun, 20 Dec 2020 09:37:43 GMT")]
[assembly: AssemblyMetadata("Platform","AnyCPU")]
[assembly: AssemblyMetadata("BuildOn","Unix")]
[assembly: AssemblyMetadata("SdkVersion","5.0.101")]

namespace YourApp
{
  internal static class ThisAssembly
  {
    public const string AssemblyVersion = "1.0.21";
    public const string AssemblyFileVersion = "2020.12.20.33529";
    public const string AssemblyInformationalVersion = "1.0.21-561387e2f6dc90046f56ef4c3ac501aad0d5ec0a";
    public static class AssemblyMetadata
    {
      public const string Date = "Sun, 20 Dec 2020 09:37:39 GMT";
      public const string Branch = "master";
      public const string Tags = "";
      public const string Author = "Kouji Matsui <k@kekyo.net>";
      public const string Committer = "Kouji Matsui <k@kekyo.net>";
      public const string Message = "Merge branch 'devel'";
      public const string Build = "";
      public const string Generated = "Sun, 20 Dec 2020 09:37:43 GMT";
      public const string Platform = "AnyCPU";
      public const string BuildOn = "Unix";
      public const string SdkVersion = "5.0.101";
    }
  }
}
Enter fullscreen mode Exit fullscreen mode

As you can see, each attribute applies and defines members that can be referenced as literal strings.

This source code is placed in a temporary directory. Generally, it is in "obj/" in the project directory. Thus, this source code will not cause unwanted contamination of the commits in the Git repository. And because the entire process is fully automated, it can also be used for continuous integration.


Packaging

Did you know that you can easily NuGet package a library project using the dotnet CLI commands? If you have the library project at hand, you can generate it with the following command:

dotnet pack -p:Configuration=Release -o artifacts FooBarComponent\FooBarComponent.csproj
Enter fullscreen mode Exit fullscreen mode

This will create a package file in the artifacts directory, such as FooBarComponent.1.0.0.nupkg. Upload it to nuget.org and you are done. It's that easy.

... Easy, isn't it?

Uploading and publishing a package requires various metadata:

  • The version number of the package
  • Project site URL
  • Name of the authors, etc.
  • Source code repository URL
  • etc...

NuGet package example

Among these, there are several pieces of information that should be versioned:

  • Package version
  • Version information applicable to the binaries (assembly files) contained in the package
  • Source code repository URL
  • The corresponding commit ID

The repository URL and commit ID are optional, but if this metadata can be provided, a powerful debugger feature called SourceLink becomes available.

For example, the famous Newtonsoft.Json package, which we have all used, allows you to step into methods in that package and reference implementations.

You can step into methods contained in the package and reference the implementation. IDEs such as Visual Studio can then automatically download source code from GitHub and continue debugging on the fly, as if the entire project existed locally.

To add even more value, you can use RelaxVersioner to apply versioning to NuGet packages completely automatically: install the RelaxVersioner package in your project, and run same as:

dotnet pack -p:Configuration=Release -o artifacts FooBarComponent\FooBarComponent.csproj
Enter fullscreen mode Exit fullscreen mode

If you want to support SourceLink, you will need to add an additional definition to FooBarComponent.csproj, but only once. In other words, once you install RelaxVersioner, you can consistently version control everything from assembly file generation to NuGet package generation using only Git.


Conclusion

.NET applications, "RelaxVersioner" can efficiently manage version numbers using tagging in Git repositories.

This reduces the time and effort required to manage version numbers by hard-coding source code, and also allows you to manage the version of the source code itself. The applied version number can be easily referenced from the code and included in the built binary, allowing the user to reference it in the explorer.

Top comments (0)