CI/CD Pipeline Optimization for Large .NET Solutions cover image

CI/CD Pipeline Optimization for Large .NET Solutions

By Julian Krause · Monday, December 15, 2025 · ~3 min read

Our CI pipeline was taking 45 minutes. Developers would push a PR, go for coffee, come back, find a failure, fix it, push again, and go for another coffee. We cut it to under 10 minutes. Here is every optimization that mattered.

Incremental Builds with NuGet Cache

The single biggest win was caching NuGet packages and build artifacts between runs. Every CI run was downloading 400+ NuGet packages from scratch, which alone took 3-4 minutes.

# GitHub Actions example
- name: Cache NuGet packages
  uses: actions/cache@v4
  with:
    path: ~/.nuget/packages
    key: nuget-${{ hashFiles('**/*.csproj', '**/Directory.Packages.props') }}
    restore-keys: nuget-

- name: Cache build output
  uses: actions/cache@v4
  with:
    path: |
      **/bin
      **/obj
    key: build-${{ hashFiles('**/*.cs', '**/*.csproj') }}

Parallelize Test Execution

Our test suite had 2,400 tests across 19 projects. Running them sequentially took 25 minutes. Running them in parallel with dotnet test maxing out available cores brought it down to 8 minutes.

<!-- Directory.Build.props -->
<PropertyGroup>
  <TestRunnerParallelization>true</TestRunnerParallelization>
</PropertyGroup>
# Run tests across projects in parallel
dotnet test Postnomic.slnx --parallel --configuration Release

We also profiled individual test projects to find the slow ones. Two projects were using real database connections in tests. Switching them to in-memory providers cut 5 minutes off the suite.

Build Only What Changed

For large solutions, building everything on every commit is wasteful. We use dotnet build with the --no-incremental flag only on the main branch. PR builds use incremental builds combined with path filters.

# Only run API tests when API code changes
on:
  pull_request:
    paths:
      - 'src/Postnomic.Api/**'
      - 'src/Postnomic.Domain/**'
      - 'src/Postnomic.Data/**'
      - 'tests/Postnomic.Api.Tests/**'

jobs:
  test-api:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - run: dotnet test tests/Postnomic.Api.Tests/Postnomic.Api.Tests.csproj

Docker Layer Caching for Container Builds

Our Dockerfile was rebuilding from scratch on every push. A multi-stage Dockerfile with proper layer ordering reduced container build time from 8 minutes to 90 seconds.

# Stage 1: Restore (cached unless csproj files change)
FROM mcr.microsoft.com/dotnet/sdk:10.0 AS restore
WORKDIR /src
COPY *.sln .
COPY src/*/*.csproj ./
RUN for file in *.csproj; do mkdir -p src/${file%.*} && mv $file src/${file%.*}/; done
RUN dotnet restore

# Stage 2: Build (cached unless source files change)
FROM restore AS build
COPY src/ src/
RUN dotnet publish src/Postnomic.Api/Postnomic.Api.csproj -c Release -o /app

# Stage 3: Runtime
FROM mcr.microsoft.com/dotnet/aspnet:10.0
COPY --from=build /app .
ENTRYPOINT ["dotnet", "Postnomic.Api.dll"]

The Compound Effect

No single optimization was dramatic. NuGet caching saved 4 minutes. Parallel tests saved 17 minutes. Incremental builds saved 6 minutes. Docker layer caching saved 7 minutes. Together, they transformed the developer experience. PRs get feedback in under 10 minutes, developers stay in flow, and the team ships faster. Pipeline speed is a direct multiplier on engineering productivity.


Comments (2)

David Friday, March 6, 2026 5:08 PM

Could you elaborate on this topic in a follow-up post?

Felix Saturday, February 28, 2026 8:08 PM

I agree, great article!

Greta Sunday, March 1, 2026 8:08 PM

Good question, I'd like to know too.

Elena Saturday, March 7, 2026 5:08 PM

This is exactly what I was looking for, thank you!

Leave a Comment