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.

Dependency Injection Registration Flow
Program.cs — Entry point — orchestrates all registrations
1st
AddCoreInfrastructure() — Core services: cache, blob, audit, etc.
AddCorsConfiguration()
AddRateLimitingConfiguration()
AddIdentityModule() — Repositories, services, DbContext
AddCoreApplication() — AstraFlow mediator, behaviors, validators
AddBackgroundJobsConfiguration()
AddSignalRConfiguration()

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.

Program.cs — Module Registration PatternC#
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.

ModuleControllerFeatureProvider.csC#
/// <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.

ConfigServiceDiscovery.csC#
/// <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();
}
appsettings.json — Service DiscoveryJSON
{
  "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.

InterfaceImplementationLifetimePurpose
ICacheServiceMemoryCacheServiceSingletonIn-memory caching (default)
ICacheServiceRedisCacheServiceSingletonDistributed caching (production)
IBlobStorageLocalBlobStorageSingletonLocal file system storage
IBlobStorageAzureBlobStorageSingletonAzure Blob Storage
IBlobStorageAwsS3BlobStorageSingletonAWS S3 storage
IBlobStorageMinIOBlobStorageSingletonMinIO self-hosted storage
IAuditServiceAuditServiceScopedAudit log creation and streaming
INotificationServiceNotificationServiceScopedReal-time notifications via SignalR
IEmailSenderSmtpEmailSenderScopedSMTP email sending
IEmailSenderDevEmailSenderScopedDevelopment: logs emails to console
IEmailQueueEmailQueueSingletonIn-memory Channel<T> queue
IEmailQueueHangfireEmailQueueSingletonPersistent Hangfire queue
IFileServiceFileServiceScopedGeneral file management
IImageServiceImageServiceScopedImage resize, validate, process
IDownloadServiceDownloadServiceScopedResumable downloads with Range/ETag
IUploadServiceUploadServiceScopedFile upload with validation
IExportServiceCsvExportServiceScopedCSV data export
IExportServiceExcelExportServiceScopedExcel data export
IExportServicePdfExportServiceScopedPDF data export
IWebhookServiceWebhookServiceScopedWebhook event delivery
IBackgroundJobServiceHangfireBackgroundJobServiceScopedEnqueue/schedule background jobs
IIdEncryptionServiceIdEncryptionServiceSingletonExternal ID obfuscation
IDataScopeServiceDataScopeServiceScopedTenant data scoping
ICurrentUserCurrentUserServiceScopedJWT 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.

InterfaceImplementationPurpose
IAdminRepositoryAdminRepositoryAdmin CRUD operations
IUserRepositoryUserRepositoryUser CRUD operations
IRoleRepositoryRoleRepositoryRole management
IPermissionRepositoryPermissionRepositoryPermission queries
ITenantRepositoryTenantRepositoryTenant CRUD + hierarchy
IMenuItemRepositoryMenuItemRepositoryMenu tree operations
IRefreshTokenRepositoryRefreshTokenRepositoryJWT refresh token storage
IOtpCodeRepositoryOtpCodeRepositoryOne-time password codes
IRolePermissionRepositoryRolePermissionRepositoryRole-permission mappings
IRoleMenuItemRepositoryRoleMenuItemRepositoryRole-menu visibility
ITenantPermissionRepositoryTenantPermissionRepositoryTenant permission overrides
ITenantMenuOverrideRepositoryTenantMenuOverrideRepositoryTenant menu overrides
IRecycleBinRepositoryRecycleBinRepositorySoft-deleted entity operations
IMessageTemplateRepositoryMessageTemplateRepositoryEmail/notification templates
ISentEmailLogRepositorySentEmailLogRepositoryEmail delivery logs
IWebhookRepositoryWebhookRepositoryWebhook subscriptions
IJwtServiceJwtServiceJWT token generation & validation
IOtpServiceOtpServiceOTP code generation & verification
ITfaServiceTfaServiceTwo-factor authentication
IExternalAuthServiceExternalAuthServiceGoogle/Facebook/Apple/MS auth
IAdminSecurityServiceAdminSecurityServiceAdmin protection & transfer
ITenantGuardianServiceTenantGuardianServiceTenant hierarchy validation
ITenantQuotaServiceTenantQuotaServiceTenant resource quotas
ITenantPasswordValidatorTenantPasswordValidatorTenant-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.

GatewayConfiguration.cs — YARP Reverse ProxyC#
/// <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;
    }
}
appsettings.json — YARP Route ConfigurationJSON
{
  "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

  1. Register interfaces in Domain/Application layers, implementations in Infrastructure
  2. Use Scoped lifetime for anything that accesses DbContext or HttpContext
  3. Use Singleton lifetime for stateless or thread-safe services only
  4. Never inject Scoped services into Singleton services (captive dependency)
  5. Use IServiceScopeFactory in Singleton services that need Scoped dependencies
  6. Keep DependencyInjection.cs in each module as the single registration point
  7. Use MODULE_NAME environment variable to control which modules are loaded
  8. Register AstraFlow mediator assemblies from each active module for handler discovery
  9. Use [BelongsToModule] on every controller for microservice compatibility
  10. Test DI registration at startup to catch missing dependencies early