65.9K
CodeProject 正在变化。 阅读更多。
Home

包含单元工作流、IoC 和单元测试的存储库

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.98/5 (26投票s)

2014 年 3 月 22 日

CPOL

11分钟阅读

viewsIcon

148546

downloadIcon

7625

带 Unit of Work 的存储库。

引言

本文讨论了工作单元存储库模式的使用。然后,它展示了如何将此模式与 IoC 和单元测试结合使用。

我将使用工作单元存储库模式来实现我的演示应用程序的数据层。我将使用 EF6 Code First 创建一个包含 3 个表的数据库:Team、Role 和 User。用户具有角色,并且是团队的成员。

本教程的目标受众是中高级 .NET 开发人员。还需要具备 EF Code First 的先验知识。

背景

本教程的驱动力是提供一个完整的关于存储库和工作单元与 IoC 和单元测试的示例。添加 IoC 和单元测试将展示所有这些组件/模式如何协同工作。

使用存储库模式的想法是为您的应用程序创建一个抽象的数据访问层。这将使您能够将所有数据访问逻辑集中在一个地方。结合泛型功能,您可以减少常用场景所需的代码量,同时仍能创建自定义存储库以实现更具体的使用。

工作单元模式有助于将一系列交互组合在一起,并使用事务一次性提交它们。如果您从头开始创建数据源,并且只使用一个 EF 上下文,那么您可能不需要工作单元,并且可以依赖于您的数据上下文,但这并非总是如此。例如,您可能希望使用多个 EF 上下文来完成您的操作,这时工作单元就派上用场了。使用工作单元中的事务功能,您可以在同一范围内使用不同的上下文,而无需担心在完成数据操作时出现异常而丢失数据完整性。

使用代码

下载与本文相关的项目并在 Visual Studio 中打开。您需要在本地安装 Sql Express 才能运行应用程序。运行应用程序后,您将看到一个菜单,其中列出了应用程序可以执行的功能。

应用程序本身是关于存储库和工作单元模式如何协同工作的演示,因此,重要的是这些模式的实现,而不是应用程序本身。

正如 zip 文件夹的名称所示,您可以发现每个文件夹都指向实现的一个部分。Demo_initial 包含初始实现。Demo_WithAutofac 包含相同的实现,但使用了 Autofac IoC 容器。Demo_WithUnitTest 包含将在最后添加的单元测试以及所需的更改。

实现

实现包含 3 个部分

  1. 模型(Team、User 和 Role 的实体类)
  2. 数据层(存储库、工作单元、EF 上下文)
  3. 用户界面(控制台应用程序)

模型

模型包含 3 个类:User、Role 和 Team。这是这些类的实现

public class User : BaseModel<int>
{
    public string Password { get; set; }
    public string email { get; set; }
 
    [Required, StringLength(100)]
    public override string Name { get; set; }
 
    public int? TeamId { get; set; }
    public virtual Team Team { get; set; }
 
    public int RoleId { get; set; }
    public virtual Role Role { get; set; }
}
 
public class Team : BaseModel<int>
{
    public virtual IEnumerable<User> Users { get; set; }
}
 
public class Role : BaseModel<int>
{
    public virtual IEnumerable<User> Users { get; set; }
} 

您会注意到这 3 个类都继承自 BaseModel 类。这个抽象类包含所有模型类的共同属性。EF Code First 将根据这些类的定义自动配置您的数据库。这是基模型类的实现。

public abstract class BaseModel<T>
{
    [DatabaseGeneratedAttribute(DatabaseGeneratedOption.Identity)]
    public virtual T Id { get; set; }
 
    [Required, StringLength(maximumLength: 250)]
    public virtual string Name { get; set; }
 
    [StringLength(maximumLength: 1000)]
    public virtual string Description { get; set; }
}  

数据层

数据层包括存储库、工作单元和 EF 数据上下文

存储库

存储库的实现是在方法级别上通用的。这种实现的优点是您不需要为每个模型创建存储库,但同时它也为您提供了创建自定义存储库的灵活性,以用于选定模型进行更专门的操作,如复杂查询。

这是 SqlRepository 类的完整实现。

public class SqlRepository : IDisposable
{
    private readonly DbContext context;
 
    public SqlRepository(DbContext context)
    {
        this.context = context;
    }
 
    public IQueryable<TEntity> GetAll<TEntity>() where TEntity : class
    {
        return GetEntities<TEntity>().AsQueryable();
    }
 
