PackageId defaults to AssemblyName, which defaults to the project file name when not explicitly set in any .NET SDK project, including those using Microsoft.NET.Sdk.Razor. This derivation chain is consistent across all .NET SDK types, with no special overrides in the Razor SDK despite its critical use of PackageId for static asset paths.
The property evaluation follows a clear precedence chain where explicit user definitions take priority, followed by conditional defaults set in the SDK's MSBuild targets files. Understanding this chain is essential for .NET developers creating NuGet packages or Razor Class Libraries, as the PackageId directly determines package identifiers and static content paths in _content/{PACKAGE ID}/ directories.
The PackageId property evaluation follows a two-step conditional default chain defined in separate MSBuild target files. First, AssemblyName is conditionally set in Microsoft.NET.Sdk.props (imported at project load), then PackageId is conditionally set in NuGet.Build.Tasks.Pack.targets (imported during pack operations).
The exact MSBuild code shows this clearly:
<!-- Step 1: In Microsoft.NET.Sdk.props -->
<AssemblyName Condition=" '$(AssemblyName)' == '' ">$(MSBuildProjectName)</AssemblyName>
<!-- Step 2: In NuGet.Build.Tasks.Pack.targets -->
<PackageId Condition=" '$(PackageId)' == '' ">$(AssemblyName)</PackageId>The MSBuildProjectName is a built-in MSBuild property representing the project filename without its .csproj extension. For example, if your project file is MyLibrary.csproj, then $(MSBuildProjectName) equals MyLibrary. This becomes the ultimate fallback value for both AssemblyName and PackageId when neither is explicitly defined.
Complete precedence order:
<PackageId> in the .csproj file (highest priority)$(AssemblyName) value (whether explicit or defaulted)$(MSBuildProjectName) (project filename without extension)A common misconception is that RootNamespace influences PackageId derivation. This is incorrect. RootNamespace and AssemblyName are independent, parallel properties that both happen to derive from MSBuildProjectName by default, but through separate evaluation paths.
The SDK defines RootNamespace separately:
<RootNamespace Condition=" '$(RootNamespace)' == '' ">$(MSBuildProjectName.Replace(" ", "_"))</RootNamespace>Note that RootNamespace has a subtle difference—it replaces spaces with underscores in the project name, while AssemblyName preserves spaces. This means for a project named My Library.csproj with no explicit properties set:
My LibraryMy_LibraryMy Library (derived from AssemblyName, not RootNamespace)RootNamespace serves different purposes: it sets the default namespace for new code files in IDEs, affects resource manifest name generation, and influences localization resource lookups. But it has no role in PackageId derivation.
The project filename is the ultimate fallback source for PackageId, but it flows through AssemblyName as an intermediary. This means changing your project filename will change your PackageId only if AssemblyName is not explicitly set.
Example 1: Default behavior
<!-- File: ProductCatalog.csproj -->
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net8.0</TargetFramework>
</PropertyGroup>
</Project>Property evaluation results:
ProductCatalogProductCatalog (defaulted)ProductCatalog (defaulted)ProductCatalog (defaulted from AssemblyName)Example 2: Explicit AssemblyName breaks the filename link
<!-- File: ProductCatalog.csproj -->
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net8.0</TargetFramework>
<AssemblyName>Contoso.Products.Catalog</AssemblyName>
</PropertyGroup>
</Project>Property evaluation results:
ProductCatalogContoso.Products.Catalog (explicitly set)ProductCatalog (still defaults to project name)Contoso.Products.Catalog (derived from the explicit AssemblyName)This demonstrates that setting AssemblyName creates a custom PackageId without needing to set PackageId explicitly. The project filename becomes irrelevant for PackageId once AssemblyName is explicitly defined.
The Microsoft.NET.Sdk.Razor SDK does not override or modify the standard PackageId derivation logic—it inherits all behavior from the base Microsoft.NET.Sdk. This was confirmed by examining the Razor SDK source code, which shows no custom PackageId property definitions in its target files.
However, PackageId has special significance for Razor Class Libraries because it determines the static web asset path prefix. Static files (CSS, JavaScript, images) in a Razor Class Library's wwwroot folder are served at the path _content/{PACKAGE ID}/.
According to the official ASP.NET Core documentation: "For example, a library with an assembly name of Razor.Class.Lib and without a <PackageId> specified in its project file results in a path to static content at _content/Razor.Class.Lib/."
This creates a critical consideration: changing AssemblyName or PackageId will break static asset references in consuming applications unless they update their paths accordingly.
Example: Razor Class Library with custom assembly name
<!-- File: MyComponents.csproj -->
<Project Sdk="Microsoft.NET.Sdk.Razor">
<PropertyGroup>
<TargetFramework>net8.0</TargetFramework>
<AssemblyName>Contoso.UI.Components</AssemblyName>
</PropertyGroup>
</Project>Results:
Contoso.UI.Components (from AssemblyName)_content/Contoso.UI.Components/Contoso.UI.Components.styles.cssThe Razor SDK uses PackageId in multiple places, including scoped CSS bundle names and static web asset base paths, but it follows the standard derivation logic to determine what that PackageId value is.
Understanding when these properties are set helps explain their precedence. The .NET SDK uses a specific import order:
Microsoft.NET.Sdk.props runs first, setting conditional defaults for AssemblyName and RootNamespaceSdk.targets imports NuGet.Build.Tasks.Pack.targets, which sets the PackageId defaultThis timing means:
Directory.Build.props) take precedence over SDK defaultsTo verify what PackageId your project will use without creating a package, use MSBuild's diagnostic output:
# Method 1: Preprocess to see all expanded properties
dotnet msbuild -preprocess:expanded.xml
# Then search expanded.xml for PackageId definitions
# Method 2: Use MSBuild property functions
dotnet build -p:PackageId=$(AssemblyName) -v:detailed
# Method 3: Examine pack output
dotnet pack --no-build
# The generated .nupkg filename shows the effective PackageIdFor Razor Class Libraries specifically, check the generated scoped CSS bundle name in obj/Debug/net8.0/scopedcss/bundle/ to see what PackageId was used during build.
Spaces in project names: Projects with spaces like My Library.csproj generate AssemblyName = My Library (with space) and PackageId = My Library, which may be valid but unconventional for NuGet package identifiers. Consider setting an explicit PackageId without spaces.
Command-line casing override: GitHub issue #6056 identified that building with a lowercase project filename (e.g., dotnet build myproject.csproj) can override the casing in AssemblyName, potentially affecting PackageId. Use consistent casing in project references.
Multi-targeting with conditional AssemblyName: Setting different AssemblyName values per target framework can cause build failures. If needed, set PackageId explicitly rather than relying on conditional AssemblyName values.
Empty RootNamespace intentionally: Setting <RootNamespace></RootNamespace> to explicitly empty doesn't work as expected in some IDE scenarios—Visual Studio may fall back to AssemblyName. This doesn't affect PackageId but can confuse namespace generation.
The authoritative implementations exist in these repository files:
NuGet.Client/src/NuGet.Core/NuGet.Build.Tasks.Pack/NuGet.Build.Tasks.Pack.targetsdotnet/sdk/src/Tasks/Microsoft.NET.Build.Tasks/targets/Microsoft.NET.Sdk.propsdotnet/sdk/src/Tasks/Microsoft.NET.Build.Tasks/sdk/Sdk.targetsdotnet/sdk/src/StaticWebAssetsSdk/Targets/Microsoft.NET.Sdk.StaticWebAssets.ScopedCss.targetsThe MSBuild PackageId derivation follows a straightforward, well-documented pattern: PackageId defaults to AssemblyName, which defaults to the project filename. This behavior is identical across all .NET SDK project types, including Microsoft.NET.Sdk.Razor, which inherits the standard logic without modification.
The key insight is that RootNamespace is not part of this chain—it's an independent property. The project filename influences PackageId only indirectly through AssemblyName, and setting AssemblyName explicitly breaks that connection. For Razor Class Libraries, this derivation is particularly important because PackageId determines static asset paths, making it a breaking change to modify after initial development.
Developers should explicitly set PackageId in the .csproj file when they need it to differ from AssemblyName, especially for libraries that will be consumed as NuGet packages or Razor Class Libraries with static assets.