Documentation Index
Fetch the complete documentation index at: https://mintlify.com/jordiaragonzaragoza/JordiAragonZaragoza.SharedKernel/llms.txt
Use this file to discover all available pages before exploring further.
By the end of this guide you will have a fully wired SharedKernel aggregate: a strongly typed ID, an Order aggregate root that raises domain events, a business rule that guards invariants, a CQRS CreateOrderCommand with a matching handler, and the DI registrations needed to dispatch the command through MediatR pipelines in a .NET 10 application.
SharedKernel uses Ardalis.Result as the standard return type for all commands and queries. Handlers return Result<TResponse> — never throw for expected business failures. Use Result.Success(value), Result.Error(...), and Result.Invalid(...) to communicate outcomes to callers.
Install packages
Add the packages you need for your layers. A typical application requires at least the Domain, Application, and Infrastructure packages.# Domain building blocks
dotnet add package JordiAragonZaragoza.SharedKernel.Domain
# Application CQRS contracts and pipeline services
dotnet add package JordiAragonZaragoza.SharedKernel.Application
dotnet add package JordiAragonZaragoza.SharedKernel.Application.Contracts
# Infrastructure bus wires and service implementations
dotnet add package JordiAragonZaragoza.SharedKernel.Infrastructure
# Entity Framework Core base contexts and repositories
dotnet add package JordiAragonZaragoza.SharedKernel.Infrastructure.EntityFramework
Create a strongly typed entity ID
SharedKernel IDs are value objects backed by BaseAggregateRootId<TId>, which in turn extends BaseEntityId<TId> and BaseValueObject. This gives you structural equality, implicit conversion to the primitive type, and EF Core compatibility out of the box.using JordiAragonZaragoza.SharedKernel.Domain.ValueObjects;
public sealed class OrderId : BaseAggregateRootId<Guid>
{
public OrderId(Guid value)
: base(value)
{
}
// Required by EF Core.
private OrderId()
{
}
}
Create a domain event
Domain events are immutable record class types that extend BaseDomainEvent. The base constructor requires the AggregateId so the event is always traceable back to its source aggregate.using JordiAragonZaragoza.SharedKernel.Domain.Events;
public sealed record class OrderCreatedEvent(
Guid AggregateId,
string CustomerName,
decimal TotalAmount)
: BaseDomainEvent(AggregateId);
Define a business rule
Business rules implement IBusinessRule from JordiAragonZaragoza.SharedKernel.Domain.Contracts. Call BaseEntity.CheckRule(rule) or BaseValueObject.CheckRule(rule) inside aggregate methods — a broken rule throws BusinessRuleValidationException before any state change is committed.using JordiAragonZaragoza.SharedKernel.Domain.Contracts.Interfaces;
public sealed class OrderTotalMustBePositiveRule : IBusinessRule
{
private readonly decimal totalAmount;
public OrderTotalMustBePositiveRule(decimal totalAmount)
{
this.totalAmount = totalAmount;
}
public string Message => "Order total amount must be greater than zero.";
public bool IsBroken() => this.totalAmount <= 0m;
}
Create the aggregate root
BaseAggregateRoot<TId> requires you to implement two abstract methods:
When(IDomainEvent) — project an event onto the aggregate’s state (used both when raising new events and when rehydrating from history).
EnsureValidState() — assert that the aggregate is in a consistent state after each event application.
Use the protected Apply(IDomainEvent) method to raise events — it calls When, then EnsureValidState, then enqueues the event for dispatch.using JordiAragonZaragoza.SharedKernel.Domain.Contracts.Interfaces;
using JordiAragonZaragoza.SharedKernel.Domain.Entities;
public sealed class Order : BaseAggregateRoot<OrderId>
{
private string customerName = string.Empty;
private decimal totalAmount;
// Factory method — the only public creation path.
public static Order Create(OrderId id, string customerName, decimal totalAmount)
{
var order = new Order();
order.Apply(new OrderCreatedEvent(id.Value, customerName, totalAmount));
return order;
}
// Required by EF Core.
private Order()
{
}
public string CustomerName => this.customerName;
public decimal TotalAmount => this.totalAmount;
protected override void When(IDomainEvent domainEvent)
{
switch (domainEvent)
{
case OrderCreatedEvent e:
this.Id = new OrderId(e.AggregateId);
this.customerName = e.CustomerName;
this.totalAmount = e.TotalAmount;
break;
}
}
protected override void EnsureValidState()
{
CheckRule(new OrderTotalMustBePositiveRule(this.totalAmount));
}
}
Create a command and handler
Commands implement ICommand<TResponse>, which wraps IRequest<Result<TResponse>> from MediatR and marks the request as transactional. Handlers implement ICommandHandler<TCommand, TResponse>, which wraps IRequestHandler<TCommand, Result<TResponse>>.using Ardalis.Result;
using JordiAragonZaragoza.SharedKernel.Application.Contracts.Interfaces;
// Command — represents the caller's intent.
public sealed record class CreateOrderCommand(
string CustomerName,
decimal TotalAmount)
: ICommand<Guid>;
// Handler — orchestrates domain logic and repository.
public sealed class CreateOrderCommandHandler
: ICommandHandler<CreateOrderCommand, Guid>
{
private readonly IRepository<Order, OrderId> orderRepository;
public CreateOrderCommandHandler(IRepository<Order, OrderId> orderRepository)
{
this.orderRepository = orderRepository;
}
public async Task<Result<Guid>> Handle(
CreateOrderCommand command,
CancellationToken cancellationToken)
{
var orderId = new OrderId(Guid.NewGuid());
var order = Order.Create(orderId, command.CustomerName, command.TotalAmount);
await this.orderRepository.AddAsync(order, cancellationToken);
return Result.Success(orderId.Value);
}
}
Register services in Program.cs
SharedKernel provides layered extension methods that register MediatR, pipeline behaviors, and infrastructure services. Call them in dependency order: Infrastructure first (concrete services), then Application (pipeline service implementations).using JordiAragonZaragoza.SharedKernel.Application;
using JordiAragonZaragoza.SharedKernel.Infrastructure;
var builder = WebApplication.CreateBuilder(args);
// 1. Register core infrastructure services:
// IDateTime, IIdGenerator, ICacheService, IIdentityService,
// IPartitionContextService, IUserContextService.
builder.Services.AddSharedKernelInfrastructure();
// 2. Register MediatR and ICommandBus with pipeline behaviors.
builder.Services.AddSharedKernelInfrastructureCommandBus();
// 3. Register application-layer pipeline service implementations:
// logging, exception handling, unit-of-work, authorization,
// validation, caching, cache invalidation, performance tracking.
builder.Services.AddSharedKernelApplicationCommandBus();
// 4. Register your own handlers and repositories here...
builder.Services.AddScoped<IRepository<Order, OrderId>, OrderRepository>();
var app = builder.Build();
app.Run();
Dispatch a command
Inject ICommandBus wherever you need to send a command — in a controller, a FastEndpoints endpoint, or a background service. SendAsync routes the command through MediatR and all registered pipeline behaviors before reaching your handler.using Ardalis.Result;
using JordiAragonZaragoza.SharedKernel.Application.Contracts.Interfaces;
public sealed class CreateOrderEndpoint
{
private readonly ICommandBus commandBus;
public CreateOrderEndpoint(ICommandBus commandBus)
{
this.commandBus = commandBus;
}
public async Task<Result<Guid>> HandleAsync(
string customerName,
decimal totalAmount,
CancellationToken cancellationToken)
{
var command = new CreateOrderCommand(customerName, totalAmount);
// Returns Result<Guid>: inspect .IsSuccess, .Value, .Errors.
return await this.commandBus.SendAsync(command, cancellationToken);
}
}