欧美bbbwbbbw肥妇,免费乱码人妻系列日韩,一级黄片

ASP.NET Core MVC如何實現(xiàn)運行時動態(tài)定義Controller類型

 更新時間:2020年06月11日 15:04:09   作者:蔣金楠  
這篇文章主要介紹了ASP.NET Core MVC如何實現(xiàn)運行時動態(tài)定義Controller類型,文中通過示例代碼介紹的非常詳細,對大家的學習或者工作具有一定的參考學習價值,需要的朋友們下面隨著小編來一起學習學習吧

昨天有個朋友在微信上問我一個問題:他希望通過動態(tài)腳本的形式實現(xiàn)對ASP.NET Core MVC應用的擴展,比如在程序運行過程中上傳一段C#腳本將其中定義的Controller類型注冊到應用中,問我是否有好解決方案。我當時在外邊,回復不太方便,所以只給他說了兩個接口/類型:IActionDescriptorProvider和ApplicationPartManager。這是一個挺有意思的問題,所以回家后通過兩種方案實現(xiàn)了這個需求。源代碼從這里下載。

一、實現(xiàn)的效果

我們先來看看實現(xiàn)的效果。如下所示的是一個MVC應用的主頁,我們可以在文本框中通過編寫C#代碼定義一個有效的Controller類型,然后點擊“Register”按鈕,定義的Controller類型將自動注冊到MVC應用中

由于我們采用了針對模板為“{controller}/{action}”的約定路由,所以我們采用路徑“/foo/bar”就可以訪問上圖中定義在FooController中的Action方法Bar,下圖證實了這一點。

二、動態(tài)編譯源代碼

要實現(xiàn)如上所示的“針對Controller類型的動態(tài)注冊”,首先需要解決的是針對提供源代碼的動態(tài)編譯問題,我們知道這個可以利用Roslyn來解決。具體來說,我們定義了如下這個ICompiler接口,它的Compile方法將會對參數(shù)sourceCode提供的源代碼進行編譯。該方法返回源代碼動態(tài)編譯生成的程序集,它的第二個參數(shù)代表引用的程序集。

public interface ICompiler
{
  Assembly Compile(string text, params Assembly[] referencedAssemblies);
}

如下所示的Compiler類型是對ICompiler接口的默認實現(xiàn)。

public class Compiler : ICompiler
{
  public Assembly Compile(string text, params Assembly[] referencedAssemblies)
  {
    var references = referencedAssemblies.Select(it => MetadataReference.CreateFromFile(it.Location));
    var options = new CSharpCompilationOptions(OutputKind.DynamicallyLinkedLibrary);
    var assemblyName = "_" + Guid.NewGuid().ToString("D");
    var syntaxTrees = new SyntaxTree[] { CSharpSyntaxTree.ParseText(text) };
    var compilation = CSharpCompilation.Create(assemblyName, syntaxTrees, references, options);
    using var stream = new MemoryStream();
    var compilationResult = compilation.Emit(stream);
    if (compilationResult.Success)
    {
      stream.Seek(0, SeekOrigin.Begin);
      return Assembly.Load(stream.ToArray());
    }
    throw new InvalidOperationException("Compilation error");
  }
}

三、自定義IActionDescriptorProvider

解決了針對提供源代碼的動態(tài)編譯問題之后,我們可以獲得需要注冊的Controller類型,那么如何將它注冊MVC應用上呢?要回答這個問題,我們得對MVC框架的執(zhí)行原理有一個大致的了解:ASP.NET Core通過一個由服務器和若干中間件構成的管道來處理請求,MVC框架建立在通過EndpointRoutingMiddleware和EndpointMiddleare這兩個中間件構成的終結點路由系統(tǒng)上。此路由系統(tǒng)維護著一組路由終結點,該終結點體現(xiàn)為一個路由模式(Route Pattern)與對應處理器(通過RequestDelegate委托表示)之間的映射。

由于針對MVC應用的請求總是指向某一個Action,所以MVC框架提供的路由整合機制體現(xiàn)在為每一個Action創(chuàng)建一個或者多個終結點(同一個Action方法可以注冊多個路由)。針對Action方法的路由終結點是根據(jù)描述Action方法的ActionDescriptor對象構建而成的。至于ActionDescriptor對象,則是通過注冊的一組IActionDescriptorProvider對象來提供的,那么我們的問題就迎刃而解:通過注冊自定義的IActionDescriptorProvider從動態(tài)定義的Controller類型中解析出合法的Action方法,并創(chuàng)建對應的ActionDescriptor對象即可。

