0

I am attempting to use the MediatR library to implement a command pattern in my net core web API, however, I am unsure how to proceed.

I have a situation where when a user attempts to registers an account, the API should check the database for a company with a domain that matches that of the provided email address then attach the company id to the user object as a foreign key or return an error if no company exists with that domain.

I have all the necessary commands and handler to perform these separately:

GetCompanyByDomainHandler.cs

using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using Application.Application.Exceptions;
using Application.Domain.Entities;
using Application.Persistence;
using MediatR;
using Microsoft.EntityFrameworkCore;

namespace Application.Application.Companies.Queries.GetCompanyByDomain
{
    public class GetCompanyByDomainHandler 
IRequestHandler<GetCompanyByDomainQuery, Company>
    {
        private readonly ApplicationDbContext _context;

        public GetCompanyByDomainHandler(ApplicationDbContext context)
        {
            _context = context;
        }
        public async Task<Company> Handle(GetCompanyByDomainQuery request, 
CancellationToken cancellationToken)
        {
            var company = await _context.Companies.Where(c => c.Domain == 
request.Domain).SingleOrDefaultAsync();

            if (company != null) {
                return company;
            }

            throw new NotFoundException(nameof(Company), request.Domain);
        }
    }
}

GetCompanyByDomainQuery.cs

using Application.Domain.Entities;
using MediatR;

namespace Application.Application.Companies.Queries.GetCompanyByDomain
{
    public class GetCompanyByDomainQuery : IRequest<Company>
    {
        public string Domain { get; set; }
    }
}

CreateUserCommand.cs

using MediatR;

namespace Application.Application.Users.Commands.CreateUser
{
    public class CreateUserCommand : IRequest<int>
    {
        public string FirstName { get; set; }
        public string LastName { get; set; }
        public string EmailAddress { get; set; }
        public string Password { get; set; }
        public string ConfirmPassword { get; set; }
        public int CompanyId { get; set; }
    }
}

CreateUserCommandHandler.cs

using MediatR;
using Application.Domain.Entities.Identity;
using Microsoft.AspNetCore.Identity;
using System.Threading;
using System.Threading.Tasks;
using System;

namespace Application.Application.Users.Commands.CreateUser
{
    public class CreateUserCommandHandler : IRequestHandler<CreateUserCommand, int>
    {
        private readonly UserManager<User> _userManager;

        public CreateUserCommandHandler(UserManager<User> userManager)
        {
            _userManager = userManager;
        }

        public async Task<int> Handle(CreateUserCommand request, CancellationToken cancellationToken)
        {
            var entity = new User
            {
                FirstName = request.FirstName,
                LastName = request.LastName,
                Email = request.EmailAddress,
                UserName = request.EmailAddress,
                CompanyId = request.CompanyId
            };

            var createUserResult = await _userManager.CreateAsync(entity, request.Password);
            if (createUserResult.Succeeded)
            {
                return entity.Id;
            }

            throw new Exception("failed to create user");
        }
    }
}

CreateUserCommandValidator.cs

using FluentValidation;

namespace Application.Application.Users.Commands.CreateUser
{
    public class CreateUserCommandValidator : AbstractValidator<CreateUserCommand>
    {
        public CreateUserCommandValidator()
        {
            RuleFor(v => v.Password)
                .Equal(v => v.ConfirmPassword).WithName("password").WithMessage("Passwords do not match");
            RuleFor(v => v.ConfirmPassword)
                .Equal(v => v.Password).WithName("confirmPassword").WithMessage("Passwords do not match");
            RuleFor(v => v.EmailAddress)
                .NotEmpty().WithName("emailAddress").WithMessage("Email Address is required")
                .EmailAddress().WithName("emailAddress").WithMessage("Invalid email address");
            RuleFor(v => v.FirstName)
                .NotEmpty().WithName("firstName").WithMessage("First Name is required");
            RuleFor(v => v.LastName)
                .NotEmpty().WithName("lastName").WithMessage("Last Name is required");
        }
    }
}

AuthenticationController.cs

using System.Threading.Tasks;
using Application.Application.Users.Commands.CreateUser;
using Microsoft.AspNetCore.Mvc;

namespace Application.WebUI.Controllers
{
    public class AuthenticationController : ControllerBase
    {
        [HttpPost]
        public async Task<IActionResult> Register([FromBody] CreateUserCommand command)
        {
            return Ok(Mediator.Send(command));
        }
    }
}

But how do I make them part of a single request?

  • `But how do I make them part of a single request?` - http request? – Alex Feb 04 '19 at 18:46
  • Apologies, technically yes I did mean a single HTTP request however, I more specifically meant a single MediatR IRequest. I managed to find an elegant solution, I have posted the solution in the comments to your suggested answer below. – Rob Jack Stewart Feb 05 '19 at 12:32
  • Many years later, having worked with MediatR a lot more, I have realised that my question is fundamentally wrong. You shouldnt call a command after a query. If you need to do that, then you should merge the two together as they are performing a single domain driven action. The `GetDomainFromEmail` should be something that the `CreateUserCommand` uses within its own Handler rather than it being its own query. IT could indeed be its own query if an endpoint existed to get the domain from an email. – Rob Jack Stewart Nov 26 '21 at 10:39

2 Answers2

1

First of all, change your GetCompanyByDomainHandler to NOT throw an exception if the company isn't found.
A company not found isn't an exception, it's a result of a query. Just return null
See why

Now, you can get the result of the query, act upon it (rather than a try catch)

//todo: implement a way of getting the domain from an email address - regex, or string.split('.') ??
var domain = GetDomainFromEmail(command.EmailAddress);

//now get the company (or null, if it doesn't exist)
var getCompanyByDomainQuery = new GetCompanyByDomainQuery() { Domain = domain}
var company = await _mediator.SendAsync(getCompanyByDomainQuery);

//if theres a company, attach the id to the createUserCommand
if(company != null)
{
    command.CompanyId = company.Id;
}

//now save the user
var createdUser = await _mediator.SendAsync(command);

You can either wrap this up in another Handler - or treat the API Action Method as the 'orchestrator' (my preference)

Alex
  • 37,502
  • 51
  • 204
  • 332
  • I should have mentioned that the exception throw when the company is not found is handled elsewhere, sorry about that. – Rob Jack Stewart Feb 05 '19 at 12:32
  • Whilst I agree that your solution would definitely work, architecturally I want to remove all logic from the controllers and only utilise one mediator command per HTTP Request. I have added my solution to the original post. – Rob Jack Stewart Feb 05 '19 at 12:47
  • 1
    @RobStewart You should add it as an answer, not an addition to your question – Alex Feb 05 '19 at 13:13
  • 1
    Apologies, first question here and was also using a phone to reply. I have removed my solution form the question body and added it as an answer now. Thanks so much for your time! – Rob Jack Stewart Feb 12 '19 at 11:04
0

Many years later, having worked with MediatR a lot more, I have realised that my question is fundamentally wrong. You shouldn't call a command after a query. If you need to do that, then you should merge the two together as they are performing a single domain driven action. The GetDomainFromEmail should be something that the CreateUserCommand uses within its own Handler rather than it being its own query. It could indeed be its own query if an endpoint existed to get the domain from an email, and both the CreateUserCommand Handler and the GetDomainFromEmailQuery handler would utilise a common utility that actually extracts the domain from an email.