Advanced Dependency Injection in .NET Core

Dependency Injection (DI) is a core feature in .NET Core that helps manage dependencies and promotes loose coupling in applications. In this guide, we’ll explore advanced DI concepts such as custom service lifetimes, scopes, and handling complex dependency graphs through a practical example: an e-commerce application.

Scenario. E-Commerce Application

In our e-commerce application, we have several services.

  • Order Service: Manages customer orders.
  • Payment Service: Handles payment processing.
  • Notification Service: Sends notifications via email or SMS.
  • Audit Service: Logs operations for auditing.

We'll ensure that these services are efficiently managed and injected into various parts of the application using DI.

Project Structure

  1. Interfaces: Define the contracts for our services.
  2. Implementations: Implement the service logic.
  3. Controllers: Use services to handle HTTP requests.
  4. Models: Define the data structures used in services.

Step-by-step implementation
 

1. Define Interfaces

Create interfaces for the services in the Interfaces folder.

Interfaces/Services/Interfaces.cs

public interface IOrderService
{
    void PlaceOrder(Order order);
}
public interface IPaymentService
{
    void ProcessPayment(Payment payment);
}
public interface INotificationService
{
    void Notify(Notification notification);
}
public interface IAuditService
{
    void LogOperation(string message);
}

2. Implement Services

Create concrete implementations for each service.

Services/OrderService.cs

public class OrderService : IOrderService
{
    private readonly IPaymentService _paymentService;
    private readonly INotificationService _notificationService;
    private readonly IAuditService _auditService;
    public OrderService(IPaymentService paymentService, INotificationService notificationService, IAuditService auditService)
    {
        _paymentService = paymentService;
        _notificationService = notificationService;
        _auditService = auditService;
    }
    public void PlaceOrder(Order order)
    {
        // Process payment
        _paymentService.ProcessPayment(order.Payment);
        // Notify customer
        _notificationService.Notify(order.Notification);
        // Log operation
        _auditService.LogOperation($"Order placed for {order.Id}");
    }
}

Services/PaymentService.cs

public class PaymentService : IPaymentService
{
    private readonly IPaymentGatewayClient _paymentGatewayClient;
    public PaymentService(IPaymentGatewayClient paymentGatewayClient)
    {
        _paymentGatewayClient = paymentGatewayClient;
    }
    public void ProcessPayment(Payment payment)
    {
        _paymentGatewayClient.MakePayment(payment);
    }
}

Services/NotificationService.cs

using System.Net;
using System.Net.Mail;
public class NotificationService : INotificationService
{
    private readonly SmtpClient _smtpClient;
    private readonly ISmsClient _smsClient; // Hypothetical SMS client interface
    public NotificationService(SmtpClient smtpClient, ISmsClient smsClient)
    {
        _smtpClient = smtpClient;
        _smsClient = smsClient;
    }
    public void Notify(Notification notification)
    {
        switch (notification.Type)
        {
            case NotificationType.Email:
                SendEmail(notification);
                break;
            case NotificationType.Sms:
                SendSms(notification);
                break;
            default:
                throw new ArgumentOutOfRangeException();
        }
    }
    private void SendEmail(Notification notification)
    {
        var mailMessage = new MailMessage
        {
            From = new MailAddress("[email protected]"),
            Subject = "Notification",
            Body = notification.Message,
            IsBodyHtml = true
        };
        mailMessage.To.Add(notification.Recipient);
        try
        {
            _smtpClient.Send(mailMessage);
        }
        catch (Exception ex)
        {
            // Handle exceptions (e.g., log errors)
            Console.WriteLine($"Failed to send email: {ex.Message}");
        }
    }
    private void SendSms(Notification notification)
    {
        try
        {
            // Assuming _smsClient.SendSms is a method to send SMS via a service
            _smsClient.SendSms(notification.Recipient, notification.Message);
        }
        catch (Exception ex)
        {
            // Handle exceptions (e.g., log errors)
            Console.WriteLine($"Failed to send SMS: {ex.Message}");
        }
    }
}

Services/AuditService.cs

using Microsoft.Extensions.Logging;
public class AuditService : IAuditService
{
    private readonly ILogger<AuditService> _logger;
    public AuditService(ILogger<AuditService> logger)
    {
        _logger = logger;
    }
    public void LogOperation(string message)
    {
        _logger.LogInformation($"Audit Log: {message}");
    }
}

Services/PaymentGatewayClient.cs

public interface IPaymentGatewayClient
{
    void MakePayment(Payment payment);
}
public class PaymentGatewayClient : IPaymentGatewayClient
{
    public void MakePayment(Payment payment)
    {
        // we will integrate the Stripe Payment Gateway Infuture
    }
}

3. Define the ISmsClient Interface

Create an interface for the SMS client.

Services/ISmsClient.cs

public interface ISmsClient
{
    void SendSms(string phoneNumber, string message);
}

