2

I'd like to know how to register the classes and setup a Simple Injector container to instantiate the classes in the following way. ie go from manual DI to having the below Consumer class have the CompositeService injected and the object graph and lifetimes setup as follows:

To bring some context (if it helps) the Consumer class might be a ViewModel from a WPF application which gets instantiated when the View is requested.

public class Consumer
{
   public Consumer()
   {
      var sharedSvc = new SharedService();
      var productSvc = new ProductService(sharedSvc, new MathHelper());
      var compositeSvc = new CompositeService(sharedSvc, productSvc, new MathHelper());
      compositeSvc.Process();
   }
}

where: MyContext should be shared within the scope of the calls. ProductService and CompositeService can be transient or shared within the scope. MathHelper must be transient.

Q: How can the above be achieved with Simple Injector?

OR more specifically: How can I instantiate multiple MathHelpers within the context of the Simple Injector Scope?

I've read up on http://simpleinjector.readthedocs.org/en/latest/lifetimes.html and read and followed the SO answer https://stackoverflow.com/a/29808487/625113 however, it seems either everything can be transient or scoped but not certain specific objects scoped and the rest transient (which seems odd).

Update 1

The following with Simple Injector will achieve the SharedService result, but if I want ProductService and CompositeService to also have a scoped lifetime it wont work:

 cont.RegisterLifetimeScope<SharedService>();
 cont.Register<MathHelper>();
 cont.Register<ProductService>();
 cont.Register<CompositeService>();

 using (cont.BeginLifetimeScope())
 {
    var compositeSvc = cont.GetInstance<CompositeService>();
    compositeSvc.Process();
 }

If I register either or both of the ProductService or CompositeService as RegisterLifetimeScope I get a Lifetime mismatch exception. ie

 cont.RegisterLifetimeScope<SharedService>();
 cont.Register<MathHelper>();
 cont.RegisterLifetimeScope<ProductService>();
 cont.Register<CompositeService>(); // or cont.RegisterLifetimeScope<CompositeService>();

 using (cont.BeginLifetimeScope())
 {
    var compositeSvc = cont.GetInstance<CompositeService>(); // Exception thrown
    compositeSvc.Process();
 }

Throws an exception leading to this page: https://simpleinjector.readthedocs.org/en/latest/LifestyleMismatches.html

I can under that in relation to Singleton should be dependent on Transient and can infer a sort of understanding that the same could be said in this case that Simple Injector is warning that Scoped can't depend on Transient because Transient isn't managed within the scope.

So my question is more specifically how can I instantiate multiple MathHelpers within the context of the Simple Injector Scope?

Update 2 - Further background and example

Brief background - This situation arose as I have a 4 year old, 2-tier, WPF based application currently using Ninject which has the bloated mixed Service architecture that @Steven describes in his blog series (ie the Services have become a mash of mixed, semi-related, command and queries). Most of these services are a good candidate for separating out into ICommandHandler/IQueryHandler architecture...but you can't do things overnight, so first crack was to convert from Ninject to SimpleInjector (yes I know Ninject can do the same thing in regards to this architecture but there are other reasons for moving to SimpleInjector).

As far as "scoping" the dependency resolution, a "scope" (in this application) is considered to be for the life of a form so one DbContext (like the SharedService in the example above) is shared amoungst the services that the form/viewModel require and MOST of the services are per scope with some injected services or helper classes needing to be injected as Transient.

This (to me) is analogous to Mark Seemann's hand-coded example from http://blog.ploeh.dk/2014/06/03/compile-time-lifetime-matching/ where he has a Per Request (singleton-scoped) service which has Transient objects injected into it.

Edit: I had misread Mark Seemann's example and was reading the code as if the BarController were a service. So whilst the BarController object composition is the same the lifetime is not. That said the SomeThreadUnsafeService could just as easily have a new SomeServiceThatMustBeTransient injected into it but, I stand corrected, his example doesn't do this.

