Skip to content

Central Package Management in ConnectSoft Microservice Template

Purpose & Overview

Central Package Management (CPM) is a .NET feature that enables centralized version management for NuGet packages across a solution. In the ConnectSoft Microservice Template, CPM is a foundational standard that ensures version consistency, simplifies dependency management, and enforces governance across all projects.

Why Central Package Management?

Central Package Management offers several key benefits:

  • Version Consistency: All projects use the exact same version of each dependency, eliminating version conflicts
  • Simplified Maintenance: Update package versions in one place (Directory.Packages.props) and all projects inherit the change
  • Easier Upgrades: Upgrade dependencies across the entire solution by updating a single file
  • Governance: Centralized audit point for all dependencies and their versions
  • Reduced Duplication: Eliminate repetitive version specifications across .csproj files
  • Build Reproducibility: Ensures consistent builds across local development, CI/CD, and production environments

.NET Feature

Central Package Management was introduced in .NET SDK 5.0 and enhanced in .NET 6/⅞+. It uses Directory.Packages.props to centralize package version definitions.

Architecture Overview

CPM works by defining package versions centrally and referencing them by name in project files:

Solution Root
    ├── Directory.Packages.props (Central version definitions)
    ├── Directory.Build.props (Build-wide settings)
    ├── nuget.config (Package sources)
    └── Projects/
        ├── Project1.csproj (References packages without versions)
        ├── Project2.csproj (References packages without versions)
        └── ...

How It Works

  1. Central Definition: Package versions are defined in Directory.Packages.props at the solution root
  2. Project References: .csproj files reference packages by name only (no version)
  3. Automatic Resolution: .NET SDK automatically resolves versions from Directory.Packages.props during restore
  4. Build Inheritance: All projects inherit build settings from Directory.Build.props

Key Integration Points

Component Location Responsibility
Directory.Packages.props Solution root Central package version definitions
Directory.Build.props Solution root Build-wide compiler and quality settings
nuget.config Solution root Package source configuration
.csproj files Individual projects Package references without versions

Core Components

1. Directory.Packages.props

The central file that defines all NuGet package versions:

<Project>
  <ItemGroup>
    <!-- Testing -->
    <PackageVersion Include="Microsoft.NET.Test.Sdk" Version="17.7.0" />
    <PackageVersion Include="xunit" Version="2.4.2" />
    <PackageVersion Include="Moq" Version="4.20.69" />

    <!-- Logging -->
    <PackageVersion Include="Serilog.AspNetCore" Version="6.0.1" />
    <PackageVersion Include="Serilog.Sinks.Console" Version="4.1.0" />

    <!-- Validation -->
    <PackageVersion Include="FluentValidation" Version="11.5.0" />

    <!-- gRPC -->
    <PackageVersion Include="Grpc.AspNetCore" Version="2.55.0" />
    <PackageVersion Include="Grpc.Tools" Version="2.57.0" />

    <!-- ORM -->
    <PackageVersion Include="NHibernate" Version="5.4.4" />
    <PackageVersion Include="NHibernate.Caches.StackExchangeRedis" Version="5.4.0" />

    <!-- Messaging -->
    <PackageVersion Include="MassTransit" Version="8.1.2" />
    <PackageVersion Include="MassTransit.RabbitMQ" Version="8.1.2" />

    <!-- Feature Management -->
    <PackageVersion Include="Microsoft.FeatureManagement" Version="3.0.0" />
    <PackageVersion Include="Microsoft.FeatureManagement.AspNetCore" Version="3.0.0" />

    <!-- AutoMapper -->
    <PackageVersion Include="AutoMapper" Version="12.0.1" />
    <PackageVersion Include="AutoMapper.Extensions.Microsoft.DependencyInjection" Version="12.0.1" />

    <!-- OpenTelemetry -->
    <PackageVersion Include="OpenTelemetry.Exporter.Console" Version="1.6.0" />
    <PackageVersion Include="OpenTelemetry.Instrumentation.AspNetCore" Version="1.6.0" />
  </ItemGroup>
</Project>

