Skip to content

A command-line tool for comparing public APIs between different versions of .NET assemblies to detect breaking changes and API evolution.

License

Notifications You must be signed in to change notification settings

jbrinkman/dotnet-api-diff

Repository files navigation

DotNet API Diff Tool

Main Branch Build PR Build and Test Code Coverage

A powerful command-line tool for comparing public APIs between different versions of .NET assemblies. Designed to help library maintainers and development teams identify breaking changes, track API evolution, and enforce semantic versioning practices.

πŸš€ Key Features

  • Comprehensive API Analysis: Analyzes types, methods, properties, fields, events, and their signatures
  • Breaking Change Detection: Automatically identifies and classifies breaking changes vs. non-breaking additions
  • Multiple Output Formats: Generate reports in Console, JSON, XML, HTML, and Markdown formats
  • Flexible Filtering: Include/exclude specific namespaces, types, or members using patterns
  • Configuration-Driven: Use JSON configuration files for complex comparison scenarios
  • CI/CD Integration: Perfect for automated builds and release pipelines
  • Semantic Versioning Support: Exit codes align with semantic versioning practices

Features

  • Compare public APIs between two .NET assembly versions
  • Detect breaking changes, additions, removals, and modifications
  • Generate reports in multiple formats (Console, JSON, XML, HTML, Markdown)
  • Comprehensive analysis of types, methods, properties, fields, and events
  • Severity classification for different types of changes

Requirements

  • .NET 8.0 SDK or later
  • Compatible with .NET Framework, .NET Core, and .NET 5+ assemblies

Building

# Clone the repository
git clone <repository-url>
cd DotNetApiDiff

# Build the solution
dotnet build

# Run tests
dotnet test

Using Taskfile

This project uses Taskfile to simplify common development tasks. Make sure you have Task installed:

# macOS
brew install go-task

# Linux
sh -c "$(curl --location https://taskfile.dev/install.sh)" -- -d -b ~/.local/bin

# Windows (with Chocolatey)
choco install go-task

Common Tasks

# List all available tasks
task

# Build the solution
task build

# Run all tests
task test

# Generate code coverage report
task coverage

# View coverage report in browser
task coverage:view

# Run the application with arguments
task run -- compare source.dll target.dll

# Run CI build sequence
task ci

πŸ“‹ Quick Start

Installation

Package Managers (Recommended)

Windows (Chocolatey):

choco install dotnetapidiff

macOS/Linux (Homebrew):

brew install dotnetapidiff

Quick Install Script (Linux/macOS)

# Install latest version to user directory
curl -fsSL https://raw.githubusercontent.com/jbrinkman/dotnet-api-diff/main/install.sh | bash

# Install specific version
curl -fsSL https://raw.githubusercontent.com/jbrinkman/dotnet-api-diff/main/install.sh | bash -s -- v1.2.3

# Install system-wide (requires sudo)
curl -fsSL https://raw.githubusercontent.com/jbrinkman/dotnet-api-diff/main/install.sh | sudo bash -s -- --system

Manual Install

Download the latest release for your platform from GitHub Releases:

  • Linux: dotnet-api-diff-linux-x64.tar.gz or dotnet-api-diff-linux-arm64.tar.gz
  • macOS: dotnet-api-diff-osx-x64.tar.gz or dotnet-api-diff-osx-arm64.tar.gz
  • Windows: dotnet-api-diff-win-x64.zip or dotnet-api-diff-win-arm64.zip

Extract the archive and add the binary to your PATH.

Build from Source

# Clone the repository
git clone https://github.com/jbrinkman/dotnet-api-diff.git
cd dotnet-api-diff

# Build the tool
dotnet build

# Or use the Task runner (recommended)
task build

Basic Usage

# Compare two assembly versions
dotnetapidiff compare MyLibrary.v1.dll MyLibrary.v2.dll

# Generate a JSON report
dotnetapidiff compare source.dll target.dll --output json

# Save report to file
dotnetapidiff compare source.dll target.dll --output markdown > api-changes.md

πŸ”§ Usage Examples

Basic Comparison