那么ActionDescriptor如何創(chuàng)建呢?我們能想到簡單的方式是調(diào)用如下這個Build方法。針對該方法的調(diào)用存在兩個問題:第一,ControllerActionDescriptorBuilder是一個內(nèi)部(internal)類型,我們指定以反射的方式調(diào)用這個方法,第二,這個方法接受一個類型為ApplicationModel的參數(shù)。

internal static class ControllerActionDescriptorBuilder
{
  public static IList<ControllerActionDescriptor> Build(ApplicationModel application);
}

ApplicationModel類型涉及到一個很大的主題:MVC應用模型,目前我們現(xiàn)在只關注如何創(chuàng)建這個對象。表示MVC應用模型的ApplicationModel對象是通過對應的工廠ApplicationModelFactory創(chuàng)建的。這個工廠會自動注冊到MVC應用的依賴注入框架中,但是這依然是一個內(nèi)部(內(nèi)部)類型,所以還得反射。

internal class ApplicationModelFactory
{
  public ApplicationModel CreateApplicationModel(IEnumerable<TypeInfo> controllerTypes);
}

我們定義了如下這個DynamicActionProvider類型實現(xiàn)了IActionDescriptorProvider接口。針對提供的源代碼向ActionDescriptor列表的轉(zhuǎn)換體現(xiàn)在AddControllers方法中:它利用ICompiler對象編譯源代碼,并在生成的程序集中解析出有效的Controller類型,然后利用ApplicationModelFactory創(chuàng)建出代表應用模型的ApplicationModel對象,后者作為參數(shù)調(diào)用ControllerActionDescriptorBuilder的靜態(tài)方法Build創(chuàng)建出描述所有Action方法的ActionDescriptor對象。

public class DynamicActionProvider : IActionDescriptorProvider
{
  private readonly List<ControllerActionDescriptor> _actions;
  private readonly Func<string, IEnumerable<ControllerActionDescriptor>> _creator;

  public DynamicActionProvider(IServiceProvider serviceProvider, ICompiler compiler)
  {
    _actions = new List<ControllerActionDescriptor>();
    _creator = CreateActionDescrptors;

    IEnumerable<ControllerActionDescriptor> CreateActionDescrptors(string sourceCode)
    {
      var assembly = compiler.Compile(sourceCode, 
        Assembly.Load(new AssemblyName("System.Runtime")),
        typeof(object).Assembly,
        typeof(ControllerBase).Assembly,
        typeof(Controller).Assembly);
      var controllerTypes = assembly.GetTypes().Where(it => IsController(it));
      var applicationModel = CreateApplicationModel(controllerTypes);

      assembly = Assembly.Load(new AssemblyName("Microsoft.AspNetCore.Mvc.Core"));
      var typeName = "Microsoft.AspNetCore.Mvc.ApplicationModels.ControllerActionDescriptorBuilder";
      var controllerBuilderType = assembly.GetTypes().Single(it => it.FullName == typeName);
      var buildMethod = controllerBuilderType.GetMethod("Build", BindingFlags.Static | BindingFlags.Public);
      return (IEnumerable<ControllerActionDescriptor>)buildMethod.Invoke(null, new object[] { applicationModel });
    }

    ApplicationModel CreateApplicationModel(IEnumerable<Type> controllerTypes)
    {
      var assembly = Assembly.Load(new AssemblyName("Microsoft.AspNetCore.Mvc.Core"));
      var typeName = "Microsoft.AspNetCore.Mvc.ApplicationModels.ApplicationModelFactory";
      var factoryType = assembly.GetTypes().Single(it => it.FullName == typeName);
      var factory = serviceProvider.GetService(factoryType);
      var method = factoryType.GetMethod("CreateApplicationModel");
      var typeInfos = controllerTypes.Select(it => it.GetTypeInfo());
      return (ApplicationModel)method.Invoke(factory, new object[] { typeInfos });
    }

    bool IsController(Type typeInfo)
    {
      if (!typeInfo.IsClass) return false;
      if (typeInfo.IsAbstract) return false;
      if (!typeInfo.IsPublic) return false;
      if (typeInfo.ContainsGenericParameters) return false;
      if (typeInfo.IsDefined(typeof(NonControllerAttribute))) return false;
      if (!typeInfo.Name.EndsWith("Controller", StringComparison.OrdinalIgnoreCase) && !typeInfo.IsDefined(typeof(ControllerAttribute))) return false;
      return true;
    }
  }

