Table of Contents

Dependency Injection

Controller-based queries support full dependency injection through their constructors, allowing you to inject services, repositories, loggers, and other dependencies from the service collection.

Constructor Injection

The most common pattern is to inject dependencies through the controller's constructor:

[Route("api/accounts")]
public class Accounts : Controller
{
    readonly IAccountService _accountService;
    readonly ILogger<Accounts> _logger;
    readonly IMongoCollection<DebitAccount> _collection;

    public Accounts(
        IAccountService accountService, 
        ILogger<Accounts> logger,
        IMongoCollection<DebitAccount> collection)
    {
        _accountService = accountService;
        _logger = logger;
        _collection = collection;
    }

    [HttpGet]
    public IEnumerable<DebitAccount> AllAccounts()
    {
        _logger.LogInformation("Retrieving all accounts");
        return _accountService.GetAllAccounts();
    }
}

Common Dependency Types

Database Collections

MongoDB collections are commonly injected:

public class Accounts : Controller
{
    readonly IMongoCollection<DebitAccount> _collection;

    public Accounts(IMongoCollection<DebitAccount> collection)
    {
        _collection = collection;
    }

    [HttpGet]
    public async Task<IEnumerable<DebitAccount>> GetAccountsAsync()
    {
        var result = await _collection.FindAsync(_ => true);
        return result.ToList();
    }
}

Entity Framework DbContext

For Entity Framework Core scenarios:

public class Accounts : Controller
{
    readonly ApplicationDbContext _dbContext;

    public Accounts(ApplicationDbContext dbContext)
    {
        _dbContext = dbContext;
    }

    [HttpGet]
    public async Task<IEnumerable<DebitAccount>> GetAccountsAsync()
    {
        return await _dbContext.Accounts.ToListAsync();
    }
}

Business Services

Inject business logic services:

public class Accounts : Controller
{
    readonly IAccountService _accountService;
    readonly ICustomerService _customerService;

    public Accounts(IAccountService accountService, ICustomerService customerService)
    {
        _accountService = accountService;
        _customerService = customerService;
    }

    [HttpGet("{id}/details")]
    public async Task<AccountDetails> GetAccountDetails(AccountId id)
    {
        var account = await _accountService.GetAccountAsync(id);
        var customer = await _customerService.GetCustomerAsync(account.Owner);
        
        return new AccountDetails(account, customer);
    }
}

Logging

Structured logging with dependency injection:

public class Accounts : Controller
{
    readonly IMongoCollection<DebitAccount> _collection;
    readonly ILogger<Accounts> _logger;

    public Accounts(IMongoCollection<DebitAccount> collection, ILogger<Accounts> logger)
    {
        _collection = collection;
        _logger = logger;
    }

    [HttpGet("search")]
    public async Task<IEnumerable<DebitAccount>> SearchAccounts([FromQuery] string term)
    {
        _logger.LogInformation("Searching accounts with term: {SearchTerm}", term);

        var filter = Builders<DebitAccount>.Filter.Regex(
            a => a.Name, 
            new BsonRegularExpression(term, "i"));

        var result = await _collection.FindAsync(filter);
        var accounts = result.ToList();

        _logger.LogInformation("Found {AccountCount} accounts", accounts.Count);
        return accounts;
    }
}

Configuration

Inject configuration objects:

public class Accounts : Controller
{
    readonly IMongoCollection<DebitAccount> _collection;
    readonly AccountQueryOptions _options;

    public Accounts(
        IMongoCollection<DebitAccount> collection, 
        IOptions<AccountQueryOptions> options)
    {
        _collection = collection;
        _options = options.Value;
    }

    [HttpGet]
    public async Task<IEnumerable<DebitAccount>> GetAccounts()
    {
        var result = await _collection.FindAsync(_ => true);
        return result.Limit(_options.DefaultPageSize).ToList();
    }
}

Service Registration

Make sure your dependencies are registered in the service collection:

// In Program.cs or Startup.cs
builder.Services.AddScoped<IAccountService, AccountService>();
builder.Services.AddScoped<ICustomerService, CustomerService>();
builder.Services.Configure<AccountQueryOptions>(
    builder.Configuration.GetSection("AccountQueries"));

Multiple Dependencies

Controllers can have many dependencies injected:

public class Accounts : Controller
{
    readonly IAccountService _accountService;
    readonly ICustomerService _customerService;
    readonly ICachingService _cache;
    readonly ILogger<Accounts> _logger;
    readonly IMapper _mapper;
    readonly AccountQueryOptions _options;

    public Accounts(
        IAccountService accountService,
        ICustomerService customerService,
        ICachingService cache,
        ILogger<Accounts> logger,
        IMapper mapper,
        IOptions<AccountQueryOptions> options)
    {
        _accountService = accountService;
        _customerService = customerService;
        _cache = cache;
        _logger = logger;
        _mapper = mapper;
        _options = options.Value;
    }

    [HttpGet("{id}")]
    public async Task<AccountDetails> GetAccount(AccountId id)
    {
        var cacheKey = $"account-{id}";
        
        var cached = await _cache.GetAsync<AccountDetails>(cacheKey);
        if (cached is not null)
        {
            _logger.LogInformation("Returning cached account {AccountId}", id);
            return cached;
        }

        _logger.LogInformation("Loading account {AccountId} from database", id);
        var account = await _accountService.GetAccountAsync(id);
        var customer = await _customerService.GetCustomerAsync(account.Owner);
        
        var result = _mapper.Map<AccountDetails>((account, customer));
        await _cache.SetAsync(cacheKey, result, _options.CacheExpiry);
        
        return result;
    }
}

Generic Dependencies

You can inject generic types:

public class GenericQueries<T> : Controller where T : class
{
    readonly IRepository<T> _repository;
    readonly ILogger<GenericQueries<T>> _logger;

    public GenericQueries(IRepository<T> repository, ILogger<GenericQueries<T>> logger)
    {
        _repository = repository;
        _logger = logger;
    }

    [HttpGet]
    public async Task<IEnumerable<T>> GetAll()
    {
        _logger.LogInformation("Getting all {EntityType}", typeof(T).Name);
        return await _repository.GetAllAsync();
    }
}

Best Practices

  1. Use readonly fields - Store injected dependencies as readonly fields
  2. Prefer constructor injection over method injection or service locator patterns
  3. Keep constructors clean - Don't perform logic in constructors, just store dependencies
  4. Use appropriate lifetimes - Register services with appropriate lifetimes (Singleton, Scoped, Transient)
  5. Validate dependencies - Ensure all required dependencies are registered in the DI container
  6. Use IOptions<T> for configuration objects rather than injecting raw configuration

Avoiding Service Locator

Don't use IServiceProvider directly in your controllers:

// ❌ Don't do this - service locator anti-pattern
public class BadAccounts : Controller
{
    readonly IServiceProvider _serviceProvider;

    public BadAccounts(IServiceProvider serviceProvider)
    {
        _serviceProvider = serviceProvider;
    }

    [HttpGet]
    public IEnumerable<DebitAccount> GetAccounts()
    {
        var service = _serviceProvider.GetRequiredService<IAccountService>();
        return service.GetAllAccounts();
    }
}

// ✅ Do this instead - constructor injection
public class GoodAccounts : Controller
{
    readonly IAccountService _accountService;

    public GoodAccounts(IAccountService accountService)
    {
        _accountService = accountService;
    }

    [HttpGet]
    public IEnumerable<DebitAccount> GetAccounts()
    {
        return _accountService.GetAllAccounts();
    }
}