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
ILifetimeScopeso 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
IControllerActivatorto 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 asautoFacContainerBuilderdoesn't implementIServiceCollection, 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 portAddDbContext)
Attempts #2
- At startup, save a
singleton copy of the originalISourceCollectionin theISourceCollection` (to easily re-get it later) - Later, upon loading a new plugin,
- Clone the original
ISourceCollection - Add to the
clonedServiceCollectionnew Plugin Services/Controllers found in by Reflection - Use standard extension methods to
AddDbContextandAddOData, etc. - Use a custom implementation of
IControllerActivatoras per above, falling back to the childIServiceProvider - Holy cow. Controllers work, OData works,
DbContextworks... - Hum...it's not working perfectly. Whereas the Controllers and being created new on every request, it's the same
DbContextevery 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
IServiceProviderwhen the module is loaded, now -- in the customIControllerActivatormaking a newIServiceProvideron 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
IServiceProvideris being created...but nothing is actually disposing of it either. - backed by the fact that I'm watching memory usage increase slowly but surely....
- A new
Attempt #4
- Same as above, but instead of creating a new
IServiceProvideron every request, I'm keeping theIServiceProviderthat 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
IServiceProviderdoesn't have a notion of child lifespan/containers, like Autofac can create. - I see a
IServiceScopeFactorymakes a newIServiceProvider. - I understand there is some middleware (what name?) that invokes
IServiceScopeFactoryto make aIServiceProvideron 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?
- are these per-request
- 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
IServiceProviderto 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
- some similarity to:
- Use custom IServiceProvider implementation in asp.net core
- but I don't see how his disposing is any different to what I'm doing, so are they too having memory issues?