ASP.NET Core for Beginners: HttpContext, IHttpContextAccessor, IConfiguration & IOptions<T>

A hands-on guide using a real ProductAPI project — with every concept explained line by line.


What You'll Learn

Concept What It Does
HttpContext Access request, response, headers, user & IP inside a controller
IHttpContextAccessor Access HttpContext from services outside a controller
IConfiguration Read raw values from appsettings.json by key path
IOptions<T> Bind a config section to a strongly-typed C# class

All examples come from a single working project — a ProductController with a GetNetworkService helper. By the end you'll understand every line of both files.


📁 Project Structure We're Working With

ProductAPI/
├── Controllers/
│   └── ProductController.cs
├── Services/
│   └── GetNetworkService.cs
├── Options/
│   └── OpenApiOptions.cs
├── appsettings.json
└── Program.cs

Chapter 1 — HttpContext

What Is HttpContext?

Every HTTP request that arrives at your API is wrapped in an object called HttpContext. Think of it as a box that holds everything about the current web request and response — headers, cookies, the user's identity, the IP address, and more.

💡 Simple Analogy: Imagine you run a post office. Every parcel arrives with a label (headers), a sender address (IP), recipient info (URL), the parcel contents (request body), and a reply envelope (response). HttpContext is that entire parcel + reply envelope combined.

Where Does HttpContext Come From?

In ASP.NET Core, the framework creates a new HttpContext for every incoming request. Because your controller inherits from ControllerBase, it already has a property called HttpContextno injection needed.

// ProductController.cs
[ApiController]
[Route("api/[controller]")]
public class ProductController : ControllerBase   // <-- inherits ControllerBase
{
    // ControllerBase gives you 'HttpContext' for free.
    // No constructor injection needed for HttpContext itself.
}

Reading Request Headers

Headers are key-value pairs sent by the client (browser, Postman, mobile app, etc.). Common ones include Content-Type, Authorization, and custom ones like X-Custom-Header.

// Inside Get() in ProductController.cs

var headers = HttpContext.Request.Headers;

foreach (var header in headers)
{
    Console.WriteLine($"{header.Key}: {header.Value}");

    // Check for a specific header
    if (header.Key == "X-Custom-Header")
    {
        Console.WriteLine($"Custom Header Value: {header.Value}");
    }
}

📝 Key Points:

  • HttpContext.Request.Headers is a dictionary of all incoming headers.

  • Keys are case-insensitive (X-Custom-Header = x-custom-header).

  • Always check if a header exists before reading its value.

Writing Response Headers

You can add headers to your response — for example, a timestamp showing when the response was generated.

// Add a custom header to the HTTP response
HttpContext.Response.Headers.Add("X-Response-Time", DateTime.UtcNow.ToString("o"));

// 'o' = ISO 8601 format, e.g. 2025-06-01T12:00:00.000Z

// Log all response headers
foreach (var header in HttpContext.Response.Headers)
{
    Console.WriteLine($"{header.Key}: {header.Value}");
}

Getting the Logged-In User

HttpContext.User holds the identity of the currently authenticated user. If no authentication is configured, the user will be "Anonymous".

// Get the username of the logged-in user (or 'Anonymous' if not logged in)
var user = HttpContext.User.Identity?.Name ?? "Anonymous";

// The ?? operator means: "if null, use this fallback value"
// Identity?.Name uses ?. to safely handle the case where Identity is null

Console.WriteLine($"Session User: {user}");

Quick Reference — HttpContext Properties

Property Type What You Get
HttpContext.Request.Headers IHeaderDictionary All incoming request headers
HttpContext.Response.Headers IHeaderDictionary Response headers you can add to
HttpContext.User.Identity?.Name string? Logged-in username (or null)
HttpContext.Connection.RemoteIpAddress IPAddress? Client's IP address
HttpContext.Request.Path PathString URL path, e.g. /api/product
HttpContext.Request.Method string HTTP verb: GET, POST, etc.

Chapter 2 — IHttpContextAccessor

The Problem: Services Don't Have HttpContext

Controllers inherit ControllerBase and get HttpContext automatically. But what about a helper service class like GetNetworkService? Service classes do NOT inherit ControllerBase, so they have no direct access to HttpContext.

// ❌ This will NOT compile
public class GetNetworkService
{
    public string GetUserIP()
    {
        // ERROR: 'HttpContext' does not exist in this context
        return HttpContext.Connection.RemoteIpAddress?.ToString();
    }
}

The solution is to inject IHttpContextAccessor — an interface provided by ASP.NET Core that lets any class access the current HttpContext.

The GetNetworkService — Full Walkthrough

public class GetNetworkService
{
    // Step 1: Declare a field to store the accessor
    public readonly IHttpContextAccessor _httpContextAccessor;

