0

I have two controller wih the same name (let's say it's XController) in two different assemblies (which are in two different projects).

One just return mock data and the second is the real deal.

If I try to access the controller http(s)://.../api/X there's of course an exception as two controllers which names are matching the uri are found.

My Goal is to be able to easilly run the web api with one or the other controller being the one reached.

I know I could wrap those class in #if <symbol> ... #endif but I would have to make the changes to both projects everytime I want to switch from one XController to the other.

I guess I could work something out of the IHttpControllerSelector but maybe that would be far fetched (maybe not, I'm asking).

How can I achieve this?

0

2 Answers 2

1

The implementation suggested by Microsoft is to use a custom route that involves "namespaces" and uses a custom configuration of IHttpControllerSelector (just as you had suggested in your question). This code is directly derived from the source code linked, I just put it here as a direct reference to any future passerbys:

    // used in your WebApiConfig class
    config.Routes.MapHttpRoute(
        name: "DefaultApi",
        routeTemplate: "api/{namespace}/{controller}/{id}",
        defaults: new { id = RouteParameter.Optional }
    );

    config.Services.Replace(typeof(IHttpControllerSelector), new NamespaceHttpControllerSelector(config));

And the implementation of NamespaceHttControllerSelector :

public class NamespaceHttpControllerSelector : IHttpControllerSelector
{
    private const string NamespaceKey = "namespace";
    private const string ControllerKey = "controller";

    private readonly HttpConfiguration _configuration;
    private readonly Lazy<Dictionary<string, HttpControllerDescriptor>> _controllers;
    private readonly HashSet<string> _duplicates;

    public NamespaceHttpControllerSelector(HttpConfiguration config)
    {
        _configuration = config;
        _duplicates = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
        _controllers = new Lazy<Dictionary<string, HttpControllerDescriptor>>(InitializeControllerDictionary);
    }

    private Dictionary<string, HttpControllerDescriptor> InitializeControllerDictionary()
    {
        var dictionary = new Dictionary<string, HttpControllerDescriptor>(StringComparer.OrdinalIgnoreCase);

        // Create a lookup table where key is "namespace.controller". The value of "namespace" is the last
        // segment of the full namespace. For example:
        // MyApplication.Controllers.V1.ProductsController => "V1.Products"
        IAssembliesResolver assembliesResolver = _configuration.Services.GetAssembliesResolver();
        IHttpControllerTypeResolver controllersResolver = _configuration.Services.GetHttpControllerTypeResolver();

        ICollection<Type> controllerTypes = controllersResolver.GetControllerTypes(assembliesResolver);

        foreach (Type t in controllerTypes)
        {
            var segments = t.Namespace.Split(Type.Delimiter);

            // For the dictionary key, strip "Controller" from the end of the type name.
            // This matches the behavior of DefaultHttpControllerSelector.
            var controllerName = t.Name.Remove(t.Name.Length - DefaultHttpControllerSelector.ControllerSuffix.Length);

            var key = String.Format(CultureInfo.InvariantCulture, "{0}.{1}", segments[segments.Length - 1], controllerName);

            // Check for duplicate keys.
            if (dictionary.Keys.Contains(key))
            {
                _duplicates.Add(key);
            }
            else
            {
                dictionary[key] = new HttpControllerDescriptor(_configuration, t.Name, t);  
            }
        }

        // Remove any duplicates from the dictionary, because these create ambiguous matches. 
        // For example, "Foo.V1.ProductsController" and "Bar.V1.ProductsController" both map to "v1.products".
        foreach (string s in _duplicates)
        {
            dictionary.Remove(s);
        }
        return dictionary;
    }

    // Get a value from the route data, if present.
    private static T GetRouteVariable<T>(IHttpRouteData routeData, string name)
    {
        object result = null;
        if (routeData.Values.TryGetValue(name, out result))
        {
            return (T)result;
        }
        return default(T);
    }

    public HttpControllerDescriptor SelectController(HttpRequestMessage request)
    {
        IHttpRouteData routeData = request.GetRouteData();
        if (routeData == null)
        {
            throw new HttpResponseException(HttpStatusCode.NotFound);
        }

        // Get the namespace and controller variables from the route data.
        string namespaceName = GetRouteVariable<string>(routeData, NamespaceKey);
        if (namespaceName == null)
        {
            throw new HttpResponseException(HttpStatusCode.NotFound);
        }

        string controllerName = GetRouteVariable<string>(routeData, ControllerKey);
        if (controllerName == null)
        {
            throw new HttpResponseException(HttpStatusCode.NotFound);
        }

        // Find a matching controller.
        string key = String.Format(CultureInfo.InvariantCulture, "{0}.{1}", namespaceName, controllerName);

        HttpControllerDescriptor controllerDescriptor;
        if (_controllers.Value.TryGetValue(key, out controllerDescriptor))
        {
            return controllerDescriptor;
        }
        else if (_duplicates.Contains(key))
        {
            throw new HttpResponseException(
                request.CreateErrorResponse(HttpStatusCode.InternalServerError,
                "Multiple controllers were found that match this request."));
        }
        else
        {
            throw new HttpResponseException(HttpStatusCode.NotFound);
        }
    }

    public IDictionary<string, HttpControllerDescriptor> GetControllerMapping()
    {
        return _controllers.Value;
    }
}
Sign up to request clarification or add additional context in comments.

5 Comments

This breaks using any version of Swagger just like other implementations of this. Microsoft also recommends not doing this in this blog post blogs.msdn.microsoft.com/webdev/2014/08/22/… I've yet to find a way to have multiple controllers with the same name in different namespaces and have swagger still work. Still on the hunt. The solution here forums.asp.net/t/… has you put the namespaces as part route setup in Register but it has problems with Swagger as well.
To that point I'd ask what you did from the Swagger side of things, and whether or not you attempted to fix swagger's API discovery mechanism or if it failed out of the box and that's what you meant
It failed out of the box, but I did attempt to find a resolution to that but have not been successful. Making the custom selector appears to be breaking the ApiExplorer so it's not really a case of breaking Swagger it's breaking something else that Swagger relies on working.
Specifically, it's breaking GetControllerMapping as it's not returning anything that is a duplicate. Rather than return 1 controller when there are duplicates it returns none when there are duplicates. I started looking into making an override for that method but couldn't find any example of what I needed to do to determine namespace and controller and how it should be returned so that Swagger could document it correctly.
I think debugging through the custom controller selector (somewhat like the code above) and finding where you plug it into your Swagger config is going to be necessary when doing things this way, unfortunately.
0

I've come with this soilution but maybe it has some unexcpected side effets I ignore. Can somebody tell me if anything is wrong with it?

    protected void Application_Start()
    {
        var specificControllerTypes = new[]
        {
            typeof(Mocks.XController)
        };
        var config = GlobalConfiguration.Configuration;
        config.Services.Replace(
            typeof(IHttpControllerTypeResolver),
            new DefaultHttpControllerTypeResolver(type => 
                type.IsVisible &&
                !type.IsAbstract &&
                typeof(IHttpController).IsAssignableFrom(type) &&
                type.Name.EndsWith(DefaultHttpControllerSelector.ControllerSuffix) &&
                !specificControllerTypes.Any(t => t != type && t.Name == type.Name)
            )
        );

Comments

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.