0

I have these classes:

Public Class ProcessRunner
  Implements IProcessRunner

  Public Sub New(Command As ICommand)
  End Sub
End Class

Public Class Command
  Implements ICommand

  Public Sub New(BootStrapper As IBootStrapper)
  End Sub
End Class

Public Class BootStrapper
  Implements IBootStrapper

  Public Sub New(Releases As IReleases)
  End Sub
End Class

Public Class Releases
  Implements IReleases

  Public Sub New(Repository As IRepository)
  End Sub
End Class

Public Class Repository
  Implements IRepository

  Public Sub New(Context As DbContext)
  End Sub
End Class

Public Class MsSqlContext
  Inherits DbContext
End Class

Public Class SqliteContext
  Inherits DbContext
End Class

I'm registering the first ones like so:

Builder.RegisterType(Of ProcessRunner).As(Of IProcessRunner)
Builder.RegisterType(Of BootStrapper).As(Of IBootStrapper)
Builder.RegisterType(Of Releases).As(Of IReleases)
Builder.RegisterType(Of Command).As(Of ICommand)

...and then I'm registering Respository and MsSqlContext/SqliteContext like so:

Dim oSqliteParameter As ResolvedParameter
Dim oSqliteAccessor As Func(Of ParameterInfo, IComponentContext, Object)
Dim oMsSqlParameter As ResolvedParameter
Dim oMsSqlAccessor As Func(Of ParameterInfo, IComponentContext, Object)
Dim oSqlPredicate As Func(Of ParameterInfo, IComponentContext, Boolean)

oSqliteAccessor = Function(Info, Context) Context.ResolveKeyed(Of DbContext)(NameOf(SqliteContext))
oMsSqlAccessor = Function(Info, Context) Context.ResolveKeyed(Of DbContext)(NameOf(MsSqlContext))
oSqlPredicate = Function(Info, Context) Info.ParameterType = GetType(DbContext)

oSqliteParameter = New ResolvedParameter(oSqlPredicate, oSqliteAccessor)
oMsSqlParameter = New ResolvedParameter(oSqlPredicate, oMsSqlAccessor)

With Builder
  .RegisterType(Of Repository(Of Dto.Release, Model.Release)).
    WithParameter(oSqliteParameter).
    Keyed(Of IRepository(Of Dto.Release, Model.Release))(NameOf(SqliteContext)).
    As(Of IRepository(Of Dto.Release, Model.Release))()

  .RegisterType(Of Repository(Of Dto.Release, Model.Release)).
    WithParameter(oMsSqlParameter).
    Keyed(Of IRepository(Of Dto.Release, Model.Release))(NameOf(MsSqlContext)).
    As(Of IRepository(Of Dto.Release, Model.Release))()

  .RegisterType(Of SqliteContext).
    InstancePerLifetimeScope.
    Keyed(Of DbContext)(NameOf(SqliteContext)).
    As(Of DbContext)()

  .RegisterType(Of MsSqlContext).
    InstancePerLifetimeScope.
    Keyed(Of DbContext)(NameOf(MsSqlContext)).
    As(Of DbContext)()
End With

The problem is that when I resolve ProcessRunner, MsSqlContext is always sent to Repository internally, presumably because it comes first alphabetically. And my tests are failing, because I end up testing against the production database.

I found this answer, but it doesn't seem to be applicable. For starters, there's no deep dependency chain like there is here. Must I really register two instances of everything all the way down? Isn't there a cleaner way to pass the context name down through the hierarchy?

I consulted ChatGPT (of course!) but it returned some mangled non-compiling code vaguely resembling an implementation of IRegistrationSource. I looked at the docs for that, but I'm not seeing a good fit. (Correct me if I'm mistaken with that assessment.)

How can I best register and resolve everything in a way that controls which Respository gets sent to the Releases constructor when I resolve ProcessRunner, given that I don't know which Respository to use until resolution time (test vs. prod)?

It'll likely end up being some way of walking NameOf(SqliteContext) or NameOf(MsSqlContext) down the stack, but I'm unsure of how to do that.

