7

I decided to create a very small IoC container in C# for a MonoGame project. The reason i decided to create one myself is to improve the performance and use less libraries that i don't control. Since IoC is something so simple, i don't think a library should handle it.

I started a naive implementation:

var container = new Container();
container.Register("service-alias",
    container => new ServiceClass(container.Resolve("other.dep"));

container.Resolve("service-alias").MethodOnServiceClass()

But I have no idea how to do this in C#'s type system. A Dictionary <string, Func<Container>>? How do I type the return of the resolve method?

Steven
  • 166,672
  • 24
  • 332
  • 435
vinnylinux
  • 7,050
  • 13
  • 61
  • 127
  • 3
    Use generics. Or use any freely available IoC container and don't reinvent the wheel. – Ondrej Tucny Sep 14 '15 at 15:11
  • 3
    As a learning exercise, by all means, write your own IoC container. I wouldn't recommend writing your own for production use. I worked on a project where they did that - it wasn't worth it at all, and was slowly (and expensively, from a manpower point of view) replaced with Autofac. – Chris Mantle Sep 14 '15 at 15:13
  • Thank you for the support, Chris! Regarding Generics, i don't understand how they can help me here. Should the Container be a generic class? But then, i'll have many containers, instead of a singleton. – vinnylinux Sep 14 '15 at 15:16
  • http://ayende.com/blog/2886/building-an-ioc-container-in-15-lines-of-code – Mark Seemann Sep 15 '15 at 07:02
  • possible duplicate of [Code your own IOC Container](http://stackoverflow.com/questions/386535/code-your-own-ioc-container) – Ruben Bartelink Sep 15 '15 at 08:39

3 Answers3

11

Here is an example implementation with 21 lines of code. But please don't be tempted to simplify development by implementing some dictionary that holds the registrations (other than doing so for educational purposes). There are many downsides to hand rolling your own DI library. As explained here, you are much better of by applying Pure DI (which means: DI without DI library) and switching from Pure DI to a DI library later on, in case -and ONLY in case- your Composition Root becomes hard to maintain without.

Steven
  • 166,672
  • 24
  • 332
  • 435
  • I just noticed a problem with that approach: What if i need to register two instances of the same class, but with a different configuration? Like, a FakeMailer and a TrueMailer, same instances of the Mailer class, but with different SMTP config? – vinnylinux Sep 14 '15 at 19:28
  • @vinyllinux - normally, you would want to do that in a test project vs a production project, in which case you would be dealing with 2 different DI containers. However, in the case you want to switch between instances at runtime, you should create an [Abstract Factory](https://sourcemaking.com/design_patterns/abstract_factory) or [Strategy](http://stackoverflow.com/questions/32296209/dependency-injection-unity-conditional-resolving/32415954#32415954) (or other design pattern) inside of your application and leave the DI container out of the equation altogether. – NightOwl888 Sep 14 '15 at 19:46
  • 2
    I hardly ever advice abstract factories, as @nightowl888 advices. I would typically create a proxy or composite `IMailer` implementation that would dispatch to the correct implementation based on the required runtime condition. But only if a runtime switch is required of course. As Nightowl said, your tests use a different container instance (in case of integration tests) or no container at all (with unit tests). – Steven Sep 14 '15 at 20:28
  • Ok, but imagine this: One mailer sends to A and one mailer sends to B. Same interface. But dependency C needs mailer configured to A and dependency D needs mailer configured to B. Do you get what i mean? I may have expressed myself badly before. – vinnylinux Sep 14 '15 at 21:12
  • @vinnylinux: No, I don't get it. But ask yourseld, how would you do this when creating these objects by hand? If in doubt, ask a new question on SO. – Steven Sep 14 '15 at 21:22
  • a = new Mailer([to: bar@bla.com]); b = new Mailer([to: foo@bla.com]); c.setMailer(a), d.setMailer(b) – vinnylinux Sep 14 '15 at 21:50
  • The error you are making is that you initialize your components with runtime data. Runtime data should be passed through method calls on initialized components. Don't inject the mail address into the constructor unless it is fixed during the lifetime of the application. – Steven Sep 14 '15 at 21:53
  • The mail address was just an example. It could be a server, or another dependency altogether. For example: Mailer A needs [formatter: BlahFormatter], Mailer B needs [formatter: BoomFormatter]. – vinnylinux Sep 14 '15 at 22:00
  • Imagine a logger. Same interface, different handlers. mail_logger and fs_logger, same LoggerService with different handlers wired. Some classes will use the fs_logger, other's, the mailer_logger. – vinnylinux Sep 14 '15 at 22:01
  • 1
    The answer stays the same. 1. Don't inject runtime data. 2. Abstract factories can be useful, but there are often better design choices. – Steven Sep 15 '15 at 05:25
2

I hope this fits the definition of small


using System;
using System.Linq;

namespace IOC
{
    /// <summary>
    /// Ioc Container
    /// </summary>
    public class Container
    {
        private readonly System.Collections.Generic.Dictionary<Type, Type> map = new System.Collections.Generic.Dictionary<Type, Type>();
        public string Name { get; private set; }
        public Container(string containerName)
        {
            Name = containerName;
            System.Diagnostics.Trace.TraceInformation("New instance of {0} created", Name);
        }

        /// <summary>
        /// Register the mapping for inversion of control
        /// </summary>
        /// <typeparam name="From">Interface </typeparam>
        /// <typeparam name="To">Insatnce</typeparam>
        public void Register<From,To>()
        {
            try
            {
                map.Add(typeof(From), typeof(To));
                System.Diagnostics.Trace.TraceInformation("Registering {0} for {1}", typeof(From).Name, typeof(To).Name);
            }
            catch(Exception registerException)
            {
                System.Diagnostics.Trace.TraceError("Mapping Exception", registerException);
                throw new IocException("Mapping Exception",registerException);
            }
        }

        /// <summary>
        /// Resolves the Instance 
        /// </summary>
        /// <typeparam name="T">Interface</typeparam>
        /// <returns></returns>
        public T Resolve<T>()
        {
            return (T)Resolve(typeof(T));
        }

        private object Resolve(Type type)
        {
            Type resolvedType = null;
            try
            {
                resolvedType = map[type];
                System.Diagnostics.Trace.TraceInformation("Resolving {0}", type.Name);
            }
            catch(Exception resolveException)
            {
                System.Diagnostics.Trace.TraceError("Could't resolve type", resolveException);
                throw new IocException("Could't resolve type", resolveException);
            }

            var ctor = resolvedType.GetConstructors().First();
            var ctorParameters = ctor.GetParameters();
            if(ctorParameters.Length ==0)
            {
                System.Diagnostics.Trace.TraceInformation("Constructor have no parameters");
                return Activator.CreateInstance(resolvedType);
            }

            var parameters = new System.Collections.Generic.List<object>();
            System.Diagnostics.Trace.TraceInformation("Constructor found to have {0} parameters",ctorParameters.Length);

            foreach (var p in ctorParameters)
            {
                parameters.Add(Resolve(p.ParameterType));
            }

            return ctor.Invoke(parameters.ToArray());
        }
    }
}

Vinod Srivastav
  • 3,644
  • 1
  • 27
  • 40
1

I really recommend a pragmatic approach here.

1) Design an abstraction for "your dream IoC container", with only the bare minimum that you need. Something like this:

public interface IContainer 
{
    void RegisterType<TSource, TDestination>();
    void RegisterType<TSource>(Func<TSource, TDestination> generator);
    T Resolve<T>(); 
}

2) Create an implementation of your abstraction that simply delegates all the functionality to an existing component. I recommend Autofac but there's plenty of fish in the sea.

3) Develop your application using your "wrapper IoC".

