Modern ASP.NET Core applications often benefit from a modular or plugin-based architecture. This allows for extending functionality by loading assemblies at runtime. A common requirement in such scenarios is for these dynamically loaded plugins to serve their own static assets, such as CSS, JavaScript, and images, which are ideally encapsulated within the plugin assembly itself as embedded resources.
ASP.NET Core’s IFileProvider
interface provides a powerful abstraction for accessing files and directories, regardless of their physical location. By leveraging this interface, specifically with implementations like ManifestEmbeddedFileProvider
, we can seamlessly serve embedded resources from assemblies loaded on the fly.
This article provides a detailed guide on how to implement this functionality, enabling your ASP.NET Core applications to serve static content from dynamically discovered and loaded plugin assemblies.
Core Concepts
Before diving into the implementation, let’s clarify the key components involved:
IFileProvider
: An interface inMicrosoft.Extensions.FileProviders
that abstracts file system access. ASP.NET Core uses it internally for various tasks, including serving static files. More details can be found in the Microsoft.Extensions.FileProviders.IFileProvider documentation.- Embedded Resources: Files (e.g.,
.css
,.js
, images) included directly within a compiled assembly. They are marked with theEmbeddedResource
build action in the project file. The resource name typically follows the patternAssemblyName.FolderPath.FileName
. - Dynamically Loaded Assembly: A .NET assembly (DLL) that is not referenced at compile time but is loaded into the application’s process at runtime. This is fundamental for plugin systems, often managed using
AssemblyLoadContext
. ManifestEmbeddedFileProvider
: A built-inIFileProvider
implementation designed to read files embedded in an assembly. It relies on a manifest that maps logical file paths to embedded resource streams. See theManifestEmbeddedFileProvider
API documentation for more.CompositeFileProvider
: AnIFileProvider
that combines multiple other file providers into a single logical view. This is useful for serving files from various sources. Details are available in theCompositeFileProvider
API documentation.
Step 1: Creating a Plugin with Embedded Resources
First, let’s create a simple class library project that will act as our plugin. This plugin will contain some embedded static assets.
1. Create the Plugin Project:
Create a new .NET Class Library project (e.g., MyPlugin
).
2. Add Static Assets:
Inside the MyPlugin
project, create a folder structure for your static assets, for example, wwwroot/css/plugin-styles.css
and wwwroot/js/plugin-script.js
.
MyPlugin/wwwroot/css/plugin-styles.css
:
|
|
MyPlugin/wwwroot/js/plugin-script.js
:
|
|
3. Configure for Embedded Resources:
Edit the MyPlugin.csproj
file to embed the files in the wwwroot
folder and generate the manifest:
|
|
The GenerateEmbeddedFilesManifest
MSBuild property (true by default in SDK-style projects targeting .NET Core 3.0+ but good to be explicit) instructs MSBuild to generate a manifest of embedded files, which ManifestEmbeddedFileProvider
uses. The EmbeddedResource
item includes all files within the plugin’s wwwroot
directory.
Build the MyPlugin
project. The output DLL (e.g., MyPlugin.dll
) will contain these embedded resources.
Step 2: Dynamically Loading the Plugin Assembly
In your main ASP.NET Core application, you’ll need a mechanism to discover and load plugin assemblies. For simplicity, we’ll assume the plugin DLL is placed in a known directory.
The following code snippet demonstrates loading an assembly. This typically happens during application startup.
|
|
Place this logic where it makes sense in your application’s startup sequence. After execution, loadedAssembly
will hold a reference to the dynamically loaded MyPlugin
assembly.
Step 3: Serving Embedded Resources with ManifestEmbeddedFileProvider
Once the plugin assembly is loaded, you can create a ManifestEmbeddedFileProvider
instance to serve its embedded resources.
|
|
The root
parameter in the ManifestEmbeddedFileProvider
constructor is crucial. It specifies the base path within the embedded resources manifest. If you embedded files under a wwwroot
folder in your plugin project like MyPlugin.wwwroot.css.plugin-styles.css
, then root: "wwwroot"
makes the provider serve css/plugin-styles.css
relative to this root.
Step 4: Configuring Static File Middleware
To make these embedded resources accessible via HTTP requests, you need to configure the Static File Middleware in ASP.NET Core.
Serving Plugin Assets from a Specific Request Path
It’s good practice to serve plugin assets under a unique request path to avoid conflicts (e.g., /plugins/myplugin/css/plugin-styles.css
).
Modify your Program.cs
(or Startup.cs
):
|
|
With this setup:
- A request to
/plugins/MyPlugin/css/plugin-styles.css
will serve the embeddedplugin-styles.css
fromMyPlugin.dll
. - A request to
/plugins/MyPlugin/js/plugin-script.js
will serve the embeddedplugin-script.js
.
Using CompositeFileProvider
for Multiple Sources
If you have multiple plugins or want to combine the plugin’s file provider with the application’s default wwwroot
provider (though app.UseStaticFiles()
already handles the main wwwroot
), you can use CompositeFileProvider
.
This is particularly useful if you want a single IFileProvider
instance for a specific purpose (e.g., for Razor view discovery from plugins, which is beyond static files but uses the same IFileProvider
abstraction). For static files, multiple app.UseStaticFiles()
calls as shown above is often simpler.
However, if needed, a CompositeFileProvider
can be constructed:
|
|
When using CompositeFileProvider
, the order of providers in the list matters. If multiple providers can satisfy a file request, the first one in the list wins.
Step 5: Testing the Setup
- Ensure
MyPlugin.dll
is copied to theplugins
directory of your main application’s output (e.g.,bin/Debug/net8.0/plugins/MyPlugin.dll
). - Create a simple HTML page in your main application’s
wwwroot
(e.g.,wwwroot/index.html
) or a Razor page to test:
MainApp/wwwroot/index.html
:
|
|
- Run your main ASP.NET Core application.
- Navigate to
/index.html
. You should see the styles fromplugin-styles.css
applied, and the JavaScript fromplugin-script.js
should execute (check browser console and try the button).
Key Considerations and Best Practices
- Resource Naming: Embedded resource names are case-sensitive and typically follow
AssemblyName.Folder.SubFolder.FileName.Extension
.ManifestEmbeddedFileProvider
uses these names. Ensure yourEmbeddedResource
paths and theroot
parameter are consistent. GenerateEmbeddedFilesManifest
: Essential forManifestEmbeddedFileProvider
. Ensure it’s set totrue
in your plugin’s.csproj
file. See the MSBuild property documentation for details.- Assembly Load Contexts: For simple cases,
AssemblyLoadContext.Default.LoadFromAssemblyPath()
is sufficient. For more advanced scenarios like plugin isolation, dependency management, or unloading plugins, you should investigate creating customAssemblyLoadContext
instances. - Path Uniqueness: Always use unique
RequestPath
segments for each plugin when configuringUseStaticFiles
to prevent conflicts. - Debugging:
- To verify embedded resource names, you can inspect the loaded assembly:This helps confirm the exact names and paths as they exist in the manifest.
1 2 3 4 5 6
// In your main app, after loading pluginAssembly // string[] resourceNames = pluginAssembly.GetManifestResourceNames(); // foreach (string name in resourceNames) // { // Console.WriteLine($"Embedded Resource: {name}"); // }
- Use browser developer tools (Network tab) to check for 404 errors and verify requested URLs.
- To verify embedded resource names, you can inspect the loaded assembly:
- Case Sensitivity: File systems can vary in case sensitivity. Embedded resource names are generally case-sensitive. Keep your paths and names consistent.
Common Pitfalls
- Incorrect Assembly for
ManifestEmbeddedFileProvider
: Passing the host assembly instead of the dynamically loaded plugin assembly. - Mismatched
root
Parameter: Theroot
parameter inManifestEmbeddedFileProvider
must match the base folder structure you used forEmbeddedResource
items (e.g., “wwwroot”). - Forgetting
GenerateEmbeddedFilesManifest
: The provider won’t find resources without the manifest. - Typos in Resource Paths or
RequestPath
: URLs are exact. Double-check for typos or case mismatches. - Order of Middleware: Ensure
UseStaticFiles()
is called beforeUseRouting()
andUseEndpoints()
/Map...()
calls that might handle the request differently. The ASP.NET Core middleware order guidance is important here.
Conclusion
Serving embedded resources from dynamically loaded assemblies is a powerful technique for building modular and extensible ASP.NET Core applications. By correctly using IFileProvider
, ManifestEmbeddedFileProvider
, and configuring the static file middleware, you can ensure that your plugins are truly self-contained, packaging their own necessary static assets. This approach promotes cleaner architecture, easier deployment of modules, and a more robust system overall. Remember to manage resource naming and request paths carefully to create a seamless experience.