How Package Installation Works

This section documents MSBuild 18.9, the .NET SDK 10.0.402 (with its dotnet CLI) and NuGet 7.9, as published in the MSBuild, .NET and NuGet documentation on learn.microsoft.com, which is the reference these pages are written and verified against. Material about .NET 11 / NuGet 8.0 is still in preview and is always flagged as such — never presented as baseline.

This content was generated with the assistance of AI and should be verified against the official documentation before being relied on in production.

The C# Reference bibliography lists the reference material consulted while preparing these pages.

"Installing" a package in a PackageReference project means two things: recording a PackageReference in the project file, and restoring — downloading the package once per machine into a shared folder and recording, in obj/project.assets.json, which of its files the project uses. Nothing is copied into the project folder. This page follows one package, Humanizer.Core 2.14.1, through that process with NuGet 7.9.0 (.NET SDK 10.0.402).

What a .nupkg is

A .nupkg is a ZIP file:

unzip -l ~/.nuget/packages/humanizer.core/2.14.1/humanizer.core.2.14.1.nupkg
Humanizer.Core.nuspec
logo.png
[Content_Types].xml
_rels/.rels
lib/net6.0/Humanizer.dll
lib/net6.0/Humanizer.xml
lib/netstandard1.0/Humanizer.dll
lib/netstandard1.0/Humanizer.xml
lib/netstandard2.0/Humanizer.dll
lib/netstandard2.0/Humanizer.xml
package/services/metadata/core-properties/575b1b6c57e04bb3a2b576d5ac444577.psmdcp
.signature.p7s
  • Humanizer.Core.nuspec — the manifest: ID, version, dependencies per framework, metadata.

  • lib/<tfm>/ — assemblies per target framework. Other conventional folders are ref/, runtimes/<rid>/, build/, buildTransitive/, contentFiles/, analyzers/, tools/.

  • [Content_Types].xml, _rels/, *.psmdcp — Open Packaging Conventions parts.

  • .signature.p7s — the repository signature nuget.org adds to every package (Signed Packages and Trust Policies).

Building packages is covered in the Publishing and Distributing Libraries group.

Acquiring the package

For each package and version the graph needs, NuGet looks in this order:

Restore looks in the global-packages folder, then local sources, then the HTTP cache, then HTTP sources, and writes the global-packages entry and the obj files

The order is visible at normal verbosity. A project with <PackageReference Include="Humanizer.Core" Version="2.14.1" /> was restored three times, with NUGET_PACKAGES and NUGET_HTTP_CACHE_PATH pointing at empty folders:

dotnet restore -v n

First restore (both folders empty):

  GET https://api.nuget.org/v3-flatcontainer/humanizer.core/index.json
  OK https://api.nuget.org/v3-flatcontainer/humanizer.core/index.json 49ms
  GET https://api.nuget.org/v3-flatcontainer/humanizer.core/2.14.1/humanizer.core.2.14.1.nupkg
  OK https://api.nuget.org/v3-flatcontainer/humanizer.core/2.14.1/humanizer.core.2.14.1.nupkg 66ms
Installed Humanizer.Core 2.14.1 from https://api.nuget.org/v3/index.json to ~/inst-gp/humanizer.core/2.14.1 with content hash lQKvtaTDOXnoVJ20ibTuSIOf2i0uO0MPbDhd1jm238I+U/2ZnRENj0cktKZhtchBMtCUSRQ5v4xBCUbKNmyVMw==.
  GET https://api.nuget.org/v3/vulnerabilities/index.json
  …
Restored ~/inst/Inst.csproj (in 1.61 sec).

Second restore, after deleting only the global-packages folder:

  CACHE https://api.nuget.org/v3-flatcontainer/humanizer.core/index.json
  CACHE https://api.nuget.org/v3-flatcontainer/humanizer.core/2.14.1/humanizer.core.2.14.1.nupkg
Installed Humanizer.Core 2.14.1 from https://api.nuget.org/v3/index.json to ~/inst-gp/humanizer.core/2.14.1 with content hash lQKvtaTDOXnoVJ20ibTuSIOf2i0uO0MPbDhd1jm238I+U/2ZnRENj0cktKZhtchBMtCUSRQ5v4xBCUbKNmyVMw==.
Restored ~/inst/Inst.csproj (in 756 ms).

Third restore, with both folders filled: no package request at all, only the audit data from the cache, 356 ms.

  • GET/OK lines are network requests; CACHE lines are served from the HTTP cache, whose entries count as fresh for 30 minutes. --no-http-cache bypasses it.

  • A package already in the global-packages folder is used without contacting any source — for an exact version. A floating version or a range without a lower bound makes NuGet ask every source for its versions first.

  • Local folder and file-share sources are checked before HTTP sources.

  • Package source mapping decides which sources may serve which package IDs before any of this happens (Package Sources and Source Mapping).

  • The audit requests (vulnerabilities/index.json) come from NuGetAudit (Auditing Packages for Vulnerabilities).