Key Characteristics

  • Exact Versions: Use specific version numbers (no wildcards like *)
  • Organized by Purpose: Group related packages together (testing, logging, messaging, etc.)
  • Solution-Wide: Applies to all projects in the solution
  • Version Control: Should be checked into source control

2. Directory.Build.props

Defines build-wide settings that apply to all projects:

<Project>
  <PropertyGroup>
    <!-- Target Framework -->
    <TargetFramework>net9.0</TargetFramework>

    <!-- Language Version -->
    <LangVersion>latest</LangVersion>

    <!-- Nullable Reference Types -->
    <Nullable>enable</Nullable>

    <!-- Code Quality -->
    <TreatWarningsAsErrors>true</TreatWarningsAsErrors>
    <WarningsAsErrors>CS1591</WarningsAsErrors> <!-- Missing XML documentation -->
    <CodeAnalysisTreatWarningsAsErrors>true</CodeAnalysisTreatWarningsAsErrors>

    <!-- Documentation -->
    <GenerateDocumentationFile>true</GenerateDocumentationFile>
    <NoWarn>$(NoWarn);1591</NoWarn> <!-- Suppress missing XML doc warnings -->

    <!-- Implicit Usings -->
    <ImplicitUsings>enable</ImplicitUsings>

    <!-- Deterministic Builds -->
    <Deterministic>true</Deterministic>
    <ContinuousIntegrationBuild>true</ContinuousIntegrationBuild>

    <!-- Assembly Info -->
    <Version>1.0.0</Version>
    <Authors>ConnectSoft</Authors>
    <Company>ConnectSoft</Company>
  </PropertyGroup>

  <!-- Common Package References (if needed) -->
  <ItemGroup>
    <PackageReference Include="Microsoft.SourceLink.GitHub" Version="8.0.0" PrivateAssets="All" />
  </ItemGroup>
</Project>

Key Settings

Property Purpose
TargetFramework Ensures all projects target the same .NET version
LangVersion Enables latest C# language features
Nullable Enables nullable reference types across all projects
TreatWarningsAsErrors Enforces code quality by treating warnings as errors
GenerateDocumentationFile Automatically generates XML documentation
Deterministic Ensures reproducible builds

3. Project Files (.csproj)

Projects reference packages without version numbers:

<Project Sdk="Microsoft.NET.Sdk">

  <PropertyGroup>
    <OutputType>Library</OutputType>
    <RootNamespace>ConnectSoft.MicroserviceTemplate.Application</RootNamespace>
  </PropertyGroup>

  <ItemGroup>
    <!-- No Version attribute - resolved from Directory.Packages.props -->
    <PackageReference Include="FluentValidation" />
    <PackageReference Include="AutoMapper.Extensions.Microsoft.DependencyInjection" />
    <PackageReference Include="Microsoft.FeatureManagement" />
  </ItemGroup>

  <ItemGroup>
    <ProjectReference Include="..\ConnectSoft.MicroserviceTemplate.DomainModel\ConnectSoft.MicroserviceTemplate.DomainModel.csproj" />
  </ItemGroup>

</Project>

Benefits

  • Cleaner Files: No version clutter in project files
  • Automatic Resolution: Versions resolved from Directory.Packages.props
  • Consistency: All projects use the same versions automatically
  • Easier Maintenance: Update versions in one place

4. nuget.config

Configures package sources for the solution:

<?xml version="1.0" encoding="utf-8"?>
<configuration>
  <packageSources>
    <!-- Public NuGet feed -->
    <add key="NuGet" value="https://api.nuget.org/v3/index.json" protocolVersion="3" />

    <!-- Private ConnectSoft feed (Azure DevOps) -->
    <add key="ConnectSoftInternal" value="https://pkgs.dev.azure.com/connectsoft/_packaging/Internal/nuget/v3/index.json" />

    <!-- Local development feed (optional) -->
    <add key="Local" value="./packages" />
  </packageSources>

  <!-- Authentication for private feeds -->
  <packageSourceCredentials>
    <ConnectSoftInternal>
      <add key="Username" value="%AZURE_DEVOPS_USERNAME%" />
      <add key="ClearTextPassword" value="%AZURE_DEVOPS_PAT%" />
    </ConnectSoftInternal>
  </packageSourceCredentials>

  <!-- Package source mapping (security) -->
  <packageSourceMapping>
    <packageSource key="NuGet">
      <package pattern="*" />
    </packageSource>
    <packageSource key="ConnectSoftInternal">
      <package pattern="ConnectSoft.*" />
    </packageSource>
  </packageSourceMapping>
