using System.Linq.Expressions; using BuildingBlocks.Core.Model; using BuildingBlocks.Web; using Humanizer; using Microsoft.AspNetCore.Builder; using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.Diagnostics; using Microsoft.EntityFrameworkCore.Metadata; using Microsoft.EntityFrameworkCore.Query; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; namespace BuildingBlocks.EFCore; public static class Extensions { public static IServiceCollection AddCustomDbContext(this WebApplicationBuilder builder, string? connectionName = "") where TContext : DbContext, IDbContext { AppContext.SetSwitch("Npgsql.EnableLegacyTimestampBehavior", true); builder.Services.AddValidateOptions(); builder.Services.AddDbContext( (sp, options) => { var aspireConnectionString = builder.Configuration.GetConnectionString(connectionName.Kebaberize()); var connectionString = aspireConnectionString ?? sp.GetRequiredService().ConnectionString; ArgumentException.ThrowIfNullOrEmpty(connectionString); options.UseNpgsql( connectionString, dbOptions => { dbOptions.MigrationsAssembly(typeof(TContext).Assembly.GetName().Name); }) .UseSnakeCaseNamingConvention(); // Suppress warnings for pending model changes options.ConfigureWarnings( w => w.Ignore(RelationalEventId.PendingModelChangesWarning)); }); builder.Services.AddScoped(); builder.Services.AddScoped(sp => sp.GetRequiredService()); return builder.Services; } public static IApplicationBuilder UseMigration(this IApplicationBuilder app) where TContext : DbContext, IDbContext { MigrateAsync(app.ApplicationServices).GetAwaiter().GetResult(); SeedAsync(app.ApplicationServices).GetAwaiter().GetResult(); return app; } // ref: https://github.com/pdevito3/MessageBusTestingInMemHarness/blob/main/RecipeManagement/src/RecipeManagement/Databases/RecipesDbContext.cs public static void FilterSoftDeletedProperties(this ModelBuilder modelBuilder) { Expression> filterExpr = e => !e.IsDeleted; foreach (var mutableEntityType in modelBuilder.Model.GetEntityTypes() .Where(m => m.ClrType.IsAssignableTo(typeof(IEntity)))) { // modify expression to handle correct child type var parameter = Expression.Parameter(mutableEntityType.ClrType); var body = ReplacingExpressionVisitor .Replace(filterExpr.Parameters.First(), parameter, filterExpr.Body); var lambdaExpression = Expression.Lambda(body, parameter); // set filter mutableEntityType.SetQueryFilter(lambdaExpression); } } // ref: https://andrewlock.net/customising-asp-net-core-identity-ef-core-naming-conventions-for-postgresql/ public static void ToSnakeCaseTables(this ModelBuilder modelBuilder) { foreach (var entity in modelBuilder.Model.GetEntityTypes()) { // Replace table names entity.SetTableName(entity.GetTableName()?.Underscore()); var tableObjectIdentifier = StoreObjectIdentifier.Table( entity.GetTableName()?.Underscore()!, entity.GetSchema()); // Replace column names foreach (var property in entity.GetProperties()) { property.SetColumnName(property.GetColumnName(tableObjectIdentifier)?.Underscore()); } foreach (var key in entity.GetKeys()) { key.SetName(key.GetName()?.Underscore()); } foreach (var key in entity.GetForeignKeys()) { key.SetConstraintName(key.GetConstraintName()?.Underscore()); } } } private static async Task MigrateAsync(IServiceProvider serviceProvider) where TContext : DbContext, IDbContext { await using var scope = serviceProvider.CreateAsyncScope(); var context = scope.ServiceProvider.GetRequiredService(); var logger = scope.ServiceProvider.GetRequiredService>(); var pendingMigrations = await context.Database.GetPendingMigrationsAsync(); if (pendingMigrations.Any()) { logger.LogInformation("Applying {Count} pending migrations...", pendingMigrations.Count()); await context.Database.MigrateAsync(); logger.LogInformation("Migrations applied successfully."); } } private static async Task SeedAsync(IServiceProvider serviceProvider) { await using var scope = serviceProvider.CreateAsyncScope(); var seedersManager = scope.ServiceProvider.GetRequiredService(); await seedersManager.ExecuteSeedAsync(); } }