Domium.Persistence.Dapper
0.1.3
dotnet add package Domium.Persistence.Dapper --version 0.1.3
NuGet\Install-Package Domium.Persistence.Dapper -Version 0.1.3
<PackageReference Include="Domium.Persistence.Dapper" Version="0.1.3" />
<PackageVersion Include="Domium.Persistence.Dapper" Version="0.1.3" />
<PackageReference Include="Domium.Persistence.Dapper" />
paket add Domium.Persistence.Dapper --version 0.1.3
#r "nuget: Domium.Persistence.Dapper, 0.1.3"
#:package Domium.Persistence.Dapper@0.1.3
#addin nuget:?package=Domium.Persistence.Dapper&version=0.1.3
#tool nuget:?package=Domium.Persistence.Dapper&version=0.1.3
<p align="center"> <img src="assets/social-preview.png" alt="Domium social preview" width="860" /> </p>
Domium
Domium is a lightweight DDD and CQRS foundation for modern .NET applications. It gives you focused building blocks for aggregate modeling, command and query pipelines, provider-selectable persistence, tenant-aware caching, eventing, and observability without forcing one infrastructure style on every application.
Why Domium
- Model domain objects with aggregate roots, strongly typed IDs, value objects, audit metadata, soft delete support, and domain events.
- Use a small CQRS application layer with command/query buses, validation, logging, transactions, and query caching behaviors.
- Protect command handlers with optional idempotency based on atomic cache reservations.
- Choose persistence per application: EF Core, Dapper, or both.
- Keep read models independent from aggregate repositories.
- Add provider packages only when needed: Redis, MassTransit, OpenTelemetry, EF Core, Dapper.
- Publish and version each package independently through NuGet.
Package Map
| Package | Purpose |
|---|---|
Domium.Domain.Abstractions |
Domain contracts for entities, aggregate roots, IDs, value objects, and domain events. |
Domium.Domain |
Concrete domain primitives such as AggregateRoot<TId>, EntityBase<TId>, AggregateId<T>, and DomainEvent. |
Domium.Configuration |
Core composition options and registration pipeline used by AddDomium. |
Domium.Application.Abstractions |
Command/query buses, handlers, validators, and pipeline contracts. |
Domium.Application |
Command/query buses, pipeline behaviors, and domain event dispatching. |
Domium.Facade.Abstractions |
Facade marker contract for exposing one module-level API to other layers. |
Domium.Facade |
Base facade helper that delegates to command and query buses while keeping CQRS inside the application layer. |
Domium.Persistence.Abstractions |
Provider-neutral aggregate repository and unit-of-work contracts. |
Domium.Persistence.EntityFrameworkCore |
EF Core aggregate repository, DbContext base, unit of work, and EF-specific specifications. |
Domium.Persistence.Dapper |
Dapper session, SQL executor, unit of work, and optional mapped aggregate repository. |
Domium.Caching.Abstractions |
Shared cache store, atomic write, policy, key, scope, and invalidation abstractions. |
Domium.Caching |
Default cache policy, key, key factory, scope, and invalidation providers. |
Domium.Caching.Memory |
In-memory cache store provider. |
Domium.Caching.Redis |
Redis cache store provider. |
Domium.Idempotency.Abstractions |
Idempotency key provider contracts and behavior options. |
Domium.Idempotency |
Default idempotency key provider built on the shared cache key factory. |
Domium.Eventing.Abstractions |
Internal and external event contracts. |
Domium.Eventing |
In-process internal event publishing and default no-op external publisher. |
Domium.Eventing.MassTransit |
MassTransit external event publishing and consumer adapter. |
Domium.Tenancy.Abstractions |
Tenant context contracts. |
Domium.Tenancy |
AsyncLocal tenant context and disposable tenant scopes. |
Domium.Observability |
ActivitySource, Meter, counters, and histograms. |
Domium.Observability.OpenTelemetry |
OpenTelemetry tracing and metrics registration. |
Domium.Extensions.DependencyInjection |
Main AddDomium composition entry point. |
Installation
Install the packages you need. A typical application starts with:
dotnet add package Domium.Domain
dotnet add package Domium.Application
dotnet add package Domium.Configuration
dotnet add package Domium.Facade
dotnet add package Domium.Extensions.DependencyInjection
Then add persistence and provider packages as needed:
dotnet add package Domium.Persistence.EntityFrameworkCore
dotnet add package Domium.Persistence.Dapper
dotnet add package Domium.Caching.Redis
dotnet add package Domium.Eventing.MassTransit
dotnet add package Domium.Observability.OpenTelemetry
Quick Start
Register Domium with the fluent API:
using Domium.Configuration;
using Domium.Extensions.DependencyInjection;
services.AddDomium(options =>
{
options
.UseValidation()
.UseLogging()
.UseTransactions()
.UseIdempotency(idempotency =>
{
idempotency.Store.UseMemory();
idempotency.Expiration = TimeSpan.FromHours(24);
})
.UseCaching(cache =>
{
cache.Store.UseMemory();
cache.DefaultExpiration = TimeSpan.FromMinutes(5);
});
});
AddDomium scans loaded non-framework application assemblies by default, so most applications do not need to pass an assembly manually. When handlers live in an assembly that is not loaded yet, register it explicitly:
services.AddDomium(options =>
{
options.AddApplicationAssembly(typeof(CreateOrderHandler).Assembly);
});
Feature toggles accept explicit booleans:
services.AddDomium(options =>
{
options
.UseValidation()
.UseLogging(false)
.UseTransactions(false)
.UseIdempotency(enabled: false)
.UseCaching(enabled: false);
});
Domain Model
public sealed class OrderId(Guid value) : AggregateId<Guid>(value);
public sealed class Order : AggregateRoot<OrderId>
{
private Order() : base(new OrderId(Guid.Empty))
{
Number = string.Empty;
}
public Order(OrderId id, string number) : base(id)
{
Number = number;
RaiseDomainEvent(new OrderCreatedDomainEvent(id));
}
public string Number { get; private set; }
}
public sealed class OrderCreatedDomainEvent(OrderId orderId) : DomainEvent
{
public OrderId OrderId { get; } = orderId;
}
Commands And Queries
Commands change the domain model:
public sealed record CreateOrderCommand(
string Number,
string IdempotencyKey) : IIdempotentCommand;
public sealed class CreateOrderHandler(IRepository<Order, OrderId> repository)
: ICommandHandler<CreateOrderCommand>
{
public Task HandleAsync(
CreateOrderCommand command,
CancellationToken cancellationToken = default)
{
var order = new Order(new OrderId(Guid.NewGuid()), command.Number);
return repository.AddAsync(order, cancellationToken);
}
}
Idempotent commands execute once for a command type and key. If a command fails, Domium removes the reservation so the same key can be retried.
Queries return read models or DTOs:
public sealed record GetOrderQuery(Guid Id) : IQuery<OrderReadModel>;
public sealed record OrderReadModel(Guid Id, string Number);
Facades
Facades provide one module-level dependency to other layers while CQRS stays enforced in the application layer.
public interface IOrderFacade : IFacade
{
Task CreateAsync(CreateOrderRequest request, CancellationToken cancellationToken = default);
Task<OrderReadModel> GetAsync(Guid id, CancellationToken cancellationToken = default);
}
public sealed class OrderFacade(ICommandBus commandBus, IQueryBus queryBus)
: DomiumFacade(commandBus, queryBus), IOrderFacade
{
public Task CreateAsync(CreateOrderRequest request, CancellationToken cancellationToken = default)
{
return ExecuteAsync(
new CreateOrderCommand(request.Number, request.IdempotencyKey),
cancellationToken);
}
public Task<OrderReadModel> GetAsync(Guid id, CancellationToken cancellationToken = default)
{
return QueryAsync<GetOrderQuery, OrderReadModel>(new GetOrderQuery(id), cancellationToken);
}
}
Persistence
Domium keeps the core repository intentionally small:
IRepository<TAggregate, TId>
This repository is for aggregate load/save behavior. Provider-specific querying belongs to the provider package or to query handlers.
EF Core
services.AddDomiumEntityFrameworkCore<AppDbContext>(options =>
{
options.UseSqlServer(connectionString);
});
services.AddDomium(options => options.UseTransactions());
Use the core repository for aggregate persistence:
var order = await repository.GetByIdAsync(orderId, cancellationToken);
await repository.UpdateAsync(order, cancellationToken);
EF-specific specifications are available through IEfRepository<TAggregate, TId>:
var orders = await efRepository.FindAsync(
new ActiveOrdersSpecification(),
cancellationToken);
Dapper
Dapper can be used for explicit SQL in query handlers:
var orders = await sql.QueryAsync<OrderReadModel>(
"select Id, Number from Orders where TenantId = @TenantId",
new { TenantId = tenantId },
cancellationToken);
Register Dapper infrastructure:
services.AddDomiumDapper(options =>
{
options.UseConnectionFactory<SqlConnectionFactory>();
});
If the application wants Dapper as the aggregate repository provider, opt in and supply explicit aggregate mapping:
services.AddScoped<IDapperAggregateMapper<Order, OrderId>, OrderMapper>();
services.AddDomiumDapper(options =>
{
options
.UseConnectionFactory<SqlConnectionFactory>()
.UseAggregateRepositories();
});
The mapper owns SQL and materialization:
public sealed class OrderMapper : IDapperAggregateMapper<Order, OrderId>
{
public string SelectByIdSql => "select Id, Number from Orders where Id = @Id";
public string InsertSql => "insert into Orders (Id, Number) values (@Id, @Number)";
public string UpdateSql => "update Orders set Number = @Number where Id = @Id";
public string DeleteSql => "delete from Orders where Id = @Id";
public object GetIdParameters(OrderId id) => new { Id = id.Value };
public object GetInsertParameters(Order order) => new { Id = order.Id.Value, order.Number };
public object GetUpdateParameters(Order order) => new { Id = order.Id.Value, order.Number };
public object GetDeleteParameters(Order order) => new { Id = order.Id.Value };
public Order Map(object row)
{
// Map from the provider row shape into the aggregate.
throw new NotImplementedException();
}
}
Query Caching
Domium uses one cache store abstraction for query caching and command idempotency. Each feature owns its own store options, so query caching and idempotency can use different Redis connections while sharing the same memory and Redis store implementations.
Use in-memory caching:
services.AddDomium(options =>
{
options.UseCaching(cache =>
{
cache.Store.UseMemory();
cache.DefaultExpiration = TimeSpan.FromMinutes(5);
});
});
Use Redis caching:
services.AddDomium(options =>
{
options.UseCaching(cache =>
{
cache.Store.UseRedis("localhost");
cache.DefaultExpiration = TimeSpan.FromMinutes(5);
});
});
Register query cache policies through IDomiumQueryCachePolicyRegistry.
Cache keys are generated by IDomiumCacheKeyFactory. Query caching uses the query category and includes the query type, scope, and a hash of the query payload. Custom cache stores must implement atomic TrySetAsync(...) as well as normal get/set/remove operations because command idempotency depends on atomic reservation.
Command Idempotency
Command idempotency uses the same cache store implementations as query caching through an idempotency-specific store registration. The store supports atomic TrySetAsync(...), so duplicate commands cannot both reserve the same idempotency key.
Use the default in-memory cache store for single-node applications or tests:
services.AddDomium(options =>
{
options.UseIdempotency(idempotency =>
{
idempotency.Store.UseMemory();
idempotency.Expiration = TimeSpan.FromHours(24);
});
});
Use Redis for multiple application instances:
services.AddDomium(options =>
{
options.UseIdempotency(idempotency =>
{
idempotency.Store.UseRedis("localhost");
idempotency.Expiration = TimeSpan.FromHours(24);
});
});
Query caching and idempotency can use different Redis connections:
services.AddDomium(options =>
{
options.UseCaching(cache =>
{
cache.Store.UseRedis(queryCacheRedis);
cache.DefaultExpiration = TimeSpan.FromMinutes(5);
});
options.UseIdempotency(idempotency =>
{
idempotency.Store.UseRedis(idempotencyRedis);
idempotency.Expiration = TimeSpan.FromHours(24);
});
});
Advanced applications can provide their own Redis connection factory:
services.AddDomium(options =>
{
options.UseIdempotency(idempotency =>
{
idempotency.Store.UseRedis(provider =>
provider.GetRequiredService<IConnectionMultiplexer>());
});
});
Commands opt in by implementing IIdempotentCommand:
public sealed record SubmitOrderCommand(
Guid OrderId,
string IdempotencyKey) : IIdempotentCommand;
Set RequireIdempotencyKey = true when every command in the application should be idempotent.
Idempotency behavior:
- Commands that do not implement
IIdempotentCommandpass through by default. - If
RequireIdempotencyKeyis enabled, non-idempotent commands fail before the handler runs. - Empty idempotency keys fail before the handler runs.
- If the handler or transaction fails, Domium removes the reservation so the command can be retried.
- If the handler succeeds, Domium keeps the reservation for the configured expiration window.
- If the completion marker write fails after the handler succeeds, Domium does not remove the reservation because the command may already be committed.
Tenancy
Tenant scope is ambient and async-flow aware:
using var scope = tenantScopeFactory.BeginScope("tenant-42");
Tenant-scoped cache policies fail clearly when no tenant context exists.
Eventing
Internal events are in-process. External events are transport-provider specific.
services.AddDomiumMassTransitEventing();
services.AddMassTransit(configurator =>
{
configurator.AddDomiumExternalEventConsumer<OrderSubmitted>();
configurator.UsingRabbitMq((context, cfg) => cfg.ConfigureEndpoints(context));
});
Observability
using Domium.Observability.OpenTelemetry;
services.AddDomiumOpenTelemetry(options =>
{
options.ServiceName = "Orders.Api";
options.Environment = "Production";
options.Otlp.Enabled = true;
options.Otlp.Endpoint = "http://localhost:4317";
});
Domium emits activities and metrics under the Domium source/meter.
Documentation
Build
dotnet restore Domium.slnx
dotnet build Domium.slnx --configuration Release --no-restore
dotnet test Domium.slnx --configuration Release --no-build
dotnet pack Domium.slnx --configuration Release --no-build --output artifacts/packages
License
Domium is licensed under the MIT license.
| Product | Versions Compatible and additional computed target framework versions. |
|---|---|
| .NET | net5.0 was computed. net5.0-windows was computed. net6.0 was computed. net6.0-android was computed. net6.0-ios was computed. net6.0-maccatalyst was computed. net6.0-macos was computed. net6.0-tvos was computed. net6.0-windows was computed. net7.0 was computed. net7.0-android was computed. net7.0-ios was computed. net7.0-maccatalyst was computed. net7.0-macos was computed. net7.0-tvos was computed. net7.0-windows was computed. net8.0 was computed. 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 was computed. 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. |
| .NET Core | netcoreapp3.0 was computed. netcoreapp3.1 was computed. |
| .NET Standard | netstandard2.1 is compatible. |
| MonoAndroid | monoandroid was computed. |
| MonoMac | monomac was computed. |
| MonoTouch | monotouch was computed. |
| Tizen | tizen60 was computed. |
| Xamarin.iOS | xamarinios was computed. |
| Xamarin.Mac | xamarinmac was computed. |
| Xamarin.TVOS | xamarintvos was computed. |
| Xamarin.WatchOS | xamarinwatchos was computed. |
-
.NETStandard 2.1
- Dapper (>= 2.1.79)
- Domium.Persistence.Abstractions (>= 0.1.3)
- Microsoft.Extensions.DependencyInjection.Abstractions (>= 10.0.8)
NuGet packages
This package is not used by any NuGet packages.
GitHub repositories
This package is not used by any popular GitHub repositories.