Skip to content

Commit 35c21df

Browse files
committed
added the CQRS demo
1 parent eee72f2 commit 35c21df

File tree

80 files changed

+5678
-3
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

80 files changed

+5678
-3
lines changed
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
using CQRSTest.Authorisation.Requirements;
2+
using Microsoft.Extensions.DependencyInjection;
3+
4+
namespace CQRSTest.Authorisation
5+
{
6+
public static class AuthorisationExtensions
7+
{
8+
public static void AddAuthorisationRequirementHandlers(this IServiceCollection services)
9+
{
10+
services.Scan(scan => scan
11+
.FromAssemblyOf<IRequirementHandler>()
12+
.AddClasses(classes => classes.AssignableTo<IRequirementHandler>())
13+
.AsImplementedInterfaces()
14+
.WithTransientLifetime());
15+
}
16+
}
17+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
namespace CQRSTest.Authorisation
2+
{
3+
public record AuthorisationResult
4+
{
5+
public bool IsAuthorised { get; set; }
6+
public string Message { get; set; }
7+
8+
public static AuthorisationResult Authorised => new AuthorisationResult { IsAuthorised = true };
9+
public static AuthorisationResult Unauthorised => new AuthorisationResult();
10+
public static AuthorisationResult UnAuthorised(string message) => new AuthorisationResult { Message = message };
11+
}
12+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
using CQRSTest.Authorisation.Requirements;
2+
using System.Collections.Generic;
3+
4+
namespace CQRSTest.Authorisation
5+
{
6+
public interface IAuthorisable
7+
{
8+
List<IRequirement> Requirements { get; }
9+
}
10+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
using CQRSTest.Authorisation.Requirements;
2+
using Microsoft.Extensions.Logging;
3+
using System.Threading.Tasks;
4+
5+
namespace CQRSTest.Authorisation.Requirements
6+
{
7+
public record CanAccessTodoRequirement(int Id) : IRequirement;
8+
public class CanAccessTodoRequirementHandler : IRequirementHandler<CanAccessTodoRequirement>
9+
{
10+
private readonly ILogger<CanAccessTodoRequirementHandler> logger;
11+
12+
public CanAccessTodoRequirementHandler(ILogger<CanAccessTodoRequirementHandler> logger)
13+
{
14+
this.logger = logger;
15+
}
16+
17+
public async Task<AuthorisationResult> Handle(CanAccessTodoRequirement requirement)
18+
{
19+
logger.LogInformation("User has access to todo item. ID: {Id}", requirement.Id);
20+
return AuthorisationResult.Authorised;
21+
}
22+
}
23+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
namespace CQRSTest.Authorisation.Requirements
2+
{
3+
public interface IRequirement { }
4+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
using System.Threading.Tasks;
2+
3+
namespace CQRSTest.Authorisation.Requirements
4+
{
5+
public interface IRequirementHandler { }
6+
public interface IRequirementHandler<T> : IRequirementHandler where T : IRequirement
7+
{
8+
Task<AuthorisationResult> Handle(T requirement);
9+
}
10+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
using CQRSTest.Authorisation;
2+
using CQRSTest.Authorisation.Requirements;
3+
using CQRSTest.DTOs;
4+
using MediatR;
5+
using Microsoft.Extensions.DependencyInjection;
6+
using Microsoft.Extensions.Logging;
7+
using System;
8+
using System.Net;
9+
using System.Threading;
10+
using System.Threading.Tasks;
11+
12+
namespace CQRSTest.Behaviours
13+
{
14+
public class AuthorisationBehaviour<TRequest, TResponse> : IPipelineBehavior<TRequest, TResponse>
15+
where TRequest : IAuthorisable
16+
where TResponse : CQRSResponse, new()
17+
{
18+
private readonly ILogger<AuthorisationBehaviour<TRequest, TResponse>> logger;
19+
private readonly IServiceProvider serviceProvider;
20+
21+
public AuthorisationBehaviour(ILogger<AuthorisationBehaviour<TRequest, TResponse>> logger,
22+
IServiceProvider serviceProvider)
23+
{
24+
this.logger = logger;
25+
this.serviceProvider = serviceProvider;
26+
}
27+
28+
public async Task<TResponse> Handle(TRequest request, CancellationToken cancellationToken, RequestHandlerDelegate<TResponse> next)
29+
{
30+
var requestName = request.GetType();
31+
logger.LogInformation("{Request} is authorisable.", requestName);
32+
33+
// Loop through each requirement for the request and authorise
34+
foreach(var requirement in request.Requirements)
35+
{
36+
var handlerType = typeof(IRequirementHandler<>).MakeGenericType(requirement.GetType());
37+
var handler = serviceProvider.GetRequiredService(handlerType);
38+
var methodInfo = handler.GetType().GetMethod(nameof(IRequirementHandler<IRequirement>.Handle));
39+
var result = await (Task<AuthorisationResult>)methodInfo.Invoke(handler, new[] { requirement });
40+
if (result.IsAuthorised)
41+
{
42+
logger.LogInformation("{Requirement} has been authorised for {Request}.", requirement.GetType().Name, requestName);
43+
continue;
44+
}
45+
46+
logger.LogWarning("{Requirement} FAILED for {Request}.", requirement.GetType().Name, requestName);
47+
return new TResponse { StatusCode = HttpStatusCode.Unauthorized };
48+
}
49+
50+
logger.LogInformation("{Request} authorisation was successful.", requestName);
51+
return await next();
52+
}
53+
}
54+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
using CQRSTest.Caching;
2+
using CQRSTest.DTOs;
3+
using MediatR;
4+
using Microsoft.Extensions.Caching.Memory;
5+
using Microsoft.Extensions.Logging;
6+
using System.Threading;
7+
using System.Threading.Tasks;
8+
9+
namespace CQRSTest.Behaviours
10+
{
11+
public class CachingBehaviour<TRequest, TResponse> : IPipelineBehavior<TRequest, TResponse>
12+
where TRequest : ICacheable
13+
where TResponse : CQRSResponse
14+
{
15+
private readonly IMemoryCache cache;
16+
private readonly ILogger<CachingBehaviour<TRequest, TResponse>> logger;
17+
public CachingBehaviour(IMemoryCache cache, ILogger<CachingBehaviour<TRequest, TResponse>> logger)
18+
{
19+
this.cache = cache;
20+
this.logger = logger;
21+
}
22+
23+
public async Task<TResponse> Handle(TRequest request, CancellationToken cancellationToken, RequestHandlerDelegate<TResponse> next)
24+
{
25+
var requestName = request.GetType();
26+
logger.LogInformation("{Request} is configured for caching.", requestName);
27+
28+
// Check to see if the item is inside the cache
29+
TResponse response;
30+
if(cache.TryGetValue(request.CacheKey, out response))
31+
{
32+
logger.LogInformation("Returning cached value for {Request}.", requestName);
33+
return response;
34+
}
35+
36+
// Item is not in the cache, execute request and add to cache
37+
logger.LogInformation("{Request} Cache Key: {Key} is not inside the cache, executing request.", requestName, request.CacheKey);
38+
response = await next();
39+
cache.Set(request.CacheKey, response);
40+
return response;
41+
}
42+
}
43+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
using MediatR;
2+
using Microsoft.Extensions.Logging;
3+
using System.Diagnostics;
4+
using System.Threading;
5+
using System.Threading.Tasks;
6+
7+
namespace CQRSTest.Behaviours
8+
{
9+
public class LoggingBehaviour<TRequest, TResponse> : IPipelineBehavior<TRequest, TResponse>
10+
{
11+
private readonly ILogger<LoggingBehaviour<TRequest, TResponse>> logger;
12+
13+
public LoggingBehaviour(ILogger<LoggingBehaviour<TRequest, TResponse>> logger)
14+
{
15+
this.logger = logger;
16+
}
17+
18+
public async Task<TResponse> Handle(TRequest request, CancellationToken cancellationToken, RequestHandlerDelegate<TResponse> next)
19+
{
20+
var requestName = request.GetType();
21+
logger.LogInformation("{Request} is starting.", requestName);
22+
var timer = Stopwatch.StartNew();
23+
var response = await next();
24+
timer.Stop();
25+
logger.LogInformation("{Request} has finished in {Time}ms.", requestName, timer.ElapsedMilliseconds);
26+
return response;
27+
}
28+
}
29+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
using CQRSTest.DTOs;
2+
using CQRSTest.Validation;
3+
using MediatR;
4+
using Microsoft.Extensions.Logging;
5+
using System.Net;
6+
using System.Threading;
7+
using System.Threading.Tasks;
8+
9+
namespace CQRSTest.Behaviours
10+
{
11+
public class ValidationBehaviour<TRequest, TResponse> : IPipelineBehavior<TRequest, TResponse>
12+
where TResponse : CQRSResponse, new()
13+
{
14+
private readonly ILogger<ValidationBehaviour<TRequest, TResponse>> logger;
15+
private readonly IValidationHandler<TRequest> validationHandler;
16+
17+
// Have 2 constructors incase the validator does not exist
18+
public ValidationBehaviour(ILogger<ValidationBehaviour<TRequest, TResponse>> logger)
19+
{
20+
this.logger = logger;
21+
}
22+
23+
public ValidationBehaviour(ILogger<ValidationBehaviour<TRequest, TResponse>> logger, IValidationHandler<TRequest> validationHandler)
24+
{
25+
this.logger = logger;
26+
this.validationHandler = validationHandler;
27+
}
28+
29+
public async Task<TResponse> Handle(TRequest request, CancellationToken cancellationToken, RequestHandlerDelegate<TResponse> next)
30+
{
31+
var requestName = request.GetType();
32+
if (validationHandler == null)
33+
{
34+
logger.LogInformation("{Request} does not have a validation handler configured.", requestName);
35+
return await next();
36+
}
37+
38+
var result = await validationHandler.Validate(request);
39+
if (!result.IsSuccessful)
40+
{
41+
logger.LogWarning("Validation failed for {Request}. Error: {Error}", requestName, result.Error);
42+
return new TResponse { StatusCode = HttpStatusCode.BadRequest, ErrorMessage = result.Error };
43+
}
44+
45+
logger.LogInformation("Validation successful for {Request}.", requestName);
46+
return await next();
47+
}
48+
}
49+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
<Project Sdk="Microsoft.NET.Sdk.Web">
2+
3+
<PropertyGroup>
4+
<TargetFramework>net5.0</TargetFramework>
5+
</PropertyGroup>
6+
7+
<ItemGroup>
8+
<PackageReference Include="MediatR" Version="9.0.0" />
9+
<PackageReference Include="MediatR.Extensions.Microsoft.DependencyInjection" Version="9.0.0" />
10+
<PackageReference Include="Microsoft.AspNetCore.Authentication.JwtBearer" Version="5.0.0" NoWarn="NU1605" />
11+
<PackageReference Include="Microsoft.AspNetCore.Authentication.OpenIdConnect" Version="5.0.0" NoWarn="NU1605" />
12+
<PackageReference Include="Scrutor" Version="3.3.0" />
13+
<PackageReference Include="Swashbuckle.AspNetCore" Version="5.6.3" />
14+
</ItemGroup>
15+
16+
</Project>
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
namespace CQRSTest.Caching
2+
{
3+
public interface ICacheable
4+
{
5+
string CacheKey { get; }
6+
}
7+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
using CQRSTest.Database;
2+
using CQRSTest.Domain;
3+
using CQRSTest.DTOs;
4+
using CQRSTest.Validation;
5+
using MediatR;
6+
using System;
7+
using System.Linq;
8+
using System.Threading;
9+
using System.Threading.Tasks;
10+
11+
namespace CQRSTest.Commands
12+
{
13+
public static class AddTodo
14+
{
15+
// Command
16+
public record Command(string Name) : IRequest<Response>;
17+
18+
// Validator
19+
// Handles all DOMAIN validation
20+
// Does NOT ensure request is formed correctly (E.G. required fields are filled out), that should be in the API layer.
21+
public class Validator : IValidationHandler<Command>
22+
{
23+
private readonly Repository repository;
24+
25+
public Validator(Repository repository) => this.repository = repository;
26+
27+
public async Task<ValidationResult> Validate(Command request)
28+
{
29+
if (repository.Todos.Any(x => x.Name.Equals(request.Name, StringComparison.OrdinalIgnoreCase)))
30+
return ValidationResult.Fail("Todo already exists.");
31+
32+
return ValidationResult.Success;
33+
}
34+
}
35+
36+
// Handler
37+
public class Handler : IRequestHandler<Command, Response>
38+
{
39+
private readonly Repository repository;
40+
41+
public Handler(Repository repository)
42+
{
43+
this.repository = repository;
44+
}
45+
46+
public async Task<Response> Handle(Command request, CancellationToken cancellationToken)
47+
{
48+
repository.Todos.Add(new Todo { Id = 10, Name = request.Name });
49+
return new Response { Id = 10 };
50+
}
51+
}
52+
53+
// Response
54+
public record Response : CQRSResponse
55+
{
56+
public int Id { get; init; }
57+
}
58+
}
59+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
using CQRSTest.Commands;
2+
using CQRSTest.Queries;
3+
using MediatR;
4+
using Microsoft.AspNetCore.Mvc;
5+
using Microsoft.Extensions.Logging;
6+
using System;
7+
using System.Collections.Generic;
8+
using System.Linq;
9+
using System.Threading.Tasks;
10+
11+
namespace CQRSTest.Controllers
12+
{
13+
[ApiController]
14+
public class TodoController : ControllerBase
15+
{
16+
private readonly IMediator mediator;
17+
private readonly ILogger<TodoController> logger;
18+
19+
public TodoController(IMediator mediator, ILogger<TodoController> logger)
20+
{
21+
this.mediator = mediator;
22+
this.logger = logger;
23+
}
24+
25+
[HttpGet("/{id}")]
26+
public async Task<IActionResult> GetTodoById(int id)
27+
{
28+
var response = await mediator.Send(new GetTodoById.Query(id));
29+
return response == null ? NotFound() : Ok(response);
30+
}
31+
32+
[HttpPost("")]
33+
public async Task<IActionResult> AddTodo(AddTodo.Command command)
34+
{
35+
return Ok(await mediator.Send(command));
36+
}
37+
}
38+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
using System.Net;
2+
3+
namespace CQRSTest.DTOs
4+
{
5+
public record CQRSResponse
6+
{
7+
public HttpStatusCode StatusCode { get; init; } = HttpStatusCode.OK;
8+
public string ErrorMessage { get; init; }
9+
}
10+
}

0 commit comments

Comments
 (0)