While learning .NET, I've been using dependency injection all along, but I never had a clear understanding of what an IoC container actually does. I only knew that you register things and then instances magically appear in the constructor—completely unaware of how it's implemented under the hood. So I decided to write the simplest possible IoC container myself, and after finishing it, I definitely gained a lot of insight.
First, figure out what IoC is actually doing
It really comes down to three things:
- You tell the container:
ILoggermaps toConsoleLogger. This is called registration. - You ask the container for an
IUserService. The container sees that its constructor requiresIUserRepositoryandILogger, so it recursively creates all those dependencies, assembles them, and then hands the result to you. This is called resolution. - Some things you want to use as a single global instance (like logging), while others you want to create anew each time. This is called lifecycle management.
Once you understand these three concepts, IoC stops being mysterious.
Container Code
I'll just paste it directly. There's only one class:
using System;
using System.Collections.Generic;
using System.Linq;
public class MyContainer
{
enum Lifetime { Transient, Singleton }
Dictionary<Type, Type> _typeMap = new Dictionary<Type, Type>();
Dictionary<Type, Lifetime> _lifetimeMap = new Dictionary<Type, Lifetime>();
Dictionary<Type, object> _singletonMap = new Dictionary<Type, object>();
public void AddTransient<TService, TImpl>() where TImpl : TService
{
_typeMap[typeof(TService)] = typeof(TImpl);
_lifetimeMap[typeof(TService)] = Lifetime.Transient;
}
public void AddSingleton<TService, TImpl>() where TImpl : TService
{
_typeMap[typeof(TService)] = typeof(TImpl);
_lifetimeMap[typeof(TService)] = Lifetime.Singleton;
}
public T Resolve<T>()
{
return (T)Resolve(typeof(T));
}
object Resolve(Type type)
{
if (!_typeMap.ContainsKey(type))
throw new Exception("没有注册类型: " + type.Name);
var implType = _typeMap[type];
var lifetime = _lifetimeMap[type];
// 单例的话看看有没有已经创建好的
if (lifetime == Lifetime.Singleton)
{
if (_singletonMap.ContainsKey(type))
return _singletonMap[type];
}
// 找构造函数,参数最多的那个
var ctor = implType.GetConstructors()
.OrderByDescending(c => c.GetParameters().Length)
.First();
// 把构造函数的每个参数都递归解析出来
var paramInfos = ctor.GetParameters();
var paramValues = new object[paramInfos.Length];
for (int i = 0; i < paramInfos.Length; i++)
{
paramValues[i] = Resolve(paramInfos[i].ParameterType);
}
// 创建实例
var obj = Activator.CreateInstance(implType, paramValues);
if (lifetime == Lifetime.Singleton)
_singletonMap[type] = obj;
return obj;
}
}
The code is a bit messy. The three Dictionaries are stored separately, but they could actually be combined into a single Tuple or an inner class. I didn't think that deeply at the time, so I left it as is since it works.
Interfaces and Implementation Classes for Testing
A three-tier dependency structure was set up: UserService depends on UserRepository and ILogger, while UserRepository in turn depends on ILogger.
public interface ILogger
{
void Log(string msg);
}
public class ConsoleLogger : ILogger
{
public void Log(string msg)
{
Console.WriteLine("[LOG] " + msg);
}
}
public interface IUserRepository
{
string GetUser(int id);
}
public class UserRepository : IUserRepository
{
ILogger _logger;
public UserRepository(ILogger logger)
{
_logger = logger;
}
public string GetUser(int id)
{
_logger.Log("查询用户 " + id);
return "User_" + id;
}
}
public interface IUserService
{
void PrintUser(int id);
}
public class UserService : IUserService
{
IUserRepository _repo;
ILogger _logger;
public UserService(IUserRepository repo, ILogger logger)
{
_repo = repo;
_logger = logger;
}
public void PrintUser(int id)
{
var user = _repo.GetUser(id);
_logger.Log("找到了: " + user);
}
}
Get It Running
static void Main(string[] args)
{
var container = new MyContainer();
container.AddSingleton<ILogger, ConsoleLogger>();
container.AddTransient<IUserRepository, UserRepository>();
container.AddTransient<IUserService, UserService>();
var service = container.Resolve<IUserService>();
service.PrintUser(1);
// 验证单例
var logger1 = container.Resolve<ILogger>();
var logger2 = container.Resolve<ILogger>();
Console.WriteLine("是同一个实例吗: " + ReferenceEquals(logger1, logger2));
Console.ReadLine();
}
Output:
[LOG] 查询用户 1
[LOG] 找到了: User_1
是同一个实例吗: True
You only Resolved an IUserService, and the container automatically created both UserRepository and ConsoleLogger, filling in the constructor arguments for you. The singleton behavior also works as expected: calling Resolve<ILogger>() twice returns the same object.
What It Actually Does
When Resolve<IUserService>() is called, the container goes through this sequence:
IUserService→ finds the mapping toUserService- Inspects
UserService's constructor, which requiresIUserRepositoryandILogger - Resolves
IUserRepositoryfirst → maps toUserRepository→ its constructor requiresILogger - Resolves
ILogger→ maps toConsoleLogger→ has a parameterless constructor, so it's created directly viaActivator.CreateInstance - Uses the
ConsoleLoggerinstance to createUserRepository - Resolves
ILoggeragain; since it's a singleton, it simply returns the previously createdConsoleLoggerinstance - Uses
UserRepositoryandConsoleLoggerto createUserService
It's essentially a recursive process that continues until it reaches leaf nodes with no dependencies, then builds the objects back up layer by layer. The for loop in the Resolve method that recursively calls itself is the core of the entire IoC container—really, that's all there is to it.
What's Missing from This Demo
Real IoC frameworks, such as Autofac and Microsoft.Extensions.DependencyInjection, do a lot more on top of this foundation:
- Scoped lifetime: For example, sharing the same instance within a single HTTP request and disposing of it when the request ends. In my implementation, I only have Transient and Singleton.
- Thread safety: My dictionaries have no locking whatsoever, so concurrent Resolve calls from multiple threads will cause issues.
- Circular dependency detection: If A depends on B and B depends on A, my code will simply cause a stack overflow, whereas a proper framework would report a clear error.
- IDisposable management: When the container is disposed, it should also dispose of any IDisposable objects it created.
However, as a demo for understanding the principles, it’s sufficient. The skeleton is just this 50-plus lines of code.
Comments (0)
No comments yet. Be the first to leave one!