Vertical Slice Architecture using .NET5, CQRS, MediatR, EF Core, C#

GitHub Repo https://github.com/srinadimpalli/Vertical-Slice-Architecture-using-.NET-5-CQRS-MediatR-EF-Core–CSharp/tree/master

Hi there, in this blog post we are going to understand what is Vertical Slice Architecture and how it is different from traditional layered architecture and finally implementing an ASP.NET Core API solution using the above tools and technologies.

In vertical slice architecture we separate application into “vertical slices”, which means a vertical slice represents the part of each layer that creates a specific feature(example- AddProducts, CreateProduct). So instead of dividing the application into layers (A layer groups classes together based on shared responsibility) we dividing it by feature, see below figure.

A feature manages its data access code, domain logic and its presentation code, this way we can lowering the coupling and high cohesion between features. lets understand advantages of vertical slice architecture.

  1. We can reduce coupling between features, we only need to think about a single vertical slice not N-layers, so this improves maintainability.
  2. We can choose how each vertical slice interacts with external resources, once slice can use T-SQL and another slice uses EF Core.
  3. Each vertical slice should contain the right amount of code need for it to be correct.
  4. Faster onboarding time for new developers in the team.
  5. You can pretty much start a new vertical slice by coping the files from another vertical slice, make few changes, and that new slice are ready to go.

Before diving into the project , here is the brief overview of Mediator and CQRS patterns.

Mediator Pattern: Mediator is a behavioral design pattern that lets you reduce dependencies between objects. The pattern restricts direct communication between the objects and forces them to collaborate only via a mediator object.

CQRS (Command Query Responsibility Segregation) pattern: As its name implies, commands and queries have different responsibilities. When analyzing business software, you will realize there are two types of operations: 1).Those that read data as queries or a query reads the state of the application but never changes. 2) Those that write data know as Commands. A command mutates the sate of an application. For example, creating, updating and deleting any entity are commands. By performing this division, we create a clear separation of concerns between mutator and accessor requests.

One final note is, in applications there are different degree of separation, ranging from no separation at all, that is, queries and commands share the same codebase, including same domain model, DTOs, and data access code, all the way to complete separation in which queries and commands each have their own codebase, domain models, deployment, and possibly even different database. In fact, having separate read and write database is quite common in high-performance applications- the read database being optimized for search.

Here we are using CQRS implementation of MediatR library, so i would like to briefly explain about CQRS design flow, so we will have a basic understanding when we talk about commands, handlers.

Command Design: here is the UML diagram of a typical command look like:

Sequence diagram representing the flow of Command

The consumer of the command creates a command object and sends it to a command handler, which then applies mutation to the application(it could be entities or SQL update, delete, create to a database or Web API call over HTTP).

Query Design: Here is the UML representation of Query command look like:

Sequence diagram representing the flow of Query

The query return a value and very importantly the query must not change the state of the application but query for data instead.

Let’s see How CQRS can help us follow the SOLID principles:

S: Dividing an application into commands, queries and handlers takes us toward encapsulating single responsibilities into different classess.

O: CQRS helps extend the software without modifying the existing code, such as by adding handlers and creating new commands.

L:N/A

I: CQRS makes easier to create multiple small interfaces with a clear distinction between commands, queries and handlers.

D: N/A

Project Oraganization:

  • The Data directory contains data Seeding and DbContext classes.
  • The Domain directory contains Entities.
  • The Features directory contains features. Each subfolder contains its related classes (vertical slices).
  • Each use case or feature is self-contained and exposes the following classes:
    • a) Command /Query represents the MediatR requests.
    • b) Result is the return value of that request
    • c) MapperProfile instructs the Autmapper to map entities to results objects.
    • d) Validator contains validation rules to validate the incoming command object.
    • e) Handler contains the actual use case logic, how to handle the request.

Project Dependencies:

The solution uses below Nuget packages:

Creating a .NET 5 solution and project:

