I was working on a .NET 10 project called Views.Blazor and I did not understand why my Blazor Assets were packaged as _content/Epsitec.Views.Blazor/... and not just _content/Views.Blazor/....
The documentation explains that for CSS isolation, the CSS files are bundled as {PACKAGE ID/ASSEMBLY NAME}.styles.css. The {PACKAGE ID/ASSEMBLY NAME} is derived from the <PackageId> MSBuild property see CSS Isolation bundling.
Add diagnostics to the .csproj
I added following <Target> to my .csproj file, in order to verify what was going on:
1<Target Name="DisplayPackageId" BeforeTargets="Build">
2 <Message Text="PackageId: $(PackageId)" Importance="high" />
3 <Message Text="AssemblyName: $(AssemblyName)" Importance="high" />
4 <Message Text="RootNamespace: $(RootNamespace)" Importance="high" />
5</Target>
The build output was as expected:
------ Build started: Project: Views.Blazor, Configuration: Debug Any CPU ------
Views.Blazor -> S:\git\solution\ui\Views.Blazor\bin\Debug\net10.0\Views.Blazor.dll
PackageId: Epsitec.Views.Blazor
AssemblyName: Views.Blazor
RootNamespace: Epsitec.Views
The <PackageId> was indeed coherent with the names used for my Blazor Assets. But how did this <PackageId> get this name? The documentation was clear: in the absence of a <PackageId> definition in the project file, the fall-back is $(AssemblyName), which is the same as the project name (i.e. Views.Blazor in my case).
I asked different LLMs. Some explained that this was the result of some smart combination of the root namespace and the assembly name (nice try, but that was plain wrong). Finally, I tried Claude with extended thinking. After 5 minutes, I got back a detailed explanation of how <PackageId> is initialized.
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.
PackageId mystery solved
Everything was pointing at the fact that some Directory.Build.props or Directory.Build.targets was doing some additional magic behind the scenes. But how can I find out what’s going on?
That’s where the -preprocess switch (aka -pp) comes in handy. It produces a full dump of all the MSBuild scripts, bits and pieces which contribute to the build:
1dotnet msbuild Views.Blazor.csproj -pp:evaluated.xml
Digging through the (huge) evaluated.xml file, I finally discovered what was going on. Our toolchain (zou) was executing additional logic:
1<!--
2S:\git\solution\zou\Directory.Build.Default.targets
3============================================================================================================================================
4-->
5 ...
6 <PropertyGroup Condition="'$(IsPackable)' != 'false'">
7 <PackageId Condition="'$(PackageId)' == ''">$(ProjectName)</PackageId>
8 <PackageId Condition="!$(PackageId.StartsWith('$(Company)')) And !$(Company.Contains(' '))">$(Company).$(PackageId)</PackageId>
9 </PropertyGroup>
and that’s where the Epsitec comes from – it’s our company name.