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 change | When | Consumer impact |
|---|---|---|
| Major | Any breaking change to a public API or behaviour | Requires a code change |
| Minor | New functionality, backwards compatible | Safe to take |
| Patch | Bug fixes only | Safe to take |
| Prerelease suffix | Nightly or preview builds | Opt-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.
ContinuousIntegrationBuildmakes 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=allon 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.
Related
NuGet and dependency management Project types, target frameworks and multi-targeting
Last refreshed 2026-09-18.