17

For a scenario, I have a ASP.NET MVC application with URLs that look like the following:

http://example.com/Customer/List
http://example.com/Customer/List/Page/2
http://example.com/Customer/List
http://example.com/Customer/View/8372
http://example.com/Customer/Search/foo/Page/5

These URLs are achieved with following routes in Global.asax.cs

routes.MapRoute(
    "CustomerSearch"
    , "Customer/Search/{query}/Page/{page}"
    , new { controller = "Customer", action = "Search" }
);

routes.MapRoute(
    "CustomerGeneric"
    , "Customer/{action}/{id}/Page/{page}"
    , new { controller = "Customer" }
);

//-- Default Route
routes.MapRoute(
    "Default",
    "{controller}/{action}/{id}",
    new { controller = "Customer", action = "Index", id = "" }
);

These all have gone well until a new requirement arrived and wants to drop keyword 'Customer' off the URL, to make the URLs look like:

http://example.com/List
http://example.com/List/Page/2
http://example.com/List
http://example.com/View/8372
http://example.com/Search/foo/Page/5

Edit: corrected example links, thanks to @haacked.

I tried to add new MapRoutes to take {action} only and have default controller set to Customer. eg/

routes.MapRoute(
    "CustomerFoo"
    , "{action}"
    , new { controller = "Customer", action = "Index" }
);

This seems to work, however now all links generated by Html.ActionLink() are weird and no longer URL friendly.

So, is this achievable? Am I approaching in the right direction?

ozziepeeps
  • 108
  • 6
Trav L
  • 14,732
  • 6
  • 30
  • 39

3 Answers3

17

don't mix a rule like: "{action}/{id}" with one that's "{controller}/{action}/{id}" ... specially when id in the later has a default value i.e. is optional.

In that case you have nothing that allows routing to know which one is the right one to use.

A workaround, if that's what you need, would be to add a constrain (see this) to the action in the earlier to a set of values i.e. List, View. Of course that with these types of rules, you can't have a controller with the same name of an action.

Also remember that if you specify a default action & id in the "{action}/{id}" rule, that will be used when you hit the route of your site.

eglasius
  • 35,831
  • 5
  • 65
  • 110
  • 1
    Apologise that my reply isn't speedy, because I want to try out the answers before response with feedbacks. Sorry about this :P – Trav L Nov 23 '09 at 12:31
  • @rockacola its k, there is no rush. – eglasius Nov 23 '09 at 16:25
  • This is what I'm looking for! Route constraints will give me the customisation I desired without jeopardise MapRoute. I need to reorganise all my existing routes tho. Thanks @Freddy! – Trav L Nov 24 '09 at 03:26
10

Why does the first URL in the new list still have "Customer". I assume that's a typo and you meant:

The following routes work for me:

routes.MapRoute(
    "CustomerSearch"
    , "Search/{query}/Page/{page}"
    , new { controller = "Customer", action = "Search" }
);

routes.MapRoute(
    "CustomerGeneric"
    , "{action}/{id}/Page/{page}"
    , new { controller = "Customer" }
);

//-- Default Route
routes.MapRoute(
    "Default",
    "{action}/{id}",
    new { controller = "Customer", action = "Index", id = "" }
);

