Pular para o conteúdo principal
Versão: main (dev) 🚧

ITLab.Template.DevPack.Domain

Contains shared domain building blocks: entities, value objects, domain events, smart enums and core DDD contracts.

Componentes

TipoNomeResponsabilidade
Abstract ClassEntityBase entity with domain event support and object equality semantics
Abstract ClassEntity<TKey>Typed-key variant of Entity with ID and creation timestamp
InterfaceIEntityDomain contract for identity (non-generic)
InterfaceIEntity<TKey>Domain contract for typed-key identity
InterfaceIAggregateRootMarker for aggregate root entities
Abstract ClassAuditedEntityBase audit entity (CreatedAt, CreatedBy, UpdatedAt, ModifiedBy, IsActive)
Abstract ClassAuditedEntity<TKey>Typed-key variant implementing IAuditable and IInactivable
Abstract ClassAuditedSoftDeletableEntity<TKey>Extends AuditedEntity<TKey> with ISoftDeletable (IsDeleted, DeletedAt, DeletedBy)
InterfaceIAuditableAudit metadata contract: CreatedAt+CreatedBy, UpdatedAt+ModifiedBy
InterfaceIInactivableActivation-state contract: IsActive
InterfaceISoftDeletableSoft-delete contract: IsDeleted, DeletedAt, DeletedBy
Sealed RecordAuditTimestampValue object (DateTime UtcDateTime, string? LocalTimeZone) — annotated [Owned] for EF auto-mapping
InterfaceIMultiTenantIdentifies an entity or model that belongs to a tenant scope
Abstract ClassDomainEventBase domain event implementation with tenant, creator and correlation metadata
InterfaceIDomainEventDomain event contract (extends Mediator.INotification)
InterfaceIEnumDomainTableMarker for enum-backed lookup table entities
ClassEnumDomainTable<T>Persisted enum value with display name, backed by a C# enum
Abstract ClassSmartEnum<TEnum, TValue>Base class for strongly typed, self-validating enumerations
Abstract ClassValueObjectStructural equality semantics for immutable value objects

Como usar

// Typical aggregate root raising a domain event.
public sealed class Order : AuditedSoftDeletableEntity<Guid>, IAggregateRoot
{
private Order() { } // EF
private Order(Guid id, string customer, decimal total) : base(id)
{
Customer = customer;
Total = total;
}

public string Customer { get; private set; } = string.Empty;
public decimal Total { get; private set; }

public void Approve()
{
Raise(new OrderApprovedEvent(Id));
}
}

// Enum-backed lookup table.
public sealed class OrderStatusTable : EnumDomainTable<EOrderStatus> { }

// Value object with structural equality.
public sealed class Address : ValueObject
{
public string Street { get; }
protected override IEnumerable<object> GetEqualityComponents()
{
yield return Street;
}
}

// Smart enum for type-safe status handling.
public sealed class Priority : SmartEnum<Priority, int>
{
public static readonly Priority Low = new(nameof(Low), 1);
public static readonly Priority High = new(nameof(High), 3);
private Priority(string name, int value) : base(name, value) { }
}

The concrete domain event referenced above — auto-suficiente, verificado pelo smoke check:

public sealed class OrderApprovedEvent : DomainEvent
{
public OrderApprovedEvent(Guid orderId)
{
OrderId = orderId;
SetEntityId(orderId);
}

public Guid OrderId { get; }

public override object?[] GetKeys() => [OrderId];
}

Mudanças na 10.2.0

  • Audit fields migraram de DateTimeOffset para o value object AuditTimestamp (record (DateTime UtcDateTime, string? LocalTimeZone)).
  • IAuditable / AuditedEntity: CreatedCreatedAt (AuditTimestamp); LastModifiedUpdatedAt (AuditTimestamp?); LastModifiedByModifiedBy. CreatedBy mantido. Atribuição de usuário preservada.
  • ISoftDeletable / AuditedSoftDeletableEntity: DeletedOnUtcDeletedAt (AuditTimestamp?). DeletedBy mantido.
  • Cada timestamp tem sua própria timezone — criação em São Paulo, update em Lisboa, deleção em Tóquio são preservadas independentemente.

Reconstrução do horário local

// Sob demanda — converte UTC + IANA tz para o DateTimeOffset original.
DateTimeOffset localCreated = entity.CreatedAt.ToLocal();

// Comparações server-side ficam diretas — UTC é a single-source-of-truth.
var oldRecords = db.Records.Where(r => r.CreatedAt.UtcDateTime < cutoffUtc);

Mapeamento EF Core

AuditTimestamp é anotado com [Owned]. EF auto-descobre como propriedade owned e persiste em duas colunas por padrão:

CreatedAt_UtcDateTime datetime2 NOT NULL
CreatedAt_LocalTimeZone nvarchar(64) NULL
UpdatedAt_UtcDateTime datetime2 NULL
UpdatedAt_LocalTimeZone nvarchar(64) NULL
DeletedAt_UtcDateTime datetime2 NULL -- se ISoftDeletable
DeletedAt_LocalTimeZone nvarchar(64) NULL -- se ISoftDeletable

Sem configuração manual no OnModelCreating para o caso comum.

Para projetos que prefiram coluna única (sacrificando query nos campos), sobrescrever no OnModelCreating:

builder.Property(e => e.CreatedAt)
.HasConversion(
v => $"{v.UtcDateTime:O}|{v.LocalTimeZone}",
s => Parse(s));

Domain events policy

IDomainEvent herda de Mediator.INotification. Cada evento declarado exige pelo menos um INotificationHandler<T> — o source generator do Mediator falha com MSG0005 caso contrário. Concretes são declarados como class : DomainEvent (records não herdam de classes não-record).

Quando criar um evento de domínio

Crie um IDomainEvent apenas quando o efeito real (notificação, projeção, integration event publish, side-effect downstream) estiver mapeado na feature — ou seja, quando o time já decidiu o que esse evento dispara.

Não crie eventos especulativamente ("talvez um dia alguém consuma"): eventos sem efeito real geram ruído e mascaram o que realmente importa. Se o efeito ainda não está mapeado, prefira não emitir o evento até o consumer aparecer.

Placeholder de transição (handler obrigatório MSG0005)

Quando o evento já está mapeado mas o efeito real ainda não está 100% implementado e pronto para build/teste, ship um handler de logging para satisfazer o source generator:

public sealed class XEventHandler(ILogger<XEventHandler> logger)
: INotificationHandler<XEvent>
{
public ValueTask Handle(XEvent notification, CancellationToken ct)
{
logger.LogInformation("XEvent dispatched: {Payload}", notification);
return ValueTask.CompletedTask;
}
}

Substitua (ou adicione siblings) quando o consumer real estiver pronto.

O que não fazer

  • Do not place infrastructure concerns inside entities or value objects.
  • Do not bypass domain event clearing/dispatch lifecycle contracts.
  • Do not use IMultiTenant for transient request context — use ICurrentUser instead.
  • Do not store DateTimeOffset for new audit columns — AuditTimestamp (UTC + IANA) is the canonical model.
  • Do not copy a creation AuditTimestamp into the update slot — each event carries its own timezone.

Extensão pelo produto

  • Products derive from shared base entities to keep audit and event behavior consistent.
  • Product-specific value objects should inherit ValueObject and declare stable equality components.
  • Products implement EnumDomainTable<T> for each enum that requires DB persistence and display names.