locked
Managing API & MVC Controllers with the same Name RRS feed

  • Question

  • User-1946294156 posted

    Have a project where MVC & API Controllers are separated by Namespace in a project.  

    Is there a way to point the routing so that it know to go to one namespace versus the other?

    Wednesday, July 5, 2017 7:44 PM

All replies

  • User475983607 posted

    Have a project where MVC & API Controllers are separated by Namespace in a project.  

    Is there a way to point the routing so that it know to go to one namespace versus the other?

    The convention is to use api/controller in your route or attribute configuration to distinguish an API controller from MVC.  The default API controller template does this for you as does the MVC/API template when the project is first created.  AFAIK, you would need to purposefully change the template code or build from an empty template.

    Consider creating a new MVC/API project and looking at the startup files depending on the type of project you're creating; .NET Core or .NET framework.

    Wednesday, July 5, 2017 7:55 PM
  • User1771544211 posted

    Hi Bob Johnston,

    Do you mean that you want to set the namespace while define the API routing? Since API doesn't have namespace signature, I think we can self-definition IHttpControllerSelector to support it.

    I defined two extended class, you can take them as a reference.

    public class NamespaceHttpControllerSelector : DefaultHttpControllerSelector
        {
            private const string NamespaceRouteVariableName = "Namespace";
            private readonly HttpConfiguration _configuration;
            private readonly Lazy<ConcurrentDictionary<string, Type>> _apiControllerCache;
     
            public NamespaceHttpControllerSelector(HttpConfiguration configuration)
                : base(configuration)
            {
                _configuration = configuration;
                _apiControllerCache = new Lazy<ConcurrentDictionary<string, Type>>(new Func<ConcurrentDictionary<string, Type>>(InitializeApiControllerCache));
            }
     
            private ConcurrentDictionary<string, Type> InitializeApiControllerCache()
            {
                IAssembliesResolver assembliesResolver = this._configuration.Services.GetAssembliesResolver();
                var types = this._configuration.Services.GetHttpControllerTypeResolver().GetControllerTypes(assembliesResolver).ToDictionary(t => t.FullName, t => t);
     
                return new ConcurrentDictionary<string, Type>(types);
            }
     
            public IEnumerable<string> GetControllerFullName(HttpRequestMessage request, string controllerName)
            {
                object namespaceName;
                var data = request.GetRouteData();
                IEnumerable<string> keys = _apiControllerCache.Value.ToDictionary<KeyValuePair<string, Type>, string, Type>(t => t.Key,
                        t => t.Value, StringComparer.CurrentCultureIgnoreCase).Keys.ToList();
                if (data.Route.DataTokens == null ||
                    !data.Route.DataTokens.TryGetValue(NamespaceRouteVariableName, out namespaceName))
                {
                    return from k in keys
                           where k.EndsWith(string.Format(".{0}{1}", controllerName, DefaultHttpControllerSelector.ControllerSuffix), StringComparison.CurrentCultureIgnoreCase)
                           select k;
                }
     
                //get the defined namespace
                string[] namespaces = (string[])namespaceName;
                return from n in namespaces
                       join k in keys on string.Format("{0}.{1}{2}", n, controllerName, DefaultHttpControllerSelector.ControllerSuffix).ToLower() equals k.ToLower()
                       select k;
            }
     
            public override HttpControllerDescriptor SelectController(HttpRequestMessage request)
            {
                Type type;
                if (request == null)
                {
                    throw new ArgumentNullException("request");
                }
                string controllerName = this.GetControllerName(request);
                if (string.IsNullOrEmpty(controllerName))
                {
                    throw new HttpResponseException(request.CreateErrorResponse(HttpStatusCode.NotFound,
                        string.Format("No route providing a controller name was found to match request URI '{0}'", new object[] { request.RequestUri })));
                }
                IEnumerable<string> fullNames = GetControllerFullName(request, controllerName);
                if (fullNames.Count() == 0)
                {
                    throw new HttpResponseException(request.CreateErrorResponse(HttpStatusCode.NotFound,
                            string.Format("No route providing a controller name was found to match request URI '{0}'", new object[] { request.RequestUri })));
                }
     
                if (this._apiControllerCache.Value.TryGetValue(fullNames.First(), out type))
                {
                    return new HttpControllerDescriptor(_configuration, controllerName, type);
                }
                throw new HttpResponseException(request.CreateErrorResponse(HttpStatusCode.NotFound,
                    string.Format("No route providing a controller name was found to match request URI '{0}'", new object[] { request.RequestUri })));
            }
        }
        public static class HttpRouteCollectionEx
        {
            public static IHttpRoute MapHttpRoute(this HttpRouteCollection routes, string name, string routeTemplate, object defaults, string[] namespaces)
            {
                return routes.MapHttpRoute(name, routeTemplate, defaults, null, null, namespaces);
            }
            public static IHttpRoute MapHttpRoute(this HttpRouteCollection routes, string name, string routeTemplate, object defaults, object constraints, HttpMessageHandler handler, string[] namespaces)
            {
                if (routes == null)
                {
                    throw new ArgumentNullException("routes");
                }
                var routeValue = new HttpRouteValueDictionary(new { Namespace = namespaces });
                var route = routes.CreateRoute(routeTemplate, new HttpRouteValueDictionary(defaults), new HttpRouteValueDictionary(constraints), routeValue, handler);
                routes.Add(name, route);
                return route;
            }
        }
    

    In Global File, Add the code below in Application_Start:

    GlobalConfiguration.Configuration.Services.Replace(typeof(IHttpControllerSelector),
        new NamespaceHttpControllerSelector(GlobalConfiguration.Configuration));
    

    Then configure API routing:

                config.Routes.MapHttpRoute(
                    name: "DefaultApi",
                    routeTemplate: "api/{controller}/{id}",
                    defaults: new { id = RouteParameter.Optional },
                    namespaces:new string[] { "Your Namespace of API" }
                );
    

    Best Regards,

    Jean

    Thursday, July 6, 2017 8:34 AM
  • User1120430333 posted

    The Web API should be consumed by  a Repository or Service layer that is accessed by the MVC client, without using a business layer. A true separation of concern and not namespace separation in a project.

    https://msdn.microsoft.com/en-us/library/ff649690.aspx

    https://msdn.microsoft.com/en-us/library/ee658090.aspx

    Thursday, July 6, 2017 12:00 PM
  • User-474980206 posted

    Have a project where MVC & API Controllers are separated by Namespace in a project.  

    Is there a way to point the routing so that it know to go to one namespace versus the other?

    the only builtin routing support for namespaces, is Areas, where the namespace is used to define the area. You can also use attribute routing, which is probably a good fit for your problem.

    Thursday, July 6, 2017 3:31 PM
  • User-1946294156 posted

    All, 

    Thank you for your replies, the issue that I am experiencing:

    The current request for action 'Index' on controller type 'CutlureCodesController' is ambiguous between the following action methods:
    System.Web.Mvc.ActionResult Index() on type Time.Controllers.MvcControllers.CutlureCodesController
    System.Web.Mvc.ActionResult Index(System.Nullable`1[System.Int32]) on type Time.Controllers.MvcControllers.CutlureCodesController

    I was hoping that there is something in the routing that would allow me to point to only specific namespaces for each type of call.

    Thursday, July 6, 2017 6:03 PM
  • User475983607 posted

    The problem you are facing has to do with the way routing works.  Routing knows nothing about the namespace as there is no namespace in the URL.  If you want api request to go to the api controller then you need to create a route to do so.  

    Again, if you create a new MVC/API project and look at the startup files generated, that will show you how to handle the situation.  There is also attribute routing and areas - also stated above.    

    If this is a .NET framework MVC project then the configuration files looks similar to the following.

    WebApiConfig.cs

    namespace MvcApiDemo
    {
        public static class WebApiConfig
        {
            public static void Register(HttpConfiguration config)
            {
                // Web API configuration and services
    
                // Web API routes
                config.MapHttpAttributeRoutes();
    
                config.Routes.MapHttpRoute(
                    name: "DefaultApi",
                    routeTemplate: "api/{controller}/{id}",
                    defaults: new { id = RouteParameter.Optional }
                );
            }
        }
    }

    RouteConfig.cs

    namespace MvcApiDemo
    {
        public class RouteConfig
        {
            public static void RegisterRoutes(RouteCollection routes)
            {
                routes.IgnoreRoute("{resource}.axd/{*pathInfo}");
    
                routes.MapRoute(
                    name: "Default",
                    url: "{controller}/{action}/{id}",
                    defaults: new { controller = "Home", action = "Index", id = UrlParameter.Optional }
                );
            }
        }
    }

    Global.asax

    namespace MvcApiDemo
    {
        public class Global : HttpApplication
        {
            void Application_Start(object sender, EventArgs e)
            {
                // Code that runs on application startup
                AreaRegistration.RegisterAllAreas();
                GlobalConfiguration.Configure(WebApiConfig.Register);
                RouteConfig.RegisterRoutes(RouteTable.Routes);            
            }
        }
    }

    Home API controller in the api namespace.

    namespace MvcApiDemo.Controllers.Api
    {
        public class HomeController : ApiController
        {
            public IEnumerable<string> Get()
            {
                return new string[] { "value1", "value2" };
            }
        }
    }
    

    Home controller in a different namespace

    namespace MvcApiDemo.Controllers
    {
        public class HomeController : Controller
        {
    
            // GET: Home
            public ActionResult Index()
            {
                return View();
            }

    To invoke the api routes simple start the URL with /api.

    Thursday, July 6, 2017 6:34 PM
  • User1120430333 posted

    MVC and Web API should not be in the same project. You should physically separate them into two projects. You should do the right thing here with SoC and stop beating a dead horse.

    https://en.wikipedia.org/wiki/Separation_of_concerns

     

    Thursday, July 6, 2017 7:04 PM
  • User1771544211 posted

    Hi Bob Johnston ,

    The current request for action 'Index' on controller type 'CutlureCodesController' is ambiguous between the following action methods:
    System.Web.Mvc.ActionResult Index() on type Time.Controllers.MvcControllers.CutlureCodesController
    System.Web.Mvc.ActionResult Index(System.Nullable`1[System.Int32]) on type Time.Controllers.MvcControllers.CutlureCodesController

    Do you mean that there is such a mistake in your project? As I can see in your code, you overload the index action and the request methods of both of them are 'HTTPGET'. However, an action can only have one HTTPGET method. I think merging them into one will be better.

    Best Regards,

    Jean

    Friday, July 7, 2017 9:35 AM
  • User945910954 posted

    I'm trying to use this to have 2 controllers with the same name one as the regular and one for testing

    POST /<g class="gr_ gr_63 gr-alert gr_spell gr_inline_cards gr_run_anim ContextualSpelling ins-del multiReplace" id="63" data-gr-id="63">api</g>/MyController/MyMethod

    POST /<g class="gr_ gr_88 gr-alert gr_spell gr_inline_cards gr_run_anim ContextualSpelling" id="88" data-gr-id="88">testapi</g>/MyController/MyMethod

    So the <g class="gr_ gr_126 gr-alert gr_spell gr_inline_cards gr_run_anim ContextualSpelling ins-del" id="126" data-gr-id="126">name spaces</g> are different.  What has broken now is my Swagger doesn't seem to like what's going on now, do you know of a way to fix that?

    Thursday, October 18, 2018 3:51 PM