Analytics portal for Malaysian COVID-19 open data, built on .NET 8 with Clean Architecture, CQRS (MediatR), and a server-rendered ASP.NET Core MVC front-end that consumes an internal ASP.NET Core REST API over HTTP.
Data source: Ministry of Health Malaysia open datasets —
github.com/MoH-Malaysia/covid19-public.
- Project Overview
- Business Objectives
- Features Implemented
- Technology Stack
- Solution Structure
- Architecture Overview
- Security Features
- Logging Strategy
- Testing Strategy
- Setup Instructions
- Running the Application
- Running Tests
- Future Enhancements
Further documentation: docs/Architecture.md · Architecture Decision Records · Diagrams.
The COVID-19 Analytics Portal ingests Malaysia's public-health datasets into a local, read-optimised store and surfaces them through three analytical experiences plus an audit log. It is built as two separate ASP.NET Core hosts:
CovidAnalyticsPortal.API— a versioned REST API (the system of record for the portal's read models).CovidAnalyticsPortal.Web— a server-rendered MVC application that consumes the API over HTTP through a typedHttpClient.
A background service keeps the local store synchronised with the upstream Ministry of Health (MoH) datasets, so the portal renders real data with no manual database or import step.
| Page | Purpose |
|---|---|
| Dashboard | National headline figures (total cases, deaths, active, recovered, plus new cases/deaths today) with a per-state breakdown and chart. |
| Trend Analysis | Time series of a chosen metric (cases, active, recovered, deaths), national or per-state, with start/end values, change, percentage change, and direction. |
| State Statistics | Filterable, tabular per-state daily figures over a date range. |
| Audit Trail | Read-only view of recorded data-access and ingestion events. |
- Make public COVID-19 data usable. Turn raw daily MoH CSV snapshots into fast, filterable national and state-level analytics.
- Stay responsive and resilient. Serve analytics from a local store so the portal does not depend on the upstream feed at request time, and degrades gracefully when the feed is briefly unreachable.
- Compute insight server-side. Aggregate headline figures and derive trends (change, percentage change, direction) on the server rather than in the browser.
- Be self-provisioning. Apply the database schema and ingest data automatically at start-up so the solution runs on a clean machine with no manual setup.
- Be accountable. Persist an immutable audit trail of data-access and ingestion events, separate from operational logs.
- Be maintainable and testable. Use Clean Architecture and CQRS to keep business rules independent of frameworks, with a meaningful automated-test suite.
- National dashboard — cumulative cases/deaths, active cases, recovered, new cases/deaths for the latest day in range, and a per-state breakdown ordered by new cases (
DashboardService). - Trend analysis — national or per-state metric series with computed start/end value, absolute change, percentage change, and direction (
Increasing/Decreasing/Stable) derived in the domain (TrendRecord,AnalyticsService). - State statistics — per-state daily figures over a date range, optionally filtered to a single state (
AnalyticsService.GetStateStatisticsAsync). - Audit trail — append-only
AuditTrailrecords written byAuditServiceand exposed read-only via the API. - Automatic data ingestion —
CovidDataSyncBackgroundServiceruns shortly after start-up and then on a configurable interval;CovidDataImporterperforms an idempotent upsert fromMohDataProvider. - Automatic schema migration — pending EF Core migrations are applied at API start-up (
MigrateDatabaseAsync). - Versioned REST API —
Asp.VersioningURL-segment versioning (/api/v1.0/...) with versioned Swagger UI in Development. - Resilient outbound integration — the MoH typed
HttpClientuses the standard resilience handler (retry, timeout, circuit breaker). - Cross-cutting CQRS behaviours —
LoggingBehaviourandValidationBehaviourrun inside the MediatR pipeline. - Security middleware — correlation IDs, security response headers, global
ProblemDetailsexception handling, and per-IP rate limiting. - Structured logging — Serilog to console and rolling file, enriched with a correlation ID.
| Concern | Technology |
|---|---|
| Runtime | .NET 8 (LTS), C# with nullable reference types and implicit usings |
| Web (presentation) | ASP.NET Core MVC, Razor Views |
| API | ASP.NET Core Web API, Asp.Versioning.Mvc 8.1 |
| Mediation / CQRS | MediatR 12.4 (queries + pipeline behaviours) |
| Validation | FluentValidation 11.10 (executed inside the MediatR pipeline) |
| Persistence | EF Core 8.0.10 — SQLite (default) or SQL Server |
| Outbound resilience | Microsoft.Extensions.Http.Resilience 8.10 (retry, timeout, circuit breaker) |
| Caching | Microsoft.Extensions.Caching.Memory (IMemoryCache) |
| Logging | Serilog 4 (console + rolling file sinks, environment/thread enrichers) |
| API docs | Swashbuckle / Swagger 6.6 (versioned) |
| UI assets | Bootstrap 5, Bootstrap Icons, Chart.js, flatpickr (via CDN) |
| Testing | xUnit 2.5, FluentAssertions 6.12, Moq 4.20, WebApplicationFactory, in-memory SQLite, Coverlet |
See each project's .csproj for exact, pinned package versions.
SeniorDeveloperAssignment/
├─ CovidAnalyticsPortal.sln
├─ README.md
├─ docs/
│ ├─ Architecture.md # Architecture & design reference
│ ├─ adr/ # Architecture Decision Records
│ └─ diagrams/ # Mermaid diagrams
├─ src/
│ ├─ CovidAnalyticsPortal.Domain/ # Core — no outward dependencies
│ │ ├─ Common/ # Entity, AuditableEntity, ValueObject
│ │ ├─ Entities/ # CovidStatistic, StateStatistic, TrendRecord, AuditTrail
│ │ ├─ ValueObjects/ # DateRange, StateCode, CaseMetrics
│ │ ├─ Enums/ # MetricType, TrendDirection, AuditAction
│ │ ├─ Exceptions/ # DomainException
│ │ └─ Repositories/ # IRepository<T>, IUnitOfWork
│ ├─ CovidAnalyticsPortal.Application/ # Use cases — depends on Domain
│ │ ├─ Common/Behaviours/ # LoggingBehaviour, ValidationBehaviour
│ │ ├─ Common/Interfaces/ # IAnalyticsService, IDashboardService, IMohDataProvider, IDateTimeProvider, IAuditService
│ │ ├─ Dashboard/ Statistics/ Trends/ Audit/ # Queries, handlers, validators, DTOs
│ │ ├─ Services/ # DashboardService, AnalyticsService
│ │ └─ DependencyInjection.cs
│ ├─ CovidAnalyticsPortal.Infrastructure/ # EF Core, MoH client, cache, Serilog, audit
│ │ ├─ Persistence/ # AppDbContext, Configurations, Repositories, UnitOfWork, Migrations
│ │ ├─ ExternalServices/Moh/ # MohDataProvider, MohApiOptions
│ │ ├─ BackgroundServices/ # CovidDataSyncBackgroundService, CovidDataImporter, options
│ │ ├─ Audit/ Logging/ Time/
│ │ └─ DependencyInjection.cs
│ ├─ CovidAnalyticsPortal.API/ # REST API — versioning, Swagger, middleware
│ │ ├─ Controllers/V1/ # DashboardController, AnalyticsController, AuditController
│ │ ├─ Middleware/ # CorrelationId, SecurityHeaders, GlobalExceptionHandling
│ │ ├─ Context/ Swagger/ Logging/
│ │ └─ Program.cs
│ └─ CovidAnalyticsPortal.Web/ # MVC — consumes the API over HTTP
│ ├─ Controllers/ # Dashboard, Statistics, Trends, Audit, Home
│ ├─ Models/ # API contracts + view models
│ ├─ Services/ # ICovidApiClient (typed HttpClient)
│ ├─ Views/ # Razor + Bootstrap 5 + Chart.js
│ └─ wwwroot/
└─ tests/
├─ CovidAnalyticsPortal.Tests.Unit/ # Services, repositories, validators
└─ CovidAnalyticsPortal.Tests.Integration/ # API endpoints (WebApplicationFactory)
The solution follows Clean Architecture: the domain sits at the centre with zero outward dependencies, and frameworks, the database, and the MoH feed are plug-in details on the outer ring. The dependency rule points strictly inward and is enforced by project references.
┌────────────────────────────────────────────────────────────┐
│ Presentation │
│ CovidAnalyticsPortal.Web (MVC) ──HTTP──▶ .API (REST) │
├────────────────────────────────────────────────────────────┤
│ Infrastructure │
│ EF Core · MoH HttpClient · IMemoryCache · Serilog · Audit │
├────────────────────────────────────────────────────────────┤
│ Application │
│ CQRS handlers · DTOs · Validators · Pipeline behaviours │
├────────────────────────────────────────────────────────────┤
│ Domain (core) │
│ Entities · Value Objects · Enums · IRepository / IUnitOfWork│
└────────────────────────────────────────────────────────────┘
- Domain references nothing. It owns entities (
CovidStatistic,StateStatistic,TrendRecord,AuditTrail), value objects (DateRange,StateCode,CaseMetrics), enums, and the persistence abstractionsIRepository<T>andIUnitOfWork. - Application references Domain only. It hosts CQRS queries/handlers, DTOs, FluentValidation validators, the MediatR behaviours (
LoggingBehaviour,ValidationBehaviour), and the service contracts/implementations (IDashboardService/DashboardService,IAnalyticsService/AnalyticsService). - Infrastructure references Application (and transitively Domain). It implements persistence (
EfRepository<T>,UnitOfWork,AppDbContext), the resilient MoH integration, in-memory caching, the audit service, the system clock, and background ingestion. - API references Application + Infrastructure. It exposes versioned REST controllers and owns the HTTP pipeline (correlation, security headers, exception handling, rate limiting, Swagger).
- Web references none of the other solution projects — it talks to the API purely over HTTP via the typed
ICovidApiClient, preserving the architectural seam.
For full detail see docs/Architecture.md and the diagrams.
Implemented in the solution today:
- Server-side validation (A03). Every query is validated by FluentValidation inside the MediatR
ValidationBehaviour: date ranges must be coherent, end dates cannot be in the future, state filters must be recognised codes, and the metric must be supported. - No SQL injection (A03). All data access goes through EF Core with parameterised LINQ; no string-concatenated SQL.
- Security response headers.
SecurityHeadersMiddlewaresetsContent-Security-Policy(default-src 'none'; frame-ancestors 'none'),X-Content-Type-Options: nosniff,X-Frame-Options: DENY,Referrer-Policy: no-referrer,Permissions-Policy, and removes theServerheader. - Safe error handling (A05/A09).
GlobalExceptionHandlingMiddlewareconverts exceptions to RFC 7807ProblemDetails:ValidationException→ 400 with anerrorsdictionary,DomainException→ 400, and any other exception → 500 with internal details suppressed outside Development. Every response carries a correlation ID. - SSRF protection (A10). The MoH base address comes only from validated configuration (
IOptions<MohApiOptions>withValidateDataAnnotations+ValidateOnStart); the app never fetches arbitrary user-supplied URLs. - Rate limiting. A per-IP fixed-window limiter (100 requests/minute, HTTP 429 on rejection) is applied to all API endpoints.
- HTTPS redirection is enabled on both hosts; HSTS is enabled on the Web host outside Development.
- No PII. The portal processes aggregate, public data only. Audit entries store actor, action, timestamp, correlation ID, and optional IP — no personal data.
- Configuration hygiene. No secrets are committed; configuration is environment-layered via
appsettings.{Environment}.json.
See ADR-004 and docs/Architecture.md for the full security design.
- Structured logging with Serilog. The API configures Serilog via
SerilogFileConfiguration/SerilogConfiguratorwith console and rolling-file sinks and environment/thread enrichers. A bootstrap logger captures failures during start-up. - Correlation IDs.
CorrelationIdMiddlewarereads an inboundX-Correlation-IDheader (or generates one), stores it inHttpContext.Items, pushes it into the SerilogLogContextso it appears on every log line, and echoes it back on the response. - Request logging.
UseSerilogRequestLogging()records method, path, status code, and elapsed time for each request, positioned to capture the final translated status code. - Pipeline logging.
LoggingBehaviourlogs the start, successful completion (with elapsed milliseconds), and any failure of every MediatR request. - Operational vs. audit logging are separate concerns. Serilog output is for diagnostics; the business
AuditTrail(persisted viaAuditService) is a distinct, queryable record. Audit failures are caught and logged so they can never break the primary request.
The suite contains 85 tests across two projects (71 unit + 14 integration), all passing, with ~88% line coverage (excluding auto-generated EF migrations).
| Suite | Targets | Approach |
|---|---|---|
| Unit — Services | DashboardService, AnalyticsService |
Moq-mocked IUnitOfWork / IRepository; deterministic clock |
| Unit — Repositories | EfRepository<T>, UnitOfWork |
Real in-memory SQLite so EF configurations, converters and owned types are exercised |
| Unit — Infrastructure | CovidDataImporter, MohDataProvider, AuditService, SystemDateTimeProvider |
Stubbed HttpMessageHandler / mocked provider; idempotent-upsert and cache assertions |
| Unit — Application | Query handlers, LoggingBehaviour, ValidationBehaviour |
Mocked services; MediatR pipeline behaviour tests |
| Unit — Validators | GetDashboardQuery, GetStateStatisticsQuery, GetTrendAnalysisQuery, GetAuditTrailQuery validators |
FluentValidation.TestHelper |
| Integration — API | Dashboard, Analytics, and data-sync ingestion | WebApplicationFactory<Program> over the full HTTP pipeline with seeded in-memory SQLite |
Principles: Arrange-Act-Assert, deterministic time via an injected IDateTimeProvider, no real network calls (the integration factory disables outbound sync and seeds its own data), and Method_Scenario_ExpectedResult naming.
- .NET 8 SDK
- A terminal (PowerShell on Windows; bash/zsh elsewhere)
- (Optional) Visual Studio 2022 17.8+ or VS Code with the C# Dev Kit
dotnet restore CovidAnalyticsPortal.sln
dotnet build CovidAnalyticsPortal.slnThe API ships with working defaults in src/CovidAnalyticsPortal.API/appsettings.json:
To target SQL Server, override the provider and connection string (e.g. via environment variables or appsettings.Production.json):
{
"Database": { "Provider": "SqlServer" },
"ConnectionStrings": { "DefaultConnection": "Server=...;Database=CovidPortal;..." }
}The Web app points at the API in src/CovidAnalyticsPortal.Web/appsettings.json:
{ "CovidApi": { "BaseUrl": "http://localhost:5114", "TimeoutSeconds": 30, "ApiVersion": "1.0" } }No manual database setup is required. On first start the API:
- Applies EF Core migrations (
MigrateDatabaseAsync) to create the SQLite schema (covidportal.db). - Synchronises COVID-19 data from the MoH open datasets via
CovidDataSyncBackgroundService, which runs afterCovidDataSync:InitialDelaySecondsand then everyCovidDataSync:IntervalHours(default 12h). Ingestion is idempotent and resilient — upstream failures are caught, logged, and retried on the next interval, so the host never crashes.
Note: the MoH dataset spans 2020–2022, so today's date may show zeros. Pass an explicit historical range, e.g.
GET /api/v1.0/dashboard?from=2021-06-01&to=2021-06-30, to see populated figures.
Run the API and the Web app in two terminals.
# Terminal 1 — REST API
dotnet run --project src/CovidAnalyticsPortal.API --launch-profile http
# API: http://localhost:5114
# Swagger UI: http://localhost:5114/swagger (Development only)# Terminal 2 — MVC Web app
dotnet run --project src/CovidAnalyticsPortal.Web --launch-profile http
# Web: http://localhost:5298 (opens on the Dashboard)Default ports (from each project's Properties/launchSettings.json):
| Host | HTTP | HTTPS |
|---|---|---|
| API | 5114 |
7135 |
| Web | 5298 |
7154 |
Start the API first so the Web app can reach it. Then browse to the Web URL and use the Dashboard, Trend Analysis, State Statistics, and Audit pages.
# Run everything
dotnet test CovidAnalyticsPortal.sln
# Run a single project
dotnet test tests/CovidAnalyticsPortal.Tests.Unit
dotnet test tests/CovidAnalyticsPortal.Tests.Integration
# Collect coverage (Cobertura)
dotnet test CovidAnalyticsPortal.sln --collect:"XPlat Code Coverage"Generate a human-readable coverage report:
dotnet tool install --global dotnet-reportgenerator-globaltool
reportgenerator -reports:"tests/**/coverage.cobertura.xml" \
-targetdir:"tests/CoverageReport" -reporttypes:"Html;TextSummary" \
-classfilters:"-*.Migrations.*" \
-assemblyfilters:"+CovidAnalyticsPortal.*;-CovidAnalyticsPortal.Tests.*"Expected result: 85 passing (71 unit + 14 integration), 0 failed, ~88% line coverage.
The core solution (Domain → Application → Infrastructure → API → Web → Tests) is complete and builds clean.
- CORS locked to the Web origin and
AllowedHoststightened per environment. - Authentication / authorization for the audit endpoint and any future write operations (e.g. an
Adminpolicy). - Architecture tests (e.g. NetArchTest) to fail the build if the dependency rule is violated.
- Containerisation (a
Dockerfileper host) and a CI/CD pipeline (build, test, coverage gate, vulnerability scan). - Centralised observability — ship Serilog to a sink such as Seq / Application Insights and alert on errors and open-circuit events.
- Gated migrations for multi-instance deploys — apply migrations as a dedicated deploy step rather than at start-up to avoid concurrent-migration races.
Built with .NET 8 · Clean Architecture · CQRS · Data courtesy of the Ministry of Health Malaysia.
{ "Database": { "Provider": "Sqlite" }, "ConnectionStrings": { "DefaultConnection": "Data Source=covidportal.db" }, "MohApi": { "BaseUrl": "https://raw.githubusercontent.com/MoH-Malaysia/covid19-public/main/", "TimeoutSeconds": 30, "RetryCount": 3, "CacheMinutes": 60 }, "CovidDataSync": { "Enabled": true, "InitialDelaySeconds": 5, "IntervalHours": 12 } }