    // Step 2: Receive IHttpContextAccessor via constructor injection
    public GetNetworkService(IHttpContextAccessor httpContextAccessor)
    {
        _httpContextAccessor = httpContextAccessor;
    }

    // Step 3: Use it — access HttpContext through the accessor
    public string GetUserIP()
    {
        return _httpContextAccessor.HttpContext
                   ?.Connection.RemoteIpAddress
                   ?.ToString() ?? "Unknown";
    }
}

Breaking Down the IP Address Line

Code Part Meaning
_httpContextAccessor.HttpContext Get the current HTTP context (may be null outside a request)
?.Connection Safe navigation: only proceed if HttpContext is not null
?.RemoteIpAddress Safe navigation: get IP (null if not available)
.ToString() Convert IPAddress object to a readable string like 127.0.0.1
?? "Unknown" Fallback value if anything above returned null

Registering the Service — Program.cs

For dependency injection to work, you must register both IHttpContextAccessor and your service in Program.cs:

// Program.cs

var builder = WebApplication.CreateBuilder(args);

// REQUIRED: Register IHttpContextAccessor with the DI container
builder.Services.AddHttpContextAccessor();

// Register your custom service
builder.Services.AddScoped<GetNetworkService>();

builder.Services.AddControllers();
var app = builder.Build();
app.MapControllers();
app.Run();

⚠️ Common Mistake — Forgetting to Register

If you forget builder.Services.AddHttpContextAccessor(), you will get:

InvalidOperationException: Unable to resolve service for type
'Microsoft.AspNetCore.Http.IHttpContextAccessor'

Always register it before building the app.

Using the Service in the Controller

// ProductController.cs — constructor receives GetNetworkService
public class ProductController : ControllerBase
{
    private readonly GetNetworkService _myService;

    public ProductController(GetNetworkService myService)
    {
        _myService = myService;
    }

    [HttpGet("getall")]
    public ActionResult<List<Product>> Get()
    {
        // Call the service — it internally uses IHttpContextAccessor
        Console.WriteLine($"Client IP: {_myService.GetUserIP()}");
        return Ok(products);
    }
}

HttpContext vs IHttpContextAccessor — Side by Side

HttpContext (in Controller) IHttpContextAccessor (in Service)
Available via ControllerBase Injected via constructor
Use directly: HttpContext.X Use via: _accessor.HttpContext?.X
Always available in controller May be null outside a request
No registration needed Requires AddHttpContextAccessor()

Chapter 3 — IConfiguration

What Is IConfiguration?

IConfiguration is ASP.NET Core's built-in way to read settings from appsettings.json (and environment variables, command-line args, etc.). You inject it into your controller or service and then read values using key paths separated by colons.

The appsettings.json File

{
  "Logging": {
    "LogLevel": {
      "Default": "Information",
      "Microsoft.AspNetCore": "Warning"
    }
  },
  "AllowedHosts": "*",
  "AppSettings": {
    "AppName": "Products API"
  },
  "OpenApi": {
    "Title": "OpenApi",
    "Version": "v1",
    "BaseUrl": "https://api.openai.com/v1",
    "Advanced": {
      "IncludeRequestingHost": true,
      "IncludeRequestingUser": true,
      "IncludeRequestingUserAgent": true
    }
  }
}

Injecting IConfiguration

// ProductController.cs
public class ProductController : ControllerBase
{
    private readonly IConfiguration _configuration;   // declare field

    public ProductController(IConfiguration configuration)  // inject
    {
        _configuration = configuration;
    }
}

No Registration Needed

Unlike GetNetworkService, IConfiguration is registered automatically by WebApplication.CreateBuilder(args) — you do NOT need to add anything to Program.cs. Just inject it and use it.

Reading Values — Key Paths with Colons

To navigate nested JSON, separate levels with a colon (:). Think of it like a file path, but with colons instead of slashes.

// Reading individual values from IConfiguration

// Reads "Products API" from AppSettings > AppName
var appName = _configuration["AppSettings:AppName"];

// Reads "OpenApi" from OpenApi > Title
var title = _configuration["OpenApi:Title"];

// Reads "v1" from OpenApi > Version
var version = _configuration["OpenApi:Version"];

// Reads "true" (as a string!) from OpenApi > Advanced > IncludeRequestingHost
var includeHost = _configuration["OpenApi:Advanced:IncludeRequestingHost"];

Iterating Over a Section

You can read an entire section at once using GetSection(). The code in our ProductController loops over all keys in the OpenApisection:

// Get all key-value pairs under the "OpenApi" section
foreach (var setting in _configuration.GetSection("OpenApi").AsEnumerable())
{
    if (!string.IsNullOrWhiteSpace(setting.Value))
    {
        Console.WriteLine($"{setting.Key}: {setting.Value}");
    }
}

