Nov 6, 2009

asp.net mvc request walkthrough

ASP.NET MVC request begin with a UrlRoutingModule. This component is not actually part of the System.Web.Mvc.dll, but of System.Web.Routing. You have to wire up in web.config like the following.

<httpModules>
 <add name="UrlRoutingModule" type="System.Web.Routing.UrlRoutingModule, System.Web.Routing, Version=3.5.0.0, Culture=neutral, PublicKeyToken=31BF3856AD364E35"/>
</httpModules>  

After that you can define your matching url pattern and the IRouteHandler which handle the matched url. You can add route like the following code.

Route route = new Route("{controller}/{action}/{id}", new MvcRouteHandler()) ;
route.Defaults = new RouteValueDictionary( new { controller = "Home", action = "Index", id = "" });
RouteTable.Routes.Add("Default", route);

Of course, you can add the use the extension method define in System.Web.Mvc.RouteCollectionExtensions. You can add a route with your own IRouteHandler. The purpose of IRouteHandler is very simple, to create a IHttpHandler.

public class MvcRouteHandler : IRouteHandler {
    protected virtual IHttpHandler GetHttpHandler(RequestContext requestContext) {
        return new MvcHandler(requestContext);
    }

    #region IRouteHandler Members
    IHttpHandler IRouteHandler.GetHttpHandler(RequestContext requestContext) {
        return GetHttpHandler(requestContext);
    }
    #endregion
}

The RequestContext object is interesting object.It contains a HttpContextBase object and a RouteData object.This object will be used through out the mvc stack. You can also change the mvc behavior by subclassing the MvcHandler.

protected virtual void ProcessRequest(HttpContext httpContext) {
    HttpContextBase iHttpContext = new HttpContextWrapper(httpContext);
    ProcessRequest(iHttpContext);
}

protected internal virtual void ProcessRequest(HttpContextBase httpContext) {
    AddVersionHeader(httpContext);

    // Get the controller type
    string controllerName = RequestContext.RouteData.GetRequiredString("controller");

    // Instantiate the controller and call Execute
    IControllerFactory factory = ControllerBuilder.GetControllerFactory();
    IController controller = factory.CreateController(RequestContext, controllerName);
    if (controller == null) {
        throw new InvalidOperationException(
            String.Format(
                CultureInfo.CurrentUICulture,
                MvcResources.ControllerBuilder_FactoryReturnedNull,
                factory.GetType(),
                controllerName));
    }
    try {
        controller.Execute(RequestContext);
    }
    finally {
        factory.ReleaseController(controller);
    }
}

The IControllerFactory by default is DefaultControllerFactory. This can be changed in the application_startup method. The _factoryThunk is an Func delegate. ControllerFactory is used to build a controller. Sometimes, you need to use IoC to inject dependency to into the container, if so you need to create your ControllerFactory.

ControllerBuilder.Current.SetControllerFactory(new 
StructureMapControllerFactory());

public class ControllerBuilder {
 //.. other members
public void SetControllerFactory(IControllerFactory controllerFactory) {
    if (controllerFactory == null) {
        throw new ArgumentNullException("controllerFactory");
    }

    _factoryThunk = () => controllerFactory;
}

public void SetControllerFactory(Type controllerFactoryType) {
    if (controllerFactoryType == null) {
        throw new ArgumentNullException("controllerFactoryType");
    }
    if (!typeof(IControllerFactory).IsAssignableFrom(controllerFactoryType)) {
        throw new ArgumentException(
            String.Format(
                CultureInfo.CurrentUICulture,
                MvcResources.ControllerBuilder_MissingIControllerFactory,
                controllerFactoryType),
            "controllerFactoryType");
    }

    _factoryThunk = delegate() {
        try {
            return (IControllerFactory)Activator.CreateInstance(controllerFactoryType);
        }
        catch (Exception ex) {
            throw new InvalidOperationException(
                String.Format(
                    CultureInfo.CurrentUICulture,
                    MvcResources.ControllerBuilder_ErrorCreatingControllerFactory,
                    controllerFactoryType),
                ex);
        }
    };
}

}

The most commonly used controller is System.Web.Mvc.Controller, which inherit ControllerBase class. It has a property ControllerContext, which holds a RequestContext object, and a reference the Controller itself. MvcHandler will call the controller's Execute(RequestContext) method, which calls ExecuteCore method. As a developer, you need to implement your action method, the ExecuteCore method will try to call you action method.

protected override void ExecuteCore() {
    TempData.Load(ControllerContext, TempDataProvider);

    try {
        string actionName = RouteData.GetRequiredString("action");
        if (!ActionInvoker.InvokeAction(ControllerContext, actionName)) {
            HandleUnknownAction(actionName);
        }
    }
    finally {
        TempData.Save(ControllerContext, TempDataProvider);
    }
}

