A:增加数据库配置和使用

This commit is contained in:
jimmy committed 2025-11-18 11:14:56 +08:00
1 parent 53d1dd2dbf
commit f7660c49e8
86 files changed
+7401 -271

No files matched your search

+184
View File
@@ -0,0 +1,184 @@
using Microsoft.EntityFrameworkCore;
using Sharp.Data;
namespace Data.Test;
/// <summary>
/// 快速开始 - 最简单的演示
///
/// 如果您是第一次使用 Sharp.Data,从这里开始!
/// </summary>
[TestClass]
public class _00_QuickStart
{
/// <summary>
/// 演示: 3 行代码开始使用 Sharp.Data
/// </summary>
[TestMethod]
public async Task QuickStart_ThreeLines()
{
// 第1行:创建构建器
var builder = new DataContextBuilder<TestDbContext>();
// 第2行:配置数据库
builder.UseInMemoryDatabase("QuickStartDemo");
// 第3行:构建并使用
using var db = builder.Build();
// 完成!现在可以使用数据库了
var productRepo = db.Repository<Product>();
// 添加产品
await productRepo.AddAsync(new Product
{
Name = "iPhone 15",
Price = 7999m,
Stock = 100
});
await db.SaveChangesAsync();
// 查询产品
var products = await productRepo.GetAllAsync();
Assert.AreEqual(1, products.Count);
Console.WriteLine($"✓ 成功添加并查询产品: {products[0].Name}");
}
/// <summary>
/// 演示: 启用审计和软删除
/// </summary>
[TestMethod]
public async Task QuickStart_WithAuditAndSoftDelete()
{
var builder = new DataContextBuilder<TestDbContext>();
builder.UseInMemoryDatabase($"AuditDemo_{Guid.NewGuid()}")
.UseAudit(true) // 启用审计
.UseSoftDelete(true) // 启用软删除
.UseCurrentUser("admin"); // 设置当前用户
using var db = builder.Build();
var repo = db.Repository<Product>();
// 添加产品(自动记录创建信息)
var product = new Product { Name = "测试产品", Price = 99m, Stock = 10 };
await repo.AddAsync(product);
await db.SaveChangesAsync();
Console.WriteLine($"✓ 产品已创建");
Console.WriteLine($" CreatedBy: {product.CreatedBy}");
Console.WriteLine($" CreatedAt: {product.CreatedAt:yyyy-MM-dd HH:mm:ss}");
// 软删除(不会真正删除)
await repo.RemoveAsync(product.Id);
await db.SaveChangesAsync();
Console.WriteLine($"\n✓ 产品已软删除");
Console.WriteLine($" IsDeleted: {product.IsDeleted}");
Console.WriteLine($" DeletedAt: {product.DeletedAt:yyyy-MM-dd HH:mm:ss}");
// 验证:正常查询看不到已删除的
var found = await repo.GetByIdAsync(product.Id);
Assert.IsNull(found);
Console.WriteLine($"\n✓ 正常查询返回 null (已删除的数据被自动过滤)");
}
/// <summary>
/// 演示: 分页查询
/// </summary>
[TestMethod]
public async Task QuickStart_Pagination()
{
var builder = new DataContextBuilder<TestDbContext>();
builder.UseInMemoryDatabase($"PageDemo_{Guid.NewGuid()}");
using var db = builder.Build();
var repo = db.Repository<Product>();
// 添加测试数据
for (int i = 1; i <= 25; i++)
{
await repo.AddAsync(new Product
{
Name = $"产品{i}",
Price = i * 10m,
Stock = i
});
}
await db.SaveChangesAsync();
// 分页查询:第2页,每页10条
var result = await repo.GetPagedAsync(2, 10);
Console.WriteLine($"分页查询结果:");
Console.WriteLine($" 当前页: {result.PageIndex}");
Console.WriteLine($" 每页数量: {result.PageSize}");
Console.WriteLine($" 总记录数: {result.TotalCount}");
Console.WriteLine($" 总页数: {result.TotalPages}");
Console.WriteLine($" 当前页产品数: {result.Items.Count}");
Assert.AreEqual(2, result.PageIndex);
Assert.AreEqual(25, result.TotalCount);
Assert.AreEqual(3, result.TotalPages);
Assert.AreEqual(10, result.Items.Count);
}
/// <summary>
/// 演示: 使用自定义实体(不使用基类)
/// </summary>
[TestMethod]
public async Task QuickStart_CustomEntity()
{
var builder = new DataContextBuilder<TestDbContext>();
builder.UseInMemoryDatabase($"CustomDemo_{Guid.NewGuid()}");
using var db = builder.Build();
var repo = db.Repository<CustomProduct>();
// CustomProduct 不使用任何基类,完全自定义
var product = new CustomProduct
{
Id = Guid.NewGuid(),
Name = "自定义产品",
Price = 299.99m,
CreatedDate = DateTime.UtcNow
};
await repo.AddAsync(product);
await db.SaveChangesAsync();
var found = await repo.GetByIdAsync(product.Id);
Assert.IsNotNull(found);
Console.WriteLine($"✓ 自定义实体也能正常工作:");
Console.WriteLine($" Name: {found.Name}");
Console.WriteLine($" Price: {found.Price:C}");
Console.WriteLine($" IsExpensive: {found.IsExpensive}");
}
/// <summary>
/// 演示: 直接使用 EF Core
/// </summary>
[TestMethod]
public async Task QuickStart_DirectEFCore()
{
var builder = new DataContextBuilder<TestDbContext>();
builder.UseInMemoryDatabase($"DirectDemo_{Guid.NewGuid()}");
using var db = builder.Build();
// 可以直接访问 DbContext
var context = db.Context;
var products = context.Set<Product>();
// 使用 EF Core 原生 API
products.Add(new Product { Name = "EF Core 产品", Price = 199m, Stock = 50 });
await context.SaveChangesAsync();
// 使用 LINQ 查询
var count = await products.CountAsync();
Assert.AreEqual(1, count);
Console.WriteLine($"✓ 可以直接使用 EF Core,完全兼容!");
}
}
+272
View File
@@ -0,0 +1,272 @@
using Sharp.Data;
using Sharp.Data.Context;
namespace Data.Test;
/// <summary>
/// 演示 01: Sharp.Data 基础使用
///
/// 本测试类演示:
/// 1. 如何创建和配置 DataContext
/// 2. 最简单的 3 行代码启动方式
/// 3. 各种配置选项的使用
/// </summary>
[TestClass]
public class _01_BasicUsageTests
{
/// <summary>
/// 演示: 最简单的使用方式 (3 行代码)
///
/// 这是最基础的用法,适合快速开始
/// </summary>
[TestMethod]
public void Demo_MinimalSetup_ThreeLines()
{
// 第 1 行: 创建构建器
var builder = new DataContextBuilder<TestDbContext>();
// 第 2 行: 配置数据库 (使用内存数据库用于演示)
builder.UseInMemoryDatabase("MinimalDemo");
// 第 3 行: 构建 DataContext
using var db = builder.Build();
// 完成!现在可以使用了
Assert.IsNotNull(db);
Assert.IsNotNull(db.Context);
Console.WriteLine("✓ 3 行代码完成 Data 模块配置");
}
/// <summary>
/// 演示: 使用 SQLite 数据库
///
/// SQLite 是最常用的轻量级数据库,适合桌面应用
/// </summary>
[TestMethod]
public void Demo_UsingSQLite()
{
var builder = new DataContextBuilder<TestDbContext>();
// 配置 SQLite 数据库
builder.UseSqlite("Data Source=test.db");
using var db = builder.Build();
Assert.IsNotNull(db);
Console.WriteLine("✓ 使用 SQLite 数据库配置成功");
}
/// <summary>
/// 演示: 启用所有功能的完整配置
///
/// 演示如何配置审计、软删除、自动迁移等功能
/// </summary>
[TestMethod]
public void Demo_FullConfiguration()
{
var builder = new DataContextBuilder<TestDbContext>();
// 链式配置所有功能
builder.UseInMemoryDatabase("FullConfigDemo")
.UseAudit(true) // 启用审计功能
.UseSoftDelete(true) // 启用软删除功能
.UseAutoMigration(true) // 启动时自动迁移数据库
.UseCurrentUser("admin") // 设置当前用户(用于审计)
.EnableDetailedErrors(true) // 启用详细错误信息
.EnableSensitiveDataLogging(true); // 启用敏感数据日志(仅开发环境)
using var db = builder.Build();
Assert.IsNotNull(db);
Console.WriteLine("✓ 完整配置成功");
Console.WriteLine(" - 审计功能: 已启用");
Console.WriteLine(" - 软删除功能: 已启用");
Console.WriteLine(" - 自动迁移: 已启用");
Console.WriteLine(" - 当前用户: admin");
}
/// <summary>
/// 演示: 动态获取当前用户
///
/// 在真实应用中,用户通常来自认证系统
/// </summary>
[TestMethod]
public void Demo_DynamicCurrentUser()
{
var builder = new DataContextBuilder<TestDbContext>();
// 模拟从认证系统获取用户
var currentUser = "zhang.san"; // 实际应用中可能来自 HttpContext 或其他认证系统
builder.UseInMemoryDatabase("DynamicUserDemo")
.UseAudit(true)
.UseCurrentUserProvider(() =>
{
// 这里可以放置获取当前用户的逻辑
// 例如: HttpContext.Current?.User?.Identity?.Name
return currentUser;
});
using var db = builder.Build();
Assert.IsNotNull(db);
Console.WriteLine($"✓ 动态用户配置成功,当前用户: {currentUser}");
}
/// <summary>
/// 演示: 只启用部分功能
///
/// 所有功能都是可选的,按需启用
/// </summary>
[TestMethod]
public void Demo_SelectiveFeatures()
{
var builder = new DataContextBuilder<TestDbContext>();
// 只启用审计,不启用软删除
builder.UseInMemoryDatabase("SelectiveDemo")
.UseAudit(true) // 启用审计
.UseSoftDelete(false); // 不启用软删除
using var db = builder.Build();
Assert.IsNotNull(db);
Console.WriteLine("✓ 选择性功能配置成功");
Console.WriteLine(" - 审计功能: 已启用");
Console.WriteLine(" - 软删除功能: 未启用");
}
/// <summary>
/// 演示: 获取仓储对象
///
/// DataContext 提供统一的仓储访问入口
/// </summary>
[TestMethod]
public void Demo_GetRepository()
{
var builder = new DataContextBuilder<TestDbContext>();
builder.UseInMemoryDatabase("RepositoryDemo");
using var db = builder.Build();
// 获取不同实体的仓储
var productRepo = db.Repository<Product>();
var categoryRepo = db.Repository<Category>();
var orderRepo = db.Repository<Order>();
Assert.IsNotNull(productRepo);
Assert.IsNotNull(categoryRepo);
Assert.IsNotNull(orderRepo);
Console.WriteLine("✓ 仓储获取成功");
Console.WriteLine(" - Product 仓储");
Console.WriteLine(" - Category 仓储");
Console.WriteLine(" - Order 仓储");
}
/// <summary>
/// 演示: 访问底层 DbContext
///
/// 可以直接访问 EF Core DbContext 进行高级操作
/// </summary>
[TestMethod]
public void Demo_AccessUnderlyingDbContext()
{
var builder = new DataContextBuilder<TestDbContext>();
builder.UseInMemoryDatabase("DirectAccessDemo");
using var db = builder.Build();
// 访问底层 DbContext
var context = db.Context;
Assert.IsNotNull(context);
Assert.IsInstanceOfType<TestDbContext>(context);
Console.WriteLine("✓ 可以访问底层 DbContext");
Console.WriteLine(" - 用于执行原生 SQL");
Console.WriteLine(" - 用于事务管理");
Console.WriteLine(" - 用于高级查询");
}
/// <summary>
/// 演示: 配置不同的数据库提供程序
///
/// Sharp.Data 支持多种数据库
/// </summary>
[TestMethod]
public void Demo_DifferentDatabaseProviders()
{
// 1. SQLite
var sqliteBuilder = new DataContextBuilder<TestDbContext>();
sqliteBuilder.UseSqlite("Data Source=app.db");
Console.WriteLine("✓ SQLite: Data Source=app.db");
// 2. SQL Server
var sqlServerBuilder = new DataContextBuilder<TestDbContext>();
sqlServerBuilder.UseSqlServer("Server=.;Database=MyApp;Trusted_Connection=True;");
Console.WriteLine("✓ SQL Server: 连接字符串已配置");
// 3. MySQL
var mysqlBuilder = new DataContextBuilder<TestDbContext>();
mysqlBuilder.UseMySql("server=localhost;database=myapp;user=root;password=123", "8.0.35");
Console.WriteLine("✓ MySQL: 连接字符串已配置");
// 4. PostgreSQL
var npgsqlBuilder = new DataContextBuilder<TestDbContext>();
npgsqlBuilder.UseNpgsql("Host=localhost;Database=myapp;Username=postgres;Password=123");
Console.WriteLine("✓ PostgreSQL: 连接字符串已配置");
// 5. InMemory (测试用)
var memoryBuilder = new DataContextBuilder<TestDbContext>();
memoryBuilder.UseInMemoryDatabase("TestDb");
using var db = memoryBuilder.Build();
Console.WriteLine("✓ InMemory: 内存数据库用于测试");
Assert.IsNotNull(db);
}
/// <summary>
/// 演示: 正确释放资源
///
/// DataContext 实现了 IDisposable,使用后要释放
/// </summary>
[TestMethod]
public void Demo_ProperResourceDisposal()
{
// 方式1: 使用 using 语句 (推荐)
using (var db = CreateDataContext())
{
// 使用 db 进行操作
var repo = db.Repository<Product>();
// ... 操作
} // 自动释放
Console.WriteLine("✓ 方式1: using 语句自动释放");
// 方式2: 手动释放
var db2 = CreateDataContext();
try
{
var repo = db2.Repository<Product>();
// ... 操作
}
finally
{
db2.Dispose(); // 手动释放
}
Console.WriteLine("✓ 方式2: 手动释放");
}
/// <summary>
/// 辅助方法: 创建测试用的 DataContext
/// </summary>
private IDataContext CreateDataContext()
{
var builder = new DataContextBuilder<TestDbContext>();
builder.UseInMemoryDatabase($"TestDb_{Guid.NewGuid()}");
return builder.Build();
}
}
+548
View File
@@ -0,0 +1,548 @@
using Microsoft.EntityFrameworkCore;
using Sharp.Data;
using Sharp.Data.Context;
namespace Data.Test;
/// <summary>
/// 演示 02: 仓储 CRUD 操作
///
/// 本测试类演示:
/// 1. 添加数据 (Create)
/// 2. 查询数据 (Read)
/// 3. 更新数据 (Update)
/// 4. 删除数据 (Delete)
/// 5. 批量操作
/// 6. 条件查询
/// </summary>
[TestClass]
public class _02_RepositoryCRUDTests
{
private IDataContext _db = null!;
[TestInitialize]
public void Setup()
{
var builder = new DataContextBuilder<TestDbContext>();
builder.UseInMemoryDatabase($"CRUDDemo_{Guid.NewGuid()}");
_db = builder.Build();
}
[TestCleanup]
public void Cleanup()
{
_db?.Dispose();
}
#region Create - 添加数据
/// <summary>
/// 演示: 添加单个实体
/// </summary>
[TestMethod]
public async Task Demo_AddSingleEntity()
{
var repo = _db.Repository<Product>();
// 创建新产品
var product = new Product
{
Name = "iPhone 15",
Price = 7999.00m,
Description = "最新款 iPhone",
Stock = 100
};
// 添加到数据库
await repo.AddAsync(product);
await _db.SaveChangesAsync();
Console.WriteLine("✓ 成功添加产品");
Console.WriteLine($" - ID: {product.Id}");
Console.WriteLine($" - 名称: {product.Name}");
Console.WriteLine($" - 价格: {product.Price:C}");
// 验证
var savedProduct = await repo.GetByIdAsync(product.Id);
Assert.IsNotNull(savedProduct);
Assert.AreEqual("iPhone 15", savedProduct.Name);
}
/// <summary>
/// 演示: 批量添加实体
/// </summary>
[TestMethod]
public async Task Demo_AddMultipleEntities()
{
var repo = _db.Repository<Product>();
// 创建多个产品
var products = new List<Product>
{
new() { Name = "MacBook Pro", Price = 12999.00m, Stock = 50 },
new() { Name = "iPad Air", Price = 4799.00m, Stock = 80 },
new() { Name = "AirPods Pro", Price = 1899.00m, Stock = 200 }
};
// 批量添加
await repo.AddRangeAsync(products);
await _db.SaveChangesAsync();
Console.WriteLine($"✓ 成功添加 {products.Count} 个产品");
foreach (var product in products)
{
Console.WriteLine($" - {product.Name}: {product.Price:C}");
}
// 验证
var allProducts = await repo.GetAllAsync();
Assert.AreEqual(3, allProducts.Count);
}
#endregion
#region Read - 查询数据
/// <summary>
/// 演示: 根据 ID 获取实体
/// </summary>
[TestMethod]
public async Task Demo_GetById()
{
var repo = _db.Repository<Product>();
// 先添加数据
var product = new Product { Name = "测试产品", Price = 99.00m, Stock = 10 };
await repo.AddAsync(product);
await _db.SaveChangesAsync();
// 根据 ID 查询
var found = await repo.GetByIdAsync(product.Id);
Assert.IsNotNull(found);
Assert.AreEqual(product.Id, found.Id);
Console.WriteLine($"✓ 通过 ID 查询成功: {found.Name}");
}
/// <summary>
/// 演示: 获取所有数据
/// </summary>
[TestMethod]
public async Task Demo_GetAll()
{
var repo = _db.Repository<Product>();
// 添加测试数据
await repo.AddRangeAsync(new[]
{
new Product { Name = "产品1", Price = 100m, Stock = 10 },
new Product { Name = "产品2", Price = 200m, Stock = 20 },
new Product { Name = "产品3", Price = 300m, Stock = 30 }
});
await _db.SaveChangesAsync();
// 获取所有数据
var allProducts = await repo.GetAllAsync();
Assert.AreEqual(3, allProducts.Count);
Console.WriteLine($"✓ 获取所有数据成功,共 {allProducts.Count} 条");
foreach (var p in allProducts)
{
Console.WriteLine($" - {p.Name}: {p.Price:C}");
}
}
/// <summary>
/// 演示: 条件查询
/// </summary>
[TestMethod]
public async Task Demo_FindWithCondition()
{
var repo = _db.Repository<Product>();
// 添加测试数据
await repo.AddRangeAsync(new[]
{
new Product { Name = "苹果手机", Price = 7999m, Stock = 50 },
new Product { Name = "苹果平板", Price = 4799m, Stock = 30 },
new Product { Name = "华为手机", Price = 6999m, Stock = 40 }
});
await _db.SaveChangesAsync();
// 查找名称包含"苹果"的产品
var appleProducts = await repo.FindAsync(p => p.Name.Contains("苹果"));
Assert.AreEqual(2, appleProducts.Count);
Console.WriteLine("✓ 条件查询成功,找到包含'苹果'的产品:");
foreach (var p in appleProducts)
{
Console.WriteLine($" - {p.Name}: {p.Price:C}");
}
}
/// <summary>
/// 演示: 获取第一个符合条件的实体
/// </summary>
[TestMethod]
public async Task Demo_FirstOrDefault()
{
var repo = _db.Repository<Product>();
// 添加测试数据
await repo.AddRangeAsync(new[]
{
new Product { Name = "产品A", Price = 100m, Stock = 10 },
new Product { Name = "产品B", Price = 200m, Stock = 20 }
});
await _db.SaveChangesAsync();
// 查找第一个价格大于150的产品
var expensiveProduct = await repo.FirstOrDefaultAsync(p => p.Price > 150);
Assert.IsNotNull(expensiveProduct);
Assert.AreEqual("产品B", expensiveProduct.Name);
Console.WriteLine($"✓ 找到第一个价格>150的产品: {expensiveProduct.Name}");
// 查找不存在的条件
var notFound = await repo.FirstOrDefaultAsync(p => p.Price > 1000);
Assert.IsNull(notFound);
Console.WriteLine("✓ 不存在的条件返回 null");
}
/// <summary>
/// 演示: 统计查询
/// </summary>
[TestMethod]
public async Task Demo_CountAndAny()
{
var repo = _db.Repository<Product>();
// 添加测试数据
await repo.AddRangeAsync(new[]
{
new Product { Name = "产品1", Price = 100m, Stock = 10 },
new Product { Name = "产品2", Price = 200m, Stock = 20 },
new Product { Name = "产品3", Price = 300m, Stock = 30 }
});
await _db.SaveChangesAsync();
// 统计总数
var totalCount = await repo.CountAsync();
Console.WriteLine($"✓ 总记录数: {totalCount}");
Assert.AreEqual(3, totalCount);
// 统计符合条件的数量
var expensiveCount = await repo.CountAsync(p => p.Price > 150);
Console.WriteLine($"✓ 价格>150的产品数: {expensiveCount}");
Assert.AreEqual(2, expensiveCount);
// 检查是否存在
var hasExpensive = await repo.AnyAsync(p => p.Price > 250);
Console.WriteLine($"✓ 是否存在价格>250的产品: {hasExpensive}");
Assert.IsTrue(hasExpensive);
var hasVeryExpensive = await repo.AnyAsync(p => p.Price > 500);
Console.WriteLine($"✓ 是否存在价格>500的产品: {hasVeryExpensive}");
Assert.IsFalse(hasVeryExpensive);
}
/// <summary>
/// 演示: 使用 Query 方法进行复杂查询
/// </summary>
[TestMethod]
public async Task Demo_ComplexQueryWithLinq()
{
var repo = _db.Repository<Product>();
// 添加测试数据
await repo.AddRangeAsync(new[]
{
new Product { Name = "iPhone", Price = 7999m, Stock = 50 },
new Product { Name = "iPad", Price = 4799m, Stock = 30 },
new Product { Name = "MacBook", Price = 12999m, Stock = 20 },
new Product { Name = "AirPods", Price = 1899m, Stock = 100 }
});
await _db.SaveChangesAsync();
// 获取可查询对象,进行复杂查询
var query = repo.Query(trackChanges: false); // trackChanges: false 提升查询性能
var result = await query
.Where(p => p.Price > 2000) // 价格大于2000
.OrderByDescending(p => p.Price) // 按价格降序
.Select(p => new // 投影到匿名类型
{
p.Name,
p.Price,
p.Stock,
TotalValue = p.Price * p.Stock // 计算总价值
})
.ToListAsync();
Console.WriteLine("✓ 复杂查询成功,价格>2000的产品(按价格降序):");
foreach (var item in result)
{
Console.WriteLine($" - {item.Name}: {item.Price:C} x {item.Stock} = {item.TotalValue:C}");
}
Assert.AreEqual(3, result.Count);
}
#endregion
#region Update - 更新数据
/// <summary>
/// 演示: 更新实体
/// </summary>
[TestMethod]
public async Task Demo_UpdateEntity()
{
var repo = _db.Repository<Product>();
// 添加产品
var product = new Product
{
Name = "旧名称",
Price = 100m,
Stock = 50
};
await repo.AddAsync(product);
await _db.SaveChangesAsync();
Console.WriteLine($"✓ 原始数据: {product.Name}, 价格: {product.Price:C}");
// 修改产品
product.Name = "新名称";
product.Price = 200m;
repo.Update(product);
await _db.SaveChangesAsync();
Console.WriteLine($"✓ 更新后: {product.Name}, 价格: {product.Price:C}");
// 验证更新
var updated = await repo.GetByIdAsync(product.Id);
Assert.AreEqual("新名称", updated!.Name);
Assert.AreEqual(200m, updated.Price);
}
/// <summary>
/// 演示: 批量更新
/// </summary>
[TestMethod]
public async Task Demo_UpdateMultipleEntities()
{
var repo = _db.Repository<Product>();
// 添加产品
var products = new List<Product>
{
new() { Name = "产品1", Price = 100m, Stock = 10 },
new() { Name = "产品2", Price = 200m, Stock = 20 },
new() { Name = "产品3", Price = 300m, Stock = 30 }
};
await repo.AddRangeAsync(products);
await _db.SaveChangesAsync();
// 批量涨价 10%
foreach (var product in products)
{
product.Price *= 1.10m;
}
repo.UpdateRange(products);
await _db.SaveChangesAsync();
Console.WriteLine("✓ 批量涨价 10% 成功:");
var updatedProducts = await repo.GetAllAsync();
foreach (var p in updatedProducts)
{
Console.WriteLine($" - {p.Name}: {p.Price:C}");
}
// 验证
Assert.AreEqual(110m, updatedProducts[0].Price);
}
#endregion
#region Delete - 删除数据
/// <summary>
/// 演示: 删除实体 (物理删除)
///
/// 注意: 对于实现了 ISoftDeletable 的实体,RemoveAsync 会变成软删除
/// 本例中的 Product 继承自 AuditableEntity,实现了软删除接口
/// 所以这个测试实际演示的是软删除,详见 04_SoftDeleteTests
/// </summary>
[TestMethod]
public async Task Demo_DeleteEntity()
{
var repo = _db.Repository<Category>(); // Category 没有软删除,是真实删除
// 添加分类
var category = new Category { Name = "测试分类" };
await repo.AddAsync(category);
await _db.SaveChangesAsync();
Console.WriteLine($"✓ 添加分类: {category.Name}, ID: {category.Id}");
// 删除分类
repo.Remove(category);
await _db.SaveChangesAsync();
Console.WriteLine("✓ 删除分类成功");
// 验证删除
var deleted = await repo.GetByIdAsync(category.Id);
Assert.IsNull(deleted);
Console.WriteLine("✓ 验证: 分类已被物理删除");
}
/// <summary>
/// 演示: 根据 ID 删除
/// </summary>
[TestMethod]
public async Task Demo_DeleteById()
{
var repo = _db.Repository<Category>();
// 添加分类
var category = new Category { Name = "要删除的分类" };
await repo.AddAsync(category);
await _db.SaveChangesAsync();
var categoryId = category.Id;
Console.WriteLine($"✓ 添加分类: ID={categoryId}");
// 根据 ID 删除
await repo.RemoveAsync(categoryId);
await _db.SaveChangesAsync();
Console.WriteLine("✓ 根据 ID 删除成功");
// 验证删除
var deleted = await repo.GetByIdAsync(categoryId);
Assert.IsNull(deleted);
}
/// <summary>
/// 演示: 批量删除
/// </summary>
[TestMethod]
public async Task Demo_DeleteMultipleEntities()
{
var repo = _db.Repository<Category>();
// 添加多个分类
var categories = new List<Category>
{
new() { Name = "分类1" },
new() { Name = "分类2" },
new() { Name = "分类3" }
};
await repo.AddRangeAsync(categories);
await _db.SaveChangesAsync();
Console.WriteLine($"✓ 添加了 {categories.Count} 个分类");
// 批量删除
repo.RemoveRange(categories);
await _db.SaveChangesAsync();
Console.WriteLine("✓ 批量删除成功");
// 验证
var remaining = await repo.GetAllAsync();
Assert.AreEqual(0, remaining.Count);
}
#endregion
#region 高级查询
/// <summary>
/// 演示: 排序查询
/// </summary>
[TestMethod]
public async Task Demo_OrderedQuery()
{
var repo = _db.Repository<Product>();
// 添加测试数据
await repo.AddRangeAsync(new[]
{
new Product { Name = "C产品", Price = 300m, Stock = 30 },
new Product { Name = "A产品", Price = 100m, Stock = 10 },
new Product { Name = "B产品", Price = 200m, Stock = 20 }
});
await _db.SaveChangesAsync();
// 按价格升序
var ascProducts = await repo.Query()
.OrderBy(p => p.Price)
.ToListAsync();
Console.WriteLine("✓ 按价格升序:");
foreach (var p in ascProducts)
{
Console.WriteLine($" - {p.Name}: {p.Price:C}");
}
// 按价格降序
var descProducts = await repo.Query()
.OrderByDescending(p => p.Price)
.ToListAsync();
Console.WriteLine("✓ 按价格降序:");
foreach (var p in descProducts)
{
Console.WriteLine($" - {p.Name}: {p.Price:C}");
}
Assert.AreEqual("A产品", ascProducts[0].Name);
Assert.AreEqual("C产品", descProducts[0].Name);
}
/// <summary>
/// 演示: 分组查询
/// </summary>
[TestMethod]
public async Task Demo_GroupQuery()
{
var repo = _db.Repository<Product>();
// 添加不同价格区间的产品
await repo.AddRangeAsync(new[]
{
new Product { Name = "低价产品1", Price = 50m, Stock = 10 },
new Product { Name = "低价产品2", Price = 80m, Stock = 20 },
new Product { Name = "中价产品1", Price = 150m, Stock = 15 },
new Product { Name = "中价产品2", Price = 180m, Stock = 25 },
new Product { Name = "高价产品1", Price = 500m, Stock = 5 },
new Product { Name = "高价产品2", Price = 600m, Stock = 8 }
});
await _db.SaveChangesAsync();
// 按价格区间分组统计
var priceGroups = await repo.Query()
.GroupBy(p => p.Price < 100 ? "低价" : p.Price < 300 ? "中价" : "高价")
.Select(g => new
{
PriceRange = g.Key,
Count = g.Count(),
AvgPrice = g.Average(p => p.Price),
TotalStock = g.Sum(p => p.Stock)
})
.ToListAsync();
Console.WriteLine("✓ 按价格区间分组统计:");
foreach (var group in priceGroups)
{
Console.WriteLine($" - {group.PriceRange}: {group.Count}个产品, 平均价格 {group.AvgPrice:C}, 总库存 {group.TotalStock}");
}
Assert.AreEqual(3, priceGroups.Count);
}
#endregion
}
+371
View File
@@ -0,0 +1,371 @@
using Sharp.Data;
using Sharp.Data.Context;
namespace Data.Test;
/// <summary>
/// 演示 03: 审计功能
///
/// 本测试类演示:
/// 1. 如何启用审计功能
/// 2. 创建时自动记录 CreatedAt 和 CreatedBy
/// 3. 更新时自动记录 UpdatedAt 和 UpdatedBy
/// 4. 如何设置当前用户
/// 5. 审计功能对不同实体的影响
/// </summary>
[TestClass]
public class _03_AuditFeatureTests
{
/// <summary>
/// 演示: 基础审计功能 - 创建时自动记录
///
/// 使用 AuditableEntity 基类的实体会自动记录审计信息
/// </summary>
[TestMethod]
public async Task Demo_AuditOnCreate()
{
// 创建启用审计功能的 DataContext
var builder = new DataContextBuilder<TestDbContext>();
builder.UseInMemoryDatabase($"AuditCreateDemo_{Guid.NewGuid()}")
.UseAudit(true) // 启用审计
.UseCurrentUser("zhang.san"); // 设置当前用户
using var db = builder.Build();
var repo = db.Repository<Product>();
var beforeCreate = DateTime.UtcNow;
// 创建产品
var product = new Product
{
Name = "测试产品",
Price = 99.00m,
Stock = 10
};
// 添加前,审计字段是空的
Console.WriteLine("添加前:");
Console.WriteLine($" CreatedAt: {product.CreatedAt}");
Console.WriteLine($" CreatedBy: {product.CreatedBy ?? "(null)"}");
await repo.AddAsync(product);
await db.SaveChangesAsync(); // SaveChanges 时自动填充审计信息
var afterCreate = DateTime.UtcNow;
// 添加后,审计字段自动填充
Console.WriteLine("\n添加后:");
Console.WriteLine($" ✓ CreatedAt: {product.CreatedAt:yyyy-MM-dd HH:mm:ss}");
Console.WriteLine($" ✓ CreatedBy: {product.CreatedBy}");
Console.WriteLine($" ✓ UpdatedAt: {product.UpdatedAt?.ToString("yyyy-MM-dd HH:mm:ss") ?? "(null)"}");
Console.WriteLine($" ✓ UpdatedBy: {product.UpdatedBy ?? "(null)"}");
// 验证
Assert.IsTrue(product.CreatedAt >= beforeCreate && product.CreatedAt <= afterCreate);
Assert.AreEqual("zhang.san", product.CreatedBy);
Assert.IsNull(product.UpdatedAt); // 创建时 UpdatedAt 为 null
Assert.IsNull(product.UpdatedBy);
}
/// <summary>
/// 演示: 审计功能 - 更新时自动记录
/// </summary>
[TestMethod]
public async Task Demo_AuditOnUpdate()
{
var builder = new DataContextBuilder<TestDbContext>();
builder.UseInMemoryDatabase($"AuditUpdateDemo_{Guid.NewGuid()}")
.UseAudit(true)
.UseCurrentUser("creator");
using var db = builder.Build();
var repo = db.Repository<Product>();
// 1. 创建产品 (创建人: creator)
var product = new Product
{
Name = "原始产品",
Price = 100m,
Stock = 10
};
await repo.AddAsync(product);
await db.SaveChangesAsync();
Console.WriteLine("创建后的审计信息:");
Console.WriteLine($" CreatedBy: {product.CreatedBy}");
Console.WriteLine($" CreatedAt: {product.CreatedAt:yyyy-MM-dd HH:mm:ss}");
var createdAt = product.CreatedAt;
var createdBy = product.CreatedBy;
// 等待一小段时间
await Task.Delay(100);
// 2. 更改当前用户并更新产品
// 注意:如果需要切换用户,请创建新的 DataContext
// (db as Sharp.Data.Context.DataContext)?.SetCurrentUser("editor");
product.Name = "更新后的产品";
product.Price = 200m;
repo.Update(product);
await db.SaveChangesAsync(); // SaveChanges 时自动填充 UpdatedAt 和 UpdatedBy
Console.WriteLine("\n更新后的审计信息:");
Console.WriteLine($" CreatedBy: {product.CreatedBy} (保持不变)");
Console.WriteLine($" CreatedAt: {product.CreatedAt:yyyy-MM-dd HH:mm:ss} (保持不变)");
Console.WriteLine($" ✓ UpdatedBy: {product.UpdatedBy}");
Console.WriteLine($" ✓ UpdatedAt: {product.UpdatedAt:yyyy-MM-dd HH:mm:ss}");
// 验证
Assert.AreEqual(createdBy, product.CreatedBy); // 创建人不变
Assert.AreEqual(createdAt, product.CreatedAt); // 创建时间不变
Assert.AreEqual("editor", product.UpdatedBy); // 更新人是新用户
Assert.IsNotNull(product.UpdatedAt);
Assert.IsTrue(product.UpdatedAt > product.CreatedAt); // 更新时间晚于创建时间
}
/// <summary>
/// 演示: 多次更新的审计记录
/// </summary>
[TestMethod]
public async Task Demo_MultipleUpdatesAudit()
{
var builder = new DataContextBuilder<TestDbContext>();
builder.UseInMemoryDatabase($"MultiUpdateDemo_{Guid.NewGuid()}")
.UseAudit(true)
.UseCurrentUser("user1");
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.CreatedBy} 于 {product.CreatedAt:HH:mm:ss}");
// 第一次更新
await Task.Delay(50);
// 注意:如果需要切换用户,请创建新的 DataContext
// (db as Sharp.Data.Context.DataContext)?.SetCurrentUser("user2");
product.Price = 150m;
repo.Update(product);
await db.SaveChangesAsync();
var firstUpdateTime = product.UpdatedAt!.Value;
Console.WriteLine($"第1次更新: {product.UpdatedBy} 于 {product.UpdatedAt:HH:mm:ss}");
// 第二次更新
await Task.Delay(50);
// 注意:如果需要切换用户,请创建新的 DataContext
// (db as Sharp.Data.Context.DataContext)?.SetCurrentUser("user3");
product.Price = 200m;
repo.Update(product);
await db.SaveChangesAsync();
Console.WriteLine($"第2次更新: {product.UpdatedBy} 于 {product.UpdatedAt:HH:mm:ss}");
// 验证
Assert.AreEqual("user1", product.CreatedBy); // 创建人始终不变
Assert.AreEqual("user3", product.UpdatedBy); // 最后更新人
Assert.IsTrue(product.UpdatedAt > firstUpdateTime); // 更新时间在变化
}
/// <summary>
/// 演示: 动态获取当前用户
///
/// 在真实应用中,用户信息通常来自认证系统
/// </summary>
[TestMethod]
public async Task Demo_DynamicUserProvider()
{
// 模拟当前登录用户(实际应用中可能来自 HttpContext、Session 等)
var currentUser = "zhang.san";
var builder = new DataContextBuilder<TestDbContext>();
builder.UseInMemoryDatabase($"DynamicUserDemo_{Guid.NewGuid()}")
.UseAudit(true)
.UseCurrentUserProvider(() =>
{
// 这里可以放置获取当前用户的逻辑
// 例如: Thread.CurrentPrincipal?.Identity?.Name
// HttpContext.Current?.User?.Identity?.Name
// 某个全局的用户服务
return currentUser;
});
using var db = builder.Build();
var repo = db.Repository<Product>();
// 用户 zhang.san 创建产品
var product = new Product { Name = "产品A", Price = 100m, Stock = 10 };
await repo.AddAsync(product);
await db.SaveChangesAsync();
Console.WriteLine($"✓ 创建人: {product.CreatedBy}");
Assert.AreEqual("zhang.san", product.CreatedBy);
// 切换用户
currentUser = "li.si";
// 用户 li.si 更新产品
product.Price = 200m;
repo.Update(product);
await db.SaveChangesAsync();
Console.WriteLine($"✓ 更新人: {product.UpdatedBy}");
Assert.AreEqual("zhang.san", product.CreatedBy); // 创建人不变
Assert.AreEqual("li.si", product.UpdatedBy); // 更新人改变
}
/// <summary>
/// 演示: 实现审计接口但不使用基类
///
/// Order 实体实现了 IAuditableEntity 接口,同样享受审计功能
/// </summary>
[TestMethod]
public async Task Demo_AuditWithInterface()
{
var builder = new DataContextBuilder<TestDbContext>();
builder.UseInMemoryDatabase($"InterfaceAuditDemo_{Guid.NewGuid()}")
.UseAudit(true)
.UseCurrentUser("admin");
using var db = builder.Build();
var repo = db.Repository<Order>();
// 创建订单
var order = new Order
{
OrderNumber = "ORD-001",
TotalAmount = 999.00m
};
await repo.AddAsync(order);
await db.SaveChangesAsync();
Console.WriteLine("✓ Order 实体(实现接口但不使用基类)同样享受审计功能");
Console.WriteLine($" CreatedBy: {order.CreatedBy}");
Console.WriteLine($" CreatedAt: {order.CreatedAt:yyyy-MM-dd HH:mm:ss}");
// 验证
Assert.AreEqual("admin", order.CreatedBy);
Assert.IsTrue(order.CreatedAt > DateTime.MinValue);
}
/// <summary>
/// 演示: 不使用审计的实体不受影响
///
/// Category 不继承 AuditableEntity,也不实现审计接口
/// </summary>
[TestMethod]
public async Task Demo_NoAuditForNonAuditableEntity()
{
var builder = new DataContextBuilder<TestDbContext>();
builder.UseInMemoryDatabase($"NoAuditDemo_{Guid.NewGuid()}")
.UseAudit(true) // 即使启用了审计功能
.UseCurrentUser("admin");
using var db = builder.Build();
var repo = db.Repository<Category>();
// 创建分类
var category = new Category { Name = "测试分类" };
await repo.AddAsync(category);
await db.SaveChangesAsync();
Console.WriteLine("✓ Category 不实现审计接口,不受审计功能影响");
Console.WriteLine($" 只有 Id 和业务字段: {category.Id}, {category.Name}");
// 验证:Category 没有审计字段
Assert.IsNotNull(category.Id);
Assert.AreEqual("测试分类", category.Name);
}
/// <summary>
/// 演示: 禁用审计功能
/// </summary>
[TestMethod]
public async Task Demo_DisableAudit()
{
var builder = new DataContextBuilder<TestDbContext>();
builder.UseInMemoryDatabase($"DisableAuditDemo_{Guid.NewGuid()}")
.UseAudit(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();
Console.WriteLine("✓ 审计功能已禁用");
Console.WriteLine($" CreatedBy: {product.CreatedBy ?? "(未设置)"}");
Console.WriteLine($" CreatedAt: {product.CreatedAt}");
// 即使实体有审计字段,但功能禁用后,字段不会自动填充
// 但 CreatedAt 可能有默认值(DateTime.MinValue 或当前时间,取决于实现)
Console.WriteLine(" 注意:禁用审计后,即使实体有审计字段,也不会自动填充");
}
/// <summary>
/// 演示: 审计功能的实际应用场景
/// </summary>
[TestMethod]
public async Task Demo_RealWorldAuditScenario()
{
var builder = new DataContextBuilder<TestDbContext>();
builder.UseInMemoryDatabase($"RealWorldDemo_{Guid.NewGuid()}")
.UseAudit(true)
.UseCurrentUser("system");
using var db = builder.Build();
var repo = db.Repository<Product>();
// 场景:产品生命周期跟踪
// 1. 系统初始化时创建产品
var product = new Product
{
Name = "iPhone 15",
Price = 7999m,
Stock = 100
};
await repo.AddAsync(product);
await db.SaveChangesAsync();
Console.WriteLine("=== 产品生命周期审计 ===\n");
Console.WriteLine($"1. 产品创建");
Console.WriteLine($" 由 {product.CreatedBy} 于 {product.CreatedAt:yyyy-MM-dd HH:mm:ss} 创建");
// 2. 仓库管理员调整库存
await Task.Delay(50);
// 注意:如果需要切换用户,请创建新的 DataContext
// (db as Sharp.Data.Context.DataContext)?.SetCurrentUser("warehouse.manager");
product.Stock = 80;
repo.Update(product);
await db.SaveChangesAsync();
Console.WriteLine($"\n2. 库存调整");
Console.WriteLine($" 由 {product.UpdatedBy} 于 {product.UpdatedAt:yyyy-MM-dd HH:mm:ss} 更新");
// 3. 市场部调整价格
await Task.Delay(50);
// 注意:如果需要切换用户,请创建新的 DataContext
// (db as Sharp.Data.Context.DataContext)?.SetCurrentUser("marketing.manager");
product.Price = 6999m;
repo.Update(product);
await db.SaveChangesAsync();
Console.WriteLine($"\n3. 价格调整");
Console.WriteLine($" 由 {product.UpdatedBy} 于 {product.UpdatedAt:yyyy-MM-dd HH:mm:ss} 更新");
Console.WriteLine($"\n✓ 通过审计信息,可以追踪产品的完整变更历史");
Console.WriteLine($" - 谁创建的:{product.CreatedBy}");
Console.WriteLine($" - 何时创建:{product.CreatedAt:yyyy-MM-dd HH:mm:ss}");
Console.WriteLine($" - 谁最后修改:{product.UpdatedBy}");
Console.WriteLine($" - 何时修改:{product.UpdatedAt:yyyy-MM-dd HH:mm:ss}");
}
}
+382
View File
@@ -0,0 +1,382 @@
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 (int 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 (int 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($" 结果:物理删除");
}
}
}
+415
View File
@@ -0,0 +1,415 @@
using Common.ExpandFun;
using Microsoft.EntityFrameworkCore;
using Sharp.Data;
using Sharp.Data.Context;
namespace Data.Test;
/// <summary>
/// 演示 05: 分页查询
///
/// 本测试类演示:
/// 1. 基础分页查询
/// 2. 带条件的分页
/// 3. 带排序的分页
/// 4. 分页结果的使用
/// 5. 复用 Common 模块的 PagedResult
/// </summary>
[TestClass]
public class _05_PaginationTests
{
private IDataContext _db = null!;
[TestInitialize]
public async Task Setup()
{
var builder = new DataContextBuilder<TestDbContext>();
builder.UseInMemoryDatabase($"PaginationDemo_{Guid.NewGuid()}");
_db = builder.Build();
// 添加测试数据:100 个产品
var repo = _db.Repository<Product>();
var products = new List<Product>();
for (int i = 1; i <= 100; i++)
{
products.Add(new Product
{
Name = $"产品{i:D3}",
Price = (i % 10 + 1) * 100m,
Stock = i * 5,
Description = i % 2 == 0 ? "热门产品" : "普通产品"
});
}
await repo.AddRangeAsync(products);
await _db.SaveChangesAsync();
Console.WriteLine("✓ 初始化完成:创建了 100 个测试产品\n");
}
[TestCleanup]
public void Cleanup()
{
_db?.Dispose();
}
/// <summary>
/// 演示: 基础分页查询
/// </summary>
[TestMethod]
public async Task Demo_BasicPagination()
{
var repo = _db.Repository<Product>();
// 查询第 1 页,每页 10 条
var page1 = await repo.GetPagedAsync(
page: 1,
pageSize: 10);
Console.WriteLine($"第 1 页:");
Console.WriteLine($" 当前页: {page1.PageIndex}");
Console.WriteLine($" 每页数量: {page1.PageSize}");
Console.WriteLine($" 总记录数: {page1.TotalCount}");
Console.WriteLine($" 总页数: {page1.TotalPages}");
Console.WriteLine($" 是否有上一页: {page1.HasPreviousPage}");
Console.WriteLine($" 是否有下一页: {page1.HasNextPage}");
// 验证
Assert.AreEqual(1, page1.PageIndex);
Assert.AreEqual(10, page1.PageSize);
Assert.AreEqual(100, page1.TotalCount);
Assert.AreEqual(10, page1.TotalPages);
Assert.AreEqual(10, page1.Items.Count);
Assert.IsFalse(page1.HasPreviousPage);
Assert.IsTrue(page1.HasNextPage);
Console.WriteLine($"\n产品列表:");
foreach (var product in page1.Items)
{
Console.WriteLine($" - {product.Name}: {product.Price:C}");
}
}
/// <summary>
/// 演示: 遍历所有页
/// </summary>
[TestMethod]
public async Task Demo_IterateAllPages()
{
var repo = _db.Repository<Product>();
int pageSize = 15;
int currentPage = 1;
Console.WriteLine($"=== 遍历所有页(每页 {pageSize} 条)===\n");
PagedResult<Product> pageResult;
do
{
pageResult = await repo.GetPagedAsync(currentPage, pageSize);
Console.WriteLine($"第 {currentPage} 页: {pageResult.Items.Count} 条记录");
Console.WriteLine($" 范围: {(currentPage - 1) * pageSize + 1} - {Math.Min(currentPage * pageSize, pageResult.TotalCount)}");
// 处理当前页数据
foreach (var product in pageResult.Items)
{
// 业务逻辑...
}
currentPage++;
} while (pageResult.HasNextPage);
Console.WriteLine($"\n✓ 总共遍历了 {currentPage - 1} 页");
Assert.AreEqual(7, currentPage - 1); // 100 / 15 = 7 页
}
/// <summary>
/// 演示: 带条件的分页查询
/// </summary>
[TestMethod]
public async Task Demo_PaginationWithFilter()
{
var repo = _db.Repository<Product>();
// 查询价格 > 500 的产品,第 1 页,每页 10 条
var result = await repo.GetPagedAsync(
page: 1,
pageSize: 10,
predicate: p => p.Price > 500m);
Console.WriteLine($"查询条件: 价格 > 500");
Console.WriteLine($" 总记录数: {result.TotalCount}");
Console.WriteLine($" 总页数: {result.TotalPages}");
Console.WriteLine($" 当前页记录数: {result.Items.Count}");
Console.WriteLine($"\n产品列表:");
foreach (var product in result.Items)
{
Console.WriteLine($" - {product.Name}: {product.Price:C}");
Assert.IsTrue(product.Price > 500m);
}
}
/// <summary>
/// 演示: 带排序的分页查询
/// </summary>
[TestMethod]
public async Task Demo_PaginationWithOrdering()
{
var repo = _db.Repository<Product>();
// 按价格降序排列
var result = await repo.GetPagedAsync(
page: 1,
pageSize: 10,
predicate: null,
orderBy: q => q.OrderByDescending(p => p.Price));
Console.WriteLine($"排序: 按价格降序");
Console.WriteLine($"\n前 10 个最贵的产品:");
foreach (var product in result.Items)
{
Console.WriteLine($" - {product.Name}: {product.Price:C}");
}
// 验证排序
for (int i = 0; i < result.Items.Count - 1; i++)
{
Assert.IsTrue(result.Items[i].Price >= result.Items[i + 1].Price);
}
}
/// <summary>
/// 演示: 复杂分页查询 - 组合条件、排序、分页
/// </summary>
[TestMethod]
public async Task Demo_ComplexPagination()
{
var repo = _db.Repository<Product>();
// 查询热门产品,价格 > 300,按库存降序,第 2 页,每页 5 条
var result = await repo.GetPagedAsync(
page: 2,
pageSize: 5,
predicate: p => p.Description == "热门产品" && p.Price > 300m,
orderBy: q => q.OrderByDescending(p => p.Stock));
Console.WriteLine($"复杂查询:");
Console.WriteLine($" 条件: 热门产品 AND 价格>300");
Console.WriteLine($" 排序: 按库存降序");
Console.WriteLine($" 分页: 第 2 页,每页 5 条");
Console.WriteLine($"\n结果:");
Console.WriteLine($" 总记录数: {result.TotalCount}");
Console.WriteLine($" 总页数: {result.TotalPages}");
Console.WriteLine($" 当前页码: {result.PageIndex}");
Console.WriteLine($"\n产品列表:");
foreach (var product in result.Items)
{
Console.WriteLine($" - {product.Name}: {product.Price:C}, 库存: {product.Stock}");
Assert.AreEqual("热门产品", product.Description);
Assert.IsTrue(product.Price > 300m);
}
}
/// <summary>
/// 演示: 分页辅助信息的使用
/// </summary>
[TestMethod]
public async Task Demo_PaginationHelpers()
{
var repo = _db.Repository<Product>();
// 第 5 页,每页 10 条
var result = await repo.GetPagedAsync(5, 10);
Console.WriteLine($"=== 分页辅助信息 ===\n");
Console.WriteLine($"当前页: {result.PageIndex}");
Console.WriteLine($"每页数量: {result.PageSize}");
Console.WriteLine($"总记录数: {result.TotalCount}");
Console.WriteLine($"总页数: {result.TotalPages}");
Console.WriteLine($"是否第一页: {result.PageIndex == 1}");
Console.WriteLine($"是否最后一页: {result.PageIndex == result.TotalPages}");
Console.WriteLine($"有上一页: {result.HasPreviousPage}");
Console.WriteLine($"有下一页: {result.HasNextPage}");
// 计算记录范围
int startIndex = (result.PageIndex - 1) * result.PageSize + 1;
int endIndex = Math.Min(result.PageIndex * result.PageSize, result.TotalCount);
Console.WriteLine($"\n记录范围: {startIndex} - {endIndex}");
// 验证
Assert.AreEqual(5, result.PageIndex);
Assert.AreEqual(41, startIndex);
Assert.AreEqual(50, endIndex);
Assert.IsTrue(result.HasPreviousPage);
Assert.IsTrue(result.HasNextPage);
}
/// <summary>
/// 演示: 空结果的分页
/// </summary>
[TestMethod]
public async Task Demo_EmptyPageResult()
{
var repo = _db.Repository<Product>();
// 查询不存在的条件
var result = await repo.GetPagedAsync(
page: 1,
pageSize: 10,
predicate: p => p.Price > 99999m);
Console.WriteLine($"查询不存在的记录:");
Console.WriteLine($" 总记录数: {result.TotalCount}");
Console.WriteLine($" 总页数: {result.TotalPages}");
Console.WriteLine($" 当前页记录数: {result.Items.Count}");
Assert.AreEqual(0, result.TotalCount);
Assert.AreEqual(0, result.TotalPages);
Assert.AreEqual(0, result.Items.Count);
Assert.IsFalse(result.HasPreviousPage);
Assert.IsFalse(result.HasNextPage);
}
/// <summary>
/// 演示: 超出范围的页码
/// </summary>
[TestMethod]
public async Task Demo_OutOfRangePage()
{
var repo = _db.Repository<Product>();
// 请求第 999 页(超出实际页数)
var result = await repo.GetPagedAsync(999, 10);
Console.WriteLine($"请求超出范围的页码 (第 999 页):");
Console.WriteLine($" 总记录数: {result.TotalCount}");
Console.WriteLine($" 总页数: {result.TotalPages}");
Console.WriteLine($" 当前页码: {result.PageIndex}");
Console.WriteLine($" 当前页记录数: {result.Items.Count}");
// 超出范围返回空列表
Assert.AreEqual(0, result.Items.Count);
Assert.AreEqual(999, result.PageIndex);
Assert.IsFalse(result.HasNextPage);
}
/// <summary>
/// 演示: 在 UI 中使用分页
///
/// 模拟 Web API 或 WPF 应用的分页场景
/// </summary>
[TestMethod]
public async Task Demo_PaginationInUI()
{
var repo = _db.Repository<Product>();
// 模拟用户请求
int requestedPage = 3;
int pageSize = 20;
string? searchTerm = "产品0"; // 用户搜索
Console.WriteLine($"=== UI 分页场景 ===");
Console.WriteLine($"用户请求: 第 {requestedPage} 页, 每页 {pageSize} 条");
Console.WriteLine($"搜索词: {searchTerm}\n");
// 执行分页查询
var result = await repo.GetPagedAsync(
page: requestedPage,
pageSize: pageSize,
predicate: string.IsNullOrEmpty(searchTerm)
? null
: p => p.Name.Contains(searchTerm),
orderBy: q => q.OrderBy(p => p.Name));
// 构建返回给前端的数据
var uiData = new
{
Success = true,
Data = new
{
Items = result.Items.Select(p => new
{
p.Id,
p.Name,
p.Price,
p.Stock
}).ToList(),
Pagination = new
{
result.PageIndex,
result.PageSize,
result.TotalCount,
result.TotalPages,
result.HasPreviousPage,
result.HasNextPage
}
}
};
Console.WriteLine($"返回数据:");
Console.WriteLine($" 记录数: {uiData.Data.Items.Count}");
Console.WriteLine($" 分页信息: {uiData.Data.Pagination.PageIndex}/{uiData.Data.Pagination.TotalPages}");
Console.WriteLine($"\n✓ 可以直接序列化为 JSON 返回给前端");
}
/// <summary>
/// 演示: PagedResult 来自 Common 模块
///
/// Sharp.Data 复用 Common.ExpandFun.PagedResult<T>,避免重复定义
/// </summary>
[TestMethod]
public async Task Demo_PagedResultFromCommonModule()
{
var repo = _db.Repository<Product>();
var result = await repo.GetPagedAsync(1, 10);
// PagedResult<T> 来自 Common.ExpandFun 命名空间
Console.WriteLine($"✓ PagedResult<T> 的类型信息:");
Console.WriteLine($" 类型: {result.GetType().FullName}");
Console.WriteLine($" 命名空间: {result.GetType().Namespace}");
Console.WriteLine($"\n✓ 复用 Common 模块,避免重复造轮子");
Assert.AreEqual("Common.ExpandFun", result.GetType().Namespace);
}
/// <summary>
/// 演示: 自定义扩展方法进行分页
/// </summary>
[TestMethod]
public async Task Demo_CustomPaginationExtension()
{
// 使用 EF Core 的 IQueryable 直接分页
var query = _db.Context.Set<Product>()
.Where(p => p.Price > 500m)
.OrderBy(p => p.Name);
// 手动实现分页逻辑
int page = 2;
int pageSize = 10;
var totalCount = await query.CountAsync();
var items = await query
.Skip((page - 1) * pageSize)
.Take(pageSize)
.ToListAsync();
// 构建 PagedResult
var result = new PagedResult<Product>
{
Items = items,
TotalCount = totalCount,
PageIndex = page,
PageSize = pageSize
};
Console.WriteLine($"自定义分页:");
Console.WriteLine($" 总记录数: {result.TotalCount}");
Console.WriteLine($" 当前页: {result.PageIndex}");
Console.WriteLine($" 记录数: {result.Items.Count}");
Console.WriteLine($"\n✓ 可以直接使用 IQueryable 和 PagedResult 进行自定义分页");
}
}
+385
View File
@@ -0,0 +1,385 @@
using Microsoft.EntityFrameworkCore;
using Sharp.Data;
using Sharp.Data.Context;
namespace Data.Test;
/// <summary>
/// 演示 06: 自定义实体(不使用基类)
///
/// 本测试类演示:
/// 1. 完全自定义的实体定义
/// 2. 不使用任何基类的实体
/// 3. 使用特殊主键类型(字符串、复合主键等)
/// 4. 仓储对自定义实体的支持
/// 5. 灵活性和自主控制
/// </summary>
[TestClass]
public class _06_CustomEntityTests
{
/// <summary>
/// 演示: 完全自定义的实体
///
/// CustomProduct 不使用任何基类,完全自定义
/// </summary>
[TestMethod]
public async Task Demo_FullyCustomEntity()
{
var builder = new DataContextBuilder<TestDbContext>();
builder.UseInMemoryDatabase($"CustomEntityDemo_{Guid.NewGuid()}");
using var db = builder.Build();
var repo = db.Repository<CustomProduct>();
Console.WriteLine("=== 完全自定义实体 ===\n");
Console.WriteLine("CustomProduct:");
Console.WriteLine(" - 不继承任何基类");
Console.WriteLine(" - 不实现任何接口");
Console.WriteLine(" - 完全自主定义所有字段");
Console.WriteLine(" - 仍然可以使用仓储模式\n");
// 创建自定义产品
var product = new CustomProduct
{
Id = Guid.NewGuid(),
Name = "自定义产品",
Price = 299.99m,
CreatedDate = DateTime.UtcNow
};
// 使用仓储添加
await repo.AddAsync(product);
await db.SaveChangesAsync();
Console.WriteLine($"✓ 添加成功:");
Console.WriteLine($" ID: {product.Id}");
Console.WriteLine($" Name: {product.Name}");
Console.WriteLine($" Price: {product.Price:C}");
Console.WriteLine($" CreatedDate: {product.CreatedDate:yyyy-MM-dd HH:mm:ss}");
Console.WriteLine($" IsExpensive: {product.IsExpensive}");
// 查询
var found = await repo.GetByIdAsync(product.Id);
Assert.IsNotNull(found);
Assert.AreEqual(product.Name, found.Name);
Console.WriteLine($"\n✓ 查询成功,完全自定义实体照样工作!");
}
/// <summary>
/// 演示: 字符串主键实体
///
/// Region 使用字符串作为主键
/// </summary>
[TestMethod]
public async Task Demo_StringPrimaryKey()
{
var builder = new DataContextBuilder<TestDbContext>();
builder.UseInMemoryDatabase($"StringPKDemo_{Guid.NewGuid()}");
using var db = builder.Build();
// Region 不能使用泛型仓储,因为主键不是 Guid
// 但可以直接使用 DbContext
var regions = db.Context.Set<Region>();
Console.WriteLine("=== 字符串主键实体 ===\n");
// 添加地区
var china = new Region
{
Code = "CN",
Name = "中国",
Description = "中华人民共和国"
};
var usa = new Region
{
Code = "US",
Name = "美国",
Description = "美利坚合众国"
};
regions.Add(china);
regions.Add(usa);
await db.SaveChangesAsync();
Console.WriteLine($"✓ 添加地区:");
Console.WriteLine($" {china.Code}: {china.Name}");
Console.WriteLine($" {usa.Code}: {usa.Name}");
// 根据字符串主键查询
var foundChina = await regions.FindAsync("CN");
Assert.IsNotNull(foundChina);
Assert.AreEqual("中国", foundChina.Name);
Console.WriteLine($"\n✓ 根据字符串主键查询成功: {foundChina.Name}");
// 列出所有地区
var allRegions = await regions.ToListAsync();
Console.WriteLine($"\n所有地区:");
foreach (var region in allRegions)
{
Console.WriteLine($" [{region.Code}] {region.Name}");
}
}
/// <summary>
/// 演示: 自增 Int 主键实体
///
/// Tag 使用 EntityInt 基类,主键是自增 Int
/// </summary>
[TestMethod]
public async Task Demo_AutoIncrementIntPrimaryKey()
{
var builder = new DataContextBuilder<TestDbContext>();
builder.UseInMemoryDatabase($"IntPKDemo_{Guid.NewGuid()}");
using var db = builder.Build();
var repo = db.Repository<Tag>();
Console.WriteLine("=== 自增 Int 主键实体 ===\n");
// 添加标签(不需要手动设置 Id)
var tags = new[]
{
new Tag { Name = "技术", Color = "#2196F3" },
new Tag { Name = "生活", Color = "#4CAF50" },
new Tag { Name = "娱乐", Color = "#FF9800" }
};
await repo.AddRangeAsync(tags);
await db.SaveChangesAsync();
Console.WriteLine($"✓ 添加标签(Id 自动生成):");
foreach (var tag in tags)
{
Console.WriteLine($" ID: {tag.Id}, Name: {tag.Name}, Color: {tag.Color}");
Assert.IsTrue(tag.Id > 0); // 自增 Id
}
// 查询
var foundTag = await repo.GetByIdAsync(1);
Assert.IsNotNull(foundTag);
Console.WriteLine($"\n✓ 根据 Int 主键查询: {foundTag.Name}");
}
/// <summary>
/// 演示: 包含值对象的实体
///
/// Customer 包含 Address 值对象
/// </summary>
[TestMethod]
public async Task Demo_EntityWithValueObject()
{
var builder = new DataContextBuilder<TestDbContext>();
builder.UseInMemoryDatabase($"ValueObjectDemo_{Guid.NewGuid()}")
.UseAudit(true)
.UseCurrentUser("admin");
using var db = builder.Build();
var repo = db.Repository<Customer>();
Console.WriteLine("=== 包含值对象的实体 ===\n");
// 创建客户(包含地址值对象)
var customer = new Customer
{
Name = "张三",
Email = "zhangsan@example.com",
ShippingAddress = new Address
{
Street = "中关村大街1号",
City = "北京市",
State = "北京",
ZipCode = "100080",
Country = "中国"
},
BillingAddress = new Address
{
Street = "五道口路2号",
City = "北京市",
State = "北京",
ZipCode = "100081",
Country = "中国"
}
};
await repo.AddAsync(customer);
await db.SaveChangesAsync();
Console.WriteLine($"✓ 客户信息:");
Console.WriteLine($" 姓名: {customer.Name}");
Console.WriteLine($" 邮箱: {customer.Email}");
Console.WriteLine($"\n 收货地址:");
Console.WriteLine($" {customer.ShippingAddress.Street}");
Console.WriteLine($" {customer.ShippingAddress.City}, {customer.ShippingAddress.ZipCode}");
Console.WriteLine($"\n 账单地址:");
Console.WriteLine($" {customer.BillingAddress.Street}");
Console.WriteLine($" {customer.BillingAddress.City}, {customer.BillingAddress.ZipCode}");
// 查询
var found = await repo.GetByIdAsync(customer.Id);
Assert.IsNotNull(found);
Assert.IsNotNull(found.ShippingAddress);
Assert.AreEqual("中关村大街1号", found.ShippingAddress.Street);
Console.WriteLine($"\n✓ 值对象被正确保存和查询");
}
/// <summary>
/// 演示: 混合使用不同类型的实体
///
/// 在同一个应用中可以混合使用各种实体定义方式
/// </summary>
[TestMethod]
public async Task Demo_MixedEntityTypes()
{
var builder = new DataContextBuilder<TestDbContext>();
builder.UseInMemoryDatabase($"MixedDemo_{Guid.NewGuid()}")
.UseAudit(true)
.UseSoftDelete(true)
.UseCurrentUser("admin");
using var db = builder.Build();
Console.WriteLine("=== 混合使用不同实体 ===\n");
// 1. 使用 AuditableEntity (带审计和软删除)
var productRepo = db.Repository<Product>();
var product = new Product { Name = "产品A", Price = 100m, Stock = 10 };
await productRepo.AddAsync(product);
Console.WriteLine($"1. Product (AuditableEntity):");
Console.WriteLine($" 支持: 审计 + 软删除");
// 2. 使用 Entity (只有 Id)
var categoryRepo = db.Repository<Category>();
var category = new Category { Name = "分类A" };
await categoryRepo.AddAsync(category);
Console.WriteLine($"\n2. Category (Entity):");
Console.WriteLine($" 支持: 仅基础 CRUD");
// 3. 完全自定义
var customRepo = db.Repository<CustomProduct>();
var custom = new CustomProduct
{
Id = Guid.NewGuid(),
Name = "自定义产品",
Price = 200m,
CreatedDate = DateTime.UtcNow
};
await customRepo.AddAsync(custom);
Console.WriteLine($"\n3. CustomProduct (完全自定义):");
Console.WriteLine($" 支持: 自定义字段和逻辑");
// 4. 实现接口 (自定义+审计)
var orderRepo = db.Repository<Order>();
var order = new Order
{
OrderNumber = "ORD-001",
TotalAmount = 500m
};
await orderRepo.AddAsync(order);
Console.WriteLine($"\n4. Order (实现接口):");
Console.WriteLine($" 支持: 自定义实现 + 审计");
await db.SaveChangesAsync();
Console.WriteLine($"\n✓ 所有实体类型都能正常工作!");
Console.WriteLine($"✓ 根据需求选择合适的实体定义方式");
}
/// <summary>
/// 演示: 自定义实体的查询
/// </summary>
[TestMethod]
public async Task Demo_QueryCustomEntities()
{
var builder = new DataContextBuilder<TestDbContext>();
builder.UseInMemoryDatabase($"QueryCustomDemo_{Guid.NewGuid()}");
using var db = builder.Build();
var repo = db.Repository<CustomProduct>();
// 添加测试数据
for (int i = 1; i <= 20; i++)
{
await repo.AddAsync(new CustomProduct
{
Id = Guid.NewGuid(),
Name = $"产品{i}",
Price = i * 100m,
CreatedDate = DateTime.UtcNow.AddDays(-i)
});
}
await db.SaveChangesAsync();
Console.WriteLine("=== 自定义实体的查询 ===\n");
// 1. 条件查询
var expensiveProducts = await repo.FindAsync(p => p.Price > 1000m);
Console.WriteLine($"1. 价格 > 1000 的产品: {expensiveProducts.Count} 个");
// 2. 使用业务属性查询
var veryExpensive = await repo.Query()
.Where(p => p.IsExpensive) // 使用计算属性
.ToListAsync();
Console.WriteLine($"2. 使用 IsExpensive 属性: {veryExpensive.Count} 个");
// 3. 复杂查询
var recentAndCheap = await repo.Query()
.Where(p => p.CreatedDate > DateTime.UtcNow.AddDays(-7) && p.Price < 500m)
.OrderByDescending(p => p.CreatedDate)
.ToListAsync();
Console.WriteLine($"3. 最近7天创建且价格<500: {recentAndCheap.Count} 个");
// 4. 分页查询
var paged = await repo.GetPagedAsync(1, 5);
Console.WriteLine($"4. 分页查询: 第1页 {paged.Items.Count} 条,共 {paged.TotalCount} 条");
Console.WriteLine($"\n✓ 自定义实体支持所有标准查询操作");
}
/// <summary>
/// 演示: 为什么要使用自定义实体?
/// </summary>
[TestMethod]
public void Demo_WhyCustomEntities()
{
Console.WriteLine("=== 为什么使用自定义实体? ===\n");
Console.WriteLine("1. 已有实体模型");
Console.WriteLine(" - 项目已经有定义好的实体");
Console.WriteLine(" - 不想改动现有代码");
Console.WriteLine(" - 快速集成 Sharp.Data\n");
Console.WriteLine("2. 特殊主键类型");
Console.WriteLine(" - 使用字符串作为主键(如: 地区代码)");
Console.WriteLine(" - 使用复合主键");
Console.WriteLine(" - 使用自增 Int 主键\n");
Console.WriteLine("3. 不需要框架功能");
Console.WriteLine(" - 不需要审计");
Console.WriteLine(" - 不需要软删除");
Console.WriteLine(" - 只需要基础 CRUD\n");
Console.WriteLine("4. 完全自主控制");
Console.WriteLine(" - 自定义字段名称");
Console.WriteLine(" - 自定义业务逻辑");
Console.WriteLine(" - 不依赖框架约束\n");
Console.WriteLine("5. 灵活性最高");
Console.WriteLine(" - 可以在同一项目中混用");
Console.WriteLine(" - 根据实体特点选择合适方式");
Console.WriteLine(" - 渐进式采用 Sharp.Data\n");
Console.WriteLine("✓ 总结:");
Console.WriteLine(" Sharp.Data 的基类和接口都是可选的");
Console.WriteLine(" 你可以完全自定义,仍然享受仓储模式的便利");
Console.WriteLine(" 灵活性和便利性可以兼得!");
}
}
+496
View File
@@ -0,0 +1,496 @@
using Microsoft.EntityFrameworkCore;
using Sharp.Data;
using Sharp.Data.Context;
namespace Data.Test;
/// <summary>
/// 演示 07: 降级使用 EF Core
///
/// 本测试类演示:
/// 1. 如何绕过 Sharp.Data 直接使用 EF Core
/// 2. 什么时候需要降级使用
/// 3. 执行原生 SQL
/// 4. 事务管理
/// 5. 高级查询
/// 6. 性能优化
/// </summary>
[TestClass]
public class _07_EFCoreDirectUsageTests
{
/// <summary>
/// 演示: 访问底层 DbContext
///
/// Sharp.Data 不会阻止你使用 EF Core 的原生功能
/// </summary>
[TestMethod]
public void Demo_AccessUnderlyingDbContext()
{
var builder = new DataContextBuilder<TestDbContext>();
builder.UseInMemoryDatabase($"DirectAccessDemo_{Guid.NewGuid()}");
using var db = builder.Build();
// 访问底层 DbContext
var context = db.Context;
Console.WriteLine("=== 访问底层 DbContext ===\n");
Console.WriteLine($"✓ DbContext 类型: {context.GetType().Name}");
Console.WriteLine($"✓ 数据库提供程序: {context.Database.ProviderName}");
Console.WriteLine($"\n可以使用所有 EF Core 功能:");
Console.WriteLine($" - context.Set<T>()");
Console.WriteLine($" - context.Database.*");
Console.WriteLine($" - context.Entry(entity)");
Console.WriteLine($" - 原生 SQL 查询");
Console.WriteLine($" - 事务管理");
Console.WriteLine($" - 等等...");
Assert.IsNotNull(context);
Assert.IsInstanceOfType<TestDbContext>(context);
}
/// <summary>
/// 演示: 直接使用 DbSet
///
/// 绕过仓储,直接操作 DbSet
/// </summary>
[TestMethod]
public async Task Demo_DirectDbSetUsage()
{
var builder = new DataContextBuilder<TestDbContext>();
builder.UseInMemoryDatabase($"DbSetDemo_{Guid.NewGuid()}");
using var db = builder.Build();
Console.WriteLine("=== 直接使用 DbSet ===\n");
// 直接获取 DbSet
var products = db.Context.Set<Product>();
// EF Core 原生操作
var product = new Product
{
Name = "直接添加的产品",
Price = 999m,
Stock = 50
};
products.Add(product);
await db.Context.SaveChangesAsync();
Console.WriteLine($"✓ 使用 DbSet.Add() 添加产品");
// 原生查询
var found = await products
.Where(p => p.Price > 500)
.OrderByDescending(p => p.Price)
.FirstOrDefaultAsync();
Assert.IsNotNull(found);
Console.WriteLine($"✓ 使用 LINQ 查询: {found.Name}");
// 更新
found.Price = 1099m;
await db.Context.SaveChangesAsync();
Console.WriteLine($"✓ 使用 EF Core 跟踪更新: 新价格 {found.Price:C}");
Console.WriteLine($"\n✓ 完全兼容 EF Core 原生操作!");
}
/// <summary>
/// 演示: 执行原生 SQL 查询
///
/// 有时需要执行复杂的 SQL 或存储过程
/// </summary>
[TestMethod]
public async Task Demo_RawSqlQuery()
{
var builder = new DataContextBuilder<TestDbContext>();
builder.UseInMemoryDatabase($"RawSqlDemo_{Guid.NewGuid()}");
using var db = builder.Build();
var repo = db.Repository<Product>();
// 先添加测试数据
await repo.AddRangeAsync(new[]
{
new Product { Name = "产品A", Price = 100m, Stock = 10 },
new Product { Name = "产品B", Price = 200m, Stock = 20 },
new Product { Name = "产品C", Price = 300m, Stock = 30 }
});
await db.SaveChangesAsync();
Console.WriteLine("=== 执行原生 SQL ===\n");
// 注意:InMemory 数据库不支持原生 SQL
// 这里演示语法,实际使用时需要真实数据库
Console.WriteLine("1. 查询型原生 SQL (FromSqlRaw):");
Console.WriteLine(" var products = await context.Products");
Console.WriteLine(" .FromSqlRaw(\"SELECT * FROM Products WHERE Price > {0}\", 150)");
Console.WriteLine(" .ToListAsync();");
Console.WriteLine("\n2. 执行型原生 SQL (ExecuteSqlRaw):");
Console.WriteLine(" var affected = await context.Database");
Console.WriteLine(" .ExecuteSqlRawAsync(\"UPDATE Products SET Price = Price * 1.1\");");
Console.WriteLine("\n3. 调用存储过程:");
Console.WriteLine(" var result = await context.Database");
Console.WriteLine(" .ExecuteSqlRawAsync(\"EXEC UpdateProductPrice @ProductId, @NewPrice\",");
Console.WriteLine(" new SqlParameter(\"@ProductId\", productId),");
Console.WriteLine(" new SqlParameter(\"@NewPrice\", newPrice));");
Console.WriteLine("\n✓ Sharp.Data 不限制原生 SQL 的使用");
Console.WriteLine("✓ 适用于复杂查询、性能优化、遗留 SQL 迁移等场景");
}
/// <summary>
/// 演示: 事务管理
///
/// 使用事务确保数据一致性
/// </summary>
[TestMethod]
public async Task Demo_TransactionManagement()
{
var builder = new DataContextBuilder<TestDbContext>();
builder.UseInMemoryDatabase($"TransactionDemo_{Guid.NewGuid()}");
using var db = builder.Build();
var productRepo = db.Repository<Product>();
var categoryRepo = db.Repository<Category>();
Console.WriteLine("=== 事务管理 ===\n");
// 方式1: 使用 using 语句自动管理事务
Console.WriteLine("方式1: 使用 BeginTransaction");
using (var transaction = await db.Context.Database.BeginTransactionAsync())
{
try
{
// 操作1: 添加产品
var product = new Product
{
Name = "事务测试产品",
Price = 100m,
Stock = 10
};
await productRepo.AddAsync(product);
await db.SaveChangesAsync();
Console.WriteLine($" ✓ 添加产品: {product.Name}");
// 操作2: 添加分类
var category = new Category { Name = "事务测试分类" };
await categoryRepo.AddAsync(category);
await db.SaveChangesAsync();
Console.WriteLine($" ✓ 添加分类: {category.Name}");
// 提交事务
await transaction.CommitAsync();
Console.WriteLine($" ✓ 事务提交成功");
}
catch (Exception ex)
{
// 回滚事务
await transaction.RollbackAsync();
Console.WriteLine($" ✗ 事务回滚: {ex.Message}");
throw;
}
}
// 方式2: 使用 SaveChanges 的隐式事务
Console.WriteLine("\n方式2: SaveChanges 的隐式事务");
Console.WriteLine(" EF Core 的 SaveChanges 本身就是事务性的");
Console.WriteLine(" 多个操作在一次 SaveChanges 中是原子的");
var product2 = new Product { Name = "产品2", Price = 200m, Stock = 20 };
var product3 = new Product { Name = "产品3", Price = 300m, Stock = 30 };
await productRepo.AddAsync(product2);
await productRepo.AddAsync(product3);
await db.SaveChangesAsync(); // 两个产品作为一个事务提交
Console.WriteLine($" ✓ 隐式事务添加了 2 个产品");
Console.WriteLine($"\n✓ Sharp.Data 完全支持 EF Core 事务管理");
}
/// <summary>
/// 演示: 批量操作优化
///
/// 对大量数据的高效操作
/// </summary>
[TestMethod]
public async Task Demo_BulkOperationsOptimization()
{
var builder = new DataContextBuilder<TestDbContext>();
builder.UseInMemoryDatabase($"BulkDemo_{Guid.NewGuid()}");
using var db = builder.Build();
Console.WriteLine("=== 批量操作优化 ===\n");
Console.WriteLine("1. 批量插入(推荐方式):");
var products = new List<Product>();
for (int i = 1; i <= 1000; i++)
{
products.Add(new Product
{
Name = $"产品{i}",
Price = i * 10m,
Stock = i
});
}
var startTime = DateTime.UtcNow;
// 使用 AddRange 一次性添加
await db.Context.Set<Product>().AddRangeAsync(products);
await db.Context.SaveChangesAsync();
var elapsed = (DateTime.UtcNow - startTime).TotalMilliseconds;
Console.WriteLine($" ✓ 一次性插入 1000 条记录: {elapsed:F2}ms");
Console.WriteLine("\n2. 批量更新:");
Console.WriteLine(" var products = await context.Products");
Console.WriteLine(" .Where(p => p.Price < 100)");
Console.WriteLine(" .ToListAsync();");
Console.WriteLine(" foreach (var p in products) p.Price *= 1.1m;");
Console.WriteLine(" await context.SaveChangesAsync();");
Console.WriteLine("\n3. 批量删除:");
Console.WriteLine(" var toDelete = await context.Products");
Console.WriteLine(" .Where(p => p.Stock == 0)");
Console.WriteLine(" .ToListAsync();");
Console.WriteLine(" context.Products.RemoveRange(toDelete);");
Console.WriteLine(" await context.SaveChangesAsync();");
Console.WriteLine("\n💡 性能提示:");
Console.WriteLine(" - 使用 AddRange/UpdateRange/RemoveRange");
Console.WriteLine(" - 避免在循环中调用 SaveChanges");
Console.WriteLine(" - 考虑分批处理大量数据");
Console.WriteLine(" - 使用 AsNoTracking() 提升查询性能");
}
/// <summary>
/// 演示: AsNoTracking 提升查询性能
///
/// 只读查询不需要跟踪实体变化
/// </summary>
[TestMethod]
public async Task Demo_AsNoTrackingOptimization()
{
var builder = new DataContextBuilder<TestDbContext>();
builder.UseInMemoryDatabase($"NoTrackingDemo_{Guid.NewGuid()}");
using var db = builder.Build();
var repo = db.Repository<Product>();
// 添加测试数据
for (int i = 1; i <= 100; i++)
{
await repo.AddAsync(new Product
{
Name = $"产品{i}",
Price = i * 10m,
Stock = i
});
}
await db.SaveChangesAsync();
Console.WriteLine("=== AsNoTracking 优化 ===\n");
Console.WriteLine("1. 默认查询(带跟踪):");
var trackedProducts = await db.Context.Set<Product>()
.Take(10)
.ToListAsync();
Console.WriteLine($" ✓ 查询 {trackedProducts.Count} 条,实体被跟踪");
Console.WriteLine($" 适用于: 需要更新的查询");
Console.WriteLine("\n2. AsNoTracking 查询(不跟踪):");
var untrackedProducts = await db.Context.Set<Product>()
.AsNoTracking() // 不跟踪实体变化
.Take(10)
.ToListAsync();
Console.WriteLine($" ✓ 查询 {untrackedProducts.Count} 条,实体不被跟踪");
Console.WriteLine($" 适用于: 只读查询、数据展示");
Console.WriteLine("\n3. 仓储默认支持:");
var query = repo.Query(trackChanges: false); // 指定不跟踪
var products = await query.Take(10).ToListAsync();
Console.WriteLine($" ✓ 仓储的 Query() 方法支持 trackChanges 参数");
Console.WriteLine("\n💡 性能优势:");
Console.WriteLine(" - 减少内存占用");
Console.WriteLine(" - 提升查询速度");
Console.WriteLine(" - 适合大量数据的只读场景");
}
/// <summary>
/// 演示: 实体变更跟踪
///
/// 检查实体的状态和变更
/// </summary>
[TestMethod]
public async Task Demo_ChangeTracking()
{
var builder = new DataContextBuilder<TestDbContext>();
builder.UseInMemoryDatabase($"ChangeTrackingDemo_{Guid.NewGuid()}");
using var db = builder.Build();
var repo = db.Repository<Product>();
Console.WriteLine("=== 实体变更跟踪 ===\n");
// 添加实体
var product = new Product
{
Name = "测试产品",
Price = 100m,
Stock = 10
};
await repo.AddAsync(product);
var entry = db.Context.Entry(product);
Console.WriteLine($"1. 新增状态:");
Console.WriteLine($" EntityState: {entry.State}"); // Added
Assert.AreEqual(EntityState.Added, entry.State);
await db.SaveChangesAsync();
Console.WriteLine($"\n2. 保存后状态:");
Console.WriteLine($" EntityState: {entry.State}"); // Unchanged
Assert.AreEqual(EntityState.Unchanged, entry.State);
// 修改实体
product.Price = 200m;
Console.WriteLine($"\n3. 修改后状态:");
Console.WriteLine($" EntityState: {entry.State}"); // Modified
Console.WriteLine($" 修改的属性: {string.Join(", ", entry.Properties.Where(p => p.IsModified).Select(p => p.Metadata.Name))}");
Assert.AreEqual(EntityState.Modified, entry.State);
// 查看原始值和当前值
var priceProperty = entry.Property(p => p.Price);
Console.WriteLine($"\n4. 属性变更详情:");
Console.WriteLine($" 原始值: {priceProperty.OriginalValue:C}");
Console.WriteLine($" 当前值: {priceProperty.CurrentValue:C}");
Console.WriteLine($" 是否修改: {priceProperty.IsModified}");
Console.WriteLine($"\n✓ 可以精确跟踪实体的每个变更");
}
/// <summary>
/// 演示: 高级查询技巧
///
/// 使用 EF Core 的高级查询功能
/// </summary>
[TestMethod]
public async Task Demo_AdvancedQueries()
{
var builder = new DataContextBuilder<TestDbContext>();
builder.UseInMemoryDatabase($"AdvancedQueryDemo_{Guid.NewGuid()}");
using var db = builder.Build();
var repo = db.Repository<Product>();
// 添加测试数据
for (int i = 1; i <= 50; i++)
{
await repo.AddAsync(new Product
{
Name = $"产品{i}",
Price = (i % 5 + 1) * 100m,
Stock = i * 2,
Description = i % 2 == 0 ? "热门" : "普通"
});
}
await db.SaveChangesAsync();
Console.WriteLine("=== 高级查询技巧 ===\n");
// 1. 投影查询(只查询需要的字段)
Console.WriteLine("1. 投影查询:");
var projection = await db.Context.Set<Product>()
.Where(p => p.Price > 200m)
.Select(p => new
{
p.Name,
p.Price,
TotalValue = p.Price * p.Stock
})
.Take(5)
.ToListAsync();
Console.WriteLine($" 查询到 {projection.Count} 条记录(只包含投影字段)");
// 2. 分组聚合
Console.WriteLine("\n2. 分组聚合:");
var groupStats = await db.Context.Set<Product>()
.GroupBy(p => p.Description)
.Select(g => new
{
Category = g.Key,
Count = g.Count(),
AvgPrice = g.Average(p => p.Price),
MaxStock = g.Max(p => p.Stock)
})
.ToListAsync();
foreach (var stat in groupStats)
{
Console.WriteLine($" {stat.Category}: {stat.Count}个, 均价 {stat.AvgPrice:C}, 最大库存 {stat.MaxStock}");
}
// 3. 子查询
Console.WriteLine("\n3. 子查询:");
var avgPrice = await db.Context.Set<Product>().AverageAsync(p => p.Price);
var aboveAverage = await db.Context.Set<Product>()
.Where(p => p.Price > avgPrice)
.CountAsync();
Console.WriteLine($" 平均价格: {avgPrice:C}");
Console.WriteLine($" 高于平均价格的产品: {aboveAverage} 个");
// 4. 联合查询
Console.WriteLine("\n4. 联合查询 (Union):");
var cheapProducts = db.Context.Set<Product>().Where(p => p.Price < 200m);
var expensiveProducts = db.Context.Set<Product>().Where(p => p.Price > 400m);
var combined = await cheapProducts.Union(expensiveProducts).CountAsync();
Console.WriteLine($" 便宜或昂贵的产品: {combined} 个");
Console.WriteLine($"\n✓ EF Core 支持几乎所有 LINQ 查询");
}
/// <summary>
/// 演示: 什么时候应该降级使用 EF Core
/// </summary>
[TestMethod]
public void Demo_WhenToUseEFCoreDirect()
{
Console.WriteLine("=== 何时降级使用 EF Core? ===\n");
Console.WriteLine("推荐使用 Sharp.Data 仓储的场景:");
Console.WriteLine(" ✓ 标准的 CRUD 操作");
Console.WriteLine(" ✓ 简单的条件查询");
Console.WriteLine(" ✓ 分页查询");
Console.WriteLine(" ✓ 需要审计和软删除");
Console.WriteLine(" ✓ 统一的数据访问模式\n");
Console.WriteLine("推荐直接使用 EF Core 的场景:");
Console.WriteLine(" ✓ 复杂的连表查询");
Console.WriteLine(" ✓ 执行原生 SQL");
Console.WriteLine(" ✓ 调用存储过程");
Console.WriteLine(" ✓ 需要精细的事务控制");
Console.WriteLine(" ✓ 批量操作性能优化");
Console.WriteLine(" ✓ 高级查询(分组、聚合、子查询等)");
Console.WriteLine(" ✓ 实体变更跟踪");
Console.WriteLine(" ✓ 数据库迁移管理\n");
Console.WriteLine("✓ 总结:");
Console.WriteLine(" Sharp.Data 封装了常用操作,提供便利");
Console.WriteLine(" 但不限制 EF Core 的任何功能");
Console.WriteLine(" 两者可以自由组合使用,根据场景选择");
Console.WriteLine(" 底层完全共用同一个 DbContext,无缝协作");
}
}
+28
View File
@@ -0,0 +1,28 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net8.0</TargetFramework>
<LangVersion>latest</LangVersion>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.12.0"/>
<PackageReference Include="MSTest" Version="3.6.4"/>
</ItemGroup>
<ItemGroup>
<Using Include="Microsoft.VisualStudio.TestTools.UnitTesting"/>
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\..\src\Sharp\Data\Data.csproj" />
<ProjectReference Include="..\..\src\Sharp\Common\Common.csproj" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="Microsoft.EntityFrameworkCore.InMemory" Version="8.0.11" />
</ItemGroup>
</Project>
+1
View File
@@ -0,0 +1 @@
[assembly: Parallelize(Scope = ExecutionScope.MethodLevel)]
+184
View File
@@ -0,0 +1,184 @@
# Sharp.Data 测试演示说明
## 概述
本测试项目不仅是单元测试,更是 **Sharp.Data 模块的完整使用演示**。通过这些测试,您可以学习如何在实际项目中使用 Sharp.Data。
## 测试文件说明
| 文件 | 说明 | 主要演示内容 |
|------|------|------------|
| `TestEntities.cs` | 实体定义演示 | 6种不同的实体定义方式 |
| `TestDbContext.cs` | DbContext 配置演示 | 如何配置各种实体 |
| `01_BasicUsageTests.cs` | 基础使用 | 3行代码快速开始、各种配置选项 |
| `02_RepositoryCRUDTests.cs` | 仓储 CRUD | 增删改查、批量操作、复杂查询 |
| `03_AuditFeatureTests.cs` | 审计功能 | 自动记录创建/修改信息 |
| `04_SoftDeleteTests.cs` | 软删除功能 | 逻辑删除 vs 物理删除 |
| `05_PaginationTests.cs` | 分页查询 | 各种分页场景 |
| `06_CustomEntityTests.cs` | 自定义实体 | 不使用基类的灵活方式 |
| `07_EFCoreDirectUsageTests.cs` | EF Core 直接使用 | 原生 SQL、事务、高级查询 |
## 运行测试
```bash
# 运行所有测试
dotnet test D:\yuanll\sharp-basic\test\Data.Test\Data.Test.csproj
# 运行特定测试类
dotnet test --filter "FullyQualifiedName~_01_BasicUsageTests"
# 运行特定测试方法
dotnet test --filter "Name~Demo_MinimalSetup"
```
## 主要演示场景
### 1. 实体定义方式(TestEntities.cs)
演示了 6 种实体定义方式,满足不同需求:
- **使用 AuditableEntity 基类**:最常用,包含审计和软删除
- **使用 Entity 基类**:只需要 Id,不需要审计
- **完全自定义**:不使用任何基类,完全自主
- **实现接口但不用基类**:灵活控制+享受功能
- **只实现软删除接口**:只要软删除,不要审计
- **包含值对象**:复杂实体结构
### 2. 快速开始(01_BasicUsageTests.cs)
演示最简单的 3 行代码启动方式:
```csharp
var builder = new DataContextBuilder<TestDbContext>();
builder.UseInMemoryDatabase("MyDb");
var db = builder.Build();
```
### 3. 完整 CRUD(02_RepositoryCRUDTests.cs)
演示所有基础操作:
- 添加单个/批量实体
- 各种查询方式(ID、条件、First、分页等)
- 更新实体
- 删除实体(物理/逻辑)
- 统计查询(Count、Any)
- 复杂 LINQ 查询
### 4. 审计追踪(03_AuditFeatureTests.cs)
演示自动审计功能:
- 创建时自动记录 CreatedAt/CreatedBy
- 更新时自动记录 UpdatedAt/UpdatedBy
- 多次更新的审计历史
- 动态获取当前用户(注:当前版本需为每个操作创建新的 DataContext)
### 5. 软删除(04_SoftDeleteTests.cs)
演示软删除与物理删除的区别:
- 软删除不会真正删除数据
- 查询时自动过滤已删除数据
- 使用 IgnoreQueryFilters 查询已删除数据
- 软删除 vs 物理删除的应用场景
### 6. 分页查询(05_PaginationTests.cs)
演示各种分页场景:
- 基础分页
- 带条件的分页
- 带排序的分页
- 遍历所有页
- 分页结果的使用(PageIndex、TotalCount、TotalPages 等)
### 7. 自定义实体(06_CustomEntityTests.cs)
演示完全不使用基类的灵活性:
- 完全自定义的实体
- 字符串主键
- 自增 Int 主键
- 包含值对象的实体
- 混合使用不同类型的实体
### 8. EF Core 直接使用(07_EFCoreDirectUsageTests.cs)
演示如何在需要时降级使用 EF Core:
- 访问底层 DbContext
- 直接使用 DbSet
- 执行原生 SQL
- 事务管理
- 批量操作优化
- AsNoTracking 性能优化
- 实体变更跟踪
- 高级查询
## 注意事项
### PagedResult 属性名称
`PagedResult<T>` 来自 `Common.ExpandFun` 命名空间,使用的属性名是:
- `PageIndex` (而不是 Page)
- `PageSize`
- `TotalCount`
- `TotalPages`
### 动态修改当前用户
当前版本的 DataContext 不支持动态修改当前用户。如果需要模拟不同用户操作,请为每个用户创建新的 DataContext 实例:
```csharp
// 用户 A 的操作
using (var dbA = CreateDataContext("userA"))
{
// ... 操作
}
// 用户 B 的操作
using (var dbB = CreateDataContext("userB"))
{
// ... 操作
}
```
### InMemory 数据库限制
测试使用 InMemory 数据库,它有一些限制:
- 不支持原生 SQL 查询(FromSqlRaw/ExecuteSqlRaw)
- 不完全模拟关系数据库的所有行为
- 主要用于快速测试
实际项目应该使用真实数据库(SQLite、SQL Server 等)。
## 学习路径建议
1. **入门**:先看 `01_BasicUsageTests.cs`,了解如何快速开始
2. **基础**:学习 `02_RepositoryCRUDTests.cs`,掌握基础 CRUD 操作
3. **进阶**:了解审计(03)、软删除(04)、分页(05)功能
4. **高级**:学习自定义实体(06)和 EF Core 直接使用(07)
## 最佳实践
1. **根据需求选择实体定义方式**
- 需要审计+软删除:使用 `AuditableEntity`
- 只需要基础 CRUD:使用 `Entity` 或完全自定义
- 已有实体模型:保持原样,直接使用
2. **性能优化**
- 只读查询使用 `AsNoTracking()`
- 批量操作使用 `AddRange/UpdateRange/RemoveRange`
- 避免在循环中调用 `SaveChangesAsync`
3. **灵活使用**
- 简单场景用仓储
- 复杂查询直接用 EF Core
- 两者可以无缝混用
## 问题反馈
如有问题或建议,请参考 Data 模块的 README.md 文件。
+10
View File
@@ -0,0 +1,10 @@
namespace Data.Test;
[TestClass]
public sealed class Test1
{
[TestMethod]
public void TestMethod1()
{
}
}
+183
View File
@@ -0,0 +1,183 @@
using Microsoft.EntityFrameworkCore;
namespace Data.Test;
/// <summary>
/// 测试用的数据库上下文
/// 演示如何配置各种类型的实体
/// </summary>
public class TestDbContext : DbContext
{
public TestDbContext(DbContextOptions<TestDbContext> options) : base(options)
{
}
// 使用基类的实体
public DbSet<Product> Products => Set<Product>();
public DbSet<Category> Categories => Set<Category>();
public DbSet<Tag> Tags => Set<Tag>();
// 自定义实体
public DbSet<CustomProduct> CustomProducts => Set<CustomProduct>();
public DbSet<Region> Regions => Set<Region>();
// 实现接口的实体
public DbSet<Order> Orders => Set<Order>();
public DbSet<Notification> Notifications => Set<Notification>();
public DbSet<Customer> Customers => Set<Customer>();
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
base.OnModelCreating(modelBuilder);
ConfigureProductEntity(modelBuilder);
ConfigureCategoryEntity(modelBuilder);
ConfigureTagEntity(modelBuilder);
ConfigureCustomProductEntity(modelBuilder);
ConfigureRegionEntity(modelBuilder);
ConfigureOrderEntity(modelBuilder);
ConfigureNotificationEntity(modelBuilder);
ConfigureCustomerEntity(modelBuilder);
}
/// <summary>
/// 配置 Product 实体 (使用 AuditableEntity)
/// </summary>
private void ConfigureProductEntity(ModelBuilder modelBuilder)
{
modelBuilder.Entity<Product>(entity =>
{
entity.HasKey(e => e.Id);
entity.Property(e => e.Name).IsRequired().HasMaxLength(100);
entity.Property(e => e.Price).HasPrecision(18, 2);
entity.Property(e => e.Description).HasMaxLength(500);
// 添加索引
entity.HasIndex(e => e.Name);
entity.HasIndex(e => e.IsDeleted); // 软删除字段索引(提升查询性能)
});
}
/// <summary>
/// 配置 Category 实体 (使用 Entity 基类)
/// </summary>
private void ConfigureCategoryEntity(ModelBuilder modelBuilder)
{
modelBuilder.Entity<Category>(entity =>
{
entity.HasKey(e => e.Id);
entity.Property(e => e.Name).IsRequired().HasMaxLength(50);
entity.Property(e => e.Description).HasMaxLength(200);
});
}
/// <summary>
/// 配置 Tag 实体 (使用 EntityInt 基类,自增主键)
/// </summary>
private void ConfigureTagEntity(ModelBuilder modelBuilder)
{
modelBuilder.Entity<Tag>(entity =>
{
entity.HasKey(e => e.Id);
entity.Property(e => e.Id).ValueGeneratedOnAdd(); // 自增
entity.Property(e => e.Name).IsRequired().HasMaxLength(50);
entity.Property(e => e.Color).HasMaxLength(20);
});
}
/// <summary>
/// 配置 CustomProduct 实体 (完全自定义)
/// </summary>
private void ConfigureCustomProductEntity(ModelBuilder modelBuilder)
{
modelBuilder.Entity<CustomProduct>(entity =>
{
entity.HasKey(e => e.Id);
entity.Property(e => e.Name).IsRequired().HasMaxLength(100);
entity.Property(e => e.Price).HasPrecision(18, 2);
entity.Property(e => e.CreatedDate).IsRequired();
// 忽略计算属性
entity.Ignore(e => e.IsExpensive);
});
}
/// <summary>
/// 配置 Region 实体 (字符串主键)
/// </summary>
private void ConfigureRegionEntity(ModelBuilder modelBuilder)
{
modelBuilder.Entity<Region>(entity =>
{
entity.HasKey(e => e.Code); // 字符串作为主键
entity.Property(e => e.Code).HasMaxLength(10);
entity.Property(e => e.Name).IsRequired().HasMaxLength(100);
entity.Property(e => e.Description).HasMaxLength(500);
});
}
/// <summary>
/// 配置 Order 实体 (实现接口但不使用基类)
/// </summary>
private void ConfigureOrderEntity(ModelBuilder modelBuilder)
{
modelBuilder.Entity<Order>(entity =>
{
entity.HasKey(e => e.Id);
entity.Property(e => e.OrderNumber).IsRequired().HasMaxLength(50);
entity.Property(e => e.TotalAmount).HasPrecision(18, 2);
// 索引
entity.HasIndex(e => e.OrderNumber).IsUnique();
entity.HasIndex(e => e.IsDeleted);
});
}
/// <summary>
/// 配置 Notification 实体 (只实现软删除)
/// </summary>
private void ConfigureNotificationEntity(ModelBuilder modelBuilder)
{
modelBuilder.Entity<Notification>(entity =>
{
entity.HasKey(e => e.Id);
entity.Property(e => e.Title).IsRequired().HasMaxLength(200);
entity.Property(e => e.Content).IsRequired();
entity.HasIndex(e => e.IsDeleted);
});
}
/// <summary>
/// 配置 Customer 实体 (包含值对象)
/// </summary>
private void ConfigureCustomerEntity(ModelBuilder modelBuilder)
{
modelBuilder.Entity<Customer>(entity =>
{
entity.HasKey(e => e.Id);
entity.Property(e => e.Name).IsRequired().HasMaxLength(100);
entity.Property(e => e.Email).IsRequired().HasMaxLength(200);
// 配置值对象 - 方式1: 使用 OwnsOne
entity.OwnsOne(e => e.ShippingAddress, address =>
{
address.Property(a => a.Street).HasMaxLength(200);
address.Property(a => a.City).HasMaxLength(100);
address.Property(a => a.State).HasMaxLength(50);
address.Property(a => a.ZipCode).HasMaxLength(20);
address.Property(a => a.Country).HasMaxLength(50);
});
entity.OwnsOne(e => e.BillingAddress, address =>
{
address.Property(a => a.Street).HasMaxLength(200);
address.Property(a => a.City).HasMaxLength(100);
address.Property(a => a.State).HasMaxLength(50);
address.Property(a => a.ZipCode).HasMaxLength(20);
address.Property(a => a.Country).HasMaxLength(50);
});
entity.HasIndex(e => e.Email);
});
}
}
+159
View File
@@ -0,0 +1,159 @@
using Sharp.Data.Entities;
namespace Data.Test;
// ============================================================
// 演示各种实体定义方式
// ============================================================
#region 方式1: 使用 AuditableEntity 基类 (最常用,包含审计和软删除)
/// <summary>
/// 演示: 使用 AuditableEntity 基类
/// 优点: 自动包含 Id、审计字段、软删除字段
/// 适用场景: 需要审计和软删除功能的业务实体
/// </summary>
public class Product : AuditableEntity
{
public string Name { get; set; } = string.Empty;
public decimal Price { get; set; }
public string? Description { get; set; }
public int Stock { get; set; }
}
#endregion
#region 方式2: 使用 Entity 基类 (只需要 Id,不需要审计)
/// <summary>
/// 演示: 使用 Entity 基类 (Guid 主键)
/// 优点: 只包含 Id,更简洁
/// 适用场景: 简单实体,不需要审计功能
/// </summary>
public class Category : Entity
{
public string Name { get; set; } = string.Empty;
public string? Description { get; set; }
}
/// <summary>
/// 演示: 使用 EntityInt 基类 (Int 主键)
/// 适用场景: 需要自增 Int 主键的实体
/// </summary>
public class Tag : EntityInt
{
public string Name { get; set; } = string.Empty;
public string Color { get; set; } = "#000000";
}
#endregion
#region 方式3: 完全自定义实体 (不使用任何基类)
/// <summary>
/// 演示: 完全自定义实体,不使用任何基类
/// 优点: 完全自主控制,不依赖框架
/// 适用场景:
/// - 已有实体模型,不想改动
/// - 需要特殊的主键类型
/// - 不需要框架提供的任何功能
/// </summary>
public class CustomProduct
{
public Guid Id { get; set; }
public string Name { get; set; } = string.Empty;
public decimal Price { get; set; }
public DateTime CreatedDate { get; set; }
// 可以有自己的业务逻辑
public bool IsExpensive => Price > 1000;
}
/// <summary>
/// 演示: 使用字符串作为主键的自定义实体
/// </summary>
public class Region
{
public string Code { get; set; } = string.Empty; // 使用字符串作为主键,如 "CN", "US"
public string Name { get; set; } = string.Empty;
public string? Description { get; set; }
}
#endregion
#region 方式4: 实现接口但不使用基类 (灵活控制)
/// <summary>
/// 演示: 实现审计接口但不使用基类
/// 优点: 可以自己控制字段名称和类型,同时享受审计功能
/// 适用场景: 需要审计功能,但想自定义实现
/// </summary>
public class Order : IAuditableEntity, ISoftDeletable
{
public Guid Id { get; set; }
public string OrderNumber { get; set; } = string.Empty;
public decimal TotalAmount { get; set; }
// 实现审计接口
public DateTime CreatedAt { get; set; }
public string? CreatedBy { get; set; }
public DateTime? UpdatedAt { get; set; }
public string? UpdatedBy { get; set; }
// 实现软删除接口
public bool IsDeleted { get; set; }
public DateTime? DeletedAt { get; set; }
public string? DeletedBy { get; set; }
}
#endregion
#region 方式5: 只实现软删除接口
/// <summary>
/// 演示: 只实现软删除接口,不要审计功能
/// 适用场景: 只需要软删除,不需要记录创建/修改信息
/// </summary>
public class Notification : ISoftDeletable
{
public Guid Id { get; set; }
public string Title { get; set; } = string.Empty;
public string Content { get; set; } = string.Empty;
public DateTime CreatedAt { get; set; }
// 只实现软删除接口
public bool IsDeleted { get; set; }
public DateTime? DeletedAt { get; set; }
public string? DeletedBy { get; set; }
}
#endregion
#region 方式6: 值对象和复杂实体
/// <summary>
/// 演示: 包含值对象的实体
/// </summary>
public class Customer : AuditableEntity
{
public string Name { get; set; } = string.Empty;
public string Email { get; set; } = string.Empty;
// 值对象
public Address? ShippingAddress { get; set; }
public Address? BillingAddress { get; set; }
}
/// <summary>
/// 值对象: 地址
/// </summary>
public class Address
{
public string Street { get; set; } = string.Empty;
public string City { get; set; } = string.Empty;
public string State { get; set; } = string.Empty;
public string ZipCode { get; set; } = string.Empty;
public string Country { get; set; } = string.Empty;
}
#endregion