Clean Architecture
Use Cases
Orchestrating entities to do one specific job, and nothing about how that job gets triggered.
A use case is application-specific: it exists because this system does this job, not because the enterprise has this concept. It orchestrates entities to get the job done, and it changes when the job changes — not when the screen that starts it gets redesigned.
One job, named for what it does
IBasketService.TransferBasketAsync in eShopOnWeb has one job: fold an anonymous basket into a signed-in user’s basket.
public async Task TransferBasketAsync(string anonymousId, string userName)
{
var anonymousBasketSpec = new BasketWithItemsSpecification(anonymousId);
var anonymousBasket = await _basketRepository.FirstOrDefaultAsync(anonymousBasketSpec);
if (anonymousBasket == null) return;The use case loads baskets by id. A missing anonymous cart is a no-op, not a UI problem.
var userBasketSpec = new BasketWithItemsSpecification(userName);
var userBasket = await _basketRepository.FirstOrDefaultAsync(userBasketSpec);
if (userBasket == null)
{
userBasket = new Basket(userName);
await _basketRepository.AddAsync(userBasket);
}Creating the signed-in basket is still the same job — not a login page concern.
foreach (var item in anonymousBasket.Items)
{
userBasket.AddItem(item.CatalogItemId, item.UnitPrice, item.Quantity);
}Merge goes through Basket.AddItem, so the entity keeps its own rules.
await _basketRepository.UpdateAsync(userBasket);
await _basketRepository.DeleteAsync(anonymousBasket);Persist and delete are the use case finishing the job, still without an HttpContext.
}
Two basket ids in, one merged basket out. Nothing here mentions a browser, a cookie, or who is logged in. Those are somebody else’s problem.
The caller can be anything
That somebody else is a Razor Page. LoginModel calls the use case once sign-in succeeds:
private async Task TransferAnonymousBasketToUserAsync(string? userName)
{
if (Request.Cookies.ContainsKey(Constants.BASKET_COOKIENAME))
{
var anonymousId = Request.Cookies[Constants.BASKET_COOKIENAME];Cookies and HttpContext stay in the Razor Page. They are how this delivery mechanism finds the anonymous id.
if (Guid.TryParse(anonymousId, out var _))
{
Guard.Against.NullOrEmpty(userName, nameof(userName));
await _basketService.TransferBasketAsync(anonymousId, userName);The use case receives two strings. It does not know a login page exists.
}
Response.Cookies.Delete(Constants.BASKET_COOKIENAME);Clearing the cookie is leftover delivery work after the job has been handed off.
}
}
Reading a cookie, parsing a GUID, clearing the cookie afterwards — that is all delivery-mechanism housekeeping, and it stays in the Razor Page. TransferBasketAsync receives two plain strings. It would run identically if the caller were an API endpoint, a background job merging carts after an outage, or a test with no HttpContext in sight. The use case does not know a login page exists; it only knows two basket ids.
That housekeeping — reading the cookie, owning the HttpContext, deciding what the browser sees next — is the job of Interface Adapters.