An in-app OpenAPI diff viewer and snapshot CLI for ASP.NET Core APIs.
Compare versioned OpenAPI snapshots side-by-side from within your running application and generate those snapshots from the command line without ever starting the web server.
| Package | Description |
|---|---|
SwaggerDiff.AspNetCore |
Library. Embeds a diff viewer UI and wires up minimal API endpoints for any ASP.NET Core project. |
SwaggerDiff.Tool |
CLI tool. Generates timestamped OpenAPI snapshots from a built assembly (no running server required). |
- .NET 8.0+
- Swashbuckle.AspNetCore configured in your API project (used by the CLI tool to resolve
ISwaggerProvider)
The library uses oasdiff under the hood to compute OpenAPI diffs. You don't need to install it yourself — on the first comparison request, the library will automatically:
- Check if
oasdiffis already on yourPATH - If not, download the correct binary for your platform to
~/.swaggerdiff/bin/{version}/ - Cache it for all subsequent calls
Supported platforms for auto-download: Linux (amd64/arm64), macOS (universal), Windows (amd64/arm64).
If you prefer to manage the binary yourself, you can either install oasdiff globally or point to a specific binary:
builder.Services.AddSwaggerDiff(options =>
{
options.OasDiffPath = "/usr/local/bin/oasdiff"; // skip auto-download, use this binary
});Add a project reference or (when published) install via NuGet:
dotnet add package SwaggerDiff.AspNetCoreusing SwaggerDiff.AspNetCore.Extensions;
builder.Services.AddSwaggerDiff();With custom options:
builder.Services.AddSwaggerDiff(options =>
{
options.VersionsDirectory = "Snapshots"; // default: "Docs/Versions"
options.FilePattern = "swagger_*.json"; // default: "doc_*.json"
options.RoutePrefix = "/api-diff"; // default: "/swagger-diff"
options.OasDiffVersion = "1.11.10"; // default: "1.11.10"
options.OasDiffPath = "/usr/local/bin/oasdiff"; // default: null (auto-detect/download)
});var app = builder.Build();
app.UseSwagger();
app.UseSwaggerUI(options =>
{
options.AddSwaggerDiffButton(); // adds a "Diff Tool" button to Swagger UI
});
app.UseSwaggerDiff(); // serves the diff viewer + maps /api-docs/versions and /api-docs/compareThat's it. Navigate to /swagger-diff to see the diff viewer, or click the injected button from within Swagger UI.
AddSwaggerDiff() registers the following services:
SwaggerDiffOptions— configurable paths, route prefix, and oasdiff binary settingsOasDiffDownloader(singleton) — locates or auto-downloads theoasdiffbinary on first useIApiDiffClient/OasDiffClient— shells out tooasdiffto compute HTML diffsSwaggerDiffService— lists available snapshots and orchestrates comparisons
UseSwaggerDiff() does two things:
-
Maps minimal API endpoints (no controllers, no
AddControllers()required):GET /api-docs/versions— returns available snapshot filenamesPOST /api-docs/compare— accepts{ oldVersionName, newVersionName, comparisonType }and returns the HTML diff- Both endpoints are excluded from the Swagger spec via
.ExcludeFromDescription()
-
Serves the embedded diff viewer at the configured route prefix using
EmbeddedFileProvider, so there are no loose files to deploy.
The library expects versioned JSON files in a directory relative to AppDomain.CurrentDomain.BaseDirectory:
bin/Debug/net8.0/
Docs/
Versions/
doc_20250101120000.json
doc_20250115093000.json
The filenames (minus .json) appear as version options in the diff viewer's dropdowns.
| Property | Default | Description |
|---|---|---|
VersionsDirectory |
Docs/Versions |
Path (relative to base directory) containing snapshot files |
FilePattern |
doc_*.json |
Glob pattern for discovering snapshot files |
RoutePrefix |
/swagger-diff |
URL path where the diff viewer UI is served |
OasDiffPath |
null |
Explicit path to an oasdiff binary. Skips PATH lookup and auto-download when set |
OasDiffVersion |
1.11.10 |
oasdiff version to auto-download if not found on PATH |
A dotnet tool that generates OpenAPI snapshots from a built assembly without starting the web server. Think dotnet ef migrations add, but for your API surface.
# Local (per-repo)
dotnet new tool-manifest
dotnet tool install SwaggerDiff.Tool
# Global
dotnet tool install -g SwaggerDiff.ToolGenerate a new OpenAPI snapshot. The simplest usage — run from your project directory:
# Auto-discovers the .csproj, builds it, and generates a snapshot
swaggerdiff snapshotWith explicit project and configuration:
swaggerdiff snapshot --project ./src/MyApi/MyApi.csproj -c Release --output Docs/VersionsOr point directly at pre-built assemblies (skips build and project discovery entirely):
# Single assembly
swaggerdiff snapshot --assembly ./bin/Release/net8.0/MyApi.dll
# Multiple assemblies
swaggerdiff snapshot \
--assembly ./src/AdminApi/bin/Release/net9.0/AdminApi.dll \
--assembly ./src/TenantApi/bin/Release/net9.0/TenantApi.dll| Option | Default | Description |
|---|---|---|
--project |
auto-discover | Path to one or more .csproj files. Repeat for multiple projects |
--assembly |
— | Direct path to built DLL(s). Overrides --project and skips build + project discovery. Repeat for multiple |
-c, --configuration |
Debug |
Build configuration (used with --project) |
--no-build |
false |
Skip the build step (assumes the project was already built) |
--output |
Docs/Versions |
Directory where snapshots are written (relative to each project directory) |
--doc-name |
v1 |
Swagger document name passed to ISwaggerProvider.GetSwagger() |
--exclude |
— | Project names to exclude from auto-discovery (without .csproj). Repeat for multiple |
--exclude-dir |
— | Directory names to exclude from auto-discovery. Repeat for multiple |
--parallelism |
0 |
Maximum concurrent snapshot subprocesses. 0 means unlimited (one per project) |
The command will:
- Build the project (unless
--no-buildor--assemblyis used), then resolve the output DLL via MSBuild. - Load the assembly and build the host — your
Program.csentry point runs, but aNoOpServerreplaces Kestrel so no ports are bound and hosted services are stripped out. - Resolve
ISwaggerProviderfrom the DI container and serialize the OpenAPI document. - Compare with the latest existing snapshot (normalizing away the
info.versionfield). - If the API surface has changed, write a new timestamped file (e.g.
doc_20250612143022.json). If nothing changed, print "No API changes detected" and exit cleanly.
When run from a solution directory (or any directory without a single .csproj), the tool automatically discovers all ASP.NET Core web projects by scanning up to 2 levels deep for .csproj files with Sdk="Microsoft.NET.Sdk.Web".
# From the solution root — discovers and snapshots all web API projects
swaggerdiff snapshot
# Explicit multi-project
swaggerdiff snapshot \
--project ./src/AdminApi/AdminApi.csproj \
--project ./src/TenantApi/TenantApi.csproj
# Auto-discover but skip specific projects or directories
swaggerdiff snapshot --exclude MyApi.Tests --exclude-dir testsEach project's snapshots are written to Docs/Versions/ relative to that project's own directory:
src/
ServiceOneApi/
Docs/Versions/doc_20250612143022.json
ServiceTwoApi/
Docs/Versions/doc_20250612143022.json
The tool skips bin/, obj/, .git/, and other well-known non-project directories automatically.
For CI pipelines or Docker builds where the solution is already built, you can combine --assembly with --no-build and --parallelism to skip all discovery and build overhead:
# Pre-build the solution once, then snapshot all APIs
dotnet build MySolution.sln -c Release
swaggerdiff snapshot --no-build -c Release \
--assembly ./src/AdminApi/bin/Release/net9.0/AdminApi.dll \
--assembly ./src/AuthApi/bin/Release/net9.0/AuthApi.dll \
--assembly ./src/TenantApi/bin/Release/net9.0/TenantApi.dll \
--parallelism 2When --assembly is used, the tool skips project discovery and MSBuild property evaluation entirely — it goes straight to launching snapshot subprocesses. The output directory for each assembly is inferred by navigating up from bin/{Config}/{TFM}/ to the project root.
When --no-build is used with --project (instead of --assembly), MSBuild property resolution runs in parallel since it's read-only.
Use --parallelism to cap concurrent subprocesses. The default (0) runs all concurrently. On resource-constrained environments like CI runners with 2 vCPUs, --parallelism 2 avoids CPU oversubscription.
When the CLI tool loads your application to generate a snapshot, your Program.cs entry point runs in full. This means any startup code that connects to external services (secret vaults, databases, message brokers, etc.) will execute and may fail if those services are unreachable.
To handle this, the tool automatically sets a SWAGGERDIFF_DRYRUN environment variable. The library provides a convenient static helper to check it:
using SwaggerDiff.AspNetCore;
var builder = WebApplication.CreateBuilder(args);
if (!SwaggerDiffEnv.IsDryRun)
{
// These only run during normal application startup — not during snapshot generation
builder.ConfigureSecretVault();
builder.Services.AddDbContext<AppDbContext>(...);
builder.Services.ConfigureMessageBroker();
}
// Swagger registration always runs — this is what the tool needs
builder.Services.AddEndpointsApiExplorer();
builder.Services.AddSwaggerGen();
builder.Services.AddSwaggerDiff();SwaggerDiffEnv.IsDryRun returns true only when the application is being loaded by the swaggerdiff CLI tool. During normal dotnet run, it returns false and all your startup code runs as usual.
List available snapshots:
swaggerdiff list --dir Docs/Versions| Option | Default | Description |
|---|---|---|
--dir |
Docs/Versions |
Directory to scan for snapshot files |
The CLI uses a two-stage subprocess pattern (similar to how the Swashbuckle CLI works):
-
Stage 1 (
snapshot): Builds the project (if needed), resolves the output DLL viadotnet msbuild --getProperty:TargetPath, then re-invokes itself viadotnet exec --depsfile <app>.deps.json --additional-deps <tool>.deps.json --runtimeconfig <app>.runtimeconfig.json <tool>.dll _snapshot .... This ensures the tool runs inside the target app's dependency graph while retaining access to its own dependencies. -
Stage 2 (
_snapshot): Now running with the correct dependencies, it loads the assembly viaAssemblyLoadContext, subscribes toDiagnosticListenerevents to intercept the host as it builds, injectsNoOpServer, and extracts the swagger document from the DI container.
// Program.cs
using SwaggerDiff.AspNetCore;
using SwaggerDiff.AspNetCore.Extensions;
var builder = WebApplication.CreateBuilder(args);
if (!SwaggerDiffEnv.IsDryRun)
{
builder.ConfigureSecretVault();
}
builder.Services.AddEndpointsApiExplorer();
builder.Services.AddSwaggerGen();
builder.Services.AddSwaggerDiff();
var app = builder.Build();
app.UseSwagger();
app.UseSwaggerUI(o => o.AddSwaggerDiffButton());
app.UseSwaggerDiff();
app.MapGet("/hello", () => "world");
app.Run();# Generate a snapshot (builds the project automatically)
swaggerdiff snapshot --output ./Docs/Versions
# Copy snapshots to build output so the UI can find them
cp -r Docs/ bin/Debug/net8.0/Docs/
# Run the app and navigate to /swagger-diff
dotnet runBoth packages are published to NuGet.org via GitHub Actions.
Add a NUGET_API_KEY secret to your GitHub repository:
- Generate an API key at nuget.org/account/apikeys with push permissions for
SwaggerDiff.AspNetCoreandSwaggerDiff.Tool - Go to your repo Settings > Secrets and variables > Actions
- Add a new secret named
NUGET_API_KEYwith the key value
Tag a commit and push it — the release workflow builds, packs, publishes to NuGet.org, and creates a GitHub Release:
git tag v1.0.0
git push origin v1.0.0The version number is derived from the tag (strips the v prefix). Both SwaggerDiff.AspNetCore and SwaggerDiff.Tool are published with the same version.
For pre-release or testing builds, trigger the workflow manually from the Actions tab:
- Go to Actions > Release > Run workflow
- Enter a version string (e.g.
1.1.0-beta.1) - Click Run workflow
Every push to master and every pull request runs the CI workflow which builds, packs (to verify packaging works), and uploads the .nupkg files as artifacts.
To build packages locally:
dotnet pack --configuration Release --output ./artifactsMIT