PANiXiDA.Core.Infrastructure.Persistence.Ef 4.3.1

dotnet add package PANiXiDA.Core.Infrastructure.Persistence.Ef --version 4.3.1
                    
NuGet\Install-Package PANiXiDA.Core.Infrastructure.Persistence.Ef -Version 4.3.1
                    
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="PANiXiDA.Core.Infrastructure.Persistence.Ef" Version="4.3.1" />
                    
For projects that support PackageReference, copy this XML node into the project file to reference the package.
<PackageVersion Include="PANiXiDA.Core.Infrastructure.Persistence.Ef" Version="4.3.1" />
                    
Directory.Packages.props
<PackageReference Include="PANiXiDA.Core.Infrastructure.Persistence.Ef" />
                    
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 PANiXiDA.Core.Infrastructure.Persistence.Ef --version 4.3.1
                    
#r "nuget: PANiXiDA.Core.Infrastructure.Persistence.Ef, 4.3.1"
                    
#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 PANiXiDA.Core.Infrastructure.Persistence.Ef@4.3.1
                    
#: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=PANiXiDA.Core.Infrastructure.Persistence.Ef&version=4.3.1
                    
Install as a Cake Addin
#tool nuget:?package=PANiXiDA.Core.Infrastructure.Persistence.Ef&version=4.3.1
                    
Install as a Cake Tool

PANiXiDA.Core.Infrastructure.Persistence.Ef

PANiXiDA.Core.Infrastructure.Persistence.Ef is a .NET library that provides Entity Framework Core persistence infrastructure for PANiXiDA Core applications.

It is designed for application and infrastructure packages that use PANiXiDA.Core.Application persistence abstractions, PostgreSQL, DDD aggregate roots, and read models.

Status

CI NuGet NuGet downloads Target Framework License

Overview

The package bridges PANiXiDA application-layer persistence contracts with EF Core. It provides base write and read DbContexts, repository base classes, a unit of work implementation, audit shadow properties, soft-delete behavior, PostgreSQL DI registration, and helpers for read-model sorting and pagination.

The library is intentionally infrastructure-focused. Domain model design, command/query handlers, and concrete repositories stay in consuming applications.

Features

  • PostgreSQL registration extensions for write/read EF Core infrastructure and automatic scoped repository registration.
  • WriteDbContext<TDbContext> with HiLo configuration, optional context-derived schema naming, assembly configuration scanning, and plural table names.
  • ReadDbContext<TDbContext> with no-tracking queries, automatic read model registration, optional context-derived schema naming, and migration exclusion for read models.
  • Base EfRepository<TDbContext, TId, TAggregateRoot> with async persistence operations integrated with IAggregateTracker.
  • AggregateTracker implementation for tracking touched aggregate roots independently of EF Core.
  • EfUnitOfWork<TDbContext> implementation for transaction boundaries.
  • Keyed IUnitOfWork registration by write DbContext type for modular applications.
  • Auditable entity configuration with CreatedAt, UpdatedAt, and DeletedAt shadow properties.
  • SaveChanges interceptor that updates audit values and converts deletes with DeletedAt into soft deletes.
  • Read repository helpers for page-based pagination, cursor pagination, dynamic sorting, and projection through IReadModelMapper.

Quick Start

Requirements

  • .NET 10 SDK
  • PostgreSQL when using the built-in DI registration methods
  • Docker for local integration tests because they use Testcontainers with PostgreSQL

Installation

Install the package in each project containing a DbContext and keep its analyzer assets enabled.

dotnet add package PANiXiDA.Core.Infrastructure.Persistence.Ef

Configuration

The built-in PostgreSQL registration methods read the connection string named PostgreSqlConnectionString.

{
  "ConnectionStrings": {
    "PostgreSqlConnectionString": "Host=localhost;Port=5432;Database=panixida;Username=postgres;Password=postgres"
  }
}

Register EF Infrastructure

using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Diagnostics;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;

using PANiXiDA.Core.Infrastructure.Persistence.Ef.DbContexts;
using PANiXiDA.Core.Infrastructure.Persistence.Ef.DependencyInjection;

