目录
背景
有时,我们可能需要使用实体框架核心从表中删除所有记录。常用的方法之一是迭代每一行并使用DBSet<T>.Remove()删除每一行,如下所示:
foreach (var item in Db.Users)
{
Db.Users.Remove(item);
}
Db.SaveChanges();
此过程比Truncate/Delete慢,不适合大型数据集。在这里,将检查替代选项,例如运行Truncate/Delete命令和单元测试选项。
清洁助手
其思想是使用原始SQL查询来Truncate表或Delete表中的所有内容。
注释助手
注释帮助程序是从模型的实体框架映射配置中获取表名和架构。
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Infrastructure;
using Microsoft.EntityFrameworkCore.Metadata;
namespace EfCoreHelper.Database.Core
{
public class AnnotationHelper
{
private static string GetName(IEntityType entityType,
string defaultSchemaName = "dbo")
{
/*3.0.1 these were working*/
//var schemaName = entityType.GetSchema();
//var tableName = entityType.GetTableName();
/*5 and 6 these are working*/
var schema = entityType.FindAnnotation("Relational:Schema").Value;
string tableName = entityType.GetAnnotation
("Relational:TableName").Value.ToString();
string schemaName = schema == null ? defaultSchemaName : schema.ToString();
/*table full name*/
string name = string.Format("[{0}].[{1}]", schemaName, tableName);
return name;
}
public static string TableName<T>(DbContext dbContext) where T : class
{
var entityType = dbContext.Model.FindEntityType(typeof(T));
return GetName(entityType);
}
public static string TableName<T>(DbSet<T> dbSet) where T : class
{
var entityType = dbSet.EntityType;
return GetName(entityType);
}
}
}
扩展方法
与Microsoft.EntityFrameworkCore一起,我们需要安装Microsoft.EntityFrameworkCore.Relational。这将使我们能够使用ExecuteSqlRaw()在实体框架核心中运行行查询。
Install-Package Microsoft.EntityFrameworkCore
Install Microsoft.EntityFrameworkCore.Relational
以下是DbSet<T>和DbContext的扩展方法:
- Truncate——使用truncate查询截断表
- Delete——使用delete查询删除表的所有行
- Clear——使用RemoveRange方法删除表的所有行
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Infrastructure;
using System.Linq;
namespace EfCoreHelper.Database.Core
{
public static class EfCleanHelper
{
public static string Truncate<T>(this DbSet<T> dbSet) where T : class
{
string cmd = $"TRUNCATE TABLE {AnnotationHelper.TableName(dbSet)}";
var context = dbSet.GetService<ICurrentDbContext>().Context;
context.Database.ExecuteSqlRaw(cmd);
return cmd;
}
public static string Delete<T>(this DbSet<T> dbSet) where T : class
{
string cmd = $"DELETE FROM {AnnotationHelper.TableName(dbSet)}";
var context = dbSet.GetService<ICurrentDbContext>().Context;
context.Database.ExecuteSqlRaw(cmd);
return cmd;
}
public static void Clear<T>(this DbContext context) where T : class
{
DbSet<T> dbSet = context.Set<T>();
if (dbSet.Any())
{
dbSet.RemoveRange(dbSet.ToList());
}
}
public static void Clear<T>(this DbSet<T> dbSet) where T : class
{
if (dbSet.Any())
{
dbSet.RemoveRange(dbSet.ToList());
}
}
public static string Truncate(this DbContext context,
string tableName, string schemaName = "dbo")
{
string name = string.Format("[{0}].[{1}]", schemaName, tableName);
string cmd = $"TRUNCATE TABLE { name }";
context.Database.ExecuteSqlRaw(cmd);
return cmd;
}
public static string Delete(this DbContext context,
string tableName, string schemaName = "dbo")
{
string name = string.Format("[{0}].[{1}]", schemaName, tableName);
string cmd = $"DELETE FROM { name }";
context.Database.ExecuteSqlRaw(cmd);
return cmd;
}
}
}
使用代码
Delete
使用名称
Db.Delete("Users");
使用DbSet<T>
Db.Users.Delete();
使用事务
using (var tran = Db.Database.BeginTransaction())
{
try
{
Db.Users.Delete();
//or
//Db.Delete("Users");
tran.Commit();
}
catch (Exception ex)
{
tran.Rollback();
}
}
Truncate
使用名称
Db.Truncate("Users");
使用DbSet<T>
Db.Users.Truncate();
Clear
使用DbSet<T>
Db.Users.Clear();
Db.SaveChanges();
使用DbContext
Db.Clear<User>();
Db.SaveChanges();
单元测试
让我们将包添加到测试项目中:
- 用于实体框架DbContext模拟的最小起订量
- 用于填充样本测试数据的NBuilder
Install-Package Nunit
Install-Package Moq
Install-Package NBuilder
Clear方法适用于单元测试。但是Truncate和Delete方法运行原始SQL,因此对于单元测试,我们需要绕过它们。
在db上下文中添加内容
在这里,CpuAppDbContext db上下文类正在实现ICpuAppDbContext接口。其强制添加void Truncate<T>() where T : class和void Delete<T>() where T : class方法,这将用于truncate表或删除表的数据。在实际实现中,我们调用了现有的扩展方法。
public interface ICpuAppDbContext : IDisposable
{
DbSet<User> Users { get; set; }
void Truncate<T>() where T : class;
void Delete<T>() where T : class;
DbSet<T> Set<T>() where T : class;
int SaveChanges();
}
public class CpuAppDbContext : DbContext, ICpuAppDbContext
{
public CpuAppDbContext(DbContextOptions<CpuAppDbContext> options) : base(options)
{
}
public CpuAppDbContext() : base()
{
}
public DbSet<User> Users { get; set; }
public void Truncate<T>() where T : class
{
this.Set<T>().Truncate();
}
public void Delete<T>() where T : class
{
this.Set<T>().Delete();
}
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder.ApplyConfiguration(new UserConfig());
}
}
在实际代码中使用方法
Db.Delete<User>();
Db.Truncate<User>();
测试方法
将List<T>转换为DbSet<T>的帮助程序方法:
public static class TestHelpers
{
public static DbSet<T> ToDbSet<T>(this List<T> list) where T : class
{
IQueryable<T> queryableList = list.AsQueryable();
var mockSet = new Mock<DbSet<T>>();
mockSet.As<IQueryable<T>>().Setup(m =>
m.Provider).Returns(queryableList.Provider);
mockSet.As<IQueryable<T>>().Setup(m =>
m.Expression).Returns(queryableList.Expression);
mockSet.As<IQueryable<T>>().Setup(m =>
m.ElementType).Returns(queryableList.ElementType);
mockSet.As<IQueryable<T>>().Setup(m =>
m.GetEnumerator()).Returns(queryableList.GetEnumerator());
return mockSet.Object;
}
}
使用接口进行单元测试
在这里,我们绕过/模拟了该Delete方法:
[Test]
public void DbContext_Moq_Using_Interface()
{
List<User> users = Builder<User>.CreateListOfSize(10).Build().ToList();
bool isCalled = false;
var dbMock = new Mock<ICpuAppDbContext>();
dbMock.Setup(x => x.Users).Returns(users.ToDbSet());
dbMock.Setup(x => x.Delete<User>()).Callback(() =>
{
users = new List<User>();
isCalled = true;
});
/*will pass this db to repo*/
ICpuAppDbContext db = dbMock.Object;
db.Delete<User>();
Assert.True(isCalled);
//Assert.False(db.Users.Any());
}
使用DbContext类进行单元测试
对于模拟,我们必须将可测试的数据库集和方法声明为虚拟的。这个虚拟的东西也与懒惰和渴望加载有关。因此,在使用或更改现有代码之前,请确保。我确实更喜欢基于界面的模拟,这是最好的选择。
[Test]
public void DbContext_Moq_Using_Class()
{
List<User> users = Builder<User>.CreateListOfSize(10).Build().ToList();
bool isCalled = false;
var dbMock = new Mock<CpuAppDbContext>();
dbMock.Setup(x => x.Users).Returns(users.ToDbSet()); /*in dbcontext,
need to add virtual, public virtual DbSet<User> Users { get; set; }*/
dbMock.Setup(x => x.Delete<User>()).Callback(() => /*in dbcontext,
need to add virtual, public virtual void Delete<T>() where T : class*/
{
users = new List<User>();
isCalled = true;
});
/*will pass this db to repo*/
CpuAppDbContext db = dbMock.Object;
db.Delete<User>();
Assert.True(isCalled);
//Assert.False(db.Users.Any());
}
程序集伪造
为了模拟扩展方法,也可以使用伪造程序集。
Moq.EntityFrameworkCore
如果我们想使用上下文类本身进行单元测试,我们也可以使用Moq.EntityFrameworkCore。但它需要EF Core 6。
Install-Package Moq.EntityFrameworkCore
局限性
- Truncate或Delete SQL语句立即执行,无论我们是否调用Db.SaveChanges()
- Clear比Truncate/Delete慢,不适合大型数据集。
- Clear适用于常规单元测试,而Truncate和Delete不适合。
引用
- EF Core测试帮助程序 GitHub - MichalJankowskii/Moq.EntityFrameworkCore: Library that provides methods that will help you with mocking EntityFrameworkCore.
- Moq助手 Redirecting…
- Fakes https://www.youtube.com/watch?v=SkdgYG6W6X
- 懒惰和急切的加载 entity framework - What is the difference between DbSet<> and virtual DbSet<>? - Stack Overflow
关于代码示例
- Visual Studio 2022 Solution
- ASP.NET 6
- EF Core 6,也在Core 5中进行了测试
Database.Test是一个有趣的单元测试项目。在 appsettings.json 中更改连接字符串。在数据库中创建用户表,检查项目Database.Application的db.sql。检查/运行EfCleanHelperTests.cs和EfCleanHelperUnitTests.cs 的测试。
{
"ConnectionStrings": {
"DatabaseConnection": "Data Source=.\\SQLEXPRESS;
Initial Catalog=Cup;Integrated Security=True"
}
}
DROP TABLE IF EXISTS [dbo].[Users]
GO
CREATE TABLE [dbo].[Users](
[Id] [bigint] IDENTITY(1,1) NOT NULL,
[Name] [nvarchar](max) NULL,
[Email] [nvarchar](max) NULL,
[IsDeleted] [bit] NOT NULL,
[CreatedOn] [datetime2](7) NOT NULL,
[CreatedBy] [nvarchar](max) NOT NULL,
[ModifiedOn] [datetime2](7) NULL,
[ModifiedBy] [nvarchar](max) NULL
)
https://www.codeproject.com/Articles/5339402/Delete-All-Rows-in-Entity-Framework-Core-6

2002

被折叠的 条评论
为什么被折叠?



