Skip to content

jerrodtuck/CygnetServices

Repository files navigation

CygnetServices

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.

What it does

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.

Architecture

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│
                                                 └─────────────┘

How a job flows through the system

  1. An engineer defines a recurring extract through the Razor UI or WebApi
  2. ScheduleJobManager persists the job definition; SchedulerService triggers it on cadence
  3. ScadaExtractService calls CygnetAccessService, which wraps the CygNet API and pulls data from ESENT
  4. Extracted points land in SQL Server via the Persistence layer
  5. HealthLifeCheckService reports worker liveness; NotificationService raises 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 layout

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

Tech stack

  • 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

Why these choices

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.

Configuration

Committed appsettings*.json files use placeholders only (YOUR_SQL_SERVER, YOUR_OIDC_CLIENT_SECRET, and so on). For local development:

  1. Copy values into User Secrets or environment variables.
  2. Set WebApiAuthentication:JwtPublicKey, Cors:AllowedOrigins, and connection strings.
  3. Set CygnetServicesWebApiSettings:BaseUrl, Oidc:*, and Syncfusion: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).

Status

Internal Targa Resources system, no longer actively maintained by me (left Targa May 2024). 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.

About the author

Jerrod Tuck — SCADA engineer and founder of narya, building AI-native software for industrial operators.

About

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.

Resources

Stars

Watchers

Forks

Releases

No releases published

Packages

 
 
 

Contributors