We have a modular, plug-in based WPF Prism application that uses DryIoc as the IoC container. App.xmal.cs
acts as our main composition root during application startup to bootstrap the IoC container and register services. Due to the decoupled, modular nature of the application, the main application (and therefore the composition root) doesn't have access to these types statically at compile time, rather they are discovered and registered dynamically at runtime.
Reflection-based assembly scanning is used to find all types implementing a custom interface, which are then created and called so that each project can register its own services with the IoC container during application startup (this is distinct from Prism's module and service resolution). The types are provided an IRegistrator
instance to register their services. This approach has worked well for us so far, but now we have a new scenario, and are unsure how to proceed.
Let's simplify and assume we have the main application, App
, and two projects, P1
and P2
. App
does not have a project reference to P1
or P2
, and P1
and P2
don't know about each other either. Using the technique described above, P1
and P2
register their respective services during startup.
Throughout the lifetime of the application, a class in P1
periodically raises a Prism event that P2
subscribes to using Prism's IEventAggregator
. The event payload contains some state that we want to store in P2
until the next event comes along and then we need to update the data. We created a class D
(for data) in P2
to hold on to this data. D
takes a dependency on IEventAggregator
so that it can update the data whenever the aforementioned event is published.
P2
has a generic "lookup service" class, L<T>
that requires an IEnumerable<T>
at construction (which is sourced from pieces of the data stored on D
) to lazily initialize private dictionaries for subsequent lookup operations. L<T>
does not depend on the whole D
class, but rather collection properties and sub-properties of D
, i.e. no more than what it needs. The following pseudo-code mimics the approach of registering services in P2
:
// Instantiated via reflection during app startup.
public class DryIocModule
{
// Called during app startup.
public void RegisterTypes(IRegistrator r)
{
r.Register<D>(Reuse.Singleton);
r.RegisterDelegate(rc => new L<C>(rc.Resolve<D>().A.B.C), Reuse.Singleton);
r.RegisterDelegate(rc => new L<Z>(rc.Resolve<D>().X.Y.Z), Reuse.Singleton);
}
}
There are then various services that depend on an instance of L<T>
to be injected in their constructor, and so on along the dependency graph.
Note that currently these services are registered with singleton lifetime. That's our "go-to" default for service lifetime, but is probably not the best option here (I'll come back to that).
The problem we have is, when the event is raised and the data on D
is updated, all dependent services, like L<T>
are now operating on stale data, and need to be updated themselves. Singleton lifetime sounds like a reasonable choice for D
so that it can subscribe to the event and update its data/properties with the new event payload whenever the event is raised, but singleton does not seem like a good lifetime choice for L<T>
because it will eventually be holding on to stale data the next time the event is raised. On the other hand, transient doesn't seem like a good lifetime choice either, because L<T>
caches some data to optimize performance. We want to hold on to that cached data until the next time the event is raised and we have to re-establish the dictionaries that it contains.
It's like we want to scope the lifetime of L<T>
to when the event is raised, where any requests for this service resolve to the same instance (to take advantage of the caching), but then have a new instance created the next time the event is raised.
Which leads me (finally) to my question... would DryIoc's scoped services be a solution to this problem? If so, how would we practically go about implementing this?
I've read some of the documentation on scoped services, but it's not clear to me what the correct/best approach is to take. Most of the examples I see assume you're in the composition root with access to the DryIoc Container
and everything is known statically at compile time, which we don't have, although we could probably update our DryIocModule
types to take a Container
rather than an IRegistrator
.
Injecting the Container
into D
(where we subscribe to the event) sounds a little like the service locator anti-pattern, and should be avoided, although we wouldn't be resolving services here, but registering them. It still doesn't feel right to be injecting the IoC container around the application though, if that's even what's required for defining scoped services that match the event's lifetime.
So, are DryIoc's scoped services a valid approach to this problem, and if so, how should they be implemented, or am I completely barking up the wrong tree, and there's a better/simpler solution?
Sorry for the long question, but wanted to try and provide as much relevant context as possible. Thanks in advance for any assistance.