Orchestrating .NET Core Continuous Integration and Deployment via GitLab CI

The integration of .NET Core projects into a GitLab Continuous Integration and Continuous Deployment (CI/CD) pipeline represents a critical transition from manual software delivery to automated, repeatable, and verifiable release cycles. By utilizing the gitlab-ci.yml configuration file, developers can define a precise set of stages—ranging from initial restoration and compilation to sophisticated deployment patterns on Internet Information Services (IIS) or package distribution via NuGet—that execute automatically upon code commit. This process eliminates the "it works on my machine" phenomenon by enforcing a standardized environment through the use of Docker images or dedicated Windows runners, ensuring that every build is subjected to the same rigorous testing and quality gates before reaching production.

Architectural Paradigms for .NET Core Runners

The selection of the runner environment is the most fundamental decision in a GitLab CI configuration for .NET Core, as it dictates the available toolsets, the shell environment, and the target deployment capabilities.

Linux-Based Shared Runners

For projects focusing on cross-platform compatibility or containerized deployments, Linux-based shared runners provided by GitLab.com are highly efficient. These environments typically leverage official Debian-based .NET Docker images. This approach allows for a clean-slate environment for every job, preventing configuration drift and ensuring that the build is reproducible. The use of the mcr.microsoft.com/dotnet/sdk image series provides the necessary build tools without requiring manual installation of the .NET SDK on the host machine.

Windows-Based Runners

When the target environment is a Windows Server hosting IIS, a Windows-based runner is mandatory. This requires the installation of the GitLab Runner executable on a Windows host. The installation process involves creating a specific directory, such as C:\GitLab-Runner, placing the gitlab-runner.exe within that directory, and registering the runner using a unique token and address found in the GitLab project settings under Settings > CICD > Runners > Specific runners. This configuration allows the pipeline to execute native PowerShell commands, which are essential for managing Windows-specific services like Application Pools and WebSites.

Implementation of the .NET Core Build and Test Pipeline

A robust pipeline is structured into logical stages that act as quality gates. If a job in an earlier stage fails, the pipeline halts, preventing unstable code from progressing toward production.

The Build Stage

The initial phase focuses on transforming source code into binary artifacts. The standard sequence of operations includes:

  • dotnet restore: This command retrieves the necessary NuGet packages defined in the project files.
  • dotnet build --no-build: This compiles the project. The --no-restore flag is often used here to speed up the process since restoration occurred in the previous step.
  • dotnet publish: This prepares the application for deployment by creating a folder containing all the files required to run the app. An example command is dotnet publish .\DotNetDocs\DotNetDocs.csproj -c Release -o $publish_path.

The Test Stage

Unit testing ensures that new changes do not introduce regressions. The dotnet test command is executed, often with the --no-build flag to avoid redundant compilation. The use of --verbosity normal provides sufficient logging to debug failures within the GitLab job logs. Advanced implementations also include code coverage analysis and unit test summaries to provide a quantitative measure of software quality.

The Deployment Stage

Deployment varies significantly based on the target. For NuGet distribution, the pipeline must add the GitLab package registry as a source and push the .nupkg files. For IIS deployment, the pipeline manages the lifecycle of the web application through PowerShell scripts.

IIS Deployment Automation and PowerShell Integration

Deploying a .NET Core application to IIS requires more than just copying files; it requires managing the state of the web server to avoid file-lock errors and ensure a clean startup.

The Deployment Workflow

The automated sequence for an IIS publish cycle follows these technical steps:

  1. Stop the WebSite: Using Stop-WebSite -Name $application_pool_name to cease all incoming traffic.
  2. Stop the Application Pool: Using Stop-WebAppPool -Name $application_pool_name to release resources and file locks.
  3. File Transfer: Using Copy-Item $publish_path -Destination $iis_worker_path -Force to overwrite the existing application files with the newly built binaries.
  4. Validation Pause: Implementing a Start-Sleep -s 5 to ensure the OS has finalized file operations.
  5. Restart the Application Pool: Using Start-WebAppPool -Name $application_pool_name.
  6. Restart the WebSite: Using Start-WebSite -Name $application_pool_name.

Technical Variable Configuration

To make these scripts reusable across different environments, variables are defined in the gitlab-ci.yml file:

Variable Purpose Example Value
publish_path Path to the built binaries .\DotNetDocs\publish\$CI_PIPELINE_ID\
published_data_path Wildcard path for files .\DotNetDocs\publish\$CI_PIPELINE_ID\*.*
application_pool_name Name of the IIS App Pool FarhadZamani
iis_worker_path Physical destination on server C:\Publish\FarhadZamani

Advanced Integration with SonarQube and Quality Analysis

Integrating static code analysis into the pipeline ensures that technical debt is monitored and security vulnerabilities are identified early.

SonarQube Configuration Requirements

To perform a scan using the .NET scanner on a Linux host (e.g., Ubuntu 20.04.6) with a Docker runner, the environment must be carefully prepared. The pipeline requires the Java Runtime Environment (JRE) because the SonarScanner for .NET relies on Java.

The configuration sequence involves:

  • Setting SONAR_USER_HOME: This defines the location of the analysis task cache (e.g., ${CI_PROJECT_DIR}/.sonar).
  • Setting GIT_DEPTH: "0": This is critical as SonarQube requires the full git history to accurately assign blame and track issues across branches.
  • Dependency Installation: The script must execute apt-get update followed by apt-get install --yes openjdk-17-jre.

