How to Create a NuGet Package in C# with dotnet pack
You have a useful C# class library. It solves a real problem. Now you want to share it -- either with your team, your organization, or the broader .NET community. That is exactly what NuGet packages are built for.
Learning how to create a NuGet package in C# is one of those skills that pays dividends repeatedly. Once you understand the workflow, packaging a library takes minutes. The dotnet pack command does the heavy lifting. Your .csproj file carries the metadata. The .nupkg output is ready to share.
This guide walks through the entire process from a bare class library to a properly configured, packable NuGet project. By the end, you will know how to configure your project properties, run dotnet pack, inspect the output, and automate package generation on every build.
If you are brand new to the concept and want to understand what a NuGet package actually is under the hood, The Complete Guide to Creating NuGet Packages in .NET is a great place to start before diving in here.
Prerequisites
Before you can create a NuGet package in C#, you need two things in place.
An SDK-style .csproj file. Any project created with .NET Core 1.0 or later uses SDK-style project files by default. If you created your library with dotnet new classlib, you are already there. The old, verbose packages.config-style projects from .NET Framework days require migration before dotnet pack works reliably.
The .NET SDK (version 5.0 or later). The dotnet pack command ships with the SDK. Verify you have it by running:
dotnet --version
You should see something like 8.0.x or 9.0.x. If you are targeting .NET Framework, the modern dotnet pack workflow still applies -- you just use netstandard2.0 or net48 in your TargetFramework property.
That is it. No external tooling. No separate packaging utility. The SDK handles everything needed to create a NuGet package from your existing project.
Configuring Your .csproj for NuGet Packaging
This is where most of the work happens. NuGet package metadata lives directly in your .csproj file using PropertyGroup elements. When you run dotnet pack, the SDK reads these properties and builds the .nuspec manifest and the final .nupkg automatically.
Here is a fully configured example:
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<!-- Target framework(s) for your library -->
<TargetFramework>net8.0</TargetFramework>
<!-- Package identity -->
<PackageId>Contoso.Utilities</PackageId>
<Version>1.0.0</Version>
<Authors>Jane Smith</Authors>
<Company>Contoso</Company>
<!-- Package discoverability -->
<Description>A collection of utility classes for Contoso applications.</Description>
<PackageTags>utilities;helpers;contoso</PackageTags>
<PackageProjectUrl>https://github.com/contoso/Contoso.Utilities</PackageProjectUrl>
<!-- License -->
<PackageLicenseExpression>MIT</PackageLicenseExpression>
<!-- Output location -->
<PackageOutputPath>./artifacts</PackageOutputPath>
<!-- Explicitly mark this project as packable -->
<IsPackable>true</IsPackable>
</PropertyGroup>
</Project>
Key Package Properties Explained
A few of these properties deserve extra attention.
PackageId -- The unique identifier for your NuGet package on any feed. It must be globally unique if you publish to NuGet.org. If you do not set it, the value defaults to the assembly name, which in turn defaults to the project file name. Set it explicitly. Relying on the default is a maintenance hazard when you rename the project later.
Version -- Defaults to 1.0.0 if not set. Set it explicitly. The version you control is the version consumers see. The version you set here becomes the version string embedded in the .nupkg filename (e.g., Contoso.Utilities.1.0.0.nupkg).
Authors -- A semicolon-separated list. Something like Jane Smith;Bob Jones is the canonical format. The .nuspec manifest uses the same separator, so no conversion is needed.
Description -- A plain-text summary of what the package does. This appears in the NuGet package browser, Visual Studio's package manager, and JetBrains Rider. Keep it concise -- one to three sentences is the sweet spot.
PackageTags -- Semicolon-separated keywords that help people discover your package on NuGet.org. Less critical for private packages, but worth including from day one.
PackageOutputPath -- Controls where the .nupkg file lands after dotnet pack succeeds. Leave it out and the .nupkg lands in bin/{Configuration}/ -- for example, bin/Release/. Compiled assemblies land one level deeper in bin/{Configuration}/{TargetFramework}/. Setting ./artifacts or ./nupkg keeps things tidy and makes CI artifacts easier to find.
IsPackable -- Defaults to true for class library projects. Set it to false on test projects, console apps, and anything you never intend to package. This prevents accidental packaging of non-library projects when you run dotnet pack at the solution level.
Running dotnet pack
Once your .csproj is configured, creating a NuGet package is a single command:
dotnet pack MyProject.csproj -c Release -o ./artifacts
Breaking this down:
MyProject.csproj-- the project to pack. You can omit this if you run the command from inside the project directory.-c Release(short for--configuration Release) -- builds in Release mode, which enables compiler optimizations and strips debug information from the assembly.-o ./artifacts(short for--output ./artifacts) -- overridesPackageOutputPathfrom the.csprojat the command line. Useful when you want a consistent output directory across multiple projects without editing every.csproj.
Running the command produces output similar to:
Build succeeded.
1 Warning(s)
0 Error(s)
Successfully created package '/path/to/artifacts/Contoso.Utilities.1.0.0.nupkg'.
That .nupkg file is your distributable NuGet package. It is ready to upload to a feed or install locally.
If you have already built the project separately -- common in CI pipelines with a dedicated build step -- skip the compilation with the --no-build flag:
dotnet pack MyProject.csproj -c Release -o ./artifacts --no-build
The --no-build flag tells dotnet pack to use the binaries already in the output folder rather than compiling again. This avoids building twice and ensures the packaged assembly matches exactly what you built and tested.
What's Inside the Generated .nupkg
A .nupkg file is a ZIP archive. Rename it to .zip and open it in any archive tool to see the contents directly. Understanding the structure removes a lot of the mystery around what dotnet pack actually produces.
A typical .nupkg contains:
Contoso.Utilities.1.0.0.nupkg
├── [Content_Types].xml # OOXML content type declarations
├── _rels/
│ └── .rels # Relationship map for the package parts
├── Contoso.Utilities.nuspec # Package manifest (auto-generated from .csproj)
└── lib/
└── net8.0/
├── Contoso.Utilities.dll # Compiled assembly
└── Contoso.Utilities.xml # XML documentation (if GenerateDocumentationFile=true)
The .nuspec file is the heart of the package. It is an XML manifest describing the package identity, version, authors, description, and dependencies. You rarely need to touch it manually -- the SDK generates it directly from your .csproj properties.
The lib/ folder holds the compiled assemblies organized by target framework moniker (TFM). When a consumer installs your NuGet package, NuGet selects the lib subfolder that best matches their project's target framework. A package targeting net8.0 installs cleanly into any .NET 8 project.
For a deeper look at the full .nupkg format and how the NuGet registry uses it, What Is a NuGet Package? covers the internal structure in detail.
Excluding Files and Content
By default, dotnet pack includes only the compiled assembly and its XML documentation file under lib/. Source files, test data, build scripts, and other project artifacts are excluded automatically.
Sometimes you need explicit control. The .csproj provides several mechanisms to refine what ends up in your package.
Excluding a project from packaging entirely -- set <IsPackable>false</IsPackable>. This is especially important for test projects. Run dotnet pack at the solution level and any project with this flag set is silently skipped, no matter what.
Excluding specific files -- use Pack="false" on individual <Content> or <None> items:
<ItemGroup>
<!-- Keep this file in the project but exclude it from the package -->
<Content Include="sample-data.json" Pack="false" />
</ItemGroup>
Including additional files -- use the PackagePath attribute to place extra files into specific locations inside the .nupkg. This is commonly used to bundle native binaries, MSBuild .targets or .props files, or content that consumers need at build time rather than runtime.
For most straightforward class libraries, the defaults work well. You only need these mechanisms when your NuGet package has non-standard content requirements beyond a compiled assembly.
Generating Packages on Build Automatically
If you want to create a NuGet package every time you build -- without running dotnet pack manually -- add GeneratePackageOnBuild to your project properties:
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net8.0</TargetFramework>
<PackageId>Contoso.Utilities</PackageId>
<Version>1.0.0</Version>
<Authors>Jane Smith</Authors>
<Description>A collection of utility classes for Contoso applications.</Description>
<PackageOutputPath>./artifacts</PackageOutputPath>
<!-- Automatically run dotnet pack at the end of every build -->
<GeneratePackageOnBuild>true</GeneratePackageOnBuild>
</PropertyGroup>
</Project>
With this flag set, every dotnet build and dotnet publish automatically invokes the full packaging step afterward. The .nupkg lands in PackageOutputPath (or bin/{Configuration}/ if that property is not set).
This is convenient during active development when you want an up-to-date package available after every build without running a second command. It does add a small overhead to every build, though. Many teams keep GeneratePackageOnBuild disabled for local development and rely on explicit dotnet pack calls in their CI pipeline -- where they have more control over when packages are produced, versioned, and published.
Building reusable .NET libraries connects naturally to larger architectural patterns. If you are building libraries intended to be loaded as plugins at runtime, Plugin Architecture in C#: The Complete Guide to Extensible .NET Applications covers how to structure those systems end to end. Packages that expose extension points through interfaces also benefit from thinking carefully about contract design -- Plugin Contracts and Interfaces in C#: Designing Extensible Plugin Systems explores that in depth.
Next Steps
You have created a NuGet package. The .nupkg file exists on disk. Here is where to go from here.
Polish the metadata. The properties configured so far produce a valid, functional package. A well-presented package also includes a README file, an icon, a license URL, and descriptive tags. Richer metadata makes a real difference for discoverability and first impressions. NuGet Package Metadata Best Practices covers exactly how to do this well.
Test locally before publishing. Before pushing to any feed, verify the package installs correctly and behaves as expected in a real consuming project. Testing Your Package Locally walks through the local NuGet feed workflow so you can catch problems before they reach anyone else.
Explore source generators as a distribution strategy. If your library needs to generate code at compile time rather than ship runtime assemblies, source generators change the packaging equation considerably. C# Reflection vs Source Generators in .NET 10: Which Should You Choose? helps you evaluate whether that approach fits your use case.
Simplify DI registration for your consumers. Packages that integrate with the .NET dependency injection system benefit from clean, discoverable registration patterns. Automatic Dependency Injection in C#: The Complete Guide to Needlr shows how to make DI registration seamless for library consumers.
Frequently Asked Questions
What is the difference between dotnet pack and dotnet build?
dotnet build compiles your project into a .dll assembly and places it in the output directory. dotnet pack does everything dotnet build does, and then additionally bundles the compiled output into a .nupkg file along with a generated .nuspec manifest. You can run them separately -- using --no-build with dotnet pack tells it to skip compilation -- or let dotnet pack handle both steps in one command.
Do I need to set PackageId explicitly?
No, but you should. If PackageId is not set, it defaults to the assembly name, which defaults to the project filename. If you ever rename the project, the package identity changes -- and consumers who installed the old package name will not receive updates automatically. Setting PackageId explicitly decouples the package identity from the project filename and gives you full control.
What does the Version property default to if I do not set it?
It defaults to 1.0.0. That is fine for an initial package, but version management becomes important the moment consumers start depending on your library. Set the version explicitly from the start and build the habit of updating it intentionally. Every meaningful change to your public API should be reflected in an updated version number.
Can I create a NuGet package from a test project?
Technically yes, but you almost certainly should not. Set <IsPackable>false</IsPackable> on any test project to prevent it from being packaged accidentally -- especially important when running dotnet pack at the solution level. NuGet packages should contain reusable library code, not test infrastructure or test dependencies.
What does the --no-build flag do in dotnet pack?
It tells dotnet pack to skip the compilation step entirely and use whatever binaries already exist in the output directory. This is common in CI pipelines where a dedicated build step runs first and the packaging step should not re-compile from scratch. Using --no-build is faster and ensures the packaged assembly matches exactly what was built and tested earlier in the pipeline.
Where does dotnet pack output the .nupkg file by default?
Without a PackageOutputPath setting or -o flag, the .nupkg lands in bin/{Configuration}/ -- for example, bin/Release/. Setting PackageOutputPath in your .csproj or passing -o on the command line moves the output to a predictable, consistent location that is easier to reference in downstream steps.
What is GeneratePackageOnBuild and when should I use it?
GeneratePackageOnBuild is a boolean property that, when set to true, runs the full dotnet pack step automatically after every dotnet build. It is convenient during active development when you want an up-to-date .nupkg available without running an extra command. For production workflows, explicit dotnet pack calls in CI are generally preferred because they give you deliberate control over when packages are produced, versioned, and published to a feed.
Wrapping Up
Creating a NuGet package in C# with dotnet pack is a straightforward process once you understand the moving parts. Configure your package metadata directly in the .csproj, run dotnet pack with your preferred flags, and the .nupkg is ready to share.
The key properties to get right from the start are PackageId, Version, Authors, and Description. Setting PackageOutputPath keeps your artifacts organized. Adding IsPackable=false to test projects prevents accidental packaging headaches at the solution level. The --no-build flag is your friend in CI when you want to separate compilation from packaging.
From here, the natural progression is to refine your package metadata, test the package locally, and then publish it to a NuGet feed. The Complete Guide to Creating NuGet Packages in .NET maps out the full journey if you want to see where every piece fits together.

