Janniks Blog

CQRS with MediatR (for Commands) and Odata (for Queries)

June 10, 2019

tl;dr: In this post we will implement a simple CQRS pattern with MediatR and Odata. We will handle Commands (aka requests that have side-effects) with MediatR and Queries (requests without side-effects) with Odata.


a bridge with separate paths for cars and pedestrians

Pragmatic CQRS with ASP.NET Core and SPAs - Series overview

  1. Getting Started with Odata: Expose a Queryable Api
  2. Enhance ASP.NET Core Api Controllers with Odata
  3. Expose ASP.NET Core Validation for HTTP Clients
  4. ▶️ CQRS with MediatR (Commands) and Odata (Queries)
  5. Shallow- and Deep Validation with FluentValidation and MediatR
  6. Centralized Validation- and Error-Handling
  7. Optimized Forms for SPAs (with Formik)
  8. Full Example

Why CQRS?

This blogpost by Martin Fowler is probably the canonical introduction to Command Query Responsibility Segregation (CQRS). The patterns core idea is that two different models can be used for reading data vs. changing data. It seems that this is much better approach for applications that go beyond some trivial features. Only the most basic examples will not benefit from the Read/Write distinction. Read the blog post for a more in-depth motivation.

Why Odata?

Odata is good at exposing a queryable api, especially in combination with Entity Framework Core (EF) and a SQL-like database. After a relative simple setup, it is trivial to enable clients to get the data they need.

Why MediatR?

Odata is not a silver bullet when it comes to api design. Especially implementing write-models is not easy and straight forward. Odata imposes to many restrictions that are annoying.

MediatR is a “simple and unambitious” in process command and event dispatcher that gives decoupling and a middle-ware pipeline to implement cross-cutting-concerns.

Related articles that give some more in-depth motivation: CQRS with MediatR and AutoMapper by Jimmy Bogard (author of MediatR) and CQRS with MediatR and ASP.NET Core by Steve Gordon.

Why this blog post?

I have never seen any one talking about combining MediatR and Odata. Usually MediatR is used for both commands and queries. And Odata seems to be avoided altogether (at least outside of the Microsoft world), as it has some problems. It seems to be a bit overengineered, has no good tooling, documentation is mediocre, the write-model is overly restrictive… However not everything in Odata is bad, especially the read-side and the integration with ASP.NET Core and EF Core is really good.

Taking the best of both worlds provides an excellent CQRS implementation. Hence this post.

Setup

In part 1 of this blog series we already got started with Odata for ASP.NET Core. In this blogpost I will just provide the steps we need to take to add MediatR and Odata (and api-versioning) to a plain ASP.NET Core web-api project without further explanations.

If you want to follow along you need to download and install the .NET Core SDK 2.2.

Lets go:

dotnet new web --name Sample
cd Sample
dotnet add package Microsoft.EntityFrameworkCore.InMemory --version 2.2.4
dotnet add package MediatR --version 7.0.0
dotnet add package MediatR.Extensions.Microsoft.DependencyInjection --version 7.0.0
dotnet add package Microsoft.AspNetCore.OData --version 7.1.0
dotnet add package Microsoft.AspNetCore.Mvc.Versioning --version 3.1.2
dotnet add package Microsoft.AspNetCore.OData.Versioning --version 3.1.0

Our csproj now looks like this:

<Project Sdk="Microsoft.NET.Sdk.Web">

  <PropertyGroup>
    <TargetFramework>netcoreapp2.2</TargetFramework>
    <AspNetCoreHostingModel>InProcess</AspNetCoreHostingModel>
    <AssemblyName>Samples</AssemblyName>
    <RootNamespace>Samples</RootNamespace>
    <PackageId>Sample4</PackageId>
  </PropertyGroup>

  <ItemGroup>
    <PackageReference Include="Microsoft.AspNetCore.App" />
    <PackageReference Include="Microsoft.AspNetCore.OData" Version="7.1.0" />
    <PackageReference Include="Microsoft.AspNetCore.Mvc.Versioning" Version="3.1.2" />
    <PackageReference Include="Microsoft.AspNetCore.OData.Versioning" Version="3.1.0" />
    <PackageReference Include="MediatR" Version="7.0.0" />
    <PackageReference Include="Microsoft.EntityFrameworkCore.InMemory" Version="2.2.4" />
    <PackageReference Include="MediatR.Extensions.Microsoft.DependencyInjection" Version="7.0.0" />
  </ItemGroup>

