Updating AssemblyInfo.cs version information via batch file

Over a year ago I wrote how to build and publish NuGet packages via Jenkins in which I stated I would follow up with another article on modifying AssemblyInfo.cs via a batch file. Of course, I forgot to write that post. Recently I was adding a NuGet publish job to a TeamCity server which reminded me and therefore finally here is the article.

Don't Jenkins and TeamCity already do this?

While both Jenkins and TeamCity include or have available plugins for updating AssemblyInfo.cs, they both suffer from the problem in that they can write a version into the file but they can't read from it first to derive a new value. However, if you simply want to set a full version from within either CI tool you can without having to bother with anything in this post. As I wish to combine part of the existing version with a CI supplied value, I need to look at alternatives.

Reading text from a file via a batch script

The "simplest" way of reading text from a file in a batch script is to use a Unix utility named sed (stream editor). Why did I quote "simplest"? You'll see!

You can download a version compiled for Windows from SourceForge. If you choose to download the portable Binaries distribution make sure you also pick up the Dependencies package as well as this contains required DLL's.

Using sed

Although sed can operate using pipes in the familiar manner for DOS/batch commands, it also has an inline editing mode which is more convenient for our purposes.

sed -i "expression" filename

When using the /i option, sed will leave a temporary file behind. This normally shouldn't be a concern if you're using a CI tool and have performed a fresh checkout or clean-up before building, but can become really annoying if you run it in your source tree. You can always including a command to delete the temporary files (for example DEL sed*.), assuming you don't have real files with a similar pattern.

Defining an expression to update the revision

I may have lied when I said sed was simple! To modify our file, we want to substitute part of the existing AssemblyFileVersion or AssemblyInformationalVersion values. To do this we'll use sed's substitution command with a source pattern (a regular expression) and then another pattern for the replacement. Due to the way sed works, all 3 of these values need to be a single string parameter. (You can use external files, but that is beyond the scope of this example)

Getting the expressions working in sed can take a lot of trial and error. To easily test your expressions omit the -i switch from the command line - sed will then output the file to the console, allowing you to see the results of your expression without modifying the original file.

As a simple example, assume I wanted to replace the word Assembly with Library. The expression s/Assembly/Library/ would handle this - s is the command to use.

In the above example I'm using forward slash / to separate the arguments - the final separator is also required. As well as a slash you can also use the ^ character, which may be easier to read.

> sed "s/Assembly/Library/" Properties\AssemblyInfo.cs
[assembly: LibraryVersion("1.0.0.0")]
[assembly: LibraryFileVersion("1.4.3.1")]
[assembly: LibraryInformationalVersion("1.4.0.1")]

Admittedly that's not a very useful example. So we'll now change it to match the attribute instead.

> sed "s/Assembly\(Informational\|File\)Version/Library/" Properties\AssemblyInfo.cs
[assembly: AssemblyVersion("1.0.0.0")]
[assembly: Library("1.4.3.1")]
[assembly: Library("1.4.0.1")]

I'm deliberately not changing the assembly version given I strong name all assemblies and definitely do not want bindings to break from build number changes.

Notice how the regex capture group and logical or characters are escaped? If they aren't they won't function as a regular expression and no matches will be made.

Now we'll extend the pattern further to include the version information

