A production .NET data extraction and orchestration platform that pulls historical SCADA data out of CygNet's ESENT-backed historian and lands it in SQL Server, where it can be consumed by engineers, data architects, and downstream analytical systems.
Built at Targa Resources to serve internal teams and a large compressor optimization initiative. Hand-coded in C# on ASP.NET Core using an Onion / Clean Architecture, predating the era of AI-assisted development.
CygNet stores high-frequency SCADA history in ESENT — an embedded database that's fast for the historian but a dead end for anyone who wants to do real analytical work. CygnetServices bridges that gap:
- Extracts time-series and event data from CygNet into a relational SQL Server warehouse on a schedule
- Schedules recurring extract jobs with configurable cadence, scope, and retry behavior
- Notifies operators and engineers when extracts fail, fall behind, or breach thresholds
- Monitors its own worker services with health and life-check endpoints
- Exposes an HTTP API and a Razor web UI for engineers to configure jobs, inspect status, and trigger ad-hoc pulls
At Targa, this fed daily analytical work by engineers and data architects, and was the data backbone for a large compressor optimization project.
CygnetServices follows an Onion Architecture: the domain sits at the center, infrastructure points inward through abstractions, and no inner layer ever depends on an outer one. The result is a codebase where the business rules don't know or care that data happens to come from CygNet's ESENT store and land in SQL Server — those are swappable details on the edge.
┌─────────────────────────────────────────────┐
│ Presentation │
│ ┌──────────────┐ ┌────────────────┐ │
│ │ WebApi │ │ CygnetServices │ │
│ │ (REST API) │ │ Web (Razor) │ │
│ └──────┬───────┘ └────────┬───────┘ │
└──────────┼─────────────────────┼────────────┘
│ │
┌──────────▼─────────────────────▼────────────┐
│ Services (Use Cases) │
│ orchestration, application logic │
└──────────┬──────────────────────────────────┘
│ depends only on ↓
┌──────────▼──────────────────────────────────┐
│ Services.Abstractions + Contracts │
│ interfaces, DTOs, the seam between │
│ the inside and the outside world │
└──────────▲──────────────────────────────────┘
│ implemented by ↑
┌──────────────────────┼────────────────────────┐
│ │ │
┌────▼─────┐ ┌──────▼──────┐ ┌──────▼──────┐
│ Domain │ │ Persistence │ │ Workers │
│ entities │ │ EF Core / │ │ ScadaExtract│
│ + rules │ │ SQL Server │ │ Scheduler │
└──────────┘ └─────────────┘ │ Notification│
│ HealthCheck │
│ JobManager │
│ CygnetAccess│
└─────────────┘
- An engineer defines a recurring extract through the Razor UI or WebApi
ScheduleJobManagerpersists the job definition;SchedulerServicetriggers it on cadenceScadaExtractServicecallsCygnetAccessService, which wraps the CygNet API and pulls data from ESENT- Extracted points land in SQL Server via the
Persistencelayer HealthLifeCheckServicereports worker liveness;NotificationServiceraises alerts on failure or lag
Each worker is a separate process — they can be deployed, scaled, and restarted independently, which mattered when one slow extract shouldn't block the rest.
| Project | Onion Layer | Role |
|---|---|---|
Domain |
Core | Entities, value objects, domain rules — no dependencies |
Services.Abstractions |
Application | Interfaces defining what the application can do |
Contracts |
Application | DTOs crossing the boundary in and out |
Services |
Application | Use case implementations |
Persistence |
Infrastructure | EF Core, SQL Server, repository implementations |
CygnetAccessService |
Infrastructure | Worker wrapping the CygNet API for ESENT reads |
ScadaExtractService |
Infrastructure | Worker performing scheduled extracts |
SchedulerService |
Infrastructure | Worker firing jobs on cadence |
ScheduleJobManager |
Infrastructure | Worker managing job lifecycle |
NotificationService |
Infrastructure | Worker dispatching alerts |
HealthLifeCheckService |
Infrastructure | Worker reporting liveness |
WebApi |
Presentation | ASP.NET Core REST API |
CygnetServicesWeb |
Presentation | Razor UI for engineers |
Presentation |
Presentation | Shared presentation concerns |
- C# / .NET — ASP.NET Core, Web API, Razor Pages
- Worker Services — long-running background processes (
IHostedService) - Entity Framework Core — SQL Server persistence
- CygNet API — proprietary SCADA historian (ESENT-backed)
- SQL Server — target warehouse for extracted SCADA data
A few decisions worth calling out, since they're the reason I'm proud of this codebase:
-
Onion over N-tier. The domain has zero references to EF Core, ASP.NET, or CygNet. Persistence and CygNet access are infrastructure details behind interfaces. This was deliberate: the CygNet API surface changed across versions, and the SQL schema evolved as analytical needs grew — neither change required touching the core.
-
Workers as separate projects, not background tasks in the API. Each long-running concern is its own process. A stuck extract on one well shouldn't take down notifications or scheduling. Operationally this also meant ops could restart one worker without bouncing the whole platform.
-
Hand-coded, pre-AI. Every line, every interface, every dependency graph was a deliberate choice. Worth noting in a portfolio where most modern code is co-authored with an LLM — this one demonstrates the architectural thinking from scratch.
This repository separates portable onion layers from CygNet/Windows adapters.
These projects should build without proprietary CygNet DLLs:
DomainContractsServices.AbstractionsServicesPresentationArchitecture.Tests
chmod +x scripts/validate-portable-architecture.sh
./scripts/validate-portable-architecture.shArchitecture tests enforce dependency direction for the inner rings.
Place CygNet API DLLs in libs/cygnet/README.md, then:
export BUILD_ADAPTERS=true
./scripts/validate-portable-architecture.sh
dotnet build CygnetServices.sln -c DebugAdapter projects (CygnetAccessService, Persistence mappers, WebApi composition root) require the CygNet SDK and build on Windows when the DLLs are present.
Committed appsettings*.json files use placeholders only (YOUR_SQL_SERVER, YOUR_OIDC_CLIENT_SECRET, and so on). For local development:
- Copy values into User Secrets or environment variables.
- Set
WebApi→Authentication:JwtPublicKey,Cors:AllowedOrigins, and connection strings. - Set
CygnetServicesWeb→ApiSettings:BaseUrl,Oidc:*, andSyncfusion:LicenseKey(optional; omit or leave placeholder to skip license registration).
WebApi listens on https://localhost:44387 and CygnetServicesWeb on https://localhost:7113 per launchSettings.json.
The repo does not include EF Core migrations. Schema mapping lives under Persistence/Configurations/; point ConnectionStrings:ScadaExtracts at an existing SQL Server database (or generate fresh migrations locally with dotnet ef).
Published as a portfolio artifact illustrating C#/.NET architecture, worker service orchestration, and industrial data integration patterns. CygNet API references are abstracted; no proprietary CygNet code or credentials are included.
Jerrod Tuck — SCADA engineer and founder of narya, building AI-native software for industrial operators.