    public void Insert<TEntity>(TEntity entity) where TEntity : class
    {
        GetEntities<TEntity>().Add(entity);
    }
 
    public void Delete<TEntity>(TEntity entity) where TEntity : class
    {
        GetEntities<TEntity>().Remove(entity);
    }
 
    private IDbSet<TEntity> GetEntities<TEntity>() where TEntity : class
    {
        return this.context.Set<TEntity>();
    }
 
    public void SaveChanges()
    {
    this.context.SaveChanges();
    }
 
    public void Dispose()
    {
        if (this.context != null)
        {
            this.context.Dispose();
        }
    }
} 

实现足够简单,可以帮助您使用您的模型。这里的主要方法是私有方法 GetEntities,它返回数据库上下文中给定模型的实体集。

基于这个通用存储库,我创建了一个 Team 存储库,其中包含与 Team 模型相关的特定查询。

public class TeamRepository : SqlRepository
{
    public TeamRepository(DbContext context) : base(context)
    {}
    
    public List<User> GetUsersInTeam(int teamId)
    {
        return (from u in this.GetAll<User>()
            where u.TeamId == teamId
            select u).ToList();
    }
}  

TeamRepository 受益于父类中的通用方法,但它实现了自己的特定方法。

工作单元

UnitOfWork 类抽象了上层事务的使用。它公开了执行此操作所需的方法。这是此类实现。

public class UnitOfWork : IDisposable
{
    private TransactionScope transaction;
 
    public void StartTransaction()
    {
        this.transaction = new TransactionScope();
    }
 
    public void CommitTransaction()
    {
        this.transaction.Complete();
    }
 
    public void Dispose()
    {
        this.transaction.Dispose();
    }
} 

您很快就会看到这个类的用法。

数据库上下文

数据库上下文是 EF Code First 上下文。这是数据库上下文的实现

public class EFContext : DbContext
{
    public EFContext() : base("ReposWithUnitOfWorkDB")
    {
        Database.SetInitializer<EFContext>(new DBInitializer());
    }
 
    public new IDbSet<TEntity> Set<TEntity>() where TEntity : class
    {
        return base.Set<TEntity>();
    }
 
    protected override void OnModelCreating(DbModelBuilder modelBuilder)
    {
        modelBuilder.Conventions.Remove<PluralizingTableNameConvention>();
        modelBuilder.Entity<User>();
        modelBuilder.Entity<Role>();
        modelBuilder.Entity<Team>();
        base.OnModelCreating(modelBuilder);
    }
} 

调用基构造函数时指定的名称将是数据库名称。OnModelCreating 的实现包含两个部分

  • 移除 PluralizingTableNameConvention 将确保表名与您的模型类名匹配。
  • 通过在 modelBuilder 对象上使用 Entity<T> 方法使上下文了解可用的模型类。然后,EF Code First 将根据模型类确定要创建的表和关系。

在构造函数中,我调用了我的 DBInitializer 来向数据库插入一些数据。此类继承自 CreateDatabaseIfNotExists 类,该类仅在数据库不存在时才创建它。它还覆盖了 Seed 方法。这是 DBInitializer 的定义。

public class DBInitializer : CreateDatabaseIfNotExists<EFContext>
{
    protected override void Seed(EFContext context)
    {
        using (var ctx = new EFContext())
        {
            // Please see source code for implementation
        }
    }
} 

用户界面

用户界面是一个控制台应用程序,提供了一些选项来查看存储库和工作单元模式的实际应用。

第一个选项将触发 DBInitializer 来创建数据库(如果它不存在),因此建议在首次运行应用程序时选择此选项。

这是选项 2 的实现。

private static void RunAndCommitTransaction()
{
    using (var uof = new UnitOfWork())
    {
        uof.StartTransaction();
        var repo = new SqlRepository(new EFContext());
 
        Console.WriteLine("\nStarting a tranaction....");
 
        var role = new Role { Name = "Tester", Description = "Tester description" };
        repo.Insert<Role>(role);
 
        var user = new User { Name = "Andy", Description = "Andy user description", email = "Andy@email.com", Password = "123" };
        repo.Insert<User>(user);
        user.Role = role;
        user.Team = repo.GetAll<Team>().FirstOrDefault(t => t.Name.Equals("Los Banditos"));
 
        repo.SaveChanges();
        uof.CommitTransaction();
 
        Console.WriteLine(string.Format("\nThe tranaction has been commited.\nUser '{0}' and Role '{1}' were added successfully", user.Name, role.Name));
    }
} 

