15 KiB
Galaxy Repository Browse
The gateway exposes a read-only browse surface over the AVEVA System Platform
Galaxy Repository (the SQL Server database named ZB). Clients use it to
enumerate the deployed object hierarchy and each object's dynamic attributes
before subscribing to runtime values via the existing MxAccessGateway RPCs.
This is a metadata layer: it never reads or writes runtime tag values, never goes through MXAccess COM, and never needs the x86 worker. It runs entirely inside the .NET 10 gateway process and talks to the Galaxy Repository over plain SQL.
Why It Exists
Without browse, a client must already know tag_name.AttributeName strings to
issue AddItem. The Galaxy Repository is the authoritative source for those
names — the same database that System Platform itself reads when the
ArchestrA IDE renders the deployment tree. Surfacing that data over gRPC lets
remote clients build a navigable address space without any coupling to the
COM layer or the host platform.
The query bodies are kept byte-for-byte identical to the equivalent OPC UA server in the OtOpcUa project so the two consumers see the same row sets.
RPC Surface
The service is defined in
src/MxGateway.Contracts/Protos/galaxy_repository.proto under package
galaxy_repository.v1.
| RPC | Purpose |
|---|---|
TestConnection |
Connectivity probe. Returns { ok: bool } after a SELECT 1. Does not throw on SQL failure — returns ok = false. Always hits SQL directly so it remains a true health check. |
GetLastDeployTime |
Returns the cached galaxy.time_of_last_deploy. Served from the shared hierarchy cache; refreshed in the background. |
DiscoverHierarchy |
Returns one page of the deployed hierarchy plus each returned object's dynamic attributes. Served from cache — see Hierarchy Cache. |
WatchDeployEvents |
Server-streaming. The server emits the current state immediately on subscribe (so clients can bootstrap without waiting), then emits one event per detected deploy change. See Deploy Notifications. |
DiscoverHierarchy is a paged unary RPC. The raw request accepts page_size
and page_token; the server defaults omitted page size to 1000 objects and
caps every page at 5000 objects. Page tokens bind to the cache sequence and the
active filter set, so changing filters between pages returns InvalidArgument
instead of mixing snapshots. Official high-level clients preserve the older
"return the full hierarchy" behavior by looping pages internally.
The request can also slice the cached hierarchy without running new SQL. A
caller may choose one root (root_gobject_id, root_tag_name, or
root_contained_path) and may combine that with max_depth, category ids,
template-chain substring filters, an anchored case-insensitive tag-name glob,
alarm-only, historized-only, and include_attributes = false for a skeleton
tree. All filters are applied with AND semantics, and total_object_count
reports the post-filter count.
Hierarchy Cache
The gateway holds a single shared IGalaxyHierarchyCache
(src/MxGateway.Server/Galaxy/GalaxyHierarchyCache.cs) — every
DiscoverHierarchy and GetLastDeployTime request reads from this cache
rather than hitting SQL. Many clients can browse concurrently with at most
one SQL query in flight.
Refresh strategy is deploy-time gated:
- The hosted
GalaxyHierarchyRefreshServiceticks everyMxGateway:Galaxy:DashboardRefreshIntervalSecondsseconds (default 30). - Each tick queries the cheap
SELECT time_of_last_deploy FROM galaxyfirst. - If the deploy timestamp is unchanged, the heavy hierarchy + attributes
queries are skipped. The cache simply marks
LastSuccessAt. - If the deploy timestamp changed (or no data has loaded yet), the cache pulls hierarchy + attributes, materializes a Galaxy object list plus a dashboard summary once, replaces the entry atomically, and publishes a deploy event.
Materializing objects and dashboard summaries at refresh time means subsequent
DiscoverHierarchy calls page over an immutable object list. The dashboard
uses the precomputed summary and does not rescan raw SQL rowsets on each
snapshot.
When SQL is unreachable, the cache retains the previous data and flips
Status to Stale (or Unavailable if no data was ever loaded). A
SqlException never bubbles out as the client-facing error.
First-load behavior
If a client calls DiscoverHierarchy before the background service has
populated the cache, the gRPC handler waits up to 5 seconds for the first
load to complete before returning. If the first load fails or times out,
the client gets Unavailable with a short reason. Once any load completes
(success or failure), this wait is skipped on subsequent calls.
Deploy Notifications
WatchDeployEvents is a server-streaming RPC backed by
IGalaxyDeployNotifier (src/MxGateway.Server/Galaxy/GalaxyDeployNotifier.cs).
The notifier maintains a private bounded channel per subscriber so a slow
client cannot back-pressure other subscribers or the publisher.
Subscriber lifecycle:
- On subscribe, the notifier emits the current event (the state of the
most recent successful refresh) so the subscriber can sync its local cache
without waiting for the next deploy. Clients that already know about that
deploy can pass
last_seen_deploy_timein the request to suppress the bootstrap event. - As the cache observes new deploy timestamps, it publishes one event per
change. Each event carries:
sequence— monotonic per server start; gaps signal a dropped event.observed_at— server wall-clock when the cache saw the deploy.time_of_last_deploy(+time_of_last_deploy_present) — the Galaxy timestamp; absent only when the source row reports null.object_count,attribute_count— counts on the new deploy, useful for dashboards and "did anything important change" gates without re-pulling.
- If the subscriber's per-subscriber buffer fills (bound = 16 events with
DropOldest), older events are dropped. Clients use thesequencefield to detect this. - Cancellation (or transport disconnect) removes the subscriber.
Typical client pattern:
1. Open WatchDeployEvents stream (with last_seen_deploy_time if you have one).
2. On each event, decide whether to call DiscoverHierarchy to refresh local cache.
3. If sequence skipped a number, treat it as a dropped event and refresh.
Reply Shape
message GalaxyObject {
int32 gobject_id = 1;
string tag_name = 2;
string contained_name = 3;
string browse_name = 4; // contained_name when present, else tag_name
int32 parent_gobject_id = 5;
bool is_area = 6;
int32 category_id = 7;
int32 hosted_by_gobject_id = 8;
repeated string template_chain = 9;
repeated GalaxyAttribute attributes = 10;
}
message GalaxyAttribute {
string attribute_name = 1;
string full_tag_reference = 2; // e.g. "DelmiaReceiver_001.DownloadPath"
int32 mx_data_type = 3; // raw Galaxy mx_data_type integer
string data_type_name = 4;
bool is_array = 5;
int32 array_dimension = 6;
bool array_dimension_present = 7; // distinguishes "no dimension" from 0
int32 mx_attribute_category = 8;
int32 security_classification = 9;
bool is_historized = 10;
bool is_alarm = 11;
}
message DiscoverHierarchyRequest {
int32 page_size = 1; // omitted/0 uses the server default of 1000
string page_token = 2; // opaque token returned by the previous page
oneof root {
int32 root_gobject_id = 3;
string root_tag_name = 4;
string root_contained_path = 5;
}
google.protobuf.Int32Value max_depth = 6;
repeated int32 category_ids = 7;
repeated string template_chain_contains = 8;
string tag_name_glob = 9;
optional bool include_attributes = 10;
bool alarm_bearing_only = 11;
bool historized_only = 12;
}
message DiscoverHierarchyReply {
repeated GalaxyObject objects = 1;
string next_page_token = 2;
int32 total_object_count = 3;
}
Contained Name vs Tag Name
Galaxy objects carry two names. tag_name is globally unique and is what
MXAccess expects in AddItem. contained_name is the human-readable name
used in the IDE browse tree, scoped to the parent. The browse RPC exposes
both: clients display browse_name to users and pass tag_name (or
full_tag_reference) into MXAccess subscriptions. When contained_name is
empty (top-level objects), browse_name falls back to tag_name.
Data Types
mx_data_type is returned as the raw Galaxy integer rather than mapped to a
language-neutral enum. The gateway makes no assumption about the client's
target type system — clients map to OPC UA, JSON, .NET CLR types, or
something else as appropriate. The Galaxy data_type table description is
also passed through as data_type_name.
array_dimension_present is a separate boolean because protobuf scalar
fields cannot express null. Use it to distinguish "no dimension reported" from
"dimension is zero."
Architecture
gRPC client(s)
-> GalaxyRepositoryGrpcService (src/MxGateway.Server/Grpc/)
DiscoverHierarchy, GetLastDeployTime -> IGalaxyHierarchyCache.Current
WatchDeployEvents -> IGalaxyDeployNotifier
TestConnection -> GalaxyRepository (direct SQL)
GalaxyHierarchyRefreshService (BackgroundService)
-> IGalaxyHierarchyCache.RefreshAsync
-> GalaxyRepository.GetLastDeployTimeAsync (cheap, every tick)
-> GalaxyRepository.GetHierarchyAsync (only on deploy change)
-> GalaxyRepository.GetAttributesAsync (only on deploy change)
-> GalaxyProtoMapper.MapObject (materialize GalaxyObject list once)
-> DashboardGalaxySummary (precompute dashboard counts once)
-> IGalaxyDeployNotifier.Publish (only on deploy change)
Component breakdown:
GalaxyRepository(src/MxGateway.Server/Galaxy/GalaxyRepository.cs) holds the SQL. Its constantsHierarchySqlandAttributesSqlare copied verbatim from the OtOpcUa project; do not edit them in isolation here. The two queries walk template-derivation and package-derivation chains via recursive CTEs and pick the most-derived attribute override per object.GalaxyHierarchyCache(src/MxGateway.Server/Galaxy/GalaxyHierarchyCache.cs) holds the most recent immutableGalaxyHierarchyCacheEntry(materialized objects + precomputed dashboard summary + counts + status). All gRPC clients share the same entry.GalaxyHierarchyRefreshService(src/MxGateway.Server/Galaxy/GalaxyHierarchyRefreshService.cs) is a hostedBackgroundServicethat drivesRefreshAsyncon the configured interval, with deploy-time gating to avoid unnecessary heavy queries.GalaxyDeployNotifier(src/MxGateway.Server/Galaxy/GalaxyDeployNotifier.cs) is a thin per-subscriber-channel fan-out for streaming clients.GalaxyProtoMapper(src/MxGateway.Server/Grpc/GalaxyProtoMapper.cs) converts row models to proto messages. Used by the cache during refresh to materialize the reply once.GalaxyRepositoryGrpcService(src/MxGateway.Server/Grpc/GalaxyRepositoryGrpcService.cs) implements the four RPCs.
Configuration
Bound to MxGateway:Galaxy via GalaxyRepositoryOptions.
| Option | Default | Description |
|---|---|---|
MxGateway:Galaxy:ConnectionString |
Server=localhost;Database=ZB;Integrated Security=True;TrustServerCertificate=True;Encrypt=False; |
SQL Server connection string for the Galaxy Repository. Integrated Security against localhost is the dev default; production deployments should override this through the standard double-underscore environment variable form, e.g. MxGateway__Galaxy__ConnectionString. |
MxGateway:Galaxy:CommandTimeoutSeconds |
60 |
Per-command SQL timeout. Applies to all three RPCs. |
The connection string is not treated as a secret in dev (Integrated Security), but production deployments that use SQL authentication should set
the override via environment variable rather than committing credentials to
appsettings.json.
The dashboard parses this connection string and displays only non-secret fields: server, database, integrated security, encrypt, and trust-server- certificate. It never displays user id, password, access token, or arbitrary unparsed connection string text.
Authorization
All four Galaxy RPCs (including WatchDeployEvents) require the
metadata:read API-key scope. Browse is read-only metadata, equivalent in
privilege to MxCommandKind.GetSessionState or MxCommandKind.GetWorkerInfo.
The mapping lives in GatewayGrpcScopeResolver; see
Authorization for the full scope catalog.
API keys can also carry browse_subtrees constraints. DiscoverHierarchy
intersects those contained-path globs with the caller's request filters.
WatchDeployEvents still emits deploy notifications, but its object and
attribute counts are scoped to the caller's browsable subtrees.
A request without an API key returns Unauthenticated. A request with a key
that lacks metadata:read returns PermissionDenied with the missing scope
embedded in the status detail.
Dashboard Surface
The gateway's Blazor dashboard surfaces a Galaxy summary in two places:
- An overview card on
/dashboardshowing connectivity status, last deploy timestamp, object count (with area count), attribute total, historized and alarm counts, and last successful refresh. - A dedicated
/dashboard/galaxypage with object-category and top-template breakdowns plus a Sync Info table covering last successful refresh, last attempt, refresh interval, redacted connection string, and command timeout.
Both views are projected from the same IGalaxyHierarchyCache that backs the
gRPC service. The dashboard does not run its own refresh — when the
background GalaxyHierarchyRefreshService updates the cache, both the
overview card and the /dashboard/galaxy page pick up the new state on the
next dashboard tick. When SQL is unreachable, the cache retains the previous
data and flips Status to Stale or Unavailable; the dashboard surfaces
that as a yellow or red status badge plus the truncated error.
Operational Notes
- The service is registered alongside
MxAccessGatewayServiceinGatewayApplication.MapGatewayEndpoints. Both services share the same authorization interceptor and authentication policy. - Failures to reach the Galaxy database surface as
Unavailable. Detailed SQL exceptions are logged atWarningand never returned to clients. - Integration tests live in
src/MxGateway.IntegrationTests/Galaxy/GalaxyRepositoryLiveTests.cs. SetMXGATEWAY_RUN_LIVE_GALAXY_TESTS=1(and optionallyMXGATEWAY_LIVE_GALAXY_CONN) to run them; otherwise they skip.