🧭 EaglesNest — Developer Trace Reference

.NET 8 · ASP.NET Core
Controller
Service
Database (EF)
External (Azure / SMTP / FTP)
In-Memory Cache
Email (SMTP)
File System
🏗️

System Architecture

🌐 API Layer

  • CargowiseDataController
  • AuthController
  • UserController
  • MessageConfigController
  • StockwellController
  • ProtectedController

⚙️ Business Services

  • MessageFilterService
  • XmlRouterService
  • NotificationService
  • CargowiseSendService
  • CertificateService
  • AuthService
  • ClientRegistrationService
  • ClientAssertionService

🚀 Transfer Services

  • FtpTransferService
  • SftpTransferService
  • ApiTransferService
  • OutboundNextTransferService
  • SoapTransferService
  • LocalTransferService

🗄️ Data Layer

  • AppDbContext (EF Core)
  • SqlDataAccess (Dapper)
  • DbConnectionFactory
  • 11 DB Tables

☁️ External

  • Azure AD (OAuth2 / OIDC)
  • MSAL (token acquisition)
  • eAdapterNext.Token (token broker)
  • CargoWise eAdaptorNext endpoint
  • SMTP Server
  • FTP / SFTP Servers
  • HTTP/SOAP Endpoints
  • Stockwell (Odyssey) DB

⚡ Cross-Cutting

  • Serilog (structured logs)
  • IMemoryCache (tokens)
  • IServiceScopeFactory
  • Options pattern + validation
  • Swagger / OpenAPI
  • CORS policy
🔗

Middleware Pipeline (request order)

#MiddlewarePurposeNotes
1Serilog Request LoggingLogs every HTTP request + status + elapsed msTemplate: HTTP {Method} {Path} → {Status} in {Elapsed}ms
2CORSAdds CORS headers for configured originsPolicy: AllowConfiguredOrigins from appsettings
3Static FilesServes wwwroot/ assetsguide.html, flowchart.html, dev-trace.html
4Default FilesServes index / guide.html for bare /guide.html is the default document
5Swagger UIInteractive API explorer/swagger — Bearer + OAuth2 Implicit
6HTTPS RedirectForces HTTPS in production
7AuthenticationResolves identity from Bearer or Basic tokenDynamicScheme selects JWT or BasicAuth by header prefix
8AuthorizationEnforces [Authorize] on controllersCustom 401/403 JSON responses via JWT events
9ControllersRoutes to controller actions+ redirect /guide/guide.html
💉

Dependency Injection Registrations

ServiceLifetimeImplementationNotes
ICertificateServiceScopedCertificateServiceBouncy Castle RSA / X.509
IAuthServiceScopedAuthServiceMSAL + OIDC token validation
IClientRegistrationServiceScopedClientRegistrationServiceEF Core writes to RegisteredClients
IClientAssertionServiceScopedClientAssertionServiceJWT RS256 signing
IMessageFilterServiceScopedMessageFilterServicetblMessageConfig + tblMessageFilter
IXmlRouterServiceScopedXmlRouterServiceDispatches to transfer services
INotificationServiceScopedNotificationServiceSMTP + IMemoryCache throttle
FtpTransferServiceScopedFluentFTP library
SftpTransferServiceScopedSSH.NET library
ApiTransferServiceScopedNamed HttpClient "XmlRouter"
OutboundNextTransferServiceSingletonHolds OAuth2 token cache across requests
SoapTransferServiceScopedtext/xml or application/soap+xml
LocalTransferServiceScopedFile.Copy to UNC or local path
IStockwellReportScopedStockwellReportDapper queries against Stockwell DB
AppDbContextScopedSQL Server via EF Core
IDbConnectionFactorySingletonDbConnectionFactoryNamed connections: Main + Stockwell
ISqlDataAccessScopedSqlDataAccessDapper wrapper with logging
IMemoryCacheSingletonBuilt-inOAuth2 token cache + notification throttle
ICargowiseSendServiceScopedCargowiseSendServiceFetches token from eAdapterNext.Token, POSTs XML to CW
HttpClient "TokenApi"Named (managed)15s timeout · BaseAddress = eAdapterSettings:TokenApiBaseUrl
HttpClient "CargowiseSend"Named (managed)60s timeout, no auto-redirect · forwards XML to CargoWise
HttpClient "XmlRouter"Named (managed)60s timeout, no auto-redirect · inbound routing (ApiTransferService)
📦

