Table of Contents

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.propsBytLabsPackageVersion.)

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, DomainEventBase, 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 requires DateTime? CreatedAt / string? CreatedBy. Derive events from DomainEventBase (these members are supplied) — note a record cannot inherit a non-record base, so payload-less events must be classes.

// With payload
public class ProductCreated(Guid id, CreateProduct data) : DomainEventBase<Guid, CreateProduct>(id, data);

// Without payload
public class ProductArchived(Guid productId) : DomainEventBase
{
    public Guid ProductId { get; } = productId;
}

Handle them with BytLabs.Application.DomainEvents.DomainEventHandler<TEvent>.

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.");