Skip to main content
SEMastery
Architecturebeginner

Getting the Current User in Clean Architecture (.NET)

Learn the clean way to read the current logged-in user in .NET Clean Architecture using an IUserContext interface, IHttpContextAccessor, and ClaimsPrincipal.

12 min readUpdated December 13, 2025

A hospital visitor pass

Imagine you walk into a big hospital to visit someone. At the front desk, the guard checks your ID and gives you a small visitor pass on a string. The pass has your name and a number on it.

Now you walk around. The nurse on the third floor does not call the front desk to ask "who is this person?" She just looks at the pass hanging around your neck. The pass already says who you are.

In a .NET app, the current user is exactly like that visitor pass. When someone logs in, the server checks them once and hands them a token full of small facts called claims — like their user id and their name. After that, every part of your code can read the pass instead of asking again.

But here is the careful part. In Clean Architecture, we do not let every room in the hospital reach into the front desk drawer. Instead, we make one clean little window that says "give me the visitor's number," and everyone uses that window. That window is the idea of this whole article.

What problem are we solving?

Almost every real app needs to know who is doing the action. "Who placed this order?" "Whose profile am I editing?" "Did this user create this note, so are they allowed to delete it?"

The user's id lives on the web request. In ASP.NET Core, that web request is the HttpContext. The naive way is to grab HttpContext everywhere and dig out the user id. It works on day one. It hurts you later.

Here is why it hurts.

ApproachWhat it depends onEasy to test?Works in a background job?
Read HttpContext inside every handlerASP.NET Core (a web detail)No, you must fake a whole requestNo, there is no request
Read IUserContext (an interface)Nothing web-specificYes, swap in a tiny fakeYes, plug a different source

Clean Architecture has a simple rule: the inside should not know about the outside. The Application layer (your use cases) and the Domain layer (your business rules) must not import web stuff. HttpContext is web stuff. So we hide it behind an interface.

The big picture in one diagram

Let us see the layers and who is allowed to touch what. Arrows point in the direction of "depends on."

Inner layers define the need; outer layers fill it in.

The trick is dependency inversion. The Application layer says "I need someone to tell me the current user id." It writes down that need as an interface, IUserContext. It does not care how the id is found. The Infrastructure or Presentation layer, which is allowed to touch HttpContext, fills in the real answer.

So the arrow of need points inward, but the arrow of "who imports the web library" points outward. The inside stays clean.

Step 1: Define the interface in the Application layer

We start small. We write what we want, not how we get it. Put this in your Application project.

namespace Application.Abstractions;
 
// This lives in the Application layer.
// It knows nothing about HttpContext or ASP.NET Core.
public interface IUserContext
{
    Guid UserId { get; }
 
    bool IsAuthenticated { get; }
}

Notice what is not here. No HttpContext. No ClaimsPrincipal. No using Microsoft.AspNetCore. Just a plain id and a yes/no flag. Any use case can ask for this and stay clean.

You can shape it to your app. Some apps add string? Email, IReadOnlyList<string> Roles, or Guid? TenantId for multi-tenant systems. Keep it to what your use cases truly need.

Step 2: Where does the id actually live?

Before we write the real code, let us understand the claims. When a user logs in, the server creates a token or cookie. Inside it are claims — little key-value facts.

ASP.NET Core reads that token on every request and builds a ClaimsPrincipal. You find it at HttpContext.User. Each claim has a type and a value.

Claim typeMeaningExample value
ClaimTypes.NameIdentifierThe user's unique ida1b2c3d4-...
ClaimTypes.NameThe display namepriya
ClaimTypes.EmailThe email address[email protected]
ClaimTypes.RoleA role the user hasAdmin

The user id we want is almost always in ClaimTypes.NameIdentifier. We read it like this: httpContext.User.FindFirstValue(ClaimTypes.NameIdentifier). That returns a string, which we then parse into a Guid.

This flow shows how the id travels from login all the way to your use case.

From login to use case

Login
Token
Request
ClaimsPrincipal
IUserContext
Use case