# Simple comparison with console output
dotnetapidiff compare MyLibrary.v1.dll MyLibrary.v2.dll

Filtering Options

# Filter to specific namespaces
dotnetapidiff compare source.dll target.dll \
  --filter "MyLibrary.Core" --filter "MyLibrary.Extensions"

# Filter to specific type patterns
dotnetapidiff compare source.dll target.dll \
  --type "MyLibrary.Core.*" --type "MyLibrary.Models.*"

# Exclude internal or test types
dotnetapidiff compare source.dll target.dll \
  --exclude "*.Internal*" --exclude "*.Tests*"

# Include internal types (normally excluded)
dotnetapidiff compare source.dll target.dll --include-internals

# Include compiler-generated types
dotnetapidiff compare source.dll target.dll --include-compiler-generated

Output Formats

# Generate JSON report
dotnetapidiff compare source.dll target.dll --output json

# Generate Markdown report
dotnetapidiff compare source.dll target.dll --output markdown

# Generate XML report
dotnetapidiff compare source.dll target.dll --output xml

# Generate HTML report
dotnetapidiff compare source.dll target.dll --output html

Configuration File Usage

# Use a configuration file for complex scenarios
dotnetapidiff compare source.dll target.dll --config my-config.json

# Enable verbose logging
dotnetapidiff compare source.dll target.dll --verbose

# Disable colored output (useful for CI/CD)
dotnetapidiff compare source.dll target.dll --no-color

βš™οΈ Configuration

Configuration File

Use JSON configuration files to customize comparison behavior for complex scenarios. This is especially useful for:

  • Large libraries with complex namespace structures
  • APIs that have been refactored or reorganized
  • Automated CI/CD pipelines with specific requirements

Complete Configuration Example

{
  "mappings": {
    "namespaceMappings": {
      "OldNamespace": ["NewNamespace"],
      "Legacy.Api": ["Modern.Api", "Modern.Api.V2"],
      "MyLibrary.V1": ["MyLibrary.V2.Core", "MyLibrary.V2.Extensions"]
    },
    "typeMappings": {
      "OldNamespace.OldType": "NewNamespace.NewType",
      "Legacy.UserManager": "Modern.Identity.UserService",
      "Utils.StringHelper": "Extensions.StringExtensions"
    },
    "autoMapSameNameTypes": true,
    "ignoreCase": true
  },
  "exclusions": {
    "excludedTypes": [
      "System.Diagnostics.Debug",
      "MyLibrary.Internal.DebugHelper"
    ],
    "excludedTypePatterns": [
      "*.Internal.*",
      "*.Private.*",
      "*.Tests.*",
      "*Helper",
      "*Utility"
    ],
    "excludedMembers": [
      "System.Object.Finalize",
      "MyLibrary.BaseClass.InternalMethod"
    ],
    "excludedMemberPatterns": [
      "*.Obsolete*",
      "*.Debug*",
      "get_Internal*",
      "set_Internal*"
    ]
  },
  "breakingChangeRules": {
    "treatTypeRemovalAsBreaking": true,
    "treatMemberRemovalAsBreaking": true,
    "treatAddedTypeAsBreaking": false,
    "treatAddedMemberAsBreaking": false,
    "treatSignatureChangeAsBreaking": true
  },
  "filters": {
    "includeNamespaces": [
      "MyLibrary.Core",
      "MyLibrary.Extensions",
      "MyLibrary.Models"
    ],
    "excludeNamespaces": [
      "MyLibrary.Internal",
      "MyLibrary.Tests",
      "System.Diagnostics"
    ],
    "includeTypes": [
      "MyLibrary.Core.*",
      "MyLibrary.Models.*"
    ],
    "excludeTypes": [
      "*.Internal*",
      "*.Helper*",
      "*Test*"
    ],
    "includeInternals": false,
    "includeCompilerGenerated": false
  },
  "outputFormat": "Console",
  "outputPath": null,
  "failOnBreakingChanges": true
}

Sample Configuration Files

