3
\$\begingroup\$

In one of my projects I needed a simple dependency incjection activator so I've built one. It performs only type/inheritance matching because I'm not creating a second Autofac and I think I won't need anything more complex any time soon (aka YAGNI).

To find the dependencies I use a Join with a custom comparer. It runs over constructor parameters and tries to match a dependency and maintain the order of the parameters. If the parameters and dependencies counts don't match I use Except to find the missing ones and put their names in the exception message.

I don't use a DI container because I use it for creating commands based on the command line.

public interface IDependencyInjectionActivator
{
    object CreateInstance(Type type, params object[] candidates);
}

public class DependencyInjectionActivator : IDependencyInjectionActivator
{
    private static readonly IEqualityComparer<Type> TypeInheritanceComparer = 
        AdHocEqualityComparer<Type>
            .CreateWithoutHashCode((candidate, parameter) => parameter.IsAssignableFrom(candidate));

    public object CreateInstance(Type type, params object[] candidates)
    {
        var constructor = type.GetConstructors().Single();

        var parameters =
            constructor
                .GetParameters()
                .Join(
                    candidates,
                    parameter => parameter.ParameterType,
                    candidate => candidate.GetType(),
                    (parameter, candidate) => candidate,
                    TypeInheritanceComparer
                ).ToArray();

        var dependenciesResolved = (parameters.Length == constructor.GetParameters().Length);
        if (dependenciesResolved)
        {
            return Activator.CreateInstance(type, parameters);
        }

        var missingDependencies =
            constructor
                .GetParameters()
                .Select(p => p.ParameterType)
                .Except(parameters.Select(p => p.GetType()), TypeInheritanceComparer)
                .ToList();

        var message =
            $"Some dependecies for {type.ToPrettyString().QuoteWith("'")} could not be resolved: " +
            $"{missingDependencies.Select(t => t.ToPrettyString()).Join(", ").EncloseWith("[]")}";
        throw DynamicException.Factory.CreateDynamicException($"MissingDependency{nameof(Exception)}", message, null);
    }
}

The compare I use here takes two Funcs for each interface method. Factory methods make the creation easier.

public class AdHocEqualityComparer<T> : IEqualityComparer<T>
{
    private readonly Func<T, T, bool> _equals;
    private readonly Func<T, int> _getHashCode;

    private AdHocEqualityComparer(Func<T, T, bool> equals, Func<T, int> getHashCode)
    {
        _equals = equals;
        _getHashCode = getHashCode;
    }

    public static IEqualityComparer<T> CreateWithoutHashCode([NotNull] Func<T, T, bool> equals)
    {
        if (equals == null) throw new ArgumentNullException(nameof(@equals));

        return Create(equals, _ => 0);
    }

    public static IEqualityComparer<T> Create([NotNull] Func<T, T, bool> equals, [NotNull] Func<T, int> getHashCode)
    {
        if (equals == null) throw new ArgumentNullException(nameof(equals));
        if (getHashCode == null) throw new ArgumentNullException(nameof(getHashCode));

        return new AdHocEqualityComparer<T>(equals, getHashCode);
    }

    public bool Equals(T x, T y)
    {
        if (ReferenceEquals(null, x)) return false;
        if (ReferenceEquals(null, y)) return false;
        if (ReferenceEquals(x, y)) return true;
        return _equals(x, y);
    }

    public int GetHashCode(T obj) => _getHashCode(obj);
}

This time without examples because I think the usage is strightforward. You create and instance of the activator, specify the type you want to create and pass an array of dependency caditates.

Do you think this DI is smart enough or does it need any improvements?

\$\endgroup\$

2 Answers 2

5
\$\begingroup\$

You are putting pretty strong limitations on classes you can activate this way - a single .ctor, no default .ctor(which can make serialization troubles later) and all .ctor arguments should have different types, otherwise .Join will go crazy. No nested dependencies support, too. No named registrations.

So, all in all, your DI is rather stupid, and if you try to make it smarter, you'll unavoidably end up with "another Autofac".

I went this way once and two months later I wished I hadn't... unless you need to save every byte of RAM, home-made DI surrogate simply doesn't pay off.

\$\endgroup\$
6
  • \$\begingroup\$ You're right, my DI is pretty stupid but it should solve only a single concrete problem, this is command instantiation with no more then just a few dependencies. They are created on demand based on the command line. It's not about saving memory but about simplicity. But I guess I probably should use Autofac for this too and figure out how to use named registrations. \$\endgroup\$
    – t3chb0t
    Commented Nov 5, 2017 at 8:36
  • \$\begingroup\$ That's a usual choice between solving a concrete problem now simply or having to learn something new and (at the first glimpse) hairy. A helpful question is "will this new code and/or knowledge be useful to me 3 years from now?" which is of course YMMV. \$\endgroup\$
    – archnae
    Commented Nov 5, 2017 at 9:05
  • \$\begingroup\$ I first wrote this and then I had my doubts about it and started looking for a feature in Autofac that could use keys. I've just tested it and IIndex<K, V> is exactly what I need ;-) \$\endgroup\$
    – t3chb0t
    Commented Nov 5, 2017 at 10:06
  • 2
    \$\begingroup\$ One can learn to do things well only by doing them badly first :) In fact, trying to implement from scratch something already implemented and then comparing the approaches gives you much better insight into whys and hows than just reading the docs. The only downside is the time spent. \$\endgroup\$
    – archnae
    Commented Nov 5, 2017 at 12:10
  • 1
    \$\begingroup\$ Of course, you are approaching the whole problem backwards. You don't pull the instances when you need them, the DI container should be pushing them to you when the class instance is created... \$\endgroup\$
    – user34073
    Commented Nov 6, 2017 at 17:40
2
\$\begingroup\$

I went down this path a while ago. The problems @Alex raised with sterilization are just the tip. Model Binding and many other things expect a no-param .ctor. It gets even more complicated when you start adding generics. Activator has generic and non-generic implementation that use different approaches. Before I knew it I was modifying .Net libraries to make things work, and knew it was time to stop... But as @Alex pointed out, trying it is a great learning experience.

\$\endgroup\$

Your Answer

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

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