BrandGhost
The Complete Guide to Creating NuGet Packages in .NET

The Complete Guide to Creating NuGet Packages in .NET

07/01/2026

The Complete Guide to Creating NuGet Packages in .NET

If you've ever referenced a library in a .NET project, you've used a nuget package. It's the standard unit of code sharing across the entire .NET ecosystem -- and for good reason. NuGet packages make it easy to distribute libraries, tools, source generators, and even build infrastructure across teams and the broader open-source community.

But consuming packages is only half the story. Knowing how to create a nuget package gives you the power to publish your own libraries, share internal utilities across projects, and contribute to the .NET ecosystem in a meaningful way. Whether you're building a utility library used by your team, an OSS project for the community, or a private SDK shared across microservices, the workflow is fundamentally the same.

This guide walks through everything you need to know -- from what a nuget package actually is, through creating and versioning one, all the way to automating releases with GitHub Actions and making your packages debuggable with SourceLink. Each section links to a dedicated deep-dive article for when you're ready to go further on any topic.

What Is a NuGet Package?

A nuget package is a versioned ZIP file (.nupkg) containing compiled assemblies, a manifest describing metadata and dependencies, and optional extras like documentation and build scripts. The public registry is nuget.org -- the default source the .NET CLI uses when you run dotnet add package.

For the full breakdown of the internal format and how NuGet resolves dependency graphs, see: What Is a NuGet Package? The .nupkg Format and Registry Explained.

Creating Your First NuGet Package

Creating a nuget package from a modern .NET project is straightforward. SDK-style .csproj files -- the default format since .NET Core -- let you define all package metadata directly in the project file. No separate .nuspec file required.

Here's a minimal .csproj showing the core package properties:

<Project Sdk="Microsoft.NET.Sdk">

  <PropertyGroup>
    <TargetFramework>net8.0</TargetFramework>

    <!-- NuGet package identity -->
    <PackageId>MyCompany.MyLibrary</PackageId>
    <Version>1.0.0</Version>
    <Description>A utility library for common string operations.</Description>
    <Authors>Nick Cosentino</Authors>

    <!-- Discoverability and licensing -->
    <PackageTags>string utilities dotnet csharp</PackageTags>
    <PackageLicenseExpression>MIT</PackageLicenseExpression>
    <RepositoryUrl>https://github.com/mycompany/mylibrary</RepositoryUrl>
  </PropertyGroup>

</Project>

Once your metadata is in place, producing the nuget package takes a single command:

dotnet pack --configuration Release --output ./nupkgs

This creates a .nupkg file in the ./nupkgs directory. From there you can inspect it, test it locally, or push it to a feed.

The process is quick. But there are nuances -- like how PackageId differs from the assembly name, why you shouldn't ship a Debug build to nuget.org, and how to handle a solution with multiple projects where only some should be packaged. The full walkthrough is at How to Create a NuGet Package in C# with dotnet pack.

Package Metadata and Discoverability

Publishing a nuget package without good metadata is like releasing a library without documentation. It exists, but nobody knows what it does or why they'd want it.

The most impactful metadata fields are:

  • PackageId -- the unique identifier on nuget.org. Choose something memorable and namespaced (e.g., MyCompany.FeatureName).
  • Description -- what the package does, in plain language. This appears on nuget.org and in IDE tooltips.
  • PackageTags -- space-separated tags that improve search discovery. Be specific.
  • PackageReadmeFile -- a README that renders directly on the nuget.org package page, visible before anyone installs a thing.
  • PackageIcon -- a logo embedded in the package itself. Makes your nuget package recognizable in search results and in the IDE.
  • PackageProjectUrl -- links to your repository or documentation site.

Good metadata doesn't just help humans -- it signals quality. Well-described, tagged, and documented nuget packages earn more downloads and more trust. Developers evaluate packages quickly. A clear description and a rendered README can be the difference between a star and a scroll-past.

There's also the question of licensing. The PackageLicenseExpression field accepts SPDX identifiers (MIT, Apache-2.0, GPL-3.0-only, etc.). This tells consumers -- and automated tools -- exactly what they're allowed to do with your code.

Deep dive: NuGet Package Metadata Best Practices: README, Icon, and Tags.

Versioning Your NuGet Package

Version numbers matter more than people expect. A wrong bump can break consumers. A forgotten bump means packages don't update. And a confusing versioning scheme erodes trust in your library over time.

NuGet follows SemVer 2.0: MAJOR.MINOR.PATCH[-prerelease][+build].

The rules are intentionally simple:

  • Bump MAJOR for breaking changes.
  • Bump MINOR for new, backward-compatible features.
  • Bump PATCH for backward-compatible bug fixes.
  • Use a prerelease suffix (e.g., -beta.1, -rc.2) for packages not yet considered stable.

