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?
| Project | Responsibility | Files to read |
|---|---|---|
| Domain | Entities/rules without HTTP or EF dependencies | Patient, Encounter |
| Application | Use cases, response types, storage contracts | Patients, ClinicalWorkflow |
| Infrastructure | EF/SQLite storage and transactions | PatientStore, ClinicalStore |
| Api | Routes, validation, composition, static assets | Program, 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, JSONStep 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:
| Outcome | Status |
|---|---|
| Successful creation | 201 |
| Successful read/update/close | 200 |
| Invalid request/domain argument | 400 |
| Missing resource or mismatched clinical parent | 404 |
| Duplicate MRN, stale version, or closed mutation | 409 |
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.