Sumapap.Persistence.Domain
1.2.0
dotnet add package Sumapap.Persistence.Domain --version 1.2.0
NuGet\Install-Package Sumapap.Persistence.Domain -Version 1.2.0
<PackageReference Include="Sumapap.Persistence.Domain" Version="1.2.0" />
<PackageVersion Include="Sumapap.Persistence.Domain" Version="1.2.0" />
<PackageReference Include="Sumapap.Persistence.Domain" />
paket add Sumapap.Persistence.Domain --version 1.2.0
#r "nuget: Sumapap.Persistence.Domain, 1.2.0"
#:package Sumapap.Persistence.Domain@1.2.0
#addin nuget:?package=Sumapap.Persistence.Domain&version=1.2.0
#tool nuget:?package=Sumapap.Persistence.Domain&version=1.2.0
Sumapap.Persistence.Abstractions
๐ก Overview
Sumapap.Persistence.Abstractions provides core domain-safe persistence contracts that can be safely referenced from your Domain layer without introducing infrastructure dependencies. The package focuses on:
- Entity marker interfaces (
IEntity,IEntity<TKey>) - Aggregate root marker (
IAggregateRoot) - Repository contracts (
IReadRepository<T>,IWriteRepository<T>,IReadWriteRepository<T>,IRepository<T>) - Specification pattern contract (
ISpecification<T>,ISpecificationEvaluator) - Unit of Work abstraction (
IUnitOfWork)
The goal is to enable Clean Architecture by allowing your domain to depend on abstractions while keeping all infrastructure concerns (EF Core, Dapper, caching, DI) in separate packages.
โจ Why use Sumapap.Persistence.Abstractions?
- Domain-Safe: Zero dependencies on EF Core, DI, caching, or other infrastructure concerns - only abstractions
- Clean Architecture Compliance: Enables proper dependency inversion - domain depends on abstractions, infrastructure implements them
- Framework-Agnostic: Works with any ORM or data access technology (EF Core, Dapper, NHibernate, ADO.NET)
- DDD-Friendly: Provides marker interfaces for entities and aggregate roots to support Domain-Driven Design patterns
- Type-Safe: Generic constraints ensure compile-time safety for entity operations
- Specification Pattern: Built-in support for the Specification pattern to encapsulate complex query logic
๐ Quick start
- Add the package to your Domain layer project:
dotnet add package Sumapap.Persistence.Abstractions
- Mark your domain entities with
IEntityorIEntity<TKey>:
public class Product : IEntity<Guid>
{
public Guid Id { get; set; }
public string Name { get; set; } = string.Empty;
public decimal Price { get; set; }
}
- Mark aggregate roots with
IAggregateRoot:
public class Order : IAggregateRoot, IEntity<int>
{
public int Id { get; set; }
public List<OrderLine> Lines { get; set; } = new();
public void AddLine(Product product, int quantity)
{
Lines.Add(new OrderLine(product, quantity));
}
}
- Define domain repository interfaces extending the base contracts:
public interface IProductRepository : IReadRepository<Product>
{
Task<IEnumerable<Product>> GetExpensiveProductsAsync(decimal minPrice);
}
- Implement repositories in your Infrastructure layer using
Sumapap.PersistenceorSumapap.Persistence.EfCore.
๐ Features and usage
Entity Interfaces
IEntity - Marker interface for all entities:
public interface IEntity;
Use this as a base marker for types that represent entities:
public class AuditLog : IEntity
{
public Guid Id { get; private set; }
public string Action { get; set; } = string.Empty;
public DateTime Timestamp { get; set; }
}
IEntity<TKey> - Entity with strongly-typed identifier:
public interface IEntity<TKey> : IEntity
where TKey : IEquatable<TKey>
{
TKey Id { get; set; }
}
Most common entity pattern with type-safe ID:
public class User : IEntity<Guid>
{
public Guid Id { get; set; }
public string Username { get; set; } = string.Empty;
public string Email { get; set; } = string.Empty;
}
public class Category : IEntity<int>
{
public int Id { get; set; }
public string Name { get; set; } = string.Empty;
}
Aggregate Root
IAggregateRoot - Marker interface for DDD aggregate roots:
public interface IAggregateRoot : IEntity;
Aggregates enforce consistency boundaries and encapsulate domain logic:
public class ShoppingCart : IAggregateRoot, IEntity<Guid>
{
public Guid Id { get; set; }
private readonly List<CartItem> _items = new();
public IReadOnlyList<CartItem> Items => _items.AsReadOnly();
public void AddItem(Product product, int quantity)
{
ArgumentNullException.ThrowIfNull(product);
if (quantity <= 0)
throw new ArgumentException("Quantity must be positive", nameof(quantity));
var existingItem = _items.FirstOrDefault(i => i.ProductId == product.Id);
if (existingItem != null)
{
existingItem.IncreaseQuantity(quantity);
}
else
{
_items.Add(new CartItem(product.Id, quantity, product.Price));
}
}
public decimal GetTotal() => _items.Sum(i => i.Subtotal);
}
Repository Interfaces
IRepository<TEntity> - Base marker for all repositories:
public interface IRepository<TEntity> where TEntity : class, IEntity;
IReadRepository<TEntity> - Read-only repository operations:
public interface IReadRepository<TEntity> : IRepository<TEntity>
where TEntity : class, IEntity
{
// Synchronous queries
TEntity? Find(object id);
IEnumerable<TEntity> GetAll();
TEntity? FirstOrDefault(ISpecification<TEntity> specification);
TEntity? SingleOrDefault(ISpecification<TEntity> specification);
int Count();
bool Any();
// Asynchronous queries
Task<TEntity?> FindAsync(object id, CancellationToken cancellationToken = default);
Task<IEnumerable<TEntity>> GetAllAsync(CancellationToken cancellationToken = default);
Task<TEntity?> FirstOrDefaultAsync(ISpecification<TEntity> specification, CancellationToken cancellationToken = default);
Task<TEntity?> SingleOrDefaultAsync(ISpecification<TEntity> specification, CancellationToken cancellationToken = default);
Task<int> CountAsync(CancellationToken cancellationToken = default);
Task<bool> AnyAsync(CancellationToken cancellationToken = default);
// Streaming
IAsyncEnumerable<TEntity> StreamAllAsync(CancellationToken cancellationToken = default);
IAsyncEnumerable<TEntity> StreamWhereAsync(ISpecification<TEntity> specification, CancellationToken cancellationToken = default);
}
IWriteRepository<TEntity> - Write operations:
public interface IWriteRepository<TEntity> : IRepository<TEntity>
where TEntity : class, IEntity
{
// Synchronous mutations
TEntity Add(TEntity entity);
void Update(TEntity entity);
void Delete(TEntity entity);
int SaveChanges();
// Asynchronous mutations
Task<TEntity> AddAsync(TEntity entity, CancellationToken cancellationToken = default);
Task UpdateAsync(TEntity entity, CancellationToken cancellationToken = default);
Task DeleteAsync(TEntity entity, CancellationToken cancellationToken = default);
Task<int> SaveChangesAsync(CancellationToken cancellationToken = default);
}
IReadWriteRepository<TEntity> - Combined read-write repository:
public interface IReadWriteRepository<TEntity> : IReadRepository<TEntity>, IWriteRepository<TEntity>
where TEntity : class, IEntity
{
}
Usage in Domain:
// Define domain-specific repository interface
public interface IOrderRepository : IReadWriteRepository<Order>
{
Task<IEnumerable<Order>> GetOrdersByCustomerAsync(Guid customerId);
Task<Order?> GetOrderWithItemsAsync(int orderId);
}
// Use in domain service
public class OrderService
{
private readonly IOrderRepository _orderRepository;
private readonly IUnitOfWork _unitOfWork;
public OrderService(IOrderRepository orderRepository, IUnitOfWork unitOfWork)
{
_orderRepository = orderRepository;
_unitOfWork = unitOfWork;
}
public async Task<Order> CreateOrderAsync(Guid customerId, IEnumerable<OrderLine> lines)
{
var order = new Order { CustomerId = customerId };
foreach (var line in lines)
{
order.AddLine(line);
}
await _orderRepository.AddAsync(order);
await _unitOfWork.SaveChangesAsync();
return order;
}
}
Specification Pattern
ISpecification<T> - Contract for reusable query specifications:
public interface ISpecification<T>;
ISpecificationEvaluator - Evaluates specifications against queryable data:
public interface ISpecificationEvaluator
{
IQueryable<T> GetQuery<T>(IQueryable<T> inputQuery, ISpecification<T> specification)
where T : class;
}
Define specifications in your domain:
public class ActiveProductsSpecification : ISpecification<Product>
{
// Specification implementation provided by infrastructure layer
}
public class ExpensiveProductsSpecification : ISpecification<Product>
{
public decimal MinimumPrice { get; }
public ExpensiveProductsSpecification(decimal minimumPrice)
{
MinimumPrice = minimumPrice;
}
}
Use specifications with repositories:
var activeProducts = await _productRepository
.GetAllAsync(new ActiveProductsSpecification());
var expensiveProducts = await _productRepository
.GetAllAsync(new ExpensiveProductsSpecification(minPrice: 1000m));
Unit of Work
IUnitOfWork - Transactional boundary and repository access:
public interface IUnitOfWork : IDisposable
{
IReadWriteRepository<TEntity> GetRepository<TEntity>() where TEntity : class, IEntity;
int SaveChanges();
Task<int> SaveChangesAsync(CancellationToken cancellationToken = default);
void BeginTransaction();
Task BeginTransactionAsync(CancellationToken cancellationToken = default);
void CommitTransaction();
Task CommitTransactionAsync(CancellationToken cancellationToken = default);
void RollbackTransaction();
Task RollbackTransactionAsync(CancellationToken cancellationToken = default);
}
Usage:
public class TransferService
{
private readonly IUnitOfWork _unitOfWork;
public TransferService(IUnitOfWork unitOfWork)
{
_unitOfWork = unitOfWork;
}
public async Task TransferFundsAsync(Guid fromAccountId, Guid toAccountId, decimal amount)
{
await _unitOfWork.BeginTransactionAsync();
try
{
var accountRepo = _unitOfWork.GetRepository<Account>();
var fromAccount = await accountRepo.FindAsync(fromAccountId);
var toAccount = await accountRepo.FindAsync(toAccountId);
if (fromAccount == null || toAccount == null)
throw new InvalidOperationException("Account not found");
fromAccount.Withdraw(amount);
toAccount.Deposit(amount);
await accountRepo.UpdateAsync(fromAccount);
await accountRepo.UpdateAsync(toAccount);
await _unitOfWork.SaveChangesAsync();
await _unitOfWork.CommitTransactionAsync();
}
catch
{
await _unitOfWork.RollbackTransactionAsync();
throw;
}
}
}
โ ๏ธ Notes & best practices
โ Do
- Reference from Domain layer - this package is safe for domain projects (zero infrastructure dependencies)
- Use
IAggregateRootfor consistency boundaries - mark aggregate roots to clarify domain boundaries - Define specific repository interfaces - extend
IReadRepository<T>orIReadWriteRepository<T>with domain-specific methods - Use specifications for complex queries - encapsulate query logic in reusable specification objects
- Prefer async methods - use
*Asyncmethods in application code to avoid thread starvation - Use
IEntity<TKey>with appropriate key types -Guidfor distributed systems,intfor simple scenarios
โ Don't
- Never reference infrastructure packages from Domain - keep domain clean by only depending on abstractions
- Avoid exposing
IQueryable<T>from repositories - use specifications instead to maintain encapsulation - Don't create repository per entity - create repository per aggregate root only
- Avoid generic repository leakage - define specific interfaces (
IProductRepository) rather than usingIRepository<T>directly in services - Don't implement repository logic in domain - keep domain focused on business rules, not data access
- Avoid using
IUnitOfWork.GetRepository<T>()everywhere - prefer injecting specific repository interfaces
Repository Per Aggregate
Correct approach:
// One repository per aggregate root
public interface IOrderRepository : IReadWriteRepository<Order> { }
// Order aggregate manages OrderLines internally
public class Order : IAggregateRoot, IEntity<int>
{
private readonly List<OrderLine> _lines = new();
public IReadOnlyList<OrderLine> Lines => _lines.AsReadOnly();
}
Incorrect approach:
// โ Don't create separate repositories for child entities
public interface IOrderLineRepository : IRepository<OrderLine> { }
Specification Pattern Best Practices
- Specifications should be reusable across the application
- Keep specifications focused on a single query concern
- Use composition to combine specifications rather than creating complex monolithic ones
- Implement specification logic in infrastructure layer, not domain
- Specifications should describe what to query, not how to query
Testing Recommendations
When testing domain logic:
- Mock repository interfaces in unit tests
- Test domain logic independently of data access
- Use in-memory implementations for integration tests
- Verify specification correctness by testing against real queries in infrastructure tests
โญ License
Distributed under the MIT License. See the LICENSE file in the repository for more information.
๐ฉ Contact
GitHub @muhammadirwanto-dev
Project Url https://github.com/muhammadirwanto-dev/sumapap
โ Support
If you like this project and want to support it, you can buy me a coffee๏ธ. Your coffee will keep me awake while developing this project โ.
<p align="center"> <a href="https://buymeacoffee.com/muhirwanto.dev"> <img src="https://img.buymeacoffee.com/button-api/?text=Buy%20me%20a%20coffee&emoji=&slug=muhirwanto.dev&button_colour=FFDD00&font_colour=000000&font_family=Comic&outline_colour=000000&coffee_colour=ffffff" alt="Buy Me A Coffee"> </a> </p>
| Product | Versions Compatible and additional computed target framework versions. |
|---|---|
| .NET | net10.0 is compatible. 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. |
-
net10.0
- Sumapap.Queries (>= 2.1.0)
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 |
|---|