// Console output:
// OpenApi:Title: OpenApi
// OpenApi:Version: v1
// OpenApi:BaseUrl: https://api.openai.com/v1
// OpenApi:Advanced:IncludeRequestingHost: True
// OpenApi:Advanced:IncludeRequestingUser: True
// OpenApi:Advanced:IncludeRequestingUserAgent: True

IConfiguration Key Path Cheatsheet

JSON Path IConfiguration Key Returns
AppSettings > AppName "AppSettings:AppName" "Products API"
OpenApi > Title "OpenApi:Title" "OpenApi"
OpenApi > Version "OpenApi:Version" "v1"
OpenApi > Advanced > IncludeRequestingHost "OpenApi:Advanced:IncludeRequestingHost" "True"

⚠️ IConfiguration Always Returns Strings

Even if your JSON value is true (boolean) or 42 (number), IConfiguration returns it as a string. To get a boolean, use:

bool flag = _configuration.GetValue<bool>("OpenApi:Advanced:IncludeRequestingHost");

This is one of the reasons IOptions<T> (Chapter 4) is often preferred.


Chapter 4 — IOptions

The Problem With Raw IConfiguration

Using IConfiguration works, but has drawbacks: string keys can have typos, values are always strings so you must manually convert types, and there is no IntelliSense. IOptions<T> solves all of these.

IConfiguration (raw) IOptions (strongly typed)
_config["OpenApi:Title"] _options.Value.Title
Returns string — must convert Returns correct type (bool, int…)
Typos only fail at runtime Typos caught at compile time
No IntelliSense for keys Full IntelliSense in IDE
No registration needed Requires 2 lines in Program.cs

Step 1 — Create the Options Classes

Create a plain C# class that mirrors the shape of your JSON section. Each property name must match the JSON key exactly (case-insensitive).

// OpenApiOptions.cs
namespace ProductAPI;

public class OpenApiOptions
{
    // SectionName is a constant holding the JSON section key.
    // This avoids 'magic strings' scattered around your code.
    public const string SectionName = "OpenApi";

    // These properties map to OpenApi > Title, Version, BaseUrl in JSON
    public string Title { get; set; } = string.Empty;
    public string Version { get; set; } = string.Empty;
    public string BaseUrl { get; set; } = string.Empty;

    // Nested class maps to the 'Advanced' sub-object
    public OpenApiAdvancedOptions Advanced { get; set; } = new();
}

public class OpenApiAdvancedOptions
{
    // These are booleans — IOptions handles the conversion automatically!
    public bool IncludeRequestingHost { get; set; }
    public bool IncludeRequestingUser { get; set; }
    public bool IncludeRequestingUserAgent { get; set; }
}

Step 2 — Register in Program.cs

Tell ASP.NET Core to bind the "OpenApi" section of appsettings.json to your OpenApiOptions class:

// Program.cs
var builder = WebApplication.CreateBuilder(args);

// Bind the 'OpenApi' JSON section to OpenApiOptions class
builder.Services.Configure<OpenApiOptions>(
    builder.Configuration.GetSection(OpenApiOptions.SectionName)
);

// Note: OpenApiOptions.SectionName is just the string "OpenApi"
// Using the constant prevents typos.

builder.Services.AddControllers();
var app = builder.Build();
app.Run();

Step 3 — Inject IOptions into the Controller

// ProductController.cs
using Microsoft.Extensions.Options;     // <-- required using statement

public class ProductController : ControllerBase
{
    private readonly OpenApiOptions _openApiOptions;

    public ProductController(
        IOptions<OpenApiOptions> openApiOptions)  // <-- inject
    {
        _openApiOptions = openApiOptions.Value;   // .Value unwraps the options
    }
}

💡 Why .Value?

IOptions<T> is a wrapper. The actual OpenApiOptions object lives inside .Value. You can either call .Value each time, or store it once in the constructor (as shown above). Storing it is the recommended pattern.

Step 4 — Read the Values

// Inside Get(int id) in ProductController.cs

Console.WriteLine($"Title: {_openApiOptions.Title}");
Console.WriteLine($"Version: {_openApiOptions.Version}");
Console.WriteLine($"BaseUrl: {_openApiOptions.BaseUrl}");

// Nested advanced options — full dot-notation access
Console.WriteLine($"IncludeRequestingHost: {_openApiOptions.Advanced.IncludeRequestingHost}");
Console.WriteLine($"IncludeRequestingUser: {_openApiOptions.Advanced.IncludeRequestingUser}");
Console.WriteLine($"IncludeRequestingUserAgent: {_openApiOptions.Advanced.IncludeRequestingUserAgent}");

