| | | 1 | | using Microsoft.EntityFrameworkCore; |
| | | 2 | | using Microsoft.Extensions.Configuration; |
| | | 3 | | using Microsoft.Extensions.DependencyInjection; |
| | | 4 | | using Microsoft.Extensions.DependencyInjection.Extensions; |
| | | 5 | | using Microsoft.Extensions.Logging; |
| | | 6 | | using ProjectTemplate.Infrastructure.Data.Auditing; |
| | | 7 | | using ProjectTemplate.Infrastructure.Data.ExternalLogins; |
| | | 8 | | using ProjectTemplate.Infrastructure.Data.Options; |
| | | 9 | | |
| | | 10 | | namespace ProjectTemplate.Infrastructure.Data.Extensions; |
| | | 11 | | |
| | | 12 | | /// <summary> |
| | | 13 | | /// Provides infrastructure-owned service registration methods for application data access. |
| | | 14 | | /// </summary> |
| | | 15 | | public static class InfrastructureDataAccessServiceExtensions |
| | | 16 | | { |
| | | 17 | | /// <summary> |
| | | 18 | | /// Registers EF Core data access services for infrastructure and non-web consumers. |
| | | 19 | | /// </summary> |
| | | 20 | | public static IServiceCollection AddApplicationInfrastructureDataAccess( |
| | | 21 | | this IServiceCollection services, |
| | | 22 | | IConfiguration configuration) |
| | | 23 | | { |
| | 216 | 24 | | ArgumentNullException.ThrowIfNull(services); |
| | 216 | 25 | | ArgumentNullException.ThrowIfNull(configuration); |
| | | 26 | | |
| | 216 | 27 | | services |
| | 216 | 28 | | .AddOptions<DataAccessOptions>() |
| | 216 | 29 | | .Bind(configuration.GetSection(DataAccessOptions.SectionName)) |
| | 290 | 30 | | .Validate(options => !string.IsNullOrWhiteSpace(options.Provider), |
| | 216 | 31 | | "ProjectTemplate:DataAccess:Provider must not be empty.") |
| | 290 | 32 | | .Validate(options => DataAccessOptions.IsDisabledProvider(options.Provider) |
| | 290 | 33 | | || !string.IsNullOrWhiteSpace(options.ConnectionStringName), |
| | 216 | 34 | | "ProjectTemplate:DataAccess:ConnectionStringName must not be empty when data access is enabled.") |
| | 290 | 35 | | .Validate(options => options.Diagnostics is not null, |
| | 216 | 36 | | "ProjectTemplate:DataAccess:Diagnostics must be configured as an object when specified.") |
| | 290 | 37 | | .Validate(options => AuditStorageModes.IsSupported(options.Auditing.StorageMode), |
| | 216 | 38 | | "ProjectTemplate:DataAccess:Auditing:StorageMode must be Local, Outbox, or ExternalSink.") |
| | 216 | 39 | | .ValidateOnStart(); |
| | | 40 | | |
| | 216 | 41 | | DataAccessRegistration registration = ResolveDataAccessRegistration(configuration); |
| | 204 | 42 | | if (registration.IsDisabled) |
| | | 43 | | { |
| | 12 | 44 | | return services; |
| | | 45 | | } |
| | | 46 | | |
| | 192 | 47 | | services.TryAddScoped<ICurrentActorAccessor, SystemCurrentActorAccessor>(); |
| | 192 | 48 | | services.TryAddScoped<IApplicationAuditContextAccessor, SystemApplicationAuditContextAccessor>(); |
| | 192 | 49 | | services.TryAddScoped<IApplicationAuditValuePolicy, DefaultApplicationAuditValuePolicy>(); |
| | 192 | 50 | | services.TryAddSingleton<IApplicationMutationManifestBuilder, CanonicalApplicationMutationManifestBuilder>(); |
| | 192 | 51 | | services.TryAddSingleton<IApplicationMutationManifestHasher, Sha256ApplicationMutationManifestHasher>(); |
| | 192 | 52 | | services.TryAddSingleton<IApplicationMutationManifestVerifier, ApplicationMutationManifestVerifier>(); |
| | | 53 | | |
| | 192 | 54 | | if (AuditStorageModes.IsLocal(registration.AuditStorageMode)) |
| | | 55 | | { |
| | 190 | 56 | | services.TryAddScoped<IApplicationAuditStore, LocalApplicationAuditStore>(); |
| | | 57 | | } |
| | | 58 | | |
| | 192 | 59 | | services.TryAddScoped<ContextIsolatedApplicationSaveChangesPipeline>(); |
| | 192 | 60 | | services.TryAddScoped<IApplicationSaveChangesPipeline>(serviceProvider => |
| | 212 | 61 | | serviceProvider.GetRequiredService<ContextIsolatedApplicationSaveChangesPipeline>()); |
| | 192 | 62 | | services.TryAddScoped<IApplicationMutationAuditReceiptRegistry>(serviceProvider => |
| | 196 | 63 | | serviceProvider.GetRequiredService<ContextIsolatedApplicationSaveChangesPipeline>()); |
| | 192 | 64 | | services.TryAddScoped<IApplicationMutationAuditReceiptAccessor, ApplicationDbContextMutationAuditReceiptAccessor |
| | 192 | 65 | | services.TryAddScoped<ApplicationSaveChangesInterceptor>(); |
| | | 66 | | |
| | 192 | 67 | | services.AddDbContext<ApplicationDbContext>((serviceProvider, options) => |
| | 214 | 68 | | ConfigureDataAccess( |
| | 214 | 69 | | serviceProvider, |
| | 214 | 70 | | options, |
| | 214 | 71 | | registration)); |
| | | 72 | | |
| | 192 | 73 | | services.AddDbContextFactory<ApplicationDbContext>( |
| | 20 | 74 | | (serviceProvider, options) => ConfigureDataAccess( |
| | 20 | 75 | | serviceProvider, |
| | 20 | 76 | | options, |
| | 20 | 77 | | registration), |
| | 192 | 78 | | ServiceLifetime.Scoped); |
| | | 79 | | |
| | 192 | 80 | | services.TryAddScoped<IExternalLoginAccountResolver, EfCoreExternalLoginAccountResolver>(); |
| | 192 | 81 | | return services; |
| | | 82 | | } |
| | | 83 | | |
| | | 84 | | private static DataAccessRegistration ResolveDataAccessRegistration(IConfiguration configuration) |
| | | 85 | | { |
| | 216 | 86 | | DataAccessOptions dataAccessOptions = configuration |
| | 216 | 87 | | .GetSection(DataAccessOptions.SectionName) |
| | 216 | 88 | | .Get<DataAccessOptions>() ?? new DataAccessOptions(); |
| | | 89 | | |
| | 216 | 90 | | string provider = dataAccessOptions.Provider?.Trim() ?? string.Empty; |
| | 216 | 91 | | string connectionStringName = dataAccessOptions.ConnectionStringName?.Trim() ?? string.Empty; |
| | 216 | 92 | | string auditStorageMode = AuditStorageModes.Normalize(dataAccessOptions.Auditing.StorageMode); |
| | | 93 | | |
| | 216 | 94 | | if (string.IsNullOrWhiteSpace(provider)) |
| | | 95 | | { |
| | 4 | 96 | | throw new InvalidOperationException("Application data access provider was not configured."); |
| | | 97 | | } |
| | | 98 | | |
| | 212 | 99 | | if (!AuditStorageModes.IsSupported(auditStorageMode)) |
| | | 100 | | { |
| | 2 | 101 | | throw new InvalidOperationException("Application audit storage mode was not configured with a supported valu |
| | | 102 | | } |
| | | 103 | | |
| | 210 | 104 | | if (DataAccessOptions.IsDisabledProvider(provider)) |
| | | 105 | | { |
| | 12 | 106 | | return DataAccessRegistration.Disabled(provider, auditStorageMode); |
| | | 107 | | } |
| | | 108 | | |
| | 198 | 109 | | if (string.IsNullOrWhiteSpace(connectionStringName)) |
| | | 110 | | { |
| | 4 | 111 | | throw new InvalidOperationException("Application data access connection string name was not configured."); |
| | | 112 | | } |
| | | 113 | | |
| | 194 | 114 | | string connectionString = configuration.GetConnectionString(connectionStringName) |
| | 194 | 115 | | ?? throw new InvalidOperationException($"Connection string '{connectionStringName}' was not configured."); |
| | | 116 | | |
| | 192 | 117 | | return DataAccessRegistration.Enabled( |
| | 192 | 118 | | provider, |
| | 192 | 119 | | connectionString, |
| | 192 | 120 | | auditStorageMode, |
| | 192 | 121 | | dataAccessOptions.Diagnostics.EnableDetailedErrors, |
| | 192 | 122 | | dataAccessOptions.Diagnostics.EnableEfCoreTraceBridge); |
| | | 123 | | } |
| | | 124 | | |
| | | 125 | | private static void ConfigureDataAccess( |
| | | 126 | | IServiceProvider serviceProvider, |
| | | 127 | | DbContextOptionsBuilder options, |
| | | 128 | | DataAccessRegistration registration) |
| | | 129 | | { |
| | 42 | 130 | | ConfigureProvider( |
| | 42 | 131 | | options, |
| | 42 | 132 | | registration.Provider, |
| | 42 | 133 | | registration.ConnectionString); |
| | | 134 | | |
| | 40 | 135 | | if (registration.EnableDetailedErrors) |
| | | 136 | | { |
| | 4 | 137 | | _ = options.EnableDetailedErrors(); |
| | | 138 | | } |
| | | 139 | | |
| | 40 | 140 | | if (registration.EnableEfCoreTraceBridge) |
| | | 141 | | { |
| | 4 | 142 | | ILogger<ApplicationDbContext> logger = serviceProvider |
| | 4 | 143 | | .GetRequiredService<ILogger<ApplicationDbContext>>(); |
| | | 144 | | |
| | 4 | 145 | | _ = options.LogTo( |
| | 8 | 146 | | message => EfCoreDiagnosticsLogging.LogEfCoreMessage(logger, message), |
| | 4 | 147 | | LogLevel.Trace); |
| | | 148 | | } |
| | 40 | 149 | | } |
| | | 150 | | |
| | | 151 | | private static void ConfigureProvider( |
| | | 152 | | DbContextOptionsBuilder options, |
| | | 153 | | string provider, |
| | | 154 | | string connectionString) |
| | | 155 | | { |
| | 42 | 156 | | if (provider.Equals(DataAccessOptions.SqliteProvider, StringComparison.OrdinalIgnoreCase)) |
| | | 157 | | { |
| | 28 | 158 | | options.UseSqlite(connectionString); |
| | 28 | 159 | | return; |
| | | 160 | | } |
| | | 161 | | |
| | 14 | 162 | | if (provider.Equals(DataAccessOptions.SqlServerProvider, StringComparison.OrdinalIgnoreCase)) |
| | | 163 | | { |
| | 12 | 164 | | options.UseSqlServer(connectionString); |
| | 12 | 165 | | return; |
| | | 166 | | } |
| | | 167 | | |
| | 2 | 168 | | throw new InvalidOperationException( |
| | 2 | 169 | | $"Unsupported data access provider '{provider}'. Supported providers: {DataAccessOptions.SqliteProvider}, {D |
| | | 170 | | } |
| | | 171 | | |
| | | 172 | | private readonly record struct DataAccessRegistration( |
| | 42 | 173 | | string Provider, |
| | 42 | 174 | | string ConnectionString, |
| | 192 | 175 | | string AuditStorageMode, |
| | 40 | 176 | | bool EnableDetailedErrors, |
| | 40 | 177 | | bool EnableEfCoreTraceBridge, |
| | 204 | 178 | | bool IsDisabled) |
| | | 179 | | { |
| | | 180 | | public static DataAccessRegistration Enabled( |
| | | 181 | | string provider, |
| | | 182 | | string connectionString, |
| | | 183 | | string auditStorageMode, |
| | | 184 | | bool enableDetailedErrors, |
| | | 185 | | bool enableEfCoreTraceBridge) |
| | | 186 | | { |
| | 192 | 187 | | return new( |
| | 192 | 188 | | provider, |
| | 192 | 189 | | connectionString, |
| | 192 | 190 | | auditStorageMode, |
| | 192 | 191 | | enableDetailedErrors, |
| | 192 | 192 | | enableEfCoreTraceBridge, |
| | 192 | 193 | | false); |
| | | 194 | | } |
| | | 195 | | |
| | | 196 | | public static DataAccessRegistration Disabled( |
| | | 197 | | string provider, |
| | | 198 | | string auditStorageMode) |
| | | 199 | | { |
| | 12 | 200 | | return new( |
| | 12 | 201 | | provider, |
| | 12 | 202 | | string.Empty, |
| | 12 | 203 | | auditStorageMode, |
| | 12 | 204 | | false, |
| | 12 | 205 | | false, |
| | 12 | 206 | | true); |
| | | 207 | | } |
| | | 208 | | } |
| | | 209 | | } |
| | | 210 | | |
| | | 211 | | internal static partial class EfCoreDiagnosticsLogging |
| | | 212 | | { |
| | | 213 | | [LoggerMessage( |
| | | 214 | | EventId = 19000, |
| | | 215 | | Level = LogLevel.Trace, |
| | | 216 | | Message = "{EfCoreMessage}")] |
| | | 217 | | internal static partial void LogEfCoreMessage( |
| | | 218 | | ILogger logger, |
| | | 219 | | string efCoreMessage); |
| | | 220 | | } |