ASP.NET How to define controller type dynamically at runtime in core MVC

Time:2020-7-26

Yesterday, a friend asked me a question on wechat: he hoped to achieve the goal of ASP.NET The extension of core MVC application, such as uploading a C ා script during the running process of the program, registering the controller type defined in it to the application, and asking if I have a good solution. I was outside at that time, and it was not convenient to reply, so I only gave him two interfaces / types: iactiondescriptorprovider and applicationpartmanager. This is a very interesting question, so I realized this requirement in two ways when I went home. The source code is downloaded from here.

1、 Effect achieved

Let’s take a look at the results. The following is the home page of an MVC application. We can define an effective controller type by writing C ා code in the text box, and then click the “register” button. The defined controller type will be automatically registered in the MVC application

Since we adopt the protocol route for template “{controller} / {action}”, we can access the action method bar defined in foocontroller in the above figure by using the path “/ foo / bar”. The figure below confirms this point.

2、 Dynamic compilation of source code

In order to implement the “dynamic registration for controller type” as shown above, the first problem to be solved is to provide dynamic compilation of source code. We know that Roslyn can be used to solve this problem. Specifically, we define the following icompiler interface. Its compile method will compile the source code provided by the parameter sourcecode. This method returns the assembly generated by dynamic compilation of source code, and its second parameter represents the referenced assembly.


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

The compiler type shown below is the default implementation of the icompiler interface.


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

3、 Custom iationdescriptorprovider

After solving the problem of dynamic compilation for providing source code, we can obtain the controller type to be registered, so how to register it to MVC application? To answer this question, we need to have a general understanding of the implementation principle of the MVC framework ASP.NET The core processes requests through a pipeline composed of servers and some middleware. MVC framework is built on the endpoint routing system composed of endpoint routing middleware and endpoint middleware. This routing system maintains a group of routing endpoints, which is represented by a mapping between a route pattern and a corresponding processor (represented by a requestdelegate delegate).

Since requests for MVC applications always point to an action, the routing integration mechanism provided by MVC framework is embodied in creating one or more endpoints for each action (the same action method can register multiple routes). The routing endpoints for action methods are constructed from the actiondescriptor object that describes the action method. As for the actiondescriptor object, it is provided through a set of registered iactiondescriptorprovider objects, so our problem is solved: we can resolve the legal action method from the dynamically defined controller type by registering the custom iactiondescriptor provider, and create the corresponding actiondescriptor object.

How to create an actiondescriptor? We can think of a simple way to call the build method as follows. There are two problems in calling this method: first, controlleractiondescriptorbuilder is an internal type. We specify to call this method in a reflective way. Second, this method accepts a parameter of type applicationmodel.


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

The application model type involves a big topic: MVC application model. At present, we only focus on how to create this object. The application model object representing the MVC application model is created through the corresponding factory applicationmodelfactory. The factory will be automatically registered into the dependency injection framework of MVC applications, but it is still an internal (internal) type, so it has to be reflected.


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

We define the following dynamic actionprovider types to implement the iactiondescriptorprovider interface. The conversion from the provided source code to the actiondescriptor list is embodied in the addcontrollers method: it compiles the source code with icompiler object, resolves the valid controller type in the generated assembly, and then creates the application model object representing the application model by using the applicationmodelfactory, and the latter calls the controlleractiondescriptorbuilder’s The static method build creates actiondescriptor objects that describe all action methods.


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

4、 Make the application aware of change

Dynamic actionprovider solves the conversion from the provided source code to the corresponding actiondescriptor list, but MVC caches the provided actiondescriptor objects by default. If the framework can use the new actiondescriptor object, it needs to tell it that the list of actiondescriptors provided by the current application has changed. This can be achieved by using the custom iactiondescriptorchangeprovider. Therefore, we define the following dynamic changetoken type, which implements the iactiondescriptorchangeprovider interface, and returns ichangetoken object by getchangetoken method to inform MVC framework that the current actiondescriptor has changed. From the implementation code, we can see that when we call the notifychanges method, the state change notification will be sent out.


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

5、 Application construction

So far, the two core types, dynamicactionprovider and dynamicchangetoken provider, have been defined. Next, we register them into the dependency injection framework of MVC applications as follows.


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

Then we define the following homecontroller. The index method for get requests renders the view shown in the figure above. When we click the “register” button, the submitted source code will be processed through the index method for the post request. As shown in the following code fragment, after calling the addcontrollers method of the dynamicactionprovider object with the submitted source code as a parameter, we call the notifychanges method of the dynamicchangetokenprovider object.


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

Here is the definition of 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>

6、 Another way of implementation

Next, we offer a simpler solution. Through the above introduction, we know that the actiondescriptor list used to describe the action method is provided by a group of iactiondescriptorprovider objects. For the MVC programming model for controller (another is the programming model for razor page), the corresponding implementation type is controlleractiondescriptor provider.

When the controlleractiondescriptor provider provides the corresponding actiondescriptor object, it will resolve all controller types from the assembly that is part of the current application. If we can make the integration of programs that are dynamically provided to source code programming as its legitimate component, then the problems we face will be solved naturally. Adding application components is actually very simple. We only need to call the add method of the applicationpartmanager object as follows. To make the MVC framework aware that the list of actiondescriptors provided has changed, we still need to call the notifychanges method of the dynamicchangetokenprovider object.


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

Since we don’t need a custom dynamic action provider, we don’t need the corresponding service registration.


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

7、 This is not a small problem

Some people may think that what we have done above seems to be just some “strange tricks”. In fact, it is not. Here is a major theme of MVC application, which I personally call “dynamic modularization”. For a controller oriented MVC application, controller type is the basic unit of application, so its application model (represented by the application model object mentioned above) presents the following structure: Application > controller > action. If an MVC application needs to be split into multiple independent modules, it means that the controller types need to be defined in different assemblies. In order to integrate these programs into an effective part of the application, the assembly needs to be encapsulated into an application part object and registered with the application part manager. Registration of application components is not static (when the application is started), but dynamic (at any time when the application is running).

From the code provided, the cost of both solutions is very small, but whether we can find a solution depends on whether we understand the architecture design and implementation principle of MVC framework. For a large number of. Net developers, their knowledge field is mostly limited to the understanding of the basic programming model. They may know all the API of controller and the various definition methods of razor view. It is good to be able to skillfully use various filters. But that is not enough.

This is about ASP.NET The article on how to dynamically define the controller type at runtime in core MVC is introduced here, more relevant ASP.NET Core MVC dynamically defines the controller content. Please search the previous articles of developeppaer or continue to browse the related articles below. I hope you can support developeppaer more in the future!

Author: Jiang Jinnan
Wechat public account: Da Nei Lao a
Microblog: www.weibo.com/artech