(Note: an answer in either VB.NET or C# will be fine.)

--EDIT--

It's a kludge, to be sure, but I was able to at least get it working using a convoluted series of extension methods:

Friend Module Extensions
  <Extension>
  Friend Function ProcessRunner(Of TContext As DbContext)(Instance As ILifetimeScope) As IProcessRunner
    Return Instance.Resolve(Of IProcessRunner)(Instance.CommandParameter(Of TContext))
  End Function

  <Extension>
  Friend Function Command(Of TContext As DbContext)(Instance As ILifetimeScope) As ICommand
    Return Instance.Resolve(Of ICommand)(Instance.BootStrapperParameter(Of TContext))
  End Function

  <Extension>
  Friend Function BootStrapper(Of TContext As DbContext)(Instance As ILifetimeScope) As IBootStrapper
    Return Instance.Resolve(Of IBootStrapper)(Instance.ReleasesParameter(Of TContext))
  End Function

  <Extension>
  Friend Function Releases(Of TContext As DbContext)(Instance As ILifetimeScope) As IReleases
    Return Instance.Resolve(Of IReleases)(Instance.RepositoryParameter(Of TContext))
  End Function

  <Extension>
  Friend Function Repository(Of TContext As DbContext)(Instance As ILifetimeScope) As IRepository
    Return Instance.ResolveKeyed(Of IRepository)(GetType(TContext).Name)
  End Function

  <Extension>
  Private Function RepositoryParameter(Of TContext As DbContext)(Instance As ILifetimeScope) As TypedParameter
    Return TypedParameter.From(Instance.Repository(Of TContext))
  End Function

  <Extension>
  Private Function ReleasesParameter(Of TContext As DbContext)(Instance As ILifetimeScope) As TypedParameter
    Return TypedParameter.From(Instance.Releases(Of TContext))
  End Function

  <Extension>
  Private Function BootStrapperParameter(Of TContext As DbContext)(Instance As ILifetimeScope) As TypedParameter
    Return TypedParameter.From(Instance.BootStrapper(Of TContext))
  End Function

  <Extension>
  Private Function CommandParameter(Of TContext As DbContext)(Instance As ILifetimeScope) As TypedParameter
    Return TypedParameter.From(Instance.Command(Of TContext))
  End Function
End Module

It's called like this:

<Fact>
Sub Process_Runner_Should_Have_Correct_Arguments()
  Dim oProcessRunner As IProcessRunner

  Using oScope As ILifetimeScope = Me.Container.BeginLifetimeScope
    ' Arrange

    ' Act
    oProcessRunner = oScope.ProcessRunner(Of SqliteContext)

    ' Assert
    oProcessRunner.Command.Arguments.Update.Count.Should.Be(4)
    oProcessRunner.Command.Arguments.Clean.Count.Should.Be(6)
  End Using
End Sub

Now tell me if THAT doesn't make you dizzy!

It works, yes, but surely there must be a better way. I'm open to any and all suggestions for improvement.

InteXX
  • 6,135
  • 6
  • 43
  • 80

1 Answers1

0

Yes, Harley, there is a better way. And oh-so-much simpler...

Just add the appropriate IContext to the list of incoming parameters. Like so:

Dim oParameters As List(Of Parameter)
Dim oContext As IContext

If My.Environment = TheOneIWant
  oContext = New SqliteContext(Me.SqliteOptions)
Else
  oContext = New MsSqlContext(Me.MsSqlOptions)
End If

oParameters = New List(Of Parameter) From {
  TypedParameter.From(oContext),
  TypedParameter.From(SomethingElse)
}

Me.Container = GetContainer(oParameters)

Here's the simplified registration:

Public Class DatabaseModule
  Inherits BaseModule

  Public Sub New(Parameters As List(Of Parameter))
    MyBase.New(Parameters)
  End Sub

  Protected Overrides Sub Load(Builder As ContainerBuilder)
    Dim oParameter As TypedParameter

    oParameter = Me.
      Parameters.
      OfType(Of TypedParameter).
      Single(Function(Parameter)
               Return Parameter.Type = GetType(IContext)
             End Function)

    With Builder
      .RegisterType(Of Repository).
        WithParameters(Me.Parameters).
        As(Of IRepository)()

      .Register(Function(Context As IComponentContext) oParameter.Value).
        InstancePerLifetimeScope.
        As(Of IContext)()
    End With
  End Sub
End Class

This way there's only one IContext in the container to resolve for IRepository, which means we can eliminate the extra IRepository as well.

We can also ditch all that extension method cruft. Whew! Good riddance.

Of course this isn't going to fit every scenario, but it does at least work for the present test v. prod setup.

YMMV

InteXX
  • 6,135
  • 6
  • 43
  • 80