From dknet-minimal
Create CRUD actions (Create/Update/Delete), DTOs, validators, specs, and domain events at the AppServices layer using this project's SlimMessageBus + FluentResults + Mapster pattern. Use after domain entity and EF Core config are ready.
How this skill is triggered — by the user, by Claude, or both
Slash command
/dknet-minimal:dknet-appservices-actionsThe summary Claude sees in its skill listing — used to decide when to auto-load this skill
Create the application service layer — request/response DTOs, command handlers, validators, query specifications, and domain events — using SlimMessageBus Fluent patterns.
Create the application service layer — request/response DTOs, command handlers, validators, query specifications, and domain events — using SlimMessageBus Fluent patterns.
[GenerateDto] for all)This project does NOT use custom repository interfaces or service classes. Instead:
Fluents.Requests.IWitResponse<TDto> (with response) or Fluents.Requests.INoResponse (without)Fluents.Requests.IHandler<TRequest, TResponse> or Fluents.Requests.IHandler<TRequest>IRepositorySpec (injected) — a generic spec-based repositorySpecification<TEntity> patternMapster via IMapper + [MapsFrom] attributeFluentResults — Result.Ok(dto), Result.Fail<T>("message")mapper.ResultOf<TDto>(entity) — maps AFTER SaveChangesAll requests extend RequestBase which provides [JsonIgnore] string? ByUser — auto-filled by SetUserIdPropertyFilter from JWT claims.
src/ApiEndpoints/Minimal.AppServices/
├── {Feature}/
│ └── V{N}/
│ ├── {Entity}Dto.cs ← Response DTO (GenerateDto)
│ ├── Actions/
│ │ ├── Create.cs ← Request + Validator + Handler
│ │ ├── Update.cs ← Request + Validator + Handler
│ │ └── Delete.cs ← Request + Handler
│ ├── Specs/
│ │ └── SpecGet{Entity}.cs ← Query specification
│ └── Events/
│ └── {Event}Handlers.cs ← Event record + handlers
├── Share/
│ ├── RequestBase.cs ← DO NOT MODIFY
│ ├── PageableQuery.cs ← DO NOT MODIFY
│ ├── IPrincipalProvider.cs ← DO NOT MODIFY
│ └── Generics/ ← Generic list/paged specs
├── Extensions/
│ ├── MapsFromAttribute.cs ← DO NOT MODIFY
│ └── LazyMapper/ ← DO NOT MODIFY
└── GlobalUsings.cs ← Global imports (Fluents, FluentResults, etc.)
global using DKNet.SlimBus.Extensions; // Fluents.Requests, Fluents.Queries, etc.
global using System.ComponentModel.DataAnnotations;
global using System.Text.Json.Serialization;
global using FluentResults;
global using FluentValidation;
global using Mapster;
global using MapsterMapper;
Create src/ApiEndpoints/Minimal.AppServices/{Feature}/V1/{Entity}Dto.cs:
using DKNet.EfCore.DtoGenerator;
using Minimal.AppServices.Extensions;
namespace Minimal.AppServices.{Feature}.V1;
[GenerateDto(typeof({Entity}), Exclude = [])]
[MapsFrom(typeof({Entity}))]
public sealed partial record {Entity}Dto;
[GenerateDto] auto-generates all properties from the entity. Use Exclude = ["InternalProp"] to hide fields.
Create src/ApiEndpoints/Minimal.AppServices/{Feature}/V1/Actions/Create.cs:
using System.Data;
using DKNet.EfCore.Specifications;
using DKNet.EfCore.Specifications.Extensions;
using Minimal.AppServices.{Feature}.V1.Events;
using Minimal.AppServices.{Feature}.V1.Specs;
using Minimal.AppServices.Extensions;
using Minimal.AppServices.Share;
namespace Minimal.AppServices.{Feature}.V1.Actions;
/// <summary>
/// Command to create a new {entity}.
/// </summary>
[MapsFrom(typeof({Entity}))]
public sealed record Create{Entity}Request : RequestBase, Fluents.Requests.IWitResponse<{Entity}Dto>
{
#region Properties
[Required] public string {RequiredField1} { get; set; } = null!;
[Required] public string {RequiredField2} { get; set; } = null!;
public string? {OptionalField} { get; set; }
// Auto-generated fields (hidden from API)
[JsonIgnore] public string {AutoField} { get; set; } = null!;
#endregion
}
/// <summary>
/// Validator for <see cref="Create{Entity}Request"/>.
/// </summary>
internal sealed class Create{Entity}RequestValidator : AbstractValidator<Create{Entity}Request>
{
public Create{Entity}RequestValidator()
{
RuleFor(a => a.{RequiredField1}).NotEmpty().Length({min}, {max});
RuleFor(a => a.{RequiredField2}).NotEmpty().EmailAddress().Length(1, {max});
}
}
/// <summary>
/// Handler: validates uniqueness, maps to entity, persists, publishes event.
/// </summary>
internal sealed class Create{Entity}Handler(
IRepositorySpec repository,
// Inject domain services if needed:
// I{Service} serviceProvider,
IMapper mapper)
: Fluents.Requests.IHandler<Create{Entity}Request, {Entity}Dto>
{
public async Task<IResult<{Entity}Dto>> OnHandle(
Create{Entity}Request request,
CancellationToken cancellationToken)
{
// 1. Auto-generate fields if needed
// if (string.IsNullOrWhiteSpace(request.{AutoField}))
// request.{AutoField} = await serviceProvider.NextValueAsync();
// 2. Check duplicates
if (await repository.AnyAsync(
new SpecGet{Entity}(by{UniqueField}: request.{UniqueField}),
cancellationToken: cancellationToken))
{
return Result.Fail<{Entity}Dto>($"{UniqueField} {request.{UniqueField}} already exists.");
}
// 3. Map request to entity
var entity = mapper.Map<{Entity}>(request);
// 3b. Defensive check — ensure auto-generated fields were mapped
// if (string.IsNullOrEmpty(entity.{AutoField}))
// throw new NoNullAllowedException(nameof(entity.{AutoField}));
// 4. Persist
await repository.AddAsync(entity, cancellationToken);
// 5. Publish domain event
entity.AddEvent(new {Entity}CreatedEvent(entity.Id, entity.{NameField}));
// 6. Return lazy-mapped DTO (resolves after SaveChanges)
return mapper.ResultOf<{Entity}Dto>(entity);
}
}
Create src/ApiEndpoints/Minimal.AppServices/{Feature}/V1/Actions/Update.cs:
using DKNet.EfCore.Specifications;
using DKNet.EfCore.Specifications.Extensions;
using Minimal.AppServices.{Feature}.V1.Specs;
using Minimal.AppServices.Extensions;
using Minimal.AppServices.Share;
namespace Minimal.AppServices.{Feature}.V1.Actions;
/// <summary>
/// Command to update an existing {entity}.
/// </summary>
[MapsFrom(typeof({Entity}))]
public record Update{Entity}Request : RequestBase, Fluents.Requests.IWitResponse<{Entity}Dto>
{
public required Guid Id { get; init; }
public string? {MutableField1} { get; init; }
public string? {MutableField2} { get; init; }
}
internal sealed class Update{Entity}Handler(
IMapper mapper,
IRepositorySpec repo) : Fluents.Requests.IHandler<Update{Entity}Request, {Entity}Dto>
{
public async Task<IResult<{Entity}Dto>> OnHandle(
Update{Entity}Request request,
CancellationToken cancellationToken)
{
if (request.Id == Guid.Empty)
return Result.Fail<{Entity}Dto>("The Id is invalid.");
var entity = await repo.FirstOrDefaultAsync(
new SpecGet{Entity}(request.Id), cancellationToken);
if (entity == null)
return Result.Fail<{Entity}Dto>($"The {Entity} {request.Id} is not found.");
// Call entity mutation method
entity.Update({mutable params}, request.ByUser!);
return Result.Ok(mapper.Map<{Entity}Dto>(entity));
}
}
Create src/ApiEndpoints/Minimal.AppServices/{Feature}/V1/Actions/Delete.cs:
using DKNet.EfCore.Specifications;
using DKNet.EfCore.Specifications.Extensions;
using Minimal.AppServices.{Feature}.V1.Specs;
using Minimal.AppServices.Share;
namespace Minimal.AppServices.{Feature}.V1.Actions;
/// <summary>
/// Command to delete a {entity} by ID.
/// </summary>
public record Delete{Entity}Request : RequestBase, Fluents.Requests.INoResponse
{
public required Guid Id { get; init; }
}
internal sealed class Delete{Entity}Handler(IRepositorySpec repository)
: Fluents.Requests.IHandler<Delete{Entity}Request>
{
public async Task<IResultBase> OnHandle(
Delete{Entity}Request request,
CancellationToken cancellationToken)
{
if (request.Id == Guid.Empty)
{
return Result.Fail("The Id is invalid.")
.WithError(new Error("The Id is invalid.") { Metadata = { ["field"] = nameof(request.Id) } });
}
var entity = await repository.FirstOrDefaultAsync(
new SpecGet{Entity}(request.Id), cancellationToken);
if (entity == null)
return Result.Fail($"The {Entity} {request.Id} is not found.");
repository.Delete(entity);
return Result.Ok();
}
}
Create src/ApiEndpoints/Minimal.AppServices/{Feature}/V1/Specs/SpecGet{Entity}.cs:
using DKNet.EfCore.Specifications;
namespace Minimal.AppServices.{Feature}.V1.Specs;
internal sealed class SpecGet{Entity} : Specification<{Entity}>
{
public SpecGet{Entity}(Guid? byId = null, string? by{UniqueField} = null)
{
var predicator = CreatePredicate();
if (byId is not null)
predicator = predicator.And(a => a.Id == byId);
if (!string.IsNullOrEmpty(by{UniqueField}))
predicator = predicator.And(a => a.{UniqueField} == by{UniqueField});
WithFilter(predicator);
}
}
Create src/ApiEndpoints/Minimal.AppServices/{Feature}/V1/Events/{Entity}CreatedEventHandlers.cs:
namespace Minimal.AppServices.{Feature}.V1.Events;
/// <summary>
/// Domain event published when a {entity} is created.
/// </summary>
public sealed record {Entity}CreatedEvent(Guid Id, string {NameField});
/// <summary>
/// In-memory handler for {Entity}CreatedEvent.
/// </summary>
internal sealed class {Entity}CreatedEventFromMemoryHandler
: Fluents.EventsConsumers.IHandler<{Entity}CreatedEvent>
{
public Task OnHandle({Entity}CreatedEvent notification, CancellationToken cancellationToken)
{
// Handle event: logging, notifications, side-effects
return Task.CompletedTask;
}
}
Edit src/ApiEndpoints/Minimal.AppServices/GlobalUsings.cs:
global using Minimal.Domains.Features.{Feature}.Entities;
CreateProfileRequest : RequestBase, Fluents.Requests.IWitResponse<CustomerProfileDto>CreateProfileCommandValidator : AbstractValidator<CreateProfileRequest>CreateProfileCommandHandler(IRepositorySpec, IMembershipService, IMapper) : Fluents.Requests.IHandler<CreateProfileRequest, CustomerProfileDto>mapper.Map<CustomerProfile>(request) → repository.AddAsync → AddEvent(new ProfileCreatedEvent(...)) → mapper.ResultOf<CustomerProfileDto>(profile)UpdateProfileRequest : RequestBase, Fluents.Requests.IWitResponse<CustomerProfileDto>entity.Update(...) → return mapper.Map<CustomerProfileDto>(entity)DeleteProfileRequest : RequestBase, Fluents.Requests.INoResponserepository.Delete(entity) → Result.Ok()[GenerateDto] + [MapsFrom] attributesFluents.Requests.IWitResponse<{Dto}>[MapsFrom(typeof({Entity}))] attributeFluents.Requests.IWitResponse<{Dto}>Fluents.Requests.INoResponseRequestBase (provides ByUser)internal sealed and extend AbstractValidator<T>internal sealed with primary constructor injectionIRepositorySpec (not custom repos)mapper.ResultOf<T>() for lazy mappingIResultBase (not IResult<T>)sealed record typesFluents.EventsConsumers.IHandler<T>internal sealed extending Specification<T>Minimal.AppServices.{Feature}.V1.Actionsdotnet build src/DKNet.Templates.sln -c Release passes| Mistake | Fix |
|---|---|
Creating custom IRepository interface | Use IRepositorySpec — it's already registered |
Using record struct for requests | Use record (reference type) — needed for bus serialization |
Making handlers public | Must be internal sealed |
Missing [MapsFrom] on create request | Mapster needs this to map request → entity |
Using Result.Ok(entity) instead of mapper.ResultOf<T>() | Lazy mapping ensures DTO reflects post-SaveChanges state |
Forgetting request.ByUser! in Update | Must pass user ID to entity mutation methods |
Not adding [JsonIgnore] on auto-fields | Fields like MembershipNo shouldn't be client-settable |
After creating AppServices actions, proceed to: → dknet-endpoint-config skill to expose these actions as REST API endpoints
Creates, edits, and optimizes skills for Claude Code, including drafting, evaluating with test prompts, iterating on performance, and improving skill descriptions for better triggering accuracy.
npx claudepluginhub baoduy/dknet.templates --plugin dknet-minimal