Dependency Injection in Razor Pages

Dependency Injection (DI) is a technique that promotes loose coupling of software through separation of concerns. In the context of a Razor Pages application, DI encourages you to develop discrete components for specific tasks, which are then injected into classes that need to use their functionality. This results in an application that is easier to maintain and test.

The Problem

Many people think that the real problem with DI is the terminology that surrounds it. This section seeks to address that by providing an illustration of the problem that DI is designed to solve.

The following sample of code features the page model class for a contact form:

public class ContactModel : PageModel
{
    [BindProperty] public string From { get; set; }
    [BindProperty] public string Email { get; set; }
    [BindProperty] public string Subject { get; set; }
    [BindProperty] public string Comments { get; set; }
    
    public async Task<IActionResult> OnPost()
    {
        using (var smtp = new SmtpClient())
        {
            var credential = new NetworkCredential
            {
                UserName = "[email protected]",  // replace with valid value
                Password = "password"  // replace with valid value
            };
            smtp.Credentials = credential;
            smtp.Host = "smtp-mail.outlook.com";
            smtp.Port = 587;
            smtp.EnableSsl = true;
            var message = new MailMessage
            {
                Body = $"From: {From} at {Email}<p>{Comments}</p>",
                Subject = Subject,
                IsBodyHtml = true
            };
            message.To.Add("[email protected]");
            await smtp.SendMailAsync(message);
            return RedirectToPage("Thanks");
        }
    }
}

And, for completeness, here is the contact form:

<form method="post">
    <label asp-for="From"></label> <input type="text" asp-for="From"/><br>
    <label asp-for="Email"></label> <input type="text" asp-for="Email"/><br>
    <label asp-for="Subject" ></label> <input type="text" asp-for="Subject" /><br>
    <label asp-for="Comments"></label> <textarea asp-for="Comments"></textarea><br>
    <input type="submit"/>
</form>

When the form is posted, the email is constructed in the OnPost handler method and sent, and the user is redirected to a page named "Thanks".

This is an extremely simple example for the purposes of explanation. The code is brief and looks similar to countless other examples of sending email using ASP.NET. But there are issues with the code - if you want to change the way that the comments are handled, you have to change the ContactModel class, which increases the chances of introducing bugs into the ContactModel. Also, you cannot possibly unit test the code in the ContactModel's OnPost method without causing an email to be sent which means that the unit test is not a unit test. It's an integration test. Finally, if you have other pages on the site that use the same code (e.g. a support form), you have multiple places to update if you want to change from Outlook to Gmail, for example.

Developers are advised to implement the SOLID principals of software design to ensure that their applications are robust and easier to maintain and extend. Another important guiding principal for developers is Don't Repeat Yourself (DRY), which states that you should aim to reduce code repetition wherever possible.

The ContactModel contravenes the S in SOLID - the Single Responsibility Principal (SRP) which states that a class should only have one responsibility. Page model classes have a responsibility - to determine the response based on the request. Any other tasks that need to be performed as part of processing the request should be handled by different classes, designed solely for those responsibilities.

The ContactModel class also contravenes the D in SOLID - the Dependency Inversion Principal (DIP) which states that high level modules (the ContactModel class) should not rely (depend) on low level modules (in this case, System.Net.Mail). They should rely on abstractions (typically interfaces, but also abstract classes) instead. Dependency Injection is the most common way to achieve DIP.

Single Responsibility Principal and DRY

The first part of the solution to reducing the issues outlined above is to implement SRP, and at the same time, adhere to DRY. This is achieved by creating a separate class for handling the comments.

using System.Net;
using System.Net.Mail;
using System.Threading.Tasks;

namespace RazorPages.Services
{
    public class CommentService
    {
        public async Task Send(string from, string subject, string email, string comments)
        {
            using (var smtp = new SmtpClient())
            {
                var credential = new NetworkCredential
                {
                    UserName = "[email protected]",  // replace with valid value
                    Password = "password"  // replace with valid value
                };
                smtp.Credentials = credential;
                smtp.Host = "smtp-mail.outlook.com";
                smtp.Port = 587;
                smtp.EnableSsl = true;
                var message = new MailMessage
                {
                    Body = $"From: {from} at {email}<p>{comments}</p>",
                    Subject = subject,
                    IsBodyHtml = true
                };
                message.To.Add("[email protected]");
                await smtp.SendMailAsync(message);
            }
        }
    }
}

Now the OnPost method can be refactored:

public class ContactModel : PageModel
{
    [BindProperty] public string From { get; set; }
    [BindProperty] public string Email { get; set; }
    [BindProperty] public string Subject { get; set; }
    [BindProperty] public string Comments { get; set; }
    
    public async Task<IActionResult> OnPost()
    {
        var service = new CommentService();
        await service.Send(From, Subject, Email, Comments);
        return RedirectToPage("Thanks");
    }
}

