Skip to content

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.

  • A .NET 10 project referencing Granit.Core
  • Familiarity with IServiceCollection and dependency injection

Every module lives in its own project (one project = one NuGet package).

Terminal window
dotnet new classlib -n Granit.Inventory -f net10.0
dotnet add Granit.Inventory package Granit.Core

Recommended folder structure:

Granit.Inventory/
Domain/
InventoryItem.cs
Internal/
InventoryRepository.cs
Extensions/
InventoryServiceCollectionExtensions.cs
GranitInventoryModule.cs

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.

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;
}
}

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 { }

The module system provides two lifecycle phases:

PhaseMethodWhen
Service registrationConfigureServices / ConfigureServicesAsyncBefore Build()
Application initializationOnApplicationInitialization / OnApplicationInitializationAsyncAfter 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");
}
}

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.

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();
}
}
PropertyTypeDescription
ServicesIServiceCollectionDI registration
ConfigurationIConfigurationApp configuration (appsettings, env vars)
BuilderIHostApplicationBuilderFull builder (needed by some modules like Observability)
ModuleAssembliesIReadOnlyList<Assembly>All loaded module assemblies in topological order
ItemsIDictionary<string, object?>Shared state for inter-module communication