In this section we create a new .NET 5 Web API project using visual studio. refer this https://www.srinadimpalli.com/2020/08/setup-and-configure-asp-net-core-applications/ on how to setup an ASP.NET Core Web API Project.

Once done the initial project set up, install the above Nuget packages.

Creating models:

Create a new folder named “Domain” under the project and create two classes “Company.cs, Employee.cs” files.

 public class Company
    {
        public int Id { get; set; }
        public string Name { get; set; }
        public string Address { get; set; }
        public string Country { get; set; }
        public ICollection<Employee> Employees { get; set; }
    }
 public class Employee
    {
        public int Id { get; set; }
        public string Name { get; set; }
        public int Age { get; set; }
        public string Position { get; set; }
        public int CompanyId { get; set; }
        public Company Company { get; set; }
    }

Context class and Initial Data Seed:

Create another folder named “Data” under the root of the project, add a new class named “CompanyEmpContext” as below.

using Microsoft.EntityFrameworkCore;
using VerticalArchApp.API.Domain;

namespace VerticalArchApp.API.Data
{
    public class CompanyEmpContext : DbContext
    {
        public CompanyEmpContext(DbContextOptions<CompanyEmpContext> options) : base(options) { }
        public DbSet<Company> Companies { get; set; }
        public DbSet<Employee> Employees { get; set; }
    }
}

Add another class called “DependencyInjectionDataModule.cs” to Data folder , this class configures the EF Core InMemory database setup and adds the dependency to Asp.Net core IServiceCollection container using ForEvolve library.

using ForEvolve.DependencyInjection;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Diagnostics;
using Microsoft.Extensions.DependencyInjection;

namespace VerticalArchApp.API.Data
{
    public class DependecnyInjectionDataModule : DependencyInjectionModule
    {
        public DependecnyInjectionDataModule(IServiceCollection services) : base(services)
        {
            services.AddDbContext<CompanyEmpContext>(options => options
                .UseInMemoryDatabase("CompanyEmployeeMemoryDB")
                .ConfigureWarnings(builder => builder.Ignore(InMemoryEventId.TransactionIgnoredWarning))
                );
            services.AddForEvolveSeeders().Scan<Startup>();
        }
    }
}

Create another folder named “Seeders” under the Data directory , add a new class named “CompanySeeder.cs” file, add database seeding logic, this class implements ISeeder<T> generic interface from ForEvolve library to create the InMemory representation at the startup of the application runtime.

using ForEvolve.EntityFrameworkCore.Seeders;
using VerticalArchApp.API.Domain;

namespace VerticalArchApp.API.Data.Seeders
{
    public class CompanySeeder : ISeeder<CompanyEmpContext>
    {
        public void Seed(CompanyEmpContext db)
        {
            db.Companies.Add(new Company
            {
                Id = 100,
                Name = "IT Solutions Ltd",
                Address = "123 Wall dr. Malta, NY 12345",
                Country = "USA"
            });
            db.Companies.Add(new Company
            {
                Id = 200,
                Name = "Admin Solutions Ltd",
                Address = "232 Corner ave. Wison, CT  52362",
                Country = "USA"
            });

            // employee seed
            db.Employees.Add(new Employee
            {
                Id = 300,
                Name = "John Deo",
                Age = 34,
                Position = "Software Developer",
                CompanyId = 100
            });
            db.Employees.Add(new Employee
            {
                Id = 301,
                Name = "Sam McLeaf",
                Age = 28,
                Position = "Administrator",
                CompanyId = 200
            });

            db.Employees.Add(new Employee
            {
                Id = 302,
                Name = "Dom Sonmaze",
                Age = 45,
                Position = "Software Developer",
                CompanyId = 100
            });

            db.SaveChanges();
        }
    }
}

Now this completes the domain and database setup. let’s move on to the

After creating the InMemory database , it’s time to create a generic service repository that will provide us with the CURD methods, so all the methods can be called upon any every service repository class in our project.

Let’s start by creating an interface the service repository inside the “Services” folder.

NOTE: I have included both Interface and Class that implements the interface in the namespace, so it will be easy navigate between the two.

