Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles
(untagged)

ASP.NET Core 3.x Dynamically Loadable Plugins with Full Static File (JS, CSS) Support

0.00/5 (No votes)
3 Mar 2021 1  
How to create an ASP.NET Core 3.x MVC application with dynamically loadable plugin libraries (Razor Class Libraries) with full support for static files too (js, css, etc.).
In this article, we'll examine an RCL statically referenced by the main application, an RCL that is loaded dynamically by the main application. We will use an ASP.NET Core MVC Web application and two RCLs.

Introduction

ASP.NET Core, starting from version 3.0, provides a way to split an application into modules by using Application Parts.

A solution may comprise a Web Application and any number of Assembly libraries that may contain controllers, views, pages, static files such as JavaScript and CSS files, and more. Those libraries are called Razor Class Libraries or RCL.

There are a number of reasons why someone would want to use Razor libraries in a solution.

But the most valuable case is when a library is loaded dynamically, as a plugin. Imagine an e-Commerce solution providing a number of Tax or Shipping Charges calculation plugins or Payment plugins, for the administrator to choose from.

There are some difficulties though. For sure, the documentation fails to provide fully descriptive examples and samples.

But the most frustrating is that it seems that Application Parts and RCLs are not created to be used with libraries that loaded dynamically, that is plugins.

Especially when it comes to static files, i.e., JavaScript and CSS files, the dynamically loaded RCLs is a failure.

The Content of This Exercise

In this text, we'll examine both use cases:

  • an RCL statically referenced by the main application
  • an RCL that is loaded dynamically by the main application

Both RCLs include static files, i.e., JavaSript and CSS files.

We will use an ASP.NET Core MVC Web application and two RCLs.

To start, create an ASP.NET Core MVC Web application and name it WebApp.

A Referenced RCL

Create an RCL following the instructions provided by the documentation.

Name the RCL StaticRCL. We'll see later why the name matters.

Delete all files and folders from the project and add three new folders, Controllers, Views and wwwroot.

Create a controller class inside the Controllers folder.

public class LibController : Controller
{
    [Route("/static")]
    public IActionResult Index()
    {
        return View();
    }
}

Create a Lib folder inside the Views folder. Add an Index.cshtml view file.

<script src="~/_content/StaticRCL/js/script.js"></script>

<div>
    <strong>STATICALLY</strong> referenced Razor Class Library
</div>

<div>
    <button onclick="StaticRCL_ShowMessage();">Click Me!</button>
</div>

Create a js folder insided the wwwroot folder. Add a script.js file.

function StaticRCL_ShowMessage() {
    alert('Hi from Statically refernced Razor Class Library javascript');
}

A Dynamically Loadable RCL

Create another RCL with similar structure and files as above. Name it DynamicRCL.

Controller

public class LibDynamicController : Controller
{
    [Route("/dynamic")]
    public IActionResult Index()
    {
        return View();
    }
}

View

<script src="js/script.js"></script>

<div>
    <strong>DYNAMICALLY</strong> loaded Razor Class Library
</div>

<div>
    <button onclick="DynamicRCL_ShowMessage();">Click Me!</button>
</div>

JavaScript File

function DynamicRCL_ShowMessage() {
    alert('Hi from Dynamically loaded Razor Class Library javascript');
}

We also need to do the following.

  • Put a rcl_ prefix in the Assembly Name of the project, i.e., <AssemblyName>rcl_DynamicRCL</AssemblyName>
  • Add a GenerateEmbeddedFilesManifest, i.e., <GenerateEmbeddedFilesManifest>true</GenerateEmbeddedFilesManifest>
  • Add the Microsoft.Extensions.FileProviders.Embedded NuGet package, i.e., <PackageReference Include="Microsoft.Extensions.FileProviders.Embedded" Version="3.1.0" />
  • Set the output path to the bin folder of the main Web Application, i.e., <OutputPath>..\WebApp\bin\Debug\</OutputPath>
  • Instruct the project to use all files in wwwroot folder as embedded resources, i.e., <EmbeddedResource Include="wwwroot\**\*" />

