This commit is contained in:
2025-10-27 17:39:18 -04:00
commit 31f723bea4
1579 changed files with 642409 additions and 0 deletions

View File

@@ -0,0 +1,37 @@
namespace Prefab.Domain.Common;
/// <summary>
/// Base implementation for aggregate entities that support domain events.
/// </summary>
/// <typeparam name="T">Entity identifier type.</typeparam>
public abstract class Entity<T> : IEntity<T>, IHasDomainEvents
{
private readonly List<Event> _events = [];
/// <inheritdoc />
public T Id { get; set; } = default!;
/// <inheritdoc />
public IReadOnlyCollection<Event> Events => _events.AsReadOnly();
/// <inheritdoc />
public void AddEvent(Event e) => _events.Add(e);
/// <inheritdoc />
public void RemoveEvent(Event e) => _events.Remove(e);
/// <inheritdoc />
public void ClearEvents() => _events.Clear();
/// <summary>
/// Generates a UUIDv7 identifier using the supplied timestamp, primarily for testing scenarios.
/// </summary>
/// <param name="timestamp">The timestamp used to seed the UUID generator.</param>
/// <exception cref="InvalidOperationException">Thrown when the entity identifier type is not <see cref="Guid"/>.</exception>
protected void GenerateIdWithTimestamp(DateTime timestamp)
{
Id = typeof(T) == typeof(Guid)
? (T)(object)Guid.CreateVersion7(timestamp)
: throw new InvalidOperationException("Cannot generate a UUIDv7 for a non-Guid entity.");
}
}

View File

@@ -0,0 +1,25 @@
namespace Prefab.Domain.Common;
/// <summary>
/// Interface representation of an entity. All entities that require auditing should implement this.
/// </summary>
/// <typeparam name="T">The type that will represent the entity's ID field.</typeparam>
public interface IEntityWithAudit<T> : IEntity<T>, IAudit
{
}
/// <inheritdoc cref="IEntityWithAudit{T}" />
public abstract class EntityWithAudit<T> : Entity<T>, IEntityWithAudit<T>
{
/// <inheritdoc />
public Guid CreatedBy { get; init; }
/// <inheritdoc />
public DateTimeOffset CreatedOn { get; init; }
/// <inheritdoc />
public Guid LastModifiedBy { get; init; }
/// <inheritdoc />
public DateTimeOffset LastModifiedOn { get; init; }
}

View File

@@ -0,0 +1,118 @@
using System.ComponentModel.DataAnnotations;
using Prefab.Data;
namespace Prefab.Domain.Common;
/// <summary>
/// Represents an audited entity that also tracks status transitions such as deactivate and delete.
/// </summary>
/// <typeparam name="T">The identifier type.</typeparam>
public interface IEntityWithAuditAndStatus<T> : IEntityWithAudit<T>, IStatus
{
/// <summary>
/// Gets or sets the identifier of the user that deactivated the entity.
/// </summary>
Guid? InactivatedBy { get; set; }
/// <summary>
/// Gets or sets when the entity was deactivated.
/// </summary>
DateTimeOffset? InactivatedOn { get; set; }
/// <summary>
/// Gets or sets the identifier of the user that soft-deleted the entity.
/// </summary>
Guid? DeletedBy { get; set; }
/// <summary>
/// Gets or sets when the entity was soft-deleted.
/// </summary>
DateTimeOffset? DeletedOn { get; set; }
/// <summary>
/// Gets or sets the concurrency token used for optimistic locking.
/// </summary>
byte[] RowVersion { get; set; }
}
/// <summary>
/// Base type for audited entities that support activation, deactivation, and soft-delete flows.
/// </summary>
/// <typeparam name="T">The identifier type.</typeparam>
public abstract class EntityWithAuditAndStatus<T> : EntityWithAudit<T>, IEntityWithAuditAndStatus<T>
{
/// <inheritdoc />
public AuditStatus AuditStatus => DeletedOn.HasValue
? AuditStatus.Deleted
: InactivatedOn.HasValue
? AuditStatus.Inactive
: AuditStatus.Active;
/// <inheritdoc />
public Guid? InactivatedBy { get; set; }
/// <inheritdoc />
public DateTimeOffset? InactivatedOn { get; set; }
/// <inheritdoc />
public Guid? DeletedBy { get; set; }
/// <inheritdoc />
public DateTimeOffset? DeletedOn { get; set; }
/// <inheritdoc />
[Timestamp]
public byte[] RowVersion { get; set; } = [];
/// <summary>
/// Marks the entity as active by clearing any inactive markers.
/// </summary>
/// <param name="userId">Identifier of the user performing the change.</param>
public virtual void Activate(Guid? userId = null)
{
if (InactivatedOn.HasValue)
{
InactivatedOn = null;
InactivatedBy = null;
}
}
/// <summary>
/// Marks the entity as inactive if it is not already inactive.
/// </summary>
/// <param name="userId">Identifier of the user performing the change.</param>
public virtual void Deactivate(Guid? userId = null)
{
if (!InactivatedOn.HasValue)
{
InactivatedOn = DateTimeOffset.UtcNow;
InactivatedBy = userId;
}
}
/// <summary>
/// Soft deletes the entity when it has not already been deleted.
/// </summary>
/// <param name="userId">Identifier of the user performing the change.</param>
public virtual void Delete(Guid? userId = null)
{
if (!DeletedOn.HasValue)
{
DeletedOn = DateTimeOffset.UtcNow;
DeletedBy = userId;
}
}
/// <summary>
/// Restores a previously soft-deleted entity to an inactive state.
/// </summary>
/// <param name="userId">Identifier of the user performing the change.</param>
public virtual void Restore(Guid? userId = null)
{
if (DeletedOn.HasValue)
{
DeletedOn = null;
DeletedBy = null;
}
}
}

