Struggling with Tight Coupling? Use Dependency Injection in .NET

Hey there, fellow developer! We recently talked about Dependency Injection (DI) in .NET. We covered the basics, the three main service lifetimes, and how to use them. Now, let’s talk about how DI can help with tight coupling.

Why Tight Coupling Is a Problem

Tight coupling happens when classes in your app rely too much on each other. This makes your code hard to keep up, test and grow. It’s like being stuck in a web, pull one thread, and everything shifts.

I remember a project where we had to change the payment module. The code was tightly coupled, making updates a challenge. Every change felt like a game of Jenga, where one wrong move could crash the system.

How Dependency Injection Rescues Us

Dependency Injection (DI) lets us add dependencies from outside, not hardcode them. This makes our code more modular, testable, and easy to maintain. In .NET, we use the IoC container to register services and set their lifetimes.

The three Lifetime Services recap:

  1. Transient: Creates a new instance every time it’s needed. Good for lightweight, stateless services.
  2. Scoped: Has one instance per HTTP request. Perfect for services that keep data in a request.
  3. Singleton: Has one instance for the whole app’s life. Best for shared, long-lived data.

Real-World Example: Payment Service

In my payment project, I used DI to separate the payment service from the main app logic. Here’s a quick look:

C#
// Registering services in Startup.cs or Program.cs
services.AddTransient<IPaymentService, PaymentService>();
services.AddScoped<ITransactionLogger, TransactionLogger>();
services.AddSingleton<ICacheService, MemoryCacheService>();

When the app starts, the DI container creates these services. It uses the lifetime to decide whether to create a new instance, reuse within a request, or keep one instance for the app.

Deep Dive into Each Service Lifetime

Transient

The Transient lifetime is great for short, stateless tasks. For example, it’s perfect for tasks that need to be independent. A classic example is a utility class that formats data or does calculations.

Scoped

Scoped lifetime is best for services that need to keep data consistent in a single request. For instance, in a web app, it’s useful for services that track user data in a request. This ensures data consistency within a request.

Singleton

Singleton lifetime is good for sharing resources across the app. A good example is caching data. In one project, I used a Singleton cache service to reduce database calls for often accessed data.

When to Avoid Certain Lifetimes

Choosing the wrong lifetime can cause big problems. For example, using Singleton for a database context can cause concurrency issues. Similarly, using Transient for a service that needs to keep data across calls will lead to data loss.

Testability with DI

DI is greate for unit testing. You can swap out real dependencies with mocks or stubs. For instance:

C#
var mockLogger = new Mock<ITransactionLogger>();
var orderService = new OrderService(new MockPaymentService(), mockLogger.Object);

This makes it easy to test the OrderService without real implementations.

Troubleshooting DI Issues

DI can sometimes be tricky. You might run into:

  • Service Not Registered: Make sure all your dependencies are in the DI container.
  • Lifetime Mismatch: Watch out when using Scoped services in Singleton services. It can cause problems.

Final Thoughts

DI makes your .NET apps more flexible and easier to test. It’s a small change that makes a big difference as your project grows.

References:

This is a series blog, check out the previous 2 blogs:

  1. What is Dependency Injection
  2. Three Lifetime Services
Assi Arai
Assi Arai
Articles: 31