External (Azure / SMTP / FTP)
🌐 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 | Purpose | Notes |
| 1 | Serilog Request Logging | Logs every HTTP request + status + elapsed ms | Template: HTTP {Method} {Path} → {Status} in {Elapsed}ms |
| 2 | CORS | Adds CORS headers for configured origins | Policy: AllowConfiguredOrigins from appsettings |
| 3 | Static Files | Serves wwwroot/ assets | guide.html, flowchart.html, dev-trace.html |
| 4 | Default Files | Serves index / guide.html for bare / | guide.html is the default document |
| 5 | Swagger UI | Interactive API explorer | /swagger — Bearer + OAuth2 Implicit |
| 6 | HTTPS Redirect | Forces HTTPS in production | |
| 7 | Authentication | Resolves identity from Bearer or Basic token | DynamicScheme selects JWT or BasicAuth by header prefix |
| 8 | Authorization | Enforces [Authorize] on controllers | Custom 401/403 JSON responses via JWT events |
| 9 | Controllers | Routes to controller actions | + redirect /guide → /guide.html |
| Service | Lifetime | Implementation | Notes |
ICertificateService | Scoped | CertificateService | Bouncy Castle RSA / X.509 |
IAuthService | Scoped | AuthService | MSAL + OIDC token validation |
IClientRegistrationService | Scoped | ClientRegistrationService | EF Core writes to RegisteredClients |
IClientAssertionService | Scoped | ClientAssertionService | JWT RS256 signing |
IMessageFilterService | Scoped | MessageFilterService | tblMessageConfig + tblMessageFilter |
IXmlRouterService | Scoped | XmlRouterService | Dispatches to transfer services |
INotificationService | Scoped | NotificationService | SMTP + IMemoryCache throttle |
FtpTransferService | Scoped | — | FluentFTP library |
SftpTransferService | Scoped | — | SSH.NET library |
ApiTransferService | Scoped | — | Named HttpClient "XmlRouter" |
OutboundNextTransferService | Singleton | — | Holds OAuth2 token cache across requests |
SoapTransferService | Scoped | — | text/xml or application/soap+xml |
LocalTransferService | Scoped | — | File.Copy to UNC or local path |
IStockwellReport | Scoped | StockwellReport | Dapper queries against Stockwell DB |
AppDbContext | Scoped | — | SQL Server via EF Core |
IDbConnectionFactory | Singleton | DbConnectionFactory | Named connections: Main + Stockwell |
ISqlDataAccess | Scoped | SqlDataAccess | Dapper wrapper with logging |
IMemoryCache | Singleton | Built-in | OAuth2 token cache + notification throttle |
ICargowiseSendService | Scoped | CargowiseSendService | Fetches 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) |
Route: api/cargowisedata · Auth: DynamicScheme (Bearer or Basic)
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.
| Step | Action | On Failure |
| 1 | Extract eAdaptor-RecipientID header | 400 Bad Request |
| 2 | Extract eAdaptor-EDIClientName header | 400 Bad Request |
| 3 | Auto-generate traceparent if absent | — |
| 4 | MessageFilterService.ValidateAsync() — check tblMessageConfig + tblMessageFilter | 403 Forbidden + fire NotifyUnregisteredSenderAsync if IsConfigured=false |
| 5 | eAdapterValidator.ValidateContentType() | 400 Bad Request |
| 6 | Read request body (StreamReader) | 500 Internal Error |
| 7 | eAdapterValidator.ValidatePayloadSize() | 400 Bad Request |
| 8 | eAdapterValidator.ValidateXml() | 400 Bad Request |
| 9 | Create directories via isDirectoryExist() | 500 Internal Error |
| 10 | Save XML to XMLFolder/{recipientId}/{ediClientName}/{timestamp}_...xml | 500 Internal Error |
| 11 | filter.TrackPendingAsync(configId) — increment pending_count | Non-fatal warning |
| 12 | Fire Task.Run background routing (IServiceScopeFactory → new scope) | — |
| 13 | Return 200 OK with CargowiseReceiveResponse | — |
BACKGROUND TASK (after 200 returned)
| Step | Action |
| B1 | XmlRouterService.RouteAsync() — dispatches to all active routes concurrently with retry |
| B2 | For each successful delivery: filter.IncrementCountAsync(configId, routeId) |
| B3 | If any success → filter.TrackSuccessAsync(configId) |
| B4 | If all failed → filter.TrackFailureAsync(configId) |
| B5 | Unhandled exception → filter.TrackFailureAsync(configId) |
CargowiseDataController
→
MessageFilterService
→
tblMessageConfig
·
tblMessageFilter
→ (bg)
XmlRouterService
→
FTP / SFTP / HTTP / SOAP
→
NotificationService
→
SMTP
→
XMLFolder
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.
| Step | Action | On Failure |
| 1 | Resolve username = User.Identity.Name (Basic Auth); validate username.Length ≥ 6 | 401 Unauthorized / 400 Bad Request |
| 2 | Read raw XML body from Request.Body (StreamReader); validate non-empty | 500 Internal Error / 400 Bad Request |
| 3 | Derive senderId = username[..6]; build TargetUrl = https://{senderId}services.wisegrid.net/eAdaptorNext | — |
| 4 | CargowiseSendService.SendAsync() — verify TokenApiBaseUrl is configured | 502 InvalidOperationException |
| 5 | POST {TokenApiBaseUrl}/api/token with { partitionKey: username } — if Token API returns "not found": TokenConfigNotFoundException → NotifyTokenConfigMissingAsync | 502 + admin alert email |
| 6 | HTTP POST XML to CargoWise with Authorization: Bearer {token}, Content-Type: application/xml — on network error: NotifySendFailureAsync | 502 + admin alert email |
| 7 | Evaluate 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)
Route: api/auth · Auth: mixed (AllowAnonymous / Bearer)
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)
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.
| Step | Action |
| 1 | If no CSR: CertificateService.GenerateKeyPairAsync() → auto-create RSA pair |
| 2 | CertificateService.ValidateAndSignCsrAsync() → verify CSR sig, sign with CA key |
| 3 | Check thumbprint uniqueness in RegisteredClients |
| 4 | Insert RegisteredClient + ClientCertificate rows |
| 5 | Return cert PEM + thumbprint + Azure token endpoint instructions |
AuthController
→
ClientRegistrationService
→
CertificateService
→
RegisteredClients
·
ClientCertificates
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
Exchanges a signed client assertion for a Microsoft Entra ID Bearer token via MSAL client-credentials flow.
| Step | Action |
| 1 | Parse cert PEM → compute SHA-256 thumbprint |
| 2 | Verify thumbprint in RegisteredClients + IsActive check |
| 3 | MSAL ConfidentialClientApplication with assertion → call Azure AD token endpoint |
| 4 | Update LastAccessDate on RegisteredClient |
| 5 | Return AccessToken + ExpiresIn + Scope |
AuthController
→
AuthService.GetTokenWithCertificateAsync()
→
RegisteredClients
→
Azure AD (MSAL)
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
Route: api/user · Data: tblUsers (EF Core)
| Method | HTTP + Route | Action | DB Op |
GetAll | GET / | Returns all active users (Status=true) | SELECT WHERE Status=true |
GetById | GET /{id} | Single user by GUID — 404 if missing | SELECT WHERE UserId=id |
Create | POST / | New user — generates random salt + SHA256 hash of password | INSERT tblUsers |
Update | PUT /{id} | Update username / password / status. Username uniqueness checked | UPDATE tblUsers |
Deactivate | DELETE /{id} | Soft delete — sets Status=false | UPDATE Status=false |
Login | POST /login | Verify SHA256 hash → generate 64-byte base64 session token + refresh token → save both to row | SELECT + UPDATE tblUsers |
Refresh | POST /refresh | Validate refresh token → generate new session + refresh tokens | SELECT + UPDATE tblUsers |
Logout | POST /logout | Clear Token + RefreshToken columns | UPDATE tblUsers |
Route: api/messageconfig · Manages sender registrations, routes, and routing toggles
| Method | HTTP + Route | Action | Tables |
GetAll | GET / | Summary list — config + route count + total message_count | tblMessageConfig · tblRoutes · tblMessageCount |
GetById | GET /{id} | Full detail — routes + per-route counts + filter state | All 4 tables |
Create | POST / | Creates config (unique EdiClientName + RecipientId) + optional inline routes | tblMessageConfig · tblRoutes |
Update | PUT /{id} | Update status / procedureId | tblMessageConfig |
Delete | DELETE /{id} | Hard delete config + cascades to routes/counts/filter | All tables |
AddRoute | POST /{id}/routes | Adds a delivery route to a config | tblRoutes |
UpdateRoute | PUT /{id}/routes/{routeId} | Updates route properties | tblRoutes |
DeleteRoute | DELETE /{id}/routes/{routeId} | Removes a route | tblRoutes |
SetFilter | POST /{id}/filter | Create / toggle tblMessageFilter routing switch | tblMessageFilter |
Route: api/stockwell · Reads Odyssey (Stockwell) financial database via Dapper
| Method | Route | Action | DB |
getInvoiceRecord | GET /stockwell/AP | AP/AR invoices filtered by ledger, debtor, dates, currency, company, outstanding flag | Stockwell DB (Dapper raw SQL) |
getAccountMovementRecord | GET /stockwell/trialbalance | GL account movements by period, account, branch, department | Stockwell DB (Dapper raw SQL) |
StockwellController
→
IStockwellReport (StockwellReport)
→
ISqlDataAccess (Dapper)
→
Stockwell / Odyssey SQL Server
Route: api/protected · Auth: Bearer token required. Diagnostic / health endpoints.
| Method | Route | Action |
GetProtectedData | GET /data | Returns all JWT claims from the token (appid, azp, sub, etc.) — useful for debugging auth |
Health | GET /health | Returns { Status, Timestamp, Authenticated } |
Gate-keeper for all inbound messages. Also owns the lifecycle counters in tblMessageCount.
| Step | Check | Result if fail |
| 1 | Find tblMessageConfig WHERE EdiClientName + RecipientId | IsConfigured=false → 403 + UnregisteredSender email |
| 2 | Check config.Status = true | IsActive=false → 403 |
| 3 | Find tblMessageFilter WHERE MessageConfigId | If 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)
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)
Lifecycle counters on the summary row (routes_id = 0). All use atomic MERGE … WITH (HOLDLOCK) to prevent race conditions under concurrent load.
| Method | Effect on Summary Row |
TrackPendingAsync | pending_count + 1 |
TrackSuccessAsync | success_count + 1, pending_count - 1 (floor 0) |
TrackFailureAsync | fail_count + 1, pending_count - 1 (floor 0) |
MessageFilterService
→
tblMessageCount routes_id=0 (raw SQL MERGE)
Dispatches an XML file to all active routes concurrently with retry. Runs entirely inside a background task scope created by IServiceScopeFactory.
| Step | Action |
| 1 | Look up TblMessageConfig for (ediClientName, recipientId) → get msgConfigId |
| 2 | Load all active TblRoutes WHERE MessageConfigId + Status=true |
| 3 | If no routes → log warning, return empty result |
| 4 | Task.WhenAll — dispatch each route concurrently |
| 5 | Per 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
|
| 6 | Route type dispatch: FTP → SFTP → HTTP/HTTPS → OUTBOUNDNEXT → SOAP/ENVELOP → LOCAL |
| 7 | Collect RouteDelivery results (success/error/elapsed) |
| 8 | If any failures: RecordFailedMessagesAsync() → batch INSERT to tblFailedMessage |
| 9 | If ALL routes failed: CopyToFailedFolderAsync() → copy file to FailedFolder/{recipientId}/{ediClientName}/{fileName} |
| 10 | If any failures: NotificationService.NotifyRoutingFailureAsync() |
XmlRouterService
→
tblMessageConfig · tblRoutes (EF)
→
FTP / SFTP / HTTP / SOAP / LOCAL
→
tblFailedMessage (INSERT)
→
FailedFolder (File.Copy)
→
NotificationService
Sends plain-English HTML alert emails via SMTP. Throttles repeat notifications to prevent inbox flooding.
Called after XmlRouterService when one or more routes fail all retries. Throttled via tblNotificationTracker.
| Step | Action |
| 1 | Query tblNotificationTracker WHERE MsgConfigId (latest row) |
| 2a | Row found → evaluate OccurrenceType window: Hourly +N hrs / Daily +N days / Minutes +N mins |
| 2b | No row → insert tracker row with OccurrenceType=Hourly, OccurrenceNo=1, send immediately |
| 3 | If allowed: BuildEmailBody() → SendEmail() via SMTP |
| 4 | Email 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)
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).
| Step | Action |
| 1 | Check IMemoryCache for key unregistered:{recipientId}:{ediClientName} |
| 2 | If cached → suppress (log only) |
| 3 | If not cached → set cache entry (TTL 1hr) → BuildUnregisteredSenderEmailBody() → SendEmail() |
| 4 | Email includes: EDI Client Name, Recipient ID, timestamp, system message (exact rejection reason from filter) |
NotificationService
→
IMemoryCache (throttle check)
→
SMTP (System.Net.Mail)
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.
| Step | Action | Throws on failure |
| 1 | Validate eAdapterSettings:TokenApiBaseUrl is set | InvalidOperationException |
| 2 | GetTokenAsync(partitionKey) — POST to /api/token via named HttpClient "TokenApi" | InvalidOperationException (network or API error) |
| 3 | Parse JSON response: data.accessToken + data.tokenSource (Cached / New) | InvalidOperationException (missing field) |
| 4 | Build HttpRequestMessage POST {TargetUrl} with XML body + Authorization: Bearer header via "CargowiseSend" client | Rethrows HttpRequestException |
| 5 | Return CargowiseSendResponse { TraceParent, SentAt, CwStatusCode, CwResponse, TokenSource, TargetUrl } | — |
TOKEN API REQUEST (step 2)
| Detail | Value |
| Named client | HttpClient "TokenApi" — BaseAddress = eAdapterSettings:TokenApiBaseUrl, 15s timeout |
| Route | POST /api/token |
| Body | { "partitionKey": "{partitionKey}" } (JSON) |
| Expected response | { success: true, data: { accessToken, tokenSource, expiresAtUtc } } |
| Cached path | Token API returns stored AccessToken if still valid (> 5 min remaining). tokenSource = "Cached" |
| New path | Token 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
| Method | Action | Library |
GenerateKeyPairAsync(request) | Creates RSA key pair (2048/4096-bit) + CSR with CN/O/C subject DN | Bouncy Castle |
ValidateAndSignCsrAsync(csrPem) | Verifies CSR self-signature → creates X509v3 cert with BasicConstraints + KeyUsage → signs with CA key | Bouncy Castle |
GetCertificateThumbprint(certPem) | SHA-256 hex thumbprint (uppercase) | Bouncy Castle |
ExportAsPemAsync / DerAsync / PfxAsync | Export signed cert in PEM / DER / PFX format | Bouncy Castle |
CertificateService
→
Bouncy Castle (in-process)
→
ca-key.pem (loaded from disk on startup)
| Method | Action | Dependency |
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 |
| Method | Action | DB |
RegisterClientAsync(request) | Auto-generates key pair if no CSR, signs CSR, dedup by thumbprint, inserts RegisteredClient + ClientCertificate | INSERT RegisteredClients · ClientCertificates |
IsClientRegisteredAsync(thumbprint) | Checks if thumbprint exists + IsActive=true | SELECT RegisteredClients |
RevokeClientAsync(clientId) | Sets IsActive=false + cascades IsRevoked=true to all certificates | UPDATE RegisteredClients · ClientCertificates |
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
| Service | Library | Protocol | Auth | Path |
FtpTransferService | FluentFTP | FTPS (explicit TLS) · Port 21 | Username + Password | {uri}/{recipientId}/{extendedPath}/{fileName} |
SftpTransferService | SSH.NET | SFTP over SSH · Port 22 | Username + Password | {uri}/{recipientId}/{extendedPath}/{fileName} |
HTTP POST of the raw XML body to a REST endpoint. Uses named HttpClient "XmlRouter" (60s timeout, no auto-redirect).
| Auth type | Headers sent |
| Basic Auth (username + password set) | Authorization: Basic {base64(user:pass)} |
| Bearer token (static token set) | Authorization: Bearer {token} |
| None | No auth header |
Like HTTP/HTTPS but uses OAuth2 client-credentials. Token is cached in IMemoryCache (Singleton) and reused until 60 seconds before expiry.
| Step | Action |
| 1 | Check IMemoryCache for cached token keyed by {clientId}:{tenantId}:{scope} |
| 2 | If missing / expiring → POST to AuthorizationUrl with client_credentials → cache result |
| 3 | HTTP POST XML to route URI with Authorization: Bearer {token} |
OutboundNextTransferService
→
IMemoryCache (token cache)
→
OAuth2 Token Endpoint
→
Destination HTTPS API
HTTP POST with SOAPAction header. SOAP uses text/xml; ENVELOP uses application/soap+xml.
| Condition | Auth Header |
| Password set, no token | Authorization: Basic {base64(user:pass)} |
| Password + token both set | Authorization: Bearer {token} |
| No password, token set | Authorization: Bearer {token} |
| SOAPAction only (no auth) | SOAPAction header only |
| Nothing set | No auth |
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})
All writes via EF Core. Raw SQL only for atomic MERGE … WITH (HOLDLOCK) counter updates in MessageFilterService.
| DbSet | Table | Used by |
RegisteredClients | RegisteredClients | AuthService · ClientRegistrationService · ClientAssertionService |
ClientCertificates | ClientCertificates | ClientRegistrationService |
TblUsers | tblUsers | UserController (direct) |
TblMessageConfigs | tblMessageConfig | MessageFilterService · XmlRouterService · MessageConfigController |
TblRoutes | tblRoutes | XmlRouterService · MessageConfigController |
TblMessageFilters | tblMessageFilter | MessageFilterService · MessageConfigController |
TblMessageCounts | tblMessageCount | MessageFilterService (MERGE) · MessageConfigController (read) |
TblFailedMessages | tblFailedMessage | XmlRouterService (INSERT on all-retry-fail) |
TblNotificationTrackers | tblNotificationTracker | NotificationService (routing failures throttle) |
TblLogs | tblLogs | — (reserved) |
TblProcedures | tblProcedures | — (reserved) |
Used exclusively by StockwellReport to query the Stockwell (Odyssey) financial database. Connection selected by DbKeys.Stockwell.
| Method | Action |
QueryAsync<T>(sql, param, db) | Returns IEnumerable<T> from Dapper query |
QueryFirstOrDefaultAsync<T> | Single row or null |
ExecuteAsync | Non-query (INSERT/UPDATE) |
| Entity | PK | Key Columns | Special |
RegisteredClient | Guid Id | ClientName, AzureClientId, Thumbprint (unique), IsActive, LastAccessDate | Nav: Certificates[] |
ClientCertificate | Guid Id | ClientId (FK), Thumbprint (unique), NotBefore, NotAfter, IsRevoked | |
TblUser | Guid UserId | Username, Password (SHA256), Salt, Token, RefreshToken, Status | Soft-delete via Status |
TblMessageConfig | Guid Id | EdiClientName, RecipientId, ProcedureId, Status | Unique (EdiClientName, RecipientId) |
TblRoute | int Id (IDENTITY) | MessageConfigId (FK), Type, Uri, ExtendedPath, Username, Password, Token, SoapAction, ClientId, TenantId, Scope, AuthorizationUrl, Status | |
TblMessageFilter | Guid Id | MessageConfigId (FK), Status (routing on/off) | |
TblMessageCount | (MessageConfigId, RoutesId) | MessageCount (per-route), PendingCount / SuccessCount / FailCount (routes_id=0 summary) | Composite PK |
TblFailedMessage | int Id (IDENTITY) | MsgConfigId, FileName, FailedPath, RouteType, ErrorDetail, Datestamp | Inserted after all retries fail |
TblNotificationTracker | int Id (IDENTITY) | MsgConfigId, OccurrenceType (Hourly/Daily/Minutes), OccurrenceNo, LastNotificationSend | Throttle window for delivery-fail emails |
| Helper | Method | Purpose |
eAdapterValidator | ValidateContentType(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 / DirectoryPath | Checks for invalid characters in paths |
Crypt | Encrypt / 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 / DecryptMD5 | TripleDES-based encryption (legacy) |
DirectoryExtensions | isDirectoryExist(path) | Extension method — ensures dir exists (creates if missing), returns path |
StockwellExtensions | ToSafePathSegment(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 |