Skip to content

Entity and Aggregate Specification

This is the API specification for Entity and Aggregate related public types provided by the Functorium framework. For design principles and implementation patterns, see the Entity and Aggregate Implementation Guide.

TypeNamespaceDescription
IEntityFunctorium.Domains.EntitiesEntity naming convention constant definitions
IEntity<TId>Functorium.Domains.EntitiesEntity base interface (ID-based equality contract)
IEntityId<T>Functorium.Domains.EntitiesUlid-based Entity ID interface
Entity<TId>Functorium.Domains.EntitiesEntity base abstract class (equality, proxy support)
AggregateRoot<TId>Functorium.Domains.EntitiesAggregate Root base abstract class (domain event management)
GenerateEntityIdAttributeFunctorium.Domains.EntitiesEntityId source generator trigger attribute
IAuditableFunctorium.Domains.EntitiesCreated/modified timestamp tracking mixin
IAuditableWithUserFunctorium.Domains.EntitiesCreated/modified timestamp + user tracking mixin
IConcurrencyAwareFunctorium.Domains.EntitiesOptimistic concurrency control mixin
ISoftDeletableFunctorium.Domains.EntitiesSoft delete mixin
ISoftDeletableWithUserFunctorium.Domains.EntitiesSoft delete + deleter tracking mixin
IDomainServiceFunctorium.Domains.ServicesDomain service marker interface

Interfaces defining the Entity contract.

public interface IEntity
{
const string CreateMethodName = "Create";
const string CreateFromValidatedMethodName = "CreateFromValidated";
}
ConstantValueDescription
CreateMethodName"Create"Factory method name for creating a new Entity
CreateFromValidatedMethodName"CreateFromValidated"Method name for restoring an Entity from validated data (for Repository/ORM)
public interface IEntity<TId> : IEntity
where TId : struct, IEntityId<TId>
{
TId Id { get; }
}
PropertyTypeDescription
IdTIdUnique identifier of the Entity

Generic constraint: TId must be a struct and implement IEntityId<TId>.


Interface for Ulid-based Entity IDs. Supports time-ordered sorting and inherits IEquatable<T>, IComparable<T>, IParsable<T>.

public interface IEntityId<T> : IEquatable<T>, IComparable<T>, IParsable<T>
where T : struct, IEntityId<T>
{
Ulid Value { get; }
static abstract T New();
static abstract T Create(Ulid id);
static abstract T Create(string id);
}
MemberReturn TypeDescription
ValueUlidThe Ulid value
New()TCreates a new EntityId (static abstract)
Create(Ulid id)TCreates an EntityId from a Ulid (static abstract)
Create(string id)TCreates an EntityId from a string (static abstract). Throws FormatException for invalid formats

Base abstract class for Entity providing ID-based equality comparison. Also handles ORM proxy types (Castle, NHibernate, EF Core Proxies).

[Serializable]
public abstract class Entity<TId> : IEntity<TId>, IEquatable<Entity<TId>>
where TId : struct, IEntityId<TId>
PropertyTypeAccessorDescription
IdTIdpublic get; protected initUnique identifier of the Entity
SignatureAccess LevelDescription
Entity()protectedDefault constructor (for ORM/serialization)
Entity(TId id)protectedCreates an Entity with a specified ID
MethodReturn TypeAccess LevelDescription
Equals(object? obj)boolpublicID-based equality comparison (proxy-type aware)
Equals(Entity<TId>? other)boolpublicType-safe equality comparison
GetHashCode()intpublicID-based hash code
operator ==(Entity<TId>?, Entity<TId>?)boolpublic staticEquality operator
operator !=(Entity<TId>?, Entity<TId>?)boolpublic staticInequality operator
CreateFromValidation<TEntity, TValue>(Validation<Error, TValue>, Func<TValue, TEntity>)Fin<TEntity>public staticFactory helper using LanguageExt Validation
GetUnproxiedType(object obj)Typeprotected staticStrips ORM proxy and returns the actual type
[GenerateEntityId]
public class Product : Entity<ProductId>
{
#pragma warning disable CS8618
private Product() { }
#pragma warning restore CS8618
private Product(ProductId id, ProductName name) : base(id)
{
Name = name;
}
public ProductName Name { get; private set; }
public static Product Create(ProductName name)
=> new(ProductId.New(), name);
public static Product CreateFromValidated(ProductId id, ProductName name)
=> new(id, name);
}

Base abstract class for Aggregate Root providing domain event management. Inherits Entity<TId> and implements IDomainEventDrain (internal).

public abstract class AggregateRoot<TId> : Entity<TId>, IDomainEventDrain
where TId : struct, IEntityId<TId>
PropertyTypeAccessorDescription
DomainEventsIReadOnlyList<IDomainEvent>public getDomain event list (read-only)
SignatureAccess LevelDescription
AggregateRoot()protectedDefault constructor (for ORM/serialization)
AggregateRoot(TId id)protectedCreates an Aggregate Root with a specified ID
MethodReturn TypeAccess LevelDescription
AddDomainEvent(IDomainEvent domainEvent)voidprotectedAdds a domain event
ClearDomainEvents()voidpublicRemoves all domain events (IDomainEventDrain implementation)

AggregateRoot<TId> separates two interfaces for domain events.

