Entities¶
Info
At ConnectSoft, Entities are modeled with utmost precision:
they are not just data holders, but lifelong identities carrying
meaningful business behaviors and state evolution across the system lifecycle.
Introduction¶
In Domain-Driven Design (DDD), an Entity is an object that is defined primarily by its identity, not just by its attributes.
While values may change over time —
the identity remains constant, ensuring that the system can track, evolve, and reason about the entity across its lifecycle.
At ConnectSoft, Entities:
- Represent core business concepts like Customers, Orders, Accounts, and Appointments.
- Carry business behavior tied to their life journey — not just static data.
- Form the foundation of Aggregates, governing transactional consistency and invariants.
Concept Definition¶
An Entity:
| Aspect | Description |
|---|---|
| Identity | Every entity must have a globally unique and immutable identifier (e.g., Id, AccountNumber). |
| Mutable State | Entities evolve — fields may change, but identity remains the same. |
| Business Behavior | Entities encapsulate domain behaviors, not just data storage. |
| Life Cycle | Entities move through different states: Created ➔ Modified ➔ Deleted ➔ Archived. |
| Equality | Entity equality is based on Identity, not attribute values. |
📚 Why Entities Matter at ConnectSoft¶
✅ Enable Real-World Traceability
- We can track a Customer, Patient, or Order reliably across years of data, evolution, and integrations.
✅ Model State and Behavior Evolution
- Entities naturally embody the state transitions and business rule validations critical to SaaS, AI, Healthcare, and Finance systems.
✅ Serve as Aggregate Roots
- Many Aggregates at ConnectSoft are Entities that protect internal consistency boundaries.
✅ Support Long-Term System Integrity
- A well-managed identity lifecycle ensures migrations, reporting, and system integrations stay reliable over time.
Entities vs Value Objects¶
| Concept | Entity | Value Object |
|---|---|---|
| Identity | Globally unique identifier | No identifier needed |
| Mutability | Attributes can change over time | Immutable once created |
| Equality | Compared by identity | Compared by all attributes |
| Lifecycle | Tracked across operations | Replaced entirely if attributes change |
| Examples | Customer, Order, Patient | Address, Money, GeoLocation |
🧩 Visual: Entity Identity Across a System¶
flowchart TD
NewEntity[Create Entity - Assign Unique Id]
UpdateEntity[Update Entity Attributes]
ArchiveEntity[Archive or Delete Entity]
NewEntity --> UpdateEntity
UpdateEntity --> UpdateEntity
UpdateEntity --> ArchiveEntity
✅ The Entity's identity remains constant,
✅ while state evolves over its lifecycle.
Strategic Design Principles for Entities¶
At ConnectSoft, we model Entities with rigorous attention to behavior, identity, and evolution —
ensuring they remain trustworthy, testable, and business-aligned across the system’s lifetime.
📚 Core Principles for Good Entity Design¶
✅ Define a Unique, Immutable Identity
- Every Entity must have a unique identifier assigned at creation time (e.g.,
Guid,CustomerId).
✅ Identity-Based Equality
- Entities are considered equal if and only if their identities match — attribute values do not matter for equality.
✅ Encapsulate Behavior with State
- Entities are responsible for protecting their own invariants, not just holding data.
✅ Support Evolution of Attributes
- Fields (like Name, Email, Address) may change over time — but Identity never changes.
✅ Maintain Isolation of Concerns
- Business rules and state transitions belong inside Entities — avoid bloating Application Services.
✅ Stay Consistent in Persistence
- Identity must persist across sessions, database operations, and system migrations.
✅ Be ORM-Friendly, but Domain-First
- Design Entities for domain clarity first, persistence concerns second (e.g., support private constructors for ORMs without polluting business logic).
ConnectSoft.Extensions.EntityModel.EntityWithTypedId Base Class¶
At ConnectSoft, we use the ConnectSoft.Extensions.EntityModel.EntityWithTypedId<TIdentity> base class
to provide a consistent, powerful foundation for all entity implementations.
The base class provides:
IdProperty — Abstract property that must be implemented by derived classesEquals()— Identity-based equality comparison (entities are equal if their IDs match)GetHashCode()— Hash code based on identity (handles transient entities gracefully)IsTransient()— Protected method to check if entity hasn't been persisted yet- Transient Entity Handling — Proper equality and hashing for entities before persistence
Key Characteristics:¶
- Typed Identity — Supports any identity type (
Guid,int,string, etc.) - Identity-Based Equality — Entities are equal if and only if their IDs match
- Transient Entity Support — Handles entities that haven't been assigned an ID yet
- ORM-Friendly — Virtual properties enable lazy loading and change tracking
- Type Safety — Compile-time checking of identity types
📚 How to Implement Identity-Based Equality Correctly¶
Real Implementation from ConnectSoft.Extensions.EntityModel:
namespace ConnectSoft.Extensions.EntityModel
{
/// <summary>
/// Base class for entities with typed id.
/// </summary>
/// <typeparam name="TIdentity">Identity type.</typeparam>
public abstract class EntityWithTypedId<TIdentity> : IGenericEntity<TIdentity>
{
private int? requestedHashCode;
/// <summary>
/// Gets or sets the unique identifier of this entity.
/// </summary>
public abstract TIdentity Id { get; protected set; }
/// <summary>
/// Indicates whether the current object is equal to another object of the same type.
/// </summary>
public override bool Equals(object obj)
{
IGenericEntity<TIdentity> that = obj as IGenericEntity<TIdentity>;
return this.Equals(that);
}
/// <summary>
/// Serves as the default hash function.
/// For transient entities the base GetHashCode method used.
/// Otherwise Id (primary key) used as hash value.
/// </summary>
public override int GetHashCode()
{
if (!this.requestedHashCode.HasValue)
{
this.requestedHashCode = this.IsTransient() ? base.GetHashCode() : this.Id.GetHashCode();
}
return this.requestedHashCode.Value;
}
/// <summary>
/// Indicates whether the current object is equal to another object of the same type.
/// </summary>
public virtual bool Equals(IGenericEntity<TIdentity> other)
{
if (other == null || !this.GetType().IsInstanceOfType(other))
{
return false;
}
if (ReferenceEquals(this, other))
{
return true;
}
bool otherIsTransient = Equals(other.Id, default(TIdentity));
bool thisIsTransient = this.IsTransient();
if (otherIsTransient && thisIsTransient)
{
return ReferenceEquals(other, this);
}
return other.Id.Equals(this.Id);
}
/// <summary>
/// Gets a value indicating whether the entity is transient.
/// </summary>
protected bool IsTransient()
{
return Equals(this.Id, default(TIdentity));
}
}
}
✅ Equality is based only on Id — Type and identity matching required
✅ Transient entity handling — Entities without IDs are compared by reference
✅ Type safety — Compile-time checking ensures entities of same type are compared
✅ Easy to unit test — Predictable equality and hashing behavior
🎯 Good vs Bad Identity Handling¶
| ✅ Good Practice | 🚫 Bad Practice |
|---|---|
| Assign identity at creation time | Assign identity later or lazily |
| Use immutable IDs (e.g., Guid, ULID) | Use mutable fields as identity |
| Compare by Id in equality | Compare by full attribute set |
| Persist Id explicitly | Rely on database auto-increment only |
| Expose behavior through methods | Expose raw data and let services mutate |
🛑 Common Anti-Patterns to Avoid with Entities¶
| Anti-Pattern | Symptom | Why It's Dangerous |
|---|---|---|
| Anemic Entities | Only getters/setters, no behavior. | Forces logic into services, procedural bloat. |
| Identity Confusion | Identity assigned inconsistently. | Broken references, hard to track entities. |
| DTO-Like Entities | Designed for database shape, not business model. | Fragile to evolution, pollutes domain purity. |
| Multiple Changing IDs | Identity mutates over time. | System cannot reliably trace entity history. |
| Overloading Equals with Attributes | Entity equality based on all fields. | Performance issues, fragile equality logic. |
🧩 Visual: Good Entity Equality Logic¶
flowchart LR
EntityA["Entity A (Id: 1234)"]
EntityB["Entity B (Id: 1234)"]
EntityC["Entity C (Id: 5678)"]
EntityA -- Equals --> EntityB
EntityA -- Not Equals --> EntityC
✅ EntityA == EntityB because IDs match.
✅ EntityA != EntityC because IDs differ, even if attributes are similar.
C# Examples: Advanced Entity Modeling at ConnectSoft¶
At ConnectSoft, Entities are behavior-rich, identity-driven, and encapsulate Value Objects to model complex business realities.
All examples below are based on real implementations from the ConnectSoft.Saas.ProductsCatalog microservice.
🛠️ Real-World Example: EditionEntity from ConnectSoft.Saas.ProductsCatalog¶
Step 1: Define the Entity Interface¶
Real Implementation from ConnectSoft.Saas.ProductsCatalog.EntityModel:
namespace ConnectSoft.Saas.ProductsCatalog.EntityModel
{
using System;
using System.Collections.Generic;
using ConnectSoft.Extensions.EntityModel;
/// <summary>
/// Edition entity contract.
/// </summary>
public interface IEdition : IGenericEntity<Guid>
{
/// <summary>
/// Gets or sets a object identifier.
/// </summary>
Guid EditionId { get; set; }
/// <summary>
/// Gets or sets the name.
/// </summary>
string Name { get; set; }
/// <summary>
/// Gets or sets the display name.
/// </summary>
string DisplayName { get; set; }
/// <summary>
/// Gets or sets the unique identifier for the edition.
/// </summary>
string Key { get; set; }
/// <summary>
/// Gets or sets the detailed description.
/// </summary>
string Description { get; set; }
/// <summary>
/// Gets or sets the date when the edition was created.
/// </summary>
DateTime CreationDate { get; set; }
/// <summary>
/// Gets or sets the status of the edition.
/// </summary>
EditionStatusEnumeration Status { get; set; }
/// <summary>
/// Gets or sets the product this edition belongs to.
/// </summary>
IProduct Product { get; set; }
/// <summary>
/// Gets or sets the edition pricing model.
/// </summary>
IEditionPricingModel EditionPricing { get; set; }
/// <summary>
/// Gets or sets the list of features included in this edition.
/// </summary>
IList<IEditionFeature> EditionFeatures { get; set; }
/// <summary>
/// Gets or sets the list of service level agreements for this edition.
/// </summary>
IList<IServiceLevelAgreement> ServiceLevelAgreements { get; set; }
}
}
Key Pattern Elements:
✅ Extends IGenericEntity<Guid> — Base interface for all entities
✅ Interface Segregation — Separates contract from implementation
✅ Entity References — References aggregate root (IProduct) and other entities via interfaces
✅ Collections — Uses IList<T> for child entity collections
✅ Uses Enumerations — EditionStatusEnumeration for domain-specific status
Step 2: Implement the Entity Class¶
Real Implementation from ConnectSoft.Saas.ProductsCatalog.EntityModel.PocoEntities:
namespace ConnectSoft.Saas.ProductsCatalog.EntityModel.PocoEntities
{
using System;
using System.Collections.Generic;
using ConnectSoft.Extensions.EntityModel;
/// <summary>
/// Edition poco entity.
/// </summary>
public class EditionEntity : EntityWithTypedId<Guid>, IEdition
{
/// <inheritdoc/>
required public virtual Guid EditionId { get; set; }
/// <inheritdoc/>
required public virtual string Name { get; set; }
/// <inheritdoc/>
required public virtual string DisplayName { get; set; }
/// <inheritdoc/>
required public virtual string Key { get; set; }
/// <inheritdoc/>
required public virtual string Description { get; set; }
/// <inheritdoc/>
required public virtual DateTime CreationDate { get; set; }
/// <inheritdoc/>
required public virtual EditionStatusEnumeration Status { get; set; }
/// <inheritdoc/>
required public virtual IProduct Product { get; set; } // ✅ References aggregate root
/// <inheritdoc/>
required public virtual IEditionPricingModel EditionPricing { get; set; }
/// <inheritdoc/>
required public virtual IList<IEditionFeature> EditionFeatures { get; set; } = new List<IEditionFeature>();
/// <inheritdoc/>
required public virtual IList<IServiceLevelAgreement> ServiceLevelAgreements { get; set; } = new List<IServiceLevelAgreement>();
/// <inheritdoc/>
public override Guid Id
{
get => this.EditionId;
protected set => this.EditionId = value;
}
}
}
Key Implementation Patterns:
✅ Inherits from EntityWithTypedId<Guid> — Base class provides identity-based equality and hashing
✅ Implements IEdition — Satisfies the entity interface contract
✅ Virtual Properties — Enables ORM lazy loading and change tracking
✅ Required Properties — Uses C# 11 required keyword for non-nullable properties
✅ Id Property Mapping — Overrides Id to map to EditionId for domain clarity
✅ Collection Initialization — Collections initialized with empty lists to avoid null reference issues
✅ Entity References — References other entities via interfaces for loose coupling
🛠️ Real-World Example: PricingModelEntity with Rich Domain Model¶
Real Implementation showing entity with enumerations and collections:
namespace ConnectSoft.Saas.ProductsCatalog.EntityModel.PocoEntities
{
using System;
using System.Collections.Generic;
using ConnectSoft.Extensions.EntityModel;
/// <summary>
/// Pricing model poco entity.
/// </summary>
public class PricingModelEntity : EntityWithTypedId<Guid>, IPricingModel
{
/// <inheritdoc/>
required public virtual Guid PricingModelId { get; set; }
/// <inheritdoc/>
required public virtual string Name { get; set; }
/// <inheritdoc/>
required public virtual string Key { get; set; }
/// <inheritdoc/>
required public virtual string DisplayName { get; set; }
/// <inheritdoc/>
required public virtual string Description { get; set; }
/// <inheritdoc/>
required public virtual DateTime CreationDate { get; set; }
/// <inheritdoc/>
required public virtual PricingTypeEnumeration PricingType { get; set; } // ✅ Using enumeration
/// <inheritdoc/>
required public virtual string Currency { get; set; }
/// <inheritdoc/>
required public virtual decimal BasePrice { get; set; }
/// <inheritdoc/>
public virtual decimal? DiscountPercentage { get; set; } // ✅ Nullable property
/// <inheritdoc/>
required public virtual BillingCycleEnumeration BillingCycle { get; set; } // ✅ Using enumeration
/// <inheritdoc/>
required public virtual decimal MinimumPrice { get; set; }
/// <inheritdoc/>
required public virtual decimal MaximumPrice { get; set; }
/// <inheritdoc/>
required public virtual IList<IEditionPricingModel> EditionPricingModels { get; set; } = new List<IEditionPricingModel>();
/// <inheritdoc/>
required public virtual IBusinessModel BusinessModel { get; set; } // ✅ References other entity
/// <inheritdoc/>
public override Guid Id
{
get => this.PricingModelId;
protected set => this.PricingModelId = value;
}
}
}
Key Entity Characteristics:
✅ Uses Enumerations — PricingTypeEnumeration and BillingCycleEnumeration for domain-specific types
✅ Nullable Properties — DiscountPercentage? allows optional values
✅ Collection Management — EditionPricingModels initialized as empty list
✅ Entity References — References IBusinessModel entity via interface
✅ Rich Domain Model — Contains business-relevant properties (currency, prices, billing cycle)
📚 Key Lessons from ConnectSoft Entity Examples¶
✅ Entities inherit from EntityWithTypedId<TIdentity> and implement domain-specific interfaces
✅ Virtual properties enable ORM lazy loading and change tracking
✅ Required properties use C# 11 required keyword for compile-time safety
✅ Id property is overridden to map to domain-specific IDs (e.g., EditionId, PricingModelId)
✅ Collections are initialized with empty lists to avoid null reference exceptions
✅ Entity references use interfaces for loose coupling and testability
✅ Enumerations express domain concepts clearly (e.g., EditionStatusEnumeration)
✅ Entities can reference aggregate roots and other entities via interfaces
📋 Complete Implementation Template¶
Here's a complete template you can use to create new entities following ConnectSoft patterns:
namespace YourNamespace.EntityModel
{
using System;
using System.Collections.Generic;
using ConnectSoft.Extensions.EntityModel;
/// <summary>
/// Your entity contract.
/// </summary>
public interface IYourEntity : IGenericEntity<Guid>
{
/// <summary>
/// Gets or sets a object identifier.
/// </summary>
Guid YourEntityId { get; set; }
/// <summary>
/// Gets or sets the name.
/// </summary>
string Name { get; set; }
// ... other properties ...
/// <summary>
/// Gets or sets child entities.
/// </summary>
IList<IChildEntity> ChildEntities { get; set; }
}
}
namespace YourNamespace.EntityModel.PocoEntities
{
using System;
using System.Collections.Generic;
using ConnectSoft.Extensions.EntityModel;
/// <summary>
/// Your entity poco class.
/// </summary>
public class YourEntity : EntityWithTypedId<Guid>, IYourEntity
{
/// <inheritdoc/>
required public virtual Guid YourEntityId { get; set; }
/// <inheritdoc/>
required public virtual string Name { get; set; }
// ... other properties ...
/// <inheritdoc/>
public virtual IList<IChildEntity> ChildEntities { get; set; } = new List<IChildEntity>();
/// <inheritdoc/>
public override Guid Id
{
get => this.YourEntityId;
protected set => this.YourEntityId = value;
}
}
}
✅ Follow this pattern for all new entities to ensure consistency across ConnectSoft projects.
🎯 ConnectSoft Entity Checklist¶
When implementing entities at ConnectSoft, ensure:
| Checklist Item | Description | Example |
|---|---|---|
✅ Inherit from EntityWithTypedId<TIdentity> |
Use base class for identity management | EntityWithTypedId<Guid> |
| ✅ Implement Domain Interface | Create interface for entity contract | IEdition : IGenericEntity<Guid> |
| ✅ Use Virtual Properties | Enable ORM lazy loading | public virtual string Name { get; set; } |
| ✅ Use Required Properties | Use C# 11 required keyword |
required public virtual string Name { get; set; } |
| ✅ Map Id Property | Override Id to map to domain-specific ID |
EditionId maps to Id |
| ✅ Initialize Collections | Avoid null reference exceptions | = new List<IChildEntity>() |
| ✅ Use Enumerations | Express domain states clearly | EditionStatusEnumeration |
| ✅ Reference Entities by Interface | Maintain loose coupling | IProduct Product { get; set; } |
| ✅ Support Nullable Properties | Use nullable types for optional values | decimal? DiscountPercentage { get; set; } |
| ✅ Document with XML Comments | Use /// <inheritdoc/> for interface implementations |
/// <inheritdoc/> |
📚 Lessons from Real-World Entity Modeling¶
| Good Practice | Why It Matters | ConnectSoft Implementation |
|---|---|---|
Inherit from EntityWithTypedId<TIdentity> |
Provides consistent identity management and equality semantics | All entities inherit from base class |
| Use Virtual Properties | Enables ORM lazy loading and change tracking | All properties marked virtual |
| Use Required Properties | Compile-time safety for non-nullable properties | C# 11 required keyword |
| Map Id Property | Domain-specific ID names improve readability | EditionId maps to base Id property |
| Initialize Collections | Prevents null reference exceptions | Collections initialized: = new List<T>() |
| Reference Entities by Interface | Loose coupling and testability | IProduct Product { get; set; } |
| Use Enumerations for Status | Type-safe domain concepts | EditionStatusEnumeration instead of string or int |
| Support Nullable Properties | Explicit optional values | decimal? DiscountPercentage { get; set; } |
| Interface-Based Design | Separates contract from implementation | IEdition interface, EditionEntity implementation |
| Document with XML Comments | IntelliSense and documentation generation | /// <inheritdoc/> for interface implementations |
🧩 Visual: Entity Lifecycle and Behavior in Complex Systems¶
flowchart LR
CreateEntity["Create Entity (Assign ID + Initial Valid State)"]
MutateEntity["Perform Valid State Changes (Business Rules Enforced)"]
EvolveEntity["Evolve State Across Life Cycle"]
EndEntity["Archive or Terminate Entity"]
CreateEntity --> MutateEntity --> EvolveEntity --> MutateEntity --> EndEntity
✅ Entities evolve carefully over time,
✅ always protected by domain rules and identity consistency.
Best Practices for Entities¶
At ConnectSoft, Entity modeling is careful, tactical, and business-driven —
ensuring identity consistency, behavioral richness, and system resilience across evolving platforms.
📚 Best Practices Checklist¶
✅ Always Define a Unique Immutable Identity
- Assign IDs at creation — never rely on mutable fields for identification.
✅ Equality Based on Identity
- Entity equality must depend solely on identifiers, not attribute comparisons.
✅ Encapsulate State and Behavior
- Entities must protect their internal state through controlled methods.
✅ Validate During Construction
- Ensure entities are created in valid, consistent states immediately.
✅ Use Value Objects for Clusters of Attributes
- Aggregate related fields (e.g., Address, Money) into Value Objects inside Entities.
✅ Encapsulate Collections Carefully
- Expose read-only access to internal collections (e.g., Appointments, MedicalRecords).
✅ Keep Entities Focused
- Entities should only carry attributes and behaviors relevant to their real-world business purpose.
✅ Persistence-Agnostic Design
- Model Entities for domain clarity — not database schema optimization.
✅ Maintain Auditability
- Design Entities to support tracking important lifecycle events (e.g., CreatedAt, ModifiedAt).
Conclusion¶
Entities are the lifeblood of domain models at ConnectSoft.
When modeled properly:
- Identity remains strong across system evolutions.
- State transitions are safe, consistent, and validated.
- Business operations become natural, reflecting real-world processes.
- Testing becomes easier, because behaviors are encapsulated and predictable.
- Event-driven workflows and resilient microservices scale reliably over years.
When modeled poorly:
- Identity confusion grows.
- State management becomes chaotic.
- Fragile systems emerge — unable to adapt safely to change.
At ConnectSoft, every Entity is treated with the respect it deserves:
a living, evolving, meaningful business concept — not just a data container.
We build Entities to last, to protect, and to empower future evolutions —
from SaaS platforms to AI ecosystems to healthcare and finance solutions.
"Entities are not records;
they are the heartbeat of meaningful systems.
Treat their identity and behavior as sacred."
References¶
-
Books and Literature
- Eric Evans — Domain-Driven Design: Tackling Complexity in the Heart of Software
- Vaughn Vernon — Implementing Domain-Driven Design
- Jimmy Nilsson — Applying Domain-Driven Design and Patterns
-
Online Resources
-
ConnectSoft Packages and Code
ConnectSoft.Extensions.EntityModel— Base classes and interfaces:EntityWithTypedId<TIdentity>— Base class for entities with typed identityIGenericEntity<TIdentity>— Base interface for all entities- Identity-based
Equals()andGetHashCode()implementations - Transient entity handling (entities without assigned IDs)
- Type-safe identity management
- Real-World Examples — See
ConnectSoft.Saas.ProductsCatalog.EntityModel.PocoEntitiesfor production implementations:EditionEntity— Complete entity example with collections and referencesPricingModelEntity— Entity with enumerations and business propertiesProductEntity— Aggregate root entity exampleFeatureEntity,BusinessModelEntity, etc. — Additional entity examples
-
ConnectSoft Internal Standards
- ConnectSoft Entity Modeling Guidelines
- ConnectSoft Domain Layer Modeling Playbook
- ConnectSoft Resilient Microservice Blueprints