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
                    
This command is intended to be used within the Package Manager Console in Visual Studio, as it uses the NuGet module's version of Install-Package.
<PackageReference Include="Domium.Persistence.Dapper" Version="0.1.3" />
                    
For projects that support PackageReference, copy this XML node into the project file to reference the package.
<PackageVersion Include="Domium.Persistence.Dapper" Version="0.1.3" />
                    
Directory.Packages.props
<PackageReference Include="Domium.Persistence.Dapper" />
                    
Project file
For projects that support Central Package Management (CPM), copy this XML node into the solution Directory.Packages.props file to version the package.
paket add Domium.Persistence.Dapper --version 0.1.3
                    
#r "nuget: Domium.Persistence.Dapper, 0.1.3"
                    
#r directive can be used in F# Interactive and Polyglot Notebooks. Copy this into the interactive tool or source code of the script to reference the package.
#:package Domium.Persistence.Dapper@0.1.3
                    
#:package directive can be used in C# file-based apps starting in .NET 10 preview 4. Copy this into a .cs file before any lines of code to reference the package.
#addin nuget:?package=Domium.Persistence.Dapper&version=0.1.3
                    
Install as a Cake Addin
#tool nuget:?package=Domium.Persistence.Dapper&version=0.1.3
                    
Install as a Cake Tool

<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 IIdempotentCommand pass through by default.
  • If RequireIdempotencyKey is 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 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. 
Compatible target framework(s)
Included target framework(s) (in package)
Learn more about Target Frameworks and .NET Standard.

NuGet packages

This package is not used by any NuGet packages.

GitHub repositories

This package is not used by any popular GitHub repositories.

Version Downloads Last Updated
0.1.3 96 5/20/2026
0.1.2 93 5/17/2026
0.1.1 99 5/17/2026
0.1.0 96 5/17/2026