Wednesday

18-06-2025 Vol 19

Mastering Dependency Injection: Effective Ways to Inject Dependencies in C#

Mastering Dependency Injection: Effective Ways to Inject Dependencies in C#

Dependency Injection (DI) adalah pola desain yang kuat yang secara signifikan meningkatkan modularitas, testabilitas, dan maintainability kode C#. Dengan memahami dan menerapkan teknik DI yang efektif, pengembang dapat membangun aplikasi yang lebih fleksibel dan mudah dikelola. Artikel ini menyelami dunia DI di C#, menjelajahi berbagai cara untuk menyuntikkan dependensi dan praktik terbaik untuk penerapannya.

Table of Contents

  1. Introduction to Dependency Injection
    • What is Dependency Injection?
    • Why Use Dependency Injection?
    • Benefits of Dependency Injection
  2. Understanding Dependencies
    • What are Dependencies?
    • Tight Coupling vs. Loose Coupling
  3. Dependency Injection Techniques in C#
    • Constructor Injection
    • Property Injection
    • Method Injection
    • Service Locator (Anti-Pattern)
  4. Dependency Injection Containers
    • What are Dependency Injection Containers?
    • Popular .NET DI Containers: Autofac, Ninject, Microsoft.Extensions.DependencyInjection
    • Registering Dependencies
    • Resolving Dependencies
  5. Implementing Dependency Injection with Microsoft.Extensions.DependencyInjection
    • Setting up the Project
    • Registering Services
    • Using the IServiceProvider
    • Service Lifetimes: Transient, Scoped, Singleton
  6. Best Practices for Dependency Injection
    • Favor Constructor Injection
    • Avoid Service Locator
    • Register Dependencies Close to the Root
    • Use Abstractions (Interfaces)
    • Keep Constructors Simple
  7. Advanced Dependency Injection Concepts
    • Named Dependencies
    • Conditional Dependency Registration
    • Asynchronous Dependency Resolution
    • Decorators
  8. Dependency Injection in ASP.NET Core
    • DI in ASP.NET Core Pipeline
    • Using DI with Controllers
    • Using DI with Middleware
    • Using DI with Razor Pages
  9. Testing with Dependency Injection
    • Mocking Dependencies
    • Unit Testing with DI
    • Integration Testing with DI
  10. Common Pitfalls and How to Avoid Them
    • Overusing DI
    • Circular Dependencies
    • DI Container Configuration Complexity
  11. Conclusion

1. Introduction to Dependency Injection

What is Dependency Injection?

Dependency Injection (DI) adalah pola desain di mana dependensi suatu kelas disuplai dari luar, bukan dibuat di dalam kelas itu sendiri. Ini mencapai pemisahan kekhawatiran (Separation of Concerns – SoC) dan memungkinkan kode yang lebih fleksibel dan dapat diuji.

Pada dasarnya, DI adalah proses memberikan dependensi yang dibutuhkan suatu objek, daripada objek itu sendiri membuatnya. Dependensi ini biasanya berupa interface atau class abstract. Dengan memberikan dependensi, kelas menjadi kurang bergantung pada implementasi spesifik dan lebih fokus pada logika intinya.

Why Use Dependency Injection?

DI menawarkan sejumlah manfaat yang signifikan yang berkontribusi pada desain perangkat lunak yang lebih baik:

Benefits of Dependency Injection

  1. Increased Testability: DI mempermudah penggantian dependensi aktual dengan mock atau stub selama pengujian unit. Ini memungkinkan isolasi kode yang sedang diuji dan verifikasi perilaku yang benar.
  2. Reduced Coupling: DI mengurangi coupling antara kelas. Kelas tidak lagi perlu mengetahui detail implementasi dependensinya, hanya interface atau class abstract.
  3. Improved Maintainability: Karena kode lebih modular dan kurang terikat, lebih mudah untuk mengubah dan memelihara. Perubahan pada satu bagian sistem cenderung tidak mempengaruhi bagian lain.
  4. Increased Reusability: Kelas yang dirancang dengan DI lebih mudah digunakan kembali dalam konteks yang berbeda. Dependensi dapat dikonfigurasi secara eksternal, memungkinkan kelas untuk beradaptasi dengan kebutuhan yang berbeda.
  5. Enhanced Readability: DI membuat kode lebih mudah dibaca dan dipahami. Dependensi kelas dinyatakan secara eksplisit, sehingga mudah untuk melihat apa yang dibutuhkan kelas untuk berfungsi.

