Files
sharp-basic/test/Data.Test/04_SoftDeleteTests.cs
T
2025-12-30 09:17:25 +08:00

371 lines
13 KiB
C#
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
using Microsoft.EntityFrameworkCore;
using Sharp.Data;
using Sharp.Data.Context;
namespace Data.Test;
/// <summary>
/// 演示 04: 软删除功能
///
/// 本测试类演示:
/// 1. 什么是软删除
/// 2. 如何启用软删除
/// 3. 软删除 vs 物理删除
/// 4. 查询时自动过滤已删除数据
/// 5. 如何查询已删除的数据
/// </summary>
[TestClass]
public class _04_SoftDeleteTests
{
/// <summary>
/// 演示: 软删除基础概念
///
/// 软删除(Soft Delete)不会真正从数据库删除记录,
/// 而是将 IsDeleted 标记设置为 true
/// </summary>
[TestMethod]
public async Task Demo_SoftDeleteBasics()
{
var builder = new DataContextBuilder<TestDbContext>();
builder.UseInMemoryDatabase($"SoftDeleteDemo_{Guid.NewGuid()}")
.UseAudit(true)
.UseSoftDelete(true) // 启用软删除
.UseCurrentUser("admin");
using var db = builder.Build();
var repo = db.Repository<Product>();
// 创建产品
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<Product>()
.IgnoreQueryFilters() // 忽略软删除过滤器
.Where(p => p.Id == productId)
.FirstOrDefaultAsync();
Assert.IsNotNull(allIncludingDeleted);
Console.WriteLine($"✓ 使用 IgnoreQueryFilters() 仍可查询到: {allIncludingDeleted.Name}");
}
/// <summary>
/// 演示: 软删除 vs 物理删除
/// </summary>
[TestMethod]
public async Task Demo_SoftDeleteVsPhysicalDelete()
{
var builder = new DataContextBuilder<TestDbContext>();
builder.UseInMemoryDatabase($"DeleteCompareDemo_{Guid.NewGuid()}")
.UseAudit(true)
.UseSoftDelete(true)
.UseCurrentUser("admin");
using var db = builder.Build();
// 1. 软删除:Product 实现了 ISoftDeletable
var productRepo = db.Repository<Product>();
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<Product>()
.IgnoreQueryFilters()
.AnyAsync(p => p.Id == product.Id);
Assert.IsTrue(productStillExists);
// 2. 物理删除:Category 不实现 ISoftDeletable
var categoryRepo = db.Repository<Category>();
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<Category>()
.AnyAsync(c => c.Id == category.Id);
Assert.IsFalse(categoryDeleted);
}
/// <summary>
/// 演示: 查询时自动过滤已删除数据
/// </summary>
[TestMethod]
public async Task Demo_AutoFilterDeletedData()
{
var builder = new DataContextBuilder<TestDbContext>();
builder.UseInMemoryDatabase($"FilterDemo_{Guid.NewGuid()}")
.UseSoftDelete(true)
.UseCurrentUser("admin");
using var db = builder.Build();
var repo = db.Repository<Product>();
// 添加 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<Product>()
.IgnoreQueryFilters()
.ToListAsync();
Console.WriteLine($"✓ IgnoreQueryFilters 返回: {allIncludingDeleted.Count} 个产品(包含已删除)");
Assert.AreEqual(5, allIncludingDeleted.Count);
// 只查询已删除的
var deletedOnly = await db.Context.Set<Product>()
.IgnoreQueryFilters()
.Where(p => p.IsDeleted)
.ToListAsync();
Console.WriteLine($"✓ 只查询已删除: {deletedOnly.Count} 个产品");
Assert.AreEqual(2, deletedOnly.Count);
}
/// <summary>
/// 演示: 软删除的实际应用场景
/// </summary>
[TestMethod]
public async Task Demo_SoftDeleteRealWorldScenario()
{
var builder = new DataContextBuilder<TestDbContext>();
builder.UseInMemoryDatabase($"RealWorldSoftDelete_{Guid.NewGuid()}")
.UseAudit(true)
.UseSoftDelete(true)
.UseCurrentUser("admin");
using var db = builder.Build();
var repo = db.Repository<Product>();
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<Product>()
.IgnoreQueryFilters()
.ToListAsync();
Console.WriteLine($"\n4. 管理员视角");
Console.WriteLine($" 所有产品(含下架): {allProducts.Count}");
Console.WriteLine($" 可恢复下架产品的历史数据");
Console.WriteLine($"\n✓ 软删除的优势:");
Console.WriteLine($" - 保留历史数据");
Console.WriteLine($" - 可以恢复");
Console.WriteLine($" - 可以审计追踪");
Console.WriteLine($" - 满足合规要求");
}
/// <summary>
/// 演示: 批量软删除
/// </summary>
[TestMethod]
public async Task Demo_BulkSoftDelete()
{
var builder = new DataContextBuilder<TestDbContext>();
builder.UseInMemoryDatabase($"BulkSoftDelete_{Guid.NewGuid()}")
.UseSoftDelete(true)
.UseCurrentUser("admin");
using var db = builder.Build();
var repo = db.Repository<Product>();
// 添加多个产品
var products = new List<Product>();
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);
}
/// <summary>
/// 演示: 只实现软删除,不要审计
///
/// Notification 只实现了 ISoftDeletable
/// </summary>
[TestMethod]
public async Task Demo_SoftDeleteWithoutAudit()
{
var builder = new DataContextBuilder<TestDbContext>();
builder.UseInMemoryDatabase($"SoftDeleteOnly_{Guid.NewGuid()}")
.UseSoftDelete(true)
.UseCurrentUser("admin");
using var db = builder.Build();
var repo = db.Repository<Notification>();
// 创建通知
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);
}
/// <summary>
/// 演示: 禁用软删除时的行为
/// </summary>
[TestMethod]
public async Task Demo_DisableSoftDelete()
{
var builder = new DataContextBuilder<TestDbContext>();
builder.UseInMemoryDatabase($"DisableSoftDelete_{Guid.NewGuid()}")
.UseSoftDelete(false) // 禁用软删除
.UseCurrentUser("admin");
using var db = builder.Build();
var repo = db.Repository<Product>();
// 添加产品
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<Product>()
.IgnoreQueryFilters()
.FirstOrDefaultAsync(p => p.Id == productId);
if (found != null)
Console.WriteLine($" 结果:软删除 (IsDeleted={found.IsDeleted})");
else
Console.WriteLine($" 结果:物理删除");
}
}