在此方法中,我使用工作单元将我的操作放入一个事务中。我还使用 SqlRepository 中的通用方法来插入和检索实体。

选项 3 的内容与选项 2 几乎相同,但它会回滚事务而不是提交事务。回滚可以通过不调用 commit transaction 或在调用 commit transaction 之前抛出异常来实现。

选项 4 使用 TeamRepository 来获取给定团队中的所有用户。这是选项 4 的实现。

private static void SelectUsersInTeam()
{
   using (var ctx = new EFContext())
   {
    var repo = new TeamRepository(ctx);
    var teams = repo.GetAll<Team>().ToList();
 
    teams.ForEach(t => Console.WriteLine(string.Format("Team Name:{0}, Team Id: {1}", t.Name, t.Id)));
    
    Console.Write("Enter team id: ");
    var teamId = Console.ReadLine();
    
    repo.GetUsersInTeam(int.Parse(teamId)).ForEach(
    u => Console.WriteLine(string.Format("Name: {0}, Email: {1}", u.Name, u.email))
            );
   }
} 

使用 Autofac 构建 IoC 容器

Autofac 将帮助解析应用程序所需的所有依赖项。它增强了不同应用程序组件之间的解耦。IoC 容器的理念是,它允许您在一个中心位置(容器)定义所有代码依赖项,并在需要时访问它们。

我为我的应用程序创建了一个 BootStrap 类。此类有一个名为 BuildContainer 的方法,该方法返回 Autofac 容器。这是 BootStrap 的实现

public class BootStrap
{
    public static IContainer BuildContainer()
    {
        var builder = new ContainerBuilder();
 
        builder.RegisterType<EFContext>().As<IDbContext>();
        builder.RegisterType<UnitOfWork>().As<IUnitOfWork>();
        builder.RegisterType<SqlRepository>().As<IRepository>();
        builder.RegisterType<TeamRepository>().As<ITeamRepository>();
 
        return builder.Build();
    }
}  

首先,我创建 ContainerBuilder ,然后注册我的类型。请注意,我使用接口注册我的实现类型。我注册的接口就是这些实现所实现的接口。

这将允许您要求容器解析接口的实现,而不是直接解析实现本身。这增强了解耦,因为您不再依赖于特定的实现,而是任何实现给定接口的类。这个概念是 IoC 的核心,因为使用接口的人不需要知道哪个类实现了它,只要它实现了该接口。

此功能允许您仅在一个位置替换给定类的实现,即在构建容器时。

定义接口

我提取了之前定义的数据层组件的接口。这是这些接口的定义。

public interface IDbContext : IDisposable
{
    IDbSet<TEntity> Set<TEntity>() where TEntity : class;
    int SaveChanges();
}
 
public interface IRepository : IDisposable
{
    void Delete<TEntity>(TEntity entity) where TEntity : class;
    IQueryable<TEntity> GetAll<TEntity>() where TEntity : class;
    void Insert<TEntity>(TEntity entity) where TEntity : class;
    void SaveChanges();
}
 
public interface ITeamRepository : IRepository
{
    List<ReposWithUnitOfWorkSol.Model.User> GetUsersInTeam(int teamId);
}
 
public interface IUnitOfWork : IDisposable
{
    void CommitTransaction();
    void StartTransaction();
} 

请注意,其中一些接口继承自 IDisposable 接口。IDisposable 将由实现该接口的类实现,例如 UnitOfWork 和 SqlRepository。这非常重要,因为我们正在处理外部资源,需要在使用完毕后进行处置,即 DbContext 和 TransactionScope。

使用 Autofac IoC 容器

您可以通过以下方式从 Autofac 容器解析依赖项:

  • 将您的依赖项注入构造函数,让 Autofac 解析它们
  • 使您的容器实例可用,并在需要时解析您的依赖项

在演示应用程序中,我同时使用了这两种方法。

注入依赖项

SqlRepository 类利用了此功能。这是该类的新实现,该类之前已定义。

public class SqlRepository : IRepository
{
   private readonly IDbContext context;
 
   public SqlRepository(IDbContext context)
   {
     this.context = context;
   }
} 
// Please refer to source code for full implementation

