diff --git a/DysonNetwork.Develop/AppDatabase.cs b/DysonNetwork.Develop/AppDatabase.cs index 13b9e9b..29aff8a 100644 --- a/DysonNetwork.Develop/AppDatabase.cs +++ b/DysonNetwork.Develop/AppDatabase.cs @@ -1,3 +1,4 @@ +using DysonNetwork.Shared.Data; using DysonNetwork.Shared.Models; using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.Design; @@ -33,36 +34,15 @@ public class AppDatabase( 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; - } - } - + this.ApplyAuditableAndSoftDelete(); return await base.SaveChangesAsync(cancellationToken); } protected override void OnModelCreating(ModelBuilder modelBuilder) { base.OnModelCreating(modelBuilder); + + modelBuilder.ApplySoftDeleteFilters(); } } diff --git a/DysonNetwork.Drive/AppDatabase.cs b/DysonNetwork.Drive/AppDatabase.cs index 2bb530d..85eded4 100644 --- a/DysonNetwork.Drive/AppDatabase.cs +++ b/DysonNetwork.Drive/AppDatabase.cs @@ -1,8 +1,8 @@ using System.Linq.Expressions; -using System.Reflection; using DysonNetwork.Drive.Billing; using DysonNetwork.Drive.Storage; using DysonNetwork.Drive.Storage.Model; +using DysonNetwork.Shared.Data; using DysonNetwork.Shared.Models; using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.Design; @@ -46,67 +46,12 @@ public class AppDatabase( protected override void OnModelCreating(ModelBuilder modelBuilder) { base.OnModelCreating(modelBuilder); - - // Apply soft-delete filter only to root entities, not derived types - var entityTypes = modelBuilder.Model.GetEntityTypes(); - foreach (var entityType in entityTypes) - { - if (!typeof(ModelBase).IsAssignableFrom(entityType.ClrType)) continue; - - // Skip derived types to avoid filter conflicts - var clrType = entityType.ClrType; - if (clrType.BaseType != typeof(ModelBase) && - typeof(ModelBase).IsAssignableFrom(clrType.BaseType)) - { - continue; // Skip derived types - } - - // Apply soft delete filter using cached reflection - ApplySoftDeleteFilter(modelBuilder, clrType); - } - } - - private static readonly MethodInfo SetSoftDeleteFilterMethod = typeof(AppDatabase) - .GetMethod(nameof(SetSoftDeleteFilter), BindingFlags.NonPublic | BindingFlags.Static)!; - - private static void ApplySoftDeleteFilter(ModelBuilder modelBuilder, Type entityType) - { - var genericMethod = SetSoftDeleteFilterMethod.MakeGenericMethod(entityType); - genericMethod.Invoke(null, [modelBuilder]); - } - - private static void SetSoftDeleteFilter(ModelBuilder modelBuilder) - where TEntity : ModelBase - { - modelBuilder.Entity().HasQueryFilter(e => e.DeletedAt == null); + modelBuilder.ApplySoftDeleteFilters(); } 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; - } - } - + this.ApplyAuditableAndSoftDelete(); return await base.SaveChangesAsync(cancellationToken); } } @@ -210,35 +155,3 @@ public class AppDatabaseFactory : IDesignTimeDbContextFactory return new AppDatabase(optionsBuilder.Options, configuration); } } - -public static class OptionalQueryExtensions -{ - public static IQueryable If( - this IQueryable source, - bool condition, - Func, IQueryable> transform - ) - { - return condition ? transform(source) : source; - } - - public static IQueryable If( - this IIncludableQueryable source, - bool condition, - Func, IQueryable> transform - ) - where T : class - { - return condition ? transform(source) : source; - } - - public static IQueryable If( - this IIncludableQueryable> source, - bool condition, - Func>, IQueryable> transform - ) - where T : class - { - return condition ? transform(source) : source; - } -} diff --git a/DysonNetwork.Drive/Storage/FileReferenceService.cs b/DysonNetwork.Drive/Storage/FileReferenceService.cs index 258ed65..ae6a53c 100644 --- a/DysonNetwork.Drive/Storage/FileReferenceService.cs +++ b/DysonNetwork.Drive/Storage/FileReferenceService.cs @@ -1,4 +1,5 @@ using DysonNetwork.Shared.Cache; +using DysonNetwork.Shared.Data; using DysonNetwork.Shared.Models; using EFCore.BulkExtensions; using Microsoft.EntityFrameworkCore; diff --git a/DysonNetwork.Insight/AppDatabase.cs b/DysonNetwork.Insight/AppDatabase.cs index 5f7d3b0..79063f2 100644 --- a/DysonNetwork.Insight/AppDatabase.cs +++ b/DysonNetwork.Insight/AppDatabase.cs @@ -1,3 +1,4 @@ +using DysonNetwork.Shared.Data; using DysonNetwork.Shared.Models; using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.Design; @@ -29,36 +30,15 @@ public class AppDatabase( 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; - } - } - + this.ApplyAuditableAndSoftDelete(); return await base.SaveChangesAsync(cancellationToken); } protected override void OnModelCreating(ModelBuilder modelBuilder) { base.OnModelCreating(modelBuilder); + + modelBuilder.ApplySoftDeleteFilters(); } } diff --git a/DysonNetwork.Pass/AppDatabase.cs b/DysonNetwork.Pass/AppDatabase.cs index 10a4c43..f846d1f 100644 --- a/DysonNetwork.Pass/AppDatabase.cs +++ b/DysonNetwork.Pass/AppDatabase.cs @@ -1,8 +1,8 @@ using System.Linq.Expressions; -using System.Reflection; using System.Text.Json; using System.Text.Json.Serialization; using DysonNetwork.Pass.Permission; +using DysonNetwork.Shared.Data; using DysonNetwork.Shared.Models; using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.Design; @@ -143,51 +143,12 @@ public class AppDatabase( .HasForeignKey(pm => pm.RealmId) .OnDelete(DeleteBehavior.Cascade); - // Automatically apply soft-delete filter to all entities inheriting BaseModel - foreach (var entityType in modelBuilder.Model.GetEntityTypes()) - { - if (!typeof(ModelBase).IsAssignableFrom(entityType.ClrType)) continue; - var method = typeof(AppDatabase) - .GetMethod(nameof(SetSoftDeleteFilter), - BindingFlags.NonPublic | BindingFlags.Static)! - .MakeGenericMethod(entityType.ClrType); - - method.Invoke(null, [modelBuilder]); - } - } - - private static void SetSoftDeleteFilter(ModelBuilder modelBuilder) - where TEntity : ModelBase - { - modelBuilder.Entity().HasQueryFilter(e => e.DeletedAt == null); + modelBuilder.ApplySoftDeleteFilters(); } 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; - } - } - + this.ApplyAuditableAndSoftDelete(); return await base.SaveChangesAsync(cancellationToken); } } @@ -266,34 +227,3 @@ public class AppDatabaseFactory : IDesignTimeDbContextFactory } } -public static class OptionalQueryExtensions -{ - public static IQueryable If( - this IQueryable source, - bool condition, - Func, IQueryable> transform - ) - { - return condition ? transform(source) : source; - } - - public static IQueryable If( - this IIncludableQueryable source, - bool condition, - Func, IQueryable> transform - ) - where T : class - { - return condition ? transform(source) : source; - } - - public static IQueryable If( - this IIncludableQueryable> source, - bool condition, - Func>, IQueryable> transform - ) - where T : class - { - return condition ? transform(source) : source; - } -} diff --git a/DysonNetwork.Ring/AppDatabase.cs b/DysonNetwork.Ring/AppDatabase.cs index 8947473..338f334 100644 --- a/DysonNetwork.Ring/AppDatabase.cs +++ b/DysonNetwork.Ring/AppDatabase.cs @@ -1,5 +1,5 @@ using System.Linq.Expressions; -using System.Reflection; +using DysonNetwork.Shared.Data; using DysonNetwork.Shared.Models; using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.Design; @@ -34,51 +34,12 @@ public class AppDatabase( { base.OnModelCreating(modelBuilder); - // Automatically apply soft-delete filter to all entities inheriting BaseModel - foreach (var entityType in modelBuilder.Model.GetEntityTypes()) - { - if (!typeof(ModelBase).IsAssignableFrom(entityType.ClrType)) continue; - var method = typeof(AppDatabase) - .GetMethod(nameof(SetSoftDeleteFilter), - BindingFlags.NonPublic | BindingFlags.Static)! - .MakeGenericMethod(entityType.ClrType); - - method.Invoke(null, [modelBuilder]); - } - } - - private static void SetSoftDeleteFilter(ModelBuilder modelBuilder) - where TEntity : ModelBase - { - modelBuilder.Entity().HasQueryFilter(e => e.DeletedAt == null); + modelBuilder.ApplySoftDeleteFilters(); } 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; - } - } - + this.ApplyAuditableAndSoftDelete(); return await base.SaveChangesAsync(cancellationToken); } } @@ -143,35 +104,3 @@ public class AppDatabaseFactory : IDesignTimeDbContextFactory return new AppDatabase(optionsBuilder.Options, configuration); } } - -public static class OptionalQueryExtensions -{ - public static IQueryable If( - this IQueryable source, - bool condition, - Func, IQueryable> transform - ) - { - return condition ? transform(source) : source; - } - - public static IQueryable If( - this IIncludableQueryable source, - bool condition, - Func, IQueryable> transform - ) - where T : class - { - return condition ? transform(source) : source; - } - - public static IQueryable If( - this IIncludableQueryable> source, - bool condition, - Func>, IQueryable> transform - ) - where T : class - { - return condition ? transform(source) : source; - } -} diff --git a/DysonNetwork.Shared/Data/OptionalQueryExtension.cs b/DysonNetwork.Shared/Data/OptionalQueryExtension.cs new file mode 100644 index 0000000..0fd541a --- /dev/null +++ b/DysonNetwork.Shared/Data/OptionalQueryExtension.cs @@ -0,0 +1,35 @@ +using Microsoft.EntityFrameworkCore.Query; + +namespace DysonNetwork.Shared.Data; + +public static class OptionalQueryExtensions +{ + public static IQueryable If( + this IQueryable source, + bool condition, + Func, IQueryable> transform + ) + { + return condition ? transform(source) : source; + } + + public static IQueryable If( + this IIncludableQueryable source, + bool condition, + Func, IQueryable> transform + ) + where T : class + { + return condition ? transform(source) : source; + } + + public static IQueryable If( + this IIncludableQueryable> source, + bool condition, + Func>, IQueryable> transform + ) + where T : class + { + return condition ? transform(source) : source; + } +} \ No newline at end of file diff --git a/DysonNetwork.Shared/Data/SoftDeleteExtension.cs b/DysonNetwork.Shared/Data/SoftDeleteExtension.cs new file mode 100644 index 0000000..ad9de32 --- /dev/null +++ b/DysonNetwork.Shared/Data/SoftDeleteExtension.cs @@ -0,0 +1,65 @@ +using System.Reflection; +using DysonNetwork.Shared.Models; +using Microsoft.EntityFrameworkCore; +using NodaTime; + +namespace DysonNetwork.Shared.Data; + +public static class SoftDeleteExtension +{ + public static void ApplySoftDeleteFilters(this ModelBuilder modelBuilder) + { + var entityTypes = modelBuilder.Model.GetEntityTypes(); + foreach (var entityType in entityTypes) + { + if (!typeof(ModelBase).IsAssignableFrom(entityType.ClrType)) continue; + + // Skip derived types to avoid filter conflicts + var clrType = entityType.ClrType; + if (clrType.BaseType != typeof(ModelBase) && + typeof(ModelBase).IsAssignableFrom(clrType.BaseType)) + { + continue; // Skip derived types + } + + // Apply soft delete filter using cached reflection + var method = typeof(SoftDeleteExtension) + .GetMethod(nameof(SetSoftDeleteFilter), BindingFlags.NonPublic | BindingFlags.Static)! + .MakeGenericMethod(entityType.ClrType); + method.Invoke(null, new object[] { modelBuilder }); + } + } + + private static void SetSoftDeleteFilter(ModelBuilder modelBuilder) + where TEntity : ModelBase + { + modelBuilder.Entity().HasQueryFilter(e => e.DeletedAt == null); + } + + public static void ApplyAuditableAndSoftDelete(this DbContext context) + { + var now = SystemClock.Instance.GetCurrentInstant(); + + foreach (var entry in context.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; + } + } + } +} diff --git a/DysonNetwork.Sphere/AppDatabase.cs b/DysonNetwork.Sphere/AppDatabase.cs index a1c74c7..5096a15 100644 --- a/DysonNetwork.Sphere/AppDatabase.cs +++ b/DysonNetwork.Sphere/AppDatabase.cs @@ -1,5 +1,5 @@ using System.Linq.Expressions; -using System.Reflection; +using DysonNetwork.Shared.Data; using DysonNetwork.Shared.Models; using DysonNetwork.Sphere.WebReader; using Microsoft.EntityFrameworkCore; @@ -142,17 +142,7 @@ public class AppDatabase( .HasIndex(a => a.Url) .IsUnique(); - // Automatically apply soft-delete filter to all entities inheriting BaseModel - foreach (var entityType in modelBuilder.Model.GetEntityTypes()) - { - if (!typeof(ModelBase).IsAssignableFrom(entityType.ClrType)) continue; - var method = typeof(AppDatabase) - .GetMethod(nameof(SetSoftDeleteFilter), - BindingFlags.NonPublic | BindingFlags.Static)! - .MakeGenericMethod(entityType.ClrType); - - method.Invoke(null, [modelBuilder]); - } + modelBuilder.ApplySoftDeleteFilters(); } private static void SetSoftDeleteFilter(ModelBuilder modelBuilder) @@ -163,30 +153,7 @@ public class AppDatabase( 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; - } - } - + this.ApplyAuditableAndSoftDelete(); return await base.SaveChangesAsync(cancellationToken); } } @@ -252,34 +219,3 @@ public class AppDatabaseFactory : IDesignTimeDbContextFactory } } -public static class OptionalQueryExtensions -{ - public static IQueryable If( - this IQueryable source, - bool condition, - Func, IQueryable> transform - ) - { - return condition ? transform(source) : source; - } - - public static IQueryable If( - this IIncludableQueryable source, - bool condition, - Func, IQueryable> transform - ) - where T : class - { - return condition ? transform(source) : source; - } - - public static IQueryable If( - this IIncludableQueryable> source, - bool condition, - Func>, IQueryable> transform - ) - where T : class - { - return condition ? transform(source) : source; - } -}