Last time I started walking through the development of some PRISM like plugins for Blazor. In that post, I outlined code to read a configuration and use it to inject plugin assemblies into a Blazor project. I also demonstrated how my extensions inject links for static resources, housed in plugins, without requiring code changes or recompiles. I also demonstrated how my plugins notify Blazor that they need optional router support, at runtime. Finally, I pulled everything together with an extension method that does almost all the work related to adding plugins to Blazor.
At the end of that post I alluded to the fact that I had discovered some additional work that needed to be done. I promised to cover that in this blog post. I’ll do that now.
I used the quick start sample application, that is included with the CG.Blazor
GitHub repository, to drive the development of this code. When I first created that sample solution, I had the main web application project, the plugin project, and the CG.Blazor
project all in the same solution. I had everything linked to the sample using project references. In that configuration, everything worked great! However, as soon as I removed the plugin reference from the sample web application, and referenced the plugin assembly indirectly, using a path (see my previous post if that part doesn’t ring a bell), then I got a 404
error loading the style sheet from the plugin.
After some contemplation and a bit of Googling, I realized I hadn’t done some things that are required, in order to embed resources in a Razor Class Library (RCL) – which is essentially, what my plugin project is. That meant before, when my plugin was linked directly to the sample app, using a project reference, Blazor must have been loading the style sheet using a the same physical file provider it always uses. That’s why it all worked the first few times I tried it. But, later, when I removed the project reference, in Visual Studio, between the sample project and my plugin project, Blazor no longer knew how to find my stylesheet – even though I had injected the link into the head element of the HTML tag (again, something I covered in the previous blog post, if this isn’t familiar to you).
So, I decided, the two things I needed to do were: (1) make sure my style sheets and/or java scripts were embedded resources in my plugin project, and (2) tell Blazor, somehow, that I needed to read those resources from the plugin assembly(s), rather than from the sample application’s wwwroot folder. Here’s how I handled the first part:
The first thing I did was create a wwwroot folder, together with a sample stylesheet and javascript, in my plugin project:
from there, I went into the project file itself and added these (highlighted) elements:
The GenerateEmbeddedFilesManifest
tag tells the compiler to add a manifest containing information about all the embedded resources in the assembly. The EmbeddedResource
tag tells the compiler that anything I add to the wwwroot folder should automatically be considered an embedded resource. That way, I don’t have to remember to manually mark each thing I add to the wwwroot folder as an embedded resource. The next thing I had to do was go add a NUGET reference, to my plugin project, for the Microsoft.Extensions.FileProviders.Embedded package. That package must be directly reference by the project, in order for the compiler to generate the manifest file that I want.
So, now my plugin assembly had the sample stylesheet and javascript embedded into the assembly. I then verified that the embedded resources were there, along with the manifest file, by building the project and looking at the results with a .NET decompiler:
Next, I needed to tell Blazor that I wanted to read some files (but not all, because that would break Blazor) from embedded resources in my plugin assemblies. That way, I wouldn’t have to link my web applications and plugin applications with project references, at compile time. Once again, I contemplated a bit, then Googled, then I decided on this approach:
public static IApplicationBuilder UsePlugins( this IApplicationBuilder applicationBuilder, IWebHostEnvironment webHostEnvironment ) { Guard.Instance().ThrowIfNull(applicationBuilder, nameof(applicationBuilder)); applicationBuilder.UseStaticFiles(); var allProviders = new List<IFileProvider>(); if (webHostEnvironment.WebRootFileProvider != null) { allProviders.Add( webHostEnvironment.WebRootFileProvider ); } var asmNameSet = new HashSet<string>(); BuildStyleSheetProviders( asmNameSet, allProviders ); BuildScriptProviders( asmNameSet, allProviders ); webHostEnvironment.WebRootFileProvider = new CompositeFileProvider( allProviders ); foreach (var module in BlazorResources.Modules) { module.Configure( applicationBuilder, webHostEnvironment ); } BlazorResources.Modules.Clear(); return applicationBuilder; }
UsePlugins
is an extension method off the IApplicationBuilder
type. That sort of implies it should be called from the Startup
class, in the web application. I envision something like this:
public void Configure(IApplicationBuilder app, IWebHostEnvironment env) { app.UsePlugins(env); }
Looking at the code, we see that the method calls the UseStaticFiles
extension method, just to ensure that Blazor is setup to use static files. After that, I added code to check for an existing file provider on the IWebHostEnvironment
object. That file provider is typically a PhysicalFileProvider
– one that is configured to read from the wwwroot folder in the main web application. I still want that to work, but, I also need to trick Blazor into reading from my plugin assemblies, as well. For that, I create a list of file providers and I add the default PhysicalFileProvider
, from Blazor, as the first provider in that collection. Next, I call the BuildStyleSheetProviders
and BuildScriptProviders
methods, which I’ll go into shortly. Next, I replace the default Blazor web root file provider, on the IWebHostEnvironment
object, with a composite file provider containing the Blazor file provider, and any other providers I added along the way.
Finally, I loop through all the module objects that were created and cached as part of the AddPlugins
method (see my previous post), and I call the Configure
method on each one, passing in the application builder and the web host environment object we got as incoming parameters. Once I’ve done that I no longer need the module objects cached, so, I call Clear
on the BlazorResources
.Modules
list.
Now let’s go look at where the other file providers come from:
private static void BuildScriptProviders( HashSet<string> asmNameSet, List<IFileProvider> allProviders ) { foreach (var resource in BlazorResources.Scripts) { var index1 = resource.IndexOf("_content/"); if (index1 == -1) { throw new InvalidOperationException( message: $"It appears the script tag '{resource}' is missing the '_content/' portion of the tag." ); } index1 += "_content/".Length; var index2 = resource.IndexOf("/", index1); if (index2 == -1) { throw new InvalidOperationException( message: $"It appears the script tag '{resource}' is missing a '/' after the assembly name." ); } var asmName = resource.Substring( index1, index2 - index1 ); if (asmNameSet.Contains(asmName)) { continue; } Assembly asm = null; try { asm = Assembly.Load( asmName ); } catch (FileNotFoundException ex) { throw new FileNotFoundException( message: $"It appears the plugin assembly '{asmName}' can't be found. See inner exception for more detail.", innerException: ex ); } try { var fileProvider = new ManifestEmbeddedFileProviderEx( asm, $"wwwroot" ); allProviders.Insert(0, fileProvider); } catch (InvalidOperationException ex) { throw new InvalidOperationException( message: $"It appears the plugin assembly '{asm.GetName().Name}' doesn't have a wwwroot folder, or doesn't have any embedded resources in that folder, or doesn't have an embedded manifest. See inner exception for more detail.", innerException: ex ); } } }
In BuildScriptProviders
, I loop through all the script tags that I previously added to the BlazorResources
class utility (I covered that in my previous blog post). For each tag I find, I do some parsing of the link to isolate the original assembly name. Once I have that assembly name, I check a hash set that is passed into the method, and that allows me to avoid adding a file provider more than once, for the same plugin assembly. Assuming I haven’t already added a provider for this plugin, I then load the assembly so I’ll have a reference to it. Next, I create a ManifestEmbeddedFileProviderEx
instance and add it to the list of file providers I’m in the process of building. I’ll cover the ManifestEmbeddedFileProviderEx
class here shortly.
In BuildStyleSheetProviders,
I follow a similar path, except for style sheets, rather than scripts. Here is the code for that method:
private static void BuildStyleSheetProviders( HashSet<string> asmNameSet, List<IFileProvider> allProviders ) { foreach (var resource in BlazorResources.StyleSheets) { var index1 = resource.IndexOf("_content/"); if (index1 == -1) { throw new InvalidOperationException( message: $"It appears the style sheet link '{resource}' is missing the '_content/' portion of the link." ); } index1 += "_content/".Length; var index2 = resource.IndexOf("/", index1); if (index2 == -1) { throw new InvalidOperationException( message: $"It appears the style sheet link '{resource}' is missing a '/' after the assembly name." ); } var asmName = resource.Substring( index1, index2 - index1 ); if (asmNameSet.Contains(asmName)) { continue; } Assembly asm = null; try { asm = Assembly.Load( asmName ); } catch (FileNotFoundException ex) { throw new FileNotFoundException( message: $"It appears the plugin assembly '{asmName}' can't be found. See inner exception for more detail.", innerException: ex ); } try { var fileProvider = new ManifestEmbeddedFileProviderEx( asm, $"wwwroot" ); allProviders.Insert(0, fileProvider); } catch (InvalidOperationException ex) { throw new InvalidOperationException( message: $"It appears the plugin assembly '{asm.GetName().Name}' doesn't have a wwwroot folder, or doesn't have any embedded resources in that folder, or doesn't have an embedded manifest. See inner exception for more detail.", innerException: ex ); } } }
The idea here, with both of these methods, is to add a ManifestEmbeddedFileProvider
for each plugin assembly that contains either a style sheet or a script tag. That’s where the other file providers come from, we add them there – assuming, of course, there are any style sheets or scripts associated with the plugin, in the Blazor app’s configuration, right?
So let’s regroup. I made sure my plugin assembly(s) has any resources properly embedded, along with an embedded manifest file. Next I added the UsePlugins
method to tell Blazor that I want it to read embedded resources from those assemblies. Everything should work now, right? …
But, it doesn’t. That’s where the ManifestEmbeddedFileProviderEx
class comes in. So, I had to dig around in the ASP.NET internals to figure this out, but, it turns out that, when ASP.NET asks for one of my embedded resources, it uses a path that is different than what the ManifestEmbeddedFileProvider
class expects. That’s not a problem when using the PhysicalFileProvider
that comes with ASP.NET and Blazor, because the path it uses there works just fine with that provider. It’s only with the ManifestEmbeddedFileProvider
that there’s an issue. Once I figured that out, I realized I would need to either write my own ManifestEmbeddedFileProvider
class, or, derive from the existing ManifestEmbeddedFileProvider
class, or wrap ManifestEmbeddedFileProvider
somehow. Well, writing my own wasn’t something I wanted to spend time on. Deriving from the existing ManifestEmbeddedFileProvider
class wasn’t going to work because Microsoft never makes anything in their classes virtual (thanks Microsoft), so, it’s incredibly difficult to derive from some of their stuff … Grrrr … So, that leaves wrapping the existing ManifestEmbeddedFileProvider
class, which is what I did. Here’s what that looks like:
internal class ManifestEmbeddedFileProviderEx : IFileProvider { protected ManifestEmbeddedFileProvider InnerProvider { get; } public ManifestEmbeddedFileProviderEx( Assembly assembly ) { Guard.Instance().ThrowIfNull(assembly, nameof(assembly)); InnerProvider = new ManifestEmbeddedFileProvider( assembly ); } public ManifestEmbeddedFileProviderEx( Assembly assembly, string root ) { Guard.Instance().ThrowIfNull(assembly, nameof(assembly)) .ThrowIfNullOrEmpty(root, nameof(root)); InnerProvider = new ManifestEmbeddedFileProvider( assembly, root ); } public ManifestEmbeddedFileProviderEx( Assembly assembly, string root, DateTimeOffset lastModified ) { Guard.Instance().ThrowIfNull(assembly, nameof(assembly)) .ThrowIfNullOrEmpty(root, nameof(root)); InnerProvider = new ManifestEmbeddedFileProvider( assembly, root, lastModified ); } public ManifestEmbeddedFileProviderEx( Assembly assembly, string root, string manifestName, DateTimeOffset lastModified ) { Guard.Instance().ThrowIfNull(assembly, nameof(assembly)) .ThrowIfNullOrEmpty(root, nameof(root)) .ThrowIfNullOrEmpty(manifestName, nameof(manifestName)); InnerProvider = new ManifestEmbeddedFileProvider( assembly, root, manifestName, lastModified ); } public IDirectoryContents GetDirectoryContents( string subpath ) { Guard.Instance().ThrowIfNullOrEmpty(subpath, nameof(subpath)); return InnerProvider.GetDirectoryContents(subpath); } public IFileInfo GetFileInfo( string subpath ) { Guard.Instance().ThrowIfNullOrEmpty(subpath, nameof(subpath)); if (subpath.StartsWith("/_content/")) { subpath = subpath.Substring("/_content/".Length); var asmName = InnerProvider.Assembly.GetName().Name; if (subpath.StartsWith($"{asmName}")) { subpath = subpath.Substring(asmName.Length); } } return InnerProvider.GetFileInfo(subpath); } public IChangeToken Watch( string filter ) { Guard.Instance().ThrowIfNullOrEmpty(filter, nameof(filter)); return InnerProvider.Watch(filter); } }
Most of this class is constructors, which aren’t very interesting. All of them simply create an inner ManifestEmbeddedFileProvider
instance and assign it to the InnerProvider
property. That’s because I’m really just wrapping the existing provider here. The method worth looking at is the GetFileInfo
method, which is what is called from ASP.NET whenever it encounters a link like these in an HTML doc:
<link rel="stylesheet" href="_content/myplugin.dll/styles/styles.css" /> <script src="_content/myplugin.dll/scripts/scripts.js"></script>
The path that ASP.NET passes into the the ManifestEmbeddedFileProviderEx
.GetFileInfo method, when it tries to load those resources, looks like this: /_content/myplugin.dll/styles/styles.css, or /_content/myplugin.dll/scripts/scripts.jss. But, it should look more like: /styles/style.css, or /scripts/script.jss, since that’s what the ManifestEmbeddedFileProvider
expects.
Because of all that, I had to perform a little surgery on the path, removing the parts I didn’t need before passing the rest onto the InnerProvider
– which as we all recall, is a standard ManifestEmbeddedFileProvider
object instance.
The end result is that, when I ran the sample again, using this extra code, the scripts and style sheets for my sample all worked again. Huzzah!!
Join me soon for the final part of this series of posts, where I’ll go into the sample application and demonstrate my Blazor plugins in action.
See you then.
Photo by Fajrina Adella on Unsplash