A sample for best practices and experimenting with Clean Architecture and CQRS
Root
- source : Source files for the solution containing the solutions and all related projects
- tests : All test projects for the solution
Source
- Presentation : All of the client facing application projects (console, web, api)
- Core : All of the application and domain logic
- Infrastructure : All of the low level services (persistence, messaging, authentication)
Tests
- Architecture Tests : Ensures the project dependencies are respected as expected
- Integration Tests : Higher level tests for interdependent projects
- Unit Tests : Low level code tests
These lessons assume you start with a pre-existing project using CQRS and an anemic model. Each lesson refactors the previous iteration starting with the initial anemic solution.
- Project Setup
- Architecture Tests
- DDD
- Entity
- Validation
- Value Objects
- Aggregate Root
- Domain Event
- Outbox
- Email Uniqueness
- CQRS
- MediatR Validation
- Polly Outbox Retry
- Idempotent Handlers
- Unit Test CQRS
- Unit Test Entity Framework
- Smart Enums
- Functional Programming
Source Detail
- Core Layer -- Application --- Abstractions --- Behaviors --- Contracts --- Exceptions --- Features -- Domain --- Entities --- Events --- Exceptions --- Shared --- ValueObject -Infrastructure -- Data -- Messaging
- Presenation -- Api --- Controllers --- Middleware
Architecture tests allow you to perform higher level checks on your solution. With architecture tests, you can check dependencies to ensure projects are referring to to the right projects/packages. This can flag unexpected issues if your dependencies change and help to avoid circular references.
Steps
- Create XUnit Test
- Add NetArchTest.Rules
- Add Assembly reference (empty class) to all projects
- Test each project for expected dependencies
- Test each project for unexpected dependencies
Refactoring starts with a working anemic model to demonstrate steps to change to a more DDD approach. The idea is that you start with a quick and dirty anemic model and then refactor the system with DDD techniques to improve the system.
Steps
- Create constructors in entities for exepected values
- Move complex construction to static Create methods
- Make constructors private to force use of Create method (why create versus constructor?)
- Make property setters private
- Move logic to methods in entity (i.e. SendInvitation)
- Change lists to readonly
This lesson comes after introducing the prebuilt solution that already contains the BaseEntity. The lesson rewinds the clock and isolates the initial entity creation.
Steps
- Add unique identifier
- Override Equals
- Override GetHashCode (use multiple of prime number)
- Add IEquatable
- Entities inherit from BaseEntity
At this stage, we talk about validation in terms of our strategy for handling invalid state. Should we throw an exception if there is a problem or return a result. Exceptions are easy to debug but hard in production. Results are hard to debug but easy in production.
Strategies
- Use Exceptions: Easier debugging with stacktrace
- Use Results: More expressive, house errors in catelog, self documenting, but does not halt execution making debugging more difficult
Exceptions can be easier for debugging because the execution will stop at the exception and you will have a stack trace. This makes debugging easy because you always know where the failure happened immediately. The downside is you need a try/catch at any point where you call the procedure to avoid creating invalid states and crashing the program.
Steps
- Create a base DomainException to identify Domain Exceptions
- Create Exceptions inheriting from Domain Exceptions
- Throw Exceptions defensively where ever domain state is theatened
Results allow you to handle conditions where returning an object may be optional and result in success or failure. For example, if you search for an entity in the database, it is possible that you retrieve an object or the object is not found. Results gives you a way to return a null object and check for success in the calling method. Further, you can return meaningful error statements that describe what happened.
The downside is returning a Result type is more difficult to debug. This strategy does not stop execution of the program while debugging. You need to set breakpoints to track failures and log errors. Debugging would involve analyzing the logs to determine where the error took place and then setting breakpoints to pause execution in order to analyze the state of the system.
Steps
- Create base Error
- Create base Result
- Create base Result
- Change return types that reference entities to Result types
- Replace Exceptions thrown to Result type of Failure with associated Error
- Create Errors static class to house all Error types
Value objects replace primitives as properties on entities. This allows you to enforce rules that preserve proper state that are not available for the primitive objects. For example, you may want to ensure that a username is never more than 50 characters.
Steps
- Create base ValueObject
- Replace entity property types with specific value types inheriting from ValueObject
- Fix references to former properties to create new value objects
An aggregate root is meant for entities that aggregate multiple other entities together. The purpose is to manage all related entities as one single transaction rather than breaking them out separately. For example, if a blog post item contains one or more tags, it is better to load and save them all at once rather than independently. If the tag fails, then the post will be in an invalid state. If you handle them all as one transaction, then you can manage invalid states.
Steps
- Create a new AggregateRoot base class inheriting from Entity simply as an empty class
- Change reference root class for aggregates
- Remove ability to retrieve subordinate entities directly (fetch and save as one whole not independently) !!! This comes before explaining CQRS or setting up the data layer so a little hard to follow
Considerations
- Need functioning data layer
Domain events allow you to separate the logic of processing a primary action from all the secondary actions that should be triggered after the primary completes. For example, you may want to send an email notification whenever a new member is added to the system. The primary action would be to add the member. The email notification should follow once the member is successfully added.
Steps
- Add IDomainEvent
- Add RaiseDomainEvent method to aggregate base
- Use RaiseDomainEvent when events happen in your aggregate entity
- Add MediatR to domain
- Add INotification to IDomainEvents
- Add event handler to application
Considerations:
- Need to separate services in handlers (i.e. email service depends on repository to pull posts - what if the database is down)
- Need outbox pattern
Added a basic data layer using entity framework and an in memory database.
Steps
- Add any missing IRepositories for entities in the domain layer
- Add anemic models to data for each entity
- Add mapping from entity to data and from data to entity
- Add repositories for each entity (Question: Only added aggregates. Should it include all entities?)
- Add Data Exceptions
The outbox pattern is used to make sure events are processed and not lost. The pattern takes in events and holds them until they can be successfully processed.
When an action completes on an aggregate, it should raise an event. An background job will periodically scan for events, pull them from the aggregate, store them to the datastore, handle them, and marked them complete.
Steps
- Add an OutboxMessage to persistence to store events to the database (Add model, include in DbSet)
- Add methods to Get and Clear the events from the aggregate root
- Create an interceptor to get the events from the aggregate, clear the events from the aggregate, and store them to the database.
- Wire up the interceptor in the dbContext initialization
- Add Quartz package for background jobs (job scheduler) to Infrastructure layer
- Add Outbox Job processing
- Need to wrap in try/catch for when job failures occur
- Add Quartz.Extensions.DependencyInjection to app
- Add Quartz Startup
TODO:
- Handle null event in job
- Add try/catch for job failures
ISSUE
- The outbox pattern implemented on the entity with separate data model does not work when using the interception method -> You intercept the model, not the entity
- Complex interaction here. Need to remember where to raise the event and ensure single point of entry
Strategies
- RaiseEvents on Model, intercept in EntityFramework, and save/clear messages
- Raise events on Entity, use EventRepository
Strategies
- Check for uniqueness in the create handler a. Pros: Simple solution where repository and entity are available b. Cons: Need to remember to do the check where ever you access.
- Check for uniqueness in the entity a. Pros: Single entry point for creation will always check without needing to remember b. Cons: Requires passing the repository to the entity (not good form)
- Check for uniqueness in the hanlder and pass result to the entity a. Pros: Keeps it simple without mixing repo/entity b. Cons: Need to remember to do the uniqueness check and pass result to entity, easily bypassed and forgotten
- Check in Repo a. Pros: Avoids passing dependencies, cleaner b. Cons: Need to remember to check update and other methods, requires try/catch or handling above
Steps
Notes
- Using base ApiController with ISender in constructor does not work with EndPointDefinition scanning. Remove and inject in method.
Command Query Responsibility Segregation separates the read only queries from the write commands. It allows you to keep the logic separate. It allows you to separate the backend database to potentially have a different database for online rights and offline reads for better performance.
Notes
- Uses Assembly reference static classes
- Separates ApiController from Api project
- Uses Screwter library to scann infrastructure and persisitence to register services
- Base API controller (ISender to send commands to mediatr (can also use publisher))
- Nothing was wired up so it is complicated to get started. Need to have a session just on wiring up without requiring a bunch of outside packages.
Query Handler strategies
- pass context to handler and query
- query directly with dapper (used in earlier lessons)
- use the repository (preferred)
Steps
- Add ICommand inheriting from IRequest to abstract MediatR
- Add ICommand inheriting from IRequest to enforce returning a result type
- Add ICommandHandler to abstract MediatR
- Add CreateMemberCommand
- Add CreateMemberRequest
- Add CreateMemberCommandHandler
- Add Assembly reference static class to all projects
- Add IQuery
- Add IQueryHandler
- Add GetMemberByIdQuery
- Add GetMemberByIdQueryHandler
- Add MemberResponse
Steps
- Create an interface to hold bad validation results IValidationResult
- Add concrete ValidationResult and ValidationResult
- Create Validator class for each command/query
Notes
- By setting public const in entities, you can share the values need for rules to maintain consistency for example with maximum length
- Somewhere along the way we created generic Result types, but never added them to our command/queries
- watch for lazy loading not loading on each query - better way?
Strategies
- throw an exception and handle in exception handling middleware
- use MediatR pipeline* to return results
Notes
- ValueObjects are too much. To create, you get a result, you must check if the result was successful, then you must check if the value is null to satisfy the compiler. Why not just return the value and check for null as success?
The outbox pattern can fail to process for a variety of reasons. For example, an email notifier could fail if the email service is temporarily down. What happens if this condition occurs?
strategies
- try/catch approach
- Polly Retry
Steps
- Add Polly
- Create a retry policy in a background job method
- Execute the target method inside the policy
- Capture the results
- Handle errored results
Avoid duplicating events if retries are used
Steps
- Create EventConsumer data class to track each consumer of the event
- Add missing IDomainEventHandler
- Add missing DomainEvent from IDomainEvent and change events to reference DomainEvents
- Create IdempotentDomainEventHandler
- Wire up the decorator (using Scrutor package)
Notes:
- Introduces non-existent IDomainEventHandler
- Introduces non-existent DomainEvent
- EventHandlers need to reference IDomainEventHandler and IDomainEventHandler needs to reference INotificationHandler - why the complexity?
Steps
- Make Application library internal visible to test
- Create UnitTest project in test folder
- reference Moq to Mock the repos
- Use Mock.Setup() to set parameters and return type of mocked methods/properties
- Write test against handlers using mocked repos
Notes
- naming convention class named for class tested
- method named for method tested, Should, expected outcome, and condition
Using the unit of work pattern also abstracting entity framework (or your data layer) from the application/presentation to avoid shared dependencies. It also for easier mocking during unit testing. It also offloads the save responsibility from the repositories allowing you to more easily create transactions across multiple repos. This method avoids using and wiring interceptors. It also allows you to build in record log and management. Similarly, abstracting the repos allows you to more easily mock in testing and shield the application layer from the data.
Steps
- start with base UnitOfWork
- Move Interceptor methods into UoW.
Notes
- starts with a very basic unit of work
- moves interceptors into unit of work
- References an Audit lesson that is not in the playlist - Audit interceptor for adding create and modify date tracking.
Basic enumerations work well, but sometimes you need more functionality. Building a SmartEnum allows you to mimic the functionality of enum and extend it to include additional functionality in a strongly typed class.
Steps
- Create an Enumeration class to inherit from
- Switch ENUM concrete type to class inheriting from Enumeration
Notes
- did not relate to the solution
SmartEnum Base
public abstract class Enumeration<TEnum> : IEquatable<Enumeration<TEnum>>
{
public int Value { get; protected init; }
public string Name { get; protected init; } = string.Empty;
public bool Equals(Enumeration<TEnum>? other)
{
if(other is null)
return false;
return GetType() == other.GetType() &&
Value == other.Value;
}
public override bool Equalt
}
SmartEnum implementation
public abstract class CreditCard : Enumeration<CreditCard>
{
public static readonly CreditCard Standard = new StandardCreditCard();
public static readonly CreditCard Premium = new PremiumCrediCard();
public static readonly CreditCard Platinum = new PlatinumCreditCard();
private CreditCard(int value, string name)
: base(value, name)
{ }
public abstract double Discount { get; }
private sealed class StandardCreditCard : CreditCard
{
public StandardCreditCard()
: base(1, "Standard") { }
public override double Discount => 0.0;
}
private sealed class PremiumCreditCard : CreditCard
{
public StandardCreditCard()
: base(1, "Premium") { }
public override double Discount => 0.05;
}
private sealed class PlatinumCreditCard : CreditCard
{
public StandardCreditCard()
: base(1, "Standard") { }
public override double Discount => 0.1;
}
}
Strategies
- Add middleware logic by adding a app.Use(() => {}) to program.cs using a try/catch before moving next
- Create a simple class for middleware
- Create a strongly typed middleware implementing IMiddleware
Steps
- Create a middleware class
- Add the class as a transient service for initialization
- UseMiddleware in program.cs
app.Use(async (context, next) =>
{
try
{
await next(context);
}
catch(Exception e)
{
context.Response.StatusCode = 500;
// TODO: Add more meaningful information and logging
}
})
Works between the API and the database
Steps
- Add Cache Repository
- Add IMemoryCache to Repository
- Make key
- Wire in program.cs - add the concrete repo, add cache implementing interface
Strategy for initialization
- Initialize the concrete DbRepo, reference the DbRepo in the CacheRepo, initiailize the interface referencing the cacheRepo Pros: Simple to wire up with no other dependencies Cons: Can be complicated if you are initializing the repo anywhere else - it will break or not function as expected
- Initialize the concrete DbRepo, reference the DbRepo in the CacheRepo, completex initialization of the interface Pros: No dependencies Cons: More complexity in the program.cs
- Use Scrutor Decorator, Cache only references IMemberRepo Pros: Clean and conflicts if referencing the interface Cons: Brings in the SCutor dependency
Option 1
builder.Services.AddScoped<MemberRepository>();
builder.Services.AddScoped<IMemberRepository, MemberCacheRepository>();
Option 2
builder.Services.AddScoped<IMemberRepository>(provider =>
{
var memberRepo = provider.GetService<MemberRepository>()!;
return new MemberCacheRepository(
memberRepository,
provider.GetService<IMemoryCache>()!);
)
})
Options 3
builder.Services.AddScoped<IMemberRepository, MemberRepository>();
builder.Services.Decorate<IMemberRepository, MemberCacheRepository>();
Railway acts as a switch with two possibilities - failure or success. Extensions are used against the Result type to apply validations. It can result in less code that reads more naturally.
It is implemented by chaining methods together to result in a Result type that can be returned.
This approach abstracts the common patter of validation checks that look for conditions to decide if a result will be successful or failure.
Alternatively, it can be hard to debug and is not a common concept that could confuse others who are not familiar with the pattern.
Steps
- Create an extension class for Result
- Update conditions in classes that produce Result
Notes
- implemented on Email value object as a test
Handles authentication by checking for a registered member and returns a token to use to authenticate further requests limiting access to some end points expecting the authorized token.
Steps
- Add Microsoft.AspNetCore.Authentication.JwtBearer nuget to the API project
- Add IJwtProvider to Infrastructure to abstract the provider implementation
- Add a LoginCommand to the Application project
- Add a LoginRequest to capture the parameters in the Application project
- Add a LoginCommandHandler to the Application project
- Add a login endpoint to the member controller of the API project
- Wire authentication/authorization to program.cs
- create JwtOptions and wire
- Create JwtBearerOptions and wire
- Add options to AppSettings ++. Add to Swagger
Notes
- Milan put IJwtProvider in Application and then reference from Infrastructure. The flow should be Infrastructure reference FROM Application not the other way around. I've plaed in Infrastructure with the idea being the implementation could be moved to a separate Infrastructure layer project.'
- Does not work as is. The JwtBearerOptionsSetup has to reference IConfigureNamedOptions instead of IConfigureOptions
Use Authorize to limit access to end points to only those members that match the authorization criteria
Strategy
- Hard code list of Roles in attribute of endpoint for authorization
- Hard code policy in attribute of endpoint for authorization
- Create an authorization type to handle permissions
Steps
- Create a HasPermissionAttribute attribute extending AuthorizeAttribute
- Create Permission class or enum
- Add attribute or .RequireAuthorization to end points that require authorization
Notes
- Approaches are using attribute - not necessary if using .RequireAuthorization(Permission.Read)
- Consider handling by value level with Admin = 1 at the top
- Should extend to post controller
Configure Roles and Permissions with EntityFramework by defining them in the domain. Designed with a hierarchy where a Member belongs to a Role which is assigned a set of Permissions.
Steps
- Add Role to entities as an Enumeration (SmartEnum) Type
- Add Permission to entities with simple properties
- Create navigation properties in Role -> ICollection, ICollection
- Add RoleConfiguration a. key b. Permission navigation manytomany c. Member navigation manytomany d. Add data to role by getting values in enumeration e. ToTable
- Add PermissionConfiguration a. Totable b. key c. use permission enum to seed data
- Add RolePermission to match join table a. composite key b. Back in RoleConfiguration, add usingEntity c. Add seed to RolePermission, use private method and seed multiple as array
- Add Migration and Update
Note
- Makes use of SmartEnum, this creates a conflict with the Role type created previously
- This assumes you've completed the EntityFramework videos which were not part of the playlist.
Using EF with related entities
Steps
- Create Entities
- Add Navigation Properties
- Create DbContext
- Configure DbContext
Note
- Not part of the playlist or solution
- EF queries can be inefficient when looping
- Execute a specified SQL statement using ExecuteSqlInterpolatedAsync
- Add Dapper including transactions using DbContext.Database & DbContext.Database.GetTransaction
- use AsNoTracking to remove tracking on objects you are going to fetch without changing - improves performance
Read optional settings from the appsettings file
Steps
- Add options to appsettings [Api]
- Add DatabaseOptions class to hold the settings [Data]
- Add DatabaseOptionsStartup to pull the options from appsettings and bind them to the options class [Api]
- Add optionsstartup to program.cs
Steps
- Create PermissionRequirement : IAuthorizationRequirement [Infra]
- Create PermissionAuthorizationHandler : AuthorizationHandler<> [Infra] a. Check for MemberId in Claim b. Check for premissions in db (different strategy in next lesson)
- Cretae IPermissionService to reference in PermissionAuthorizationHandler
- Implement PermissionService
- Update Configuration a. AddAuthorization() b. Add IAuthorizationHandler
- Add PermissionAuhotoirzationPolicyProvder
- Add to startup
- Add Policy to endpoint?
Notes
- Following along correctly, but some elements change between the creation and the debug
- Does not run as is. The claims are missing from the user
Change the service from hitting the database to checking the claims directly
Steps
- Change JwtProvider (and IJwtProvider) to async add permissions from the service
- Add CustomClaims to store the permissions name to a const
- Call the new method from the MemberController
Notes
- Claims can get really large if you use multiple permissions
- Token lifetimes can be used as long as valid - long timeframe can be a problem
Steps
- Add EditorConfig
- Add StyleCop.Analyzers
- Add SonarAnalyzer.CSharp
- Add true in project to enforce the style rules instead of suggest
- Update EditorConfig to selectively apply or omit particular rules you like or do not like
- Move to solution level to enforce on all projects in the solution a. Add Directory.Build.Prop solution item b. Add configuration for nuget packages, treatwarnings, code analysis and other settings *. How to exclude from Tests?
Notes
- Severities are none (no warning/error), warning (warn but do not fail build), error (fail build)
- dotnet_diagnostic.AA####.severity = none -- SA = StyleCope -- S = SonarAnalyzer -- CA = .Net Code analyzers -- IDE = Visual Studio
Strategies
- Add Services.Configure() to program.cs
Steps
- Add ApplicationOptions class with values you want to use
- Add to your AppSettings, ApplicationOptions section with key/values matching the properties from step 1
- Wire to your program.cs S1. builder.Services.Configure<ApplicationOptions(builder.Configuration.GetSection(nameof(ApplicationOptions))); S2. 1. Add ApplicationOptionsSetup : IConfigureOptions 2. Wire with builder.Services.ConfigureOptions()
- Reference IOptions options with options.Value.PROPERTY where ever you need configurations passed to your program
ApplicationOptions.cs
public class ApplicationOptions
{
public string ApplicationName { get; set; }
}
ApplicationOptionsStartup.cs
public class ApplicationOptionsStartup : IConfigurationOptions<ApplicationOptions>
{
private const string cSectionName = nameof(ApplicationOptions);
private readonly IConfiguration mConfiguration;
public ApplicationOptionsStartup(IConfiguration configuration)
{
mConfiguration = configuration;
}
public void Configure(ApplicationOptions options)
{
mConfiguration.GetSection(cSectionName).Bind(options);
}
}
Program.cs
builder.Services.ConfigureOptions<ApplicationOptionsStartup>();
Service.cs
public class Service
{
private ApplicationOptions mOptions;
private string ApplicationName;
public Service(ApplicationOptions options)
{
mOptions = options;
ApplicationName = options.ApplicationName;
}
}
Notes
- Binding Options does not allow change notification
Use extension methods to segregate different types of startup routines into an easier to maintain collection of startup files.
Strategies
- extension method for IServiceCollection
- Use IServiceInstaller classes and use reflection to find and execute
Steps for Strategy 1
- Create a static class with a static method returning an IServiceCollection and taking in a this IServiceCollection
- Move items from Program.cs to the new static method adjusting
StartupExtension.cs
public static class StartupExtension
{
public static IServiceCollection AddSomeStartup(this IServiceCollection services, IConfiguration configuration)
{
services.AddSingleton<ISomeInterface, SomeClass>(config => config.Option = configuration.Option);
}
}
Program.cs
builder.Services.AddSomeStartup(builder.Configuration);
Steps for Strategy 2
- Create IServiceInstaller with void Install(IServiceCollection, IConfiguration)
- Create one installer for each startup method
- Create an extension method that takes in an array of Assemblies to search for IServiceInstaller
IServiceInstaller.cs
public interface IServiceInstaller
{
public void Install(IServiceCollection services, IConfiguration configuration)
{
services.AddSingleton<ISomeInterface, SomeClass>(config => config.Option = configuration.Option);
}
}
SomeServiceStartup.cs
public class SomeServiceInstaller : IServiceInstaller
{
public IServiceCollection Install(IServiceCollection services, IConfiguration configuration)
}
StartupExtension.cs
public static class StartupExtension
{
public static IServiceCollection InstallServices(this IServiceCollection services, IConfiguration configuration, params Assembly[] assemblies)
{
IEnumerable<IServiceInstaller> serviceInstallers = assemblies
.SelectMany(a => a.DefinedTypes)
.Where(IsAssignableToType<IServiceInstaller>)
.Select(Activator.CreateInstance)
.Case<IServiceInstaller>();
foreach(IServiceInstaller serviceInstaller in serviceInstallers)
serviceInstaller.Install(services, configuration);
return services;
static bool IsAssignableToType<T>(TypeInfo typeInfo) =>
typeof(T).IsAssignableFrom(typeInfo) &&
!typeInfo.IsInterface &&
!typeInfo.IsAbstract;
}
}
Notes
- Either have one class with multiple extension methods or several classes with a single extension method
- returning IServiceColelction allows chaining of methods.
Milan Jovanovic Clean Architecture & DDD Series https://www.youtube.com/playlist?list=PLYpjLpq5ZDGstQ5afRz-34o_0dexr1RGa