This question is similar to my previous question about Razor Components, but instead, this question is about Razor Pages, which requires a different interception point.
I am making an ASP.NET Core application using the Pure DI approach explained in the book Dependency Injection Principles, Practices, and Patterns (DIPP&P). Part of my application has a web API controller. To implement Pure DI with my controller, I was easily able to follow section 7.3.1 "Creating a custom controller activator" from DIPP&P to create a controller activator class, similar to the example found in DIPP&P. This was done by implementing IControllerActivator
and composing my composition root within the create
method.
My application will also feature Razor Pages. I would like to continue using the Pure DI approach but I cannot find any examples on how to do this. My assumption is I need to create a RazorPageActivator
class, which implements IRazorPageActivator
and add my composition root to the Activate
method. However, after reviewing the RazorPageActivator
class found in the ASP.NET Core GitHub, it looks very complex and I fear if I intercept it (or override it?) by making my own class that implements IRazorPageActivator
things will break and I'll be in a mess.
My question is how does one go about implementing Pure DI with Razor Pages, if possible?
CodePudding user response:
With Razor Pages, the IPageModelActivatorProvider
functions as your Composition Root's Composer. Here's an example based on the default Visual Studio (2019) Razor Pages project template.
Let's start with the custom IPageModelActivatorProvider
, which acts as your Composer, which is part of your Composition Root:
public class CommercePageModelActivatorProvider : IPageModelActivatorProvider, IDisposable
{
// Singletons
private readonly ILoggerFactory loggerFactory;
public CommercePageModelActivatorProvider(ILoggerFactory f) => this.loggerFactory = f;
public Func<PageContext, object> CreateActivator(CompiledPageActionDescriptor desc) =>
c => this.CreatePageModelType(c, desc.ModelTypeInfo.AsType());
public Action<PageContext, object> CreateReleaser(CompiledPageActionDescriptor desc) =>
(c, pm) => { };
private object CreatePageModelType(PageContext c, Type pageModelType)
{
// Create Scoped components
var context = new CommerceContext().TrackDisposable(c);
// Create Transient components
switch (pageModelType.Name)
{
case nameof(IndexModel):
return new IndexModel(this.Logger<IndexModel>(), context);
case nameof(PrivacyModel):
return new PrivacyModel(this.Logger<PrivacyModel>());
default: throw new NotImplementedException(pageModelType.FullName);
}
}
public void Dispose() { /* Release Singletons here, if needed */ }
private ILogger<T> Logger<T>() => this.loggerFactory.CreateLogger<T>();
}
Notice a few things with this implementation:
- The structure of this class is very similar to the one's given in the book's code samples.
- It implements
IDisposable
to allow disposing of its own created singletons. In this case, no singletons are created in its constructor, so nothing needs to be disposed. TheILoggerFactory
is "externally owned"; it is created by the framework, and will be disposed (if needed) by the framework. - The class uses a custom
TrackDisposable
extension method (shown later on) that allows tracking scoped and transient dependencies. TheTrackDisposable
method will add those instances to the request and allows the framework to dispose them when the request ends. - This is why the
CreateReleaser
method returns an empty delegate. The disposing of objects is done by the framework, so you will typically have nothing to implement in this method. - There is a handy
Logger<T>()
method that simplifies the creation ofILogger<T>
implementations. Those come from the framework and are created by theILoggerFactory
.
Here's the TrackDisposable
extension method:
public static class DisposableExtensions
{
public static T TrackDisposable<T>(this T instance, PageContext c) where T : IDisposable
{
c.HttpContext.Response.RegisterForDispose(instance);
return instance;
}
}
The last missing piece of infrastructure required is the registration of your CommercePageModelActivatorProvider
into the framework's DI Container. This is done inside the Startup class:
public class Startup
{
public Startup(IConfiguration configuration)
{
Configuration = configuration;
}
public IConfiguration Configuration { get; }
public void ConfigureServices(IServiceCollection services)
{
services.AddRazorPages();
// Register your custom component activator here
services
.AddSingleton<IPageModelActivatorProvider, CommercePageModelActivatorProvider>();
}
public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
...
}
}