2. Understanding Dependencies

What are Dependencies?

Dependensi adalah objek yang dibutuhkan oleh kelas untuk berfungsi dengan benar. Misalnya, kelas `UserService` mungkin bergantung pada interface `IUserRepository` untuk mengakses data pengguna.

Sebuah dependensi bisa berupa:

  • Sebuah interface
  • Sebuah class abstract
  • Sebuah class konkret

Namun, praktik terbaik DI adalah mengandalkan abstraksi (interface atau class abstract) daripada implementasi konkret.

Tight Coupling vs. Loose Coupling

Tight coupling terjadi ketika kelas sangat bergantung pada implementasi spesifik dependensinya. Ini membuat kode sulit untuk diuji, diubah, dan digunakan kembali.

Loose coupling terjadi ketika kelas hanya bergantung pada abstraksi (interface atau class abstract) dari dependensinya. Ini meningkatkan fleksibilitas, testabilitas, dan maintainability.

Contoh Tight Coupling:

Tanpa DI:


public class EmailService {
private GmailClient _gmailClient = new GmailClient();

public void SendEmail(string to, string subject, string body) {
_gmailClient.Connect();
_gmailClient.Send(to, subject, body);
_gmailClient.Disconnect();
}
}

Kelas `EmailService` secara langsung membuat instance `GmailClient`. Ini adalah tight coupling. Sulit untuk menguji `EmailService` secara terpisah atau menggunakan klien email lain di masa mendatang.

Contoh Loose Coupling:

Dengan DI:


public interface IEmailClient {
void Connect();
void Send(string to, string subject, string body);
void Disconnect();
}

public class GmailClient : IEmailClient {
public void Connect() { /* ... */ }
public void Send(string to, string subject, string body) { /* ... */ }
public void Disconnect() { /* ... */ }
}

public class EmailService {
private readonly IEmailClient _emailClient;

public EmailService(IEmailClient emailClient) {
_emailClient = emailClient;
}

public void SendEmail(string to, string subject, string body) {
_emailClient.Connect();
_emailClient.Send(to, subject, body);
_emailClient.Disconnect();
}
}

Sekarang, `EmailService` bergantung pada interface `IEmailClient`. Kita dapat dengan mudah menukar `GmailClient` dengan implementasi `IEmailClient` lain, dan pengujian menjadi lebih mudah karena kita dapat menggunakan mock `IEmailClient`.

3. Dependency Injection Techniques in C#

Ada beberapa cara untuk menyuntikkan dependensi di C#:

Constructor Injection

Constructor Injection adalah teknik yang paling umum dan disukai. Dependensi disuntikkan melalui constructor kelas.


public class UserService {
private readonly IUserRepository _userRepository;

public UserService(IUserRepository userRepository) {
_userRepository = userRepository;
}

public User GetUser(int id) {
return _userRepository.GetUserById(id);
}
}

Keuntungan:

  • Membuat dependensi eksplisit dan jelas.
  • Memastikan bahwa dependensi tersedia sebelum kelas digunakan.
  • Memudahkan pengujian karena dependensi dapat disuntikkan saat membuat instance kelas.

Kekurangan:

  • Dapat menghasilkan constructor yang panjang jika kelas memiliki banyak dependensi.

Property Injection

Property Injection terjadi ketika dependensi disuntikkan melalui properti publik kelas.


public class ProductService {
public IProductRepository ProductRepository { get; set; }

public Product GetProduct(int id) {
return ProductRepository.GetProductById(id);
}
}

Keuntungan:

  • Memungkinkan dependensi opsional.
  • Dapat berguna untuk dependensi melingkar.

