BytLabs.Domain
Domain-driven design building blocks: entities, aggregate roots, value objects, domain events, audit info, business rules, and dynamic-data support. This package has no infrastructure dependencies — it is the pure domain core every service builds on.
Install
<PackageReference Include="BytLabs.Domain" />
(Version is managed centrally via Directory.Packages.props → BytLabsPackageVersion.)
What's inside
| Area | Types |
|---|---|
| Entities | Entity<TId>, AggregateRootBase<TId>, IEntity, IAggregateRoot<TId>, IEntityId<TId>, ISoftDeletable |
| Value objects | ValueObject, Metadata.EntityMetadata, Metadata.SubEntityMetadata |
| Domain events | IDomainEvent, IDomainEvent<TId>, IDomainEvent<TId, TData>, DomainEventBase, DomainEventBase<TId>, DomainEventBase<TId, TData> |
| Audit | IAuditable, AuditInfo |
| Dynamic data | IHaveDynamicData |
| Business rules | BusinessRule<T>, AggregateBusinessRule<T>, BusinessRuleException |
| Exceptions | DomainException |
Core concepts & usage
Entity
Base class for any entity with identity. Id is protected init (set once at construction).
public sealed class ProductVariant : Entity<Guid>
{
public string Sku { get; private set; }
private ProductVariant(Guid id, string sku) : base(id) => Sku = sku;
public static ProductVariant Create(Guid id, string sku) => new(id, sku);
}
Aggregate root + domain events
AggregateRootBase<TId> extends Entity<TId>, owns a list of domain events, and carries an
AuditInfo. Raise events from factory methods/mutators with AddDomainEvent; the infrastructure
dispatches and clears them after persistence (DomainEvents, ClearDomainEvents()).
public sealed class Product : AggregateRootBase<Guid>
{
public string Name { get; private set; }
private Product(Guid id, string name) : base(id) => Name = name;
public static Product Create(Guid id, string name)
{
var product = new Product(id, name);
product.AddDomainEvent(new ProductCreated(id, name));
return product;
}
}
AggregateRootBase exposes: IReadOnlyCollection<IDomainEvent> DomainEvents, AddDomainEvent(...),
ClearDomainEvents(), and AuditInfo AuditInfo (created/modified/deleted stamps).
Domain events
IDomainEvent : INotification (MediatR) and carries audit metadata DateTimeOffset CreatedAt /
string CreatedBy (both init-only). DomainEventBase and its generic variants are records, so
derive your events as records too. The base supplies CreatedAt (defaults to DateTimeOffset.UtcNow)
and CreatedBy (defaults to "System"); the infrastructure re-stamps them at publish time with the
current user and timestamp (see below).
Pick the base that matches the event's shape:
| Base | Adds | Interface |
|---|---|---|
DomainEventBase |
CreatedAt, CreatedBy |
IDomainEvent |
DomainEventBase<TId> |
Id |
IDomainEvent<TId> |
DomainEventBase<TId, TData> |
Id, Data |
IDomainEvent<TId, TData> |
// With id + payload
public record ProductCreated(Guid Id, CreateProduct Data)
: DomainEventBase<Guid, CreateProduct>(Id, Data);
// With id only
public record ProductArchived(Guid Id) : DomainEventBase<Guid>(Id);
// No id / no payload
public record MaintenanceStarted() : DomainEventBase;
Handle them with BytLabs.Application.DomainEvents.DomainEventHandler<TEvent>.
Audit stamping:
CreatedBy/CreatedAtare set byDomainEventDispatcherDecoratorwhen events are dispatched after persistence. Because events are immutable records, the decorator publishes a copy produced with awithexpression rather than mutating the original.
Value objects
ValueObject gives value-based equality (==, !=, Equals, GetHashCode) — implement
GetEqualityComponents().
public class Money : ValueObject
{
public decimal Amount { get; }
public string Currency { get; }
public Money(decimal amount, string currency) { Amount = amount; Currency = currency; }
protected override IEnumerable<object> GetEqualityComponents() { yield return Amount; yield return Currency; }
}
EntityMetadata(type, id) is a ready-made value object for referencing another entity by type+id.
Soft delete
Implement ISoftDeletable (bool IsDeleted) so the entity can be flagged instead of physically
removed; query helpers in BytLabs.DataAccess.MongoDB exclude these.
public sealed class Product : AggregateRootBase<Guid>, ISoftDeletable
{
public bool IsDeleted { get; private set; }
public void Remove() { IsDeleted = true; AddDomainEvent(new ProductArchived(Id)); }
}
Dynamic data
Implement IHaveDynamicData (JsonElement Data) to carry schema-less, caller-defined fields. The
MongoDB and GraphQL packages can filter on this field.
public sealed class Product : AggregateRootBase<Guid>, IHaveDynamicData
{
public JsonElement Data { get; private set; }
}
Audit
IAuditable exposes a nested AuditInfo AuditInfo with CreatedAt/CreatedBy,
LastModifiedAt/LastModifiedBy, DeletedAt/DeletedBy. AggregateRootBase already carries an
AuditInfo; the data-access layer stamps it automatically. Read DTOs typically implement IAuditable
too so audit data flows to the API.
Business rules (FluentValidation-based)
BusinessRule<T> is an AbstractValidator<T> that throws BusinessRuleException (carrying the
ValidationFailures) instead of FluentValidation's default. AggregateBusinessRule<T> composes
several rules and validates them together.
public class ProductNameRule : BusinessRule<Product>
{
public ProductNameRule() => RuleFor(p => p.Name).NotEmpty().MaximumLength(200);
}
public class ProductRules : AggregateBusinessRule<Product>
{
public ProductRules() : base(new ProductNameRule(), /* ...more rules */) { }
}
// In the aggregate:
new ProductRules().ValidateAndThrow(product); // throws BusinessRuleException on failure
Exceptions
DomainException(message, code?, property?) signals a violated domain invariant. The GraphQL layer
maps it to a BusinessError on mutation payloads.
if (!items.Any())
throw new DomainException("An order must have at least one item.");
Related packages
- BytLabs.Application — CQRS, repositories, domain-event handlers built on these types.
- BytLabs.DataAccess.MongoDB — persists aggregates; honors
ISoftDeletable/IHaveDynamicData. - BytLabs.Api.Graphql — exposes aggregates/DTOs; maps
DomainException→BusinessError.