CargowiseDataController

Controller
Route: api/cargowisedata  ·  Auth: DynamicScheme (Bearer or Basic)
POST

Receive

/api/cargowisedata/receive

Core inbound endpoint. Receives CargoWise XML payload, validates all headers + content, saves to disk, then fires an async background routing task. Returns 200 before routing completes.

StepActionOn Failure
1Extract eAdaptor-RecipientID header400 Bad Request
2Extract eAdaptor-EDIClientName header400 Bad Request
3Auto-generate traceparent if absent
4MessageFilterService.ValidateAsync() — check tblMessageConfig + tblMessageFilter403 Forbidden + fire NotifyUnregisteredSenderAsync if IsConfigured=false
5eAdapterValidator.ValidateContentType()400 Bad Request
6Read request body (StreamReader)500 Internal Error
7eAdapterValidator.ValidatePayloadSize()400 Bad Request
8eAdapterValidator.ValidateXml()400 Bad Request
9Create directories via isDirectoryExist()500 Internal Error
10Save XML to XMLFolder/{recipientId}/{ediClientName}/{timestamp}_...xml500 Internal Error
11filter.TrackPendingAsync(configId) — increment pending_countNon-fatal warning
12Fire Task.Run background routing (IServiceScopeFactory → new scope)
13Return 200 OK with CargowiseReceiveResponse

BACKGROUND TASK (after 200 returned)

StepAction
B1XmlRouterService.RouteAsync() — dispatches to all active routes concurrently with retry
B2For each successful delivery: filter.IncrementCountAsync(configId, routeId)
B3If any success → filter.TrackSuccessAsync(configId)
B4If all failed → filter.TrackFailureAsync(configId)
B5Unhandled exception → filter.TrackFailureAsync(configId)
CargowiseDataController MessageFilterService tblMessageConfig · tblMessageFilter → (bg) XmlRouterService FTP / SFTP / HTTP / SOAP NotificationService SMTP XMLFolder
POST

Send

/api/cargowisedata/send

Outbound endpoint. Caller authenticates with Basic Auth; the username is used as the partitionKey. The API derives SenderID from username[..6], fetches a Bearer token from eAdapterNext.Token, then POSTs the XML to CargoWise. Response is synchronous — includes CW status code and response body. An admin alert email is sent (throttled 1/hour per username) on any failure: missing Token config, CargoWise non-2xx, network error, or unexpected exception.

StepActionOn Failure
1Resolve username = User.Identity.Name (Basic Auth); validate username.Length ≥ 6401 Unauthorized / 400 Bad Request
2Read raw XML body from Request.Body (StreamReader); validate non-empty500 Internal Error / 400 Bad Request
3Derive senderId = username[..6]; build TargetUrl = https://{senderId}services.wisegrid.net/eAdaptorNext
4CargowiseSendService.SendAsync() — verify TokenApiBaseUrl is configured502 InvalidOperationException
5POST {TokenApiBaseUrl}/api/token with { partitionKey: username } — if Token API returns "not found": TokenConfigNotFoundExceptionNotifyTokenConfigMissingAsync502 + admin alert email
6HTTP POST XML to CargoWise with Authorization: Bearer {token}, Content-Type: application/xml — on network error: NotifySendFailureAsync502 + admin alert email
7Evaluate CW HTTP status: 2xx → success=true; non-2xx → success=false + NotifySendFailureAsync (fire-and-forget)500 on unexpected exception + admin alert
CargowiseDataController CargowiseSendService HttpClient "TokenApi" → eAdapterNext.Token /api/token HttpClient "CargowiseSend" → CargoWise endpoint NotificationService (fire-and-forget on any failure)
🔐

AuthController

