asp.net core: how to late-upload a Module Assembly, containing an dependency injectable Controller?

Viewed 33

Considering how hard ASP.NET Core is making this, I'm open to being told I'm going about this the wrong way.

Objective

The objective is to be able to permit end users to upload Modules package (eg: nuget package,zip, etc.) containing API Controllers.

Great Lead

The following has great advice on using IActionDescriptorChangeProvider to permit adding Controllers later:

Works (almost)

Based on the above advice, had some partial success...

Works somewhat in the Default AssemblyLoadContext

When loading an external Module into the Default AssemblyLoadContext:

  • Module's ExampleM1Controller (without constructor dependencies) is accessible (impressed!)
  • Module's ExampleM2Controller (with a dependency on a IExampleHService previously registered in IServiceCollection at startup) works (highly impressed!).
  • Module's ExampleM3Controller (with a dependency on a IExampleMServicedefined in the Module, and therefore not registered at Startup/known to IServiceCollection) raises an exception it can't instantiate IExampleMService.

Doesn't work much in a Custom AssemblyLoadContext

Beyond the small issue noted above, the situation is actually a little bit worse...

Because it was uploaded into the Default AssemblyLoadContext, the module can't be unloaded/replaced (starting/stopping the app would be highly disruptive to other users).

If I change the above to load into a Custom AssemblyLoadContext and try again this time:

  • ExampleM1Controller (without dependencies) is accessible
  • ExampleM2Controller (with a dependency on a service defined in Host) raises an exception it can't instantiate the IExampleMService FIXED: SEE EDIT BELOW REGARDING 'PRIVATE' FLAG...
  • ExampleM3Controller (with a dependency on a service defined in Modules) raises an exception it can't instantiate the IExampleHService

So...that's turned out to be useless when the tires are kicked :-(

ServiceCollection is all locked up

So then went back to see if that Startup sequence couldn't be squeezed into delivering more functionality:

            var builder = WebApplication.CreateBuilder(args);

            //Add a service
            builder.Services.AddSingleton<IExampleSharedService, ExampleSharedService>();
            // Add services to the container.
            builder.Services.AddControllersWithViews();
            // Wire up custom Resetter invoked by upload controller.
            AddActionDescriptorChangeProvider(builder);
            //That was the last chance to add anthing before Build is called:
            var app = builder.Build();
            //Works, no problem:
            var x1 = app.Services.GetService<IExampleSharedService>();
            // Adding additional Services after the build event:
            // Will raise Exception
            // Not fixable by setting the flag by refletion, as it's too late.
            //builder.Services.GetType()
            //    .GetProperties(
            //    BindingFlags.Public|BindingFlags.SetProperty|BindingFlags.Instance
            //    )
            //    .Where(x => x.Name == "IsReadOnly")
            //    .First()
            //    .SetValue(builder.Services, false);
            // Without using reflection to re-permit adding services without raising
            // Exception "Cannot modify ServiceCollection after application is built."
            // it's still too late (Host already built, so no longer looking at source info)
            //builder.Services.AddSingleton<ILateService, LateService>();
            // Calling build twice also raises an exception:
            // builder.Build();
            // So...if we load a new DLL into a new AppDomainLoadContext,
            // that contains a Controller
            // that has a dependency on a Service, 
            // how do we teach the DI (IServiceProvider) how to build
            // it, and dependency service first???
            
            // Returns null.
            // ie: Unable to register late Controller or Module defined service:
            var x2 = app.Services.GetService<ILateService>();

A Module Load Event - But how can it be put to use?

Some examples regarding Plugins do exist on the net reference and discuss catching an event when an assembly is loaded, iterating over the Types in the Assembly that match an interface (eg: ICalculator), and instantiating them. But...

a) It's the other way around (we're trying to inject into the domain descriptors about services that already exist in the Host) b) even if we caught the event to teach the app's IServiceCollection about old and new services...it's locked as per above.

Impossible/Wrong way?

So, the fact

  • that ASP.NET Core framework is really designed to lock in services at startup,
  • doesn't seem to have a mechanism to late register them
  • combined with a funny feeling that security -- even if ONLY giving Modules access to very specific Application Layer (ie, API) services -- might not be that hot...

...makes me hesitate and wonder whether I'm going about this all wrong.

  • Yet...isn't there a single website out there that caters for end users to upload new modules to make an ecosystem, no? Not one? Really? Can this kind of thing only be done with a scripted language (eg: PHP), where files trigger an instant update?
  • Or does the website HAVE to be reset to repass 'Go' and build up the IServiceCollection then IServiceProvider. (And that kind of disruption requires the uploaded modules only being loaded into memory at some deferred time -- eg midnight?)
  • THAT sounds SO naff, just hard to believe that would be a recommended approach.
  • Or is it that Modules are kept separate. and client side javascript is used to inject in ux modules at the right place on a page, that call back to their own services, hosted on their own servers, which in turn invoke Host APIs behind the scenes...?

Thanks

Thanks for any and all input leading to making SPA capable webpages+APIs+SPA modular.

Edit

Doesn't solve everything, but I missed something crucial!

As per https://learn.microsoft.com/en-us/dotnet/core/tutorials/creating-app-with-plugin-support

One MUST not forget to mark the source/shared assembly as Private. Once that was changed, the second example started working, as it was ensured that the Controller's Constructor recognised the IExampleHService as being the one defined in the Host assembly -- and not something to look for in the Module directory. Whew. With that solved, only one condition to go -- the third example, trying to resolve a Service that is unknown at Startup - but the IServiceCollection is no longer taking changes.

I think @davidfowl is onto something, and it's time to investigate taking over the controller creator. My current idea is so...yuck...I hesitate to write it down here. But first thoughts on it involves

a) importing the Module b) Using Autofac to create a new scope c) iterating over its Types for valid Interface/Instance types to register in the scope d) Save the scope, associated to the imported Assembly, in a singleton dictionary (!) e) Use a Controller Factory, that uses the Controller Type's Assembly to find the right child scope again, to use it when instantiating the controller.... f) Hope? It's messy as...but if one can't update the root container...worth giving it a shot and reporting back if it worked at all.

0 Answers
Related