1
votes

Using the library AttributeRouting, I was able to configure attribute routing to use a default route constraint based on the parameter name.

For example, to add a RegexRouteConstraint for all route parameters ending with year:

routes.MapAttributeRoutes(cfg =>
    {
        cfg.AddDefaultRouteConstraint(@"year$",
            new RegexRouteConstraint(@"^([1-2]\d{3})$"));
    }
);

I know about about custom route constraints in MVC5, but I'd really like to be able to add some constraints by convention based simply on the parameter name.

Is it possible to do the same with the attribute routing features of MVC5.1 ?

1

1 Answers

1
votes

I have the same need and found a solution, although I have not thoroughly tested it. My scenario is a multi-tenant API where the routes all begin with "api/{tenant}/..." The tenants are pulled from web.config, so I have the added complexity that my custom resolver should be a singleton. The solution below is for Web API but I expect it will work with in MVC with a few namespace adjustments.

Create an implementation of IHttpRouteConstraint. This is mine:

public class TenantRouteConstraint : IHttpRouteConstraint
{
    public const string TenantKey = "tenant";

    private readonly ISet<string> _tenants;

    public TenantRouteConstraint()
    {
        _tenants = new HashSet<string>();
        foreach (ConnectionStringSettings connectionString in ConfigurationManager.ConnectionStrings)
        {
            _tenants.Add(connectionString.Name.ToLowerInvariant());
        }
    }

    private static string GetTenant(IDictionary<string, object> values)
    {
        object tenant;
        if (values.TryGetValue(TenantKey, out tenant))
        {
            return tenant.ToString().ToLowerInvariant();
        }
        return null;
    }

    public bool Match(HttpRequestMessage request, IHttpRoute route, string parameterName, IDictionary<string, object> values, HttpRouteDirection routeDirection)
    {
        var tenant = GetTenant(values);
        return tenant != null && _tenants.Contains(tenant);
    }
}

That part was easy, then it got tricky. The routes are typically initialized in WebApiConfig.Register, which is passed as a delegated to GlobalConfiguration.Configure in Global.asax. However, the attribute based routes are not populated in the Routes collection at the end of the Register method. To get around this, I added a RegisterRouteConstraints method to WebApiConfig that gets called after Register.

The RegisterRouteConstraints method loops through the Routes collection and adds my constraint if "{tenant}" is present in the route template. The Routes collection contains three types of routes: RouteCollectionRoute, HostedHttpRoute, and LinkGeneratioRoute. The attribute based routes are in RouteCollectionRoute but these classes are internal so I can't test for the type directly. Fortunately it implements IEnumerable<IHttpRoute> so I check for that.

public static void RegisterRouteConstraints(HttpConfiguration config)
{
    var tenantConstraint = new TenantRouteConstraint();
    AddConstraint(config.Routes, "tenant", tenantConstraint);
}

private static void AddConstraint(IEnumerable<IHttpRoute> routes, string key, IHttpRouteConstraint constraint)
{
    foreach (var route in routes)
    {
        if (route.RouteTemplate.Contains("{" + key + "}") && !route.Constraints.ContainsKey(key))
        {
            route.Constraints.Add(key, constraint);
        }

        var routeCollection = route as IEnumerable<IHttpRoute>;
        if (routeCollection != null)
        {
            AddConstraint(routeCollection, key, constraint);
        }
    }
}

This is called in Global.asax Application_Start:

// ...
GlobalConfiguration.Configure(WebApiConfig.Register);
GlobalConfiguration.Configure(WebApiConfig.RegisterRouteConstraints);
// ...