In my initial days of learning MVC, I was curious about its life cycle. But I found it a bit confusing. So I have documented my understanding. I hope this may help someone. This is a purely conceptual thing. To understand this, one needs to have a solid understanding of OOP concepts (especially about classes, interfaces, abstraction, inheritances, and so on).
What happens in a normal ASP.NET application?
- In an ASP.NET application, each ASP.NET page inherits from System.Web.UI.Page that implements the IHTTPHandler interface.
- This interface has an abstract method ProcessRequest() and hence is implemented in the Page class. This method is called when you request a page.
- The ProcessRequest() method takes an instance of HttpContext and is responsible for processing the request and generating the response.
So in an ASP.NET application, it is straightforward, you request a page with a URL like http://mysite/default.aspx. Then, ASP.NET searches for that page on the disk executes the ProcessRequest() method, and generates and renders the response. There is a one-to-one mapping between the URL and the physical page.
The ASP.NET MVC Process
In an MVC application, no physical page exists for a specific request. All the requests are routed to a special class called the Controller. The controller is responsible for generating the response and sending the content back to the browser. Also, there is a many-to-one mapping between the URL and controller.
When you request an MVC application, you are directly calling the action method of a controller.
When you request http://mysite/Controller1/method1, you are calling Controller1's method1. We will see how our request is routed to an ActionMethod of a controller.
The procedure involved is
- An instance of the RouteTable class is created on application start. This happens only once when the application is requested for the first time.
- The UrlRoutingModule intercepts each request, finds a matching RouteData from a RouteTable, and instantiates an MVCHandler (an HttpHandler).
- The MVCHandler creates a DefaultControllerFactory (you can create your controller factory also). It processes the RequestContext and gets a specific controller (from the controllers you have written). Creates a ControllerContext. es the controller a ControllerContext and executes the controller.
- Gets the ActionMethod from the RouteData based on the URL. The Controller Class then builds a list of parameters (to the ActionMethod) from the request.
- The ActionMethod returns an instance of a class inherited from the ActionResult class and the View Engine renders a view as a web page.
Now, let's understand in detail
Every ASP.NET MVC application has a RouteTable class. This RouteTable is responsible for mapping the MVC requests to a specific controller's ActionMethod.Whenever the application starts, the Application_Start event will be fired and it will call the RegisterRoutes() with the collection of all the available routes of the MVC application as a parameter that will add the routes to the Routes property of the System.Web.Routing.RouteTable class. The Routes property is of type RouteCollection.
Global.asax
public class MvcApplication : System.Web.HttpApplication
{
protected void Application_Start()
{
WebSecurity.InitializeDatabaseConnection(
"DefaultConnection",
"UserProfile",
"UserId",
"UserName",
autoCreateTables: true
);
AreaRegistration.RegisterAllAreas();
WebApiConfig.Register(GlobalConfiguration.Configuration);
FilterConfig.RegisterGlobalFilters(GlobalFilters.Filters);
RouteConfig.RegisterRoutes(RouteTable.Routes);
BundleConfig.RegisterBundles(BundleTable.Bundles);
}
}
RouteConfig.cs
public class RouteConfig
{
public static void RegisterRoutes(RouteCollection routes)
{
routes.IgnoreRoute("{resource}.axd/{*pathInfo}"); // Ignore route ending with axd
routes.MapRoute(
name: "Default", // route name
url: "{controller}/{action}/{id}", // Url pattern
defaults: new { controller = "Home", action = "Index", id = UrlParameter.Optional } // create a default route
);
}
}
The MapRoute() creates a default route. Adds all routes to the RouteTable and associates the RouteHandlers with the routes.
Now, all the mapped routes are stored as a RouteCollection in the Routes property of the RouteTable class.
Note. The RouteTable class has a Routes property that holds a collection of objects that derive from the RouteBase class. The RouteCollection class is derived from Collection<RouteBase>. Hence RegisterRoutes() is taking an object of RouteCollection.
When an ASP.NET MVC application handles a request, the application iterates through the collection of routes in the Routes property to find the route that matches the format of the URL requested. The application uses the first route that it finds in the collection that matches the URL. So the most specific route should be added first then the general ones.
Whenever you request an ASP.NET MVC application, the request is intercepted by the UrlRoutingModule (an HTTP Module) and
The UrlRoutingModule wraps up the current HttpContext (including the URL, form parameters, query string parameters, and cookies associated with the current request) in an HttpContextWrapper object as below.
Note. The HttpContext class has no base class and isn't virtual hence is unusable for testing (it cannot be mocked). The HttpContextBase class is a (from C# 3.5) replacement for HttpContext. Since it is abstract, it is mockable. It is concretely implemented by HttpContextWrapper. To create an instance of HttpContextBase in a normal web page, we use
New HttpContextWrapper(HttpContext.Current).
private void OnApplicationPostResolveRequestCache(object sender, EventArgs e)
{
HttpApplication application = (HttpApplication)sender;
HttpContextBase context = new HttpContextWrapper(application.Context);
this.PostResolveRequestCache(context);
}
Now, the module sends this HTTPContextBase object to the PostResolveRequestCache() method.
Based on the HttpBaseContext object, the postResolveRequestCache() will return the correct RouteData from the RouteTable (that was created in the previous Step #1).
If the UrlRoutingModule successfully retrieves a RouteData object then it creates a RequestContext object that represents the current HttpContext and RouteData.
The UrlRoutingModule then instantiates a new HttpHandler based on the RouteTable and passes the RequestContext (created in Step C) to the new handler's constructor.
For an ASP.NET MVC application, the handler returned from the RouteTable will always be a MvcHandler. This MVCHandler implements an IHTTPHandler interface and hence the ProcessRequest() method.
Finally, it will call the RemapHandler() method that will set the MVCHandler just obtained to be the Current HTTP Handler.
public virtual void PostResolveRequestCache(HttpContextBase context)
{
RouteData routeData = this.RouteCollection.GetRouteData(context);
if (routeData != null)
{
IRouteHandler routeHandler = routeData.RouteHandler;
if (routeHandler == null)
{
throw new InvalidOperationException(string.Format(CultureInfo.CurrentCulture,
SR.GetString("UrlRoutingModule_NoRouteHandler"), new object[0]));
}
if (!(routeHandler == StopRoutingHandler))
{
RequestContext requestContext = new RequestContext(context, routeData);
context.Request.RequestContext = requestContext;
IHttpHandler httpHandler = routeHandler.GetHttpHandler(requestContext);
if (httpHandler == null)
{
throw new InvalidOperationException(
string.Format(CultureInfo.CurrentUICulture,
SR.GetString("UrlRoutingModule_NoHttpHandler"),
new object[] { routeHandler.GetType() }));
}
if (httpHandler == UrlAuthFailureHandler)
{
if (!FormsAuthenticationModule.FormsAuthRequired)
{
throw new HttpException(0x191, SR.GetString("Assess_Denied_Description3"));
}
UrlAuthorizationModule.ReportUrlAuthorizationFailure(HttpContext.Current, this);
}
else
{
context.RemapHandler(httpHandler);
}
}
}
}
MVCHandler is also inherited from the IHTTPAsyncHandler hence implementing the ProcessRequest() method. When MVCHandler executes, it calls the ProcessRequest() method which in turn calls the ProcessRequestInit() method.
The ProcessRequestInit() method creates a ControllerFactory and a Controller. The Controller is created from a ControllerFactory. There is a ControllerBuilder class that will set the ControllerFactory.
Note. By default, it will be DefaultControllerFactory. But you can create your own ControllerFactory as well.
By implementing the IControllerFactory interface and then adding the following code to the Application_Start event in the global. asax.
ControllerBuilder.Current.SetControllerFactory(type of (NewFactory))
Now, the NewFactory will be used instead of the DefaultControllerFactory.
The RequestContext and the name of the Contoller (from the URL) will be passed to the CreateController() method to get the specific Contoller (that you have written).
Next, a ControllerContext object is constructed from the RequestContext and the controller using the method GetContollerInstance().
private void ProcessRequestInit(HttpContextBase httpContext, out IController controller, out IControllerFactory factory)
{
bool? isRequestValidationEnabled = ValidationUtility.IsValidationEnabled(HttpContext.Current);
if (isRequestValidationEnabled == true)
{
ValidationUtility.EnableDynamicValidation(HttpContext.Current);
}
AddVersionHeader(httpContext);
RemoveOptionalRoutingParameters();
// Get the controller type
string controllerName = RequestContext.RouteData.GetRequiredString("controller");
// Instantiate the controller and call Execute
factory = ControllerBuilder.GetControllerFactory();
controller = factory.CreateController(RequestContext, controllerName);
if (controller == null)
{
throw new InvalidOperationException(
String.Format(CultureInfo.CurrentCulture, MvcResources.ControllerBuilder_FactoryReturnedNull, factory.GetType(), controllerName));
}
}
public virtual IController CreateController(RequestContext requestContext, string controllerName)
{
if (requestContext == null)
{
throw new ArgumentNullException("requestContext");
}
if (string.IsNullOrEmpty(controllerName))
{
throw new ArgumentException(MvcResources.Common_NullOrEmpty, "controllerName");
}
Type controllerType = this.GetControllerType(requestContext, controllerName);
return this.GetControllerinstance(requestContext, controllerType);
}
Our Controller inherits from the Controller class that inherits from ControllerBase that implements the Icontroller interface. The Controller interface has an Execute() abstract method that is implemented in the ControllerBase class.
public abstract class ControllerBase : IController
{
protected virtual void Execute(RequestContext requestContext)
{
if (requestContext == null)
{
throw new ArgumentNullException("requestContext");
}
if (requestContext.HttpContext == null)
{
throw new ArgumentException(
MvcResources.ControllerBase_CannotExecuteWithNullHttpContext,
"requestContext");
}
VerifyExecuteCalledOnce();
Initialize(requestContext);
using (ScopeStorage.CreateTransientScope())
{
ExecuteCore();
}
}
protected abstract void ExecuteCore();
// Other stuff here
}
This controller class is something that you wrote. So one of the methods that you wrote for your controller class is invoked.
Note. controller methods that are decorated with the [NonAction] attribute will never be executed.
Finally, It will call the InvokeAction method to execute the Action.
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");
}
ControllerDescriptor controllerDescriptor = this.GetControllerDescriptor(controllerContext);
ActionDescriptor actionDescriptor = this.FindAction(controllerContext, controllerDescriptor, actionName);
if (actionDescriptor == null)
{
return false;
}
FilterInfo filters = this.GetFilters(controllerContext, actionDescriptor);
try
{
AuthorizationContext context = this.InvokeAuthorizationFilters(controllerContext, filters.AuthorizationFilters, actionDescriptor);
if (context.Result != null)
{
this.InvokeActionResult(controllerContext, context.Result);
}
else
{
if (controllerContext.Controller.ValidateRequest)
{
ValidateRequest(controllerContext);
}
IDictionary<string, object> parameterValues = this.GetParameterValues(controllerContext, actionDescriptor);
ActionExecutedContext context2 = this.InvokeActionMethodWithFilters(controllerContext, filters.ActionFilters, actionDescriptor, parameterValues);
this.InvokeActionResultWithFilters(controllerContext, filters.ResultFilters, context2.Result);
}
}
catch (ThreadAbortException)
{
throw;
}
catch (Exception exception)
{
ExceptionContext context3 = this.InvokeExceptionFilters(controllerContext, filters.ExceptionFilters, exception);
if (!context3.ExceptionHandled)
{
throw;
}
this.InvokeActionResult(controllerContext, context3.Result);
}
return true;
}
The ViewBag, ViewData, TempData, and so on properties of the ControllerBase class are initialized. These properties are used for passing data from the View to the Controller or vice-versa or among action methods.
The Execute() method of the ControllerBase class is executed which calls the ExecuteCore() abstract method. ExecuteCore() is implemented in the Controller class.
protected override void ExecuteCore()
{
PossiblyLoadTempData();
try
{
string actionName = RouteData.GetRequiredString("action");
if (!ActionInvoker.InvokeAction(ControllerContext, actionName))
{
HandleUnknownAction(actionName);
}
}
finally
{
PossiblySaveTempData();
}
}
The ExecuteCore() method gets the Action name from the RouteData based on the URL.
The ExecuteCore() method then calls the InvokeAction() method of the ActionInvoker class. This builds a list of parameters from the request. This list of parameters is ed as method parameters to the action method that is executed. Here the Descriptor objects viz.ControllerDescriptor and ActionDescriptor, which provide information on the controller (like name, type, and actions) and Action (name, parameter, and controller) respectively play a major role. Now you have your Controller name and Action name.
The Controller returns an instance of ActionResult. The Controller typically executes one of the helper methods (mostly View() that returns an instance of the ViewResult class, that is derived from the ActionResult class). Here's the list of classes that extend from the ActionResult class. You just need to call a specific Helper method to return the respective ActionResult.
[Courtesy: Controllers and Action Methods in ASP.NET MVC Applications ]
public abstract class ActionResult
{
public abstract void ExecuteResult(ControllerContext context);
}
ExecuteResult() is implemented differently in various sub-classes of ActionResult. ViewResult is the most commonly used ActionResult. So let's discuss this.
The following happens after the ExecuteResult() method of ViewResult is called.
- ViewResultBase calls the FindView() method of the ViewResult class.
- The FindView() method of the ViewResult class returns an instance of the ViewEngineResult class.
- The Render() method of the ViewEngineResult class is called to Render the view using the ViewEngine.
- The Render() method internally calls the RenderViewPage() method that sets the master page location and ViewData.
The response is rendered on the client's browser.
public virtual ViewEngineResult FindView(ControllerContext controllerContext, string viewName, string masterName)
{
if (controllerContext == null)
{
throw new ArgumentNullException("ControllerContext");
}
if (string.IsNullOrEmpty(viewName))
{
throw new ArgumentException(MvcResources.Common_NullOrEmpty, "viewName");
}
Func<IViewEngine, ViewEngineResult> cacheLocator = e => e.FindView(controllerContext, viewName, masterName, true);
Func<IViewEngine, ViewEngineResult> locator = e => e.FindView(controllerContext, viewName, masterName, false);
return Find(cacheLocator, locator);
}
public virtual void Render(ViewContext viewContext, TextWriter writer)
{
if (viewContext == null)
{
throw new ArgumentNullException("viewContext");
}
object obj2 = this.BuildManager.CreateInstanceFromVirtualPath(this.Viewpath, typeof(object));
if (obj2 == null)
{
throw new InvalidOperationException(string.Format(CultureInfo.CurrentUICulture, MvcResources.WebFormViewEngine_ViewCouldNotBeCreated, new object[] { this.ViewPath }));
}
ViewPage page = (ViewPage)obj2;
if (page != null)
{
this.RenderViewPage(viewContext, page);
}
else
{
ViewUserControl control = (ViewUserControl)obj2;
if (control == null)
{
throw new InvalidOperationException(string.Format(CultureInfo.CurrentUICulture, MvcResources.WebFormViewEngine_WrongViewBase, new object[] { this.ViewPath }));
}
this.RenderViewUserControl(viewContext, control);
}
}
private void RenderViewPage(ViewContext context, ViewPage page)
{
if (!string.IsNullOrEmpty(this.MasterPath))
{
page.MasterLocation = this.MasterPath;
}
page.ViewData = context.ViewData;
page.RenderView(context);
}
Note. The ViewPage class is derived from the System.Web.UI.Page class. This is the same base class from which classic ASP.NET pages are derived.
The RenderView() method finally calls the ProcessRequest() method of the Page class that renders the view in the client browser.
References