Nov 11, 2009

How Routing Works in asp.net mvc

The mvc stack start with the UrlRoutingModule. The module handle two HttpApplication event.

/* the order of application event is:
OnBeginRequest
OnAuthenticateRequest
OnPostAuthenticateRequest
OnAuthorizeRequest
OnPostAuthorizeRequest
OnResolveRequestCache
OnPostResolveRequestCache --> handled by  PostResolveRequestCache
OnPostMapRequestHandler --> handled by PostMapRequestHandler
OnAcquireRequestState
OnPostAcquireRequestState
OnPreRequestHandlerExecute
Page_Load Event of the Page
OnPostRequestHandlerExecute
OnReleaseRequestState
OnPostReleaseRequestState
OnUpdateRequestCache
OnPostUpdateRequestCache
OnEndRequest
OnPreSendRequestHeaders */
public virtual void PostResolveRequestCache(HttpContextBase context) { 
    // Match the incoming URL against the route table
    RouteData routeData = RouteCollection.GetRouteData(context); 

    // Do nothing if no route found
    if (routeData == null) { 
        return;
    }

    // If a route was found, get an IHttpHandler from the route's RouteHandler 
    IRouteHandler routeHandler = routeData.RouteHandler;
    if (routeHandler == null) { 
        throw new InvalidOperationException( 
            String.Format(
                CultureInfo.CurrentUICulture, 
                RoutingResources.UrlRoutingModule_NoRouteHandler));
    }

    // This is a special IRouteHandler that tells the routing module to stop processing 
    // routes and to let the fallback handler handle the request.
    if (routeHandler is StopRoutingHandler) { 
        return; 
    }

    RequestContext requestContext = new RequestContext(context, routeData);

    IHttpHandler httpHandler = routeHandler.GetHttpHandler(requestContext);
    if (httpHandler == null) { 
        throw new InvalidOperationException(
            String.Format( 
                CultureInfo.CurrentUICulture, 
                RoutingResources.UrlRoutingModule_NoHttpHandler,
                routeHandler.GetType())); 
    }

    // Save data to be used later in pipeline
    context.Items[_requestDataKey] = new RequestData() { 
        OriginalPath = context.Request.Path,
        HttpHandler = httpHandler 
    }; 

    // Rewrite path to something registered as a managed handler in IIS.  This is necessary so IIS7 will 
    // execute our managed handler (instead of say the static file handler).
    context.RewritePath("~/UrlRouting.axd");
}

public virtual void PostMapRequestHandler(HttpContextBase context)
{
    RequestData requestData = (RequestData)context.Items[_requestDataKey];
    if (requestData != null)
    {
        // Rewrite the path back to its original value, so the request handler only sees the original path.
        context.RewritePath(requestData.OriginalPath);

        // Set Context.Handler to the IHttpHandler determined earlier in the pipeline.
        context.Handler = requestData.HttpHandler;
    }
}

The route matching is handled by "RouteData routeData = RouteCollection.GetRouteData(context);". It search the Routes defined in start up event. If RouteData is not found, the handling will be passed back. Below is the code how RouteCollection handle the matching.

//RouteCollection member
public RouteData GetRouteData(HttpContextBase httpContext) { 
//... code skip ...
    // Go through all the configured routes and find the first one that returns a match
    using (GetReadLock()) { 
        foreach (RouteBase route in this) {
            RouteData routeData = route.GetRouteData(httpContext);
            if (routeData != null) {
                //the first matching wins
                return routeData; 
            }
        } 
    } 
    return null; 
}