View File

@@ -0,0 +1,17 @@
namespace Prefab.Domain.Common;
/// <summary>
/// All entities that require auditing, statuses and tenant information should use this.
/// </summary>
/// <typeparam name="T">The type that will represent the entity's ID field.</typeparam>
public interface IEntityWithAuditAndStatusAndTenant<T> : IEntityWithAuditAndStatus<T>, ITenant
{
}
/// <inheritdoc cref="IEntityWithAuditAndStatusAndTenant{T}" />
public abstract class EntityWithAuditAndStatusAndTenant<T> : EntityWithAuditAndStatus<T>,
IEntityWithAuditAndStatusAndTenant<T>
{
/// <inheritdoc />
public Guid TenantId { get; set; }
}

View File

@@ -0,0 +1,16 @@
namespace Prefab.Domain.Common;
/// <summary>
/// All entities that require auditing, statuses and tenant information should use this.
/// </summary>
/// <typeparam name="T">The type that will represent the entity's ID field.</typeparam>
public interface IEntityWithAuditAndTenant<T> : IEntityWithAudit<T>, ITenant
{
}
/// <inheritdoc cref="IEntityWithAuditAndTenant{T}" />
public class EntityWithAuditAndTenant<T> : EntityWithAudit<T>, IEntityWithAuditAndTenant<T>
{
/// <inheritdoc />
public Guid TenantId { get; set; }
}

View File

@@ -0,0 +1,17 @@
namespace Prefab.Domain.Common;
/// <summary>
/// All entities that require tenant information should use this. Use
/// <see cref="EntityWithAuditAndStatusAndTenant{T}" />
/// </summary>
/// <typeparam name="T">The type that will represent the entity's ID field.</typeparam>
public interface IEntityWithTenant<T> : IEntity<T>, ITenant
{
}
/// <inheritdoc cref="IEntityWithTenant{T}" />
public abstract class EntityWithTenant<T> : Entity<T>, IEntityWithTenant<T>
{
/// <inheritdoc />
public Guid TenantId { get; set; }
}

View File

@@ -0,0 +1,22 @@
using System.ComponentModel.DataAnnotations.Schema;
using Prefab.Base;
namespace Prefab.Domain.Common;
/// <summary>
/// Base type for domain events captured during aggregate processing.
/// </summary>
[NotMapped]
public abstract record Event : IEvent
{
/// <summary>
/// Gets a value indicating whether the event has been dispatched to external transports.
/// </summary>
public bool IsPublished { get; internal set; }
/// <summary>
/// Gets the timestamp for when the event occurred.
/// </summary>
public DateTimeOffset DateOccurred { get; protected set; } = DateTimeOffset.UtcNow;
}

View File

@@ -0,0 +1,17 @@
namespace Prefab.Domain.Common;
/// <summary>
/// Describes audit metadata captured on entities.
/// </summary>
public interface IAudit : ICreated
{
/// <summary>
/// Gets the identifier of the user that last updated the entity.
/// </summary>
Guid LastModifiedBy { get; }
/// <summary>
/// Gets when the entity was last updated.
/// </summary>
DateTimeOffset LastModifiedOn { get; }
}

View File