</configuration>

Configuration

Enabling Central Package Management

CPM is automatically enabled when Directory.Packages.props exists in the solution root. No additional configuration is required.

Project File Structure

Solution Root/
├── Directory.Packages.props
├── Directory.Build.props
├── nuget.config
├── ConnectSoft.MicroserviceTemplate.sln
└── Projects/
    ├── ConnectSoft.MicroserviceTemplate.DomainModel/
    │   └── ConnectSoft.MicroserviceTemplate.DomainModel.csproj
    ├── ConnectSoft.MicroserviceTemplate.ApplicationModel/
    │   └── ConnectSoft.MicroserviceTemplate.ApplicationModel.csproj
    └── ...

Adding a New Package

  1. Add to Directory.Packages.props:
<ItemGroup>
  <PackageVersion Include="NewPackage" Version="1.2.3" />
</ItemGroup>
  1. Reference in project file:
<ItemGroup>
  <PackageReference Include="NewPackage" />
</ItemGroup>
  1. Restore packages:
dotnet restore

Conditional Package Management

Target Framework Conditions

Different versions for different target frameworks:

<ItemGroup>
  <PackageVersion Include="System.Text.Json" Version="8.0.0" 
                  Condition="'$(TargetFramework)' == 'net8.0'" />
  <PackageVersion Include="System.Text.Json" Version="7.0.0" 
                  Condition="'$(TargetFramework)' == 'net7.0'" />
</ItemGroup>

Feature Flags

Conditional packages based on build properties:

<Project>
  <PropertyGroup>
    <UseMassTransit>true</UseMassTransit>
    <UseNServiceBus>false</UseNServiceBus>
  </PropertyGroup>

  <ItemGroup>
    <PackageVersion Include="MassTransit" Version="8.1.2" 
                    Condition="'$(UseMassTransit)' == 'true'" />
    <PackageVersion Include="NServiceBus" Version="8.1.0" 
                    Condition="'$(UseNServiceBus)' == 'true'" />
  </ItemGroup>
</Project>

In project files:

<ItemGroup>
  <PackageReference Include="MassTransit" 
                    Condition="'$(UseMassTransit)' == 'true'" />
</ItemGroup>

Environment-Based Conditions

<ItemGroup>
  <PackageVersion Include="Azure.Storage.Blobs" Version="12.17.0"
                  Condition="'$(Configuration)' == 'Release'" />
</ItemGroup>

Best Practices

Do's

  1. Always use exact versions
  2. Use specific version numbers: Version="1.2.3"
  3. Avoid wildcards: Version="1.*" or Version="*"

  4. Organize packages by purpose

  5. Group related packages together
  6. Add comments for clarity
  7. Example: Testing, Logging, Messaging, ORM sections

  8. Keep Directory.Packages.props in source control

  9. Essential for reproducible builds
  10. Enables team-wide version consistency

  11. Use Directory.Build.props for common settings

  12. Avoid duplicating build settings in each project
  13. Centralize compiler and quality settings

  14. Configure package source mapping

  15. Restrict which packages come from which feeds
  16. Enhances security and prevents supply chain attacks

  17. Group related packages

    <!-- Testing -->
    <PackageVersion Include="Microsoft.NET.Test.Sdk" Version="17.7.0" />
    <PackageVersion Include="xunit" Version="2.4.2" />
    
    <!-- Logging -->
    <PackageVersion Include="Serilog.AspNetCore" Version="6.0.1" />
    

  18. Document complex versions

    <!-- Version pinned for compatibility with NHibernate 5.4 -->
    <PackageVersion Include="NHibernate.Caches.StackExchangeRedis" Version="5.4.0" />
    

  19. Use conditional packages wisely

  20. Only use conditions when necessary
  21. Prefer explicit feature flags over environment-based conditions
  22. Document why conditions are needed

