Entity and Identity
Overview
Section titled “Overview”There are two products with the same name and the same price. Are they the same product? If they need to be managed separately even though all their attributes are identical, how do you distinguish them?
In DDD, an Entity is a domain object distinguished by a unique identifier (Identity). Even if two entities have the same attribute values, they are different entities if their IDs differ, and even if attributes change, they are the same entity if their IDs match. This chapter solves this problem using Functorium’s Entity<TId> and the Ulid-based IEntityId<TId>.
Learning Objectives
Section titled “Learning Objectives”After completing this chapter, you will be able to:
- Explain the principle by which
Entity<TId>automatically provides ID-based equality comparison - Create, restore, and compare Ulid-based identifiers using the
IEntityId<TId>interface - Distinguish the equality differences between Entity and Value Object
What You Will Verify Through Hands-on Practice
Section titled “What You Will Verify Through Hands-on Practice”- ProductId: Ulid-based Entity identifier implementation (manual implementation of code generated by
[GenerateEntityId]) - Product: A domain Entity that inherits
Entity<ProductId>and automatically receives ID-based equality
Core Concepts
Section titled “Core Concepts”Why Is This Needed?
Section titled “Why Is This Needed?”What happens if you compare Entities only by their attributes?
// Same name, same price, but different IDs mean different Entitiesvar product1 = Product.Create("Laptop", 1_500_000m);var product2 = Product.Create("Laptop", 1_500_000m);product1 == product2 // false - different IDs
// Same ID means same Entityvar id = ProductId.New();var productA = Product.CreateFromValidated(id, "Mouse", 25_000m);var productB = Product.CreateFromValidated(id, "Mouse", 25_000m);productA == productB // true - same IDEven if two products have identical names and prices, they are different products if they have different IDs. Conversely, even if attributes change, they are the same product if the ID matches. This is the concept of Entity Identity.
IEntityId Interface
Section titled “IEntityId Interface”So what type should you use for the ID? Functorium provides strongly-typed IDs based on Ulid (Universally Unique Lexicographically Sortable Identifier).
public interface IEntityId<T> : IEquatable<T>, IComparable<T> where T : struct, IEntityId<T>{ Ulid Value { get; } static abstract T New(); // Create new ID static abstract T Create(Ulid id); // Restore from Ulid static abstract T Create(string id); // Restore from string}Using Ulid provides the following benefits:
- Chronological sorting possible (first 48 bits are timestamp)
- String representation is short and URL-safe (26 characters)
- UUID compatible
[GenerateEntityId] Source Generator
Section titled “[GenerateEntityId] Source Generator”In practice, you don’t implement Entity IDs manually but use the [GenerateEntityId] source generator. In this chapter, we manually implement the code that the source generator creates for learning purposes.
Project Description
Section titled “Project Description”Project Structure
Section titled “Project Structure”EntityAndIdentity/├── Program.cs # Demo execution├── ProductId.cs # Ulid-based Entity ID├── Product.cs # Product Entity└── EntityAndIdentity.csproj
EntityAndIdentity.Tests.Unit/├── ProductTests.cs # ID equality, Entity equality tests├── Using.cs├── xunit.runner.json└── EntityAndIdentity.Tests.Unit.csprojCore Code
Section titled “Core Code”ProductId.cs
Section titled “ProductId.cs”Implementing IEntityId<ProductId> completes a Ulid-based strongly-typed identifier. Use New() to create a new ID and Create() to restore from an existing value.
public readonly record struct ProductId : IEntityId<ProductId>{ public Ulid Value { get; }
private ProductId(Ulid value) => Value = value;
public static ProductId New() => new(Ulid.NewUlid()); public static ProductId Create(Ulid id) => new(id); public static ProductId Create(string id) => new(Ulid.Parse(id));
public bool Equals(ProductId other) => Value == other.Value; public int CompareTo(ProductId other) => Value.CompareTo(other.Value); public override int GetHashCode() => Value.GetHashCode(); public override string ToString() => Value.ToString();}Using readonly record struct ensures immutability, and all comparison operations are delegated to the internal Ulid value.
Product.cs
Section titled “Product.cs”Now let’s create an Entity using this ID. Inheriting from Entity<ProductId> automatically provides ID-based equality comparison.
public sealed class Product : Entity<ProductId>{ public string Name { get; private set; } public decimal Price { get; private set; }
private Product(ProductId id, string name, decimal price) { Id = id; Name = name; Price = price; }
public static Product Create(string name, decimal price) { return new Product(ProductId.New(), name, price); }
public static Product CreateFromValidated(ProductId id, string name, decimal price) { return new Product(id, name, price); }}Create() automatically generates a new ID, and CreateFromValidated() restores an Entity with an existing ID. Note that the constructor is private, forcing creation exclusively through factory methods.
Summary at a Glance
Section titled “Summary at a Glance”Entity vs Value Object
Section titled “Entity vs Value Object”Entity and Value Object differ in how equality is determined. Check the key differences in the table below.
| Aspect | Entity | Value Object |
|---|---|---|
| Equality | ID-based | Value-based |
| Mutability | State can change | Immutable |
| Lifecycle | Unique lifecycle | Dependent on Entity |
| Examples | Product, Order | Money, Address |
IEntityId Key Methods
Section titled “IEntityId Key Methods”A summary of the methods provided by the ID type and their purposes.
| Method | Description |
|---|---|
New() | Create a new Ulid-based ID |
Create(Ulid) | Restore ID from Ulid value |
Create(string) | Restore ID from string (DB/API deserialization) |
Value | Access internal Ulid value |
Q1: Why use Ulid instead of Guid?
Section titled “Q1: Why use Ulid instead of Guid?”A: Unlike Guid, Ulid supports chronological sorting. The first 48 bits are millisecond timestamps, providing good DB index performance and natural ordering by creation time. Additionally, its 26-character string representation is shorter than UUID’s 36 characters.
Q2: Why use readonly record struct?
Section titled “Q2: Why use readonly record struct?”A: Entity IDs must be immutable, so readonly struct is used. While record struct auto-generates Equals, GetHashCode, and ToString, we implement them explicitly to meet IEntityId<T> requirements.
Q3: What’s the difference between Create and CreateFromValidated?
Section titled “Q3: What’s the difference between Create and CreateFromValidated?”A: Create is a general factory method that auto-generates a new ID. CreateFromValidated restores an Entity with an already existing ID, used when converting DB data to Entities in the Repository.
Q4: Why use protected init in Entity?
Section titled “Q4: Why use protected init in Entity?”A: The Id property should only be set at creation time, hence init. protected restricts setting to derived classes only, preventing external ID modification.
You’ve learned how to handle Entity identity with IDs. But what happens if external code directly modifies an Entity’s internal state? In the next chapter, we’ll look at how to protect consistency boundaries through Aggregate Root.