如何在运行时动态加载ASP.NET Core Razor视图

SIk*_*ebe 6 razor asp.net-core

是否可以在运行时从单独的程序集中引用ASP.NET Core Razor视图?
我知道如何使用来动态加载控制器,IActionDescriptorChangeProvider但是找不到视图的方法。
我想创建一个简单的插件系统并管理插件,而无需重新启动应用程序。

Cha*_*nas 8

我正在创建一个动态且完全模块化(基于插件)的应用程序,用户可以在运行时将插件程序集放在文件监视目录中以添加控制器和编译视图。

我遇到了和你一样的问题。起初,尽管我已通过ApplicationPartManager服务正确添加了程序集,但 MVC 并未“检测到”控制器和视图。

我解决了控制器问题,正如您所说,可以使用 IActionDescriptorChangeProvider 来处理

不过,对于观点问题,似乎没有内置类似的机制。我在谷歌上爬了几个小时,找到了你的帖子(以及许多其他帖子),但没有得到答复。我几乎放弃了。几乎。

我开始爬取 ASP.NET Core 源代码并实现我认为与查找已编译视图相关的所有服务。我晚上的大部分时间都用来拉我的头发,然后……我发现了。

我发现负责提供这些编译视图的服务是默认的 IViewCompiler(又名 DefaultViewCompiler),而该服务又由 IViewCompilerProvider(又名 DefaultViewCompilerProvider)提供。

实际上,您需要同时实现这两个功能才能使其按预期工作。

IViewCompilerProvider:

 public class ModuleViewCompilerProvider
    : IViewCompilerProvider
{

    public ModuleViewCompilerProvider(ApplicationPartManager applicationPartManager, ILoggerFactory loggerFactory)
    {
        this.Compiler = new ModuleViewCompiler(applicationPartManager, loggerFactory);
    }

    protected IViewCompiler Compiler { get; }

    public IViewCompiler GetCompiler()
    {
        return this.Compiler;
    }

}
Run Code Online (Sandbox Code Playgroud)

IView编译器:

