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
Constructor Injection (Most Common)
public class OrderService
{
private readonly INotificationService _notificationService;
public OrderService(INotificationService notificationService)
{
_notificationService = notificationService;
}
}
- The dependency is passed through the constructor.
Property Injection
public class OrderService
{
public INotificationService NotificationService { get; set; }
}
- The dependency is assigned via a property.
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.
📌 Why use layering?
- It keeps the business logic separate from the UI and database.
- It makes your application more scalable and maintainable.
Example of a Layered Architecture:
// Domain Layer (Core Business Logic)
public class Order
{
public int Id { get; set; }
public string ProductName { get; set; }
public decimal Price { get; set; }
}
// Data Access Layer (Repository)
public class OrderRepository
{
public void Save(Order order)
{
// Save order to database...
}
}
// Application Layer (Business Service)
public class OrderService
{
private readonly OrderRepository _orderRepository;
public OrderService(OrderRepository orderRepository)
{
_orderRepository = orderRepository;
}
public void ProcessOrder(Order order)
{
_orderRepository.Save(order);
}
}
// Presentation Layer (UI)
public class OrderController
{
private readonly OrderService _orderService;
public OrderController(OrderService orderService)
{
_orderService = orderService;
}
public void CreateOrder()
{
var order = new Order { Id = 1, ProductName = "Laptop", Price = 1500 };
_orderService.ProcessOrder(order);
}
}
✅ Advantages of Layering:
- If the database changes, only the
OrderRepository
needs to be updated. - The UI (
OrderController
) does not depend directly on the database. - Business logic (
OrderService
) is isolated and reusable.
5. Implementing Layered Architecture in C#
Let’s build a simple E-Commerce system with proper layering.
Step 1: Data Access Layer (DAL)
Handles database operations.
public interface IProductRepository
{
Product GetProductById(int id);
}
public class ProductRepository : IProductRepository
{
public Product GetProductById(int id)
{
Console.WriteLine("Fetching product from the database...");
return new Product { Id = id, Name = "Laptop", Price = 1200 };
}
}
Step 2: Business Logic Layer (BLL)
Contains business rules and validation.
public class ProductService
{
private readonly IProductRepository _repository;
public ProductService(IProductRepository repository)
{
_repository = repository;
}
public Product GetProduct(int id)
{
var product = _repository.GetProductById(id);
if (product == null)
{
throw new Exception("Product not found!");
}
return product;
}
}
Step 3: Presentation Layer (UI)
Handles user interaction (Console, API, UI).
class Program
{
static void Main()
{
IProductRepository repository = new ProductRepository();
ProductService service = new ProductService(repository);
try
{
var product = service.GetProduct(1);
Console.WriteLine($"Product: {product.Name}, Price: ${product.Price}");
}
catch (Exception ex)
{
Console.WriteLine(ex.Message);
}
}
}
✅ Now, changing databases does not affect the business or UI layers!
6. Modern Layering Best Practices in C#
✅ Use Dependency Injection (DI) with ASP.NET Core
public void ConfigureServices(IServiceCollection services)
{
services.AddScoped<IProductRepository, ProductRepository>(); // Register DAL
services.AddScoped<ProductService>(); // Register BLL
}
🚀 ASP.NET Core injects dependencies automatically!
✅ Follow Hexagonal (Clean) Architecture for Large Apps
Instead of hard-layered architecture, use Ports & Adapters:
Application Core (Business Logic) <-- Interfaces (Ports)
Infrastructure (Databases, APIs) <-- Implementations (Adapters)
Example:
public interface IPaymentProcessor
{
void ProcessPayment(Order order);
}
public class StripePaymentProcessor : IPaymentProcessor
{
public void ProcessPayment(Order order)
{
Console.WriteLine("Processing payment with Stripe...");
}
}
public class OrderService
{
private readonly IPaymentProcessor _paymentProcessor;
public OrderService(IPaymentProcessor paymentProcessor)
{
_paymentProcessor = paymentProcessor;
}
public void Checkout(Order order)
{
_paymentProcessor.ProcessPayment(order);
}
}
✅ Now, we can replace StripePaymentProcessor
with PayPalPaymentProcessor
without modifying OrderService
!
7. Anti-Patterns to Avoid
While working with dependencies, avoid these common mistakes:
- Service Locator Pattern (Bad Practice)
var emailService = ServiceLocator.GetService<EmailService>();
- ❌ Hides dependencies, making code harder to test.
- Static Dependencies
public class OrderService
{
private static readonly EmailService _emailService = new EmailService();
}
- ❌ Hard to replace in tests or modify behavior dynamically.
8. Conclusion
✔️ Dependencies should be injected using DI to reduce tight coupling.
✔️ Layered architecture organizes applications into independent modules.
✔️ Using interfaces (abstractions) ensures flexibility and testability.
✔️ Dependency Inversion Principle (DIP) ensures high-level modules depend on abstractions, not low-level implementations.
✔️ Dependency Injection (DI) makes dependencies flexible and testable.
✔️ ASP.NET Core DI container automatically manages dependencies.
✔️ Hexagonal (Clean) Architecture works best for large applications.
✔️ Avoid anti-patterns like Service Locator and Static Dependencies.
Top comments (0)