RICH.Persistence 1.1.1

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

RICH.Persistence

A lightweight, asynchronous EF Core persistence abstraction for CQRS applications — built with clean separation between read and write repositories, criteria-based filtering, and built-in pagination support.


📦 Installation

dotnet add package RICH.Persistence

🚀 Quick Start

1. Register in Program.cs

using RICH.Persistence.Extensions;

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddDbContext<AppDbContext>(...);

// Register repositories
builder.Services.AddRichPersistence();

2. Inject and use repositories

public class GetUsersHandler
{
    private readonly IReadRepository<User, int> _readRepository;

    public GetUsersHandler(IReadRepository<User, int> readRepository)
        => _readRepository = readRepository;

    public async Task<PagedList<User>> HandleAsync(int page, int size, CancellationToken ct)
    {
        return await _readRepository.GetListAsync(page, size, tracking: false, ct);
    }
}

🧱 Architecture Overview

Layer Purpose
IReadRepository Query operations (Get, List, Criteria, Projection, Paging)
IWriteRepository Commands (Create, Update, Delete, Track, SaveChanges)
Criteria<T> Encapsulates query logic (filters, includes, ordering)
PagedList<T> Immutable paged result with total count and metadata

Repositories are async-only, generic, and CQRS-friendly, designed for clean separation of query and command concerns.


⚙️ Core Features

✅ Asynchronous EF Core queries
✅ Read/write repository separation
✅ Criteria pattern for filtering and includes
✅ Built-in paging with total count and navigation metadata
✅ Strongly-typed projection support
✅ No dependency on UoW or transactions (lightweight)
✅ Fully DI-compatible


🧩 Example Usage

1. Basic paging (no filters or projection)

PagedList<User> users = await _readRepository.GetListAsync(
    page: 1,
    size: 20,
    tracking: false
);

2. Filtered paging (using condition)

PagedList<User> users = await _readRepository.GetListAsync(
    page: 1,
    size: 10,
    condition: u => u.IsActive,
    tracking: false
);

3. Criteria-based paging

var criteria = new UserCriteriaBuilder()
    .IncludeRoles()
    .WhereActive()
    .Build();

PagedList<User> users = await _readRepository.GetListAsync(
    page: 1,
    size: 10,
    criteria: criteria
);

4. Projection (to DTOs)

PagedList<UserDto> users = await _readRepository.GetListAsync(
    page: 1,
    size: 10,
    projecter: u => new UserDto { Id = u.Id, Name = u.Name }
);

5. Filtered + Projection paging

PagedList<UserDto> users = await _readRepository.GetListAsync(
    page: 2,
    size: 5,
    condition: u => u.IsActive && u.LastLogin > DateTime.UtcNow.AddDays(-30),
    projecter: u => new UserDto
    {
        Id = u.Id,
        Name = u.FirstName + " " + u.LastName,
        RoleCount = u.Roles.Count
    },
    tracking: false
);

6. Criteria + Projection paging

var criteria = new UserCriteriaBuilder()
    .WhereActive()
    .IncludeRoles()
    .Build();

PagedList<UserDto> users = await _readRepository.GetListAsync(
    page: 1,
    size: 10,
    criteria: criteria,
    projecter: u => new UserDto
    {
        Id = u.Id,
        Name = u.FirstName + " " + u.LastName,
        Roles = u.Roles.Select(r => r.Name).ToList()
    },
    tracking: false
);

🧬 PagedList<T> Example

{
  "list": [
    { "id": 1, "name": "Alice" },
    { "id": 2, "name": "Bob" }
  ],
  "page": 1,
  "size": 2,
  "totalPages": 5,
  "totalCount": 10,
  "hasPrevious": false,
  "hasNext": true
}

🧩 Criteria Example

1. Basic custom criteria

public class ActiveUsersCriteria : Criteria<User>
{
    public override IQueryable<User> Evaluate(IQueryable<User> query)
        => query.Where(u => u.IsActive)
                .Include(u => u.Roles);
}

Usage:

var criteria = new ActiveUsersCriteria();
var result = await _readRepository.GetListAsync(page: 1, size: 10, criteria);

2. Domain-specific CriteriaBuilder subclass

public class UserCriteriaBuilder : CriteriaBuilder<User>
{
    public UserCriteriaBuilder WhereActive()
    {
        With(q => q.Where(u => u.IsActive));
        return this;
    }

    public UserCriteriaBuilder IncludeRoles()
    {
        With(q => q.Include(u => u.Roles));
        return this;
    }