SqlRepository 现在实现了 IRepository 接口。注意构造函数中的 IDbContext 接口。当您尝试从 Autofac 容器解析 SqlRepository 时,只要您注册了依赖项(即 IDbContext,我们在前面的 BootStrap 中已经这样做了),Autofac 就会自动注入此依赖项。

按需解析依赖项

这意味着您不会在构造函数中注入依赖项,而是在需要使用某个依赖项时才请求它。

以下是 RunAndRollbackTransaction 方法中此用法的示例。

private static void RunAndRollbackTransaction()
{
   using (var uof = container.Resolve<IUnitOfWork>())
   using (var repo = container.Resolve<IRepository>())
   {
        uof.StartTransaction();
 
        Console.WriteLine("\nStarting a tranaction....");
 
        var role = new Role { Name = "ProductOwner", Description = "Product Owner role description" };
        repo.Insert<Role>(role);
 
        var user = new User { Name = "Mark", Description = "Mark user description", email = "Mark@email.com", Password = "123" };
        repo.Insert<User>(user);
    user.Role = role;
    user.Team = repo.GetAll<Team>().FirstOrDefault(t => t.Name.Equals("Los Banditos"));
 
    Console.WriteLine("\nSaving changes....");
    repo.SaveChanges();
 
    Console.WriteLine("\nRolling back the transaction....");
    Console.WriteLine(string.Format("\nThe tranaction has been rolled back"));
   }
} 

容器对象在 Main 方法中定义。它保存 Autofac 容器,该容器由 BootStrap 类从其 BuildContainer 方法返回。请注意 using 语句的使用。这是一个好的做法,可以在 using 块执行完毕后处置任何资源。

如您所见,实现与之前定义的相同,只有一个例外,那就是从 Autoface 容器解析依赖项。

此方法利用 UnitOfWork 类强制回滚事务,因此方法中创建的 User 将永远不会被添加,因为我们没有在 UnitOfWork 对象上调用 CommitTransaction 方法。有关已提交事务的实现,请参见源代码中的 RunAndCommitTransaction 实现。

为单元测试做准备

理想情况下,如果您使用测试驱动开发 (TDD),您会先编写测试。但我打算将此留到最后,以便您可以看到我们如何从僵化的实现迁移到使用 Autofac,这为我们的单元测试奠定了良好的基础。

为了对我们的 Program 类进行单元测试,我们需要对其进行一些更改。这些更改包括一个可以设置 IoC 容器的函数。这是必需的,因为我们需要构建我们的容器,但不是添加实际实现,我们将使用 moq 库向我们的类添加模拟。

由于我们的方法是私有的,我们将无法直接测试它们。但是,我们应该测试一个将触发我们方法的特定场景。这意味着我们需要通过模拟来达到我们的方法。正如您可能看到的,UI 使用 Console 类输出文本并读取用户输入以执行所需选项。我们需要添加一个接口,然后我们可以模拟 Console。

这是 IConsole 的接口和实现

public interface IConsole
{
   string ReadInput();
   void WriteOutputOnNewLine(string output);
   void WriteOutput(string output);
}
 
public class ConsoleReadWrite : IConsole
{
   public string ReadInput()
   {
    return Console.ReadLine();
   }
 
   public void WriteOutput(string output)
   {
    Console.Write(output);
   }
 
   public void WriteOutputOnNewLine(string output)
   {
    Console.WriteLine(output);
   }
} 

为了使用这个类,我们将把它添加到我们的 Autofac 容器中,并在 Program 类的 Main 方法中解析它。这是 Program 类的部分实现,重点介绍了更改。请注意,这不是完整实现。有关 Program 类的完整实现,请参阅源代码。

public class Program
{
    private static IContainer container;
    private static IConsole console;
 
    public static void SetContainer(IContainer mockedContainer)
    {
        container = mockedContainer;
    }
 
