关于 dotnet-domain-entity-generator
面向使用 DDD 建模聚合与领域行为的 .NET 开发者,用于生成领域实体。内容涵盖封装、带校验的工厂方法、领域事件、值对象和类型化错误。
主要功能
- 01聚合根与子实体模板
- 02带校验的工厂方法
- 03状态变更领域事件
- 04不可变值对象模式
- 05类型化错误与 Result 处理
适用场景
- 01在 .NET 中建模领域聚合
- 02将经过校验的概念建为值对象
- 03通过领域事件传递实体变更
安装
复制下面的命令,在终端中运行。
$
npx skills add https:/ / github.com/ ronnythedev/ dotnet-clean-architecture-skills --skill dotnet-domain-entity-generator安装后在 Claude Code 中输入/dotnet-domain-entity-generator确认已加载。
详细介绍
领域实体生成器
概述
此技能遵循领域驱动设计(DDD)原则生成领域实体:
- 封装 - 私有 setter,受控修改
- 工厂方法 - 带验证的静态
Create()方法 - 领域事件 - 状态变更时引发事件
- 充血领域模型 - 行为位于实体中,而非服务中
- 不变量保护 - 实体始终处于有效状态
快速参考
| 概念 | 用途 | 示例 |
|---|---|---|
| 聚合根 | 聚合的入口点 | Organization、User |
| 子实体 | 聚合的一部分,在聚合外没有自己的标识 | OrderItem、AssessmentDetail |
| 值对象 | 不可变、没有标识 | Email、Money、Address |
| 领域事件 | 表示状态变更 | UserCreatedDomainEvent |
实体结构
/Domain/{Aggregate}/
├── {Entity}.cs # Main entity
├── {Entity}Errors.cs # Typed errors
├── I{Entity}Repository.cs # Repository interface
├── ValueObjects/
│ ├── {ValueObject}.cs
│ └── ...
└── Events/
├── {Entity}CreatedDomainEvent.cs
├── {Entity}UpdatedDomainEvent.cs
└── ...
模板:聚合根实体
// src/{name}.domain/{Aggregate}/{Entity}.cs
using {name}.domain.abstractions;
using {name}.domain.{aggregate}.events;
namespace {name}.domain.{aggregate};
public sealed class {Entity} : Entity
{
// ═══════════════════════════════════════════════════════════════
// PRIVATE COLLECTIONS (encapsulated)
// ═══════════════════════════════════════════════════════════════
private readonly List<{ChildEntity}> _{childEntities} = new();
// ═══════════════════════════════════════════════════════════════
// PROPERTIES (private setters)
// ═══════════════════════════════════════════════════════════════
public string Name { get; private set; }
public string? Description { get; private set; }
public bool IsActive { get; private set; }
public DateTime CreatedAt { get; private set; }
public DateTime UpdatedAt { get; private set; }
// Navigation property (read-only collection)
public IReadOnlyCollection<{ChildEntity}> {ChildEntities} => _{childEntities}.AsReadOnly();
// ═══════════════════════════════════════════════════════════════
// CONSTRUCTORS
// ═══════════════════════════════════════════════════════════════
// Private constructor for EF Core
private {Entity}() { }
// Private constructor for factory method
private {Entity}(
Guid id,
string name,
string? description,
DateTime createdAt)
: base(id)
{
Name = name;
Description = description;
IsActive = true;
CreatedAt = createdAt;
UpdatedAt = createdAt;
}
// ═══════════════════════════════════════════════════════════════
// FACTORY METHODS
// ═══════════════════════════════════════════════════════════════
/// <summary>
/// Creates a new {Entity} with validation
/// </summary>
public static Result<{Entity}> Create(
string name,
string? description,
DateTime createdAt)
{
// Validate invariants
if (string.IsNullOrWhiteSpace(name))
{
return Result.Failure<{Entity}>({Entity}Errors.NameIsRequired);
}
if (name.Length > 100)
{
return Result.Failure<{Entity}>({Entity}Errors.NameTooLong);
}
var {entity} = new {Entity}(
Guid.NewGuid(),
name,
description,
createdAt);
// Raise domain event
{entity}.RaiseDomainEvent(new {Entity}CreatedDomainEvent({entity}.Id));
return {entity};
}
// ═══════════════════════════════════════════════════════════════
// DOMAIN METHODS
// ═══════════════════════════════════════════════════════════════
/// <summary>
/// Updates the {Entity} properties
/// </summary>
public Result Update(
string name,
string? description,
DateTime updatedAt)
{
if (string.IsNullOrWhiteSpace(name))
{
return Result.Failure({Entity}Errors.NameIsRequired);
}
if (name.Length > 100)
{
return Result.Failure({Entity}Errors.NameTooLong);
}
Name = name;
Description = description;
UpdatedAt = updatedAt;
RaiseDomainEvent(new {Entity}UpdatedDomainEvent(Id));
return Result.Success();
}
/// <summary>
/// Deactivates the {Entity}
/// </summary>
public Result Deactivate(DateTime updatedAt)
{
if (!IsActive)
{
return Result.Failure({Entity}Errors.AlreadyDeactivated);
}
IsActive = false;
UpdatedAt = updatedAt;
RaiseDomainEvent(new {Entity}DeactivatedDomainEvent(Id));
return Result.Success();
}
/// <summary>
/// Reactivates the {Entity}
/// </summary>
public Result Activate(DateTime updatedAt)
{
if (IsActive)
{
return Result.Failure({Entity}Errors.AlreadyActive);
}
IsActive = true;
UpdatedAt = updatedAt;
return Result.Success();
}
// ═══════════════════════════════════════════════════════════════
// CHILD ENTITY MANAGEMENT
// ═══════════════════════════════════════════════════════════════
/// <summary>
/// Adds a child entity to this aggregate
/// </summary>
public Result Add{ChildEntity}({ChildEntity} {childEntity})
{
if ({childEntity} is null)
{
return Result.Failure({Entity}Errors.Child{ChildEntity}Required);
}
if (_{childEntities}.Any(c => c.Name == {childEntity}.Name))
{
return Result.Failure({Entity}Errors.Duplicate{ChildEntity}Name);
}
_{childEntities}.Add({childEntity});
RaiseDomainEvent(new {ChildEntity}AddedDomainEvent(Id, {childEntity}.Id));
return Result.Success();
}
/// <summary>
/// Removes a child entity from this aggregate
/// </summary>
public Result Remove{ChildEntity}(Guid {childEntity}Id)
{
var {childEntity} = _{childEntities}.FirstOrDefault(c => c.Id == {childEntity}Id);
if ({childEntity} is null)
{
return Result.Failure({Entity}Errors.{ChildEntity}NotFound);
}
_{childEntities}.Remove({childEntity});
return Result.Success();
}
// ═══════════════════════════════════════════════════════════════
// QUERY METHODS
// ═══════════════════════════════════════════════════════════════
public bool HasActiveChildren() => _{childEntities}.Any(c => c.IsActive);
public {ChildEntity}? GetChildById(Guid childId) =>
_{childEntities}.FirstOrDefault(c => c.Id == childId);
}
模板:子实体(聚合的一部分)
// src/{name}.domain/{Aggregate}/{ChildEntity}.cs
using {name}.domain.abstractions;
namespace {name}.domain.{aggregate};
public sealed class {ChildEntity} : Entity
{
// ═══════════════════════════════════════════════════════════════
// PROPERTIES
// ═══════════════════════════════════════════════════════════════
public Guid {Parent}Id { get; private set; }
public string Name { get; private set; }
public string? Description { get; private set; }
public int SortOrder { get; private set; }
public bool IsActive { get; private set; }
public DateTime CreatedAt { get; private set; }
public DateTime UpdatedAt { get; private set; }
// Navigation property
public {Parent} {Parent} { get; private set; } = null!;
// ═══════════════════════════════════════════════════════════════
// CONSTRUCTORS
// ═══════════════════════════════════════════════════════════════
private {ChildEntity}() { } // EF Core
private {ChildEntity}(
Guid id,
Guid {parent}Id,
string name,
string? description,
int sortOrder,
DateTime createdAt)
: base(id)
{
{Parent}Id = {parent}Id;
Name = name;
Description = description;
SortOrder = sortOrder;
IsActive = true;
CreatedAt = createdAt;
UpdatedAt = createdAt;
}
// ═══════════════════════════════════════════════════════════════
// FACTORY METHOD
// ═══════════════════════════════════════════════════════════════
public static {ChildEntity} Create(
Guid {parent}Id,
string name,
string? description,
int sortOrder,
DateTime createdAt)
{
return new {ChildEntity}(
Guid.NewGuid(),
{parent}Id,
name,
description,
sortOrder,
createdAt);
}
// ═══════════════════════════════════════════════════════════════
// DOMAIN METHODS
// ═══════════════════════════════════════════════════════════════
public void Update(
string name,
string? description,
int sortOrder,
DateTime updatedAt)
{
Name = name;
Description = description;
SortOrder = sortOrder;
UpdatedAt = updatedAt;
}
public void Deactivate(DateTime updatedAt)
{
IsActive = false;
UpdatedAt = updatedAt;
}
}
模板:值对象
// src/{name}.domain/{Aggregate}/ValueObjects/Email.cs
namespace {name}.domain.{aggregate}.valueobjects;
public sealed record Email
{
public string Value { get; }
private Email(string value)
{
Value = value;
}
public static Result<Email> Create(string email)
{
if (string.IsNullOrWhiteSpace(email))
{
return Result.Failure<Email>(EmailErrors.Empty);
}
email = email.Trim().ToLowerInvariant();
if (email.Length > 255)
{
return Result.Failure<Email>(EmailErrors.TooLong);
}
if (!IsValidFormat(email))
{
return Result.Failure<Email>(EmailErrors.InvalidFormat);
}
return new Email(email);
}
private static bool IsValidFormat(string email)
{
// Simple email validation
var atIndex = email.IndexOf('@');
var dotIndex = email.LastIndexOf('.');
return atIndex > 0
&& dotIndex > atIndex + 1
&& dotIndex < email.Length - 1;
}
public override string ToString() => Value;
// Implicit conversion for convenience
public static implicit operator string(Email email) => email.Value;
}
public static class EmailErrors
{
public static readonly Error Empty = new("Email.Empty", "Email cannot be empty");
public static readonly Error TooLong = new("Email.TooLong", "Email cannot exceed 255 characters");
public static readonly Error InvalidFormat = new("Email.InvalidFormat", "Email format is invalid");
}
更多值对象示例
// Money Value Object
public sealed record Money
{
public decimal Amount { get; }
public string Currency { get; }
private Money(decimal amount, string currency)
{
Amount = amount;
Currency = currency;
}
public static Result<Money> Create(decimal amount, string currency = "USD")
{
if (amount < 0)
return Result.Failure<Money>(MoneyErrors.NegativeAmount);
if (string.IsNullOrWhiteSpace(currency) || currency.Length != 3)
return Result.Failure<Money>(MoneyErrors.InvalidCurrency);
return new Money(Math.Round(amount, 2), currency.ToUpperInvariant());
}
public Money Add(Money other)
{
if (Currency != other.Currency)
throw new InvalidOperationException("Cannot add different currencies");
return new Money(Amount + other.Amount, Currency);
}
public static Money Zero(string currency = "USD") => new(0, currency);
}
// DateRange Value Object
public sealed record DateRange
{
public DateTime Start { get; }
public DateTime End { get; }
private DateRange(DateTime start, DateTime end)
{
Start = start;
End = end;
}
public static Result<DateRange> Create(DateTime start, DateTime end)
{
if (end <= start)
return Result.Failure<DateRange>(DateRangeErrors.EndMustBeAfterStart);
return new DateRange(start, end);
}
public bool Contains(DateTime date) => date >= Start && date <= End;
public bool Overlaps(DateRange other) =>
Start < other.End && End > other.Start;
public int DurationInDays => (End - Start).Days;
}
模板:领域错误
// src/{name}.domain/{Aggregate}/{Entity}Errors.cs
using {name}.domain.abstractions;
namespace {name}.domain.{aggregate};
public static class {Entity}Errors
{
// Not found errors
public static readonly Error NotFound = new(
"{Entity}.NotFound",
"The {entity} with the specified ID was not found");
// Validation errors
public static readonly Error NameIsRequired = new(
"{Entity}.NameRequired",
"{Entity} name is required");
public static readonly Error NameTooLong = new(
"{Entity}.NameTooLong",
"{Entity} name cannot exceed 100 characters");
// Business rule errors
public static readonly Error AlreadyExists = new(
"{Entity}.AlreadyExists",
"A {entity} with this name already exists");
public static readonly Error AlreadyDeactivated = new(
"{Entity}.AlreadyDeactivated",
"The {entity} is already deactivated");
public static readonly Error AlreadyActive = new(
"{Entity}.AlreadyActive",
"The {entity} is already active");
public static readonly Error CannotDeleteWithActiveRelationships = new(
"{Entity}.CannotDeleteWithActiveRelationships",
"Cannot delete {entity} with active relationships");
// Child entity errors
public static readonly Error {ChildEntity}NotFound = new(
"{Entity}.{ChildEntity}NotFound",
"The {childEntity} was not found in this {entity}");
public static readonly Error Duplicate{ChildEntity}Name = new(
"{Entity}.Duplicate{ChildEntity}Name",
"A {childEntity} with this name already exists");
public static readonly Error Child{ChildEntity}Required = new(
"{Entity}.Child{ChildEntity}Required",
"{ChildEntity} cannot be null");
}
模板:领域事件
// src/{name}.domain/{Aggregate}/Events/{Entity}CreatedDomainEvent.cs
using {name}.domain.abstractions;
namespace {name}.domain.{aggregate}.events;
public sealed record {Entity}CreatedDomainEvent(Guid {Entity}Id) : IDomainEvent;
// src/{name}.domain/{Aggregate}/Events/{Entity}UpdatedDomainEvent.cs
public sealed record {Entity}UpdatedDomainEvent(Guid {Entity}Id) : IDomainEvent;
// src/{name}.domain/{Aggregate}/Events/{Entity}DeactivatedDomainEvent.cs
public sealed record {Entity}DeactivatedDomainEvent(Guid {Entity}Id) : IDomainEvent;
// src/{name}.domain/{Aggregate}/Events/{ChildEntity}AddedDomainEvent.cs
public sealed record {ChildEntity}AddedDomainEvent(
Guid {Entity}Id,
Guid {ChildEntity}Id) : IDomainEvent;
模板:仓储接口
// src/{name}.domain/{Aggregate}/I{Entity}Repository.cs
namespace {name}.domain.{aggregate};
public interface I{Entity}Repository
{
// ═══════════════════════════════════════════════════════════════
// READ OPERATIONS
// ═══════════════════════════════════════════════════════════════
Task<{Entity}?> GetByIdAsync(
Guid id,
CancellationToken cancellationToken = default);
Task<{Entity}?> GetByNameAsync(
string name,
CancellationToken cancellationToken = default);
Task<IReadOnlyList<{Entity}>> GetByOrganizationIdAsync(
Guid organizationId,
CancellationToken cancellationToken = default);
Task<bool> ExistsAsync(
Guid id,
CancellationToken cancellationToken = default);
// ═══════════════════════════════════════════════════════════════
// WRITE OPERATIONS
// ═══════════════════════════════════════════════════════════════
void Add({Entity} {entity});
void AddRange(IEnumerable<{Entity}> {entities});
void Update({Entity} {entity});
void Remove({Entity} {entity});
}
实体基类
// src/{name}.domain/Abstractions/Entity.cs
namespace {name}.domain.abstractions;
public abstract class Entity
{
private readonly List<IDomainEvent> _domainEvents = new();
protected Entity(Guid id)
{
Id = id;
}
protected Entity() { } // EF Core
public Guid Id { get; init; }
public IReadOnlyList<IDomainEvent> GetDomainEvents() => _domainEvents.ToList();
public void ClearDomainEvents() => _domainEvents.Clear();
protected void RaiseDomainEvent(IDomainEvent domainEvent)
{
_domainEvents.Add(domainEvent);
}
public override bool Equals(object? obj)
{
if (obj is not Entity other)
return false;
if (ReferenceEquals(this, other))
return true;
if (GetType() != other.GetType())
return false;
if (Id == Guid.Empty || other.Id == Guid.Empty)
return false;
return Id == other.Id;
}
public static bool operator ==(Entity? left, Entity? right)
{
if (left is null && right is null)
return true;
if (left is null || right is null)
return false;
return left.Equals(right);
}
public static bool operator !=(Entity? left, Entity? right) => !(left == right);
public override int GetHashCode() => Id.GetHashCode() * 41;
}
DDD 关键规则
- 始终使用私有 setter - 不允许从外部直接修改属性
- 使用工厂方法创建 - 使用带验证的静态
Create()方法 - 用领域事件表示状态变更 - 发出重要变更的信号
- 实体始终有效 - 在构造函数和方法中保护不变量
- 聚合根控制子实体 - 通过根实体管理子实体
- 值对象不可变 - 使用
record类型 - 每个聚合根对应一个仓储 - 而非每个实体一个
- setter 中不包含逻辑 - 使用命名方法
- 使用 Result 模式 - 返回错误,不要抛出异常
- 保持实体与持久化无关 - 领域层不添加 EF Core 特性
应避免的反模式
// ❌ WRONG: Public setters
public string Name { get; set; }
// ✅ CORRECT: Private setters
public string Name { get; private set; }
// ❌ WRONG: Constructor with all parameters
public User(Guid id, string name, string email, DateTime createdAt, ...)
// ✅ CORRECT: Factory method
public static Result<User> Create(string name, string email, DateTime createdAt)
// ❌ WRONG: Throwing exceptions
if (name == null) throw new ArgumentNullException(nameof(name));
// ✅ CORRECT: Return Result
if (string.IsNullOrWhiteSpace(name))
return Result.Failure<Entity>(EntityErrors.NameRequired);
// ❌ WRONG: Anemic domain model
public class User
{
public string Name { get; set; }
public void SetName(string name) => Name = name; // Just a setter!
}
// ✅ CORRECT: Rich domain model with behavior
public class User
{
public string Name { get; private set; }
public Result ChangeName(string newName, DateTime updatedAt)
{
if (string.IsNullOrWhiteSpace(newName))
return Result.Failure(UserErrors.NameRequired);
Name = newName;
UpdatedAt = updatedAt;
RaiseDomainEvent(new UserNameChangedDomainEvent(Id, newName));
return Result.Success();
}
}
// ❌ WRONG: Exposing internal collections
public List<OrderItem> Items { get; set; } = new();
// ✅ CORRECT: Encapsulated collections
private readonly List<OrderItem> _items = new();
public IReadOnlyCollection<OrderItem> Items => _items.AsReadOnly();
相关技能
dotnet-repository-pattern- 实现仓储dotnet-ef-core-configuration- 将实体映射到数据库dotnet-domain-events-generator- 处理领域事件dotnet-result-pattern- 错误处理
评论
支持 Markdown