Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Dynamic dependency injection

Second approach

I have a family of applications that offer an extensible (i.e. not fixed) set of variables which can be used by various plugins.

Examples are:

  1. A source for log events
  2. A source of computation results
  3. A source for system resource usage
  4. A source for performace indicators
  5. ...

Plugins may use any combination of these.

Sample plugins could be:

  • A custom error logger, using 1.
  • A custom statistic module, using 2.
  • A performace tool using 3. and 4.

What I want to achieve is to

  • present a list of plugins that can be used given a set of variables present in this application (when there is no log event source, you should not be able to select the custom error logger).
  • get a simple and safe to use way to pass the variables to the plugins so that there is no chance for runtime errors due to missing variables.

A bonus would be to allow plugins to optionally require a variable, e.g. a plugin that requires 4. and optionally uses 3. if available (but is also available otherwise).

First approach

I want to implement some kind of "dynamic dependency injection". Let me explain it with a use case.

I'm building a set of libraries that will be used for a family of applications. Each application can provide a different set of variables which are available to certain "handlers" in need of these variables. Depending on the concrete available variables, the number of available handlers must be determined, since handlers can only be used if they have access to all required variables. Also I'm looking for a way to make the invocation as safe as possible. Compile-time will probably not possible, but "check once, never fails afterwards" would be fine.

Below is a first sketch. Everything can still be changed in this stage.

class DynamicDependencyInjectionTest
{
    private ISomeAlwaysPresentClass a;
    private ISomeOptionalClass optionA;
    private ISomeOtherOptionalClass optionB;
    private ISomeMultipleOption[] multi;

    private IDependentFunction dependentFunction;

    void InvokeDependency()
    {
        // the number of available dependencies varies.
        // some could be guaranteed, others are optional, some maybe have several instances
        var availableDependencies = new IDependencyBase[] {a, optionA, optionB}.Concat(multi).ToArray();
        //var availableDependencies = new IDependencyBase[] { a  };
        //var availableDependencies = new IDependencyBase[] { a, optionA }.ToArray();
        //var availableDependencies = new IDependencyBase[] { a, optionB }.ToArray();
        //var availableDependencies = new IDependencyBase[] { a , multi.First() };

        //ToDo
        // this is what I want to do
        // since we checked it before, this must always succeed
        somehowInvoke(dependentFunction, availableDependencies);

    }

    void SetDependentFunction(IDependentFunction dependentFunction)
    {
        if (! WeCanUseThisDependentFunction(dependentFunction))
            throw new ArgumentException();

        this.dependentFunction = dependentFunction;
    }

    private bool WeCanUseThisDependentFunction(IDependentFunction dependentFunction)
    {
        //ToDo
        //check if we can fulfill the requested dependencies
        return true;
    }


    /// <summary>
    /// Provide a list which can be used by the user (e.g. selected from a combobox)
    /// </summary>
    IDependentFunction[] AllDependentFunctionsAvailableForThisApplication()
    {
        IDependentFunction[] allDependentFunctions = GetAllDependentFunctionsViaReflection();
        return allDependentFunctions.Where(WeCanUseThisDependentFunction).ToArray();
    }

    /// <summary>
    /// Returns all possible candidates
    /// </summary>
    private IDependentFunction[] GetAllDependentFunctionsViaReflection()
    {
        var types = Assembly.GetEntryAssembly()
            .GetTypes()
            .Where(t => t.IsClass && typeof (IDependentFunction).IsAssignableFrom(t))
            .ToArray();

        var instances = types.Select(t => Activator.CreateInstance(t) as IDependentFunction).ToArray();
        return instances;
    }


    private void somehowInvoke(IDependentFunction dependentFunction, IDependencyBase[] availableDependencies)
    {
        //ToDo
    }
}

// the interfaces may of course by changed!

/// <summary>
/// Requires a default constructor
/// </summary>
interface IDependentFunction
{
    void Invoke(ISomeAlwaysPresentClass a, IDependencyBase[] dependencies);
    Type[] RequiredDependencies { get; }
}

interface IDependencyBase { }
interface ISomeAlwaysPresentClass : IDependencyBase { }
interface ISomeOptionalClass : IDependencyBase { }
interface ISomeOtherOptionalClass : IDependencyBase { }
interface ISomeMultipleOption : IDependencyBase { }


class BasicDependentFunction : IDependentFunction
{
    public void Invoke(ISomeAlwaysPresentClass a, IDependencyBase[] dependencies)
    {
        ;
    }

    public Type[] RequiredDependencies
    {
        get { return new[] {typeof(ISomeAlwaysPresentClass)}; }
    }
}

class AdvancedDependentFunction : IDependentFunction
{
    public void Invoke(ISomeAlwaysPresentClass a, IDependencyBase[] dependencies)
    {
        ;
    }

    public Type[] RequiredDependencies
    {
        get { return new[] { typeof(ISomeAlwaysPresentClass), typeof(ISomeOptionalClass) }; }
    }
}

