.NET Application Modernization Report: ContosoUniversity¶
Modernization Notes¶
1. Target Framework & Project System Modernization¶
- Port to .NET 8 (LTS): Migrated
ContosoUniversity.csprojfrom legacynet5.0tonet8.0. - SDK & Package Cleanup:
- Retained the modern
Microsoft.NET.Sdk.WebSDK project format. - Replaced legacy Entity Framework 6 (
EntityFramework 6.4.4) withMicrosoft.EntityFrameworkCore(8.0.11). - Added PostgreSQL provider
Npgsql.EntityFrameworkCore.PostgreSQL(8.0.11). - Updated
Microsoft.AspNetCore.Mvc.NewtonsoftJsonto8.0.11. - Retained
BuildBundlerMinifier(3.2.449) to compile and minify CSS bundles duringdotnet publish. - Removed deprecated analyzers and obsolete packages
(
System.ComponentModel.Annotations,System.Configuration.ConfigurationManager,Google.Cloud.Diagnostics.AspNetCore,Grpc.AspNetCore). - Added
<EnableSourceControlManagerQueries>false</EnableSourceControlManagerQueries>to ensure reproducible builds across varied container and git reftable environments.
- Retained the modern
2. Data Layer Migration (SQL Server to PostgreSQL)¶
- SchoolContext Refactoring:
- Migrated
ContosoUniversity.DAL.SchoolContextfromSystem.Data.Entity.DbContexttoMicrosoft.EntityFrameworkCore.DbContext. - Configured DI options via
DbContextOptions<SchoolContext>. - Mapped entity table name explicitly in
OnModelCreating(modelBuilder.Entity<Student>().ToTable("Student")).
- Migrated
- Database Initialization & Seeding:
- Refactored
SchoolInitializerintoDbInitializerimplementingcontext.Database.EnsureCreated()and idempotent seed data population. - Retained
SchoolInitializeras an alias wrapper for backward compatibility. - At startup, the application creates the database and populates seed students if the table is empty.
- Refactored
-
PostgreSQL Case Sensitivity & Raw SQL:
- PostgreSQL folds unquoted SQL identifiers to lowercase, whereas EF Core creates quoted table and column identifiers.
-
Refactored the raw SQL query in
StudentController.GetEnrollmentSummaryfrom unquoted SQL to quoted identifiers: -
Switched from EF6's
Database.SqlQuery<T>to EF Core 8'sDatabase.SqlQueryRaw<T>.
-
Timestamp Compatibility:
- Enabled
AppContext.SetSwitch("Npgsql.EnableLegacyTimestampBehavior", true)inProgram.csto allow seamless persistence ofDateTimevalues (withKind=Unspecifiedproduced by HTML form bindings) into PostgreSQL timestamp columns.
- Enabled
- Exception Handling:
- In
StudentController.Create, replaced EF6RetryLimitExceededExceptionwith EF Core'sDbUpdateExceptionand injectedILogger<StudentController>for structured error logging. - Removed manual
Disposeoverride inStudentControllerso the scopedSchoolContextlifetime is managed by ASP.NET Core dependency injection.
- In
3. Google Cloud Run Container Contract Compliance¶
- Port Binding:
- Dynamically binds to
http://0.0.0.0:${PORT:-8080}as required by Cloud Run. - Configured both
ASPNETCORE_HTTP_PORTSandPORTenvironment variables in the container image.
- Dynamically binds to
- Structured JSON Logging:
- Removed all legacy or file-based logging configurations.
- Configured
ILoggerwithlogging.AddJsonConsole()to output single-line structured JSON logs to stdout/console for native Google Cloud Logging ingestion.
- Graceful Shutdown & Signal Handling:
- Relies on the .NET Generic Host (
Host.CreateDefaultBuilder) nativeSIGTERMhandling and registered a structured logging lifecycle listener onIHostApplicationLifetime.ApplicationStoppingfor clean shutdown.
- Relies on the .NET Generic Host (
- Concurrency & In-Memory State Warning:
- Registered
IMemoryCacheinStartup.cswith architectural documentation warning that in-memory cache is local to each container instance and does not persist across Cloud Run horizontal auto-scaling or scale-to-zero events. Cloud Memorystore (Redis) is recommended for distributed caching.
- Registered
4. Containerization & Dockerfile Best Practices¶
- Multi-Stage Build:
- Build stage:
mcr.microsoft.com/dotnet/sdk:8.0. - Runtime stage:
mcr.microsoft.com/dotnet/aspnet:8.0.
- Build stage:
- Build Checks: Verified against Docker Build Checks with
docker build --check .(0 errors, 0 warnings). - Static Asset Serving: Bundler generates
wwwroot/css/styles.cssduring publish, andfavicon.icois placed inwwwroot, serving all static assets with200 OK.
Local Testing¶
Container Build¶
Build the container image using Docker:
To run Docker build checks without building:
Direct Container Run¶
Run the container directly (ensure an accessible PostgreSQL instance is running):
docker run --rm \
-p 8080:8080 \
-e PORT=8080 \
-e ConnectionStrings__SchoolContext="Host=host.docker.internal;Port=5432;Database=contosouniversity;Username=postgres;Password=postgres" \
contosouniversity:latest
(Note: all docker run commands use the --rm flag to remove the container
upon exit).
Docker Compose¶
Start the application and PostgreSQL database with build and detach options:
Wait for the application to become ready:
for i in $(seq 60); do curl -sf -o /dev/null http://localhost:8080/ && break; sleep 2; done; docker compose logs --tail 50
Viewing Logs¶
View aggregate or service-specific logs:
# View combined logs
docker compose logs
# Tail last 50 lines of logs
docker compose logs --tail 50
# Follow live logs in real time
docker compose logs -f
# View application logs only
docker compose logs web
# View database logs only
docker compose logs db
Stopping and Cleaning Up¶
Endpoints¶
The application exposes the following HTTP endpoints:
| Endpoint | HTTP Method | Description |
|---|---|---|
/ |
GET | Default application route (maps to StudentController.Index). Displays the student roster table, student creation form, and enrollment date summary. |
/Student |
GET | Student controller root view (maps to StudentController.Index). |
/Student/Index |
GET | Explicit student index view. |
/Student/Create |
POST | Handles new student creation (LastName, FirstMidName, EnrollmentDate). Protected with ASP.NET Core Anti-Forgery Token. Redirects to / on success. |
/css/styles.css |
GET | Bundled and minified CSS stylesheet containing Bootstrap and application styles. |
/favicon.ico |
GET | Application favicon. |
Tested Endpoints Report¶
I tested all supported HTTP endpoints and main user flows against the running
Docker Compose environment using curl.
| Endpoint | HTTP Method | Command Used to Test | Command Output / Verification Result |
|---|---|---|---|
/ |
GET | curl -s -i http://localhost:8080/ |
HTTP/1.1 200 OK; Content-Type: text/html; charset=utf-8; HTML rendered containing student table, creation form, and enrollment date summary. |
/Student |
GET | curl -s -i http://localhost:8080/Student |
HTTP/1.1 200 OK; Content-Type: text/html; charset=utf-8; HTML rendered with anti-forgery token and initial seed data (8 students). |
/Student/Index |
GET | curl -s -i http://localhost:8080/Student/Index |
HTTP/1.1 200 OK; Content-Type: text/html; charset=utf-8; Full student index view matching default route. |
/css/styles.css |
GET | curl -s -i http://localhost:8080/css/styles.css |
HTTP/1.1 200 OK; Content-Type: text/css; Content-Length: 122329; CSS stylesheet successfully served. |
/favicon.ico |
GET | curl -s -i http://localhost:8080/favicon.ico |
HTTP/1.1 200 OK; Content-Type: image/x-icon; Content-Length: 32038; Favicon successfully served. |
/Student/Create |
POST | curl -s -i -c cookies.txt -b cookies.txt --data-urlencode "__RequestVerificationToken=$TOKEN" --data-urlencode "NewStudent.LastName=Curie" --data-urlencode "NewStudent.FirstMidName=Marie" --data-urlencode "NewStudent.EnrollmentDate=2024-09-01" -X POST http://localhost:8080/Student/Create |
HTTP/1.1 302 Found; Location: /; Form submission processed successfully with anti-forgery validation and redirected to /. |
/Student/Index (Post-Creation Verification) |
GET | curl -s -b cookies.txt http://localhost:8080/Student/Index |
HTTP/1.1 200 OK; Verified that new student "Curie, Marie" (Enrollment Date: 2024-09-01) is persisted in PostgreSQL and rendered in both the student table and enrollment date summary. |