  public int Order => -100;
  public void OnProvidersExecuted(ActionDescriptorProviderContext context) { }
  public void OnProvidersExecuting(ActionDescriptorProviderContext context)
  {
    foreach (var action in _actions)
    {
      context.Results.Add(action);
    }
  }
  public void AddControllers(string sourceCode) => _actions.AddRange(_creator(sourceCode));
}

四、讓應用感知到變化

DynamicActionProvider 解決了將提供的源代碼向?qū)狝ctionDescriptor列表的轉(zhuǎn)換,但是MVC默認情況下對提供的ActionDescriptor對象進行了緩存。如果框架能夠使用新的ActionDescriptor對象,需要告訴它當前應用提供的ActionDescriptor列表發(fā)生了改變,而這可以利用自定義的IActionDescriptorChangeProvider來實現(xiàn)。為此我們定義了如下這個DynamicChangeTokenProvider類型,該類型實現(xiàn)了IActionDescriptorChangeProvider接口,并利用GetChangeToken方法返回IChangeToken對象通知MVC框架當前的ActionDescriptor已經(jīng)發(fā)生改變。從實現(xiàn)實現(xiàn)代碼可以看出,當我們調(diào)用NotifyChanges方法的時候,狀態(tài)改變通知會被發(fā)出去。

public class DynamicChangeTokenProvider : IActionDescriptorChangeProvider
{
  private CancellationTokenSource _source;
  private CancellationChangeToken _token;
  public DynamicChangeTokenProvider()
  {
    _source = new CancellationTokenSource();
    _token = new CancellationChangeToken(_source.Token);
  }
  public IChangeToken GetChangeToken() => _token;

  public void NotifyChanges()
  {
    var old = Interlocked.Exchange(ref _source, new CancellationTokenSource());
    _token = new CancellationChangeToken(_source.Token);
    old.Cancel();
  }
}

五、應用構建

到目前為止,核心的兩個類型DynamicActionProvider和DynamicChangeTokenProvider已經(jīng)定義好了,接下來我們按照如下的方式將它們注冊到MVC應用的依賴注入框架中。

public class Program
{
  public static void Main()
  {

    Host.CreateDefaultBuilder()
      .ConfigureWebHostDefaults(web => web
        .ConfigureServices(svcs => svcs
          .AddSingleton<ICompiler, Compiler>()
          .AddSingleton<DynamicActionProvider>()
          .AddSingleton<DynamicChangeTokenProvider>()
          .AddSingleton<IActionDescriptorProvider>(provider => provider.GetRequiredService<DynamicActionProvider>())
          .AddSingleton<IActionDescriptorChangeProvider>(provider => provider.GetRequiredService<DynamicChangeTokenProvider>())
          .AddRouting().AddControllersWithViews())
        .Configure(app => app
          .UseRouting()
          .UseEndpoints(endpoints => endpoints.MapControllerRoute(
            name: default,
            pattern: "{controller}/{action}"
            ))))
      .Build()
      .Run();
  }
}

然后我們定義了如下這個HomeController。針對GET請求的Index方法會將上圖所示的視圖呈現(xiàn)出來。當我們點擊“Register”按鈕之后,提交的源代碼會通過針對POST請求的Index方法進行處理。如下面的代碼片段所示,在將將提交的源代碼作為參數(shù)調(diào)用了DynamicActionProvider對象的 AddControllers方法之后,我們調(diào)用了DynamicChangeTokenProvider對象的 NotifyChanges方法。

public class HomeController : Controller
{

  [HttpGet("/")]
  public IActionResult Index() => View();

  [HttpPost("/")]
  public IActionResult Index(
    string source,
    [FromServices]DynamicActionProvider actionProvider,
    [FromServices] DynamicChangeTokenProvider tokenProvider)
  {
    try
    {
      actionProvider.AddControllers(source);
      tokenProvider.NotifyChanges();
      return Content("OK");
    }
    catch (Exception ex)
    {
      return Content(ex.Message);
    }
  }
}

如下所示的是View的定義。

<html>
<body>
  <form method="post">
    <textarea name="source" cols="50" rows="10">Define your controller here...</textarea>
    <br/>
    <button type="submit">Register</button>
  </form>
</body>
</html>

