In This Series
- ASP.NET Core MVC - Repository Overview: Model Binding
- ASP.NET MVC Core - Repository Overview: Value Providers
- ASP.NET Core 2.0 - Repository Overview: Action Discovery
- ASP.NET Core 2.0 - Repository Overview: Action Selection
- ASP.NET Core 2.0 - Repository Overview: Razor Pages
- ASP.NET Core 2.0 - Repository Overview: Action Results
Contents
- Introduction
- What is the View Result
- Returning a view result
- Executing a view result
- The Executor
- Finding and Compiling your View
- Summary
Introduction
This series tries to explain the underlying mechanisms of ASP.NET Core in a unique way. Instead of only introducing main features, it digs deep into the ASP.NET Core GitHub repository, giving you insight on how it all works. This instalment will discuss action results, taking a deep dive into the popular view result implementation.
What is the View Result
In ASP.NET Core, and in traditional MVC, there are many different types of action results. Some of the most popular are:
The ViewResult is the main action result for rendering razor views to the http response body. It is the interaction point for a routing handler to invoke your views, and complex enough to dedicate its own article to.
Returning a View Result
When creating a controller action, we often return a view result:
The View() method above will call the following on the Controller class:
This will eventually create a ViewResult class for us, demonstrated in the second method.
A view result is one example of an IActionResult. IActionResult’s are responsible for processing the result of an action. The IActionResult’s signature looks like the following:
A very simple signature, with one key method: The ExecuteResultAsync method.
Executing a View Result
As described in previous articles of this series, the system will have a list of action descriptors to be ran. The MvcRouteHandler and MvcAttributeRouteHandler are the main IRouter implementations to kick this off. These handlers will be called by routing and given a route context.
Once the action selection process has finished, these handlers will generate an action invoker (in our case the ControllerActionInvoker), and give it a newly created action context.
N.B. The ResourceInvoker class is responsible for the MVC action invocation pipeline. It chooses when things should be ran, and at what point the execution of the action result should occur. More can be read here.
In the above diagram, we can see the call hierarchy from the route handler, all the way down to the call of the ExecuteResultAsync method.
The Executor
For each type of view result, there is also a corresponding IActionResultExecutor. So, the view result will call ExecuteAsync on this class:
Finding and Compiling your View
The following image shows the execution path of find and compiling a razor view, after the view result is executed:
The first job of the ViewResultExecutor class is to find the specific view specified. This is the first point at which the RazorViewEngine class is used.
N.B. The ViewResultExecutor class uses either the IViewEngine instance registered in dependency injection, or one specified on your view result if any. This means you can assign a view engine at runtime to your view result.
You can register your own view engines with the dependency injection system. The first view engine to return a result will be used. This logic can be seen in the CompositeViewEngine.
Due to us not specifying a view name, the razor view engine will have to find our view using the current action context.
View Location Expanders
With the help of IViewLocationExpander interfaces, the engine populates values for the view name, controller name, area name, and page name:
View location expanders are a great way to extend the functionality of locating views. For example, the framework itself provides a language view location expander. This means that we can provide views such as Index.en-US.cshtml, to serve a view specific to a locale. These can be added at startup through the AddViewLocalization extension method.
View Locator Cache
Using the view location expanders, along with the current action context, we create a ViewLocationCacheKey. This key is used to identify a ViewLocationCacheResult. If there is no cache entry, a ViewLocationCacheResult is created through the OnCacheMiss method in the engine.
The first step is to find the view location formats specified in the system. These are defined in the global RazorViewEngineOptions and configured in its setup class:
These are the default locations for views the system uses. The placeholders {0} and {1} are filled out by the current context-e.g., the controller’s name, or the current culture.
In our very first example, we had a standard action in a controller. This means the view location formats will be chosen.
For each view location, the view, controller, and area names are filled in, and the path is resolved. For example, with the controller “Home”, and action “Index”, we will have the following view locations by default:
The first location to be able to generate a viable page - i.e., a .cshtml file exists at the relative path - will be chosen as the cache result.
Generating an IRazorView
We are trying to find the first location that generates us a new razor page cache result. But how does the razor page get generated? through the use of a IRazorPageFactoryProvider.
Based on the relative path given - e.g., /Views/Home/Index - the factory will try and generate a razor page. As can be seen in the diagram above, the factory will use the RazorViewCompiler, to generate an IRazorView instance.
Cache Invalidation
The view compiler itself has its own cache. The cache is invalidated by file provider expiration tokens. This means that if you change a .cshtml file, you invalidate the cache. This is why you do not have to re-build your application in order to modify a razor view:
The view compiler will use the templating engine found in the Razor GitHub repository to generate your view. This is outside the scope of this article.
The RazorViewEngine will also find any view start files that it needs to compile - i.e., any _ViewStart.cshtml that are found in the hierarchy. These will then be added to the cache result.
Once the factory has generated us the razor page, and the view start pages, this is added to the view engine cache. A RazorView class is then instantiated - i.e., an IRazorView implementation - and returned to the ViewResultExecutor as a ViewEngineResult.
Rendering the Razor Page
The following diagram show the execution path of rendering the razor view, after the lookup and compilation phases:
Now we have the razor view compiled, we need to render this to the http response. This is achieved in the base class ViewExecutor of the ViewResultExecutor class:
Here we can see the executor call the RazorView instance. The view will then render the view start and main page files.
As the view start files were already found, these are looped over and executed:
The RenderPageCoreAsync will call ExecuteAsync on the razor page, rendering and writing it to the http response body.
After this, it will render the main page, and the layout pages, to the current body stream.
Creating a Custom Action Result
Now that we have knowledge on what an action result is, we can create our own.
All we have to do is implement the IActionResult interface, and return that from our action.
This could be advantageous in scenarios which require complicated http responses. For example, you might be interacting with an api that posts callbacks, and these callbacks could provide the api with extra metadata in the response body.
Here is an example that returns lorem ipsum text to the body stream:
This is a trivial example, and there is really no reason why you would hide this away in an action result. However, this example highlights what action results can give you: an easy way to modify the http response.
Summary
In this article, we have stepped through the code of the ASP.NET Core GitHub repository, exploring how a view result transforms from a razor view, to a http response body.
Action results are the best way to modify the output of an action. They can perform post-processing, such as turning objects into JSON, e.g., the JsonResult object, or provide a way to stream files given through your own code.
This article has revealed the innards of the ASP.NET Core github repository, and given insight into how you can extend and customise the discovery and render view phases.