DEV Community

Taki (Kieu Dang)
Taki (Kieu Dang)

Posted on

2

Dependencies and Layering

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);
    }
}
Enter fullscreen mode Exit fullscreen mode

Problems with This Approach

Tightly CoupledOrderService 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);
    }
}
Enter fullscreen mode Exit fullscreen mode

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}");
    }
}
Enter fullscreen mode Exit fullscreen mode

✅ 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;
       }
   }
Enter fullscreen mode Exit fullscreen mode
  • The dependency is passed through the constructor.

Property Injection

   public class OrderService
   {
       public INotificationService NotificationService { get; set; }
   }
Enter fullscreen mode Exit fullscreen mode
  • The dependency is assigned via a property.

Method Injection

   public class OrderService
   {
       public void PlaceOrder(Order order, INotificationService notificationService)
       {
           notificationService.SendConfirmation(order);
       }
   }
Enter fullscreen mode Exit fullscreen mode
  • 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>();
Enter fullscreen mode Exit fullscreen mode

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);
}
Enter fullscreen mode Exit fullscreen mode

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...");
    }
}
Enter fullscreen mode Exit fullscreen mode

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);
    }
}
Enter fullscreen mode Exit fullscreen mode

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" });
    }
}
Enter fullscreen mode Exit fullscreen mode

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:

  1. Presentation Layer (UI) → Handles user interactions.
  2. Application Layer → Contains business logic.
  3. Domain Layer (Core Logic) → Represents the business model.
  4. 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);
    }
}
Enter fullscreen mode Exit fullscreen mode

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 };
    }
}
Enter fullscreen mode Exit fullscreen mode

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;
    }
}
Enter fullscreen mode Exit fullscreen mode

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);
        }
    }
}
Enter fullscreen mode Exit fullscreen mode

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
}
Enter fullscreen mode Exit fullscreen mode

🚀 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)
Enter fullscreen mode Exit fullscreen mode

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);
    }
}
Enter fullscreen mode Exit fullscreen mode

Now, we can replace StripePaymentProcessor with PayPalPaymentProcessor without modifying OrderService!


7. Anti-Patterns to Avoid

While working with dependencies, avoid these common mistakes:

  1. Service Locator Pattern (Bad Practice)
   var emailService = ServiceLocator.GetService<EmailService>();
Enter fullscreen mode Exit fullscreen mode
  • ❌ Hides dependencies, making code harder to test.
  1. Static Dependencies
   public class OrderService
   {
       private static readonly EmailService _emailService = new EmailService();
   }
Enter fullscreen mode Exit fullscreen mode
  • ❌ 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.

Qodo Takeover

Introducing Qodo Gen 1.0: Transform Your Workflow with Agentic AI

While many AI coding tools operate as simple command-response systems, Qodo Gen 1.0 represents the next generation: autonomous, multi-step problem-solving agents that work alongside you.

Read full post →

Top comments (0)

AWS GenAI LIVE image

Real challenges. Real solutions. Real talk.

From technical discussions to philosophical debates, AWS and AWS Partners examine the impact and evolution of gen AI.

Learn more

👋 Kindness is contagious

Please leave a ❤️ or a friendly comment on this post if you found it helpful!

Okay