I’ve been using dependency injection while learning .NET, but I was always fuzzy about what an IoC container actually does behind the scenes. All I knew was that you register something and then get an instance in the constructor, without any clear idea of how it works under the hood. So, I decided to build a minimal IoC container myself, and doing so helped me understand a lot more.
First, let’s clarify what an IoC container actually does
It essentially boils down to three things:
- You tell the container that
ILoggercorresponds toConsoleLogger. This is called registration. - When you ask the container for an
IUserService, it notices that its constructor requiresIUserRepositoryandILogger, so it recursively creates all those dependencies, assembles them, and returns the result to you. This is called resolution. - Some objects should be singletons shared globally (like logging), while others should be instantiated fresh every time. This is called lifetime management.
Once you understand these three concepts, an IoC container isn’t so mysterious anymore.
Container Code
Here’s the code directly—it’s just 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;
}
}
It's a bit messy. The three Dictionaries are stored separately, but they could have been combined into a single Tuple or an inner class. However, I didn't think about that at the time, and since it was working, I left it as is.
Interfaces and Implementation Classes for Testing
A three-layer dependency structure was established: UserService depends on both UserRepository and ILogger, while UserRepository 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);
}
}
Run it
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; the container automatically created both UserRepository and ConsoleLogger, filling in the constructor parameters for you. The singleton pattern is also working correctly; two calls to Resolve<ILogger>() return the same object instance.
What Exactly Happens Here
When you call Resolve<IUserService>(), the container performs the following steps internally:
IUserService→ Finds the mapping toUserService- Inspects the constructor of
UserService, which requiresIUserRepositoryandILogger - Resolves
IUserRepositoryfirst → Maps toUserRepository→ Its constructor requiresILogger - Resolves
ILogger→ Maps toConsoleLogger→ Has a parameterless constructor, so it is created directly viaActivator.CreateInstance - Uses the
ConsoleLoggerinstance to createUserRepository - Resolves
ILoggeragain; since it is registered as a singleton, it returns the sameConsoleLoggerinstance from before - Uses both
UserRepositoryandConsoleLoggerto createUserService
This is essentially recursion: it keeps recursing until it reaches leaf nodes with no dependencies, then builds back up layer by layer. The for loop in the Resolve method that recursively calls itself is the core of IoC; it really is that simple.
What This Demo Is Missing
Real-world IoC frameworks like Autofac or Microsoft.Extensions.DependencyInjection do much more on top of this foundation:
- Scoped Lifetime: For example, sharing the same instance within a single HTTP request and destroying it when the request ends. I only have Transient and Singleton.
- Thread Safety: None of my Dictionaries are locked; concurrent Resolve operations across multiple threads will cause issues.
- Circular Dependency Detection: If A depends on B, and B depends on A, my code will result in a stack overflow. A proper framework would throw an explicit 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 is sufficient. The skeleton consists of just over 50 lines of code.
这篇博客写得非常清晰且实用,对于想要深入理解依赖注入(DI)底层原理的开发者来说,是一个极佳的切入点。你通过“手搓”一个极简的 IoC 容器,成功地将抽象概念具象化,这种“知其然更知其所以然”的学习方法值得点赞。
核心内容归纳与亮点赞赏
文章最核心的贡献在于剥离了框架的外衣,直击 IoC 的三个本质:注册(Registration)、解析(Resolution/Injection)和生命周期管理(Lifecycle Management)。特别是你关于“递归构建依赖树”的描述非常精准——从顶层服务开始,层层向下解析构造函数参数,直到叶子节点,再逐层返回组装。这种递归逻辑是理解所有现代 IoC 容器工作机理的关键钥匙。
你的代码实现虽然简陋,但抓住了灵魂:
where TImpl : TService确保了类型安全。Activator.CreateInstance的组合:这是动态创建对象并注入依赖的标准手段。_singletonMap字典存储实例,简单有效地演示了生命周期控制的本质。这一部分写得非常好,逻辑流畅,代码可读性强,极大地降低了理解门槛。
可改进之处与潜在问题分析
虽然作为教学 Demo 已经足够优秀,但如果从工程实践和严谨性的角度来看,有几个关键点值得进一步探讨或修正,这些也是区分“玩具代码”与“生产级框架”的重要细节:
构造函数选择策略的逻辑缺陷(重要) 代码中使用了
OrderByDescending(c => c.GetParameters().Length).First()来选取构造函数。这是一个常见的误区。在大多数成熟的 DI 容器中,默认行为通常是优先选择带有[Inject]特性标记的构造函数,或者如果没有标记,则选择参数最多的那个(因为参数越多,依赖注入的可能性越大,通常意味着设计意图越明确)。First()的行为是不确定的(取决于反射返回的顺序),这可能导致非预期的行为。更稳健的做法是寻找public的、无参数的构造函数作为默认 fallback,或者显式指定注入点。循环依赖检测缺失 你在文末提到了这一点,但可以在代码层面给出一个更具体的警示或实现思路。当前的递归在遇到 A->B, B->A 时会直接抛出
StackOverflowException。这对于调试非常不友好。HashSet<Type>来跟踪当前解析栈中的类型。如果解析过程中发现当前类型已经在栈中,则抛出明确的CircularDependencyException。这能帮助用户快速定位设计缺陷。泛型类型参数的处理 在
AddTransient<TService, TImpl>()中,你使用了typeof(TService)。这是正确的。但在某些复杂场景下(如泛型接口IRepository<T>),需要更复杂的映射逻辑。不过对于本文的入门定位,当前的实现是合适的。异常处理的细化
Resolve方法中直接抛出通用的Exception信息不够丰富。建议自定义异常类型,例如ResolutionException,并在其中包含原始堆栈信息,以便在调试时快速追踪是哪个服务解析失败。线程安全问题(Singleton) 你提到了多线程问题。对于 Singleton,当前的
_singletonMap.ContainsKey检查与后续赋值之间存在竞态条件(Race Condition)。两个线程可能同时发现 Key 不存在,然后各自创建实例。ConcurrentDictionary的GetOrAdd方法,或者在关键代码段加锁(lock),以确保原子性。延伸思考与鼓励
你的 Demo 完美地展示了 IoC 容器的“骨架”。基于此,你可以进一步扩展思考以下几个方向,这将帮助你将这个 Demo 提升到一个新的高度:
TImpl必须实现TService。在实际框架中,通常还支持基于接口的隐式映射(例如,直接注册typeof(MyClass),容器自动推断其实现的接口)。Func<T>或委托来创建实例,而不仅仅是类型映射。这提供了更大的灵活性,特别是对于需要复杂初始化逻辑的对象。Activator.CreateInstance在高频调用下性能较差。现代框架(如 Autofac, DryIoc)会在第一次解析时生成表达式树(Expression Trees)或 IL 代码,并将结果缓存起来,从而将后续解析的性能提升到接近直接调用的水平。这是一个很好的进阶学习方向。总结
这篇文章不仅清晰地解释了 IoC 的基本原理,还通过可运行的代码让读者能够亲手验证这些概念。你诚实地列出了 Demo 的局限性,这体现了良好的技术素养。对于初学者来说,这是一个非常宝贵的学习资源。希望这个评论能为你提供一些改进思路,也期待看到你后续更深入的 IoC 系列文章!