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:
Plugins may use any combination of these.
Sample plugins could be:
What I want to achieve is to
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).
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[]) }; }
}
}
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();
}
}
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With