Kekurangan:

  • Membuat dependensi kurang eksplisit.
  • Memungkinkan kelas dibuat tanpa dependensi yang diperlukan, yang dapat menyebabkan kesalahan runtime.

Method Injection

Method Injection terjadi ketika dependensi disuntikkan melalui metode kelas.


public class OrderService {
private IOrderRepository _orderRepository;

public void SetOrderRepository(IOrderRepository orderRepository) {
_orderRepository = orderRepository;
}

public Order GetOrder(int id) {
return _orderRepository.GetOrderById(id);
}
}

Keuntungan:

  • Memungkinkan dependensi yang berubah seiring waktu.
  • Dapat berguna untuk dependensi opsional yang hanya dibutuhkan dalam kasus tertentu.

Kekurangan:

  • Membuat dependensi kurang eksplisit.
  • Memungkinkan kelas digunakan tanpa dependensi yang diperlukan, yang dapat menyebabkan kesalahan runtime.
  • Dapat membuat kode lebih sulit dibaca dan dipahami.

Service Locator (Anti-Pattern)

Service Locator adalah pola di mana kelas menggunakan locator pusat untuk mendapatkan dependensinya. Meskipun tampak mirip dengan DI, Service Locator dianggap sebagai anti-pattern karena menyembunyikan dependensi kelas dan membuat pengujian lebih sulit.


public class Logger {
public void Log(string message) {
ILoggerService loggerService = ServiceLocator.Instance.GetService<ILoggerService>();
loggerService.Log(message);
}
}

Alasan Service Locator dianggap Anti-Pattern:

  1. Menyembunyikan Dependensi: Tidak seperti Constructor Injection, Service Locator tidak membuat dependensi kelas eksplisit dalam constructornya. Ini menyulitkan untuk melihat dependensi yang dibutuhkan kelas tanpa memeriksa kode internalnya.
  2. Membuat Pengujian Lebih Sulit: Karena kelas menggunakan locator pusat untuk mendapatkan dependensinya, sulit untuk mengganti dependensi aktual dengan mock atau stub selama pengujian.
  3. Melanggar Prinsip Solid: Service Locator melanggar prinsip Dependency Inversion (D dari SOLID), karena kelas bergantung pada abstraksi (Service Locator) dan implementasi (layanan aktual).
  4. Membuat Kode Kurang Modular: Service Locator memperkenalkan ketergantungan global (Service Locator itu sendiri), yang mengurangi modularitas kode.

4. Dependency Injection Containers

What are Dependency Injection Containers?

Dependency Injection Containers (juga dikenal sebagai Inversion of Control (IoC) Containers) adalah kerangka kerja yang mengotomatiskan proses penyuntikan dependensi. Mereka bertanggung jawab untuk membuat instance kelas dan menyuntikkan dependensi yang diperlukan.

Container DI menyediakan cara untuk mengkonfigurasi dependensi aplikasi dan mengelola masa hidup objek.

Popular .NET DI Containers: Autofac, Ninject, Microsoft.Extensions.DependencyInjection

Beberapa container DI populer di .NET meliputi:

  • Autofac: Container DI yang kuat dan fleksibel dengan fitur-fitur canggih seperti interseptor dan modul.
  • Ninject: Container DI ringan dan mudah digunakan dengan sintaks yang bersih.
  • Microsoft.Extensions.DependencyInjection: Container DI bawaan yang disediakan oleh .NET Core dan ASP.NET Core. Ini adalah pilihan yang baik untuk aplikasi yang lebih kecil atau jika Anda tidak membutuhkan fitur-fitur canggih dari container lain.

Registering Dependencies

Mendaftarkan dependensi melibatkan memberitahu container DI tentang interface atau class abstract mana yang harus digunakan untuk implementasi konkret tertentu.

Contoh (Microsoft.Extensions.DependencyInjection):


var serviceCollection = new ServiceCollection();
serviceCollection.AddTransient<IUserRepository, UserRepository>();
serviceCollection.AddScoped<IUserService, UserService>();
serviceCollection.AddSingleton<ILoggerService, LoggerService>();

