Subtleties with using Url.RouteUrl to get fully-qualified URLs

At some point I missed the Url.RouteUrl overload that took a protocol and returned an absolute URL based on the current context. It is quite handy when you are sending URLs out into the world (e.g., RSS feed link). I ended up using the less-handy overload that took an explicit host (same as the current, in this case) and passing it in. When someone pointed out the simpler overload, I did the obvious and deleted the host from the call. That didn’t quite work.

For those looking for a way to get a fully-qualified URL through the route system, the less-than-obvious answer is to call the overload for Url.RouteUrl that gets a URL with a different protocol (Url.RouteUrl(string, object, string)), passing in Request.Url.Scheme for the current protocol.

Url.RouteUrl("Default", new { action = "Index", controller = "Department", id = 1 }, Request.Url.Scheme);
// https://www.currentdomain.com/department/index/1

Say you want to send someone to a different subdomain in your app while using the same routes. There’s an overload for that: Url.RouteUrl(string, RouteValueDictionary, string, string). Combined with the above example, here’s how these all play out if you are currently handling a www.currentdomain.com request and your route table includes the fallback default ({controller}/{action}/{id}).

Url.RouteUrl("Default", new { action = "Index", controller = "Department", id = 1 });
// /department/index/1
Url.RouteUrl("Default", new { action = "Index", controller = "Department", id = 1 }, Request.Url.Scheme);
// https://www.currentdomain.com/department/index/1
Url.RouteUrl("Default", new RouteValueDictionary(new { action = "Index", controller = "Department", id = 1 }), "http", "sub.currentdomain.com");
// https://sub.currentdomain.com/department/index/1

Now if you switch between the two fully-qualified calls, you may try just deleting or adding the hostName parameter, respectively. One direction is a compile error, and one direction is a runtime oddity resulting in a hideous URL.

Url.RouteUrl("Default", new { action = "Index", controller = "Department", id = 1 }, "http", "sub.currentdomain.com");
// Compile error (expects a RouteValueDictionary)
Url.RouteUrl("Default", new RouteValueDictionary(new { action = "Index", controller = "Department", id = 1 }), "http", "sub.currentdomain.com");
// Eye-bleeding and incorrect route as it "serializes" the RouteValueDictionary.
// In my case, I ended up with something like this:
// https://www.currentdomain.com/current/route/1/?Count=3&Keys=System.Collections.Generic.Dictionary%602%2BKeyCollection%5BSystem.String%2CSystem.Object%5D&Values=System.Collections.Generic.Dictionary%602%2BValueCollection%5BSystem.String%2CSystem.Object%5D

On a side note, if your development environment uses localhost with a port and you use some web.config app setting for that URL change between development and production (“localhost:12345” vs “www.currentdomain.com”). You will want your host setting to be without the port. Url.RouteUrl will hiccup on your development environment if the port is part of the host name (it’s no longer just the host at that point).

Url.RouteUrl("Default", new RouteValueDictionary(new { action = "Index", controller = "Department", id = 1 }), "http", ConfigurationManager.AppSettings["hostwithport"]);
// https://localhost:12345:12345/department/index/1

Handling case-insensitive enum action method parameters in ASP.NET MVC

Skip to solution

Using enums as action method parameters

Say you have a enum, a sorting enum in this case.

public enum SortType {
    NewestFirst,
    OldestFirst,
    HighestRated,
    MostReviews
}

ASP.NET will gladly let you use that enum as an action method parameter; you can even make it an optional parameter. To make it optional by routing, you need to make it nullable for the action method function parameter in your controller and add some guarding logic (!sort.HasValue or the like).

routes.MapRoute(
    "DepartmentProducts",
    "Department/Products/{sort}",
    new { controller = "Department", action = "Products", sort = UrlParameter.Optional }
);
public ActionResult Products(SortType? sort) {
    SortType requestedSort = sort ?? SortType.NewestFirst;
    ...
}

To make the parameter optional by function parameter, just give the parameter a default value.

routes.MapRoute(
    "DepartmentProducts",
    "Department/Products/{sort}",
    new { controller = "Department", action = "Products" }
);
public ActionResult Products(SortType sort = SortType.NewestFirst) { ... }

Both work just fine, though I lean toward the function parameter default. Regardless of implementation, you can call the method by a number of different URLs:

  • https://www.somedomain.com/department/products/ (sort == null or SortType.NewestFirst, respectively)
  • https://www.somedomain.com/department/products/OldestFirst/
  • https://www.somedomain.com/department/products/HighestRated/
  • https://www.somedomain.com/department/products/?sort=MostReviews

Unfortunately, it requires the route value to be an exact match for the enum name, proper case included. This URL will not result in the correct value for the sort parameter.

https://www.somedomain.com/department/products/oldestfirst (results in null on the former, NewestFirst on the later)

This can be a problem with the internet being mostly case-insensitive, especially if you have a lower case routing system and/or use the lowercase SEO tweak in the IIS URL rewrite system (future post coming about a gotcha and work-around on applying this handy system to existing sites).

Solution

While looking into this, I came across just the code I thought I needed from Rupert Bates. It is case-insensitive model binder that is designed to allow you to declare a site-wide default for a given enum type. This was a great starting point. Since I tend to use the optional function parameters, though, this actually meant the model binder would give me a default before the action method could even try to do the same. I pulled that part from the version I used.

I tied the generic, as best as possible, to an enum (where T : struct) so it is harder to use it for types that aren’t compatible. While I was poking around, I flipped it to use a once-built look-up table for the enum using a case-insensitive Dictionary<string, T> rather than repeat calls to Enum.Parse (called once per value) and Enum.GetNames (called only once). As far as my manual testing has shown me, it works quite well and should be at least slightly faster, not that Rupert’s solution couldn’t hold its own just fine.

using System;
using System.Web.Mvc;
using System.Collections.Generic;

namespace StpWeb.CustomModelBinders {
    public class EnumBinderIgnoreCase<T> : IModelBinder where T : struct {
        public EnumBinderIgnoreCase() {
            foreach (string enumName in enumNames) {
                enumLookups.Add(enumName, (T)Enum.Parse(typeof(T), enumName, true));
            }
        }
        public object BindModel(ControllerContext controllerContext, ModelBindingContext bindingContext) {
            return bindingContext.ValueProvider.GetValue(bindingContext.ModelName) == null
                ? (T?)null
                : GetEnumValue(bindingContext.ValueProvider.GetValue(bindingContext.ModelName).AttemptedValue);
        }

        private string[] enumNames = Enum.GetNames(typeof(T));
        private Dictionary<string, T> enumLookups = new Dictionary<string, T>(StringComparer.OrdinalIgnoreCase);
        private T GetEnumValue(string value) {
            T foundEnumValue = default(T);
            if (!String.IsNullOrEmpty(value) && enumLookups.ContainsKey(value))
                foundEnumValue = enumLookups[value];

            return foundEnumValue;
        }
    }
}