The repository includes several sample configuration files for common scenarios:

  • samples/basic-config.json - Basic configuration for simple libraries
  • samples/enterprise-config.json - Configuration for large enterprise libraries
  • samples/strict-breaking-changes.json - Strict breaking change detection
  • samples/lenient-changes.json - Lenient configuration for pre-release versions
  • samples/namespace-filtering.json - Advanced namespace filtering examples

Configuration Sections

  • mappings: Define namespace and type name mappings between assemblies

    • namespaceMappings: Map source namespaces to target namespaces
    • typeMappings: Map specific source types to target types
    • autoMapSameNameTypes: Automatically map types with the same name but different namespaces
    • ignoreCase: Ignore case when comparing type and namespace names
  • exclusions: Specify types and members to exclude from comparison

    • excludedTypes: List of fully qualified type names to exclude
    • excludedTypePatterns: Wildcard patterns for excluding types
    • excludedMembers: List of fully qualified member names to exclude
    • excludedMemberPatterns: Wildcard patterns for excluding members
  • breakingChangeRules: Configure what changes are considered breaking

    • treatTypeRemovalAsBreaking: Whether removing a type is a breaking change
    • treatMemberRemovalAsBreaking: Whether removing a member is a breaking change
    • treatAddedTypeAsBreaking: Whether adding a type is a breaking change
    • treatAddedMemberAsBreaking: Whether adding a member is a breaking change
    • treatSignatureChangeAsBreaking: Whether changing a member signature is a breaking change
  • filters: Control which types and namespaces are included in the comparison

    • includeNamespaces: List of namespaces to include (if empty, all namespaces are included)
    • excludeNamespaces: List of namespaces to exclude
    • includeTypes: List of type patterns to include
    • excludeTypes: List of type patterns to exclude
    • includeInternals: Whether to include internal types
    • includeCompilerGenerated: Whether to include compiler-generated types
  • outputFormat: Format for the comparison report (Console, Json, Markdown)

  • outputPath: Path to save the report (if null, output to console)

  • failOnBreakingChanges: Whether to return a non-zero exit code if breaking changes are found

πŸ”„ CI/CD Integration

GitHub Actions

Create .github/workflows/api-compatibility.yml:

name: API Compatibility Check

on:
  pull_request:
    branches: [ main ]

jobs:
  api-compatibility:
    runs-on: ubuntu-latest
    
    steps:
    - uses: actions/checkout@v4
      with:
        fetch-depth: 0  # Need full history to compare with main
    
    - name: Setup .NET
      uses: actions/setup-dotnet@v4
      with:
        dotnet-version: 8.0.x
    
    - name: Build current version
      run: dotnet build --configuration Release
    
    - name: Checkout main branch
      run: |
        git checkout main
        dotnet build --configuration Release --output ./baseline
        git checkout -
    
    - name: Build PR version
      run: dotnet build --configuration Release --output ./current
    
    - name: Run API Diff
      run: |
        dotnetapidiff compare \
          ./baseline/MyLibrary.dll \
          ./current/MyLibrary.dll \
          --config .github/api-diff-config.json \
          --output markdown \
          --no-color > api-changes.md
    
    - name: Comment PR with API Changes
      uses: actions/github-script@v7
      with:
        script: |
          const fs = require('fs');
          const apiChanges = fs.readFileSync('api-changes.md', 'utf8');
          github.rest.issues.createComment({
            issue_number: context.issue.number,
            owner: context.repo.owner,
            repo: context.repo.repo,
            body: `## API Changes\n\n${apiChanges}`
          });

Azure DevOps Pipeline

Create azure-pipelines-api-check.yml:

trigger:
- main

pool:
  vmImage: 'ubuntu-latest'

variables:
  buildConfiguration: 'Release'

steps:
- task: DotNetCoreCLI@2
  displayName: 'Build Baseline'
  inputs:
    command: 'build'
    configuration: $(buildConfiguration)
    outputDir: '$(Build.ArtifactStagingDirectory)/baseline'

- task: DotNetCoreCLI@2
  displayName: 'Build Current'
  inputs:
    command: 'build'
    configuration: $(buildConfiguration)
    outputDir: '$(Build.ArtifactStagingDirectory)/current'

