A small, clean-architecture .NET 8 Minimal API that models LTE and 5G NR cells, persists them to PostgreSQL via EF Core, and exposes telecoms analytics — distance, coverage overlap, and RF interference detection (PCI collisions, co-channel overlap).
The point of the project is to demonstrate the SOLID principles in a realistic but small codebase — every layer is a deliberate example of one or more principles, not an academic toy.
┌──────────────────────────────────────────────────────────────────┐
│ InterferenceDetection.Api (Minimal API) │
│ └─ depends on Application + Infrastructure (composition root) │
├──────────────────────────────────────────────────────────────────┤
│ InterferenceDetection.Infrastructure │
│ EF Core / Npgsql / Repositories — implements abstractions │
├──────────────────────────────────────────────────────────────────┤
│ InterferenceDetection.Application │
│ Abstractions (interfaces) + Services + DTOs + Mapping │
├──────────────────────────────────────────────────────────────────┤
│ InterferenceDetection.Domain │
│ Entities, Value Objects, Enums — no framework dependencies │
└──────────────────────────────────────────────────────────────────┘
Reference direction is strict: outer layers depend on inner layers, never the other way around. The Domain project has zero NuGet dependencies.
Each service does one thing:
| Type | Responsibility |
|---|---|
HaversineDistanceCalculator |
Compute great-circle distance between two coordinates |
CircularCoverageAnalyzer |
Decide whether two cell coverage circles overlap |
FrequencyInterferenceDetector |
Identify pairs of cells that may interfere on the RF layer |
CellRepository |
Persist and retrieve cells |
CellMapper |
Map domain entities to DTOs |
No "manager" or "service" class doing four things at once.
- Add a new technology (e.g.
NbIotCell) by creating a newCellsubclass and one branch inFrequencyInterferenceDetector.GetInterferenceReason. The rest of the system — repository, endpoints, analyzers — is untouched. - Add a new distance algorithm (e.g. Vincenty) by writing a class that implements
IDistanceCalculatorand changing one DI line.
LteCell and FiveGCell both substitute cleanly for Cell:
ICellRepository.GetAllAsync()returnsIReadOnlyList<Cell>and analytics operate on the base type.ICoverageAnalyzer.Analyze(Cell, Cell)works regardless of subtype.
Instead of one fat ICellService, there are four small interfaces:
ICellRepository, IDistanceCalculator, ICoverageAnalyzer, IInterferenceDetector.
A caller that just needs distances depends on IDistanceCalculator and nothing else.
Applicationdefines abstractions;Infrastructureimplements them.- The API project knows about abstractions and uses
AddInfrastructure(...)(a single DI extension) to bind them. AppDbContextimplementsIUnitOfWorkso the Application layer never references EF Core.
Cell(abstract) —Id,Name,Location,CoverageRadiusMeters,TransmitPowerDbmLteCell : Cell—Earfcn,PhysicalCellId(0–503),TrackingAreaCodeFiveGCell : Cell—Nrarfcn,PhysicalCellId(0–1007),GnbId,TrackingAreaCodeGeoCoordinate— immutable value object with latitude/longitude bounds validation
Persisted using EF Core Table-Per-Hierarchy (technology discriminator column).
Two cells are reported as interfering if:
- They share the same channel (EARFCN for LTE, NR-ARFCN for 5G), and
- Their coverage circles overlap.
A finding is escalated to a PCI collision when the Physical Cell ID also matches — that's a hard configuration error in a real network.
docker compose up -d postgresThe container exposes Postgres on localhost:5434 (host port 5434 → container port 5432, to avoid clashing with any system Postgres on 5432). Database interference_detection, user/password telecom/telecom. The connection string in appsettings.json already matches.
dotnet run --project src/InterferenceDetection.ApiOn first startup the API will:
- Apply the EF Core migration (creates the
cellstable). - Seed six demo cells around Cape Town — including two pairs deliberately configured to trigger interference findings.
Swagger UI is available at https://localhost:<port>/swagger.
# list all cells
curl http://localhost:5000/cells
# distance between two cells
curl "http://localhost:5000/analytics/distance?cellA=11111111-1111-1111-1111-111111111111&cellB=22222222-2222-2222-2222-222222222222"
# all interference findings in the network
curl http://localhost:5000/analytics/interferencedotnet test| Method | Route | Purpose |
|---|---|---|
GET |
/cells |
List all cells (polymorphic JSON, discriminated by technology) |
GET |
/cells/{id} |
Get a cell |
GET |
/cells/lte |
List LTE cells only |
GET |
/cells/5g |
List 5G cells only |
POST |
/cells/lte |
Create an LTE cell |
POST |
/cells/5g |
Create a 5G cell |
DELETE |
/cells/{id} |
Delete a cell |
GET |
/analytics/distance?cellA=&cellB= |
Distance in meters |
GET |
/analytics/coverage-overlap?cellA=&cellB= |
Coverage overlap |
GET |
/analytics/interference |
Network-wide interference scan |
Everything below is a worked-out example of how easily the architecture absorbs new functionality:
- Add a new cell type (NB-IoT, Wi-Fi AP) — subclass
Cell, add EF discriminator value, add one switch arm in the interference detector. - Swap the distance algorithm — implement
IDistanceCalculator, replace the DI registration. No other file changes. - Sector-aware coverage — replace
CircularCoverageAnalyzerwith a beam/sector-shape analyzer that still implementsICoverageAnalyzer. - PostGIS / spatial queries — change only the repository and migration; the Application and API layers don't move.
- Background interference monitoring — add a
BackgroundServicethat resolvesIInterferenceDetectorand pushes findings to a queue.
- .NET 8 (Minimal APIs)
- EF Core 8 + Npgsql (Postgres provider)
- EFCore.NamingConventions (snake_case)
- xUnit, FluentAssertions, NSubstitute
- Swashbuckle (OpenAPI / Swagger UI)
- PostgreSQL 16 via Docker