Don'ts

  1. Don't specify versions in .csproj files

    <!-- ❌ BAD -->
    <PackageReference Include="Serilog" Version="3.1.1" />
    
    <!-- ✅ GOOD -->
    <PackageReference Include="Serilog" />
    

  2. Don't use wildcard versions

    <!-- ❌ BAD -->
    <PackageVersion Include="Serilog" Version="3.*" />
    
    <!-- ✅ GOOD -->
    <PackageVersion Include="Serilog" Version="3.1.1" />
    

  3. Don't duplicate package definitions

  4. Each package should appear once in Directory.Packages.props
  5. Use conditions if different versions are needed for different scenarios

  6. Don't forget to update both files

  7. When adding a package, update both Directory.Packages.props and the project's .csproj
  8. Missing either will cause build failures

  9. Don't bypass CPM with local overrides

  10. Avoid using Version attribute in project files
  11. This breaks version consistency across the solution

  12. Don't ignore build warnings

  13. Directory.Build.props sets TreatWarningsAsErrors=true for a reason
  14. Fix warnings before checking in code

CI/CD Integration

Azure DevOps Pipeline

# azure-pipelines.yml
trigger:
  branches:
    include:
      - main
      - develop

pool:
  vmImage: 'windows-latest'

variables:
  - name: DotNet.SDK.Version
    value: '9.0.x'

steps:
  - task: UseDotNet@2
    inputs:
      packageType: 'sdk'
      version: '$(DotNet.SDK.Version)'

  - task: DotNetCoreCLI@2
    displayName: 'Restore packages'
    inputs:
      command: 'restore'
      projects: '**/*.sln'
      arguments: '--locked-mode'

  - task: DotNetCoreCLI@2
    displayName: 'Build solution'
    inputs:
      command: 'build'
      projects: '**/*.sln'
      arguments: '--configuration Release --no-restore'

  - task: DotNetCoreCLI@2
    displayName: 'Run tests'
    inputs:
      command: 'test'
      projects: '**/*.Tests/**/*.csproj'
      arguments: '--configuration Release --no-build --verbosity normal'

Key CI/CD Practices

  1. Use --locked-mode during restore
    dotnet restore --locked-mode
    
  2. Ensures exact versions from Directory.Packages.props
  3. Prevents version drift

  4. Pin SDK version

  5. Ensure consistent .NET SDK version across all builds
  6. Prevents build inconsistencies

  7. Restore at solution level

    dotnet restore ConnectSoft.MicroserviceTemplate.sln
    

  8. Ensures CPM applies to all projects
  9. More efficient than per-project restore

  10. Check for outdated packages

    - script: |
        dotnet list package --outdated
      displayName: 'Check for outdated dependencies'
    

Version Management Workflow

Updating a Package Version

  1. Update Directory.Packages.props:

    <PackageVersion Include="Serilog.AspNetCore" Version="6.0.1" />
    
    Change to:
    <PackageVersion Include="Serilog.AspNetCore" Version="7.0.0" />
    

  2. Restore packages:

    dotnet restore
    

  3. Build and test:

    dotnet build
    dotnet test
    

  4. Commit changes:

  5. Commit both Directory.Packages.props and any project files that needed updates

Bulk Package Updates

For updating multiple packages:

  1. Use tools like dotnet-outdated or Renovate bot
  2. Review breaking changes for each package
  3. Test thoroughly before committing
  4. Update in logical groups (e.g., all logging packages together)

Package Source Mapping

Package source mapping enhances security by restricting which packages can be downloaded from which sources:

<packageSourceMapping>
  <!-- Public NuGet feed - all packages -->
  <packageSource key="NuGet">
    <package pattern="*" />
  </packageSource>

  <!-- Private feed - only ConnectSoft packages -->
  <packageSource key="ConnectSoftInternal">
    <package pattern="ConnectSoft.*" />
  </packageSource>
