ASP.NET Core Web API · Building APIs

Dependency Injection

APIs depend on lots of collaborators: a DbContext, a payment gateway, a shipping-rate service, an external currency API. DI wires them together. This lesson goes beyond the basics with two features APIs use constantly: keyed services and typed HTTP clients.

Intermediate 6 min Lesson 10 of 37Examples target .NET 9.
0/37 in ASP.NET Core Web API
On this page · 6 min · Intermediate

In simple words#

Controllers order their tools from the DI “stores department” through the constructor instead of building them — easy to replace with test tools later.

AddScoped
One instance per HTTP request.
Constructor injection
Dependencies arrive as parameters.
Interfaces
Depend on contracts for testability.

Learning objectives#

  • Register and inject services with the right lifetime
  • Choose between implementations with keyed services
  • Call external APIs with a typed HttpClient from IHttpClientFactory
  • Configure services with options

Explanation#

Lifetimes work as in MVC: transient (new every time), scoped (one per request — DbContext, unit-of-work services), singleton (shared, stateless or thread-safe). Never inject scoped into singleton.

Keyed services (.NET 8+) register several implementations of one interface under keys: AddKeyedScoped<IShippingCalculator, ExpressShipping>("express"). Inject one with [FromKeyedServices("express")] or resolve by key at runtime.

Never new up HttpClient per request (socket exhaustion) and never keep one forever without care (DNS changes). builder.Services.AddHttpClient<ExchangeRateClient>(c => c.BaseAddress = …) gives you a typed client with pooled handlers.

Typed clients are classes that receive HttpClient in their constructor and expose meaningful methods (GetRateAsync("USD")). Add resilience (retries, timeouts) with Microsoft.Extensions.Http.Resilience.

Configure services with options classes bound from configuration, as shown in the configuration lessons.

Syntax#

C# Illustrative
builder.Services.AddKeyedScoped<IShippingCalculator, StandardShipping>("standard");
builder.Services.AddKeyedScoped<IShippingCalculator, ExpressShipping>("express");
builder.Services.AddHttpClient<ExchangeRateClient>(c => c.BaseAddress = new Uri("https://rates.example.com"));


public IActionResult Quote([FromKeyedServices("express")] IShippingCalculator express) => …;

Code example: Keyed shipping calculators and a typed HTTP client#

C#Program.cs Illustrative
// Program.cs
var builder = WebApplication.CreateBuilder(args);
builder.Logging.ClearProviders();                                   // keep the demo output clean
builder.Services.AddControllers();
builder.Services.AddKeyedScoped<IShippingCalculator, StandardShipping>("standard");
builder.Services.AddKeyedScoped<IShippingCalculator, ExpressShipping>("express");
builder.Services.AddHttpClient<ExchangeRateClient>(client =>
{
    client.BaseAddress = new Uri("http://127.0.0.1:5210");          // a real app points at the rates provider
    client.Timeout = TimeSpan.FromSeconds(5);
});


var app = builder.Build();
app.MapControllers();
app.MapGet("/fake-rates/{currency}", (string currency) =>          // stands in for the external rates API
    Results.Ok(new { currency, rate = currency == "USD" ? 0.012m : 0.011m }));
app.Urls.Add("http://127.0.0.1:5210");
await app.StartAsync();


