Codebelt

Savvyio.Extensions.DependencyInjection.Domain

Register aggregate and traced aggregate repositories with DI-friendly marker abstractions for Savvy I/O domain models.

.NET 10.0 / .NET 9.0 MIT v5.0.8 12,174 downloads

Overview

Savvyio.Extensions.DependencyInjection.Domain adds domain-focused dependency injection support on top of Savvyio.Extensions.DependencyInjection. Its public surface is centered on registering aggregate repositories for IAggregateRoot<IDomainEvent, TKey> and traced aggregate repositories for ITracedAggregateRoot<TKey>, while keeping room for multiple implementations distinguished by marker types.

The package is about composition rather than persistence. Source and tests show it working alongside concrete storage packages, while this package itself contributes the marker-aware repository interfaces and the IServiceCollection extensions that wire those abstractions into Microsoft Dependency Injection.

Key APIs

AddAggregateRepository<TService, TEntity, TKey> registers an implementation of IAggregateRepository<TEntity, TKey> in IServiceCollection, defaults the lifetime to Scoped when no setup delegate is supplied, guards against a null service collection, and then chains into AddRepository<TService, TEntity, TKey> so the same implementation is also available through the lower-level persistent repository abstractions. TService must implement IAggregateRepository<TEntity, TKey>, and TEntity must be a class that implements IAggregateRoot<IDomainEvent, TKey>.

IAggregateRepository<TEntity, TKey, TMarker> combines the domain-level IAggregateRepository<TEntity, TKey> abstraction from Savvyio.Domain with the marker-aware IPersistentRepository<TEntity, TKey, TMarker> abstraction from Savvyio.Extensions.DependencyInjection. Use it when one application needs more than one aggregate repository implementation for the same aggregate type and you want DI resolution to stay explicit.

AddTracedAggregateRepository<TService, TEntity, TKey> registers implementations for event-sourced aggregate roots. Like AddAggregateRepository, it defaults to a scoped lifetime and validates the service collection, but its forwarding predicate targets ITracedAggregateRepository<TEntity, TKey>, IWritableRepository<TEntity, TKey>, and IReadableRepository<TEntity, TKey> so traced aggregate repositories can be resolved through their traced, readable, and writable contracts.

ITracedAggregateRepository<TEntity, TKey, TMarker> is the marker-aware companion for traced aggregate roots. It extends ITracedAggregateRepository<TEntity, TKey> together with marker-specific readable and writable repository interfaces, which lets one traced repository implementation participate in multiple DI registrations without introducing a concrete storage dependency here.

Basic usage

using System;
using System.Collections.Generic;
using System.Linq.Expressions;
using System.Threading.Tasks;
using Codebelt.Extensions.Xunit;
using Cuemon.Threading;
using Microsoft.Extensions.DependencyInjection;
using Savvyio.Domain;
using Savvyio.Extensions.DependencyInjection.Domain;
using Xunit;

namespace MyProject.Tests;

public class AggregateRepositoryRegistrationTest : Test
{
    public AggregateRepositoryRegistrationTest(ITestOutputHelper output) : base(output) { }

    [Fact]
    public void AddAggregateRepository_CustomType_RegistersContracts()
    {
        var services = new ServiceCollection();
        services.AddAggregateRepository<LedgerAccountRepository, LedgerAccountAggregate, Guid>(o => o.Lifetime = ServiceLifetime.Singleton);
        using var provider = services.BuildServiceProvider();

        var aggregateRepository = provider.GetRequiredService<IAggregateRepository<LedgerAccountAggregate, Guid, PrimaryStoreMarker>>();
        var persistentRepository = provider.GetRequiredService<IPersistentRepository<LedgerAccountAggregate, Guid, PrimaryStoreMarker>>();

        TestOutput.WriteLine($"Resolved {aggregateRepository.GetType().Name} for aggregate and persistent repository contracts.");

        Assert.IsType<LedgerAccountRepository>(aggregateRepository);
        Assert.IsType<LedgerAccountRepository>(persistentRepository);
    }
}

public sealed class LedgerAccountAggregate : AggregateRoot<Guid>
{
    public LedgerAccountAggregate(Guid id) : base(id) { }
}

public sealed class PrimaryStoreMarker;

public sealed class LedgerAccountRepository : IAggregateRepository<LedgerAccountAggregate, Guid, PrimaryStoreMarker>
{
    public void Add(LedgerAccountAggregate entity) { }
    public void AddRange(IEnumerable<LedgerAccountAggregate> entities) { }
    public Task<IEnumerable<LedgerAccountAggregate>> FindAllAsync(Expression<Func<LedgerAccountAggregate, bool>>? predicate = null, Action<AsyncOptions>? setup = null) =>
        Task.FromResult<IEnumerable<LedgerAccountAggregate>>(Array.Empty<LedgerAccountAggregate>());
    public Task<LedgerAccountAggregate> GetByIdAsync(Guid id, Action<AsyncOptions>? setup = null) =>
        Task.FromResult(new LedgerAccountAggregate(id));
    public void Remove(LedgerAccountAggregate entity) { }
    public void RemoveRange(IEnumerable<LedgerAccountAggregate> entities) { }
}

Use this pattern when one application contains multiple aggregate persistence strategies and you want domain services to resolve the correct implementation through a marker-qualified aggregate repository abstraction.

It matters because AddAggregateRepository keeps the registration at the DDD aggregate level while still forwarding the same implementation to the lower-level persistent repository contracts used elsewhere in the Savvy I/O stack.

Installation

dotnet add package Savvyio.Extensions.DependencyInjection.Domain

Usage guidance

Adopt this package when your application already models domain entities as Savvy I/O aggregates and you want Microsoft Dependency Injection registrations that stay explicit at the aggregate-repository level, including marker-based differentiation between implementations. If you only need the lower-level repository registrations from Savvyio.Extensions.DependencyInjection, or you need an actual persistence implementation such as the EF Core-oriented packages exercised in the tests, this package is not the whole solution by itself.

Family packages