Skip to content

Troubleshooting

Practical problem/solution guide for Pragmatic.Persistence. Each section covers a common issue, the likely causes, and the fix.


Your entity compiles, has [Entity<Guid>], but the DbContext does not include a DbSet for it.

  1. Does the entity have [BelongsTo<TBoundary>]? Without a boundary assignment, the SG does not know which DbContext should include the entity. Add the attribute.

  2. Does the host project declare a [Database] class? The DbContext is configured in the host, not in the module. Verify a class with [Database<Provider>] and either [PersistAll] or [Persist<TBoundary>] exists.

  3. Does the [Persist] attribute include your entity’s boundary? If you use [Persist<BillingBoundary>] but your entity belongs to SalesBoundary, it is excluded. Use [PersistAll] to include all boundaries, or add the correct [Persist<TBoundary>].

  4. Is the [Database] class partial? The SG generates the DbContext into a partial class. Without partial, diagnostic PRAG0602 fires.

  5. Is the SG analyzer referenced? In your .csproj, verify the Pragmatic.SourceGenerator is referenced with OutputItemType="Analyzer":

    <ProjectReference Include="..\Pragmatic.SourceGenerator\...\Pragmatic.SourceGenerator.csproj"
    OutputItemType="Analyzer"
    ReferenceOutputAssembly="false" />
  6. Is Pragmatic.Persistence.EFCore referenced? 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.


You inject IRepository<Order, Guid> but get InvalidOperationException at runtime: “No service for type has been registered.”

  1. Did you call the generated repository registration? The SG generates an extension method like AddBillingRepositories() or AddMyAppRepositories(). Verify it is called in ConfigureServices.

  2. Did you call the DbContext registration? The repository depends on the DbContext. Call AddBillingDbContext(options => ...) before the repository registration.

  3. Is the entity in a boundary? Repositories are only generated for entities that have [BelongsTo<TBoundary>].

  4. Are you injecting the right type? The repository is registered as IRepository<Order, Guid>, not as Order.Repository. If you need the concrete type, register it explicitly or resolve via IServiceProvider.

  5. Check the SG output. In Visual Studio, expand Dependencies > Analyzers > Pragmatic.SourceGenerator to see generated files. Look for Order.Repository.g.cs and _Infra.Persistence.Registration.g.cs.

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.


You added [SoftDelete] to an entity, but soft-deleted rows still appear in query results.

  1. Did you register generated query filters? Call AddMyAppQueryFilters() or AddGeneratedQueryFilters() in your service registration. Without this, the SoftDeleteFilter class exists but is never registered in DI.

  2. Is IQueryFilterToggle accidentally in Raw mode? Check if any code in the request pipeline calls filterToggle.UseMode(FilterMode.Raw) or filterToggle.DisableAll(). These disable all filters including soft-delete.

  3. Are you using raw DbContext instead of the repository? The filter pipeline runs through IQueryFilterProvider, which is called by the generated repository. If you query dbContext.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.

  4. Check the generated SoftDeleteFilter file. Look for Order.SoftDeleteFilter.g.cs in the SG output. Verify it exists and implements IQueryFilter<Order>.

  5. Is the entity actually soft-deletable? Confirm [SoftDelete] is on the entity class declaration, not just on a related entity.


[Auditable] is on the entity, but CreatedAt, CreatedBy, UpdatedAt, UpdatedBy remain default values after save.

  1. Is Pragmatic.Persistence.EFCore referenced? The AuditingInterceptor lives in the EF Core package. Without it, audit fields are generated but never populated.

  2. Is the interceptor registered? If using Composition, this is automatic. Without Composition, verify the DbContext options include the auditing interceptor.

  3. Is ICurrentUser registered in DI? The *By fields (CreatedBy, UpdatedBy) are populated from ICurrentUser. If it is not registered, these fields remain null. The *At fields (CreatedAt, UpdatedAt) still populate because they use the clock.

  4. Are you using SaveChangesAsync through the correct path? The interceptor hooks into EF Core’s SaveChangesAsync. If you bypass it (e.g., raw SQL, bulk operations without interceptor), audit fields are not set.

  5. For background jobs: If no HTTP request context exists, ICurrentUser may not be available. The *By fields will be null, which is expected. The *At fields still populate.


dotnet ef migrations add fails or the migration produces unexpected results.

Wrong migration context. Each boundary has its own migration context. Use the correct one:

Terminal window
dotnet ef migrations add InitBilling --context BillingMigrationDbContext

If you use the wrong context, entities from other boundaries may be included or missing.