The Scanning Process

Once the environment is ready, the following commands are executed:

  • dotnet tool install --global dotnet-sonarscanner: Installs the scanner tool.
  • export PATH=\"$PATH:$HOME/.dotnet/tools\": Ensures the tool is accessible in the current shell.
  • dotnet sonarscanner begin: Initializes the scan with the project key and tokens.
  • dotnet build: The actual build process where the scanner hooks into the compiler.
  • dotnet sonarscanner end: Finalizes the analysis and uploads the results to the SonarQube server.

Troubleshooting Common Pipeline Failures

During the implementation of .NET Core CI/CD, several recurring errors may emerge, typically related to environment mismatches or missing dependencies.

The MSB1003 Error

A common error encountered during dotnet pack is MSBUILD : error MSB1003: Geben Sie eine Projekt- oder Projektmappendatei an. This occurs when the current working directory of the runner does not contain a .csproj or .sln file. This is often caused by the runner being in the wrong directory or the project files not being properly cloned.

The "Unable to Locate Package" Error

When attempting to install openjdk-17-jre in a Dockerized pipeline using mcr.microsoft.com/dotnet/core/sdk:latest, some users report E: Unable to locate package openjdk-17-jre. This is typically not a distribution error but an image-specific issue. The solution is to ensure that apt-get update is successfully executed immediately before the installation command to refresh the package lists within the container.

Runner Tag Mismatches

If a job fails with a message stating "no runner defined," it is often because the tags section in the .gitlab-ci.yml does not match the tags assigned to the registered runner. For a Windows machine, tags such as Windows, Powershell, and Stage_Deploy must be explicitly declared in the job definition to route the work to the correct machine.

Detailed Configuration Example for .NET Core

The following is a synthesized representation of a complete pipeline configuration, integrating build, test, and deployment phases.

```yaml
variables:
publishpath: '.\DotNetDocs\publish\$CIPIPELINEID\'
published
datapath: '.\DotNetDocs\publish\$CIPIPELINEID*.*'
application
poolname: 'FarhadZamani'
iis
worker_path: 'C:\Publish\FarhadZamani'

stages:
- build
- tests
- deploy

build:
stage: build
script:
- dotnet restore
- dotnet build --no-restore
- dotnet publish .\DotNetDocs\DotNetDocs.csproj -c Release -o $publishpath
artifacts:
paths:
- $publish
path
expire_in: '1 hrs'
tags:
- dotnet
only:
- develop
- master

unit-test:
stage: tests
script:
- dotnet test --no-build --verbosity normal
tags:
- dotnet
only:
- master
- develop
needs: [build]

production:
stage: deploy
script:
- |
if((Get-WebSiteState -Name $applicationpoolname).Value -ne 'Stopped'){
Write-Output ('Stopping WebSite: {0}' -f $applicationpoolname)
Stop-WebSite -Name $applicationpoolname
}
- |
if((Get-WebAppPoolState -Name $applicationpoolname).Value -ne 'Stopped'){
Write-Output ('Stopping Application Pool: {0}' -f $applicationpoolname)
Stop-WebAppPool -Name $applicationpoolname
}
- "Copy-Item $publisheddatapath -Destination $iisworkerpath -Force"
- "Start-Sleep -s 5"
- |
if((Get-WebAppPoolState -Name $applicationpoolname).Value -ne 'Started'){
Start-WebAppPool -Name $applicationpoolname
}
- "Start-WebSite -Name $applicationpoolname"
tags:
- Windows
- Powershell
only:
- master
```

NuGet Package Distribution Strategy

For developers distributing libraries rather than deploying applications, the pipeline focuses on the creation and upload of NuGet packages.

The Packaging Sequence

The process begins with the dotnet pack -c Release command, which generates the .nupkg file. To authenticate with the GitLab package registry, the pipeline must configure the NuGet source using a job token.

The authentication command is structured as follows:

bash dotnet nuget add source "${CI_API_V4_URL}/projects/${CI_PROJECT_ID}/packages/nuget/index.json" --name gitlab --username gitlab-ci-token --password $CI_JOB_TOKEN --store-password-in-clear-text

Once the source is added, the package is pushed using:

bash dotnet nuget push "bin/Release/*.nupkg" --source gitlab

This ensures that any versioned release of the library is automatically available to other projects within the organization, maintaining a centralized and secure package management system.

Conclusion

The transition to an automated GitLab CI/CD pipeline for .NET Core projects fundamentally alters the software development lifecycle by replacing manual intervention with programmable logic. Whether utilizing Linux-based shared runners for microservices or dedicated Windows runners for IIS-hosted monoliths, the core principle remains the same: the isolation of the build environment and the automation of deployment steps. The integration of tools like SonarQube adds a layer of qualitative assessment, ensuring that the automation does not just deliver code faster, but delivers better code. The success of such a system relies on the precise configuration of the .gitlab-ci.yml file, the correct tagging of runners, and the use of PowerShell for Windows-specific orchestration. By treating the infrastructure as code, organizations can achieve a level of stability and predictability in their release cycles that is impossible to attain through manual processes.

Sources

  1. gitlab-ci-example-dotnetcore GitHub Repository
  2. dotnetdocs.ir - GitLab CI/CD .NET Core Guide
  3. GitLab Forum - Build Server for .NET Core Project
  4. SonarSource Community - .NET Scanner GitLab CI Issues

Related Posts