Previously in this Series

Contents

Introduction

This article is the third in a series I’m dedicating to reviewing the code and design of the ASP.NET Core GitHub repository. The series tries to explain the underlying mechanisms of ASP.NET Core, and how all the code fits together to create the framework as we know it at the published date.

ASP.NET Core action discovery is the bread and butter of the framework. The most useful idea that has come out of the framework is the ability to find sections of code based on metadata of the current HTTP request-i.e., actions.

Actions are the main point within ASP.NET Core that a developer can interact with the framework itself. In this article, I am going to discuss how the framework finds your specific action and how the brand new razor pages fits into the picture.

N.B. This article will not talk about how razor pages work within the 2.0 framework version. I will dedicate a razor pages topic to a later post.

Registering Actions

Actions are provided to the action selection system through the IActionDescriptorCollectionProvider abstraction. The collection provider has a property called ActionDescriptors, which is the main property that provides all action descriptors given to the system. This collection provider is registered with the dependency injection system, and so can be used anywhere in code. This is useful if you want to work with all or a subset of the metadata found in the action descriptors of your application.

The collection provider itself will has an enumeration of IActionDescriptorProvider implementations.

An action descriptor provider is something that creates metadata about the actions of your application. These providers are aggregated to build up the collection that the ActionDescriptors property provides:

for (var i = 0; i < _actionDescriptorProviders.Length; i++) { _actionDescriptorProviders[i].OnProvidersExecuting(context); }

The two main action descriptor provider instances are the ControllerActionDescriptorProvider, and the PageActionDescriptorProvider. One is the classic MVC action provider and one is the new Razor Pages action descriptor provider, respectively.

What this means is that the ASP.NET team have hooked up the new Razor Pages introduced in ASP.NET Core 2.0, into the bog standard action selection sub-system. This allows you to use the classic MVC actions alongside the new Razor Pages.

Finding Controller Actions

The ControllerActionDescriptorProvider is the main class that will explore your current application for applicable controller actions. The provider will try and retrieve all the action descriptors it can find, adding each one to the main ActionDescriptorProviderContext context, passed down from the collection provider:

foreach (var descriptor in GetDescriptors()) { context.Results.Add(descriptor); }

The provider will call build on the ControllerActionDescriptorBuilder class, passing the application model:

protected internal IEnumerable GetDescriptors() { var applicationModel = BuildModel(); ApplicationModelConventions.ApplyConventions(applicationModel, _conventions); return ControllerActionDescriptorBuilder.Build(applicationModel); }

The BuildModel method is where most of the work is done to retrieve all the different controller action descriptors. Firstly, the method will retrieve all the controllers through the main ApplicationPartManager class. The application part manager can be used to add application parts to your runtime. For example, if you wanted to register controllers within another assembly, as to be used for retrieval of action descriptors, you can do the following:

var assembly = typeof(ControllerInAssembly).GetTypeInfo().Assembly; var part = new AssemblyPart(assembly); services .AddMvc() .ConfigureApplicationPartManager(apm => p.ApplicationParts.Add(part));

This is also explained in my previous post about sharing controllers in assemblies.

The ApplicationPartManager class also contains a bunch of feature providers. A feature provider is a class that implement IApplicationFeatureProvider. The ones that exist within the core framework are the:

ControllerFeatureProvider,

MetadataReferenceFeatureProvider,

ViewsFeatureProvider,

TagHelperFeatureProvider,

and the ViewComponentFeatureProvider.

As can be seen from the names of these providers, they all have a specific feature in mind when building the application model.

The Controller Feature Provider

The ControllerFeatureProvider class is the feature we are interested in, as this is the main feature that contains all the controller actions we need to discover.

The provider will populate a ControllerFeature class. This class will accept all the types given by implementations of the IApplicationPartTypeProvider class.

The AssemblyPart is the classic example that implements IApplicationPartTypeProvider. This means that the ControllerFeatureProvider can use it to provide Controller types within an assembly:

foreach (var type in part.Types) { if (IsController(type) && !feature.Controllers.Contains(type)) { feature.Controllers.Add(type); } }

Once the ControllerFeature is filled with the registered Controllers, then this is used by the ControllerActionDescriptorProvider to build up an eventual ApplicationModel class through the use of IApplicationModelProvider instances. The application model essentially provides all the metadata associated with the controllers and filters.

The list of IApplicationModelProvider instances are the:

DefaultApplicationModelProvider,

AuthorizationApplicationModelProvider,

CorsApplicationModelProvider,

and TempDataApplicationModelProvider.

Each instance is registered with the dependency injection system, and so can be used anywhere.

The Default Application Model Provider

The DefaultApplicationModelProvider instance is the one we are interested in for action discovery. This provider populates the controller models within the main ApplicationModel class.

