Tự xây dựng Dependency Injection (DI) từ đầu bằng C#
Để hiểu rõ bản chất của DI, ta có thể tự viết một DI Container đơn giản mà không dùng thư viện có sẵn của ASP.NET Core. Dưới đây là cách triển khai từng bước:
1. Cơ bản về DI thủ công (Manual Dependency Injection)
1.1. Constructor Injection không dùng Container
Trước khi xây dựng DI Container, ta có thể truyền dependency thủ công qua constructor.
Ví dụ:
public interface ILogger
{ void Log(string message);
} public class ConsoleLogger : ILogger
{ public void Log(string message) => Console.WriteLine(message);
} public class UserService
{ private readonly ILogger _logger; // Dependency được truyền vào từ bên ngoài public UserService(ILogger logger) { _logger = logger; } public void RegisterUser(string username) { _logger.Log($"User {username} registered."); }
} // Sử dụng:
ILogger logger = new ConsoleLogger();
var userService = new UserService(logger); // Manual DI
userService.RegisterUser("Alice");
✅ Ưu điểm:
- Không phụ thuộc vào framework.
- Dễ hiểu, dễ kiểm thử.
❌ Nhược điểm:
- Phải tự quản lý dependency ở lớp cao nhất (thường là
Main()
hoặcStartup
). - Khó scale khi ứng dụng phức tạp.
2. Xây dựng DI Container đơn giản
Ta sẽ tạo một DI Container có thể:
- Đăng ký dịch vụ (
Register
). - Resolve dependency (
Resolve
). - Hỗ trợ Singleton & Transient.
2.1. Triển khai DI Container
using System;
using System.Collections.Generic; public class DIContainer
{ private readonly Dictionary<Type, Type> _registrations = new(); private readonly Dictionary<Type, object> _singletons = new(); // Đăng ký dịch vụ public void Register<TInterface, TImplementation>(bool isSingleton = false) where TImplementation : TInterface { _registrations[typeof(TInterface)] = typeof(TImplementation); if (isSingleton) { _singletons[typeof(TInterface)] = null!; // Khởi tạo sau } } // Resolve dependency public TInterface Resolve<TInterface>() { return (TInterface)Resolve(typeof(TInterface)); } private object Resolve(Type serviceType) { // Kiểm tra nếu là Singleton đã tồn tại if (_singletons.TryGetValue(serviceType, out var singleton) && singleton != null) { return singleton; } // Lấy kiểu implementation if (!_registrations.TryGetValue(serviceType, out var implementationType)) { throw new InvalidOperationException($"Service {serviceType.Name} chưa được đăng ký."); } // Tạo instance (dùng Constructor Injection đệ quy) var constructor = implementationType.GetConstructors()[0]; var parameters = constructor.GetParameters(); var dependencies = new object[parameters.Length]; for (int i = 0; i < parameters.Length; i++) { dependencies[i] = Resolve(parameters[i].ParameterType); } var instance = Activator.CreateInstance(implementationType, dependencies); // Lưu Singleton nếu cần if (_singletons.ContainsKey(serviceType)) { _singletons[serviceType] = instance; } return instance; }
}
2.2. Sử dụng DI Container tự viết
// Đăng ký dependencies
var container = new DIContainer();
container.Register<ILogger, ConsoleLogger>(isSingleton: true); // Singleton
container.Register<IUserRepository, UserRepository>(); // Transient
container.Register<UserService, UserService>(); // Resolve và sử dụng
var userService = container.Resolve<UserService>();
userService.RegisterUser("Bob");
✅ Kết quả:
ConsoleLogger
là Singleton, chỉ tạo 1 lần.UserRepository
là Transient, tạo mới mỗi lần resolve.UserService
tự động được injectILogger
vàIUserRepository
.
3. Giải thích cách hoạt động
-
Đăng ký dịch vụ (
Register
)- Lưu ánh xạ
Interface → Implementation
vàoDictionary
. - Nếu là Singleton, lưu vào một
Dictionary
riêng.
- Lưu ánh xạ
-
Resolve dependency (
Resolve
)- Kiểm tra nếu là Singleton đã tồn tại → trả về instance cũ.
- Nếu không, tìm constructor và đệ quy resolve từng dependency.
- Dùng
Activator.CreateInstance
để tạo đối tượng.
-
Constructor Injection đệ quy
- Nếu
UserService
phụ thuộc vàoILogger
vàIUserRepository
, DI Container sẽ tự động resolve chúng.
- Nếu
4. Ưu & Nhược điểm của DI Container tự viết
Ưu điểm
- Hiểu rõ cách DI hoạt động bên trong.
- Không phụ thuộc vào thư viện.
- Có thể tùy chỉnh (ví dụ: thêm vòng đời Scoped).
Nhược điểm
- Không hỗ trợ Scope (như
AddScoped
trong ASP.NET Core). - Không hỗ trợ Factory Pattern (như
Func<T>
,Lazy<T>
). - Không tự động dispose (nếu service implement
IDisposable
).
5. Mở rộng: Thêm Scoped Lifetime
Nếu muốn hỗ trợ Scoped (1 instance mỗi scope), ta có thể cải tiến DIContainer
:
public class DIContainer : IDisposable
{ private readonly Dictionary<Type, Func<object>> _transientRegistrations = new(); private readonly Dictionary<Type, object> _singletons = new(); private readonly Dictionary<Type, object> _scopedInstances = new(); public void Register<TInterface, TImplementation>(ServiceLifetime lifetime = ServiceLifetime.Transient) where TImplementation : TInterface { Func<object> factory = () => CreateInstance(typeof(TImplementation)); switch (lifetime) { case ServiceLifetime.Singleton: _singletons[typeof(TInterface)] = factory(); break; case ServiceLifetime.Scoped: _transientRegistrations[typeof(TInterface)] = factory; break; default: _transientRegistrations[typeof(TInterface)] = factory; break; } } public T Resolve<T>() => (T)Resolve(typeof(T)); private object Resolve(Type serviceType) { if (_singletons.TryGetValue(serviceType, out var singleton)) return singleton; if (_transientRegistrations.TryGetValue(serviceType, out var factory)) return factory(); throw new InvalidOperationException($"Service {serviceType.Name} chưa được đăng ký."); } private object CreateInstance(Type implementationType) { var ctor = implementationType.GetConstructors()[0]; var parameters = ctor.GetParameters(); var dependencies = parameters.Select(p => Resolve(p.ParameterType)).ToArray(); return Activator.CreateInstance(implementationType, dependencies); } public void Dispose() { foreach (var disposable in _scopedInstances.Values.OfType<IDisposable>()) { disposable.Dispose(); } _scopedInstances.Clear(); }
} public enum ServiceLifetime
{ Transient, Scoped, Singleton
}
Kết luận
- DI thủ công giúp hiểu rõ cách DI hoạt động.
- Tự viết DI Container giúp kiểm soát tốt hơn, nhưng không mạnh bằng các thư viện như ASP.NET Core DI.
- Nên dùng DI Container có sẵn trong dự án thực tế để tiết kiệm thời gian và tránh bug.
Bạn có thể mở rộng DI Container này bằng cách:
- Thêm Property Injection.
- Hỗ trợ Interception (AOP).
- Tích hợp Auto-Registration (quét assembly).
Hy vọng qua ví dụ này, bạn đã hiểu sâu hơn về bản chất của Dependency Injection! 🚀