PublicApiSharp.Analyzers 2.1.0

dotnet add package PublicApiSharp.Analyzers --version 2.1.0
                    
NuGet\Install-Package PublicApiSharp.Analyzers -Version 2.1.0
                    
This command is intended to be used within the Package Manager Console in Visual Studio, as it uses the NuGet module's version of Install-Package.
<PackageReference Include="PublicApiSharp.Analyzers" Version="2.1.0">
  <PrivateAssets>all</PrivateAssets>
  <IncludeAssets>runtime; build; native; contentfiles; analyzers</IncludeAssets>
</PackageReference>
                    
For projects that support PackageReference, copy this XML node into the project file to reference the package.
<PackageVersion Include="PublicApiSharp.Analyzers" Version="2.1.0" />
                    
Directory.Packages.props
<PackageReference Include="PublicApiSharp.Analyzers">
  <PrivateAssets>all</PrivateAssets>
  <IncludeAssets>runtime; build; native; contentfiles; analyzers</IncludeAssets>
</PackageReference>
                    
Project file
For projects that support Central Package Management (CPM), copy this XML node into the solution Directory.Packages.props file to version the package.
paket add PublicApiSharp.Analyzers --version 2.1.0
                    
#r "nuget: PublicApiSharp.Analyzers, 2.1.0"
                    
#r directive can be used in F# Interactive and Polyglot Notebooks. Copy this into the interactive tool or source code of the script to reference the package.
#:package PublicApiSharp.Analyzers@2.1.0
                    
#:package directive can be used in C# file-based apps starting in .NET 10 preview 4. Copy this into a .cs file before any lines of code to reference the package.
#addin nuget:?package=PublicApiSharp.Analyzers&version=2.1.0
                    
Install as a Cake Addin
#tool nuget:?package=PublicApiSharp.Analyzers&version=2.1.0
                    
Install as a Cake Tool

CI Build Coverage Reliability Rating Duplicated Lines (%) Vulnerabilities Security Rating NuGet Downloads GitHub stars License: MIT

<br> <a href="https://github.com/glennawatson/PublicApiRoslynGenerator"> <img width="160" height="160" src="https://raw.githubusercontent.com/glennawatson/PublicApiRoslynGenerator/main/assets/icon.png" alt="PublicApiSharp.Analyzers"> </a> <br>

PublicApiSharp.Analyzers

A Roslyn analyzer that keeps a library's public API surface in a checked-in text file, and reports an analyzer error when the two disagree.

The baseline is ordinary C#, one file per project per target framework:

namespace Refit;

public sealed class ApiResponse<T> : Refit.IApiResponse, System.IDisposable
{
    public ApiResponse(System.Net.Http.HttpResponseMessage response, T? content, Refit.RefitSettings settings) { }
    public T Content { get; }
    public Refit.ApiExceptionBase? Error { get; }
    public bool IsSuccessful { get; }
    public void Dispose() { }
}

That shape is the point. A reviewer reads the diff the way they read code and can tell at a glance whether a change is additive — which a flat list of one-line-per-member entries does not give you.

Installation

dotnet add package PublicApiSharp.Analyzers

The package is analyzer-only. It adds no runtime assemblies to your output, ships as a DevelopmentDependency, and is not transitive to consumers of your library.

Until a baseline file exists the analyzer does nothing at all, so adding the package cannot break a build before you have adopted it.

Quick start

Create an empty baseline for each target framework:

mkdir -p src/MyProject/PublicAPI/net10.0
touch     src/MyProject/PublicAPI/net10.0/PublicAPI.txt

Build. Every member is reported as PAS0001; apply the Update the public API baseline fix once and the file is written. From then on the build fails whenever the surface drifts from the file, and the same fix accepts the change.

From the command line:

dotnet format analyzers MyProject.csproj --diagnostics PAS0001 PAS0003 --severity info

On a multi-targeting project dotnet format writes one baseline per run — it applies the fix for a single inner build at a time — so run it once per target framework, or simply repeat it until the build is clean.

Enable PAS0004 to be told which target frameworks are still untracked.

How it differs from the alternatives

this package shipped/unshipped analyzer assembly-reflection approval
Feedback analyzer error, on the declaration analyzer error test failure
Format nested C#, reads like source one flat line per member nested C#, reads like source
Promotion step none — one file, always current move entries unshipped → shipped none
Multi-targeting one baseline per TFM, wired up automatically manual manual
New C# features inherited from the host compiler inherited from the host compiler waits on the tool to add support

There is no shipped/unshipped split. The baseline states what the assembly exposes right now, so an API change is accepted by updating the file in the same commit that makes the change. The diff a reviewer reads is the API change itself, not a later promotion.

Rules

Id Rule Severity Fix
PAS0001 Public API is not in the baseline Error Yes
PAS0002 Public API in the baseline no longer exists Error Yes
PAS0003 Public API differs from the baseline Error Yes
PAS0004 No public API baseline for this target framework Warning, off Yes
PAS0005 Public API baseline could not be read Error No

