Writing and publishing NuGet libraries

Package metadata, symbol packages, semantic versioning and how to validate a package before anyone depends on it.

Metadata that consumers see

<Project Sdk="Microsoft.NET.Sdk">
  <PropertyGroup>
    <TargetFrameworks>net8.0;netstandard2.0</TargetFrameworks>
    <GenerateDocumentationFile>true</GenerateDocumentationFile>
    <Nullable>enable</Nullable>
  </PropertyGroup>

  <PropertyGroup>
    <PackageId>Example.Orders</PackageId>
    <Version>2.3.1</Version>
    <Authors>Platform Team</Authors>
    <Company>Example Ltd</Company>
    <Description>Order modelling and validation for Example services.</Description>
    <PackageTags>orders;validation;example</PackageTags>
    <PackageLicenseExpression>MIT</PackageLicenseExpression>
    <PackageReadmeFile>README.md</PackageReadmeFile>
    <PackageProjectUrl>https://github.com/example/orders</PackageProjectUrl>
    <RepositoryUrl>https://github.com/example/orders.git</RepositoryUrl>
    <RepositoryType>git</RepositoryType>
    <PublishRepositoryUrl>true</PublishRepositoryUrl>
    <EmbedUntrackedSources>true</EmbedUntrackedSources>
    <IncludeSymbols>true</IncludeSymbols>
    <SymbolPackageFormat>snupkg</SymbolPackageFormat>
    <ContinuousIntegrationBuild Condition="'$(CI)' == 'true'">true</ContinuousIntegrationBuild>
  </PropertyGroup>

  <ItemGroup>
    <None Include="README.md" Pack="true" PackagePath="\" />
  </ItemGroup>
</Project>
Version changeWhenConsumer impact
MajorAny breaking change to a public API or behaviourRequires a code change
MinorNew functionality, backwards compatibleSafe to take
PatchBug fixes onlySafe to take
Prerelease suffixNightly or preview buildsOpt-in only, never resolved by default
  • Source link plus an embedded repository URL lets a consumer step into your source from a stack trace, which turns a bug report into a pull request.
  • ContinuousIntegrationBuild makes paths in the PDB deterministic, which is what makes Source Link resolve correctly.
  • A symbol package is a separate upload and is not put in the global package cache; consumers fetch it on demand.
  • Mark the assembly with [assembly: InternalsVisibleTo("Example.Orders.Tests")] rather than making types public just to test them.

Packing, validating and releasing

# Build once and pack from the build output
dotnet pack -c Release -o ./artifacts

# Inspect what actually went into the package before pushing
unzip -l ./artifacts/Example.Orders.2.3.1.nupkg

# Verify against a local folder, not the world
dotnet nuget push ./artifacts/Example.Orders.2.3.1.nupkg \
  --source ./local-feed --api-key ignored

# Then a smoke test that consumes the package from the local feed
mkdir -p /tmp/consumer && cd /tmp/consumer
dotnet new console -o . >/dev/null
dotnet add package Example.Orders --source ./local-feed
dotnet build
# Validate a package automatically on every pull request
- name: Pack and check
  run: |
    dotnet pack -c Release -o ./artifacts
    # A package without a readme or with a placeholder version is a defect
    dotnet tool run dotnet-validate --package ./artifacts/*.nupkg
  • Push to a local feed and consume the package in a throwaway project before publishing. This catches a missing file, a wrong package path and an invalid dependency in seconds.
  • Never publish a version that has already been published, even after deleting it. Some feeds cache the version forever and consumers will get the old content.
  • Keep a prerelease channel for a real pipeline: nightly builds as 2.3.2-nightly.20260918, stable releases as clean versions.
  • Delete the API key from the machine after a manual push, and use a scoped, revocable key in CI rather than an account-wide one.
  • A package with PrivateAssets=all on an analyser or build tool does not flow to consumers, which is what you want for anything only needed at build time.
⚠️
Once a version is published it is effectively permanent: it may be cached in CI, in a global packages folder and in consumers' lock files. Treat every push as irrevocable and never rely on unlisting to undo a bad release.

FAQ

Should a library reference other packages?
Yes when it needs them, but keep the count low and the ranges tight. Every dependency you add becomes a version conflict in someone else's application.
What is the difference between unlisting and deleting a package?
Unlisting hides it from search but leaves it restorable for anyone who has already referenced it. Deletion is restricted and does not reach caches that already hold the package.

NuGet and dependency management Project types, target frameworks and multi-targeting

Last refreshed 2026-09-18.