65.9K
CodeProject 正在变化。 阅读更多。
Home

ASP.NET Core 3.x 可动态加载的插件,支持完整的静态文件(JS、CSS)

starIconstarIconstarIconstarIconstarIcon

5.00/5 (3投票s)

2021 年 3 月 4 日

CPOL

4分钟阅读

viewsIcon

15516

如何创建一个 ASP.NET Core 3.x MVC 应用程序,其中包含可动态加载的插件库(Razor 类库),同时完整支持静态文件(js、css 等)。

引言

ASP.NET Core 从 3.0 版本开始,提供了一种通过使用 应用程序部件(Application Parts)将应用程序拆分成模块的方法。

一个解决方案可能包含一个 Web 应用程序和任意数量的程序集库,这些库可能包含控制器、视图、页面、静态文件(如 JavaScript 和 CSS 文件等)。这些库被称为 Razor 类库(Razor Class Libraries) 或 RCL。

有人希望在解决方案中使用 Razor 库有多种原因。

但最有价值的场景是当一个库被动态加载时,作为一种 插件。想象一个电子商务解决方案,提供多种税费或运费计算 插件 或支付 插件,供管理员选择。

当然,也有一些困难。文档未能提供完全描述性的示例和样本。

但最令人沮丧的是,似乎 应用程序部件RCL 并不是为与动态加载的库(即 插件)一起使用而创建的。

尤其是在处理静态文件,即 JavaScript 和 CSS 文件时,动态加载的 RCL 会失败。

本练习的内容

在本文中,我们将研究两种用例:

  • 被主应用程序静态引用的 RCL
  • 被主应用程序动态加载的 RCL

这两个 RCL 都包含静态文件,即 JavaScript 和 CSS 文件。

我们将使用一个 ASP.NET Core MVC Web 应用程序和两个 RCL。

首先,创建一个 ASP.NET Core MVC Web 应用程序,并将其命名为 WebApp

已引用的 RCL

按照 文档 中提供的说明创建一个 RCL。

将 RCL 命名为 StaticRCL。稍后我们将看到为什么名称很重要。

删除项目中的所有文件和文件夹,然后添加三个新文件夹:ControllersViewswwwroot

Controllers 文件夹中创建一个控制器类。

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

Views 文件夹中创建一个 Lib 文件夹。添加一个 Index.cshtml 视图文件。

    <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>

wwwroot 文件夹中创建一个 js 文件夹。添加一个 script.js 文件。

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

可动态加载的 RCL

创建另一个具有与上述类似的结构和文件的 RCL。将其命名为 DynamicRCL

控制器 (Controller)

    public class LibDynamicController : Controller
    {
        [Route("/dynamic")]
        public IActionResult Index()
        {
            return 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 文件

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

我们还需要执行以下操作。

  • 在项目的程序集名称中添加 rcl_ 前缀,即 <AssemblyName>rcl_DynamicRCL</AssemblyName>
  • 添加 GenerateEmbeddedFilesManifest,即 <GenerateEmbeddedFilesManifest>true</GenerateEmbeddedFilesManifest>
  • 添加 Microsoft.Extensions.FileProviders.Embedded NuGet 包,即 <PackageReference Include="Microsoft.Extensions.FileProviders.Embedded" Version="3.1.0" />
  • 将输出路径设置为主 Web 应用程序的 bin 文件夹,即 <OutputPath>..\WebApp\bin\Debug\</OutputPath>
  • 指示项目将 wwwroot 文件夹中的所有文件作为嵌入式资源,即 <EmbeddedResource Include="wwwroot\**\*" />

这是整个项目源文件

<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>

处理已引用的 RCL

WebApp Web 应用程序应具有对第一个 RCL,即 StaticRCL项目引用

HomeControllerIndex.cshtml 文件如下所示:

@{
    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>

如您所见,有两个锚元素调用了相应的 RCL 路由。

Startup 类的 ConfigureServices() 方法同时处理静态引用的和动态加载的库。

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

ApplicationPartManager 类管理 ASP.NET Core MVC 或 Razor Pages 应用程序的部件和功能。

其逻辑是获取对 Web 应用程序已引用的 Assembly 的引用,为该 Assembly 创建一个 AssemblyPart,然后通过调用 ApplicationPartManager 注册该 AssemblyPart

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

        PartManager.ApplicationParts.Add(ApplicationPart);
    }

以上对于路由到 Razor 视图(和 Razor Pages)来说效果很好。但在处理静态文件(如 JavaScript 和 CSS 文件)时,会有一个转折

文档是这样说的:

RCL 的 wwwroot 文件夹中包含的文件可以通过 _content/{LIBRARY NAME}/ 前缀暴露给 RCL 或消费应用程序。例如,一个名为 Razor.Class.Lib 的库,其静态内容路径将是 _content/Razor.Class.Lib/

StaticRCL 项目的 Index.cshtml 文件如下所示,遵循了上述规定。

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

处理动态加载的 RCL

根据 相关文档,需要继承 AssemblyLoadContext 类来加载插件库。

如下所示

    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;
        }
    }

我们在加载插件库时使用此 LibraryLoadContext

下面这个 LoadDynamicLibraries() 方法从 ConfigureServices() 调用,它根据前缀(在本例中为 rcl_)加载库,即插件程序集。这就是我们上面将 DynamicRCL 项目的程序集名称更改为 rcl_DynamicRCL 的原因。

我希望代码易于理解。

    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);
            } 
        }
    }

现在是棘手的部分。

我们已经配置 DynamicRCL 中的 JavaScript、CSS 和其他 静态 资源为嵌入式资源。此外,我们要求该库为这些嵌入式文件创建一个清单

现在我们必须读取该清单,在该 DynamicRCL 程序集及其 wwwroot 文件夹上创建一个 IFileProvider,然后将该文件提供程序注册到系统中。

    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); 
        }
    }

上述方法在 app.UseStaticFiles() 调用之前,由 Startup 类的 Configure() 方法调用。

    app.UseHttpsRedirection();

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

    app.UseStaticFiles();

DynamicRCL 项目的 Index.cshtml 文件为了使用 JavaScript 文件,其做法如下:

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

这里没有使用 _content/{LIBRARY NAME}/ 方案。我们直接使用了 js 文件夹,因为我们已将 DynamicRCL 程序集的 wwwroot 文件夹注册为Web 根文件夹。

就是这样。

测试于

  • Windows 10
  • ASP.NET Core 3.1
  • Microsoft Visual Studio 2019 预览版,版本 16.9.0 预览版 5.0

历史

  • 2021 年 3 月 4 日:初始版本
© . All rights reserved.