Dependency Injection in C#: .NET Core DI Container & Service Lifetimes

1. Dependency Injection: Concept and Benefits

Q: What is Dependency Injection (DI) in C#?

Dependency Injection (DI) is a design pattern in which a class’s dependencies (e.g., services, objects) are provided externally rather than created internally. In C#, DI is commonly used to pass dependencies via constructors, methods, or properties, promoting loose coupling and testable code. .NET Core provides a built-in DI container to manage dependency registration and resolution.

Q: What are the key components of DI?

Q: What are the benefits of Dependency Injection?

Q: How does DI in C#/.NET Core differ from C/C++?

2. Using Built-in DI in .NET Core

Q: What is the built-in DI container in .NET Core?

.NET Core includes a lightweight DI container (IServiceProvider) integrated into the framework, configured in the Startup class or Program.cs (in .NET 6+). It supports registering services, resolving dependencies, and managing service lifetimes:

Q: How do you configure DI in .NET Core?

Syntax (in Program.cs):

builder.Services.AddTransient<IService, Service>();

Q: What are the common DI methods in .NET Core?

Q: Can you give an example of using built-in DI in .NET Core?

Below is an example of a .NET Core console application demonstrating DI with constructor injection, service registration, and different lifetimes.

using System;
using Microsoft.Extensions.DependencyInjection;

// Interface for dependency
public interface ILogger
{
    void Log(string message);
}

// Concrete implementation
public class ConsoleLogger : ILogger
{
    private readonly string _instanceId;
    public ConsoleLogger()
    {
        _instanceId = Guid.NewGuid().ToString();
    }

    public void Log(string message)
    {
        Console.WriteLine($"[{_instanceId}] {message}");
    }
}

// Service using dependency
public class UserService
{
    private readonly ILogger _logger;

    public UserService(ILogger logger)
    {
        _logger = logger ?? throw new ArgumentNullException(nameof(logger));
    }

    public void ProcessUser(string userName)
    {
        _logger.Log($"Processing user: {userName}");
    }
}

class Program
{
    static void Main(string[] args)
    {
        // Set up DI container
        var serviceProvider = new ServiceCollection()
            .AddSingleton<ILogger, ConsoleLogger>() // Singleton: same instance
            .AddTransient<UserService>() // Transient: new instance each time
            .BuildServiceProvider();

        // Resolve and use services
        try
        {
            // First instance
            var userService1 = serviceProvider.GetService<UserService>();
            userService1.ProcessUser("Krishna");

            // Second instance
            var userService2 = serviceProvider.GetService<UserService>();
            userService2.ProcessUser("Kristal");

            // Note: Same logger instance (singleton) used by both services
        }
        catch (Exception ex)
        {
            Console.WriteLine($"Error: {ex.Message}");
        }
        finally
        {
            // Dispose services
            (serviceProvider as IDisposable)?.Dispose();
        }
    }
}

Output (example):

[123e4567-e89b-12d3-a456-426614174000] Processing user: Krishna
[123e4567-e89b-12d3-a456-426614174000] Processing user: Kristal

Note: The GUID (_instanceId) will be the same for both calls, indicating the ConsoleLogger is a singleton.

Q: How do you use DI in an ASP.NET Core application?

In ASP.NET Core, DI is configured in Program.cs (or Startup.cs in older versions), and dependencies are injected into controllers, services, or middleware. Below is an example of DI in an ASP.NET Core minimal API.

using Microsoft.AspNetCore.Builder;
using Microsoft.Extensions.DependencyInjection;

// Interface and implementation (same as above)
public interface ILogger
{
    void Log(string message);
}

public class ConsoleLogger : ILogger
{
    private readonly string _instanceId;
    public ConsoleLogger()
    {
        _instanceId = Guid.NewGuid().ToString();
    }

    public void Log(string message)
    {
        Console.WriteLine($"[{_instanceId}] {message}");
    }
}

public class UserService
{
    private readonly ILogger _logger;

    public UserService(ILogger logger)
    {
        _logger = logger;
    }

    public void ProcessUser(string userName)
    {
        _logger.Log($"Processing user: {userName}");
    }
}

var builder = WebApplication.CreateBuilder(args);

// Register services
builder.Services.AddScoped<ILogger, ConsoleLogger>(); // Scoped lifetime
builder.Services.AddScoped<UserService>();

var app = builder.Build();

// Minimal API endpoint
app.MapGet("/process/{userName}", (string userName, UserService userService) =>
{
    userService.ProcessUser(userName);
    return $"Processed {userName}";
});

app.Run();

Usage: Run the app and navigate to http://localhost:5000/process/Krishna.

Output (example in console):

[456f789a-b12c-34d5-e678-901234567890] Processing user: Krishna

Note: The GUID will differ per HTTP request (scoped lifetime).

Q: How does DI in .NET Core differ from C/C++?

3. Common Mistakes & Best Practices

Q: Common mistakes?

Concept/Implementation:

.NET Core DI:

General:

Q: Best practices?

Concept/Implementation:

.NET Core DI:

General: