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 asautoFacContainerBuilder
doesn'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 original
ISourceCollectionin the
ISourceCollection` (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
andAddOData
, etc. - Use a custom implementation of
IControllerActivator
as per above, falling back to the childIServiceProvider
- 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 customIControllerActivator
making a newIServiceProvider
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....
- A new
Attempt #4
- Same as above, but instead of creating a new
IServiceProvider
on every request, I'm keeping theIServiceProvider
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 newIServiceProvider
. - I understand there is some middleware (what name?) that invokes
IServiceScopeFactory
to make aIServiceProvider
on every single request (correct?)- are these per-request
IServiceProvider
s 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
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/DbContext
s 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?