Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
40 changes: 40 additions & 0 deletions SampleHere/LEARNINGS.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
# Transitioning from MediatR to WolverineFx in a Clean Architecture Solution

This document outlines the learnings and steps taken to transition from MediatR to WolverineFx in a standard Clean Architecture solution.

## Key Learnings

1. **Dependency Injection:**
- MediatR requires explicit registration, e.g., `services.AddMediatR(...)`.
- WolverineFx configures itself on the `IHostBuilder` (or `IWebHostBuilder`), which is typically done in `Program.cs` via `builder.Host.UseWolverine(opts => { ... })`.

2. **Message Models (Commands and Queries):**
- In MediatR, command and query messages inherit from marker interfaces like `IRequest<T>` or `IRequest`.
- WolverineFx does not require strict marker interfaces. You can just pass plain POCO classes. We removed `IRequest<T>` and `IRequest` inheritance from our commands and queries.

3. **Message Handlers:**
- MediatR handlers implement `IRequestHandler<TRequest, TResponse>`.
- WolverineFx uses conventions. Any class ending with `Handler` and containing a `Handle` or `HandleAsync` method where the first argument is the message type is automatically discovered. We removed the `IRequestHandler` inheritance.
- Importantly, if handlers are located in a different assembly than where Wolverine is configured (e.g., Application project vs Api project), you must explicitly tell Wolverine to scan it:
```csharp
opts.Discovery.IncludeAssembly(typeof(Application.DependencyInjection).Assembly);
```

4. **Middleware (Behaviours):**
- MediatR uses pipeline behaviors implementing `IPipelineBehavior<TRequest, TResponse>`. These often wrap the entire handler invocation.
- WolverineFx encourages conventional middleware. You create static classes with `Before`, `BeforeAsync`, `After`, `AfterAsync`, `Finally`, or `FinallyAsync` methods.
- Wolverine injects required dependencies (like `ILogger`, `IConfiguration`, etc.) directly into these static methods.
- You can access the message being handled via the `Envelope envelope` parameter.
- For error handling, a `Finally` method can receive an `Exception? exception` parameter, which is null if no exception occurred. However, note that if you don't need the exception, you don't need to specify it.
- Middleware is registered in Wolverine options using `opts.Policies.AddMiddleware(typeof(YourMiddleware))`.
- Middleware can be scoped to specific message types using constraints, e.g.:
```csharp
opts.Policies.AddMiddleware(typeof(UnitOfWorkBehaviour), chain => typeof(ICommand).IsAssignableFrom(chain.MessageType));
```
- For Validation, Wolverine provides the `WolverineFx.FluentValidation` package and an `opts.UseFluentValidation()` option which automatically hooks up validation, replacing the need for a custom validation pipeline behavior.

5. **Dispatching Messages:**
- Instead of injecting MediatR's `ISender` (or `IMediator`) and calling `_mediator.Send(command)`, Wolverine uses `IMessageBus` and you call `_bus.InvokeAsync<TResult>(command)`.