</packageSourceMapping>

Benefits

  • Security: Prevents supply chain attacks
  • Compliance: Ensures internal packages only come from trusted sources
  • Performance: Faster restores by directing packages to correct feeds

Troubleshooting

Common Issues

Issue: Package version not found

Symptom: NU1101: Unable to find package 'PackageName'. No packages exist with this id in source(s)

Solution: - Verify package is defined in Directory.Packages.props - Check nuget.config has correct package sources - Ensure package source credentials are configured

Issue: Version conflicts

Symptom: NU1107: Version conflict detected for 'PackageName'

Solution: - Check for duplicate PackageVersion entries in Directory.Packages.props - Verify no projects specify Version attribute - Review transitive dependencies that might conflict

Issue: Build warnings about versions

Symptom: Warning about package version mismatch

Solution: - Ensure all projects use <PackageReference> without Version attribute - Verify Directory.Packages.props is in solution root - Run dotnet restore --force to clear cache

Issue: CI build fails but local works

Symptom: Build succeeds locally but fails in CI

Solution: - Verify Directory.Packages.props is checked into source control - Check CI has access to all package sources in nuget.config - Ensure CI uses same SDK version as local

Migration to CPM

Migrating Existing Projects

  1. Create Directory.Packages.props:

    <Project>
      <ItemGroup>
        <!-- Extract all package versions from projects -->
      </ItemGroup>
    </Project>
    

  2. Remove versions from .csproj files:

    <!-- Before -->
    <PackageReference Include="Serilog" Version="3.1.1" />
    
    <!-- After -->
    <PackageReference Include="Serilog" />
    

  3. Test build:

    dotnet restore
    dotnet build
    dotnet test
    

  4. Verify all projects build successfully

Advanced Scenarios

Private Package Feeds

For internal packages hosted in Azure DevOps Artifacts:

<!-- Directory.Packages.props -->
<ItemGroup>
  <PackageVersion Include="ConnectSoft.Common" Version="1.0.0" />
  <PackageVersion Include="ConnectSoft.Extensions" Version="2.1.0" />
</ItemGroup>
<!-- nuget.config -->
<packageSources>
  <add key="ConnectSoftInternal" 
       value="https://pkgs.dev.azure.com/connectsoft/_packaging/Internal/nuget/v3/index.json" />
</packageSources>

<packageSourceMapping>
  <packageSource key="ConnectSoftInternal">
    <package pattern="ConnectSoft.*" />
  </packageSource>
</packageSourceMapping>

Multi-Target Framework (MTF)

When projects target multiple frameworks:

<Project>
  <PropertyGroup>
    <TargetFrameworks>net8.0;net9.0</TargetFrameworks>
  </PropertyGroup>

  <ItemGroup>
    <!-- Framework-specific packages -->
    <PackageVersion Include="System.Text.Json" Version="8.0.0" 
                    Condition="'$(TargetFramework)' == 'net8.0'" />
    <PackageVersion Include="System.Text.Json" Version="9.0.0" 
                    Condition="'$(TargetFramework)' == 'net9.0'" />
  </ItemGroup>
</Project>

Package Version Ranges (Advanced)

While CPM typically uses exact versions, you can use ranges with caution:

<PackageVersion Include="SomePackage" Version="[1.0.0,2.0.0)" />

Version Ranges

Use version ranges sparingly. Exact versions provide better reproducibility and predictability.

Summary

Central Package Management in the ConnectSoft Microservice Template provides:

  • Version Consistency: Single source of truth for all package versions
  • Simplified Maintenance: Update versions in one file, apply everywhere
  • Build Reproducibility: Consistent builds across all environments
  • Governance: Centralized audit point for dependencies
  • Clean Project Files: No version clutter in individual projects
  • CI/CD Integration: Seamless integration with Azure DevOps pipelines
  • Security: Package source mapping for supply chain security

By following these patterns and best practices, CPM becomes a powerful foundation for managing dependencies across large microservice solutions, ensuring consistency, maintainability, and reliability.