Package Layout and Multi-Targeting

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.

A consumer never opens a package; NuGet reads its folders and decides which files the consuming project compiles against, copies to its output, imports into its build or runs as analyzers. Choosing the right folder is therefore part of a library’s design. This page lists the conventions, packs Ledgerline for three target frameworks and consumes it from net8.0 and net48, and shows native, localized and typed packages, with .NET SDK 10.0.402 (NuGet 7.9.0). P/Invoke itself is covered in Native Interop.

The folder conventions

Folder Meaning to a consumer

lib/<tfm>/

Assemblies used at compile time and copied at run time; .xml documentation beside them.

ref/<tfm>/

Compile-time-only reference assemblies; when present, lib/ (or runtimes/) supplies the run-time ones.

runtimes/<rid>/lib/<tfm>/

RID-specific managed assemblies, used at run time only.

runtimes/<rid>/native/

Native libraries (.so, .dylib, .dll) for a RID.

build/<tfm>/, build/

<PackageId>.props and <PackageId>.targets imported into the project that references the package.

buildMultiTargeting/

The same, imported once into the outer build of a multi-targeted project (NuGet 4.0+).

buildTransitive/

The same, imported into every project that gets the package transitively (NuGet 5.0+).

contentFiles/<lang>/<tfm>/

Files added to the consuming project as content or compile items (PackageReference).

content/

The legacy equivalent for packages.config, copied into the project once.

analyzers/dotnet/cs/

Roslyn analyzers and source generators.

tools/

.NET tool payloads; for packages.config, scripts on the Package Manager Console’s PATH.

The parts of a .nupkg: the .nuspec manifest, lib, ref and runtimes folders, build, contentFiles, analyzers and tools folders, metadata files, OPC packaging files and an optional signature

Multi-targeted packages

A project with TargetFrameworks gets one lib/<tfm>/ folder and one dependency group per framework, each listing only the dependencies of that build. Ledgerline targets net10.0;net8.0;netstandard2.0, with two polyfill dependencies only on netstandard2.0 (Designing a Library for NuGet).

When a consumer’s framework is not in the package, NuGet picks the nearest compatible one: the same framework family at the highest version not above the consumer’s, then .NET Standard, then nothing (NU1202). Two consumers, restored from a folder feed holding the packed Ledgerline.1.0.0.nupkg:

<!-- App.net80/App.net80.csproj; App.net48 is the same with net48 -->
<PropertyGroup>
  <OutputType>Exe</OutputType>
  <TargetFramework>net8.0</TargetFramework>
</PropertyGroup>
<ItemGroup>
  <PackageReference Include="Ledgerline" Version="1.0.0" />
</ItemGroup>

The compile asset chosen for each, read from obj/project.assets.json, and the packages each got:

net8.0 ['lib/net8.0/Ledgerline.dll'] []
net48 ['lib/netstandard2.0/Ledgerline.dll'] ['Microsoft.Bcl.AsyncInterfaces/10.0.13', 'Microsoft.Bcl.HashCode/6.0.0', 'System.Text.Json/10.0.13', 'System.Text.Encodings.Web/10.0.13']

net8.0 got its exact folder and no extra packages; net48, with no net4x folder in the package, fell back to netstandard2.0 and pulled the polyfills and their own dependencies. The net48 project built on Linux because the SDK adds Microsoft.NETFramework.ReferenceAssemblies (1.0.3 here) implicitly when the .NET Framework targeting pack is not installed; it compiles but can only run on Windows or Mono. Adding a net462 target to the package would give .NET Framework consumers their own assembly, as the library guidance suggests.

Mismatched folders and groups: NU5128 and NU5131

A package’s lib//ref/ folders and its dependency groups should list the same frameworks; otherwise a consumer may match a group but not a folder, or the reverse. A hand-placed ref/net8.0/C.dll in a net10.0 project gave:

warning NU5128: Some target frameworks declared in the dependencies group of the nuspec and the lib/ref folder do not have exact matches in the other location. Consult the list of actions below:
warning NU5128: - Add a dependency group for net8.0 to the nuspec
warning NU5131: - Add C.dll to the net8.0 reference group in the nuspec

NU5131 concerns the <references> groups that limit which lib/ assemblies are compile-time references.

Source: NU5128, NU5131.

