BytLabs.Application
The application layer: CQRS (commands/queries via MediatR), repository & unit-of-work abstractions, domain-event handling, a validation + logging pipeline, user-context resolution, and dynamic-data filter/sort inputs.
Install
<PackageReference Include="BytLabs.Application" />
What's inside
| Area | Types |
|---|---|
| Commands | ICommand, ICommand<TResult>, ICommandHandler<TCommand>, ICommandHandler<TCommand,TResponse> |
| Queries | IQuery<TResult>, IQueryHandler<TQuery,TResponse> |
| Data access | IRepository<TAggregateRoot,TIdentity>, IUnitOfWork |
| Domain events | DomainEventHandler<TDomainEvent> |
| Pipeline | command/query validation decorators, LoggingDecorator (registered by AddCQS) |
| User context | IUserContextProvider, IUserContextResolver, UserContextBuilder, AddUserContextProviders() |
| Dynamic data | InputFilteringDynamicData, DataOperationFilter, FilterOperation, SortOrderInput, ValueKind |
| Exceptions | ApplicationOperationException, EntityNotFoundException, CommandValidationException, QueryValidationException |
| Registration | AddCQS(assemblies, options?), AddUserContextProviders() |
Registration
AddCQS wires MediatR (scanning your assemblies + this one), the FluentValidation pipeline for
commands and queries, and the logging decorator. Call it once in your infrastructure setup.
services.AddCQS(new[] { typeof(CreateProductCommand).Assembly });
This registers: MediatR handlers, CommandValidationDecorator/QueryValidationDecorator
(auto-discovered AbstractValidator<T> run before handlers), and LoggingDecorator.
Usage
Command + handler
A command is a record implementing ICommand<TResult> (or ICommand for void). The handler
implements ICommandHandler<,> and uses IRepository<,>.
public record CreateProductCommand(Guid Id, string Name) : ICommand<ProductDto>;
public class CreateProductCommandHandler : ICommandHandler<CreateProductCommand, ProductDto>
{
private readonly IRepository<Product, Guid> _repository;
private readonly IMapper _mapper;
public CreateProductCommandHandler(IRepository<Product, Guid> repository, IMapper mapper)
{ _repository = repository; _mapper = mapper; }
public async Task<ProductDto> Handle(CreateProductCommand request, CancellationToken ct)
{
var product = Product.Create(request.Id, request.Name);
var saved = await _repository.InsertAsync(product, ct);
return _mapper.Map<ProductDto>(saved);
}
}
Query + handler
public record GetProductByIdQuery(Guid Id) : IQuery<ProductDto>;
public class GetProductByIdQueryHandler : IQueryHandler<GetProductByIdQuery, ProductDto>
{
private readonly IRepository<Product, Guid> _repository;
public GetProductByIdQueryHandler(IRepository<Product, Guid> repository) => _repository = repository;
public async Task<ProductDto> Handle(GetProductByIdQuery request, CancellationToken ct)
=> /* map */ default!;
}
Note: most read endpoints in these services are GraphQL resolvers that query MongoDB directly with projection;
IQueryHandleris available when you prefer the mediator path.
Validation (automatic)
Define a FluentValidation AbstractValidator<TCommand> in a scanned assembly — the pipeline runs it
before the handler and throws CommandValidationException (→ GraphQL ValidationError).
public class CreateProductCommandValidator : AbstractValidator<CreateProductCommand>
{
public CreateProductCommandValidator() => RuleFor(c => c.Name).NotEmpty();
}
Repository
IRepository<TAggregateRoot, TIdentity> (where TAggregateRoot : IAggregateRoot<TIdentity>) provides:
GetByIdAsync (throws EntityNotFoundException if missing), FindByIdAsync (null if missing),
SingleOrDefaultAsync, InsertAsync, UpdateAsync, DeleteAsync (soft delete), FindAllAsync(ids),
FindAllByAsync(predicate), and batch variants (InsertBatchAsync, UpdateBatchAsync,
DeleteBatchAsync). Inject it into handlers; the MongoDB implementation comes from
BytLabs.DataAccess.MongoDB.
Domain-event handler
React to domain events raised by aggregates. Implemented as MediatR notification handlers via
DomainEventHandler<TEvent>; discovered by AddCQS.
public class SendWelcomeEmailHandler : DomainEventHandler<ProductCreated>
{
protected override async Task HandleDomainEvent(ProductCreated e, CancellationToken ct)
{
// side effect: email, read-model update, integration event...
}
}
User context
Resolve "who is acting" from pluggable sources (HTTP, known/system user, etc.). Configure resolvers
with AddUserContextProviders() and inject IUserContextProvider to read GetUserId().
services.AddUserContextProviders()
.AddResolver<HttpUserContextResolver>(); // from BytLabs.Api
The data-access layer uses the resolved user id to stamp AuditInfo on writes.
Dynamic-data filtering inputs
InputFilteringDynamicData, DataOperationFilter, FilterOperation, SortOrderInput, and
ValueKind model filtering/sorting over an aggregate's schema-less Data (IHaveDynamicData). You
rarely construct these by hand — the GraphQL layer binds them from the query where/order
arguments and the MongoDB layer translates them into filters (see
BytLabs.DataAccess.MongoDB).
Related packages
- BytLabs.Domain — the entities/events these abstractions operate on.
- BytLabs.DataAccess — transaction + domain-event-dispatch decorators around commands.
- BytLabs.DataAccess.MongoDB —
IRepository/IUnitOfWorkimplementations.