Blazor CSS Isolation and the PackageId

45 Views Asked by At

I am not actually detailing a problem, I am detailing a potential solution for anyone who may be interested. Firstly, I have established that to use CSS Isolation in a Blazor server application you need a stylesheet adding to your _Host or _Layout file that follows a very specific naming convention. The main advice I have found suggests that you use the Project Name. Assuming my project was called Test, I would add something like:

<!DOCTYPE html>
<html lang="en">
<head>
   <link rel="stylesheet" href="Test.styles.css" />
<head>
<body>
</body>
</html>

It turns out this isn't true or certainly isn't true for .NET 7. It is the PackageId that should be used and not the project name. I am sure there are many articles on the subject but I didn't find any which is why I am posting this. If my PackageId was TestPackageId then I would actually need to include the following:

<!DOCTYPE html>
<html lang="en">
<head>
   <link rel="stylesheet" href="TestPackageId.styles.css" />
<head>
<body>
</body>
</html>

The issue I have with anything like this is that it can easily be missed if you ever change the PackageId or project name (had it have been that) and so I wanted a way in which I could grab that information during compile time and inject it into my code.

First I started by looking to see if it already exists but I could not find anything on the subject. If anyone is able to link a better more formal way to get this information then I would be grateful. In the meantime, anyone like me will be able to use the following solution. It may actually be useful for other details stored in the project file as you will see.

Firstly, somewhere in the project file will be the following line. If you don't see it, this won't work:

<Project Sdk="Microsoft.NET.Sdk.Web">
  ...
  <PropertyGroup>
    ...
    <PackageId>TestPackageId</PackageId>
    ...
  </PropertyGroup>
  ...
</Project>

Somewhere before the end of your project file you need to add the following. This piece of code will leverage the WriteCodeFragment action that is built into MSBuild. Note the directory and file name - I have highlighted them with asterix (you wouldn't normally include ** in your names) - these can be changed as required:

<Project Sdk="Microsoft.NET.Sdk.Web">
  ...
  <PropertyGroup>
    ...
    <PackageId>TestPackageId</PackageId>
    ...
  </PropertyGroup>
  ...
  <Target Name="WritePackageId" BeforeTargets="BeforeBuild">
      <ItemGroup>
        <PackageAttributes Include="AssemblyPackageId">
          <_Parameter1>$(PackageId)</_Parameter1>
        </PackageAttributes>
      </ItemGroup>
      <WriteCodeFragment Language="C#" OutputDirectory="**$(ProjectDir)\Properties**" OutputFile="**PackageId.cs**" ContinueOnError="false" AssemblyAttributes="@(PackageAttributes)" />
  </Target>
</Project>

Each time you build your project this will create a file, in my case, a file called 'PackageId.cs' in the 'Properties' folder within my project. Once my project has been built for the first time, the file will contain something like the following:

//------------------------------------------------------------------------------
// <auto-generated>
//     This code was generated by a tool.
//     Runtime Version:4.0.30319.42000
//
//     Changes to this file may cause incorrect behavior and will be lost if
//     the code is regenerated.
// </auto-generated>
//------------------------------------------------------------------------------

using System;
using System.Reflection;

[assembly: AssemblyPackageId("TestPackageId")]

// Generated by the MSBuild WriteCodeFragment class.

Now that we have this we can take advantage of it and load the assembly attribute in the usual manner. Before we do, we must define the attribute otherwise we will have compile errors. Add the following code to a new file in your project:

namespace System
{
    [AttributeUsage( AttributeTargets.Assembly, Inherited = false )]
    public sealed class AssemblyPackageIdAttribute : Attribute
    {
        public AssemblyPackageIdAttribute( string packageId )
        {
            PackageId = packageId;
        }

        public string PackageId { get; }
    }
}

Note how we are using the namespace System here, we could have used System.Reflection instead as these are the two included in the new file that was written. I have not experimented with further namespaces as I had no need to.

Next I need to grab the content of this attribute programatically so I can embed it in my html. I have simply added this to the Program.cs file:

using System;
using System.Reflection;

namespace Think
{
    public class Program
    {
        private static string __packageId;

        /// <summary>
        /// Main entry point.
        /// </summary>
        public static void Main( string[] args )
        {
            Assembly assembly;

            // Obtain the assembly's details
            assembly = typeof( Program ).Assembly;
            __packageId = assembly.GetCustomAttribute<AssemblyPackageIdAttribute>().PackageId;

            // Create host builder
            ...
        }

        /// <summary>
        /// Gets the PackageId of this assembly.
        /// </summary>
        public static string PackageId
        {
            get
            {
                lock( __packageId )
                {
                    return __packageId;
                }
            }
        }
    }
}

Now I have thread-safe static property available for use where I need it. I can go back to my html and tweak it as follows:

<!DOCTYPE html>
<html lang="en">
<head>
   <link rel="stylesheet" href="@(Program.PackageId).styles.css" />
<head>
<body>
</body>
</html>

As a result of this, it doesn't matter what I configure my PackageId to be inside the project file, it will always be correct thus allowing CSS Isolation to work regardless. I hope this helps anyone else stuck with this kind of problem.

0

There are 0 best solutions below