如何在运行时动态加载ASP.NET Core Razor视图 [英] How to load ASP.NET Core Razor view dynamically at runtime

查看:353
本文介绍了如何在运行时动态加载ASP.NET Core Razor视图的处理方法,对大家解决问题具有一定的参考价值,需要的朋友们下面随着小编来一起学习吧!

问题描述

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

Is it possible to reference ASP.NET Core Razor views from separate assembly at runtime?
I know how to load controllers dynamically using IActionDescriptorChangeProvider but cannot find a way as for views.
I'd like to create a simple plugin system and manage plugins without restart app.

推荐答案

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

I am creating a dynamic and fully modular (plugin-based) application in which the user can drop a plugin assembly at run time in a file watched directory to add controllers and compiled views.

我遇到了与您相同的问题.最初,即使我通过 ApplicationPartManager 服务正确添加了程序集,MVC仍未检测"控制器和视图.

I ran in the same issues than you. At first, both controllers and views were not being 'detected' by MVC, even though I add correctly added the assemblies through the ApplicationPartManager service.

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

I solved the controllers issue which, as you said, can be handled with the IActionDescriptorChangeProvider.

但是,对于视图问题,似乎没有内置类似的机制.我在Google上搜寻了几个小时,找到了您的信息(以及许多其他信息),但都没有得到答复.我差点放弃.差不多.

For the views issue, though, it seemed there was no similar mechanism built-in. I crawled google for hours, found your post (and many others), but none were answered. I almost gave up. Almost.

我开始对ASP.NET Core源进行爬网,并实现了我认为与查找编译视图有关的所有服务.晚上的大部分时间都花了我的头发,然后……EUREKA.

I started crawling the ASP.NET Core sources and implemented all service I thought was related to finding the compiled views. A good part of my evening was gone pulling my hairs, and then... EUREKA.

我发现负责提供那些已编译视图的服务是默认的IViewCompiler(又名DefaultViewCompiler),它又由IViewCompilerProvider(又名DefaultViewCompilerProvider)提供.

I found that the service responsible for supplying those compiled views was the default IViewCompiler (aka DefaultViewCompiler), which was in turn provided by the IViewCompilerProvider (aka DefaultViewCompilerProvider).

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

You actually need to implement both those to get it working as expected.

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

}

IViewCompiler:

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

}

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

Now, you need to find the existing IViewCompilerProvider descriptor, and replace it with your own, as follows:

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

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

Then, upon loading a compiled view plugin assembly, just make the following call:

ModuleViewCompiler.Current.LoadModuleCompiledViews(compiledViewsAssembly);

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

Upon unloading a compiled view plugin assembly, make that call:

ModuleViewCompiler.Current.UnloadModuleCompiledViews(compiledViewsAssembly);

这将取消并摆脱我们与插件程序集加载的已编译视图关联的IChangeToken. 这非常重要,如果您打算在运行时加载,卸载然后重新加载特定的插件程序集,因为否则MVC会对其进行跟踪,可能会禁止您的AssemblyLoadContext的卸载,并且在编译时会引发错误由于模型类型不匹配(在时间T加载的装配z的模型x与在时间T + 1加载的装配z的模型x不同)

That will cancel and get rid of the IChangeToken that we have associated with the compiled views loaded with our plugin assembly. This is very important if you intend to load, unload then reload a specific plugin assembly at runtime, because otherwise MVC will keep track of it, possibly forbidding the unloading of your AssemblyLoadContext, and will throw error upon compilation because of model types mismatch (model x from assembly z loaded at time T is considered different than model x from assembly z loaded at time T+1)

希望有帮助;)

这篇关于如何在运行时动态加载ASP.NET Core Razor视图的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持IT屋!

查看全文
登录 关闭
扫码关注1秒登录
发送“验证码”获取 | 15天全站免登陆