Dependencies and Layering with C#
Absolutely! Let’s go slowly and carefully through Dependencies and Layering with C#, making sure we cover all crucial knowledge in an easy-to-follow way. 1. Understanding Dependencies in C# A dependency occurs when one class relies on another to function. Dependencies refer to how different parts of your code interact with each other. If dependencies are not managed well, your code can become tightly coupled, making it hard to maintain and modify. For example, if an OrderService class needs a database to save orders, it depends on OrderRepository. Example of Tight Coupling (Bad Design) public class OrderService { private OrderRepository _repository = new OrderRepository(); // Direct instantiation (BAD ❌) public void ProcessOrder(Order order) { _repository.Save(order); } } Problems with This Approach ❌ Tightly Coupled – OrderService is locked to OrderRepository. ❌ Hard to Change – If we switch from SQL Server to MongoDB, we must modify OrderService. ❌ Difficult to Test – Cannot use mock dependencies for unit testing. 2. Applying Dependency Injection (DIP - Dependency Inversion Principle) The D in SOLID stands for Dependency Inversion Principle, which helps manage dependencies effectively. What does DIP say? High-level modules (business logic) should not depend on low-level modules (database or UI). Both should depend on abstractions (interfaces). Abstractions should not depend on details. Details should depend on abstractions. Fixing the Bad Dependency Issue We replace the direct dependency on EmailService with an interface: public interface INotificationService { void SendConfirmation(Order order); } public class EmailService : INotificationService { public void SendConfirmation(Order order) { Console.WriteLine($"Email sent for order: {order.ProductName}"); } } public class OrderService { private readonly INotificationService _notificationService; public OrderService(INotificationService notificationService) { _notificationService = notificationService; } public void PlaceOrder(Order order) { // Process the order... _notificationService.SendConfirmation(order); } } Benefits of DIP: ✅ If we switch to SMS notifications, we just implement a new class: public class SmsService : INotificationService { public void SendConfirmation(Order order) { Console.WriteLine($"SMS sent for order: {order.ProductName}"); } } ✅ No need to modify OrderService, keeping our code open for extension but closed for modification (OCP). 3. Dependency Injection (DI) Dependency Injection (DI) is a technique that helps follow the Dependency Inversion Principle (DIP) by providing dependencies from the outside rather than creating them inside the class. Three Types of Dependency Injection 1. **Constructor Injection (Most Common)** public class OrderService { private readonly INotificationService _notificationService; public OrderService(INotificationService notificationService) { _notificationService = notificationService; } } The dependency is passed through the constructor. 2. **Property Injection** public class OrderService { public INotificationService NotificationService { get; set; } } The dependency is assigned via a property. 3. **Method Injection** public class OrderService { public void PlaceOrder(Order order, INotificationService notificationService) { notificationService.SendConfirmation(order); } } The dependency is passed as a method parameter. Using a DI Container (Inversion of Control - IoC) If you are using a framework like ASP.NET Core, you can register dependencies automatically: services.AddTransient(); services.AddTransient(); This way, when you request OrderService, it will automatically get an EmailService instance. Instead of directly creating dependencies, we pass them as interfaces. Step 1: Define an Interface (Abstraction) public interface IOrderRepository { void Save(Order order); } Step 2: Implement Different Repositories public class SqlOrderRepository : IOrderRepository { public void Save(Order order) { Console.WriteLine("Saving order to SQL Server..."); } } public class NoSqlOrderRepository : IOrderRepository { public void Save(Order order) { Console.WriteLine("Saving order to NoSQL database..."); } } Step 3: Inject Dependency via Constructor public class OrderService { private readonly IOrderRepository _repository; public OrderService(IOrderRepository repository) // Injecting dependency (GOOD ✅) { _repository = repository; } public void ProcessO

