Dockerize .NET Framework Applications on GitHub Actions

2024-09-17

If you're still using .NET Framework to maintain some older systems, you might run into issues with maintaining a pipeline for it.

Let's explore how to mitigate this by containerizing .NET Framework applications.

For the purpose of this blog post, I'll be using GitHub Actions, but your CI tool probably has equivalent commands to those you see here.

Docker has a good article on the what's and why's of containerization which you can go through to understand a bit more on the topic.

Note

.NET Framework doesn't work out of the box on Linux systems, we would need a Windows Runner for running them on Docker. Windows docker images will be slightly different from Linux docker images - syntax, capabilities, features etc.

The Application

You probably have an app you already want to dockerize, for the purpose of this blog I've created a sample github project. You can pull this repository or follow the remaining blog with your own codebase.

The Dockerfile

Base Image

To begin creating our Dockerfile, we need to get a base image which has the tools we need - of course another way to do this is to get a base image for the Windows OS and install the minimal tools required. We'll stick with Microsoft's official images for .NET which will get support & updates so we can take that burden away from ourselves.

There's an issue with syntax highlighting plugin I'm using. The code at the end of the Dockerfile section has a highlighted snippet.
1FROM mcr.microsoft.com/dotnet/framework/sdk:4.8-20240709-windowsservercore-ltsc2019

Next, we need a few tools which will be used later to collect coverage and generate reports - you can skip this if it isn't needed.

3RUN dotnet tool install --global altcover.global
4RUN dotnet tool install --global dotnet-reportgenerator-globaltool

It must be puzzling as to why we have used the dotnet CLI on a .NET Framework project.

These dependencies are not project specific in my demo app, and I just need the .exe files anywhere in the system and the dotnet CLI comes preinstalled in the image we have. You can alternatively install a NuGet package for these locally to your project.

Build

To build the project we first need the solution files in the image & then we can use MSBuild to create the dlls.

6COPY . .
7RUN nuget restore
8
9# Build Project
10RUN msbuild "./DockerApp/DockerApp.csproj" /p:Configuration=Release

Coverage & Testing

There are a few intricacies to testing & collecting coverage for .NET framework projects in the CLI - there are quite a few tools available and we chose to use AltCover on top of the xUnit.net console runner.

I have another post on coverage for .NET framework projects for a more detailed look into the process.

12# Test & Collect Coverage
13RUN msbuild "./DockerApp.Test/DockerApp.Test.csproj"
14RUN AltCover.exe --inputDirectory=C:/DockerApp.Test/bin/Debug --localSource --assemblyFilter=DockerApp.Test --outputDirectory=__Instrumented
15RUN AltCover.exe Runner --recorderDirectory __Instrumented --executable C:/packages/xunit.runner.console.2.9.0/tools/net472/xunit.console.exe -- ./__Instrumented/DockerApp.Test.dll
16
17# Generate reports
18RUN reportgenerator.exe -reports:coverage.xml -targetdir:reports

All together, it would look like this

FROM mcr.microsoft.com/dotnet/framework/sdk:4.8-20240709-windowsservercore-ltsc2019

RUN dotnet tool install --global altcover.global
RUN dotnet tool install --global dotnet-reportgenerator-globaltool

COPY . .
RUN nuget restore

# Build Project
RUN msbuild "./DockerApp/DockerApp.csproj" /p:Configuration=Release

# Test & Collect Coverage
RUN msbuild "./DockerApp.Test/DockerApp.Test.csproj"
RUN AltCover.exe --inputDirectory=C:/DockerApp.Test/bin/Debug --localSource --assemblyFilter=DockerApp.Test --outputDirectory=__Instrumented
RUN AltCover.exe Runner --recorderDirectory __Instrumented  --executable C:/packages/xunit.runner.console.2.9.0/tools/net472/xunit.console.exe   --  ./__Instrumented/DockerApp.Test.dll

# Generate reports
RUN reportgenerator.exe -reports:coverage.xml -targetdir:reports

The Pipeline

I've used GitHub actions as the easiest integration to showcase a sample project pipeline. You might be using something else like Jenkins, Azure Pipelines etc. - the flow of steps are going to remain same, the only difference would be the syntax on your choice of CI.

Stages

  • Do a docker build to create the image
  • Create a container out of this image so that we can access any of the artifacts we need - coverage, dlls etc.
  • Extract whatever artifacts you need out of the image, then cleanup
  • Use your artifacts accordingly

I've left in an example of extracting and uploading an artifact (coverage) to GitHub actions. What you want to do with the coverage and dlls might be different - you might want to upload it to an S3 bucket, or host the coverage to a static site, or something else entirely.

name: dotnet-framework-docker-build

on:
  push:
    branches:
      - "main"

jobs:
  build-and-test:
    runs-on: windows-2019
    steps:
      - uses: actions/checkout@v4

      - name: Build & Test
        run: |
          docker build -f Dockerfile -t dockerapp .

      - name: Extract Coverage Artifact
        run: |
          docker create --name dockerapp-container dockerapp
          docker cp dockerapp-container:C:/reports/ c:/coverage/

      - name: Cleanup docker daemon
        if: always()
        run: |
          docker container prune -f
          docker image prune -f

      - name: Upload coverage artifact
        uses: actions/upload-artifact@v4
        with:
          name: coverage
          path: c:/coverage/

With that, you would have successfully dockerized a system that needs to migrate to newer .NET Core versions ASAP!

Till the next blog post, Annyeong!