A Non-Trivial Example of MediatR Usage

I have been using Jimmy Bogard's MediatR library on my current project for the last few months. I first read about the project on his blog which has a series of posts including his explaination of the benefits of using the Mediator Pattern.

I took the leap and gave the library a try. I was quickly impressed by how clean and simple my controllers become. Which makes them really simple to test. I fell into a pattern of seperating actions that change state in my system from queries of the current state, a CQRS lite kind of pattern. I also liked the concept of the Mediator Pipeline described in Jimmy's blog post. It creates a nice seperation of concerns where I can focus on one bit of command processing at a time and compose the entire process at runtime.

Let's take a look at a full example taken directly from my current application. The application is a psudo-multi-tenant financial tracking system intended to track funds on behalf of residents of various institutions. These institutions belong to a greater organizational heirarcy. For an insitution to use the system they first have to be added to the appropriate palce in the heirarcy.

The data entry from looks like this:

organization editor

The form allows a user to specify various details about the orgaization including an active directory group containing the organization's users, a name and abbreviation that must all be unqiue. There are some simple client side validations that make ajax calls to ensure active directory info actually exists active directory. The form also allows a user to select what features are enabled for the organization, I intend for this list to grow as features are added throughout the life of the project.

The controller handler for the post back of this form looks like this:

// POST: Organizations/Create
[HttpPost]
[ValidateAntiForgeryToken]
public async Task<ActionResult> Create(OrganizationEditorForm form)
{
    Logger.Trace("Create::Post::{0}", form.ParentOrganizationId);

    if (ModelState.IsValid)
    {
        var command = new AddOrEditOrganizationCommand(form, ModelState);
        var result = await mediator.SendAsync(command);
                    
        if(result.IsSuccess)
            return RedirectToAction("Index", new { Id = result.Result });

        ModelState.AddModelError("", result.Result.ToString());
     }
            
     return View(form);
}

I lean on MVC model binding to hydrate a form view model and do basic data attribute validation. If that validation fails, I simply redisplay the form with ModelState errors displayed. This is simply ensuring that the client side validation is still valid.

Next, I construct an AddOrEditOrganzationCommand containing my form view model and the current ModelState. This allows me to use the ModelState to attach errrors to the form concerning server side only validations. The command object is fairly simple just containing the bits of data needed to perform the work.

public class AddOrEditOrganizationCommand : IAsyncRequest<ICommandResult>
{
    public OrganizationEditorForm Editor { get; set; }
    public ModelStateDictionary ModelState { get; set; }

    public AddOrEditOrganizationCommand(OrganizationEditorForm editor,
    	ModelStateDictionary modelState)
    {
        Editor = editor;
        ModelState = modelState;
    }
}

The command is sent using a mediator reference and a result is retrieved. My result types (SuccessResult and FailureResult) are based on a simple interface:

public interface ICommandResult
{
	bool IsSuccess { get; }
    bool IsFailure { get; }
    object Result { get; set; }
 }

If the result is a success, I redirect the user to the newly created organization. On failure I add any message passed back by the failure result to ModelState and redisplay the form.

Now for the various handlers that are needed to process this command. The first step is to do any server side validation of my form before attempting to change state in the database. I do this with a pre processor called OrganizationEditorFormValidatorHandler.

