タイトル:(c)はプラグインを最初からASP.NETコアMVCの開発を達成するために-ランタイム時にコンポーネントを有効にする方法
著者:Lamond呂
住所:https://www.cnblogs.com/lwqlun/p/11260750.html
ソースコードを:https://github.com/lamondlu/DynamicPlugins
先行レビューレビュー
- 使用アプリケーションパート動的負荷コントローラとビュー - プラグイン(A)の発展を達成するために最初からASP.NET MVCのコア
- プロジェクトテンプレートを作成する方法 - プラグイン(2)の開発を達成するために最初からASP.NET MVCのコア
前の2で、私はどのようにアプリケーションパート動的負荷コントローラとビューを使用するだけでなく、プラグインテンプレートの操作を簡素化するための作成方法を示しました。
前の仕上げの後、私は突然、プラグインは最初の2のように設計されたシステムを構築する場合、すなわち、非常に深刻な問題があるだろう、問題を考えました
あなたがプラグインを追加すると、プログラム全体はすぐに、プラグインを有効にすることはできません負荷プラグインを補正するために、全体のみASP.NET Coreアプリケーションの後に再起動してください。すべてのプラグインは、プログラムの起動時にロードされるため
ConfigureService
の設定方法。
このようにプラグインシステムが困難になり、私たちは、実行時に動的に有効または無効にするプラグインの効果を期待し、解決策はありませんか?答えはイエスです。その下に、ステップバイステップでは、私は自分の考え、コーディングの問題点、ならびにこれらの問題の解決策を説明します。
この機能を達成するために、私は回り道をたくさん取る、現在のプログラムは最高ではないかもしれないが、実際に実行可能なオプション、あなたがよりよい解決策を持っている場合、我々はそれを議論することができます。
アクションでのアクティベーションコンポーネント
この質問に直面したとき、私の最初のアイデアはしているApplicationPartManager
アクションのプラグインライブラリに移動するには、コードをロードします。だから私は、プライマリ・サイトを作成PluginsController
し、呼ばれるプログラムを追加有効にEnable
対処方法を。
public class PluginsController : Controller
{
public IActionResult Enable()
{
var assembly = Assembly.LoadFile(AppDomain.CurrentDomain.BaseDirectory + "DemoPlugin1\\DemoPlugin1.dll");
var viewAssembly = Assembly.LoadFile(AppDomain.CurrentDomain.BaseDirectory + "DemoPlugin1\\DemoPlugin1.Views.dll");
var viewAssemblyPart = new CompiledRazorAssemblyPart(viewAssembly);
var controllerAssemblyPart = new AssemblyPart(assembly);
_partManager.ApplicationParts.Add(controllerAssemblyPart);
_partManager.ApplicationParts.Add(viewAssemblyPart);
return Content("Enabled");
}
}
コードを変更した後、我々は最初に呼び出すプログラム、実行し/Plugins/Enable
、アクティブなコンポーネントをアクティブにしようした後に、我々は再び呼び出します/Plugin1/HelloWorld
ここでは、すなわち、ビューとコントローラが正しく起動されていない、プログラムは404に戻っています。
ここでは、質問、なぜそれを活性化することができないだろうを有していても良いですか?
その理由は、ここではASP.NETコア・アプリケーションが起動したときにのみ、それは実行時に新しいコントローラアセンブリに追加されたものの、組立負荷コントローラとApplicationPartマネージャのビューに移動することでApplicationPart
管理者が、 ASP.NETコアは自動的に更新されませんので、ここで私たちは、ASP.NETコアは、コントローラのメソッドをリロードしてみましょうする方法を見つける必要があります。
あらゆる種類の情報を照会することによって、私は最終的に出発点を見つけ、ASP.NETコア2.2であり、クラスにされActionDescriptorCollectionProvider
、そのサブクラスは、DefaultActionDescriptorCollectionProvider
コントローラとアクションを設定するために使用されています。
出典:
internal class DefaultActionDescriptorCollectionProvider : ActionDescriptorCollectionProvider
{
private readonly IActionDescriptorProvider[] _actionDescriptorProviders;
private readonly IActionDescriptorChangeProvider[] _actionDescriptorChangeProviders;
private readonly object _lock;
private ActionDescriptorCollection _collection;
private IChangeToken _changeToken;
private CancellationTokenSource _cancellationTokenSource;
private int _version = 0;
public DefaultActionDescriptorCollectionProvider(
IEnumerable<IActionDescriptorProvider> actionDescriptorProviders,
IEnumerable<IActionDescriptorChangeProvider> actionDescriptorChangeProviders)
{
...
ChangeToken.OnChange(
GetCompositeChangeToken,
UpdateCollection);
}
public override ActionDescriptorCollection ActionDescriptors
{
get
{
Initialize();
return _collection;
}
}
...
private IChangeToken GetCompositeChangeToken()
{
if (_actionDescriptorChangeProviders.Length == 1)
{
return _actionDescriptorChangeProviders[0].GetChangeToken();
}
var changeTokens = new IChangeToken[_actionDescriptorChangeProviders.Length];
for (var i = 0; i < _actionDescriptorChangeProviders.Length; i++)
{
changeTokens[i] = _actionDescriptorChangeProviders[i].GetChangeToken();
}
return new CompositeChangeToken(changeTokens);
}
...
private void UpdateCollection()
{
lock (_lock)
{
var context = new ActionDescriptorProviderContext();
for (var i = 0; i < _actionDescriptorProviders.Length; i++)
{
_actionDescriptorProviders[i].OnProvidersExecuting(context);
}
for (var i = _actionDescriptorProviders.Length - 1; i >= 0; i--)
{
_actionDescriptorProviders[i].OnProvidersExecuted(context);
}
var oldCancellationTokenSource = _cancellationTokenSource;
_collection = new ActionDescriptorCollection(
new ReadOnlyCollection<ActionDescriptor>(context.Results),
_version++);
_cancellationTokenSource = new CancellationTokenSource();
_changeToken = new CancellationChangeToken(_cancellationTokenSource.Token);
oldCancellationTokenSource?.Cancel();
}
}
}
- この
ActionDescriptors
ASP.NETコア・プログラムの開始時にプロパティは、すべてのコントローラ/アクションセットにマッチした、記録されています。 - 更新するUpdateCollection方法
ActionDescriptors
コレクションを。 - コンストラクタでは、トリガーをデザイン
ChangeToken.OnChange(GetCompositeChangeToken,UpdateCollection)
。トークンオブジェクトが引き金と自動的に変更されたときにここでのプログラムは、トークンオブジェクトを監視するUpdateCollection
方法を。 - ここでトークンは、グループで
IActionDescriptorChangeProvider
インタフェースオブジェクトによって得られた組成物。
所以这里我们就可以通过自定义一个IActionDescriptorChangeProvider
接口对象,并在组件激活方法Enable中修改这个接口Token的方式,使DefaultActionDescriptorCollectionProvider
中的CompositeChangeToken
发生变化,从而实现控制器的重新装载。
使用IActionDescriptorChangeProvider
在运行时激活控制器
这里我们首先创建一个MyActionDescriptorChangeProvider
类,并让它实现IActionDescriptorChangeProvider
接口
public class MyActionDescriptorChangeProvider : IActionDescriptorChangeProvider
{
public static MyActionDescriptorChangeProvider Instance { get; } = new MyActionDescriptorChangeProvider();
public CancellationTokenSource TokenSource { get; private set; }
public bool HasChanged { get; set; }
public IChangeToken GetChangeToken()
{
TokenSource = new CancellationTokenSource();
return new CancellationChangeToken(TokenSource.Token);
}
}
然后我们需要在Startup.cs
的ConfigureServices
方法中,将MyActionDescriptorChangeProvider.Instance
属性以单例的方式注册到依赖注入容器中。
public void ConfigureServices(IServiceCollection services)
{
...
services.AddSingleton<IActionDescriptorChangeProvider>(MyActionDescriptorChangeProvider.Instance);
services.AddSingleton(MyActionDescriptorChangeProvider.Instance);
...
}
最后我们在Enable
方法中通过两行代码来修改当前MyActionDescriptorChangeProvider
对象的Token。
public class PluginsController : Controller
{
public IActionResult Enable()
{
var assembly = Assembly.LoadFile(AppDomain.CurrentDomain.BaseDirectory + "DemoPlugin1\\DemoPlugin1.dll");
var viewAssembly = Assembly.LoadFile(AppDomain.CurrentDomain.BaseDirectory + "DemoPlugin1\\DemoPlugin1.Views.dll");
var viewAssemblyPart = new CompiledRazorAssemblyPart(viewAssembly);
var controllerAssemblyPart = new AssemblyPart(assembly);
_partManager.ApplicationParts.Add(controllerAssemblyPart);
_partManager.ApplicationParts.Add(viewAssemblyPart);
MyActionDescriptorChangeProvider.Instance.HasChanged = true;
MyActionDescriptorChangeProvider.Instance.TokenSource.Cancel();
return Content("Enabled");
}
}
修改代码之后重新运行程序,这里我们依然首先调用/Plugins/Enable
,然后再次调用/Plugin1/Helloworld
, 这时候你会发现Action被触发了,只是没有找到对应的Views。
如何解决插件的预编译Razor视图不能重新加载的问题?
通过以上的方式,我们终于获得了在运行时加载插件控制器程序集的能力,但是插件的预编译Razor视图程序集没有被正确加载,这就说明IActionDescriptorChangeProvider
只会触发控制器的重新加载,不会触发预编译Razor视图的重新加载。ASP.NET Core只会在整个应用启动时,才会加载插件的预编译Razor程序集,所以我们并没有获得在运行时重新加载预编译Razor视图的能力。
针对这一点,我也查阅了好多资料,最终也没有一个可行的解决方案,也许使用ASP.NET Core 3.0的Razor Runtime Compilation可以实现,但是在ASP.NET Core 2.2版本,我们还没有获得这种能力。
为了越过这个难点,最终我还是选择了放弃预编译Razor视图,改用原始的Razor视图。
因为在ASP.NET Core启动时,我们可以在Startup.cs
的ConfigureServices
方法中配置Razor视图引擎检索视图的规则。
这里我们可以把每个插件组织成ASP.NET Core MVC中一个Area, Area的名称即插件的名称, 这样我们就可以将为Razor视图引擎的添加一个检索视图的规则,代码如下
services.Configure<RazorViewEngineOptions>(o =>
{
o.AreaViewLocationFormats.Add("/Modules/{2}/{1}/Views/{0}" + RazorViewEngine.ViewExtension);
});
这里{2}
代表Area名称, {1}
代表Controller名称, {0}
代表Action名称。
这里Modules是我重新创建的一个目录,后续所有的插件都会放置在这个目录中。
同样的,我们还需要在Configure
方法中为Area注册路由。
app.UseMvc(routes =>
{
routes.MapRoute(
name: "default",
template: "{controller=Home}/{action=Index}/{id?}");
routes.MapRoute(
name: "default",
template: "Modules/{area}/{controller=Home}/{action=Index}/{id?}");
});
因为我们已经不需要使用Razor的预编译视图,所以Enable
方法我们的最终代码如下
public IActionResult Enable()
{
var assembly = Assembly.LoadFile(AppDomain.CurrentDomain.BaseDirectory + "Modules\\DemoPlugin1\\DemoPlugin1.dll");
var controllerAssemblyPart = new AssemblyPart(assembly);
_partManager.ApplicationParts.Add(controllerAssemblyPart);
MyActionDescriptorChangeProvider.Instance.HasChanged = true;
MyActionDescriptorChangeProvider.Instance.TokenSource.Cancel();
return Content("Enabled");
}
以上就是针对主站点的修改,下面我们再来修改一下插件项目。
首先我们需要将整个项目的Sdk类型改为由之前的Microsoft.Net.Sdk.Razor改为Microsoft.Net.Sdk.Web, 由于之前我们使用了预编译的Razor视图,所以我们使用了Microsoft.Net.Sdk.Razor,它会将视图编译为一个dll文件。但是现在我们需要使用原始的Razor视图,所以我们需要将其改为Microsoft.Net.Sdk.Web, 使用这个Sdk, 最终的Views文件夹中的文件会以原始的形式发布出来。
<Project Sdk="Microsoft.NET.Sdk.Web">
<PropertyGroup>
<TargetFramework>netcoreapp2.2</TargetFramework>
</PropertyGroup>
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Debug|AnyCPU'">
<OutputPath></OutputPath>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.AspNetCore.App" Version="2.2.0" />
<PackageReference Include="Microsoft.AspNetCore.Razor" Version="2.2.0" />
<PackageReference Include="Microsoft.AspNetCore.Razor.Design" Version="2.2.0" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\DynamicPlugins.Core\DynamicPlugins.Core.csproj" />
</ItemGroup>
</Project>
最后我们需要在Plugin1Controller上添加Area配置, 并将编译之后的程序集以及Views目录放置到主站点项目的Modules目录中
[Area("DemoPlugin1")]
public class Plugin1Controller : Controller
{
public IActionResult HelloWorld()
{
return View();
}
}
最终主站点项目目录结构
The files tree is:
=================
|__ DynamicPlugins.Core.dll
|__ DynamicPlugins.Core.pdb
|__ DynamicPluginsDemoSite.deps.json
|__ DynamicPluginsDemoSite.dll
|__ DynamicPluginsDemoSite.pdb
|__ DynamicPluginsDemoSite.runtimeconfig.dev.json
|__ DynamicPluginsDemoSite.runtimeconfig.json
|__ DynamicPluginsDemoSite.Views.dll
|__ DynamicPluginsDemoSite.Views.pdb
|__ Modules
|__ DemoPlugin1
|__ DemoPlugin1.dll
|__ Views
|__ Plugin1
|__ HelloWorld.cshtml
|__ _ViewStart.cshtml
现在我们重新启动项目,重新按照之前的顺序,先激活插件,再访问新的插件路由/Modules/DemoPlugin1/plugin1/helloworld
, 页面正常显示了。
总结
本篇中,我为大家演示了如何在运行时启用一个插件,这里我们借助IActionDescriptorChangeProvider
, 让ASP.NET Core在运行时重新加载了控制器,虽然不支持预编译Razor视图的加载,但是我们通过配置原始Razor视图加载的目录规则,同样实现了动态读取视图的功能。
下一篇我将继续将这个项目重构,编写业务模型,并尝试编写插件的安装以及升降级版本的代码。