Routing Based On Domain/Subdomain

Showing 1-2 of 2 messages
Routing Based On Domain/Subdomain Dave Glick 4/9/12 8:59 AM
First off, I love what I see from Nancy. I'm a library/console developer at heart and having written several low-end embedded web server systems from scratch I really like the way Nancy provides just enough to make it easy while leaving almost everything else at the high end up to the developer (which is what we're here for, right?).
Anyway, I'm starting out on a project that is designed to support dynamic multi-tenancy situations (think something like WordPress or maybe a hosted CMS). Users will register and select a domain or subdomain and the application will handle incoming requests based in large part on the request domain. I know I can get access to this information in the context object, and using the context from a before handler is one thought I had. However, I think it would be more straightforward and perhaps be useful for other scenarios if I could specify routes with domain information just like we can for path information. For example, being able to add a route for both "{domain}/admin" and "" and allow the route selection logic to choose the more specific route for requests to "" and the more general one for a request like "" would be really cool - and allow some nice separation of logic into modules.
Does Nancy have any notion of domain/host outside of setting it in the request context? Can the default routing behavior be overridden or augmented to add this kind of support?
Re: Routing Based On Domain/Subdomain Dave Glick 4/9/12 12:46 PM
So I went ahead and used this as a chance to learn how Nancy works under the hood and was able to come up with a pretty simple solution that implements IRoutePatternMatcher (included below). It allows specifying host headers by prefixing the route path with a "!" (though I could have used any character). For routes with the prefix, the host header is inserted as the first segment and the default pattern matcher is then run. For example, a request for "" will be expanded to the request path "/" if the specified route path is "!{host}/test" but will stay as "/test" if the specified route path is "/test".

This all works well except for one thing - the scoring doesn't work like I'd like. Because the DefaultRouteResolver uses the count of "/" characters in the specified route path, the route paths "!{host}/test" and "/test" compare the same whereas I'd like the one with the domain to take precedence. There are a number of things I could do such as create a new route resolver with different scoring logic (yuck), somehow insert an extra slash into route paths that use a host name, etc. In general though, I wonder if the scoring mechanism could be better generalized. My thought is to add a score property to the IRoutePatternMatchResult interface and allow pattern matchers to score matches however is appropriate for them. This abstracts the concept of a "score" a little bit while still maintaining the underlying meaning because scores from a given pattern matcher should be consistent relative to each other. Thoughts? If this seems acceptable, I don't mind taking the lead on implementing the change and submitting a pull request.

- Dave

My current HostHeaderRoutePatternMatcher:

    /// <summary>
    /// A route pattern matcher that prepends the requested host header to the requested
    /// path if (and only if) the route path starts with "!".
    /// </summary>
    public class HostHeaderRoutePatternMatcher : IRoutePatternMatcher
        private readonly DefaultRoutePatternMatcher _default = new DefaultRoutePatternMatcher();

        public IRoutePatternMatchResult Match(string requestedPath, string routePath, NancyContext context)
            if (routePath.StartsWith("!"))
                // Treat the "!" as a "/" for the default pattern matcher
                routePath = "/" + routePath.Substring(1);

                // Use the Uri class to convert the requested host header and extract just the host without port, etc.
                Uri requestUri = new Uri("http://" + context.Request.Headers.Host);
                requestedPath = "/" + requestUri.Host + requestedPath;
            return _default.Match(requestedPath, routePath, context);