docs(components): reference docs batch 1/4 — Commons, ConfigurationDatabase, Communication, ClusterInfrastructure, Host, Security
This commit is contained in:
@@ -0,0 +1,320 @@
|
||||
# Commons
|
||||
|
||||
Commons is the foundational shared library that all other ScadaBridge components depend on — it defines the POCO entity classes, repository interfaces, service interfaces, message contracts, shared enums, and utility types that the system builds on top of.
|
||||
|
||||
## Overview
|
||||
|
||||
Commons (#16) is not a runtime component. It has no actors, no hosted services, and no DI registrations of its own. Its single role is to hold the shared type vocabulary — entity shapes, interface contracts, and message definitions — so that every component agrees on the same types without depending on each other.
|
||||
|
||||
The project enforces minimal dependencies by design: only core .NET SDK. It must not reference Akka.NET, ASP.NET Core, Entity Framework Core, or any persistence or framework library, because it is referenced by all other projects and a framework dependency here becomes a transitive constraint on everything.
|
||||
|
||||
Source lives in `src/ZB.MOM.WW.ScadaBridge.Commons/`, organized into four top-level namespaces: `Types/`, `Interfaces/`, `Entities/`, and `Messages/`.
|
||||
|
||||
## Key Concepts
|
||||
|
||||
### Persistence-ignorant entity classes
|
||||
|
||||
All configuration database entity classes live in `Entities/` as plain C# classes with no EF attributes, no EF base classes, and no persistence-framework annotations. Navigation properties (for example `Template.Attributes`) are plain `ICollection<T>` — EF Fluent API configuration is the Configuration Database component's job, not Commons'. The entities may include constructors that enforce required fields:
|
||||
|
||||
```csharp
|
||||
// Entities/Templates/Template.cs
|
||||
public class Template
|
||||
{
|
||||
public int Id { get; set; }
|
||||
public string Name { get; set; }
|
||||
public string? Description { get; set; }
|
||||
public int? ParentTemplateId { get; set; }
|
||||
public int? FolderId { get; set; }
|
||||
public ICollection<TemplateAttribute> Attributes { get; set; } = new List<TemplateAttribute>();
|
||||
public ICollection<TemplateAlarm> Alarms { get; set; } = new List<TemplateAlarm>();
|
||||
public ICollection<TemplateScript> Scripts { get; set; } = new List<TemplateScript>();
|
||||
public ICollection<TemplateComposition> Compositions { get; set; } = new List<TemplateComposition>();
|
||||
public ICollection<TemplateNativeAlarmSource> NativeAlarmSources { get; set; } = new List<TemplateNativeAlarmSource>();
|
||||
public bool IsDerived { get; set; }
|
||||
public int? OwnerCompositionId { get; set; }
|
||||
|
||||
public Template(string name)
|
||||
{
|
||||
Name = name ?? throw new ArgumentNullException(nameof(name));
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Repository interfaces
|
||||
|
||||
Commons defines one repository interface per consuming component. Implementations live entirely in the Configuration Database component. Each interface accepts and returns the POCO entity classes from Commons, and every interface includes `SaveChangesAsync()` to support the unit-of-work pattern without requiring a dependency on EF Core.
|
||||
|
||||
### Message contracts and additive-only evolution
|
||||
|
||||
Messages in `Messages/` are `record` types or immutable classes. Because sites and central may temporarily run different software versions, the rule is additive-only: new fields may be added with defaults; existing fields must not be removed or have their types changed. Contracts that cross the site→central gRPC boundary — `CachedCallTelemetry`, `AuditTelemetryEnvelope`, `NotificationSubmit`, and the pull reconciliation messages — are the most version-sensitive and have this rule explicitly called out in their XML docs.
|
||||
|
||||
### Pure-helper carve-out
|
||||
|
||||
Commons may contain stateless, side-effect-free helper types that transform or validate the data types it already defines. Anything that would require I/O, shared mutable state across calls beyond a self-contained instance, or knowledge of another component is excluded. Current examples: `Result<T>`, `ScriptParameters`, `ValueFormatter`, `DynamicJsonElement`, `StaleTagMonitor`, `OpcUaEndpointConfigSerializer`, and `OpcUaEndpointConfigValidator`.
|
||||
|
||||
## Architecture
|
||||
|
||||
### Namespace and folder structure
|
||||
|
||||
```
|
||||
ZB.MOM.WW.ScadaBridge.Commons/
|
||||
├── Types/ # Enums/, Alarms/, Audit/, DataConnections/,
|
||||
│ # Flattening/, InboundApi/, Notifications/,
|
||||
│ # Transport/, Scripts/ + top-level utility types
|
||||
├── Interfaces/ # Protocol/, Repositories/, Services/, Transport/,
|
||||
│ # Security/
|
||||
├── Entities/ # Templates/, Instances/, Sites/, ExternalSystems/,
|
||||
│ # Notifications/, InboundApi/, Security/,
|
||||
│ # Deployment/, Scripts/, Audit/
|
||||
├── Messages/ # Deployment/, Lifecycle/, Health/, Communication/,
|
||||
│ # Streaming/, DebugView/, ScriptExecution/,
|
||||
│ # Artifacts/, DataConnection/, Instance/,
|
||||
│ # Integration/, Notification/, InboundApi/,
|
||||
│ # RemoteQuery/, Audit/, Management/
|
||||
├── Observability/ # ScadaBridgeTelemetry (meter + instrument definitions)
|
||||
├── Serialization/ # OpcUaEndpointConfigSerializer, MxGatewayEndpointConfigSerializer
|
||||
└── Validators/ # OpcUaEndpointConfigValidator, MxGatewayEndpointConfigValidator
|
||||
```
|
||||
|
||||
Namespaces mirror folders: `ZB.MOM.WW.ScadaBridge.Commons.Entities.Templates`, `ZB.MOM.WW.ScadaBridge.Commons.Interfaces.Repositories`, and so on.
|
||||
|
||||
### Entity classes by domain area
|
||||
|
||||
| Folder | Classes |
|
||||
|---|---|
|
||||
| `Entities/Templates/` | `Template`, `TemplateAttribute`, `TemplateAlarm`, `TemplateNativeAlarmSource`, `TemplateScript`, `TemplateComposition`, `TemplateFolder` |
|
||||
| `Entities/Instances/` | `Instance`, `InstanceAttributeOverride`, `InstanceConnectionBinding`, `InstanceAlarmOverride`, `InstanceNativeAlarmSourceOverride`, `Area` |
|
||||
| `Entities/Sites/` | `Site`, `DataConnection` |
|
||||
| `Entities/ExternalSystems/` | `ExternalSystemDefinition`, `ExternalSystemMethod`, `DatabaseConnectionDefinition` |
|
||||
| `Entities/Notifications/` | `NotificationList`, `NotificationRecipient`, `SmtpConfiguration`, `Notification` |
|
||||
| `Entities/InboundApi/` | `ApiKey`, `ApiMethod` |
|
||||
| `Entities/Security/` | `LdapGroupMapping`, `SiteScopeRule` |
|
||||
| `Entities/Deployment/` | `DeploymentRecord`, `SystemArtifactDeploymentRecord`, `DeployedConfigSnapshot` |
|
||||
| `Entities/Scripts/` | `SharedScript` |
|
||||
| `Entities/Audit/` | `AuditLogEntry` (config-change audit), `SiteCall` (SiteCalls operational mirror) |
|
||||
|
||||
The `Instance` entity illustrates the typical POCO shape — required fields enforced by a constructor, navigation collections as plain `ICollection<T>`, and no persistence annotations:
|
||||
|
||||
```csharp
|
||||
// Entities/Instances/Instance.cs
|
||||
public class Instance
|
||||
{
|
||||
public int Id { get; set; }
|
||||
public int TemplateId { get; set; }
|
||||
public int SiteId { get; set; }
|
||||
public int? AreaId { get; set; }
|
||||
public string UniqueName { get; set; }
|
||||
public InstanceState State { get; set; }
|
||||
public ICollection<InstanceAttributeOverride> AttributeOverrides { get; set; } = new List<InstanceAttributeOverride>();
|
||||
public ICollection<InstanceAlarmOverride> AlarmOverrides { get; set; } = new List<InstanceAlarmOverride>();
|
||||
public ICollection<InstanceConnectionBinding> ConnectionBindings { get; set; } = new List<InstanceConnectionBinding>();
|
||||
public ICollection<InstanceNativeAlarmSourceOverride> NativeAlarmSourceOverrides { get; set; } = new List<InstanceNativeAlarmSourceOverride>();
|
||||
|
||||
public Instance(string uniqueName)
|
||||
{
|
||||
UniqueName = uniqueName ?? throw new ArgumentNullException(nameof(uniqueName));
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Repository interfaces by consuming component
|
||||
|
||||
| Interface | Consuming component | Scope |
|
||||
|---|---|---|
|
||||
| `ITemplateEngineRepository` | Template Engine | Templates, attributes, alarms, native alarm sources, scripts, compositions, folders, instances, overrides, connection bindings, areas, shared scripts |
|
||||
| `IDeploymentManagerRepository` | Deployment Manager | Deployment records, snapshots, system artifact deployments |
|
||||
| `ISecurityRepository` | Security & Auth | LDAP group mappings, site scope rules |
|
||||
| `IInboundApiRepository` | Inbound API | API keys, API method definitions |
|
||||
| `IExternalSystemRepository` | External System Gateway | External system definitions, methods, database connection definitions |
|
||||
| `INotificationRepository` | Notification Service | Notification lists, recipients, SMTP configuration |
|
||||
| `INotificationOutboxRepository` | Notification Outbox | `Notifications` table: ingest, due-row polling, status transitions, KPI queries, bulk purge |
|
||||
| `ISiteCallAuditRepository` | Site Call Audit | `SiteCalls` table: ingest, upsert-on-newer-status, KPI queries, bulk purge |
|
||||
| `IAuditLogRepository` | Audit Log | `AuditLog` table: idempotent ingest, keyset-paged query, partition switch-out, KPI snapshots, execution tree walk |
|
||||
| `ISiteRepository` | Central UI, Site Runtime | Sites, data connections, site assignments |
|
||||
| `ICentralUiRepository` | Central UI | Read-spanning queries for display |
|
||||
|
||||
`IAuditLogRepository` enforces the append-only contract at the API level — it exposes no Update and no single-row Delete. Bulk purge is `SwitchOutPartitionAsync` only. Ingest is idempotent on `EventId`:
|
||||
|
||||
```csharp
|
||||
// Interfaces/Repositories/IAuditLogRepository.cs
|
||||
public interface IAuditLogRepository
|
||||
{
|
||||
Task InsertIfNotExistsAsync(AuditEvent evt, CancellationToken ct = default);
|
||||
|
||||
Task<IReadOnlyList<AuditEvent>> QueryAsync(
|
||||
AuditLogQueryFilter filter,
|
||||
AuditLogPaging paging,
|
||||
CancellationToken ct = default);
|
||||
|
||||
Task<long> SwitchOutPartitionAsync(DateTime monthBoundary, CancellationToken ct = default);
|
||||
|
||||
Task<IReadOnlyList<DateTime>> GetPartitionBoundariesOlderThanAsync(
|
||||
DateTime threshold,
|
||||
CancellationToken ct = default);
|
||||
|
||||
Task<AuditLogKpiSnapshot> GetKpiSnapshotAsync(
|
||||
TimeSpan window,
|
||||
DateTime? nowUtc = null,
|
||||
CancellationToken ct = default);
|
||||
|
||||
Task<IReadOnlyList<ExecutionTreeNode>> GetExecutionTreeAsync(
|
||||
Guid executionId,
|
||||
CancellationToken ct = default);
|
||||
|
||||
Task<IReadOnlyList<string>> GetDistinctSourceNodesAsync(CancellationToken ct = default);
|
||||
}
|
||||
```
|
||||
|
||||
### Cross-cutting service interfaces
|
||||
|
||||
`Interfaces/Services/` holds service interfaces for cross-cutting concerns that multiple components consume but do not implement in Commons itself.
|
||||
|
||||
| Interface | Purpose | Implemented by |
|
||||
|---|---|---|
|
||||
| `IAuditService` | Configuration-change audit log entry (`LogAsync`). Central components call this through the UoW. | Configuration Database |
|
||||
| `IAuditWriter` | Site hot-path audit write (`WriteAsync`). Best-effort; must never throw back at the caller. | Audit Log |
|
||||
| `ICentralAuditWriter` | Central direct-write for central-originated audit rows; insert-if-not-exists on `EventId`. | Audit Log |
|
||||
| `ISiteAuditQueue` | Hands off site audit rows to the gRPC telemetry forwarder. | Audit Log |
|
||||
| `ICachedCallLifecycleObserver` / `ICachedCallTelemetryForwarder` | Bridge between the S&F Engine's lifecycle transitions and the `CachedCallTelemetry` packet. | Audit Log |
|
||||
| `INodeIdentityProvider` | Resolves the current node's `SourceNode` label (`node-a`, `central-b`, etc.). | Host |
|
||||
| `IOperationTrackingStore` | Site-local SQLite tracking status store for `Tracking.Status(id)`. | Site Runtime |
|
||||
| `IPartitionMaintenance` | Central partition-switch / retention purge hook. | Audit Log |
|
||||
| `IDatabaseGateway` | Script-facing ADO.NET database access via named connections. | External System Gateway |
|
||||
| `IExternalSystemClient` | Script-facing `ExternalSystem.Call()` / `CachedCall()` invocation. | External System Gateway |
|
||||
| `IInstanceLocator` | Resolves instance unique name to site identifier for `Route.To()`. | Management Service |
|
||||
| `INotificationDeliveryService` | Script-facing `Notify.Send()` routing with S&F fallback. | Notification Service |
|
||||
|
||||
Transport bundle interfaces (`IBundleExporter`, `IBundleImporter`, `IBundleSessionStore`, `IAuditCorrelationContext`) live in `Interfaces/Transport/` and are defined in Commons so the Configuration Database and Central UI can depend on the abstraction without taking a Transport component dependency.
|
||||
|
||||
### Key shared types
|
||||
|
||||
**`Result<T>`** is the system-wide discriminated result type. A failed result always carries a non-blank error message; callers pattern-match via `Match`:
|
||||
|
||||
```csharp
|
||||
// Types/Result.cs
|
||||
public sealed class Result<T>
|
||||
{
|
||||
public bool IsSuccess { get; }
|
||||
public bool IsFailure => !IsSuccess;
|
||||
public T Value => IsSuccess ? _value! : throw new InvalidOperationException("...");
|
||||
public string Error => IsFailure ? _error! : throw new InvalidOperationException("...");
|
||||
|
||||
public static Result<T> Success(T value) => new(value);
|
||||
public static Result<T> Failure(string error) => new(error);
|
||||
|
||||
public TResult Match<TResult>(Func<T, TResult> onSuccess, Func<string, TResult> onFailure) =>
|
||||
IsSuccess ? onSuccess(_value!) : onFailure(_error!);
|
||||
}
|
||||
```
|
||||
|
||||
**`TrackedOperationId`** is the strongly-typed GUID that identifies a cached outbound operation end-to-end — it is the idempotency key on every `AuditLog` row for that lifecycle and the primary key on the central `SiteCalls` row:
|
||||
|
||||
```csharp
|
||||
// Types/TrackedOperationId.cs
|
||||
public readonly record struct TrackedOperationId(Guid Value)
|
||||
{
|
||||
public static TrackedOperationId New() => new(Guid.NewGuid());
|
||||
public static TrackedOperationId Parse(string s) => new(Guid.Parse(s));
|
||||
public static bool TryParse(string? s, out TrackedOperationId result) { ... }
|
||||
public override string ToString() => Value.ToString("D");
|
||||
}
|
||||
```
|
||||
|
||||
**`AlarmConditionState`** is the unified, read-only alarm condition model shared by computed and native alarms. Computed alarms populate it from `State` + `Priority`; native alarms mirror it from the OPC UA or MxAccess source:
|
||||
|
||||
```csharp
|
||||
// Types/Alarms/AlarmConditionState.cs
|
||||
public record AlarmConditionState(
|
||||
bool Active,
|
||||
bool Acknowledged,
|
||||
bool? Confirmed, // null when the condition is not confirmable
|
||||
AlarmShelveState Shelve,
|
||||
bool Suppressed,
|
||||
int Severity); // 0–1000 unified scale
|
||||
```
|
||||
|
||||
**`ScadaBridgeAuditEventFactory`** is the single construction point for a canonical `AuditEvent`. Every audit emit site builds its row through `Create` so the domain-vocabulary-to-canonical-field mapping (`Channel`/`Kind`/`Status` → `Action`/`Category`/`Outcome`; all other ScadaBridge domain fields → `DetailsJson`) is applied identically everywhere with no per-site drift.
|
||||
|
||||
### Protocol abstraction
|
||||
|
||||
`Interfaces/Protocol/` defines the Data Connection Layer's protocol-neutral interfaces.
|
||||
|
||||
`IDataConnection` is the base interface for reading, writing, and subscribing to device data regardless of protocol. `IBrowsableDataConnection` is an optional capability interface for address-space browsing. `IAlarmSubscribableConnection` is an optional capability interface for connections that can mirror native alarms — implementations expose `SubscribeAlarmsAsync` and `UnsubscribeAlarmsAsync`, delivering transitions as protocol-neutral `NativeAlarmTransition` records via `AlarmTransitionCallback`. The `DataConnectionActor` consumes these via capability checks (runtime `is` cast), keeping protocol knowledge out of the core actor logic.
|
||||
|
||||
### Message contracts
|
||||
|
||||
`Messages/` organizes contracts by concern rather than by sender/receiver:
|
||||
|
||||
| Folder | Key types |
|
||||
|---|---|
|
||||
| `Deployment/` | `DeployInstanceCommand`, `DeploymentStatusResponse`, `FlattenedConfigurationSnapshot` |
|
||||
| `Lifecycle/` | `DisableInstanceCommand`, `EnableInstanceCommand`, `DeleteInstanceCommand`, `InstanceLifecycleResponse` |
|
||||
| `Health/` | `SiteHealthReport`, `HeartbeatMessage`, `NodeStatus`, `TagQualityCounts` |
|
||||
| `Streaming/` | `AttributeValueChanged`, `AlarmStateChanged` (additively enriched for both computed and native alarms) |
|
||||
| `Integration/` | `CachedCallTelemetry`, `AuditTelemetryEnvelope`, `PullAuditEventsRequest/Response` |
|
||||
| `Notification/` | `NotificationSubmit`, `NotificationSubmitAck`, `NotificationStatusQuery/Response` |
|
||||
| `Audit/` | `IngestAuditEventsCommand/Reply`, `IngestCachedTelemetryCommand/Reply`, `UpsertSiteCallCommand/Reply` |
|
||||
| `RemoteQuery/` | Event log queries, parked-message queries, `ParkedOperationRelayMessages` |
|
||||
| `Management/` | All HTTP Management API commands per domain area, `ManagementEnvelope`, `TransportCommands` |
|
||||
|
||||
`CachedCallTelemetry` carries one combined packet per lifecycle event so central can write the `AuditLog` row and the `SiteCalls` upsert in a single MS SQL transaction:
|
||||
|
||||
```csharp
|
||||
// Messages/Integration/CachedCallTelemetry.cs
|
||||
public sealed record CachedCallTelemetry(
|
||||
AuditEvent Audit,
|
||||
SiteCallOperational Operational);
|
||||
```
|
||||
|
||||
`AlarmStateChanged` demonstrates the additive-only evolution rule in practice — the original positional constructor still compiles; native alarm fields are `init` properties with safe defaults, so existing computed-alarm emitters need no change:
|
||||
|
||||
```csharp
|
||||
// Messages/Streaming/AlarmStateChanged.cs
|
||||
public record AlarmStateChanged(
|
||||
string InstanceUniqueName,
|
||||
string AlarmName,
|
||||
AlarmState State,
|
||||
int Priority,
|
||||
DateTimeOffset Timestamp) : ISiteStreamEvent
|
||||
{
|
||||
public AlarmLevel Level { get; init; } = AlarmLevel.None;
|
||||
public AlarmKind Kind { get; init; } = AlarmKind.Computed;
|
||||
public AlarmConditionState Condition { get; init; } = ...; // defaults to computed projection
|
||||
public string SourceReference { get; init; } = string.Empty;
|
||||
// ... additional native-alarm fields with empty defaults
|
||||
}
|
||||
```
|
||||
|
||||
### Observability
|
||||
|
||||
`Observability/ScadaBridgeTelemetry` defines the singleton `Meter` named `ZB.MOM.WW.ScadaBridge` and the application-wide instrument definitions. Components call the static emit helpers (`RecordDeploymentApplied`, `SiteConnectionOpened`, etc.) rather than creating their own meters. Instruments are no-ops until an OTel listener attaches, so uninstrumented hosts pay no overhead.
|
||||
|
||||
## Usage
|
||||
|
||||
Commons is consumed through direct project references — all other components in the solution reference it. There is nothing to register or configure; the types are available as soon as the project reference is in place.
|
||||
|
||||
When adding a new entity class: add the POCO to the appropriate `Entities/<DomainArea>/` subfolder with no EF attributes, then add the corresponding repository method signature to the relevant interface in `Interfaces/Repositories/`. The Configuration Database component owns the EF mapping and the implementation.
|
||||
|
||||
When adding a new message contract: add an immutable `record` to the appropriate `Messages/<Concern>/` subfolder. If the message will cross the site→central version-skew boundary, apply the additive-only rule immediately — use `init` properties with defaults for any fields beyond the initial set so older receivers can safely ignore unknown fields.
|
||||
|
||||
## Dependencies & Interactions
|
||||
|
||||
- **No runtime dependencies** — Commons references only the core .NET SDK. It does not reference Akka.NET, ASP.NET Core, Entity Framework Core, or any third-party library.
|
||||
- [Configuration Database (#17)](./ConfigurationDatabase.md) — implements every repository interface defined here (`ITemplateEngineRepository`, `IAuditLogRepository`, etc.) via EF Core Fluent API; maps the POCO entity classes to the underlying MS SQL schema.
|
||||
- **All other components** — reference Commons for shared types, entity classes, interface contracts, and message definitions. The dependency graph is strictly one-way: Commons knows nothing about its consumers.
|
||||
- [Audit Log (#23)](./AuditLog.md) — implements `IAuditWriter`, `ICentralAuditWriter`, `ISiteAuditQueue`, `ICachedCallLifecycleObserver`, and `ICachedCallTelemetryForwarder`; consumes `ScadaBridgeAuditEventFactory`, `AuditDetailsCodec`, `AuditRowProjection`, and the audit message contracts defined here.
|
||||
- [Site Call Audit (#22)](./SiteCallAudit.md) — consumes `ISiteCallAuditRepository` and the `CachedCallTelemetry` / `UpsertSiteCallCommand` message types.
|
||||
- [Notification Outbox (#21)](./NotificationOutbox.md) — consumes `INotificationOutboxRepository` and the `NotificationSubmit` / `NotificationSubmitAck` contracts.
|
||||
- [Transport (#24)](./Transport.md) — its interfaces (`IBundleExporter`, `IBundleImporter`, `IBundleSessionStore`, `IAuditCorrelationContext`) and value objects (`BundleManifest`, `ImportPreview`, etc.) are defined in Commons so Configuration Database and Central UI can depend on the abstraction without a Transport project reference.
|
||||
- Design spec: [Component-Commons.md](../requirements/Component-Commons.md).
|
||||
|
||||
## Related Documentation
|
||||
|
||||
- [Commons design specification](../requirements/Component-Commons.md)
|
||||
- [Configuration Database](./ConfigurationDatabase.md)
|
||||
- [Audit Log](./AuditLog.md)
|
||||
- [Site Call Audit](./SiteCallAudit.md)
|
||||
- [Notification Outbox](./NotificationOutbox.md)
|
||||
- [Transport](./Transport.md)
|
||||
- [Data Connection Layer](./DataConnectionLayer.md)
|
||||
- [Health Monitoring](./HealthMonitoring.md)
|
||||
Reference in New Issue
Block a user