Skip to main content

Documentation Index

Fetch the complete documentation index at: https://mintlify.com/0Crazy-0/ClinicFlow/llms.txt

Use this file to discover all available pages before exploring further.

ClinicFlow is designed to grow through a small number of repeatable extension patterns. Whether you are adding a new business operation, exposing existing data through a query, or modelling a new domain concept, the same conventions apply throughout. This page walks through each pattern with real code examples drawn from the existing codebase.
Follow the existing naming and folder conventions exactly. Each command lives in its own sub-directory named after the operation (e.g., AppointmentTypes/Commands/AddAllowedSpecialtyToAppointmentType/) and contains exactly three files: the command record, the handler, and the validator. Keeping them co-located makes discovery trivial and enforces a consistent “one responsibility per folder” layout.

Adding a New Command

A CQRS command represents an operation that changes state. The Application layer pattern consists of three co-located classes: the command record, the handler, and the validator.
1
Define the command record
2
Create a sealed record implementing IRequest (for void commands) or IRequest<TResponse> (for commands that return a value). Place it in a new sub-directory under the relevant feature folder inside ClinicFlow.Application:
3
// ClinicFlow.Application/AppointmentTypes/Commands/AddAllowedSpecialtyToAppointmentType/
// AddAllowedSpecialtyToAppointmentTypeCommand.cs

using MediatR;

namespace ClinicFlow.Application.AppointmentTypes.Commands.AddAllowedSpecialtyToAppointmentType;

public sealed record AddAllowedSpecialtyToAppointmentTypeCommand(
    Guid AppointmentTypeId,
    Guid SpecialtyId
) : IRequest;
4
Implement the command handler
5
Create a sealed class implementing IRequestHandler<TCommand> (or IRequestHandler<TCommand, TResponse>). Inject dependencies through the primary constructor — MediatR registers handlers automatically via AddMediatR(cfg => cfg.RegisterServicesFromAssembly(...)).
6
// AddAllowedSpecialtyToAppointmentTypeCommandHandler.cs

using ClinicFlow.Domain.Common;
using ClinicFlow.Domain.Entities;
using ClinicFlow.Domain.Exceptions.Base;
using ClinicFlow.Domain.Interfaces;
using ClinicFlow.Domain.Interfaces.Repositories;
using MediatR;

namespace ClinicFlow.Application.AppointmentTypes.Commands.AddAllowedSpecialtyToAppointmentType;

public sealed class AddAllowedSpecialtyToAppointmentTypeCommandHandler(
    IAppointmentTypeDefinitionRepository appointmentTypeRepository,
    IUnitOfWork unitOfWork
) : IRequestHandler<AddAllowedSpecialtyToAppointmentTypeCommand>
{
    public async Task Handle(
        AddAllowedSpecialtyToAppointmentTypeCommand request,
        CancellationToken cancellationToken
    )
    {
        var appointmentType =
            await appointmentTypeRepository.GetByIdAsync(
                request.AppointmentTypeId,
                cancellationToken
            )
            ?? throw new EntityNotFoundException(
                DomainErrors.General.NotFound,
                nameof(AppointmentTypeDefinition),
                request.AppointmentTypeId
            );

        appointmentType.AddAllowedSpecialty(request.SpecialtyId);

        await unitOfWork.SaveChangesAsync(cancellationToken);
    }
}
7
Add a FluentValidation validator
8
Create a class inheriting AbstractValidator<TCommand>. The ValidationBehavior<,> MediatR pipeline behaviour automatically discovers and runs all validators registered in the Application assembly before the handler executes.
9
// AddAllowedSpecialtyToAppointmentTypeCommandValidator.cs

using ClinicFlow.Domain.Common;
using FluentValidation;

namespace ClinicFlow.Application.AppointmentTypes.Commands.AddAllowedSpecialtyToAppointmentType;