Additions and changes are reported on the declaration in your source. Removals are reported on the baseline line, because there is nothing left in the source to point at.

Multi-targeting

A multi-targeting project genuinely has a different surface per target — conditional compilation, framework-only types, nullability that differs by target — so one shared baseline can only ever describe one of them. The package's MSBuild targets resolve the path per inner build and add it to @(AdditionalFiles) for you:

src/MyProject/
  PublicAPI/
    net472/PublicAPI.txt
    net8.0/PublicAPI.txt
    net10.0/PublicAPI.txt
  MyProject.csproj

Set these MSBuild properties in the project file:

Property Default Purpose
EnablePublicApiBaseline true Set to false to disable the package's baseline setup.
PublicApiBaselineDirectory $(MSBuildProjectDirectory)\PublicAPI Where the per-TFM folders live.
PublicApiBaselineFileName PublicAPI.txt The file name inside each folder.
PublicApiBaselineFile $(PublicApiBaselineDirectory)\$(TargetFramework)\$(PublicApiBaselineFileName) Set it to place one baseline explicitly.

The default path is resolved only when TargetFramework is set. With baseline setup enabled, the package supplies it as build_property.PublicApiBaselineFile even if the file is missing, and adds the file to @(AdditionalFiles) only when it exists.

For manual setup, include the baseline as an AdditionalFiles item. The configured path takes precedence; if it is absent or does not match an available file, a file named PublicAPI.txt is also recognized. EnablePublicApiBaseline=false disables the package's setup, but does not prevent recognition of a manually included baseline.

Configuration

Options are read from .editorconfig. They describe the whole surface rather than one file, so write them once for the project — under [*.cs], or [*] — rather than varying them between folders. A global config (.globalconfig, or anything with is_global = true) is also read, and takes precedence where both set the same key.

Key Default Purpose
publicapisharp.include_assembly_attributes true Record assembly-level attributes at all.
publicapisharp.include_generated_code false Record declarations a tool marked as its own output.
publicapisharp.excluded_attributes (empty) Attribute patterns to leave out.
publicapisharp.included_attributes (empty) Attribute patterns to keep despite the built-in list.
publicapisharp.excluded_namespace_prefixes (empty) Namespace prefixes to leave out.

The boolean options accept true or false; invalid values use the defaults. List entries are comma-separated, with surrounding whitespace trimmed and empty entries ignored.

The namespace list is case-sensitive and has no wildcards. Each entry excludes that namespace and its descendants: Sample.Int excludes Sample.Int.Nested, but leaves Sample.Internals in the surface.

Generated declarations

A declaration carrying [GeneratedCode] is left out of the surface. What a build tool writes into an assembly is usually its own plumbing, and it comes and goes with build inputs rather than with the library's API. WPF is the clearest case: PresentationBuildTasks emits a public GeneratedInternalTypeHelper into XamlGeneratedNamespace, but only while some XAML file happens to reference an internal type — so a baseline that recorded it would churn for a reason no consumer can see.

If your library's API is itself generated — a source generator emitting types or members that consumers call directly — record it:

[*.cs]
publicapisharp.include_generated_code = true

The marker is the attribute on the declaration, so a generator that adds members to a hand-written partial type only removes the members it wrote.

Stripping attributes

The attribute lists are comma-separated patterns of fully qualified names, and * matches any run of characters, so a whole family or a naming convention is one entry rather than a list:

[*.cs]
publicapisharp.excluded_attributes = System.Diagnostics.CodeAnalysis.*, *.InternalUseAttribute

Attribute names are case-sensitive and include the Attribute suffix.

Some attributes are dropped without being asked for, because they are the build's own bookkeeping rather than API. The version and target-framework stamps matter most: the SDK writes them into every assembly, so keeping them would rewrite every baseline in the repository on each release — a version bump is not an API change — and would restate the target framework the baseline's own folder already names.

dropped by default why
AssemblyVersion, AssemblyFileVersion, AssemblyInformationalVersion, AssemblyMetadata churn on every release
TargetFramework, TargetPlatform already stated by the baseline's folder
AssemblyCompany, AssemblyProduct, AssemblyTitle, AssemblyCopyright, AssemblyTrademark, AssemblyConfiguration, AssemblyDescription packaging metadata
AssemblyKeyFile, AssemblyKeyName, AssemblyDelaySign, AssemblySignatureKey, AssemblyCulture signing and build configuration
CompilerGenerated, Nullable, NullableContext, IsReadOnly, IsByRefLike, RequiredMember, Extension, and the rest of the compiler's own markers described by the declaration itself
Debuggable, DebuggerStepThrough, DebuggerNonUserCode, GeneratedCode, DefaultMember build and tooling detail
SuppressMessage conditional on CODE_ANALYSIS, so whether it reaches the assembly at all depends on how the project is built
UnconditionalSuppressMessage a justification aimed at whoever reads the source; rewording it is not an API change

To keep one of them, name it in included_attributes:

publicapisharp.included_attributes = System.Reflection.AssemblyVersionAttribute

