Prompt file imported from mhmtdev/CleanArcStack (
.github/prompts/add-feature.prompt.md). Fill in{{featureName}}before use. Copyright stays with the author.
Add a complete vertical slice for a new feature: {{featureName}}
Follow this exact order. Implement all layers without asking for confirmation unless you hit an ambiguity that blocks progress.
1 — Domain layer (src/CleanArcStack.Domain/)
If a new entity is needed:
-
Strongly typed ID:
src/Domain/Entities/{Entity}Id.cspublic sealed record {Entity}Id(Guid Value); -
Entity:
src/Domain/Entities/{Entity}.cs- Inherit
AggregateRoot(aggregates) orBaseEntity(child entities) private/protectedconstructor +static Create(...)factory receivingTimeProvider- No public setters — state changes via domain methods
- Private
List<T>collections exposed asIReadOnlyList<T> - Call
RaiseDomainEvent(...)inside factory/domain methods Guid.CreateVersion7()— neverGuid.NewGuid()- Throw
DomainExceptionfor non-recoverable invariant violations - Return
Resultfor recoverable failures
- Inherit
-
Domain event:
src/Domain/Events/{Entity}{Action}DomainEvent.cspublic sealed record {Entity}{Action}DomainEvent(Guid {Entity}Id) : IDomainEvent; -
Repository interface:
src/Domain/Repositories/I{Entity}Repository.csvoid Add/Update/Remove(not async — flushed byIUnitOfWork)Task<{Entity}?> GetByIdAsync({Entity}Id id, CancellationToken ct = default)IReadOnlyList<T>for collection returns
-
Error class:
{Entity}Errorsstatic class with Turkish messages:public static readonly Error NotFound = new("Entity.NotFound", "Türkçe mesaj.");
2 — Application layer (src/CleanArcStack.Application/Features/{FeatureName}/)
Structure:
Features/{FeatureName}/
├── Commands/ {Action}{Entity}Command.cs (Command record + Validator in same file)
├── Queries/ Get{Entity}ByIdQuery.cs (Query record + Validator in same file)
├── EventHandlers/ {Entity}{Action}DomainEventHandler.cs
├── DTOs/ {Entity}Response.cs (+ static ToResponse() Mapster extension)
└── {Entity}Errors.cs
Rules:
- Handler return type:
ValueTask<Result<T>>(Mediator, not MediatR —using Mediator;) - Every Command/Query must have a paired
AbstractValidator<T>in the same file - Validation messages in Turkish
- No
ConfigureAwait(false)in Application layer
3 — Infrastructure layer (src/CleanArcStack.Infrastructure/)
-
EF Core config:
Persistence/Configurations/{Entity}Configuration.csHasConversionfor strongly typed IDHasIndexfor common query patterns
-
Repository:
Persistence/Repositories/{Entity}Repository.csAsNoTracking()on ALL read paths — no exceptionsConfigureAwait(false)on every async call
-
Register in
DependencyInjection.cs→AddPersistence:services.AddScoped<I{Entity}Repository, {Entity}Repository>(); -
Migration (after configs are done):
dotnet ef migrations add $(date +%Y%m%d)_Add{Entity} \ --project src/CleanArcStack.Infrastructure \ --startup-project src/CleanArcStack.API
4 — API layer (src/CleanArcStack.API/Controllers/)
[ApiVersion("1.0")]
[Route("api/v{version:apiVersion}/[controller]")]
[Authorize]
public sealed class {Feature}Controller : ApiControllerBase
{
[HttpPost]
public async Task<IActionResult> Create([FromBody] Create{Entity}Request request, CancellationToken ct)
{
var result = await Sender.Send(new Create{Entity}Command(...), ct);
return result.IsSuccess
? CreatedResponse("Get{Entity}ById", new { id = result.Value }, result.Value)
: ErrorResponse(result.Error);
}
[HttpGet("{id:guid}", Name = "Get{Entity}ById")]
public async Task<IActionResult> GetById(Guid id, CancellationToken ct) =>
FromResult(await Sender.Send(new Get{Entity}ByIdQuery(id), ct));
}
5 — Unit Tests (tests/CleanArcStack.Unit.Tests/{FeatureName}/)
{Entity}Tests.cs— domain entity behaviour (factory, invariants, domain events)Create{Entity}CommandHandlerTests.cs— handler with NSubstitute mocks +FakeTimeProvider- Naming:
{Method}_{Scenario}_{ExpectedResult} Guid.CreateVersion7()in test data
Checklist before finishing
Run dotnet build CleanArcStack.sln and fix all errors. Then verify:
- Strongly typed ID has
HasConversionin EF config - Every Command/Query has a validator
- Migration created and named
YYYYMMDD_{Description} - Repository registered in
AddPersistence - Error messages in Turkish, codes in English
-
ConfigureAwait(false)on Infrastructure async calls -
AsNoTracking()on all read paths -
[AllowAnonymous]on any public endpoints