public class OrganizationEditorFormValidatorHandler : CommandValidator<AddOrEditOrganizationCommand>
    {
        private readonly ApplicationDbContext context;

        public OrganizationEditorFormValidatorHandler(ApplicationDbContext context)
        {
            this.context = context;
            Validators = new Action<AddOrEditOrganizationCommand>[]
            {
                EnsureNameIsUnique, EnsureGroupIsUnique, EnsureAbbreviationIsUnique
            };
        }

        public void EnsureNameIsUnique(AddOrEditOrganizationCommand message)
        {
            Logger.Trace("EnsureNameIsUnique::{0}", message.Editor.Name);

            var isUnique = !context.Organizations
                .Where(o => o.Id != message.Editor.OrganizationId)
                .Any(o => o.Name.Equals(message.Editor.Name,
                		StringComparison.InvariantCultureIgnoreCase));

            if(isUnique)
                return;

            message.ModelState.AddModelError("Name", 
            		"The organization name ({0}) is in use by another organization."
                    .FormatWith(message.Editor.Name));
        }

        public void EnsureGroupIsUnique(AddOrEditOrganizationCommand message)
        {
            Logger.Trace("EnsureGroupIsUnique::{0}", message.Editor.GroupName);

            var isUnique = !context.Organizations
                .Where(o => o.Id != message.Editor.OrganizationId)
                .Any(o => o.GroupName.Equals(message.Editor.GroupName,
                		StringComparison.InvariantCultureIgnoreCase));

            if (isUnique)
                return;

            message.ModelState.AddModelError("Group", 
            	"The Active Directory Group ({0}) is in use by another organization."
                    .FormatWith(message.Editor.GroupName));
        }

        public void EnsureAbbreviationIsUnique(AddOrEditOrganizationCommand message)
        {
            Logger.Trace("EnsureAbbreviationIsUnique::{0}",
            		message.Editor.Abbreviation);

            var isUnique = !context.Organizations
                .Where(o => o.Id != message.Editor.OrganizationId)
                .Any(o => o.Abbreviation.Equals(message.Editor.Abbreviation,
                		StringComparison.InvariantCultureIgnoreCase));

            if (isUnique)
                return;

            message.ModelState.AddModelError("Abbreviation", 
            		"The Abbreviation ({0}) is in use by another organization."
                        .FormatWith(message.Editor.Name));
        }
    }

