LegacyLibAnalyzer 0.2.6

dotnet add package LegacyLibAnalyzer --version 0.2.6
                    
NuGet\Install-Package LegacyLibAnalyzer -Version 0.2.6
                    
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="LegacyLibAnalyzer" Version="0.2.6">
  <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="LegacyLibAnalyzer" Version="0.2.6" />
                    
Directory.Packages.props
<PackageReference Include="LegacyLibAnalyzer">
  <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 LegacyLibAnalyzer --version 0.2.6
                    
#r "nuget: LegacyLibAnalyzer, 0.2.6"
                    
#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 LegacyLibAnalyzer@0.2.6
                    
#: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=LegacyLibAnalyzer&version=0.2.6
                    
Install as a Cake Addin
#tool nuget:?package=LegacyLibAnalyzer&version=0.2.6
                    
Install as a Cake Tool

LegacyLibAnalyzer

NuGet

LegacyLibAnalyzer keeps an eye on your NuGet references so you don't have to — it flags legacy or forbidden packages the moment they're referenced, right during the build, and reports them to MSBuild (errors/warnings/notes).

It ships without built-in rules: you declare your rules directly via <LegacyLibraryRule> items in your project, or centrally via your own company package (see below). The analyzer can also write a SARIF report, e.g. for importing into SonarQube.

🕰️ Why this analyzer? Why not just fix the issue? If you maintain multiple repositories, you can't always drop a forbidden package right away — priorities and dependencies get in the way. And sometimes there isn't a replacement (yet).

This analyzer won't fix the problem for you, but it will make sure nobody forgets about it either.

📦 Installation

<ItemGroup>
  <PackageReference Include="LegacyLibAnalyzer" Version="0.*" PrivateAssets="all" />
</ItemGroup>

PrivateAssets="all" is recommended for normal application/library projects. If you're building your own company package (see below), do not set PrivateAssets="all" there, since that would prevent the build integration from reaching consumers of your company package.

⚙️ Defining rules

Rules are expressed as <LegacyLibraryRule> items. Add them to your own .csproj (after the PackageReference to LegacyLibAnalyzer):

<ItemGroup>
  
  <LegacyLibraryRule Include="Moq"
                     ErrorCode="LegacyLib-Moq"
                     Severity="Error"
                     Message="Moq is forbidden here; use NSubstitute." />

  
  <LegacyLibraryRule Include="xUnitV1"
                     ErrorCode="LegacyLib-xUnitV1"
                     IncludePattern="xunit*"
                     ExcludePattern="xunit.v3*"
                     MaxVersion="1"
                     Severity="Error"
                     Message="xUnit v1 is legacy; upgrade to xunit.v3." />
  <LegacyLibraryRule Include="xUnitV2"
                     ErrorCode="LegacyLib-xUnitV2"
                     IncludePattern="xunit*"
                     ExcludePattern="xunit.v3*"
                     MinVersion="2"
                     MaxVersion="2"
                     Severity="Warning"
                     Message="xUnit v2 is legacy; upgrade to xunit.v3." />

  
  <LegacyLibraryRule Include="XunitV3Outdated"
                     ErrorCode="LegacyLib-XunitV3Outdated"
                     IncludePattern="xunit.v3*"
                     MaxVersion="3"
                     Severity="Note"
                     Message="xunit.v3 package is on major version 3.x; consider upgrading to 4.x." />
</ItemGroup>

🚫 This analyzer doesn't ship an opinion on which packages to forbid - it only enforces whatever policy you configure. One common trigger is a licensing change (e.g. FluentAssertions requiring a commercial license for commercial use as of v8). 💡 Always put the reason in the Message so developers understand the "why", not just the "what".

Rule options

Metadata Required Description
Include yes Key of the rule and in general, the package name. Used to merge defaults with overrides (matched by this key). Wildcards are not supported here, if wildcards are needed, also specify IncludePattern
IncludePattern no The value actually matched against resolved package names. Supports * and ? wildcards (e.g. xunit*). Falls back to Include when not specified.
ExcludePattern no A wildcard pattern (*/?) of package names to still exclude, even if they match IncludePattern. Useful when a family of packages was renamed (e.g. xUnit v3 uses different package names than v2).
ErrorCode no The error/warning code (e.g. LegacyLib-Moq). Free-form text, no required format. Defaults to LegacyLib-Forbidden. Used as the SARIF rule id too.
Severity no Error, Warning or Note. Defaults to Warning.
Category no Maintainability, Reliability or Security. Defaults to Maintainability. Reported as a SARIF rule tag so SonarQube can group the issue under the matching category.
Message no The message shown to the user. Defaults to a generic message mentioning the package name. Use this to explain why the package is forbidden, not just that it is.
MinVersion / MaxVersion no Inclusive allowed version range. When the resolved package version falls within [MinVersion, MaxVersion] it is not flagged. 2, 2.0 are matched as 2.0.0.0. If version 2 is not allowed, set both MinVersion and MaxVersion to 2. Pre-release versions are matched by their release version, so 2.2.0-rc1 is treated as 2.2.0.

