Commons (third-party dep, 7 namespaces, retired ApiKey, repo SaveChanges carve-out), ConfigurationDatabase (5 persisted + 1 non-persisted computed col), ClusterInfrastructure (abbreviated HOCON note, RemotingPort default), Host (component matrix: CI/HealthMonitoring/ExternalSystemGateway have no actors; DeadLetterMonitorActor runs on both roles), Security (Bearer not X-API-Key; ApiKeyAdmin registered by Host), Communication (Task.Run/Sender).
21 KiB
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: it references the ZB.MOM.WW.Audit package (for the canonical AuditEvent type) and the 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 seven top-level namespaces: Types/, Interfaces/, Entities/, Messages/, Observability/, Serialization/, and Validators/.
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:
// 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. Most repository interfaces expose SaveChangesAsync() to support the unit-of-work pattern without requiring a dependency on EF Core; the append-only audit repositories (IAuditLogRepository, ISiteCallAuditRepository) do not — they use upsert/insert-only operations that do not require an explicit save step.
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/ |
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:
// 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:
// 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 |
IAuditActorAccessor |
Resolves the authenticated principal's actor string for audit rows (inbound API middleware). | Security & Auth |
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:
// 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:
// 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:
// 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:
// 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:
// 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;
// Condition uses a private backing field so the getter can return a
// computed default (AlarmConditionStateFactory.ForComputed(State, Priority))
// when no explicit value has been set via the init accessor.
private AlarmConditionState? _condition;
public AlarmConditionState Condition
{
get => _condition ?? AlarmConditionStateFactory.ForComputed(State, Priority);
init => _condition = value;
}
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
- Minimal dependencies — Commons references the core .NET SDK and the
ZB.MOM.WW.Auditpackage (for the canonicalAuditEventtype). It does not reference Akka.NET, ASP.NET Core, Entity Framework Core, or any other third-party library. - Configuration Database (#17) — 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) — implements
IAuditWriter,ICentralAuditWriter,ISiteAuditQueue,ICachedCallLifecycleObserver, andICachedCallTelemetryForwarder; consumesScadaBridgeAuditEventFactory,AuditDetailsCodec,AuditRowProjection, and the audit message contracts defined here. - Site Call Audit (#22) — consumes
ISiteCallAuditRepositoryand theCachedCallTelemetry/UpsertSiteCallCommandmessage types. - Notification Outbox (#21) — consumes
INotificationOutboxRepositoryand theNotificationSubmit/NotificationSubmitAckcontracts. - Transport (#24) — 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.