public class ModuleViewCompiler
    : IViewCompiler
{

    public static ModuleViewCompiler Current;

    public ModuleViewCompiler(ApplicationPartManager applicationPartManager, ILoggerFactory loggerFactory)
    {
        this.ApplicationPartManager = applicationPartManager;
        this.Logger = loggerFactory.CreateLogger<ModuleViewCompiler>();
        this.CancellationTokenSources = new Dictionary<string, CancellationTokenSource>();
        this.NormalizedPathCache = new ConcurrentDictionary<string, string>(StringComparer.Ordinal);
        this.PopulateCompiledViews();
        ModuleViewCompiler.Current = this;
    }

    protected ApplicationPartManager ApplicationPartManager { get; }

    protected ILogger Logger { get; }

    protected Dictionary<string, CancellationTokenSource> CancellationTokenSources { get; }

    protected ConcurrentDictionary<string, string> NormalizedPathCache { get; }

    protected Dictionary<string, CompiledViewDescriptor> CompiledViews { get; private set; }

    public void LoadModuleCompiledViews(Assembly moduleAssembly)
    {
        if (moduleAssembly == null)
            throw new ArgumentNullException(nameof(moduleAssembly));
        CancellationTokenSource cancellationTokenSource = new CancellationTokenSource();
        this.CancellationTokenSources.Add(moduleAssembly.FullName, cancellationTokenSource);
        ViewsFeature feature = new ViewsFeature();
        this.ApplicationPartManager.PopulateFeature(feature);
        foreach(CompiledViewDescriptor compiledView in feature.ViewDescriptors
            .Where(v => v.Type.Assembly == moduleAssembly))
        {
            if (!this.CompiledViews.ContainsKey(compiledView.RelativePath))
            {
                compiledView.ExpirationTokens = new List<IChangeToken>() { new CancellationChangeToken(cancellationTokenSource.Token) };
                this.CompiledViews.Add(compiledView.RelativePath, compiledView);
            }
        }
    }

    public void UnloadModuleCompiledViews(Assembly moduleAssembly)
    {
        if (moduleAssembly == null)
            throw new ArgumentNullException(nameof(moduleAssembly));
        foreach (KeyValuePair<string, CompiledViewDescriptor> entry in this.CompiledViews
            .Where(kvp => kvp.Value.Type.Assembly == moduleAssembly))
        {
            this.CompiledViews.Remove(entry.Key);
        }
        if (this.CancellationTokenSources.TryGetValue(moduleAssembly.FullName, out CancellationTokenSource cancellationTokenSource))
        {
            cancellationTokenSource.Cancel();
            this.CancellationTokenSources.Remove(moduleAssembly.FullName);
        }
    }

    private void PopulateCompiledViews()
    {
        ViewsFeature feature = new ViewsFeature();
        this.ApplicationPartManager.PopulateFeature(feature);
        this.CompiledViews = new Dictionary<string, CompiledViewDescriptor>(feature.ViewDescriptors.Count, StringComparer.OrdinalIgnoreCase);
        foreach (CompiledViewDescriptor compiledView in feature.ViewDescriptors)
        {
            if (this.CompiledViews.ContainsKey(compiledView.RelativePath))
                continue;
            this.CompiledViews.Add(compiledView.RelativePath, compiledView);
        };
    }

    public async Task<CompiledViewDescriptor> CompileAsync(string relativePath)
    {
        if (relativePath == null)
            throw new ArgumentNullException(nameof(relativePath));
        if (this.CompiledViews.TryGetValue(relativePath, out CompiledViewDescriptor cachedResult))
            return cachedResult;
        string normalizedPath = this.GetNormalizedPath(relativePath);
        if (this.CompiledViews.TryGetValue(normalizedPath, out cachedResult))
            return cachedResult;
        return await Task.FromResult(new CompiledViewDescriptor()
        {
            RelativePath = normalizedPath,
            ExpirationTokens = Array.Empty<IChangeToken>(),
        });
    }

    protected string GetNormalizedPath(string relativePath)
    {
        if (relativePath.Length == 0)
            return relativePath;
        if (!this.NormalizedPathCache.TryGetValue(relativePath, out var normalizedPath))
        {
            normalizedPath = this.NormalizePath(relativePath);
            this.NormalizedPathCache[relativePath] = normalizedPath;
        }
        return normalizedPath;
    }

    protected string NormalizePath(string path)
    {
        bool addLeadingSlash = path[0] != '\\' && path[0] != '/';
        bool transformSlashes = path.IndexOf('\\') != -1;
        if (!addLeadingSlash && !transformSlashes)
            return path;
        int length = path.Length;
        if (addLeadingSlash)
            length++;
        return string.Create(length, (path, addLeadingSlash), (span, tuple) =>
        {
            var (pathValue, addLeadingSlashValue) = tuple;
            int spanIndex = 0;
            if (addLeadingSlashValue)
                span[spanIndex++] = '/';
            foreach (var ch in pathValue)
            {
                span[spanIndex++] = ch == '\\' ? '/' : ch;
            }
        });
    }

}
Run Code Online (Sandbox Code Playgroud)

现在,您需要找到现有的 IViewCompilerProvider 描述符,并将其替换为您自己的描述符,如下所示:

    public void ConfigureServices(IServiceCollection services)
    {
        services.AddControllersWithViews();
        ServiceDescriptor descriptor = services.FirstOrDefault(s => s.ServiceType == typeof(IViewCompilerProvider));
        services.Remove(descriptor);
        services.AddSingleton<IViewCompilerProvider, ModuleViewCompilerProvider>();
    }
Run Code Online (Sandbox Code Playgroud)

然后,在加载已编译的视图插件程序集后,只需进行以下调用:

ModuleViewCompiler.Current.LoadModuleCompiledViews(compiledViewsAssembly);
Run Code Online (Sandbox Code Playgroud)

卸载已编译的视图插件程序集后,进行该调用:

ModuleViewCompiler.Current.UnloadModuleCompiledViews(compiledViewsAssembly);
Run Code Online (Sandbox Code Playgroud)

这将取消并删除与我们的插件程序集加载的已编译视图关联的 IChangeToken。如果您打算在运行时加载、卸载然后重新加载特定的插件程序集,这一点非常重要,因为否则 MVC 将跟踪它,可能会禁止卸载 AssemblyLoadContext,并且会在编译时因模型类型不匹配而抛出错误(模型在时间 T 加载的装配体 z 中的 x 被认为与在时间 T+1 加载的装配体 z 中的模型 x 不同)

希望有帮助;)