Dalam contoh ini:

  • AddTransient mendaftarkan `UserRepository` untuk `IUserRepository`. Instance baru `UserRepository` dibuat setiap kali `IUserRepository` diminta.
  • AddScoped mendaftarkan `UserService` untuk `IUserService`. Instance baru `UserService` dibuat sekali per scope (biasanya per request web).
  • AddSingleton mendaftarkan `LoggerService` untuk `ILoggerService`. Hanya satu instance `LoggerService` yang dibuat selama masa pakai aplikasi.

Resolving Dependencies

Resolving dependensi melibatkan meminta container DI untuk membuat instance kelas dan menyuntikkan dependensi yang diperlukan.

Contoh (Microsoft.Extensions.DependencyInjection):


var serviceProvider = serviceCollection.BuildServiceProvider();
var userService = serviceProvider.GetService<IUserService>();

Dalam contoh ini, container DI membuat instance `UserService` dan menyuntikkan instance `UserRepository` yang terdaftar ke dalam constructor `UserService`.

5. Implementing Dependency Injection with Microsoft.Extensions.DependencyInjection

Microsoft.Extensions.DependencyInjection adalah container DI yang kuat dan ringan yang disediakan oleh .NET Core dan ASP.NET Core. Ini adalah pilihan yang baik untuk aplikasi yang lebih kecil atau jika Anda tidak membutuhkan fitur-fitur canggih dari container lain seperti Autofac atau Ninject.

Setting up the Project

Untuk menggunakan Microsoft.Extensions.DependencyInjection, Anda perlu menginstal paket NuGet yang sesuai:


dotnet add package Microsoft.Extensions.DependencyInjection

Registering Services

Untuk mendaftarkan layanan dengan container DI, Anda menggunakan metode ekstensi pada IServiceCollection. Seperti yang ditunjukkan sebelumnya, ada tiga metode ekstensi utama:

  • AddTransient<TService, TImplementation>(): Membuat instance baru layanan setiap kali diminta.
  • AddScoped<TService, TImplementation>(): Membuat instance baru layanan sekali per scope (biasanya per request web).
  • AddSingleton<TService, TImplementation>(): Membuat satu instance layanan selama masa pakai aplikasi.

Anda juga dapat mendaftarkan layanan menggunakan pabrik (factory):


serviceCollection.AddTransient<IConfiguration>(provider => {
var configurationBuilder = new ConfigurationBuilder()
.SetBasePath(Directory.GetCurrentDirectory())
.AddJsonFile("appsettings.json", optional: false, reloadOnChange: true);
return configurationBuilder.Build();
});

Ini memungkinkan Anda untuk mengontrol bagaimana layanan dibuat dan dikonfigurasi.

Using the IServiceProvider

Setelah Anda mendaftarkan layanan, Anda dapat mengambilnya dari IServiceProvider:


var serviceProvider = serviceCollection.BuildServiceProvider();
var userService = serviceProvider.GetService<IUserService>();

Anda juga dapat menggunakan IServiceScope untuk membuat scope baru untuk resolusi layanan:


using (var scope = serviceProvider.CreateScope()) {
var scopedUserService = scope.ServiceProvider.GetService<IUserService>();
// ...
}

Ini berguna untuk mengelola masa pakai layanan yang di-scope (scoped services).

Service Lifetimes: Transient, Scoped, Singleton

Memahami masa pakai layanan sangat penting untuk menggunakan DI dengan benar:

  • Transient: Instance baru layanan dibuat setiap kali diminta. Ini cocok untuk layanan yang ringan dan stateless.
  • Scoped: Instance baru layanan dibuat sekali per scope. Ini cocok untuk layanan yang perlu berbagi state dalam scope tertentu, seperti permintaan web.
  • Singleton: Hanya satu instance layanan yang dibuat selama masa pakai aplikasi. Ini cocok untuk layanan yang mahal untuk dibuat atau perlu berbagi state di seluruh aplikasi.

6. Best Practices for Dependency Injection

Mengikuti praktik terbaik ini akan membantu Anda menulis kode yang lebih bersih, lebih mudah dipelihara, dan lebih mudah diuji:

