Common Errors — Diagnostic & Resolution Guide
Module loading failures
Section titled “Module loading failures”Missing [DependsOn] attribute
Section titled “Missing [DependsOn] attribute”Symptom: InvalidOperationException at startup — a service cannot be
resolved because the module that registers it was never loaded.
System.InvalidOperationException: Unable to resolve service for type'Granit.Caching.IDistributedCacheManager' while attempting to activate'MyApp.Services.ProductService'.Cause: Your module uses a Granit service but does not declare the dependency.
Fix: Add the [DependsOn] attribute to your module class.
// Before -- missing dependencypublic sealed class MyAppModule : GranitModule { }
// After[DependsOn(typeof(GranitCachingModule))]public sealed class MyAppModule : GranitModule { }Circular dependencies
Section titled “Circular dependencies”Symptom: InvalidOperationException at startup listing the modules involved
in the cycle.
System.InvalidOperationException: Circular dependency detected:ModuleA -> ModuleB -> ModuleC -> ModuleAFix: Break the cycle by extracting shared abstractions into a separate package. The dependent modules reference the abstractions package instead of each other.
DbContext configuration issues
Section titled “DbContext configuration issues”DbContext does not derive from GranitDbContext
Section titled “DbContext does not derive from GranitDbContext”Symptom: Soft-deleted entities still appear in queries. Multi-tenant data
leaks across tenants. IActive and IPublishable filters do not apply.
Cause: The isolated DbContext derives from DbContext directly, so the
conventions pass that registers the named query filters never runs.
Fix: Derive from GranitDbContext and move entity configuration into
OnGranitModelCreating — OnModelCreating is sealed on the base class.
public class MyDbContext( DbContextOptions<MyDbContext> options, ICurrentTenant currentTenant, IDataFilter? dataFilter = null) : GranitDbContext(options, currentTenant, dataFilter){ protected override void OnGranitModelCreating(ModelBuilder modelBuilder) { modelBuilder.ApplyConfigurationsFromAssembly(typeof(MyDbContext).Assembly); }}Missing interceptors
Section titled “Missing interceptors”Symptom: CreatedAt, CreatedBy, ModifiedAt, ModifiedBy fields are never
populated. Soft-deleted entities are hard-deleted instead of flagged.
Cause: The DbContext was registered via raw AddDbContextFactory<T> (or
AddDbContext<T>) without wiring Granit’s five interceptors
(AuditedEntityInterceptor, VersioningInterceptor, ConcurrencyStampInterceptor,
DomainEventDispatcherInterceptor, SoftDeleteInterceptor).
Fix: Register the DbContext through AddGranitDbContext<T> — or
AddGranitIsolatedDbContext<T> for strictly tenant-scoped contexts (BlobStorage,
Privacy, anything carrying [Encrypted] fields). Both extensions resolve
UseGranitInterceptors(sp) internally and pre-seed HostDbSchema.
services.AddGranitDbContext<MyDbContext>(options =>{ options.UseNpgsql(connectionString);});
// Per-tenant data (PR #2021 / #2028)services.AddGranitIsolatedDbContext<MyTenantDbContext>(options =>{ options.UseNpgsql(connectionString);});Validator not executing
Section titled “Validator not executing”Missing registration
Section titled “Missing registration”Symptom: Invalid input passes through endpoints without triggering
FluentValidation rules. No validation errors are returned.
Cause: Validators are not registered in the DI container.
FluentValidationEndpointFilter<T> silently skips validation when no validator
is found for the request type.
Fix: The registration method depends on whether the module uses Wolverine.
With Wolverine (module has [assembly: WolverineHandlerModule]): Validators
are discovered automatically by AddGranitWolverine(). No manual registration
needed.
Without Wolverine: Call AddGranitValidatorsFromAssemblyContaining<T>()
manually in your module’s ConfigureServices.
public override void ConfigureServices(ServiceConfigurationContext context){ context.Services .AddGranitValidatorsFromAssemblyContaining<CreateProductRequestValidator>();}Validator in the wrong assembly
Section titled “Validator in the wrong assembly”Symptom: AddGranitValidatorsFromAssemblyContaining<T>() was called, but
some validators are still not running.
Cause: The type T used as the assembly marker is in a different assembly
from the missing validators.
Fix: Verify that the validator class lives in the same assembly as the marker type. If validators are spread across multiple assemblies, call the registration method once per assembly.
Multi-tenancy errors
Section titled “Multi-tenancy errors”TenantId is null
Section titled “TenantId is null”Symptom: DbUpdateException when inserting entities that implement
IMultiTenant — the TenantId column has a NOT NULL constraint but the value
is null.
Cause: The ICurrentTenant context was not set before the operation. This
typically happens in background jobs or message handlers where the tenant context
is not propagated automatically.
Fix: Wrap the operation in a tenant scope.
using (currentTenant.Change(tenantId)){ await dbContext.Products.AddAsync(product, cancellationToken); await dbContext.SaveChangesAsync(cancellationToken);}IsAvailable not checked
Section titled “IsAvailable not checked”Symptom: InvalidOperationException when accessing ICurrentTenant.Id —
the tenant context is not available.
Cause: Code accesses Id directly without checking IsAvailable first.
When multi-tenancy is not installed, a NullTenantContext (IsAvailable = false) is registered by default.
Fix: Always check IsAvailable before accessing Id.
if (currentTenant.IsAvailable){ var tenantId = currentTenant.Id; // tenant-specific logic}Authentication issues
Section titled “Authentication issues”JWT token rejected
Section titled “JWT token rejected”Symptom: All authenticated requests return 401 Unauthorized even with a
valid token.
Common causes:
- Wrong authority URL: The
AuthorityinJwtBearerOptionsdoes not match theissclaim in the token. - Clock skew: The server clock is ahead of the token issuer. Default
ClockSkewis 5 minutes. - Audience mismatch: The
ValidAudiencedoes not match theaudclaim.
Diagnosis: Decode the token at jwt.io and compare the
iss, aud, and exp claims against your configuration.
Keycloak claims not mapped
Section titled “Keycloak claims not mapped”Symptom: ICurrentUserService returns null or empty values for user
properties (name, email, roles) even though the token contains them.
Cause: Keycloak uses non-standard claim names by default (e.g.,
preferred_username instead of name, realm_access.roles instead of role).
Fix: Ensure GranitAuthenticationJwtBearerKeycloakModule is included in your
dependency chain. It registers the KeycloakClaimsTransformation that maps
Keycloak-specific claims to standard ClaimTypes.
[DependsOn(typeof(GranitAuthenticationJwtBearerKeycloakModule))]public sealed class MyAppModule : GranitModule { }Caching issues
Section titled “Caching issues”Redis connection refused
Section titled “Redis connection refused”Symptom: RedisConnectionException at startup or on the first cache
operation.
StackExchange.Redis.RedisConnectionException: It was not possible to connectto the redis server(s). UnableToConnect on localhost:6379/InteractiveCommon causes:
- Redis is not running or not reachable at the configured address
- TLS is required but not configured
- Password authentication is required but not provided
Diagnosis: Test connectivity with redis-cli ping from the application host.
Check the connection string in your configuration.
FusionCache not invalidating across pods
Section titled “FusionCache not invalidating across pods”Symptom: Stale data is served from cache after updates on another pod.
Common causes:
- Missing cache key invalidation: After a write operation, the
corresponding cache entry was not expired. Use
ExpireAsync(notRemoveAsync) so fail-safe can serve stale data as fallback. - Backplane not configured: The Redis backplane propagates L1 invalidation
across pods. It requires
GranitCachingStackExchangeRedisModuleto be loaded and Redis to support pub/sub.
Fix: Verify that GranitCachingStackExchangeRedisModule is loaded (via the bundle or
[DependsOn]) and that the Redis instance supports pub/sub (not all managed
Redis services enable it by default). Check Cache:Redis:IsEnabled is true.