</Project>

In the Startup.cs we need to configure our services and the request pipeline:

public void ConfigureServices(IServiceCollection services)
{
    services.AddMvc().SetCompatibilityVersion(CompatibilityVersion.Version_2_2);

    services.AddDbContext<DataContext>(options =>
    {
        options.UseInMemoryDatabase("inmem");
    });

    services.AddMediatR(typeof(Startup).Assembly);
}

public void Configure(IApplicationBuilder app, IHostingEnvironment env, VersionedODataModelBuilder modelBuilder)
{
    if (env.IsDevelopment())
    {
        app.UseDeveloperExceptionPage();
    }
    else
    {
        app.UseHsts();
    }

    app.UseHttpsRedirection();
    app.UseMvc(builder =>
    {
        builder.Select().Expand().Filter().OrderBy().Count().MaxTop(100);
        builder.MapVersionedODataRoutes("odata", "odata", modelBuilder.GetEdmModels());
    });
}

Our DataContext and database model is implemented like this:

// The (code-first) database model
public class Person
{
    public Guid Id { get; set; }
    public string FirstName { get; set; }
    public int Age { get; set; }
}

public class DataContext : DbContext
{
    public DataContext(DbContextOptions<DataContext> options) : base(options) { }

    public DbSet<Person> Persons { get; set; }
}

We als need to add an IModelConfiguration implementation where the Odata model is configured:

public class OdataModelConfiguration : IModelConfiguration
{
    public void Apply(ODataModelBuilder builder, ApiVersion apiVersion)
    {
        builder.EntitySet<Person>("Persons");
    }
}

Commands

Incoming commands are data transfer objects (DTOs) and implement MediatRs IRequest<Response> marker interface. They are deserialized by a web-api Controller and handled by a MediatR IRequestHandler<Request,Response>:

/// Create Person command DTO
public class CreatePerson : IRequest<Person>
{
    public string FirstName { get; set; }
    public int Age { get; set; }
}

// Controller deserializes requests at specified routes
// (and applies web-concerns like api-versioning, authorization etc.)
[Route("api/[controller]")]
[ApiController]
[ApiVersion("1.0")]
public class PersonCommandsController : ControllerBase
{
    private readonly IMediator mediator;

    public PersonCommandsController(IMediator mediator)
    {
        this.mediator = mediator;
    }

    [HttpPost("create")]
    public async Task<ActionResult<Person>> CreatePerson(CreatePerson request)
    {
        // let MediatR dispatch the incoming command
        var person = await mediator.Send(request);

        return person;
    }
}

public class PersonHandler : IRequestHandler<CreatePerson, Person>
{
    private readonly DataContext ctx;

    public PersonHandler(DataContext ctx)
    {
        this.ctx = ctx;
    }

    // Implements and encapsulates the real business logic
    public async Task<Person> Handle(CreatePerson request, CancellationToken cancellationToken)
    {
        var person = new Person
        {
            FirstName = request.FirstName,
            Age = request.Age,
        };
        ctx.Add(person);

        await ctx.SaveChangesAsync();

        return person;
    }
}

Queries

Now that we are able to create Persons let’s implement the Query-side with an ODataController:

public class PersonsController : ODataController
{
    private readonly DataContext ctx;

    public PersonsController(DataContext ctx)
    {
        this.ctx = ctx;
    }

    [EnableQuery]
    public SingleResult<Person> GetPerson(Guid key)
    {
        return new SingleResult<Person>(ctx.Persons.Where(v => v.Id == key));
    }

