012 Clean Architecture - CarrieKroutil/Reactivities GitHub Wiki
This diagram - https://blog.cleancoder.com/uncle-bob/2012/08/13/the-clean-architecture.html represents the ideal clean architecture pattern to follow.
To achieve the clean architecture depicted above, we need to implement the CQRS and Mediator patterns for our use case (not a fit for all use cases!).
CQRS stands for Command and Query Responsibility Segregation and uses to separate read(queries) and write(commands). In that, queries perform a read operation, and command performs writes operations like create, update, delete, and return data.
Command | Query |
---|---|
Does something | Answers a question |
Modifies State | Does not modify State |
Should not return a value | Should return a value |
In a lot of applications, a single data model to read and write data will work fine and perform CRUD operations easily. But, when the application out grows that case, and queries need to return different types of data as an object it becomes harder to manage with different DTO objects. Also, the model becomes more comples when the same model is used to perform a write operation.
When the same model is used for both reads and write operations the security is also hard to manage when the application is large and the entity might expose data in the wrong context due to the workload on the same model.
In summary, CQRS helps to decouple operations and make the application more scalable, securable, and flexible on a large scale.
Best use case is when separate read and write databases are used:
flowchart TD
A(API) --> B(Query)
B --> C(Data access)
C --> |Optimized for read| E[fa:fa-database No SQL DB]
A --> F(Command)
F --> G(Domain)
G --> H(Persistence)
H --> |Optimized for write| I[fa:fa-database SQL DB]
NOTE: The SQL DB pushes data to the No SQL DB to be eventually consistent.
Mediator design pattern is one of the important and widely used behavioral design pattern. Mediator enables decoupling of objects by introducing a layer in between so that the interaction between objects happen via the layer. If the objects interact with each other directly, the system components are tightly-coupled with each other that makes higher maintainability cost and not hard to extend. Mediator pattern focuses on providing a mediator between objects for communication and help in implementing loose-coupling between objects.
Air traffic controller is a great example of mediator pattern where the airport control room works as a mediator for communication between different flights. Mediator works as a router between objects and it can have it’s own logic to provide way of communication.
Open Nuget Gallery and then add Nuget package: MediatR for ASP.NET Core
- [shift + command + p] = opens command pallet.
- Then type: “Nuget Gallery"
- Search "MediatR"
- Select
MediatR.Extensions.Microsoft.DependencyInjection
by Jimmy Bogard - Check "Application.csproj" and click "Install"
In the Application project, add an "Activities" folder, and a new C# class inside called "List" with the following contents:
using Domain;
using MediatR;
using Microsoft.EntityFrameworkCore;
using Persistence;
namespace Application.Activities
{
public class List
{
public class Query : IRequest<List<Activity>> {}
public class Handler : IRequestHandler<Query, List<Activity>> {
private readonly DataContext _context;
public Handler(DataContext context)
{
_context = context;
}
public async Task<List<Activity>> Handle(Query request, CancellationToken cancellationToken) {
return await _context.Activities.ToListAsync();
}
}
}
}
In the ActivitiesController.cs, update the constructor to no longer inject the DataContext, and instead just contain (Mediator)
. Now there should be an error at this point, and to resolve, in a terminal at the solution level, run dotnet restore
. Now the API project should know about the Mediatr package, and the error should be resolved with a using statement for using Mediatr;
.
Add the named "mediator" parameter and [command + .] to “initialize field from property”.
using Microsoft.AspNetCore.Mvc;
using Microsoft.EntityFrameworkCore;
using Domain;
using Persistence;
using MediatR;
using Application.Activities;
namespace API.Controllers
{
public class ActivitiesController : BaseApiController
{
private readonly IMediator _mediator;
public ActivitiesController(IMediator mediator)
{
_mediator = mediator;
}
[HttpGet] //api/activities
public async Task<ActionResult<List<Activity>>> GetActivities()
{
return await _mediator.Send(new List.Query());
}
[HttpGet("{id}")] //api/activities/wpmvoseml
public async Task<ActionResult<Activity>> GetActivity(Guid id)
{
return Ok(); // Todo: Fix soon
}
}
}
Next, in ..\API\Program.cs
, after the builder call for CORS, add builder for MediatR, including two new using statements:
using MediatR;
using Application.Activities;
// Instruct which assembly the handlers live via typeof() pointing to any handler.
builder.Services.AddMediatR(typeof(List.Handler));
Test the changes are working by using Postman, or running the API and React App together via:
- In one terminal, cd to API folder and run
dotnet run
. - In another terminal, cd to client-app folder and run
npm start
.
Remember, [control + c] to kill each process.
To reduce redundant code, move the mediator logic out of each API controller's constructer into the BaseApiController. Note the private prop is only accessible within the class used, but protected props are available to inherited classes too. Update "..\API\BaseApiController.cs" with the following:
using MediatR;
using Microsoft.AspNetCore.Mvc;
namespace API.Controllers
{
[ApiController]
[Route("api/[controller]")]
public class BaseApiController : ControllerBase
{
private IMediator _mediator;
protected IMediator Mediator => _mediator ??= HttpContext.RequestServices.GetService<IMediator>();
}
}
Note: ??=
says if _mediator
is null, assign anything to the right to that property.
Then in the ActivitiesController, remove the constructor and all should continue to work after a quick rename from _mediator
to Mediator
.
Add new ..\Application\Activities\Details.cs
file with content:
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Domain;
using MediatR;
using Persistence;
namespace Application.Activities
{
public class Details
{
public class Query : IRequest<Activity>
{
public Guid Id { get; set; }
}
public class Handler : IRequestHandler<Query, Activity>
{
private readonly DataContext _context;
public Handler(DataContext context)
{
_context = context;
}
public async Task<Activity> Handle(Query request, CancellationToken cancellationToken)
{
return await _context.Activities.FindAsync(request.Id);
}
}
}
}
Then update ..\API\Controllers\ActivitiesController.cs
with new method:
[HttpGet("{id}")] //api/activities/wpmvoseml
public async Task<ActionResult<Activity>> GetActivity(Guid id)
{
// Specifying curly brackets initializes an object during class instantiation
return await Mediator.Send(new Details.Query{Id = id});
}
To test in Postman:
- First, start up the API in a terminal via
dotnet start
if not already running. - Do a "GET" call to
{{url}}/api/activities/1cbfbc52-ed45-419e-bb79-7ab1a37b6c1f
- Remember the {{url}} is setup in the Collection's Variables section:
- Variable = url, Initial value = http://localhost:5000
- Remember the {{url}} is setup in the Collection's Variables section:
Add new ..\Application\Activities\Create.cs
file with content:
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Domain;
using MediatR;
using Persistence;
namespace Application.Activities
{
public class Create
{
public class Command : IRequest
{
public Activity Activity { get; set; }
}
public class Handler : IRequestHandler<Command>
{
private readonly DataContext _context;
public Handler(DataContext context)
{
_context = context;
}
public async Task<Unit> Handle(Command request, CancellationToken cancellationToken)
{
// Only adding actiity to memory in EF tracking, no db change until SaveChanges is called.
_context.Activities.Add(request.Activity);
await _context.SaveChangesAsync();
// Let controller know the work is finished.
return Unit.Value;
}
}
}
}
Then update ..\API\Controllers\ActivitiesController.cs
with new method:
/// <summary>
/// Since this controller class inherietes from BaseApiController, which has the [ApiController] attribute,
/// when an object is sent through the request body, the code is smart enough
/// to look in the method's parameter to get the needed object, and if the properties available match the expected object.
///
/// Note: Could also add attribute to be more explicit. E.g. CreateActivity([FromBody]Activity activity)
/// </summary>
[HttpPost]
public async Task<IActionResult> CreateActivity([FromBody]Activity activity)
{
return Ok(await Mediator.Send(new Create.Command{Activity = activity}));
}
To test in Postman:
- First, start up the API in a terminal via
dotnet start
if not already running. - Create a "POST" call to
{{url}}/api/activities
- On the Body tab, select "raw" "JSON" and paste the following:
- (note - it's using the built in Postman Guid creator function)
{
"id": "{{$guid}}",
"title": "Test Create Activity 2",
"description": "Activity created two weeks in future",
"category": "film",
"date": "{{activityDate}}",
"city": "Estes Park",
"venue": "Cinema"
}
- On the Pre-request Script tab, set a date environment variable using the JS lib called moment via:
var moment = require("moment");
pm.environment.set('activityDate', moment().add(14, 'days').toISOString());
Add new ..\Application\Activities\Edit.cs
file with content:
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Domain;
using MediatR;
using Persistence;
namespace Application.Activities
{
public class Edit
{
public class Command : IRequest
{
public Activity Activity { get; set; }
}
public class Handler : IRequestHandler<Command>
{
private readonly DataContext _context;
public Handler(DataContext context)
{
_context = context;
}
public async Task<Unit> Handle(Command request, CancellationToken cancellationToken)
{
var actiity = await _context.Activities.FindAsync(request.Activity.Id);
actiity.Title = request.Activity.Title ?? actiity.Title;
await _context.SaveChangesAsync();
return Unit.Value;
}
}
}
}
Then update ..\API\Controllers\ActivitiesController.cs
with new method:
[HttpPut("{id}")]
public async Task<IActionResult> EditActivity(Guid id, Activity activity)
{
activity.Id = id;
return Ok(await Mediator.Send(new Edit.Command{Activity = activity}));
}
To test in Postman:
- First, start up the API in a terminal via
dotnet start
if not already running. - Do a "PUT" call to
{{url}}/api/activities/1cbfbc52-ed45-419e-bb79-7ab1a37b6c1f
- On the Body tab, select "raw" "JSON" and paste the following:
{
"title": "Test Create Activity updated",
"description": "Activity can now be editied",
"category": "film",
"date": "{{activityDate}}",
"city": "Estes Park",
"venue": "Cinema"
}
Note, only the title will actually update as that is all we've told the edit handler to do so far.
AutoMapper is a tool to map objects together without explicit code for each property.
To setup:
- Access package manager in vscode, hit {shift + cmd + p} and type "Open NuGet Gallery".
- Search for "AutoMapper.Extensions.Microsoft.DependencyInjection" by Jimmy Bogard for ASP.NET Core
- Select the package and choose the "Application.csproj" project only, and click "Install"
To use with the Edit Handler:
Add a new "Core" folder in the Application project, and a new file inside there called "MappingProfiles.cs". Add the following content:
using AutoMapper;
using Domain;
namespace Application.Core
{
public class MappingProfiles : Profile
{
public MappingProfiles()
{
CreateMap<Activity, Activity>();
}
}
}
Update the "..\Application\Activities\Edit.cs" file to inject an IMapper into the class and the Handle method implements the Map function:
using AutoMapper;
using Domain;
using MediatR;
using Persistence;
namespace Application.Activities
{
public class Edit
{
public class Command : IRequest
{
public Activity Activity { get; set; }
}
public class Handler : IRequestHandler<Command>
{
private readonly DataContext _context;
private readonly IMapper _mapper;
public Handler(DataContext context, IMapper mapper)
{
_context = context;
_mapper = mapper;
}
public async Task<Unit> Handle(Command request, CancellationToken cancellationToken)
{
var actiity = await _context.Activities.FindAsync(request.Activity.Id);
//actiity.Title = request.Activity.Title ?? actiity.Title;
_mapper.Map(request.Activity, actiity);
await _context.SaveChangesAsync();
return Unit.Value;
}
}
}
}
Last step is to tell Program.cs how to instantiate the IMapper service after AddMediaR line:
builder.Services.AddAutoMapper(typeof(MappingProfiles).Assembly);
Remember, if the AddAutoMapper is not able to resolve w/a using statement, to run dotnet restore
in a terminal window at the solution root.
Run the PUT command in Postman and now all fields should be persisted.
Add new ..\Application\Activities\Delete.cs
file with content:
using MediatR;
using Persistence;
namespace Application.Activities
{
public class Delete
{
public class Command : IRequest
{
public Guid Id { get; set; }
}
public class Handler : IRequestHandler<Command>
{
private readonly DataContext _context;
public Handler(DataContext context)
{
_context = context;
}
public async Task<Unit> Handle(Command request, CancellationToken cancellationToken)
{
var actiity = await _context.Activities.FindAsync(request.Id);
// TODO: Handle null object
// Removed just from EF Change Tracking in memory.
_context.Remove(actiity);
// Commit to DB
await _context.SaveChangesAsync();
return Unit.Value;
}
}
}
}
Then update ..\API\Controllers\ActivitiesController.cs
with new method:
[HttpDelete("{id}")]
public async Task<IActionResult> DeleteActivity(Guid id)
{
return Ok(await Mediator.Send(new Delete.Command{Id = id}));
}
To test in Postman:
- First, start up the API in a terminal via
dotnet start
if not already running. - Do a "DELETE" call to
{{url}}/api/activities/1cbfbc52-ed45-419e-bb79-7ab1a37b6c1f
- Note: Nothing is needed in the body, the URL is where the Id comes from.
Create an extension class to shift the services creation to in order to trim down the Program.cs class.
Start by creating a new folder and class under the API folder: "..\API\Extensions\ApplicationServiceExtensions.cs" with the following content:
using Application.Activities;
using Application.Core;
using MediatR;
using Microsoft.EntityFrameworkCore;
using Persistence;
namespace API.Extensions
{
/// <summary>
/// Extension classes and methods need to be static.
/// Important to use the "this" keyword to denote what is being extended.
///
/// The Application Service Extensions class is intended to clean up the Program.cs code.
/// </summary>
public static class ApplicationServiceExensions
{
/// <summary>
/// Add App
/// </summary>
/// <param name="services"></param>
/// <param name="config"></param>
/// <returns></returns>
public static IServiceCollection AddApplicationServices(this IServiceCollection services, IConfiguration config)
{
// Learn more about configuring Swagger/OpenAPI at https://aka.ms/aspnetcore/swashbuckle
services.AddEndpointsApiExplorer();
services.AddSwaggerGen();
services.AddDbContext<DataContext>(opt =>
{
opt.UseSqlite(config.GetConnectionString("DefaultConnection"));
});
// Needs to match URL where requests are coming from, in our case, client-app aka our React application.
services.AddCors(options =>
{
options.AddPolicy("CorsPolicy",
s => s.SetIsOriginAllowedToAllowWildcardSubdomains()
.WithOrigins(
"http://localhost:3000"
)
.AllowAnyMethod()
.AllowAnyHeader()
.AllowCredentials()
);
});
// Instruct which assembly the handlers live via typeof() pointing to any handler.
services.AddMediatR(typeof(List.Handler));
services.AddAutoMapper(typeof(MappingProfiles).Assembly);
return services;
}
}
}
Then remove the redundant code from "..\API\Program.cs" and replace it with the following line right below the builder.AddControllers();
line:
builder.Services.AddApplicationServices(builder.Configuration);
Remember to clean up the unused using statements.
- To quickly rename a variable, highlight text, Cmd + Shift + L on macOS and Ctrl + Shfit + l on Windows to highlight all other instances in file that matches the highlighted text.
- To format code file, Shift + Option + F on macOS or Shift + Alt + F on Windows.
Allows for the request to be canceled if needed, like if the user close the browser. Important that a cancellation token is passed from the API controller method thru the Mediator.Send in order for the Handlers to receive the message to cancel the task. Example code:
In "..\API\Controllers\ActivitiesController.cs", update the get activities endpoint with:
[HttpGet] //api/activities
public async Task<ActionResult<List<Activity>>> GetActivities(CancellationToken cancellationToken)
{
return await Mediator.Send(new List.Query(), cancellationToken);
}
Update the ".\Application\Activities\List.cs" to contain a logger and logic to delay for cancel request to be seen in Postman.
using Domain;
using MediatR;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Logging;
using Persistence;
namespace Application.Activities
{
public class List
{
public class Query : IRequest<List<Activity>> { }
public class Handler : IRequestHandler<Query, List<Activity>>
{
private readonly DataContext _context;
private readonly ILogger<List> _logger;
public Handler(DataContext context, ILogger<List> logger)
{
_logger = logger;
_context = context;
}
public async Task<List<Activity>> Handle(Query request, CancellationToken cancellationToken)
{
try
{
for (int i = 0; i < 10; i++)
{
cancellationToken.ThrowIfCancellationRequested();
await Task.Delay(1000, cancellationToken);
_logger.LogInformation($"Task {i} has completed");
}
}
catch (System.Exception)
{
_logger.LogInformation("Task was cancelled");
}
return await _context.Activities.ToListAsync();
}
}
}
}
Make a call in Postman to GET Activities, and then click "Cancel" and watch the terminal to see the logged messages.
Inside the .vscode
folder at the root of the solution, should be a launch.json
file, which should contain:
{
"name": ".NET Core Attach",
"type": "coreclr",
"request": "attach"
}
If that does not exist, press Shift + Cmd + P and type "Generate Assets for Build and Debug".
Next, to attach to the debugger, the API needs to first be running in a terminal. Remember to cd api
and dotnet run
.
Add a breakpoint in the EditActivity method to test things out.
Open up the debugging window from the left nav (or by using Shift + Cmd + D) and click the dropdown at the top for ".NET Core Attach".
Click on the play icon and select the process to attach to - API.dll for macOS and API.exe for Windows.
Perform an edit action in PostMan and see the breakpoint hit.
Click the "Detach" button (or Shift + F5 on macOS) to stop debugging but leave the process running.
To learn a more advanced topic, check out youtube video: CQRS and Event Sourcing Introduction with Greg Young part 1.