Controller
Route: api/auth  ·  Auth: mixed (AllowAnonymous / Bearer)
POST

GenerateKeyPair

/api/auth/generate-keypair

Generates an RSA private key + CSR. Client uses CSR for registration and keeps the private key locally.

AuthController CertificateService.GenerateKeyPairAsync() Bouncy Castle (RSA in-memory)
POST

RegisterClient

/api/auth/register

Registers a new client. Signs the supplied CSR (or auto-generates a key pair if none provided) and stores a RegisteredClient + ClientCertificate row in the DB.

StepAction
1If no CSR: CertificateService.GenerateKeyPairAsync() → auto-create RSA pair
2CertificateService.ValidateAndSignCsrAsync() → verify CSR sig, sign with CA key
3Check thumbprint uniqueness in RegisteredClients
4Insert RegisteredClient + ClientCertificate rows
5Return cert PEM + thumbprint + Azure token endpoint instructions
AuthController ClientRegistrationService CertificateService RegisteredClients · ClientCertificates
POST

GenerateClientAssertion

/api/auth/generate-assertion

Creates a signed JWT assertion (RS256) from a certificate + private key. Used as the credential when requesting a token from Azure AD.

AuthController ClientAssertionService.GenerateAssertionAsync() RegisteredClients (thumbprint lookup) RS256 JWT signed in-memory
POST

GetToken

/api/auth/token

Exchanges a signed client assertion for a Microsoft Entra ID Bearer token via MSAL client-credentials flow.

StepAction
1Parse cert PEM → compute SHA-256 thumbprint
2Verify thumbprint in RegisteredClients + IsActive check
3MSAL ConfidentialClientApplication with assertion → call Azure AD token endpoint
4Update LastAccessDate on RegisteredClient
5Return AccessToken + ExpiresIn + Scope
AuthController AuthService.GetTokenWithCertificateAsync() RegisteredClients Azure AD (MSAL)
POST

ValidateToken / RevokeClient

/api/auth/validate-token  ·  /api/auth/revoke/{clientId}

ValidateToken: Validates a JWT against Azure AD OIDC metadata (issuer, audience, lifetime, signing keys).
RevokeClient: Sets IsActive=false on RegisteredClient + IsRevoked=true on all its certificates.

AuthController AuthService.ValidateTokenAsync() Azure AD OIDC metadata
AuthController ClientRegistrationService.RevokeClientAsync() RegisteredClients · ClientCertificates
👤

UserController

Controller
Route: api/user  ·  Data: tblUsers (EF Core)
MethodHTTP + RouteActionDB Op
GetAllGET /Returns all active users (Status=true)SELECT WHERE Status=true
GetByIdGET /{id}Single user by GUID — 404 if missingSELECT WHERE UserId=id
CreatePOST /New user — generates random salt + SHA256 hash of passwordINSERT tblUsers
UpdatePUT /{id}Update username / password / status. Username uniqueness checkedUPDATE tblUsers
DeactivateDELETE /{id}Soft delete — sets Status=falseUPDATE Status=false
LoginPOST /loginVerify SHA256 hash → generate 64-byte base64 session token + refresh token → save both to rowSELECT + UPDATE tblUsers
RefreshPOST /refreshValidate refresh token → generate new session + refresh tokensSELECT + UPDATE tblUsers
LogoutPOST /logoutClear Token + RefreshToken columnsUPDATE tblUsers
📋

MessageConfigController

Controller
Route: api/messageconfig  ·  Manages sender registrations, routes, and routing toggles
MethodHTTP + RouteActionTables
GetAllGET /Summary list — config + route count + total message_counttblMessageConfig · tblRoutes · tblMessageCount
GetByIdGET /{id}Full detail — routes + per-route counts + filter stateAll 4 tables
CreatePOST /Creates config (unique EdiClientName + RecipientId) + optional inline routestblMessageConfig · tblRoutes
UpdatePUT /{id}Update status / procedureIdtblMessageConfig
DeleteDELETE /{id}Hard delete config + cascades to routes/counts/filterAll tables
AddRoutePOST /{id}/routesAdds a delivery route to a configtblRoutes
UpdateRoutePUT /{id}/routes/{routeId}Updates route propertiestblRoutes
DeleteRouteDELETE /{id}/routes/{routeId}Removes a routetblRoutes
SetFilterPOST /{id}/filterCreate / toggle tblMessageFilter routing switchtblMessageFilter
📊

