如何为MVC数据存储库注入依赖注入接缝?

时间:2017-05-30 16:06:42

标签: vb.net unit-testing dependency-injection asp.net-mvc-5 repository

我是依赖注入(DI)和单元测试的新手。我成功地按照教程示例创建了一个使用DI来松散耦合代码的MVC应用程序。主要的DI是创建一个具体的SQL存储库,传递给控制器​​,然后将存储库传递给域层,松散地将UI层与UI /表示层和数据访问层耦合。这很好用。这很复杂,因为它需要以下内容:

'This custom Controller Factory is used to create Controllers so that required Dependency Injection parameters can be passed in
Public Class CommerceControllerFactory
  Inherits DefaultControllerFactory
  Private ReadOnly controllerMap As Dictionary(Of String, Func(Of RequestContext, IController))

  Public Sub New(repository As DomainLyrDi02Commerce.Domain.ProductRepository)
    If repository Is Nothing Then
      Throw New ArgumentNullException("repository")
    End If
    controllerMap = New Dictionary(Of String, Func(Of RequestContext, IController))()
    controllerMap("Account") = Function(ctx) New AccountController()
    controllerMap("Home") = Function(ctx) New HomeController(repository)
  End Sub

  Public Overrides Function CreateController(requestContext As RequestContext, controllerName As String) As IController
    Return controllerMap(controllerName)(requestContext)
  End Function

  Public Overrides Sub ReleaseController(controller As IController)
  End Sub

  Protected Overrides Function GetControllerInstance(requestContext As RequestContext, controllerType As Type) As IController
    Dim connectionString As String = ConfigurationManager.ConnectionStrings("CommerceObjectDbConnection").ConnectionString
    Dim productRepository = New SqlDataAccessLyrDi02Commerce.DataAccess.SqlProductRepository(connectionString)
    If controllerType = GetType(HomeController) Then
      Return New HomeController(productRepository)
    End If
    Return MyBase.GetControllerInstance(requestContext, controllerType)
  End Function
End Class

MVC设置还要求:

'This is where the concrete SQLProductRepository is instantiated for use in the Controller Factory
Public Class CompositionRoot
  Private ReadOnly m_controllerFactory As IControllerFactory

  Public Sub New()
    m_controllerFactory = CompositionRoot.CreateControllerFactory()
  End Sub

  Public ReadOnly Property ControllerFactory() As IControllerFactory
    Get
      Return m_controllerFactory
    End Get
  End Property

  Private Shared Function CreateControllerFactory() As IControllerFactory
    Dim connectionString As String = ConfigurationManager.ConnectionStrings("CommerceObjectDbConnection").ConnectionString
    Dim productRepositoryType = GetType(DataAccess.SqlProductRepository)
    Dim repository = DirectCast(Activator.CreateInstance(productRepositoryType, connectionString),  DataAccess.SqlProductRepository)

    Dim controllerFactory = New CommerceControllerFactory(repository)
    Return controllerFactory
  End Function
End Class

'The CompositionRoot is called from this code in the global.asax when registering the custom Controller Factory above
Dim root = New CompositionRoot
ControllerBuilder.Current.SetControllerFactory(root.ControllerFactory)

这一切都很顺利。现在我想进行单元测试。使用HomeController作为潜在表示层逻辑的示例,我的第一个障碍就是开始我将需要在测试方法中创建一个新的HomeController。但是这需要传入一个实际上不会联系SQL服务器的SQLProductRepository,这将是一个集成测试。但是为了替换假的我必须在代码中创建一个接缝,但我不明白如何在这种情况下设置它。我认为这需要一个单独的DI,但由于我还没有确定DI,我不确定如何做到这一点。

此代码来自数据访问层。它实际上有2个变化,这是我为SqlProductRepository设置DI的前两个步骤。我改变了:

DbSet to IDbSet
Extracted the Interface ICommerceObjectContext from CommerceObjectContext

Public Interface ICommerceObjectContext
  Property ProductsInSql As IDbSet(Of Product)
End Interface

'This is the class used for the code first EF to SQL connection
Public Class Product
  Public Property ProductId As Integer
  Public Property name As String
  Public Property UnitPrice As Decimal
  Public Property IsFeatured As Boolean
End Class

Public Class CommerceObjectContext
 Inherits DbContext
 Implements ICommerceObjectContext

 Public Sub New()
   MyBase.New("CommerceObjectDbConnection")
 End Sub
 Public Sub New(connectionString As String)
   MyBase.New(connectionString)
 End Sub
 Public Property ProductsInSql As IDbSet(Of Product) Implements ICommerceObjectContext.ProductsInSql