In practice, many .NET teams also embed build metadata (+build.123) so CI build numbers are traceable. This is especially useful in automated release pipelines where you want to correlate a nuget package version with a specific build run.

One common pitfall: setting <Version> directly in your .csproj is the simplest approach, but teams doing automated releases often prefer <VersionPrefix> combined with <VersionSuffix> so the CI pipeline can inject the prerelease tag dynamically without editing the project file.

Another thing to think about: how you communicate breaking changes to your consumers. SemVer handles the signal, but clear changelogs and deprecation notices via [Obsolete] attributes handle the transition. A nuget package that versions predictably is one that teams adopt with confidence.

Full versioning guide: NuGet Versioning with SemVer in .NET.

Publishing to NuGet.org

Publishing a nuget package to nuget.org takes two things: a .nupkg file and an API key. The dotnet nuget push command handles the rest.

dotnet nuget push ./nupkgs/MyCompany.MyLibrary.1.0.0.nupkg 
  --api-key $NUGET_API_KEY 
  --source https://api.nuget.org/v3/index.json

A few things worth knowing. First, you need a nuget.org account and a scoped API key -- generate one in your account settings and restrict it to specific package IDs or prefixes. Second, package IDs are globally unique on nuget.org, so MyCompany.MyLibrary is yours once you publish it. Third, nuget packages cannot be deleted from nuget.org -- only unlisted, meaning the package still exists and can be downloaded by anyone who knows the exact version. Plan your versioning accordingly.

For the first publish of a nuget package, you can also reserve your package ID prefix (MyCompany.*) on nuget.org. This gives your packages a verified checkmark and prevents namespace squatting by other publishers. It's worth doing early.

Full publishing walkthrough: How to Publish a NuGet Package to NuGet.org.

Private NuGet Feeds for Internal Libraries

Not every nuget package belongs on nuget.org. Internal utilities, proprietary SDKs, and pre-production libraries should live on a private feed -- accessible to your team but not the public.

The two most popular options in the .NET ecosystem are:

  • Azure Artifacts -- part of Azure DevOps. Supports NuGet, npm, Maven, and more. Integrates tightly with Azure Pipelines and role-based access control.
  • GitHub Packages -- GitHub's built-in package registry. Works seamlessly with GitHub Actions and existing repository permissions.

Both options support the standard NuGet protocol. That means dotnet nuget push and dotnet add package work exactly the same way -- you just point them at a different source URL. The developer experience for consumers changes very little.

Private nuget feeds also solve an important architectural problem: sharing packages across microservices or teams within a larger organization. This matters especially when you're building plugin-based architectures. If you distribute plugin contracts as a nuget package, every team consuming your plugin system gets a consistent, versioned interface. The plugin loading patterns described in Plugin Architecture in C#: The Complete Guide to Extensible .NET Applications depend on exactly this kind of shared, versioned contract distribution.

Full guide: Private NuGet Feeds with Azure Artifacts and GitHub Packages.

Supporting Multiple .NET Versions

The .NET ecosystem runs on several active releases simultaneously -- LTS releases like .NET 8 and .NET 10, the current release, and .NET Standard for cross-platform targeting. A well-maintained nuget package often needs to support several of these at once.

Multi-targeting in SDK-style projects is clean. Replace <TargetFramework> with <TargetFrameworks>:

<TargetFrameworks>net8.0;net10.0;netstandard2.0</TargetFrameworks>

With this change, dotnet pack produces a single .nupkg containing separate lib folders for each target framework. Consumers get the right assembly automatically based on what their project targets. One package to publish, multiple frameworks served.

Multi-targeting does add complexity. You may need #if preprocessor directives to handle API availability differences between frameworks. Testing gets more involved -- you should run your test suite against each target to catch framework-specific regressions. And some APIs available in net8.0 simply don't exist in netstandard2.0, which means you need conditional implementations.

The key question to answer before multi-targeting: who actually needs this nuget package? A tool targeting internal Azure services probably doesn't need netstandard2.0. A general-purpose utility library consumed across both legacy .NET Framework apps and modern .NET 8 services almost certainly does.

Full guide: Multi-Targeting Your NuGet Package for .NET 6, .NET 8, and .NET Standard.

Automating NuGet Releases with GitHub Actions

Manually running dotnet pack and dotnet nuget push from your laptop is fine for a first release. It's a liability for every release after that. Human steps introduce human errors -- wrong branch, stale build, forgotten version bump.

GitHub Actions makes automated nuget package releases reliable and repeatable. Here's the core workflow pattern:

name: Publish NuGet Package

on:
  push:
    tags:
      - 'v*'

