Every .NET shop eventually writes a helper library that gets copy-pasted across three or four repos before someone says "this should just be a NuGet package." We hit that point a while back with a set of internal validation and HTTP retry helpers, and packaging them up turned out to be far less painful than the team expected. Here is the walkthrough we now hand to anyone shipping their first package.
Setting Up the Class Library
Start with a plain Class Library project targeting the widest reasonable TFM — net6.0 or netstandard2.0 if you need older consumers to reference it too. Everything NuGet needs to know about your package lives right in the .csproj as MSBuild properties, no separate manifest file required unless you want fine-grained control over content files.
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net6.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
</PropertyGroup>
<PropertyGroup>
<PackageId>XtremeThoughts.Common.Http</PackageId>
<Version>1.0.0</Version>
<Authors>Xtreme Thoughts Softtech</Authors>
<Company>Xtreme Thoughts Softtech</Company>
<Description>Retry-aware HttpClient helpers and Polly policies shared across our .NET services.</Description>
<PackageTags>http;retry;polly;resilience</PackageTags>
<RepositoryUrl>https://github.com/xtremethoughts/common-http</RepositoryUrl>
<PackageLicenseExpression>MIT</PackageLicenseExpression>
<GenerateDocumentationFile>true</GenerateDocumentationFile>
</PropertyGroup>
</Project>
Packing It
From Visual Studio, right-click the project and choose Pack — it drops a .nupkg into bin/Debug or bin/Release depending on your configuration. From the CLI, which is what our build pipeline actually runs, dotnet pack does the same thing and is easier to script consistently.
dotnet pack --configuration Release --output ./nupkgs
dotnet nuget push ./nupkgs/XtremeThoughts.Common.Http.1.0.0.nupkg \
--api-key %NUGET_API_KEY% \
--source https://api.nuget.org/v3/index.json
The API key comes from your nuget.org account under API Keys — scope it to a specific package pattern rather than "all packages" so a leaked key does not put every package in your account at risk. Once pushed, indexing on nuget.org usually takes a few minutes before the package shows up in search, though direct restore by exact version works almost immediately.
Versioning Discipline
NuGet does not enforce semantic versioning, but consumers assume you follow it, so treat MAJOR.MINOR.PATCH as a contract. Bump PATCH for bug fixes that do not change any public signature, MINOR for backwards-compatible additions, and MAJOR the moment you rename or remove a public member, change a method signature, or alter behavior consumers might depend on. Pre-release builds use a suffix like 1.1.0-beta1 and are excluded from restore unless a project explicitly opts into pre-release packages.
- Confirm the TargetFramework actually matches what consumers need — netstandard2.0 for maximum reach, net6.0+ if you use newer BCL features
- Fill in Description, Authors, and PackageTags — an empty package page looks abandoned even on day one
- Enable GenerateDocumentationFile so IntelliSense shows your XML doc comments to consumers
- Run dotnet pack locally and inspect the .nupkg contents (it is just a zip) before your first push
- Never reuse a version number — once 1.0.0 is pushed to nuget.org it is immutable; you can only unlist it
If you push the wrong version by mistake, dotnet nuget delete cannot fully remove it — nuget.org only supports unlisting, which hides it from search but does not break existing restores pinned to that version. Get the version number right before you push; there is no clean undo.
Wrapping Up
The mechanics here take twenty minutes the first time and about ninety seconds every time after. What actually takes discipline is the metadata and versioning hygiene — get PackageId, license, and semver right from version 1.0.0 and you save yourself a painful major-version bump later to fix a naming mistake everyone is already depending on.