Search code examples
.netblazorblazor-server-side.net-9.0razor-class-library

Static content and CSS isolation bundles from dynamically added Razor Class Library not working


Using .NET 9 and a Blazor web app (with server interactive rendering mode), I'm struggling to get static assets (or CSS isolation bundles) from a Razor class library to work with my Blazor web app.

I have followed Microsoft's instructions and my .razor file has a .razor.css as a child. In addition I've created a wwwroot with a styles2.css file in it.

The RCL is dynamically loaded through

app.MapRazorComponents<App>().AddAdditionalAssemblies()

The RCL is definitely loaded as a page defined in Components/Pages/MyPage.razor in the RCL, using @page /myurl, is accessible from /myurl.

When running the app I can find neither the bundle nor the CSS file. I've tried various version of the supposed syntax, but none work:

  • _content/MyRclAssemblyName/MyRclAssemblyName.bundle.scp.css
  • _content/MyRclAssemblyName/styles2.css
  • MyRclAssemblyName/MyRclAssemblyName.bundle.scp.css
  • MyRclAssemblyName/styles2.css
  • Probably more but I've forgotten

Adding this to the WebApplicationBuilder has no effect:

builder.WebHost.UseWebRoot("wwwroot");
builder.WebHost.UseStaticWebAssets();

Though this is not really surprising as per the documentation

running the consuming app from build output (dotnet run), static web assets are enabled by default in the Development environment.

My suspicion is that because this is added through .AddAdditionalAssemblies() that this creates some weird scenario where you need additional configuration that is not documented or hidden in a way that I've overlooked it.

How can I use static assets, or CSS bundles, from a Razor class library that is dynamically loaded through .AddAdditionAssembles() and not referenced directly?


Solution

  • What I'm asking for doesn't seem possible for dynamically loaded RCLs according to this issue: https://github.com/dotnet/aspnetcore/issues/33284 It is a few years old, but seems to still hold true: Feel free to add answers if this changes.

    What I ended up doing was marking all content in my wwwroot folder as "Embedded resource", and creating a middleware to serve the content.

    Making files an "Embedded resource" looks like this in the .csproj file:

    <ItemGroup>
      <Content Remove="wwwroot\styles.css" />
      <EmbeddedResource Include="wwwroot\styles.css" />
    </ItemGroup>
    

    This is the middleware (code is shortened for brevity. Add null checks and other validation as needed!):

    public class EmbeddedResourceMiddleware(RequestDelegate next)
    {
        private readonly string _prefix = "_embeddedResource";
    
        public async Task Invoke(HttpContext context)
        {
            if (!context.Request.Path.Value.StartsWith($"/{_prefix}/")))
            {
                await next(context);
                return;
            }
    
            string[] pathSegments = context.Request.Path.Value.Split('/', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries);
    
            string assemblyName = pathSegments[1];
            Assembly assembly = GetYourAssemblyHere(assemblyName);
    
            string filePath = string.Join('/', pathSegments.Skip(2));
            string resourceName = "wwwroot." + filePath.Replace('/', '.');
            string resourceFullName = assembly.GetManifestResourceNames()
                    .FirstOrDefault(name => name.EndsWith(resourceName));
    
            await using Stream stream = assembly.GetManifestResourceStream(resourceFullName);
    
            string contentType = GetContentType(resourceName);
            context.Response.ContentType = contentType;
            await stream.CopyToAsync(context.Response.Body);
    
        }
    
        private static string GetContentType(string fileName)
        {
            var provider = new FileExtensionContentTypeProvider();
        
            if (!provider.TryGetContentType(fileName, out string contentType))
            {
                contentType = "application/octet-stream";
            }
    
            return contentType;
        }
    }
    

    And inserted it into the middleware pipeline. Inserted right after UseStaticFiles() should hopefully allow it to behave just like any other static file:

    WebApplicationBuilder builder = WebApplication.CreateBuilder(args);
    WebApplication app = builder.Build();
    ...
    app.UseStaticFiles()
    app.UseMiddleware<EmbeddedResourceMiddleware>();
    ...
    

    Now I can make a request to /_embeddedResource/MyRclAssemblyName/styles.css and get the stylesheet from the wwwroot folder in my RCL.