using var http = new HttpClient { BaseAddress = new Uri("http://127.0.0.1:5210") };
foreach (var path in new[] { "/api/shipping/quote?weightKg=2&speed=standard", "/api/shipping/quote?weightKg=2&speed=express",
                             "/api/shipping/quote?weightKg=2&speed=express&currency=USD", "/api/shipping/quote?weightKg=2&speed=drone" })
    Console.WriteLine(
quot;
{path,-58} {await (await http.GetAsync(path)).Content.ReadAsStringAsync()}");
await app.StopAsync(); // Services/Shipping.cs public interface IShippingCalculator { decimal Cost(decimal weightKg); } public class StandardShipping : IShippingCalculator { public decimal Cost(decimal weightKg) => 49m + 10m * weightKg; } public class ExpressShipping : IShippingCalculator { public decimal Cost(decimal weightKg) => 149m + 25m * weightKg; } // Services/ExchangeRateClient.cs using System.Net.Http.Json; public record RateResponse(string Currency, decimal Rate); public class ExchangeRateClient(HttpClient http) // typed client: HttpClient comes from the factory { public async Task<decimal> GetRateAsync(string currency) => (await http.GetFromJsonAsync<RateResponse>(
quot;/fake-rates/
{currency}"))!.Rate;
} // Controllers/ShippingController.cs using Microsoft.AspNetCore.Mvc; [ApiController] [Route("api/shipping")] public class ShippingController(IServiceProvider services, ExchangeRateClient rates) : ControllerBase { [HttpGet("quote")] public async Task<IActionResult> Quote(decimal weightKg, string speed, string currency = "INR") { var calculator = services.GetKeyedService<IShippingCalculator>(speed); // choose by key at runtime if (calculator is null) return BadRequest(new { error =
quot;Unknown speed '
{speed}'" });
var costInr = calculator.Cost(weightKg); var cost = currency == "INR" ? costInr : Math.Round(costInr * await rates.GetRateAsync(currency), 2); return Ok(new { speed, currency, cost }); } }
Expected output
/api/shipping/quote?weightKg=2&speed=standard              {"speed":"standard","currency":"INR","cost":69}
/api/shipping/quote?weightKg=2&speed=express               {"speed":"express","currency":"INR","cost":199}
/api/shipping/quote?weightKg=2&speed=express&currency=USD  {"speed":"express","currency":"USD","cost":2.39}
/api/shipping/quote?weightKg=2&speed=drone                 {"error":"Unknown speed 'drone'"}

This example is illustrative. Copy or download it and run it in your own environment; the expected output is shown above.

Step-by-step explanation#

  1. Two IShippingCalculator implementations are registered under the keys "standard" and "express".
  2. The controller picks one at runtime with GetKeyedService; an unknown key returns null → 400.
  3. ExchangeRateClient is a typed client: AddHttpClient configures its base address and timeout and manages the handlers.
  4. For USD, the controller calls the (fake) external rates API through the typed client and converts the price.
  5. Nothing in the controller creates services or HttpClients itself.

Real-world use case#

ShopSmart’s checkout API chooses between Delhivery, Blue Dart and in-house delivery calculators by key, and calls currency, address-validation and payment providers through typed clients with retry policies.

Common mistakes#

new HttpClient() per request

Each instance opens sockets that linger in TIME_WAIT; under load the server runs out. Use IHttpClientFactory.

Switch statements over implementations

switch (speed) { case "express": new ExpressShipping() … } bypasses DI and is hard to extend. Use keyed services.

Scoped services captured by singletons

A singleton holding a scoped DbContext shares it across requests. Keep lifetimes compatible.

Best practices#

  • Typed clients for every external API
  • Keyed services for interchangeable strategies
  • Timeouts and resilience on outgoing calls
  • Options classes for settings

Remember this#

AddKeyed… + [FromKeyedServices("key")] or GetKeyedService.

AddHttpClient<TClient>() → pooled, configurable HttpClient.

Never new HttpClient() per request.

Practice exercise#

Your task: Inject the express calculator directly into an action with [FromKeyedServices("express")] and return its cost for 1 kg.

Need a hint?
public decimal ExpressOneKg([FromKeyedServices("express")] IShippingCalculator express) => express.Cost(1);
Show solution
C# Illustrative
// Controllers/ExpressController.cs
using Microsoft.AspNetCore.Mvc;


public interface IShippingCalculator { decimal Cost(decimal weightKg); }


[ApiController]
[Route("api/express")]
public class ExpressController : ControllerBase
{
    [HttpGet("one-kg")]
    public decimal ExpressOneKg([FromKeyedServices("express")] IShippingCalculator express) => express.Cost(1);
}

Mini quiz#

Dependency Injection quizMultiple choice Question 1 of 3

Why use IHttpClientFactory instead of new HttpClient() in each request?

Score so far: 0/0

Want more? Practise all ASP.NET Core Web API MCQs · ASP.NET Core Web API interview questions

My notes#

Saved in this browser.

Progress is saved in this browser. Sign in to sync.