六、換一種實現(xiàn)方式

接下來我們提供一種更加簡單的解決方案。通過上面的介紹我們知道,用來描述Action方法的ActionDescriptor列表是由一組IActionDescriptorProvider對象提供的,對于針對Controller的MVC編程模型(另一種是針對Razor Page的編程模型)來說,對應的實現(xiàn)類型為ControllerActionDescriptorProvider。

當ControllerActionDescriptorProvider在提供對應ActionDescriptor對象之前,會從作為當前應用組成部分(ApplicationPart)的程序集中解析出所有Controller類型。如果我們能夠讓動態(tài)提供給源代碼編程生成的程序集成為其合法的組成部分,那么我們面對的問題自然就能迎刃而解。添加應用組成部分其實很簡單,我們只需要按照如下的方式調(diào)用ApplicationPartManager對象的Add方法就可以了。為了讓MVC框架感知到提供的ActionDescriptor列表已經(jīng)發(fā)生改變,我們還是需要調(diào)用DynamicChangeTokenProvider對象的NotifyChanges方法。

public class HomeController : Controller
{

  [HttpGet("/")]
  public IActionResult Index() => View();

  [HttpPost("/")]
  public IActionResult Index(string source,
    [FromServices] ApplicationPartManager manager,
    [FromServices] ICompiler compiler,
    [FromServices] DynamicChangeTokenProvider tokenProvider)
  {
    try
    {
      manager.ApplicationParts.Add(new AssemblyPart(compiler.Compile(source, Assembly.Load(new AssemblyName("System.Runtime")),
        typeof(object).Assembly,
        typeof(ControllerBase).Assembly,
        typeof(Controller).Assembly)));
      tokenProvider.NotifyChanges();
      return Content("OK");
    }
    catch (Exception ex)
    {
      return Content(ex.Message);
    }
  }
}

由于我們不在需要自定義的DynamicActionProvider,自然也就不需要對應的服務注冊了。

public class Program
{
  public static void Main()
  {

    Host.CreateDefaultBuilder()
      .ConfigureWebHostDefaults(web => web
        .ConfigureServices(svcs => svcs
          .AddSingleton<ICompiler, Compiler>()
          .AddSingleton<DynamicChangeTokenProvider>()
          .AddSingleton<IActionDescriptorChangeProvider>(provider => provider.GetRequiredService<DynamicChangeTokenProvider>())
          .AddRouting().AddControllersWithViews())
        .Configure(app => app
          .UseRouting()
          .UseEndpoints(endpoints => endpoints.MapControllerRoute(
            name: default,
            pattern: "{controller}/{action}"
            ))))
      .Build()
      .Run();
  }
}

七、這其實不是一個小問題

有人可能覺得上面我們所做的好像只是一些“奇淫巧計”,其實不然,這里涉及到MVC應用一個重大的主題,我個人將它稱為“動態(tài)模塊化”。對于一個面向Controller的MVC應用來說,Controller類型是應用基本的組成單元,所以其應用模型(通過上面提到的ApplicationModel對象表示)呈現(xiàn)出這樣的結構:Application->Controller->Action。如果一個MVC應用需要拆分為多個獨立的模塊,意味著需要將Controller類型分別定義在不同的程序集中。為了讓這些程序集成為應用的一個有效組成部分,程序集需要封裝成ApplicationPart對象并利用ApplicationPartManager進行注冊。針對應用組成部分的注冊不是靜態(tài)的(在應用啟動的時候進行),而是動態(tài)的(在運行的任意時刻都可以進行)。

從提供的代碼來看,兩種解決方案所需的成本都是很少的,但是能否找到解決方案,取決于我們是否對MVC框架的架構設計和實現(xiàn)原理的了解。對于很大一部分.NET 開發(fā)人員來說,他們的知識領域大都僅限于對基本編程模型的了解,他們可能知道Controller的所有API,也了解各種Razor View的各種定義方式,能夠熟練使用各種過濾器已經(jīng)算是很不錯的了。但是這是不夠的。

到此這篇關于ASP.NET Core MVC如何實現(xiàn)運行時動態(tài)定義Controller類型的文章就介紹到這了,更多相關ASP.NET Core MVC動態(tài)定義Controller內(nèi)容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關文章希望大家以后多多支持腳本之家!

作者:蔣金楠
微信公眾賬號:大內(nèi)老A
微博:www.weibo.com/artech

相關文章

最新評論