Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -116,7 +116,7 @@ public async Task T02_WhenSaveChangesFails_AggregateEventsAreAlreadyCleared_Risk
var eventsBeforeSave = config.DomainEvents.GetUncommittedChanges().Count;
Assert.Equal(1, eventsBeforeSave); // one AppConfigCreatedEvent

// Simulate the SaveEntitiesAsync logic from SqlServerAppConfigurationRepository
// Simulate the SaveEntitiesAsync logic from PostgreSqlAppConfigurationRepository
// (reproduced here because we cannot call the SQL repository without DB)
var outboxMessages = config.DomainEvents.GetUncommittedChanges().ToList();
Assert.Single(outboxMessages); // outbox materialized
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -31,9 +31,6 @@ static ContractTestWebApplicationFactory()
{
Environment.SetEnvironmentVariable("ASPNETCORE_ENVIRONMENT", "Development");
Environment.SetEnvironmentVariable("Persistence__Provider", "InMemory");
Environment.SetEnvironmentVariable("Persistence__UseSqlServerIdentityStores", "false");
Environment.SetEnvironmentVariable("Persistence__UseSqlServerAuthorizationStores", "false");
Environment.SetEnvironmentVariable("Persistence__UseSqlServerConfigurationStores", "false");
Environment.SetEnvironmentVariable("Persistence__SeedDevData", "true");
Environment.SetEnvironmentVariable("Persistence__EnableOutbox", "false");
Environment.SetEnvironmentVariable("Persistence__InitializePlatformStoreOnStartup","false");
Expand All @@ -49,9 +46,6 @@ protected override void ConfigureWebHost(IWebHostBuilder builder)
cfg.AddInMemoryCollection(new Dictionary<string, string?>
{
["Persistence:Provider"] = PersistenceProvider.InMemory.ToString(),
["Persistence:UseSqlServerIdentityStores"] = bool.FalseString,
["Persistence:UseSqlServerAuthorizationStores"] = bool.FalseString,
["Persistence:UseSqlServerConfigurationStores"] = bool.FalseString,
["Persistence:SeedDevData"] = bool.TrueString,
["Persistence:EnableOutbox"] = bool.FalseString,
["Persistence:InitializePlatformStoreOnStartup"]= bool.FalseString,
Expand Down
122 changes: 33 additions & 89 deletions src/apps/ums.api/Ums.Infrastructure/DependencyInjection.cs
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,7 @@ public static IServiceCollection AddInfrastructure(
.Validate(
options => options.Provider == PersistenceProvider.InMemory
|| !string.IsNullOrWhiteSpace(configuration.GetConnectionString("DefaultConnection")),
"ConnectionStrings:DefaultConnection is required when Persistence.Provider is SqlServer, Sqlite or PostgreSql.")
"ConnectionStrings:DefaultConnection is required when Persistence.Provider is Sqlite or PostgreSql.")
.ValidateOnStart();

services.AddHttpContextAccessor();
Expand Down Expand Up @@ -219,52 +219,12 @@ void ConfigurePayload(IBusFactoryConfigurator cfg) =>
});

// REC-04: Cross-aggregate transaction scope
if (persistence.Provider == PersistenceProvider.SqlServer || persistence.Provider == PersistenceProvider.Sqlite || persistence.Provider == PersistenceProvider.PostgreSql)
if (persistence.Provider == PersistenceProvider.Sqlite || persistence.Provider == PersistenceProvider.PostgreSql)
services.AddScoped<IUnitOfWorkScope, UnitOfWorkScope>();
else
services.AddSingleton<IUnitOfWorkScope, NoOpUnitOfWorkScope>();

if (persistence.Provider == PersistenceProvider.SqlServer)
{
var connectionString = configuration.GetConnectionString("DefaultConnection")
?? throw new InvalidOperationException("ConnectionStrings:DefaultConnection must be configured for SQL Server persistence.");

services.AddScoped<OrganizationDbContextInterceptor>();
services.AddScoped<AuditSaveChangesInterceptor>(); // FIX-08: auto-stamp audit columns

// REC-08: Polly resilience pipeline — circuit breaker on top of EF Core's transient retry
services.AddResiliencePipeline("ums-sql", pipelineBuilder =>
{
pipelineBuilder
.AddRetry(new Polly.Retry.RetryStrategyOptions
{
MaxRetryAttempts = 3,
Delay = TimeSpan.FromMilliseconds(200),
BackoffType = Polly.DelayBackoffType.Exponential,
})
.AddCircuitBreaker(new Polly.CircuitBreaker.CircuitBreakerStrategyOptions
{
FailureRatio = 0.5,
SamplingDuration = TimeSpan.FromSeconds(30),
MinimumThroughput = 10,
BreakDuration = TimeSpan.FromSeconds(60),
});
});

services.AddDbContext<UmsPlatformDbContext>((serviceProvider, options) =>
{
options.UseSqlServer(connectionString, sqlServer =>
{
sqlServer.MigrationsHistoryTable("__EFMigrationsHistory", UmsPlatformDbContext.DefaultSchema);
sqlServer.EnableRetryOnFailure(3); // REC-08: reduced; circuit breaker handles sustained failures
});

options.AddInterceptors(
serviceProvider.GetRequiredService<OrganizationDbContextInterceptor>(),
serviceProvider.GetRequiredService<AuditSaveChangesInterceptor>());
});
}
else if (persistence.Provider == PersistenceProvider.Sqlite)
if (persistence.Provider == PersistenceProvider.Sqlite)
{
var connectionString = configuration.GetConnectionString("DefaultConnection")
?? throw new InvalidOperationException("ConnectionStrings:DefaultConnection must be configured for SQLite persistence.");
Expand Down Expand Up @@ -336,15 +296,14 @@ void ConfigurePayload(IBusFactoryConfigurator cfg) =>
options.UseNpgsql(connectionString, pgOptions => pgOptions.EnableRetryOnFailure(3)));
}