Absolutely! Let’s go slowly and carefully through Dependencies and Layering with C#, making sure we cover all crucial knowledge in an easy-to-follow way.
1. Understanding Dependencies in C#
A dependency occurs when one class relies on another to function.
Dependencies refer to how different parts of your code interact with each other. If dependencies are not managed well, your code can become tightly coupled, making it hard to maintain and modify.
For example, if an OrderService
class needs a database to save orders, it depends on OrderRepository
.
Example of Tight Coupling (Bad Design)
public class OrderService
{
private OrderRepository _repository = new OrderRepository(); // Direct instantiation (BAD ❌)
public void ProcessOrder(Order order)
{
_repository.Save(order);
}
}
Problems with This Approach
❌ Tightly Coupled – OrderService
is locked to OrderRepository
.
❌ Hard to Change – If we switch from SQL Server to MongoDB, we must modify OrderService
.
❌ Difficult to Test – Cannot use mock dependencies for unit testing.
2. Applying Dependency Injection (DIP - Dependency Inversion Principle)
The D in SOLID stands for Dependency Inversion Principle, which helps manage dependencies effectively.
What does DIP say?
- High-level modules (business logic) should not depend on low-level modules (database or UI).
- Both should depend on abstractions (interfaces).
- Abstractions should not depend on details. Details should depend on abstractions.
Fixing the Bad Dependency Issue
We replace the direct dependency on EmailService
with an interface:
public interface INotificationService
{
void SendConfirmation(Order order);
}
public class EmailService : INotificationService
{
public void SendConfirmation(Order order)
{
Console.WriteLine($"Email sent for order: {order.ProductName}");
}
}
public class OrderService
{
private readonly INotificationService _notificationService;
public OrderService(INotificationService notificationService)
{
_notificationService = notificationService;
}
public void PlaceOrder(Order order)
{
// Process the order...
_notificationService.SendConfirmation(order);
}
}
Benefits of DIP:
✅ If we switch to SMS notifications, we just implement a new class:
public class SmsService : INotificationService
{
public void SendConfirmation(Order order)
{
Console.WriteLine($"SMS sent for order: {order.ProductName}");
}
}
✅ No need to modify OrderService
, keeping our code open for extension but closed for modification (OCP).
3. Dependency Injection (DI)
Dependency Injection (DI) is a technique that helps follow the Dependency Inversion Principle (DIP) by providing dependencies from the outside rather than creating them inside the class.
Three Types of Dependency Injection
1. **Constructor Injection (Most Common)**
public class OrderService
{
private readonly INotificationService _notificationService;
public OrderService(INotificationService notificationService)
{
_notificationService = notificationService;
}
}
- The dependency is passed through the constructor.
2. **Property Injection**
public class OrderService
{
public INotificationService NotificationService { get; set; }
}
- The dependency is assigned via a property.
3. **Method Injection**
public class OrderService
{
public void PlaceOrder(Order order, INotificationService notificationService)
{
notificationService.SendConfirmation(order);
}
}
- The dependency is passed as a method parameter.
Using a DI Container (Inversion of Control - IoC)
If you are using a framework like ASP.NET Core, you can register dependencies automatically:
services.AddTransient<INotificationService, EmailService>();
services.AddTransient<OrderService>();
This way, when you request OrderService
, it will automatically get an EmailService
instance.
Instead of directly creating dependencies, we pass them as interfaces.
Step 1: Define an Interface (Abstraction)
public interface IOrderRepository
{
void Save(Order order);
}
Step 2: Implement Different Repositories
public class SqlOrderRepository : IOrderRepository
{
public void Save(Order order)
{
Console.WriteLine("Saving order to SQL Server...");
}
}
public class NoSqlOrderRepository : IOrderRepository
{
public void Save(Order order)
{
Console.WriteLine("Saving order to NoSQL database...");
}
}
Step 3: Inject Dependency via Constructor
public class OrderService
{
private readonly IOrderRepository _repository;
public OrderService(IOrderRepository repository) // Injecting dependency (GOOD ✅)
{
_repository = repository;
}
public void ProcessOrder(Order order)
{
_repository.Save(order);
}
}
Step 4: Use Dependency Injection (DI)
class Program
{
static void Main()
{
IOrderRepository repository = new SqlOrderRepository(); // Choose implementation dynamically
OrderService service = new OrderService(repository);
service.ProcessOrder(new Order { Id = 1, Description = "Laptop" });
}
}
✅ Now, we can switch databases WITHOUT modifying OrderService
!
4. What is Layering?
Layering is a design pattern that organizes code into logical layers to separate concerns. This reduces dependencies between different parts of the system.
Common Software Layers:
- Presentation Layer (UI) → Handles user interactions.
- Application Layer → Contains business logic.
- Domain Layer (Core Logic) → Represents the business model.
- Data Access Layer (DAL) → Handles database interactions.