Create a module
A Granit module is a self-contained unit that registers its own services into the
DI container. Modules declare their dependencies with [DependsOn] and are loaded
in topological order — one call in Program.cs replaces dozens of manual
Add*() invocations.
Prerequisites
Section titled “Prerequisites”- A .NET 10 project referencing
Granit.Core - Familiarity with
IServiceCollectionand dependency injection
Step 1 — Create the project
Section titled “Step 1 — Create the project”Every module lives in its own project (one project = one NuGet package).
dotnet new classlib -n Granit.Inventory -f net10.0dotnet add Granit.Inventory package Granit.CoreRecommended folder structure:
Granit.Inventory/ Domain/ InventoryItem.cs Internal/ InventoryRepository.cs Extensions/ InventoryServiceCollectionExtensions.cs GranitInventoryModule.csStep 2 — Define the module class
Section titled “Step 2 — Define the module class”Every module inherits from GranitModule and overrides lifecycle methods as needed.
using Granit.Core.Modularity;using Granit.Inventory.Extensions;
namespace Granit.Inventory;
public sealed class GranitInventoryModule : GranitModule{ public override void ConfigureServices(ServiceConfigurationContext context) { context.Services.AddInventory(context.Configuration); }}The base class provides empty (no-op) implementations for all lifecycle methods. A module that only needs to declare dependencies can inherit without overriding anything.
Step 3 — Register services
Section titled “Step 3 — Register services”Create an extension method that encapsulates all DI registrations for the module.
using Microsoft.Extensions.Configuration;using Microsoft.Extensions.DependencyInjection;
namespace Granit.Inventory.Extensions;
public static class InventoryServiceCollectionExtensions{ public static IServiceCollection AddInventory( this IServiceCollection services, IConfiguration configuration) { services.Configure<InventoryOptions>( configuration.GetSection("Inventory"));
services.AddScoped<IInventoryRepository, InventoryRepository>(); services.AddScoped<IInventoryService, InventoryService>();
return services; }}Step 4 — Declare dependencies
Section titled “Step 4 — Declare dependencies”Use [DependsOn] to declare which modules must be loaded before yours. Dependencies
are resolved transitively and deduplicated automatically.
using Granit.Core.Modularity;using Granit.Persistence;using Granit.Security;
namespace Granit.Inventory;
[DependsOn(typeof(GranitPersistenceModule))][DependsOn(typeof(GranitSecurityModule))]public sealed class GranitInventoryModule : GranitModule{ public override void ConfigureServices(ServiceConfigurationContext context) { context.Services.AddInventory(context.Configuration); }}You can also combine multiple types in a single attribute:
[DependsOn( typeof(GranitPersistenceModule), typeof(GranitSecurityModule))]public sealed class GranitInventoryModule : GranitModule { }Step 5 — Use lifecycle hooks
Section titled “Step 5 — Use lifecycle hooks”The module system provides two lifecycle phases:
| Phase | Method | When |
|---|---|---|
| Service registration | ConfigureServices / ConfigureServicesAsync | Before Build() |
| Application initialization | OnApplicationInitialization / OnApplicationInitializationAsync | After Build(), before Run() |
public sealed class GranitInventoryModule : GranitModule{ public override void ConfigureServices(ServiceConfigurationContext context) { context.Services.AddInventory(context.Configuration); }
public override void OnApplicationInitialization( ApplicationInitializationContext context) { var logger = context.ServiceProvider .GetRequiredService<ILogger<GranitInventoryModule>>(); logger.LogInformation("Inventory module initialized"); }}public sealed class GranitInventoryModule : GranitModule{ public override async Task ConfigureServicesAsync( ServiceConfigurationContext context) { var remoteConfig = await FetchRemoteConfigAsync( context.Configuration); context.Services.Configure<InventoryOptions>( o => o.WarehouseId = remoteConfig.WarehouseId); }}Step 6 — Conditional loading with IsEnabled
Section titled “Step 6 — Conditional loading with IsEnabled”A module can disable itself based on configuration or environment:
public sealed class GranitInventoryModule : GranitModule{ public override bool IsEnabled(ServiceConfigurationContext context) { var options = context.Configuration .GetSection("Inventory").Get<InventoryOptions>(); return options?.IsEnabled ?? false; }
public override void ConfigureServices(ServiceConfigurationContext context) { context.Services.AddInventory(context.Configuration); }}A disabled module remains in the dependency graph (its dependents still work) but
its ConfigureServices and OnApplicationInitialization methods are not called.
It appears as [DISABLED] in the startup logs.
Step 7 — Wire up in Program.cs
Section titled “Step 7 — Wire up in Program.cs”using Granit.Core.Extensions;using MyApp.Host;
var builder = WebApplication.CreateBuilder(args);
await builder.AddGranitAsync<MyAppHostModule>();
var app = builder.Build();
await app.UseGranitAsync();
app.Run();Your application host module references the inventory module:
[DependsOn(typeof(GranitInventoryModule))]public sealed class MyAppHostModule : GranitModule{ public override void ConfigureServices(ServiceConfigurationContext context) { context.Services.AddHealthChecks(); }}ServiceConfigurationContext reference
Section titled “ServiceConfigurationContext reference”| Property | Type | Description |
|---|---|---|
Services | IServiceCollection | DI registration |
Configuration | IConfiguration | App configuration (appsettings, env vars) |
Builder | IHostApplicationBuilder | Full builder (needed by some modules like Observability) |
ModuleAssemblies | IReadOnlyList<Assembly> | All loaded module assemblies in topological order |
Items | IDictionary<string, object?> | Shared state for inter-module communication |
Next steps
Section titled “Next steps”- Add an endpoint — expose your module via Minimal API
- Configure multi-tenancy — add tenant isolation to your module
- Granit.Core reference — module system internals