With the use of reflection, it will enumerate all the methods on the controller types given to it by the controller feature provider. Methods within the controller are filtered out based on the metadata of the method. The logic for determining this can be found here. The main thing to note is that you can use the NonActionAttribute to stop methods being registered and being added to the controller model being built.

Conventions are then applied on the application model, allowing you to modify the application model based on the conventions you give.

Customising Controller Action Discovery

With the above explained, an easy way to extend the way actions are discovered in traditional MVC is by providing an instance of the IApplicationFeatureProvider<ControllerFeature> class:

public class MyControllerFeatureProvider : IApplicationFeatureProvider<ControllerFeature> { private readonly Type _myCustomControllerType; public MyControllerFeatureProvider(Type myCustomControllerType) { _myCustomControllerType = myCustomControllerType; } public void PopulateFeature(IEnumerable<ApplicationPart> parts, ControllerFeature feature) { feature.Controllers.Add(_myCustomControllerType); } }

This feature provider can then be registered with the application part manager at startup:

services.AddMvc() .ConfigureApplicationPartManager(p => p.FeatureProviders.Add(new MyControllerFeatureProvider(typeof(MyCustomerController))));

Now the feature provider is registered, it can then be used by the default application model provider to discover the actions that are present on the controller.

Finding Razor Pages

The brand new Razor Pages are built in such a way as to fit into the existing framework. Razor Pages resolve to actions just like controller actions do. To retrieve all the razor page action descriptors for a particular application, it starts with the PageActionDescriptorProvider.

The PageActionDescriptorProvider implements IActionDescriptorProvider, which is subsequently registered with dependency injection, and thus hooked up to the main IActionDescriptorCollectionProvider (N.B. the collection provider is the main source of action descriptors for your application as previously discussed). The collection provider class will take all registered implementations of the IActionDescriptorProvider, and build up a complete list of action descriptors. This is how Controllers and Razor Pages can co-exist.

In a similar way the ControllerActionDescriptorProvider loops through IApplicationModelProvider instances, the PageActionDescriptorProvider class will look through all available IPageRouteModelProviders implementations.

The main implementations of IPageRouteModelProviders are the:

RazorProjectPageRouteModelProvider,

and the CompiledPageRouteModelProvider.

Both these instances are registered with the dependency injection system.

The Razor Project PageRouteModel Provider

The RazorProjectPageRouteModelProvider will enumerate all the items under the razor pages root directory options (N.B. this option can be setup at startup configuration through the RazorPageOptions class):

services.Configure ( options => options.RootDirectory = "/CustomFolder")

By default, Razor Pages will be found under the “/Pages” subdirectory of your application. The above snippet shows how you can customise this. The above snippet can also be achieved by using the WithRazorPagesRoot IMvcBuilder extension method.

If you supply just a “/”, this is the same as saying that the Razor Pages should be rooted at the content root. This can also be achieved through the WithRazorPagesAtContentRoot IMvcBuilder extension method.

The RazorProjectPageRouteModelProvider will select files within the root directory that match the correct predicates. Files starting with “_” will be ignored-e.g., _Layout.cshtml. It will also ignore any .cshtml files that do not have the @page directive at the top of the file.

If the file found in the root directory passes all of these inspections, then a PageRouteModel is created for that page, and added to current PageRouteModelProviderContext RouteModels property. The PageRouteModel is essentially metadata that describes the routing for a razor page.

The Compiled PageRouteModel Provider

The CompiledPageRouteModelProvider will fetch any compiled PageRouteModel classes. This class will also cache the page route models it has. This provider has a lower Order property than RazorProjectPageRouteModelProvider. This means PageActionDescriptorProvider will pass it the current context first.

The RazorProjectPageRouteModelProvider will not add any more PageRouteModel classes to the context, if it already exists in the current context. With this in mind, you could add a brand new file to the root directory of the razor pages area, and this should be dynamically found by the provider.

Razor Pages Route Template

For each of your razor pages you can define a route template as part of the @Page directive:

@page "{handler?}"

Handlers are discussed at length in the microsoft documentation, and it is beyond the scope of this article to discuss them. However, the route template provided above allows specific route parameters such as the handler parameter, into the URL path.

These route templates are added to the PageActionDescriptor as part of the attribute route information, just like the traditional attribute routing in classic MVC.

Route templates is just another example of how the ASP.NET team have based Razor Pages on top of the existing ecosystem.

Summary

Throughout this article, I have discussed how ASP.NET Core 2.0 discovers actions within your application. I have discussed how controller actions are discovered, as well as how the new razor pages are discovered.

I have also pointed out multiple times that razor pages is just an extension of how actions are currently working within the MVC world. With this in mind, you can use both MVC and Razor Pages at the same time, and both will be placed within the same routing model.

Thanks for reading and I hope this helps you on understanding more about ASP.NET Core.