Problem statement
I have an application that needs to get files from various sources (disk, embedded, http, ftp, ...) so I use a common API for them:
interface IResourceProvider { }
Then I just provide implementations:
class PhysicalFileProvider : IResourceProvider { } class HttpProvider : IResourceProvider { public HttpProvider(IClient client) { client.Dump(); } }
I register them as Named
services:
builder.RegisterType<PhysicalFileProvider>().Named<IResourceProvider>("App"); builder.RegisterType<HttpProvider>().Named<IResourceProvider>("Remote");
I do this because I want to be able to replace them for testing so I need to know which service I'm replacing. Here, PhysicalFileProvider
gets replaces by a mock.
builder.RegisterType<PhysicalFileProviderMock>().Named<IResourceProvider>("Remote");
All these services are injected into the CompositeProvider
which knows how to find a resource (usually by trying all of them).
class CompositeProvider : IResourceProvider { public CompositeProvider(IEnumerable<IResourceProvider> providers) { } }
This service should not know anything about the names so IIndexed
isn't an option. It gets injected where required and it knows how to determine the correct provider.
Resolving IResourceProvider
In order to inject all Named
services to the CompositeProvider
I do some magic. I search for KeyedService
s where their ServiceType
is IResourceProvider
. Then I group them by name and pick the last one as this one would be registered by the test as these registrations happen last just before I call builder.Build()
.
builder.Register(context =>
{
var services =
from r in context.ComponentRegistry.Registrations
from s in r.Services.OfType<KeyedService>()
where typeof(IResourceProvider).IsAssignableFrom(s.ServiceType)
group (r, s) by s.ServiceKey into g
let last = g.Last()
select last.r.Activator.ActivateInstance(context, Enumerable.Empty<Parameter>());
return new CompositeProvider(services.Cast<IResourceProvider>());
});
Where's the catch?
Usually, you would register services with As<T>
so that it flawlessly works with IEnumerable<T>
but this disables Named
and without additional filtering you would end up with three services injected into CompositeProvider
These extensions seem to be mutually exclusive. That's what the manual Register
and service search is for. It needs to pick the last Named
service from a group of servcies with the same name.
Demo
In order to test this logic I create this demo code. Interface and class names are real. They just don't have bodies here.
void Main()
{
var builder = new ContainerBuilder();
builder.RegisterType<PhysicalFileProvider>().Named<IResourceProvider>("App");
builder.RegisterType<HttpProvider>().Named<IResourceProvider>("Remote");
builder.RegisterType<WebClient>().As<IClient>();
builder.Register(context =>
{
var services =
from r in context.ComponentRegistry.Registrations
from s in r.Services.OfType<KeyedService>()
where typeof(IResourceProvider).IsAssignableFrom(s.ServiceType)
group (r, s) by s.ServiceKey into g
let last = g.Last()
select last.r.Activator.ActivateInstance(context, Enumerable.Empty<Parameter>());
return new CompositeProvider(services.Cast<IResourceProvider>());
});
// Override B for testing
builder.RegisterType<PhysicalFileProviderMock>().Named<IResourceProvider>("Remote");
var container = builder.Build();
var scope = container.BeginLifetimeScope();
scope.ResolveNamed<IResourceProvider>("App").Dump();
scope.ResolveNamed<IResourceProvider>("Remote").Dump();
scope.Resolve<CompositeProvider>().Dump();
scope.Resolve<IClient>().Dump();
}
interface IResourceProvider { }
interface IClient { }
class WebClient : IClient { }
class PhysicalFileProvider : IResourceProvider { }
class HttpProvider : IResourceProvider
{
public HttpProvider(IClient client)
{
client.Dump();
}
}
class PhysicalFileProviderMock : IResourceProvider { }
class CompositeProvider : IResourceProvider
{
public CompositeProvider(IEnumerable<IResourceProvider> providers)
{
providers.Dump();
}
}
Questions
- Would you say this solution is sane?
- Is there a better one?
Remote
so you'll get the latest registration but you'll get all three when you manually search them o_O this is why I need the grouping and last. \$\endgroup\$