← Back to Clean Architecture

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.