一键导入
entity-generator
Generate entity classes, unit tests, and database migrations following hexagonal architecture DDD principles for this project
用 Codex 或 Claude 帮你安装 复制这段 Prompt,粘贴到 Codex、Claude 或其他助手里,让它检查 Skill 页面并帮你完成安装。
菜单
Generate entity classes, unit tests, and database migrations following hexagonal architecture DDD principles for this project
用 Codex 或 Claude 帮你安装 复制这段 Prompt,粘贴到 Codex、Claude 或其他助手里,让它检查 Skill 页面并帮你完成安装。
基于 SOC 职业分类
BFF code review gate for standards, tests, complexity <= 30, modern C#, and security validation.
Contracts code review gate for standards, tests, complexity <= 30, modern C#, and security validation.
Full template code review gate for DDD/SOLID standards, tests, complexity <= 30, modern C#, and security validation.
Create or update BFF contracts in src/Contracts (records, DTOs, and .proto). Use when user asks to add/change request/response contracts, order DTOs, base contract wrappers, or gRPC proto contracts.
Create a new minimal API endpoint in src/WebApp and add matching integration tests in tests/IntegrationTests. Use when user asks for new GET/POST/PUT/PATCH/DELETE endpoints plus end-to-end validation.
Create or extend k6 load tests for this BFF template. Use when user asks for HTTP/gRPC performance scenarios, thresholds, custom metrics, or environment-driven load profiles in tests/LoadTests.
| name | entity-generator |
| description | Generate entity classes, unit tests, and database migrations following hexagonal architecture DDD principles for this project |
Generate domain entity classes, corresponding unit tests, and EF Core migrations following the project's hexagonal architecture patterns.
Activate this skill when:
| File | Purpose |
|---|---|
./references/entity-template.md | Aggregate root template (Create, Update, Delete, Result<T>) |
./references/tests-template.md | Domain unit test template with naming convention |
Keep this skill self-contained: prefer these in-folder references when generating entities.
Result<T> + static Create)Use this for aggregate roots or entities with complex invariants and multiple business operations (create, update, delete).
Example: Order, any root entity of a bounded context.
using Domain.Common;
using Domain.Common.Extensions;
namespace Domain.Products;
public sealed class Product : DomainEntity
{
public Product() { } // Required for EF Core
private Product(
string name,
decimal price,
string? createdBy = null,
string? timezoneId = null
) : base(createdBy ?? "System", timezoneId)
{
Name = name;
Price = price;
}
public string Name { get; private set; }
public decimal Price { get; private set; }
public static Result<Product> Create(
string name,
decimal price,
string user = "System",
string? timezoneId = null
) => Handle(activity =>
{
if (string.IsNullOrWhiteSpace(name))
return Result.Fail<Product>("Product name is required.");
if (price <= 0)
return Result.Fail<Product>("Product price must be greater than zero.");
var product = new Product(name, price, user, timezoneId);
activity?.SetTag(nameof(name), name);
activity?.SetTag(nameof(price), price);
return Result.Ok(product);
});
public Result Update(
string name,
decimal price,
string user = "System",
string? timezoneId = null
) => Handle(activity =>
{
if (IsDeleted)
return Result.Fail("Cannot update a deleted product.");
if (string.IsNullOrWhiteSpace(name))
return Result.Fail("Product name is required.");
if (price <= 0)
return Result.Fail("Product price must be greater than zero.");
Name = name;
Price = price;
var updateResult = Update(user, timezoneId);
if (updateResult.IsFailure)
return updateResult;
activity?.SetTag(nameof(name), name);
return Result.Ok();
});
}
DomainException)Use this for child entities or simpler domain objects that belong to an aggregate (e.g., line items, embedded sub-objects). Validation throws DomainException directly.
Example: Item, Address, PhoneNumber.
using Domain.Common;
using Domain.Common.Exceptions;
namespace Domain.Products;
public sealed class ProductVariant : DomainEntity
{
public ProductVariant() { } // Required for EF Core
public ProductVariant(
string sku,
decimal additionalCost,
string? createdBy = null,
string? timezoneId = null
) : base(createdBy ?? "System", timezoneId)
{
if (string.IsNullOrWhiteSpace(sku))
throw new DomainException("SKU is required.");
if (additionalCost < 0)
throw new DomainException("Additional cost cannot be negative.");
Sku = sku;
AdditionalCost = additionalCost;
}
public string Sku { get; private set; }
public decimal AdditionalCost { get; private set; }
}
| Criteria | Pattern A (aggregate root) | Pattern B (child entity) |
|---|---|---|
| Is it the root of a bounded context? | ✅ | |
| Does it have complex business workflows? | ✅ | |
| Does another entity own its lifecycle? | ✅ | |
| Is it a simple value container with basic validation? | ✅ |
File path: src/Domain/{PluralContext}/{EntityName}.cs
Example: src/Domain/Products/Product.cs
Checklist:
public sealed class extending DomainEntitypublic EntityName() { } — parameterless constructor for EF Coreprivate set (use init only for base-class-assigned properties)Result<TEntity> Create(...) method using Handle(activity => ...) (Pattern A)Result using Handle(activity => ...) (Pattern A)DomainException thrown for violations in constructors (Pattern B)activity?.SetTag(nameof(property), value) for meaningful telemetry tagsUpdate(user, timezoneId) from DomainEntity base when modifying audit fieldsFile path: src/Domain/Common/Enums/{EnumName}.cs
namespace Domain.Common.Enums;
public enum ProductCategory
{
Electronics,
Clothing,
Food
}
If the entity needs persistence, add its EF Core configuration in the Infrastructure layer. Check existing patterns in src/Infrastructure/Data/.
File path: tests/UnitTests/Domain/{EntityName}Tests.cs
GivenContext_WhenCondition_ThenExpectedResult
Examples: GivenANewProductWhenValidPriceThenShouldCreateWithSuccess[Fact(DisplayName = nameof(MethodName))]// Arrange, // Act, // Assert (combine Arrange+Act when trivial)sealed classes by method under testusing Domain.Common.Exceptions;
using Domain.Products;
namespace UnitTests.Domain;
public sealed class ProductTests
{
// ── Create ────────────────────────────────────────────────────────────────
[Fact(DisplayName = nameof(GivenANewProductWhenValidInputThenShouldCreateWithSuccess))]
public void GivenANewProductWhenValidInputThenShouldCreateWithSuccess()
{
// Arrange, Act
var result = Product.Create("Laptop", 999.99m, "John Doe", "America/New_York");
// Assert
Assert.True(result.Success);
Assert.NotNull(result.Value);
Assert.Equal("Laptop", result.Value.Name);
Assert.Equal(999.99m, result.Value.Price);
Assert.Equal("John Doe", result.Value.CreatedBy);
Assert.Equal("America/New_York", result.Value.CreatedByTimezoneId);
}
[Fact(DisplayName = nameof(GivenANewProductWhenNoUserProvidedThenShouldDefaultToSystem))]
public void GivenANewProductWhenNoUserProvidedThenShouldDefaultToSystem()
{
// Arrange, Act
var result = Product.Create("Laptop", 999.99m);
// Assert
Assert.True(result.Success);
Assert.Equal("System", result.Value.CreatedBy);
Assert.Equal("UTC", result.Value.CreatedByTimezoneId);
}
[Fact(DisplayName = nameof(GivenANewProductWhenNameIsEmptyThenShouldReturnFailure))]
public void GivenANewProductWhenNameIsEmptyThenShouldReturnFailure()
{
// Arrange, Act
var result = Product.Create("", 999.99m);
// Assert
Assert.True(result.IsFailure);
Assert.Equal("Product name is required.", result.Message);
}
[Fact(DisplayName = nameof(GivenANewProductWhenPriceIsZeroThenShouldReturnFailure))]
public void GivenANewProductWhenPriceIsZeroThenShouldReturnFailure()
{
// Arrange, Act
var result = Product.Create("Laptop", 0m);
// Assert
Assert.True(result.IsFailure);
Assert.Equal("Product price must be greater than zero.", result.Message);
}
// ── Update ─────────────────────────────────────────────────────────
[Fact(DisplayName = nameof(GivenAnExistingProductWhenValidUpdateThenShouldUpdateWithSuccess))]
public void GivenAnExistingProductWhenValidUpdateThenShouldUpdateWithSuccess()
{
// Arrange
var product = Product.Create("Laptop", 999.99m).Value;
// Act
var result = product.Update("Gaming Laptop", 1499.99m, "Jane Doe");
// Assert
Assert.True(result.Success);
Assert.Equal("Gaming Laptop", product.Name);
Assert.Equal(1499.99m, product.Price);
}
[Fact(DisplayName = nameof(GivenADeletedProductWhenUpdateIsCalledThenShouldReturnFailure))]
public void GivenADeletedProductWhenUpdateIsCalledThenShouldReturnFailure()
{
// Arrange
var product = Product.Create("Laptop", 999.99m).Value;
product.Delete();
// Act
var result = product.Update("Gaming Laptop", 1499.99m);
// Assert
Assert.True(result.IsFailure);
Assert.Equal("Cannot update a deleted product.", result.Message);
}
}
DomainException)[Fact(DisplayName = nameof(GivenANewVariantWhenSkuIsEmptyThenShouldThrowDomainException))]
public void GivenANewVariantWhenSkuIsEmptyThenShouldThrowDomainException()
{
// Arrange, Act
var exception = Assert.Throws<DomainException>(() => new ProductVariant("", 10m));
// Assert
Assert.Equal("SKU is required.", exception.Message);
}
[Fact(DisplayName = nameof(GivenANewVariantWhenNegativeCostThenShouldThrowDomainException))]
public void GivenANewVariantWhenNegativeCostThenShouldThrowDomainException()
{
// Arrange, Act
var exception = Assert.Throws<DomainException>(() => new ProductVariant("SKU-001", -5m));
// Assert
Assert.Equal("Additional cost cannot be negative.", exception.Message);
}
Always check and set IsDeleted guard in update methods before performing changes:
if (IsDeleted)
return Result.Fail("Cannot update a deleted entity.");
Use decimal — never double or float — for prices, amounts, and totals.
src/Domain/Common/Enums/Domain.Common.Enumsusing Domain.Common.Enums;Set meaningful tags on activity to aid distributed tracing. Prefer business identifiers:
activity?.SetTag(nameof(name), name);
activity?.SetTag(nameof(price), price);
Avoid tagging sensitive personal or financial data.
Update() from Base ClassCall DomainEntity.Update(user, timezoneId) at the end of every mutating method to keep audit fields (UpdatedAt, UpdatedBy, UpdatedByTimezoneId) in sync:
var updateResult = Update(user, timezoneId);
if (updateResult.IsFailure)
return updateResult;
Handle())When a constructor needs telemetry but cannot use Handle() (e.g., Pattern B constructors), start an activity manually as seen in Notification.cs:
using var activity = ActivitySource.StartActivity($"{GetType().Name}.Constructor");
activity.SetDefaultTags();
The Domain layer typically has no DI registrations beyond what is added by EF Core configurations in Infrastructure. If you add a domain service, register it in src/Domain/DomainDependencyInjection.cs.
Before delivering generated entity code, confirm:
public sealed class extending DomainEntitypublic EntityName() { } present for EF Coreprivate setResult.Fail<T>(message) (Pattern A) or throws DomainException (Pattern B)Update(user, timezoneId) called from all mutating methods (Pattern A)ActivitySource telemetry tags added for meaningful fieldsdecimal used for all monetary valuessrc/Domain/Common/Enums/GivenContext_WhenCondition_ThenExpectedResult naming[Fact(DisplayName = nameof(MethodName))]