Generated entity configuration not applied. The migration context must include the SG-generated IEntityTypeConfiguration<T>. Verify the generated OnModelCreating calls ApplyConfigurationsFromAssembly.

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.


A custom IQueryFilter or IPermissionBasedFilter is registered but does not affect query results.

  1. 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 like ICurrentUser.

  2. Does the filter return the correct entity type? IQueryFilter<Order> only applies to Order queries. Check the generic parameter matches your entity.

  3. Is FilterMode overriding your filter? In Admin mode, permission-based filters are skipped. In Background mode, tenant and permission filters are skipped. In Raw mode, everything is skipped.

  4. For IPermissionBasedFilter: Is ICurrentUser available? 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.

  5. Does the user have the bypass permission? IPermissionBasedFilter has a BypassPermission property. 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.

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. FilterOperator.Contains on a non-string property (e.g., int) produces a compile error or unexpected behavior. Use Equals for exact match on non-string types.

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.


DbUpdateConcurrencyException is thrown when saving an entity.

  1. Is [ConcurrencyAware] on the entity? This generates a RowVersion property used as a concurrency token. The exception is expected behavior when two concurrent writes target the same row.

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

  3. Handling the exception:

    try
    {
    await uow.SaveChangesAsync(ct);
    }
    catch (DbUpdateConcurrencyException)
    {
    // Reload entity and retry, or return ConflictError to the caller
    return new ConflictError("Order", "The order was modified by another user");
    }
  4. Bulk operations and concurrency. Bulk insert/upsert operations may bypass EF Core’s concurrency token checking. Use UpsertOptions.ConcurrencyCheck for bulk upserts if you need conflict detection.


Data from other tenants appears in query results.

  1. Does the entity implement ITenantEntity? The SG generates a tenant filter only for entities with a TenantId property that implements ITenantEntity.

  2. Is IMultiTenancyContext registered and populated? The tenant filter reads TenantId from IMultiTenancyContext. If this service is not registered or returns null, the filter has nothing to compare against.

  3. Is FilterMode set to Background or Raw? Both modes skip tenant filtering. If a background job uses these modes, it sees all tenants’ data.

  4. Are you querying the DbContext directly? The Pragmatic tenant filter runs through IQueryFilterProvider. Direct dbContext.Set<T>() queries bypass it. Ensure EF Core global query filters are configured as a safety net.

  5. Check the generated TenantFilter file. Look for {Entity}.TenantFilter.g.cs in the SG output.


IDSeverityCauseFix
PRAG0600ErrorEntity type is not partialAdd partial to the class declaration
PRAG0601ErrorEntity type referenced in attribute not foundVerify the type exists and is accessible
PRAG0602Error[Database] DbContext type is not partialAdd partial to the DbContext declaration
PRAG0603Error[ReadAccess] entity in different databaseMove entity to same database or use app-level join
PRAG0604Error[ReadAccess] owner module not includedAdd [Include<TModule>] for the owner
PRAG0610ErrorCollection navigation without [Relation.*]Replace manual navigation with [Relation.OneToMany<T>]
PRAG0611ErrorReference navigation without [Relation.*]Replace manual navigation with [Relation.ManyToOne<T>]
PRAG0612ErrorMultiple relations to same entity are ambiguousAdd .WithNavigation("...") to disambiguate
PRAG0613ErrorInverse navigation name not foundFix inverse name to match the generated navigation
PRAG0614ErrorRelation sides disagree on shapeAlign One-to-Many / Many-to-One properly
PRAG0615ErrorDuplicate navigation nameRename one relation with .WithNavigation("...")
PRAG0616WarningChild-side relation within same boundaryPrefer declaring from parent side for readability
PRAG0617InfoCross-boundary relation — FK onlyExpect FK-only behavior, no navigation property
PRAG0650WarningNavigation without FK propertyAdd FK property like CustomerId
PRAG0651WarningProperty type may need value converterAdd EF Core value converter or use provider-native type
PRAG0705WarningRequired navigation to soft-deletable entityConsider making navigation optional
PRAG0710WarningDTO navigation name mismatchAlign DTO property name with entity navigation
PRAG0711WarningInclude depth greater than 3Reduce depth or use projection
PRAG0716WarningDTO with many navigation pathsUse projection or split queries
SeverityWhat It MeansWhat to Do
ErrorThe SG cannot produce a safe modelFix before trusting generated output
WarningGeneration continues but shape may be inefficientReview and either simplify or document the tradeoff
InfoThe SG is informing you about a behavior choiceRead 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.

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.

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.

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.

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.