ConductR is an opinionated lightweight CQRS-ish mediator library built with dependency injection in mind.
As a heavy user of Jimmy Bogards infamous MediatR library, I was dissatisfied with it's lacking support of streaming via IAsyncEnumerable<T>. Also I favor the possibility to conform to the CQRS pattern cleanly by separating commands from queries.
So when I read Cezary Piątek's blog post Why I don't use MediatR for CQRS I wanted to try and build a lightweight CQRS-ish mediator library myself, that doesn't do much magic, but rather utilizes well established patterns and practices like ìnversion of control, decorator pattern etc.
Since I either use Microsoft.Extensions.DependencyInjection or Autofac as DI container frameworks, packages for those two are already included via ConductR.Microsoft and ConductR.Autofac. If you feel like contributing a package for another container framework (or anything else), I will happily review/accept your PR :)
Mediator, CQRS and domain use-cases that are currently supported out of the box are:
-
Commands (
ICommand,ICommandHandler<TCommand, TResult>)Mutating (writing) requests, that can be handled asynchronously, returning a result
-
Queries (
IQuery,IQueryHandler<TQuery, TResult>)Non-mutating (read-only) requests, that can be handled asynchronously, returning a result
-
Streams (
IStreamQuery,IStreamHandler<TStreamQuery, TResult)Non-mutating (read-only) requests, that can be handled asynchronously, returning a stream of results
-
Events (
IEvent,IEventHandler<TEvent>)Domain events that can be handled asynchronously
-
Interceptors (
CommandInterceptor,QueryInterceptor,StreamInterceptor,EventInterceptor)Middleware classes, that can intercept commands, queries, streams (up to individual item level) and events, that can perform various tasks with the input and/or output, forming a pipeline-ish construct. Usual use-cases include input validation, logging, authorization, etc.
dotnet add package ConductR.Microsoftdotnet add package ConductR.Autofacbuilder.Services.AddConductR(typeof(CreateGreetingCommand));builder.Host.ConfigureContainer<ContainerBuilder>(builder =>
builder.AddConductR(typeof(CreateGreetingCommand));The type parameter is used to select the assembly containing your Commands, Queries, Handlers, etc.
public record CreateGreetingCommand(string Name) : ICommand;
public record Greeting(string Phrase);
public class CreateGreetingHandler : ICommandHandler<CreateGreetingCommand, Greeting>
{
private readonly IGreetingGenerator greetingGenerator;
public CreateGreetingHandler(IGreetingGenerator greetingGenerator)
=> this.greetingGenerator = greetingGenerator;
public async ValueTask<Greeting> HandleAsync(CreateGreetingCommand command, CancellationToken token = default)
{
var phrase = await greetingGenerator.GenerateAsync(command.Name, token);
return new Greeting(phrase);
}
}app.MapPost("/greeting", async (IConductor conductor, string name, CancellationToken token) =>
await conductor.CommandAsync<CreateGreetingCommand, Greeting>(new(name), token));Features like building a pipeline via interceptors, configuring the DI service lifetime, etc. can be found in the samples area of the repository.