Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How to deal with run-time parameters when using lifetime scoping?

Warning, long post ahead.

I've been thinking a lot about this lately and I'm struggling to find a satisfying solution here. I will be using C# and autofac for the examples.

The problem

IoC is great for constructing large trees of stateless services. I resolve services and pass the data only to the method calls. Great.

Sometimes, I want to pass a data parameter into the constructor of a service. That's what factories are for. Instead of resolving the service I resolve its factory and call create method with the parameter to get my service. Little more work but OK.

From time to time, I want my services to resolve to the same instance within a certain scope. Autofac provides InstancePerLifeTimeScope() which is very handy. It allows me to always resolve to the same instance within an execution sub-tree. Good.

And there are times when I want to combine both approaches. I want data parameter in constructor and have have the instances scoped. I have not found a satisfying way to accomplish this.

Solutions

1. Initialize method

Instead of passing data into the constructor, just pass it to Initialize method.

Interface:

interface IMyService {     void Initialize(Data data);     void DoStuff(); } 

Class:

class MyService : IMyService {     private Data mData;     public void Initialize(Data data)     {         mData = data;     }      public void DoStuff()     {         //...     } } 

Registration:

builder.RegisterType<MyService>().As<IMyService>().InstancePerLifetimeScope(); 

Usage:

var myService = context.Resolve<IMyService>(); myService.Init(data);  // somewhere else var myService = context.Resolve<IMyService>(); 

After resolving the service for the first time and calling Initialize I can happily resolve within the same context and get the same initialized instance. I don't like the fact that before calling Initialize I have an unusable object. There is a danger that the instance will be resolved and used somewhere else before I call Initialize().

2. Holder pattern

This is a pattern that holds a reference to the data object and instead of injecting the data object itself I inject the holder object.

Interface:

interface IMyService {     void DoStuff(); } 

Class:

class MyService : IMyService {     private Data mData;     public MyService(IDataHolder dataHolder)     {         mData = dataHolder.Data;     }      public void DoStuff()     {         //...     } } 

Registration:

builder.RegisterType<MyService>().As<IMyService>(); builder.RegisterType<DataHolder>().As<IDataHolder>().InstancePerLifetimeScope(); 

Usage:

var holder = context.Resolve<IDataHolder>(); holder.Data = data;  // somewhere else var myService = context.Resolve<IMyService>(); 

This is a little bit better as I moved the responsibility of holding an instance to a different class. I can now use the holder in other services too. Other advantage is that I can hot swap data in holder if necessary. I don't like the fact that it obfuscates the code and adds another interface I have to mock during testing.

3. Let container hold the instance

Interface:

interface IMyService {     void DoStuff(); } 

Class:

class MyService : IMyService {     private Data mData;     public MyService(Data data)     {         mData = dataHolder.Data;     }      public void DoStuff()     {         //...     } } 

Registration:

builder.RegisterType<MyService>().As<IMyService>().InstancePerLifetimeScope(); 

Usage:

var myServiceFactory = context.Resolve<Func<Data, IMyService>>(); myServiceFactory(data);  // somewhere else var myService = context.Resolve<IMyService>(); 

That's right. I don't store the result of a factory call anywhere, because autofac stores it for me. This is pretty surprising to anybody who will read the code. I'm not sure if autofac was even meant to be used like this. Nice thing about this is that I need neither an extra initialize method nor extra class for holding instance.

Question

What is your take on this? How do you handle a situation with run-time data parameters and lifetime scoping? Am I missing a better approach?

like image 946
Kugel Avatar asked Feb 17 '12 19:02

Kugel


2 Answers

Most of the time, runtime data is the non static info you need to pass in any process, like x in a math function, so the easiest way to deal with it is using a parameter in the function:

class MyService : IMyService {     public MyService(){}      public void DoStuff(Data mData)     {         //...     } }  var myService = context.Resolve<IMyService>(); myService.DoStuff(data); 

But, assuming your example is just a example and you are asking because your class need to keep runtime data to run more processes and you don't wanna to pass the same argument in every function:

1.- If you don't loose the scope of the runtime data in every Resolve you can resolve with TypedParameter:

Ej:

//initilization var builder = new ContainerBuilder(); builder.RegisterType<MyService>().As<IMyService>().InstancePerLifetimeScope(); var container = builder.Build();  //any point of your app Data mData = new Data("runtimeData"); // must to be accesible in every place you Resolve  using(var scope = container.BeginLifetimeScope()) {    var service = scope.Resolve<IMyService>(new TypedParameter(typeof(Data), mData)); service.DoStuff(); }  using(var scope = container.BeginLifetimeScope()) {    var service2 = scope.Resolve<IMyService>(new TypedParameter(typeof(Data), mData)); service2.DoStuff(); } 

2.- If you don't have a reference to runtime data in every place you are resolving you can RegisterInstance when and where you create runtime data. Autofac should inyect mData instance thanks to Direct Depency Policy

//initilization var builder = new ContainerBuilder(); builder.RegisterType<MyService>().As<IMyService>().InstancePerLifetimeScope(); var container = builder.Build();  //where you create or modify runtime data. When runtime data changes you have to update the container again. var mData = new Data("runtimeData"); updatedBuilder= new ContainerBuilder(); updatedBuilder.RegisterInstance(mData).As<Data> updatedBuilder.Update(builder);  //in any point of your app using(var scope = updatedBuilder.BeginLifetimeScope())     {        var service = scope.Resolve<IMyService>();     service.DoStuff();     }  //in any other point of your app     using(var scope = updatedBuilder.BeginLifetimeScope())     {        var service2 = scope.Resolve<IMyService>();     service2.DoStuff();     } 
like image 22
jlvaquero Avatar answered Sep 25 '22 08:09

jlvaquero


Autofac now supports this out of the box with an extension to the lifetime scopes. The BeginLifetimeScope() method has an overload that takes an Action<ContainerBuilder> that allows for adding new registrations specific to only that lifetime scope. So for the given example it would look something like:

var builder = new ContainerBuilder(); builder.RegisterType<MyService>().As<IMyService>().InstancePerLifetimeScope(); var container = builder.Build();  using(var scope = container.BeginLifetimeScope(   builder =>   {     builder.RegisterInstance(new Data(....));   })) {   // References to 'IMyService' will always be resolved to the same instance within this lifetime scop   // References to 'Data' will be resolved to the instance registered just for this lifetime scope.   var svc = scope.Resolve<IMyService>(); } 
like image 85
Martijn Evens Avatar answered Sep 23 '22 08:09

Martijn Evens