using Microsoft.EntityFrameworkCore;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Linq.Expressions;
using System.Threading.Tasks;
using VerticalArchApp.API.Data;

namespace VerticalArchApp.API.Services
{
    #region Interface
    public interface IServiceBase<T>
    {
        IQueryable<T> FindAll(bool trackChanges);
        IQueryable<T> FindByCondition(Expression<Func<T, bool>> expression, bool trackChanges);
        void Create(T entity);
        Task CreateAsync(T enity);
        void Update(T entity);
        void Delete(T entity);
    }
    #endregion

    #region Implementation
    public abstract class ServiceBase<T> : IServiceBase<T> where T : class
    {
        private readonly CompanyEmpContext _db;
        public ServiceBase(CompanyEmpContext db)
        {
            _db = db;
        }
        public void Create(T entity) => _db.Set<T>().Add(entity);
        public async Task CreateAsync(T entity) => await _db.Set<T>().AddAsync(entity);
        public void Update(T entity) => _db.Set<T>().Update(entity);
        public void Delete(T entity) => _db.Set<T>().Remove(entity);

        public IQueryable<T> FindAll(bool trackChanges) =>
            !trackChanges ? _db.Set<T>().AsNoTracking() : _db.Set<T>();


        public IQueryable<T> FindByCondition(Expression<Func<T, bool>> expression, bool trackChanges) =>

            !trackChanges ? _db.Set<T>()
                            .Where(expression)
                            .AsNoTracking() : _db.Set<T>()
                            .Where(expression);
    }
    #endregion
}

The abstract class as well as the IServiceBase interface works with the generic type T. This type T gives more reusability to the ServiceBase class. That means we don’t have to specify the exact model right now for the ServiceBase class to work with, as you can see the trackChanges parameter to use it to improve our read-only query performance, this greatly improves the speed of a query.

Creating a Service Manager: It is quite common for the API to return a response that consists of data from multiple resource or a service class may also trace down foreign key constraints, retrieving information where needed to return required data.

Service manager class creates instances of service class for each feature use case and register it inside the dependency injection container. let’s create a new interface IServiceManager and ServiceManager class in the Services folder.

using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using VerticalArchApp.API.Data;
using VerticalArchApp.API.Features.Companies;
using VerticalArchApp.API.Features.Employees;

namespace VerticalArchApp.API.Services
{
    #region Interface
    public interface IServiceManager
    {
        ICompanyService Company { get; }
        IEmployeeService Employee { get; }
        Task SaveAsync();
    }
    #endregion

    #region Implementation
    public class ServiceManager : IServiceManager
    {
        private readonly CompanyEmpContext _companyEmpContext;
        private ICompanyService _companyService;
        private IEmployeeService _employeeService;
        public ServiceManager(CompanyEmpContext companyEmpContext)
        {
            _companyEmpContext = companyEmpContext;
        }
        public ICompanyService Company
        {
            get
            {
                if (_companyService == null)
                    _companyService = new CompanyService(_companyEmpContext);
                return _companyService;
            }
        }

        public IEmployeeService Employee
        {
            get
            {
                if (_employeeService == null)
                    _employeeService = new EmployeeService(_companyEmpContext);
                return _employeeService;
            }
        }

        public Task SaveAsync() => _companyEmpContext.SaveChangesAsync();
    }
    #endregion

}

As you can see, we are creating properties that will expose the concrete service repositories ( one for each feature) and also we have the Save() method to be used after all the modifications are finished on a certain object.

For example add two companies, modify two employees and delete on company- all in one action- and then just call the Save method once.

_serviceManager.Company.Create(company);
_serviceManager.Company.Create(anotherCompany);
_serviceManager.Employee.Update(employee);
_serviceManager.Employee.Update(anotherEmployee);
_serviceManager.Company.Delete(oldCompany);
_serviceManager.Save();

After these changes, we need to register our manager class in the Startup class inside the ConfigureServices method.

Add below lines of code to the ConfigureServices method, add references.

