Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Child container registration based on route parameters

We have a multi-tennant ASP.NET MVC application that hosts a booking engine for multiple clients. Each of these clients has multiple packages that can influence Unity Container configuration. We are creating a child container per request and registering different interface implementations based on the client and package parameters passed through the route.

Currently we are accomplishing this by doing the following:

  1. Controller has a property ServiceLocator that uses a unity container to resolve dependencies.
  2. Controller gets IUnityContainer injected and assigned to a property.
  3. Controller has a custom ActionFilterAttribute that accesses the controllers unity container, creates a child container, conditionally registers dependency implementations based on client and package route parameters, then assigns this child container to the controller's serviceLocator.
  4. Controller uses serviceLocator on demand to resolve individual dependencies.

This works but is really clumsy and I feel eventually it will be unsustainable. I'm looking for a better solution.

We're stuck on .NET 4.0 at the moment until we wrap up some legacy stuff so I'm targeting Unity 2 specifically.

I've tried creating a custom IDependencyResolver to create the child container and register dependencies based on route parameters storing the container in either Session or in HttpContext items but ran into the null HttpContext problems. Is there any other way to base registrations on the route and have the dependencies injected to the controller constructor?

Eventually I will need a solution for Web API as well.

Edit: Example

public interface IRateService { ... }
public class RemoteRateService : IRateService { ... }
public class LocalRateService : IRateService { ... }

public class CustomDependencyResolver : IDependencyResolver
{
    public object GetService(Type serviceType)
    {
        if(ChildContainer == null)
        {
            ChildContainer = _container.CreateChildContainer();
            var routeData = HttpContext.Current.Request.RequestContext.RouteData.Values;
            if(routeData["client"] == "ClientA")
                ChildContainer.RegisterType<IRateService, RemoteRateService>();
            else
                ChildContainer.RegisterType<IRateService, LocalRateService>();
        }
        return ChildContainer.Resolve(serviceType);
    }
}

public class RateController : Controller
{
    private IRateService _rateService;
    public RateController(IRateService rateService)
    {
        _rateService = rateService;
    }
    ...
}

url: /ClientA/Package1/Rate - RateController gets RemoteRateService
url: /ClientB/Package2/Rate - RateController gets LocalRateService

like image 381
Stacy Gay Avatar asked Oct 21 '22 14:10

Stacy Gay


1 Answers

Abatishchev answered my question in the comments by pointing me in the right direction with IControllerFactory. For the random google searches that end here, here is the basic setup I used by inheriting from DefaultControllerFactory:

public class UnitySessionControllerFactory : DefaultControllerFactory
{
    private const string HttpContextKey = "Container";
    private readonly IUnityContainer _container;

    public UnitySessionControllerFactory (IUnityContainer container)
    {
        _container = container;
    }

    protected IUnityContainer GetChildContainer(RequestContext requestContext)
    {
        var routeData = requestContext.RouteData.Values
        ?? new RouteValueDictionary();
        var clientName = routeData["clientName"] as string;
        var packageId = routeData["packageID"] as int?;

        if (clientName == null)
            throw new ArgumentException("ClientName not included in route parameters");

        var childContainer = requestContext.HttpContext.Session[clientName + HttpContextKey] as IUnityContainer;

        if (childContainer != null)
            return childContainer;

        requestContext.HttpContext.Session[clientName + HttpContextKey] = childContainer = _container.CreateChildContainer();

        var moduleLoader = childContainer.Resolve<ModuleLoader>();

        moduleLoader.LoadModules(clientName, packageId);

        return childContainer;
    }

    public override IController CreateController(RequestContext requestContext, string controllerName)
    {
        var controllerType = GetControllerType(requestContext, controllerName);
        var container = GetChildContainer(requestContext);
        return container.Resolve(controllerType) as IController;
    }

    public override void ReleaseController(IController controller) 
    {
        _container.Teardown(controller);
    }
}

Forgive the use of session here. In the future I will exchange it for HttpContext.Items once I am able to wrangle in our project's use of session.

To enable the custom controller factory I added this line to the Bootstrapper.Initialise() method

ControllerBuilder.Current
    .SetControllerFactory(new UnitySessionControllerFactory(container));
like image 198
Stacy Gay Avatar answered Oct 23 '22 03:10

Stacy Gay