@@ -0,0 +1,17 @@
namespace Prefab.Domain.Common;
/// <summary>
/// Captures creation metadata for auditable entities.
/// </summary>
public interface ICreated
{
/// <summary>
/// Gets the identifier of the user that created the entity.
/// </summary>
Guid CreatedBy { get; }
/// <summary>
/// Gets when the entity was created.
/// </summary>
DateTimeOffset CreatedOn { get; }
}

View File

@@ -0,0 +1,13 @@
namespace Prefab.Domain.Common;
/// <summary>
/// Interface representation of an entity. All entities should implement this.
/// </summary>
/// <typeparam name="T">The type that will represent the entity's ID field.</typeparam>
public interface IEntity<T>
{
/// <summary>
/// Gets or sets the ID of the entity.
/// </summary>
T Id { get; set; }
}

View File

@@ -0,0 +1,29 @@
using Prefab.Base;
namespace Prefab.Domain.Common;
/// <summary>
/// Interface representation of an event in the domain model.
/// </summary>
public interface IHasDomainEvents : IEvent
{
/// <summary>
/// Gets the events of the entity.
/// </summary>
IReadOnlyCollection<Event> Events { get; }
/// <summary>
/// Adds an event to the entity.
/// </summary>
public void AddEvent(Event e);
/// <summary>
/// Removes an event from the entity.
/// </summary>
public void RemoveEvent(Event e);
/// <summary>
/// Clears all events from the entity.
/// </summary>
public void ClearEvents();
}

View File

@@ -0,0 +1,8 @@
namespace Prefab.Domain.Common;
/// <summary>
/// Interface to represent a marker on an entity, informing that the entity is considered an aggregate root.
/// </summary>
public interface IRoot
{
}

View File

@@ -0,0 +1,14 @@
using Prefab.Data;
namespace Prefab.Domain.Common;
/// <summary>
/// Interface representation of an entity's status.
/// </summary>
public interface IStatus
{
/// <summary>
/// Gets or sets the current status of the entity.
/// </summary>
public AuditStatus AuditStatus { get; }
}

View File

@@ -0,0 +1,12 @@
namespace Prefab.Domain.Common;
/// <summary>
/// Marks an entity as belonging to a tenant.
/// </summary>
public interface ITenant
{
/// <summary>
/// Gets the unique tenant identifier.
/// </summary>
Guid TenantId { get; }
}

View File

@@ -0,0 +1,11 @@
using Prefab.Domain.Common;
namespace Prefab.Domain.Events;
/// <summary>
/// Event raised when a generic attribute is added or updated on an entity.
/// </summary>
/// <param name="Entity">The entity that owns the attribute.</param>
/// <param name="Key">The attribute key.</param>
/// <param name="Value">The new attribute value.</param>
public record GenericAttributeAddedOrUpdatedEvent(object Entity, string Key, object Value) : Event;

View File

@@ -0,0 +1,25 @@
using Prefab.Domain.Common;
namespace Prefab.Domain.Events;
/// <summary>
/// Event raised whenever a generic attribute is added, updated, or removed from an entity.
/// </summary>
/// <param name="Entity">The entity that owns the attribute.</param>
/// <param name="Key">The attribute key.</param>
/// <param name="Value">The attribute value involved in the change.</param>
/// <param name="ChangeType">Indicates the type of mutation performed.</param>
public record GenericAttributeChangedEvent(object Entity, string Key, object? Value, GenericAttributeChangeType ChangeType) : Event;
/// <summary>
/// Represents the types of mutations that can occur for a generic attribute.
/// </summary>
public enum GenericAttributeChangeType
{
/// <summary>Attribute was added for the first time.</summary>
Add,
/// <summary>Attribute was updated with a new value.</summary>
Update,
/// <summary>Attribute was removed.</summary>
Remove
}

View File

@@ -0,0 +1,10 @@
using Prefab.Domain.Common;
namespace Prefab.Domain.Events;
/// <summary>
/// Event raised when a generic attribute is removed from an entity.
/// </summary>
/// <param name="Entity">The entity that owned the attribute.</param>
/// <param name="Key">The key that was removed.</param>
public record GenericAttributeRemovedEvent(object Entity, string Key) : Event;

View File

@@ -0,0 +1,9 @@
namespace Prefab.Domain.Exceptions;
/// <summary>
/// Thrown when a concurrency conflict is detected.
/// </summary>
public class ConcurrencyException : DomainException
{
public ConcurrencyException() : base("The record you attempted to modify was changed by another process.") { }
}

View File

@@ -0,0 +1,6 @@
namespace Prefab.Domain.Exceptions;
/// <summary>
/// Root of all domainrule violations.
/// </summary>
public class DomainException(string message) : Exception(message);