Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

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")]
like image 682
Frank van Eykelen Avatar asked Jun 25 '15 14:06

Frank van Eykelen


1 Answers

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

  • _3bTechTalkMultiplePrefixDirectRouteProvider.cs


    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");
      }
     }
    }

  • 3bTechTalkRoutePrefix.cs


    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 :)

like image 111
Asad Ullah Avatar answered Oct 22 '22 12:10

Asad Ullah