Alan
Alan

Reputation: 1637

How do I inject a Dependency Injection seam for an MVC data repository?

I am new to Dependency Injection (DI) and Unit Testing. I successfully followed a tutorial example to create an MVC app that uses DI to loosely couple the code. The primary DI was to create a concrete SQL Repository to be passed in to the Controllers which then pass the Repository to the Domain layer, loosely coupling the Domain layer from the UI/Presentation Layer and from the Data Access layer. That worked OK. It was complicated because it required the following:

'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

The MVC setup also required this:

'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)

All this worked nicely. Now I want to unit test. Using HomeController as a sample piece of potential presentation layer logic, my first obstacle is just to begin I will need to create a New HomeController in the Test Method. But that requires a SQLProductRepository to be passed in that doesn’t actually contact the SQL server, which would be an integration test. But in order to substitute a fake I'll have to create a seam in the code, but I don’t understand how to set that up in this situation. I think it requires a separate DI, but since I’m not that sure-footed with DI just yet, I’m not certain how to do this.

This code is from the Data Access layer. It actually has 2 changes in it that are my first 2 steps toward setting up the DI for the SqlProductRepository. I changed:

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

Finally, this is the Repository I need to work with.

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

From what I have read, I think the next step is to inject the CommerceObjectContext dependency into the code somewhere to create a new seam, but I don’t understand how that is done. It seems more complicated because I’m actually creating the concrete instance in CompositionRoot and that itself is part of a DI.

It may be there are better ways to do DI with an MVC project, but I’m doing this to learn DI, so I would like to know at least how to accomplish the DI to enable the application of a fake in the unit tests.

What are my next steps just to get the production code dependency injected, so I can then create the unit tests correctly? While I might eventually need help with the unit test, I have to get the code ready first.

Upvotes: 0

Views: 134

Answers (1)

NightOwl888
NightOwl888

Reputation: 56909

create a concrete SQL Repository to be passed in to the Controllers which then pass the Repository to the Domain layer, loosely coupling the Domain layer from the UI/Presentation Layer and from the Data Access layer.

Many people struggle with this at first. DI is about injecting object graphs. This means that any dependency your MVC controllers have can in turn be injected by dependencies, which can in turn be injected by dependencies, etc. To create a loosely-coupled application, your controller should know nothing about the dependencies of dependencies, only dependencies of itself. You would not pass a repository to the controller, but pass it only to the services that require it.

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

The magic that happens to make your services injectable, also makes them testable. It is usually a waste of effort to make unit tests use a DI container. Instead, you should new up the dependencies for each test or group of tests.

<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

NOTE: It is also common practice to inject the DI container into the ControllerFactory so it can call container.Resolve(Type). This is OK if the ControllerFactory is part of your composition root (meaning it should live in the MVC project). This allows the ControllerFactory to resolve the requested controller along with its entire dependency graph. There is an example of a ControllerFactory implementation in this article.


NOTE: IProductService is probably too general for a real application. Services that are suffixed with Service or Manager are code smells that indicate violations of the Single Responsibility Principle.

For example, if using CQS, there might just be a service called IQueryHandler<GetFeaturedProducts> that depends on a DBContext or DBContextFactory that only has a single Handle(GetFeaturedProducts) method. In general it is better for maintainability to have a complex network of simple classes than a simple network of complex classes.

Upvotes: 1

Related Questions