Favor Constructor Injection

Constructor Injection adalah teknik yang paling direkomendasikan untuk menyuntikkan dependensi. Ini membuat dependensi kelas eksplisit, memastikan bahwa dependensi tersedia sebelum kelas digunakan, dan memudahkan pengujian.

Avoid Service Locator

Seperti yang disebutkan sebelumnya, Service Locator dianggap sebagai anti-pattern karena menyembunyikan dependensi kelas dan membuat pengujian lebih sulit.

Register Dependencies Close to the Root

Daftarkan dependensi sedekat mungkin dengan akar aplikasi. Ini membuat konfigurasi DI lebih terpusat dan mudah dikelola.

Use Abstractions (Interfaces)

Bergantunglah pada abstraksi (interface atau class abstract) daripada implementasi konkret. Ini mengurangi coupling antara kelas dan membuat kode lebih fleksibel.

Keep Constructors Simple

Konstruktor kelas harus sederhana dan hanya bertanggung jawab untuk menerima dependensi. Hindari melakukan logika kompleks dalam konstruktor.

7. Advanced Dependency Injection Concepts

Setelah Anda memahami dasar-dasar DI, Anda dapat menjelajahi konsep-konsep lanjutan ini:

Named Dependencies

Named dependencies memungkinkan Anda untuk mendaftarkan beberapa implementasi dari interface yang sama dan kemudian memilih implementasi yang ingin Anda gunakan berdasarkan nama.

Contoh (Autofac):


builder.RegisterType<SmsNotificationService>().Named<INotificationService>("sms");
builder.RegisterType<EmailNotificationService>().Named<INotificationService>("email");

// Resolve a specific implementation by name
var smsService = container.ResolveNamed<INotificationService>("sms");

Conditional Dependency Registration

Conditional dependency registration memungkinkan Anda untuk mendaftarkan dependensi berdasarkan kondisi tertentu.

Contoh (Microsoft.Extensions.DependencyInjection):


serviceCollection.AddTransient<IDataAccess, SqlDataAccess>();
if (Environment.GetEnvironmentVariable("DB_TYPE") == "Postgres") {
serviceCollection.AddTransient<IDataAccess, PostgresDataAccess>();
}

Asynchronous Dependency Resolution

Beberapa container DI mendukung resolusi dependensi asynchronous. Ini dapat berguna untuk dependensi yang mahal untuk dibuat atau perlu melakukan operasi I/O.

Decorators

Decorators memungkinkan Anda untuk menambahkan perilaku tambahan ke layanan tanpa mengubah implementasi aslinya.

Contoh (Autofac):


public interface IOrderProcessor {
void Process(Order order);
}

public class OrderProcessor : IOrderProcessor {
public void Process(Order order) {
// Actual processing logic
}
}

public class OrderProcessorDecorator : IOrderProcessor {
private readonly IOrderProcessor _decorated;

public OrderProcessorDecorator(IOrderProcessor decorated) {
_decorated = decorated;
}

public void Process(Order order) {
// Add extra behavior before processing
_decorated.Process(order);
// Add extra behavior after processing
}
}

8. Dependency Injection in ASP.NET Core

ASP.NET Core memiliki dukungan bawaan untuk DI. Anda dapat menggunakan container DI bawaan (Microsoft.Extensions.DependencyInjection) atau mengintegrasikan container DI pihak ketiga seperti Autofac atau Ninject.

DI in ASP.NET Core Pipeline

DI digunakan secara luas dalam pipeline ASP.NET Core. Middleware, filter, dan komponen lainnya dapat menyuntikkan dependensi melalui constructor injection.

Using DI with Controllers

Controller dapat menyuntikkan dependensi melalui constructor injection. Ini memungkinkan Anda untuk mengakses layanan dan konfigurasi yang diperlukan dalam controller Anda.


public class ProductsController : ControllerBase {
private readonly IProductService _productService;

public ProductsController(IProductService productService) {
_productService = productService;
}

[HttpGet]
public IActionResult GetProducts() {
var products = _productService.GetProducts();
return Ok(products);
}
}

