Home > Mobile >  How to dynamically add a controller in a ASP.NET Core 6 MVC application
How to dynamically add a controller in a ASP.NET Core 6 MVC application

Time:11-24

I need to dynamically creates controllers in a ASP.NET Core 6 MVC application.

I found some way to somewhat achieve this but not quite.

I'm able to dynamically add my controller but somehow it reflects only on the second request.

So here is what I do: first I initialize my console app as follows:

using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Mvc.ApplicationParts;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.AspNetCore.Mvc.Infrastructure;

namespace DynamicControllerServer
{
    internal class Program
    {
        static void Main(string[] args)
        {
            var builder = WebApplication.CreateBuilder(args);

            builder.Services.AddControllers();

            ApplicationPartManager partManager = builder.Services.AddMvc().PartManager;

            // Store thePartManager in my Middleware to be able to add controlelr after initialization is done
            MyMiddleware._partManager = partManager;

            // Register controller change event
            builder.Services.AddSingleton<IActionDescriptorChangeProvider>(MyActionDescriptorChangeProvider.Instance);
            builder.Services.AddSingleton(MyActionDescriptorChangeProvider.Instance);

            var app = builder.Build();

            app.UseAuthorization();
            app.MapControllers();

            // Add Middleware which is responsible to cactn the request and dynamically add the missing controller
            app.UseMiddleware<MyMiddleware>();

            app.RunAsync();

            Console.WriteLine("Server has been started successfully ...");

            Console.ReadLine();
        }
    }
}

Then my middleware looks like this: it basically detects that there is the "dynamic" keyword in the url. If so, it will load the assembly containing the DynamicController:

using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc.ApplicationParts;
using System;
using System.Reflection;

namespace DynamicControllerServer
{
    public class MyMiddleware
    {
        public RequestDelegate _next { get; }
        private string dllName = "DynamicController1.dll";
        static public ApplicationPartManager _partManager = null;

        public MyMiddleware(RequestDelegate next)
        {
            _next = next;
        }

        public async Task Invoke(HttpContext httpContext)
        {

            if (httpContext.Request.Path.HasValue)
            {
                var queryParams = httpContext.Request.Path.Value;
                if(httpContext.Request.Path.Value.Contains("api/dynamic"))
                {
                    // Dynamically load assembly 
                    Assembly assembly = assembly = Assembly.LoadFrom(@"C:\Temp\"   dllName);

                    // Add controller to the application
                    AssemblyPart _part = new AssemblyPart(assembly);
                    _partManager.ApplicationParts.Add(_part);

                    // Notify change
                    MyActionDescriptorChangeProvider.Instance.HasChanged = true;
                    MyActionDescriptorChangeProvider.Instance.TokenSource.Cancel();
                }
            }

            await _next(httpContext); // calling next middleware

        }
    }
}

The ActionDescriptorChange provider looks like this:

using Microsoft.AspNetCore.Mvc.Infrastructure;
using Microsoft.Extensions.Primitives;

namespace DynamicControllerServer
{
    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);
        }
    }
}

Dynamic controller is in separate dll and is very simple:

using Microsoft.AspNetCore.Mvc;

namespace DotNotSelfHostedOwin
{
    [Route("api/[controller]")]
    [ApiController]
    public class DynamicController : ControllerBase
    {
        public string[] Get()
        {
            return new string[] { "dynamic1", "dynamic1", DateTime.Now.ToString() };
        }
    }
}

Here are the packages used in that project:

<PackageReference Include="Microsoft.AspNetCore" Version="2.2.0" />
<PackageReference Include="Swashbuckle.AspNetCore" Version="6.2.3" />

This works "almost" fine ... when first request is made to:

https://localhost:5001/api/dynamic

then it goes in the middleware and load the assembly, but returns a 404 error.

Then second request will actually work as expected:

enter image description here

Second request returns the expected result:

enter image description here

I must doing it wrong and probably my middleware is executed too late in the flow to reflect the dynamic controller right away.

Question is: what should be the proper way to achieve this?

Second question I have is say now the external dll holding our dynamic controller is updated.

How can I reload that controller to get the new definition?

Any help would be appreciated

Thanks in advance

Nick

CodePudding user response:

I have done a similar solution (used for managing a web app plugins) with some differences that may help you:

  1. List all the external assemblies in a config file or appsettings.json so all the dll names and/or addresses are known at startup

  2. Instead of registering controllers when they are called, register them at program.cs/start up :

    //Foreah dllName from settings file
    var assembly = Assembly.LoadFrom(@"Base address" dllNameLoadedFromSettings);
    var part = new AssemblyPart(assembly); services.AddControllersWithViews() .ConfigureApplicationPartManager(apm => apm.ApplicationParts.Add(part)); // Any other configuration based on the usage you want

Second: I usually keep plugin dlls in the bin folder so when using IIS as soon as a dll file in bin is changed the upper-level app is automatically reset. So your second question would be solved too.

CodePudding user response:

Here is the answer to my own question in case it can help somebody out there. It seems building and loading the controller from the middleware will always end up with failure on the first call. This makes sense since we are already in the http pipeline. I end up doing same thing from outside the middleware. Basically my application detect a change in the controller assembly, unload the original assembly and load the new one. You cannot use the Default context since it will not allow reloading different dll for same assembly:

var assembly = AssemblyLoadContext.Default.LoadFromAssemblyPath(assemblyPath); // Produce an exception on updates

To be able to reload new dll for same assembly, I’m loading each controller in its own assembly context. To do that you need to create your own class deriving from AssemblyLoadContext and managing assembly load:

public class MyOwnContext: AssemblyLoadContext
    {
      // You can find lots of example in the net
    }

When you want to unload the assembly, you just unload the context:

MyOwnContextObj.Unload();

Now to add or remove the controller on the fly, you need to keep reference of the PartManager and the ApplicationPart. To add controller

ApplicationPart part = new AssemblyPart(assembly);
_PartManager.ApplicationParts.Add(part);

To remove:

_PartManager.ApplicationParts.Remove(part);

On course once done, still use following piece of code to acknowledge the change:

MyActionDescriptorChangeProvider.Instance.HasChanged = true;
MyActionDescriptorChangeProvider.Instance.TokenSource.Cancel();

That allow updating controller on the fly with no interruption of service. Hope this helps people out there.

  • Related