Thursday, August 6, 2020

Displaying Git Commit and Build Info in an ASP.Net website

Scott Hanselman wrote a really good blog post on March 6, 2020 called Adding a git commit hash and Azure DevOps Build Number and Build ID to an ASP.NET website. In the post he covered passing build information into an ASP.Net web app so that it could be displayed on a web page to confirm which version of your code was running in the cloud. If you work with multiple machines, deployment slots or even if you just forget when you last pushed your code up you know how important it can be to confirm which version is running.

His article was well written, contained lots of great information and steps to follow but could it be improved. As I set out to implement it I found a couple of ways it could be.

My build process on Azure DevOps was the classic (legacy) UI build pipeline using MSBuild. I've wanted to update it to use dotnet for awhile so I figured this was a good reason to do so. I created a new Pipeline using the ASP.NET template. That created a new yaml file but it was using MSBuild. After some googling I was able to replace that with a simple dotnet driven pipeline.

The YAML pipeline shown below is about as basic as it gets (sidenote I had never used YAML before but it's pretty straighforward once you start working with it). Thi pipeline is triggered when a new commit happens on 'master'. It gets a windows vm and sets some variables and then starts the build. 'dotnet build' also does a NuGet restore first but you can break that out separate if you have packages from different sources. It then runs 'dotnet test' which runs through all of the unit tests. Then 'dotnet publish' to zip up the results and finally publishes the build artifact (the website zip). This artifact can be used in a Release deploy.


trigger:
- master

pool:
  vmImage: 'windows-latest'

variables:
  solution: '**/*.sln'
  buildPlatform: 'Any CPU'
  buildConfiguration: 'Release'

steps:
- task: DotNetCoreCLI@2
  displayName: 'dotnet build $(buildConfiguration)'
  inputs:
    command: 'build'
    arguments: '--configuration $(buildConfiguration)'

# Run Unit Tests
- task: DotNetCoreCLI@2
  displayName: 'dotnet test'
  inputs:
    command: 'test'
    projects: '**/*Tests/*.csproj'
	
# Prepare output to send to the Release pipeline
- task: DotNetCoreCLI@2
  displayName: 'dotnet publish'
  inputs:
    command: publish
    publishWebProjects: True
    arguments: '--configuration $(BuildConfiguration) --output $(Build.ArtifactStagingDirectory)'
    zipAfterPublish: True

# Take all the files in $(Build.ArtifactStagingDirectory) and upload them as an artifact of the build.
- task: PublishBuildArtifacts@1
  inputs:
    PathtoPublish: '$(Build.ArtifactStagingDirectory)'
    ArtifactName: 'drop'

Once that was all working I started to add the pieces Scott talks about in his article. I added '/p:SourceRevisionId=$(Build.SourceVersion)' to the build command to pass in the git commit hash as an assembly attribute. Using the code he provided I was able to read this value back out and display it on a webpage. Unfortunately this is the only variable that works this way. For the build number and id you can pass them in but you have to create custom attributes for each one along with specialized code to read them out. Scott doesn't include the code to read them back out instead preferring to create a file containing each of these values.

As I was working on implementing his code to output the build number and id into a file it occurred to me that it would probably be simpler to place all of the values in this buildinfo file. If I also format it as JSON it would super easy to read in these values in my application code. So starting with Scott's code I made the following changes.

In the build YAML I added the following task. It uses the echo command to create a minimal JSON file with the build number, build id and commit hash. I also wanted to include the build date as a separate field but after much searching (and build runs) was unable to figure out how to accomplish that.


- script: 'echo {"buildNumber":"$(Build.BuildNumber)","buildId":"$(Build.BuildId)","sourceVersion":"$(Build.SourceVersion)"} > .buildinfo.json'
  displayName: "Emit build info"
  workingDirectory: '$(Build.SourcesDirectory)/Neptune.Web'
  failOnStderr: true

I created the following small class to match the buildinfo JSON


    public class BuildInformation {
        public string BuildNumber { get; set; }
        public string BuildId { get; set; }
        public string SourceVersion { get; set; }
    }

I then simplified his 'AppVersionInfo' class into the following. It reads in the JSON on creation


    public class ApplicationInfo {

        private const string BuildFileName = ".buildinfo.json";
        private BuildInformation BuildInfo { get; set; }

        public ApplicationInfo(IHostEnvironment hostEnvironment) {
            var buildFilePath = Path.Combine(hostEnvironment.ContentRootPath, BuildFileName);
            if (File.Exists(buildFilePath)) {
                var fileContents = File.ReadAllText(buildFilePath);
                BuildInfo = JsonConvert.DeserializeObject<BuildInformation>(fileContents);
            }
        }

        /// <summary>
        /// Return the Build Id
        /// </summary>
        public string BuildId {
            get {
                return BuildInfo == null ? "123" : BuildInfo.BuildId;
            }
        }

        /// <summary>
        /// Return the Build Number
        /// </summary>
        public string BuildNumber {
            get {
                return BuildInfo == null ? DateTime.UtcNow.ToString("yyyyMMdd") + ".0" : BuildInfo.BuildNumber;
            }
        }

        /// <summary>
        /// Return the git hash of the commit that triggered the build
        /// </summary>
        public string GitHash {
            get {
                return BuildInfo == null ? "" : BuildInfo.SourceVersion;
            }
        }

        /// <summary>
        /// Return a short version (6 chars) of the git hash (or local)
        /// </summary>
        public string ShortGitHash {
            get {
                return GitHash.Length >= 6 ? GitHash.Substring(0, 6) : "local";
            }
        }
    }

As Scott does you add this class to Services in Startup.cs: 'services.AddSingleton();'

Then rather than displaying it in the footer I added it to an admin sys info page in a table. Note: the ApplicationInfo class is injected into the page.


@page
@inject ApplicationInfo appInfo

<h1>System Info</h1>
<table class="table">
    <tr>
        <td>Commit:</td>
        <td><a href="https://vs.com/commit/@appInfo.GitHash" target="_blank"><i class="fal fa-code-branch"></i> @appInfo.ShortGitHash</a></td>
    </tr>
    <tr>
        <td>Build:</td>
        <td><a href="https://vs.com/_build/results?buildId=@appInfo.BuildId&view=results" target="_blank"><i class="fab fa-simplybuilt"></i> @appInfo.BuildNumber</a></td>
    </tr>
    <tr>
        <td>Powered by:</td>
        <td>@System.Runtime.InteropServices.RuntimeInformation.FrameworkDescription</td>
    </tr>
</table>

Friday, December 6, 2019

Enumerating a Collection with an Index

There is often the case where you want to loop over a collection of objects along with an indicator of where you are in the loop. While some languages have a built-in syntax for expressing this C# does not (yet). Using a bit of LINQ you can easily accomplish this.

Normally starting with a collection of things you might use a for loop to iterate over list and provide your index.

For example:


 var items = new List<string>();

 for (int i = 0; i < items.Count; i++) {
  var item = items[i];
  Console.WriteLine($"Item {i}: {item}");
 }

You can also use a foreach and use an temp variable to track the index.


 int i = 0;
 foreach (var item in items) {
  Console.WriteLine($"Item {i}: {item}");
  i++;
 }

Recently I discovered this gem. The following code uses a Linq Select statement to transform the list into tuples containing the index and the object as named properties. Not quite as nice as if this was built-in to the language but still nice to be able to reduce the code down.


 foreach (var (item, index) in items.Select((v, i) => (v, i))) {
  Console.WriteLine($"Item {index}: {item}");
 }

You can also add an extension method to encapsulate the Select and make it a little cleaner.


  /// <summary>
  /// Return the list as an enumerable of tuples with the item and it's index in the list. 
  /// </summary>
  /// <typeparam name="T">The typeof items in the list</typeparam>
  /// <param name="list">List to converted</param>
  /// <returns>Enumerable of tuples with the item and it's index</returns>
  public static IEnumerable<(T, int)> WithIndex<T>(this IEnumerable<T> list) => 
    list.Select((value, index) => (value, index));

And then use it like this:


 foreach (var (item, index) in items.WithIndex()) {
  Console.WriteLine($"Item {index}: {item}");
 }

Friday, September 14, 2018

SurveyGizmo Checkbox Options Selected

SurveyGizmo custom script has a function (sgapiGetQuestionOptionSelected) to return the selected option for a single or multiple select question. For a single select question it always returns a single number. Unfortunately for a multiple select question the return value depends on the number of options selected. If multiple items are selected an array is returned but if only one item is selected just that option's id is returned. Makes it harder to write code to check if certain items were checked.

So I wrote the following function to always return an array regardless of the number of items selected.


Friday, July 21, 2017

Colorful serialization

I was writing some code to serialize a C# class to JSON and needed to export a System.Drawing.Color field as a hex string. Thankfully it's easy to customize the output using Json.NET. I created the following custom converter to save colors as hex strings (like #FFF0B6)

And example usage:

Monday, March 13, 2017

Exclusive checkbox column

We had a customer that had a checkbox grid and wanted the checkboxes in one of the rows to be exclusive (e.g. when checked no other items in that column are checked). Most survey platforms have this functionality for a multiple choice checkbox question but not for columns in a checkbox grid. Some quick jQuery and JavaScript coding I had the following mostly generic function working.

Working demo: JSFiddle