Hence I was wanting to know how to do the object composition Mark Seemann does in Simple Injector but outside the context of web reqeusts (my assumption is that Simple Injector's Per web request scoping is in essence a specific type of Lifetime Scoping).

To address @Steve and @Ric .net's comment and answer, I can see that there is the potential to end up with the scenario where 2 different services use another, shared service that uses a transient object (storing state) and the supposedly transient object becomes a Singleton Scoped object in the context of "some" of those services. eg

public class SingletonScopedService1
{
   private readonly TransientX _transientA;

   public SingletonScopedService1(TransientX transientA)
   {
      _transientA = transientA;
   }

   public void PokeTransient()
   {
      _transientA.Poke();
   }
}

public class SingletonScopedService2 
{
   private readonly SingletonScopedService1 _service1;
   private readonly TransientX _transientB;

   public SingletonScopedService2(SingletonScopedService1 service1, TransientX transientB) 
   {
      _service1 = service1;
      _transientB = transientB;
   }

   public void GoFishing()
   {
      _service1.PokeTransient();
      // This TransientX instance isn't affected
      _transientB.Poke();
   }
}

public class SingletonService3 
{
   private readonly SingletonScopedService1 _service1;

   public SingletonService3(SingletonScopedService1 service1)
   { 
      _service1 = service1; 
   }

   public void DoSomething()
   {
      _service1.PokeTransient();
   }
}

If DoSomething() is called on SingletonScopedService3 and GoFishing() is called on SingletonScopedService2 (and assuming TransientX maintains state) then results "may" be unexpected depending on the purpose of TransientX.

So I'm happy to accept this since the application is operating as expected (but also accept that the current composition is fragile).

With that said, can my original composition or Mark Seemann's example be registered with Simple Injector with the required life-times or is it strictly not possible by design and better to manually compose the object graph (or inject a Func as @Ric .net suggests) for the instances where this is required until further refactoring/hardening can be done?

Update 3 - Conclusion

Whilst Ninject allows you to register my original composition like:

     var kernel = new StandardKernel();
     kernel.Bind<SharedService>().ToSelf().InCallScope();
     kernel.Bind<MathHelper>().ToSelf();
     kernel.Bind<ProductService>().ToSelf().InCallScope();
     kernel.Bind<CompositeService>().ToSelf().InCallScope();

Simple Injector by design does not and I believe my second example is an example as to why.

FYI: In my real-world case, for the few instances that the object graph had this, I've manually constructed the graph (just to get switched to Simple Injector) with the intent on refactoring these potential issues out.

Community
  • 1
  • 1
mips
  • 2,137
  • 2
  • 19
  • 21
  • 1
    Your question raises a big red flag for me. What you are basically saying is that each scoped instance should get its own instance of the MathHelper. This indicates that this MathHelper has mutable state. In general, services should be immutable. Try changing the design of the MathHelper. Once its immutable, your problem will go away because it can be registered as scoped as well. – Steven Mar 18 '16 at 07:37
  • 'where he has a Per Request (singleton-scoped) service which has Transient objects injected into it.' I'm sorry but I read this article many times before and I just read it again. Mark is not injecting transient services into Singletons!! Doing that, even with Pure DI leads to captive dependency. That is also clearly stated in the article! – Ric .Net Mar 22 '16 at 15:08
  • AFAIS the statement "where he has a Per Request (singleton-scoped) service which has Transient objects injected into it" is incorrect. I don't see Mark injecting any transient into a longer lived service, and I can't imagine he doing this, since Mark clearly describes that Captive Dependencies are a problem in a different blog post. – Steven Mar 22 '16 at 15:39
  • @Steven - I was misreading the BarController as a per-request scoped service. I have updated my question accordingly and reached a general conclusion. Please correct me if my conclusions/assumptions are incorrect. – mips Mar 22 '16 at 23:07
  • @Ric .Net - I was misreading the BarController as a per-request scoped service. I have updated my question accordingly and reached a general conclusion. As for the other article about captive dependency the penny wasn't dropping for me about the potential problem they pose until I had worked through a scenario myself. – mips Mar 22 '16 at 23:09
  • If you really want, you can inject transients into scoped snd singletons. SI only makes it harder to shoot yourself in the foot. You can suppress the Lifestyle Mismatch warning on your transient component (or use delegate registrations), as long as you recognize this to be an intermediate solution of course. For suppressing warnings, see: https://simpleinjector.readthedocs.org/en/latest/diagnostics.html#suppressing-warnings – Steven Mar 23 '16 at 02:21

1 Answers1

3

As explained in the linked SO question what the Lifestyle Mismatch exception is basically saying is that you're creating a so called captive dependency when you let a object depend on another object which has a shorter lifestyle.

While it maybe sounds odd to you, a captive dependency is:

  1. A real life problem which, if undetected, would typically lead to very strange bugs which are very hard to debug
  2. A sign, as indicated by Steven, that the service has some kind of state which is never a good sign

If MathHelper has mutable state and therefore needs to be injected in other services as transient, MathHelper has become some sort of 'runtime data'. And injecting runtime data is an anti-pattern.

This blogpost describes in detail what the problem is with runtime data and how to solve this. Solving your problem as described there is the way to go!

Because you did not specify the implementation details of MathHelper, it is hard to give you some advice how you should refactor MathHelper. So the only advice I can give you is, let runtime data or 'state' flow through the system as messages. You can read about message based design here and here.

There are however several other options, which will work but aren't good design IMO. So the advice is not to use these, but to be complete:

Instead of injecting MathHelper as a transient you could inject a MathHelperProvider or even simpler inject a Func<MathHelper> which you could register as singleton:

container.RegisterSingleton<Func<MathHelper>>(() => container.GetInstance<MathHelper>());

Notice that by registering a delegate you will make the container blind. It won't be able to warn you of misconfigurations in this part of the object graph anymore.

The other solutions I had in mind are so ugly in its design, that after writing them, I decided to leave them out of this answer!

If you would add some details about why MathHelper needs to be transient, I could give you some advice where you could make adjustments to make it scoped or even better: singleton.

Community
  • 1
  • 1
Ric .Net
  • 5,540
  • 1
  • 20
  • 39
  • the RegisterSingleton Func doesn't work as Simple Injector still sees this as a lifetime mismatch (ie MathHelper is still considered to be transient). – mips Mar 22 '16 at 23:22
  • Yes, have certainly read up on Steven's articles although sometimes relating simple examples to the real-world blob of code u have in front of you takes some time :). A basic example might be to count how many times critical functions might be called on various services during that scope. That being said, considering the MathHelper (and similarly say a DbContext/repositor) as stateful/runtime data is useful for refactoring them. Thanks. – mips Mar 22 '16 at 23:35
  • @mips Sorry for my (maybe too) late reaction, but registering the factory as Func should not trigger a lifestyle mismatch. I can't reproduce this. If you need any help how to register this, please update your question a ask a new one. – Ric .Net Apr 10 '16 at 13:32