有没有办法让 RoutePrefix 以可选参数开头?

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

我想通过这些 URL 联系自行车控制器:

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

实现这一点的一种方法是在每个操作中使用多个路由属性:

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

为了保持干爽,我更愿意使用 RoutePrefix,但不允许使用多个 Route Prefix:

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

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

我试过只使用这个路由前缀:

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

结果:/ca/bikes 有效,/bikes 404s。

我试过将国家设为可选:

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

同样的结果:/ca/bikes 有效,/bikes 404s。

我试过给国家一个默认值:

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

同样的结果:/ca/bikes 有效,/bikes 404s。

还有其他方法可以使用属性路由实现我的 objective 吗? (是的,我知道我可以通过在 RouteConfig.cs 中注册路由来完成这些工作,但这不是我在这里寻找的)。

我正在使用 Microsoft.AspNet.Mvc 5.2.2.

仅供参考:这些是简化的示例 - 实际代码有一个用于 {country} 值的 IRouteConstraint,例如:

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

你说得对,你不能有多个路由前缀,这意味着解决这个特定的用例不会是直截了当的。我能想到的以对项目进行最少的修改来实现您想要的效果的最佳方法是对控制器进行子类化。例如:

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

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

您的子类控制器将继承 BikeController 的所有操作,因此您本身不需要重新定义任何内容。但是,当涉及到生成 URL 并让它们到达正确的位置时,您要么需要明确控制器名称:

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

或者,如果您使用的是命名路由,则必须在子类控制器中覆盖您的操作,以便您可以应用新的路由名称:

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

此外,请记住,在路由前缀中使用参数时,该控制器中的所有操作都应采用参数:

public ActionResult Index(string country = "us")
{
    ...

您可以使用带有两个有序选项的属性路由。

public partial class GlossaryController : Controller {

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

如果您计划为您的所有页面设置区域特定路由,您可以在默认路由配置之上添加一个路由。这仅适用于没有属性路由的 views/controllers。

  routes.MapRoute(
     name: "Region",
     url: "{countryCode}/{controller}/{action}/{id}",
     defaults: new { controller = "Home", action = "Index", id = UrlParameter.Optional },
     constraints: new { countryCode = @"\w{2}" }
  );

NightOwl888 在回答以下问题时详细介绍了我遇到的最佳解决方案:。下面的代码是我对他 post 的简化版本。它在 MVC5 中为我工作。

用单个 RoutePrefix 装饰每个控制器,没有文化段。当应用程序启动时,自定义 MapLocalizedMvc​​AttributeRoutes 方法为每个控制器操作添加本地化路由条目。

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

我来晚了一点,但我有一个解决这个问题的有效方法。关于这个问题请看我的详细博客posthere

我在下面写下摘要

您需要创建 2 个文件,如下所示



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

完成后,打开 WebApiConfig.cs 并将其添加到给定行下方


config.MapHttpAttributeRoutes(new _3bTechTalkMultiplePrefixDirectRouteProvider());

就是这样,现在您可以在 controller 中添加多个路由前缀。下面的例子



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

我已经上传了一个有效的解决方案here

快乐编码:)