StockwellController

Controller
Route: api/stockwell  ·  Reads Odyssey (Stockwell) financial database via Dapper
MethodRouteActionDB
getInvoiceRecordGET /stockwell/APAP/AR invoices filtered by ledger, debtor, dates, currency, company, outstanding flagStockwell DB (Dapper raw SQL)
getAccountMovementRecordGET /stockwell/trialbalanceGL account movements by period, account, branch, departmentStockwell DB (Dapper raw SQL)
StockwellController IStockwellReport (StockwellReport) ISqlDataAccess (Dapper) Stockwell / Odyssey SQL Server
🛡️

ProtectedController

Controller
Route: api/protected  ·  Auth: Bearer token required. Diagnostic / health endpoints.
MethodRouteAction
GetProtectedDataGET /dataReturns all JWT claims from the token (appid, azp, sub, etc.) — useful for debugging auth
HealthGET /healthReturns { Status, Timestamp, Authenticated }
🔍

MessageFilterService

Service

Gate-keeper for all inbound messages. Also owns the lifecycle counters in tblMessageCount.

SVC

ValidateAsync(ediClientName, recipientId)

StepCheckResult if fail
1Find tblMessageConfig WHERE EdiClientName + RecipientIdIsConfigured=false → 403 + UnregisteredSender email
2Check config.Status = trueIsActive=false → 403
3Find tblMessageFilter WHERE MessageConfigIdIf found + Status=false → IsRoutingEnabled=false (save only, no routing)

Returns: MessageFilterResult { IsConfigured, IsActive, IsRoutingEnabled, ShouldAccept, ShouldRoute, Config, RejectionReason }

MessageFilterService tblMessageConfig (EF SELECT) tblMessageFilter (EF SELECT)
SVC

IncrementCountAsync(messageConfigId, routeId)

Increments per-route delivery counter. Finds or creates a row in tblMessageCount keyed by (messageConfigId, routeId). Non-fatal on error.

MessageFilterService tblMessageCount (EF FindAsync → INSERT or UPDATE)
SVC

TrackPendingAsync / TrackSuccessAsync / TrackFailureAsync

Lifecycle counters on the summary row (routes_id = 0). All use atomic MERGE … WITH (HOLDLOCK) to prevent race conditions under concurrent load.

MethodEffect on Summary Row
TrackPendingAsyncpending_count + 1
TrackSuccessAsyncsuccess_count + 1, pending_count - 1 (floor 0)
TrackFailureAsyncfail_count + 1, pending_count - 1 (floor 0)
MessageFilterService tblMessageCount routes_id=0 (raw SQL MERGE)
🚀

XmlRouterService

Service

Dispatches an XML file to all active routes concurrently with retry. Runs entirely inside a background task scope created by IServiceScopeFactory.

SVC

RouteAsync(localFilePath, fileName, recipientId, ediClientName)

StepAction
1Look up TblMessageConfig for (ediClientName, recipientId) → get msgConfigId
2Load all active TblRoutes WHERE MessageConfigId + Status=true
3If no routes → log warning, return empty result
4Task.WhenAll — dispatch each route concurrently
5Per route — retry loop (max 3 attempts):
 • NotSupportedException / InvalidOperationException → break immediately (config error)
 • Other exception → Task.Delay(attempt × 5s) then retry
 • Attempt 1: immediate · Attempt 2: wait 5s · Attempt 3: wait 10s
