A RESTful API built with ASP.NET Core and Entity Framework Core that demonstrates advanced database concepts including transactions, concurrency control, aggregate queries, indexes, and soft deletes β applied to a clinical trial patient enrollment scenario.
API is live at: https://clinical-enrollment-system.azurewebsites.net/swagger
- ASP.NET Core 10 β Web API framework
- Entity Framework Core β ORM for database access
- Azure SQL Database β Fully managed cloud relational database
- Swagger β API documentation and testing UI
- Docker β Containerized deployment
- Microsoft Azure β Cloud hosting via App Service
The enrollment process uses an explicit database transaction wrapping multiple steps:
- Validate patient exists
- Validate trial exists and is active
- Check trial capacity
- Check patient not already enrolled
- Create enrollment record
If any step fails, the entire transaction is rolled back β no partial data is ever saved.
using var transaction = await _context.Database.BeginTransactionAsync();
try
{
// ... multiple operations ...
await transaction.CommitAsync();
}
catch (Exception)
{
await transaction.RollbackAsync();
}Prevents race conditions when two doctors try to enroll the last available slot simultaneously.
Uses a RowVersion timestamp column β EF Core throws DbUpdateConcurrencyException if the
row was modified between read and write.
[Timestamp]
public byte[] RowVersion { get; set; } = null!;If a conflict is detected, the API returns 409 Conflict instead of silently over-enrolling the trial.
Used when closing a trial β a critical one-time operation that must not run concurrently.
Locks the specific row using SQL Server hints UPDLOCK and ROWLOCK for the duration of the transaction.
SELECT * FROM Trials WITH (UPDLOCK, ROWLOCK) WHERE Id = {0}| Optimistic | Pessimistic | |
|---|---|---|
| Strategy | Detect conflicts after | Prevent conflicts before |
| Locking | No row lock | Row locked during transaction |
| Best for | Frequent operations | Critical one-time operations |
| Our usage | Enrollment | Closing a trial |
Indexes are added on frequently queried columns to improve read performance.
modelBuilder.Entity<Patient>()
.HasIndex(p => p.Name)
.HasDatabaseName("IX_Patients_Name");
modelBuilder.Entity<Trial>()
.HasIndex(t => t.Status)
.HasDatabaseName("IX_Trials_Status");The /api/trials/stats endpoint demonstrates database-level aggregations β all computed
in SQL, not in memory.
ActiveEnrollments = t.Enrollments.Count(e => e.Status == "Active"),
AveragePatientAge = t.Enrollments
.Where(e => e.Status == "Active")
.Select(e => e.Patient.Age)
.Average(),
IsFull = t.Enrollments.Count(e => e.Status == "Active") >= t.MaxPatientsWithdrawing a patient does not delete the enrollment record β it marks it as Withdrawn.
This preserves audit history and allows reporting on withdrawn patients.
enrollment.Status = "Withdrawn"; // Never DELETE from the database
await _context.SaveChangesAsync();A patient can enroll in multiple trials. A trial can have multiple patients.
Modeled through an Enrollment junction table that also carries extra data
(enrollment date, status).
Patient ββββ Enrollment ββββ Trial
EnrolledAt
Status
Demo data is automatically inserted when the app starts via EF Core seed data, ensuring the API always has realistic data to demonstrate.
| Method | Endpoint | Description |
|---|---|---|
| GET | /api/enrollment | Get all enrollments |
| POST | /api/enrollment | Enroll a patient in a trial (with transaction) |
| DELETE | /api/enrollment/{id} | Withdraw a patient (soft delete) |
| Method | Endpoint | Description |
|---|---|---|
| GET | /api/trials | Get all trials with enrollment counts |
| GET | /api/trials/stats | Get aggregate statistics per trial |
| POST | /api/trials | Create a new trial |
| Method | Endpoint | Description |
|---|---|---|
| POST | /api/trialmanagement/{id}/close | Close a trial (pessimistic lock) |
| Method | Endpoint | Description |
|---|---|---|
| GET | /api/patients | Get all patients with active trial count |
| POST | /api/patients | Create a new patient |
Visit the live API directly:
https://clinical-enrollment-system.azurewebsites.net/swagger
git clone https://github.com/adham1177/ClinicalEnrollmentSystem.git
cd ClinicalEnrollmentSystem
docker build -t clinical-enrollment-system .
docker run -p 8080:8080 -e ConnectionStrings__DefaultConnection="your-connection-string" clinical-enrollment-systemThen open: http://localhost:8080/swagger
git clone https://github.com/adham1177/ClinicalEnrollmentSystem.git
cd ClinicalEnrollmentSystem
cp appsettings.example.json appsettings.jsonEdit appsettings.json and add your connection string, then:
dotnet restore
dotnet ef database update
dotnet runIdβ unique identifierTitleβ trial nameStatusβ Active / ClosedMaxPatientsβ maximum enrollment capacityRowVersionβ optimistic concurrency tokenEnrollmentsβ list of enrollments
Idβ unique identifierNameβ patient nameAgeβ patient ageConditionβ medical conditionEnrollmentsβ list of enrollments
Idβ unique identifierPatientIdβ foreign key to patientTrialIdβ foreign key to trialEnrolledAtβ enrollment timestampStatusβ Active / Withdrawn