Search code examples
inheritancedependency-injectionabstract-classautofac

Autofac: how to register/resolve a type whose constructor takes an abstract type as its parameter?


I have two concrete classes that inherit from an abstract class:

Public Class UpdateCommand
  Inherits BaseCommand
  Implements IUpdateCommand

  Public Sub New(Root As DirectoryInfo)
    MyBase.New(Root)
  End Sub
End Class

Public Class CleanCommand
  Inherits BaseCommand
  Implements ICleanCommand

  Public Sub New(Root As DirectoryInfo)
    MyBase.New(Root)
  End Sub
End Class

Public MustInherit Class BaseCommand
  Implements IBaseCommand

  Public Sub New(Root As DirectoryInfo)
    Me.Root = Root
  End Sub

  Protected ReadOnly Property Root As DirectoryInfo Implements IBaseCommand.Root
End Class

...and then I have this class next to them:

Public Class Downloader
  Implements IDownloader

  Public Sub New(Command As IBaseCommand)
    Me.Root = Command.Root
  End Sub

  Private ReadOnly Root As DirectoryInfo Implements IDownloader.Root
End Class

The interfaces are constructed similarly:

Public Interface IUpdateCommand
  Inherits IBaseCommand
End Interface

Public Interface ICleanCommand
  Inherits IBaseCommand
End Interface

Public Interface IBaseCommand
  ReadOnly Property Root As DirectoryInfo
End Interface

Public Interface IDownloader
  ReadOnly Property Root As DirectoryInfo
End Interface

My registration goes like so:

oRoot = New DirectoryInfo(Options.Root)
oParameters = New List(Of Parameter) From {
  New NamedParameter(NameOf(Options.Root), oRoot)
}

oBuilder = New ContainerBuilder
oBuilder.RegisterType(Of UpdateCommand).As(Of IUpdateCommand).WithParameters(oParameters)
oBuilder.RegisterType(Of CleanCommand).As(Of ICleanCommand).WithParameters(oParameters)
oBuilder.RegisterType(Of Downloader).As(Of IDownloader)()

...and here's how I'm resolving them:

Dim oUpdateCommand As IUpdateCommand
Dim oCleanCommand As ICleanCommand

Using oScope As ILifetimeScope = Container.BeginLifetimeScope
  oUpdateCommand = oScope.Resolve(Of IUpdateCommand)
  oCleanCommand = oScope.Resolve(Of ICleanCommand)
  oDownloader = oScope.Resolve(Of IDownloader)
End Using

Here's the error I'm getting when I try to resolve oDownloader:

None of the constructors found on type 'VsLayout.Downloader' can be invoked with the available services and parameters: Cannot resolve parameter 'VsLayout.IBaseCommand Command' of constructor 'Void .ctor(VsLayout.IBaseCommand)'.

See https://autofac.rtfd.io/help/no-constructors-bindable for more info.

The documentation doesn't address this scenario, unfortunately.

I tried altering my registration slightly, specifying IBaseCommand instead of IUpdateCommand/ICleanCommand:

oBuilder = New ContainerBuilder
oBuilder.RegisterType(Of UpdateCommand).As(Of IBaseCommand).WithParameters(oParameters)
oBuilder.RegisterType(Of CleanCommand).As(Of IBaseCommand).WithParameters(oParameters)
oBuilder.RegisterType(Of Downloader).As(Of IDownloader)()

...but that results in a different error when resolving:

Autofac.Core.Registration.ComponentNotRegisteredException HResult=0x80131500 Message=The requested service 'VsLayout.IUpdateCommand' has not been registered. To avoid this exception, either register a component to provide the service, check for service registration using IsRegistered(), or use the ResolveOptional() method to resolve an optional dependency.

See https://autofac.rtfd.io/help/service-not-registered for more info.

Again, the docs don't cover this specific scenario.

I then tried shifting the resolution a bit:

Dim oUpdateCommand As IUpdateCommand
Dim oCleanCommand As ICleanCommand
Dim oDownloader As IDownloader

Using oScope As ILifetimeScope = Container.BeginLifetimeScope
  oUpdateCommand = oScope.Resolve(Of IBaseCommand)
  oCleanCommand = oScope.Resolve(Of IBaseCommand)
  oDownloader = oScope.Resolve(Of IDownloader)
End Using

That doesn't work, because an IBaseCommand object can't be cast to an IUpdateCommand. Sure, I could declare oUpdateCommand as an IBaseCommand, and that'd run, but IUpdateCommand is eventually going to have unique properties on it that I'm going to need.

In the end, the Downloader class must take an abstract as its parameter, because both UpdateCommand and CleanCommand are going to use it.

So are we stuck? How do we use Autofac with inheritance like this?


Solution

  • I've found the solution for this, in a great tip in the comments for this answer by @nemesv.

    The working code:

    Imports System
    Imports System.Collections.Generic
    Imports System.IO
    Imports System.Linq
    Imports Autofac
    Imports Autofac.Core
    Imports CommandLine
    Imports CommandLine.Text
    Imports Intexx.Linq
    
    Friend Module Program
      Friend Sub Main(Args As String())
        Dim oParameters As List(Of Parameter)
        Dim oHelpText As HelpText
        Dim oResult As ParserResult(Of Options)
        Dim oParser As Parser
        Dim oRoot As DirectoryInfo
        Dim eYear As Years
    
        oParser = New Parser(Sub(Settings)
                             End Sub)
    
        oResult = oParser.ParseArguments(Of Options)(Args)
    
        oResult.
          WithParsed(Sub(Options)
                       If Enums(Of Years).Descriptions.Contains(Options.Year) Then
                         oRoot = New DirectoryInfo(Options.Root)
                         Enums(Of Years).TryGetValue(Options.Year, eYear, SearchBy.Description)
    
                         oParameters = New List(Of Parameter) From {
                           New NamedParameter(NameOf(Options.Root), oRoot),
                           New NamedParameter(NameOf(Options.Year), eYear),
                           New NamedParameter(NameOf(Options.Edition), Options.Edition)
                         }
    
                         With New ContainerBuilder
                           .RegisterType(Of UpdateCommand).As(Of IBaseCommand).
                           Named(Of IBaseCommand)(NameOf(IUpdateCommand)).
                           WithParameters(oParameters)
    
                           .RegisterType(Of CleanCommand).As(Of IBaseCommand).
                           Named(Of IBaseCommand)(NameOf(ICleanCommand)).
                           WithParameters(oParameters)
    
                           .RegisterType(Of Downloader).As(Of IDownloader)()
    
                           Run(.Build)
                         End With
                       Else
                         oHelpText = HelpText.AutoBuild(oResult,
                                                        Function(HelpText) HelpText,
                                                        Function(Example) Example,
                                                        maxDisplayWidth:=120)
    
                         Console.WriteLine(oHelpText)
                         Environment.Exit(-1)
                       End If
                     End Sub).
          WithNotParsed(Sub(Errors)
    
                        End Sub)
      End Sub
    
    
    
      Friend Sub Run(Container As IContainer)
        Dim oUpdateCommand As IUpdateCommand
        Dim oCleanCommand As ICleanCommand
        Dim oDownloader As IDownloader
    
        Using oScope As ILifetimeScope = Container.BeginLifetimeScope
          oUpdateCommand = oScope.ResolveNamed(Of IBaseCommand)(NameOf(IUpdateCommand))
          oCleanCommand = oScope.ResolveNamed(Of IBaseCommand)(NameOf(ICleanCommand))
          oDownloader = oScope.Resolve(Of IDownloader)
        End Using
      End Sub
    End Module
    

    This has been an adventure!