public static class PersistenceRegistration
{
    public static IServiceCollection AddPersistence(
        IServiceCollection services,
        IConfiguration configuration)
    {
        return services.AddPostgreSqlEfRepository<AppWriteDbContext, AppReadDbContext>(
            configuration);
    }
}

public sealed class AppWriteDbContext(
    DbContextOptions<AppWriteDbContext> options,
    IEnumerable<IInterceptor> interceptors)
    : WriteDbContext<AppWriteDbContext>(options, interceptors)
{
}

public sealed class AppReadDbContext(
    DbContextOptions<AppReadDbContext> options)
    : ReadDbContext<AppReadDbContext>(options)
{
}

By default, a DbContext uses the provider's default schema for its tables, and a write DbContext keeps __EFMigrationsHistory there as well. Override UseContextNameAsSchema to derive the table schema from the context type name. For a write DbContext, its migration history follows the same schema:

public sealed class OrdersWriteDbContext(
    DbContextOptions<OrdersWriteDbContext> options,
    IEnumerable<IInterceptor> interceptors)
    : WriteDbContext<OrdersWriteDbContext>(options, interceptors)
{
    protected override bool UseContextNameAsSchema => true;
}

public sealed class OrdersReadDbContext(
    DbContextOptions<OrdersReadDbContext> options)
    : ReadDbContext<OrdersReadDbContext>(options)
{
    protected override bool UseContextNameAsSchema => true;
}

services.AddPostgreSqlEfRepository<OrdersWriteDbContext, OrdersReadDbContext>(
    configuration);

The WriteDbContext, ReadDbContext, and DbContext suffixes are removed before conversion to snake_case, so both contexts above use the orders schema. Only write DbContexts configure migration history; read DbContexts currently configure table mapping only and are not migration owners.

Use AddPostgreSqlWriteEfRepository<TWriteDbContext> when the application only needs write-side infrastructure, or AddPostgreSqlReadEfRepository<TReadDbContext> when it only needs read-side infrastructure.

Repositories are registered automatically as scoped services. Keep write repositories in the write DbContext assembly and read repositories in the read DbContext assembly.

Use concrete, non-generic internal or public implementations and internal or public non-generic interfaces derived from IRepository<TId, TAggregateRoot> or IReadRepository<TId>. Register each repository contract only once.

Each write registration exposes its IUnitOfWork under the write DbContext type as a keyed service:

var unitOfWork = serviceProvider.GetRequiredKeyedService<IUnitOfWork>(
    typeof(AppWriteDbContext));

Persistence infrastructure does not register a non-keyed IUnitOfWork. A host-level mediator or messaging runtime can expose its own non-keyed proxy that resolves the keyed Unit of Work for the active module.

Usage

Write Repository

using Microsoft.EntityFrameworkCore.Metadata.Builders;

using PANiXiDA.Core.Application.Persistence;
using PANiXiDA.Core.Domain.Abstractions;
using PANiXiDA.Core.Domain.AggregateRoots;
using PANiXiDA.Core.Domain.Identifiers;
using PANiXiDA.Core.Infrastructure.Persistence.Ef.Write;

public readonly record struct OrderId(Guid Value) : IStronglyTypedId;

public sealed class Order(OrderId id) : AggregateRoot<OrderId>(id)
{
    public string Number { get; private set; } = string.Empty;
}

public sealed class OrderConfiguration : AuditableEntityConfiguration<Order>
{
    protected override void ConfigureEntity(EntityTypeBuilder<Order> builder)
    {
        builder.HasKey(order => order.Id);
        builder.Property(order => order.Id)
            .HasConversion(id => id.Value, value => new OrderId(value));
        builder.Property(order => order.Number).HasMaxLength(64).IsRequired();
    }
}

public interface IOrderRepository : IRepository<OrderId, Order>
{
}

public sealed class OrderRepository(
    AppWriteDbContext dbContext,
    IAggregateTracker aggregateTracker)
    : EfRepository<AppWriteDbContext, OrderId, Order>(dbContext, aggregateTracker), IOrderRepository
{
}

