Saturday, March 30, 2019

ASP.NET Core Identity with MediatR

A good way to play with ASP.NET Core 3.0 Identity (using VS 2019 preview) and MediatR is this sample project on GitHub.

MediatR is a small but very useful utility library which main goal is to implement Mediator pattern, but is more than that. For instance comes with ASP.NET Core Dependency Injection extensions, pipelines (or behaviors) that implement decorator pattern, and few other goodies. Simply speaking, it may be a very first step toward CQRS, to split up queries and commands. In many cases is used more like a command pattern, I think.

In my case I wanted to use MediatR to extract from identity Razor pages all references to Microsoft.AspNetCore.Identity and move the code in some handlers. This should help unit testing because page's OnGetAsync() or OnPostAsync() are only calling mediator Send() method which does the job, without any reference to Identity's SigInManager class.

To start with, just create a new ASP.NET Core web application with local identity accounts, so the entire back-end to identities are already created. Then I run the database migration:

dotnet ef database update

If you ran the application at this stage it should work, so you should be able to register a new account and login. By default, ASP.NET uses default identity UI which hides from us all details. If we want to customize the login, for instance, as we do, we need to scaffold the pages and manually change them in the project, see this docs link:

dotnet tool install -g dotnet-aspnet-codegenerator
dotnet add package Microsoft.VisualStudio.Web.CodeGeneration.Design
dotnet restore

To actually generate the pages use this:

 dotnet aspnet-codegenerator identity -dc SampleMediatR.Data.ApplicationDbContext --files "Account.Register;Account.Login;Account.Logout"

Newly generated login, logout and registration pages are available under /Areas/Identity/Pages/Account:


At this stage, if you want to run the application is important to comment out in Startup.cs in ConfigureServices() the call of AddDefaultUI() because we don't need it anymore (once scaffolded in the app):

services.AddDefaultIdentity<IdentityUser>()
        //.AddDefaultUI(UIFramework.Bootstrap4)
       .AddEntityFrameworkStores<ApplicationDbContext>();

Actually, scaffolding operation created a new file in the root of the project called ScaffoldingReadme.txt with instruction on how to set it up.

Also, I created a new folder called 'MediatR' under /Areas/Identity for classes needed by MediatR library, but first let's add those references:

dotnet add package MediatR
dotnet add package MediatR.Extensions.Microsoft.DependencyInjection

Then add it for dependency injection in Startup.cs ConfigureServices():

services.AddMediatR();

Now we are ready to implement query & handler classes needed by MediatR. For instance MediatR/LoginGet.cs has 2 classes: query (request) called "LoginGet" and handler called "LoginGetHandler". First one wraps the data passed to the handler, while the second is the handler doing the job in method called Handler(), called automatically by the MediatR library.

public class LoginGet: IRequest<IEnumerable<AuthenticationScheme>> { }

public class LoginGetHandler : IRequestHandler<LoginGet, IEnumerable<AuthenticationScheme>>
{
    private readonly SignInManager<IdentityUser> _signInManager;
    private readonly IHttpContextAccessor _httpContextAccessor;

    public LoginGetHandler(SignInManager<IdentityUser> signInManager, IHttpContextAccessor httpContextAccessor)
    {
        _signInManager = signInManager;
        _httpContextAccessor = httpContextAccessor;
    }

    public async Task<IEnumerable<AuthenticationScheme>> Handle(LoginGet request, CancellationToken cancellationToken)
    {
        await _httpContextAccessor.HttpContext.SignOutAsync(IdentityConstants.ExternalScheme);
        return await _signInManager.GetExternalAuthenticationSchemesAsync();
    }
}

Notice couple things:
  • LoginGet class implements MediatR's IRequest interface with type IEnumerable<AuthenticationScheme>, which is exactly the type returned by Handle() method. In this case the class doesn't need any properties (but see some in LoginPost.cs).
  • In LoginGetHandler() constructor is injected from DI both SignInManager and HttpContextAccessor (which was added in Startup.cs with services.AddHttpContextAccessor(), just to get access to HttpContext in a safe way). 
  • Handle() function uses SignInManager and HttpContextAccessor to call necessary functions.
Then OnGetAsync() in page Login.cshtml.cs uses it this way:

ExternalLogins = (await _mediator.Send(new LoginGet())).ToList();

All the previous logic that didn't belong to the OnGetAsync() controller was moved to the MediatR handler, and similarly for OnPostAsync(), as well as Logout.cshtml.cs OnPostAsync().

See the code for other details.

No comments:

Post a Comment