在何处放置与数据访问相关的业务逻辑层,同时通过依赖关系注入提供单元可测试性

本文关键字:依赖 关系 注入 可测试性 单元 数据 访问 业务 在何处 | 更新日期: 2023-09-27 18:34:54

我正在寻找一种方法,在具有实体框架的 MVC4 应用程序中拥有单独的业务逻辑层,而无需在其中注入我的(真实或虚假(数据库上下文。现在我们有一个虚假和真实的数据库上下文,它派生自 IContext ,一个存储库Repository和一个控制器MyControllerIContext注入到控制器和存储库中,具有以下模式:

public class MyController : Controller
{
    readonly Repository _repo;
    /// <summary>
    ///     Initializes a new instance of the <see cref="Controllers.MyController"/> class which
    ///     makes use of the real Entity Framework context that connects with the database.
    /// </summary>
    public MyController()
    {
        _repo = new Repository();
    }
    /// <summary>
    ///     Initializes a new instance of the <see cref="Controllers.MyController"/> class which
    ///     can make use of a "Fake" Entity Framework context for unit testing purposes.
    /// </summary>
    /// <param name="db">Database context.</param>
    public MyController(IContext db)
    {
        _repo = new Repository(db); 
    }
    (...)
}

这是这样设置的,因为它是具有访问数据库的方法并需要假上下文的存储库,而单元测试针对控制器并且需要实例化。

现在,我正在寻找一种将业务逻辑类添加到项目中的方法的方法,该项目将放置具有业务逻辑的方法。这些方法也需要从数据访问层(Repository(获得的信息。

上面的模式在这个业务逻辑层中也注入RepositoryIContext感觉有点奇怪。 例如,

BusinessLogic(IContext db) 
{
     _repo = new Repository(db);
}

我看到两种方法可以避免此问题:

  1. 在控制器中调用存储库,并将获得的数据用作业务逻辑层中方法的参数。
  2. 无论如何都要注射。

我正在寻找一个好主意来设置与数据访问相关的业务逻辑层并提供单元可测试性,最好不必在每个类中注入数据库上下文。

希望有人能为我提供一些见解。谢谢。

在何处放置与数据访问相关的业务逻辑层,同时通过依赖关系注入提供单元可测试性

你有 EF 上下文,它继承自 IContext,或者如果要使用 EF6,则继承 DbContext:

    public class UserContext : DbContext 
    {
        DbSet<User> users { get; set; }        
    }

您不想将上下文直接注入控制器。

在构造函数中使用采用 IContext/DbContext 实例的服务/DAO/存储库并处理数据访问。在我的示例中,我使用一个 IUserService,它采用上下文并按 ID 返回用户。

    public interface IUserService
    {        
       User GetUserByID(int userId);        
    }
    public class UserService : IUserService
    {
      private readonly UserContext _context;
    
      //Inject your mock or real context here
      public UserService(UserContext context)
      { 
         this._context = context;
      }
      //Implement IUserService
      public User GetUserByID(int userId)
      {  
          _context.Users.Where(u=>u.ID==userId).FirstOrDefault();
      }
    }

然后通过接口将服务注入控制器。

  public class UserController : Controller
  {
     private readonly IUserService _service;
     //Consider using a Dependency injection framework e.g. Unity
     public UserController(IUserService service)
     { 
        this.service = service;
     } 
     //The method that is tightly coupled to the view and uses the service
     [HttpGet] 
     public ActionResult GetUserByID(int id)
     {
          return View(_service.GetUserById(id));
     }
 }

此外,请考虑使用模拟框架来模拟您的上下文,例如 Moq

[TestMethod]
public void UserServiceTest()
{
        //Initialize your Mock Data
        var testDataUser = new List<Users> 
        { 
            new User{
                                ID = 1,
                                Name = "MockUser"
            }
        }.AsQueryable();
         //Initialize the Mock DbSet
        var mockSetUser = new Mock<DbSet<User>>();
        mockSetUser.As<IQueryable<User>>().Setup(m => m.Provider).Returns(testDataUser. .Provider);
        mockSetUser.As<IQueryable<User>>().Setup(m => m.Expression).Returns(testDataUser .Expression);
        mockSetUser.As<IQueryable<User>>().Setup(m => m.ElementType).Returns(testDataUser .ElementType);
        mockSetUser.As<IQueryable<User>>().Setup(m => m.GetEnumerator()).Returns(testDataUser .GetEnumerator());
         //Initialize the mock Context
        var mockContext = new Mock<UserContext>();
        //Return the mock DbSet via the mock context
        mockContext.Setup(c => c.Users).Returns(mockSetUser.Object);
        //Create the service and inject the mock context
        var userService = new UserService(mockContext.Object)
        
        //Test your context via the service
        var user = userService.GetUserByID(1);
        Assert.AreEqual("MockUser", user.Name);
}

我同意GMich的观点。 也。。。图层之间应该有硬边界。 它可能看起来像这样:

IUserController <-- IUserProcessor <-- IUserRepository <-- IContext

您的 DI 和构造函数可以是:

public UserController(IUserProcessor userProcessor){...}
public UserProcessor(IUserRepository userRepository){...}
public UserRepository(IContext context){...}
public MyAppContext(string connectionString){...}

创建将业务逻辑和存储库封装为属性的容器也很方便。 例如:

public class RepositoryContainer : IRepositoryContainer
{
  private readonly IContext _context;
  
  private IUserRepository _userRepository;
  private IUserProfileRepository _userProfileRepository;
  
  public RepositoryContainer(IContext context)
  {
    if (context == null) throw new ArgumentNullException("context");
    
    _context = context;
  }
  
  public IUserRepository UserRepository
  {
    get 
    { 
      return _userRepository = _userRepository ?? new UserRepository(_context); 
    }
  }
  
  public IUserProfileRepository UserProfileRepository
  {
    get
    {
      return _userProfileRepository = _userProfileRepository ?? new UserProfileRepository(_context);
    }
  }
}
澄清一下,当使用这种方法时,也可能有一个IBusinessLogicContainer。 我们不希望将 BL 和 Repo 对象联接在同一容器中。