Search code examples
asp.net-mvcasp.net-mvc-routingattributerouting

Is there a way to have a RoutePrefix that starts with an optional parameter?


I want to reach the Bikes controller with these URL's:

/bikes     // (default path for US)
/ca/bikes  // (path for Canada)

One way of achieving that is using multiple Route Attributes per Action:

[Route("bikes")]
[Route("{country}/bikes")]
public ActionResult Index()

To keep it DRY I'd prefer to use a RoutePrefix, but multiple Route Prefixes are not allowed:

[RoutePrefix("bikes")]
[RoutePrefix("{country}/bikes")] // <-- Error: Duplicate 'RoutePrefix' attribute    
public class BikesController : BaseController

    [Route("")]
    public ActionResult Index()

I've tried using just this Route Prefix:

[RoutePrefix("{country}/bikes")]
public class BikesController : BaseController

Result: /ca/bikes works, /bikes 404s.

I've tried making country optional:

[RoutePrefix("{country?}/bikes")]
public class BikesController : BaseController

Same result: /ca/bikes works, /bikes 404s.

I've tried giving country a default value:

[RoutePrefix("{country=us}/bikes")]
public class BikesController : BaseController

Same result: /ca/bikes works, /bikes 404s.

Is there another way to achieve my objective using Attribute Routing? (And yes, I know I can do this stuff by registering routes in RouteConfig.cs, but that's what not I'm looking for here).

I'm using Microsoft.AspNet.Mvc 5.2.2.

FYI: these are simplified examples - the actual code has an IRouteConstraint for the {country} values, like:

[Route("{country:countrycode}/bikes")]

Solution

  • I am a bit late to the party, but i have a working solution for this problem. Please find my detailed blog post on this issue here

    I am writing down summary below

    You need to create 2 files as given below

    
    
        using System;
        using System.Collections.Generic;
        using System.Collections.ObjectModel;
        using System.Web.Http.Controllers;
        using System.Web.Http.Routing;
    
        namespace _3bTechTalk.MultipleRoutePrefixAttributes {
         public class _3bTechTalkMultiplePrefixDirectRouteProvider: DefaultDirectRouteProvider {
          protected override IReadOnlyList  GetActionDirectRoutes(HttpActionDescriptor actionDescriptor, IReadOnlyList  factories, IInlineConstraintResolver constraintResolver) {
           return CreateRouteEntries(GetRoutePrefixes(actionDescriptor.ControllerDescriptor), factories, new [] {
            actionDescriptor
           }, constraintResolver, true);
          }
    
          protected override IReadOnlyList  GetControllerDirectRoutes(HttpControllerDescriptor controllerDescriptor, IReadOnlyList  actionDescriptors, IReadOnlyList  factories, IInlineConstraintResolver constraintResolver) {
           return CreateRouteEntries(GetRoutePrefixes(controllerDescriptor), factories, actionDescriptors, constraintResolver, false);
          }
    
          private IEnumerable  GetRoutePrefixes(HttpControllerDescriptor controllerDescriptor) {
           Collection  attributes = controllerDescriptor.GetCustomAttributes  (false);
           if (attributes == null)
            return new string[] {
             null
            };
    
           var prefixes = new List  ();
           foreach(var attribute in attributes) {
            if (attribute == null)
             continue;
    
            string prefix = attribute.Prefix;
            if (prefix == null)
             throw new InvalidOperationException("Prefix can not be null. Controller: " + controllerDescriptor.ControllerType.FullName);
            if (prefix.EndsWith("/", StringComparison.Ordinal))
             throw new InvalidOperationException("Invalid prefix" + prefix + " in " + controllerDescriptor.ControllerName);
    
            prefixes.Add(prefix);
           }
    
           if (prefixes.Count == 0)
            prefixes.Add(null);
    
           return prefixes;
          }
    
    
          private IReadOnlyList  CreateRouteEntries(IEnumerable  prefixes, IReadOnlyCollection  factories, IReadOnlyCollection  actions, IInlineConstraintResolver constraintResolver, bool targetIsAction) {
           var entries = new List  ();
    
           foreach(var prefix in prefixes) {
            foreach(IDirectRouteFactory factory in factories) {
             RouteEntry entry = CreateRouteEntry(prefix, factory, actions, constraintResolver, targetIsAction);
             entries.Add(entry);
            }
           }
    
           return entries;
          }
    
    
          private static RouteEntry CreateRouteEntry(string prefix, IDirectRouteFactory factory, IReadOnlyCollection  actions, IInlineConstraintResolver constraintResolver, bool targetIsAction) {
           DirectRouteFactoryContext context = new DirectRouteFactoryContext(prefix, actions, constraintResolver, targetIsAction);
           RouteEntry entry = factory.CreateRoute(context);
           ValidateRouteEntry(entry);
    
           return entry;
          }
    
    
          private static void ValidateRouteEntry(RouteEntry routeEntry) {
           if (routeEntry == null)
            throw new ArgumentNullException("routeEntry");
    
           var route = routeEntry.Route;
           if (route.Handler != null)
            throw new InvalidOperationException("Direct route handler is not supported");
          }
         }
        }
    
    
    
    
        using System;
        using System.Collections.Generic;
        using System.Linq;
        using System.Web;
        using System.Web.Http;
    
        namespace _3bTechTalk.MultipleRoutePrefixAttributes
        {
            [AttributeUsage(AttributeTargets.Class, AllowMultiple = true, Inherited = true)]
            public class _3bTechTalkRoutePrefix : RoutePrefixAttribute
            {
                public int Order { get; set; }
    
                public _3bTechTalkRoutePrefix(string prefix) : this(prefix, 0) { }
    
                public _3bTechTalkRoutePrefix(string prefix, int order) : base(prefix)
                {
                    Order = order;
                }        
            }
        }
    
    

    Once done, open WebApiConfig.cs and add this below given line

    
    config.MapHttpAttributeRoutes(new _3bTechTalkMultiplePrefixDirectRouteProvider());
    

    That's it, now you can add multiple route prefix in your controller. Example below

    
    
        [_3bTechTalkRoutePrefix("api/Car", Order = 1)]
        [_3bTechTalkRoutePrefix("{CountryCode}/api/Car", Order = 2)]
        public class CarController: ApiController {
         [Route("Get")]
         public IHttpActionResult Get() {
          return Ok(new {
           Id = 1, Name = "Honda Accord"
          });
         }
        }
    
    

    I have uploaded a working solution here

    Happy Coding :)