Skip to main content
Version: 2.4.1

Entities & Aggregate Roots

In Domain-Driven Design, entities are objects with a distinct identity that persists over time. Aggregate roots are the primary entities that serve as the entry point for a cluster of related objects, enforcing consistency boundaries. RCommon provides a hierarchy of base classes that give you identity, equality, domain event tracking, and optimistic concurrency out of the box.

Installation

NuGet Package
dotnet add package RCommon.Entities

Type Hierarchy

RCommon provides several base types for building your domain model. Understanding when to use each is important:

ITrackedEntity
IBusinessEntity
BusinessEntity — composite keys, event tracking (no explicit key type)
BusinessEntity<TKey> — single typed key, event tracking

IAggregateRoot : IBusinessEntity
IAggregateRoot<TKey> : IAggregateRoot, IBusinessEntity<TKey>
AggregateRoot<TKey> : BusinessEntity<TKey>, IAggregateRoot<TKey>

DomainEntity<TKey> — child entities inside an aggregate (identity only, no event tracking)

BusinessEntity

BusinessEntity is the foundational base class for all entities. It provides local (transactional) event tracking without requiring a specific key type — useful for entities with composite keys.

BusinessEntity<TKey> extends this with a strongly-typed Id property and is the type you will most commonly extend when building aggregate roots or tracked persistence entities.

// BusinessEntity<TKey> gives you a typed Id and event tracking
public abstract class BusinessEntity<TKey> : BusinessEntity, IBusinessEntity<TKey>
where TKey : IEquatable<TKey>
{
public virtual TKey Id { get; protected set; }
public override object[] GetKeys() => new object[] { Id };
}

Key characteristics:

  • GetKeys() — returns the entity's key values as an object array, enabling generic repository operations
  • LocalEvents — a read-only collection of events accumulated during a unit of work, dispatched after persistence
  • AllowEventTracking — opt-out flag; set to false to suppress event dispatch for a given entity
  • EntityEquals(IBusinessEntity other) — identity-based equality via binary comparison

AggregateRoot

AggregateRoot<TKey> extends BusinessEntity<TKey> and is the entry point for a consistency boundary in your domain. It adds:

  • Domain event management — typed AddDomainEvent, RemoveDomainEvent, and ClearDomainEvents methods
  • Optimistic concurrency — a Version property decorated with [ConcurrencyCheck]
public abstract class AggregateRoot<TKey> : BusinessEntity<TKey>, IAggregateRoot<TKey>
where TKey : IEquatable<TKey>
{
[ConcurrencyCheck]
public virtual int Version { get; protected set; }

[NotMapped]
public IReadOnlyCollection<IDomainEvent> DomainEvents { get; }

protected void AddDomainEvent(IDomainEvent domainEvent) { ... }
protected void RemoveDomainEvent(IDomainEvent domainEvent) { ... }
public void ClearDomainEvents() { ... }
protected void IncrementVersion() => Version++;
}

Creating an Aggregate Root

Extend AggregateRoot<TKey> and raise domain events inside your business methods rather than from application services:

public class Order : AggregateRoot<Guid>
{
public string CustomerId { get; private set; }
public OrderStatus Status { get; private set; }
private readonly List<OrderLine> _lines = new();
public IReadOnlyCollection<OrderLine> Lines => _lines.AsReadOnly();

protected Order() { } // required for ORM deserialization

public Order(Guid id, string customerId)
{
Id = id;
CustomerId = customerId;
Status = OrderStatus.Draft;
IncrementVersion();
AddDomainEvent(new OrderCreatedEvent(id, customerId));
}

public void AddLine(string productId, int quantity, decimal unitPrice)
{
_lines.Add(new OrderLine(productId, quantity, unitPrice));
IncrementVersion();
AddDomainEvent(new OrderLineAddedEvent(Id, productId, quantity));
}

public void Submit()
{
if (Status != OrderStatus.Draft)
throw new InvalidOperationException("Only draft orders can be submitted.");

Status = OrderStatus.Submitted;
IncrementVersion();
AddDomainEvent(new OrderSubmittedEvent(Id));
}
}

DomainEntity

DomainEntity<TKey> is for child entities that live inside an aggregate boundary. It provides identity-based equality but does not participate in event tracking. All domain events must be raised on the aggregate root.

public abstract class DomainEntity<TKey> : IEquatable<DomainEntity<TKey>>
where TKey : IEquatable<TKey>
{
public virtual TKey Id { get; protected set; }

public bool IsTransient() => Id is null || Id.Equals(default);
// Equals, GetHashCode, == and != are implemented
}

Use DomainEntity for objects within the aggregate that have their own identity but should not be accessed directly outside the aggregate root:

public class OrderLine : DomainEntity<Guid>
{
public string ProductId { get; private set; }
public int Quantity { get; private set; }
public decimal UnitPrice { get; private set; }

protected OrderLine() { }

public OrderLine(string productId, int quantity, decimal unitPrice)
{
Id = Guid.NewGuid();
ProductId = productId;
Quantity = quantity;
UnitPrice = unitPrice;
}

public decimal LineTotal => Quantity * UnitPrice;
}

Note: Because DomainEntity<TKey> does not implement IBusinessEntity, the InMemoryEntityEventTracker's object graph walker will not traverse it. Events raised by child entities must be delegated up to the aggregate root.

Identity and Equality

Entities in RCommon use identity-based equality. Two entity instances are equal if and only if they are the same type and have the same key value.

var a = new Order(id: guid1, customerId: "C1");
var b = new Order(id: guid1, customerId: "C2");

bool same = a.EntityEquals(b); // true — same Id

Transient entities (those without an assigned persistent identity) are never equal to any other entity, including themselves by reference:

var transient = new Order(); // Id is default(Guid)
bool isTransient = transient.IsTransient(); // true

IAggregateRoot Interfaces

Two interfaces are provided for infrastructure scenarios:

  • IAggregateRoot — non-generic marker; useful for repository filtering and middleware
  • IAggregateRoot<TKey> — generic version with where TKey : IEquatable<TKey>

Both expose Version and DomainEvents for infrastructure code that needs to dispatch events or enforce concurrency after persistence operations.

API Summary

TypeDescription
BusinessEntityAbstract base with composite key support and transactional event tracking
BusinessEntity<TKey>Abstract base with typed Id and event tracking
AggregateRoot<TKey>Extends BusinessEntity<TKey>; adds domain events and optimistic concurrency
DomainEntity<TKey>Lightweight child entity with identity equality; no event tracking
IBusinessEntityInterface exposing key access, local events, and equality
IBusinessEntity<TKey>Typed key interface extending IBusinessEntity
IAggregateRootNon-generic marker interface for infrastructure use
IAggregateRoot<TKey>Generic aggregate root interface
ITrackedEntityMinimal interface controlling whether an entity participates in event tracking
RCommonRCommon