Skip to main content

.NET Versioning Strategies

As your .NET applications grow and evolve, having a solid versioning strategy becomes crucial. Versioning isn't just about assigning numbers to your software releases—it's about communicating changes, managing dependencies, and ensuring compatibility across your application ecosystem.

Introduction to Versioning in .NET

Versioning in .NET involves tracking changes to your applications and libraries over time. A good versioning strategy helps you:

  • Track the evolution of your software
  • Communicate the nature of changes to users
  • Manage dependencies between components
  • Ensure compatibility between different parts of your system
  • Support side-by-side execution of different versions

Let's explore the key aspects of versioning in the .NET ecosystem.

Understanding Version Numbers in .NET

Assembly Versions

In .NET, assemblies (DLLs and EXEs) have four distinct version numbers:

  1. Assembly Version: The version used by the runtime for binding
  2. File Version: Typically used for file information and Windows updates
  3. Informational Version: A human-readable version string that can include additional information
  4. Package Version: Used in NuGet packages

Here's how you define these versions in your project file:

xml
<PropertyGroup>
<AssemblyVersion>1.0.0.0</AssemblyVersion>
<FileVersion>1.0.0.0</FileVersion>
<Version>1.0.0</Version> <!-- This affects the package version -->
<InformationalVersion>1.0.0-beta+metadata</InformationalVersion>
</PropertyGroup>

Or using assembly attributes in your code:

csharp
[assembly: AssemblyVersion("1.0.0.0")]
[assembly: AssemblyFileVersion("1.0.0.0")]
[assembly: AssemblyInformationalVersion("1.0.0-beta+metadata")]

Semantic Versioning (SemVer)

Semantic Versioning is a widely adopted versioning scheme that gives meaning to version numbers. It follows the pattern MAJOR.MINOR.PATCH[-prerelease][+metadata]:

  • MAJOR: Increment when making incompatible API changes
  • MINOR: Increment when adding functionality in a backward-compatible manner
  • PATCH: Increment when making backward-compatible bug fixes

For example:

  • 1.0.0: Initial release
  • 1.1.0: Added new features (backward-compatible)
  • 1.1.1: Bug fixes
  • 2.0.0: Breaking changes
  • 1.0.0-alpha: Alpha pre-release
  • 1.0.0+20230615: Release with build metadata

Implementing Versioning in .NET Projects

Central Version Management

For solutions with multiple projects, you can manage versions centrally:

xml
<!-- Directory.Build.props at solution root -->
<Project>
<PropertyGroup>
<Version>1.2.3</Version>
<AssemblyVersion>1.2.3.0</AssemblyVersion>
<FileVersion>1.2.3.0</FileVersion>
</PropertyGroup>
</Project>

This sets the same version for all projects in your solution.

Automatic Versioning

You can automate versioning using build tools like GitVersion:

  1. Install GitVersion in your CI pipeline
  2. Configure it with a GitVersion.yml file:
yaml
mode: ContinuousDelivery
branches:
main:
tag: ''
develop:
tag: 'beta'
  1. GitVersion will generate version numbers based on your Git history

Assembly Binding Redirects

When dealing with assembly versions, you might need binding redirects to resolve version conflicts:

xml
<configuration>
<runtime>
<assemblyBinding xmlns="urn:schemas-microsoft-com:asm.v1">
<dependentAssembly>
<assemblyIdentity name="Newtonsoft.Json" publicKeyToken="30ad4fe6b2a6aeed" culture="neutral" />
<bindingRedirect oldVersion="0.0.0.0-13.0.0.0" newVersion="13.0.0.0" />
</dependentAssembly>
</assemblyBinding>
</runtime>
</configuration>

Versioning Strategies for Different Scenarios

Libraries and NuGet Packages

For libraries distributed as NuGet packages:

  • Follow Semantic Versioning strictly
  • Increment the MAJOR version when making breaking changes
  • Use pre-release tags for beta versions (e.g., 1.0.0-beta.1)
  • Consider using SourceLink for better debugging experience
