Common Errors
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”Missing ApplyGranitConventions
Section titled “Missing ApplyGranitConventions”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 does not call ApplyGranitConventions in
OnModelCreating.
Fix: Call modelBuilder.ApplyGranitConventions(currentTenant, dataFilter) at
the end of OnModelCreating.
public class MyDbContext( DbContextOptions<MyDbContext> options, ICurrentTenant? currentTenant = null, IDataFilter? dataFilter = null) : DbContext(options){ protected override void OnModelCreating(ModelBuilder modelBuilder) { base.OnModelCreating(modelBuilder); modelBuilder.ApplyConfigurationsFromAssembly(typeof(MyDbContext).Assembly);
// This line is mandatory modelBuilder.ApplyGranitConventions(currentTenant, dataFilter); }}Missing interceptors
Section titled “Missing interceptors”Symptom: CreatedAt, CreatedBy, LastModifiedAt, LastModifiedBy fields
are never populated. Soft-deleted entities are hard-deleted instead of flagged.
Cause: The AddDbContextFactory call does not resolve
AuditedEntityInterceptor and SoftDeleteInterceptor.
Fix: Use the (sp, options) overload to resolve interceptors from the
service provider.
services.AddDbContextFactory<MyDbContext>((sp, options) =>{ options.UseNpgsql(connectionString); options.AddInterceptors( sp.GetRequiredService<AuditedEntityInterceptor>(), sp.GetRequiredService<SoftDeleteInterceptor>());}, ServiceLifetime.Scoped);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 GranitAuthenticationKeycloakModule is included in your
dependency chain. It registers the KeycloakClaimsTransformation that maps
Keycloak-specific claims to standard ClaimTypes.
[DependsOn(typeof(GranitAuthenticationKeycloakModule))]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.
HybridCache not invalidating
Section titled “HybridCache not invalidating”Symptom: Stale data is served from cache after updates.
Common causes:
- Missing cache key invalidation: After a write operation, the corresponding cache entry was not removed or updated.
- L1/L2 desynchronization: The in-memory (L1) cache on one instance is not aware of invalidation performed by another instance. HybridCache handles this via Redis pub/sub, but only if the Redis backplane is correctly configured.
Fix: Verify that AddGranitHybridCache() is called with a valid Redis
connection and that the Redis instance supports pub/sub (not all managed Redis
services enable it by default).