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

4

8

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")]
Stalinabad answered 25/6, 2015 at 14:49 Comment(0)
A
2

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

Ascend answered 7/5, 2017 at 6:51 Comment(1)
That looks great! I think you could improve your answer by adding the code of the two files. From the help: "Always quote the most relevant part of an important link, in case the target site is unreachable or goes permanently offline."Stalinabad
F
0

You're correct that you can't have multiple route prefixes, which means solving this particular use case is not going to be straight forward. About the best way I can think of to achieve what you want with the minimal amount of modifications to your project is to subclass your controller. For example:

[RoutePrefix("bikes")]
public class BikeController : Controller
{
    ...
}

[RoutePrefix("{country}/bikes")]
public class CountryBikeController : BikeController
{
}

You subclassed controller will inherit all the actions from BikeController, so you don't need to redefine anything, per se. However, when it comes to generating URLs and getting them to go to the right place, you'll either need to be explicit with the controller name:

@Url.Action("Index", "CountryBike", new { country = "us" }

Or, if you're using named routes, you'll have to override your actions in your subclassed controller so you can apply new route names:

[Route("", Name = "CountryBikeIndex")]
public override ActionResult Index()
{
    base.Index();
}

Also, bear in mind, that when using parameters in route prefixes, all of your actions in that controller should take the parameter:

public ActionResult Index(string country = "us")
{
    ...
Fogged answered 1/7, 2015 at 16:40 Comment(4)
This looked promising, but I could not get it to work. I created a new MVC project, and added a BikesController : Controller and CountryBikesController : BikesController, added the string country = "usa", but the routes are not recognized: @Html.ActionLink("Canada Bikes", "Index", "Bikes", new { country = "canada" }, null) ==> /Bikes?country=canada | @Html.ActionLink("Canada CountryBikes", "Index", "CountryBikes", new { country = "canada" }, null) ==> /CountryBikes?country=canada. Thanks for your help! I'm now going to try out github.com/Dresel/RouteLocalizationStalinabad
You forgot to turn on attribute routing most likely. Go into RouteConfig.cs and uncomment the line for attribute routing.Fogged
No, that's not it: routes.MapMvcAttributeRoutes(); is in the proper place.Stalinabad
Doesn't work. Sub-classed controller is never invoked. I thought perhaps it was because RoutePrefix was inherited, but no -- the flag is set to false: sourcebrowser.io/Browse/ASP-NET-MVC/aspnetwebstack/src/…Empennage
B
0

You could use attribute routes with two ordered options.

public partial class GlossaryController : Controller {

    [Route("~/glossary", Order = 2)]
    [Route("~/{countryCode}/glossary", Order = 1)]
    public virtual ActionResult Index()
    {
      return View();
    }
}

If you're planning to have region specific routes for all your pages you could add a route to the route config above the default. This will work only for views/controllers without attribute routes.

  routes.MapRoute(
     name: "Region",
     url: "{countryCode}/{controller}/{action}/{id}",
     defaults: new { controller = "Home", action = "Index", id = UrlParameter.Optional },
     constraints: new { countryCode = @"\w{2}" }
  );
Blame answered 13/7, 2016 at 1:27 Comment(2)
My question is about RoutePrefixAttributes on the controller, not about RouteAttributes on the actions. RoutePrefixAttribute does not have an Order property, so this does not fix my "Duplicate 'RoutePrefix' attribute" error problem.Stalinabad
I'm working on adding regions to a site as well. I've tried everything that you've tried and there isnt a way to do it with RoutePrefix. However this has worked for me, adding dual ordered route prefixes where needed and using a global route def for actions that do not use attr routing.Blame
E
0

The best solution I've come across is detailed by NightOwl888 in response to the following question: ASP.NET MVC 5 culture in route and url. The code below is my trimmed down version of his post. It's working for me in MVC5.

Decorate each controller with a single RoutePrefix, without a culture segment. When the application starts up, the custom MapLocalizedMvcAttributeRoutes method adds a localized route entry for each controller action.

public class RouteConfig
{
    public static void RegisterRoutes(RouteCollection routes)
    {
        // Omitted for brevity

        MapLocalizedMvcAttributeRoutes(routes, "{culture}/", new { culture = "[a-z]{2}-[A-Z]{2}" });
    }

    static void MapLocalizedMvcAttributeRoutes(RouteCollection routes, string urlPrefix, object constraints)
    {
        var routeCollectionRouteType = Type.GetType("System.Web.Mvc.Routing.RouteCollectionRoute, System.Web.Mvc");
        var subRouteCollectionType = Type.GetType("System.Web.Mvc.Routing.SubRouteCollection, System.Web.Mvc");
        var linkGenerationRouteType = Type.GetType("System.Web.Mvc.Routing.LinkGenerationRoute, System.Web.Mvc");
        FieldInfo subRoutesInfo = routeCollectionRouteType.GetField("_subRoutes", BindingFlags.NonPublic | BindingFlags.Instance);
        PropertyInfo entriesInfo = subRouteCollectionType.GetProperty("Entries");
        MethodInfo addMethodInfo = subRouteCollectionType.GetMethod("Add");

        var localizedRouteTable = new RouteCollection();
        var subRoutes = Activator.CreateInstance(subRouteCollectionType);
        Func<Route, RouteBase> createLinkGenerationRoute = (Route route) => (RouteBase)Activator.CreateInstance(linkGenerationRouteType, route);

        localizedRouteTable.MapMvcAttributeRoutes();

        foreach (var routeCollectionRoute in localizedRouteTable.Where(rb => rb.GetType().Equals(routeCollectionRouteType)))
        {
            // routeCollectionRoute._subRoutes.Entries
            foreach (RouteEntry routeEntry in (IEnumerable)entriesInfo.GetValue(subRoutesInfo.GetValue(routeCollectionRoute)))
            {
                var localizedRoute = CreateLocalizedRoute(routeEntry.Route, urlPrefix, constraints);
                var localizedRouteEntry = new RouteEntry(string.IsNullOrEmpty(routeEntry.Name) ? null : $"{routeEntry.Name}_Localized", localizedRoute);
                // Add localized and default routes and subroute entries
                addMethodInfo.Invoke(subRoutes, new[] { localizedRouteEntry });
                addMethodInfo.Invoke(subRoutes, new[] { routeEntry });
                routes.Add(createLinkGenerationRoute(localizedRoute));
                routes.Add(createLinkGenerationRoute(routeEntry.Route));
            }
        }
        var routeEntries = Activator.CreateInstance(routeCollectionRouteType, subRoutes);
        routes.Add((RouteBase)routeEntries);
    }

    static Route CreateLocalizedRoute(Route route, string urlPrefix, object constraints)
    {
        var routeUrl = urlPrefix + route.Url;
        var routeConstraints = new RouteValueDictionary(constraints);
        // combine with any existing constraints
        foreach (var constraint in route.Constraints)
        {
            routeConstraints.Add(constraint.Key, constraint.Value);
        }
        return new Route(routeUrl, route.Defaults, routeConstraints, route.DataTokens, route.RouteHandler);
    }
}
Empennage answered 8/4, 2017 at 23:40 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.