public void ConfigureServices(IServiceCollection services)
{
	var currentAssembly = GetType().Assembly;

	services.AddAutoMapper(currentAssembly);
	services.AddMediatR(currentAssembly);
	services.AddDependencyInjectionModules(currentAssembly);
	// service manager
	services.AddScoped<IServiceManager, ServiceManager>();

	services.AddControllers();
	services.AddSwaggerGen(c =>
	{
		c.SwaggerDoc("v1", new OpenApiInfo { Title = "VerticalArchApp.API", Version = "v1" });
	});
}

Exploring Features: MediatR enables the implementation of CQRS by building controllers that are simple and well-structured. Let’s look at EmployeeController.

The constructor accepts an injected mediator service, which is saved in a local variable. The action methods fall into two categories: queries or commands. They are grouped into regions: Queries, Commands.

All queries are GET request, all commands are POST requests. The parameters in the action methods are Query or Command objects which are forwarded to the mediator. If it is Query request, mediator returns a model which is then sent to client or view. For command request, the posted data is validated before being passed to the mediator. The controller does not process the request, it only dispatches the message, Query handlers performs the actual processing of the request.

Let’s explore the CreateEmployeeForComapny feature. This class contains multiple nested classes to help organize our feature.

using AutoMapper;
using FluentValidation;
using MediatR;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using VerticalArchApp.API.Data;
using VerticalArchApp.API.Domain;
using VerticalArchApp.API.Services;

namespace VerticalArchApp.API.Features.Employees
{
    public class CreateEmployeeForCompany
    {
        //Input
        public class Command : IRequest<EmpResult>
        {
            public string Name { get; set; }
            public int Age { get; set; }
            public string Position { get; set; }
            public int CompanyId { get; set; }
        }
        //Output
        public class EmpResult
        {
            public int Id { get; set; }
            public string Name { get; set; }
            public int Age { get; set; }
            public string Position { get; set; }
            public int CompanyId { get; set; }
        }
        //Validator
        public class Validator : AbstractValidator<Command>
        {
            public Validator()
            {
                RuleFor(x => x.Name).NotNull().MaximumLength(30).WithMessage("Maximum lenght for the Name is 30 characters.");
                RuleFor(x => x.Age).NotNull();
                RuleFor(x => x.Position).NotNull().MaximumLength(20).WithMessage("Maximum length for the position is 20 characters.");
            }
        }
        //Handler
        public class Handler : IRequestHandler<Command, EmpResult>
        {
            private readonly IServiceManager _serviceManager;
            private readonly IMapper _mapper;
            public Handler(IServiceManager serviceManager, IMapper mapper)
            {
                _serviceManager = serviceManager ?? throw new ArgumentNullException(nameof(serviceManager));
                _mapper = mapper ?? throw new ArgumentNullException(nameof(mapper));
            }
            public async Task<EmpResult> Handle(Command request, CancellationToken cancellationToken)
            {
                var company = await _serviceManager.Company.GetCompanyAsync(request.CompanyId, trackChanges: false);

                if (company == null)
                {
                    throw new NoCompanyExistsException(request.CompanyId);
                }

                // var empEntity = _mapper.Map<Employee>(request);
                var empEntity = new Employee()
                {
                    Name = request.Name,
                    Age = request.Age,
                    CompanyId = request.CompanyId,
                    Position = request.Position
                };
                _serviceManager.Employee.CreateEmployeeForCompany(request.CompanyId, empEntity);
                await _serviceManager.SaveAsync();

                //return Unit.Value;
                //var resultByCompany = _db.Employees.Where(e => e.CompanyId == request.CompanyId).ToList();
                var result = _mapper.Map<EmpResult>(empEntity);
                return result;
            }
        }
    }
}

The Command class is the input of the use case (Creating a new Employee for a give company) . The request contains everything needed to execute the operation(Create a new Employee). The IReqeust<TResult> interface tells the MediatR that the Command class is a request and should be routed to its handler. Ther EmpResult class (which follows here) is the return value of that handler.