Duplicate frameworks and NU5051

Since NuGet 7.6 / SDK 10.0.300 a project may list several TargetFrameworks aliases that resolve to the same framework — for example builds for two host applications. A package can contain only one lib/net10.0/, so pack refuses to choose:

<PropertyGroup>
  <TargetFrameworks>apple;banana</TargetFrameworks>
</PropertyGroup>
<PropertyGroup Condition="'$(TargetFramework)' != ''">
  <TargetFrameworkIdentifier>.NETCoreApp</TargetFrameworkIdentifier>
  <TargetFrameworkVersion>v10.0</TargetFrameworkVersion>
  <TargetFrameworkMoniker>.NETCoreApp,Version=v10.0</TargetFrameworkMoniker>
</PropertyGroup>
error NU5051: This project is using aliased frameworks, the following TargetFramework properties map to the same effective frameworks 'apple, banana => net10.0' and pack is unable to disambiguate and create a single package. Ambiguous package dependencies. The following target frameworks are both declaring package dependencies for packing: apple, banana

The NU5051 page’s sample sets the three properties unconditionally; on 10.0.402 that failed earlier, in evaluation (MSB4184: The expression "[MSBuild]::VersionGreaterThanOrEquals('', 8.0)" cannot be evaluated), until the group was limited to the inner builds with the '$(TargetFramework)' != '' condition shown. The fix the page gives: set IncludeBuildOutput=false and SuppressDependenciesWhenPacking=true on all but one alias per framework.

Native and RID-specific packages

A wrapper around a native library ships its managed assembly in lib/ and the native binary per RID:

<ItemGroup>
  <None Include="native/libledgerhash.so" Pack="true" PackagePath="runtimes/linux-x64/native/" />
  <None Include="native/ledgerhash.dll" Pack="true" PackagePath="runtimes/win-x64/native/" />
</ItemGroup>

A net10.0 application built without a RID copied the whole runtimes/ tree into its output, for the host to pick at run time; built with -r linux-x64, it got only the Linux file, next to the assemblies:

$ find bin/Debug/net10.0 -path '*runtimes*' -type f
./runtimes/linux-x64/native/libledgerhash.so
./runtimes/win-x64/native/ledgerhash.dll
$ ls bin/Debug/net10.0/linux-x64 | grep -i ledger
Ledgerline.Native.dll
libledgerhash.so

(The "native" files here are placeholders; the point is where they land.) The native-files guide recommends putting the managed assembly in ref/<tfm>/ and runtimes/any/lib/<tfm>/ rather than lib/, because a lib/ folder makes NuGet believe that packages.config projects are supported — and those never copy native assets. .NET Framework projects need a separate .targets-based approach, described on the same page.

Localized resources

Satellite resource assemblies go either in the main package, under lib/<tfm>/<culture>/<Id>.resources.dll, or in satellite packages named <Id>.<culture> (for example Ledgerline.de) that depend on the exact primary version ([1.0.0]) and contain only the <culture> folder. The SDK packs satellite assemblies built from .resx files into the main package automatically: a project with Strings.resx and Strings.de.resx produced lib/net10.0/Loc.dll and lib/net10.0/de/Loc.resources.dll.

Package types

PackageType tells clients what a package is for:

Type Use

Dependency

The default: a library. Leave PackageType unset.

DotnetTool

A .NET tool, installed with dotnet tool install or run with dnx (Tool and Template Packages).

Template

dotnet new templates.

MSBuildSdk

An MSBuild project SDK, referenced as Sdk="Name/Version".

McpServer

A Model Context Protocol server; always together with DotnetTool, run with dnx, ideally with an embedded .mcp/server.json describing its inputs.

custom

Any other name, for an application’s own extension packages; Visual Studio and nuget.exe cannot install them.

Several types are separated by ;. Package types were added in NuGet 3.5.

Common mistakes

  • Only netstandard2.0 in lib/ — every modern consumer gets the fallback, with its polyfill dependencies.

  • A dependency group that doesn’t match a lib/ folder — NU5128; consumers on that framework may get assemblies without their dependencies.

  • Native binaries in lib/ — they are referenced as managed assemblies and fail to load.

  • .props or .targets not named after the package ID — they are never imported (NU5129, see MSBuild Props, Targets, Tasks and Analyzers in Packages).