jobs:
  publish:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4

      - name: Setup .NET
        uses: actions/setup-dotnet@v4
        with:
          dotnet-version: '8.0.x'

      - name: Restore dependencies
        run: dotnet restore

      - name: Build
        run: dotnet build --configuration Release --no-restore

      - name: Pack
        run: dotnet pack --configuration Release --no-build --output ./nupkgs

      - name: Push
        run: |
          dotnet nuget push ./nupkgs/*.nupkg 
            --api-key ${{ secrets.NUGET_API_KEY }} 
            --source https://api.nuget.org/v3/index.json 
            --skip-duplicate

This workflow triggers on version tags (e.g., v1.2.0). It sets up .NET 8 using actions/setup-dotnet@v4, packs the project, then pushes the nuget package to nuget.org using a secret API key stored in GitHub repository secrets -- never in code.

The --skip-duplicate flag is important. Without it, re-running the workflow for the same tag fails noisily if the version was already published. With it, the push step succeeds gracefully.

There's more to consider for production-grade release automation: tag-based versioning via MinVer or Nerdbank.GitVersioning, separate workflows for prerelease channels, handling multi-project solutions, and generating changelogs automatically. But this pattern is the foundation that everything else builds on.

Full automation guide: Automating NuGet Publishing with GitHub Actions.

Testing Your Package Before Publishing

One of the most common mistakes in nuget package development is testing the source project directly rather than testing the actual package. These are not the same thing -- and the differences matter.

The package that consumers install has a specific structure. It contains compiled assemblies, not source files. Dependencies get resolved from the NuGet dependency graph, not from local project references. File paths work differently. A library that works perfectly in source form can fail in surprising ways once it's installed from a feed.

Before publishing any nuget package to nuget.org or a private feed, run through this checklist:

  1. Pack locally -- run dotnet pack --configuration Release and inspect the .nupkg contents. Unzip it. Verify the right assemblies are present and no build artifacts snuck in.
  2. Add a local feed -- configure a local directory as a NuGet source: dotnet nuget add source ./nupkgs --name local. This lets you test against the packed artifact without publishing anywhere.
  3. Test against the packed version -- create a test consumer project that references your package via the local feed, not via a <ProjectReference>. This is the actual install experience your consumers will have.
  4. Validate the public API surface -- confirm that public types, namespaces, and method signatures are exactly what you documented.

This discipline is especially important when shipping packages that do something unusual at build time. Source generators, for example, are distributed as nuget packages but execute during the consumer's compilation -- which introduces a category of failure modes that don't exist in standard library packages. The tradeoffs between reflection-based runtime approaches and compile-time source generators are covered in C# Reflection vs Source Generators in .NET 10: Which Should You Choose?.

Another real-world consideration: if your nuget package registers services via IServiceCollection extension methods -- a common pattern for library authors -- make sure those registrations work correctly in the consumer's DI container. The techniques explored in Scrutor in C# -- 3 Simple Tips to Level Up Dependency Injection show how well-designed library packages expose clean DI integration without forcing opinions on consumers.

Full testing guide: How to Test and Debug Your NuGet Package Locally.

When someone installs your nuget package and hits a bug, they want to step into your code in the debugger. Without SourceLink and symbol packages, that's not possible -- they're stuck staring at decompiled IL with no context.

SourceLink is a standard that embeds source control metadata into the compiled assembly. When a debugger requests source for a method in your package, it fetches the exact file from your GitHub repository at the precise commit that built the package. No manual configuration for the consumer. It just works.

Enabling SourceLink for a GitHub-hosted project is mostly a matter of adding one package reference:

<PackageReference Include="Microsoft.SourceLink.GitHub" Version="8.0.0" PrivateAssets="all" />

Note: Check nuget.org for the latest version and replace 8.0.0 with the current release before adding this reference.

Then add these properties to your PropertyGroup:

<!-- Embed source control info and make sources available for debugging -->
<PublishRepositoryUrl>true</PublishRepositoryUrl>
<EmbedUntrackedSources>true</EmbedUntrackedSources>
<DebugType>embedded</DebugType>

Symbol packages (.snupkg) are the complementary piece. They're pushed alongside your main nuget package and contain the .pdb files that debuggers use. NuGet.org hosts a symbol server -- once you push a .snupkg, any consumer with the NuGet symbol server configured can step through your library in their debugger as if they had the source locally.

Together, SourceLink and symbol packages transform your nuget package from a black box into something developers can trust, debug, and contribute to. This matters a lot for libraries used in production. It's also a quality signal -- packages with source debugging support tend to be better maintained overall.

The Plugin Loading in .NET: AssemblyLoadContext with Dependency Injection article is a good example of the kind of runtime complexity that benefits enormously from having SourceLink configured -- when something goes wrong in dynamic assembly loading, being able to step into the package code rather than guessing from stack traces saves hours.

Full guide: NuGet SourceLink and Symbol Packages in .NET.

Frequently Asked Questions

What is the difference between PackageId and AssemblyName in a nuget package?

PackageId is the identifier used on NuGet feeds -- it's what you type in dotnet add package MyCompany.MyLibrary. AssemblyName is the name of the compiled .dll file inside the package. They're often the same, but they don't have to be. A single nuget package can contain multiple assemblies. Set PackageId explicitly in your .csproj to control exactly what consumers and nuget.org see.

Can I include multiple projects in a single nuget package?

Yes, but it's uncommon and usually a sign of scope creep. The standard approach is one project per nuget package. If you genuinely need to bundle multiple assemblies, you can add project references and mark them with <PrivateAssets>all</PrivateAssets> to fold them into the package. Before going that route, ask whether splitting into multiple packages would serve consumers better.

How do I handle breaking changes in my nuget package?

Follow SemVer strictly: bump the MAJOR version for any breaking API change. Maintain a changelog so consumers understand what changed and why. If you need to rename or remove a public type, consider providing a compatibility shim via [Obsolete] first, giving consumers time to migrate before you remove it in the next major release. Breaking changes in minor or patch versions erode trust quickly -- once you've done it, consumers start pinning versions defensively.

What should I put in a .nuspec file versus the .csproj?

For modern SDK-style projects, put everything in the .csproj using the Package* properties. The .nuspec file is the older approach and is only needed for non-SDK projects or advanced scenarios like complex file mappings or content that dotnet pack can't express directly. If you're starting a new nuget package today, stay in .csproj -- it's simpler and version-controlled alongside your code.

Do I need to sign my nuget package?

Package signing is optional but recommended for high-trust scenarios. Signing proves the package came from a specific publisher and hasn't been tampered with since it was signed. NuGet.org supports both author signing (with a certificate you provide) and repository signing (applied automatically by nuget.org at publish time). For most OSS packages, the repository signing nuget.org applies automatically is sufficient for consumer confidence.

How does dotnet pack handle transitive dependencies?

dotnet pack records direct PackageReference dependencies in the package's dependency metadata. Transitive dependencies are resolved by NuGet during installation in the consumer's project -- they are NOT bundled into your .nupkg. This keeps packages lean and avoids version conflicts. If you want a dependency bundled as a private implementation detail (invisible to consumers), add <PrivateAssets>all</PrivateAssets> to the PackageReference.

Can I preview a nuget package before publishing it to nuget.org?

Yes -- and you should, every time. Run dotnet pack, add the output folder as a local source with dotnet nuget add source ./nupkgs --name local-test, then reference the package by ID in a test consumer project. This gives you the full package install experience without touching nuget.org. Always do this before publishing a major release. It catches metadata mistakes, missing assets, and dependency resolution surprises before they reach real consumers.

Wrapping Up

The nuget package ecosystem is one of .NET's greatest strengths. From tiny utility libraries to complex multi-target SDKs distributed across global teams, everything travels the same well-worn path: project metadata, dotnet pack, a versioned .nupkg, and a push to a feed.

Here's a quick recap of the full journey covered in this guide:

  • Understand the format -- a nuget package is a versioned ZIP containing assemblies and metadata.
  • Set metadata properly -- PackageId, Version, Description, Authors, PackageLicenseExpression, and RepositoryUrl are the minimum viable set.
  • Version with intent -- SemVer 2.0 isn't bureaucracy. It's communication between you and your consumers.
  • Test the package, not just the project -- consume it from a local feed before publishing to catch packaging mistakes early.
  • Automate releases -- dotnet nuget push belongs in GitHub Actions, not on your local machine.
  • Make it debuggable -- SourceLink and symbol packages take minutes to configure and save your consumers hours.

Each section in this guide has a dedicated spoke article with the full implementation walkthrough. Whether this is your first nuget package or you're hardening an established library for production use, the depth is there when you're ready for it.

Dependency Injection with Autofac: Not as Difficult as You Think

Looking to get started using dependency injection with Autofac in your projects? Here's a quick primer on what it is and how to get going for your next project.

Automatic Module Discovery With Autofac - Simplified Registration

If you're familiar with Autofac and module registration but want to make things easier, automatic module discovery might be for you! Let's see how it works!

GitHub Copilot SDK Installation and Project Setup in C#: Step-by-Step Guide

Set up the GitHub Copilot SDK in C# with this step-by-step guide covering NuGet package install, GitHub Copilot CLI authentication, and project configuration.

An error has occurred. This application may no longer respond until reloaded. Reload