    public UserCriteriaBuilder IncludeDepartment()
    {
        With(q => q.Include(u => u.Department));
        return this;
    }

    public UserCriteriaBuilder CreatedWithinDays(int days)
    {
        var since = DateTime.UtcNow.AddDays(-days);
        With(q => q.Where(u => u.CreatedAt >= since));
        return this;
    }
}

Usage:

var criteria = new UserCriteriaBuilder()
    .WhereActive()
    .IncludeRoles()
    .CreatedWithinDays(30)
    .Build();

PagedList<User> users = await _readRepository.GetListAsync(1, 10, criteria);

3. Combine criteria dynamically

public class ExtendedUserCriteriaBuilder : UserCriteriaBuilder
{
    public ExtendedUserCriteriaBuilder IncludeDepartmentAndRoles()
    {
        With(new RawCriteria<User>(q => q.Include(u => u.Department)
                                         .Include(u => u.Roles)));
        return this;
    }
}

Usage:

var criteria = new ExtendedUserCriteriaBuilder()
    .WhereActive()
    .IncludeDepartmentAndRoles()
    .Build();

PagedList<User> users = await _readRepository.GetListAsync(1, 10, criteria);

4. Criteria + projection

var criteria = new UserCriteriaBuilder()
    .WhereActive()
    .IncludeRoles()
    .Build();

PagedList<UserDto> users = await _readRepository.GetListAsync(
    page: 1,
    size: 10,
    criteria: criteria,
    projecter: u => new UserDto
    {
        Id = u.Id,
        Name = u.FirstName + " " + u.LastName,
        Roles = u.Roles.Select(r => r.Name).ToList()
    }
);

5. Multiple builder variants for different use cases

public class InactiveUserCriteriaBuilder : CriteriaBuilder<User>
{
    public InactiveUserCriteriaBuilder WithoutRoles()
    {
        With(q => q.Where(u => !u.IsActive));
        return this;
    }
}

public class UserWithDepartmentCriteriaBuilder : CriteriaBuilder<User>
{
    public UserWithDepartmentCriteriaBuilder IncludeDepartment()
    {
        With(q => q.Include(u => u.Department));
        return this;
    }
}

Usage:

var inactiveUsersCriteria = new InactiveUserCriteriaBuilder()
    .WithoutRoles()
    .Build();

var deptUsersCriteria = new UserWithDepartmentCriteriaBuilder()
    .IncludeDepartment()
    .Build();

List<User> inactive = await _readRepository.GetListAsync(inactiveUsersCriteria);
List<User> withDept = await _readRepository.GetListAsync(deptUsersCriteria);

6. CriteriaBuilder in CQRS query handler

public record GetRecentActiveUsersQuery(int Page, int Size) : IRequest<PagedList<UserDto>>;

public class GetRecentActiveUsersHandler : IRequestHandler<GetRecentActiveUsersQuery, PagedList<UserDto>>
{
    private readonly IReadRepository<User, int> _readRepository;

    public GetRecentActiveUsersHandler(IReadRepository<User, int> readRepository)
        => _readRepository = readRepository;

    public async Task<PagedList<UserDto>> Handle(GetRecentActiveUsersQuery request, CancellationToken ct)
    {
        var criteria = new UserCriteriaBuilder()
            .WhereActive()
            .CreatedWithinDays(7)
            .IncludeRoles()
            .Build();

        return await _readRepository.GetListAsync(
            page: request.Page,
            size: request.Size,
            criteria: criteria,
            projecter: u => new UserDto
            {
                Id = u.Id,
                FullName = u.FirstName + " " + u.LastName,
                Email = u.Email
            },
            tracking: false,
            ct
        );
    }
}

Design Philosophy

Principle Description
Async-first All database operations are async-only.
Lightweight No UoW, no transactions; direct EF Core usage.
Composable Criteria and expression-based filtering.
Extensible You can override or extend repositories per aggregate.
Performant Uses server-side Select, Skip, and Take in single SQL query.

📄 License

MIT © 2025 Rəşad Məmmədov


Summary

Feature Status
Async-only methods
Criteria support
Paging
Projection
Dependency injection
NuGet-ready

RICH.Persistence provides a clean, extensible EF Core abstraction layer — ideal for CQRS and Domain-Driven Design applications.

Product Compatible and additional computed target framework versions.
.NET net8.0 is compatible.  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. 
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
1.1.1 749 11/13/2025
1.0.5 219 10/21/2025
1.0.4 200 10/20/2025
1.0.3 221 10/6/2025