xml
<PropertyGroup>
<Version>1.0.0-beta.1</Version>
<PackageReleaseNotes>
- Added new feature X
- Fixed bug in component Y
</PackageReleaseNotes>
<PublishRepositoryUrl>true</PublishRepositoryUrl>
<EmbedUntrackedSources>true</EmbedUntrackedSources>
<IncludeSymbols>true</IncludeSymbols>
<SymbolPackageFormat>snupkg</SymbolPackageFormat>
</PropertyGroup>

Web Applications

For web applications:

  • Version your API endpoints (e.g., /api/v1/users, /api/v2/users)
  • Use continuous deployment with rolling versions
  • Consider date-based versioning for deployments (e.g., 2023.06.15.1)

Example API versioning in ASP.NET Core:

csharp
public void ConfigureServices(IServiceCollection services)
{
services.AddApiVersioning(options =>
{
options.DefaultApiVersion = new ApiVersion(1, 0);
options.AssumeDefaultVersionWhenUnspecified = true;
options.ReportApiVersions = true;
});

services.AddVersionedApiExplorer(options =>
{
options.GroupNameFormat = "'v'VVV";
options.SubstituteApiVersionInUrl = true;
});

services.AddControllers();
}
csharp
[ApiController]
[Route("api/v{version:apiVersion}/[controller]")]
[ApiVersion("1.0")]
public class UsersV1Controller : ControllerBase
{
// V1 implementation
}

[ApiController]
[Route("api/v{version:apiVersion}/[controller]")]
[ApiVersion("2.0")]
public class UsersV2Controller : ControllerBase
{
// V2 implementation
}

Microservices

For microservices:

  • Version each service independently
  • Use API contracts to manage compatibility
  • Consider using consumer-driven contract testing
  • Use service discovery with version metadata
csharp
// Service registration with version info
public void ConfigureServices(IServiceCollection services)
{
services.AddServiceDiscovery(options =>
{
options.ServiceName = "UsersService";
options.ServiceVersion = "1.2.3";
});
}

Practical Examples

Example 1: Versioning a Class Library

Let's create a versioned class library and manage its evolution:

  1. Initial Version (1.0.0)
csharp
// MathLibrary v1.0.0
namespace MathLibrary
{
public class Calculator
{
public int Add(int a, int b)
{
return a + b;
}

public int Subtract(int a, int b)
{
return a - b;
}
}
}
  1. Minor Update (1.1.0) - Adding new functionality
csharp
// MathLibrary v1.1.0
namespace MathLibrary
{
public class Calculator
{
public int Add(int a, int b)
{
return a + b;
}

public int Subtract(int a, int b)
{
return a - b;
}

// New method (backward-compatible)
public int Multiply(int a, int b)
{
return a * b;
}
}
}
  1. Major Update (2.0.0) - Breaking change
csharp
// MathLibrary v2.0.0
namespace MathLibrary
{
public class Calculator
{
// Changed to double return type (breaking change)
public double Add(int a, int b)
{
return a + b;
}

public double Subtract(int a, int b)
{
return a - b;
}

public double Multiply(int a, int b)
{
return a * b;
}

public double Divide(int a, int b)
{
return (double)a / b;
}
}
}

Example 2: API Versioning in Practice

Here's how you might implement versioned API endpoints:

csharp
// OrdersController.cs
[ApiController]
[ApiVersion("1.0")]
[Route("api/v{version:apiVersion}/orders")]
public class OrdersV1Controller : ControllerBase
{
[HttpGet("{id}")]
public IActionResult GetOrder(int id)
{
// V1 implementation
return Ok(new { Id = id, Created = DateTime.Now });
}
}

