Skip to content

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.

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 entity
public 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...
}

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.

Three packages form the stack:

PackageRoleTarget
Pragmatic.PersistenceAttributes, interfaces, query/filter primitives, mutation contractsnet10.0
Pragmatic.Persistence.EFCoreEF Core runtime: DbContext generation, interceptors, bulk ops, query executornet10.0
Pragmatic.SourceGeneratorUnified analyzer that emits all .g.cs filesnetstandard2.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.

Terminal window
dotnet add package Pragmatic.Persistence
dotnet add package Pragmatic.Persistence.EFCore
dotnet 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.)

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.

[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.

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.

GuideWhat you will learn
Architecture and ConceptsMental model, entity lifecycle, decision tree for choosing patterns
Getting StartedActivation requirements, partial classes, minimal example
BoundariesHow boundaries partition entities, DbContexts, and transaction scopes
GuideWhat 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 FeaturesTemporal relations, inheritance, hierarchy, polymorphic, lifecycle, presets, batch
GuideWhat you’ll learn
RepositoryIRepository<T, TId>, concrete repos, specifications, IUnitOfWork, bulk operations
MutationsMutation<T>, ApplyTo(), collection strategies, modes
Patch[Patch<T>], property-set tracking, true PATCH semantics
GuideWhat you’ll learn
Query System[Query<T,R>], [Filter], [Sort], [FilterDto<T>], [Join<T>], query executor
Query FiltersSoft-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
GuideWhat you’ll learn
Query PipelineEndpoint → filters → Apply → count/page/project → result
Mutation PipelineEndpoint → validation → load/apply → persist → events
DiagnosticsPRAG06xx/07xx diagnostics, root causes, mitigations
Common MistakesWrong code → right code for the most common pitfalls
TroubleshootingChecklists for generation, DI, filter, query, and migration issues

DbContext generation · Repository internals · Entity configuration · Interceptors & runtime · Bulk operations · Filter pipeline · Testing · Migration patterns

  • .NET 10.0+
  • Pragmatic.SourceGenerator analyzer

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).