Diff the public API of two versions of your assemblies, from disk, NuGet or a git commit, and find out whether the change is breaking.
Proc 0.14.0 added a CancellationToken to its Start* methods. Diffing it against 0.13.0 straight from nuget.org shows
each old overload deleted and its replacement added, which makes the release a major change:
$ assembly-differ diff "nuget|Proc|0.13.0|netstandard2.0" "nuget|Proc|0.14.0|netstandard2.0" -t proc
[diff] Difference found: NuGetAssemblyProvider:proc.dll and NuGetAssemblyProvider:proc.dll
<Assembly Name="proc, Version=0.0.0.0, Culture=neutral, PublicKeyToken=96c599bbe3e70f5d" DiffType="Modified">
<Module Name="proc.dll" DiffType="Modified">
<Type Name="ProcNet.LongRunningApplicationSubscription" DiffType="Modified">
<Property Name="WaitHandle" DiffType="Modified">
<Method Name="get" DiffType="Modified">
<DeclarationDiffs>
<DiffItem DiffType="Modified">Member is more visible.</DiffItem>
</DeclarationDiffs>
</Method>
</Property>
</Type>
<Type Name="ProcNet.ObservableProcessBase<TConsoleOut>" DiffType="Modified">
<Method Name="WaitForCompletion(Nullable<TimeSpan>, CancellationToken)" DiffType="New" />
<Method Name="WaitForCompletion(Nullable<TimeSpan>)" DiffType="Deleted" />
</Type>
<Type Name="ProcNet.Proc" DiffType="Modified">
<Method Name="Start(StartArguments, CancellationToken)" DiffType="New" />
<Method Name="Start(StartArguments)" DiffType="Deleted" />
<Method Name="StartLongRunning(LongRunningArguments, TimeSpan, IConsoleOutWriter, CancellationToken)" DiffType="New" />
<Method Name="StartLongRunning(LongRunningArguments, TimeSpan, IConsoleOutWriter)" DiffType="Deleted" />
<Method Name="StartRedirected(StartArguments, StandardInputHandler, IConsoleLineHandler, CancellationToken)" DiffType="New" />
<Method Name="StartRedirected(StartArguments, StandardInputHandler, IConsoleLineHandler)" DiffType="Deleted" />
<Method Name="StartRedirected(StartArguments, IConsoleLineHandler, CancellationToken)" DiffType="New" />
<Method Name="StartRedirected(StartArguments, IConsoleLineHandler)" DiffType="Deleted" />
</Type>
</Module>
</Assembly>[diff] Suggested version change: Majordotnet tool install --global assembly-differOn Linux (x64, arm64), Windows (x64, arm64) and macOS (arm64) this installs a self-contained native-AOT executable: no shared .NET runtime, no JIT warm-up. Everywhere else it falls back to a framework-dependent build, which needs the .NET runtime installed.
Diff the last release on nuget.org against what you just built, as Markdown, and fail the build on a breaking change:
assembly-differ diff "previous-nuget|MyLib|1.3.0|net8.0" "directory|src/MyLib/bin/Release/net8.0" -t MyLib -f markdown -p majorprevious-nuget finds the latest release before 1.3.0, so the same command keeps working as you release.
Each side is a provider spec, and the two sides can mix:
| Spec | Assemblies |
|---|---|
assembly|<path> |
one dll |
directory|<path> |
every dll in a directory, matched by file name against the other side |
nuget|<id>|<version>|[tfm] |
a package from the NuGet sources your NuGet.config lists |
previous-nuget|<id>|<version>|[tfm] |
the release on nuget.org before <version> |
github|<owner/repo>|<commit>|<build command>|<relative output path> |
a commit, cloned and built with the command you give |
| Flag | What |
|---|---|
-t, --target |
only diff these assemblies (repeatable); defaults to every assembly both sides have |
-f, --format |
xml (default), markdown, asciidoc or github-comment (a pull request comment with a collapsible diff per assembly) |
-o, --output |
output directory or file; without it the diff only goes to the console |
-p, --prevent-change |
exit with 4 when the suggested change is at least patch, minor or major; none (the default) never fails |
-a, --allow-empty-previous-nuget |
succeed when previous-nuget finds no earlier release, for a first release |
The suggested change is Patch when the public API is unchanged and Major when something was deleted or changed.
Additions alone are currently reported as Major too: diffing Proc 0.12.0 against 0.13.0, which only adds
NoWrapInThread, suggests Major. Always name the diff subcommand: a provider spec as the first argument is read as
a command name.
More examples:
# two local builds, every dll matched by name
assembly-differ diff "directory|build/1.2.0" "directory|build/1.3.0"
# two published packages
assembly-differ diff "nuget|Proc|0.13.0|netstandard2.0" "nuget|Proc|0.14.0|netstandard2.0" -t proc -f markdown
# the last release before 0.14.0 against a local build
assembly-differ diff "previous-nuget|Proc|0.14.0|netstandard2.0" "directory|build/proc" -t proc- uses: nullean/assembly-differ@main
with:
first: "previous-nuget|MyLib|1.3.0|net8.0"
second: "directory|src/MyLib/bin/Release/net8.0"
args: --target MyLib --format markdownThe action runs assembly-differ diff <first> <second> <args> in the prebuilt, distroless
ghcr.io/nullean/assembly-differ:edge container, so the workflow needs no .NET SDK. args passes through verbatim.
Container actions run on Linux runners only.
The same image works anywhere docker run does, such as GitLab CI or a machine without the .NET SDK:
docker run --rm -v "$(pwd)":/workspace ghcr.io/nullean/assembly-differ:edge diff "directory|/workspace/old" "directory|/workspace/new"It is native-AOT on a chiseled runtime-deps base, with no shell, running as a non-root user. edge tracks the latest
build of main; latest and the version tags (for example 0.18.1) follow releases.
The tool ships as one package per platform. Install the root package; the .NET SDK picks the right one.
| Package | What it is | |
|---|---|---|
assembly-differ |
the tool; points the SDK at the package for your platform | |
assembly-differ.<rid> |
the native-AOT build for linux-x64, linux-arm64, win-x64, win-arm64 and osx-arm64 |
|
assembly-differ.any |
the framework-dependent fallback (net8.0, net10.0) for every other platform |
assembly-differ builds on JustAssembly, licensed under Apache 2.0.
MIT licensed. Copyright Nullean and contributors.