CaseR 0.1.0
See the version list below for details.
dotnet add package CaseR --version 0.1.0
NuGet\Install-Package CaseR -Version 0.1.0
<PackageReference Include="CaseR" Version="0.1.0" />
<PackageVersion Include="CaseR" Version="0.1.0" />
<PackageReference Include="CaseR" />
paket add CaseR --version 0.1.0
#r "nuget: CaseR, 0.1.0"
#:package CaseR@0.1.0
#addin nuget:?package=CaseR&version=0.1.0
#tool nuget:?package=CaseR&version=0.1.0
CaseR
CaseR is not another MediatR clone, but tries to solve the same problem in a different way (in context .NET 10).
The main task of this library is to model use cases in an application and separate cross-cutting concerns (like logging, caching, monitoring, transactions,...) and support vertical slice architecture in ASP.NET Core 8+ applications, even with support for AOT compilation.
CaseR is a conceptual project.
The following criticism does not only apply to the MediatR library, but also to its clones and some similar libraries. In my opinion, MediatR showed a good direction, which was due to the time of its creation. The CaseR project tries to show a different principle but solve the same problems.
I tried to solve mainly these problems:
- Interface
IMediator
is too generic, it's like injecting anIServiceProvider
, so class dependencies or MinimalAPI don't make it clear what the class requires. - In a project using a mediator, it is difficult to navigate because it is not easy to get to the handler implementation.
- MediatR is not type-safe in compile time. It is possible to call
IMediator.Send()
with a request for which there is no handler. - The necessity to use
IRequest<>
andIResponse
, we understand why these interfaces are in MediatR, but it bothers me a bit.
After a few projects where I used MediatR I realized a few things. Developers actually use MediatR to implement their use cases. There is no CQRS support, this arises naturally by having each HTTP request implemented in a separate class. It also doesn't directly implement the message queue either.
Therefore, I decided to create a library that uses the correct terminology for Use Case (and interactor from Clean Architecture).
Key Features
- Modeling use cases
- Built entirely on top of Dependency Injection
- Zero runtime reflection after registration
- CaseR.SourceGenerator eliminate reflection at registration
- Direct code reference to business logic (no problem with trimming and orientation in codebase - F12 work)
- Compile-time type safety (using generic constraints and source generator)
- Interaction interceptor pipeline
- Supports keyed pipelines
- Supports domain events publisher
Get started
1. Install nugets
Install nugets into project using:
dotnet add package CaseR
dotnet add package CaseR.SourceGenerator
2. Register CaseR services
builder.Services.AddCaseR();
builder.Services.AddCaseRInteractors();
3. Create interactor
public record GetTodoInteractorRequest();
public record Todo(int Id, string? Title, DateOnly? DueBy = null, bool IsComplete = false);
public class GetTodoInteractor : IUseCaseInterceptor<GetTodoInteractorRequest, Todo[]>
{
public GetTodoInteractor()
{
}
public ValueTask<Todo[]> InterceptExecution(GetTodoInteractorRequest request, CancellationToken cancellationToken)
{
Todo[] sampleTodos = new Todo[]
{
new Todo(1, "Walk the dog"),
new Todo(2, "Do the dishes", DateOnly.FromDateTime(DateTime.Now)),
new Todo(3, "Do the laundry", DateOnly.FromDateTime(DateTime.Now.AddDays(1))),
new Todo(4, "Clean the bathroom"),
new Todo(5, "Clean the car", DateOnly.FromDateTime(DateTime.Now.AddDays(2)))
};
return new ValueTask<Todo[]>(sampleTodos);
}
}
4. Use use case in minimal API
var todosApi = app.MapGroup("/todos");
todosApi.MapGet("/", async (IUseCase<GetTodoInteractor> getTodoInteractor,
CancellationToken cancellationToken) =>
{
WebAppExample.Todo.UseCases.Todo[] todos = await getTodoInteractor.Execute(new GetTodoInteractorRequest(), cancellationToken);
return todos;
});
Domain events
1. Create domain event
public record GetTodoItemInteractorEvent(int Id) : IDomainEvent;
2. Create domain event handlers
public class GetTodoItemInteractorEventHandler : IDomainEventHandler<GetTodoItemInteractorEvent>
{
public ValueTask Handle(GetTodoItemInteractorEvent domainEvent, CancellationToken cancellationToken)
{
//...
return ValueTask.CompletedTask;
}
}
3. Publish event
Inject IDomainEventPublisher
into your interactor or handler and use it to publish events.
IDomainEventPublisher domainEventPublisher
Publish event:
await this.domainEventPublisher.Publish(new GetTodoItemInteractorEvent(request), cancellationToken);
Use Case interceptors
Interceptors are used to create a pipeline that wraps the interactor call.
public class ElapsedTimeInterceptor<TRequest, TResponse> : IUseCaseInterceptor<TRequest, TResponse>
{
private readonly ILogger<ElapsedTimeInterceptor<TRequest, TResponse>> logger;
public ElapsedTimeInterceptor(ILogger<ElapsedTimeInterceptor<TRequest, TResponse>> logger)
{
this.logger = logger;
}
public async ValueTask<TResponse> InterceptExecution(IUseCaseInteractor<TRequest, TResponse> useCaseInteractor, TRequest request, UseCasePerformDelegate<TRequest, TResponse> next, CancellationToken cancellationToken)
{
long timestamp = Stopwatch.GetTimestamp();
try
{
return await next(request).ConfigureAwait(false);
}
finally
{
TimeSpan elapsedTime = Stopwatch.GetElapsedTime(timestamp);
this.logger.LogCritical("Elapsed {ElapsedTime}ms in {InteractorName}.", elapsedTime.TotalMilliseconds, useCaseInteractor.GetType().Name);
}
}
}
And register interceptors in registration:
builder.Services.AddCaseR(options =>
{
options.AddGenericInterceptor(typeof(ElapsedTimeInterceptor<,>));
// register another interceptors
});
builder.Services.AddCaseRInteractors();
Or use keyed piplines:
builder.Services.AddKeyedCaseR("GrpcPipeline", options =>
{
options.AddGenericInterceptor(typeof(AnotherInterceptor<,>));
options.AddGenericInterceptor(typeof(nestedLogInterceptor<,>));
});
builder.Services.AddKeyedCaseR("MinimalApiPipeline", options =>
{
options.AddGenericInterceptor(typeof(AnotherInterceptor<,>));
options.AddGenericInterceptor(typeof(nestedLogInterceptor<,>));
});
Usage:
var todosApi = app.MapGroup("/todos");
todosApi.MapGet("/", async ([FromKeyedServices("MinimalApiPipeline")] IUseCase<GetTodoInteractor> getTodoInteractor,
CancellationToken cancellationToken) =>
{
WebAppExample.Todo.UseCases.Todo[] todos = await getTodoInteractor.Execute(new GetTodoInteractorRequest(), cancellationToken);
return todos;
});
CaseR
The CaseR library contains all the logic and can be used without a source generator.
However, it has two limitations:
- Registering interactors is done using reflection
builder.Services.AddCaseRInteractors(typeof(Program));
, - In the execute method, generic parameters must be passed when calling
await useCase.Execute<TInteractor, TRequest, TResponse>(request, cancellationToken);
.
CaseR.SourceGenerator
The CaseR.SourceGenerator library is a source generator that generates the necessary code for the CaseR library to work without reflection
and typed Execute
method.
Product | Versions Compatible and additional computed target framework versions. |
---|---|
.NET | net8.0 is compatible. net8.0-android was computed. net8.0-browser was computed. net8.0-ios was computed. net8.0-maccatalyst was computed. net8.0-macos was computed. net8.0-tvos was computed. net8.0-windows was computed. net9.0 is compatible. net9.0-android was computed. net9.0-browser was computed. net9.0-ios was computed. net9.0-maccatalyst was computed. net9.0-macos was computed. net9.0-tvos was computed. net9.0-windows was computed. net10.0 was computed. net10.0-android was computed. net10.0-browser was computed. net10.0-ios was computed. net10.0-maccatalyst was computed. net10.0-macos was computed. net10.0-tvos was computed. net10.0-windows was computed. |
-
net8.0
- Microsoft.Extensions.DependencyInjection.Abstractions (>= 8.0.0 && < 9.0.0)
-
net9.0
- Microsoft.Extensions.DependencyInjection.Abstractions (>= 9.0.0 && < 10.0.0)
NuGet packages
This package is not used by any NuGet packages.
GitHub repositories
This package is not used by any popular GitHub repositories.