1
votes

I'm using asp.net mvc 4 to develop a multi-tenant mvc application.

I am using Autofac for the IOC container and have configured controllers to be registered for each client in different assemblies.

Autofac will switch out which controller it returns when asked to resolve depending on the current client context, which is determineid by looking at the route data.

I am getting an exception

Multiple types were found that match the controller named 'Home'.

Which would seem to indicate that Autofac is returning more than one match, however on closer inspection it seems that MVC is not even calling Autofac for controller resolution.

I can see a call that asks for an IControllerFactory from the DependencyResolver but never for the controller itself.

Do I need to implement my own controller factory ontop of using the dependency resolver?

I have had a look through the source code of mvc and from what I can tell the DefaultControllerFactory uses the dependencyresolver and should resolve the controllers but I'm having trouble debugging into it to see exactly whats happenning.

The tenant dlls are not referenced by the main website but copied in after build.

My global.asax looks as follows: // First, create your application-level defaults using a standard // ContainerBuilder, just as you are used to. var builder = new ContainerBuilder(); var appContainer = builder.Build();

// Once you've built the application-level default container, you
// need to create a tenant identification strategy. The details of this
// are discussed later on.
var tenantIdentifier = new RouteDataTenantIdentificationStrategy();

// Now create the multitenant container using the application
// container and the tenant identification strategy.
var mtc = new MultitenantContainer(tenantIdentifier, appContainer);

// Configure the overrides for each tenant by passing in the tenant ID
// and a lambda that takes a ContainerBuilder.

var assemblies = AppDomain.CurrentDomain.GetAssemblies()
    .AsQueryable()
    .Where(a => a.IsDefined(typeof (PluginAssemblyAttribute), false));


foreach (var assembly in assemblies)
{
    var pluginSpecification =
        assembly.GetCustomAttributes(typeof(PluginAssemblyAttribute), false)
            .OfType<PluginAssemblyAttribute>()
            .Single();
    var assembly1 = assembly;
    mtc.ConfigureTenant(pluginSpecification.Tenant, 
        b => b.RegisterControllers(assembly1));
}

DependencyResolver.SetResolver(new AutofacDependencyResolver(mtc));

The tenant identification strategy looks as follows:

public class RouteDataTenantIdentificationStrategy 
    : ITenantIdentificationStrategy
{
    public bool TryIdentifyTenant(out object tenantId)
    {
        tenantId = null;
        var context = HttpContext.Current;
        try
        {
            if (context == null || context.Request == null)
            {
                return false;
            }
        }
        catch (HttpException)
        {
            // This will happen at application startup in MVC3
            // integration since the ILifetimeScopeProvider tries
            // to be resolved from the container at the point where
            // a new AutofacDependencyResolver is created.
            tenantId = null;
            return false;
        }

        var routeData = RouteTable.Routes.GetRouteData(
            new HttpContextWrapper(context));

        if (routeData != null)
            tenantId = routeData.Values.ContainsKey("tenant") ? 
                routeData.Values["tenant"] : null;

        return tenantId != null;
    }
}

EDIT: The full exception is

The request for 'Home' has found the following matching controllers:
MultiTenantViewEngine.Web.Controllers.HomeController
MultiTenantViewEngine.Web.Tenant1.Controllers.HomeController]
   System.Web.Mvc.DefaultControllerFactory.GetControllerTypeWithinNamespaces(RouteBase route, String controllerName, HashSet`1 namespaces) +230
   System.Web.Mvc.DefaultControllerFactory.GetControllerType(RequestContext requestContext, String controllerName) +833
   System.Web.Mvc.DefaultControllerFactory.System.Web.Mvc.IControllerFactory.GetControllerSessionBehavior(RequestContext requestContext, String controllerName) +196
   System.Web.Mvc.MvcRouteHandler.GetSessionStateBehavior(RequestContext requestContext) +267
   System.Web.Mvc.MvcRouteHandler.GetHttpHandler(RequestContext requestContext) +61
   System.Web.Mvc.MvcRouteHandler.System.Web.Routing.IRouteHandler.GetHttpHandler(RequestContext requestContext) +44
   System.Web.Routing.UrlRoutingModule.PostResolveRequestCache(HttpContextBase context) +352
   System.Web.Routing.UrlRoutingModule.OnApplicationPostResolveRequestCache(Object sender, EventArgs e) +144
   System.Web.SyncEventExecutionStep.System.Web.HttpApplication.IExecutionStep.Execute() +239
   System.Web.HttpApplication.ExecuteStep(IExecutionStep step, Boolean& completedSynchronously) +114

Looking at this in more depth it would seem to indicate that this error happens because GetControllerTypeWithinNamespaces() is returning more than one namespace.

Is there a way to overcome this?

1
What does your RouteDataTenantIdentificationStrategy look like, and have you put a breakpoint in it to see if it is being called when the controller is resolved? - Simon C
just added it, will double check to see if its being called - Daniel Powell
it appears it is being called and returning the correct tenantid - Daniel Powell
If you haven't yet, you should check out Zack Owens' piece on this. weblogs.asp.net/zowens/archive/2010/05/26/… - unforgiven1987
Yeah I saw that probably is the route I will end up going down - Daniel Powell

1 Answers

0
votes

You have to specify namespace in all of your routes:

routes.MapRoute(
"Default",                                              // Route name
    "{controller}/{action}/{id}",                           // URL with parameters
    new { controller = "Home", action = "Index", id = "" },  // Parameter defaults
    "Some.NameSpace.To.Controllers" // required
);

If you would like to use the home controller in the tenant DLL: either redirect to it from the main site HomeController or create your custom route (inherit from Route) and do the namespace selection in it.