Steps

1

Login

Server checks password once

2

Token

Claims like user id baked in

3

Request

Browser sends token each call

4

ClaimsPrincipal

ASP.NET builds HttpContext.User

5

IUserContext

Reads the id claim

6

Use case

Gets a clean Guid

The user id rides along on the request as a claim.

Step 3: Implement it with IHttpContextAccessor

Now the real work. The implementation lives outside the Application layer — usually in Infrastructure or Presentation. This is the only place allowed to touch HttpContext.

To reach HttpContext from a normal service, ASP.NET Core gives us IHttpContextAccessor. It is a small helper whose only job is to hand you the current request's HttpContext.

using System.Security.Claims;
using Microsoft.AspNetCore.Http;
using Application.Abstractions;
 
namespace Infrastructure.Authentication;
 
public sealed class UserContext : IUserContext
{
    private readonly IHttpContextAccessor _accessor;
 
    public UserContext(IHttpContextAccessor accessor)
    {
        _accessor = accessor;
    }
 
    public Guid UserId =>
        GetUserIdOrNull()
        ?? throw new InvalidOperationException("User is not available.");
 
    public bool IsAuthenticated =>
        _accessor.HttpContext?.User?.Identity?.IsAuthenticated ?? false;
 
    private Guid? GetUserIdOrNull()
    {
        string? raw = _accessor.HttpContext?
            .User
            .FindFirstValue(ClaimTypes.NameIdentifier);
 
        return Guid.TryParse(raw, out Guid id) ? id : null;
    }
}

Read it slowly. The ?. marks (called null-conditional operators) protect us. If there is no request, HttpContext is null, and the whole chain quietly becomes null instead of crashing mid-way. We decide what to do at the end.

For UserId, we choose to throw a clear error if no user is found. That is a fair choice for endpoints that require a logged-in user. If your use case can run for guests, expose a Guid? UserIdOrNull instead and let the caller decide.

Step 4: Register it in dependency injection

An interface and a class do nothing until you wire them up. In Program.cs, tell the container two things: how to find HttpContext, and which class is the IUserContext.

var builder = WebApplication.CreateBuilder(args);
 
// 1. Lets any service reach the current HttpContext.
builder.Services.AddHttpContextAccessor();
 
// 2. When something asks for IUserContext, give it UserContext.
builder.Services.AddScoped<IUserContext, UserContext>();
 
var app = builder.Build();

We register IUserContext as scoped. Scoped means "one instance per web request." That matches a request perfectly: each request has its own user, so each request gets its own UserContext. Do not register it as a singleton — a single shared instance across all users would be a serious bug.

This sequence shows what happens at runtime when a request comes in.

The container builds a fresh UserContext per request and injects it.

Step 5: Use it inside a use case

Here is the payoff. A use case in the Application layer just asks for IUserContext in its constructor. It never sees the web.

using Application.Abstractions;
 
namespace Application.Orders;
 
public sealed class PlaceOrderHandler
{
    private readonly IUserContext _user;
    private readonly IOrderRepository _orders;
 
    public PlaceOrderHandler(IUserContext user, IOrderRepository orders)
    {
        _user = user;
        _orders = orders;
    }
 
    public async Task<Guid> Handle(PlaceOrderCommand command)
    {
        // Clean and simple. No HttpContext anywhere.
        Guid customerId = _user.UserId;
 
        var order = Order.Create(customerId, command.Items);
        await _orders.AddAsync(order);
 
        return order.Id;
    }
}

Look how readable this is. _user.UserId reads like plain English. The handler has no idea that the id came from a JWT claim on an HTTP request. It could come from anywhere. That is the whole point.

Why this is so easy to test

Because IUserContext is just an interface, a test can hand the handler a fake one. No web server, no fake request, no HttpContext gymnastics.

public sealed class FakeUserContext : IUserContext
{
    public Guid UserId { get; init; } = Guid.NewGuid();
    public bool IsAuthenticated => true;
}
 