InterfaceAccess LevelRole
IHasDomainEventspublicEvent read-only access (DomainEvents property)
IDomainEventDraininternalEvent cleanup (ClearDomainEvents()) — infrastructure concern
[GenerateEntityId]
public class Order : AggregateRoot<OrderId>
{
#pragma warning disable CS8618
private Order() { }
#pragma warning restore CS8618
private Order(OrderId id, Money totalAmount) : base(id)
{
TotalAmount = totalAmount;
Status = OrderStatus.Pending;
}
public Money TotalAmount { get; private set; }
public OrderStatus Status { get; private set; }
public static Order Create(Money totalAmount)
{
var id = OrderId.New();
var order = new Order(id, totalAmount);
order.AddDomainEvent(new OrderCreatedEvent(id, totalAmount));
return order;
}
public Fin<Unit> Confirm()
{
if (!Status.CanTransitionTo(OrderStatus.Confirmed))
return Fin<Unit>.Fail(Error.New("Cannot confirm order"));
Status = OrderStatus.Confirmed;
AddDomainEvent(new OrderConfirmedEvent(Id));
return unit;
}
}

When applied to an Entity class, the source generator automatically generates EntityId-related types.

[AttributeUsage(AttributeTargets.Class, AllowMultiple = false, Inherited = false)]
public sealed class GenerateEntityIdAttribute : Attribute;

Applying [GenerateEntityId] to the Product class generates the following types.

Generated TypeKindDescription
ProductIdreadonly partial record structUlid-based EntityId (implements IEntityId<ProductId>)
ProductIdComparersealed classEF Core ValueComparer<ProductId> (for change tracking)
ProductIdConvertersealed classEF Core ValueConverter<ProductId, string> (string conversion for DB storage)
MemberType/ReturnDescription
Nameconst stringType name constant ("ProductId")
Namespaceconst stringNamespace constant
Emptystatic readonly ProductIdEmpty value (based on Ulid.Empty)
ValueUlid { get; init; }The Ulid value
New()static ProductIdCreates a new ID
Create(Ulid id)static ProductIdCreates from a Ulid
Create(string id)static ProductIdCreates from a string (FormatException possible)
CompareTo(ProductId other)intUlid-based comparison
<, >, <=, >=boolComparison operators
Parse(string, IFormatProvider?)static ProductIdIParsable<T> implementation
TryParse(string?, IFormatProvider?, out ProductId)static boolIParsable<T> implementation
ToString()stringUlid string representation

Generated EntityIds automatically have [JsonConverter] and [TypeConverter] attributes applied, supporting JSON serialization and type conversion.

// Create a new ID
var productId = ProductId.New();
// Convert from string
var parsed = ProductId.Create("01ARZ3NDEKTSV4RRFFQ69G5FAV");
// Comparison
bool isNewer = productId > parsed;
// EF Core configuration
builder.Property(x => x.Id)
.HasConversion(new ProductIdConverter())
.Metadata.SetValueComparer(new ProductIdComparer());

Interfaces that can be optionally mixed into Entity or Aggregate Root to add cross-cutting concerns.

Tracks creation/modification timestamps.

public interface IAuditable
{
DateTime CreatedAt { get; }
Option<DateTime> UpdatedAt { get; }
}
PropertyTypeDescription
CreatedAtDateTimeCreation timestamp
UpdatedAtOption<DateTime>Last modification timestamp (None if never modified)

Extends IAuditable to additionally track user information.

public interface IAuditableWithUser : IAuditable
{
Option<string> CreatedBy { get; }
Option<string> UpdatedBy { get; }
}
PropertyTypeDescription
CreatedByOption<string>Creator identifier
UpdatedByOption<string>Last modifier identifier

Manages row version for optimistic concurrency control. Maps to EF Core’s [Timestamp]/IsRowVersion().

public interface IConcurrencyAware
{
byte[] RowVersion { get; }
}
PropertyTypeDescription
RowVersionbyte[]Row version for optimistic concurrency control

Supports soft delete. IsDeleted provides a default implementation (default interface method) derived from DeletedAt.

public interface ISoftDeletable
{
Option<DateTime> DeletedAt { get; }
bool IsDeleted => DeletedAt.IsSome;
}
PropertyTypeDescription
DeletedAtOption<DateTime>Deletion timestamp (None if not deleted)
IsDeletedboolWhether deleted (derived from DeletedAt.IsSome, default implementation)

Extends ISoftDeletable to additionally track deleter information.

public interface ISoftDeletableWithUser : ISoftDeletable
{
Option<string> DeletedBy { get; }
}
PropertyTypeDescription
DeletedByOption<string>Deleter identifier
[GenerateEntityId]
public class Product : AggregateRoot<ProductId>, IAuditableWithUser, ISoftDeletable, IConcurrencyAware
{
public DateTime CreatedAt { get; private set; }
public Option<DateTime> UpdatedAt { get; private set; }
public Option<string> CreatedBy { get; private set; }
public Option<string> UpdatedBy { get; private set; }
public Option<DateTime> DeletedAt { get; private set; }
public byte[] RowVersion { get; private set; } = [];
}

A marker interface for expressing domain logic that spans multiple Aggregates.

public interface IDomainService { }
RuleDescription
StatelessDoes not maintain mutable state between calls (Evans Blue Book Ch.9)
Default patternImplemented as pure functions (no external I/O)
Repository dependency allowedMay depend on Repository interfaces for large-scale cross-data queries
Port/Adapter forbiddenNo IObservablePort dependency (Port/Adapter is used in Usecases)
LocationDomain Layer
public sealed class PricingService : IDomainService
{
public static Fin<Money> CalculateDiscount(
Money originalPrice,
DiscountRate rate,
CustomerGrade grade)
{
// Pure function logic that references values from multiple Aggregates
var discount = originalPrice.Value * rate.Value * grade.Multiplier;
return Money.Create(originalPrice.Value - discount);
}
}