public IActionInvoker ActionInvoker {
    get {
        if (_actionInvoker == null) {
            _actionInvoker = new ControllerActionInvoker();
        }
        return _actionInvoker;
    }
    set {
        _actionInvoker = value;
    }
}

The ActionInvoker is also an extension point, you can set this property when the controller is created, you can do this in your ControllerFactory. By default, it is ControllerActionInvoker. Below is the code how ControllerActionInvoker select a action method, and invoking it. This is most interesting part.

// class ControllerActionInvoker
public virtual bool InvokeAction(ControllerContext controllerContext, string actionName) {
    if (controllerContext == null) {
        throw new ArgumentNullException("controllerContext");
    }
    if (String.IsNullOrEmpty(actionName)) {
        throw new ArgumentException(MvcResources.Common_NullOrEmpty, "actionName");
    }
    //this will return ReflectedControllerDescriptor, which is used to expose
   //some meta of the controller, which implement System.Reflection.ICustomAttributeProvider
    ControllerDescriptor controllerDescriptor = GetControllerDescriptor(controllerContext);
    //Use controllerDescriptor build a ActionDescripter which is a ReflectedActionDescriptor, which also implements ICustomAttributeProvider
    ActionDescriptor actionDescriptor = FindAction(controllerContext, controllerDescriptor, actionName);
    if (actionDescriptor != null) {
        //FilterInfo has four list,ActionFilters(IActionFilter), AuthorizationFilters(IAuthorizationFilter), ExceptionFilters(IExceptionFilter), ResultFilters(IResultFilter), these list contains FilterAttribute, but also the controller itself, because Controller implements  IActionFilter, IAuthorizationFilter, IExceptionFilter, IResultFilter
        FilterInfo filterInfo = GetFilters(controllerContext, actionDescriptor);

        try {
            AuthorizationContext authContext = InvokeAuthorizationFilters(controllerContext, filterInfo.AuthorizationFilters, actionDescriptor);
            if (authContext.Result != null) {
                // the auth filter signaled that we should let it short-circuit the request
                InvokeActionResult(controllerContext, authContext.Result);
            }
            else {
                if (controllerContext.Controller.ValidateRequest) {
                    ValidateRequest(controllerContext.HttpContext.Request);
                }
                //parameters can build from controllerContext, and will be feed to actionMethod later.
                IDictionary<string, object> parameters = GetParameterValues(controllerContext, actionDescriptor);
                //your action method will be executed, and ActionResult will be 
                //returned here, which normally is ViewResult
                ActionExecutedContext postActionContext = InvokeActionMethodWithFilters(controllerContext, filterInfo.ActionFilters, actionDescriptor, parameters);
                //the ActionExecutedContext.Result will be examined, if it is ActionResult, it will be invoked to render a view
                InvokeActionResultWithFilters(controllerContext, filterInfo.ResultFilters, postActionContext.Result);
            }
        }
        catch (ThreadAbortException) {
            // This type of exception occurs as a result of Response.Redirect(), but we special-case so that
            // the filters don't see this as an error.
            throw;
        }
        catch (Exception ex) {
            // something blew up, so execute the exception filters
            ExceptionContext exceptionContext = InvokeExceptionFilters(controllerContext, filterInfo.ExceptionFilters, ex);
            if (!exceptionContext.ExceptionHandled) {
                throw;
            }
            InvokeActionResult(controllerContext, exceptionContext.Result);
        }

        return true;
    }
    // notify controller that no method matched
    return false;
}

There are lots implementation details here, I am going to skip most of them here. And focus on how a view is rendered. When ActionResult is returned from the method call InvokeActionMethodWithFilters, it will be passed in the method InvokeActionResultWithFilters, that is the beginning of rendering a view. The following code will be eventually run.

//ControllerActionInvoker member
protected virtual void InvokeActionResult(ControllerContext controllerContext, ActionResult actionResult)
{
    actionResult.ExecuteResult(controllerContext);
}

ViewResult inherit from ViewResultBase, which inherite ActionResult. The interesting method is ExecuteResult(ControllerContext), it basically to find a view and then render the view.

//ViewResultBase member
public override void ExecuteResult(ControllerContext context) {
    if (context == null) {
        throw new ArgumentNullException("context");
    }
    if (String.IsNullOrEmpty(ViewName)) {
        ViewName = context.RouteData.GetRequiredString("action");
    }

    ViewEngineResult result = null;

    //View is property Of ViewResultBase, it is Type IView
    if (View == null) {
        result = FindView(context);
        View = result.View;
    }
    //push the ViewData, TempData into viewCotext, which is passed into View.Render()
    ViewContext viewContext = new ViewContext(context, View, ViewData, TempData);
    View.Render(viewContext, context.HttpContext.Response.Output);

    if (result != null) {
        result.ViewEngine.ReleaseView(context, View);
    }
}