class MaximalDependentFunction : IDependentFunction
{
    public void Invoke(ISomeAlwaysPresentClass a, IDependencyBase[] dependencies)
    {
        ;
    }

    public Type[] RequiredDependencies
    {
        // note the array in the type of ISomeMultipleOption[]
        get { return new[] { typeof(ISomeAlwaysPresentClass), typeof(ISomeOptionalClass), typeof(ISomeOtherOptionalClass), typeof(ISomeMultipleOption[]) }; }
    }
}
like image 315
Onur Avatar asked Feb 24 '16 09:02

Onur


1 Answers

Keep it simple. Let plugins rely on Constructor Injection, which has the advantage that constructors statically announce each class' dependencies. Then use Reflection to figure out what you can create.

Assume, for example, that you have three Services:

public interface IFoo { }

public interface IBar { }

public interface IBaz { }

Assume, furthermore, that three plugins exist:

public class Plugin1
{
    public readonly IFoo Foo;

    public Plugin1(IFoo foo)
    {
        this.Foo = foo;
    }
}

public class Plugin2
{
    public readonly IBar Bar;
    public readonly IBaz Baz;

    public Plugin2(IBar bar, IBaz baz)
    {
        this.Bar = bar;
        this.Baz = baz;
    }
}

public class Plugin3
{
    public readonly IBar Bar;
    public readonly IBaz Baz;

    public Plugin3(IBar bar)
    {
        this.Bar = bar;
    }

    public Plugin3(IBar bar, IBaz baz)
    {
        this.Bar = bar; ;
        this.Baz = baz;
    }
}

It's clear that Plugin1 requires IFoo, and Plugin2 requires IBar and IBaz. The third class, Plugin3, is a bit more special, because it has an optional dependency. While it requires IBar, it can also use IBaz if it's available.

You can define a Composer that uses some basic reflection to examine whether or not it'd be possible to create instances of various plugins, based on available services:

public class Composer
{
    public readonly ISet<Type> services;

    public Composer(ISet<Type> services)
    {
        this.services = services;
    }

    public Composer(params Type[] services) :
        this(new HashSet<Type>(services))
    {
    }

    public IEnumerable<Type> GetAvailableClients(params Type[] candidates)
    {
        return candidates.Where(CanCreate);
    }

    private bool CanCreate(Type t)
    {
        return t.GetConstructors().Any(CanCreate);
    }

    private bool CanCreate(ConstructorInfo ctor)
    {
        return ctor.GetParameters().All(p => 
            this.services.Contains(p.ParameterType));
    }
}

As you can see, you configure a Composer instance with a set of available Services, and you can then call the GetAvailableClients method with a list of candidates to get a sequence of available plugins.

You can easily expand the Composer class to also be able to create instances of the desired plugins, instead of only telling you which ones are available.

You may be able to find this feature already available in some DI Containers. IIRC, Castle Windsor exposes a Tester/Doer API, and I wouldn't be surprised if MEF supports such a feature as well.

The following xUnit.net Parametrised Test demonstrates that the above Composer works.

public class Tests
{
    [Theory, ClassData(typeof(TestCases))]
    public void AllServicesAreAvailable(
        Type[] availableServices,
        Type[] expected)
    {
        var composer = new Composer(availableServices);
        var actual = composer.GetAvailableClients(
            typeof(Plugin1), typeof(Plugin2), typeof(Plugin3));
        Assert.True(new HashSet<Type>(expected).SetEquals(actual));
    }
}

internal class TestCases : IEnumerable<Object[]>
{
    public IEnumerator<object[]> GetEnumerator()
    {
        yield return new object[] {
            new[] { typeof(IFoo), typeof(IBar), typeof(IBaz) },
            new[] { typeof(Plugin1), typeof(Plugin2), typeof(Plugin3) }
        };
        yield return new object[] {
            new[] { typeof(IBar), typeof(IBaz) },
            new[] { typeof(Plugin2), typeof(Plugin3) }
        };
        yield return new object[] {
            new[] { typeof(IFoo), typeof(IBaz) },
            new[] { typeof(Plugin1) }
        };
        yield return new object[] {
            new[] { typeof(IFoo), typeof(IBar) },
            new[] { typeof(Plugin1), typeof(Plugin3) }
        };
        yield return new object[] {
            new[] { typeof(IFoo) },
            new[] { typeof(Plugin1) }
        };
        yield return new object[] {
            new[] { typeof(IBar) },
            new[] { typeof(Plugin3) }
        };
        yield return new object[] {
            new[] { typeof(IBaz) },
            new Type[0]
        };
        yield return new object[] {
            new Type[0],
            new Type[0]
        };
    }

    IEnumerator IEnumerable.GetEnumerator()
    {
        return this.GetEnumerator();
    }
}
like image 199
Mark Seemann Avatar answered Oct 02 '22 21:10

Mark Seemann