Search code examples
c#system.reactivesimple-injector

Reactive extensions delayed initialization


It's fairly established that doing work in ctors for types that are resolved using SimpleInjector is bad practice. Although this often leads to certain late initializations of such types, a particularly interesting case is Reactive Extensions subscriptions.

Take for instance an observable sequence that exhibits Replay(1) semantics (actually BehaviorSubject if we take the StartWith into account), e.g.

private readonly IObservable<Value> _myObservable;

public MyType(IService service)
{
    _myObservable = service.OtherObservable
        .StartWith(service.Value)
        .Select(x => SomeTransform())
        .Replay(1)
        .RefCount();
}

public IObservable<Value> MyObservable => _myObservable;

Assume now, that SomeTransform is computationally expensive. From the point of view of SimpleInjector, the above is bad practice. Ok, so we need some kind of Initialize() method to call after SimpleInjector is finished. But what about our replay semantics and our StartWith()? Our consumers expect a value when they Subscribe (assume now that this is guaranteed to happen after initialization)!

How do we get around these restrictions in a nice way while still satisfying SimpleInjector? Here's a summary of requirements:

  1. Don't do extensive work in the ctor (i.e. SomeTransform) should not run
  2. _myObservable should be readonly
  3. MyObservable should exhibit Replay(1) semantics
  4. We should always have an initial value (hence the StartWith)
  5. We do not want to Subscribe inside MyType and cache the value (we like immutability)

I experimented with creating an additional observable that starts with false and then gets set to true on initialize, and then merging that together with _myObservable, but couldn't quite get it to work. Additionally, it doesn't seem like the best solution. In essence, all I want to do is delay until Initialize() is done. There must be some way to do this that I'm not seeing?


Solution

  • Injection constructors should be simple and reliable. This means that the following practices are frowned upon:

    • Doing any I/O operations inside the constructor. I/O operations can fail and make construction of the object graph unreliable.
    • Using the class's dependencies inside the constructor. Not only could a called dependency cause I/O of its own, sometimes injected dependencies are not (yet) fully initialized, and final initialization happens at a later point in time. Perhaps after the object graph has been constructed.

    Considering how Reactive Extensions work, your MyType constructor doesn't seem to do any I/O. Its SomeTransform method is not called during the creation of MyType. Instead, the observable is configured to call SomeTransform when objects are pushed. This means that from a DI perspective, your injection is still 'simple' and fast. Sometimes your classes need some initialization on top of storing incoming dependencies. Creating and storing a Lazy<T>, for instance, is a good example. It allows delaying doing some I/O while still having more code than merely "receiving the dependencies."

    But still you are accessing a dependency inside your constructor, which might cause trouble if that dependency, or its dependencies are not fully initialized. Further more, with Reactive Extensions you make a runtime dependency from IService back to MyType (you already have a design-time dependency from MyType to IService). This is very similar to working with events in .NET. Consequence of this is that it could cause MyType to be kept alive by IService, even when MyType lifetime is expected to be shorter.

    So, strictly spoken, from a DI perspective this configuration might be troublesome. But it's hard to imagine a different model when working with Reactive Extensions. That would mean you have to move this configuration of the observables out of the constructors, and do it after the object graph has been constructed. But that will likely cause having to open up your classes so the Composition Root has access to the methods that need to be called. It also causes Temporal Coupling.

    In other words, when using Reactive Extensions, it is probably good to have some design rules in place to prevent trouble. These rules could be:

    • All exposed IObservable<T> properties should always be fully initialized and usable after its type's construction.
    • All observers and observables should have the same lifetime.