using Microsoft.EntityFrameworkCore; using Sharp.Data; using Sharp.Data.Context; namespace Data.Test; /// /// 演示 04: 软删除功能 /// /// 本测试类演示: /// 1. 什么是软删除 /// 2. 如何启用软删除 /// 3. 软删除 vs 物理删除 /// 4. 查询时自动过滤已删除数据 /// 5. 如何查询已删除的数据 /// [TestClass] public class _04_SoftDeleteTests { /// /// 演示: 软删除基础概念 /// /// 软删除(Soft Delete)不会真正从数据库删除记录, /// 而是将 IsDeleted 标记设置为 true /// [TestMethod] public async Task Demo_SoftDeleteBasics() { var builder = new DataContextBuilder(); builder.UseInMemoryDatabase($"SoftDeleteDemo_{Guid.NewGuid()}") .UseAudit(true) .UseSoftDelete(true) // 启用软删除 .UseCurrentUser("admin"); using var db = builder.Build(); var repo = db.Repository(); // 创建产品 var product = new Product { Name = "要删除的产品", Price = 100m, Stock = 10 }; await repo.AddAsync(product); await db.SaveChangesAsync(); Console.WriteLine($"✓ 创建产品: {product.Name}"); Console.WriteLine($" IsDeleted: {product.IsDeleted}"); var productId = product.Id; // 软删除产品 await repo.RemoveAsync(productId); await db.SaveChangesAsync(); Console.WriteLine($"\n✓ 软删除后:"); Console.WriteLine($" IsDeleted: {product.IsDeleted}"); Console.WriteLine($" DeletedAt: {product.DeletedAt:yyyy-MM-dd HH:mm:ss}"); Console.WriteLine($" DeletedBy: {product.DeletedBy}"); // 验证 Assert.IsTrue(product.IsDeleted); Assert.IsNotNull(product.DeletedAt); Assert.AreEqual("admin", product.DeletedBy); // 正常查询看不到已删除的数据 var found = await repo.GetByIdAsync(productId); Assert.IsNull(found); Console.WriteLine($"\n✓ 正常查询返回 null (数据被过滤)"); // 但数据仍在数据库中 var allIncludingDeleted = await db.Context.Set() .IgnoreQueryFilters() // 忽略软删除过滤器 .Where(p => p.Id == productId) .FirstOrDefaultAsync(); Assert.IsNotNull(allIncludingDeleted); Console.WriteLine($"✓ 使用 IgnoreQueryFilters() 仍可查询到: {allIncludingDeleted.Name}"); } /// /// 演示: 软删除 vs 物理删除 /// [TestMethod] public async Task Demo_SoftDeleteVsPhysicalDelete() { var builder = new DataContextBuilder(); builder.UseInMemoryDatabase($"DeleteCompareDemo_{Guid.NewGuid()}") .UseAudit(true) .UseSoftDelete(true) .UseCurrentUser("admin"); using var db = builder.Build(); // 1. 软删除:Product 实现了 ISoftDeletable var productRepo = db.Repository(); var product = new Product { Name = "软删除产品", Price = 100m, Stock = 10 }; await productRepo.AddAsync(product); await db.SaveChangesAsync(); await productRepo.RemoveAsync(product.Id); await db.SaveChangesAsync(); Console.WriteLine("1. 软删除 (Product):"); Console.WriteLine($" ✓ IsDeleted 被设置为 true"); Console.WriteLine($" ✓ 数据仍在数据库中"); Console.WriteLine($" ✓ 正常查询看不到"); // 验证数据仍在 var productStillExists = await db.Context.Set() .IgnoreQueryFilters() .AnyAsync(p => p.Id == product.Id); Assert.IsTrue(productStillExists); // 2. 物理删除:Category 不实现 ISoftDeletable var categoryRepo = db.Repository(); var category = new Category { Name = "物理删除分类" }; await categoryRepo.AddAsync(category); await db.SaveChangesAsync(); categoryRepo.Remove(category); await db.SaveChangesAsync(); Console.WriteLine("\n2. 物理删除 (Category):"); Console.WriteLine($" ✓ 记录被真正删除"); Console.WriteLine($" ✓ 数据库中不再存在"); // 验证数据不在 var categoryDeleted = await db.Context.Set() .AnyAsync(c => c.Id == category.Id); Assert.IsFalse(categoryDeleted); } /// /// 演示: 查询时自动过滤已删除数据 /// [TestMethod] public async Task Demo_AutoFilterDeletedData() { var builder = new DataContextBuilder(); builder.UseInMemoryDatabase($"FilterDemo_{Guid.NewGuid()}") .UseSoftDelete(true) .UseCurrentUser("admin"); using var db = builder.Build(); var repo = db.Repository(); // 添加 5 个产品 for (var i = 1; i <= 5; i++) await repo.AddAsync(new Product { Name = $"产品{i}", Price = i * 100m, Stock = i * 10 }); await db.SaveChangesAsync(); Console.WriteLine("✓ 创建了 5 个产品"); // 软删除其中 2 个 var allProducts = await repo.GetAllAsync(); await repo.RemoveAsync(allProducts[0].Id); await repo.RemoveAsync(allProducts[1].Id); await db.SaveChangesAsync(); Console.WriteLine("✓ 软删除了 2 个产品"); // 正常查询只返回未删除的 var activeProducts = await repo.GetAllAsync(); Console.WriteLine($"\n✓ 正常查询返回: {activeProducts.Count} 个产品(自动过滤已删除)"); Assert.AreEqual(3, activeProducts.Count); // 使用 IgnoreQueryFilters 查询所有 var allIncludingDeleted = await db.Context.Set() .IgnoreQueryFilters() .ToListAsync(); Console.WriteLine($"✓ IgnoreQueryFilters 返回: {allIncludingDeleted.Count} 个产品(包含已删除)"); Assert.AreEqual(5, allIncludingDeleted.Count); // 只查询已删除的 var deletedOnly = await db.Context.Set() .IgnoreQueryFilters() .Where(p => p.IsDeleted) .ToListAsync(); Console.WriteLine($"✓ 只查询已删除: {deletedOnly.Count} 个产品"); Assert.AreEqual(2, deletedOnly.Count); } /// /// 演示: 软删除的实际应用场景 /// [TestMethod] public async Task Demo_SoftDeleteRealWorldScenario() { var builder = new DataContextBuilder(); builder.UseInMemoryDatabase($"RealWorldSoftDelete_{Guid.NewGuid()}") .UseAudit(true) .UseSoftDelete(true) .UseCurrentUser("admin"); using var db = builder.Build(); var repo = db.Repository(); Console.WriteLine("=== 产品下架场景 ===\n"); // 1. 创建产品 var product = new Product { Name = "iPhone 14", Price = 6999m, Stock = 100 }; await repo.AddAsync(product); await db.SaveChangesAsync(); Console.WriteLine($"1. 产品上架"); Console.WriteLine($" {product.Name} - {product.Price:C}"); // 2. 产品下架(软删除) // (db as Sharp.Data.Context.DataContext)?.SetCurrentUser("product.manager"); await repo.RemoveAsync(product.Id); await db.SaveChangesAsync(); Console.WriteLine($"\n2. 产品下架"); Console.WriteLine($" 操作人: {product.DeletedBy}"); Console.WriteLine($" 下架时间: {product.DeletedAt:yyyy-MM-dd HH:mm:ss}"); // 3. 用户无法看到下架产品 var userVisibleProducts = await repo.GetAllAsync(); Console.WriteLine($"\n3. 用户视角"); Console.WriteLine($" 可见产品数: {userVisibleProducts.Count} (下架产品不可见)"); // 4. 管理员可以查看下架产品历史 var allProducts = await db.Context.Set() .IgnoreQueryFilters() .ToListAsync(); Console.WriteLine($"\n4. 管理员视角"); Console.WriteLine($" 所有产品(含下架): {allProducts.Count}"); Console.WriteLine($" 可恢复下架产品的历史数据"); Console.WriteLine($"\n✓ 软删除的优势:"); Console.WriteLine($" - 保留历史数据"); Console.WriteLine($" - 可以恢复"); Console.WriteLine($" - 可以审计追踪"); Console.WriteLine($" - 满足合规要求"); } /// /// 演示: 批量软删除 /// [TestMethod] public async Task Demo_BulkSoftDelete() { var builder = new DataContextBuilder(); builder.UseInMemoryDatabase($"BulkSoftDelete_{Guid.NewGuid()}") .UseSoftDelete(true) .UseCurrentUser("admin"); using var db = builder.Build(); var repo = db.Repository(); // 添加多个产品 var products = new List(); for (var i = 1; i <= 10; i++) products.Add(new Product { Name = $"产品{i}", Price = i * 100m, Stock = i * 10 }); await repo.AddRangeAsync(products); await db.SaveChangesAsync(); Console.WriteLine($"✓ 创建了 {products.Count} 个产品"); // 批量软删除价格 > 500 的产品 var expensiveProducts = await repo.FindAsync(p => p.Price > 500); repo.RemoveRange(expensiveProducts); await db.SaveChangesAsync(); Console.WriteLine($"✓ 批量软删除了 {expensiveProducts.Count} 个产品(价格>500)"); // 验证 var remaining = await repo.GetAllAsync(); Console.WriteLine($"✓ 剩余可见产品: {remaining.Count} 个"); Assert.AreEqual(5, remaining.Count); foreach (var p in expensiveProducts) Assert.IsTrue(p.IsDeleted); } /// /// 演示: 只实现软删除,不要审计 /// /// Notification 只实现了 ISoftDeletable /// [TestMethod] public async Task Demo_SoftDeleteWithoutAudit() { var builder = new DataContextBuilder(); builder.UseInMemoryDatabase($"SoftDeleteOnly_{Guid.NewGuid()}") .UseSoftDelete(true) .UseCurrentUser("admin"); using var db = builder.Build(); var repo = db.Repository(); // 创建通知 var notification = new Notification { Title = "系统通知", Content = "这是一条测试通知", CreatedAt = DateTime.UtcNow }; await repo.AddAsync(notification); await db.SaveChangesAsync(); // 软删除 await repo.RemoveAsync(notification.Id); await db.SaveChangesAsync(); Console.WriteLine("✓ Notification 只实现软删除,不要审计:"); Console.WriteLine($" IsDeleted: {notification.IsDeleted}"); Console.WriteLine($" DeletedAt: {notification.DeletedAt:yyyy-MM-dd HH:mm:ss}"); Console.WriteLine($" DeletedBy: {notification.DeletedBy}"); Console.WriteLine($" 没有 CreatedBy/UpdatedBy 字段"); Assert.IsTrue(notification.IsDeleted); } /// /// 演示: 禁用软删除时的行为 /// [TestMethod] public async Task Demo_DisableSoftDelete() { var builder = new DataContextBuilder(); builder.UseInMemoryDatabase($"DisableSoftDelete_{Guid.NewGuid()}") .UseSoftDelete(false) // 禁用软删除 .UseCurrentUser("admin"); using var db = builder.Build(); var repo = db.Repository(); // 添加产品 var product = new Product { Name = "测试产品", Price = 100m, Stock = 10 }; await repo.AddAsync(product); await db.SaveChangesAsync(); var productId = product.Id; // 删除产品(禁用软删除后,RemoveAsync 会尝试物理删除) // 但 Product 实体仍有 IsDeleted 字段,所以可能变成软删除 await repo.RemoveAsync(productId); await db.SaveChangesAsync(); Console.WriteLine("✓ 禁用软删除功能后:"); Console.WriteLine($" RemoveAsync 的行为取决于实现"); Console.WriteLine($" 建议:如果不需要软删除,不要让实体实现 ISoftDeletable"); // 查询 var found = await db.Context.Set() .IgnoreQueryFilters() .FirstOrDefaultAsync(p => p.Id == productId); if (found != null) Console.WriteLine($" 结果:软删除 (IsDeleted={found.IsDeleted})"); else Console.WriteLine($" 结果:物理删除"); } }