using System; using System.Collections.Generic; using System.Runtime.InteropServices; using Google.Protobuf.WellKnownTypes; using MxGateway.Contracts.Proto; namespace MxGateway.Worker.MxAccess; public sealed class MxAccessSession : IDisposable { private readonly object mxAccessComObject; private readonly IMxAccessServer mxAccessServer; private readonly IMxAccessEventSink eventSink; private readonly MxAccessHandleRegistry handleRegistry; private readonly MxAccessValueCache valueCache; private bool disposed; private MxAccessSession( object mxAccessComObject, IMxAccessServer mxAccessServer, IMxAccessEventSink eventSink, MxAccessHandleRegistry handleRegistry, MxAccessValueCache valueCache, int creationThreadId) { this.mxAccessComObject = mxAccessComObject ?? throw new ArgumentNullException(nameof(mxAccessComObject)); this.mxAccessServer = mxAccessServer ?? throw new ArgumentNullException(nameof(mxAccessServer)); this.eventSink = eventSink ?? throw new ArgumentNullException(nameof(eventSink)); this.handleRegistry = handleRegistry ?? throw new ArgumentNullException(nameof(handleRegistry)); this.valueCache = valueCache ?? throw new ArgumentNullException(nameof(valueCache)); CreationThreadId = creationThreadId; } /// The thread ID where this session was created. public int CreationThreadId { get; } /// The registry for tracking opened handles. public MxAccessHandleRegistry HandleRegistry => handleRegistry; /// /// Per-session last-value cache populated by the event sink. ReadBulk /// consults this cache before falling back to its own snapshot /// lifecycle so it can serve a "current value" for an already-advised /// tag without touching the existing subscription. /// public MxAccessValueCache ValueCache => valueCache; /// Creates a WorkerReady message with session metadata. /// Process ID of the worker. public WorkerReady CreateWorkerReady(int workerProcessId) { return new WorkerReady { WorkerProcessId = workerProcessId, MxaccessProgid = MxAccessInteropInfo.ProgId, MxaccessClsid = MxAccessInteropInfo.Clsid, ReadyTimestamp = Timestamp.FromDateTime(DateTime.UtcNow), }; } /// Creates and initializes an MXAccess COM session. /// Factory to create the MXAccess COM object. /// Event sink to attach to the COM object. /// Identifier of the session. public static MxAccessSession Create( IMxAccessComObjectFactory factory, IMxAccessEventSink eventSink, string sessionId) { if (factory is null) { throw new ArgumentNullException(nameof(factory)); } if (eventSink is null) { throw new ArgumentNullException(nameof(eventSink)); } object? mxAccessComObject = null; try { mxAccessComObject = factory.Create(); if (mxAccessComObject is null) { throw new InvalidOperationException("MXAccess COM factory returned null."); } eventSink.Attach(mxAccessComObject, sessionId); // Share the event sink's value cache when one is wired (the // production MxAccessBaseEventSink path) so OnDataChange writes and // ReadBulk reads both see the same instance. Fall back to a fresh // cache for test fakes that supply their own sink — ReadBulk simply // never serves cached values in that case. MxAccessValueCache valueCache = eventSink is MxAccessBaseEventSink baseSink ? baseSink.ValueCache : new MxAccessValueCache(); return new MxAccessSession( mxAccessComObject, new MxAccessComServer(mxAccessComObject), eventSink, new MxAccessHandleRegistry(), valueCache, Environment.CurrentManagedThreadId); } catch (Exception exception) { try { eventSink.Detach(); } catch { // Preserve the creation failure while still releasing the COM object below. } if (mxAccessComObject is not null && Marshal.IsComObject(mxAccessComObject)) { Marshal.FinalReleaseComObject(mxAccessComObject); } throw MxAccessCreationException.From(exception); } } /// Registers a client with MXAccess and returns the server handle. /// Name of the client to register. public int Register(string clientName) { ThrowIfDisposed(); int serverHandle = mxAccessServer.Register(clientName); handleRegistry.RegisterServerHandle(serverHandle, clientName); return serverHandle; } /// Unregisters a client from MXAccess. /// Handle returned by the worker. public void Unregister(int serverHandle) { ThrowIfDisposed(); mxAccessServer.Unregister(serverHandle); handleRegistry.UnregisterServerHandle(serverHandle); } /// Adds an item to an MXAccess server and returns the item handle. /// Handle returned by the worker. /// Definition or address of the item to add. public int AddItem( int serverHandle, string itemDefinition) { ThrowIfDisposed(); int itemHandle = mxAccessServer.AddItem(serverHandle, itemDefinition); handleRegistry.RegisterItemHandle( serverHandle, itemHandle, itemDefinition, string.Empty, hasItemContext: false); return itemHandle; } /// Adds an item with context to an MXAccess server and returns the item handle. /// Handle returned by the worker. /// Definition or address of the item to add. /// Context string for the item. public int AddItem2( int serverHandle, string itemDefinition, string itemContext) { ThrowIfDisposed(); int itemHandle = mxAccessServer.AddItem2(serverHandle, itemDefinition, itemContext); handleRegistry.RegisterItemHandle( serverHandle, itemHandle, itemDefinition, itemContext, hasItemContext: true); return itemHandle; } /// Removes an item from an MXAccess server. /// Handle returned by the worker. /// Handle returned by the worker. public void RemoveItem( int serverHandle, int itemHandle) { ThrowIfDisposed(); mxAccessServer.RemoveItem(serverHandle, itemHandle); handleRegistry.RemoveItemHandle(serverHandle, itemHandle); // Evict the last-value entry so a future AddItem + Advise on the // same handle id (which MXAccess may reuse) does not serve a stale // OnDataChange snapshot from the previous lifetime. valueCache.Remove(serverHandle, itemHandle); } /// Advises on item changes with plain subscription. /// Handle returned by the worker. /// Handle returned by the worker. public void Advise( int serverHandle, int itemHandle) { ThrowIfDisposed(); mxAccessServer.Advise(serverHandle, itemHandle); handleRegistry.RegisterAdviceHandle( serverHandle, itemHandle, MxAccessAdviceKind.Plain); } /// Removes plain advice subscription from an item. /// Handle returned by the worker. /// Handle returned by the worker. public void UnAdvise( int serverHandle, int itemHandle) { ThrowIfDisposed(); mxAccessServer.UnAdvise(serverHandle, itemHandle); handleRegistry.RemoveAdviceHandles(serverHandle, itemHandle); } /// Advises on item changes with supervisory subscription. /// Handle returned by the worker. /// Handle returned by the worker. public void AdviseSupervisory( int serverHandle, int itemHandle) { ThrowIfDisposed(); mxAccessServer.AdviseSupervisory(serverHandle, itemHandle); handleRegistry.RegisterAdviceHandle( serverHandle, itemHandle, MxAccessAdviceKind.Supervisory); } /// Writes a value to an item. /// Handle returned by the worker. /// Handle returned by the worker. /// COM-marshalable value to write. /// MXAccess user id (security classification) for the write. public void Write( int serverHandle, int itemHandle, object? value, int userId) { ThrowIfDisposed(); mxAccessServer.Write(serverHandle, itemHandle, value, userId); } /// Writes a value with an explicit source timestamp to an item. /// Handle returned by the worker. /// Handle returned by the worker. /// COM-marshalable value to write. /// COM-marshalable source timestamp for the write. /// MXAccess user id (security classification) for the write. public void Write2( int serverHandle, int itemHandle, object? value, object? timestamp, int userId) { ThrowIfDisposed(); mxAccessServer.Write2(serverHandle, itemHandle, value, timestamp, userId); } /// Performs a secured/verified write to an item. /// Handle returned by the worker. /// Handle returned by the worker. /// MXAccess user id of the operator performing the write. /// MXAccess user id of the verifier authorizing the write. /// COM-marshalable value to write. public void WriteSecured( int serverHandle, int itemHandle, int currentUserId, int verifierUserId, object? value) { ThrowIfDisposed(); mxAccessServer.WriteSecured(serverHandle, itemHandle, currentUserId, verifierUserId, value); } /// Performs a secured/verified write with an explicit source timestamp. /// Handle returned by the worker. /// Handle returned by the worker. /// MXAccess user id of the operator performing the write. /// MXAccess user id of the verifier authorizing the write. /// COM-marshalable value to write. /// COM-marshalable source timestamp for the write. public void WriteSecured2( int serverHandle, int itemHandle, int currentUserId, int verifierUserId, object? value, object? timestamp) { ThrowIfDisposed(); mxAccessServer.WriteSecured2(serverHandle, itemHandle, currentUserId, verifierUserId, value, timestamp); } /// Adds multiple items in bulk, returning success/failure results. /// Handle returned by the worker. /// Enumerable of item definitions to add. public IReadOnlyList AddItemBulk( int serverHandle, IEnumerable tagAddresses) { ThrowIfDisposed(); if (tagAddresses is null) { throw new ArgumentNullException(nameof(tagAddresses)); } List results = new(); foreach (string? tagAddress in tagAddresses) { if (string.IsNullOrWhiteSpace(tagAddress)) { results.Add(Failed(serverHandle, tagAddress ?? string.Empty, itemHandle: 0, "Tag address is required.")); continue; } try { int itemHandle = AddItem(serverHandle, tagAddress); results.Add(Succeeded(serverHandle, tagAddress, itemHandle)); } catch (Exception exception) { results.Add(Failed(serverHandle, tagAddress, itemHandle: 0, exception.Message)); } } return results; } /// Advises on multiple items in bulk, returning success/failure results. /// Handle returned by the worker. /// Enumerable of item handles to advise on. public IReadOnlyList AdviseItemBulk( int serverHandle, IEnumerable itemHandles) { ThrowIfDisposed(); if (itemHandles is null) { throw new ArgumentNullException(nameof(itemHandles)); } List results = new(); foreach (int itemHandle in itemHandles) { try { Advise(serverHandle, itemHandle); results.Add(Succeeded(serverHandle, string.Empty, itemHandle)); } catch (Exception exception) { results.Add(Failed(serverHandle, string.Empty, itemHandle, exception.Message)); } } return results; } /// Removes multiple items in bulk, returning success/failure results. /// Handle returned by the worker. /// Enumerable of item handles to remove. public IReadOnlyList RemoveItemBulk( int serverHandle, IEnumerable itemHandles) { ThrowIfDisposed(); if (itemHandles is null) { throw new ArgumentNullException(nameof(itemHandles)); } List results = new(); foreach (int itemHandle in itemHandles) { try { RemoveItem(serverHandle, itemHandle); results.Add(Succeeded(serverHandle, string.Empty, itemHandle)); } catch (Exception exception) { results.Add(Failed(serverHandle, string.Empty, itemHandle, exception.Message)); } } return results; } /// Removes advice subscriptions from multiple items in bulk, returning success/failure results. /// Handle returned by the worker. /// Enumerable of item handles to unadvise. public IReadOnlyList UnAdviseItemBulk( int serverHandle, IEnumerable itemHandles) { ThrowIfDisposed(); if (itemHandles is null) { throw new ArgumentNullException(nameof(itemHandles)); } List results = new(); foreach (int itemHandle in itemHandles) { try { UnAdvise(serverHandle, itemHandle); results.Add(Succeeded(serverHandle, string.Empty, itemHandle)); } catch (Exception exception) { results.Add(Failed(serverHandle, string.Empty, itemHandle, exception.Message)); } } return results; } /// Adds multiple items and subscribes to them in bulk, returning success/failure results. /// Handle returned by the worker. /// Enumerable of item definitions to add and subscribe to. public IReadOnlyList SubscribeBulk( int serverHandle, IEnumerable tagAddresses) { ThrowIfDisposed(); if (tagAddresses is null) { throw new ArgumentNullException(nameof(tagAddresses)); } List results = new(); foreach (string? tagAddress in tagAddresses) { if (string.IsNullOrWhiteSpace(tagAddress)) { results.Add(Failed(serverHandle, tagAddress ?? string.Empty, itemHandle: 0, "Tag address is required.")); continue; } int itemHandle = 0; try { itemHandle = AddItem(serverHandle, tagAddress); Advise(serverHandle, itemHandle); results.Add(Succeeded(serverHandle, tagAddress, itemHandle)); } catch (Exception exception) { string errorMessage = exception.Message; if (itemHandle != 0) { errorMessage = AppendRemoveItemCleanup(serverHandle, itemHandle, errorMessage); } results.Add(Failed(serverHandle, tagAddress, itemHandle, errorMessage)); } } return results; } /// Unsubscribes from multiple items in bulk, returning success/failure results. /// Handle returned by the worker. /// Enumerable of item handles to unsubscribe from. public IReadOnlyList UnsubscribeBulk( int serverHandle, IEnumerable itemHandles) { ThrowIfDisposed(); if (itemHandles is null) { throw new ArgumentNullException(nameof(itemHandles)); } List results = new(); foreach (int itemHandle in itemHandles) { List errors = new(); try { UnAdvise(serverHandle, itemHandle); } catch (Exception exception) { errors.Add($"UnAdvise failed: {exception.Message}"); } try { RemoveItem(serverHandle, itemHandle); } catch (Exception exception) { errors.Add($"RemoveItem failed: {exception.Message}"); } results.Add(errors.Count == 0 ? Succeeded(serverHandle, string.Empty, itemHandle) : Failed(serverHandle, string.Empty, itemHandle, string.Join("; ", errors))); } return results; } /// /// Bulk write — runs sequentially for each entry. /// Each entry's turns the protobuf /// MxValue into a COM-marshalable variant. Per-item failures are /// captured as entries with /// was_successful = false; the loop never throws. /// public IReadOnlyList WriteBulk( int serverHandle, IReadOnlyList entries, Func convertValue) { ThrowIfDisposed(); if (entries is null) { throw new ArgumentNullException(nameof(entries)); } if (convertValue is null) { throw new ArgumentNullException(nameof(convertValue)); } List results = new(entries.Count); foreach (WriteBulkEntry entry in entries) { results.Add(ExecuteBulkWriteEntry( serverHandle, entry.ItemHandle, () => Write(serverHandle, entry.ItemHandle, convertValue(entry.Value), entry.UserId))); } return results; } /// Bulk Write2 — sequential MXAccess per entry. public IReadOnlyList Write2Bulk( int serverHandle, IReadOnlyList entries, Func convertValue) { ThrowIfDisposed(); if (entries is null) { throw new ArgumentNullException(nameof(entries)); } if (convertValue is null) { throw new ArgumentNullException(nameof(convertValue)); } List results = new(entries.Count); foreach (Write2BulkEntry entry in entries) { results.Add(ExecuteBulkWriteEntry( serverHandle, entry.ItemHandle, () => Write2( serverHandle, entry.ItemHandle, convertValue(entry.Value), convertValue(entry.TimestampValue), entry.UserId))); } return results; } /// Bulk WriteSecured — sequential MXAccess per entry. public IReadOnlyList WriteSecuredBulk( int serverHandle, IReadOnlyList entries, Func convertValue) { ThrowIfDisposed(); if (entries is null) { throw new ArgumentNullException(nameof(entries)); } if (convertValue is null) { throw new ArgumentNullException(nameof(convertValue)); } List results = new(entries.Count); foreach (WriteSecuredBulkEntry entry in entries) { results.Add(ExecuteBulkWriteEntry( serverHandle, entry.ItemHandle, () => WriteSecured( serverHandle, entry.ItemHandle, entry.CurrentUserId, entry.VerifierUserId, convertValue(entry.Value)))); } return results; } /// Bulk WriteSecured2 — sequential MXAccess per entry. public IReadOnlyList WriteSecured2Bulk( int serverHandle, IReadOnlyList entries, Func convertValue) { ThrowIfDisposed(); if (entries is null) { throw new ArgumentNullException(nameof(entries)); } if (convertValue is null) { throw new ArgumentNullException(nameof(convertValue)); } List results = new(entries.Count); foreach (WriteSecured2BulkEntry entry in entries) { results.Add(ExecuteBulkWriteEntry( serverHandle, entry.ItemHandle, () => WriteSecured2( serverHandle, entry.ItemHandle, entry.CurrentUserId, entry.VerifierUserId, convertValue(entry.Value), convertValue(entry.TimestampValue)))); } return results; } /// /// Bulk read snapshot. For each requested tag, returns the most recent /// OnDataChange value if the tag is already advised AND a cached value /// exists (no subscription side effects); otherwise takes the AddItem /// + Advise + wait + UnAdvise + RemoveItem snapshot lifecycle itself. /// bounds the wait per-tag in the snapshot /// case; is invoked on every poll /// iteration so the worker's STA can dispatch the incoming MXAccess /// message that carries the value. /// public IReadOnlyList ReadBulk( int serverHandle, IReadOnlyList tagAddresses, TimeSpan timeout, Action pumpStep) { ThrowIfDisposed(); if (tagAddresses is null) { throw new ArgumentNullException(nameof(tagAddresses)); } if (pumpStep is null) { throw new ArgumentNullException(nameof(pumpStep)); } List results = new(tagAddresses.Count); foreach (string? tagAddress in tagAddresses) { if (string.IsNullOrWhiteSpace(tagAddress)) { results.Add(FailedRead(serverHandle, tagAddress ?? string.Empty, itemHandle: 0, wasCached: false, "Tag address is required.")); continue; } results.Add(ReadOneTag(serverHandle, tagAddress, timeout, pumpStep)); } return results; } private BulkReadResult ReadOneTag( int serverHandle, string tagAddress, TimeSpan timeout, Action pumpStep) { // 1. Cached-and-advised fast path: scan the registry for a live item // handle matching this tag and check whether the value cache has a // payload for it. If so, return the cached value without touching // the existing subscription — the caller didn't create it, so // ReadBulk must not tear it down. if (TryGetCachedReadFor(serverHandle, tagAddress, out int cachedItemHandle, out MxAccessValueCache.CachedValue cachedValue)) { return SucceededRead( serverHandle, tagAddress, cachedItemHandle, wasCached: true, cachedValue); } // 2. Snapshot lifecycle. Reserve our own item handle, advise, pump // until we see a fresh OnDataChange (or the deadline elapses), // then tear it down. int itemHandle = 0; bool advised = false; try { itemHandle = AddItem(serverHandle, tagAddress); ulong baseline = valueCache.CurrentVersion(serverHandle, itemHandle); Advise(serverHandle, itemHandle); advised = true; DateTime deadline = DateTime.UtcNow + timeout; bool gotValue = valueCache.TryWaitForUpdate( serverHandle, itemHandle, baseline, deadline, pumpStep, out MxAccessValueCache.CachedValue snapshot); return gotValue ? SucceededRead(serverHandle, tagAddress, itemHandle, wasCached: false, snapshot) : FailedRead( serverHandle, tagAddress, itemHandle, wasCached: false, $"ReadBulk timed out after {timeout.TotalMilliseconds:F0} ms waiting for first OnDataChange."); } catch (Exception exception) { return FailedRead(serverHandle, tagAddress, itemHandle, wasCached: false, exception.Message); } finally { // Snapshot teardown — best-effort. Errors here are noted on the // diagnostic message of the original result (above) by appending // a cleanup suffix; we never re-throw from finally. if (advised) { try { UnAdvise(serverHandle, itemHandle); } catch { /* swallow — best effort */ } } if (itemHandle != 0) { try { RemoveItem(serverHandle, itemHandle); } catch { /* swallow — best effort */ } } } } private bool TryGetCachedReadFor( int serverHandle, string tagAddress, out int itemHandle, out MxAccessValueCache.CachedValue cachedValue) { // Linear scan — bulk-read sizes are small in practice and the registry // is keyed by handle, not by tag. If profiling ever shows this hot, a // reverse tag→handle map can be added on the registry side. foreach (RegisteredItemHandle registered in handleRegistry.ItemHandles) { if (registered.ServerHandle != serverHandle) { continue; } if (!string.Equals(registered.ItemDefinition, tagAddress, StringComparison.Ordinal)) { continue; } if (!handleRegistry.ContainsAdviceHandle(serverHandle, registered.ItemHandle, MxAccessAdviceKind.Plain) && !handleRegistry.ContainsAdviceHandle(serverHandle, registered.ItemHandle, MxAccessAdviceKind.Supervisory)) { // Tag is added but not advised — no fresh OnDataChange will // arrive without us advising. Fall through to the snapshot // path which advises explicitly. continue; } if (valueCache.TryGet(serverHandle, registered.ItemHandle, out cachedValue)) { itemHandle = registered.ItemHandle; return true; } } itemHandle = 0; cachedValue = default; return false; } private BulkWriteResult ExecuteBulkWriteEntry( int serverHandle, int itemHandle, Action invokeWrite) { try { invokeWrite(); return new BulkWriteResult { ServerHandle = serverHandle, ItemHandle = itemHandle, WasSuccessful = true, ErrorMessage = string.Empty, }; } catch (System.Runtime.InteropServices.COMException comException) { BulkWriteResult result = new() { ServerHandle = serverHandle, ItemHandle = itemHandle, WasSuccessful = false, ErrorMessage = comException.Message, }; result.Hresult = comException.HResult; return result; } catch (Exception exception) { return new BulkWriteResult { ServerHandle = serverHandle, ItemHandle = itemHandle, WasSuccessful = false, ErrorMessage = exception.Message, }; } } private static BulkReadResult SucceededRead( int serverHandle, string tagAddress, int itemHandle, bool wasCached, MxAccessValueCache.CachedValue snapshot) { BulkReadResult result = new() { ServerHandle = serverHandle, TagAddress = tagAddress, ItemHandle = itemHandle, WasSuccessful = true, WasCached = wasCached, Quality = snapshot.Quality, ErrorMessage = string.Empty, }; if (snapshot.Value is not null) { result.Value = snapshot.Value; } if (snapshot.SourceTimestamp is not null) { result.SourceTimestamp = snapshot.SourceTimestamp; } if (snapshot.Statuses is not null) { result.Statuses.Add(snapshot.Statuses); } return result; } private static BulkReadResult FailedRead( int serverHandle, string tagAddress, int itemHandle, bool wasCached, string errorMessage) { return new BulkReadResult { ServerHandle = serverHandle, TagAddress = tagAddress, ItemHandle = itemHandle, WasSuccessful = false, WasCached = wasCached, ErrorMessage = errorMessage, }; } /// Gracefully shuts down the session, cleaning up all handles. public MxAccessShutdownResult ShutdownGracefully() { if (disposed) { return new MxAccessShutdownResult(Array.Empty()); } List failures = new(); CleanupAdviceHandles(failures); CleanupItemHandles(failures); CleanupServerHandles(failures); DisposeCore(failures); return new MxAccessShutdownResult(failures); } /// Releases the MXAccess COM object and resources. public void Dispose() { if (disposed) { return; } DisposeCore(failures: null); } private void CleanupAdviceHandles(ICollection failures) { HashSet cleanedPairs = new(); foreach (RegisteredAdviceHandle adviceHandle in handleRegistry.AdviceHandles) { long key = CreateItemKey(adviceHandle.ServerHandle, adviceHandle.ItemHandle); if (!cleanedPairs.Add(key)) { continue; } try { mxAccessServer.UnAdvise(adviceHandle.ServerHandle, adviceHandle.ItemHandle); handleRegistry.RemoveAdviceHandles(adviceHandle.ServerHandle, adviceHandle.ItemHandle); } catch (Exception exception) { failures.Add(new MxAccessShutdownFailure( nameof(UnAdvise), adviceHandle.ServerHandle, adviceHandle.ItemHandle, exception)); } } } private void CleanupItemHandles(ICollection failures) { foreach (RegisteredItemHandle itemHandle in handleRegistry.ItemHandles) { try { mxAccessServer.RemoveItem(itemHandle.ServerHandle, itemHandle.ItemHandle); handleRegistry.RemoveItemHandle(itemHandle.ServerHandle, itemHandle.ItemHandle); } catch (Exception exception) { failures.Add(new MxAccessShutdownFailure( nameof(RemoveItem), itemHandle.ServerHandle, itemHandle.ItemHandle, exception)); } } } private void CleanupServerHandles(ICollection failures) { foreach (RegisteredServerHandle serverHandle in handleRegistry.ServerHandles) { try { mxAccessServer.Unregister(serverHandle.ServerHandle); handleRegistry.UnregisterServerHandle(serverHandle.ServerHandle); } catch (Exception exception) { failures.Add(new MxAccessShutdownFailure( nameof(Unregister), serverHandle.ServerHandle, itemHandle: null, exception)); } } } private static long CreateItemKey( int serverHandle, int itemHandle) { return ((long)serverHandle << 32) | (uint)itemHandle; } private string AppendRemoveItemCleanup( int serverHandle, int itemHandle, string errorMessage) { try { RemoveItem(serverHandle, itemHandle); return $"{errorMessage}; cleanup RemoveItem succeeded."; } catch (Exception cleanupException) { return $"{errorMessage}; cleanup RemoveItem failed: {cleanupException.Message}"; } } private static SubscribeResult Succeeded( int serverHandle, string tagAddress, int itemHandle) { return new SubscribeResult { ServerHandle = serverHandle, TagAddress = tagAddress, ItemHandle = itemHandle, WasSuccessful = true, ErrorMessage = string.Empty, }; } private static SubscribeResult Failed( int serverHandle, string tagAddress, int itemHandle, string errorMessage) { return new SubscribeResult { ServerHandle = serverHandle, TagAddress = tagAddress, ItemHandle = itemHandle, WasSuccessful = false, ErrorMessage = errorMessage, }; } private void DisposeCore(ICollection? failures) { Exception? detachException = null; try { eventSink.Detach(); } catch (Exception exception) { detachException = exception; failures?.Add(new MxAccessShutdownFailure( "DetachEvents", serverHandle: null, itemHandle: null, exception)); } try { if (Marshal.IsComObject(mxAccessComObject)) { Marshal.FinalReleaseComObject(mxAccessComObject); } } catch (Exception exception) when (failures is not null) { failures.Add(new MxAccessShutdownFailure( "ReleaseComObject", serverHandle: null, itemHandle: null, exception)); } disposed = true; if (detachException is not null && failures is null) { throw detachException; } } private void ThrowIfDisposed() { if (disposed) { throw new ObjectDisposedException(nameof(MxAccessSession)); } } }