[ApiController]
[ApiVersion("2.0")]
[Route("api/v{version:apiVersion}/orders")]
public class OrdersV2Controller : ControllerBase
{
[HttpGet("{id}")]
public IActionResult GetOrder(int id)
{
// V2 implementation with more detailed response
return Ok(new {
Id = id,
Created = DateTime.Now,
Status = "Processing",
Items = new[] { "Item1", "Item2" }
});
}
}

Example 3: Managing Version Changes in CI/CD

yaml
# azure-pipelines.yml
trigger:
- main
- release/*

pool:
vmImage: 'ubuntu-latest'

variables:
buildConfiguration: 'Release'

steps:
- task: UseDotNet@2
inputs:
version: '7.0.x'

- task: DotNetCoreCLI@2
displayName: 'Install GitVersion'
inputs:
command: custom
custom: tool
arguments: 'install --global GitVersion.Tool'

- script: |
dotnet-gitversion /output buildserver
displayName: 'Calculate version'

- task: DotNetCoreCLI@2
displayName: 'Build'
inputs:
command: 'build'
projects: '**/*.csproj'
arguments: '/p:Version=$(GitVersion.SemVer) /p:AssemblyVersion=$(GitVersion.AssemblySemVer) /p:FileVersion=$(GitVersion.AssemblySemFileVer)'

- task: DotNetCoreCLI@2
displayName: 'Pack'
inputs:
command: 'pack'
packagesToPack: '**/*.csproj'
versioningScheme: 'byEnvVar'
versionEnvVar: 'GitVersion.NuGetVersion'

Best Practices for .NET Versioning

  1. Be consistent: Choose one versioning strategy and stick to it
  2. Communicate clearly: Document your versioning policy for other developers
  3. Follow SemVer: Use semantic versioning for libraries and APIs
  4. Automate versioning: Use tools to generate version numbers automatically
  5. Use assembly binding redirects: For handling dependency conflicts
  6. Consider strong naming: For assemblies that need to be uniquely identified
  7. Test compatibility: Ensure new versions work with existing code
  8. Document breaking changes: Clearly communicate when changes break compatibility

Common Versioning Challenges

DLL Hell

"DLL Hell" refers to conflicts between different versions of the same library. In modern .NET, you can mitigate this with:

  • Package references instead of direct assembly references
  • Assembly binding redirects
  • Strong naming (when necessary)
  • Side-by-side execution

Dependency Version Conflicts

When different components depend on different versions of the same library:

xml
<!-- PackageReference with version range -->
<PackageReference Include="Newtonsoft.Json" Version="[12.0.1,13.0.0)" />

This example specifies a version range that accepts versions from 12.0.1 up to (but not including) 13.0.0.

Breaking Changes

When you must make breaking changes:

  1. Create a new major version
  2. Consider supporting both versions for a transition period
  3. Provide migration guidance
  4. Use [Obsolete] attributes to warn about deprecated APIs:
csharp
[Obsolete("Use NewMethod instead. This method will be removed in v3.0")]
public void OldMethod()
{
// Implementation
}

Summary

A well-planned versioning strategy is crucial for maintaining, deploying, and evolving .NET applications. By following semantic versioning principles and using the appropriate tooling, you can effectively communicate changes, manage dependencies, and ensure compatibility across your application ecosystem.

Remember these key points:

  • Use semantic versioning for clear communication of change impact
  • Implement automated versioning in your CI/CD pipeline
  • Choose appropriate versioning strategies based on your project type
  • Document your versioning policies for developers and users
  • Test compatibility between versions

Additional Resources

Exercises

  1. Create a simple class library and implement a versioning strategy as you add features and make changes.
  2. Implement API versioning in an ASP.NET Core web API project.
  3. Set up GitVersion in a CI/CD pipeline to automate versioning based on Git history.
  4. Create a solution with multiple projects and implement central version management.
  5. Simulate a breaking change scenario and implement appropriate versioning and migration strategies.


If you spot any mistakes on this website, please let me know at [email protected]. I’d greatly appreciate your feedback! :)