The CommandValidator`T type contains simple helper methods for iterating the defined validation methods and executing them. Each validation method performs its specific logic and adds a model state error on failure. All server side validation happens here.

Next up is the actual command handler that does the change of state in the database. Due to adding and upating an organization being so similar, I handle both actions with a single handler.

public class AddOrEditOrganizationCommandHandler : IAsyncRequestHandler<AddOrEditOrganizationCommand, ICommandResult>
    {
        public ILogger Logger { get; set; }

        private readonly ApplicationDbContext context;

        public AddOrEditOrganizationCommandHandler(ApplicationDbContext context)
        {
            this.context = context;
        }

        public async Task<ICommandResult> Handle(AddOrEditOrganizationCommand message)
        {
            Logger.Trace("Handle");

            if (message.ModelState.NotValid())
                return new FailureResult("Validation Failed");

            if (message.Editor.OrganizationId.HasValue)
                return await Edit(message);
            
            return await Add(message);
        }


        private async Task<ICommandResult> Add(AddOrEditOrganizationCommand message)
        {
            Logger.Trace("Add");

            var organization = message.Editor.BuildOrganiation(context);
            
            context.Organizations.Add(organization);

            await context.SaveChangesAsync();

            Logger.Information("Add::Success Id:{0}", organization.Id);

            return new SuccessResult(organization.Id);
        }

        private async Task<ICommandResult> Edit(AddOrEditOrganizationCommand message)
        {
            Logger.Trace("Edit::{0}", message.Editor.OrganizationId);

            var organization = context.Organizations
            		.Find(message.Editor.OrganizationId);
                    
            message.Editor.UpdateOrganization(organization);

            await context.SaveChangesAsync();

            Logger.Information("Edit::Success Id:{0}", organization.Id);

            return new SuccessResult(organization.Id);
        }
    }

The main handle method of this handler is fairly simple. it checks the validation state of the form and returns a failure result if the previous validation step failed. It then determines if we are adding or updating the organization and delegates that work to specific methods.

The add method has the editor build a new organization entity and saves it to the database. A success result is returned with the id of the saved entity.

The edit method loads the entity, has the editor update it and saves changes to the database. A success result is returned with the id here as well.

While not obvious from the code above, I have yet to do anything with the features that are enabled for the organiation. I wanted to seperate the logic of processing the organization from the handling of associating the enabled features to that organization.

So I have a a post processor handler called UpdateOrganizationFeaturesPostHandler to provide that functionality.

public class UpdateOrganizationFeaturesPostHandler : IAsyncPostRequestHandler<AddOrEditOrganizationCommand, ICommandResult>
    {
        public ILogger Logger { get; set; }

        private readonly ApplicationDbContext context;

        public UpdateOrganizationFeaturesPostHandler(ApplicationDbContext context)
        {
            this.context = context;
        }

        public async Task Handle(AddOrEditOrganizationCommand command, 
        	ICommandResult result)
        {
            Logger.Trace("Handle");

            if (result.IsFailure)
                return;
            
            var organization = await context.Organizations
                                        .Include(o => o.Features)
                                        .FirstAsync(o => o.Id == (int) result.Result);

            
            
            var enabledFeatures = command.Editor.EnabledFeatures
                                    .Select(int.Parse).ToArray();

            //disable features
            organization.Features
                .Where(f => !enabledFeatures.Contains(f.Id))
                .ToArray()
                .ForEach(f => organization.Features.Remove(f));
                    
            //enable features    
            context.Features
                .Where(f => enabledFeatures.Contains(f.Id))
                .ToArray()
                .ForEach(organization.Features.Add);

            await context.SaveChangesAsync();
        }
    }

This handler does nothing if the command result is a failure. On success it gets the organization including its currently enabled feature set which will be empty on a new organization and may contain some features on an updated organization.

Next I pull the enabled features out of the editor form and modify the associated features of the organization removing disabled features and adding enabled features.

These handlers represent the entire pipeline of handling the AddOrEditOrganizationCommand so far in my system.

Here are some lessons learned from my usage of Mediatr. It decomposes processes nicely into tiny easily testable chunks but I have encountered some weirdness in specific cases. This weirdness is most definintely on my end and not the libraries. 8)

One, when an editor form has database retrieved lookup lists I have not found a clean way of rehydrating them for redisplay of the form. At the moment I am handing that in a custom model binder and that just feels dirty.

My form get controller method looks like this:

// GET: Organizations/Create/{1}
public async Task<ActionResult> Create(int? id)
{
    Logger.Trace("Create::Get::{0}", id);

    var query = new OrganizationEditorFormQuery(parentOrganizationId: id);
    var form = await mediator.SendAsync(query);
    return View(form);
 }

And the model binder like this:

[ModelBinderType(typeof(OrganizationEditorForm))]
public class OrganizationEditorFormModelBinder : DefaultModelBinder
{
    public ILogger Logger { get; set; }

    private readonly ApplicationDbContext dbContext;

    public OrganizationEditorFormModelBinder(ApplicationDbContext dbContext)
    {
        this.dbContext = dbContext;
    }

    public override object BindModel(ControllerContext controllerContext,
    	ModelBindingContext bindingContext)
    {
        Logger.Trace("BindModel");

        var form = base.BindModel(controllerContext, bindingContext)
                .CastOrDefault<OrganizationEditorForm>();

        if (form.ParentOrganizationId.HasValue)
            form.ParentOrganization = dbContext.Organizations
                .FirstOrDefault(o => o.Id == form.ParentOrganizationId);

        return form;

    }
}

Both ensure that a parent organization is apart of the editor form. I need to come up with a better way of doing this that works for the inital load of the form and the post redisplay of the form. As the forms are becoming more complex this becomes more of an issue. I may need to start doing some asyncronous callbacks from the client to get lookup lists values. The problem goes away at that point, I think.

Two, MediatR has a concept of notifications where you can fire and forget almost event like information out to parties that care about a specific change. In my system, I am eventally going to need to take some action when a feature is enabled or disabled.

I am not sure if the MediatR notification is the right way to approach this. Taking on a dependency to my mediator in a handler feels dirty to me, but its just a feeling. I need to reach out to Jimmy and see what his thought are on such a strategy. Am I creating a tangled nightmare by allowing handlers to issue notifications or even other commands? I am just not sure about that yet.

At the end of the day, I like what MediatR has done to my codebase. My controllers are nice and skinny and my handlers are tight and focused. This alone is worth looking into th using the pattern/library.

Follow me on Mastodon!