Reputation: 301
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?
Upvotes: 2
Views: 302
Reputation: 172835
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 loggerFactory) =>
this.loggerFactory = loggerFactory;
public Func<PageContext, object> CreateActivator(
CompiledPageActionDescriptor desc) =>
c => this.CreatePageModelType(c, desc.ModelTypeInfo.AsType());
public Action<PageContext, object> CreateReleaser(
CompiledPageActionDescriptor desc) =>
(c, pm) => (pm as IDisposable)?.Dispose();
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:
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. The ILoggerFactory
is "externally owned"; it is created by the framework, and will be disposed (if needed) by the framework.TrackDisposable
extension method (shown later on) that allows tracking scoped and transient dependencies. The TrackDisposable
method will add those instances to the request and allows the framework to dispose them when the request ends.CreateReleaser
method only disposes the Page itself. The disposing of all other created components is done by the framework when you track them for disposal. You can also choose to track the Page itself; in that case you can leave the CreateReleaser
delegate empty.Logger<T>()
method that simplifies the creation of ILogger<T>
implementations. Those come from the framework and are created by the ILoggerFactory
.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)
{
...
}
}
Upvotes: 4