🏢 Building your own company package

Got more than one repo and a policy everyone should follow? Instead of copy-pasting the same <LegacyLibraryRule> items into every .csproj, bundle them once into a thin company package — for example MyCompany.LegacyLibAnalyzer — that depends on LegacyLibAnalyzer and adds only your own rules. Projects then just reference your company package and get the whole organization's policy for free.

This works because the build integration files live under buildTransitive\ (not build\), which NuGet happily propagates through the entire dependency chain, no matter how many packages deep.

Steps:

  1. Create a new class library project (no source code needed) and reference this package without PrivateAssets="all", so the build integration flows through to consumers:
    <ItemGroup>
      <PackageReference Include="LegacyLibAnalyzer" Version="0.1.0" />
    </ItemGroup>
    
  2. Add your own buildTransitive\<packageId>.targets file with your organization's <LegacyLibraryRule> items:
    <Project>
      <ItemGroup>
        <LegacyLibraryRule Include="Moq" ErrorCode="LegacyLib-Moq" Severity="Error"
                           Message="Moq is forbidden; use NSubstitute instead." />
        <LegacyLibraryRule Include="MyCompany.OldInternalLib" ErrorCode="MyCompany-OldInternalLib"
                           Severity="Error" Message="Use MyCompany.NewInternalLib instead." />
      </ItemGroup>
    </Project>
    
  3. Pack and publish your company package. Consumers reference only your company package.

⚠️ Watch out for NU5129 — don't just suppress it. NuGet only imports a .props/.targets file from build\, buildTransitive\, buildCrossTargeting\ or buildMultiTargeting\ if its filename matches the package id exactly (e.g. buildTransitive\MyCompany.LegacyLibAnalyzer.targets for package id MyCompany.LegacyLibAnalyzer, as used above). Get the name wrong and NuGet silently skips the file — no error at pack time, no error at restore time. The analyzer then simply never runs, and forbidden packages sneak through undetected while everything still "looks" green. So even though NuGet reports it as a warning, treat NU5129 as a build-breaking error and fix it right away — never add it to NoWarn.

In other words: buildTransitive\<file>.targets (and .props) must be named exactly after your package's <PackageId>, with matching casing, e.g. a package id of MyCompany.LegacyLibAnalyzer requires buildTransitive\MyCompany.LegacyLibAnalyzer.targets . See examples/LegacyLibAnalyzerExample1/README.md for a working example, where the package id LegacyLibAnalyzerExample1 is matched by buildTransitive\LegacyLibAnalyzerExample1.targets.

Overruling company-wide rules in an individual project

Because <LegacyLibraryRule> items are merged by their Include key (last one wins, matched case-insensitively), a consumer project can override — or locally soften/tighten — a rule that came from your company package by declaring a <LegacyLibraryRule> with the same Include value, after the PackageReference to the company package:

<ItemGroup>
  <PackageReference Include="MyCompany.LegacyLibAnalyzer" Version="1.0.0" PrivateAssets="all" />

  
  <LegacyLibraryRule Include="Moq" Severity="Warning"
                     Message="Moq is deprecated here; migration to NSubstitute in progress." />

  
</ItemGroup>

See examples/LegacyLibAnalyzerExample1 and examples/SampleLegacyProject in this repository for a complete, working sample of this pattern. Note that LegacyLibAnalyzerExample1 now lives in its own solution, examples/LegacyLibAnalyzerExample1.slnx, and consumes LegacyLibAnalyzer as a NuGet package from the local feed, rather than via a project reference.

🔕 Disabling the check

Skips the analyzer entirely for the project: no rules are evaluated, and no errors, warnings, notes or SARIF output are produced.

<PropertyGroup>
  <LegacyLibAnalyzerEnabled>false</LegacyLibAnalyzerEnabled>
</PropertyGroup>

📄 SARIF output

Enable SARIF output as follows:

<PropertyGroup>
  <LegacyLibGenerateSarif>true</LegacyLibGenerateSarif>
  
  <LegacyLibSarifOutputPath>$(MSBuildProjectDirectory)\legacylib.sarif</LegacyLibSarifOutputPath>
</PropertyGroup>

Add the path to your SonarQube analysis via the sonar.sarifReportPaths property, e.g. in the sonarqube:configure/sonarqube:analyze Azure DevOps task's extra properties, or your sonar-project.properties file:

sonar.sarifReportPaths=legacylib.sarif

The path is relative to the SonarQube analysis base directory (usually your repo root), so adjust it to match where LegacyLibSarifOutputPath actually writes the file for your project. Findings then show up as SonarQube issues, tagged with the Category you configured on the rule (Maintainability, Reliability or Security).