## Conclusion
WolverineFx simplifies the application by removing boilerplate interfaces and focusing on standard C# conventions. Its middleware approach (using static classes and specific method names) allows for clear injection points and highly performant execution pipelines compared to traditional wrapper pipelines. However, one must pay attention to assembly discovery and middleware scoping to ensure handlers are found and middleware isn't applied indiscriminately.
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
using System.Net.Mime;
using Intent.RoslynWeaver.Attributes;
using MediatR;
using Wolverine;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using WolverineToBe.Api.Controllers.ResponseTypes;
Expand All @@ -20,11 +20,11 @@ namespace WolverineToBe.Api.Controllers
[ApiController]
public class OrdersController : ControllerBase
{
private readonly ISender _mediator;
private readonly IMessageBus _bus;

public OrdersController(ISender mediator)
public OrdersController(IMessageBus bus)
{
_mediator = mediator ?? throw new ArgumentNullException(nameof(mediator));
_bus = bus ?? throw new ArgumentNullException(nameof(bus));
}

/// <summary>
Expand All @@ -40,7 +40,7 @@ public async Task<ActionResult<JsonResponse<Guid>>> CreateOrder(
[FromBody] CreateOrderCommand command,
CancellationToken cancellationToken = default)
{
var result = await _mediator.Send(command, cancellationToken);
var result = await _bus.InvokeAsync<Guid>(command, cancellationToken);
return CreatedAtAction(nameof(GetOrderById), new { id = result }, new JsonResponse<Guid>(result));
}

Expand All @@ -56,7 +56,7 @@ public async Task<ActionResult<JsonResponse<Guid>>> CreateOrder(
[ProducesResponseType(typeof(ProblemDetails), StatusCodes.Status500InternalServerError)]
public async Task<ActionResult> DeleteOrder([FromRoute] Guid id, CancellationToken cancellationToken = default)
{
await _mediator.Send(new DeleteOrderCommand(id: id), cancellationToken);
await _bus.InvokeAsync(new DeleteOrderCommand(id: id), cancellationToken);
return Ok();
}

Expand Down Expand Up @@ -85,7 +85,7 @@ public async Task<ActionResult> UpdateOrder(
return BadRequest();
}

await _mediator.Send(command, cancellationToken);
await _bus.InvokeAsync(command, cancellationToken);
return NoContent();
}

Expand All @@ -103,7 +103,7 @@ public async Task<ActionResult<OrderDto>> GetOrderById(
[FromRoute] Guid id,
CancellationToken cancellationToken = default)
{
var result = await _mediator.Send(new GetOrderByIdQuery(id: id), cancellationToken);
var result = await _bus.InvokeAsync<OrderDto>(new GetOrderByIdQuery(id: id), cancellationToken);
return result == null ? NotFound() : Ok(result);
}

Expand All @@ -115,8 +115,8 @@ public async Task<ActionResult<OrderDto>> GetOrderById(
[ProducesResponseType(typeof(ProblemDetails), StatusCodes.Status500InternalServerError)]
public async Task<ActionResult<List<OrderDto>>> GetOrders(CancellationToken cancellationToken = default)
{
var result = await _mediator.Send(new GetOrdersQuery(), cancellationToken);
var result = await _bus.InvokeAsync<List<OrderDto>>(new GetOrdersQuery(), cancellationToken);
return Ok(result);
}
}
}
}
Original file line number Diff line number Diff line change
@@ -1,10 +1,13 @@
using Intent.RoslynWeaver.Attributes;
using Serilog;
using Serilog.Events;
using Wolverine;
using Wolverine.FluentValidation;
using WolverineToBe.Api.Configuration;
using WolverineToBe.Api.Filters;
using WolverineToBe.Api.Logging;
using WolverineToBe.Application;
using WolverineToBe.Application.Common.Behaviours;
using WolverineToBe.Infrastructure;

[assembly: DefaultIntentManaged(Mode.Fully)]
Expand Down Expand Up @@ -32,6 +35,18 @@ public static void Main(string[] args)
.ReadFrom.Services(services)
.Destructure.With(new BoundedLoggingDestructuringPolicy()));

builder.Host.UseWolverine(opts =>
{
opts.Discovery.IncludeAssembly(typeof(WolverineToBe.Application.DependencyInjection).Assembly);

opts.Policies.AddMiddleware(typeof(UnhandledExceptionBehaviour));
opts.Policies.AddMiddleware(typeof(PerformanceBehaviour));
opts.Policies.AddMiddleware(typeof(LoggingBehaviour));
opts.Policies.AddMiddleware(typeof(AuthorizationBehaviour));
opts.Policies.AddMiddleware(typeof(UnitOfWorkBehaviour), chain => typeof(WolverineToBe.Application.Common.Interfaces.ICommand).IsAssignableFrom(chain.MessageType));
opts.UseFluentValidation();
});