Here is the whole project source file:

<Project Sdk="Microsoft.NET.Sdk.Razor">

  <PropertyGroup>
    <TargetFramework>netcoreapp3.1</TargetFramework>
    <AddRazorSupportForMvc>true</AddRazorSupportForMvc>
    <AssemblyName>rcl_DynamicRCL</AssemblyName>
    <CopyLocalLockFileAssemblies>true</CopyLocalLockFileAssemblies>     
    <GenerateEmbeddedFilesManifest>true</GenerateEmbeddedFilesManifest>
  </PropertyGroup>

  <PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Debug|AnyCPU'">
    <OutputPath>..\WebApp\bin\Debug\</OutputPath>
  </PropertyGroup>

  <ItemGroup>
    <FrameworkReference Include="Microsoft.AspNetCore.App" />
    <PackageReference Include="Microsoft.Extensions.FileProviders.Embedded" Version="3.1.0" />
  </ItemGroup> 

   <ItemGroup>
      <EmbeddedResource Include="wwwroot\**\*" />
   </ItemGroup>

</Project>

Handing a Referenced RCL

The WebApp Web Application should have a project reference to the first RCL, the StaticRCL.

The Index.cshtml of the HomeController is as follows:

@{
    ViewData["Title"] = "Home Page";
}

<div class="text-center">
    <h1 class="display-4">Welcome</h1>
    <div><a href="/static">Statically referenced Razor Class Library View</a></div>
    <div><a href="/dynamic">Dynamically loaded Razor Class Library View</a></div>
</div>

As you can see, there are two anchor elements calling a corresponding RCL route.

The ConfigureServices() method of the Startup class handles both, the statically referenced and the dynamically loaded libraries.

public void ConfigureServices(IServiceCollection services)
{
    services.AddControllersWithViews().
        ConfigureApplicationPartManager((PartManager) => {
            ConfigureStaticLibraries(PartManager);  // static RCLs
            LoadDynamicLibraries(PartManager);      // dynamic RCLs
        });
}

The ApplicationPartManager class manages the parts and features of an ASP.NET Core MVC or Razor Pages application.

The logic is to get a reference to an Assembly already referenced by the Web Application, create an AssemblyPart for that Assembly and then register that AssemblyPart calling the ApplicationPartManager.

void ConfigureStaticLibraries(ApplicationPartManager PartManager)
{
    Assembly Assembly = typeof(StaticRCL.Controllers.LibController).Assembly;
    ApplicationPart ApplicationPart = new AssemblyPart(Assembly);

    PartManager.ApplicationParts.Add(ApplicationPart);
}

The above works fine with routing to Razor Views (and Razor Pages). With a twist when comes to static files, such as JavaScript and CSS files.

Here is what the documentation says:

The files included in the wwwroot folder of the RCL are exposed to either the RCL or the consuming app under the prefix _content/{LIBRARY NAME}/. For example, a library named Razor.Class.Lib results in a path to static content at _content/Razor.Class.Lib/.

Here is what the Index.cshtml of the StaticRCL project does, conforming to the above.

<script src="~/_content/StaticRCL/js/script.js"></script>

Handling a Dynamically Loaded RCL

A descendant of the AssemblyLoadContext class is required in order to load the plugin libraries, according to the relevent documentation.

Here it is:

public class LibraryLoadContext: AssemblyLoadContext
{
    private AssemblyDependencyResolver fResolver;

    public LibraryLoadContext(string BinFolder)
    {
        fResolver = new AssemblyDependencyResolver(BinFolder);
    }

    protected override Assembly Load(AssemblyName assemblyName)
    {
        string assemblyPath = fResolver.ResolveAssemblyToPath(assemblyName);
        if (assemblyPath != null)
        {
            return LoadFromAssemblyPath(assemblyPath);
        }

        return null;
    }

