Learnixo
Back to blog
AI Systemsintermediate

Building a Feature Slice — End to End

Build a complete vertical slice from endpoint to database: command, validator, handler, domain logic, persistence, and response — a full CreatePrescription feature as a worked example.

Asma Hafeez KhanMay 16, 20264 min read
Vertical SliceFeatureMediatRASP.NET Core.NETEnd to End
Share:𝕏

A Complete Feature Slice

A vertical slice for CreatePrescription includes:
  1. Endpoint:   receives HTTP POST, sends Command to MediatR
  2. Command:    immutable record with all input data
  3. Validator:  FluentValidation rules for the command
  4. Handler:    business logic + persistence
  5. Response:   what the endpoint returns to the client

All five files live in Features/Prescriptions/CreatePrescription/
No other folder needs to change.

Step 1 — Command and Response

C#
// Features/Prescriptions/CreatePrescription/CreatePrescriptionCommand.cs
public sealed record CreatePrescriptionCommand(
    Guid    PatientId,
    string  MedicationName,
    decimal DoseAmount,
    string  DoseUnit,
    Guid    PrescriberId,
    string? Instructions) : IRequest<Result<CreatePrescriptionResponse>>;

// Features/Prescriptions/CreatePrescription/CreatePrescriptionResponse.cs
public sealed record CreatePrescriptionResponse(
    Guid     PrescriptionId,
    string   MedicationName,
    decimal  DoseAmount,
    string   DoseUnit,
    DateTime PrescribedAt);

Step 2 — Validator

C#
// Features/Prescriptions/CreatePrescription/CreatePrescriptionValidator.cs
public sealed class CreatePrescriptionValidator
    : AbstractValidator<CreatePrescriptionCommand>
{
    public CreatePrescriptionValidator()
    {
        RuleFor(c => c.PatientId)
            .NotEmpty()
            .WithMessage("PatientId is required.");

        RuleFor(c => c.MedicationName)
            .NotEmpty()
            .MaximumLength(200)
            .WithMessage("Medication name must be between 1 and 200 characters.");

        RuleFor(c => c.DoseAmount)
            .GreaterThan(0)
            .LessThanOrEqualTo(10000)
            .WithMessage("Dose amount must be between 0 and 10000.");

        RuleFor(c => c.DoseUnit)
            .NotEmpty()
            .Must(u => new[] { "mg", "mcg", "g", "units/kg", "IU" }.Contains(u))
            .WithMessage("Dose unit must be a recognized unit.");

        RuleFor(c => c.PrescriberId)
            .NotEmpty()
            .WithMessage("PrescriberId is required.");
    }
}

Step 3 — Handler

C#
// Features/Prescriptions/CreatePrescription/CreatePrescriptionHandler.cs
public sealed class CreatePrescriptionHandler
    : IRequestHandler<CreatePrescriptionCommand, Result<CreatePrescriptionResponse>>
{
    private readonly ApplicationDbContext _db;
    private readonly IUnitOfWork          _uow;
    private readonly IMediator            _mediator;

    public CreatePrescriptionHandler(
        ApplicationDbContext db,
        IUnitOfWork          uow,
        IMediator            mediator)
    {
        _db       = db;
        _uow      = uow;
        _mediator = mediator;
    }

    public async Task<Result<CreatePrescriptionResponse>> Handle(
        CreatePrescriptionCommand command, CancellationToken ct)
    {
        // 1. Load patient — fail fast if not found
        var patient = await _db.Patients
            .FirstOrDefaultAsync(p => p.Id == new PatientId(command.PatientId), ct);

        if (patient is null)
            return Result.Failure<CreatePrescriptionResponse>(DomainErrors.Patient.NotFound);

        // 2. Load prescriber — fail fast if not found
        var prescriber = await _db.Clinicians
            .FirstOrDefaultAsync(c => c.Id == new ClinicianId(command.PrescriberId), ct);

        if (prescriber is null)
            return Result.Failure<CreatePrescriptionResponse>(DomainErrors.Clinician.NotFound);

        // 3. Domain: create prescription
        var prescriptionResult = Prescription.Create(
            patient.Id,
            command.MedicationName,
            new DosageValue(command.DoseAmount, command.DoseUnit),
            prescriber.Id,
            command.Instructions);

        if (prescriptionResult.IsFailure)
            return Result.Failure<CreatePrescriptionResponse>(prescriptionResult.Error);

        var prescription = prescriptionResult.Value;

        // 4. Deactivate previous active prescriptions for same medication
        await _db.Prescriptions
            .Where(p => p.PatientId == patient.Id &&
                        p.MedicationName == command.MedicationName &&
                        p.IsActive)
            .ExecuteUpdateAsync(
                s => s.SetProperty(p => p.IsActive, false), ct);

        // 5. Persist
        await _db.Prescriptions.AddAsync(prescription, ct);
        await _uow.SaveChangesAsync(ct);

        // 6. Publish domain event (fire-and-forget side effects)
        await _mediator.Publish(new PrescriptionCreatedNotification(
            prescription.Id.Value, patient.Id.Value, command.MedicationName), ct);

        return Result.Success(new CreatePrescriptionResponse(
            prescription.Id.Value,
            prescription.MedicationName,
            prescription.Dose.Amount,
            prescription.Dose.Unit,
            prescription.PrescribedAt));
    }
}