- task: DotNetCoreCLI@2
  displayName: 'Run API Diff'
  inputs:
    command: 'custom'
    custom: 'dotnetapidiff'
    arguments: >
      compare
      $(Build.ArtifactStagingDirectory)/baseline/MyLibrary.dll
      $(Build.ArtifactStagingDirectory)/current/MyLibrary.dll
      --config api-diff-config.json
      --output json
      --no-color
  continueOnError: true

- task: PublishTestResults@2
  displayName: 'Publish API Diff Results'
  inputs:
    testResultsFormat: 'JUnit'
    testResultsFiles: 'api-diff-results.xml'

πŸ“Š Exit Codes

The tool uses semantic exit codes to integrate with CI/CD systems:

  • 0: Success - Comparison completed successfully with no breaking changes detected
  • 1: Breaking Changes - Comparison completed successfully but breaking changes were detected
  • 2: Comparison Error - An error occurred during the comparison process
  • 3: Assembly Load Error - Failed to load one or more assemblies for comparison
  • 4: Configuration Error - Configuration error or invalid settings detected
  • 5: Invalid Arguments - Invalid command line arguments provided
  • 6: File Not Found - One or more required files could not be found
  • 99: Unexpected Error - An unexpected error occurred during execution

Using Exit Codes in Scripts

#!/bin/bash

# Run API diff and capture exit code
dotnetapidiff compare old.dll new.dll --config config.json
EXIT_CODE=$?

case $EXIT_CODE in
  0)
    echo "βœ… Comparison completed successfully with no breaking changes"
    ;;
  1)
    echo "⚠️  Breaking changes detected - review before release"
    exit 1
    ;;
  2)
    echo "❌ Comparison error occurred"
    exit 1
    ;;
  3)
    echo "πŸ’₯ Assembly loading failed"
    exit 1
    ;;
  4)
    echo "βš™οΈ  Configuration error"
    exit 1
    ;;
  5)
    echo "πŸ“ Invalid command line arguments"
    exit 1
    ;;
  6)
    echo "πŸ“ Required files not found"
    exit 1
    ;;
  99)
    echo "πŸ”₯ Unexpected error occurred"
    exit 1
    ;;
  *)
    echo "❓ Unknown exit code: $EXIT_CODE"
    exit 1
    ;;
esac

🎯 Real-World Examples

Example 1: NuGet Package Release Check

# Download previous version from NuGet
nuget install MyLibrary -Version 1.0.0 -OutputDirectory packages

# Compare with current build
dotnetapidiff compare \
  packages/MyLibrary.1.0.0/lib/net8.0/MyLibrary.dll \
  src/MyLibrary/bin/Release/net8.0/MyLibrary.dll \
  --output json > api-changes.json

# Parse results for automated decision making
if [ $? -eq 1 ]; then
  echo "Breaking changes detected - increment major version"
elif [ $? -eq 0 ]; then
  echo "No breaking changes detected - increment minor or patch version"
else
  echo "Error occurred during comparison - check logs"
  exit 1
fi

Example 2: Multi-Target Framework Analysis

# Compare .NET Framework vs .NET Core implementations
dotnetapidiff compare \
  MyLibrary.net48.dll \
  MyLibrary.net8.0.dll \
  --config framework-migration-config.json \
  --output html > framework-differences.html

Example 3: Pre-Release Validation

# Lenient checking for pre-release versions
dotnetapidiff compare \
  MyLibrary.1.0.0-stable.dll \
  MyLibrary.1.1.0-beta.dll \
  --config samples/lenient-changes.json \
  --output markdown

πŸ“– Command Line Reference

Compare Command

  • <sourceAssembly>: Path to the source/baseline assembly (required)
  • <targetAssembly>: Path to the target/current assembly (required)
  • --config, -c: Path to configuration file
  • --output, -o: Output format (console, json, xml, html, markdown)
  • --filter, -f: Filter to specific namespaces (can be specified multiple times)
  • --type, -t: Filter to specific type patterns (can be specified multiple times)
  • --exclude, -e: Exclude types matching pattern (can be specified multiple times)
  • --include-internals: Include internal types in the comparison
  • --include-compiler-generated: Include compiler-generated types in the comparison
  • --no-color: Disable colored output (useful for CI/CD)
  • --verbose, -v: Enable verbose output
  • --help, -h: Show help information

