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’s application layer is built on the Command Query Responsibility Segregation (CQRS) pattern, mediated by the MediatR library. Every user-facing operation is expressed as either a command (mutates state) or a query (reads state) — the two never mix. This separation makes the intent of each operation explicit, keeps handler classes small, and makes the pipeline easy to extend with cross-cutting behaviors.

Commands vs. Queries

AspectCommandQuery
Return typeGuid (new entity ID) or Unit (void)A DTO or list of DTOs
Side effectsYes — persists through IUnitOfWorkNo — read-only repository calls
ValidationValidationBehavior runs before the handlerValidationBehavior runs before the handler
ExampleScheduleByPatientCommand → GuidGetAppointmentTypeByIdQuery → AppointmentTypeDto

The MediatR Pipeline

Every request — command or query — travels through the same pipeline before reaching its handler.
1

Client dispatches a request

The caller (host application, test, or integration entry point) calls mediator.Send(command). MediatR resolves the matching IPipelineBehavior chain and IRequestHandler.
2

ValidationBehavior intercepts the request

All IValidator<TRequest> implementations registered in the DI container are discovered. Each validator runs asynchronously in parallel via Task.WhenAll. If any failures are found, a ValidationException is thrown immediately — the handler is never invoked.
3

Handler executes domain logic

The handler loads aggregates from repositories, calls domain services, and appends new entities to repositories. Domain events are registered on aggregates during this phase.
4

UnitOfWork persists changes

The handler calls await unitOfWork.SaveChangesAsync(cancellationToken). All pending repository writes are committed in a single database transaction. Domain events are dispatched after the commit succeeds.

ValidationBehavior

// ClinicFlow.Application/Behaviors/ValidationBehavior.cs
public class ValidationBehavior<TRequest, TResponse>(IEnumerable<IValidator<TRequest>> validators)
    : IPipelineBehavior<TRequest, TResponse>
    where TRequest : IRequest<TResponse>
{
    public async Task<TResponse> Handle(
        TRequest request,
        RequestHandlerDelegate<TResponse> next,
        CancellationToken cancellationToken
    )
    {
        if (validators.Any())
        {
            var context = new ValidationContext<TRequest>(request);

            var validationResults = await Task.WhenAll(
                validators.Select(v => v.ValidateAsync(context, cancellationToken))
            );

            var failures = validationResults
                .Where(r => r.Errors.Count > 0)
                .SelectMany(r => r.Errors)
                .ToList();

            if (failures.Count > 0)
                throw new ValidationException(failures);
        }
        return await next(cancellationToken);
    }
}
All validators in ClinicFlow extend AbstractValidator<T> from the FluentValidation library. They are automatically discovered and registered in DI by scanning the Application assembly. You do not need to register them manually.

A Concrete Command: ScheduleByPatient

// ClinicFlow.Application/Appointments/Commands/ScheduleByPatient/ScheduleByPatientCommand.cs
public sealed record ScheduleByPatientCommand(
    Guid InitiatorUserId,
    Guid TargetPatientId,
    Guid DoctorId,
    Guid AppointmentTypeId,
    DateOnly ScheduledDate,
    TimeOnly StartTime,
    TimeOnly EndTime,
    string? PatientNotes = null
) : IRequest<Guid>;
The command is a C# record — immutable, structurally equatable, and self-documenting. It implements IRequest<Guid>, meaning the handler will return the new appointment’s Id.

Validator

// ScheduleByPatientCommandValidator.cs
public sealed class ScheduleByPatientCommandValidator
    : AbstractValidator<ScheduleByPatientCommand>
{
    public ScheduleByPatientCommandValidator(TimeProvider timeProvider)
    {
        RuleFor(x => x.InitiatorUserId)
            .NotEmpty().WithMessage(DomainErrors.Validation.InvalidValue);
        RuleFor(x => x.TargetPatientId)
            .NotEmpty().WithMessage(DomainErrors.Validation.InvalidValue);
        RuleFor(x => x.DoctorId)
            .NotEmpty().WithMessage(DomainErrors.Validation.InvalidValue);
        RuleFor(x => x.AppointmentTypeId)
            .NotEmpty().WithMessage(DomainErrors.Validation.InvalidValue);
        RuleFor(x => x.ScheduledDate)
            .GreaterThanOrEqualTo(_ =>
                DateOnly.FromDateTime(timeProvider.GetUtcNow().UtcDateTime))
            .WithMessage(DomainErrors.Validation.ValueMustBeInFuture);
        RuleFor(x => x.EndTime)
            .GreaterThan(x => x.StartTime)
            .WithMessage(DomainErrors.Validation.EndTimeMustBeAfterStartTime);
        RuleFor(x => x.PatientNotes)
            .MaximumLength(500)
            .WithMessage(DomainErrors.Validation.ValueTooLong);
    }
}

Handler