The EmpResult class represents the output of the use case. That’s what the handler will return.

MapperProfile class encapsulate AutoMapper maps that are related to the use case.

Validator class allows validating the input (Command) before it his the handler. To make this work, we need to implement an IPipelineBehaviour<TRequst, TResult> interface that is added to the MediatR pipeline [I will explain this in the Request Validation section]

The Handler class inherits from IRequestHandler<Command, EmpResult> and encapsulates the use case logic.

Request Validation: We will see how to add validation in our MediatR pipeline, only validators.

Step1: Add a MediatR IPipelineBehaviour<TRequest, TResult> that validates requests and throws a ValidatioException when the request validation fails.

Step2: Add an IExceptionFilter that catches ValidationException in the MVC Pipeline.

Those two classes (ThrowFluentValidationExceptionBehavior.cs, FluentValidationExceptionFilter.cs) will intercept and handle all validation exceptions thrown by any feature.

Below figure gives you a visual clue Request flow including request validation details.

  1. The user sends an HTTP request.
  2. The controller sends a command through the mediator.
  3. The mediator runs the request through its pipeline.
  4. The IPipelineBehavior implementation validates the request.
  5. If the request is valid, the following occurs:
    a) The request continues through the MediatR pipeline until it reaches the handler.
    b) The Handler is executed.
    c) The Handler returns a Result instance.
    d) The controller transfers that Result object into an OkObjectResult object.
  6. If the validation of the request fails, the following occurs:
    a) The IPipelineBehavior implementation throws ValidationException.
    b) The IActionFilter implementation catches and handles the Exception.
    c) The filter sets the action result to a BadRequestObjectResult.
  7. MVC transforms the resulting IActionResult into a 200 OK (success) or
    a 400 BadRequest (validation failure) response and serializes the resulting object into the response body.

Implementation of IPipelineBehavior, add a new class under the project root directory, and name it as “ThrowFluentValidationExceptionBehavior.cs”

public class ThrowFluentValidationExceptionBehavior<TRequest, TResponse> : IPipelineBehavior<TRequest, TResponse>
        where TRequest : IBaseRequest
    {
        private readonly IEnumerable<IValidator<TRequest>> _validators;
        public ThrowFluentValidationExceptionBehavior(IEnumerable<IValidator<TRequest>> validators)
        {
            _validators = validators;
        }
        public Task<TResponse> Handle(TRequest request, CancellationToken cancellationToken, RequestHandlerDelegate<TResponse> next)
        {
            var failures = _validators
               .Select(v => v.Validate(request))
               .SelectMany(r => r.Errors);
            if (failures.Any())
            {
                throw new ValidationException(failures);
            }
            return next();
        }
    }

Add this lines of code in the Startup Class.

  services.AddScoped(typeof(IPipelineBehavior<,>), typeof(ThrowFluentValidationExceptionBehavior<,>));

Implementation of IExceptionFilter (of MVC): add a new class under the project root directory, and name it as “FluentValidationExceptionFilter.cs

public class FluentValidationExceptionFilter : IExceptionFilter
    {
        public void OnException(ExceptionContext context)
        {
            if (context.Exception is ValidationException ex)
            {
                context.Result = new BadRequestObjectResult(new
                {
                    ex.Message,
                    ex.Errors
                });
                context.ExceptionHandled = true;
            }
        }
    }

In the Startup class , we replace the empty services.AddControllers() method call with the following.

 services.AddControllers(options => options.Filters.Add<FluentValidationExceptionFilter>())
                .AddFluentValidation(config => config.RegisterValidatorsFromAssembly(currentAssembly));

Add below lines of code in the Startup class of Configure method

 mapper.ConfigurationProvider.AssertConfigurationIsValid();
 app.Seed<Data.CompanyEmpContext>();

Once everything done, hit F5 run the application, after successfully build, you will see the swagger UI as below.

Github Repo

local_offerevent_note May 6, 2021

account_box srinivasaraju nadimpalli


local_offer