6Route type dispatch: FTP → SFTP → HTTP/HTTPS → OUTBOUNDNEXT → SOAP/ENVELOP → LOCAL
7Collect RouteDelivery results (success/error/elapsed)
8If any failures: RecordFailedMessagesAsync() → batch INSERT to tblFailedMessage
9If ALL routes failed: CopyToFailedFolderAsync() → copy file to FailedFolder/{recipientId}/{ediClientName}/{fileName}
10If any failures: NotificationService.NotifyRoutingFailureAsync()
XmlRouterService tblMessageConfig · tblRoutes (EF) FTP / SFTP / HTTP / SOAP / LOCAL tblFailedMessage (INSERT) FailedFolder (File.Copy) NotificationService
📧

NotificationService

Service

Sends plain-English HTML alert emails via SMTP. Throttles repeat notifications to prevent inbox flooding.

SVC

NotifyRoutingFailureAsync(msgConfigId, recipientId, EDIClientName, fileName, failedDeliveries)

Called after XmlRouterService when one or more routes fail all retries. Throttled via tblNotificationTracker.

StepAction
1Query tblNotificationTracker WHERE MsgConfigId (latest row)
2aRow found → evaluate OccurrenceType window: Hourly +N hrs / Daily +N days / Minutes +N mins
2bNo row → insert tracker row with OccurrenceType=Hourly, OccurrenceNo=1, send immediately
3If allowed: BuildEmailBody()SendEmail() via SMTP
4Email includes: file name, EDI client name, recipient ID, failed routes table with plain-English errors
NotificationService tblNotificationTracker (EF SELECT + INSERT/UPDATE) SMTP (System.Net.Mail)
SVC

NotifyUnregisteredSenderAsync(ediClientName, recipientId, systemMessage)

Called by CargowiseDataController when filterResult.IsConfigured = false. Fires before returning 403. Throttled via IMemoryCache (1 hour TTL — no DB row needed since there is no msgConfigId).

StepAction
1Check IMemoryCache for key unregistered:{recipientId}:{ediClientName}
2If cached → suppress (log only)
3If not cached → set cache entry (TTL 1hr) → BuildUnregisteredSenderEmailBody()SendEmail()
4Email includes: EDI Client Name, Recipient ID, timestamp, system message (exact rejection reason from filter)
NotificationService IMemoryCache (throttle check) SMTP (System.Net.Mail)
📤

CargowiseSendService

Service

Handles outbound XML delivery to CargoWise. Acquires a Bearer token from eAdapterNext.Token, then POSTs the XML payload to the resolved CargoWise endpoint. Fully synchronous — caller receives the CargoWise HTTP status and response body.

SVC

SendAsync(request, ct)

StepActionThrows on failure
1Validate eAdapterSettings:TokenApiBaseUrl is setInvalidOperationException
2GetTokenAsync(partitionKey) — POST to /api/token via named HttpClient "TokenApi"InvalidOperationException (network or API error)
3Parse JSON response: data.accessToken + data.tokenSource (Cached / New)InvalidOperationException (missing field)
4Build HttpRequestMessage POST {TargetUrl} with XML body + Authorization: Bearer header via "CargowiseSend" clientRethrows HttpRequestException
5Return CargowiseSendResponse { TraceParent, SentAt, CwStatusCode, CwResponse, TokenSource, TargetUrl }

TOKEN API REQUEST (step 2)

DetailValue
Named clientHttpClient "TokenApi" — BaseAddress = eAdapterSettings:TokenApiBaseUrl, 15s timeout
RoutePOST /api/token
Body{ "partitionKey": "{partitionKey}" } (JSON)
Expected response{ success: true, data: { accessToken, tokenSource, expiresAtUtc } }
Cached pathToken API returns stored AccessToken if still valid (> 5 min remaining). tokenSource = "Cached"
New pathToken API calls Azure AD OAuth2 client_credentials, caches result in Azure Table. tokenSource = "New"
CargowiseSendService HttpClient "TokenApi" → eAdapterNext.Token Azure Table Storage (token cache) Azure AD (OAuth2 — if cache miss) HttpClient "CargowiseSend" → CargoWise endpoint
🔑

CertificateService