    public static void Main(string[] args)
    {
        if (container == null)
        {
            container = BootStrap.BuildContainer();
        }
 
        console = container.Resolve<IConsole>();
 
        var userChoice = string.Empty;
 
        while (userChoice != "5")
        {
            console.WriteOutputOnNewLine("\nChoose one of the following options by entering option's number:\n ");
            console.WriteOutputOnNewLine("1- Initialize DB\n");
            console.WriteOutputOnNewLine("2- Run and commit a transaction\n");
            console.WriteOutputOnNewLine("3- Run and rollback a transaction\n");
            console.WriteOutputOnNewLine("4- Select users in a team\n");
            console.WriteOutputOnNewLine("5- Exit");
 
            userChoice = console.ReadInput();
 
            switch (userChoice)
            {
                case "1":
                    InitializeDB();
                    break;
                case "2":
                    RunAndCommitTransaction();
                    break;
                case "3":
                    RunAndRollbackTransaction();
                    break;
                case "4":
                    SelectUsersInTeam();
                    break;
            }
        }
    }
    // Please view source code for full implementation
} 

在 Main 方法中,我正在检查 Autofac 容器是否已设置。此检查允许我从单元测试中设置我的 Autofac 容器,您将在稍后看到。

单元测试

现在我们已经具备了开始编写单元测试所需的一切。我将要进行单元测试的方法是:RunAndCommitTransaction。此单元测试将测试我们是否实际调用了 UnitOfWork 类上的 StartTransaction 和 CommitTransaction。这是此单元测试的定义。

[TestMethod]
public void RunAndCommitTransaction_WithDefault()
{
  // Arrange
  var contextMock = new Mock<IDbContext>();
  contextMock.Setup(a => a.Set<User>()).Returns(Mock.Of<IDbSet<User>>);
  contextMock.Setup(a => a.Set<Role>()).Returns(Mock.Of<IDbSet<Role>>);
  contextMock.Setup(a => a.Set<Team>()).Returns(Mock.Of<IDbSet<Team>>);
 
  var unitOfWorkMock = new Mock<IUnitOfWork>();
 
  var consoleMock = new Mock<IConsole>();
  consoleMock.Setup(c => c.ReadInput()).Returns(new Queue<string>(new[] { "2", "5" }).Dequeue);
 
  var container = GetMockedContainer(contextMock.Object, unitOfWorkMock.Object, consoleMock.Object);
    
  // Act
  Program.SetContainer(container);
  Program.Main(null);
 
  // Assert
  unitOfWorkMock.Verify(a => a.StartTransaction(), Times.Exactly(1));
  unitOfWorkMock.Verify(a => a.CommitTransaction(), Times.Exactly(1));
} 

Arrange

在单元测试的 arrange 部分,我正在模拟 IDbContext、UnitOfWork 和 IConsole。我将这些模拟传递给 GetMockContainer,它将使用这些模拟来构建 Autofac 容器。

IDbContext 的设置侧重于模拟 Set<T> 方法,该方法将由存储库用于插入/选择实体。IUnitOfWork 模拟不需要设置。

IConsole 模拟的设置很有趣。要执行 RunAndCommitTransaction 函数的选项是 2,但我们也想模拟第二个输入以模拟退出选项(编号 5)。我将 Setup 的 Returns 方法的值设置为 Queue 对象的 Deque 函数。Returns 函数的工作方式是,它返回上次设置为返回的值,但它接受一个函数,在我们的例子中,Deque 函数将被调用 2 次以模拟选项 2 然后选项 5。

这是 GetMockedContainer 的定义

private IContainer GetMockedContainer(IDbContext ctx, IUnitOfWork uow, IConsole console)
{
  var builder = new ContainerBuilder();
 
  builder.RegisterInstance(ctx).As<IDbContext>();
  builder.RegisterInstance(uow).As<IUnitOfWork>();
  builder.RegisterInstance(new Mock<IRepository>().Object).As<IRepository>();
  builder.RegisterInstance(console).As<IConsole>();
 
  return builder.Build();
}  

由于 IRepository 模拟不需要任何设置,我只是从 Mock 获取 Object,而不是将其传递进去。

Act

单元测试中的 act 部分是您采取行动以执行要测试的部分的功能。在我们的例子中,我们将 Autofac 容器设置为模拟容器,然后执行 Program 中的 Main 方法。

断言(Assert)

单元测试中的 assert 部分是您验证操作结果的地方。在这种情况下,我们期望 UnitOfWork 对象上的 StartTransaction 和 CommitTransaction 方法都被调用一次。

结论

在本文中,我解释了如何实现带工作单元的存储库模式。之后,我们添加了使用 Autofac 的 IoC 容器的使用,然后我们为演示应用程序添加了单元测试。希望您喜欢阅读本文,并希望它为您带来了新的知识。

历史

2014 年 3 月 22 日:V 1.0:创建

© . All rights reserved.