5

How do I register a different service implementation within a scope using miscrosoft's default DI mechanism?

My case is this:

I have a service (let's call it MyJobService) that depends on context values (logged in user and some other information). I have registered another service (UserSessionProvider) that reads the data needed from the current HttpContext. But my initial service may also run in a background job, where HttpContext does not exist, but the job is fired from within a web request.

So I would like to have a second implementation of UserSessionProvider where the information of current user is not read from the http context but it would be passed as readonly data to the service implementation and then I would use this instance of UserSessionProvider as the implementation within the created scope.

public IActionResult ControllerMethod( [FromService] IUserSessionProvider sessionProvider, // this instance reads from http context [FromServices] IServiceProvider sp) { var staticSessionProvider = new StaticDataSessionProvider( // this instance uses what you pass to the constructor userName: sessionProvider.userName, userData: sessionProvider.userData ); ExecuteInBackground(()=>{ using(var scope = sp.CreateScope()){ scope.AddScoped<IUserSessionProvider>(staticSessionProvider); // can I do that? var myJob=scope.ServiceProvider.GetService<MyJobService>(); // here 'myJob' would use the staticDataSessionProvider instance myJob.Run(); } }); } 

2 Answers 2

4

How do I register a different service implementation within a scope using miscrosoft's default DI mechanism?

You can't. At the end of the startup process, the container is created, and its set of registrations is fixed. This is a good thing, because allowing to change the container midway can lead to very unfortunate consequences (as is discussed in this documentation section of this competing DI Container). You'll have to come up with another solution.

For instance, you can create a proxy implementation and use that as a stand in:

// Proxy implementation that dispatches to the real IUserSessionProvider public sealed class OverwritableUserSessionProvider : IUserSessionProvider { // NOTE: Depends on the UserSessionProvider implementation public OverwritableUserSessionProvider(UserSessionProvider provider) { this.Provider = provider; } // Allows replacing the dependency public IUserSessionProvider Provider { get; set; } // Implement IUserSessionProvider public string UserName => this.Provider.UserName; public UserData UserData => this.Provider.UserData; } 

This proxy and the real provider can be registered as follows:

// Register both the 'real' provider and the proxy. services.AddScoped<UserSessionProvider>(); services.AddScoped<OverwritableUserSessionProvider>(); // Register the proxy as IUserSessionProvider. services.AddScoped<IUserSessionProvider>( sp => sp.GetRequiredService<OverwritableUserSessionProvider>()); 

This allows you to implement your ControllerMethod as follows:

var staticSessionProvider = new StaticDataSessionProvider( userName: sessionProvider.userName, userData: sessionProvider.userData); ExecuteInBackground(() => { using (var scope = sp.CreateScope()) { // Resolve the proxy var provider = scope.ServiceProvider .GetRequiredService<OverwritableUserSessionProvider>(); // Replace the default dependency provider.Provider = staticSessionProvider; // Resolve and execute the service as usual. var myJob = scope.ServiceProvider.GetRequiredService<MyJobService>(); myJob.Run(); } }); 

TIP: Extract this logic out of the controller and into a specialized class; this logic is infrastructure and has a different concern compared to the controller.

There are other solutions and variations possible based on the solution above, but the trick here is to always ensure that the container composes the same object graph. Don't make the composed object structure dynamic; don't base it on runtime data.

Sign up to request clarification or add additional context in comments.

6 Comments

Thanks. That's pretty much how I imagined it should be implemented if DI did not support that scenario. I will try it and come back!
I understand that it is better to lock the registrations before first use, but wouldn't be more convenient, just for scoped services, to be able to overwrite registration when a new scope is created? I mean: create scope->override registrations->lock registrations within thar scope. If you need another override, create another nested scope. And all this, just for the scoped services.
Thanks, I'll close my question as a dupe. Perhaps it's obvious, but still worth emphasizing that this only works for scoped services, doing it on a singleton would be a bad idea. :)
FYI Autofac allows precisely this feature. Take a dependency on ILifetimeScope. Call BeginScope and pass in a delegate which allows further registrations just for that scope. This is immensely useful for situations where a new scope is needed with a different dependency.
@Timo, for the past years, the Autofac maintainers are slowly moving away from this design with the goal of ending up with a container instance that can't be changed at runtime, because of the issues that arise as explained in my answer. If you're currently depending on this feature in Autofac, be aware that, in the future, you might not be able to upgrade to the latest version of Autofac.
|
0

I think there is a simpler solution than what Steven suggested and more general (for all similar cases)...

You only need to change two lines in the source code:

  1. when registering services, add overrideability
using OverridableDependencyInjection; // + nuget package var provider = new ServiceCollection() .AddScoped<UserSessionProvider>() // + Adds override capability for UserSessionProvider .AddOverridability(typeof(UserSessionProvider)) .BuildServiceProvider(); 
  1. override the service implementation inside the scope
public IActionResult ControllerMethod( [FromService] IUserSessionProvider sessionProvider, [FromServices] IServiceProvider sp) { var staticSessionProvider = new StaticDataSessionProvider( userName: sessionProvider.userName, userData: sessionProvider.userData ); ExecuteInBackground(()=>{ using(var scope = sp.CreateScope()){ // Override the implementation scope.Override<UserSessionProvider>(staticSessionProvider); var myJob=scope.ServiceProvider.GetService<MyJobService>(); myJob.Run(); } }); } 

1 Comment

If you want to promote the package you just created, remember to actually link to your package. And unless you want the question to be deleted as spam, make it clear you're the creator. That package is so new, with only a couple of downloads when you first posted this answer, it's very hard to convince anyone you aren't the author

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.