Using IReadonlyDictionary instead of IIndex with Autofac
Dependency injection is awesome. And Autofac is probably the best DI-container out there. It's like magic :) I really like to fiddle around with special registration concepts, like here, here or here. Or down below :)
Keyed service registration and resolving
Autofac has a great set of features called implicit relationship types. One of those is the keyed service lookup, which allows for the registration of services with a service key and then resolving it by the key. Let's say you have the following interfaces and classes:
public interface ITest { }
public class ATest : ITest { }
public class BTest : ITest { }
And then you have another component that depends on ITest
, but you want to decide at runtime, which actual implementation should be used. In this case you can register the two components for the same service with different keys, then use the key to resolve the right one in your dependent component.
ContainerBuilder builder = new ContainerBuilder();
builder.RegisterType<ATest>().As<ITest>().Keyed<ITest>("A");
builder.RegisterType<BTest>().As<ITest>().Keyed<ITest>("B");
var container = builder.Build();
This will register the two components for the same service with a different key. Now if you have a dependent component, let's say Owner
, you have to inject IIndex<TKey,TValue>
into the component. The actual instance of this interface will collect all the registration for TValue
and let you resolve the actual component based on the key used at registration time.
public class Owner
{
private readonly IIndex<string, ITest> testServices;
public Owner(IIndex<string, ITest> testServices)
{
this.testServices = testServices;
}
public void M()
{
if (DateTime.Now.DayOfWeek == DayOfWeek.Sunday)
{
ITest t = this.testServices["A"];
// ...
}
else
{
ITest t = this.testServices["B"];
// ...
}
}
(Of course, you still have to register Owner
to the container and resolve from that).
As awesome as this is, unfortunately this requires you to inject the IIndex
interface into you components, and with that, you create a dependency on Autofac. That might not be that big of issue, but still, the whole point of DI is loosely-coupling. Why ruin that if we can avoid it?
Using a dictionary instead of IIndex
Basically, IIndex<TKey,TValue>
is just a dictionary, so why can't we use the IDictionary<TKey,TValue>
intead. Seems reasonable — or so I thought. But IDictionary<TKey,TValue>
is mutable. How would you define the Add
method in this context? I don't think you can, and neither should you. The dictionary here only serves the purpose of accessing already registered elements, not adding new ones. But there's an immutable dictionary interface, IReadonlyDictionary<TKey,TValue>
, that could better serve our purpose.
IIndex<TKey,TValue>
has an implementation in the Autofac library, that actually handles the resolving. This interface provides an indexer and a TryGetValue method. The same is true for the IReadonlyDictionary<TKey,TValue>
so we can basically lift and shift this implementation to our own implementation:
public class DependencyDictionary<TKey, TValue> : IReadOnlyDictionary<TKey, TValue>
{
private readonly IComponentContext _context;
public DependencyDictionary(IComponentContext context)
{
_context = context ?? throw new ArgumentNullException(nameof(context));
}
private static KeyedService GetService(TKey key) => new KeyedService(key, typeof(TValue));
public TValue this[TKey key] => (TValue)_context.ResolveService(GetService(key));
public bool TryGetValue(TKey key, out TValue value)
{
if (_context.TryResolveService(GetService(key), out object result))
{
value = (TValue)result;
return true;
}
value = default(TValue);
return false;
}
public bool ContainsKey(TKey key) => throw new NotImplementedException();
IEnumerator IEnumerable.GetEnumerator() => throw new NotImplementedException();
public IEnumerator<KeyValuePair<TKey, TValue>> GetEnumerator() => throw new NotImplementedException();
public IEnumerable<TKey> Keys => throw new NotImplementedException();
public IEnumerable<TValue> Values => throw new NotImplementedException();
public int Count => throw new NotImplementedException();
}
Now comes the question: how to define all the other methods defined in the interface? After some thought, here's what I came up with:
- ContainsKey: Should return true if there is a component register with this key, false otherwise (basically, the
IsRegistered
method from Autofac). Note that this shouldn't resolve the service yet, just indicate the fact of being registered. - Keys: Should enumerate all the keys of keyed registration for the service. Again, no actual activation should happen.
- Values: Should enumerate all the services registered. Now this should resolve the services from the container, while of course respecting every rule of lifetime and whatnot.
- GetEnumerator: Again, this should enumerate through the services with their keys and resolve the service.
- Count: This should return the number of keyed service registrations for the service, bt again, not doing any actual activation.
So after all this thinking, here's the actual implementation of the interface:
public class DependencyDictionary<TKey, TValue> : IReadOnlyDictionary<TKey, TValue>
{
private readonly IComponentContext _context;
public DependencyDictionary(IComponentContext context)
{
_context = context ?? throw new ArgumentNullException(nameof(context));
}
private static KeyedService GetService(TKey key) => new KeyedService(key, typeof(TValue));
private IEnumerable<KeyedService> GetServices() =>
_context.ComponentRegistry.Registrations
.SelectMany(r => r.Services)
.OfType<KeyedService>()
.Where(ks => ks.ServiceKey.GetType() == typeof(TKey) && ks.ServiceType == typeof(TValue));
public TValue this[TKey key] => (TValue)_context.ResolveService(GetService(key));
public bool ContainsKey(TKey key) => _context.IsRegisteredWithKey<TValue>(key);
IEnumerator IEnumerable.GetEnumerator() => this.GetEnumerator();
public bool TryGetValue(TKey key, out TValue value)
{
if (_context.TryResolveService(GetService(key), out object result))
{
value = (TValue)result;
return true;
}
value = default(TValue);
return false;
}
public IEnumerator<KeyValuePair<TKey, TValue>> GetEnumerator()
{
foreach (var service in GetServices())
{
yield return new KeyValuePair<TKey, TValue>((TKey)service.ServiceKey, (TValue)_context.ResolveService(GetService((TKey)service.ServiceKey)));
}
}
public IEnumerable<TKey> Keys => GetServices().Select(ks => ks.ServiceKey).Cast<TKey>();
public IEnumerable<TValue> Values => GetServices().Select(ks => (TValue)_context.ResolveService(GetService((TKey)ks.ServiceKey)));
public int Count => GetServices().Count();
}
Now the Owner
can depend on IReadonlyDictionary<TKey,TValue>
:
public class Owner
{
private readonly IReadOnlyDictionary<string, ITest> testServices;
public Owner(IReadOnlyDictionary<string, ITest> testServices)
{
this.testServices = testServices;
}
}
And finally, you have to register this component to the container before building:
ContainerBuilder builder = new ContainerBuilder();
builder.RegisterType<ATest>().As<ITest>().Keyed<ITest>("A");
builder.RegisterType<BTest>().As<ITest>().Keyed<ITest>("B");
builder.RegisterGeneric(typeof(DependencyDictionary<,>)).As(typeof(IReadOnlyDictionary<,>));
builder.RegisterType<Owner>();
var container = builder.Build();
var owner = container.Resolve<Owner>();
Testing the new component:
To validate that the new component actually works, and of course respects Autofac-rules like lifetime, I created this little piece of code:
ContainerBuilder builder = new ContainerBuilder();
builder.RegisterType<ATest>().As<ITest>().Keyed<ITest>("A").SingleInstance().OnActivated(a => Trace.WriteLine("Activated A"));
builder.RegisterType<BTest>().As<ITest>().Keyed<ITest>("B").SingleInstance().OnActivated(a => Trace.WriteLine("Activated B"));
builder.RegisterGeneric(typeof(DependencyDictionary<,>)).As(typeof(IReadOnlyDictionary<,>));
builder.RegisterType<Owner>();
var container = builder.Build();
var owner = container.Resolve<Owner>();
owner.TestResolving();
This basically registers the components and singletons, so if they are resolved multiple times, the same instance should be resolved. Also, to see when it is actually resolved, I added the OnActivated
event handler. And the code for TestResolving
method actually checks that these rules are enforced in when resolving:
public void TestResolving()
{
Trace.WriteLine("Counting...");
var c = testServices.Count;
Trace.WriteLine("Counting done");
Trace.WriteLine("Enumerating keys");
foreach (var item in testServices.Keys)
{
Trace.WriteLine(item);
}
Trace.WriteLine("Enumerating keys done");
Trace.WriteLine("Enumerating dictionary...");
ITest a1 = null;
ITest b1 = null;
int i = 0;
foreach (var item in testServices)
{
if (i == 0) a1 = item.Value;
else b1 = item.Value;
Trace.WriteLine($"{item.Key}: {item.Value}");
i++;
}
Trace.WriteLine("Enumerating dictionary done");
Trace.WriteLine("Resolving by key...");
var a = testServices["A"];
var b = testServices["B"];
Trace.WriteLine(a1 == a);
Trace.WriteLine(b1 == b);
Trace.WriteLine("Resolving by key done");
}
Now if you run this code, you can check for yourself that there are no resolutions when counting or enumerating key, only when enumerating the dictionary itself or accessing it via the indexer. Also, the components really are resolved as singletons, and only created once.
You can use the code for DependencyDictionary<TKey,TValue>
as listed above, so no Github repo this time. Have fun :)