// Console output:
// Title: OpenApi
// Version: v1
// BaseUrl: https://api.openai.com/v1
// IncludeRequestingHost: True    <-- already a bool, no conversion!
// IncludeRequestingUser: True
// IncludeRequestingUserAgent: True

How JSON Maps to C# Class

appsettings.json C# Property Type
OpenApi > Title OpenApiOptions.Title string
OpenApi > Version OpenApiOptions.Version string
OpenApi > BaseUrl OpenApiOptions.BaseUrl string
OpenApi > Advanced > IncludeRequestingHost OpenApiAdvancedOptions.IncludeRequestingHost bool
OpenApi > Advanced > IncludeRequestingUser OpenApiAdvancedOptions.IncludeRequestingUser bool
OpenApi > Advanced > IncludeRequestingUserAgent OpenApiAdvancedOptions.IncludeRequestingUserAgent bool

Chapter 5 — Putting It All Together

Complete Program.cs

// Program.cs — Complete setup
using ProductAPI;

var builder = WebApplication.CreateBuilder(args);

// ── 1. IHttpContextAccessor ──────────────────────────────────────────
//    Required so GetNetworkService can access HttpContext outside a controller
builder.Services.AddHttpContextAccessor();

// ── 2. Custom Services ───────────────────────────────────────────────
//    Register GetNetworkService with Scoped lifetime
//    (new instance per HTTP request)
builder.Services.AddScoped<GetNetworkService>();

// ── 3. IOptions<T> ───────────────────────────────────────────────────
//    Bind the 'OpenApi' section of appsettings.json to OpenApiOptions
builder.Services.Configure<OpenApiOptions>(
    builder.Configuration.GetSection(OpenApiOptions.SectionName)
);

// ── 4. Standard MVC ──────────────────────────────────────────────────
builder.Services.AddControllers();
builder.Services.AddEndpointsApiExplorer();

var app = builder.Build();
app.UseHttpsRedirection();
app.UseAuthorization();
app.MapControllers();
app.Run();

Complete Controller Constructor

// ProductController.cs — constructor receives all four dependencies
public class ProductController : ControllerBase
{
    private readonly GetNetworkService _myService;       // uses IHttpContextAccessor internally
    private readonly IConfiguration _configuration;      // raw config access
    private readonly OpenApiOptions _openApiOptions;     // strongly-typed config

    public ProductController(
        GetNetworkService myService,
        IConfiguration configuration,
        IOptions<OpenApiOptions> openApiOptions)
    {
        _myService = myService;
        _configuration = configuration;
        _openApiOptions = openApiOptions.Value;
    }

    // HttpContext is available directly — no constructor injection needed.
}

How Everything Connects

HTTP Request arrives
        │
        ▼
ASP.NET Core creates HttpContext
        │
        ▼
DI Container injects:
  ├── IConfiguration          (reads appsettings.json)
  ├── IOptions<OpenApiOptions> (strongly-typed config)
  └── GetNetworkService        (uses IHttpContextAccessor internally)
        │
        ▼
ProductController runs
  ├── Uses HttpContext directly (headers, user, response)
  └── Calls _myService.GetUserIP()
              │
              ▼
        IHttpContextAccessor → HttpContext.Connection.RemoteIpAddress

Final Checklist

  • [ ] Create OpenApiOptions class — match property names to JSON keys exactly

  • [ ] Register IHttpContextAccessorbuilder.Services.AddHttpContextAccessor()

  • [ ] Register GetNetworkServicebuilder.Services.AddScoped<GetNetworkService>()

  • [ ] Register IOptions<T>builder.Services.Configure<OpenApiOptions>(...)

  • [ ] Inject in constructor — receive IConfiguration, IOptions<T>, GetNetworkService

  • [ ] Use HttpContext directly — in controller: .Request.Headers, .User.Identity, etc.

  • [ ] Call .Value on IOptions<T> — store openApiOptions.Value in a field


Appendix — Quick Reference Cards

All Four Concepts at a Glance

Concept Inject Via Registration in Program.cs Use Case
HttpContext Inherited (ControllerBase) None needed Headers, user, IP inside controller
IHttpContextAccessor Constructor injection AddHttpContextAccessor() HttpContext access in services
IConfiguration Constructor injection None (auto) Read raw string values from JSON
IOptions<T> IOptions<MyClass> Configure<T>(section) Strongly-typed config binding

Service Lifetime Cheatsheet

Lifetime Method When to Use
Transient AddTransient<T>() New instance every time the service is requested
Scoped AddScoped<T>() One instance per HTTP request (most common for web APIs)
Singleton AddSingleton<T>() One instance for the entire application lifetime

📌 Rule of Thumb: Use AddScoped for most web API services (like GetNetworkService). Use AddSingleton only for stateless, thread-safe services (e.g. caches). Use AddTransient for lightweight, stateless utilities.