- vừa được xem lúc

Để hiểu rõ bản chất của DI => Tự xây dựng Dependency Injection (DI) từ đầu bằng C#

0 0 4

Người đăng: Anh Duong

Theo Viblo Asia

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ặc Startup).
  • 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ể:

  1. Đăng ký dịch vụ (Register).
  2. Resolve dependency (Resolve).
  3. 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ả:

  • ConsoleLoggerSingleton, chỉ tạo 1 lần.
  • UserRepositoryTransient, tạo mới mỗi lần resolve.
  • UserService tự động được inject ILoggerIUserRepository.

3. Giải thích cách hoạt động

  1. Đăng ký dịch vụ (Register)

    • Lưu ánh xạ Interface → Implementation vào Dictionary.
    • Nếu là Singleton, lưu vào một Dictionary riêng.
  2. 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.
  3. Constructor Injection đệ quy

    • Nếu UserService phụ thuộc vào ILoggerIUserRepository, DI Container sẽ tự động resolve chúng.

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! 🚀

Bình luận

Bài viết tương tự

- vừa được xem lúc

Các loại tham chiếu Nullable trong C# (Phần 1)

1. Giới thiệu. C# 8.0 giới thiệu kiểu tham chiếu nullable và kiểu tham chiếu non-nullable cho phép bạn đưa ra các lựa chọn quan trọng về thuộc tính cho các biến kiểu tham chiếu:.

0 0 66

- vừa được xem lúc

Command pattern qua ví dụ !

Command pattern là gì . Command pattern khá phổ biến trong C #, đặc biệt khi chúng ta muốn trì hoãn hoặc xếp hàng đợi việc thực hiện một yêu cầu hoặc khi chúng ta muốn theo dõi các hoạt động. Hơn nữa, chúng ta có thể hoàn tác tác chúng. .

0 0 205

- vừa được xem lúc

Hiểu Liskov Substitution Principle qua ví dụ !

Liskov Substitution Principle là gì . Nguyên tắc đóng mở xác đinh rằng các instance của lớp con có thể thay thế được instance lớp cha mà vẫn đảm bảo tính đúng đắn của chương trình.

0 0 46

- vừa được xem lúc

Creating custom Controls Wpf

Introduction. Wpf/winforms provides various controls like Buttons, Textbox, TextBlock, Labels etc.

0 0 64

- vừa được xem lúc

[P1] Chọn công nghệ nào để xây dựng website?

Hiện nay nhu cầu phát triển website, app tăng rất cao do xu hướng "số hóa 4.0" trong và ngoài nước.

0 0 94

- vừa được xem lúc

Kiểu dữ liệu trong C#

Dẫn nhập. Ở bài BIẾN TRONG C# chúng ta đã tìm hiểu về biến và có một thành phần không thể thiếu khi khai báo biến – Đó là kiểu dữ liệu.

0 0 47