2. Read the C# used in MiniEHR
C# is pronounced “C sharp.” It is statically typed: the compiler checks types before the application runs. Learn these patterns first; you do not need every language feature to understand this project.
Variables and types
This explanatory snippet is not a new project file:
string givenName = "Synthetic";
int page = 1;
long version = 1;
bool isClosed = false;
decimal temperature = 36.5m;
var id = Guid.NewGuid();string is text; int/long are whole numbers; bool is true/false; decimal holds decimal values. The m suffix selects decimal. var asks the compiler to infer the type, not to make it dynamic. Here id is a Guid, represented as an identifier string in JSON. Statements normally end in ;; braces enclose blocks.
Patient.cs uses DateOnly for birth dates. Encounters use DateTimeOffset, a date/time with an offset. JSON uses dates like 2000-01-01 and timestamps like 2020-01-01T10:00:00Z; Z means UTC.
Namespaces and imports
using MiniEHR.Domain;
namespace MiniEHR.Application;A namespace groups names. using makes imported type names available without their full prefix. Project references and packages determine which assemblies are available. ImplicitUsings supplies common imports automatically.
Classes, objects, properties, and methods
Excerpt from Patient.cs:
public sealed class Patient
{
public Guid Id { get; private set; }
public string GivenName { get; private set; } = "";
}A class defines a type; an object is an instance. A property exposes a value. get allows reading; private set restricts assignment to that class. Other code must use validated domain methods instead of casually assigning a name. public permits external access; private confines access; sealed prevents inheritance. A method is a named operation; void returns no result.
public void UpdateDemographics(string givenName, string familyName,
DateOnly birthDate, long expectedVersion)Read this as “an accessible operation named UpdateDemographics accepts four typed inputs and returns nothing.” Its body validates before assigning properties. The private constructor blocks normal callers from creating incomplete patients. Patient.Register(...) is a static factory: call it on the type, without an existing instance, to obtain a validated patient.
Records carry data
From Patients.cs:
public sealed record RegisterPatient(string MedicalRecordNumber,
string GivenName, string FamilyName, DateOnly BirthDate);Records group data and provide value-based equality. This positional declaration generates a constructor and properties. This record carries an application command, not a database entity. HTTP request records describe input; response records describe output.
Interfaces and dependency injection
public interface IPatientStore
{
Task<Patient?> FindAsync(Guid id, CancellationToken cancellationToken);
}
public sealed class PatientService(IPatientStore store)This is a shortened excerpt; the actual interface declares more methods. An interface describes operations an implementation supplies. The service needs an IPatientStore; it does not choose SQLite. The parentheses on the class declare a primary constructor containing that dependency.
Program.cs connects the contract to its implementation:
builder.Services.AddScoped<IPatientStore, PatientStore>();ASP.NET resolves dependencies when creating the service/controller. This is dependency injection. Scoped services normally share a lifetime within one HTTP request; the database context is scoped too. See Microsoft's dependency-injection guide.
Null and question marks
Patient? permits no result (null); DateOnly? permits an omitted date. A non-nullable numeric request property such as long can default to zero if omitted, which is why [Range(1, long.MaxValue)] is needed.
var patient = await store.FindAsync(id, cancellationToken)
?? throw new PatientNotFoundException();?? uses its right side if the left is null. value?.Trim() calls Trim only when value is not null. The ! in request.BirthDate!.Value tells the compiler to assume non-null; it does not validate at runtime. The preceding API Required validation establishes that boundary. See Microsoft's null-safety guide.
Async and await
public async Task<PatientDetails> GetAsync(Guid id, CancellationToken ct)
=> PatientDetails.From(await store.FindAsync(id, ct)
?? throw new PatientNotFoundException());Task<PatientDetails> represents an operation producing patient details. async permits await; await waits for completion without blocking a thread during asynchronous waiting. It does not automatically start a new thread. CancellationToken lets callers signal cancellation; it is forwarded to storage.
=> here makes a method with a single-expression body. It also appears in lambdas like p => p.Id == id: a small function comparing a patient's ID.
Collections, generics, and queries
IReadOnlyList<Patient> exposes a list of patients through a read-only interface. The angle brackets give a type argument. ContentWrite<T> reuses a response shape for notes and vitals; T becomes the particular result type.
database.Patients.AsNoTracking().OrderBy(p => p.MedicalRecordNumber)
.Skip(offset).Take(limit).ToListAsync(cancellationToken)This builds a query: no change tracking, sorted by MRN, skipping earlier rows, taking a page, then executing asynchronously. LINQ is the query operation family. EF translates supported expressions to SQL here; LINQ can also process objects already in memory.
Attributes, exceptions, and cleanup
[HttpPost], [Required], and [Fact] are attributes: metadata interpreted by frameworks, not ordinary function calls. throw signals failure; catch handles it. The API maps known exceptions to HTTP errors.
using var disposes a resource when its scope ends. await using disposes asynchronously, used for transactions and database resources. checked makes integer overflow throw instead of wrapping to a different number.
Checkpoint
Open Patient.cs and explain public, private set, static, Guid.NewGuid(), throw, and checked(Version + 1). Then read PatientTests.cs; you should recognize most of the syntax. For more, use Microsoft's C# guide.
Next: Request flow.