The code for sending emails is located in one place - the CommentService class. Its Send method contains the code that previously occupied the majority of the ContactModel's OnPost method. The service class can be called anywhere in the application where its functionality is required. This satisfies DRY. The ContactModel is no longer responsible for creating and sending the email. It uses the CommentService to do that. Both classes satisfy SRP.

Dependency Inversion Principal

The ContactModel is still dependent on a specific comment handling component - the CommentService class. It is "tightly coupled" to this dependency. It instantiates an instance of CommentService in the OnPost method. There is currently no getting away from it. If you want to change the way that comments are handled, you still have to make changes to the body of the ContactModel to change the component that provides the service, and/or the method that is called.

DIP states that the CommentService should be represented as an abstraction - an interface or abstract class. The most common approach is to use interfaces to provide the abstraction. Here is an interface that represents sending a message:

using System.Threading.Tasks;

namespace RazorPages.Services
{
    public interface ICommentService
    {
        Task Send(string from, string subject, string email, string comments);
    }
}

Next, the existing CommentService has to implement the interface:

namespace RazorPages.Services
{
    public class CommentService : ICommentService
    {
        public async Task Send(string from, string subject, string email, string comments)
        {
            // rest of existing code

Now the ContactModel can depend on an interface:

using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.RazorPages;
using RazorPages.Services;
using System.Threading.Tasks;

namespace RazorPages.Pages
{
    public class ContactModel : PageModel
    {
        private readonly ICommentService _commentService;

        public ContactModel(ICommentService commentService)
        {
            _commentService = commentService;
        }

        [BindProperty] public string From { get; set; }
        [BindProperty] public string Email { get; set; }
        [BindProperty] public string Subject { get; set; }
        [BindProperty] public string Comments { get; set; }

        public async Task<IActionResult> OnPost()
        {
            await _commentService.Send(From, Subject, Email, Comments);
            return RedirectToPage("Thanks");
        }
    }
}

The change sees a private field called _commentService added to the ContactModel. The ContactModel also has a constructor added that takes a parameter of type ICommentService. This is assigned to the private field in the constructor, and then it is used in the OnPost method.

Now you can provide any component to the ContactModel, so long as it implements the ICommentService interface i.e. it has a Send method that takes four strings. It doesn't matter whether the Send method uses SMTP to send an email, stores the comments in a text file, Tweets them or posts them to Facebook. The ContactModel doesn't need to know, nor will it need to be modified if the Send action changes. Concerns are separated into different classes which are now loosely coupled. They are not dependent on each other.

At the moment, the code above will compile, but it will generate an InvalidOperationException at runtime whenever the ASP.NET framework attempts to create an instance of ContactModel. The reason for this is that currently, the framework is unable to resolve an implementation of ICommentService to pass to the constructor of the ContactModel when an instance is instantiated.

So how does the CommentService class used by the ContactModel class get resolved?

Inversion of Control Containers

At their most basic, Inversion of Control (IoC) containers, also know as Dependency Injection Containers, are components that

  • maintain a registry of interfaces and concrete implementations
  • resolve and provide the registered concrete implementation when they are requested
  • manage the lifetime of the component.

ASP.NET Core's built in DI system supports constructor injection, so it resolves implementations of dependencies passed in as parameters to the constructor method of objects. Before it can do that, the implementations must be registered with the container. Typically, implementations (or "services") are registered in Program.cs from .NET 6 onwards, or the ConfigureServices method in the Startup class in earlier versions of .NET. The following code shows the CommentService being registered:

builder.Services.AddRazorPages();
builder.Services.AddTransient<ICommentService, CommentService>();
public void ConfigureServices(IServiceCollection services)
{
    services.AddRazorPages();
    services.AddTransient<ICommentService, CommentService>();
}
public void ConfigureServices(IServiceCollection services)
{
    services.AddMvc();
    services.AddTransient<ICommentService, CommentService>();
}

Service Lifetime

In this example, the CommentService is registered with the AddTransient method which, which is one of three options that determine the lifetime of the service:

Method Description
AddTransient This method ensures that a new instance of the service is created each time it is needed, where "needed" means injected into the constructor of a dependent class (e.g. a PageModel).
AddScoped Scoped services are ones that remain valid for the duration of a web request. You would favour scoped services where the cost of instantiation is high and the service is likely to be reused across operations during the same request, or if you want to maintain state across operations during the same request. A typical example of this is an Entity Framework context where you will want to reuse the connection, and may want to access tracked objects across operations. You would also use this option for services that depend on other services that have a scoped lifetime.
AddSingleton The service will be instantiated as a Singleton, and will be reused across all requests for the lifetime of the application.

Registering a Service with Constructor Parameters

Sometimes the service implementation that you register requires one or more constructor parameters to be passed to it. For example, you might decide to use a data access technology that requires an explicit connection string to be passed to it (e.g. Dapper). Rather than refer to the same connection string throughout the application, you create a Factory class to create the connection that the application can use, and pass the connection string as a parameter in one place - in the Startup class.

Here is an example Factory class that returns a connection object, preceded by an interface that it implements:

public interface IConnectionFactory
{ 
    IDbConnection CreateConnection();
}

public class SqlConnectionFactory: IConnectionFactory 
{
        
	readonly string _connectionString;

    public SqlConnectionFactory(string connectionString)
    {
        _connectionString = connectionString;
    }

    public IDbConnection CreateConnection()
    {
        return new SqlConnection(_connectionString);
    }
}

The constructor of the Factory class requires a parameter representing the connection string to be passed to the connection. The following example illustrates how to use an overload of the AddSingleton method to register the IConnectionFactory as a service, resolving it to the SqlConnectionFactory, while satisfying the requirement to provide a connection string:

var connString = Configuration.GetConnectionString("DefaultConnection");
if (connString == null)
        throw new ArgumentNullException("Connection string cannot be null");
builder.Services.AddSingleton<IConnectionFactory>(s =>  new SqlConnectionFactory(connString));
public void ConfigureServices(IServiceCollection services)
{
    var connString = Configuration.GetConnectionString("DefaultConnection");
    if (connString == null)
        throw new ArgumentNullException("Connection string cannot be null");
    
    services.AddSingleton<IConnectionFactory>(s =>  new SqlConnectionFactory(connString));
	//...
}

Similar overloads exist for the AddTransient and AddScoped methods.

IServiceCollection Extension Methods

The AddMvc method is an extension method on IServiceCollection that wraps the registration of all the dependencies related to the MVC framework, such as model binding, action and page invokers and so on in one tidy method call. Similar wrapper methods exist for registering other commonly used services within a Razor Pages application such as AddDbContext to register an Entity Framework DbContext.

You can create your own extension methods easily enough. Here's an example for the CommentService:

using Microsoft.Extensions.DependencyInjection;
using RazorPages.Services;

namespace RazorPages
{
    public static class ServiceExtensions
    {
        public static IServiceCollection RegisterCommentService(this IServiceCollection services)
        {
            return services.AddTransient<ICommentService, CommentService>();
        }
    }
}

This can be used in the ConfigureServices method as follows:

builder.Services.AddRazorPages();
builder.Services.RegisterCommentService();
public void ConfigureServices(IServiceCollection services)
{
    services.AddRazorPages();
    services.RegisterCommentService();
}
public void ConfigureServices(IServiceCollection services)
{
    services.AddMvc();
    services.RegisterCommentService();
}

This approach helps to keep application configuration a lot less cluttered, especially as you can chain calls to the various AddTransient, AddScoped etc. methods, which means your extension method could look like this:

public static IServiceCollection RegisterMyServices(this IServiceCollection services)
{
    return services
            .AddTransient<ICommentService, CommentService>()
            .AddTransient<ISecondService, SecondService>()
            .AddTransient<IThirdService, ThirdService>()
            .AddTransient<IFourthService, FourthService>();
}

And then only one line is required in the ConfigureServices method to register numerous services:

builder.Services.AddRazorPages();
builder.Services.RegisterMyServices();
public void ConfigureServices(IServiceCollection services)
{
    services.AddRazorPages();
    services.RegisterMyServices();
}
public void ConfigureServices(IServiceCollection services)
{
    services.AddMvc();
    services.RegisterMyServices();
}

Injecting Into Content Pages Or Views

The examples so far all feature the use of constructor injection to make services available to a class that needs them, such as the PageModel class. However, there might be occasions when you want to use a service within a Razor page itself. For example, you might not be using a PageModel class. You might have chosen to use an @functions block to house your application logic. Or you might simply want to avail yourself of a utility within the UI, such as the IAntigorgery interface for generating a request verification token for an AJAX post. In these cases, services can be injected in to the page via the @inject directive:

@page
@model LearnRazorPages.Pages.Index
@inject IAntiforgery antiforgery
@{
    var token = antiforgery.GetAndStoreTokens(HttpContext).RequestToken;
}
...

In the example above, the @inject directive is followed by the type that you want to make available to the page, and then the name for an instance of that type that you can use further down the page.

In this particular case, the IAntiforgery interface belongs to a namespace (Microsoft.AspNetCore.Antiforgery) that is not made available to Razor pages by default, so you either need to reference it by its fully qualified name:

@inject Microsoft.AspNetCore.Antiforgery.IAntiforgery antiforgery

Or you can add a using directive the the _ViewImports file that affects the Razor page in question:

@using Microsoft.AspNetCore.Antiforgery
Last updated: 11/12/2022 08:46:47

© 2018 - 2024 - Mike Brind.
All rights reserved.
Contact me at Outlook.com