ipjohnson / Grace

Grace is a feature rich dependency injection container library
MIT License
336 stars 33 forks source link

How to create transient instances of singleton? #282

Closed jods4 closed 3 years ago

jods4 commented 3 years ago

I'm trying to inject a factory that would return additional instances of something that is registered as a SingletonPerScope.

For context, this could be a DbContext in an ASP.NET application. It's typically registered as a SingletonPerScope so that the same context is available everywhere in the application during a single request, but sometimes you want to do some additional work on a separate unit of work / DbContext.

For those cases, I first figured I would inject Func<DbContext> to get a factory. I indeed got a factory, but of course it always return the same value (at least in the same scope), which kind of makes sense.

Then I thought Func<Owned<DbContext>> would be it. I was a bit surprised that it still returns the same instance every time. Looks like it's a "disposal scope" but no more, i.e. it doesn't change how lifetimes and "real" scopes work.

I could work around the problem with Func<Scoped<DbContext>> as my export is a singleton per scope but that probably wouldn't work if it was a real Singleton for example. Also it may have unintended side-effects if DbContext has dependencies that are SingeletonPerScope themselves (-> I wouldn't want new copies of those, only a transient DbContext with everything else as "normal").

Am I missing an API? Is there a way to locate a transient instance of something that is registered with a different lifetime, e.g. a singleton? I thought Owned would mean I take ownership of a specific instance and I'm responsible for disposing it, but it doesn't look that way.

Related: scopes are nested, right? If I inject a Scoped<X> into the method of an ASP.NET controller, this new scope is a child of the current request scope, so when the request ends, X would be automatically disposed even if I do not explicitly dispose of Scoped<X>, is that correct?

Thank you!

ipjohnson commented 3 years ago

Hi @jods4

There isn't really a way to change the lifestyle of a registered type once it's been registered. You could however register it twice once with a key and once without a key (though that would require you locating it directly).

Owned does give you control over the disposal lifetime it doesn't allow you control over the lifestyle. Ultimately I had envisioned it as a way to control the disposal of transients and to keep them from being tracked for disposal in a resolution scope.

Scoped as you noticed scoped created a new scope and then resolves your instance from said scope then requires you to dispose of Scoped wrapper yourself.

Normally the way I handle a situation like this is have the long running service just create a new scope in a using statement then resolve the type from the new scope (whether it's a grace scope or asp.net di scope doesn't matter).

Grace by default do not track child scopes for performance reason and it's assumed if you're opening a scope you'd close the scope. In the scenario you described you could just add your newly created scope to the request scope and it would dispose of it.

Upside and downside of DI is that there are many way to solve the problem and they all have their pluses and minuses.

jods4 commented 3 years ago

Hi @ipjohnson

Thanks for your insights!

Grace by default do not track child scopes for performance reason and it's assumed if you're opening a scope you'd close the scope.

Noted!

In the scenario you described you could just add your newly created scope to the request scope and it would dispose of it.

How would you do that?

Normally the way I handle a situation like this is have the long running service just create a new scope in a using statement then resolve the type from the new scope

It's what I went for. As I said it's not perfect / exactly the same depending on other dependencies, but in my case it's good enough.

I am not sure which DI framework it was, I think Unity, would allow you to create factories of transient instances, regardless of how it would be normally configured. Something like:

ctor(
  Thing x, // regular inject, e.g. singleton
  Transient<Thing> factory, // factory that produces new transient instance each time, your responsibility
)
{
  // You can get new Things if you need to like so:
  using var y = factory.Create();
  using var z = factory.Create();
}

Somehow I think I thought Owned was similar.

Anyway no need for anything, I have plenty of options. In addition to creating a scope, EF also has a DbContextFactory that you can register for that purpose I suppose; or I could inject DbContextOptions and just go for a new myself; or I could register my own factory, maybe based on the keyed idea you suggested.

Have a nice day!

EDIT: thinking about my options, I think I like the keyed option a lot, because it's the easiest to consume in a method --- if you need just one extra instance, not an arbitrary amount, there is no need to call a factory. Transient instances aren't disposed by Grace, though, it would be my responsibility?

ipjohnson commented 3 years ago

Transients created by Grace will be disposed of when the scope that created it is disposed.

In the scenario you described you could just add your newly created scope to the request scope and it would dispose of it.

How would you do that?

If you're injecting the IExportLocatorScope you can directly call AddDisposable an instance for disposal