    [EnableQuery]
    public IQueryable<Person> GetPersons()
    {
        return ctx.Persons;
    }
}

Note that we are exposing the code-first database model directly. If the database model is well designed and/or the client does not need an optimized read-model this is fine. In my experience to get started this is really enough. In some scenarios special read-models that are based on SQL-Views / custom SQL-Queries are needed.

Now we can create and query persons:

POST /api/Persons
content-type: application/json
{
    "firstName": "Alice",
    "age": 25
}

GET /odata/Persons

GET /odata/Persons&$orderby=firstName&$top10

GET /odata/Persons/:id

More Commands

Lets add features to updates and deletes. As we only really differentiate between requests that have side-effects and requests that don’t, we also only need two HTTP verbs: POST for Commands and GET for Queries.

public class UpdatePerson: IRequest<Person>
{
    /// <summary>
    /// Id of the person
    /// </summary>
    public Guid Id { get; set; }
    public string FirstName { get; set; }
    public int Age { get; set; }
}

public class DeletePerson : IRequest
{
    /// <summary>
    /// Id of the person
    /// </summary>
    public Guid Id { get; set; }
}

[Route("api/Persons")]
[ApiController]
public class PersonCommandsController : ControllerBase
{
    private readonly IMediator mediator;

    public PersonCommandsController(IMediator mediator)
    {
        this.mediator = mediator;
    }

    [HttpPost("create")]
    public async Task<ActionResult<Person>> Create(CreatePerson request)
    {
        var person = await mediator.Send(request);

        return person;
    }

    [HttpPost("update")]
    public async Task<ActionResult<Person>> Update(UpdatePerson request)
    {
        var person = await mediator.Send(request);

        return person;
    }

    [HttpPost("delete")]
    public async Task<IActionResult> Delete(DeletePerson request)
    {
        await mediator.Send(request);
        return Ok();
    }
}

public class PersonHandler
    : IRequestHandler<CreatePerson, Person>
    , IRequestHandler<UpdatePerson, Person>
    , IRequestHandler<DeletePerson>
{
    private readonly DataContext ctx;

    public PersonHandler(DataContext ctx)
    {
        this.ctx = ctx;
    }

    public async Task<Person> Handle(CreatePerson request, CancellationToken cancellationToken)
    {
        var person = new Person {
            Age = request.Age,
            FirstName = request.FirstName
        };
        ctx.Add(person);
        await ctx.SaveChangesAsync();
        return person;
    }

    public  async Task<Person> Handle(UpdatePerson request, CancellationToken cancellationToken)
    {
        var person = await ctx.Persons.SingleOrDefaultAsync(v => v.Id == request.Id);
        if (person == null)
        {
            // instead of throwing an exception here, we ideally indicate to the
            // client that he is sending a bad request. I will tackle this in an
            // upcoming post
            throw new Exception("Record does not exist"); 
        }
        person.Age = request.Age;
        person.FirstName = request.FirstName;
        ctx.Persons.Update(person);
        await ctx.SaveChangesAsync();
        return person;
    }

    public async Task<Unit> Handle(DeletePerson request, CancellationToken cancellationToken)
    {
        var person = await ctx.Persons.SingleOrDefaultAsync(v => v.Id == request.Id);
        if (person == null)
        {
            throw new Exception("Record does not exist");
        }
        ctx.Persons.Remove(person);
        await ctx.SaveChangesAsync();
        return Unit.Value;
    }
}

The api now can handle Commands at the routes

POST /api/Persons/create
POST /api/Person/update
POST /api/Persons/delete

and Queries at

GET /odata/Persons
GET /odata/Persons/:id

POST for Commands (arguments in the payload), GET for Queries (arguments in the odata-query-parameters). Simple and effective!

Closing notes

The CQRS pattern itself is quite simple and straight forward. Implementing it in ASP.NET Core with the help of MediatR and Odata is a breeze. The accompanying source code is available on github (Sample #4):

git clone https://github.com/jannikbuschke/pragmatic-cqrs-with-asp-net-core-for-spas

Github / Twitter