4
\$\begingroup\$

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 KeyedServices 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?
\$\endgroup\$
7
  • \$\begingroup\$ I'm a bit confused, doesn't the latest registration overwrite earlier ones? stackoverflow.com/questions/15754702/… \$\endgroup\$
    – dfhwze
    Jul 11, 2019 at 17:27
  • \$\begingroup\$ @dfhwze yes and no; it does when you resolve 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\$
    – t3chb0t
    Jul 11, 2019 at 17:29
  • \$\begingroup\$ Wouldn't it be better to make an extension method on container that does this for you given any type T ? I think ComponentRegistry is available on the container as well. \$\endgroup\$
    – dfhwze
    Jul 11, 2019 at 17:35
  • \$\begingroup\$ @dfhwze mhmm... usually yes but this kind of tricks are so rare that I actually wasn't going to reuse it anywhere else. \$\endgroup\$
    – t3chb0t
    Jul 11, 2019 at 17:37
  • \$\begingroup\$ The unit test does not care about the name. How will you let it use the mock if scope.Resolve<CompositeProvider>() returns the last registered value for each named key? \$\endgroup\$
    – dfhwze
    Jul 11, 2019 at 17:52

2 Answers 2

2
\$\begingroup\$

Your solution is sane, and it can easily be extended to a generic extension method. Last() is the best you can do. It would have been better if context.ComponentRegistry.Registrations somehow stored some registration meta info like DateTime of registration. There is a meta dictionary foreseen, but it is unclear what is stored inside.

public static class AutofacExtension
{
    public static void RegisterComposite<T, TComposite>(
        this ContainerBuilder builder) where TComposite : class
    {
        if (builder == null) throw new ArgumentNullException(nameof(builder));

        builder.Register(context =>
        {
            var services =
                from r in context.ComponentRegistry.Registrations
                from s in r.Services.OfType<KeyedService>()
                where typeof(T).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 Activator.CreateInstance(
                typeof(TComposite), services.Cast<IResourceProvider>());
        });
    }
}

And registered..

builder.RegisterComposite<IResourceProvider, CompositeProvider>();

Instead of..

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>());
});
\$\endgroup\$
1
\$\begingroup\$

I'm not into that whole brevity thing, so I like longer variable names, even for lambda expressions. Plus I like method chaining over query comprehension syntax. I'm weird like that. So the big wiring up block gets a little bigger .. definitely not better, but I wanted to just show an alternative for funsies.

builder.Register(context => new CompositeProvider(context.ComponentRegistry.Registrations
    .SelectMany(
        registration => registration.Services.OfType<KeyedService>(),
        (registration, service) => (registration, service))
    .Where(registrationService => typeof(IResourceProvider).IsAssignableFrom(registrationService.service.ServiceType))
    .GroupBy(registrationService => registrationService.service.ServiceKey, registrationService => (registrationService.registration, registrationService.service))
    .Select(registrationService => (registrationService, Last: registrationService.Last()))
    .Select(registrationService => registrationService.Last.registration.Activator.ActivateInstance(context, Enumerable.Empty<Parameter>()) as IResourceProvider)));
\$\endgroup\$
2
  • 2
    \$\begingroup\$ I think you lose readability when using long variable names in lambda expressions. Besides that, since we now have ValueTuple, I would also prefer that over an anonymous class, if used as a local intermediate class, again for readability. \$\endgroup\$
    – dfhwze
    Jul 12, 2019 at 4:45
  • 1
    \$\begingroup\$ ValueTuple - excellent call! \$\endgroup\$ Jul 12, 2019 at 14:16

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge that you have read and understand our privacy policy and code of conduct.

Not the answer you're looking for? Browse other questions tagged or ask your own question.