How to configure services based on request in ASP.

2020-06-04 08:59发布

问题:

In ASP.NET Core we can register all dependencies during start up, which executed when application starts. Then registered dependencies will be injected in controller constructor.

public class ReportController
{
    private IReportFactory _reportFactory;

    public ReportController(IReportFactory reportFactory)
    {
        _reportFactory = reportFactory;
    }

    public IActionResult Get()
    {
        vart report = _reportFactory.Create();
        return Ok(report);
    }
}

Now I want to inject different implementations of IReportFactory based on data in current request (User authorization level or some value in the querystring passed with an request).

Question: is there any built-in abstraction(middleware) in ASP.NET Core where we can register another implementation of interface?

What is the possible approach for this if there no built-in features?

Update IReportFactory interface was used as a simple example. Actually I have bunch of low level interfaces injected in different places. And now I want that different implementation of those low level interfaces will be injected based on request data.

public class OrderController
{
    private IOrderService _orderService;

    public OrderController(IOrderService orderService)
    {
        _orderService = orderService;
    }

    public IActionResult Create()
    {
        var order = _orderService.Create();
        return Ok(order);
    }
}    

 public class OrderService
 {
    private OrderBuilder _orderBuilder;
    private IShippingService _shippingService; // This now have many different implementations

    public OrderService(
        OrderBuilder _orderBuilder,
        IShippingService _shippingService)
    {
        _orderService = orderService;
        _shippingService = shippingService;
    }

    public Order Create()
    {
        var order = _orderBuilder.Build();
        var order.ShippingInfo = _shippingService.Ship();
        return order;
    }
 }  

Because we know which implementation we need to use on entry point of our application (I think controller action can be considered as entry point of application), we want inject correct implementation already there - no changes required in already existed design.

回答1:

No, you can't. The IServiceCollection is populated during application startup and built before Configure method is called. After that (container being built), the registrations can't be changed anymore.

You can however implement an abstract factory, be it as factory method or as an interface/class.

// Its required to register the IHttpContextAccessor first
services.AddSingleton<IHttpContextAccessor, HttpContextAccessor>();
services.AddScoped<IReportService>(provider => {
    var httpContext = provider.GetRequired<IHttpContextAccessor>().HttpContext;

    if(httpContext.User.IsAuthorized) 
    {
        return new AuthorizedUserReportService(...);
        // or resolve it provider.GetService<AuthorizedUserReportService>()
    }

    return new AnonymousUserReportService(...);
    // or resolve it provider.GetService<AnonymousUserReportService>()
});

Alternatively use an abstract factory class



回答2:

I'm afraid you can not directly acheive the goal via simple dependency injection , as the the dependency injection configured at Startup stage , in other words , all services and implementions has been configured before a request comming .

However , you can inject a Create Service delegate so that can we create the required service implemention instance in runtime .

For instance , if we have a IReportFactory Interface and two implementions as blew :

public interface IReportFactory
{
    object Create();
}

public class ReportFactory1 : IReportFactory
{
    public object Create()
    {
        return new { F = 1, };
    }
}
public class ReportFactory2 : IReportFactory {
    public object Create()
    {
        return new { F = 2, }; 
    }
}

As we want to get the required implemention in future , we need to register the Implementions first .

services.AddScoped<ReportFactory1>();
services.AddScoped<ReportFactory2>();

and here's where the magic happens :

  1. We don't register a IReportFactory
  2. We just add a Func<HttpContext,IReportFactory> instead , which is a CreateReportFactoryDelegate

    public delegate IReportFactory CreateReportFactoryDelegate(Microsoft.AspNetCore.Http.HttpContext context);

We need add the CreateReportFactoryDelegate to servies too.

services.AddScoped<CreateReportFactoryDelegate>(sp => {
    // return the required implemention service by the context;
    return context => {
        // now we have the http context ,
        // we can decide which factory implemention should be returned;
        // ...
        if (context.Request.Path.ToString().Contains("factory1")) {
            return sp.GetRequiredService<ReportFactory1>();
        }
        return sp.GetRequiredService<ReportFactory2>();
    };
});

Now , we can inject a CreateReportFactoryDelegate into controller :

public class HomeController : Controller
{
    private CreateReportFactoryDelegate _createReportFactoryDelegate;

    public HomeController(CreateReportFactoryDelegate createDelegate) {
        this._createReportFactoryDelegate = createDelegate;
        // ...
    }

    public async Task<IActionResult> CacheGetOrCreateAsync() {

        IReportFactory reportFactory = this._createReportFactoryDelegate(this.HttpContext);
        var x=reportFactory.Create();

        // ...
        return View("Cache", cacheEntry);
    }
}