if ((persistence.Provider == PersistenceProvider.SqlServer && persistence.UseSqlServerIdentityStores) ||
(persistence.Provider == PersistenceProvider.Sqlite && persistence.UseSqliteIdentityStores) ||
if ((persistence.Provider == PersistenceProvider.Sqlite && persistence.UseSqliteIdentityStores) ||
(persistence.Provider == PersistenceProvider.PostgreSql && persistence.UsePostgreSqlIdentityStores))
{
services.AddScoped<ITenantRepository, SqlServerTenantRepository>();
services.AddScoped<ITenantParameterRepository, SqlServerTenantParameterRepository>();
services.AddScoped<ITenantSignupRequestRepository, SqlServerTenantSignupRequestRepository>();
services.AddScoped<IUserAccountRepository, SqlServerUserAccountRepository>();
services.AddScoped<IUserManagementDelegationRepository, SqlServerUserManagementDelegationRepository>();
services.AddScoped<ITenantRepository, PostgreSqlTenantRepository>();
services.AddScoped<ITenantParameterRepository, PostgreSqlTenantParameterRepository>();
services.AddScoped<ITenantSignupRequestRepository, PostgreSqlTenantSignupRequestRepository>();
services.AddScoped<IUserAccountRepository, PostgreSqlUserAccountRepository>();
services.AddScoped<IUserManagementDelegationRepository, PostgreSqlUserManagementDelegationRepository>();
}
else
{
Expand All @@ -364,15 +323,14 @@ void ConfigurePayload(IBusFactoryConfigurator cfg) =>
services.AddSingleton<IUserManagementDelegationRepository>(sp => sp.GetRequiredService<InMemoryUserManagementDelegationRepository>());
}

if ((persistence.Provider == PersistenceProvider.SqlServer && persistence.UseSqlServerAuthorizationStores) ||
(persistence.Provider == PersistenceProvider.Sqlite && persistence.UseSqliteAuthorizationStores) ||
if ((persistence.Provider == PersistenceProvider.Sqlite && persistence.UseSqliteAuthorizationStores) ||
(persistence.Provider == PersistenceProvider.PostgreSql && persistence.UsePostgreSqlAuthorizationStores))
{
services.AddScoped<IProfileRepository, SqlServerProfileRepository>();
services.AddScoped<ISystemSuiteRepository, SqlServerSystemSuiteRepository>();
services.AddScoped<IPermissionTemplateRepository, SqlServerPermissionTemplateRepository>();
services.AddScoped<IRoleRepository, SqlServerRoleRepository>();
services.AddScoped<ITemplateAssignmentRuleRepository, SqlServerTemplateAssignmentRuleRepository>();
services.AddScoped<IProfileRepository, PostgreSqlProfileRepository>();
services.AddScoped<ISystemSuiteRepository, PostgreSqlSystemSuiteRepository>();
services.AddScoped<IPermissionTemplateRepository, PostgreSqlPermissionTemplateRepository>();
services.AddScoped<IRoleRepository, PostgreSqlRoleRepository>();
services.AddScoped<ITemplateAssignmentRuleRepository, PostgreSqlTemplateAssignmentRuleRepository>();
}
else
{
Expand All @@ -392,16 +350,15 @@ void ConfigurePayload(IBusFactoryConfigurator cfg) =>
services.AddSingleton<ITemplateAssignmentRuleRepository>(sp => sp.GetRequiredService<InMemoryTemplateAssignmentRuleRepository>());
}

if ((persistence.Provider == PersistenceProvider.SqlServer && persistence.UseSqlServerConfigurationStores) ||
(persistence.Provider == PersistenceProvider.Sqlite && persistence.UseSqliteConfigurationStores) ||
if ((persistence.Provider == PersistenceProvider.Sqlite && persistence.UseSqliteConfigurationStores) ||
(persistence.Provider == PersistenceProvider.PostgreSql && persistence.UsePostgreSqlConfigurationStores))
{
services.AddScoped<IAppConfigurationRepository, SqlServerAppConfigurationRepository>();
services.AddScoped<IFeatureFlagRepository, SqlServerFeatureFlagRepository>();
services.AddScoped<IIdpConfigurationRepository, SqlServerIdpConfigurationRepository>();
services.AddScoped<IParameterDefinitionRepository, SqlServerParameterDefinitionRepository>();
services.AddScoped<IParameterGlobalValueRepository, SqlServerParameterGlobalValueRepository>();
services.AddScoped<IParameterTenantValueRepository, SqlServerParameterTenantValueRepository>();
services.AddScoped<IAppConfigurationRepository, PostgreSqlAppConfigurationRepository>();
services.AddScoped<IFeatureFlagRepository, PostgreSqlFeatureFlagRepository>();
services.AddScoped<IIdpConfigurationRepository, PostgreSqlIdpConfigurationRepository>();
services.AddScoped<IParameterDefinitionRepository, PostgreSqlParameterDefinitionRepository>();
services.AddScoped<IParameterGlobalValueRepository, PostgreSqlParameterGlobalValueRepository>();
services.AddScoped<IParameterTenantValueRepository, PostgreSqlParameterTenantValueRepository>();
}
else
{
Expand All @@ -420,26 +377,25 @@ void ConfigurePayload(IBusFactoryConfigurator cfg) =>
services.AddSingleton<IParameterTenantValueRepository>(sp => sp.GetRequiredService<InMemoryParameterRepositories>());
}

if (persistence.Provider == PersistenceProvider.SqlServer || persistence.Provider == PersistenceProvider.Sqlite || persistence.Provider == PersistenceProvider.PostgreSql)
if (persistence.Provider == PersistenceProvider.Sqlite || persistence.Provider == PersistenceProvider.PostgreSql)
{
services.AddScoped<IAuditRecordRepository, SqlServerAuditRecordRepository>();
services.AddScoped<IAuditRecordRepository, PostgreSqlAuditRecordRepository>();
}
else
{
services.AddSingleton<InMemoryAuditRecordRepository>();
services.AddSingleton<IAuditRecordRepository>(sp => sp.GetRequiredService<InMemoryAuditRecordRepository>());
}

if ((persistence.Provider == PersistenceProvider.SqlServer && persistence.UseSqlServerApprovalsStores) ||
(persistence.Provider == PersistenceProvider.Sqlite && persistence.UseSqliteApprovalsStores) ||
if ((persistence.Provider == PersistenceProvider.Sqlite && persistence.UseSqliteApprovalsStores) ||
(persistence.Provider == PersistenceProvider.PostgreSql && persistence.UsePostgreSqlApprovalsStores))
{
services.AddScoped<IApprovalWorkflowRepository, SqlServerApprovalWorkflowRepository>();
services.AddScoped<IApprovalRequestRepository, SqlServerApprovalRequestRepository>();
services.AddScoped<INotificationRuleRepository, SqlServerNotificationRuleRepository>();
services.AddScoped<IDocumentTypeRepository, SqlServerDocumentTypeRepository>();
services.AddScoped<IUserDocumentRepository, SqlServerUserDocumentRepository>();
services.AddScoped<IAccessEnforcementPolicyRepository, SqlServerAccessEnforcementPolicyRepository>();
services.AddScoped<IApprovalWorkflowRepository, PostgreSqlApprovalWorkflowRepository>();
services.AddScoped<IApprovalRequestRepository, PostgreSqlApprovalRequestRepository>();
services.AddScoped<INotificationRuleRepository, PostgreSqlNotificationRuleRepository>();
services.AddScoped<IDocumentTypeRepository, PostgreSqlDocumentTypeRepository>();
services.AddScoped<IUserDocumentRepository, PostgreSqlUserDocumentRepository>();
services.AddScoped<IAccessEnforcementPolicyRepository, PostgreSqlAccessEnforcementPolicyRepository>();
}
else
{
Expand Down Expand Up @@ -542,21 +498,9 @@ public static IServiceCollection AddInfrastructureHealthChecks(

var builder = services.AddHealthChecks();

if (persistence.Provider == PersistenceProvider.SqlServer || persistence.Provider == PersistenceProvider.Sqlite || persistence.Provider == PersistenceProvider.PostgreSql)
if (persistence.Provider == PersistenceProvider.Sqlite || persistence.Provider == PersistenceProvider.PostgreSql)
{
if (persistence.Provider == PersistenceProvider.SqlServer)
{
var connectionString = configuration.GetConnectionString("DefaultConnection");
if (!string.IsNullOrWhiteSpace(connectionString))
{
builder.AddSqlServer(
connectionString,
name: "sql_server",
tags: ["ready", "db"]);
}
}

if (persistence.Provider == PersistenceProvider.PostgreSql)
if (persistence.Provider == PersistenceProvider.PostgreSql)
{
var connectionString = configuration.GetConnectionString("DefaultConnection");
if (!string.IsNullOrWhiteSpace(connectionString))
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,29 +14,29 @@ public Task StartAsync(CancellationToken cancellationToken)
var options = persistenceOptions.Value;

logger.LogInformation(
"UMS persistence configured with provider {Provider}, aggregate store mode {AggregateStoreMode}, identity SQL stores {UseSqlServerIdentityStores}, authorization SQL stores {UseSqlServerAuthorizationStores}, outbox enabled {EnableOutbox}.",
"UMS persistence configured with provider {Provider}, aggregate store mode {AggregateStoreMode}, identity PostgreSQL stores {UsePostgreSqlIdentityStores}, authorization PostgreSQL stores {UsePostgreSqlAuthorizationStores}, outbox enabled {EnableOutbox}.",
options.Provider,
options.AggregateStoreMode,
options.UseSqlServerIdentityStores,
options.UseSqlServerAuthorizationStores,
options.UsePostgreSqlIdentityStores,
options.UsePostgreSqlAuthorizationStores,
options.EnableOutbox);

if (options.Provider == PersistenceProvider.SqlServer
if (options.Provider == PersistenceProvider.PostgreSql
&& options.AggregateStoreMode == AggregateStoreMode.InMemory
&& !options.UseSqlServerIdentityStores)
&& !options.UsePostgreSqlIdentityStores)
{
logger.LogWarning(
"SQL Server is configured as the platform provider, but aggregate repositories still run in-memory. This is a valid transitional modular-monolith mode, not the final production persistence model.");
"PostgreSQL is configured as the platform provider, but aggregate repositories still run in-memory. This is a valid transitional modular-monolith mode, not the final production persistence model.");
}

if (options.Provider == PersistenceProvider.SqlServer && options.UseSqlServerIdentityStores)
if (options.Provider == PersistenceProvider.PostgreSql && options.UsePostgreSqlIdentityStores)
{
logger.LogInformation("Identity aggregates are configured to run on SQL Server repositories while the remaining contexts stay in transitional mode.");
logger.LogInformation("Identity aggregates are configured to run on PostgreSQL repositories while the remaining contexts stay in transitional mode.");
}

if (options.Provider == PersistenceProvider.SqlServer && options.UseSqlServerAuthorizationStores)
if (options.Provider == PersistenceProvider.PostgreSql && options.UsePostgreSqlAuthorizationStores)
{
logger.LogInformation("Authorization profile aggregates are configured to run on SQL Server repositories.");
logger.LogInformation("Authorization profile aggregates are configured to run on PostgreSQL repositories.");
}

return Task.CompletedTask;
Expand Down
Loading
Loading