An explicit exclusion always wins over an explicit inclusion, and neither can bring back an attribute whose own type is not visible outside the assembly.

Keeping up with C#

The surface is rendered from Roslyn's own symbol model rather than from compiled metadata, so a language feature the host compiler understands renders correctly without a change here — required, scoped, params spans, static abstract interface members, ref-struct constraints, and whatever comes next. That is the whole reason this reads the compilation instead of the built assembly.

The package ships a slot per Roslyn line and the SDK loads the highest one your compiler supports:

Slot Roslyn Host
roslyn4.8 4.8 .NET 8 SDK / VS 17.8 (C# 12) and .NET 9
roslyn4.14 4.14 .NET 10 SDK / VS 17.14 (C# 14)
roslyn5.3 5.3 .NET 11 line (C# 15)
roslyn5.6 5.6 current Roslyn

A construct can only appear in source the host compiler can parse, so a project that builds on the 4.8 floor cannot contain C# 15 syntax and renders identically on every slot. That is what keeps a baseline stable across a mixed developer/CI SDK estate.

The output uses a file-scoped namespace when the assembly exposes a single namespace, which is the common case; C# permits only one file-scoped namespace per file, so an assembly with two or more falls back to the block form.

Known limits. C# 14 extension blocks round-trip from roslyn5.3 onward; on roslyn4.14 the symbol model exposes them but the parser cannot read the syntax back, so they are left out of the baseline rather than written in a form that could not be re-read. Roslyn 5.6 parses union declarations but exposes no public API for them yet, so unions are recorded as ordinary types until it does.

Repository layout

Project Purpose
src/PublicApiSharp.Analyzers rendering, baseline parsing, and the analyzer itself
src/PublicApiSharp.Analyzers.CodeFixes the fix that writes the baseline
src/PublicApiSharp.Analyzers.Package NuGet packaging and the consumer-side MSBuild targets
src/Shared/Analyzers helpers shared by both assemblies
src/tests/* one TUnit project per Roslyn slot, over a single shared set of sources
src/benchmarks/* BenchmarkDotNet harness for the render and parse paths

Data flows in one direction: rendering turns the compilation's visible symbols into nested C#, the parser reads that text back into declarations, and the analyzer compares. The same parser runs over the freshly rendered surface and over the checked-in baseline, so the two sides of the comparison cannot drift apart through two different notions of what a declaration is.

Building and testing

Run these from src/:

dotnet build PublicApiSharp.Analyzers.slnx -c Release
dotnet test  --solution PublicApiSharp.Analyzers.slnx -c Release

The analyzer and code fix are built once per Roslyn slot, and each slot is a separate project instance with its own obj/. Restoring the solution only covers the default one, so a fresh clone needs each slot restored explicitly before the first build:

dotnet restore PublicApiSharp.Analyzers.slnx
for slot in roslyn4.8 roslyn4.14 roslyn5.3 roslyn5.6; do
  dotnet restore PublicApiSharp.Analyzers/PublicApiSharp.Analyzers.csproj -p:RoslynVersion=$slot
  dotnet restore PublicApiSharp.Analyzers.CodeFixes/PublicApiSharp.Analyzers.CodeFixes.csproj -p:RoslynVersion=$slot
done

Every slot is the gate, not just the floor: the source compiled into each one differs, so a green roslyn4.8 build proves little on its own. dotnet test --solution runs the whole suite against all four, which is what catches a construct that only one compiler can express.

To pack every slot into a single NuGet package:

dotnet pack PublicApiSharp.Analyzers.Package/PublicApiSharp.Analyzers.Packages.csproj -c Release

Contributing

Issues and pull requests are welcome.

Adding a rule means updating all of:

  • a descriptor in Rules/PublicApiRules.cs
  • the analyzer change, and the code fix if the rule is fixable
  • tests, added as a .cs file under src/tests/PublicApiSharp.Analyzers.Tests.Shared so every slot runs them
  • docs/rules/PAS####.md
  • a row in AnalyzerReleases.Unshipped.md

Two invariants are worth not breaking. Additions and changes are reported from a symbol action, because a diagnostic raised by a compilation action is not local to a document and Roslyn will not offer a code fix for one. And the renderer's output must parse back — anything emitted that C# cannot re-read is a defect here rather than in the consumer's code, and the comparison abandons itself rather than blaming them for it.

License

MIT. See LICENSE.

There are no supported framework assets in this package.

Learn more about Target Frameworks and .NET Standard.

This package has no dependencies.

NuGet packages

This package is not used by any NuGet packages.

GitHub repositories

This package is not used by any popular GitHub repositories.

Version Downloads Last Updated
2.1.0 309 9/25/2026
2.0.1 3,234 9/16/2026
2.0.0 908 9/14/2026
1.0.8 481 9/12/2026
1.0.7 3,655 8/3/2026
1.0.6 113 8/3/2026
1.0.5 149 8/2/2026
1.0.4 114 8/2/2026
1.0.3 112 8/2/2026
1.0.2 123 8/2/2026
1.0.1 1,414 7/31/2026
1.0.0 113 7/31/2026