Add scoped service when creating a new scope

Viewed 3662

Is it possible to add a service to an IServiceProvider when creating a new scope?

E.g. a scope is created like this:

using (var scope = Services.CreateScope())
{
    // Register some service using "AddScoped"
    scope.??.AddScoped<IOrganizationIdProvider, OrganizationIdProvider>((x) => { return new OrganizationIdProvider("dynamic-parameter")});

    // Resolve service
    scope.ServiceProvider.GetService<IOrganizationIdProvider>();

}

The service should be resolved when creating new instances in the scope.

3 Answers

@Nkosi already mentioned that you can't make new registrations in MS.DI once the container has been created. But even if you could, changing the container in-flight is a bad idea, which is why most DI Container do not support such feature. The newest versions of Autofac and Ninject, for instance, recently removed such feature, and the Simple Injector documentation explains why it never had this feature.

But you can achieve what you want by making small changes to your code. Probably the easiest way to implement this is by making changing injecting the dynamic parameter through property injection instead of constructor injection. For instance:

Changes in application code:

public class OrganizationIdProvider : IOrganizationIdProvider
{
    // Only required code change (notice that the interface doesn't change, this
    // property is an implementation detail)
    public string DynamicParameter { get; set; }
}

Configuration of the container:

// Register the implementation by itself to allow it to be requested by your infrastructure
services.AddScoped<OrganizationIdProvider>();

// Register the provider by its interface so it can be injected into application components
services.AddScoped<IOrganizationIdProvider>(c => c.GetService<OrganizationIdProvider>());

Usage:

using (var scope = Services.CreateScope())
{
    // Here we resolve the OrganizationIdProvider *implementation*
    var provider = scope.ServiceProvider.GetRequiredService<OrganizationIdProvider>();

    // Set the runtime parameter
    provider.DynamicParameter = "dynamic-parameter";

    // Resolve service that might have a dependency on IOrganizationIdProvider.
    var handler =
        scope.ServiceProvider.GetRequiredService(typeof(IMessageHandler<DoSomething>));

    // Use the service
    handler.Handle(new DoSomething());
}

There are certainly more options when it comes to dealing with runtime data, but for that I would recommend you to read this series on DI Composition Models.

You can not modify a ServiceProvider, but you can create a child service provider, which includes all services from the parent, instead of creating a scope. Before calling BuildServiceProvider() on the child service provider you can add your services.

  var childServiceCollection = CreateChildServiceCollection(Services, ServiceProvider);

  // Add your services
  childServiceCollection.AddSingleton<IOrganizationIdProvider, OrganizationIdProvider>((x) => { return new OrganizationIdProvider("dynamic-parameter")});

  using var childServiceProvider = childServiceCollection.BuildServiceProvider();

  // Resolve service
  childServiceProvider.GetService<IOrganizationIdProvider>();

And here is how to create the child service collection:

static ServiceCollection CreateChildServiceCollection(IServiceCollection parentServices, IServiceProvider serviceProvider) {
  var childServiceCollection = new ServiceCollection();
  foreach (ServiceDescriptor service in parentServices) {
    if (service.Lifetime == ServiceLifetime.Singleton && !service.ServiceType.IsGenericTypeDefinition) {
      object? serviceInstance = serviceProvider.GetService(service.ServiceType);
      if (serviceInstance != null) {
        childServiceCollection.AddSingleton(service.ServiceType, serviceInstance);
        continue;
      }
    }

    childServiceCollection.Add(service);
  }

  return childServiceCollection;
}

The important thing here is, that you add already created singletons with the created instance and not with the provided factory.

To get the service collection you can inject the service collection as singleton.

What you want to achieve is not possible.

What I reccommend you to do is create a scoped per option that you have. But istead of passing a parameter as a dynamic string you will need to pass a class, one per each dynamic-parameter, not sure if will be good for your use case.

public class IdClass1 : ParentClassWithId
{}

public class IdClass2 : ParentClassWithId
{}
public class ParentClassWithId
{
    public string Id { get; set; }
}

then your IOrganizationIdProvider it will become IOrganizationIdProvider<T> where T is this class.

public interface IOrganizationIdProvider<T>
{
    //code   
}

For reusable code, convert your main OrganizationProvider class to abstract

public abstract class OrganizationProvider<T>
{

}

and then for each one of the "dynamic" you need a class to use it:

public class OrganizationIdProvider1<T> : OrganizationProvider<T>, IOrganizationIdProvider<T>
{
    //Code
}

public class OrganizationIdProvider2<T> : OrganizationProvider<T>, IOrganizationIdProvider<T>
{
    //Code
}

in you initial dependency injection you will need to declare all of the possibilites, on this example:

scope.AddScoped<IOrganizationIdProvider<IdClass2>, OrganizationIdProvider2<IdClass2>>()
.AddScoped<IOrganizationIdProvider<IdClass1>, OrganizationIdProvider1<IdClass1>>();

and you will need some logic around to get the correct Interface, but this may solve your problem.

Related