public sealed class AddAllowedSpecialtyToAppointmentTypeCommandValidator
    : AbstractValidator<AddAllowedSpecialtyToAppointmentTypeCommand>
{
    public AddAllowedSpecialtyToAppointmentTypeCommandValidator()
    {
        RuleFor(x => x.AppointmentTypeId)
            .NotEmpty()
            .WithMessage(DomainErrors.Validation.InvalidValue);

        RuleFor(x => x.SpecialtyId)
            .NotEmpty()
            .WithMessage(DomainErrors.Validation.InvalidValue);
    }
}
10
Register in DI (automatic)
11
No manual registration is needed. AddApplicationServices() in ClinicFlow.Application/DependencyInjection.cs calls:
12
services.AddValidatorsFromAssembly(Assembly.GetExecutingAssembly());

services.AddMediatR(cfg =>
{
    cfg.RegisterServicesFromAssembly(Assembly.GetExecutingAssembly());
    cfg.AddBehavior(typeof(IPipelineBehavior<,>), typeof(ValidationBehavior<,>));
});
13
Any IRequestHandler<,> and AbstractValidator<> classes in the Application assembly are picked up automatically.

Adding a New Query

Queries follow the same three-file pattern but return a response object instead of Unit. They must not modify state.
// ClinicFlow.Application/AppointmentTypes/Queries/GetAppointmentTypeById/
// GetAppointmentTypeByIdQuery.cs

using MediatR;

public sealed record GetAppointmentTypeByIdQuery(Guid Id) : IRequest<AppointmentTypeDto>;
// GetAppointmentTypeByIdQueryHandler.cs

public sealed class GetAppointmentTypeByIdQueryHandler(
    IAppointmentTypeDefinitionRepository repository
) : IRequestHandler<GetAppointmentTypeByIdQuery, AppointmentTypeDto>
{
    public async Task<AppointmentTypeDto> Handle(
        GetAppointmentTypeByIdQuery request,
        CancellationToken cancellationToken
    )
    {
        var entity = await repository.GetByIdAsync(request.Id, cancellationToken)
            ?? throw new EntityNotFoundException(
                DomainErrors.General.NotFound,
                nameof(AppointmentTypeDefinition),
                request.Id
            );

        return new AppointmentTypeDto(entity.Id, entity.Name, entity.IsActive);
    }
}
For simple queries, a validator that guards against an empty Guid is still worth adding — it provides a clear 400 response before hitting the database.

Implementing a New Repository

Repositories are defined as interfaces in the Domain layer and implemented in the Infrastructure layer. The existing repository contracts live in ClinicFlow.Domain/Interfaces/Repositories/.

Step 1 — Define the interface in Domain

// ClinicFlow.Domain/Interfaces/Repositories/IMyEntityRepository.cs

namespace ClinicFlow.Domain.Interfaces.Repositories;

public interface IMyEntityRepository
{
    Task<MyEntity?> GetByIdAsync(Guid id, CancellationToken cancellationToken = default);
    Task CreateAsync(MyEntity entity, CancellationToken cancellationToken = default);
}

Step 2 — Implement in Infrastructure

// ClinicFlow.Infrastructure/Persistence/Repositories/MyEntityRepository.cs

using ClinicFlow.Domain.Entities;
using ClinicFlow.Domain.Interfaces.Repositories;
using Microsoft.EntityFrameworkCore;

namespace ClinicFlow.Infrastructure.Persistence.Repositories;

internal sealed class MyEntityRepository(ApplicationDbContext context)
    : IMyEntityRepository
{
    public Task<MyEntity?> GetByIdAsync(Guid id, CancellationToken cancellationToken = default) =>
        context.Set<MyEntity>().FirstOrDefaultAsync(e => e.Id == id, cancellationToken);

    public async Task CreateAsync(MyEntity entity, CancellationToken cancellationToken = default) =>
        await context.Set<MyEntity>().AddAsync(entity, cancellationToken);
}

