m MiniEHR / learn
Foundry ↗About MiniEHROpen workspace ↗
LESSON 3 OF 94 min read

3. Trace patient registration through the application

Follow one request: create the invented patient Synthetic Example. Read the linked files in order to connect the C# concepts to a working use case.

Why four projects?

ProjectResponsibilityFiles to read
DomainEntities/rules without HTTP or EF dependenciesPatient, Encounter
ApplicationUse cases, response types, storage contractsPatients, ClinicalWorkflow
InfrastructureEF/SQLite storage and transactionsPatientStore, ClinicalStore
ApiRoutes, validation, composition, static assetsProgram, PatientsController

Application uses Domain; Infrastructure implements Application's storage contracts; Api assembles the pieces. Project references connect these dependencies. Domain can be tested without starting a web server or database.

View detailed diagram notation
sequenceDiagram
    participant B as Browser or curl
    participant C as PatientsController
    participant S as PatientService
    participant D as Patient
    participant P as PatientStore and EF
    participant DB as SQLite
    B->>C: POST /api/patients with JSON
    C->>S: RegisterAsync(command)
    S->>D: Register(...)
    D-->>S: Validated patient, version 1
    S->>P: AddAsync(patient)
    P->>DB: Insert and save
    DB-->>P: Saved or constraint error
    P-->>S: Completion
    S-->>C: PatientDetails
    C-->>B: 201, Location, JSON

Step 1: Startup connects the parts

Program.cs creates the web builder. AddControllers() enables controllers; UseSqlite(...) selects the database provider and reads the MiniEHR connection string, falling back to Data Source=miniehr.db.

AddScoped tells dependency injection how to construct the services, stores, and database context. UseExceptionHandler() activates exception handling. Default and static-file middleware serves the browser; MapControllers() makes API routes reachable. Before app.Run(), a service scope applies migrations with MigrateAsync(). Startup can fail here if database access is broken.

/health checks database readiness. app.Run() starts serving requests; it is a C# method, different from Foundry's CLI run command.

Step 2: The controller handles HTTP

[Route("api/patients")] gives the path prefix; [HttpPost] on Register matches POST at that prefix. ASP.NET reads the JSON into RegisterPatientRequest. Required/StringLength attributes participate in validation. [ApiController] supplies automatic responses for invalid model state.

The controller translates transport input into an Application RegisterPatient command and forwards the request cancellation token. It does not create tables or write SQL.

Step 3: The service coordinates the use case

PatientService.RegisterAsync calls Patient.Register(...), awaits store.AddAsync(patient), then returns PatientDetails.From(patient). The IPatientStore dependency keeps database mechanics out of this use case.

PatientDetails is a DTO (data transfer object) containing ID, MRN, names, date of birth, version, and timestamps. It describes the response without directly exposing a domain entity as the transport contract.

Step 4: The domain establishes valid state

Patient.Register rejects a future birth date, trims required text, bounds its length, uppercases MRN, generates an ID, sets timestamps, and starts version 1. Attributes validate the HTTP boundary; domain checks protect callers that do not come through HTTP. Both layers have a purpose.

Step 5: The store persists it

PatientStore.AddAsync adds the entity to database.Patients and calls SaveAsync. EF's change tracker recognizes the new entity; SaveChangesAsync inserts it. SQLite's unique MRN index rejects duplicate normalized MRNs. The store translates that specific uniqueness failure to PatientConflictException.

For updates, the domain checks the supplied version, and EF's concurrency token detects intervening database changes. Checking only the in-memory object would not cover a race between reading and saving.

Step 6: The controller responds

CreatedAtAction returns HTTP 201, the patient body, and a Location for its GET route. Default JSON output uses camelCase names such as medicalRecordNumber, while C# properties use PascalCase.

ApplicationExceptionHandler.cs maps known exceptions to Problem Details:

OutcomeStatus
Successful creation201
Successful read/update/close200
Invalid request/domain argument400
Missing resource or mismatched clinical parent404
Duplicate MRN, stale version, or closed mutation409

Unexpected exceptions are left to framework error handling, not recast as normal conflicts. Malformed GUIDs may fail route matching with 404 before controller code.

Checkpoint

After SYN-001 exists, a request registers syn-001. Which layer normalizes it, which detects the duplicate, and which returns 409? Domain, database/store, and API exception handler, respectively.

Next: Data and closure.

MiniEHR learning guide · Based on this project’s source · Read on GitHub