Using DI with Middleware

Middleware dapat menyuntikkan dependensi melalui constructor injection. Ini memungkinkan Anda untuk mengakses layanan dan konfigurasi yang diperlukan dalam middleware Anda.


public class RequestLoggingMiddleware {
private readonly RequestDelegate _next;
private readonly ILogger<RequestLoggingMiddleware> _logger;

public RequestLoggingMiddleware(RequestDelegate next, ILogger<RequestLoggingMiddleware> logger) {
_next = next;
_logger = logger;
}

public async Task InvokeAsync(HttpContext context) {
_logger.LogInformation($"Request: {context.Request.Path}");
await _next(context);
_logger.LogInformation($"Response: {context.Response.StatusCode}");
}
}

Using DI with Razor Pages

Razor Pages juga mendukung DI. Anda dapat menyuntikkan layanan ke dalam constructor kelas halaman.


public class IndexModel : PageModel {
private readonly IProductService _productService;

public IndexModel(IProductService productService) {
_productService = productService;
}

public List<Product> Products { get; set; }

public void OnGet() {
Products = _productService.GetProducts();
}
}

9. Testing with Dependency Injection

DI membuat pengujian kode Anda jauh lebih mudah karena Anda dapat mengganti dependensi aktual dengan mock atau stub.

Mocking Dependencies

Mocking melibatkan membuat objek palsu yang meniru perilaku dependensi. Ini memungkinkan Anda untuk mengisolasi kode yang sedang diuji dan memverifikasi perilaku yang benar.

Ada beberapa kerangka kerja mocking yang tersedia di .NET, seperti Moq, NSubstitute, dan FakeItEasy.

Contoh (Moq):


var mockUserRepository = new Mock<IUserRepository>();
mockUserRepository.Setup(repo => repo.GetUserById(1)).Returns(new User { Id = 1, Name = "Test User" });

var userService = new UserService(mockUserRepository.Object);

var user = userService.GetUser(1);

Assert.AreEqual("Test User", user.Name);

Unit Testing with DI

Saat menguji unit dengan DI, Anda membuat instance kelas yang sedang diuji dan menyuntikkan dependensi mock atau stub. Kemudian, Anda memverifikasi bahwa kelas berperilaku seperti yang diharapkan.

Integration Testing with DI

Saat melakukan pengujian integrasi dengan DI, Anda menggunakan implementasi nyata dari dependensi (atau versi “in-memory” dari dependensi nyata). Ini memungkinkan Anda untuk menguji bagaimana kelas Anda berinteraksi dengan dependensinya.

10. Common Pitfalls and How to Avoid Them

Berikut adalah beberapa kesalahan umum yang perlu dihindari saat menggunakan DI:

Overusing DI

DI adalah pola yang kuat, tetapi tidak cocok untuk setiap situasi. Hindari menggunakannya untuk dependensi sederhana yang tidak perlu di-mock atau diganti.

Circular Dependencies

Circular dependencies terjadi ketika dua atau lebih kelas bergantung satu sama lain secara langsung atau tidak langsung. Ini dapat menyebabkan kesalahan runtime atau performa yang buruk.

Untuk menghindari circular dependencies, cobalah untuk memecah siklus dependensi dengan memperkenalkan interface atau class abstract perantara.

DI Container Configuration Complexity

Konfigurasi container DI dapat menjadi kompleks, terutama untuk aplikasi yang lebih besar. Pertimbangkan untuk menggunakan modul atau profil konfigurasi untuk menyederhanakan konfigurasi.

Conclusion

Dependency Injection adalah pola desain yang kuat yang dapat meningkatkan modularitas, testabilitas, dan maintainability kode C#. Dengan memahami dan menerapkan teknik DI yang efektif, pengembang dapat membangun aplikasi yang lebih fleksibel dan mudah dikelola. Dengan praktik yang baik, DI menjadi aset yang sangat berharga untuk pengembangan perangkat lunak modern.

“`

omcoding

Leave a Reply

Your email address will not be published. Required fields are marked *