Real-time Air Quality Monitoring System
A production-ready .NET Web API for monitoring air quality data from multiple stations, computing EPA-standard Air Quality Index (AQI) values, and providing real-time access to air quality metrics.
AirAware is a comprehensive backend system designed to collect, process, and serve air quality data from distributed monitoring stations. The system accepts readings from sensors or public feeds, computes standardized AQI values based on EPA guidelines, and provides RESTful APIs for data access.
- β Multi-station air quality monitoring
- β Real-time AQI computation (PM2.5 & PM10)
- β EPA-standard breakpoint calculations
- β Flexible data ingestion with raw payload storage
- β Geolocation support for stations
- β API key authentication for secure access
- β Comprehensive test coverage (89 unit tests)
- β RESTful API for managing stations and readings
- .NET 10 - Latest ASP.NET Core Web API
- Entity Framework Core 10 - ORM with SQLite (production: PostgreSQL ready)
- xUnit - Unit testing framework
- Moq - Mocking framework for tests
- GitHub Actions - CI/CD automation
AirAware/
βββ AirAware/ # Main Web API project
β βββ Controllers/ # API endpoints
β β βββ StationController.cs # Station management
β β βββ ReadingController.cs # Reading ingestion & retrieval
β βββ Models/ # Domain entities
β β βββ Station.cs # Monitoring station
β β βββ Reading.cs # Sensor reading
β β βββ AqiRecord.cs # Computed AQI data
β βββ Services/ # Business logic
β β βββ AqiCalculator.cs # EPA AQI computation
β β βββ IAqiCalculator.cs # Calculator interface
β βββ ViewModels/ # Request/Response DTOs
β βββ Data/ # Database context
β β βββ AppDbContext.cs
β βββ Migrations/ # EF Core migrations
βββ AirAware.Tests/ # Comprehensive test suite
β βββ Services/ # Service layer tests
β βββ Controllers/ # API endpoint tests
β βββ Models/ # Domain model tests
βββ .github/workflows/ # CI/CD pipelines
βββ README.md # This file
Represents an air quality monitoring station with geolocation.
| Field | Type | Description |
|---|---|---|
| Id | Guid | Unique identifier |
| Name | string | Station name |
| Latitude | double | Geographic latitude |
| Longitude | double | Geographic longitude |
| Provider | string? | Data provider name |
| Metadata | string? | JSON metadata for extensibility |
| Active | bool | Soft delete flag (default: true) |
| CreatedAt | DateTime | Creation timestamp |
Raw air quality measurements from sensors.
| Field | Type | Description |
|---|---|---|
| Id | Guid | Unique identifier |
| StationId | Guid | Foreign key to Station |
| Pm25 | double | PM2.5 concentration (Β΅g/mΒ³) |
| Pm10 | double? | PM10 concentration (Β΅g/mΒ³, optional) |
| RawPayload | string? | Original JSON payload from sensor |
| CreatedAt | DateTime | Reading timestamp |
Computed Air Quality Index values.
| Field | Type | Description |
|---|---|---|
| Id | Guid | Unique identifier |
| ReadingId | Guid | Foreign key to Reading |
| StationId | Guid | Foreign key to Station |
| AqiValue | int | Overall AQI value (0-500+) |
| Category | string | EPA category (Good, Moderate, etc.) |
| Pm25Aqi | int? | PM2.5 AQI component |
| Pm10Aqi | int? | PM10 AQI component |
| Pm25Category | string? | PM2.5 category |
| Pm10Category | string? | PM10 category |
| ComputedAt | DateTime | Computation timestamp |
X-API-KEY header in all requests.
Example:
curl -H "X-API-KEY: your-api-key-here" http://localhost:5000/api/v1/stationsList all stations.
Response: 200 OK
[
{
"id": "uuid",
"name": "Downtown Station",
"latitude": 40.7128,
"longitude": -74.0060,
"provider": "PurpleAir",
"active": true,
"createdAt": "2026-02-08T10:00:00Z"
}
]Get station by ID.
Response: 200 OK or 404 Not Found
Create a new monitoring station.
Request Body:
{
"name": "Downtown Station",
"latitude": 40.7128,
"longitude": -74.0060,
"provider": "PurpleAir",
"metadata": "{\"sensorType\":\"optical\"}"
}Response: 201 Created
Update station (partial update supported).
Request Body:
{
"name": "Updated Name",
"active": false
}Response: 200 OK or 404 Not Found
Get latest AQI data for a station.
Response: 200 OK
{
"id": "uuid",
"aqiValue": 101,
"category": "Unhealthy for Sensitive Groups",
"computedAt": "2026-02-08T10:15:00Z",
"reading": {
"id": "uuid",
"pm25": 35.5,
"pm10": 154,
"createdAt": "2026-02-08T10:14:00Z"
}
}List all readings.
Response: 200 OK
Get reading by ID.
Response: 200 OK or 404 Not Found
Submit a new air quality reading.
Request Body:
{
"stationId": "uuid",
"pm25": 35.5,
"pm10": 154,
"rawPayload": "{\"sensor\":\"BME680\",\"temp\":22.5}"
}Response: 201 Created
{
"reading": {
"id": "uuid",
"stationId": "uuid",
"pm25": 35.5,
"pm10": 154,
"createdAt": "2026-02-08T10:14:00Z"
},
"aqi": {
"id": "uuid",
"aqiValue": 101,
"category": "Unhealthy for Sensitive Groups",
"pm25Aqi": 101,
"pm10Aqi": 100,
"computedAt": "2026-02-08T10:14:00Z"
}
}Features:
- β Validates station exists
- β Automatically computes AQI on ingestion
- β
Extracts PM10 from
rawPayloadif not provided - β
Supports multiple JSON field names:
pm10,pm_10,pm10_atm
The system implements the official EPA Air Quality Index calculation using standard breakpoint tables.
| Concentration Range | AQI Range | Category |
|---|---|---|
| 0.0 - 12.0 | 0 - 50 | Good |
| 12.1 - 35.4 | 51 - 100 | Moderate |
| 35.5 - 55.4 | 101 - 150 | Unhealthy for Sensitive Groups |
| 55.5 - 150.4 | 151 - 200 | Unhealthy |
| 150.5 - 250.4 | 201 - 300 | Very Unhealthy |
| 250.5 - 500.4 | 301 - 500 | Hazardous |
| Concentration Range | AQI Range | Category |
|---|---|---|
| 0 - 54 | 0 - 50 | Good |
| 55 - 154 | 51 - 100 | Moderate |
| 155 - 254 | 101 - 150 | Unhealthy for Sensitive Groups |
| 255 - 354 | 151 - 200 | Unhealthy |
| 355 - 424 | 201 - 300 | Very Unhealthy |
| 425 - 504 | 301 - 500 | Hazardous |
AQI = ((I_hi - I_lo) / (C_hi - C_lo)) Γ (C - C_lo) + I_lo
Where:
- C = measured concentration
- C_lo, C_hi = concentration breakpoints
- I_lo, I_hi = index breakpoints
- Final AQI = max(PM2.5 AQI, PM10 AQI)
See AirAware/Services/AqiCalculator.cs for implementation.
89 passing tests covering all major components:
AirAware.Tests/
βββ Services/AqiCalculatorTests.cs (16 tests)
β β
All EPA breakpoints (PM2.5 & PM10)
β β
Linear interpolation accuracy
β β
Edge cases and boundary values
β β
Final AQI selection logic
β
βββ Controllers/StationControllerTests.cs (9 tests)
β β
CRUD operations
β β
Partial updates
β β
Validation handling
β
βββ Controllers/ReadingControllerTests.cs (12 tests)
β β
Reading ingestion
β β
AQI computation integration
β β
Raw payload parsing
β β
PM10 extraction variants
β
βββ Models/ (18 tests)
β
Domain model behavior
β
Default values
β
Relationships
# Run all tests
dotnet test
# Run with detailed output
dotnet test --verbosity normal
# Run specific test class
dotnet test --filter "FullyQualifiedName~AqiCalculatorTests"Test summary: total: 89, failed: 0, succeeded: 89, skipped: 0
Build succeeded β
- .NET 10 SDK
- SQLite (included) or PostgreSQL (production)
- Git
-
Clone the repository
git clone https://github.com/yourusername/AirAware.git cd AirAware -
Restore dependencies
dotnet restore
-
Configure API Key
Set the API key as an environment variable:
export ApiKey="your-secure-api-key-here"
Note: The API key must be set as an environment variable for security reasons. Do not store API keys in configuration files.
-
Run migrations
cd AirAware dotnet ef database update -
Start the application
dotnet run
The API will be available at
http://localhost:5000Note: All API requests must include the
X-API-KEYheader:curl -H "X-API-KEY: your-secure-api-key-here" http://localhost:5000/api/v1/stations
# Build the solution
dotnet build
# Run tests
dotnet test
# Run the application with hot reload
dotnet watch run
# Create a new migration
dotnet ef migrations add MigrationName
# Update database
dotnet ef database updateAutomated PR creation from feature branches to main:
- Triggers on push to
feature/*branches - Creates pull request automatically
- Prevents duplicate PRs
See .github/workflows/1-feature-to-main.yml
Set up automatic upstream tracking:
git config --global push.autoSetupRemote true20260208171606_CreateAqiRecordsService- Initial schema with Stations, Readings, AqiRecords
cd AirAware
dotnet ef migrations add YourMigrationName
dotnet ef database updateThe system uses SQLite by default but is designed for PostgreSQL in production:
-
Install package:
dotnet add package Npgsql.EntityFrameworkCore.PostgreSQL
-
Update
AppDbContext.cs:optionsBuilder.UseNpgsql("your-connection-string");
Edit appsettings.json for configuration:
{
"Logging": {
"LogLevel": {
"Default": "Information",
"Microsoft.AspNetCore": "Warning"
}
},
"AllowedHosts": "*"
}Current: SQLite (app.db)
Production: PostgreSQL (update connection string in AppDbContext.cs)
Contributions are welcome! Please follow these guidelines:
- Fork the repository
- Create a feature branch:
git checkout -b feature/your-feature - Commit your changes:
git commit -am 'Add some feature' - Push to the branch:
git push origin feature/your-feature - Submit a pull request
- Follow C# naming conventions
- Write unit tests for new features
- Maintain test coverage above 80%
- Document public APIs with XML comments
Copyright (c) 2026 JoΓ£o Ferreira
Non-Commercial License - This software is free for personal and non-commercial use.
Key restrictions:
- β Commercial use is strictly prohibited
- β Attribution required (credit to JoΓ£o Ferreira)
- β Free to use, modify, and distribute for non-commercial purposes
See LICENSE for full terms.
JoΓ£o Ferreira
- Built with .NET 10 and β€οΈ
- February 2026
- Authentication & Authorization (API keys) β
- Rate limiting for API endpoints
- WebSocket support for real-time updates
- Historical data aggregation
- Geographic queries (nearest stations)
- Alert system for unhealthy AQI levels
- Support for additional pollutants (O3, NO2, SO2, CO)
- Data export endpoints (CSV, JSON)
- Grafana/Prometheus monitoring
- Docker containerization
- Kubernetes deployment configs
Last Updated: February 8, 2026