The global-packages entry

~/.nuget/packages/humanizer.core/2.14.1/
  .nupkg.metadata
  .signature.p7s
  humanizer.core.2.14.1.nupkg
  humanizer.core.2.14.1.nupkg.sha512
  humanizer.core.nuspec
  lib/net6.0/Humanizer.dll  lib/net6.0/Humanizer.xml
  lib/netstandard1.0/…      lib/netstandard2.0/…
  logo.png

The folder and file names are the lower-cased ID and the normalized version. .nupkg.metadata records where the package came from and its content hash:

{
  "version": 2,
  "contentHash": "lQKvtaTDOXnoVJ20ibTuSIOf2i0uO0MPbDhd1jm238I+U/2ZnRENj0cktKZhtchBMtCUSRQ5v4xBCUbKNmyVMw==",
  "source": "https://api.nuget.org/v3/index.json"
}

The content hash is what lock files store and compare (Lock Files and Locked Mode). The global-packages folder is append-only: once a version is there, restores trust it, so never edit files inside it.

project.assets.json and asset selection

Restore writes obj/project.assets.json with, among others, a libraries section (every package in the graph, with its path and file list) and a targets section (for each target framework and RID, which of those files the project uses):

"targets": {
  "net10.0": {
    "Humanizer.Core/2.14.1": {
      "type": "package",
      "compile": { "lib/net6.0/Humanizer.dll": { "related": ".xml" } },
      "runtime": { "lib/net6.0/Humanizer.dll": { "related": ".xml" } }
    }
  }
}

For a net10.0 project NuGet chose lib/net6.0 — the nearest compatible framework in the package — over netstandard2.0. The asset groups are:

Group Package folder Used for

compile

ref/<tfm>/ or lib/<tfm>/

what the compiler references

runtime

lib/<tfm>/

what is copied to the output and loaded

native / runtimeTargets

runtimes/<rid>/native/, runtimes/<rid>/lib/<tfm>/

platform-specific files, chosen by RID at publish or run time

build

build/, buildTransitive/

.props/.targets imported into the project

contentFiles

contentFiles/<lang>/<tfm>/

source or content files added to the project

analyzers

analyzers/dotnet/<lang>/

Roslyn analyzers and source generators

A package with native code shows the other groups. SQLitePCLRaw.lib.e_sqlite3 2.1.4, from the NETSDK1206 example on Troubleshooting SDK Errors:

"SQLitePCLRaw.lib.e_sqlite3/2.1.4": {
  "type": "package",
  "compile": { "lib/netstandard2.0/_._": {} },
  "runtime": { "lib/netstandard2.0/_._": {} },
  "build": { "buildTransitive/net7.0/SQLitePCLRaw.lib.e_sqlite3.targets": {} },
  "runtimeTargets": {
    "runtimes/alpine-arm/native/libe_sqlite3.so": { "assetType": "native", "rid": "alpine-arm" },
    "runtimes/alpine-arm64/native/libe_sqlite3.so": { "assetType": "native", "rid": "alpine-arm64" },
    "runtimes/alpine-x64/native/libe_sqlite3.so": { "assetType": "native", "rid": "alpine-x64" },
    …
  }
}

_._ is an empty placeholder meaning "compatible, but nothing to reference". Which groups a project actually takes is controlled by IncludeAssets/ExcludeAssets/PrivateAssets (PackageReference); the build then reads the assets file in ResolvePackageAssets (How a .NET Build Works).

Finding and evaluating packages

Before adding a dependency, check it on nuget.org or with dotnet package search:

  • Search syntax — <property>:<term> searches one field: id: (substring), packageid: and owner: (exact), tags:, author:, description:, title:, summary:, version: (owner:dotnet tags:json).

  • Supported frameworks — the package page’s "Frameworks" tab lists the frameworks it has assets for (net6.0, netstandard2.0 for Humanizer.Core 2.14.1), and whether net10.0 is computed as compatible.

  • Prerelease — nuget.org lists them by default; dotnet package search and dotnet package add skip them unless --prerelease is given.

  • Evaluate — downloads and recent versions, the owner (and whether the ID prefix is reserved), the source repository, the licence, vulnerabilities and deprecation shown on the page.

  • Licence — the license expression in the .nuspec (Apache-2.0, MIT) or a licence file inside the package.

Common mistakes

  • Expecting package files in the project folder — they are in the global-packages folder; the assets file points there.

  • Editing or deleting single files in the global-packages folder — delete the whole version folder, or clear it.

  • Debugging a "stale" package without knowing about the HTTP cache — --no-http-cache or dotnet nuget locals http-cache --clear.

  • Assuming netstandard2.0 assets are used on .NET 10 — the nearest framework (here net6.0) wins.