Dependency Injection
Program.cs registration flow, module DI pattern, service discovery, core + identity service maps, lifetime rules, and YARP gateway.
SCRIPE uses .NET's built-in Dependency Injection container with a structured registration pattern. Program.cs orchestrates all registrations: core infrastructure first, then modules conditionally based on MODULE_NAME, and finally the application layer with SCRIPE mediator. This page documents every service registration, lifetime decision, and the module DI pattern.
DI Registration Architecture
Program.cs follows a strict 4-phase registration order: (1) Core Infrastructure — cache, blob storage, audit, etc. (2) CORS & Rate Limiting. (3) Modules — each module registers its own DbContext, repositories, and services. (4) Application Layer — SCRIPE mediator, behaviors, validators.
Module Registration Pattern
Each module exposes an AddXxxModule() extension method that registers all its services. The MODULE_NAME environment variable controls which modules are loaded: empty = monolith (all modules), 'Identity' = only Identity module, 'Gateway' = API gateway mode.
var builder = WebApplication.CreateBuilder(args);
// ─── Environment Detection ─────────────────────────────
var moduleName = Environment.GetEnvironmentVariable("MODULE_NAME") ?? "";
var isMonolith = string.IsNullOrEmpty(moduleName);
var isGateway = moduleName.Equals("Gateway", StringComparison.OrdinalIgnoreCase);
// ─── Core Infrastructure (always registered) ───────────
builder.Services.AddInfrastructureServices(builder.Configuration);
builder.Services.AddCorsConfiguration(builder.Configuration);
builder.Services.AddRateLimitingConfiguration();
builder.Services.AddCoreInfrastructure(builder.Configuration);
// ─── Module Registration (conditional) ──────────────────
var mediatorAssemblies = new List<Type>();
if (isMonolith || moduleName == "Identity")
{
mediatorAssemblies.Add(typeof(Identity.Application.DependencyInjection));
builder.Services.AddIdentityModule(builder.Configuration);
}
// Future modules follow same pattern:
// if (isMonolith || moduleName == "Inventory")
// {
// mediatorAssemblies.Add(typeof(Inventory.Application.DependencyInjection));
// builder.Services.AddInventoryModule(builder.Configuration);
// }
// ─── Application Layer (needs all module assemblies) ────
builder.Services.AddCoreApplication(mediatorAssemblies.ToArray());Module Controller Feature Provider
The ModuleControllerFeatureProvider filters which controllers are loaded at startup based on the MODULE_NAME. In monolith mode, all controllers are loaded. In microservice mode, only controllers tagged with the matching [BelongsToModule] attribute are loaded.
/// <summary>
/// Custom controller feature provider that filters controllers
/// based on the MODULE_NAME environment variable.
/// In monolith mode: all controllers are loaded.
/// In microservice mode: only controllers with matching [BelongsToModule] are loaded.
/// </summary>
public class ModuleControllerFeatureProvider : ControllerFeatureProvider
{
private readonly string? _moduleName;
protected override bool IsController(TypeInfo typeInfo)
{
if (!base.IsController(typeInfo))
return false;
if (string.IsNullOrEmpty(_moduleName))
return true; // Monolith: load all
var attr = typeInfo.GetCustomAttribute<BelongsToModuleAttribute>();
return attr?.ModuleName == _moduleName;
}
}
/// <summary>
/// Attribute to tag controllers with their owning module.
/// </summary>
[AttributeUsage(AttributeTargets.Class)]
public class BelongsToModuleAttribute : Attribute
{
public string ModuleName { get; }
public BelongsToModuleAttribute(string moduleName)
=> ModuleName = moduleName;
}
// Usage on controllers:
// [BelongsToModule("Identity")]
// public class AdminsController : ControllerBase { }Service Discovery
In microservice mode, services need to discover each other's URLs. SCRIPE uses configuration-based service discovery (from appsettings.json) to resolve service names to URLs.
/// <summary>
/// Discovers service URLs from appsettings.json configuration.
/// Used in microservice mode to route requests between services.
/// </summary>
public class ConfigServiceDiscovery : IServiceDiscovery
{
private readonly ServiceDiscoveryOptions _options;
public Task<string> GetServiceUrlAsync(string serviceName)
{
if (_options.Services.TryGetValue(serviceName, out var url))
return Task.FromResult(url);
throw new InvalidOperationException(
$"Service '{serviceName}' not registered in ServiceDiscovery config");
}
}
/// <summary>
/// Configuration for service discovery.
/// </summary>
public class ServiceDiscoveryOptions
{
public const string SectionName = "ServiceDiscovery";
public Dictionary<string, string> Services { get; set; } = new();
}{
"ServiceDiscovery": {
"Services": {
"Identity": "https://identity-service:5001",
"Inventory": "https://inventory-service:5002",
"Gateway": "https://gateway:5000"
}
}
}Core Infrastructure Services
The following services are registered by AddCoreInfrastructure() and are available to all modules. They provide cross-cutting concerns like caching, file storage, auditing, email, webhooks, and data scoping.
| Interface | Implementation | Lifetime | Purpose |
|---|---|---|---|
| ICacheService | MemoryCacheService | Singleton | In-memory caching (default) |
| ICacheService | RedisCacheService | Singleton | Distributed caching (production) |
| IBlobStorage | LocalBlobStorage | Singleton | Local file system storage |
| IBlobStorage | AzureBlobStorage | Singleton | Azure Blob Storage |
| IBlobStorage | AwsS3BlobStorage | Singleton | AWS S3 storage |
| IBlobStorage | MinIOBlobStorage | Singleton | MinIO self-hosted storage |
| IAuditService | AuditService | Scoped | Audit log creation and streaming |
| INotificationService | NotificationService | Scoped | Real-time notifications via SignalR |
| IEmailSender | SmtpEmailSender | Scoped | SMTP email sending |
| IEmailSender | DevEmailSender | Scoped | Development: logs emails to console |
| IEmailQueue | EmailQueue | Singleton | In-memory Channel<T> queue |
| IEmailQueue | HangfireEmailQueue | Singleton | Persistent Hangfire queue |
| IFileService | FileService | Scoped | General file management |
| IImageService | ImageService | Scoped | Image resize, validate, process |
| IDownloadService | DownloadService | Scoped | Resumable downloads with Range/ETag |
| IUploadService | UploadService | Scoped | File upload with validation |
| IExportService | CsvExportService | Scoped | CSV data export |
| IExportService | ExcelExportService | Scoped | Excel data export |
| IExportService | PdfExportService | Scoped | PDF data export |
| IWebhookService | WebhookService | Scoped | Webhook event delivery |
| IBackgroundJobService | HangfireBackgroundJobService | Scoped | Enqueue/schedule background jobs |
| IIdEncryptionService | IdEncryptionService | Singleton | External ID obfuscation |
| IDataScopeService | DataScopeService | Scoped | Tenant data scoping |
| ICurrentUser | CurrentUserService | Scoped | JWT claims extraction |
Identity Module Services
The Identity module registers 24 repository interfaces and 10 service interfaces. All repositories are registered as Scoped (one instance per HTTP request) to match the DbContext lifetime.
| Interface | Implementation | Purpose |
|---|---|---|
| IAdminRepository | AdminRepository | Admin CRUD operations |
| IUserRepository | UserRepository | User CRUD operations |
| IRoleRepository | RoleRepository | Role management |
| IPermissionRepository | PermissionRepository | Permission queries |
| ITenantRepository | TenantRepository | Tenant CRUD + hierarchy |
| IMenuItemRepository | MenuItemRepository | Menu tree operations |
| IRefreshTokenRepository | RefreshTokenRepository | JWT refresh token storage |
| IOtpCodeRepository | OtpCodeRepository | One-time password codes |
| IRolePermissionRepository | RolePermissionRepository | Role-permission mappings |
| IRoleMenuItemRepository | RoleMenuItemRepository | Role-menu visibility |
| ITenantPermissionRepository | TenantPermissionRepository | Tenant permission overrides |
| ITenantMenuOverrideRepository | TenantMenuOverrideRepository | Tenant menu overrides |
| IRecycleBinRepository | RecycleBinRepository | Soft-deleted entity operations |
| IMessageTemplateRepository | MessageTemplateRepository | Email/notification templates |
| ISentEmailLogRepository | SentEmailLogRepository | Email delivery logs |
| IWebhookRepository | WebhookRepository | Webhook subscriptions |
| IJwtService | JwtService | JWT token generation & validation |
| IOtpService | OtpService | OTP code generation & verification |
| ITfaService | TfaService | Two-factor authentication |
| IExternalAuthService | ExternalAuthService | Google/Facebook/Apple/MS auth |
| IAdminSecurityService | AdminSecurityService | Admin protection & transfer |
| ITenantGuardianService | TenantGuardianService | Tenant hierarchy validation |
| ITenantQuotaService | TenantQuotaService | Tenant resource quotas |
| ITenantPasswordValidator | TenantPasswordValidator | Tenant-level password policies |
Service Lifetime Rules
Singleton Lifetime
- One instance for entire application lifetime
- Use for: cache, blob storage, encryption, queues
- Must be thread-safe
- Cannot inject Scoped services
- Example: ICacheService, IBlobStorage, IIdEncryptionService
Scoped Lifetime
- One instance per HTTP request
- Use for: repositories, DbContext, current user
- Automatically disposed at end of request
- Can inject other Scoped and Singleton services
- Example: IAdminRepository, ICurrentUser, IAuditService
YARP Gateway Configuration
When MODULE_NAME=Gateway, the application acts as a YARP reverse proxy. It routes requests to backend microservices based on URL path prefix matching. The gateway handles SSL termination, request buffering, and can be extended with custom middleware.
/// <summary>
/// Configures YARP (Yet Another Reverse Proxy) for the API Gateway.
/// Routes requests to appropriate microservices based on path prefix.
/// Only active when MODULE_NAME=Gateway.
/// </summary>
public static class GatewayConfiguration
{
public static IServiceCollection AddGatewayConfiguration(
this IServiceCollection services,
IConfiguration configuration)
{
services.AddReverseProxy()
.LoadFromConfig(configuration.GetSection("ReverseProxy"));
return services;
}
public static WebApplication UseGatewayConfiguration(
this WebApplication app)
{
app.MapReverseProxy();
return app;
}
}{
"ReverseProxy": {
"Routes": {
"identity-route": {
"ClusterId": "identity-cluster",
"Match": { "Path": "/api/v1/auth/{**catch-all}" }
},
"admin-route": {
"ClusterId": "identity-cluster",
"Match": { "Path": "/api/v1/admins/{**catch-all}" }
}
},
"Clusters": {
"identity-cluster": {
"Destinations": {
"destination1": {
"Address": "https://identity-service:5001"
}
}
}
}
}
}DI Best Practices
- Register interfaces in Domain/Application layers, implementations in Infrastructure
- Use Scoped lifetime for anything that accesses DbContext or HttpContext
- Use Singleton lifetime for stateless or thread-safe services only
- Never inject Scoped services into Singleton services (captive dependency)
- Use IServiceScopeFactory in Singleton services that need Scoped dependencies
- Keep DependencyInjection.cs in each module as the single registration point
- Use MODULE_NAME environment variable to control which modules are loaded
- Register AstraFlow mediator assemblies from each active module for handler discovery
- Use [BelongsToModule] on every controller for microservice compatibility
- Test DI registration at startup to catch missing dependencies early