// ScheduleByPatientCommandHandler.cs
public sealed class ScheduleByPatientCommandHandler(
    IPatientPenaltyRepository penaltyRepository,
    IPatientRepository patientRepository,
    IDoctorRepository doctorRepository,
    IAppointmentTypeDefinitionRepository appointmentTypeRepository,
    IScheduleRepository scheduleRepository,
    IAppointmentRepository appointmentRepository,
    IUserRepository userRepository,
    IRegionalSchedulingService regionalSchedulingService,
    IUnitOfWork unitOfWork
) : IRequestHandler<ScheduleByPatientCommand, Guid>
{
    public async Task<Guid> Handle(
        ScheduleByPatientCommand request,
        CancellationToken cancellationToken
    )
    {
        // 1. Load all required aggregates from repositories
        var targetPatient  = await patientRepository.GetByIdAsync(...) ?? throw ...;
        var initiatorPatient = await patientRepository.GetByUserIdAsync(...) ?? throw ...;
        var targetDoctor   = await doctorRepository.GetByIdAsync(...) ?? throw ...;
        var user           = await userRepository.GetByIdAsync(...) ?? throw ...;
        var appointmentType = await appointmentTypeRepository.GetByIdAsync(...) ?? throw ...;
        var penalties      = await penaltyRepository.GetByPatientIdAsync(...);
        var doctorSchedule = await scheduleRepository.GetByDoctorAndDayAsync(...) ?? throw ...;

        // 2. Build value objects
        var timeRange = TimeRange.Create(request.StartTime, request.EndTime);

        // 3. Check for scheduling conflicts
        if (await appointmentRepository.HasConflictAsync(...))
            throw new AppointmentConflictException(...);

        // 4. Obtain regional scheduling clearance
        var clearance = regionalSchedulingService.EnforceSchedulingRegulations(
            targetDoctor, targetPatient, appointmentType
        );

        // 5. Delegate to domain service (enforces all business rules)
        var appointment = AppointmentSchedulingService.ScheduleByPatient(
            appointmentType,
            new PatientSchedulingArgs { ... },
            new PatientSchedulingContext { Penalties = penalties, DoctorSchedule = doctorSchedule },
            clearance
        );

        // 6. Persist and commit
        await appointmentRepository.CreateAsync(appointment, cancellationToken);
        await unitOfWork.SaveChangesAsync(cancellationToken);

        return appointment.Id;
    }
}

UnitOfWork Pattern

All repository mutations are flushed through a single IUnitOfWork.SaveChangesAsync() call at the end of every command handler. Repositories themselves only stage changes in memory — nothing is written to the database until SaveChangesAsync is called. This guarantees that a command is either fully committed or fully rolled back.
Query handlers never call SaveChangesAsync. Queries use read-only repository methods and return mapped DTOs directly.

Command Groups

ClinicFlow organises commands by the aggregate they operate on. The table below lists every command namespace in the Application layer.
RegisterUser, RegisterDoctorUser, RegisterReceptionistUser, RegisterAdminUser, LoginUser, LogoutUser, ChangePassword, RequestPasswordReset, ResetPassword, SendPhoneVerification, VerifyPhone, DeactivateUser, ReactivateUser
ScheduleByPatient, ScheduleByDoctor, ScheduleByStaff, RescheduleByPatient, RescheduleByDoctor, RescheduleByStaff, CancelAppointmentByPatient, CancelAppointmentByDoctor, CancelAppointmentByStaff, CheckInAppointmentByStaff, StartAppointmentByDoctor, MarkAppointmentAsNoShowByDoctor, MarkAppointmentAsNoShowByStaff, ReassignAppointment, CleanExpiredDisplacedAppointments, UpdatePatientNotesByPatient, UpdatePatientNotesByStaff, UpdateReceptionistNotesByStaff
CreateDoctorProfile, UpdateDoctorProfile, SuspendDoctorProfile, ReactivateDoctorProfile
CreatePatientProfile, CreateCompletePatientProfile, UpdatePatientProfile, AddFamilyMember, AddCompleteFamilyMember, RemoveFamilyMember, ClosePatientAccount
CreateSchedule, SetupWeeklySchedule, UpdateSchedule, DeactivateSchedule
BlockPatient, RemovePenalty
CompleteMedicalEncounter, AddClinicalDetailToMedicalRecord
CreateAppointmentType, UpdateAppointmentType, ChangeAppointmentTypeAgePolicy, RestrictAppointmentTypeToSpecialties, MakeAppointmentTypeUnrestricted, AddAllowedSpecialtyToAppointmentType, RemoveAllowedSpecialtyFromAppointmentType, AddRequiredTemplateToAppointmentType, RemoveRequiredTemplateFromAppointmentType, DeactivateAppointmentType, ReactivateAppointmentType
CreateClinicalFormTemplate, UpdateClinicalFormTemplate, DeactivateClinicalFormTemplate, ReactivateClinicalFormTemplate
CreateMedicalSpecialty, UpdateMedicalSpecialty, DeactivateMedicalSpecialty, ReactivateMedicalSpecialty

Build docs developers (and LLMs) love