Step 4 — Endpoint

C#
// Features/Prescriptions/CreatePrescription/CreatePrescriptionEndpoint.cs
public sealed class CreatePrescriptionEndpoint : IEndpoint
{
    public void MapEndpoint(IEndpointRouteBuilder app)
    {
        app.MapPost("api/prescriptions",
            async (CreatePrescriptionRequest request, ISender sender, CancellationToken ct) =>
            {
                var command = new CreatePrescriptionCommand(
                    request.PatientId,
                    request.MedicationName,
                    request.DoseAmount,
                    request.DoseUnit,
                    request.PrescriberId,
                    request.Instructions);

                var result = await sender.Send(command, ct);

                return result.IsSuccess
                    ? Results.CreatedAtRoute("GetPrescription",
                        new { prescriptionId = result.Value.PrescriptionId },
                        result.Value)
                    : result.Error == DomainErrors.Patient.NotFound
                        ? Results.NotFound(new { error = result.Error.Description })
                        : Results.UnprocessableEntity(new { error = result.Error.Description });
            })
            .WithName("CreatePrescription")
            .WithSummary("Create a new prescription for a patient")
            .Produces<CreatePrescriptionResponse>(StatusCodes.Status201Created)
            .Produces(StatusCodes.Status404NotFound)
            .Produces(StatusCodes.Status422UnprocessableEntity)
            .RequireAuthorization("CanPrescribe");
    }
}

// Separate request DTO (decoupled from the command)
public sealed record CreatePrescriptionRequest(
    Guid    PatientId,
    string  MedicationName,
    decimal DoseAmount,
    string  DoseUnit,
    Guid    PrescriberId,
    string? Instructions);

Pipeline Execution Flow

HTTP POST /api/prescriptions
  → Endpoint receives request, maps to Command
  → ISender.Send(command)
    → LoggingBehavior: logs "Handling CreatePrescriptionCommand"
    → ValidationBehavior: runs CreatePrescriptionValidator
      → If invalid: throws ValidationException → 400
    → CreatePrescriptionHandler.Handle()
      → Load patient, prescriber
      → Create domain entity
      → Deactivate old prescriptions
      → SaveChanges
      → Publish PrescriptionCreatedNotification
    → LoggingBehavior: logs "Handled CreatePrescriptionCommand in 45ms"
  → Endpoint maps Result to 201 Created or error response

Production issue I've seen: A team put all the prescription creation logic directly in a controller action method — 80 lines of code including validation, database queries, business rules, and event publishing. When a second endpoint (a bulk import endpoint) needed the same logic, they copied and pasted it. Six months later, a business rule changed, and only one copy was updated. The feature slice pattern with a command handler makes the logic discoverable and reusable — call the same handler from any endpoint, background job, or test.


Key Takeaway

A complete feature slice contains: command (input), validator (rules), handler (logic + persistence), endpoint (HTTP mapping), and response (output) — all in one folder. The handler is the single source of truth for the feature's business logic. The pipeline behaviors run cross-cutting concerns without the handler knowing about them. Testing the handler directly (no HTTP context needed) gives fast, focused unit tests.

Enjoyed this article?

Explore the AI Systems learning path for more.

Found this helpful?

Share:𝕏

Leave a comment

Have a question, correction, or just found this helpful? Leave a note below.