How are you generating your links. Since the Controller is no longer in the URL of your route (aka, you don't have "{controller}" in the route URL), but it's a default value, you need to make sure to specify the controller when generating routes.

Thus instead of

Html.ActionLink("LinkText", "ActionName")

do

Html.ActionLink("LinkText", "ActionName", "Customer")

Why? Suppose you had the following routes.

routes.MapRoute(
    "Default",
    "foo/{action}",
    new { controller = "Cool" }
);

routes.MapRoute(
    "Default",
    "bar/{action}",
    new { controller = "Neat" }
);

Which route did you mean when you call this?

<%= Html.ActionLink("LinkText", "ActionName") %>

You can differentiate by specifying the controller and we'll pick the one that has a default value that matches the specified one.

Haacked
  • 58,045
  • 14
  • 90
  • 114
  • Thanks for reply Phil. The routes work fine **IF all pages are from Customer controller**. Say if I have `User` controller with `SignIn` action: /User/SignIn/ request will be hijacked by **CustomerDefault** (which is {action}/{id}) where I want it to fall into **Default** (which is {controller}/{action}). Is there a way to teach your route which parameter meant to be **action** and which meant to be **controller**? Thanks again, love your blog. – Trav L Nov 23 '09 at 01:48
  • @rockacola I just added an answer about this (before seeing your comment), add a constraint to limit {action} so it doesn't use that rule for {customer}. – eglasius Nov 23 '09 at 01:59
  • Use a constraint, just as Freddy said. – Haacked Nov 25 '09 at 17:55
4

You can create a route that is constrained to only match actions in your Customer controller.

public static class RoutingExtensions {
    ///<summary>Creates a route that maps URLs without a controller to action methods in the specified controller</summary>
    ///<typeparam name="TController">The controller type to map the URLs to.</typeparam>
    public static void MapDefaultController<TController>(this RouteCollection routes) where TController : ControllerBase {
        routes.MapControllerActions<TController>(typeof(TController).Name, "{action}/{id}", new { action = "Index", id = UrlParameter.Optional });
    }
    ///<summary>Creates a route that only matches actions from the given controller.</summary>
    ///<typeparam name="TController">The controller type to map the URLs to.</typeparam>
    public static void MapControllerActions<TController>(this RouteCollection routes, string name, string url, object defaults) where TController : ControllerBase {
        var methods = typeof(TController).GetMethods()
                                         .Where(m => !m.ContainsGenericParameters)
                                         .Where(m => !m.IsDefined(typeof(ChildActionOnlyAttribute), true))
                                         .Where(m => !m.IsDefined(typeof(NonActionAttribute), true))
                                         .Where(m => !m.GetParameters().Any(p => p.IsOut || p.ParameterType.IsByRef))
                                         .Select(m => m.GetActionName());

        routes.Add(name, new Route(url, new MvcRouteHandler()) {
            Defaults = new RouteValueDictionary(defaults) { { "controller", typeof(TController).Name.Replace("Controller", "") } },
            Constraints = new RouteValueDictionary { { "action", new StringListConstraint(methods) } }
        });
    }

    private static string GetActionName(this MethodInfo method) {
        var attr = method.GetCustomAttribute<ActionNameAttribute>();
        if (attr != null)
            return attr.Name;
        return method.Name;
    }

    class StringListConstraint : IRouteConstraint {
        readonly HashSet<string> validValues;
        public StringListConstraint(IEnumerable<string> values) { validValues = new HashSet<string>(values, StringComparer.OrdinalIgnoreCase); }

        public bool Match(HttpContextBase httpContext, Route route, string parameterName, RouteValueDictionary values, RouteDirection routeDirection) {
            return validValues.Contains(values[parameterName]);
        }
    }

    #region GetCustomAttributes
    ///<summary>Gets a custom attribute defined on a member.</summary>
    ///<typeparam name="TAttribute">The type of attribute to return.</typeparam>
    ///<param name="provider">The object to get the attribute for.</param>
    ///<returns>The first attribute of the type defined on the member, or null if there aren't any</returns>
    public static TAttribute GetCustomAttribute<TAttribute>(this ICustomAttributeProvider provider) where TAttribute : Attribute {
        return provider.GetCustomAttribute<TAttribute>(false);
    }
    ///<summary>Gets the first custom attribute defined on a member, or null if there aren't any.</summary>
    ///<typeparam name="TAttribute">The type of attribute to return.</typeparam>
    ///<param name="provider">The object to get the attribute for.</param>
    ///<param name="inherit">Whether to look up the hierarchy chain for attributes.</param>
    ///<returns>The first attribute of the type defined on the member, or null if there aren't any</returns>
    public static TAttribute GetCustomAttribute<TAttribute>(this ICustomAttributeProvider provider, bool inherit) where TAttribute : Attribute {
        return provider.GetCustomAttributes<TAttribute>(inherit).FirstOrDefault();
    }
    ///<summary>Gets the custom attributes defined on a member.</summary>
    ///<typeparam name="TAttribute">The type of attribute to return.</typeparam>
    ///<param name="provider">The object to get the attribute for.</param>
    public static TAttribute[] GetCustomAttributes<TAttribute>(this ICustomAttributeProvider provider) where TAttribute : Attribute {
        return provider.GetCustomAttributes<TAttribute>(false);
    }
    ///<summary>Gets the custom attributes defined on a member.</summary>
    ///<typeparam name="TAttribute">The type of attribute to return.</typeparam>
    ///<param name="provider">The object to get the attribute for.</param>
    ///<param name="inherit">Whether to look up the hierarchy chain for attributes.</param>
    public static TAttribute[] GetCustomAttributes<TAttribute>(this ICustomAttributeProvider provider, bool inherit) where TAttribute : Attribute {
        if (provider == null) throw new ArgumentNullException("provider");

        return (TAttribute[])provider.GetCustomAttributes(typeof(TAttribute), inherit);
    }
    #endregion
}
SLaks
  • 868,454
  • 176
  • 1,908
  • 1,964