[SOLVED] How is Pure DI implemented with Razor Pages

Issue

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?

Solution

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:

  • 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. The ILoggerFactory 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. The TrackDisposable 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 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.
  • There is a handy 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)
    {
        ...
    }
}

Answered By – Steven

Answer Checked By – Marilyn (BugsFixing Volunteer)

Leave a Reply

Your email address will not be published. Required fields are marked *