⚠️ sonar.sarifReportPaths does not support wildcards. You must list every SARIF file explicitly, comma-separated, e.g. sonar.sarifReportPaths=legacylib.sarif,other/project/legacylib.sarif. This matters for multi-project repositories: each project writes its own legacylib.sarif (see LegacyLibSarifOutputPath above), so you'll need to add each path individually to your SonarQube configuration — a glob like **/legacylib.sarif will not work.

Example SARIF output

A minimal SARIF v2.1.0 document produced for a single flagged package looks like this:

{
  "version": "2.1.0",
  "runs": [
    {
      "tool": { "driver": { "name": "LegacyLibAnalyzer" } },
      "results": [
        {
          "ruleId": "LegacyLib-Moq",
          "level": "error",
          "message": { "text": "Moq is forbidden here; use NSubstitute. (package 'Moq' 4.20.0)" },
          "locations": [
            {
              "physicalLocation": {
                "artifactLocation": { "uri": "src/SampleProject/SampleProject.csproj" }
              }
            }
          ]
        }
      ]
    }
  ]
}

🗂️ Project structure

  • src/LegacyLibAnalyzer — the package project (MSBuild task + buildTransitive/ integration).
  • tests/LegacyLibAnalyzer.Tests — unit tests for the merging, matching and SARIF logic.
  • examples/LegacyLibAnalyzerExample1 — example company package with a curated set of rules, see examples/LegacyLibAnalyzerExample1/README.md for how it's built and consumed.
  • examples/SampleLegacyProject — sample consumer project (an executable) using the example company package, see examples/SampleLegacyProject/README.md.
  • examples/SampleLegacyClassLibrary — same as SampleLegacyProject but a class library instead of an executable, see examples/SampleLegacyClassLibrary/README.md.

All example projects live in their own solutions (examples/LegacyLibAnalyzerExample1.slnx, examples/SampleLegacyProject.slnx, examples/SampleLegacyClassLibrary.slnx), separate from the main LegacyLibAnalyzer.slnx. LegacyLibAnalyzerExample1 consumes LegacyLibAnalyzer as a NuGet package (not a project reference), and SampleLegacyProject/SampleLegacyClassLibrary in turn depend on the NuGet package produced by packing LegacyLibAnalyzerExample1 (see the linked READMEs for details on building them). CI builds and validates all of these as "systemtests" on both Windows and Linux, and for both the net8.0 and net10.0 buildTransitive task assemblies, explicitly checking for the symptoms from issue #11 (see .github/workflows/build-test-pack.yml and scripts/Validate-LegacyLibWarnings.ps1).

Working with the examples locally

  • The task ships three assemblies: net472 (Visual Studio / msbuild.exe), and net8.0/net10.0 (dotnet build), selected in LegacyLibAnalyzer.targets based on the MSBuild runtime/SDK. Verify both: dotnet build and msbuild examples/SampleLegacyClassLibrary.slnx -t:Restore,Build.
  • NuGet caches packages per version in ~/.nuget/packages. If you change LegacyLibAnalyzer or LegacyLibAnalyzerExample1 without bumping <Version>, consumers keep using the stale cached copy (so you may not see the expected warnings). Always bump the version, or clear the cached copy with dotnet nuget locals all --clear (or delete ~/.nuget/packages/legacylibanalyzer*) and restore again.

🏗️ How it works

  1. On each build, LegacyLibAnalyzer.CheckLegacyLibrariesTask runs (hooked before Build).
  2. The task reads resolved @(PackageReference) items and the merged @(LegacyLibraryRule) items (rules from any company package + consumer overrides).
  3. It matches packages against the configured rules (wildcard patterns, optional exclude pattern, optional MinVersion/MaxVersion allowed range) and reports findings as an MSBuild error, warning or note, depending on the configured severity.
  4. Optionally writes a SARIF report.
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
0.2.6 94 10/17/2026
0.2.5 87 10/17/2026

0.2.6:
- Harden the SARIF JSON writer: limit nesting depth, replace lone surrogates, and escape HTML-sensitive and Unicode line separator characters
0.2.5: package icon
0.2.4:
- Ship a net472 task assembly so Visual Studio / msbuild.exe can load the task (MSB4061)- #31
- Remove dependency on System.Text.Json to avoid runtime conflicts with Visual Studio / msbuild.exe
0.2.3:
- Fix MSB4061/MissingMethodException with TaskHostFactory by also packaging LegacyLibAnalyzer.runtimeconfig.json
alongside the task assembly and its deps.json (the out-of-process task host needs the runtimeconfig.json to resolve the correct runtime/dependencies)
0.2.2:
- Fix MSB4061/MissingMethodException with TaskHostFactory by packaging LegacyLibAnalyzer.deps.json alongside the task assembly
0.2.1:
- Fix out-of-process execution warning v2
0.2:
- Added `Category` option: `Maintainability` (default), `Reliability` or `Security`
- Renamed `Code` to `ErrorCode`, default to `LegacyLib-Forbidden`
- Fix out-of-process execution warning
- .NET 8 support