Getting Started
What Is Pragmatic.Persistence?
Section titled “What Is Pragmatic.Persistence?”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.SourceGeneratoranalyzer
Without those two pieces, you still have the abstractions, but not the generated repositories, DbContexts, or entity members.
Activation Requirements
Section titled “Activation Requirements”For a project that wants generated persistence code, the minimum setup is:
dotnet add package Pragmatic.Persistencedotnet add package Pragmatic.Persistence.EFCoreAnd 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.
The Mental Model
Section titled “The Mental Model”Think about the stack in three layers:
- Your code Entities, DTOs, boundaries, DbContexts, and attributes that describe intent.
- Analyzer and source generator
Reads those declarations at compile time and emits
.g.csfiles. - Generated code Entity members, repositories, filters, configuration, and DI registration.
You never edit generated files directly. You change the source declarations and rebuild.
Partial Classes
Section titled “Partial Classes”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.
What Gets Generated
Section titled “What Gets Generated”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 artifact | What it contains |
|---|---|
Order.g.cs | PersistenceId, Id, interface implementation |
Order.Auditable.g.cs | CreatedAt, CreatedBy, UpdatedAt, UpdatedBy |
Order.SoftDelete.g.cs | IsDeleted, DeletedAt, DeletedBy |
Order.Create.g.cs | Static Create(...) factory |
Order.Setters.g.cs | Typed setters for private-set properties |
Order.Repository.g.cs | Nested Order.Repository class implementing IRepository<Order, Guid> with CRUD, includes, bulk methods |
EntityConfig.Order.g.cs | EF Core IEntityTypeConfiguration<Order> (host-level) |
Order.SoftDeleteFilter.g.cs | Nested Order.SoftDeleteFilter query filter |
...RegistrationExtensions.g.cs | DI registration for DbContext, repositories, query filters |
The rows above assume the project references both EF Core support and the analyzer.
Boundaries
Section titled “Boundaries”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:
DbContextgeneration is boundary-based- repositories resolve the correct
DbContextthrough keyed DI IUnitOfWorkis registered keyed by boundary
PersistenceId
Section titled “PersistenceId”Every generated entity gets a PersistenceId property.
| Entity ID type | Assignment strategy |
|---|---|
Guid | Generated Guid7 / UUID v7 |
int / long | Usually database-generated |
string | Passed in explicitly by the factory |
Id is a convenience alias to PersistenceId.
Minimal Example
Section titled “Minimal Example”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.
Next Steps
Section titled “Next Steps”- Entity System — Define entities, relationships, and attributes
- Repository — CRUD operations and specifications
- Query Pipeline — How queries flow from HTTP to SQL
- Mutation Pipeline — How mutations flow through validation and persistence
- Boundaries — Logical partitions and transaction scopes
- Query Filters — Automatic soft-delete, tenant, and visibility filtering
- Data Sources & Loading — Control tracking, includes, and filter behavior
- Diagnostics Guide — Understanding PRAG06xx/07xx diagnostics