0
Objective

Create a asp.net core based solution that permits plugins loaded in runtime, way after IServiceCollection/IServiceProvider have been locked down to change.

Issue

IServiceCollection is configured at startup, from which IServiceProvider is developed, then both are locked for change before run is started.

I'm sure there are great reasons to do this....but I rue the day they came up with it being the only way to do things... so:

Attempt #1
  • Was based on using Autofac's ability to make child containers, falling back to parent containers for whatever is not specific to the child container,
  • where, right after uploading the new plugin, I create a new ILifetimeScope so that I can add Services given its containerBuilder:
moduleLifetimeScope = _lifetimeScope.BeginLifetimeScope(autoFacContainerBuilder =>
{
  //can add services now
  autoFacContainerBuilder.AddSingleton(serviceType, tInterface);
}

  • save the scope and its Container in a dictionary, against controllerTypes found in the dll, so that:
  • later can use a custom implementation of IControllerActivator to first try with the default IServiceProvider before falling back to try in the child plugin's child container.
  • The upside was, Holy cow, with a bit of hacking around, slowly got Controllers to work, then DI into Controllers, then OData....
  • The downside was that its custom to a specific DI library, and the Startup extensions (AddDbContext, AddOData) were not available as autoFacContainerBuilder doesn't implement IServiceCollection, so it became a huge foray into innards...that sooner or later couldn't keep on being pushed uphill (eg: couldn't figure out how to port AddDbContext)
Attempts #2
  • At startup, save a singleton copy of the original ISourceCollectionin theISourceCollection` (to easily re-get it later)
  • Later, upon loading a new plugin,
  • Clone the original ISourceCollection
  • Add to the clonedServiceCollection new Plugin Services/Controllers found in by Reflection
  • Use standard extension methods to AddDbContext and AddOData, etc.
  • Use a custom implementation of IControllerActivator as per above, falling back to the child IServiceProvider
  • Holy cow. Controllers work, OData works, DbContext works...
  • Hum...it's not working perfectly. Whereas the Controllers and being created new on every request, it's the same DbContext every time, because it's not being disposed, because it's not scoped by some form of scopefactory.
Attempt #3
  • Same thing as #2, but instead of making the IServiceProvider when the module is loaded, now -- in the custom IControllerActivator making a new IServiceProvider on each request.
    • No idea how much memory/time this is wasting, but I'm guessing its ...not brilliant
  • But sure...but I've really just pushed the problem a bit further along, not gotten rid of it:
    • A new IServiceProvider is being created...but nothing is actually disposing of it either.
    • backed by the fact that I'm watching memory usage increase slowly but surely....
Attempt #4
  • Same as above, but instead of creating a new IServiceProvider on every request, I'm keeping the IServiceProvider that i first built when I uploaded the module, but
  • using it to built a new Scope, and get its nested IServiceProvider,
  • hold on to the scope for later disposal.

It's a hack as follows:

public class AppServiceBasedControllerActivator : IControllerActivator {
       public object Create(ControllerContext actionContext)
        {
         ...
         find the cached (ControllerType->module Service Provider)
         ...   
         var scope = scopeDictionaryEntry.ServiceProvider.CreateScope();
         httpController = serviceProvider.GetService(controllerType);
         actionContext.HttpContext.Items["SAVEMEFROMME"] = scope;
         return httpController;
        }
        public virtual void Release(ControllerContext context, object controller)
        {
            var scope = context.HttpContext.Items["SAVEMEFROMME"] as IServiceScope;
            if (scope == null){return;}
            context.HttpContext.Items.Remove("SAVEMEFROMME");
            scope.Dispose(); //Memory should go back down..but doesn't.
            }
        }
}
Attempt #5
  • No idea. Hence this Question.
  • I feel like I'm a little further along...but just not closing the chasm to success.
  • What would you suggest to permit this, in a memory safe way?
Background Musings/Questions in case it helps?
  • As I understand it, the default IServiceProvider doesn't have a notion of child lifespan/containers, like Autofac can create.
  • I see a IServiceScopeFactory makes a new IServiceProvider.
  • I understand there is some middleware (what name?) that invokes IServiceScopeFactory to make a IServiceProvider on every single request (correct?)
    • are these per-request IServiceProviders really separate/duplicate, and don't 'descend' from a parent one and falls back to parent if a asked for a singleton?
  • What is the Middleware doing different to dispose/reduce memory at the end of the call?
  • Should I be thinking about replacing the middleware? But even if it could -- it's so early that I only would have an url, not yet a Controller Type, therefore don't know what Plugin Assembly the Controller came from, therefore don't know what IServiceProvider to use for it...therefore too early to be of use?
Thank you

Getting a real grip on adding plugin sourced scoped services/controllers/DbContexts would be...wow. Been looking for this capability for several months now.

Thanks.

Other Posts
Sky
  • 55
  • 7
  • _"`AddDbContext`, `AddOData` were not available..."_ - AFAIK you are free to call those methods on `IServiceCollection` and use it with Autofac. See the [docs](https://autofac.readthedocs.io/en/latest/integration/aspnetcore.html#asp-net-core-3-0-and-generic-hosting). – Guru Stron Dec 06 '22 at 21:22
  • Maybe an XY-ish question, but my suggestion would be skip ALL of the use of in-built DI, and/or, autofac DI as those were designed for static injection at startup. This might be a good case of roll-your-own. – Kit Dec 06 '22 at 21:30
  • @GuruStron: agreed..you can use AddDbContext/etc. in startup as per docs,-- but after IServiceProvider is closed to change, no. Maybe I wasn't clear that when I was working with Autofac classes, I was registering services within a ILifetimeScope and attaching services directly to an autofacContainerBuilder...ie, skipping the use of building an IServiceContainer...) Later I stopped using those Autofac classes directly, and only used Asp.net core platform wrappers (ie, IServiceCollection, etc.) – Sky Dec 06 '22 at 21:43
  • In your Autofac approach, were you using the `MultitenantContainer` in combination with an `ITenantIdentificationStrategy`? – pfx Dec 06 '22 at 22:38
  • @pfx : I didn't see a direct link with Tenancy? In multitenancy, is it not that all tenancies use the same services but different state (OrgId and maybe connectionstring, etc.)... Whereas in this case the root container may have 300 services, and the child/per-plugin containers, add maybe 5 more, 7 in the next, etc. – Sky Dec 07 '22 at 00:33
  • Your question is fairly wide ranging. I'm wondering, though, whether you can go for static constructions of factories. The factory(ies) can then do the heavy lifting at runtime, responding to or using context to do what you need. You'd probably have to dive in a bit to "lifetime" of the objects and look into each of the extension methods as these methods sometimes do a bit more than construction and configuration... – Kit Dec 07 '22 at 01:25
  • How do you load the plugins in runtime? – Ruikai Feng Dec 07 '22 at 08:20
  • @sky IMHO the name of the `MultitenancyContainer` is not the best one. It manages a separate container for anything that you want to call a 'tenant'; that might also be a plugin. It has automatic fallback to the root container. Unfortunately, your question doesn't include enough details in where you got stuck in your #1 approach. – pfx Dec 07 '22 at 10:51

0 Answers0