WriteDbContext applies concrete IEntityTypeConfiguration<TEntity> implementations from its assembly through the bundled source generator. Keep configuration and entity types internal or public, and give configurations a parameterless constructor; non-public constructors are supported. Abstract types, open generic types and configurations without a parameterless constructor are skipped. No attributes, partial classes or additional registration calls are needed. Overrides of ConfigureEntity, ConfigureAudit, ConfigureSoftDelete and IsSoftDeleteEnabled work as usual.

EfRepository persists aggregate roots through AddAsync, UpdateAsync, and DeleteAsync, and tracks touched aggregate roots through IAggregateTracker. The built-in write registration adds the generic AggregateTracker implementation as scoped, but consumers can register their own tracker before calling the EF registration methods. When a unit-of-work transaction is active, repository saves participate in that transaction and EfUnitOfWork commits or rolls it back.

Read Models

using PANiXiDA.Core.Infrastructure.Persistence.Ef.Read.Mapping;
using PANiXiDA.Core.Infrastructure.Persistence.Ef.Read.Models;
using PANiXiDA.Core.Infrastructure.Persistence.Ef.Read.Sorting;
using PANiXiDA.Core.Application.Querying;
using PANiXiDA.Core.Application.Querying.Sorting;

public sealed class OrderReadDbModel : AuditableReadDbModel<Guid>
{
    public string Number { get; set; } = string.Empty;
}

public sealed record OrderReadModel(Guid Id, string Number) : IReadModel;

public sealed class OrderReadModelMapper
    : IReadModelMapper<Guid, OrderReadDbModel, OrderReadModel>
{
    public static IQueryable<OrderReadModel> ProjectTo(IQueryable<OrderReadDbModel> query)
    {
        return query.Select(order => new OrderReadModel(order.Id, order.Number));
    }
}

public sealed partial class OrderReadModelSorting : IReadModelSorting<OrderReadModel>
{
    public static SortingParameters DefaultSorting { get; } =
        SortingParameters.Ascending(nameof(OrderReadModel.Number));
}

Keep concrete, non-generic internal or public ReadDbModel<TId> types in the read DbContext project and reference this package there with analyzers enabled. The bundled generator registers them automatically and adds a typed soft-delete filter for AuditableReadDbModel<TId>; no extra startup calls or attributes are required. By default they are mapped as no-tracking models and excluded from migrations, which is useful when read models point to tables or views owned by another context.

The package generates ApplySorting for partial IReadModelSorting<TReadModel> implementations from public scalar properties, including nested paths such as department.name. At the first repeated type, scalar fields such as manager.name remain available; further nesting stops. CLR and camelCase paths are matched ignoring case. DefaultSorting is required; use SortingParameters.None for no defaults. Client criteria take precedence, and missing default fields are appended automatically.

Plain positional records are supported across assemblies, including Application read models and nested record structs. Keep read models as DTOs and perform transformations in Select, for example new Model(row.Name.ToUpper()). Referenced constructors are matched by parameter/property names and types; this relies on the DTO convention because hidden transformations cannot be verified from metadata. Nullable projections (IReadModelSorting<Model?>) use the model's field paths and null keys for null rows. For nullable root projections in EF queries, use reference types: nullable structs are supported in memory, but their SQL translation is limited by EF. Sorting and pagination run after projection; counts also use the projected query, including supported GroupBy and Distinct projections.

Read Repository

using PANiXiDA.Core.Application.Persistence;
using PANiXiDA.Core.Application.Querying.Pagination;
using PANiXiDA.Core.Application.Querying.Sorting;
using PANiXiDA.Core.Infrastructure.Persistence.Ef.Read;

public interface IOrderReadRepository : IReadRepository<Guid>
{
    Task<OrderReadModel?> GetByIdAsync(Guid id, CancellationToken cancellationToken);

    Task<PaginationResult<OrderReadModel>> GetPageAsync(
        PaginationParameters pagination,
        SortingParameters sortingParameters,
        CancellationToken cancellationToken);
}

public sealed class OrderReadRepository(AppReadDbContext dbContext)
    : EfReadRepository<AppReadDbContext, Guid, OrderReadDbModel>(dbContext), IOrderReadRepository
{
    public Task<OrderReadModel?> GetByIdAsync(Guid id, CancellationToken cancellationToken)
    {
        return GetByIdAsync<OrderReadModel, OrderReadModelMapper>(id, cancellationToken);
    }

    public Task<PaginationResult<OrderReadModel>> GetPageAsync(
        PaginationParameters pagination,
        SortingParameters sortingParameters,
        CancellationToken cancellationToken)
    {
        return GetPagedResultAsync<OrderReadModel, OrderReadModelMapper, OrderReadModelSorting>(
            Query,
            pagination,
            sortingParameters,
            cancellationToken);
    }
}