    protected override IntPtr LoadUnmanagedDll(string unmanagedDllName)
    {
        string FilePath = fResolver.ResolveUnmanagedDllToPath(unmanagedDllName);
        if (FilePath != null)
        {
            return LoadUnmanagedDllFromPath(FilePath);
        }

        return IntPtr.Zero;
    }
}

We use that LibraryLoadContext in loading plugin libraries.

The following LoadDynamicLibraries() called from ConfigureServices() loads the libraries, i.e., plugin assemblies, based on a prefix, in this case rcl_. That's why we've changed the assembly name of the DynamicRCL project to rcl_DynamicRCL, above.

I hope the code is easily understandable.

void LoadDynamicLibraries(ApplicationPartManager PartManager)
{
    // get the output folder of this application
    string BinFolder = this.GetType().Assembly.ManifestModule.FullyQualifiedName;
    BinFolder = Path.GetDirectoryName(BinFolder);

    // get the full filepath of any dll starting with the rcl_ prefix
    string Prefix = "rcl_";
    string SearchPattern = $"{Prefix}*.dll";
    string[] LibraryPaths = Directory.GetFiles(BinFolder, SearchPattern);

    if (LibraryPaths != null && LibraryPaths.Length > 0)
    {
        // create the load context
        LibraryLoadContext LoadContext = new LibraryLoadContext(BinFolder);

        Assembly Assembly;
        ApplicationPart ApplicationPart;
        foreach (string LibraryPath in LibraryPaths)
        {
            // load each assembly using its filepath
            Assembly = LoadContext.LoadFromAssemblyPath(LibraryPath);

            // create an application part for that assembly
            ApplicationPart = LibraryPath.EndsWith(".Views.dll") ?
                              new CompiledRazorAssemblyPart(Assembly)
                              as ApplicationPart : new AssemblyPart(Assembly);

            // register the application part
            PartManager.ApplicationParts.Add(ApplicationPart);

            // if it is NOT the *.Views.dll add it to a list for later use
            if (!LibraryPath.EndsWith(".Views.dll"))
                DynamicallyLoadedLibraries.Add(Assembly);
        }
    }
}

Now the tricky part.

We have configured so JavaScript, CSS and other static resources to be embedded resources in the DynamicRCL. Furthermore, we asked that library to create a manifest for those embedded files.

Now we have to read that manifest, create an IFileProvider on that DynamicRCL Assembly and its wwwroot folder, and then register that file provider with the system.

void RegisterDynamicLibariesStaticFiles(IWebHostEnvironment env)
{
    IFileProvider FileProvider;
    foreach (Assembly A in DynamicallyLoadedLibraries)
    {
        // create a "web root" file provider for the embedded static files
        // found on wwwroot folder
        FileProvider = new ManifestEmbeddedFileProvider(A, "wwwroot");

        // register a new composite provider containing
        // the old web root file provider
        // and the new one we just created
        env.WebRootFileProvider = new CompositeFileProvider
                                  (env.WebRootFileProvider, FileProvider);
    }
}

The above method is called by the Configure() method of the Startup class just before the app.UseStaticFiles() call.

app.UseHttpsRedirection();

// register file providers for the dynamically loaded libraries
if (DynamicallyLoadedLibraries.Count > 0)
    RegisterDynamicLibariesStaticFiles(env);

app.UseStaticFiles();

Here is what the Index.cshtml of the DynamicRCL project does in order to use a JavaScript file.

<script src="js/script.js"></script>

There is no use of the _content/{LIBRARY NAME}/ scheme here. We just use the js folder since we have registered the wwwroot folder of the DynamicRCL assembly as the web root folder.

That's all.

Tested on:

  • Windows 10
  • ASP.NET Core 3.1
  • Microsoft Visual Studio 2019 Preview, Version 16.9.0 Preview 5.0

History

  • 4th March, 2021: Initial version

License

This article has no explicit license attached to it but may contain usage terms in the article text or the download files themselves. If in doubt please contact the author via the discussion board below.

A list of licenses authors might use can be found here