using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.Design; using NodaTime; namespace DysonNetwork.Sphere; public abstract class BaseModel { public Instant CreatedAt { get; set; } public Instant UpdatedAt { get; set; } public Instant? DeletedAt { get; set; } } public class AppDatabase(DbContextOptions options) : DbContext(options) { public DbSet Accounts { get; set; } public DbSet AccountContacts { get; set; } public DbSet AccountAuthFactors { get; set; } protected override void OnModelCreating(ModelBuilder modelBuilder) { base.OnModelCreating(modelBuilder); // Automatically apply soft-delete filter to all entities inheriting BaseModel foreach (var entityType in modelBuilder.Model.GetEntityTypes()) { if (typeof(BaseModel).IsAssignableFrom(entityType.ClrType)) { var method = typeof(AppDatabase) .GetMethod(nameof(SetSoftDeleteFilter), System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Static)! .MakeGenericMethod(entityType.ClrType); method.Invoke(null, [modelBuilder]); } } } private static void SetSoftDeleteFilter(ModelBuilder modelBuilder) where TEntity : BaseModel { modelBuilder.Entity().HasQueryFilter(e => e.DeletedAt == null); } public override async Task SaveChangesAsync(CancellationToken cancellationToken = default) { var now = SystemClock.Instance.GetCurrentInstant(); foreach (var entry in ChangeTracker.Entries()) { switch (entry.State) { case EntityState.Added: entry.Entity.CreatedAt = now; entry.Entity.UpdatedAt = now; break; case EntityState.Modified: entry.Entity.UpdatedAt = now; break; case EntityState.Deleted: entry.State = EntityState.Modified; entry.Entity.DeletedAt = now; break; case EntityState.Detached: case EntityState.Unchanged: default: break; } } return await base.SaveChangesAsync(cancellationToken); } } public class AppDatabaseFactory : IDesignTimeDbContextFactory { public AppDatabase CreateDbContext(string[] args) { var configuration = new ConfigurationBuilder() .SetBasePath(Directory.GetCurrentDirectory()) .AddJsonFile("appsettings.json") .Build(); var optionsBuilder = new DbContextOptionsBuilder(); optionsBuilder.UseNpgsql( configuration.GetConnectionString("App"), o => o.UseNodaTime() ).UseSnakeCaseNamingConvention(); return new AppDatabase(optionsBuilder.Options); } }