♻️ Replace the soft delete logic with the new shared one
This commit is contained in:
@@ -1,3 +1,4 @@
|
|||||||
|
using DysonNetwork.Shared.Data;
|
||||||
using DysonNetwork.Shared.Models;
|
using DysonNetwork.Shared.Models;
|
||||||
using Microsoft.EntityFrameworkCore;
|
using Microsoft.EntityFrameworkCore;
|
||||||
using Microsoft.EntityFrameworkCore.Design;
|
using Microsoft.EntityFrameworkCore.Design;
|
||||||
@@ -33,36 +34,15 @@ public class AppDatabase(
|
|||||||
|
|
||||||
public override async Task<int> SaveChangesAsync(CancellationToken cancellationToken = default)
|
public override async Task<int> SaveChangesAsync(CancellationToken cancellationToken = default)
|
||||||
{
|
{
|
||||||
var now = SystemClock.Instance.GetCurrentInstant();
|
this.ApplyAuditableAndSoftDelete();
|
||||||
|
|
||||||
foreach (var entry in ChangeTracker.Entries<ModelBase>())
|
|
||||||
{
|
|
||||||
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);
|
return await base.SaveChangesAsync(cancellationToken);
|
||||||
}
|
}
|
||||||
|
|
||||||
protected override void OnModelCreating(ModelBuilder modelBuilder)
|
protected override void OnModelCreating(ModelBuilder modelBuilder)
|
||||||
{
|
{
|
||||||
base.OnModelCreating(modelBuilder);
|
base.OnModelCreating(modelBuilder);
|
||||||
|
|
||||||
|
modelBuilder.ApplySoftDeleteFilters();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
using System.Linq.Expressions;
|
using System.Linq.Expressions;
|
||||||
using System.Reflection;
|
|
||||||
using DysonNetwork.Drive.Billing;
|
using DysonNetwork.Drive.Billing;
|
||||||
using DysonNetwork.Drive.Storage;
|
using DysonNetwork.Drive.Storage;
|
||||||
using DysonNetwork.Drive.Storage.Model;
|
using DysonNetwork.Drive.Storage.Model;
|
||||||
|
using DysonNetwork.Shared.Data;
|
||||||
using DysonNetwork.Shared.Models;
|
using DysonNetwork.Shared.Models;
|
||||||
using Microsoft.EntityFrameworkCore;
|
using Microsoft.EntityFrameworkCore;
|
||||||
using Microsoft.EntityFrameworkCore.Design;
|
using Microsoft.EntityFrameworkCore.Design;
|
||||||
@@ -46,67 +46,12 @@ public class AppDatabase(
|
|||||||
protected override void OnModelCreating(ModelBuilder modelBuilder)
|
protected override void OnModelCreating(ModelBuilder modelBuilder)
|
||||||
{
|
{
|
||||||
base.OnModelCreating(modelBuilder);
|
base.OnModelCreating(modelBuilder);
|
||||||
|
modelBuilder.ApplySoftDeleteFilters();
|
||||||
// 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<TEntity>(ModelBuilder modelBuilder)
|
|
||||||
where TEntity : ModelBase
|
|
||||||
{
|
|
||||||
modelBuilder.Entity<TEntity>().HasQueryFilter(e => e.DeletedAt == null);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public override async Task<int> SaveChangesAsync(CancellationToken cancellationToken = default)
|
public override async Task<int> SaveChangesAsync(CancellationToken cancellationToken = default)
|
||||||
{
|
{
|
||||||
var now = SystemClock.Instance.GetCurrentInstant();
|
this.ApplyAuditableAndSoftDelete();
|
||||||
|
|
||||||
foreach (var entry in ChangeTracker.Entries<ModelBase>())
|
|
||||||
{
|
|
||||||
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);
|
return await base.SaveChangesAsync(cancellationToken);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -210,35 +155,3 @@ public class AppDatabaseFactory : IDesignTimeDbContextFactory<AppDatabase>
|
|||||||
return new AppDatabase(optionsBuilder.Options, configuration);
|
return new AppDatabase(optionsBuilder.Options, configuration);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public static class OptionalQueryExtensions
|
|
||||||
{
|
|
||||||
public static IQueryable<T> If<T>(
|
|
||||||
this IQueryable<T> source,
|
|
||||||
bool condition,
|
|
||||||
Func<IQueryable<T>, IQueryable<T>> transform
|
|
||||||
)
|
|
||||||
{
|
|
||||||
return condition ? transform(source) : source;
|
|
||||||
}
|
|
||||||
|
|
||||||
public static IQueryable<T> If<T, TP>(
|
|
||||||
this IIncludableQueryable<T, TP> source,
|
|
||||||
bool condition,
|
|
||||||
Func<IIncludableQueryable<T, TP>, IQueryable<T>> transform
|
|
||||||
)
|
|
||||||
where T : class
|
|
||||||
{
|
|
||||||
return condition ? transform(source) : source;
|
|
||||||
}
|
|
||||||
|
|
||||||
public static IQueryable<T> If<T, TP>(
|
|
||||||
this IIncludableQueryable<T, IEnumerable<TP>> source,
|
|
||||||
bool condition,
|
|
||||||
Func<IIncludableQueryable<T, IEnumerable<TP>>, IQueryable<T>> transform
|
|
||||||
)
|
|
||||||
where T : class
|
|
||||||
{
|
|
||||||
return condition ? transform(source) : source;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
using DysonNetwork.Shared.Cache;
|
using DysonNetwork.Shared.Cache;
|
||||||
|
using DysonNetwork.Shared.Data;
|
||||||
using DysonNetwork.Shared.Models;
|
using DysonNetwork.Shared.Models;
|
||||||
using EFCore.BulkExtensions;
|
using EFCore.BulkExtensions;
|
||||||
using Microsoft.EntityFrameworkCore;
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
using DysonNetwork.Shared.Data;
|
||||||
using DysonNetwork.Shared.Models;
|
using DysonNetwork.Shared.Models;
|
||||||
using Microsoft.EntityFrameworkCore;
|
using Microsoft.EntityFrameworkCore;
|
||||||
using Microsoft.EntityFrameworkCore.Design;
|
using Microsoft.EntityFrameworkCore.Design;
|
||||||
@@ -29,36 +30,15 @@ public class AppDatabase(
|
|||||||
|
|
||||||
public override async Task<int> SaveChangesAsync(CancellationToken cancellationToken = default)
|
public override async Task<int> SaveChangesAsync(CancellationToken cancellationToken = default)
|
||||||
{
|
{
|
||||||
var now = SystemClock.Instance.GetCurrentInstant();
|
this.ApplyAuditableAndSoftDelete();
|
||||||
|
|
||||||
foreach (var entry in ChangeTracker.Entries<ModelBase>())
|
|
||||||
{
|
|
||||||
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);
|
return await base.SaveChangesAsync(cancellationToken);
|
||||||
}
|
}
|
||||||
|
|
||||||
protected override void OnModelCreating(ModelBuilder modelBuilder)
|
protected override void OnModelCreating(ModelBuilder modelBuilder)
|
||||||
{
|
{
|
||||||
base.OnModelCreating(modelBuilder);
|
base.OnModelCreating(modelBuilder);
|
||||||
|
|
||||||
|
modelBuilder.ApplySoftDeleteFilters();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
using System.Linq.Expressions;
|
using System.Linq.Expressions;
|
||||||
using System.Reflection;
|
|
||||||
using System.Text.Json;
|
using System.Text.Json;
|
||||||
using System.Text.Json.Serialization;
|
using System.Text.Json.Serialization;
|
||||||
using DysonNetwork.Pass.Permission;
|
using DysonNetwork.Pass.Permission;
|
||||||
|
using DysonNetwork.Shared.Data;
|
||||||
using DysonNetwork.Shared.Models;
|
using DysonNetwork.Shared.Models;
|
||||||
using Microsoft.EntityFrameworkCore;
|
using Microsoft.EntityFrameworkCore;
|
||||||
using Microsoft.EntityFrameworkCore.Design;
|
using Microsoft.EntityFrameworkCore.Design;
|
||||||
@@ -143,51 +143,12 @@ public class AppDatabase(
|
|||||||
.HasForeignKey(pm => pm.RealmId)
|
.HasForeignKey(pm => pm.RealmId)
|
||||||
.OnDelete(DeleteBehavior.Cascade);
|
.OnDelete(DeleteBehavior.Cascade);
|
||||||
|
|
||||||
// Automatically apply soft-delete filter to all entities inheriting BaseModel
|
modelBuilder.ApplySoftDeleteFilters();
|
||||||
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<TEntity>(ModelBuilder modelBuilder)
|
|
||||||
where TEntity : ModelBase
|
|
||||||
{
|
|
||||||
modelBuilder.Entity<TEntity>().HasQueryFilter(e => e.DeletedAt == null);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public override async Task<int> SaveChangesAsync(CancellationToken cancellationToken = default)
|
public override async Task<int> SaveChangesAsync(CancellationToken cancellationToken = default)
|
||||||
{
|
{
|
||||||
var now = SystemClock.Instance.GetCurrentInstant();
|
this.ApplyAuditableAndSoftDelete();
|
||||||
|
|
||||||
foreach (var entry in ChangeTracker.Entries<ModelBase>())
|
|
||||||
{
|
|
||||||
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);
|
return await base.SaveChangesAsync(cancellationToken);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -266,34 +227,3 @@ public class AppDatabaseFactory : IDesignTimeDbContextFactory<AppDatabase>
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public static class OptionalQueryExtensions
|
|
||||||
{
|
|
||||||
public static IQueryable<T> If<T>(
|
|
||||||
this IQueryable<T> source,
|
|
||||||
bool condition,
|
|
||||||
Func<IQueryable<T>, IQueryable<T>> transform
|
|
||||||
)
|
|
||||||
{
|
|
||||||
return condition ? transform(source) : source;
|
|
||||||
}
|
|
||||||
|
|
||||||
public static IQueryable<T> If<T, TP>(
|
|
||||||
this IIncludableQueryable<T, TP> source,
|
|
||||||
bool condition,
|
|
||||||
Func<IIncludableQueryable<T, TP>, IQueryable<T>> transform
|
|
||||||
)
|
|
||||||
where T : class
|
|
||||||
{
|
|
||||||
return condition ? transform(source) : source;
|
|
||||||
}
|
|
||||||
|
|
||||||
public static IQueryable<T> If<T, TP>(
|
|
||||||
this IIncludableQueryable<T, IEnumerable<TP>> source,
|
|
||||||
bool condition,
|
|
||||||
Func<IIncludableQueryable<T, IEnumerable<TP>>, IQueryable<T>> transform
|
|
||||||
)
|
|
||||||
where T : class
|
|
||||||
{
|
|
||||||
return condition ? transform(source) : source;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
using System.Linq.Expressions;
|
using System.Linq.Expressions;
|
||||||
using System.Reflection;
|
using DysonNetwork.Shared.Data;
|
||||||
using DysonNetwork.Shared.Models;
|
using DysonNetwork.Shared.Models;
|
||||||
using Microsoft.EntityFrameworkCore;
|
using Microsoft.EntityFrameworkCore;
|
||||||
using Microsoft.EntityFrameworkCore.Design;
|
using Microsoft.EntityFrameworkCore.Design;
|
||||||
@@ -34,51 +34,12 @@ public class AppDatabase(
|
|||||||
{
|
{
|
||||||
base.OnModelCreating(modelBuilder);
|
base.OnModelCreating(modelBuilder);
|
||||||
|
|
||||||
// Automatically apply soft-delete filter to all entities inheriting BaseModel
|
modelBuilder.ApplySoftDeleteFilters();
|
||||||
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<TEntity>(ModelBuilder modelBuilder)
|
|
||||||
where TEntity : ModelBase
|
|
||||||
{
|
|
||||||
modelBuilder.Entity<TEntity>().HasQueryFilter(e => e.DeletedAt == null);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public override async Task<int> SaveChangesAsync(CancellationToken cancellationToken = default)
|
public override async Task<int> SaveChangesAsync(CancellationToken cancellationToken = default)
|
||||||
{
|
{
|
||||||
var now = SystemClock.Instance.GetCurrentInstant();
|
this.ApplyAuditableAndSoftDelete();
|
||||||
|
|
||||||
foreach (var entry in ChangeTracker.Entries<ModelBase>())
|
|
||||||
{
|
|
||||||
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);
|
return await base.SaveChangesAsync(cancellationToken);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -143,35 +104,3 @@ public class AppDatabaseFactory : IDesignTimeDbContextFactory<AppDatabase>
|
|||||||
return new AppDatabase(optionsBuilder.Options, configuration);
|
return new AppDatabase(optionsBuilder.Options, configuration);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public static class OptionalQueryExtensions
|
|
||||||
{
|
|
||||||
public static IQueryable<T> If<T>(
|
|
||||||
this IQueryable<T> source,
|
|
||||||
bool condition,
|
|
||||||
Func<IQueryable<T>, IQueryable<T>> transform
|
|
||||||
)
|
|
||||||
{
|
|
||||||
return condition ? transform(source) : source;
|
|
||||||
}
|
|
||||||
|
|
||||||
public static IQueryable<T> If<T, TP>(
|
|
||||||
this IIncludableQueryable<T, TP> source,
|
|
||||||
bool condition,
|
|
||||||
Func<IIncludableQueryable<T, TP>, IQueryable<T>> transform
|
|
||||||
)
|
|
||||||
where T : class
|
|
||||||
{
|
|
||||||
return condition ? transform(source) : source;
|
|
||||||
}
|
|
||||||
|
|
||||||
public static IQueryable<T> If<T, TP>(
|
|
||||||
this IIncludableQueryable<T, IEnumerable<TP>> source,
|
|
||||||
bool condition,
|
|
||||||
Func<IIncludableQueryable<T, IEnumerable<TP>>, IQueryable<T>> transform
|
|
||||||
)
|
|
||||||
where T : class
|
|
||||||
{
|
|
||||||
return condition ? transform(source) : source;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|||||||
35
DysonNetwork.Shared/Data/OptionalQueryExtension.cs
Normal file
35
DysonNetwork.Shared/Data/OptionalQueryExtension.cs
Normal file
@@ -0,0 +1,35 @@
|
|||||||
|
using Microsoft.EntityFrameworkCore.Query;
|
||||||
|
|
||||||
|
namespace DysonNetwork.Shared.Data;
|
||||||
|
|
||||||
|
public static class OptionalQueryExtensions
|
||||||
|
{
|
||||||
|
public static IQueryable<T> If<T>(
|
||||||
|
this IQueryable<T> source,
|
||||||
|
bool condition,
|
||||||
|
Func<IQueryable<T>, IQueryable<T>> transform
|
||||||
|
)
|
||||||
|
{
|
||||||
|
return condition ? transform(source) : source;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static IQueryable<T> If<T, TP>(
|
||||||
|
this IIncludableQueryable<T, TP> source,
|
||||||
|
bool condition,
|
||||||
|
Func<IIncludableQueryable<T, TP>, IQueryable<T>> transform
|
||||||
|
)
|
||||||
|
where T : class
|
||||||
|
{
|
||||||
|
return condition ? transform(source) : source;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static IQueryable<T> If<T, TP>(
|
||||||
|
this IIncludableQueryable<T, IEnumerable<TP>> source,
|
||||||
|
bool condition,
|
||||||
|
Func<IIncludableQueryable<T, IEnumerable<TP>>, IQueryable<T>> transform
|
||||||
|
)
|
||||||
|
where T : class
|
||||||
|
{
|
||||||
|
return condition ? transform(source) : source;
|
||||||
|
}
|
||||||
|
}
|
||||||
65
DysonNetwork.Shared/Data/SoftDeleteExtension.cs
Normal file
65
DysonNetwork.Shared/Data/SoftDeleteExtension.cs
Normal file
@@ -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<TEntity>(ModelBuilder modelBuilder)
|
||||||
|
where TEntity : ModelBase
|
||||||
|
{
|
||||||
|
modelBuilder.Entity<TEntity>().HasQueryFilter(e => e.DeletedAt == null);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void ApplyAuditableAndSoftDelete(this DbContext context)
|
||||||
|
{
|
||||||
|
var now = SystemClock.Instance.GetCurrentInstant();
|
||||||
|
|
||||||
|
foreach (var entry in context.ChangeTracker.Entries<ModelBase>())
|
||||||
|
{
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
using System.Linq.Expressions;
|
using System.Linq.Expressions;
|
||||||
using System.Reflection;
|
using DysonNetwork.Shared.Data;
|
||||||
using DysonNetwork.Shared.Models;
|
using DysonNetwork.Shared.Models;
|
||||||
using DysonNetwork.Sphere.WebReader;
|
using DysonNetwork.Sphere.WebReader;
|
||||||
using Microsoft.EntityFrameworkCore;
|
using Microsoft.EntityFrameworkCore;
|
||||||
@@ -142,17 +142,7 @@ public class AppDatabase(
|
|||||||
.HasIndex(a => a.Url)
|
.HasIndex(a => a.Url)
|
||||||
.IsUnique();
|
.IsUnique();
|
||||||
|
|
||||||
// Automatically apply soft-delete filter to all entities inheriting BaseModel
|
modelBuilder.ApplySoftDeleteFilters();
|
||||||
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<TEntity>(ModelBuilder modelBuilder)
|
private static void SetSoftDeleteFilter<TEntity>(ModelBuilder modelBuilder)
|
||||||
@@ -163,30 +153,7 @@ public class AppDatabase(
|
|||||||
|
|
||||||
public override async Task<int> SaveChangesAsync(CancellationToken cancellationToken = default)
|
public override async Task<int> SaveChangesAsync(CancellationToken cancellationToken = default)
|
||||||
{
|
{
|
||||||
var now = SystemClock.Instance.GetCurrentInstant();
|
this.ApplyAuditableAndSoftDelete();
|
||||||
|
|
||||||
foreach (var entry in ChangeTracker.Entries<ModelBase>())
|
|
||||||
{
|
|
||||||
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);
|
return await base.SaveChangesAsync(cancellationToken);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -252,34 +219,3 @@ public class AppDatabaseFactory : IDesignTimeDbContextFactory<AppDatabase>
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public static class OptionalQueryExtensions
|
|
||||||
{
|
|
||||||
public static IQueryable<T> If<T>(
|
|
||||||
this IQueryable<T> source,
|
|
||||||
bool condition,
|
|
||||||
Func<IQueryable<T>, IQueryable<T>> transform
|
|
||||||
)
|
|
||||||
{
|
|
||||||
return condition ? transform(source) : source;
|
|
||||||
}
|
|
||||||
|
|
||||||
public static IQueryable<T> If<T, TP>(
|
|
||||||
this IIncludableQueryable<T, TP> source,
|
|
||||||
bool condition,
|
|
||||||
Func<IIncludableQueryable<T, TP>, IQueryable<T>> transform
|
|
||||||
)
|
|
||||||
where T : class
|
|
||||||
{
|
|
||||||
return condition ? transform(source) : source;
|
|
||||||
}
|
|
||||||
|
|
||||||
public static IQueryable<T> If<T, TP>(
|
|
||||||
this IIncludableQueryable<T, IEnumerable<TP>> source,
|
|
||||||
bool condition,
|
|
||||||
Func<IIncludableQueryable<T, IEnumerable<TP>>, IQueryable<T>> transform
|
|
||||||
)
|
|
||||||
where T : class
|
|
||||||
{
|
|
||||||
return condition ? transform(source) : source;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|||||||
Reference in New Issue
Block a user