Service
MethodActionLibrary
GenerateKeyPairAsync(request)Creates RSA key pair (2048/4096-bit) + CSR with CN/O/C subject DNBouncy Castle
ValidateAndSignCsrAsync(csrPem)Verifies CSR self-signature → creates X509v3 cert with BasicConstraints + KeyUsage → signs with CA keyBouncy Castle
GetCertificateThumbprint(certPem)SHA-256 hex thumbprint (uppercase)Bouncy Castle
ExportAsPemAsync / DerAsync / PfxAsyncExport signed cert in PEM / DER / PFX formatBouncy Castle
CertificateService Bouncy Castle (in-process) ca-key.pem (loaded from disk on startup)
🪙

AuthService

Service
MethodActionDependency
GetTokenWithCertificateAsync(certPem, assertion)MSAL ConfidentialClient → client_credentials → Azure AD token endpoint. Updates LastAccessDate.MSAL · RegisteredClients (EF)
ValidateTokenAsync(token)Fetch OIDC metadata → validate JWT (issuer, audience, lifetime, signing keys)Azure AD OIDC discovery
📝

ClientRegistrationService

Service
MethodActionDB
RegisterClientAsync(request)Auto-generates key pair if no CSR, signs CSR, dedup by thumbprint, inserts RegisteredClient + ClientCertificateINSERT RegisteredClients · ClientCertificates
IsClientRegisteredAsync(thumbprint)Checks if thumbprint exists + IsActive=trueSELECT RegisteredClients
RevokeClientAsync(clientId)Sets IsActive=false + cascades IsRevoked=true to all certificatesUPDATE RegisteredClients · ClientCertificates
🖊️

ClientAssertionService

Service

GenerateAssertionAsync(certPem, privateKeyPem) — Parses the cert, looks up the matching registered client by thumbprint, signs a JWT with alg=RS256, kid={thumbprint}. Claims: iss/sub=clientId, aud=Azure token endpoint, jti=random GUID, exp=+10 min. Returns signed JWT string.

ClientAssertionService RegisteredClients (thumbprint lookup) RS256 JWT signed in-memory
📁

FtpTransferService / SftpTransferService

Transfer
ServiceLibraryProtocolAuthPath
FtpTransferServiceFluentFTPFTPS (explicit TLS) · Port 21Username + Password{uri}/{recipientId}/{extendedPath}/{fileName}
SftpTransferServiceSSH.NETSFTP over SSH · Port 22Username + Password{uri}/{recipientId}/{extendedPath}/{fileName}
🌐

ApiTransferService (HTTP / HTTPS)

Transfer

HTTP POST of the raw XML body to a REST endpoint. Uses named HttpClient "XmlRouter" (60s timeout, no auto-redirect).

Auth typeHeaders sent
Basic Auth (username + password set)Authorization: Basic {base64(user:pass)}
Bearer token (static token set)Authorization: Bearer {token}
NoneNo auth header
🔑

OutboundNextTransferService (OUTBOUNDNEXT)

Transfer

Like HTTP/HTTPS but uses OAuth2 client-credentials. Token is cached in IMemoryCache (Singleton) and reused until 60 seconds before expiry.

StepAction
1Check IMemoryCache for cached token keyed by {clientId}:{tenantId}:{scope}
2If missing / expiring → POST to AuthorizationUrl with client_credentials → cache result
3HTTP POST XML to route URI with Authorization: Bearer {token}
OutboundNextTransferService IMemoryCache (token cache) OAuth2 Token Endpoint Destination HTTPS API
🧼

SoapTransferService (SOAP / ENVELOP)

Transfer

HTTP POST with SOAPAction header. SOAP uses text/xml; ENVELOP uses application/soap+xml.

ConditionAuth Header
Password set, no tokenAuthorization: Basic {base64(user:pass)}
Password + token both setAuthorization: Bearer {token}
No password, token setAuthorization: Bearer {token}
SOAPAction only (no auth)SOAPAction header only
Nothing setNo auth
🗂️

LocalTransferService (LOCAL)

Transfer

Copies the XML file to a local folder or UNC network path. Creates destination sub-directories if they don't exist. No authentication needed — access is controlled by OS permissions on the path.

LocalTransferService File.Copy({src}, {uri}/{extendedPath}/{fileName})
🗄️

