6

I'm in the process of developing an extensible framework using DI and IoC. Users must be able override existing functionality within the framework by dropping their own implementations into the container.

How can I allow users to do this without requiring them to know which IoC container I am using?

My current half-way solution is to structure my assemblies as follows:

1) Define abstract assemblies containing only interfaces.

2) Define concrete assemblies which implement these interfaces. Users may define their own to override existing functionality.

3) Define the container bindings in separate assemblies; i.e. one binding assembly per concrete assembly.

This means the concrete assemblies are not coupled with a particular IoC container, and they would be closed against change if I used a different container. However, users are still required to know which container my framework is using in order to write the binding assemblies, and they would need to release new binding assemblies if I changed the IoC container (i.e. from Ninject to Spring).

Am I missing something?

Lawrence Wagerfield
  • 6,471
  • 5
  • 42
  • 84
  • The problem with this approach is that you are limited by the lowest common denominator of what containers can support, which in many cases is not going to meet your internal needs. I've tried this exact approach and it causes too much confusion for users new to the project. – Chris Patterson Apr 26 '11 at 13:59
  • What do you use instead? – Lawrence Wagerfield Apr 26 '11 at 14:38
  • The framework provides an explicit set of registration points, and manages internal dependencies without a container. The registration points include factory methods (typically Func-based). Then the container extensions (in separate assemblies) can register and provide the factory method for the container. An intermediate model for registration can also be used to transfer metadata from the container to the framework. – Chris Patterson Apr 26 '11 at 17:44

5 Answers5

9

Write loosely coupled code. Applications should depend on containers. Frameworks should not.

Community
  • 1
  • 1
Mark Seemann
  • 225,310
  • 48
  • 427
  • 736
  • Whilst I appreciate your point, I feel it’s unreasonable to ask users to bind everything for themselves in the composition root. Sure, they should define when composition occurs, but isn’t it favorable to define preset bindings inside modules, which the user can then call using their own DI container? What if V2 of my framework requires an additional subsystem-service? Is it fair to ask the user to update their composition root? – Lawrence Wagerfield Apr 26 '11 at 13:41
  • You can provide sensible defaults. There's no reason to force anything upon users, but make extensibility possible. – Mark Seemann Apr 26 '11 at 13:43
  • This leads me back to the original problem: how do I define sensible defaults while remaining container-agnostic? Bindings are defined against a container... – Lawrence Wagerfield Apr 26 '11 at 13:48
  • 1
    Provide default implementations of the interfaces your Fx consumes. That's good Fx design practice in any case. Provide **Seams** for users to override the default behavior. – Mark Seemann Apr 26 '11 at 14:01
  • Okay, just to recap (as I think we might be getting our wires crossed). I'll define IService with default implementation ServiceImpl. _Where_ do I define the default binding from IService->ServiceImpl? And how do I define this binding without going through a container, and hence remaining container-agnostic. – Lawrence Wagerfield Apr 26 '11 at 14:22
  • Take a look at the Facade Fluent Builder in the above link. – Mark Seemann Apr 26 '11 at 15:09
2

Common approach is to abstract container with common service locator

Author of MvcExtensions have abstracted IoC away quite successfully.

Arnis Lapsa
  • 45,880
  • 29
  • 115
  • 195
1

Common Service Locator is one approach, but it only contains methods for resolving, not for registering.

You may want to have a look at how this is implemented in the agatha-rrsl project. There's a more complete explanation here, but in short:

  • define a container-agnostic interface for registering and resolving types
  • provide implementations for the different containers (or let users submit implementations)

Caveat: you probably won't be able to directly use your container of choice in your library.

jeroenh
  • 26,362
  • 10
  • 73
  • 104
  • I've toyed with this idea before; knowing that others are doing it makes it seem more acceptable :) – Lawrence Wagerfield Apr 26 '11 at 12:55
  • Aww... That's not a good solution. I'd automatically dismiss using any Fx that uses the Service Locator anti-pattern: http://blog.ploeh.dk/2010/02/03/ServiceLocatorIsAnAntiPattern.aspx – Mark Seemann Apr 26 '11 at 13:09
  • @Mark Seemann why do you assume this is the case here? Service Locator as an anti-pattern is an orthogonal concern to the problem at hand. Furthermore, Agatha does not endorse/rely/promote Service Locator... – jeroenh Apr 26 '11 at 13:35
  • 2
    You wrote "container-agnostic interface for registering and resolving types". That sounds to me like you'd be pulling instances from the interface based on their type. That's the definition of Service Location: http://blog.ploeh.dk/2010/11/01/PatternRecognitionAbstractFactoryOrServiceLocator.aspx – Mark Seemann Apr 26 '11 at 13:42
  • This question/answer is not at all about Service Location yes or no. We're discussing ways to provide IoC facilities in reusable libraries without tying oneself to a specific IoC container implementation. I really fail to see the link with Service Location (which I agree is an antipattern, but is completely beside the point of the discussion here). – jeroenh Apr 26 '11 at 14:18
  • True, but it makes life easier. And I start to see where our argument is actually about. You're essentially saying: "as a library developer, you shouldn't use a container". While I say: "as a library developer, IF you want to use a container [which the OP asked], here's how you can avoid tying yourself to a specific implementation." I still fail to see where Service Location enters the picture. – jeroenh Apr 26 '11 at 14:25
0

Two simple options would be to either provide your user with some dictionary where they can register type mappings, or alternatively just provide them with a container interface that provides all of the services you think you're likely to require and allow users to supply their own wrapped containers. Apologies if these don't quite fit your scenario, I primarily use Unity, so I don't know if Ninject, etc. do fancy things Unity doesn't.

Moonshield
  • 925
  • 9
  • 16
0

I've solved this by using an attribute and provide a scanner class which is used to locate all implementations and the interfaces that they provide.

public class ComponentAttribute : Attribute
{}

// class that should be registered in the container.
[Component]
public class MyService : IMyService
{}

// Contains information for each class that should be 
// registered in the container.
public interface IContainerMapping
{
    public Type ImplementationType {get;}
    public IEnumerable<T> ImplementedServices {get; }
}

public class ComponentProvider
{
    public static IEnumerable<IContainerMapping> Find() 
    {
        var componentType = typeof(ComponentAttribute);
        foreach (var type in Assembly.GetExecutingAssembly().GetTypes())
        {
           if (type.GetCustomAttributes(componentType, false).Count == 0)
              continue;

           var mapping = new ContainerMapping(type);
           List<Type> interfaces new List<Type>();
           foreach (var interfac in type.GetInterfaces())
           {
             //only get our own interfaces.
             if (interface.Assembly != Assembly.GetExecutingAssembly())
               continue;

             interfaces.Add(interfac);
           }

           mapping.ImplementedServices = interfaces;
           yield return mapping;
        }
    }
}

This solution gives the user much flexibility. He can provide his own solution by using the [Component] attribute directly or by using your solution.

What the user should do is something like:

foreach (var mapping in ComponentProvider.Find())
    myContainer.Register(mapping.ImplementationType).As(mapping.ImplementedServices);

I usually create an all-ready solution by providing a MyProject.autofac project which registers everything in my favorite container.

jgauffin
  • 99,844
  • 45
  • 235
  • 372