//Route member
public override RouteData GetRouteData(HttpContextBase httpContext) {
    // Parse incoming URL (we trim off the first two chars since they're always "~/")
    string requestPath = httpContext.Request.AppRelativeCurrentExecutionFilePath.Substring(2) + httpContext.Request.PathInfo;

    RouteValueDictionary values = _parsedRoute.Match(requestPath, Defaults); 

    if (values == null) {
        // If we got back a null value set, that means the URL did not match 
        return null;
    }

    RouteData routeData = new RouteData(this, RouteHandler); 

    // Validate the values 
    if (!ProcessConstraints(httpContext, values, RouteDirection.IncomingRequest)) { 
        return null;
    } 

    // Copy the matched values
    foreach (var value in values) {
        routeData.Values.Add(value.Key, value.Value); 
    }

    // Copy the DataTokens from the Route to the RouteData 
    if (DataTokens != null) {
        foreach (var prop in DataTokens) { 
            routeData.DataTokens[prop.Key] = prop.Value;
        }
    }

    return routeData;
} 

public string Url {
    get { 
        return _url ?? String.Empty; 
    }
    set { 
        // The parser will throw for invalid routes. We don't have to worry
        // about _parsedRoute getting out of sync with _url since the latter
        // won't get set unless we can parse the route.
        _parsedRoute = RouteParser.Parse(value); 

        // If we passed the parsing stage, save the original URL value 
        _url = value; 
    }
} 

When the Url is set for a route, a _parsedRoute is created based on the Url, the _parseRoute is used to match the request url with the help of default values. Then the constraint is applied. If match is found, the RouteData can be returned. Before that DataTokens is copied to RouteData as well. DataToken is not actually used in the process of matching, we can think of DataToken is some predefined RouteData. RouteCollection has another use , to generate url based on routes. When search RouteData, constraints are also tested. Normally, it is an regular expression, but you can roll out your customized constraints, here is an article about this.

//route collection member
public VirtualPathData GetVirtualPath(RequestContext requestContext, RouteValueDictionary values) {
    requestContext = GetRequestContext(requestContext); 

    // Go through all the configured routes and find the first one that returns a match 
    using (GetReadLock()) { 
        foreach (RouteBase route in this) {
            VirtualPathData vpd = route.GetVirtualPath(requestContext, values); 
            if (vpd != null) {
                vpd.VirtualPath = GetUrlWithApplicationPath(requestContext, vpd.VirtualPath);
                return vpd;
            } 
        }
    } 

    return null;
} 

public VirtualPathData GetVirtualPath(RequestContext requestContext, string name, RouteValueDictionary values) {
    requestContext = GetRequestContext(requestContext);

    if (!String.IsNullOrEmpty(name)) {
        RouteBase namedRoute; 
        bool routeFound; 
        using (GetReadLock()) {
            routeFound = _namedMap.TryGetValue(name, out namedRoute); 
        }
        if (routeFound) {
            VirtualPathData vpd = namedRoute.GetVirtualPath(requestContext, values);
            if (vpd != null) { 
                vpd.VirtualPath = GetUrlWithApplicationPath(requestContext, vpd.VirtualPath);
                return vpd; 
            } 
            return null;
        } 
        else {
            throw new ArgumentException(
                String.Format(
                    CultureInfo.CurrentUICulture, 
                    RoutingResources.RouteCollection_NameNotFound,
                    name), 
                "name"); 
        }
    } 
    else {
        return GetVirtualPath(requestContext, values);
    }
} 

You can choose a named route or use all routes to build a url. It delegate build url to Route.GetVirtualPath method

public override VirtualPathData GetVirtualPath(RequestContext requestContext, RouteValueDictionary values) {
    // Try to generate a URL that represents the values passed in based on current 
    // values from the RouteData and new values using the specified Route.

    BoundUrl result = _parsedRoute.Bind(requestContext.RouteData.Values, values, Defaults, Constraints);

    if (result == null) {
        return null; 
    } 

    // Verify that the route matches the validation rules 
    if (!ProcessConstraints(requestContext.HttpContext, result.Values, RouteDirection.UrlGeneration)) {
        return null;
    }

    VirtualPathData vpd = new VirtualPathData(this, result.Url);

    // Add the DataTokens from the Route to the VirtualPathData 
    if (DataTokens != null) {
        foreach (var prop in DataTokens) { 
            vpd.DataTokens[prop.Key] = prop.Value;
        }
    }
    return vpd; 
}

Route is importaint object, it is used in url generation. It has Constraints, DataTokens, Defaults properties.