Granit.Core
Granit.Core is the foundation of the entire framework. Every Granit module depends on it. It provides the module system (topological loading, DI orchestration), shared domain-driven design base types, GDPR/ISO 27001 data filtering, domain events, and a structured exception hierarchy.
Package: Granit.Core
Dependencies: None (only Microsoft.AspNetCore.App framework reference)
Dependents: 24 packages (all other Granit modules)
Package structure
Section titled “Package structure”Granit.Core is a single package — no providers, no EF Core sub-package. It defines abstractions that other modules implement.
Directorysrc/Granit.Core/
DirectoryModularity/
- GranitModule.cs
- GranitBuilder.cs
- DependsOnAttribute.cs
- ModuleLoader.cs
DirectoryDomain/
- Entity.cs
- ValueObject.cs
- AggregateRoot.cs
- ISoftDeletable.cs
- IMultiTenant.cs
- IActive.cs
- IPublishable.cs
- IProcessingRestrictable.cs
DirectoryDataFiltering/
- IDataFilter.cs
- DataFilter.cs
DirectoryMultiTenancy/
- ICurrentTenant.cs
- NullTenantContext.cs
DirectoryEvents/
- IDomainEvent.cs
- IIntegrationEvent.cs
- IDomainEventDispatcher.cs
DirectoryExceptions/
- BusinessException.cs
- ValidationException.cs
- EntityNotFoundException.cs
DirectoryEndpoints/
- ModuleConfigEndpointExtensions.cs
- BatchResult.cs
DirectoryExtensions/
- GranitHostBuilderExtensions.cs
- GranitApplicationExtensions.cs
DirectoryLocalization/
- LocalizableString.cs
- LocalizationResourceNameAttribute.cs
DirectoryDiagnostics/
- GranitActivitySourceRegistry.cs
Minimal setup
Section titled “Minimal setup”var builder = WebApplication.CreateBuilder(args);
// Option 1: Single root module (recommended)builder.AddGranit<AppModule>();
// Option 2: Fluent builder (multi-root)builder.AddGranit(granit => granit .AddModule<GranitSecurityModule>() .AddModule<GranitPersistenceModule>() .AddModule<AppModule>());
var app = builder.Build();app.UseGranit();app.Run();Async variants
Section titled “Async variants”Both AddGranit and UseGranit have async counterparts for modules that need
async initialization (e.g., fetching secrets from Vault at startup):
await builder.AddGranitAsync<AppModule>();var app = builder.Build();await app.UseGranitAsync();Module system
Section titled “Module system”GranitModule
Section titled “GranitModule”Every module inherits from GranitModule and declares dependencies via [DependsOn]:
[DependsOn(typeof(GranitPersistenceModule))][DependsOn(typeof(GranitSecurityModule))]public class AppModule : GranitModule{ public override void ConfigureServices(ServiceConfigurationContext context) { context.Services.AddScoped<IAppointmentService, AppointmentService>(); }
public override void OnApplicationInitialization(ApplicationInitializationContext context) { var logger = context.ServiceProvider.GetRequiredService<ILogger<AppModule>>(); logger.LogInformation("AppModule initialized"); }}Lifecycle
Section titled “Lifecycle”Modules execute in topological order (dependencies first), resolved via Kahn’s algorithm.
Circular dependencies throw InvalidOperationException at startup.
sequenceDiagram
participant Host as Host Builder
participant ML as ModuleLoader
participant M1 as GranitCoreModule
participant M2 as GranitPersistenceModule
participant M3 as AppModule
Host->>ML: AddGranit(AppModule)
ML->>ML: Discover [DependsOn] graph
ML->>ML: Topological sort (Kahn)
ML->>M1: ConfigureServices()
ML->>M2: ConfigureServices()
ML->>M3: ConfigureServices()
Note over Host: builder.Build()
ML->>M1: OnApplicationInitialization()
ML->>M2: OnApplicationInitialization()
ML->>M3: OnApplicationInitialization()
Each lifecycle method has sync and async variants. Both are called — sync first, then async.
ServiceConfigurationContext
Section titled “ServiceConfigurationContext”Passed to ConfigureServices / ConfigureServicesAsync:
| Property | Type | Description |
|---|---|---|
Services | IServiceCollection | DI container |
Configuration | IConfiguration | appsettings + environment |
Builder | IHostApplicationBuilder | Full builder access |
ModuleAssemblies | IReadOnlyList<Assembly> | All loaded module assemblies (topological order) |
Items | IDictionary<string, object?> | Inter-module state sharing during configuration |
Conditional modules
Section titled “Conditional modules”Override IsEnabled to conditionally skip a module:
public class GranitNotificationsEmailSmtpModule : GranitModule{ public override bool IsEnabled(ServiceConfigurationContext context) { return context.Configuration.GetValue<bool>("Granit:Notifications:Email:Smtp:Enabled"); }}GranitBuilder (fluent API)
Section titled “GranitBuilder (fluent API)”Alternative to [DependsOn] for composing modules in Program.cs:
builder.AddGranit(granit => granit .AddModule<GranitCoreModule>() .AddModule<GranitPersistenceModule>() .AddModule<GranitObservabilityModule>());Modules are deduplicated — adding the same module twice is safe.
Domain base types
Section titled “Domain base types”Entity hierarchy
Section titled “Entity hierarchy”Choose the right base class based on your audit requirements:
Simplest — just an Id. Use for lookup tables and value-like records.
public class Country : Entity{ public string Code { get; set; } = string.Empty; public string Name { get; set; } = string.Empty;}Tracks who created the record and when.
public class Appointment : CreationAuditedEntity{ public Guid PatientId { get; set; } public Guid DoctorId { get; set; } public DateTimeOffset ScheduledAt { get; set; }}// Adds: CreatedAt, CreatedByTracks creation and last modification.
public class Invoice : AuditedEntity{ public string Number { get; set; } = string.Empty; public decimal Amount { get; set; }}// Adds: CreatedAt, CreatedBy, ModifiedAt, ModifiedByFull audit trail with soft delete. Implements ISoftDeletable.
public class Patient : FullAuditedEntity{ public string FirstName { get; set; } = string.Empty; public string LastName { get; set; } = string.Empty; public DateOnly DateOfBirth { get; set; }}// Adds: CreatedAt, CreatedBy, ModifiedAt, ModifiedBy,// IsDeleted, DeletedAt, DeletedByAggregate root hierarchy
Section titled “Aggregate root hierarchy”Aggregate roots mirror the entity hierarchy but add domain event support via
IDomainEventSource:
| Base class | Audit level | Domain events |
|---|---|---|
AggregateRoot | None (just Id) | Yes |
CreationAuditedAggregateRoot | Creation | Yes |
AuditedAggregateRoot | Creation + modification | Yes |
FullAuditedAggregateRoot | Full + soft delete | Yes |
public class LegalAgreement : FullAuditedAggregateRoot{ public string Title { get; set; } = string.Empty; public int Version { get; set; }
public void Sign(string signedBy) { // Business logic... AddDomainEvent(new AgreementSignedEvent(Id, signedBy)); }}ValueObject
Section titled “ValueObject”Equality by value, not by reference. Override GetEqualityComponents():
public class Address : ValueObject{ public string Street { get; init; } = string.Empty; public string City { get; init; } = string.Empty; public string PostalCode { get; init; } = string.Empty; public string Country { get; init; } = string.Empty;
protected override IEnumerable<object?> GetEqualityComponents() { yield return Street; yield return City; yield return PostalCode; yield return Country; }}Data filtering
Section titled “Data filtering”Five marker interfaces enable automatic EF Core global query filters. Entities
implementing these interfaces are filtered transparently — no manual WHERE
clauses needed.
| Interface | Filter behavior | GDPR/ISO relevance |
|---|---|---|
ISoftDeletable | Hides IsDeleted = true | Right to erasure (Art. 17) |
IMultiTenant | Scopes to current TenantId | Tenant isolation |
IActive | Hides IsActive = false | Data minimization |
IPublishable | Hides IsPublished = false | Draft/published lifecycle |
IProcessingRestrictable | Hides IsProcessingRestricted = true | Right to restriction (Art. 18) |
IDataFilter
Section titled “IDataFilter”Selectively disable filters at runtime using IDataFilter:
public class PatientPurgeService( IDataFilter dataFilter, AppDbContext dbContext){ public async Task PurgeSoftDeletedAsync(CancellationToken cancellationToken) { using (dataFilter.Disable<ISoftDeletable>()) { var deleted = await dbContext.Patients .Where(p => p.IsDeleted && p.DeletedAt < DateTimeOffset.UtcNow.AddYears(-3)) .ToListAsync(cancellationToken) .ConfigureAwait(false);
dbContext.Patients.RemoveRange(deleted); await dbContext.SaveChangesAsync(cancellationToken).ConfigureAwait(false); } // Filter automatically re-enabled here }}DataFilter is registered as Singleton. It uses AsyncLocal<ImmutableDictionary<Type, bool>>
internally — thread-safe, supports nested scopes with automatic restoration.
Additional domain interfaces
Section titled “Additional domain interfaces”| Interface | Properties | Purpose |
|---|---|---|
IVersioned | BusinessId, Version | Versioned audit history (stable ID across versions) |
ITranslatable<T> | Translations collection | Multi-language entity content |
ITranslation | ParentId, Culture | Translation record (with Translation<T> and AuditedTranslation<T> base classes) |
Translation resolution chain (GetTranslation() extension):
exact culture → parent culture (fr-BE → fr) → default culture (en) → first available.
Multi-tenancy abstraction
Section titled “Multi-tenancy abstraction”ICurrentTenant lives in Granit.Core.MultiTenancy — available in every module
without referencing Granit.MultiTenancy.
public interface ICurrentTenant{ bool IsAvailable { get; } Guid? Id { get; } string? Name { get; } IDisposable Change(Guid? id, string? name = null);}By default, NullTenantContext is registered (IsAvailable = false). When
Granit.MultiTenancy is installed, it replaces the registration with a real
implementation.
Domain events
Section titled “Domain events”| Type | Interface | Convention | Delivery |
|---|---|---|---|
| Domain event | IDomainEvent | XxxOccurred (e.g., PatientDischargedOccurred) | In-process, transactional |
| Integration event | IIntegrationEvent | XxxEvent (e.g., BedReleasedEvent) | Cross-module, durable |
Domain events are collected on aggregate roots via AddDomainEvent() and dispatched
by IDomainEventDispatcher. The default implementation is NullDomainEventDispatcher
(no-op). Granit.Wolverine replaces it with a real dispatcher that routes events
through the Wolverine message bus.
Exception hierarchy
Section titled “Exception hierarchy”All exceptions map to specific HTTP status codes via Granit.ExceptionHandling
(RFC 7807 Problem Details):
| Exception | Status | Interfaces | Use case |
|---|---|---|---|
BusinessException | 400 | IHasErrorCode, IUserFriendlyException | Business rule violation |
BusinessRuleViolationException | 422 | (inherits from BusinessException) | Semantic validation failure |
ValidationException | 422 | IHasValidationErrors, IUserFriendlyException | FluentValidation errors |
EntityNotFoundException | 404 | IUserFriendlyException | Entity lookup miss |
NotFoundException | 404 | IUserFriendlyException | General “not found” |
ConflictException | 409 | IHasErrorCode, IUserFriendlyException | Concurrency/duplicate conflict |
ForbiddenException | 403 | IUserFriendlyException | Authorization denial |
Only exceptions implementing IUserFriendlyException expose their message to
clients. All others return a generic error message (ISO 27001 — no internal
details leak).
// Business rule with error codethrow new BusinessException("Appointment:PastDate", "Cannot schedule in the past");
// Entity not found (generic message to client, details in logs)throw new EntityNotFoundException(typeof(Patient), patientId);
// Validation errors (from FluentValidation)throw new ValidationException(new Dictionary<string, string[]>{ ["Email"] = ["Email address is required"], ["DateOfBirth"] = ["Must be in the past"]});Module configuration endpoint
Section titled “Module configuration endpoint”Expose read-only module configuration to frontend clients:
// 1. Define the response DTOpublic record NotificationConfigResponse(bool EmailEnabled, bool SmsEnabled, int MaxRetries);
// 2. Implement the providerpublic class NotificationConfigProvider(IOptions<NotificationOptions> options) : IModuleConfigProvider<NotificationConfigResponse>{ public NotificationConfigResponse GetConfig() => new( options.Value.EmailEnabled, options.Value.SmsEnabled, options.Value.MaxRetries);}
// 3. Map the endpoint (in your module's OnApplicationInitialization)app.MapGranitModuleConfig<NotificationConfigProvider, NotificationConfigResponse>( routePrefix: "api/notifications", endpointName: "GetNotificationConfig", tag: "Notifications");This maps GET /api/notifications/config → 200 OK with the response DTO.
Batch operations
Section titled “Batch operations”BatchResult<T> and BatchResultHelper standardize partial-success responses:
var results = new List<BatchItemResult<Patient>>();
foreach (var command in commands){ try { var patient = await CreatePatientAsync(command, cancellationToken).ConfigureAwait(false); results.Add(BatchResultHelper.Success(patient)); } catch (ValidationException ex) { results.Add(BatchResultHelper.Failure<Patient>(ex.Message)); }}
var batch = BatchResultHelper.Create(results);return batch.ToResult(); // 200 if all succeeded, 207 Multi-Status if any failedDiagnostics
Section titled “Diagnostics”GranitActivitySourceRegistry is a process-global registry for System.Diagnostics.ActivitySource
names. Modules call Register("Granit.ModuleName") during ConfigureServices, and
Granit.Observability auto-discovers all registered sources at startup for OpenTelemetry
tracing.
Public API summary
Section titled “Public API summary”| Category | Types | Count |
|---|---|---|
| Module system | GranitModule, GranitBuilder, DependsOnAttribute, ServiceConfigurationContext, ApplicationInitializationContext, GranitApplication, IModuleConfigProvider<T>, ModuleDescriptor | 8 |
| Domain base classes | Entity, ValueObject, AggregateRoot + Creation/Audited/FullAudited variants | 10 |
| Filter interfaces | ISoftDeletable, IMultiTenant, IActive, IPublishable, IProcessingRestrictable | 5 |
| Data filtering | IDataFilter, DataFilter | 2 |
| Events | IDomainEvent, IIntegrationEvent, IDomainEventSource, IDomainEventDispatcher | 4 |
| Multi-tenancy | ICurrentTenant, AllowAnonymousTenantAttribute | 2 |
| Exceptions | BusinessException, ValidationException, EntityNotFoundException, NotFoundException, ConflictException, ForbiddenException, BusinessRuleViolationException | 7 |
| Exception interfaces | IUserFriendlyException, IHasErrorCode, IHasValidationErrors | 3 |
| Translation | ITranslatable<T>, ITranslation, Translation<T>, AuditedTranslation<T> | 4 |
| Other | IVersioned, AuditLogEntry, LocalizableString, GranitActivitySourceRegistry, BatchResult<T> | 5 |
See also
Section titled “See also”- Module system concept
- Persistence — EF Core interceptors that implement audit and soft delete
- Security —
ICurrentUserServicethat populatesCreatedBy/ModifiedBy - Multi-tenancy — Full tenant isolation implementation
- API Reference (auto-generated from XML docs)