A powerful, lightweight, and extensible implementation of the Mediator pattern and Command Query Responsibility Segregation (CQRS) for .NET applications.
Arbiter is designed for building clean, modular architectures like Vertical Slice Architecture and CQRS. It provides a comprehensive suite of libraries that work together to simplify complex application patterns while maintaining high performance and flexibility.
- Clean Architecture: Perfect for Vertical Slice Architecture and CQRS patterns
- High Performance: Minimal overhead with efficient mediator implementation
- Extensible: Pipeline behaviors, custom handlers, and extensive customization options
- Observable: Built-in OpenTelemetry support for tracing and metrics
- Database Agnostic: Support for Entity Framework Core, MongoDB, and more
- Web Ready: Minimal API endpoints and MVC controller support
- Communication: Integrated email and SMS messaging capabilities
- Blazor Ready: First-class Dispatcher support for Blazor Auto, Server Interactive, and WebAssembly render modes
- Quick Start
- Packages
- Core Libraries
- Data Providers
- Web Integration
- Blazor Dispatcher
- Documentation
- Samples
- Contributing
- License
Get started with Arbiter in just a few steps:
dotnet add package Arbiter.Mediation// Define your requestpublicclassGetUserQuery:IRequest<User>{publicintUserId{get;set;}}// Implement the handlerpublicclassGetUserHandler:IRequestHandler<GetUserQuery,User>{publicasyncValueTask<User?>Handle(GetUserQueryrequest,CancellationTokencancellationToken){// Your business logic herereturnawaitGetUserFromDatabase(request.UserId);}}services.AddMediator();services.AddTransient<IRequestHandler<GetUserQuery,User>,GetUserHandler>();publicclassUserController:ControllerBase{privatereadonlyIMediator_mediator;publicUserController(IMediatormediator)=>_mediator=mediator;[HttpGet("{id}")]publicasyncTask<User>GetUser(intid){returnawait_mediator.Send(newGetUserQuery{UserId=id});}}| Library | Package | Description |
|---|---|---|
| Arbiter.Mediation | Lightweight and extensible implementation of the Mediator pattern | |
| Arbiter.Queue | Background request queue for mediator commands | |
| Arbiter.CommandQuery | Base package for Commands, Queries and Behaviors | |
| Arbiter.Mapping | Source-generated, compile-time object mapping | |
| Arbiter.Services | Utility services for CSV, encryption, caching, and tokens | |
| Arbiter.Communication | Message template communication for email and SMS services |
| Library | Package | Description |
|---|---|---|
| Arbiter.CommandQuery.EntityFramework | Entity Framework Core handlers for the base Commands and Queries | |
| Arbiter.CommandQuery.MongoDB | MongoDB handlers for the base Commands and Queries |
| Library | Package | Description |
|---|---|---|
| Arbiter.CommandQuery.Endpoints | Minimal API endpoints for base Commands and Queries | |
| Arbiter.CommandQuery.Mvc | MVC Controllers for base Commands and Queries |
| Library | Package | Description |
|---|---|---|
| Arbiter.Dispatcher.Server | ASP.NET Core endpoint that receives dispatcher messages from Blazor WASM clients | |
| Arbiter.Dispatcher.Client | Client-side dispatcher for Blazor: JSON/MessagePack (WASM) and ServerDispatcher |
| Library | Package | Description |
|---|---|---|
| Arbiter.Communication.Azure | Communication implementation for Azure Communication Services | |
| Arbiter.Communication.Graph | Communication implementation for Microsoft Graph email delivery | |
| Arbiter.Communication.Twilio | Communication implementation for SendGrid and Twilio |
A lightweight and extensible implementation of the Mediator pattern for .NET applications, designed for clean, modular architectures like Vertical Slice Architecture and CQRS.
- Request/Response Pattern: Handle requests with typed responses using
IRequest<TResponse>andIRequestHandler<TRequest, TResponse> - Notifications/Events: Publish events using
INotificationandINotificationHandler<TNotification> - Pipeline Behaviors: Middleware-like cross-cutting concerns using
IPipelineBehavior<TRequest, TResponse> - Dependency Injection: Seamless integration with .NET's DI container
- High Performance: Minimal allocations and efficient execution
- OpenTelemetry Ready: Built-in observability support
dotnet add package Arbiter.Mediation1. Define Request and Response
publicclassPing:IRequest<Pong>{publicstring?Message{get;set;}}publicclassPong{publicstring?Message{get;set;}}2. Implement Handler
publicclassPingHandler:IRequestHandler<Ping,Pong>{publicasyncValueTask<Pong?>Handle(Pingrequest,CancellationTokencancellationToken=default){// Simulate async workawaitTask.Delay(100,cancellationToken);returnnewPong{Message=$"{request.Message} Pong"};}}3. Define Pipeline Behavior (Optional)
publicclassLoggingBehavior<TRequest,TResponse>:IPipelineBehavior<TRequest,TResponse>whereTRequest:IRequest<TResponse>{privatereadonlyILogger<LoggingBehavior<TRequest,TResponse>>_logger;publicLoggingBehavior(ILogger<LoggingBehavior<TRequest,TResponse>>logger){_logger=logger;}publicasyncValueTask<TResponse>Handle(TRequestrequest,RequestHandlerDelegate<TResponse>next,CancellationTokencancellationToken=default){_logger.LogInformation("Handling {RequestType}",typeof(TRequest).Name);varresponse=awaitnext(cancellationToken);_logger.LogInformation("Handled {RequestType}",typeof(TRequest).Name);returnresponse;}}4. Register Services
// Register Mediator servicesservices.AddMediator();// Register handlersservices.AddTransient<IRequestHandler<Ping,Pong>,PingHandler>();// Register pipeline behaviors (optional)services.AddTransient(typeof(IPipelineBehavior<,>),typeof(LoggingBehavior<,>));5. Use in Controllers
[ApiController][Route("api/[controller]")]publicclassPingController:ControllerBase{privatereadonlyIMediator_mediator;publicPingController(IMediatormediator)=>_mediator=mediator;[HttpGet]publicasyncTask<ActionResult<Pong>>Get([FromQuery]string?message=null,CancellationTokencancellationToken=default){varrequest=newPing{Message=message??"Hello"};varresponse=await_mediator.Send(request,cancellationToken);returnOk(response);}}5. OpenTelemetry Integration (Optional)
The mediator provides built-in support for OpenTelemetry tracing and metrics:
usingArbiter.Mediation;services.AddOpenTelemetry().WithTracing(tracing =>tracing.AddSource(MediatorTelemetry.SourceName).AddAspNetCoreInstrumentation().AddConsoleExporter()).WithMetrics(metrics =>metrics.AddMeter(MediatorTelemetry.MeterName).AddAspNetCoreInstrumentation().AddConsoleExporter());Background request queue for mediator commands. Queued work is represented as an IRequest<Unit> and processed through IMediator, so request handlers and pipeline behaviors continue to apply.
dotnet add package Arbiter.Queueservices.AddBackgroundQueue();services.AddTransient<IRequestHandler<SendWelcomeEmail,Unit>,SendWelcomeEmailHandler>();awaitbackgroundQueue.Enqueue(newSendWelcomeEmail(userId),cancellationToken);Arbiter.Messaging.ServiceBus also provides a durable Service Bus-backed IBackgroundQueue implementation through AddServiceBusBackgroundQueue(...). ServiceBusBackgroundQueue enqueues serialized mediator requests, and ServiceBusBackgroundService processes those requests from either the hosted AddServiceBusBackgroundProcessor(...) worker or an Azure Functions Service Bus trigger.
A comprehensive Command Query Responsibility Segregation (CQRS) framework built on top of the mediator pattern.
- CQRS Implementation: Clear separation between commands and queries
- Pre-built Operations: Common CRUD operations out of the box
- Generic Handlers: Reusable handlers for typical data operations
- Smart Behaviors: Hybrid caching, auditing, validation, and soft delete support
- Source-Generated Mapping: Compile-time object mapping via
Arbiter.Mappingwith[GenerateMapper]attribute - Enhanced Querying: Powerful filter, sort, and pagination support with type-safe operators
- Multi-tenancy Ready: Built-in tenant isolation support
- Unified Query System: Single
EntityQueryclass handles both paged and non-paged scenarios - Flexible Filtering: Support for complex filter expressions with multiple operators and logic combinations
dotnet add package Arbiter.CommandQueryservices.AddCommandQuery();The library provides several pre-built commands and queries for common operations:
Entity Queries:
EntityIdentifierQuery<TKey, TReadModel>- Get entity by IDEntityIdentifiersQuery<TKey, TReadModel>- Get multiple entities by IDsEntityPagedQuery<TReadModel>- Queries both paged and non-paged scenarios with filtering and sorting
Entity Commands:
EntityCreateCommand<TKey, TCreateModel, TReadModel>- Create new entitiesEntityUpdateCommand<TKey, TUpdateModel, TReadModel>- Update existing entities (includes upsert)EntityPatchCommand<TKey, TReadModel>- Partial updates to entitiesEntityDeleteCommand<TKey, TReadModel>- Delete entities
Query by ID:
varquery=newEntityIdentifierQuery<int,ProductReadModel>(principal,123);varresult=awaitmediator.Send(query);Query with Filtering and Sorting:
varfilters=newList<EntityFilter>{newEntityFilter{Name="Status",Operator=FilterOperators.Equal,Value="Active"},newEntityFilter{Name="Price",Operator=FilterOperators.GreaterThan,Value=10.00m}};varsorts=newList<EntitySort>{newEntitySort{Name="Name",Direction=SortDirections.Ascending}};varquery=newEntityQuery{Filter=newEntityFilter{Filters=filters},Sort=sorts};// no page or page size will return all matchesvarcommand=newEntityPagedQuery<ProductReadModel>(principal,query);varresult=awaitmediator.Send(command);Paginated Query:
varentityQuery=newEntityQuery{Filter=newEntityFilter{Name="Category",Operator=FilterOperators.Equal,Value="Electronics"},Sort=newList<EntitySort>{newEntitySort{Name="CreatedDate",Direction=SortDirections.Descending}},Page=1,PageSize=20};varquery=newEntityPagedQuery<ProductReadModel>(principal,entityQuery);varresult=awaitmediator.Send(query);Update Command:
varupdateModel=newProductUpdateModel{Name="Updated Product",Description="Updated description",Price=29.99m};varcommand=newEntityUpdateCommand<int,ProductUpdateModel,ProductReadModel>(principal,123,updateModel);varresult=awaitmediator.Send(command);Complex Filter Logic:
varcomplexEntityQuery=newEntityQuery{Filter=newEntityFilter{Filters=newList<EntityFilter>{newEntityFilter{Name="Category",Operator=FilterOperators.In,Value=new[]{"Electronics","Computers"}},newEntityFilter{Name="Price",Operator=FilterOperators.GreaterThanOrEqual,Value=100.00m},newEntityFilter{Name="Name",Operator=FilterOperators.Contains,Value="Gaming"}},Logic=FilterLogic.And},Sort=newList<EntitySort>{newEntitySort{Name="Price",Direction=SortDirections.Descending},newEntitySort{Name="Name",Direction=SortDirections.Ascending}}};varquery=newEntityPagedQuery<ProductReadModel>(principal,complexEntityQuery);varresult=awaitmediator.Send(query);Source-generated, compile-time object mapping with support for custom property expressions and IQueryable projection.
dotnet add package Arbiter.Mapping- Source-Generated: Roslyn incremental source generator emits mapping code at build time
- Zero Reflection: No runtime reflection or expression compilation
- AOT Compatible: Fully compatible with Native AOT
- Auto Property Matching: Automatically maps properties with matching names and compatible types
- Custom Expressions: Configure custom property mappings via
ConfigureMapping - Query Projection: Built-in
ProjectTosupport for Entity Framework and other query providers - Record Support: Supports mapping to records,
initproperties, and primary constructors
1. Define a Mapper
[GenerateMapper]publicpartialclassUserToUserDtoMapper:MapperProfile<User,UserDto>{protectedoverridevoidConfigureMapping(MappingBuilder<User,UserDto>mapping){mapping.Property(d =>d.FullName).From(s =>s.FirstName+" "+s.LastName);mapping.Property(d =>d.Age).From(s =>DateTime.Now.Year-s.BirthDate.Year);mapping.Property(d =>d.DepartmentName).From(s =>s.Department!.Name);mapping.Property(d =>d.AddressCount).From(s =>s.Addresses.Count());}}2. Register the Mapper
services.AddSingleton<IMapper<User,UserDto>,UserToUserDtoMapper>();services.AddSingleton<IMapper,ServiceProviderMapper>();3. Use the Mapper
publicclassUserService{privatereadonlyIMapper<User,UserDto>_userMapper;publicUserService(IMapper<User,UserDto>userMapper)=>_userMapper=userMapper;publicUserDtoGetUserDto(Useruser)=>_userMapper.Map(user);}Utility library providing common infrastructure services for .NET applications.
dotnet add package Arbiter.Services- CSV Parsing: Read and write CSV data with flexible configuration
- Encryption: Symmetric encryption and hashing utilities
- Caching: Helpers for building cache keys and managing cache entries
- Token Management: Secure token generation and validation
- URL Building: Fluent API for constructing URLs with query parameters
Entity Framework Core integration providing ready-to-use handlers for all base commands and queries.
dotnet add package Arbiter.CommandQuery.EntityFramework- Complete CRUD Operations: Pre-built handlers for all entity operations
- Change Tracking: Automatic audit fields and soft delete support
- Optimized Queries: Efficient EF Core query patterns
- Transaction Support: Proper transaction management
- Bulk Operations: Support for bulk insert/update operations
// Add Entity Framework Core servicesservices.AddDbContext<TrackerContext>(options =>options.UseSqlServer(connectionString));// Register Command Query servicesservices.AddCommandQuery();// Register source-generated mappersservices.AddSingleton<IMapper<Product,ProductReadModel>,ProductToReadModelMapper>();services.AddSingleton<IMapper<ProductCreateModel,Product>,ProductCreateModelToProductMapper>();services.AddSingleton<IMapper<ProductUpdateModel,Product>,ProductUpdateModelToProductMapper>();services.AddSingleton<IMapper,ServiceProviderMapper>();// Register Entity Framework handlers for each entityservices.AddEntityQueries<TrackerContext,Product,int,ProductReadModel>();services.AddEntityCommands<TrackerContext,Product,int,ProductReadModel,ProductCreateModel,ProductUpdateModel>();MongoDB integration providing handlers for all base commands and queries with document database optimizations.
dotnet add package Arbiter.CommandQuery.MongoDB// Add MongoDB Repository servicesservices.AddMongoRepository("Tracker");services.AddCommandQuery();// Register source-generated mappersservices.AddSingleton<IMapper<Product,ProductReadModel>,ProductToReadModelMapper>();services.AddSingleton<IMapper<ProductCreateModel,Product>,ProductCreateModelToProductMapper>();services.AddSingleton<IMapper<ProductUpdateModel,Product>,ProductUpdateModelToProductMapper>();services.AddSingleton<IMapper,ServiceProviderMapper>();// Register MongoDB handlers for each entityservices.AddEntityQueries<IMongoEntityRepository<Product>,Product,string,ProductReadModel>();services.AddEntityCommands<IMongoEntityRepository<Product>,Product,string,ProductReadModel,ProductCreateModel,ProductUpdateModel>();Minimal API endpoints that automatically expose your commands and queries as REST APIs.
dotnet add package Arbiter.CommandQuery.Endpointsvarbuilder=WebApplication.CreateBuilder(args);// Add endpoint servicesbuilder.Services.AddEndpointRoutes();varapp=builder.Build();// Map endpoint routesapp.MapEndpointRoutes();app.Run();Custom Endpoint:
publicclassProductEndpoint:EntityCommandEndpointBase<int,ProductReadModel,ProductReadModel,ProductCreateModel,ProductUpdateModel>{publicProductEndpoint(ILoggerFactoryloggerFactory):base(loggerFactory,"Product"){}}// Register the endpointbuilder.Services.AddSingleton<IEndpointRoute,ProductEndpoint>();MVC Controllers for base Commands and Queries with full ASP.NET Core integration.
dotnet add package Arbiter.CommandQuery.MvcThe Dispatcher libraries provide a unified IDispatcher abstraction for sending commands and queries from Blazor components, with full support for Blazor Auto render mode. Components depend only on IDispatcher; the correct transport is wired at startup based on the render environment.
| Render mode | Implementation | Transport |
|---|---|---|
| WebAssembly | MessagePackDispatcher / JsonDispatcher | HTTP POST to /api/dispatcher/send |
| Server Interactive | ServerDispatcher | Direct IMediator call (in-process) |
Provides the DispatcherEndpoint—a single POST /api/dispatcher/send HTTP endpoint that deserializes incoming requests (JSON or MessagePack), resolves the target handler via IMediator, and streams the response back to the WASM client.
dotnet add package Arbiter.Dispatcher.Server// Program.cs — Blazor host projectbuilder.Services.AddDispatcherService();// ...app.MapDispatcherService().RequireAuthorization();Provides IDispatcher and its implementations, plus DispatcherDataService, ModelStateManager<TModel>, ModelStateLoader<TKey, TModel>, and ModelStateEditor<TKey, TReadModel, TUpdateModel> for Blazor component state management.
dotnet add package Arbiter.Dispatcher.Client// Program.cs — WASM client projectbuilder.Services.AddMessagePackDispatcher((sp,client)=>{client.BaseAddress=newUri(builder.HostEnvironment.BaseAddress);});// Program.cs — Blazor host projectbuilder.Services.AddServerDispatcher();Register both dispatchers so each render environment resolves IDispatcher correctly:
// Host project: server-side rendering + serves WASM clientsbuilder.Services.AddServerDispatcher();builder.Services.AddDispatcherService();// WASM client project: WebAssembly renderingbuilder.Services.AddMessagePackDispatcher((sp,client)=>{client.BaseAddress=newUri(builder.HostEnvironment.BaseAddress);});Inject IDispatcher or the higher-level IDispatcherDataService into any component or service:
// Low-level: IDispatchervarquery=newEntityIdentifierQuery<int,UserReadModel>(principal,userId);varuser=awaitdispatcher.Send<EntityIdentifierQuery<int,UserReadModel>,UserReadModel>(query,cancellationToken);// High-level: IDispatcherDataServicevaruser=awaitdataService.Get<int,UserReadModel>(userId);varpage=awaitdataService.Page<UserReadModel>(entityQuery);varsaved=awaitdataService.Save<int,UserUpdateModel,UserReadModel>(userId,updateModel);ModelStateEditor manages the full load–edit–save–delete lifecycle for a Blazor edit form:
@injectModelStateEditor<int,UserReadModel,UserUpdateModel> Store
@implements IDisposable
protectedoverrideasyncTaskOnInitializedAsync(){Store.OnStateChanged+=(_,_)=>InvokeAsync(StateHasChanged);if(IsCreate)Store.New();elseawaitStore.Load(Id);}// Store.IsBusy — true while a load/save/delete is in progress// Store.IsDirty — true when the model has unsaved changes// Store.Model — the editable update model bound to the form// Store.Save() / Store.Delete() / Store.Cancel()publicvoidDispose()=>Store.OnStateChanged-=HandleStateChanged;A flexible message templating system for email and SMS communications with support for multiple providers.
dotnet add package Arbiter.CommunicationAzure Communication Services:
dotnet add package Arbiter.Communication.AzureMicrosoft Graph Email:
dotnet add package Arbiter.Communication.GraphSendGrid and Twilio:
dotnet add package Arbiter.Communication.Twilio- Complete Documentation - Comprehensive guides and API reference
- Quick Start Guide - Get up and running quickly
- Architecture Patterns - Best practices and patterns
- Blazor Dispatcher Overview - Dispatcher architecture, WASM and Server Interactive setup
- Dispatcher Server - Server endpoint configuration, security, and diagnostics
- Dispatcher Client - Client registration, sending commands and queries
- State Management - ModelStateManager, ModelStateLoader, ModelStateEditor
Explore practical examples in the samples directory:
- Entity Framework Sample - Complete CRUD operations with EF Core
- MongoDB Sample - Document database implementation
Contributions are welcome! Please feel free to submit a Pull Request. For major changes, please open an issue first to discuss what you would like to change.
- Fork the repository
- Create your feature branch (git checkout -b feature/amazing-feature)
- Commit your changes (git commit -m 'Add amazing feature')
- Push to the branch (git push origin feature/amazing-feature)
- Open a Pull Request
This project is licensed under the MIT License - see the LICENSE file for details.
If you find this project useful, please consider:
- Starring the repository
- Reporting issues
- Contributing improvements
- Spreading the word
- Removed
EntitySelectQuery: Replaced with enhancedEntityQuerythat now supports both paged and non-paged results - Removed
EntityUpsertCommand: Upsert functionality has been unified intoEntityUpdateCommandwith built-in upsert logic - Removed
EntityContinuationQueryandEntityContinuationResult: Functionality now integrated intoEntityQuery - Command/Query Reorganization:
- Moved query classes (
EntityIdentifierQuery,EntityIdentifiersQuery,EntityPagedQuery) toCommandsnamespace for better organization - Renamed base classes for consistency:
EntityIdentifierCommand→EntityIdentifierBaseEntityIdentifiersCommand→EntityIdentifiersBaseEntityModelCommand→EntityModelBase
- Renamed filter, logic, and sort values for more consistent query building
EntityFilterOperators→FilterOperatorsEntityFilterLogic→FilterLogicEntitySortDirections→SortDirections
- Moved query classes (
Simplified Query System:
- Consolidated multiple query types into a single, more powerful
EntityQueryclass - Removed redundant query handlers and behaviors
- Enhanced filter and sort capabilities with better type safety
- Consolidated multiple query types into a single, more powerful
Enhanced Filtering:
- Moved filter logic to dedicated
Queries.FilterLogicandQueries.FilterOperatorsenums - Improved
EntityFilterConverterwith better validation and error handling - Enhanced
LinqExpressionBuilderwith more robust query building capabilities
- Moved filter logic to dedicated
Caching Simplification:
- Removed
DistributedCacheQueryBehaviorandMemoryCacheQueryBehavior - Consolidated caching logic into
HybridCacheQueryBehaviorfor better performance - Removed
IDistributedCacheSerializerinterface in favor of built-in serialization
- Removed
Tenant Behaviors:
- Removed
TenantFilterBehaviorBaseandTenantSelectQueryBehavior - Enhanced
TenantPagedQueryBehaviorto handle all tenant-related query filtering
- Removed
Soft Delete Behaviors:
- Removed
DeletedFilterBehaviorBaseandDeletedSelectQueryBehavior - Enhanced
DeletedPagedQueryBehaviorto handle all soft delete scenarios
- Removed
Removed Legacy Behaviors:
TrackChangeCommandBehavior- functionality moved to handlers- Various base behavior classes that were no longer needed
Updating Query Usage:
// Version 1.x - EntitySelectQuery (REMOVED)varoldQuery=newEntitySelectQuery<ProductReadModel>(principal,filter,sort);// Version 2.0 - Use EntityPagedQuery with EntityQuery insteadvarentityQuery=newEntityQuery{Filter=filter,Sort=sorts};varnewQuery=newEntityPagedQuery<ProductReadModel>(principal,entityQuery);Updating Filter Operators:
// Version 1.x - String operators (DEPRECATED)varoldFilter=newEntityFilter{Name="Status",Operator="eq",Value="Active"};// Version 2.0 - Enum operatorsvarnewFilter=newEntityFilter{Name="Status",Operator=FilterOperators.Equal,Value="Active"};Updating Sort Directions:
// Version 1.x - String directions (DEPRECATED)varoldSort=newEntitySort{Name="Name",Direction="asc"};// Version 2.0 - Enum directionsvarnewSort=newEntitySort{Name="Name",Direction=SortDirections.Ascending};Upsert Operations:
// Version 1.x - Separate EntityUpsertCommand (REMOVED)varoldUpsert=newEntityUpsertCommand<int,ProductUpsertModel,ProductReadModel>(principal,model);// Version 2.0 - Use EntityUpdateCommand with upsert behaviorvarnewUpdate=newEntityUpdateCommand<int,ProductUpdateModel,ProductReadModel>(principal,id,model,true);