In MVC’s generation of outgoing URLs, I thought that the Bind attribute would be useful to leave out route values that are unbound. It turns out that this is not the case, so I was getting URL parameters that represent ViewModel properties that I didn’t need to pass around:

http://example.com/?Sizes=System.String[]

Now, I could use fields instead of properties, but that seems unnecessarily restrictive. Extension methods to the rescue! In the following, onlyBoundValues tells the UrlHelper to only include bound values as specified by Include, Exlcude, or both.

/// <summary>
/// Add extension methods that construct outgoing URL's but leave out
/// route values that are excluded by the Bind attribute's Include or
/// Exclude.  The methods to mirror are those that take an object as
///  an argument:
///
/// public string Action(string actionName, object routeValues);
/// public string Action(string actionName, string controllerName
///                    , object routeValues);
/// public string Action(string actionName, string controllerName
///                    , object routeValues, string protocol);
///
/// public string RouteUrl(object routeValues);
/// public string RouteUrl(string routeName, object routeValues);
/// public string RouteUrl(string routeName, object routeValues
///                      , string protocol);
/// </summary>
public static class UrlHelperExtensions
{
    public static string Action(this UrlHelper helper, string actionName
                            , object routeValues, bool onlyBoundValues)
    {
        RouteValueDictionary finalRouteValues =
            new RouteValueDictionary(routeValues);

        if (onlyBoundValues)
        {
            RemoveUnboundValues(finalRouteValues, routeValues);
        }

        // Internally, MVC calls an overload of GenerateUrl with
        // hard-coded defaults.  Since we shouldn't know what these
        // defaults are, we call the non-extension equivalents.
        return helper.Action(actionName, routeValues);
    }

    public static string Action(this UrlHelper helper, string actionName
                            , string controllerName, object routeValues
                            , bool onlyBoundValues)
    {
        RouteValueDictionary finalRouteValues =
            new RouteValueDictionary(routeValues);

        if (onlyBoundValues)
        {
            RemoveUnboundValues(finalRouteValues, routeValues);
        }

        return helper.Action(actionName, controllerName, finalRouteValues);
    }

    public static string Action(this UrlHelper helper, string actionName
                            , string controllerName, object routeValues
                            , string protocol, bool onlyBoundValues)
    {
        RouteValueDictionary finalRouteValues =
            new RouteValueDictionary(routeValues);

        if (onlyBoundValues)
        {
            RemoveUnboundValues(finalRouteValues, routeValues);
        }

        return helper.Action(actionName, controllerName
                         , finalRouteValues, protocol);
    }

    public static string RouteUrl(this UrlHelper helper, object routeValues
                              , bool onlyBoundValues)
    {
        RouteValueDictionary finalRouteValues =
            new RouteValueDictionary(routeValues);
        if (onlyBoundValues)
        {
            RemoveUnboundValues(finalRouteValues, routeValues);
        }
        return helper.RouteUrl(finalRouteValues);
    }

    public static string RouteUrl(this UrlHelper helper, string routeName
                              , object routeValues, bool onlyBoundValues)
    {
        RouteValueDictionary finalRouteValues =
            new RouteValueDictionary(routeValues);
        if (onlyBoundValues)
        {
            RemoveUnboundValues(finalRouteValues, routeValues);
        }
        return helper.RouteUrl(routeName, finalRouteValues);
    }

    public static string RouteUrl(this UrlHelper helper, string routeName
                              , object routeValues, string protocol
                              , bool onlyBoundValues)
    {
        RouteValueDictionary finalRouteValues =
            new RouteValueDictionary(routeValues);

        if (onlyBoundValues)
        {
            RemoveUnboundValues(finalRouteValues, routeValues);
        }

        return helper.RouteUrl(routeName, finalRouteValues, protocol);
    }

    /// <summary>
    /// Reflect into the routeValueObject and remove any keys from
    /// routeValues that are not bound by the Bind attribute
    /// </summary>
    private static void RemoveUnboundValues(RouteValueDictionary routeValues
                                        , object source)
    {
        if (source == null)
        {
            return;
        }

        var type = source.GetType();

        BindAttribute b = null;

        foreach (var attribute in type.GetCustomAttributes(true))
        {
            if (attribute is BindAttribute)
            {
                b = (BindAttribute)attribute;
                break;
            }
        }

        if (b == null)
        {
            return;
        }

        foreach (var property in type.GetProperties())
        {
            var propertyName = property.Name;
            if (!b.IsPropertyAllowed(propertyName))
            {
                routeValues.Remove(propertyName);
            }
        }
    }
}