Skip to content

Getting Started

Pragmatic.Persistence is the core contract layer for the Pragmatic persistence stack.

It gives you:

  • entity and relationship attributes
  • repository and unit-of-work interfaces
  • query/filter primitives
  • lifecycle hooks and supporting types

Generated persistence code is emitted when the consuming project also references:

  • Pragmatic.Persistence.EFCore
  • the Pragmatic.SourceGenerator analyzer

Without those two pieces, you still have the abstractions, but not the generated repositories, DbContexts, or entity members.

For a project that wants generated persistence code, the minimum setup is:

Terminal window
dotnet add package Pragmatic.Persistence
dotnet add package Pragmatic.Persistence.EFCore

And an analyzer reference:

<ProjectReference Include="..\Pragmatic.SourceGenerator\src\Pragmatic.SourceGenerator\Pragmatic.SourceGenerator.csproj"
OutputItemType="Analyzer"
ReferenceOutputAssembly="false" />

If you consume published packages instead of project references, add the analyzer package Pragmatic.SourceGenerator to the application project as well.

Think about the stack in three layers:

  1. Your code Entities, DTOs, boundaries, DbContexts, and attributes that describe intent.
  2. Analyzer and source generator Reads those declarations at compile time and emits .g.cs files.
  3. Generated code Entity members, repositories, filters, configuration, and DI registration.

You never edit generated files directly. You change the source declarations and rebuild.

Every generated persistence type must be declared as partial.

[Entity<Guid>]
public partial class Order
{
public string Name { get; private set; } = "";
}

If you forget partial, diagnostics such as PRAG0600 stop generation before the code becomes inconsistent.

Given an entity like this:

[Entity<Guid>]
[Auditable]
[SoftDelete]
[BelongsTo<SalesBoundary>]
public partial class Order
{
[LogicKey]
public string OrderNumber { get; private set; } = "";
public decimal Total { get; private set; }
}

The generator can emit:

Generated artifactWhat it contains
Order.g.csPersistenceId, Id, interface implementation
Order.Auditable.g.csCreatedAt, CreatedBy, UpdatedAt, UpdatedBy
Order.SoftDelete.g.csIsDeleted, DeletedAt, DeletedBy
Order.Create.g.csStatic Create(...) factory
Order.Setters.g.csTyped setters for private-set properties
Order.Repository.g.csNested Order.Repository class implementing IRepository<Order, Guid> with CRUD, includes, bulk methods
EntityConfig.Order.g.csEF Core IEntityTypeConfiguration<Order> (host-level)
Order.SoftDeleteFilter.g.csNested Order.SoftDeleteFilter query filter
...RegistrationExtensions.g.csDI registration for DbContext, repositories, query filters

The rows above assume the project references both EF Core support and the analyzer.

A boundary is the logical group that binds entities, repositories, and unit-of-work scope together.

public sealed class SalesBoundary;
[Entity<Guid>]
[BelongsTo<SalesBoundary>]
public partial class Order
{
public decimal Total { get; private set; }
}

Boundaries matter because:

  • DbContext generation is boundary-based
  • repositories resolve the correct DbContext through keyed DI
  • IUnitOfWork is registered keyed by boundary

Every generated entity gets a PersistenceId property.

Entity ID typeAssignment strategy
GuidGenerated Guid7 / UUID v7
int / longUsually database-generated
stringPassed in explicitly by the factory

Id is a convenience alias to PersistenceId.

using Microsoft.Extensions.DependencyInjection;
using Pragmatic.Persistence.Repository;
public sealed class StoreBoundary;
[Entity<Guid>]
[BelongsTo<StoreBoundary>]
public partial class Product
{
[LogicKey]
public string Sku { get; private set; } = "";
public string Name { get; private set; } = "";
public decimal Price { get; private set; }
}
public sealed class ProductService(
IRepository<Product, Guid> products,
IServiceProvider services)
{
public async Task<Guid> CreateProduct(string sku, string name, decimal price, CancellationToken ct)
{
var product = Product.Create(sku, name, price);
products.Add(product);
var uow = services.GetRequiredKeyedService<IUnitOfWork>(typeof(StoreBoundary));
await uow.SaveChangesAsync(ct);
return product.PersistenceId;
}
public Task<Product?> FindBySku(string sku, CancellationToken ct)
=> products.GetBySkuAsync(sku, ct);
}

Why the concrete repository in the example:

  • GetBySkuAsync(...) is generated on the concrete repository class
  • the stable interfaces stay intentionally smaller

Use IRepository<T, TId> / IReadRepository<T, TId> when you want your application code to depend only on the stable contract surface.