4) If at some point you find that the external IoC component has performance issues (or any other type of issues), write another implementation of your IoC abstraction that uses another external component, your own code, or a combination of both. Even if your application is in an advanced state, you just have to change the small piece of code that instantiates the IoC wrapper (perhaps just one line of code).

Advantages of this approach:

  1. You use a mature and well-tested IoC container (if you choose wisely) while hiding its complexity behind a small interface. This helps improve code readability.
  2. You don't fall in the premature optimization trap.
  3. You can entirely switch from one IoC container to another one with very little impact in your existing code (if your abstraction is well designed). This wipes out (or at least minimizes) the "using libraries that I don't control" concern.

Of course, you will have to make the abstraction grow as you need more advanced functionality. But you should always start with a simple abstraction.

At my work place we are using a more elaborate version of this approach and it's working nicely.

Konamiman
  • 49,681
  • 17
  • 108
  • 138
  • 6
    You could also just use [Pure DI](http://blog.ploeh.dk/2014/06/10/pure-di) and skip this extra layer of abstraction entirely. I find this advice neither pragmatic nor good. – Mark Seemann Sep 15 '15 at 07:07
  • -1 For the same reasons as @markseemann + because as you say so yourself, such an abstraction will grow over time which is a clear violation of ISP – Ric .Net Sep 18 '15 at 22:07
  • 1
    I like this approach as it can be used not only for the IOC containers but for any external libraries or packages. It gives you the option to safely replace them in the future and isolate them from your code. – DonO Oct 27 '16 at 19:39