EF Core — AppDbContext

Data

All writes via EF Core. Raw SQL only for atomic MERGE … WITH (HOLDLOCK) counter updates in MessageFilterService.

DbSetTableUsed by
RegisteredClientsRegisteredClientsAuthService · ClientRegistrationService · ClientAssertionService
ClientCertificatesClientCertificatesClientRegistrationService
TblUserstblUsersUserController (direct)
TblMessageConfigstblMessageConfigMessageFilterService · XmlRouterService · MessageConfigController
TblRoutestblRoutesXmlRouterService · MessageConfigController
TblMessageFilterstblMessageFilterMessageFilterService · MessageConfigController
TblMessageCountstblMessageCountMessageFilterService (MERGE) · MessageConfigController (read)
TblFailedMessagestblFailedMessageXmlRouterService (INSERT on all-retry-fail)
TblNotificationTrackerstblNotificationTrackerNotificationService (routing failures throttle)
TblLogstblLogs— (reserved)
TblProcedurestblProcedures— (reserved)

Dapper — SqlDataAccess

Data

Used exclusively by StockwellReport to query the Stockwell (Odyssey) financial database. Connection selected by DbKeys.Stockwell.

MethodAction
QueryAsync<T>(sql, param, db)Returns IEnumerable<T> from Dapper query
QueryFirstOrDefaultAsync<T>Single row or null
ExecuteAsyncNon-query (INSERT/UPDATE)
📐

Entity Quick Reference

Data
EntityPKKey ColumnsSpecial
RegisteredClientGuid IdClientName, AzureClientId, Thumbprint (unique), IsActive, LastAccessDateNav: Certificates[]
ClientCertificateGuid IdClientId (FK), Thumbprint (unique), NotBefore, NotAfter, IsRevoked
TblUserGuid UserIdUsername, Password (SHA256), Salt, Token, RefreshToken, StatusSoft-delete via Status
TblMessageConfigGuid IdEdiClientName, RecipientId, ProcedureId, StatusUnique (EdiClientName, RecipientId)
TblRouteint Id (IDENTITY)MessageConfigId (FK), Type, Uri, ExtendedPath, Username, Password, Token, SoapAction, ClientId, TenantId, Scope, AuthorizationUrl, Status
TblMessageFilterGuid IdMessageConfigId (FK), Status (routing on/off)
TblMessageCount(MessageConfigId, RoutesId)MessageCount (per-route), PendingCount / SuccessCount / FailCount (routes_id=0 summary)Composite PK
TblFailedMessageint Id (IDENTITY)MsgConfigId, FileName, FailedPath, RouteType, ErrorDetail, DatestampInserted after all retries fail
TblNotificationTrackerint Id (IDENTITY)MsgConfigId, OccurrenceType (Hourly/Daily/Minutes), OccurrenceNo, LastNotificationSendThrottle window for delivery-fail emails
🔧

Helpers & Utilities

HelperMethodPurpose
eAdapterValidatorValidateContentType(ct, allowed)Strips charset, checks against allowlist. Returns (bool, string)
ValidatePayloadSize(bytes, max)Reports size in MB on failure
ValidateXml(xml, allowedRoots)Parses XML, checks root element if allowlist is non-empty. Returns (bool, string, XDocument?)
ValidateFilePath / DirectoryPathChecks for invalid characters in paths
CryptEncrypt / Decrypt(text, salt)Rijndael CBC with PKCS7 padding
ComputeSha256Hash(raw)SHA256 hex (used for password hashing)
GetSalt(maxLen)Random 32-byte base64 string
Base64ToString(input)Decode base64
CreateMD5 / DecryptMD5TripleDES-based encryption (legacy)
DirectoryExtensionsisDirectoryExist(path)Extension method — ensures dir exists (creates if missing), returns path
StockwellExtensionsToSafePathSegment(value)Strips unsafe path characters from header values before use in file paths
ToSafeBoolean(value)Parse bool from string safely (default false)
CheckAPIAuth(headerValues)Async Bearer token validation extension