Building and testing an ASP.Net Core 3.0 application using GitHub Actions

GitHub has finally learned from GitLab and added GitHub Actions to their repertoire. It’s in beta now, but this means that we can use only GitHub for everything from code hosting, project management, CI and finally CD. All in one place. An alluring thought, but how mature is it?

I recently started a new project using ASP.Net Core 3.0 and decided to take GitHub Actions out for a spin.

The project to build and test

Simplified, the solution’s structure within the repository looks like this:

src ├── Solution.sln ├── Service │ ├── Service.csproj ├── Service.WebApi │ ├── Service.WebApi.csproj └── Tests ├── Service.Tests │ ├── Service.Tests.csproj └── Service.WebApi.Tests └── Service.WebApi.Tests.csproj

One .Net 2.1 standard library for the business logic, one ASP.Net Core 3.0 Web API for the actual service controllers, one xUnit unit test library for unit tests and lastly one xUnit library for end-to-end tests of the whole API. In the following I’ll show how to build and test the structure above on macOS, Ubuntu and Windows on every commit on any branch or pull request.

Add GitHub actions to your repo

First, we add GitHub Actions to our repo. The images are illustrative from another repo than the one mentioned above:

GitHub Actions, unsurprisingly, relies on actions to specify workflows using YAML . To get started, we’ll scroll down in Popular continuous integration workflows until we find the one for ASP.Net Core:

Selecting this workflow will create [repo]/.github/workflows/aspnetcore.yml and populate the file with sensible defaults. Changing this file will change our workflow and it can be extended to use other actions created by GitHub, the community or by even yourself. Let’s go through the file line by line and explain the conventions used in this specific YAML dialect.

1 : The name of the workflow. Visible multiple places in the GitHub Actions UI and logs.

: The name of the workflow. Visible multiple places in the GitHub Actions UI and logs. 3 : A list of the triggers to trigger this workflow. [push] runs the workflow on any push in any branch or PR.

: A list of the triggers to trigger this workflow. runs the workflow on any push in any branch or PR. 5 : A workflow run is made up of one or more jobs. Jobs run in parallel by default.

: A workflow run is made up of one or more jobs. Jobs run in parallel by default. 6 : This is the unique job id.

: This is the unique job id. 8 : The workflow will run on this OS on a virtual host machine.

: The workflow will run on this OS on a virtual host machine. 10 : Jobs contain a list of tasks called steps.

: Jobs contain a list of tasks called steps. 11 : Uses selects an action, a reusable unit of code, to run as part of a step in your job. actions/checkout@v1 checks out the code so the workflow can access the contents of your repository. Actions are versioned and this is version 1.

: Uses selects an action, a reusable unit of code, to run as part of a step in your job. actions/checkout@v1 checks out the code so the workflow can access the contents of your repository. Actions are versioned and this is version 1. 12 : Every step has a name and

: Every step has a name and 13 : this action installs .Net Core

: this action installs .Net Core 14 : with

: with 15 : a dotnet version of 2.2.108.

: a dotnet version of 2.2.108. 16 : The next step has another name and

: The next step has another name and 17: runs a dotnet build in Release mode.

As far as default templates go, this is both reasonable and readable.

Finalize your workflow.yml

This default template will not work on the structure above, building on all OSes and run tests, so let’s change it to better suit our needs.

name : ASP.NET Core CI on : [ push ] jobs : build_and_test : runs-on : ${{ matrix.os }} strategy : matrix : os : [ macOS-latest , ubuntu-latest , windows-latest ] steps : - name : Setup .NET Core if needed uses : actions/setup-dotnet@v1.2.0 with : dotnet-version : 3.0.100 if : matrix.os == 'macOS-latest' || matrix.os == 'ubuntu-latest' - uses : actions/checkout@v1 - name : Build with dotnet run : dotnet build ./src/Solution.sln --configuration Release - name : Test with dotnet run : dotnet test ./src/Solution.sln --configuration Release

Three changes are interesting here:

matrix.os makes is possible to run a workflow on multiple OSes. With the os matrix specified, the workflow will run on macOS-latest , ubuntu-latest , and windows-latest , building and testing on each one.

makes is possible to run a workflow on multiple OSes. With the os matrix specified, the workflow will run on , , and , building and testing on each one. The if: matrix.os == 'macOS-latest' || matrix.os == 'ubuntu-latest' in Setup .NET Core runs the .Net Core installation only if the OS is macOS or Ubuntu. Windows has this built in.

in runs the .Net Core installation only if the OS is macOS or Ubuntu. Windows has this built in. And lastly, tests are run an own step using the dotnet test command.

Finally when you press Start commit the workflow will begin running in the same commit as you added or changed the file in.

What should’ve worked

While the above YAML works, it breaks the important DevOps principle of being explicit, describing the entire pipeline using code. Above, we implicitly assume that the Windows image contains the correct .Net Core version. A dangerous thing to assume, as this might change at any time.

The below YAML is better in that it’s explicit about the version of .Net Core that’s needed.

name : ASP.NET Core CI on : [ push ] jobs : build_and_test : runs-on : ${{ matrix.os }} strategy : matrix : os : [ macOS-latest , ubuntu-latest , windows-2019 ] steps : - uses : actions/checkout@v1 - name : Setup .NET Core uses : actions/setup-dotnet@v1 with : dotnet-version : 3.0.100 - name : Build with dotnet run : dotnet build ./src/Solution.sln --configuration Release - name : Test with dotnet run : dotnet test ./src/Solution.sln --configuration Release

Running this today, however, yields the following error on Windows.

Testhost process exited with error: A fatal error occurred, the required library hostfxr.dll could not be found. If this is a self-contained application, that library should exist in [D:\a\Repo\src\Tests\Service.WebApi.Tests\bin\Release

etcoreapp3.0\]. If this is a framework-dependent application, install the runtime in the default location [C:\Program Files\dotnet] or use the DOTNET_ROOT environment variable to specify the runtime location. Please check the diagnostic logs for more information. Test Run Aborted.

The setup-dontet action does not work correctly when .Net Core already comes pre-installed. There are both issues and pull-request at the time of writing, but the problem is not fixed yet. And a build pipeline must always be possible to run successfully. A single bug in an application does not make the entire application useless, a bug in a build pipeline might.

Final thoughts

GitHub Actions are indeed promising. The workflows are easy to understand, the online editor helps with a kind of Intellisense, the available actions are open source, easily available and rapidly improving.

And it actually works, with everything in one place.

The product is still in beta though, things are still changing and the documentation is somewhat lacking. It’s also no native way to test your workflows or actions locally. However, this will change for the better as the product reaches maturity with an even bigger audience, proven patterns, and stable documentation. And the community is taking to it, providing solutions where none existed before, for instance, publishing artifacts to Azure Blob Storage.

I’ll continue to use GitHub Actions for new projects but will not port existing ones before having evaluated the final version.