End Class

最后,这是我需要使用的存储库。

Public Class SqlProductRepository
  Inherits Domain.ProductRepository
  Private ReadOnly context As CommerceObjectContext

  Public Sub New(connectionString As String)
    context = New CommerceObjectContext(connectionString)
  End Sub
  Public Overrides Function GetFeaturedProducts() As IEnumerable(Of Domain.Product)
    Dim products = (From p In context.ProductsInSql Where p.IsFeatured Select p).AsEnumerable()
    Return From p In products Select p.ToDomainProduct()
  End Function
End Class

从我读过的内容来看,我认为下一步是将CommerceObjectContext依赖项注入到某个地方的代码中以创建一个新的接缝,但我不明白它是如何完成的。这似乎更复杂,因为我实际上是在CompositionRoot中创建具体实例,而且它本身也是DI的一部分。

可能有更好的方法与MVC项目进行DI,但我这样做是为了学习DI,所以我想知道至少如何完成DI以启用假的应用程序单元测试。

为了获得生产代码依赖项,我接下来的步骤是什么,以便我可以正确地创建单元测试?虽然我最终可能需要单元测试的帮助,但我必须先准备好代码。

1 个答案:

答案 0 :(得分:1)

  

创建一个具体的SQL存储库,传入控制器,然后将存储库传递给域层,松散地将UI层与UI /表示层和数据访问层耦合。

许多人起初都在为此而斗争。 DI是关于注入对象图。这意味着你的MVC控制器所依赖的任何依赖关系都可以依次注入依赖关系,依赖关系也可以依赖注入依赖关系等。要创建一个松散耦合的应用程序,你的控制器应该对依赖关系的依赖性一无所知,只有依赖关系。本身。您不会将存储库传递给控制器​​,而只是将其传递给需要它的服务。

Public Class ProductController
    Inherits Controller
    Private ProductService As IProductService

    Public Sub New(productService As IProductService)
       Me.ProductService = productService
    End Sub

    Public Function Index() As ActionResult
        Dim Model As IEnumerable(Of Domain.Product) = Me.ProductService.GetFeaturedProducts();
        Return View(Model)
    End Function
End Class

Public Class ProductService
    Private ProductRepository As IProductRepository

    ' NOTE: There is some debate whether a repository is worth the effort. 
    ' My view is that you should just make a single generic repository
    ' with a common set of CRUD methods that can manipulate any table (DRY)
    ' and any other query that doesn't conform to this strict model should
    ' be its own separate service.
    Public Sub New(productRepository As IProductRepository) ' Alternative: IRepository(Of Domain.Product)
       Me.ProductRepository = productRepository
    End Sub

    ' Implement service methods that use the ProductRepository and/or DBContext directly
End Class

使您的服务可注入的魔法也使它们可测试。使单元测试使用DI容器通常是浪费精力。相反,您应该为每个测试或一组测试创建依赖项。

<Test> _
Public Sub TestGetFeaturedProducts
    'Arrange
    Dim mockRepository As IProductRepository = New Mock(Of IProductRepository)

    ' mockRepository.SetUp() ' Setup the repository to return fake data

    Dim target As ProductService = New ProductService(mockRepository.Object)

    'Act
    Dim result As IEnumerable(Of Domain.Product) = target.GetFeaturedProducts()

    'Assert
    Assert.AreEqual(3, result.Count())
    Assert.AreEqual("Product1", result.ElementAt(0).Name)

    ' Assert the rest of the data set to ensure it is what was setup in the above mock
End Sub
  

注意:将DI容器注入ControllerFactory也是一种常见做法,因此可以调用container.Resolve(Type)。如果ControllerFactory是组合根的一部分(意味着它应该存在于MVC项目中),这是可以的。这允许ControllerFactory解析请求的控制器及其整个依赖关系图this article中有一个ControllerFactory实施示例。

  

注意IProductService对于实际应用来说可能过于笼统。带有ServiceManager后缀的服务是代码气味,表明违反了单一责任原则。

     

例如,如果使用CQS,可能只有一个名为IQueryHandler<GetFeaturedProducts>的服务取决于只有DBContext方法的DBContextFactoryHandle(GetFeaturedProducts)。一般来说,可维护性比复杂类的简单网络具有复杂的简单类网络更好。