// In a test:
var handler = new PlaceOrderHandler(
    new FakeUserContext { UserId = knownId },
    fakeOrderRepository);

This is the reward for hiding HttpContext behind an interface. Testing the rule "an order belongs to the user who placed it" takes three lines, not thirty.

The null trap: requests vs. background work

This is the mistake almost everyone hits, so let us be very clear about it.

HttpContext lives only during a real web request. The moment you step outside a request, it is gone.

HttpContext exists only inside the request box.

So if a background job, a hosted service, a message consumer, or a startup task calls a handler that needs IUserContext, the real UserContext will find a null HttpContext. Our UserId property would then throw "User is not available."

That is actually good — it fails loudly instead of silently using a wrong user. But you must plan for it. You have two clean choices.

SituationWhat to do
Background job tied to a userPass the user id into the job's data, and register a different IUserContext that returns it
Truly system work (no user)Use a SystemUserContext that returns a fixed "system" id, or skip user checks entirely

The beauty is that the Application layer never changes. You only swap the implementation behind the interface. That is dependency inversion paying off again.

A second flow: how the request picks an implementation

It helps to picture the decision the container makes. In a web request it uses the HTTP-based UserContext. In a worker it can use a different one bound at startup.

Choosing the right IUserContext

Caller
Container
Pick impl
Return id

Steps

1

Caller

Handler asks for IUserContext

2

Container

Looks at what is registered

3

Pick impl

Web: HttpContext, Worker: job data

4

Return id

Handler gets a clean Guid

Same interface, different source depending on where the code runs.

Common mistakes to avoid

A few traps catch beginners. Keep this short list nearby.

  • Putting IUserContext in the wrong project. The interface belongs in the Application layer. The class belongs in Infrastructure or Presentation. If your Application project references ASP.NET Core, something is in the wrong place.
  • Trusting the user id blindly for permission. Knowing who the user is differs from checking what they may do. Use the id to load the resource, then check ownership or roles before allowing the action.
  • Registering it as a singleton. It must be scoped (per request). A singleton would leak one user's identity into other users' requests.
  • Forgetting AddHttpContextAccessor(). Without it, IHttpContextAccessor is not available and your app throws at startup or returns null.
  • Reading the wrong claim type. Make sure the id is really under ClaimTypes.NameIdentifier. Some identity providers put it under "sub". Check your token if UserId keeps coming back empty.

A small upgrade: caching the full user

Sometimes you need more than an id — you want the whole user record from the database. You do not want to hit the database on every single call within one request.

A neat pattern is a provider that reads the id from claims, loads the user once, and caches it for the lifetime of that request (because the service is scoped).

public sealed class CurrentUserProvider : ICurrentUserProvider
{
    private readonly IUserContext _userContext;
    private readonly IUserRepository _users;
    private User? _cached;
 
    public CurrentUserProvider(IUserContext userContext, IUserRepository users)
    {
        _userContext = userContext;
        _users = users;
    }
 
    public async Task<User> GetAsync()
    {
        // Scoped lifetime means _cached survives the whole request.
        _cached ??= await _users.GetByIdAsync(_userContext.UserId);
        return _cached;
    }
}

Because the service is scoped, _cached is filled at most once per request. The second and third calls return the stored object for free. Simple and fast.

Quick recap

  • The current user is like a hospital visitor pass: checked once at login, then read everywhere.
  • The user's id lives in claims on the request, reachable through HttpContext.User as a ClaimsPrincipal.
  • In Clean Architecture, the Application layer defines a small IUserContext interface and never touches HttpContext.
  • The real implementation uses IHttpContextAccessor and reads ClaimTypes.NameIdentifier, and it lives in Infrastructure or Presentation.
  • Register AddHttpContextAccessor() and add IUserContext as scoped (one per request).
  • HttpContext is null outside a web request, so plan for background jobs by swapping the implementation behind the interface.
  • This design makes your use cases clean, readable, and easy to test with a tiny fake.

References and further reading

Related Posts