> sed "s/\(Assembly\(Informational\|File\)Version(\d34[0-9]\+\.[0-9]\+\.[0-9]\+\.\)[0-9]/Library/" Properties\AssemblyInfo.cs
[assembly: AssemblyVersion("1.0.0.0")]
[assembly: Library")]
[assembly: Library")]

The lovely looking expression now captures the version as well. Note that I couldn't get sed to accept a quote character in the expression regardless of if I tried escaping it, but fortunately it provides the \d sequence for special characters - \d34 is the quote. Although it's awkward to read, we capture AttributeName("nnn.nnn.nnn. in a separate capture group so we can use it in our replacement expression.

Finally, lets actually replace the value with something useful. Jenkins and TeamCity both set an environment variable named BUILD_NUMBER so you can simply combine that with the group captured by the expression.

> set BUILD_NUMBER=0325

> sed "s/\(Assembly\(Informational\|File\)Version(\d34[0-9]\+\.[0-9]\+\.[0-9]\+\.\)[0-9]\+/\1%BUILD_NUMBER%/" Properties\AssemblyInfo.cs
[assembly: AssemblyVersion("1.0.0.0")]
[assembly: AssemblyFileVersion("1.4.3.0325")]
[assembly: AssemblyInformationalVersion("1.4.0.0325")]

The \1 component of the replacement pattern states which zero-based capture group to use, so in this example the second group which excludes the final version part.

And there we have it, a sed expression to update our assembly information.

Updating the build number instead

The above expression works very well with versions that use four components, e.g. 4.0.0.0. However, if you follow Semantic Versioning then you probably only use versions containing 3 components, in which case you'll want to update the 3rd part (build) instead of the 4th (revision).

Although I still use 4 part versions for product versions and for assemblies that aren't currently packaged, those that are try to follow SemVer. For these assemblies, I set AssemblyInformationalVersion to be a 3 part version, with the last part always zero. I leave AssemblyFileVersion at 4 parts with the third and fourth parts always zero.

The following expression can be used to update the third part of a version - it's identical to the 4 part version, except for dropping one set of captured digits.

s/\(Assembly\(Informational\|File\)Version(\d34[0-9]\+\.[0-9]\+\.\)[0-9]\+/\1%BUILD_NUMBER%/

Putting it all together

Below is an example batch script that I've been using for just over two years at time of writing to handle updating the version of my components. I have two versions of this file, one for when I want to update the third part of a version, and another for updating the fourth. I also prefer using ^ as the sed separator rather than /.

The calls to cecho can be replaced with just echo (and also remove the {x} sequences); this is a utility for printing to the console in colour.

@ECHO OFF

SET SRC=%1

IF "%~1"=="" GOTO :error
IF "%BUILD_NUMBER%"=="" GOTO :notset
IF NOT EXIST "%SRC%" GOTO :notfound

SED -i "s^\(Assembly\(Informational\|File\)Version(\d34[0-9]\+\.[0-9]\+\.[0-9]\+\.\)[0-9]\+^\1%BUILD_NUMBER%^" "%SRC%"

IF %ERRORLEVEL% NEQ 0 GOTO :failed

GOTO :eof

:notset
CECHO {0e}WARNING: BUILD_NUMBER environment variable not set, performing no action{#}{\n}
EXIT /b 0

:failed
CECHO {0c}ERROR  : Failed to process command{#}{\n}
EXIT /b 1

:notfound
CECHO {0c}ERROR  : Source '%1' not found{#}{\n}
EXIT /b 1

:error
CECHO {0c}ERROR  : Source not specified{#}{\n}
EXIT /b 1

Updating all AssemblyInfo.cs files

Although the above batch scripts are quite handy when updating a single file, what happens if you have multiple files to update? I recently started applying build numbers to the versions of our product suites and I had no intention of manually keeping track of which files to update.

Modern version of Windows include the forfiles.exe utility which "Selects a file (or set of files) and executes a command on that file. This is helpful for batch jobs.". And helpful it is for numerous scenarios - as well as file based matching it can also search by date and so another task I use it for is clearing old temporary files.

In the below example, I use the /S flag to search sub-directories, and the /M parameter to specify I want to match AssemblyInfo.cs. And finally, I set the command to run our updater batch file. This lets me update everything in a single directory tree.

@FORFILES /S /M AssemblyInfo.cs /C "CMD /C CALL updateversioninfo.cmd @path"

In some of my products, I have a shared AssemblyInfo.cs, imaginatively named SharedAssemblyInfo.cs. To have this picked up by the above command, change the mask argument to be *AssemblyInfo.cs.

What about PowerShell?

If you wanted a vanilla option which didn't require a third party program you could use PowerShell. As I'm slightly old school in regards to how I set up my build files, I still mainly use batch and so I haven't explored this option.

What about Visual Basic?

While I haven't programmed in Visual Basic .NET for over a decade, everything in this article can be used just as easily with VB projects - you'd just need to adjust the expression to cover how you define attributes in VB.net, and of course change .cs to .vb

What about Visual Studio 2017 projects?

Some projects created with Visual Studio 2017 store the assembly information directly in the XML project file. You could use the above technique with these projects too, but it's not something I've looked at as I still haven't fully switched to Visual Studio 2017 yet, and the projects that I do use it with, I deliberately choose to continue to have the meta data stored in AssemblyInfo.cs files.

About The Author

Gravatar

The founder of Cyotek, Richard enjoys creating new blog content for the site. Much more though, he likes to develop programs, and can often found writing reams of code. A long term gamer, he has aspirations in one day creating an epic video game. Until that time, he is mostly content with adding new bugs to WebCopy and the other Cyotek products.