builder.Services.AddControllers(
opt =>
{
Expand Down Expand Up @@ -74,4 +89,4 @@ public static void Main(string[] args)
}
}
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -21,5 +21,7 @@
<PackageReference Include="Microsoft.AspNetCore.Authentication.JwtBearer" Version="10.0.2" />
<PackageReference Include="Serilog.AspNetCore" Version="10.0.0" />
<PackageReference Include="Swashbuckle.AspNetCore" Version="9.0.6" />
<PackageReference Include="WolverineFx" Version="5.21.0" />
<PackageReference Include="WolverineFx.FluentValidation" Version="5.21.0" />
</ItemGroup>
</Project>
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
using System.Reflection;
using Intent.RoslynWeaver.Attributes;
using MediatR;
using Wolverine;
using WolverineToBe.Application.Common.Exceptions;
using WolverineToBe.Application.Common.Interfaces;
using WolverineToBe.Application.Common.Security;
Expand All @@ -10,27 +10,21 @@

namespace WolverineToBe.Application.Common.Behaviours
{
public class AuthorizationBehaviour<TRequest, TResponse> : IPipelineBehavior<TRequest, TResponse>
where TRequest : notnull
public static class AuthorizationBehaviour
{
private readonly ICurrentUserService _currentUserService;

public AuthorizationBehaviour(ICurrentUserService currentUserService)
public static async Task BeforeAsync(
ICurrentUserService currentUserService,
Envelope envelope)
{
_currentUserService = currentUserService;
}
var request = envelope.Message;
if (request == null) return;

public async Task<TResponse> Handle(
TRequest request,
RequestHandlerDelegate<TResponse> next,
CancellationToken cancellationToken)
{
var authorizeAttributes = request.GetType().GetCustomAttributes<AuthorizeAttribute>();

foreach (var authorizeAttribute in authorizeAttributes)
{
// Must be an authenticated user
if (await _currentUserService.GetAsync() is null)
if (await currentUserService.GetAsync() is null)
{
throw new UnauthorizedAccessException();
}
Expand All @@ -43,7 +37,7 @@ public async Task<TResponse> Handle(

foreach (var role in roles)
{
var isInRole = await _currentUserService.IsInRoleAsync(role);
var isInRole = await currentUserService.IsInRoleAsync(role);
if (isInRole)
{
authorized = true;
Expand All @@ -66,7 +60,7 @@ public async Task<TResponse> Handle(

foreach (var policy in policies)
{
var isAuthorized = await _currentUserService.AuthorizeAsync(policy);
var isAuthorized = await currentUserService.AuthorizeAsync(policy);
if (isAuthorized)
{
authorized = true;
Expand All @@ -81,9 +75,6 @@ public async Task<TResponse> Handle(
}
}
}

// User is authorized / authorization not required
return await next(cancellationToken);
}
}
}
}
Original file line number Diff line number Diff line change
@@ -1,43 +1,34 @@
using Intent.RoslynWeaver.Attributes;
using MediatR.Pipeline;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.Logging;
using WolverineToBe.Application.Common.Interfaces;
using Wolverine;

[assembly: DefaultIntentManaged(Mode.Fully)]
[assembly: IntentTemplate("Intent.Application.MediatR.Behaviours.LoggingBehaviour", Version = "1.0")]

namespace WolverineToBe.Application.Common.Behaviours
{
public class LoggingBehaviour<TRequest> : IRequestPreProcessor<TRequest>
where TRequest : notnull
public static class LoggingBehaviour
{
private readonly ILogger<LoggingBehaviour<TRequest>> _logger;
private readonly ICurrentUserService _currentUserService;
private readonly bool _logRequestPayload;

public LoggingBehaviour(ILogger<LoggingBehaviour<TRequest>> logger,
public static async Task BeforeAsync(
ILogger logger,
ICurrentUserService currentUserService,
IConfiguration configuration)
{
_logger = logger;
_currentUserService = currentUserService;
_logRequestPayload = configuration.GetValue<bool?>("CqrsSettings:LogRequestPayload") ?? false;
}

public async Task Process(TRequest request, CancellationToken cancellationToken)
IConfiguration configuration,
Envelope envelope)
{
var requestName = typeof(TRequest).Name;
var user = await _currentUserService.GetAsync();
var requestName = envelope.Message?.GetType().Name ?? "Unknown";
var user = await currentUserService.GetAsync();
var logRequestPayload = configuration.GetValue<bool?>("CqrsSettings:LogRequestPayload") ?? false;

if (_logRequestPayload)
if (logRequestPayload)
{
_logger.LogInformation("WolverineToBe Request: {Name} {@UserId} {@UserName} {@Request}", requestName, user?.Id, user?.Name, request);
logger.LogInformation("WolverineToBe Request: {Name} {@UserId} {@UserName} {@Request}", requestName, user?.Id, user?.Name, envelope.Message);
}
else
{
_logger.LogInformation("WolverineToBe Request: {Name} {@UserId} {@UserName}", requestName, user?.Id, user?.Name);
logger.LogInformation("WolverineToBe Request: {Name} {@UserId} {@UserName}", requestName, user?.Id, user?.Name);
}
}
}
}
}
Original file line number Diff line number Diff line change
@@ -1,61 +1,50 @@
using System.Diagnostics;
using Intent.RoslynWeaver.Attributes;
using MediatR;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.Logging;
using WolverineToBe.Application.Common.Interfaces;
using Wolverine;

[assembly: DefaultIntentManaged(Mode.Fully)]
[assembly: IntentTemplate("Intent.Application.MediatR.Behaviours.PerformanceBehaviour", Version = "1.0")]

namespace WolverineToBe.Application.Common.Behaviours
{
public class PerformanceBehaviour<TRequest, TResponse> : IPipelineBehavior<TRequest, TResponse>
where TRequest : notnull
public static class PerformanceBehaviour
{
private readonly ILogger<PerformanceBehaviour<TRequest, TResponse>> _logger;
private readonly ICurrentUserService _currentUserService;
private readonly Stopwatch _timer;
private readonly bool _logRequestPayload;

public PerformanceBehaviour(ILogger<PerformanceBehaviour<TRequest, TResponse>> logger,
ICurrentUserService currentUserService,
IConfiguration configuration)
public static Stopwatch Before()
{
_timer = new Stopwatch();
_logger = logger;
_currentUserService = currentUserService;
_logRequestPayload = configuration.GetValue<bool?>("CqrsSettings:LogRequestPayload") ?? false;
var timer = new Stopwatch();
timer.Start();
return timer;
}

public async Task<TResponse> Handle(
TRequest request,
RequestHandlerDelegate<TResponse> next,
CancellationToken cancellationToken)
public static async Task FinallyAsync(
Stopwatch timer,
ILogger logger,
ICurrentUserService currentUserService,
IConfiguration configuration,
Envelope envelope)
{
_timer.Start();

var response = await next(cancellationToken);

_timer.Stop();
timer.Stop();

var elapsedMilliseconds = _timer.ElapsedMilliseconds;
var elapsedMilliseconds = timer.ElapsedMilliseconds;

if (elapsedMilliseconds > 500)
{
var requestName = typeof(TRequest).Name;
var user = await _currentUserService.GetAsync();
var requestName = envelope.Message?.GetType().Name ?? "Unknown";
var user = await currentUserService.GetAsync();
var logRequestPayload = configuration.GetValue<bool?>("CqrsSettings:LogRequestPayload") ?? false;

if (_logRequestPayload)
if (logRequestPayload)
{
_logger.LogWarning("WolverineToBe Long Running Request: {Name} ({ElapsedMilliseconds} milliseconds) {@UserId} {@UserName} {@Request}", requestName, elapsedMilliseconds, user?.Id, user?.Name, request);
logger.LogWarning("WolverineToBe Long Running Request: {Name} ({ElapsedMilliseconds} milliseconds) {@UserId} {@UserName} {@Request}", requestName, elapsedMilliseconds, user?.Id, user?.Name, envelope.Message);
}
else
{
_logger.LogWarning("WolverineToBe Long Running Request: {Name} ({ElapsedMilliseconds} milliseconds) {@UserId} {@UserName}", requestName, elapsedMilliseconds, user?.Id, user?.Name);
logger.LogWarning("WolverineToBe Long Running Request: {Name} ({ElapsedMilliseconds} milliseconds) {@UserId} {@UserName}", requestName, elapsedMilliseconds, user?.Id, user?.Name);
}
}
return response;
}
}
}
}
Loading