Pragmatic.Persistence
Source-generated persistence layer for .NET 10. Declare entities with attributes; the source generator
emits repositories, query pipelines, filters, mutations, EF Core configurations, and DI registration —
all visible in obj/, fully debuggable, zero reflection.
The Problem
Section titled “The Problem”Every EF Core project accumulates the same infrastructure per entity: identity properties, equality
overrides, audit fields, soft-delete fields, Create factories, typed setters, repository classes,
entity configuration, query filters, DI registration. For one entity with auditing, soft-delete, a
relationship, and a search query, that is ~150 lines of mechanical code. For 30 entities, ~4,500 lines
you write, maintain, and keep in sync — and every new property means touching the entity, the setter,
the factory, the configuration, the DTO mapping, and maybe the query filter.
// Without Pragmatic: ~150 lines per entitypublic class Order : IEntity<Guid>, IAuditable, ISoftDelete{ public Guid Id { get; set; } public string OrderNumber { get; private set; } = ""; public decimal Total { get; private set; } public DateTimeOffset CreatedAt { get; set; } // repeated on every auditable entity public string? CreatedBy { get; set; } public bool IsDeleted { get; set; } // repeated on every soft-deletable entity public Guid CustomerId { get; private set; } // manual FK public Customer? Customer { get; set; } // manual navigation
public static Order Create(string number, decimal total, Guid customerId) { /* ... */ } public void SetTotal(decimal value) => Total = value; // + audit/soft-delete fields, Equals, GetHashCode, Repository, EF config, query filter, DI...}The Solution
Section titled “The Solution”You declare the entity’s shape and intent. The generator produces everything else at compile time — zero reflection.
// With Pragmatic: 10 lines, everything else is generated[Entity<Guid>][Auditable][SoftDelete][BelongsTo<SalesBoundary>][Relation.OneToMany<LineItem>][Relation.ManyToOne<Customer>]public partial class Order{ [LogicKey] public string OrderNumber { get; private set; } = ""; public decimal Total { get; private set; }}From this, the generator emits: PersistenceId, Id, equality, a Create() factory, typed setters,
audit + soft-delete fields, FK + navigation properties, a nested Repository, EF Core entity
configuration, a SoftDeleteFilter, and DI registration. Rename or add a property and the generated
code follows — no drift, no silently-forgotten mapper.
Architecture
Section titled “Architecture”Three packages form the stack:
| Package | Role | Target |
|---|---|---|
| Pragmatic.Persistence | Attributes, interfaces, query/filter primitives, mutation contracts | net10.0 |
| Pragmatic.Persistence.EFCore | EF Core runtime: DbContext generation, interceptors, bulk ops, query executor | net10.0 |
| Pragmatic.SourceGenerator | Unified analyzer that emits all .g.cs files | netstandard2.0 |
Pragmatic.Persistence is the contract layer (what you declare); Pragmatic.Persistence.EFCore is the
runtime; the generator bridges them at compile time. IRepository<T, TId> and IUnitOfWork live in
Pragmatic.Abstractions (Layer 0) so other packages depend on them without the full stack. Deep dive:
Architecture and Concepts.
Installation
Section titled “Installation”dotnet add package Pragmatic.Persistencedotnet add package Pragmatic.Persistence.EFCoredotnet add package Pragmatic.SourceGenerator # the unified analyzer (ships generated code)Depending on the features you use you may also pull Pragmatic.Actions ([Mutation]),
Pragmatic.Endpoints ([Endpoint]), Pragmatic.Caching, or Pragmatic.Validation. (Building inside
this monorepo? See Monorepo Structure.)
Quick Start
Section titled “Quick Start”1. Define a boundary — it groups entities into a module; the generator creates a per-boundary DbContext.
[Boundary]public partial class CatalogBoundary;2. Declare an entity:
[Entity<Guid>][Auditable][SoftDelete][BelongsTo<CatalogBoundary>][Relation.ManyToMany<Amenity>.WithNavigation("Amenities", Inverse = "Properties")]public partial class Property : IEntity<Guid>{ [Required, LogicKey] public string Code { get; private set; } = "";
[Required] public string Name { get; private set; } = "";
[Range(1, 5)] public int StarRating { get; private set; }}The generator produces PersistenceId/Id, audit + soft-delete fields, a Create(...) factory, typed
SetXxx() setters, FK + navigation properties, a nested Repository : IRepository<Property, Guid>, EF
Core configuration, a SoftDeleteFilter, and DI registration.
3. Write a mutation (auto-maps to the entity setters; the invoker handles create/validate/persist/events):
[Mutation(Mode = MutationMode.Create)][Endpoint(HttpVerb.Post, "api/v1/amenities")]public partial class CreateAmenityMutation : Mutation<Amenity>{ public required string Name { get; init; } public AmenityCategory Category { get; init; }}4. Write a query (the generator builds the LINQ pipeline from [Filter]/[Sort]; the executor
handles paging, projection, global filters, caching):
[Query<Amenity, AmenityDto>][Endpoint(HttpVerb.Get, "api/v1/amenities/search")]public partial class SearchAmenitiesQuery{ [Filter(Operator = FilterOperator.Contains)] public string? Name { get; init; }
[Sort(DefaultDirection = 0)] public SortDirection? NameSort { get; init; }
public int Page { get; init; } = 1; public int PageSize { get; init; } = 50;}5. Use the repository:
public sealed class PropertyService( IRepository<Property, Guid> properties, [FromKeyedServices(typeof(CatalogBoundary))] IUnitOfWork uow){ public async Task<Guid> CreateProperty(string code, string name, CancellationToken ct) { var property = Property.Create(code, name); properties.Add(property); await uow.SaveChangesAsync(ct); return property.PersistenceId; }}Full walkthrough: Getting Started.
Operational note
Section titled “Operational note”[WithoutFilter<T>] is a privileged escape hatch. It bypasses the configured query filters
(tenant, soft-delete, ownership, scope) for the duration of a call. Use it only in privileged contexts
— admin tooling, background jobs, cross-tenant reports — never under a normal request principal. Treat
every call site as an authorization boundary. See Query Filters.
Status
Section titled “Status”Core entity/repository/query/mutation surface is stable within the 0.8 preview; some advanced areas are still settling. See the roadmap for what is moving before 1.0.
Documentation
Section titled “Documentation”| Guide | What you will learn |
|---|---|
| Architecture and Concepts | Mental model, entity lifecycle, decision tree for choosing patterns |
| Getting Started | Activation requirements, partial classes, minimal example |
| Boundaries | How boundaries partition entities, DbContexts, and transaction scopes |
Entity model
Section titled “Entity model”| Guide | What you’ll learn |
|---|---|
| Entity System | [Entity<TId>], PersistenceId, Create(), setters, [LogicKey], [AggregateRoot], identifiers (Guid7, OpaqueId, ShortGuid) |
| Attributes | [Auditable], [SoftDelete], [ConcurrencyAware], [Lookup], [StateMachine] |
| Relationships | [Relation.*] attributes, FK generation, cross-boundary rules |
| State Machine | [StateMachine<T>] + [TransitionFrom] — compile-time guarded status transitions |
| Advanced Features | Temporal relations, inheritance, hierarchy, polymorphic, lifecycle, presets, batch |
Data access
Section titled “Data access”| Guide | What you’ll learn |
|---|---|
| Repository | IRepository<T, TId>, concrete repos, specifications, IUnitOfWork, bulk operations |
| Mutations | Mutation<T>, ApplyTo(), collection strategies, modes |
| Patch | [Patch<T>], property-set tracking, true PATCH semantics |
Querying
Section titled “Querying”| Guide | What you’ll learn |
|---|---|
| Query System | [Query<T,R>], [Filter], [Sort], [FilterDto<T>], [Join<T>], query executor |
| Query Filters | Soft-delete, tenant, permission, visibility filters, toggle, modes |
| Grid Filtering | [GridFilter<T>], [GridAdapter<T>], DevExpress/PrimeNG adapters |
| Projections & Views | [Projectable], [ComputedFilter], [QueryView<T>], aggregations |
| Query Strategy & Loading | [QueryStrategy], strategies, [LoadWith<T>], split queries |
| Data Ownership & Visibility | [OwnedEntity], [ScopedEntity], data-level authorization |
Pipelines & diagnostics
Section titled “Pipelines & diagnostics”| Guide | What you’ll learn |
|---|---|
| Query Pipeline | Endpoint → filters → Apply → count/page/project → result |
| Mutation Pipeline | Endpoint → validation → load/apply → persist → events |
| Diagnostics | PRAG06xx/07xx diagnostics, root causes, mitigations |
| Common Mistakes | Wrong code → right code for the most common pitfalls |
| Troubleshooting | Checklists for generation, DI, filter, query, and migration issues |
EF Core implementation
Section titled “EF Core implementation”DbContext generation · Repository internals · Entity configuration · Interceptors & runtime · Bulk operations · Filter pipeline · Testing · Migration patterns
Requirements
Section titled “Requirements”- .NET 10.0+
Pragmatic.SourceGeneratoranalyzer
License
Section titled “License”Part of the Pragmatic.Design ecosystem — see Licensing. Pragmatic.Persistence is licensed under the PolyForm Small Business 1.0.0 license (free for small businesses; commercial license above the threshold).