Step 3 — Register in DI

Add the registration to ClinicFlow.Infrastructure/DependencyInjection.cs:
services.AddScoped<IMyEntityRepository, MyEntityRepository>();

Adding a Domain Event

Domain events allow entities to signal that something meaningful happened without coupling them to the downstream side effects.

Step 1 — Define the event record

Domain event records live in ClinicFlow.Domain/Events/<Feature>/ and implement IDomainEvent:
// ClinicFlow.Domain/Events/AppointmentTypes/AppointmentTypeCreatedEvent.cs

using ClinicFlow.Domain.Common;

namespace ClinicFlow.Domain.Events.AppointmentTypes;

public sealed record AppointmentTypeCreatedEvent(Guid AppointmentTypeId) : IDomainEvent;

Step 2 — Raise the event from an entity

Inside the aggregate root method that performs the operation, call AddDomainEvent:
public static AppointmentTypeDefinition Create(/* ... */)
{
    var entity = new AppointmentTypeDefinition(/* ... */);
    entity.AddDomainEvent(new AppointmentTypeCreatedEvent(entity.Id));
    return entity;
}

Step 3 — Handle the event in Application

Domain events are wrapped in DomainEventNotification<TEvent> by UnitOfWork before being published via MediatR’s IPublisher. Your handler must therefore implement INotificationHandler<DomainEventNotification<TEvent>>:
// ClinicFlow.Application/AppointmentTypes/EventHandlers/AppointmentTypeCreatedEventHandler.cs

using ClinicFlow.Application.Common.Models;
using ClinicFlow.Domain.Events.AppointmentTypes;
using MediatR;

namespace ClinicFlow.Application.AppointmentTypes.EventHandlers;

public sealed class AppointmentTypeCreatedEventHandler
    : INotificationHandler<DomainEventNotification<AppointmentTypeCreatedEvent>>
{
    public Task Handle(
        DomainEventNotification<AppointmentTypeCreatedEvent> notification,
        CancellationToken cancellationToken
    )
    {
        var domainEvent = notification.DomainEvent;
        // Perform side effects: send notification, update read model, etc.
        return Task.CompletedTask;
    }
}

Implementing IRegionalSchedulingService

If your deployment region has specific appointment scheduling regulations (minimum lead times, specialty-level restrictions, age-based policies), implement IRegionalSchedulingService to enforce them:
using ClinicFlow.Domain.Entities;
using ClinicFlow.Domain.Interfaces.Services;
using ClinicFlow.Domain.ValueObjects;

public sealed class DefaultRegionalSchedulingService : IRegionalSchedulingService
{
    public SchedulingClearance EnforceSchedulingRegulations(
        Doctor targetDoctor,
        Patient targetPatient,
        AppointmentTypeDefinition appointmentType
    )
    {
        // No regional restrictions — always grant clearance.
        return SchedulingClearance.Granted();
    }
}
For a region with custom rules, inject configuration or a rules engine into the constructor and evaluate the relevant conditions before returning the clearance result. Register in your host project:
services.AddScoped<IRegionalSchedulingService, DefaultRegionalSchedulingService>();

Summary Checklist

ExtensionFiles to createRegistration
New command<Name>Command.cs, <Name>CommandHandler.cs, <Name>CommandValidator.csAutomatic (assembly scan)
New query<Name>Query.cs, <Name>QueryHandler.cs, optional <Name>QueryValidator.csAutomatic
New repositoryI<Name>Repository.cs in Domain, <Name>Repository.cs in InfrastructureManual in Infrastructure/DependencyInjection.cs
New domain event<Name>Event.cs in Domain Events folder, <Name>EventHandler.cs in ApplicationAutomatic (MediatR notification scan)
New service interfaceInterface in Domain or Application Interfaces, implementation in InfrastructureManual in host/Infrastructure DI

Build docs developers (and LLMs) love