ITLab.Template.DevPack.Domain
Contains shared domain building blocks: entities, value objects, domain events, smart enums and core DDD contracts.
Componentes
| Tipo | Nome | Responsabilidade |
|---|---|---|
| Abstract Class | Entity | Base entity with domain event support and object equality semantics |
| Abstract Class | Entity<TKey> | Typed-key variant of Entity with ID and creation timestamp |
| Interface | IEntity | Domain contract for identity (non-generic) |
| Interface | IEntity<TKey> | Domain contract for typed-key identity |
| Interface | IAggregateRoot | Marker for aggregate root entities |
| Abstract Class | AuditedEntity | Base audit entity (CreatedAt, CreatedBy, UpdatedAt, ModifiedBy, IsActive) |
| Abstract Class | AuditedEntity<TKey> | Typed-key variant implementing IAuditable and IInactivable |
| Abstract Class | AuditedSoftDeletableEntity<TKey> | Extends AuditedEntity<TKey> with ISoftDeletable (IsDeleted, DeletedAt, DeletedBy) |
| Interface | IAuditable | Audit metadata contract: CreatedAt+CreatedBy, UpdatedAt+ModifiedBy |
| Interface | IInactivable | Activation-state contract: IsActive |
| Interface | ISoftDeletable | Soft-delete contract: IsDeleted, DeletedAt, DeletedBy |
| Sealed Record | AuditTimestamp | Value object (DateTime UtcDateTime, string? LocalTimeZone) — annotated [Owned] for EF auto-mapping |
| Interface | IMultiTenant | Identifies an entity or model that belongs to a tenant scope |
| Abstract Class | DomainEvent | Base domain event implementation with tenant, creator and correlation metadata |
| Interface | IDomainEvent | Domain event contract (extends Mediator.INotification) |
| Interface | IEnumDomainTable | Marker for enum-backed lookup table entities |
| Class | EnumDomainTable<T> | Persisted enum value with display name, backed by a C# enum |
| Abstract Class | SmartEnum<TEnum, TValue> | Base class for strongly typed, self-validating enumerations |
| Abstract Class | ValueObject | Structural 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
DateTimeOffsetpara o value objectAuditTimestamp(record(DateTime UtcDateTime, string? LocalTimeZone)). IAuditable/AuditedEntity:Created→CreatedAt(AuditTimestamp);LastModified→UpdatedAt(AuditTimestamp?);LastModifiedBy→ModifiedBy.CreatedBymantido. Atribuição de usuário preservada.ISoftDeletable/AuditedSoftDeletableEntity:DeletedOnUtc→DeletedAt(AuditTimestamp?).DeletedBymantido.- 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
IMultiTenantfor transient request context — useICurrentUserinstead. - Do not store
DateTimeOffsetfor new audit columns —AuditTimestamp(UTC + IANA) is the canonical model. - Do not copy a creation
AuditTimestampinto 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
ValueObjectand declare stable equality components. - Products implement
EnumDomainTable<T>for each enum that requires DB persistence and display names.