Project Structure

src/
β”œβ”€β”€ DotNetApiDiff/
β”‚   β”œβ”€β”€ Interfaces/          # Core service interfaces
β”‚   β”œβ”€β”€ Models/              # Data models and enums
β”‚   β”œβ”€β”€ Services/            # Implementation classes (to be added)
β”‚   └── Program.cs           # Main entry point
tests/
└── DotNetApiDiff.Tests/     # Unit tests

πŸ” Troubleshooting

Common Issues

Assembly Loading Errors

# Ensure assemblies are built for compatible target frameworks
dotnetapidiff compare source.dll target.dll --verbose

Missing Dependencies

# Place all dependencies in the same directory as the assemblies
# Or use the --assembly-path option to specify additional search paths

Large Memory Usage

# For very large assemblies, consider filtering to specific namespaces
dotnetapidiff compare source.dll target.dll \
  --filter "MyLibrary.Core" --exclude "*.Internal*"

Performance Issues

# Use configuration files to exclude unnecessary types
dotnetapidiff compare source.dll target.dll \
  --config samples/enterprise-config.json

Debug Mode

Enable detailed logging for troubleshooting:

# Set environment variable for detailed logging
export DOTNET_API_DIFF_LOG_LEVEL=Debug

# Run with verbose output
dotnetapidiff compare source.dll target.dll --verbose

πŸš€ Advanced Usage

Custom Report Formats

Extend the tool with custom report generators:

public class CustomReportGenerator : IReportGenerator
{
    public string GenerateReport(ComparisonResult result, ReportFormat format)
    {
        // Custom report logic
        return customReport;
    }
}

Programmatic Usage

Use the tool as a library in your own applications:

var services = new ServiceCollection();
// Configure services...
var serviceProvider = services.BuildServiceProvider();

var comparer = serviceProvider.GetRequiredService<IApiComparer>();
var result = await comparer.CompareAssembliesAsync(sourceAssembly, targetAssembly);

if (result.HasBreakingChanges)
{
    Console.WriteLine($"Found {result.BreakingChangesCount} breaking changes");
}

Integration with Build Tools

MSBuild Integration

<Target Name="CheckApiCompatibility" BeforeTargets="Pack">
  <Exec Command="dotnetapidiff compare $(PreviousVersionDll) $(OutputPath)$(AssemblyName).dll --config api-config.json" 
        ContinueOnError="false" />
</Target>

Cake Build Integration

Task("CheckApiCompatibility")
    .Does(() =>
{
    var exitCode = StartProcess("dotnetapidiff", new ProcessSettings
    {
        Arguments = "compare baseline.dll current.dll --config config.json"
    });
    
    if (exitCode == 1)
    {
        throw new Exception("Breaking changes detected!");
    }
    else if (exitCode != 0)
    {
        throw new Exception($"API comparison failed with exit code {exitCode}");
    }
});

🀝 Contributing

We welcome contributions! Here's how to get started:

Development Setup

# Clone the repository
git clone https://github.com/jbrinkman/dotnet-api-diff.git
cd dotnet-api-diff

# Install dependencies
dotnet restore

# Run tests
task test

# Run with coverage
task coverage

Contribution Guidelines

  1. Fork the repository and create a feature branch
  2. Write tests for new functionality
  3. Follow coding standards (enforced by StyleCop)
  4. Update documentation for user-facing changes
  5. Ensure all tests pass including code coverage requirements
  6. Submit a pull request with a clear description

πŸ“„ License

This project is licensed under the MIT License - see the LICENSE file for details.

πŸ™ Acknowledgments

About

A command-line tool for comparing public APIs between different versions of .NET assemblies to detect breaking changes and API evolution.

Resources

License

Stars

Watchers

Forks

Packages

No packages published

Contributors 2

  •  
  •