For an unpaginated list, the same sorting class applies its defaults:

var query = OrderReadModelMapper.ProjectTo(Query);
query = OrderReadModelSorting.ApplySorting(query, sortingParameters);
var items = await query.ToListAsync(cancellationToken);

Behavior Notes

  • Audit timestamps are stored as EF Core shadow properties for write entities configured through AuditableEntityConfiguration<TEntity>.
  • Added entities receive CreatedAt and UpdatedAt.
  • Modified entities receive a new UpdatedAt; CreatedAt is marked as not modified.
  • Deleted entities that have DeletedAt are converted to modified entities and receive DeletedAt and UpdatedAt.
  • AuditableReadDbModel<TId> and auditable write configurations apply a query filter that hides rows where DeletedAt is not null.
  • EfReadRepository sorts by projected read model fields through generated typed selectors. Unsupported fields and directions are rejected; sorting does not discover members through runtime reflection.

Project Structure

.
|-- src/
|   |-- PANiXiDA.Core.Infrastructure.Persistence.Ef/
|   `-- PANiXiDA.Core.Infrastructure.Persistence.Ef.Generators/
|-- tests/
|   |-- PANiXiDA.Core.Infrastructure.Persistence.Ef.IntegrationTests/
|   `-- PANiXiDA.Core.Infrastructure.Persistence.Ef.UnitTests/
|-- .github/workflows/ci.yml
|-- Directory.Build.props
|-- Directory.Build.targets
|-- Directory.Packages.props
|-- global.json
|-- version.json
|-- LICENSE
`-- README.md

Development

Build

dotnet restore
dotnet build --configuration Release

Format

dotnet format

Test

dotnet test --configuration Release

Integration tests start a PostgreSQL container through Testcontainers. Docker must be running before executing the full test suite.

To run only unit tests:

dotnet test tests/PANiXiDA.Core.Infrastructure.Persistence.Ef.UnitTests/PANiXiDA.Core.Infrastructure.Persistence.Ef.UnitTests.csproj --configuration Release

To run only integration tests:

dotnet test tests/PANiXiDA.Core.Infrastructure.Persistence.Ef.IntegrationTests/PANiXiDA.Core.Infrastructure.Persistence.Ef.IntegrationTests.csproj --configuration Release

Test With Coverage

dotnet test --configuration Release --coverage --coverage-output-format cobertura --coverage-output coverage.cobertura.xml

Pack

dotnet pack --configuration Release

Full Local Validation

dotnet restore
dotnet format
dotnet build --configuration Release
dotnet test --configuration Release
dotnet pack --configuration Release

Continuous integration

Every pull request and push to main runs formatting, tests, and mandatory SonarQube analysis. Publishing from main starts only after the SonarQube Quality Gate succeeds.

Tooling and Conventions

This repository uses:

  • .NET 10
  • Nullable enabled
  • Implicit usings enabled
  • Central package management
  • Microsoft Testing Platform
  • xUnit v3
  • FluentAssertions
  • Testcontainers for PostgreSQL integration tests
  • Nerdbank.GitVersioning

License

This project is licensed under the Apache-2.0 license.

See the LICENSE file for details.

Maintainers

Maintained by PANiXiDA.

For questions or improvements, use GitHub Issues or Pull Requests.

Product 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. 
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
4.3.1 119 9/20/2026
4.2.1 91 9/20/2026
4.1.1 121 9/20/2026
4.0.1 124 9/17/2026
3.0.2 254 8/30/2026
3.0.1 99 8/29/2026
2.0.2 109 8/28/2026
2.0.1 245 7/28/2026
1.0.7 130 7/25/2026
1.0.6 183 6/26/2026
1.0.5 209 6/13/2026
1.0.4 130 6/13/2026
1.0.3 123 6/12/2026
1.0.2 121 5/18/2026