Troubleshooting
Practical problem/solution guide for Pragmatic.Persistence. Each section covers a common issue, the likely causes, and the fix.
Entity Not Appearing in DbContext
Section titled “Entity Not Appearing in DbContext”Your entity compiles, has [Entity], but the DbContext does not include a DbSet for it.
Checklist
Section titled “Checklist”-
Does the entity have
[BelongsTo<TBoundary>]? Without a boundary assignment, the SG does not know which DbContext should include the entity. Add the attribute. -
Does the host declare a database? A class deriving from
PragmaticDatabaseand carrying[PragmaticDatabase(Provider = …, ConfigKey = …)]. The topology is a host decision, not a module one. -
Does the host place your entity’s module on it?
[Include<TModule, TDatabase>]on the host module. A module nobody includes has no database, so nothing is generated for its entities. -
Is
ConfigKeyset? Left empty, the host looks up an empty configuration key at startup, getsnull, and fails with an exception that names something else entirely. -
Is the SG analyzer referenced? In your
.csproj, verify the Pragmatic.SourceGenerator is referenced withOutputItemType="Analyzer":<ProjectReference Include="..\Pragmatic.SourceGenerator\...\Pragmatic.SourceGenerator.csproj"OutputItemType="Analyzer"ReferenceOutputAssembly="false" /> -
Is
Pragmatic.Persistence.EFCorereferenced? The repository and entity configuration are only generated when the EF Core runtime package is referenced. Without it, you get basic entity members but no DbContext integration.
Repository Not Resolving from DI
Section titled “Repository Not Resolving from DI”You inject IRepository<Order> but get InvalidOperationException at runtime: “No service for type has been registered.”
Checklist
Section titled “Checklist”-
Are you inside a Pragmatic host? If so this should not happen: the generated host calls
RegisterAllRepositories()itself. Check thatPragmaticApp.RunAsync()is what starts the app. -
Outside a host, did you call the registration? It is
services.AddPragmaticPersistenceRepositories<BillingDbContext>(), one generic method per DbContext, not a per-boundaryAdd{Boundary}Repositories(). That name does not exist. -
Did you register the DbContext first?
AddBillingDbContext(options => …), orAddAllPragmaticDbContexts(options => …). The repository registration resolvesTDbContextfrom the container. -
Is the entity in a boundary? Repositories are only generated for entities with
[BelongsTo<TBoundary>]. -
Which type are you injecting? All three are registered and resolve to the same scoped instance: the concrete
Order.Repository,IRepository<Order>andIReadRepository<Order>. Nothing needs registering by hand. -
Check the SG output. In Visual Studio, expand Dependencies > Analyzers > Pragmatic.SourceGenerator. The files are
{Namespace}.Order.Repository.g.csand_Infra.Persistence.RepositoryRegistration.g.cs.
If Using Composition
Section titled “If Using Composition”When you use Pragmatic.Composition with PragmaticApp.RunAsync(), repository and DbContext registration is automatic – the SG-generated host calls the registration methods. If repositories still do not resolve, verify the Composition package is referenced and PragmaticApp.RunAsync() is called.
Soft Delete Not Filtering
Section titled “Soft Delete Not Filtering”You added [SoftDelete] to an entity, but soft-deleted rows still appear in query results.
Checklist
Section titled “Checklist”-
Did you register generated query filters? Call
AddMyAppQueryFilters()orAddGeneratedQueryFilters()in your service registration. Without this, theSoftDeleteFilterclass exists but is never registered in DI. -
Is
IQueryFilterToggleaccidentally inRawmode? Check if any code in the request pipeline callsfilterToggle.UseMode(FilterMode.Raw)orfilterToggle.DisableAll(). These disable all filters including soft-delete. -
Are you using raw
DbContextinstead of the repository? The filter pipeline runs throughIQueryFilterProvider, which is called by the generated repository. If you querydbContext.Set<Order>()directly, you bypass the Pragmatic filter pipeline. EF Core global query filters (HasQueryFilter) serve as a fallback but must be configured in the entity configuration. -
Check the generated SoftDeleteFilter file. Look for
Order.SoftDeleteFilter.g.csin the SG output. Verify it exists and implementsIQueryFilter<Order>. -
Is the entity actually soft-deletable? Confirm
[SoftDelete]is on the entity class declaration, not just on a related entity.
Audit Fields Not Populating
Section titled “Audit Fields Not Populating”[Auditable] is on the entity, but CreatedAt, CreatedBy, UpdatedAt, UpdatedBy remain default values after save.
Checklist
Section titled “Checklist”-
Is
Pragmatic.Persistence.EFCorereferenced? TheAuditingInterceptorlives in the EF Core package. Without it, audit fields are generated but never populated. -
Is the interceptor registered? If using Composition, this is automatic. Without Composition, verify the DbContext options include the auditing interceptor.
-
Is
ICurrentUserregistered in DI? The*Byfields (CreatedBy,UpdatedBy) are populated fromICurrentUser. If it is not registered, these fields remainnull. The*Atfields (CreatedAt,UpdatedAt) still populate because they use the clock. -
Are you using
SaveChangesAsyncthrough the correct path? The interceptor hooks into EF Core’sSaveChangesAsync. If you bypass it (e.g., raw SQL, bulk operations without interceptor), audit fields are not set. -
For background jobs: If no HTTP request context exists,
ICurrentUsermay not be available. The*Byfields will benull, which is expected. The*Atfields still populate.
Migration Errors
Section titled “Migration Errors”dotnet ef migrations add fails or the migration produces unexpected results.
Possible Causes
Section titled “Possible Causes”You are probably not meant to be running dotnet ef migrations add at all. Pragmatic.Migrations
reads the generated schema and applies a declarative diff at startup: no migration files, no
Add-Migration. See the migrations guide.
Wrong migration context. The migration context is per database, not per boundary: boundaries
sharing a connection share the schema, so they share the context that describes it. It is named after
the database (SalesDatabaseMigrationDbContext), or plain MigrationDbContext when no per-database
topology is declared.
Generated entity configuration not applied. The migration context must include the SG-generated
IEntityTypeConfiguration<T>. Verify the generated OnModelCreating applies them.
Cross-boundary FK without matching entity. If an entity references another entity in a different boundary, the FK column is generated but the referenced entity is not in the same DbContext. This can cause “entity type not found” warnings during migration. This is expected – the FK column is a raw Guid/int column without a foreign key constraint across boundaries.
Pending model changes. If you add an attribute (like [Auditable]) and rebuild, the SG generates new properties. Run dotnet ef migrations add to capture these changes. If you forget, the database schema diverges from the model.
Query Filter Not Applying
Section titled “Query Filter Not Applying”A custom IQueryFilter or IPermissionBasedFilter is registered but does not affect query results.
Checklist
Section titled “Checklist”-
Is the filter registered with the correct DI lifetime? Query filters should be
Scoped:services.AddScoped<IQueryFilter, TeamOrdersFilter>();If registered as
Singleton, the filter cannot access scoped services likeICurrentUser. -
Does the filter return the correct entity type?
IQueryFilter<Order>only applies toOrderqueries. Check the generic parameter matches your entity. -
Is
FilterModeoverriding your filter? The modes are cumulative:Adminskips visibility and permission filters,Elevatedalso skips data-scope ones,Backgroundalso skips the tenant filter, andRawskips everything including soft-delete. -
For
IPermissionBasedFilter: IsICurrentUseravailable? If no authenticated user exists in the current scope (background job, migration), permission-based filters are automatically skipped. This is by design – to prevent crashes in non-HTTP contexts. -
Does the user have the bypass permission?
IPermissionBasedFilterhas aBypassPermissionproperty. If the current user has that permission, the filter is skipped for them.
GridFilter / Query Returning Wrong Results
Section titled “GridFilter / Query Returning Wrong Results”The [Filter] or [Filterable] properties produce unexpected SQL or return the wrong rows.
Possible Causes
Section titled “Possible Causes”Property name does not match entity property. The SG derives the entity property from the filter property name. CustomerNameSort maps to CustomerName (removes “Sort” suffix). If the entity property is Name but your filter is CustomerName, use MapTo:
[Filter(MapTo = "Customer.Name")]public string? CustomerName { get; init; }Wrong operator for the data type. It is not a compile error and not a runtime error: the generated
switch renders Contains/StartsWith/EndsWith only for a string and In only for a collection,
and everything else falls through to ==. So FilterOperator.Contains on an int? builds, runs, and
quietly filters by equality. Between, which would disappear the same way, is PRAG0701, an
error.
Required filter property is null. If a filter property is required but the caller sends null, the filter is always applied with the default value (empty string, zero), potentially returning no results.
ComplexFilter JSON not parsing. [ComplexFilter] properties expect JSON in the query string. Verify the JSON is URL-encoded and matches the [FilterDto<T>] shape.
Sort priority confusion. When multiple [Sort] properties have values, the SG applies them in Priority order (lower first). If Priority is not set, the order is declaration order.
Concurrency Conflict
Section titled “Concurrency Conflict”DbUpdateConcurrencyException is thrown when saving an entity.
Checklist
Section titled “Checklist”-
Is
[ConcurrencyAware]on the entity? It declares aRowVersionshadow property (nothing is added to your entity), configured as a concurrency token in the generated DbContext, and provider-specific: a nativerowversionon SQL Server, thexminsystem column on PostgreSQL. -
Are you loading stale data? If you load an entity, hold it for a long time (e.g., across an HTTP request-response cycle), and then save, another request may have modified it. Reload the entity before saving, or implement retry logic.
-
Where it surfaces depends on how you save. Through the generated repository it is a
Result, not an exception: on a[ConcurrencyAware]entitySaveChangesAsyncreturnsResult<int, ConcurrencyError>(Code = "CONCURRENCY_CONFLICT", status 409):var saved = await repository.SaveChangesAsync(ct);return saved.Match(success: _ => Results.NoContent(),failure: e => Results.Problem(statusCode: e.StatusCode, detail: e.Message));Through
IUnitOfWork.SaveChangesAsynctheDbUpdateConcurrencyExceptiontravels out as-is, on purpose: a stale row is two writers meeting, not a rule the schema enforces, so it is not reclassified alongside constraint violations. -
Bulk operations and concurrency. Bulk insert/upsert operations may bypass EF Core’s concurrency token checking. Use
UpsertOptions.ConcurrencyCheckfor bulk upserts if you need conflict detection.
Multi-Tenant Data Leaking
Section titled “Multi-Tenant Data Leaking”Data from other tenants appears in query results.
Checklist
Section titled “Checklist”-
Does the entity implement
ITenantEntity? The SG generates a tenant filter only for entities with aTenantIdproperty that implementsITenantEntity. -
Is
ITenantContextregistered and populated? That is the interface the generated filter takes in its constructor:Pragmatic.MultiTenancy.ITenantContext, notIMultiTenancyContext. Unregistered, the filter cannot even be constructed; returningnull, it has nothing to compare against. -
Is
FilterModeatBackgroundor above?BackgroundandRawskip tenant filtering;Normal,AdminandElevateddo not. A background job inBackgroundmode sees every tenant’s data, which is the point of the mode and the risk of it. -
Are you querying the DbContext directly? The Pragmatic tenant filter runs through
IQueryFilterProvider. DirectdbContext.Set<T>()queries bypass it. Ensure EF Core global query filters are configured as a safety net. -
Check the generated TenantFilter file. Look for
{Namespace}.{Entity}.TenantFilter.g.csin the SG output; the filter is a class nested in the entity,{Entity}.TenantFilter.
Diagnostics Reference
Section titled “Diagnostics Reference”Persistence Diagnostics (PRAG06xx)
Section titled “Persistence Diagnostics (PRAG06xx)”| ID | Severity | Cause | Fix |
|---|---|---|---|
| PRAG0600 | Error | A persistence-attributed type is not partial |
Add partial to the class declaration |
| PRAG0602 | Error | A generated DbContext type is not partial |
Add partial to the DbContext declaration |
| PRAG0612 | Warning | Several relations to one entity collide on a derived navigation name | .WithNavigation("...") on each |
| PRAG0613 | Error | Inverse matches no navigation on the target |
Name an existing one, or declare the other side |
| PRAG0614 | Error | The inverse navigation has the wrong type | Point it at the navigation that comes back here |
| PRAG0615 | Error | Two relations would generate the same navigation | Rename one |
| PRAG0620 | Warning | State machine has no [InitialState] |
Mark one enum value [InitialState] |
| PRAG0621 | Warning | State is unreachable | Add a [TransitionFrom(...)], or mark it [InitialState] |
| PRAG0622 | Error | [TransitionFrom(...)] names a non-existent enum member |
Use a real enum member |
| PRAG0623 | Error | The state machine governs a property the entity lacks | Property = nameof(...); defaults to Status |
| PRAG0624 | Error | A trait’s properties are only partly declared by hand | Declare them all, or none |
| PRAG0651 | Warning | Property type may need value converter | Add EF Core value converter or use provider-native type |
| PRAG0701 | Error | FilterOperator.Between, which no generator renders |
Two properties over one column: GreaterOrEqual + LessOrEqual, same MapTo |
| PRAG0702 | Error | [CascadeOn<TSource>] without the {Source}Id foreign key |
Add the FK, or declare the relation |
| PRAG0703 | Warning | An attribute argument nothing consumes | Remove it; the message names what does work |
| PRAG0704 | Error | The query’s result type has no Projection |
[GenerateProjection] beside [MapFrom<TEntity>] on the DTO |
| PRAG0705 | Warning | A required navigation points at a [SoftDelete] entity |
Required = false, or drop [SoftDelete], or IgnoreQueryFilters() |
| PRAG0706 | Warning | [ReadAccess] crosses a database |
Read through that boundary’s operations |
| PRAG0710 | Warning | DTO navigation name mismatch | Align DTO property name with entity navigation |
| PRAG0711 | Warning | Include depth greater than 3 | Reduce depth or use projection |
| PRAG0716 | Warning | DTO with many navigation paths | Use projection or split queries |
| PRAG1100 | Error | [HasOwner] on a non-partial type |
Add partial |
| PRAG1104 | Info | OwnerId is declared manually |
Nothing required; drop it to use the generated property |
PRAG0600 and PRAG0602 are reported by Pragmatic.SourceGenerator.Analyzers, which is what makes
the “make class partial” quick fix available in the IDE.
Analyzer Diagnostics over your own code (PRAG068x)
Section titled “Analyzer Diagnostics over your own code (PRAG068x)”| ID | Severity | Cause | Fix |
|---|---|---|---|
| PRAG0680 | Warning | new Entity() instead of the generated factory |
Entity.Create(...) |
| PRAG0681 | Warning | default used for an entity |
Entity.Create(...) |
| PRAG0682 | Warning | Activator.CreateInstance used for an entity |
Entity.Create(...) |
| PRAG0683 | Warning | Entity declares a behavior method | Move the state change into a mutation or domain action |
| PRAG0684 | Warning | Non-constant SQL in FromSqlRaw/ExecuteSqlRaw |
Use interpolated FromSql/ExecuteSql, or pass parameters |
| PRAG0685 | Info | Aggregate with many child collections | Split it, or reference other aggregates by id |
| PRAG0686 | Warning | Injecting another boundary’s DbContext |
Call that boundary’s actions/queries instead |
| PRAG0687 | Warning | ExecuteDelete on a [SoftDelete] entity |
It never reaches the change tracker: the row is really gone |
Reading Diagnostics
Section titled “Reading Diagnostics”| Severity | What It Means | What to Do |
|---|---|---|
| Error | The SG cannot produce a safe model | Fix before trusting generated output |
| Warning | Generation continues but shape may be inefficient | Review and either simplify or document the tradeoff |
| Info | The SG is informing you about a behavior choice | Read once so the result does not surprise you later |
Can I use multiple boundaries for the same database?
Section titled “Can I use multiple boundaries for the same database?”Yes. Multiple boundaries can share the same connection string – they just have separate DbContexts. This gives you logical isolation (separate DbSets, separate migration history) while sharing the physical database.
How do I see the generated SQL?
Section titled “How do I see the generated SQL?”Enable EF Core logging:
optionsBuilder.LogTo(Console.WriteLine, LogLevel.Information);Or in appsettings.Development.json:
{ "Logging": { "LogLevel": { "Microsoft.EntityFrameworkCore.Database.Command": "Information" } }}Why does my entity get PersistenceId instead of just Id?
Section titled “Why does my entity get PersistenceId instead of just Id?”PersistenceId is the technical identifier used for database persistence. Id is a convenience alias (Guid Id => PersistenceId). The distinction keeps the naming clear: PersistenceId is the column in the database, Id is what your code uses. Both point to the same value.
Can I override the generated EF Core configuration?
Section titled “Can I override the generated EF Core configuration?”Yes. Create a custom IEntityTypeConfiguration<T> in your project. EF Core’s ApplyConfigurationsFromAssembly will pick it up. If you need to override specific generated configuration, apply your overrides after the generated configuration runs.
How do I restore a soft-deleted entity?
Section titled “How do I restore a soft-deleted entity?”Use MutationMode.Restore:
[Mutation(Mode = MutationMode.Restore)][Endpoint(HttpVerb.Post, "api/v1/orders/{id}/restore")]public partial class RestoreOrderMutation : Mutation<Order>{ public required Guid Id { get; init; }}This bypasses all filters (including soft-delete), loads the entity, and resets IsDeleted, DeletedAt, and DeletedBy.
Why does my repository return stale data?
Section titled “Why does my repository return stale data?”If you use AsNoTracking() (the default for queries), the data is a snapshot at query time. For mutations, ensure the entity is loaded within the same scope as the save. If you cache entity references across scopes, they may become stale.
How do I debug generated code?
Section titled “How do I debug generated code?”All generated files live under obj/Debug/net10.0/generated/. In Visual Studio, expand Dependencies > Analyzers > Pragmatic.SourceGenerator in Solution Explorer. You can set breakpoints in generated .g.cs files.
Getting Help
Section titled “Getting Help”- GitHub Issues: github.com/pragmatic-design/Pragmatic.Design/issues
- Showcase Examples: See the
Showcase.Catalog,Showcase.Booking, andShowcase.Billingprojects for working entity, repository, query, and mutation implementations. - Diagnostics Guide: See diagnostics.md for detailed explanation of every PRAG06xx diagnostic.
- Query Pipeline: See query-pipeline.md for the full query execution flow.
- Mutation Pipeline: See mutation-pipeline.md for the full mutation execution flow.
