Clean Architecture
Entities
Business rules a change of framework, UI, or database has no reason to touch.
Entities hold what Martin calls Enterprise Business Rules: the most general, most stable statements a system makes about its subject. If you are not writing for an enterprise, they are simply the business objects of your one application. Either way, the test stays the same. No operational change — a new endpoint, a redesigned screen, a swapped database — should touch them.
Behaviour lives on the object, not around it
CatalogItem in eShopOnWeb keeps its price and name behind private setters. Nothing outside the class can assign them directly:
public class CatalogItem : BaseEntity, IAggregateRoot
{
public string Name { get; private set; }
public string Description { get; private set; }
public decimal Price { get; private set; }Private setters keep the rule on the object. A controller cannot assign Price directly.
public void UpdateDetails(CatalogItemDetails details)
{
Guard.Against.NullOrEmpty(details.Name, nameof(details.Name));
Guard.Against.NullOrEmpty(details.Description, nameof(details.Description));
Guard.Against.NegativeOrZero(details.Price, nameof(details.Price));
Name = details.Name;
Description = details.Description;
Price = details.Price;
}UpdateDetails is the only way in, and it refuses a negative price before any field changes.
}
A controller cannot set Price = -5. It has to call UpdateDetails, and UpdateDetails refuses. That rule — a catalog item cannot cost a negative amount — is enforced once, on the object, regardless of whether the caller is a web controller, a console command, or a test.
The one concession an ORM asks for
The purity has a limit. Order carries a private, parameterless constructor that exists for one reason: Entity Framework needs it to materialise a row back into an object.
public class Order : BaseEntity, IAggregateRoot
{
#pragma warning disable CS8618
private Order() {}The empty constructor exists for Entity Framework to materialise a row. Application code should never call it.
public Order(string buyerId, Address shipToAddress, List<OrderItem> items)
{
Guard.Against.NullOrEmpty(buyerId, nameof(buyerId));
BuyerId = buyerId;
ShipToAddress = shipToAddress;
_orderItems = items;
}The real constructor is where the entity’s own rules run — still without referencing EF.
}
Order still does not reference Microsoft.EntityFrameworkCore or know a DbContext exists anywhere. But its shape — a private constructor no application code would ever call — is bent slightly by the tool that will load it later. That is the honest version of the rule: dependencies stay pointed inward, but “inward” does not mean an entity is deaf to every constraint its eventual persistence will impose on its shape.
Once an entity’s rule needs data or behaviour beyond itself, something has to orchestrate the sequence. That orchestration, one job at a time, is Use Cases.