Add XML documentation across gateway, worker, and .NET client

This commit is contained in:
Joseph Doherty
2026-04-30 11:49:58 -04:00
parent 4731ab535c
commit eed1e88a37
269 changed files with 4555 additions and 13 deletions
@@ -10,6 +10,9 @@ namespace MxGateway.Server.Galaxy;
/// other subscribers or the publisher. When a subscriber's channel is full the oldest
/// event is dropped — clients use the sequence field to detect gaps.
/// </summary>
/// <summary>
/// Publishes Galaxy deploy events to streaming gRPC subscribers via private bounded channels.
/// </summary>
public sealed class GalaxyDeployNotifier : IGalaxyDeployNotifier
{
private const int SubscriberQueueCapacity = 16;
@@ -17,8 +20,12 @@ public sealed class GalaxyDeployNotifier : IGalaxyDeployNotifier
private readonly ConcurrentDictionary<Guid, Channel<GalaxyDeployEventInfo>> _subscribers = new();
private GalaxyDeployEventInfo? _latest;
/// <summary>
/// The most recent deploy event, or null if none has been published.
/// </summary>
public GalaxyDeployEventInfo? Latest => Volatile.Read(ref _latest);
/// <inheritdoc />
public void Publish(GalaxyDeployEventInfo info)
{
ArgumentNullException.ThrowIfNull(info);
@@ -33,6 +40,7 @@ public sealed class GalaxyDeployNotifier : IGalaxyDeployNotifier
}
}
/// <inheritdoc />
public async IAsyncEnumerable<GalaxyDeployEventInfo> SubscribeAsync(
[EnumeratorCancellation] CancellationToken cancellationToken)
{
@@ -25,6 +25,11 @@ public sealed class GalaxyHierarchyCache : IGalaxyHierarchyCache
private readonly SemaphoreSlim _refreshGate = new(1, 1);
private GalaxyHierarchyCacheEntry _current = GalaxyHierarchyCacheEntry.Empty;
/// <summary>Initializes a new instance of the <see cref="GalaxyHierarchyCache"/> class.</summary>
/// <param name="repository">Galaxy Repository client for SQL queries.</param>
/// <param name="notifier">Galaxy deploy event notifier.</param>
/// <param name="timeProvider">Provider for current time; defaults to system time.</param>
/// <param name="logger">Optional logger for diagnostic output.</param>
public GalaxyHierarchyCache(
GalaxyRepository repository,
IGalaxyDeployNotifier notifier,
@@ -37,6 +42,7 @@ public sealed class GalaxyHierarchyCache : IGalaxyHierarchyCache
_logger = logger;
}
/// <summary>Gets the current Galaxy hierarchy cache entry with projected status.</summary>
public GalaxyHierarchyCacheEntry Current
{
get
@@ -47,6 +53,9 @@ public sealed class GalaxyHierarchyCache : IGalaxyHierarchyCache
}
}
/// <summary>Refreshes the Galaxy hierarchy cache if the deploy time has advanced.</summary>
/// <param name="cancellationToken">Token to cancel the asynchronous operation.</param>
/// <returns>Asynchronous task representing the refresh operation.</returns>
public async Task RefreshAsync(CancellationToken cancellationToken)
{
await _refreshGate.WaitAsync(cancellationToken).ConfigureAwait(false);
@@ -60,6 +69,9 @@ public sealed class GalaxyHierarchyCache : IGalaxyHierarchyCache
}
}
/// <summary>Waits for the Galaxy hierarchy cache to complete its first load.</summary>
/// <param name="cancellationToken">Token to cancel the asynchronous operation.</param>
/// <returns>Asynchronous task representing the wait operation.</returns>
public Task WaitForFirstLoadAsync(CancellationToken cancellationToken)
{
return _firstLoad.Task.WaitAsync(cancellationToken);
@@ -23,6 +23,7 @@ public sealed record GalaxyHierarchyCacheEntry(
int HistorizedAttributeCount,
int AlarmAttributeCount)
{
/// <summary>Gets an empty Galaxy hierarchy cache entry.</summary>
public static GalaxyHierarchyCacheEntry Empty { get; } = new(
Status: GalaxyCacheStatus.Unknown,
Sequence: 0,
@@ -39,5 +40,6 @@ public sealed record GalaxyHierarchyCacheEntry(
HistorizedAttributeCount: 0,
AlarmAttributeCount: 0);
/// <summary>Gets a value indicating whether the cache entry contains usable data.</summary>
public bool HasData => Status is GalaxyCacheStatus.Healthy or GalaxyCacheStatus.Stale;
}
@@ -4,11 +4,7 @@ using Microsoft.Extensions.Options;
namespace MxGateway.Server.Galaxy;
/// <summary>
/// Periodically refreshes <see cref="IGalaxyHierarchyCache"/> off the request path. The
/// interval comes from <see cref="GalaxyRepositoryOptions.DashboardRefreshIntervalSeconds"/>;
/// each tick is cheap when the deploy timestamp is unchanged.
/// </summary>
/// <summary>Background service that periodically refreshes the Galaxy Repository hierarchy cache off the request path.</summary>
public sealed class GalaxyHierarchyRefreshService(
IGalaxyHierarchyCache cache,
IOptions<GalaxyRepositoryOptions> options,
@@ -17,6 +13,7 @@ public sealed class GalaxyHierarchyRefreshService(
{
private readonly TimeProvider _timeProvider = timeProvider ?? TimeProvider.System;
/// <inheritdoc />
protected override async Task ExecuteAsync(CancellationToken stoppingToken)
{
TimeSpan interval = TimeSpan.FromSeconds(Math.Max(1, options.Value.DashboardRefreshIntervalSeconds));
@@ -6,30 +6,70 @@ namespace MxGateway.Server.Galaxy;
/// </summary>
public sealed class GalaxyHierarchyRow
{
/// <summary>Gets the Galaxy object identifier.</summary>
public int GobjectId { get; init; }
/// <summary>Gets the tag name.</summary>
public string TagName { get; init; } = string.Empty;
/// <summary>Gets the contained name.</summary>
public string ContainedName { get; init; } = string.Empty;
/// <summary>Gets the browse name.</summary>
public string BrowseName { get; init; } = string.Empty;
/// <summary>Gets the parent Galaxy object identifier.</summary>
public int ParentGobjectId { get; init; }
/// <summary>Gets a value indicating whether this is an area.</summary>
public bool IsArea { get; init; }
/// <summary>Gets the category identifier.</summary>
public int CategoryId { get; init; }
/// <summary>Gets the Galaxy object identifier of the host.</summary>
public int HostedByGobjectId { get; init; }
/// <summary>Gets the template derivation chain.</summary>
public IReadOnlyList<string> TemplateChain { get; init; } = Array.Empty<string>();
}
/// <summary>One row from <see cref="GalaxyRepository.GetAttributesAsync"/>.</summary>
public sealed class GalaxyAttributeRow
{
/// <summary>Gets the Galaxy object identifier.</summary>
public int GobjectId { get; init; }
/// <summary>Gets the tag name.</summary>
public string TagName { get; init; } = string.Empty;
/// <summary>Gets the attribute name.</summary>
public string AttributeName { get; init; } = string.Empty;
/// <summary>Gets the full tag reference.</summary>
public string FullTagReference { get; init; } = string.Empty;
/// <summary>Gets the MXAccess data type code.</summary>
public int MxDataType { get; init; }
/// <summary>Gets the data type name.</summary>
public string? DataTypeName { get; init; }
/// <summary>Gets a value indicating whether this is an array.</summary>
public bool IsArray { get; init; }
/// <summary>Gets the array dimension, if applicable.</summary>
public int? ArrayDimension { get; init; }
/// <summary>Gets the MXAccess attribute category code.</summary>
public int MxAttributeCategory { get; init; }
/// <summary>Gets the security classification code.</summary>
public int SecurityClassification { get; init; }
/// <summary>Gets a value indicating whether this is historized.</summary>
public bool IsHistorized { get; init; }
/// <summary>Gets a value indicating whether this is an alarm.</summary>
public bool IsAlarm { get; init; }
}
@@ -10,6 +10,8 @@ namespace MxGateway.Server.Galaxy;
/// </summary>
public sealed class GalaxyRepository(GalaxyRepositoryOptions options)
{
/// <summary>Tests the connection to the Galaxy Repository database.</summary>
/// <param name="ct">Token to cancel the asynchronous operation.</param>
public async Task<bool> TestConnectionAsync(CancellationToken ct = default)
{
try
@@ -24,6 +26,8 @@ public sealed class GalaxyRepository(GalaxyRepositoryOptions options)
catch (InvalidOperationException) { return false; }
}
/// <summary>Retrieves the last deployment time from the Galaxy Repository.</summary>
/// <param name="ct">Token to cancel the asynchronous operation.</param>
public async Task<DateTime?> GetLastDeployTimeAsync(CancellationToken ct = default)
{
using SqlConnection conn = new(options.ConnectionString);
@@ -34,6 +38,8 @@ public sealed class GalaxyRepository(GalaxyRepositoryOptions options)
return result is DateTime dt ? dt : null;
}
/// <summary>Retrieves the complete hierarchy of Galaxy objects from the repository.</summary>
/// <param name="ct">Token to cancel the asynchronous operation.</param>
public async Task<List<GalaxyHierarchyRow>> GetHierarchyAsync(CancellationToken ct = default)
{
List<GalaxyHierarchyRow> rows = new();
@@ -70,6 +76,8 @@ public sealed class GalaxyRepository(GalaxyRepositoryOptions options)
return rows;
}
/// <summary>Retrieves all attributes for Galaxy objects from the repository.</summary>
/// <param name="ct">Token to cancel the asynchronous operation.</param>
public async Task<List<GalaxyAttributeRow>> GetAttributesAsync(CancellationToken ct = default)
{
List<GalaxyAttributeRow> rows = new();
@@ -8,9 +8,11 @@ public sealed class GalaxyRepositoryOptions
{
public const string SectionName = "MxGateway:Galaxy";
/// <summary>The SQL Server connection string for the Galaxy Repository database.</summary>
public string ConnectionString { get; init; } =
"Server=localhost;Database=ZB;Integrated Security=True;TrustServerCertificate=True;Encrypt=False;";
/// <summary>The timeout in seconds for SQL commands executed against the Galaxy Repository.</summary>
public int CommandTimeoutSeconds { get; init; } = 60;
/// <summary>
@@ -4,6 +4,9 @@ namespace MxGateway.Server.Galaxy;
public static class GalaxyRepositoryServiceCollectionExtensions
{
/// <summary>Registers Galaxy Repository services in the dependency injection container.</summary>
/// <param name="services">The service collection.</param>
/// <returns>The service collection for chaining.</returns>
public static IServiceCollection AddGalaxyRepository(this IServiceCollection services)
{
services
@@ -1,18 +1,17 @@
namespace MxGateway.Server.Galaxy;
/// <summary>Publishes Galaxy repository deploy events to subscribers.</summary>
public interface IGalaxyDeployNotifier
{
/// <summary>The most recently published event, or <c>null</c> if no event has fired yet.</summary>
/// <summary>The most recently published event, or null if no event has fired yet.</summary>
GalaxyDeployEventInfo? Latest { get; }
/// <summary>Publishes a deploy event to all current subscribers and stores it as <see cref="Latest"/>.</summary>
/// <summary>Publishes a deploy event to all current subscribers and stores it as Latest.</summary>
/// <param name="info">The deploy event to publish.</param>
void Publish(GalaxyDeployEventInfo info);
/// <summary>
/// Subscribe to deploy events. The async sequence yields events as they fire. If
/// <see cref="Latest"/> is set, it is yielded first so subscribers can bootstrap their
/// local cache without waiting for the next deploy. Pass a cancellation token to
/// unsubscribe.
/// </summary>
/// <summary>Subscribes to deploy events. The sequence yields the latest event first (if available) then streams new events as they fire.</summary>
/// <param name="cancellationToken">Token to cancel the asynchronous operation.</param>
/// <returns>Async enumerable of deploy events.</returns>
IAsyncEnumerable<GalaxyDeployEventInfo> SubscribeAsync(CancellationToken cancellationToken);
}
@@ -1,5 +1,6 @@
namespace MxGateway.Server.Galaxy;
/// <summary>Cache for Galaxy Repository hierarchy data.</summary>
public interface IGalaxyHierarchyCache
{
/// <summary>The latest cache entry. Status freshness is recomputed against the clock.</summary>
@@ -11,6 +12,7 @@ public interface IGalaxyHierarchyCache
/// attributes rowsets when the deploy time has changed since the last successful
/// refresh.
/// </summary>
/// <param name="cancellationToken">Token to cancel the asynchronous operation.</param>
Task RefreshAsync(CancellationToken cancellationToken);
/// <summary>
@@ -18,5 +20,6 @@ public interface IGalaxyHierarchyCache
/// gRPC handlers that want to serve from cache without returning Unavailable on the
/// very first request after gateway start.
/// </summary>
/// <param name="cancellationToken">Token to cancel the asynchronous operation.</param>
Task WaitForFirstLoadAsync(CancellationToken cancellationToken);
}