public interface IView {
    void Render(ViewContext viewContext, TextWriter writer);
}

//ViewResult member
protected override ViewEngineResult FindView(ControllerContext context) {
    //ViewEngineCollection
    ViewEngineResult result = ViewEngineCollection.FindView(context, ViewName, MasterName);
    if (result.View != null) {
        return result;
    }

    // we need to generate an exception containing all the locations we searched
    StringBuilder locationsText = new StringBuilder();
    foreach (string location in result.SearchedLocations) {
        locationsText.AppendLine();
        locationsText.Append(location);
    }
    throw new InvalidOperationException(String.Format(CultureInfo.CurrentUICulture,
        MvcResources.Common_ViewNotFound, ViewName, locationsText));
}

//ViewResultBase member, this is an extension point
//we can add our ViewEngine here.
[SuppressMessage("Microsoft.Usage", "CA2227:CollectionPropertiesShouldBeReadOnly",
    Justification = "This entire type is meant to be mutable.")]
public ViewEngineCollection ViewEngineCollection {
    get {
        return _viewEngineCollection ?? ViewEngines.Engines;
    }
    set {
        _viewEngineCollection = value;
    }
}

public static class ViewEngines {

    private readonly static ViewEngineCollection _engines = new ViewEngineCollection {
        new WebFormViewEngine() 
    };

    public static ViewEngineCollection Engines {
        get {
            return _engines;
        }
    }
}

We can see here, to find a view, we need a ViewEngineCollection. By default this ViewEngineCollection has one IViewEngin(WebFormViewEngine). The purpose a IViewEngine is to find partial view and view, like the following. But WebFormViewEngine inherit VirtualPathProviderViewEngine, which implmenet IViewEngine. To find View, first we need to locate the Physical path of the view file(aspx or ascx), to decide whether a file exist, there is to ask a buildManager to build an instance of it. Physical path searhing follow a couple of pattern, if the view file can not be located. Then an exeption will be throw. If found a the path will be cached for next probing.

public interface IViewEngine {
    ViewEngineResult FindPartialView(ControllerContext controllerContext, string partialViewName, bool useCache);
    ViewEngineResult FindView(ControllerContext controllerContext, string viewName, string masterName, bool useCache);
    void ReleaseView(ControllerContext controllerContext, IView view);
}

//VirtualPathProviderViewEngine member
public virtual ViewEngineResult FindView(ControllerContext controllerContext, string viewName, string masterName, bool useCache) {
    if (controllerContext == null) {
        throw new ArgumentNullException("controllerContext");
    }
    if (String.IsNullOrEmpty(viewName)) {
        throw new ArgumentException(MvcResources.Common_NullOrEmpty, "viewName");
    }

    string[] viewLocationsSearched;
    string[] masterLocationsSearched;

    string controllerName = controllerContext.RouteData.GetRequiredString("controller");
    string viewPath = GetPath(controllerContext, ViewLocationFormats, "ViewLocationFormats", viewName, controllerName, _cacheKeyPrefix_View, useCache, out viewLocationsSearched);
    string masterPath = GetPath(controllerContext, MasterLocationFormats, "MasterLocationFormats", masterName, controllerName, _cacheKeyPrefix_Master, useCache, out masterLocationsSearched);

    if (String.IsNullOrEmpty(viewPath) || (String.IsNullOrEmpty(masterPath) && !String.IsNullOrEmpty(masterName))) {
        return new ViewEngineResult(viewLocationsSearched.Union(masterLocationsSearched));
    }

    return new ViewEngineResult(CreateView(controllerContext, viewPath, masterPath), this);
}


//WebFormViewEngine constructor
public WebFormViewEngine() {
    MasterLocationFormats = new[] {
        "~/Views/{1}/{0}.master",
        "~/Views/Shared/{0}.master"
    };

    ViewLocationFormats = new[] {
        "~/Views/{1}/{0}.aspx",
        "~/Views/{1}/{0}.ascx",
        "~/Views/Shared/{0}.aspx",
        "~/Views/Shared/{0}.ascx"
    };

    PartialViewLocationFormats = ViewLocationFormats;
}

The WebFormViewEngine defines how to create view, it implement two abstract method of VirtualPathProviderViewEngine.

protected override IView CreatePartialView(ControllerContext controllerContext, string partialPath) {
    return new WebFormView(partialPath, null);
}

protected override IView CreateView(ControllerContext controllerContext, string viewPath, string masterPath) {
    return new WebFormView(viewPath, masterPath);
}