4. Implement the ISmsClient Interface

Here’s a simple implementation of the ISmsClient.

Services/SmsClient.cs

public class SmsClient : ISmsClient
{
    public void SendSms(string phoneNumber, string message)
    {
        Console.WriteLine($"Sending SMS to {phoneNumber}: {message}");
       // Infuture we will integrate Twilio in this application
    }
}

5. Register Services with Custom Lifetimes

In the Startup class, register the services with the DI container.

public class Startup
{
    public void ConfigureServices(IServiceCollection services)
    {
        // Singleton: Only one instance throughout the application lifetime
        services.AddSingleton<SmtpClient>(provider => new SmtpClient("smtp.example.com")
        {
            Port = 587,
            Credentials = new NetworkCredential("[email protected]", "your-password"),
            EnableSsl = true
        });
        // Singleton: SmsClient
        services.AddScoped<ISmsClient, SmsClient>();
        // Scoped: One instance per request
        services.AddScoped<IOrderService, OrderService>();
        services.AddScoped<IPaymentService, PaymentService>();
        services.AddScoped<INotificationService, NotificationService>();
        services.AddScoped<IAuditService, AuditService>();
        // Add other services like MVC, EF Core, etc.
        services.AddControllersWithViews();
    }
    public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
    {
        if (env.IsDevelopment())
        {
            app.UseDeveloperExceptionPage();
        }
        else
        {
            app.UseExceptionHandler("/Home/Error");
            app.UseHsts();
        }
        app.UseHttpsRedirection();
        app.UseStaticFiles();
        app.UseRouting();
        app.UseAuthorization();
        app.UseEndpoints(endpoints =>
        {
            endpoints.MapControllerRoute(
                name: "default",
                pattern: "{controller=Home}/{action=Index}/{id?}");
        });
    }
}

6. Create Models

Define the models used in the services.

Models/Order.cs

public class Order
{
    public int Id { get; set; }
    public Payment Payment { get; set; }
    public Notification Notification { get; set; }
}

Models/Payment.cs

public class Payment
{
    public int PaymentId { get; set; }
    public decimal Amount { get; set; }
}

Models/Notification.cs

public class Notification
{
    public string Recipient { get; set; }
    public string Message { get; set; }
    public NotificationType Type { get; set; }
}
public enum NotificationType
{
    Email,
    Sms
}

7. Implement a Controller

Use the services in a controller to handle HTTP requests.

Controllers/OrderController.cs

public class OrderController : Controller
{
    private readonly IOrderService _orderService;

    public OrderController(IOrderService orderService)
    {
        _orderService = orderService;
    }
    [HttpPost]
    public IActionResult PlaceOrder(Order order)
    {
        if (ModelState.IsValid)
        {
            _orderService.PlaceOrder(order);
            return RedirectToAction("OrderSuccess");
        }
        return View(order);
    }
    public IActionResult OrderSuccess()
    {
        return View();
    }
}

8. Create Views

Create views to interact with users.

Views/Order/PlaceOrder.cshtml

@model YourNamespace.Models.Order

<form asp-action="PlaceOrder" method="post">
    <div class="form-group">
        <label asp-for="Payment.Amount"></label>
        <input asp-for="Payment.Amount" class="form-control" />
        <span asp-validation-for="Payment.Amount" class="text-danger"></span>
    </div>
    <div class="form-group">
        <label asp-for="Notification.Message"></label>
        <input asp-for="Notification.Message" class="form-control" />
        <span asp-validation-for="Notification.Message" class="text-danger"></span>
    </div>
    <button type="submit" class="btn btn-primary">Place Order</button>
</form>

Views/Order/OrderSuccess.cshtml

<h2>Your order has been placed successfully!</h2>

Conclusion

In this guide, we've delved into advanced dependency injection (DI) techniques in .NET Core by constructing a modular e-commerce application. We explored various aspects of DI, including defining services, implementing them, and managing their lifetimes. By following this approach, we have.

  1. Promoted Loose Coupling: Our application components are loosely coupled, enhancing flexibility and making it easier to modify or extend functionality without affecting other parts of the system.
  2. Ensured Maintainability: With clear service interfaces and their implementations, the codebase is organized and easier to maintain. Each service is responsible for a specific part of the application logic, promoting single-responsibility principles.
  3. Enhanced Scalability: The DI setup allows for better scalability. Services can be swapped or upgraded with minimal impact on the overall application, and different lifetimes (singleton, scoped, and transient) are used to manage resource utilization effectively.
  4. Streamlined Configuration: By registering services with different lifetimes in the Startup class, we ensure that the DI container handles service instantiation and dependency resolution automatically, reducing boilerplate code and potential errors.

In the end, leveraging advanced DI techniques in .NET Core not only simplifies the development process but also contributes to creating a robust, scalable, and maintainable application architecture.