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.
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.
| Approach | What it depends on | Easy to test? | Works in a background job? |
|---|---|---|---|
Read HttpContext inside every handler | ASP.NET Core (a web detail) | No, you must fake a whole request | No, there is no request |
Read IUserContext (an interface) | Nothing web-specific | Yes, swap in a tiny fake | Yes, 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."
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 type | Meaning | Example value |
|---|---|---|
ClaimTypes.NameIdentifier | The user's unique id | a1b2c3d4-... |
ClaimTypes.Name | The display name | priya |
ClaimTypes.Email | The email address | [email protected] |
ClaimTypes.Role | A role the user has | Admin |
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
Steps
Login
Server checks password once
Token
Claims like user id baked in
Request
Browser sends token each call
ClaimsPrincipal
ASP.NET builds HttpContext.User
IUserContext
Reads the id claim
Use case
Gets a clean Guid
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.
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.
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.
| Situation | What to do |
|---|---|
| Background job tied to a user | Pass 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
Steps
Caller
Handler asks for IUserContext
Container
Looks at what is registered
Pick impl
Web: HttpContext, Worker: job data
Return id
Handler gets a clean Guid
Common mistakes to avoid
A few traps catch beginners. Keep this short list nearby.
- Putting
IUserContextin 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,IHttpContextAccessoris 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 ifUserIdkeeps 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.Useras aClaimsPrincipal. - In Clean Architecture, the Application layer defines a small
IUserContextinterface and never touchesHttpContext. - The real implementation uses
IHttpContextAccessorand readsClaimTypes.NameIdentifier, and it lives in Infrastructure or Presentation. - Register
AddHttpContextAccessor()and addIUserContextas scoped (one per request). HttpContextis 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
- Access HttpContext in ASP.NET Core — Microsoft Learn
- ClaimsPrincipal Class — Microsoft Learn
- Getting the Current User in Clean Architecture — Milan Jovanović
- Master Claims Transformation for ASP.NET Core Authorization — Milan Jovanović
Related Posts
Building Your First Use Case With Clean Architecture in .NET
A beginner-friendly, step-by-step guide to building your first use case in .NET Clean Architecture: command, handler, repository, and endpoint, with diagrams.
Balancing Cross-Cutting Concerns in Clean Architecture (.NET)
Learn how to handle logging, validation, caching, and security in Clean Architecture with .NET, using simple words, diagrams, and real code examples.
Clean Architecture Folder Structure in .NET: A Simple Guide
Learn how to organise a .NET solution with Clean Architecture. Understand the Domain, Application, Infrastructure, and Presentation layers, the dependency rule, and the exact folders, with diagrams and examples.
Why Clean Architecture Is Great for Complex .NET Projects
A friendly guide to why Clean Architecture shines on big, complex .NET projects: testable business rules, swappable infrastructure, and code that stays kind to change.
Clean Architecture in .NET: The Benefits of Structured Software Design
A beginner-friendly guide to Clean Architecture in .NET. Learn the four layers, the dependency rule, and why structured software design keeps your code easy to change.
Screaming Architecture in .NET: Let Your Folders Tell the Story
Learn Screaming Architecture in .NET in plain words. Make your folder structure shout the business purpose, not the framework, with diagrams and real examples.