The WebFormView use tranditional aspx ViewPage or ViewUserControl ascx to render view.

//WebFormView  members
public virtual void Render(ViewContext viewContext, TextWriter writer) {
    if (viewContext == null) {
        throw new ArgumentNullException("viewContext");
    }
    
    //viewInstance is an instance of ViewPage(aspx or ascx)
    object viewInstance = BuildManager.CreateInstanceFromVirtualPath(ViewPath, typeof(object));
    if (viewInstance == null) {
        throw new InvalidOperationException(
            String.Format(
                CultureInfo.CurrentUICulture,
                MvcResources.WebFormViewEngine_ViewCouldNotBeCreated,
                ViewPath));
    }


    ViewPage viewPage = viewInstance as ViewPage;
    if (viewPage != null) {
        RenderViewPage(viewContext, viewPage);
        return;
    }
    //we can see that viewInstance does not necessary to be a ViewPage, it can be ViewUserControl as well
    ViewUserControl viewUserControl = viewInstance as ViewUserControl;
    if (viewUserControl != null) {
        RenderViewUserControl(viewContext, viewUserControl);
        return;
    }

    throw new InvalidOperationException(
        String.Format(
            CultureInfo.CurrentUICulture,
            MvcResources.WebFormViewEngine_WrongViewBase,
            ViewPath));
}

private void RenderViewPage(ViewContext context, ViewPage page) {
    if (!String.IsNullOrEmpty(MasterPath)) {
        page.MasterLocation = MasterPath;
    }
    //passed in ViewData to aspx or ascx
    page.ViewData = context.ViewData;
    page.RenderView(context);
}

private void RenderViewUserControl(ViewContext context, ViewUserControl control) {
    if (!String.IsNullOrEmpty(MasterPath)) {
        throw new InvalidOperationException(MvcResources.WebFormViewEngine_UserControlCannotHaveMaster);
    }

    //passed in ViewData to aspx or ascx
    control.ViewData = context.ViewData;
    control.RenderView(context);
}

To render a ViewPage, firstly, it initialized three very handy object will be used extensively in the aspx or ascx, they are Ajax(AjaxHelper), Html(HtmlHelper),Url(UrlHelper). Then it delegate the control to ProcessRequest method of Page class.

//public class ViewPage : Page, IViewDataContainer
public virtual void RenderView(ViewContext viewContext) {
    ViewContext = viewContext;
    InitHelpers();
    // Tracing requires Page IDs to be unique.
    ID = Guid.NewGuid().ToString();
    ProcessRequest(HttpContext.Current);
}


public virtual void InitHelpers() {
    Ajax = new AjaxHelper(ViewContext, this);
    Html = new HtmlHelper(ViewContext, this);
    Url = new UrlHelper(ViewContext.RequestContext);
}

HtmlHelper is responsible to render partial view.

internal virtual void RenderPartialInternal(string partialViewName, ViewDataDictionary viewData, object model, ViewEngineCollection viewEngineCollection) {
    if (String.IsNullOrEmpty(partialViewName)) {
        throw new ArgumentException(MvcResources.Common_NullOrEmpty, "partialViewName");
    }
    
    //a new copy a ViewData is created 
    ViewDataDictionary newViewData = null;

    if (model == null) {
        if (viewData == null) {
            newViewData = new ViewDataDictionary(ViewData);
        }
        else {
            newViewData = new ViewDataDictionary(viewData);
        }
    }
    else {
        if (viewData == null) {
            newViewData = new ViewDataDictionary(model);
        }
        else {
            newViewData = new ViewDataDictionary(viewData) { Model = model };
        }
    }
    //a newViewContext is created, not shared with passed in data
    ViewContext newViewContext = new ViewContext(ViewContext, ViewContext.View, newViewData, ViewContext.TempData);
    //use the passed in viewEngineCollectio to newViewContext to find a partial view
    IView view = FindPartialView(newViewContext, partialViewName, viewEngineCollection);
    view.Render(newViewContext, ViewContext.HttpContext.Response.Output);
}

//ViewDataDictionary member
[SuppressMessage("Microsoft.Usage", "CA2214:DoNotCallOverridableMethodsInConstructors",
    Justification = "See note on SetModel() method.")]
public ViewDataDictionary(ViewDataDictionary dictionary) {
    if (dictionary == null) {
        throw new ArgumentNullException("dictionary");
    }

    foreach (var entry in dictionary) {
        _innerDictionary.Add(entry.Key, entry.Value);
    }
    foreach (var entry in dictionary.ModelState) {
        ModelState.Add(entry.Key, entry.Value);
    }
    Model = dictionary.Model;
}