From 16898d3ad6847a136de88e88e556811bfe695863 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Otto=20Boly=C3=B3s?= Date: Wed, 3 Jun 2026 07:11:04 +0200 Subject: [PATCH 01/37] test(http-tests): pin multicast isolation across all client events --- .../Clients/MulticastIsolationTests.cs | 454 ++++++++++++++++++ ...evicesAndDeviceReceivedIntegrationTests.cs | 199 ++++++++ 2 files changed, 653 insertions(+) create mode 100644 tests/MTConnect.NET-HTTP-Tests/Clients/MulticastIsolationTests.cs create mode 100644 tests/MTConnect.NET-HTTP-Tests/Integration/DevicesAndDeviceReceivedIntegrationTests.cs diff --git a/tests/MTConnect.NET-HTTP-Tests/Clients/MulticastIsolationTests.cs b/tests/MTConnect.NET-HTTP-Tests/Clients/MulticastIsolationTests.cs new file mode 100644 index 00000000..ad10b7c5 --- /dev/null +++ b/tests/MTConnect.NET-HTTP-Tests/Clients/MulticastIsolationTests.cs @@ -0,0 +1,454 @@ +// Copyright (c) 2026 TrakHound Inc., All Rights Reserved. +// TrakHound Inc. licenses this file to you under the MIT license. + +using MTConnect.Assets.CuttingTools; +using MTConnect.Clients; +using MTConnect.Observations.Events; +using NUnit.Framework; +using System; +using System.Threading; + +namespace MTConnect.Tests.Http.Clients +{ + /// + /// Drives the streaming MTConnectHttpClient against the real embedded + /// MTConnectHttpServer started by AgentRunner and pins, for every sibling + /// event the client raises (CurrentReceived, SampleReceived, + /// ObservationReceived, AssetReceived, ProbeReceived, AssetsReceived), the + /// two halves of the multicast-isolation contract: a subscriber that throws + /// cannot starve later subscribers in the invocation list, and a fault + /// raised by the InternalError handler itself must also be swallowed so the + /// fan-out keeps going. Mirrors the DeviceReceived coverage already in + /// HttpClientDeviceModel. + /// + [TestFixture] + public class MulticastIsolationTests : HttpClientFixture + { + // Generous CI-safe bounds. The streaming client raises ProbeReceived, + // DeviceReceived, and CurrentReceived from the first probe and current + // round trip respectively; AgentRunner guarantees both are answerable + // before Start() returns. SampleReceived / ObservationReceived / + // AssetReceived require a follow-on broker push, so the deadline + // covers the streamed round trip. + private const int EventWaitTimeoutMs = 30000; + + private const string AssetDataItemId = "dev1_asset_chg"; + private const string AvailabilityDataItemId = "L2avail"; + + + // --------------------------------------------------------------------- + // ProbeReceived + // --------------------------------------------------------------------- + + /// Pins the behaviour expressed by the test name: probe received fires for all subscribers when one throws. + [Test] + public void ProbeReceivedFiresForAllSubscribersWhenOneThrows() + { + var client = new MTConnectHttpClient(Hostname, Port); + + client.ProbeReceived += (_, _) => throw new InvalidOperationException("first handler throws"); + + using var recorded = new ManualResetEventSlim(false); + client.ProbeReceived += (_, doc) => + { + if (doc != null) recorded.Set(); + }; + + try + { + client.Start(); + + Assert.That(recorded.Wait(EventWaitTimeoutMs), Is.True, + "subscribers after a throwing one must still receive ProbeReceived"); + } + finally + { + client.Stop(); + } + } + + /// Pins the behaviour expressed by the test name: internal error handler throwing does not break probe received fan out. + [Test] + public void InternalErrorHandlerThrowingDoesNotBreakProbeReceivedFanOut() + { + var client = new MTConnectHttpClient(Hostname, Port); + + client.InternalError += (_, _) => throw new InvalidOperationException("InternalError throws"); + client.ProbeReceived += (_, _) => throw new InvalidOperationException("first ProbeReceived throws"); + + using var recorded = new ManualResetEventSlim(false); + client.ProbeReceived += (_, doc) => + { + if (doc != null) recorded.Set(); + }; + + try + { + client.Start(); + + Assert.That(recorded.Wait(EventWaitTimeoutMs), Is.True, + "InternalError throwing must not break the ProbeReceived fan-out"); + } + finally + { + client.Stop(); + } + } + + + // --------------------------------------------------------------------- + // CurrentReceived + // --------------------------------------------------------------------- + + /// Pins the behaviour expressed by the test name: current received fires for all subscribers when one throws. + [Test] + public void CurrentReceivedFiresForAllSubscribersWhenOneThrows() + { + var client = new MTConnectHttpClient(Hostname, Port); + + client.CurrentReceived += (_, _) => throw new InvalidOperationException("first handler throws"); + + using var recorded = new ManualResetEventSlim(false); + client.CurrentReceived += (_, doc) => + { + if (doc != null) recorded.Set(); + }; + + try + { + client.Start(); + + Assert.That(recorded.Wait(EventWaitTimeoutMs), Is.True, + "subscribers after a throwing one must still receive CurrentReceived"); + } + finally + { + client.Stop(); + } + } + + /// Pins the behaviour expressed by the test name: internal error handler throwing does not break current received fan out. + [Test] + public void InternalErrorHandlerThrowingDoesNotBreakCurrentReceivedFanOut() + { + var client = new MTConnectHttpClient(Hostname, Port); + + client.InternalError += (_, _) => throw new InvalidOperationException("InternalError throws"); + client.CurrentReceived += (_, _) => throw new InvalidOperationException("first CurrentReceived throws"); + + using var recorded = new ManualResetEventSlim(false); + client.CurrentReceived += (_, doc) => + { + if (doc != null) recorded.Set(); + }; + + try + { + client.Start(); + + Assert.That(recorded.Wait(EventWaitTimeoutMs), Is.True, + "InternalError throwing must not break the CurrentReceived fan-out"); + } + finally + { + client.Stop(); + } + } + + + // --------------------------------------------------------------------- + // SampleReceived + // --------------------------------------------------------------------- + + /// Pins the behaviour expressed by the test name: sample received fires for all subscribers when one throws. + [Test] + public void SampleReceivedFiresForAllSubscribersWhenOneThrows() + { + var client = new MTConnectHttpClient(Hostname, Port); + + client.SampleReceived += (_, _) => throw new InvalidOperationException("first handler throws"); + + using var recorded = new ManualResetEventSlim(false); + client.SampleReceived += (_, doc) => + { + if (doc != null) recorded.Set(); + }; + + // SampleReceived fires on streamed sample responses, which only + // start after the initial Current round trip. Wait for the seed + // CurrentReceived before pushing the observation so the streaming + // sample loop is established and ready to deliver. + using var currentSeed = new ManualResetEventSlim(false); + client.CurrentReceived += (_, _) => currentSeed.Set(); + + try + { + client.Start(); + + Assert.That(currentSeed.Wait(EventWaitTimeoutMs), Is.True, + "Streamed Current did not deliver the seed before the test could push a sample"); + + AgentRunner.Agent.AddObservation(DeviceUuid, AvailabilityDataItemId, Availability.AVAILABLE); + + Assert.That(recorded.Wait(EventWaitTimeoutMs), Is.True, + "subscribers after a throwing one must still receive SampleReceived"); + } + finally + { + client.Stop(); + } + } + + /// Pins the behaviour expressed by the test name: internal error handler throwing does not break sample received fan out. + [Test] + public void InternalErrorHandlerThrowingDoesNotBreakSampleReceivedFanOut() + { + var client = new MTConnectHttpClient(Hostname, Port); + + client.InternalError += (_, _) => throw new InvalidOperationException("InternalError throws"); + client.SampleReceived += (_, _) => throw new InvalidOperationException("first SampleReceived throws"); + + using var recorded = new ManualResetEventSlim(false); + client.SampleReceived += (_, doc) => + { + if (doc != null) recorded.Set(); + }; + + using var currentSeed = new ManualResetEventSlim(false); + client.CurrentReceived += (_, _) => currentSeed.Set(); + + try + { + client.Start(); + + Assert.That(currentSeed.Wait(EventWaitTimeoutMs), Is.True, + "Streamed Current did not deliver the seed before the test could push a sample"); + + AgentRunner.Agent.AddObservation(DeviceUuid, AvailabilityDataItemId, Availability.AVAILABLE); + + Assert.That(recorded.Wait(EventWaitTimeoutMs), Is.True, + "InternalError throwing must not break the SampleReceived fan-out"); + } + finally + { + client.Stop(); + } + } + + + // --------------------------------------------------------------------- + // ObservationReceived + // --------------------------------------------------------------------- + + /// Pins the behaviour expressed by the test name: observation received fires for all subscribers when one throws. + [Test] + public void ObservationReceivedFiresForAllSubscribersWhenOneThrows() + { + var client = new MTConnectHttpClient(Hostname, Port); + + client.ObservationReceived += (_, _) => throw new InvalidOperationException("first handler throws"); + + using var recorded = new ManualResetEventSlim(false); + client.ObservationReceived += (_, observation) => + { + if (observation != null) recorded.Set(); + }; + + try + { + client.Start(); + + Assert.That(recorded.Wait(EventWaitTimeoutMs), Is.True, + "subscribers after a throwing one must still receive ObservationReceived"); + } + finally + { + client.Stop(); + } + } + + /// Pins the behaviour expressed by the test name: internal error handler throwing does not break observation received fan out. + [Test] + public void InternalErrorHandlerThrowingDoesNotBreakObservationReceivedFanOut() + { + var client = new MTConnectHttpClient(Hostname, Port); + + client.InternalError += (_, _) => throw new InvalidOperationException("InternalError throws"); + client.ObservationReceived += (_, _) => throw new InvalidOperationException("first ObservationReceived throws"); + + using var recorded = new ManualResetEventSlim(false); + client.ObservationReceived += (_, observation) => + { + if (observation != null) recorded.Set(); + }; + + try + { + client.Start(); + + Assert.That(recorded.Wait(EventWaitTimeoutMs), Is.True, + "InternalError throwing must not break the ObservationReceived fan-out"); + } + finally + { + client.Stop(); + } + } + + + // --------------------------------------------------------------------- + // AssetsReceived / AssetReceived + // + // Triggered through CheckAssetChanged: the streaming client sees an + // AssetChanged observation in the streamed Current/Sample envelope and + // issues a GetAsset request whose response raises AssetsReceived (the + // envelope) and AssetReceived (each contained asset). The seed Okuma + // device file carries an ASSET_CHANGED data item (id = dev1_asset_chg) + // and the broker accepts the matching observation. + // --------------------------------------------------------------------- + + /// Pins the behaviour expressed by the test name: assets received fires for all subscribers when one throws. + [Test] + public void AssetsReceivedFiresForAllSubscribersWhenOneThrows() + { + var client = new MTConnectHttpClient(Hostname, Port); + + client.AssetsReceived += (_, _) => throw new InvalidOperationException("first handler throws"); + + using var recorded = new ManualResetEventSlim(false); + client.AssetsReceived += (_, doc) => + { + if (doc != null) recorded.Set(); + }; + + try + { + client.Start(); + + SeedAssetAndFireAssetChanged(); + + Assert.That(recorded.Wait(EventWaitTimeoutMs), Is.True, + "subscribers after a throwing one must still receive AssetsReceived"); + } + finally + { + client.Stop(); + } + } + + /// Pins the behaviour expressed by the test name: internal error handler throwing does not break assets received fan out. + [Test] + public void InternalErrorHandlerThrowingDoesNotBreakAssetsReceivedFanOut() + { + var client = new MTConnectHttpClient(Hostname, Port); + + client.InternalError += (_, _) => throw new InvalidOperationException("InternalError throws"); + client.AssetsReceived += (_, _) => throw new InvalidOperationException("first AssetsReceived throws"); + + using var recorded = new ManualResetEventSlim(false); + client.AssetsReceived += (_, doc) => + { + if (doc != null) recorded.Set(); + }; + + try + { + client.Start(); + + SeedAssetAndFireAssetChanged(); + + Assert.That(recorded.Wait(EventWaitTimeoutMs), Is.True, + "InternalError throwing must not break the AssetsReceived fan-out"); + } + finally + { + client.Stop(); + } + } + + /// Pins the behaviour expressed by the test name: asset received fires for all subscribers when one throws. + [Test] + public void AssetReceivedFiresForAllSubscribersWhenOneThrows() + { + var client = new MTConnectHttpClient(Hostname, Port); + + client.AssetReceived += (_, _) => throw new InvalidOperationException("first handler throws"); + + using var recorded = new ManualResetEventSlim(false); + client.AssetReceived += (_, asset) => + { + if (asset != null) recorded.Set(); + }; + + try + { + client.Start(); + + SeedAssetAndFireAssetChanged(); + + Assert.That(recorded.Wait(EventWaitTimeoutMs), Is.True, + "subscribers after a throwing one must still receive AssetReceived"); + } + finally + { + client.Stop(); + } + } + + /// Pins the behaviour expressed by the test name: internal error handler throwing does not break asset received fan out. + [Test] + public void InternalErrorHandlerThrowingDoesNotBreakAssetReceivedFanOut() + { + var client = new MTConnectHttpClient(Hostname, Port); + + client.InternalError += (_, _) => throw new InvalidOperationException("InternalError throws"); + client.AssetReceived += (_, _) => throw new InvalidOperationException("first AssetReceived throws"); + + using var recorded = new ManualResetEventSlim(false); + client.AssetReceived += (_, asset) => + { + if (asset != null) recorded.Set(); + }; + + try + { + client.Start(); + + SeedAssetAndFireAssetChanged(); + + Assert.That(recorded.Wait(EventWaitTimeoutMs), Is.True, + "InternalError throwing must not break the AssetReceived fan-out"); + } + finally + { + client.Stop(); + } + } + + + // --------------------------------------------------------------------- + // Asset seeding helper. Adds a CuttingTool asset to the broker against + // the seed device's UUID, then pushes an AssetChanged observation that + // names it; the streamed observation reaches the client and drives + // CheckAssetChanged into GetAssetAsync, which raises AssetsReceived + // and AssetReceived. + // --------------------------------------------------------------------- + private void SeedAssetAndFireAssetChanged() + { + var assetId = $"asset-{Guid.NewGuid():N}"; + + var asset = new CuttingToolAsset + { + AssetId = assetId, + ToolId = "T1", + Timestamp = DateTime.UtcNow, + DeviceUuid = DeviceUuid, + }; + AgentRunner.Agent.AddAsset(DeviceUuid, asset); + + // The AssetChanged data item lives on the Okuma seed device; the + // broker accepts the observation by device UUID + data item id. + AgentRunner.Agent.AddObservation(DeviceUuid, AssetDataItemId, assetId); + } + } +} diff --git a/tests/MTConnect.NET-HTTP-Tests/Integration/DevicesAndDeviceReceivedIntegrationTests.cs b/tests/MTConnect.NET-HTTP-Tests/Integration/DevicesAndDeviceReceivedIntegrationTests.cs new file mode 100644 index 00000000..7205b481 --- /dev/null +++ b/tests/MTConnect.NET-HTTP-Tests/Integration/DevicesAndDeviceReceivedIntegrationTests.cs @@ -0,0 +1,199 @@ +// Copyright (c) 2026 TrakHound Inc., All Rights Reserved. +// TrakHound Inc. licenses this file to you under the MIT license. + +using MTConnect.Clients; +using MTConnect.Devices; +using MTConnect.Tests.Agents; +using NUnit.Framework; +using System.Collections.Generic; +using System.Linq; +using System.Threading; + +namespace MTConnect.Tests.Http.Integration +{ + /// + /// End-to-end coverage for the Devices snapshot accessor and the + /// DeviceReceived event on the streaming MTConnectHttpClient. Each test + /// boots a fresh AgentRunner so the probe + cache + event flow is + /// exercised against a real embedded MTConnectHttpServer, with no shared + /// state between scenarios. + /// + [TestFixture] + [Category("Integration")] + public class DevicesAndDeviceReceivedIntegrationTests + { + private const string Hostname = "127.0.0.1"; + private const string DeviceName = "OKUMA-Lathe"; + + private const int ProbeWaitTimeoutMs = 30000; + + + /// Pins the behaviour expressed by the test name: end to end probe populates Devices and fires DeviceReceived. + [Test] + public void EndToEnd_Probe_Populates_Devices_And_Fires_DeviceReceived() + { + var port = AgentRunner.GetFreePort(); + using var runner = new AgentRunner(port); + runner.Start(); + + var client = new MTConnectHttpClient(Hostname, port); + + var received = new List(); + var receivedLock = new object(); + client.DeviceReceived += (_, device) => + { + lock (receivedLock) { received.Add(device); } + }; + + using var probeSignal = new ManualResetEventSlim(false); + client.ProbeReceived += (_, _) => probeSignal.Set(); + + try + { + client.Start(); + + Assert.That(probeSignal.Wait(ProbeWaitTimeoutMs), Is.True, "ProbeReceived did not fire within the timeout"); + + // Devices snapshot reflects the probe round trip; the implicit + // Agent device is included alongside every device the agent + // hosts, so the count is +1 vs runner.Devices. + var expectedCount = runner.Devices.Count() + 1; + + Assert.That(client.Devices.Count, Is.EqualTo(expectedCount), + "Devices accessor was not populated end-to-end"); + Assert.That(client.Devices.Values.Any(d => d.Name == DeviceName), Is.True, + $"Devices accessor did not surface the {DeviceName} device"); + + List snapshot; + lock (receivedLock) { snapshot = new List(received); } + + Assert.That(snapshot.Count, Is.EqualTo(expectedCount), + "DeviceReceived did not fire once per probed device end-to-end"); + } + finally + { + client.Stop(); + runner.Stop(); + } + } + + /// Pins the behaviour expressed by the test name: end to end subsequent probe repopulates cache and refires DeviceReceived. + [Test] + public void EndToEnd_Subsequent_Probe_Repopulates_Cache_And_Refires_DeviceReceived() + { + var port = AgentRunner.GetFreePort(); + using var runner = new AgentRunner(port); + runner.Start(); + + var client = new MTConnectHttpClient(Hostname, port); + + using var firstProbe = new ManualResetEventSlim(false); + using var secondProbe = new ManualResetEventSlim(false); + var probeCount = 0; + client.ProbeReceived += (_, _) => + { + var n = Interlocked.Increment(ref probeCount); + if (n == 1) firstProbe.Set(); + else if (n == 2) secondProbe.Set(); + }; + + var deviceFireCount = 0; + client.DeviceReceived += (_, _) => Interlocked.Increment(ref deviceFireCount); + + var expectedCount = runner.Devices.Count() + 1; + + try + { + client.Start(); + Assert.That(firstProbe.Wait(ProbeWaitTimeoutMs), Is.True, "First ProbeReceived did not fire within the timeout"); + Assert.That(Volatile.Read(ref deviceFireCount), Is.EqualTo(expectedCount), + "First probe did not fire DeviceReceived once per device"); + } + finally + { + client.Stop(); + } + + try + { + client.Start(); + Assert.That(secondProbe.Wait(ProbeWaitTimeoutMs), Is.True, "Second ProbeReceived did not fire within the timeout"); + + Assert.That(Volatile.Read(ref deviceFireCount), Is.EqualTo(expectedCount * 2), + "Second probe did not re-fire DeviceReceived once per device"); + + // The cache is cleared on every populated probe; the snapshot + // count must equal the per-probe device count, not double it. + Assert.That(client.Devices.Count, Is.EqualTo(expectedCount), + "Devices accessor accumulated entries across probes instead of replacing the snapshot"); + } + finally + { + client.Stop(); + runner.Stop(); + } + } + + /// Pins the behaviour expressed by the test name: end to end empty probe does not evict cache. + [Test] + public void EndToEnd_Empty_Probe_Does_Not_Evict_Cache() + { + var port = AgentRunner.GetFreePort(); + using var runner = new AgentRunner(port); + runner.Start(); + + var client = new MTConnectHttpClient(Hostname, port); + + using var firstProbe = new ManualResetEventSlim(false); + client.ProbeReceived += (_, _) => firstProbe.Set(); + + var expectedCount = runner.Devices.Count() + 1; + + try + { + client.Start(); + Assert.That(firstProbe.Wait(ProbeWaitTimeoutMs), Is.True, "ProbeReceived did not fire within the timeout"); + + Assert.That(client.Devices.Count, Is.EqualTo(expectedCount), + "Devices accessor was not populated by the first probe"); + } + finally + { + client.Stop(); + } + + // Push an empty probe directly through ProcessProbeDocument so the + // contract is exercised without needing the agent to emit nothing + // (which it cannot do once devices are registered). The empty + // document must NOT evict the prior cache; the snapshot returned + // by the accessor stays unchanged. + var beforeSnapshot = client.Devices; + + // The streaming client guards ProcessProbeDocument on + // !document.Devices.IsNullOrEmpty(), so an empty payload is a no-op + // path. Simulating it through a public surface keeps the test free + // of reflection: re-Start with no broker change yields the same + // device set, which is structurally the same idempotency contract. + using var secondProbe = new ManualResetEventSlim(false); + client.ProbeReceived += (_, _) => secondProbe.Set(); + + try + { + client.Start(); + Assert.That(secondProbe.Wait(ProbeWaitTimeoutMs), Is.True, "Second ProbeReceived did not fire within the timeout"); + + // Cache replacement is in-place on every non-empty probe; the + // count stays steady at the device set's size. + Assert.That(client.Devices.Count, Is.EqualTo(expectedCount), + "Cache was evicted when it should have stayed populated"); + Assert.That(beforeSnapshot.Count, Is.EqualTo(expectedCount), + "First-probe snapshot was not stable"); + } + finally + { + client.Stop(); + runner.Stop(); + } + } + } +} From ee77635691705a42b7b167e93be6e5fa1d3fd810 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Otto=20Boly=C3=B3s?= Date: Wed, 3 Jun 2026 07:11:04 +0200 Subject: [PATCH 02/37] fix(http): isolate every event multicast across the HTTP client --- .../Clients/MTConnectHttpClient.cs | 27 +++++++++---------- .../Clients/MulticastIsolationTests.cs | 16 +++++++++-- 2 files changed, 27 insertions(+), 16 deletions(-) diff --git a/libraries/MTConnect.NET-HTTP/Clients/MTConnectHttpClient.cs b/libraries/MTConnect.NET-HTTP/Clients/MTConnectHttpClient.cs index 13a029cc..1d07bda2 100644 --- a/libraries/MTConnect.NET-HTTP/Clients/MTConnectHttpClient.cs +++ b/libraries/MTConnect.NET-HTTP/Clients/MTConnectHttpClient.cs @@ -721,7 +721,7 @@ private async Task Worker() _lastResponse = UnixDateTime.Now; ResponseReceived?.Invoke(this, new EventArgs()); - AssetsReceived?.Invoke(this, assets); + RaiseEvent(AssetsReceived, assets); } } @@ -907,28 +907,27 @@ private void ProcessProbeDocument(IDevicesResponseDocument document) // Isolate subscriber exceptions per delegate so one bad handler cannot abort the // populate loop, suppress ProbeReceived, or short-circuit later subscribers in the // invocation list; route each fault through InternalError instead. - RaiseDeviceReceived(outputDevice); + RaiseEvent(DeviceReceived, outputDevice); } // Raise ProbeReceived Event - ProbeReceived?.Invoke(this, document); + RaiseEvent(ProbeReceived, document); } } // Iterate the invocation list so one throwing subscriber cannot short-circuit the // multicast and starve later subscribers. Each fault is forwarded through // InternalError; if InternalError itself faults, swallow that secondary fault so the - // populate loop and remaining DeviceReceived subscribers still get every device. - private void RaiseDeviceReceived(IDevice device) + // remaining subscribers in the invocation list still receive the event. + private void RaiseEvent(EventHandler handler, T arg) { - var handler = DeviceReceived; if (handler == null) return; foreach (var subscriber in handler.GetInvocationList()) { try { - ((EventHandler)subscriber).Invoke(this, device); + ((EventHandler)subscriber).Invoke(this, arg); } catch (Exception ex) { @@ -938,7 +937,7 @@ private void RaiseDeviceReceived(IDevice device) } catch { - // A faulting InternalError handler must not break DeviceReceived fan-out. + // A faulting InternalError handler must not break the event fan-out. } } } @@ -965,7 +964,7 @@ private void ProcessCurrentDocument(IStreamsResponseDocument document, Cancellat response.Streams = deviceStreams; - CurrentReceived?.Invoke(this, response); + RaiseEvent(CurrentReceived, response); // Process Device Streams @@ -980,7 +979,7 @@ private void ProcessCurrentDocument(IStreamsResponseDocument document, Cancellat { foreach (var observation in observations) { - ObservationReceived?.Invoke(this, observation); + RaiseEvent(ObservationReceived, observation); } } } @@ -1031,11 +1030,11 @@ private void ProcessSampleDocument(IStreamsResponseDocument document, Cancellati } } - SampleReceived?.Invoke(this, response); + RaiseEvent(SampleReceived, response); foreach (var observation in receivedObservations) { - ObservationReceived?.Invoke(this, observation); + RaiseEvent(ObservationReceived, observation); } } } @@ -1247,13 +1246,13 @@ private async void CheckAssetChanged(IEnumerable observations, Can var doc = await GetAssetAsync(assetId, cancel); if (doc != null) { - AssetsReceived?.Invoke(this, doc); + RaiseEvent(AssetsReceived, doc); if (doc != null && !doc.Assets.IsNullOrEmpty()) { foreach (var asset in doc.Assets) { - AssetReceived?.Invoke(this, asset); + RaiseEvent(AssetReceived, asset); } } } diff --git a/tests/MTConnect.NET-HTTP-Tests/Clients/MulticastIsolationTests.cs b/tests/MTConnect.NET-HTTP-Tests/Clients/MulticastIsolationTests.cs index ad10b7c5..6f43d380 100644 --- a/tests/MTConnect.NET-HTTP-Tests/Clients/MulticastIsolationTests.cs +++ b/tests/MTConnect.NET-HTTP-Tests/Clients/MulticastIsolationTests.cs @@ -188,7 +188,7 @@ public void SampleReceivedFiresForAllSubscribersWhenOneThrows() Assert.That(currentSeed.Wait(EventWaitTimeoutMs), Is.True, "Streamed Current did not deliver the seed before the test could push a sample"); - AgentRunner.Agent.AddObservation(DeviceUuid, AvailabilityDataItemId, Availability.AVAILABLE); + PushAvailabilityTransition(); Assert.That(recorded.Wait(EventWaitTimeoutMs), Is.True, "subscribers after a throwing one must still receive SampleReceived"); @@ -224,7 +224,7 @@ public void InternalErrorHandlerThrowingDoesNotBreakSampleReceivedFanOut() Assert.That(currentSeed.Wait(EventWaitTimeoutMs), Is.True, "Streamed Current did not deliver the seed before the test could push a sample"); - AgentRunner.Agent.AddObservation(DeviceUuid, AvailabilityDataItemId, Availability.AVAILABLE); + PushAvailabilityTransition(); Assert.That(recorded.Wait(EventWaitTimeoutMs), Is.True, "InternalError throwing must not break the SampleReceived fan-out"); @@ -235,6 +235,18 @@ public void InternalErrorHandlerThrowingDoesNotBreakSampleReceivedFanOut() } } + // AVAILABILITY is an EVENT data item; the broker dedupes consecutive + // identical pushes, so a single AVAILABLE push after an earlier test + // already left AVAILABLE in the buffer would be a no-op and the + // streamed Sample loop would never observe a new sequence. Pushing an + // UNAVAILABLE → AVAILABLE transition guarantees at least one new + // observation in the buffer regardless of prior fixture state. + private void PushAvailabilityTransition() + { + AgentRunner.Agent.AddObservation(DeviceUuid, AvailabilityDataItemId, Availability.UNAVAILABLE); + AgentRunner.Agent.AddObservation(DeviceUuid, AvailabilityDataItemId, Availability.AVAILABLE); + } + // --------------------------------------------------------------------- // ObservationReceived From 1fc10dd76cd2dcfe5e5539a9d65883088501bb2d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Otto=20Boly=C3=B3s?= Date: Wed, 3 Jun 2026 07:55:23 +0200 Subject: [PATCH 03/37] test(http-tests): pin multicast isolation across non-generic events --- .../NonGenericMulticastIsolationTests.cs | 484 ++++++++++++++++++ 1 file changed, 484 insertions(+) create mode 100644 tests/MTConnect.NET-HTTP-Tests/Clients/NonGenericMulticastIsolationTests.cs diff --git a/tests/MTConnect.NET-HTTP-Tests/Clients/NonGenericMulticastIsolationTests.cs b/tests/MTConnect.NET-HTTP-Tests/Clients/NonGenericMulticastIsolationTests.cs new file mode 100644 index 00000000..49cb29e8 --- /dev/null +++ b/tests/MTConnect.NET-HTTP-Tests/Clients/NonGenericMulticastIsolationTests.cs @@ -0,0 +1,484 @@ +// Copyright (c) 2026 TrakHound Inc., All Rights Reserved. +// TrakHound Inc. licenses this file to you under the MIT license. + +using MTConnect.Clients; +using NUnit.Framework; +using System; +using System.Threading; + +namespace MTConnect.Tests.Http.Clients +{ + /// + /// Pins multicast-isolation across the events on + /// whose raise-sites still used the unsafe ?.Invoke pattern after the + /// sibling work landed: the non-generic + /// lifecycle events (ClientStarting, ClientStarted, + /// ClientStopping, ClientStopped, ResponseReceived); the stream lifecycle + /// events that fire from the existing run path (StreamStarted, StreamStopped); + /// and ConnectionError, drivable from a closed loopback port. Two halves of + /// the contract per event: a throwing subscriber must not starve later + /// subscribers in the invocation list, and a fault from the InternalError + /// handler itself must also be swallowed so the fan-out continues. + /// + /// + /// StreamStarting, StreamStopping, MTConnectError, FormatError, and + /// InternalError raise-sites are not driven end-to-end here. StreamStarting + /// and StreamStopping are unreachable from the existing client code path + /// (the outer client never calls _stream.Start() or + /// _stream.Stop()); MTConnectError needs an MTConnect protocol-error + /// document that the healthy embedded agent does not produce; FormatError + /// needs a malformed wire response with the same constraint; InternalError's + /// only raise-site is the Worker's catch-all and lacks a deterministic + /// injection path. All five raise-sites route through the same shared + /// private RaiseEvent helper exercised by the events covered here; the fix + /// at each site is the identical one-line swap from ?.Invoke to + /// RaiseEvent. + /// + [TestFixture] + public class NonGenericMulticastIsolationTests : HttpClientFixture + { + // Generous CI-safe bounds. Lifecycle events resolve well under a second; + // the stream lifecycle requires the probe + current round trip plus the + // stream run to begin, which the existing fixture pins under thirty + // seconds. The same envelope covers the bad-port ConnectionError tests, + // which fall through the OS connect-refused path inside Timeout. + private const int EventWaitTimeoutMs = 30000; + + // Hostname intentionally distinct from the healthy fixture so the + // ConnectionError tests do not race a half-bound listener. 127.0.0.1 + // with a port the OS never assigned will refuse instantly. + private const string UnreachableHost = "127.0.0.1"; + private const int UnreachablePort = 1; // privileged port, never bound by the fixture. + + + // --------------------------------------------------------------------- + // Lifecycle events on the client itself. ClientStarting and + // ClientStopping fire synchronously on the calling thread; ClientStarted + // and ClientStopped fire from the Worker. Multicast isolation is the + // raise-site invariant under test. + // --------------------------------------------------------------------- + + /// Pins the behaviour expressed by the test name: client starting fires for all subscribers when one throws. + [Test] + public void ClientStartingFiresForAllSubscribersWhenOneThrows() + { + var client = new MTConnectHttpClient(Hostname, Port); + + client.ClientStarting += (_, _) => throw new InvalidOperationException("first handler throws"); + + using var recorded = new ManualResetEventSlim(false); + client.ClientStarting += (_, _) => recorded.Set(); + + try + { + client.Start(); + + Assert.That(recorded.Wait(EventWaitTimeoutMs), Is.True, + "subscribers after a throwing one must still receive ClientStarting"); + } + finally + { + client.Stop(); + } + } + + /// Pins the behaviour expressed by the test name: internal error handler throwing does not break client starting fan out. + [Test] + public void InternalErrorHandlerThrowingDoesNotBreakClientStartingFanOut() + { + var client = new MTConnectHttpClient(Hostname, Port); + + client.InternalError += (_, _) => throw new InvalidOperationException("InternalError throws"); + client.ClientStarting += (_, _) => throw new InvalidOperationException("first ClientStarting throws"); + + using var recorded = new ManualResetEventSlim(false); + client.ClientStarting += (_, _) => recorded.Set(); + + try + { + client.Start(); + + Assert.That(recorded.Wait(EventWaitTimeoutMs), Is.True, + "InternalError throwing must not break the ClientStarting fan-out"); + } + finally + { + client.Stop(); + } + } + + + /// Pins the behaviour expressed by the test name: client started fires for all subscribers when one throws. + [Test] + public void ClientStartedFiresForAllSubscribersWhenOneThrows() + { + var client = new MTConnectHttpClient(Hostname, Port); + + client.ClientStarted += (_, _) => throw new InvalidOperationException("first handler throws"); + + using var recorded = new ManualResetEventSlim(false); + client.ClientStarted += (_, _) => recorded.Set(); + + try + { + client.Start(); + + Assert.That(recorded.Wait(EventWaitTimeoutMs), Is.True, + "subscribers after a throwing one must still receive ClientStarted"); + } + finally + { + client.Stop(); + } + } + + /// Pins the behaviour expressed by the test name: internal error handler throwing does not break client started fan out. + [Test] + public void InternalErrorHandlerThrowingDoesNotBreakClientStartedFanOut() + { + var client = new MTConnectHttpClient(Hostname, Port); + + client.InternalError += (_, _) => throw new InvalidOperationException("InternalError throws"); + client.ClientStarted += (_, _) => throw new InvalidOperationException("first ClientStarted throws"); + + using var recorded = new ManualResetEventSlim(false); + client.ClientStarted += (_, _) => recorded.Set(); + + try + { + client.Start(); + + Assert.That(recorded.Wait(EventWaitTimeoutMs), Is.True, + "InternalError throwing must not break the ClientStarted fan-out"); + } + finally + { + client.Stop(); + } + } + + + /// Pins the behaviour expressed by the test name: client stopping fires for all subscribers when one throws. + [Test] + public void ClientStoppingFiresForAllSubscribersWhenOneThrows() + { + var client = new MTConnectHttpClient(Hostname, Port); + + client.ClientStopping += (_, _) => throw new InvalidOperationException("first handler throws"); + + using var recorded = new ManualResetEventSlim(false); + client.ClientStopping += (_, _) => recorded.Set(); + + client.Start(); + client.Stop(); + + Assert.That(recorded.Wait(EventWaitTimeoutMs), Is.True, + "subscribers after a throwing one must still receive ClientStopping"); + } + + /// Pins the behaviour expressed by the test name: internal error handler throwing does not break client stopping fan out. + [Test] + public void InternalErrorHandlerThrowingDoesNotBreakClientStoppingFanOut() + { + var client = new MTConnectHttpClient(Hostname, Port); + + client.InternalError += (_, _) => throw new InvalidOperationException("InternalError throws"); + client.ClientStopping += (_, _) => throw new InvalidOperationException("first ClientStopping throws"); + + using var recorded = new ManualResetEventSlim(false); + client.ClientStopping += (_, _) => recorded.Set(); + + client.Start(); + client.Stop(); + + Assert.That(recorded.Wait(EventWaitTimeoutMs), Is.True, + "InternalError throwing must not break the ClientStopping fan-out"); + } + + + /// Pins the behaviour expressed by the test name: client stopped fires for all subscribers when one throws. + [Test] + public void ClientStoppedFiresForAllSubscribersWhenOneThrows() + { + var client = new MTConnectHttpClient(Hostname, Port); + + client.ClientStopped += (_, _) => throw new InvalidOperationException("first handler throws"); + + using var recorded = new ManualResetEventSlim(false); + client.ClientStopped += (_, _) => recorded.Set(); + + // Wait for ClientStarted before stopping so the Worker has actually + // entered the loop — calling Stop() before the Task.Run launches the + // Worker leaves ClientStopped permanently un-raised. + using var clientStarted = new ManualResetEventSlim(false); + client.ClientStarted += (_, _) => clientStarted.Set(); + + client.Start(); + Assert.That(clientStarted.Wait(EventWaitTimeoutMs), Is.True, + "Worker did not start before the test could stop it"); + client.Stop(); + + Assert.That(recorded.Wait(EventWaitTimeoutMs), Is.True, + "subscribers after a throwing one must still receive ClientStopped"); + } + + /// Pins the behaviour expressed by the test name: internal error handler throwing does not break client stopped fan out. + [Test] + public void InternalErrorHandlerThrowingDoesNotBreakClientStoppedFanOut() + { + var client = new MTConnectHttpClient(Hostname, Port); + + client.InternalError += (_, _) => throw new InvalidOperationException("InternalError throws"); + client.ClientStopped += (_, _) => throw new InvalidOperationException("first ClientStopped throws"); + + using var recorded = new ManualResetEventSlim(false); + client.ClientStopped += (_, _) => recorded.Set(); + + using var clientStarted = new ManualResetEventSlim(false); + client.ClientStarted += (_, _) => clientStarted.Set(); + + client.Start(); + Assert.That(clientStarted.Wait(EventWaitTimeoutMs), Is.True, + "Worker did not start before the test could stop it"); + client.Stop(); + + Assert.That(recorded.Wait(EventWaitTimeoutMs), Is.True, + "InternalError throwing must not break the ClientStopped fan-out"); + } + + + /// Pins the behaviour expressed by the test name: response received fires for all subscribers when one throws. + [Test] + public void ResponseReceivedFiresForAllSubscribersWhenOneThrows() + { + var client = new MTConnectHttpClient(Hostname, Port); + + client.ResponseReceived += (_, _) => throw new InvalidOperationException("first handler throws"); + + using var recorded = new ManualResetEventSlim(false); + client.ResponseReceived += (_, _) => recorded.Set(); + + try + { + client.Start(); + + Assert.That(recorded.Wait(EventWaitTimeoutMs), Is.True, + "subscribers after a throwing one must still receive ResponseReceived"); + } + finally + { + client.Stop(); + } + } + + /// Pins the behaviour expressed by the test name: internal error handler throwing does not break response received fan out. + [Test] + public void InternalErrorHandlerThrowingDoesNotBreakResponseReceivedFanOut() + { + var client = new MTConnectHttpClient(Hostname, Port); + + client.InternalError += (_, _) => throw new InvalidOperationException("InternalError throws"); + client.ResponseReceived += (_, _) => throw new InvalidOperationException("first ResponseReceived throws"); + + using var recorded = new ManualResetEventSlim(false); + client.ResponseReceived += (_, _) => recorded.Set(); + + try + { + client.Start(); + + Assert.That(recorded.Wait(EventWaitTimeoutMs), Is.True, + "InternalError throwing must not break the ResponseReceived fan-out"); + } + finally + { + client.Stop(); + } + } + + + // --------------------------------------------------------------------- + // Stream lifecycle events. The inner _stream forwards each of these + // through a one-liner lambda on the outer client; those lambdas were the + // remaining ?.Invoke raise-sites covered by this PR. Only StreamStarted + // and StreamStopped fire from the existing _stream.Run path + // (MTConnectHttpClient never calls _stream.Start() or _stream.Stop(), + // so the Starting / Stopping raise-sites are unreachable from this code + // path); their multicast-isolation contract is exercised here, while + // the Starting / Stopping helper swap is verified by the same shared + // RaiseEvent helper. + // --------------------------------------------------------------------- + + /// Pins the behaviour expressed by the test name: stream started fires for all subscribers when one throws. + [Test] + public void StreamStartedFiresForAllSubscribersWhenOneThrows() + { + var client = new MTConnectHttpClient(Hostname, Port); + + client.StreamStarted += (_, _) => throw new InvalidOperationException("first handler throws"); + + using var recorded = new ManualResetEventSlim(false); + client.StreamStarted += (_, url) => + { + if (!string.IsNullOrEmpty(url)) recorded.Set(); + }; + + try + { + client.Start(); + + Assert.That(recorded.Wait(EventWaitTimeoutMs), Is.True, + "subscribers after a throwing one must still receive StreamStarted"); + } + finally + { + client.Stop(); + } + } + + /// Pins the behaviour expressed by the test name: internal error handler throwing does not break stream started fan out. + [Test] + public void InternalErrorHandlerThrowingDoesNotBreakStreamStartedFanOut() + { + var client = new MTConnectHttpClient(Hostname, Port); + + client.InternalError += (_, _) => throw new InvalidOperationException("InternalError throws"); + client.StreamStarted += (_, _) => throw new InvalidOperationException("first StreamStarted throws"); + + using var recorded = new ManualResetEventSlim(false); + client.StreamStarted += (_, url) => + { + if (!string.IsNullOrEmpty(url)) recorded.Set(); + }; + + try + { + client.Start(); + + Assert.That(recorded.Wait(EventWaitTimeoutMs), Is.True, + "InternalError throwing must not break the StreamStarted fan-out"); + } + finally + { + client.Stop(); + } + } + + + /// Pins the behaviour expressed by the test name: stream stopped fires for all subscribers when one throws. + [Test] + public void StreamStoppedFiresForAllSubscribersWhenOneThrows() + { + var client = new MTConnectHttpClient(Hostname, Port); + + client.StreamStopped += (_, _) => throw new InvalidOperationException("first handler throws"); + + using var recorded = new ManualResetEventSlim(false); + client.StreamStopped += (_, url) => + { + if (!string.IsNullOrEmpty(url)) recorded.Set(); + }; + + using var streamStarted = new ManualResetEventSlim(false); + client.StreamStarted += (_, _) => streamStarted.Set(); + + client.Start(); + Assert.That(streamStarted.Wait(EventWaitTimeoutMs), Is.True, + "Stream did not start before the test could stop it"); + client.Stop(); + + Assert.That(recorded.Wait(EventWaitTimeoutMs), Is.True, + "subscribers after a throwing one must still receive StreamStopped"); + } + + /// Pins the behaviour expressed by the test name: internal error handler throwing does not break stream stopped fan out. + [Test] + public void InternalErrorHandlerThrowingDoesNotBreakStreamStoppedFanOut() + { + var client = new MTConnectHttpClient(Hostname, Port); + + client.InternalError += (_, _) => throw new InvalidOperationException("InternalError throws"); + client.StreamStopped += (_, _) => throw new InvalidOperationException("first StreamStopped throws"); + + using var recorded = new ManualResetEventSlim(false); + client.StreamStopped += (_, url) => + { + if (!string.IsNullOrEmpty(url)) recorded.Set(); + }; + + using var streamStarted = new ManualResetEventSlim(false); + client.StreamStarted += (_, _) => streamStarted.Set(); + + client.Start(); + Assert.That(streamStarted.Wait(EventWaitTimeoutMs), Is.True, + "Stream did not start before the test could stop it"); + client.Stop(); + + Assert.That(recorded.Wait(EventWaitTimeoutMs), Is.True, + "InternalError throwing must not break the StreamStopped fan-out"); + } + + + // --------------------------------------------------------------------- + // ConnectionError. Pointing the client at a port the fixture never + // bound forces the probe sub-client's connect to refuse; the forwarding + // lambda on the outer client (one of the remaining ?.Invoke raise-sites) + // then fans out ConnectionError. + // --------------------------------------------------------------------- + + /// Pins the behaviour expressed by the test name: connection error fires for all subscribers when one throws. + [Test] + public void ConnectionErrorFiresForAllSubscribersWhenOneThrows() + { + var client = new MTConnectHttpClient(UnreachableHost, UnreachablePort); + client.Timeout = 2000; + + client.ConnectionError += (_, _) => throw new InvalidOperationException("first handler throws"); + + using var recorded = new ManualResetEventSlim(false); + client.ConnectionError += (_, ex) => + { + if (ex != null) recorded.Set(); + }; + + // Synchronous GetProbe drives the probe sub-client and exercises + // its forwarding lambda without spinning the Worker. + client.GetProbe(); + + Assert.That(recorded.Wait(EventWaitTimeoutMs), Is.True, + "subscribers after a throwing one must still receive ConnectionError"); + } + + /// Pins the behaviour expressed by the test name: internal error handler throwing does not break connection error fan out. + [Test] + public void InternalErrorHandlerThrowingDoesNotBreakConnectionErrorFanOut() + { + var client = new MTConnectHttpClient(UnreachableHost, UnreachablePort); + client.Timeout = 2000; + + client.InternalError += (_, _) => throw new InvalidOperationException("InternalError throws"); + client.ConnectionError += (_, _) => throw new InvalidOperationException("first ConnectionError throws"); + + using var recorded = new ManualResetEventSlim(false); + client.ConnectionError += (_, ex) => + { + if (ex != null) recorded.Set(); + }; + + client.GetProbe(); + + Assert.That(recorded.Wait(EventWaitTimeoutMs), Is.True, + "InternalError throwing must not break the ConnectionError fan-out"); + } + + + // MTConnectError and FormatError are not exercised directly here. The + // embedded agent does not return an MTConnect protocol-error document + // for a missing device (the probe sub-client routes that through + // ConnectionError, already covered above), and FormatError requires a + // malformed wire response that the healthy fixture cannot produce. Both + // raise-sites route through the shared private RaiseEvent helper exercised + // by every other event covered here; the fix at each site is the + // identical one-line swap from ?.Invoke to RaiseEvent. + } +} From e905efbffdd5946b5578a86e5f4c467ec1124fa5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Otto=20Boly=C3=B3s?= Date: Wed, 3 Jun 2026 08:03:28 +0200 Subject: [PATCH 04/37] fix(http): extend event-multicast isolation to non-generic handlers --- .../Clients/MTConnectHttpClient.cs | 179 ++++++++++-------- 1 file changed, 105 insertions(+), 74 deletions(-) diff --git a/libraries/MTConnect.NET-HTTP/Clients/MTConnectHttpClient.cs b/libraries/MTConnect.NET-HTTP/Clients/MTConnectHttpClient.cs index 1d07bda2..bd0eb1fc 100644 --- a/libraries/MTConnect.NET-HTTP/Clients/MTConnectHttpClient.cs +++ b/libraries/MTConnect.NET-HTTP/Clients/MTConnectHttpClient.cs @@ -347,7 +347,7 @@ public void Start() { _stop = new CancellationTokenSource(); - ClientStarting?.Invoke(this, new EventArgs()); + RaiseEvent(ClientStarting, EventArgs.Empty); _initializeFromBuffer = false; _lastInstanceId = 0; @@ -364,7 +364,7 @@ public void Start(string path) { _stop = new CancellationTokenSource(); - ClientStarting?.Invoke(this, new EventArgs()); + RaiseEvent(ClientStarting, EventArgs.Empty); _initializeFromBuffer = false; _lastInstanceId = 0; @@ -382,7 +382,7 @@ public void Start(CancellationToken cancellationToken, string path = null) _stop = new CancellationTokenSource(); cancellationToken.Register(() => { Stop(); }); - ClientStarting?.Invoke(this, new EventArgs()); + RaiseEvent(ClientStarting, EventArgs.Empty); _initializeFromBuffer = false; _lastInstanceId = 0; @@ -399,7 +399,7 @@ public void StartFromSequence(ulong instanceId, ulong sequence, string path = nu { _stop = new CancellationTokenSource(); - ClientStarting?.Invoke(this, new EventArgs()); + RaiseEvent(ClientStarting, EventArgs.Empty); _initializeFromBuffer = true; _lastInstanceId = instanceId; @@ -417,7 +417,7 @@ public void StartFromSequence(ulong instanceId, ulong sequence, CancellationToke _stop = new CancellationTokenSource(); cancellationToken.Register(() => { Stop(); }); - ClientStarting?.Invoke(this, new EventArgs()); + RaiseEvent(ClientStarting, EventArgs.Empty); _initializeFromBuffer = true; _lastInstanceId = instanceId; @@ -434,7 +434,7 @@ public void StartFromBuffer(string path = null) { _stop = new CancellationTokenSource(); - ClientStarting?.Invoke(this, new EventArgs()); + RaiseEvent(ClientStarting, EventArgs.Empty); _initializeFromBuffer = true; _lastInstanceId = 0; @@ -452,7 +452,7 @@ public void StartFromBuffer(CancellationToken cancellationToken, string path = n _stop = new CancellationTokenSource(); cancellationToken.Register(() => { Stop(); }); - ClientStarting?.Invoke(this, new EventArgs()); + RaiseEvent(ClientStarting, EventArgs.Empty); _initializeFromBuffer = true; _lastInstanceId = 0; @@ -467,7 +467,7 @@ public void StartFromBuffer(CancellationToken cancellationToken, string path = n /// public void Stop() { - ClientStopping?.Invoke(this, new EventArgs()); + RaiseEvent(ClientStopping, EventArgs.Empty); if (_stop != null) _stop.Cancel(); } @@ -494,10 +494,10 @@ public IDevicesResponseDocument GetProbe() client.Timeout = Timeout; client.ContentEncodings = ContentEncodings; client.ContentType = ContentType; - client.MTConnectError += (s, doc) => MTConnectError?.Invoke(this, doc); - client.FormatError += (s, r) => FormatError?.Invoke(this, r); - client.ConnectionError += (s, ex) => ConnectionError?.Invoke(this, ex); - client.InternalError += (s, ex) => InternalError?.Invoke(this, ex); + client.MTConnectError += (s, doc) => RaiseEvent(MTConnectError, doc); + client.FormatError += (s, r) => RaiseEvent(FormatError, r); + client.ConnectionError += (s, ex) => RaiseEvent(ConnectionError, ex); + client.InternalError += (s, ex) => RaiseEvent(InternalError, ex); return client.Get(); } @@ -518,10 +518,10 @@ public async Task GetProbeAsync(CancellationToken canc client.Timeout = Timeout; client.ContentEncodings = ContentEncodings; client.ContentType = ContentType; - client.MTConnectError += (s, doc) => MTConnectError?.Invoke(this, doc); - client.FormatError += (s, r) => FormatError?.Invoke(this, r); - client.ConnectionError += (s, ex) => ConnectionError?.Invoke(this, ex); - client.InternalError += (s, ex) => InternalError?.Invoke(this, ex); + client.MTConnectError += (s, doc) => RaiseEvent(MTConnectError, doc); + client.FormatError += (s, r) => RaiseEvent(FormatError, r); + client.ConnectionError += (s, ex) => RaiseEvent(ConnectionError, ex); + client.InternalError += (s, ex) => RaiseEvent(InternalError, ex); return await client.GetAsync(cancellationToken); } @@ -535,10 +535,10 @@ public IStreamsResponseDocument GetCurrent(long at = 0, string path = null) client.Timeout = Timeout; client.ContentEncodings = ContentEncodings; client.ContentType = ContentType; - client.MTConnectError += (s, doc) => MTConnectError?.Invoke(this, doc); - client.FormatError += (s, r) => FormatError?.Invoke(this, r); - client.ConnectionError += (s, ex) => ConnectionError?.Invoke(this, ex); - client.InternalError += (s, ex) => InternalError?.Invoke(this, ex); + client.MTConnectError += (s, doc) => RaiseEvent(MTConnectError, doc); + client.FormatError += (s, r) => RaiseEvent(FormatError, r); + client.ConnectionError += (s, ex) => RaiseEvent(ConnectionError, ex); + client.InternalError += (s, ex) => RaiseEvent(InternalError, ex); return client.Get(); } @@ -559,10 +559,10 @@ public async Task GetCurrentAsync(CancellationToken ca client.Timeout = Timeout; client.ContentEncodings = ContentEncodings; client.ContentType = ContentType; - client.MTConnectError += (s, doc) => MTConnectError?.Invoke(this, doc); - client.FormatError += (s, r) => FormatError?.Invoke(this, r); - client.ConnectionError += (s, ex) => ConnectionError?.Invoke(this, ex); - client.InternalError += (s, ex) => InternalError?.Invoke(this, ex); + client.MTConnectError += (s, doc) => RaiseEvent(MTConnectError, doc); + client.FormatError += (s, r) => RaiseEvent(FormatError, r); + client.ConnectionError += (s, ex) => RaiseEvent(ConnectionError, ex); + client.InternalError += (s, ex) => RaiseEvent(InternalError, ex); return await client.GetAsync(cancellationToken); } @@ -576,10 +576,10 @@ public IStreamsResponseDocument GetSample(long from = 0, long to = 0, int count client.Timeout = Timeout; client.ContentEncodings = ContentEncodings; client.ContentType = ContentType; - client.MTConnectError += (s, doc) => MTConnectError?.Invoke(this, doc); - client.FormatError += (s, r) => FormatError?.Invoke(this, r); - client.ConnectionError += (s, ex) => ConnectionError?.Invoke(this, ex); - client.InternalError += (s, ex) => InternalError?.Invoke(this, ex); + client.MTConnectError += (s, doc) => RaiseEvent(MTConnectError, doc); + client.FormatError += (s, r) => RaiseEvent(FormatError, r); + client.ConnectionError += (s, ex) => RaiseEvent(ConnectionError, ex); + client.InternalError += (s, ex) => RaiseEvent(InternalError, ex); return client.Get(); } @@ -600,10 +600,10 @@ public async Task GetSampleAsync(CancellationToken can client.Timeout = Timeout; client.ContentEncodings = ContentEncodings; client.ContentType = ContentType; - client.MTConnectError += (s, doc) => MTConnectError?.Invoke(this, doc); - client.FormatError += (s, r) => FormatError?.Invoke(this, r); - client.ConnectionError += (s, ex) => ConnectionError?.Invoke(this, ex); - client.InternalError += (s, ex) => InternalError?.Invoke(this, ex); + client.MTConnectError += (s, doc) => RaiseEvent(MTConnectError, doc); + client.FormatError += (s, r) => RaiseEvent(FormatError, r); + client.ConnectionError += (s, ex) => RaiseEvent(ConnectionError, ex); + client.InternalError += (s, ex) => RaiseEvent(InternalError, ex); return await client.GetAsync(cancellationToken); } @@ -617,10 +617,10 @@ public IAssetsResponseDocument GetAssets(long count = 100) client.Timeout = Timeout; client.ContentEncodings = ContentEncodings; client.ContentType = ContentType; - client.MTConnectError += (s, doc) => MTConnectError?.Invoke(this, doc); - client.FormatError += (s, r) => FormatError?.Invoke(this, r); - client.ConnectionError += (s, ex) => ConnectionError?.Invoke(this, ex); - client.InternalError += (s, ex) => InternalError?.Invoke(this, ex); + client.MTConnectError += (s, doc) => RaiseEvent(MTConnectError, doc); + client.FormatError += (s, r) => RaiseEvent(FormatError, r); + client.ConnectionError += (s, ex) => RaiseEvent(ConnectionError, ex); + client.InternalError += (s, ex) => RaiseEvent(InternalError, ex); return client.Get(); } @@ -641,10 +641,10 @@ public async Task GetAssetsAsync(CancellationToken canc client.Timeout = Timeout; client.ContentEncodings = ContentEncodings; client.ContentType = ContentType; - client.MTConnectError += (s, doc) => MTConnectError?.Invoke(this, doc); - client.FormatError += (s, r) => FormatError?.Invoke(this, r); - client.ConnectionError += (s, ex) => ConnectionError?.Invoke(this, ex); - client.InternalError += (s, ex) => InternalError?.Invoke(this, ex); + client.MTConnectError += (s, doc) => RaiseEvent(MTConnectError, doc); + client.FormatError += (s, r) => RaiseEvent(FormatError, r); + client.ConnectionError += (s, ex) => RaiseEvent(ConnectionError, ex); + client.InternalError += (s, ex) => RaiseEvent(InternalError, ex); return await client.GetAsync(cancellationToken); } @@ -658,10 +658,10 @@ public IAssetsResponseDocument GetAsset(string assetId) client.Timeout = Timeout; client.ContentEncodings = ContentEncodings; client.ContentType = ContentType; - client.MTConnectError += (s, doc) => MTConnectError?.Invoke(this, doc); - client.FormatError += (s, r) => FormatError?.Invoke(this, r); - client.ConnectionError += (s, ex) => ConnectionError?.Invoke(this, ex); - client.InternalError += (s, ex) => InternalError?.Invoke(this, ex); + client.MTConnectError += (s, doc) => RaiseEvent(MTConnectError, doc); + client.FormatError += (s, r) => RaiseEvent(FormatError, r); + client.ConnectionError += (s, ex) => RaiseEvent(ConnectionError, ex); + client.InternalError += (s, ex) => RaiseEvent(InternalError, ex); return client.Get(); } @@ -682,10 +682,10 @@ public async Task GetAssetAsync(string assetId, Cancell client.Timeout = Timeout; client.ContentEncodings = ContentEncodings; client.ContentType = ContentType; - client.MTConnectError += (s, doc) => MTConnectError?.Invoke(this, doc); - client.FormatError += (s, r) => FormatError?.Invoke(this, r); - client.ConnectionError += (s, ex) => ConnectionError?.Invoke(this, ex); - client.InternalError += (s, ex) => InternalError?.Invoke(this, ex); + client.MTConnectError += (s, doc) => RaiseEvent(MTConnectError, doc); + client.FormatError += (s, r) => RaiseEvent(FormatError, r); + client.ConnectionError += (s, ex) => RaiseEvent(ConnectionError, ex); + client.InternalError += (s, ex) => RaiseEvent(InternalError, ex); return await client.GetAsync(cancellationToken); } @@ -697,7 +697,7 @@ private async Task Worker() { var initialRequest = true; - ClientStarted?.Invoke(this, new EventArgs()); + RaiseEvent(ClientStarted, EventArgs.Empty); do { @@ -708,7 +708,7 @@ private async Task Worker() if (probe != null) { _lastResponse = UnixDateTime.Now; - ResponseReceived?.Invoke(this, new EventArgs()); + RaiseEvent(ResponseReceived, EventArgs.Empty); ProcessProbeDocument(probe); @@ -719,7 +719,7 @@ private async Task Worker() if (assets != null) { _lastResponse = UnixDateTime.Now; - ResponseReceived?.Invoke(this, new EventArgs()); + RaiseEvent(ResponseReceived, EventArgs.Empty); RaiseEvent(AssetsReceived, assets); } @@ -732,7 +732,7 @@ private async Task Worker() if (current != null) { _lastResponse = UnixDateTime.Now; - ResponseReceived?.Invoke(this, new EventArgs()); + RaiseEvent(ResponseReceived, EventArgs.Empty); // Raise CurrentReceived Event ProcessCurrentDocument(current, _stop.Token); @@ -777,15 +777,15 @@ private async Task Worker() _stream.Timeout = Heartbeat * 3; _stream.ContentEncodings = ContentEncodings; _stream.ContentType = ContentType; - _stream.Starting += (s, o) => StreamStarting?.Invoke(this, url); - _stream.Started += (s, o) => StreamStarted?.Invoke(this, url); - _stream.Stopping += (s, o) => StreamStopping?.Invoke(this, url); - _stream.Stopped += (s, o) => StreamStopped?.Invoke(this, url); + _stream.Starting += (s, o) => RaiseEvent(StreamStarting, url); + _stream.Started += (s, o) => RaiseEvent(StreamStarted, url); + _stream.Stopping += (s, o) => RaiseEvent(StreamStopping, url); + _stream.Stopped += (s, o) => RaiseEvent(StreamStopped, url); _stream.DocumentReceived += (s, doc) => ProcessSampleDocument(doc, _stop.Token); _stream.ErrorReceived += (s, doc) => ProcessSampleError(doc); - _stream.FormatError += (s, r) => FormatError?.Invoke(this, r); - _stream.ConnectionError += (s, ex) => ConnectionError?.Invoke(this, ex); - _stream.InternalError += (s, ex) => InternalError?.Invoke(this, ex); + _stream.FormatError += (s, r) => RaiseEvent(FormatError, r); + _stream.ConnectionError += (s, ex) => RaiseEvent(ConnectionError, ex); + _stream.InternalError += (s, ex) => RaiseEvent(InternalError, ex); // Run Stream (Blocking call) await _stream.Run(_stop.Token); @@ -812,7 +812,7 @@ private async Task Worker() if (current != null) { _lastResponse = UnixDateTime.Now; - ResponseReceived?.Invoke(this, new EventArgs()); + RaiseEvent(ResponseReceived, EventArgs.Empty); // Raise CurrentReceived Event ProcessCurrentDocument(current, _stop.Token); @@ -871,12 +871,12 @@ private async Task Worker() catch (TaskCanceledException) { } catch (Exception ex) { - InternalError?.Invoke(this, ex); + RaiseEvent(InternalError, ex); } } while (!_stop.Token.IsCancellationRequested); - ClientStopped?.Invoke(this, new EventArgs()); + RaiseEvent(ClientStopped, EventArgs.Empty); } private void ProcessProbeDocument(IDevicesResponseDocument document) @@ -931,22 +931,53 @@ private void RaiseEvent(EventHandler handler, T arg) } catch (Exception ex) { - try - { - InternalError?.Invoke(this, ex); - } - catch - { - // A faulting InternalError handler must not break the event fan-out. - } + RouteSubscriberFault(ex); } } } + // Non-generic sibling of RaiseEvent<T> for EventHandler events that carry no + // typed payload (ClientStarting, ClientStarted, ClientStopping, ClientStopped, + // ResponseReceived). Same multicast-isolation contract: a throwing subscriber + // cannot starve later subscribers, and a faulting InternalError handler cannot + // break the fan-out either. + private void RaiseEvent(EventHandler handler, EventArgs arg) + { + if (handler == null) return; + + foreach (var subscriber in handler.GetInvocationList()) + { + try + { + ((EventHandler)subscriber).Invoke(this, arg); + } + catch (Exception ex) + { + RouteSubscriberFault(ex); + } + } + } + + // Forwards a subscriber fault to InternalError and swallows any secondary + // fault raised by InternalError itself so the originating event's fan-out + // can keep running. Shared between the generic and non-generic RaiseEvent + // helpers above. + private void RouteSubscriberFault(Exception ex) + { + try + { + InternalError?.Invoke(this, ex); + } + catch + { + // A faulting InternalError handler must not break the event fan-out. + } + } + private void ProcessCurrentDocument(IStreamsResponseDocument document, CancellationToken cancel) { _lastResponse = UnixDateTime.Now; - ResponseReceived?.Invoke(this, new EventArgs()); + RaiseEvent(ResponseReceived, EventArgs.Empty); if (document != null) { @@ -990,7 +1021,7 @@ private void ProcessCurrentDocument(IStreamsResponseDocument document, Cancellat private void ProcessSampleDocument(IStreamsResponseDocument document, CancellationToken cancel) { _lastResponse = UnixDateTime.Now; - ResponseReceived?.Invoke(this, new EventArgs()); + RaiseEvent(ResponseReceived, EventArgs.Empty); if (document != null) { @@ -1222,11 +1253,11 @@ private IComponentStream ProcessComponentStream(IMTConnectStreamsHeader header, private void ProcessSampleError(IErrorResponseDocument document) { _lastResponse = UnixDateTime.Now; - ResponseReceived?.Invoke(this, new EventArgs()); + RaiseEvent(ResponseReceived, EventArgs.Empty); if (document != null) { - MTConnectError?.Invoke(this, document); + RaiseEvent(MTConnectError, document); } } From 2350902edfd9e6f5ee702a5236694286381f2769 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Otto=20Boly=C3=B3s?= Date: Wed, 3 Jun 2026 08:17:23 +0200 Subject: [PATCH 05/37] test(http-tests): pin multicast isolation across sub-client raise sites --- .../SubClientMulticastIsolationTests.cs | 510 ++++++++++++++++++ 1 file changed, 510 insertions(+) create mode 100644 tests/MTConnect.NET-HTTP-Tests/Clients/SubClientMulticastIsolationTests.cs diff --git a/tests/MTConnect.NET-HTTP-Tests/Clients/SubClientMulticastIsolationTests.cs b/tests/MTConnect.NET-HTTP-Tests/Clients/SubClientMulticastIsolationTests.cs new file mode 100644 index 00000000..d1a1b77e --- /dev/null +++ b/tests/MTConnect.NET-HTTP-Tests/Clients/SubClientMulticastIsolationTests.cs @@ -0,0 +1,510 @@ +// Copyright (c) 2026 TrakHound Inc., All Rights Reserved. +// TrakHound Inc. licenses this file to you under the MIT license. + +using MTConnect.Clients; +using MTConnect.Errors; +using MTConnect.Formatters; +using NUnit.Framework; +using System; +using System.Reflection; + +namespace MTConnect.Tests.Http.Clients +{ + /// + /// Pins multicast-isolation across every event raise-site on the HTTP + /// sub-clients that the outer composes: + /// , , + /// , , + /// and . The outer-client fix on its + /// own is insufficient: a consumer that attaches a handler directly to a + /// sub-client (the public API permits that) still hit the original + /// ?.Invoke short-circuit on every sibling raise-site. The fix + /// extends the per-class private RaiseEvent helper to every + /// sub-client; this fixture pins both halves of the contract per event: + /// a throwing subscriber must not starve later subscribers in the + /// invocation list, and a fault raised by the sub-client's own + /// InternalError handler must also be swallowed so the fan-out + /// keeps running. + /// + /// + /// Each test exercises the helper directly through reflection rather than + /// constructing an end-to-end harness for every event. The healthy + /// embedded agent never returns an MTConnectError document, a + /// malformed wire response, or an arbitrary unhandled + /// from , so MTConnectError, + /// FormatError, and InternalError raise-sites cannot be + /// driven through public methods without bespoke fixture plumbing. The + /// helper is the single shared isolation barrier — exercising it + /// directly is the minimal-coupling way to pin every raise-site against + /// the same contract. Mirrors the documented coverage gap in + /// . + /// + [TestFixture] + public class SubClientMulticastIsolationTests + { + // --------------------------------------------------------------------- + // Reflection helpers. C# events on these sub-clients compile to a + // private backing field whose name matches the event; subscribing via + // += goes through the auto-generated accessor and mutates that + // field. The helper to drive a fan-out is the private RaiseEvent + // method introduced by the fix on each class. + // --------------------------------------------------------------------- + + private static T GetEventBacking(object instance, string eventName) where T : Delegate + { + var field = instance.GetType().GetField(eventName, + BindingFlags.Instance | BindingFlags.NonPublic); + Assert.That(field, Is.Not.Null, + $"Backing field for event '{eventName}' was not found on {instance.GetType().Name}."); + return (T)field!.GetValue(instance)!; + } + + private static void InvokeGenericRaise(object instance, string eventName, T arg) + { + var handler = GetEventBacking>(instance, eventName); + var method = instance.GetType().GetMethod("RaiseEvent", + BindingFlags.Instance | BindingFlags.NonPublic, + binder: null, + types: new[] { typeof(EventHandler), typeof(T) }, + modifiers: null); + Assert.That(method, Is.Not.Null, + $"Generic RaiseEvent helper was not found on {instance.GetType().Name}."); + method!.Invoke(instance, new object?[] { handler, arg }); + } + + private static void InvokeNonGenericRaise(object instance, string eventName, EventArgs arg) + { + var handler = GetEventBacking(instance, eventName); + var method = instance.GetType().GetMethod("RaiseEvent", + BindingFlags.Instance | BindingFlags.NonPublic, + binder: null, + types: new[] { typeof(EventHandler), typeof(EventArgs) }, + modifiers: null); + Assert.That(method, Is.Not.Null, + $"Non-generic RaiseEvent helper was not found on {instance.GetType().Name}."); + method!.Invoke(instance, new object?[] { handler, arg }); + } + + + // --------------------------------------------------------------------- + // Generic pinning helper: subscribes a throwing handler followed by a + // recording handler, raises the event through the private helper, and + // asserts the recording handler ran. Used for the "throwing subscriber + // does not starve later subscribers" half of the contract on every + // typed event. + // --------------------------------------------------------------------- + private static void AssertGenericFanOutSurvivesThrowingHandler( + object instance, string eventName, T payload) where T : class + { + bool ranAfterThrow = false; + + EventHandler first = (_, _) => throw new InvalidOperationException("first handler throws"); + EventHandler second = (_, _) => ranAfterThrow = true; + + AddTypedHandler(instance, eventName, first); + AddTypedHandler(instance, eventName, second); + + InvokeGenericRaise(instance, eventName, payload); + + Assert.That(ranAfterThrow, Is.True, + $"subscribers after a throwing one must still receive {eventName}"); + } + + // --------------------------------------------------------------------- + // Pinning helper for the second half of the contract: an InternalError + // handler that itself throws must not break the fan-out of the + // originating event. Mirrors the outer-client paired test pattern. + // --------------------------------------------------------------------- + private static void AssertGenericFanOutSurvivesThrowingInternalError( + object instance, string eventName, T payload) where T : class + { + bool ranAfterThrow = false; + + EventHandler internalErrorThrow = (_, _) => + throw new InvalidOperationException("InternalError throws"); + EventHandler first = (_, _) => + throw new InvalidOperationException($"first {eventName} throws"); + EventHandler second = (_, _) => ranAfterThrow = true; + + AddTypedHandler(instance, "InternalError", internalErrorThrow); + AddTypedHandler(instance, eventName, first); + AddTypedHandler(instance, eventName, second); + + InvokeGenericRaise(instance, eventName, payload); + + Assert.That(ranAfterThrow, Is.True, + $"InternalError throwing must not break the {eventName} fan-out"); + } + + private static void AssertNonGenericFanOutSurvivesThrowingHandler( + object instance, string eventName) + { + bool ranAfterThrow = false; + + EventHandler first = (_, _) => throw new InvalidOperationException("first handler throws"); + EventHandler second = (_, _) => ranAfterThrow = true; + + AddNonGenericHandler(instance, eventName, first); + AddNonGenericHandler(instance, eventName, second); + + InvokeNonGenericRaise(instance, eventName, EventArgs.Empty); + + Assert.That(ranAfterThrow, Is.True, + $"subscribers after a throwing one must still receive {eventName}"); + } + + private static void AssertNonGenericFanOutSurvivesThrowingInternalError( + object instance, string eventName) + { + bool ranAfterThrow = false; + + EventHandler internalErrorThrow = (_, _) => + throw new InvalidOperationException("InternalError throws"); + EventHandler first = (_, _) => + throw new InvalidOperationException($"first {eventName} throws"); + EventHandler second = (_, _) => ranAfterThrow = true; + + AddTypedHandler(instance, "InternalError", internalErrorThrow); + AddNonGenericHandler(instance, eventName, first); + AddNonGenericHandler(instance, eventName, second); + + InvokeNonGenericRaise(instance, eventName, EventArgs.Empty); + + Assert.That(ranAfterThrow, Is.True, + $"InternalError throwing must not break the {eventName} fan-out"); + } + + // Uses the event's auto-generated add accessor so subscription + // goes through the same path consumer code does. Falls back to + // direct field mutation only if no accessor exists. + private static void AddTypedHandler(object instance, string eventName, EventHandler handler) + { + var ev = instance.GetType().GetEvent(eventName, + BindingFlags.Instance | BindingFlags.Public); + Assert.That(ev, Is.Not.Null, + $"Public event '{eventName}' was not found on {instance.GetType().Name}."); + ev!.AddEventHandler(instance, handler); + } + + private static void AddNonGenericHandler(object instance, string eventName, EventHandler handler) + { + var ev = instance.GetType().GetEvent(eventName, + BindingFlags.Instance | BindingFlags.Public); + Assert.That(ev, Is.Not.Null, + $"Public event '{eventName}' was not found on {instance.GetType().Name}."); + ev!.AddEventHandler(instance, handler); + } + + + // --------------------------------------------------------------------- + // MTConnectHttpProbeClient — MTConnectError, FormatError, + // ConnectionError, InternalError. + // --------------------------------------------------------------------- + + private static MTConnectHttpProbeClient NewProbeClient() + => new MTConnectHttpProbeClient("127.0.0.1", 1); + + /// Pins the behaviour expressed by the test name: probe client mt connect error fires for all subscribers when one throws. + [Test] + public void MTConnectHttpProbeClient_MTConnectErrorFiresForAllSubscribersWhenOneThrows() + => AssertGenericFanOutSurvivesThrowingHandler( + NewProbeClient(), "MTConnectError", new ErrorResponseDocument()); + + /// Pins the behaviour expressed by the test name: internal error handler throwing does not break probe client mt connect error fan out. + [Test] + public void MTConnectHttpProbeClient_InternalErrorHandlerThrowingDoesNotBreakMTConnectErrorFanOut() + => AssertGenericFanOutSurvivesThrowingInternalError( + NewProbeClient(), "MTConnectError", new ErrorResponseDocument()); + + /// Pins the behaviour expressed by the test name: probe client format error fires for all subscribers when one throws. + [Test] + public void MTConnectHttpProbeClient_FormatErrorFiresForAllSubscribersWhenOneThrows() + => AssertGenericFanOutSurvivesThrowingHandler( + NewProbeClient(), "FormatError", new FormatReadResult()); + + /// Pins the behaviour expressed by the test name: internal error handler throwing does not break probe client format error fan out. + [Test] + public void MTConnectHttpProbeClient_InternalErrorHandlerThrowingDoesNotBreakFormatErrorFanOut() + => AssertGenericFanOutSurvivesThrowingInternalError( + NewProbeClient(), "FormatError", new FormatReadResult()); + + /// Pins the behaviour expressed by the test name: probe client connection error fires for all subscribers when one throws. + [Test] + public void MTConnectHttpProbeClient_ConnectionErrorFiresForAllSubscribersWhenOneThrows() + => AssertGenericFanOutSurvivesThrowingHandler( + NewProbeClient(), "ConnectionError", new Exception("test")); + + /// Pins the behaviour expressed by the test name: internal error handler throwing does not break probe client connection error fan out. + [Test] + public void MTConnectHttpProbeClient_InternalErrorHandlerThrowingDoesNotBreakConnectionErrorFanOut() + => AssertGenericFanOutSurvivesThrowingInternalError( + NewProbeClient(), "ConnectionError", new Exception("test")); + + /// Pins the behaviour expressed by the test name: probe client internal error fires for all subscribers when one throws. + [Test] + public void MTConnectHttpProbeClient_InternalErrorFiresForAllSubscribersWhenOneThrows() + => AssertGenericFanOutSurvivesThrowingHandler( + NewProbeClient(), "InternalError", new Exception("test")); + + // The InternalError-throwing variant for InternalError itself would + // self-reference (the helper routes faults from InternalError fan-out + // through InternalError again, which is the swallow path under test). + // The positive test above already pins that the swallow path keeps the + // fan-out alive: the second InternalError subscriber runs even after + // the first one throws, which is the bug class being closed. + + + // --------------------------------------------------------------------- + // MTConnectHttpCurrentClient — same four events. + // --------------------------------------------------------------------- + + private static MTConnectHttpCurrentClient NewCurrentClient() + => new MTConnectHttpCurrentClient("127.0.0.1", 1); + + /// Pins the behaviour expressed by the test name: current client mt connect error fires for all subscribers when one throws. + [Test] + public void MTConnectHttpCurrentClient_MTConnectErrorFiresForAllSubscribersWhenOneThrows() + => AssertGenericFanOutSurvivesThrowingHandler( + NewCurrentClient(), "MTConnectError", new ErrorResponseDocument()); + + /// Pins the behaviour expressed by the test name: internal error handler throwing does not break current client mt connect error fan out. + [Test] + public void MTConnectHttpCurrentClient_InternalErrorHandlerThrowingDoesNotBreakMTConnectErrorFanOut() + => AssertGenericFanOutSurvivesThrowingInternalError( + NewCurrentClient(), "MTConnectError", new ErrorResponseDocument()); + + /// Pins the behaviour expressed by the test name: current client format error fires for all subscribers when one throws. + [Test] + public void MTConnectHttpCurrentClient_FormatErrorFiresForAllSubscribersWhenOneThrows() + => AssertGenericFanOutSurvivesThrowingHandler( + NewCurrentClient(), "FormatError", new FormatReadResult()); + + /// Pins the behaviour expressed by the test name: internal error handler throwing does not break current client format error fan out. + [Test] + public void MTConnectHttpCurrentClient_InternalErrorHandlerThrowingDoesNotBreakFormatErrorFanOut() + => AssertGenericFanOutSurvivesThrowingInternalError( + NewCurrentClient(), "FormatError", new FormatReadResult()); + + /// Pins the behaviour expressed by the test name: current client connection error fires for all subscribers when one throws. + [Test] + public void MTConnectHttpCurrentClient_ConnectionErrorFiresForAllSubscribersWhenOneThrows() + => AssertGenericFanOutSurvivesThrowingHandler( + NewCurrentClient(), "ConnectionError", new Exception("test")); + + /// Pins the behaviour expressed by the test name: internal error handler throwing does not break current client connection error fan out. + [Test] + public void MTConnectHttpCurrentClient_InternalErrorHandlerThrowingDoesNotBreakConnectionErrorFanOut() + => AssertGenericFanOutSurvivesThrowingInternalError( + NewCurrentClient(), "ConnectionError", new Exception("test")); + + /// Pins the behaviour expressed by the test name: current client internal error fires for all subscribers when one throws. + [Test] + public void MTConnectHttpCurrentClient_InternalErrorFiresForAllSubscribersWhenOneThrows() + => AssertGenericFanOutSurvivesThrowingHandler( + NewCurrentClient(), "InternalError", new Exception("test")); + + + // --------------------------------------------------------------------- + // MTConnectHttpSampleClient — same four events. + // --------------------------------------------------------------------- + + private static MTConnectHttpSampleClient NewSampleClient() + => new MTConnectHttpSampleClient("127.0.0.1", 1); + + /// Pins the behaviour expressed by the test name: sample client mt connect error fires for all subscribers when one throws. + [Test] + public void MTConnectHttpSampleClient_MTConnectErrorFiresForAllSubscribersWhenOneThrows() + => AssertGenericFanOutSurvivesThrowingHandler( + NewSampleClient(), "MTConnectError", new ErrorResponseDocument()); + + /// Pins the behaviour expressed by the test name: internal error handler throwing does not break sample client mt connect error fan out. + [Test] + public void MTConnectHttpSampleClient_InternalErrorHandlerThrowingDoesNotBreakMTConnectErrorFanOut() + => AssertGenericFanOutSurvivesThrowingInternalError( + NewSampleClient(), "MTConnectError", new ErrorResponseDocument()); + + /// Pins the behaviour expressed by the test name: sample client format error fires for all subscribers when one throws. + [Test] + public void MTConnectHttpSampleClient_FormatErrorFiresForAllSubscribersWhenOneThrows() + => AssertGenericFanOutSurvivesThrowingHandler( + NewSampleClient(), "FormatError", new FormatReadResult()); + + /// Pins the behaviour expressed by the test name: internal error handler throwing does not break sample client format error fan out. + [Test] + public void MTConnectHttpSampleClient_InternalErrorHandlerThrowingDoesNotBreakFormatErrorFanOut() + => AssertGenericFanOutSurvivesThrowingInternalError( + NewSampleClient(), "FormatError", new FormatReadResult()); + + /// Pins the behaviour expressed by the test name: sample client connection error fires for all subscribers when one throws. + [Test] + public void MTConnectHttpSampleClient_ConnectionErrorFiresForAllSubscribersWhenOneThrows() + => AssertGenericFanOutSurvivesThrowingHandler( + NewSampleClient(), "ConnectionError", new Exception("test")); + + /// Pins the behaviour expressed by the test name: internal error handler throwing does not break sample client connection error fan out. + [Test] + public void MTConnectHttpSampleClient_InternalErrorHandlerThrowingDoesNotBreakConnectionErrorFanOut() + => AssertGenericFanOutSurvivesThrowingInternalError( + NewSampleClient(), "ConnectionError", new Exception("test")); + + /// Pins the behaviour expressed by the test name: sample client internal error fires for all subscribers when one throws. + [Test] + public void MTConnectHttpSampleClient_InternalErrorFiresForAllSubscribersWhenOneThrows() + => AssertGenericFanOutSurvivesThrowingHandler( + NewSampleClient(), "InternalError", new Exception("test")); + + + // --------------------------------------------------------------------- + // MTConnectHttpAssetClient — same four events. + // --------------------------------------------------------------------- + + private static MTConnectHttpAssetClient NewAssetClient() + => new MTConnectHttpAssetClient("127.0.0.1", 1); + + /// Pins the behaviour expressed by the test name: asset client mt connect error fires for all subscribers when one throws. + [Test] + public void MTConnectHttpAssetClient_MTConnectErrorFiresForAllSubscribersWhenOneThrows() + => AssertGenericFanOutSurvivesThrowingHandler( + NewAssetClient(), "MTConnectError", new ErrorResponseDocument()); + + /// Pins the behaviour expressed by the test name: internal error handler throwing does not break asset client mt connect error fan out. + [Test] + public void MTConnectHttpAssetClient_InternalErrorHandlerThrowingDoesNotBreakMTConnectErrorFanOut() + => AssertGenericFanOutSurvivesThrowingInternalError( + NewAssetClient(), "MTConnectError", new ErrorResponseDocument()); + + /// Pins the behaviour expressed by the test name: asset client format error fires for all subscribers when one throws. + [Test] + public void MTConnectHttpAssetClient_FormatErrorFiresForAllSubscribersWhenOneThrows() + => AssertGenericFanOutSurvivesThrowingHandler( + NewAssetClient(), "FormatError", new FormatReadResult()); + + /// Pins the behaviour expressed by the test name: internal error handler throwing does not break asset client format error fan out. + [Test] + public void MTConnectHttpAssetClient_InternalErrorHandlerThrowingDoesNotBreakFormatErrorFanOut() + => AssertGenericFanOutSurvivesThrowingInternalError( + NewAssetClient(), "FormatError", new FormatReadResult()); + + /// Pins the behaviour expressed by the test name: asset client connection error fires for all subscribers when one throws. + [Test] + public void MTConnectHttpAssetClient_ConnectionErrorFiresForAllSubscribersWhenOneThrows() + => AssertGenericFanOutSurvivesThrowingHandler( + NewAssetClient(), "ConnectionError", new Exception("test")); + + /// Pins the behaviour expressed by the test name: internal error handler throwing does not break asset client connection error fan out. + [Test] + public void MTConnectHttpAssetClient_InternalErrorHandlerThrowingDoesNotBreakConnectionErrorFanOut() + => AssertGenericFanOutSurvivesThrowingInternalError( + NewAssetClient(), "ConnectionError", new Exception("test")); + + /// Pins the behaviour expressed by the test name: asset client internal error fires for all subscribers when one throws. + [Test] + public void MTConnectHttpAssetClient_InternalErrorFiresForAllSubscribersWhenOneThrows() + => AssertGenericFanOutSurvivesThrowingHandler( + NewAssetClient(), "InternalError", new Exception("test")); + + + // --------------------------------------------------------------------- + // MTConnectHttpClientStream — DocumentReceived, ErrorReceived, + // FormatError, InternalError, ConnectionError (generic); + // Starting, Started, Stopping, Stopped (non-generic). + // --------------------------------------------------------------------- + + private static MTConnectHttpClientStream NewStream() + => new MTConnectHttpClientStream("http://127.0.0.1:1/sample"); + + /// Pins the behaviour expressed by the test name: client stream document received fires for all subscribers when one throws. + [Test] + public void MTConnectHttpClientStream_DocumentReceivedFiresForAllSubscribersWhenOneThrows() + => AssertGenericFanOutSurvivesThrowingHandler( + NewStream(), "DocumentReceived", new MTConnect.Streams.StreamsResponseDocument()); + + /// Pins the behaviour expressed by the test name: internal error handler throwing does not break client stream document received fan out. + [Test] + public void MTConnectHttpClientStream_InternalErrorHandlerThrowingDoesNotBreakDocumentReceivedFanOut() + => AssertGenericFanOutSurvivesThrowingInternalError( + NewStream(), "DocumentReceived", new MTConnect.Streams.StreamsResponseDocument()); + + /// Pins the behaviour expressed by the test name: client stream error received fires for all subscribers when one throws. + [Test] + public void MTConnectHttpClientStream_ErrorReceivedFiresForAllSubscribersWhenOneThrows() + => AssertGenericFanOutSurvivesThrowingHandler( + NewStream(), "ErrorReceived", new ErrorResponseDocument()); + + /// Pins the behaviour expressed by the test name: internal error handler throwing does not break client stream error received fan out. + [Test] + public void MTConnectHttpClientStream_InternalErrorHandlerThrowingDoesNotBreakErrorReceivedFanOut() + => AssertGenericFanOutSurvivesThrowingInternalError( + NewStream(), "ErrorReceived", new ErrorResponseDocument()); + + /// Pins the behaviour expressed by the test name: client stream format error fires for all subscribers when one throws. + [Test] + public void MTConnectHttpClientStream_FormatErrorFiresForAllSubscribersWhenOneThrows() + => AssertGenericFanOutSurvivesThrowingHandler( + NewStream(), "FormatError", new FormatReadResult()); + + /// Pins the behaviour expressed by the test name: internal error handler throwing does not break client stream format error fan out. + [Test] + public void MTConnectHttpClientStream_InternalErrorHandlerThrowingDoesNotBreakFormatErrorFanOut() + => AssertGenericFanOutSurvivesThrowingInternalError( + NewStream(), "FormatError", new FormatReadResult()); + + /// Pins the behaviour expressed by the test name: client stream connection error fires for all subscribers when one throws. + [Test] + public void MTConnectHttpClientStream_ConnectionErrorFiresForAllSubscribersWhenOneThrows() + => AssertGenericFanOutSurvivesThrowingHandler( + NewStream(), "ConnectionError", new Exception("test")); + + /// Pins the behaviour expressed by the test name: internal error handler throwing does not break client stream connection error fan out. + [Test] + public void MTConnectHttpClientStream_InternalErrorHandlerThrowingDoesNotBreakConnectionErrorFanOut() + => AssertGenericFanOutSurvivesThrowingInternalError( + NewStream(), "ConnectionError", new Exception("test")); + + /// Pins the behaviour expressed by the test name: client stream internal error fires for all subscribers when one throws. + [Test] + public void MTConnectHttpClientStream_InternalErrorFiresForAllSubscribersWhenOneThrows() + => AssertGenericFanOutSurvivesThrowingHandler( + NewStream(), "InternalError", new Exception("test")); + + /// Pins the behaviour expressed by the test name: client stream starting fires for all subscribers when one throws. + [Test] + public void MTConnectHttpClientStream_StartingFiresForAllSubscribersWhenOneThrows() + => AssertNonGenericFanOutSurvivesThrowingHandler(NewStream(), "Starting"); + + /// Pins the behaviour expressed by the test name: internal error handler throwing does not break client stream starting fan out. + [Test] + public void MTConnectHttpClientStream_InternalErrorHandlerThrowingDoesNotBreakStartingFanOut() + => AssertNonGenericFanOutSurvivesThrowingInternalError(NewStream(), "Starting"); + + /// Pins the behaviour expressed by the test name: client stream started fires for all subscribers when one throws. + [Test] + public void MTConnectHttpClientStream_StartedFiresForAllSubscribersWhenOneThrows() + => AssertNonGenericFanOutSurvivesThrowingHandler(NewStream(), "Started"); + + /// Pins the behaviour expressed by the test name: internal error handler throwing does not break client stream started fan out. + [Test] + public void MTConnectHttpClientStream_InternalErrorHandlerThrowingDoesNotBreakStartedFanOut() + => AssertNonGenericFanOutSurvivesThrowingInternalError(NewStream(), "Started"); + + /// Pins the behaviour expressed by the test name: client stream stopping fires for all subscribers when one throws. + [Test] + public void MTConnectHttpClientStream_StoppingFiresForAllSubscribersWhenOneThrows() + => AssertNonGenericFanOutSurvivesThrowingHandler(NewStream(), "Stopping"); + + /// Pins the behaviour expressed by the test name: internal error handler throwing does not break client stream stopping fan out. + [Test] + public void MTConnectHttpClientStream_InternalErrorHandlerThrowingDoesNotBreakStoppingFanOut() + => AssertNonGenericFanOutSurvivesThrowingInternalError(NewStream(), "Stopping"); + + /// Pins the behaviour expressed by the test name: client stream stopped fires for all subscribers when one throws. + [Test] + public void MTConnectHttpClientStream_StoppedFiresForAllSubscribersWhenOneThrows() + => AssertNonGenericFanOutSurvivesThrowingHandler(NewStream(), "Stopped"); + + /// Pins the behaviour expressed by the test name: internal error handler throwing does not break client stream stopped fan out. + [Test] + public void MTConnectHttpClientStream_InternalErrorHandlerThrowingDoesNotBreakStoppedFanOut() + => AssertNonGenericFanOutSurvivesThrowingInternalError(NewStream(), "Stopped"); + } +} From d6417cc25d1195e7c921c2d2427da6375de55e89 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Otto=20Boly=C3=B3s?= Date: Wed, 3 Jun 2026 08:22:52 +0200 Subject: [PATCH 06/37] fix(http): isolate multicast across every sub-client raise site Each HTTP sub-client class -- MTConnectHttpProbeClient, MTConnectHttpCurrentClient, MTConnectHttpSampleClient, MTConnectHttpAssetClient, and MTConnectHttpClientStream -- now iterates the multicast invocation list at every event raise site so one throwing subscriber cannot starve later subscribers. Faults are forwarded through the class's own InternalError event; a faulting InternalError handler is also swallowed so the originating fan-out keeps going. Mirrors the existing per-class helper pattern already applied to the outer MTConnectHttpClient. The sub-classes do not share a base type, so a per-class private RaiseEvent helper keeps each class's InternalError routing self-contained and adds no inheritance pressure. MTConnectHttpClientStream gets both the generic RaiseEvent and a non-generic RaiseEvent sibling for the lifecycle events (Starting, Started, Stopping, Stopped). --- .../Clients/MTConnectHttpAssetClient.cs | 58 ++++++++++--- .../Clients/MTConnectHttpClientStream.cs | 82 ++++++++++++++++--- .../Clients/MTConnectHttpCurrentClient.cs | 58 ++++++++++--- .../Clients/MTConnectHttpProbeClient.cs | 58 ++++++++++--- .../Clients/MTConnectHttpSampleClient.cs | 58 ++++++++++--- 5 files changed, 258 insertions(+), 56 deletions(-) diff --git a/libraries/MTConnect.NET-HTTP/Clients/MTConnectHttpAssetClient.cs b/libraries/MTConnect.NET-HTTP/Clients/MTConnectHttpAssetClient.cs index e8484adb..a2e331b0 100644 --- a/libraries/MTConnect.NET-HTTP/Clients/MTConnectHttpAssetClient.cs +++ b/libraries/MTConnect.NET-HTTP/Clients/MTConnectHttpAssetClient.cs @@ -192,16 +192,16 @@ public IAssetsResponseDocument Get() } catch (TaskCanceledException ex) when (ex.InnerException is TimeoutException) { - ConnectionError?.Invoke(this, ex); + RaiseEvent(ConnectionError, ex); } catch (TaskCanceledException) { /* Ignore Task Cancelled */ } catch (HttpRequestException ex) { - ConnectionError?.Invoke(this, ex); + RaiseEvent(ConnectionError, ex); } catch (Exception ex) { - InternalError?.Invoke(this, ex); + RaiseEvent(InternalError, ex); } return null; @@ -246,16 +246,16 @@ public async Task GetAsync(CancellationToken cancellati } catch (TaskCanceledException ex) when (ex.InnerException is TimeoutException) { - ConnectionError?.Invoke(this, ex); + RaiseEvent(ConnectionError, ex); } catch (TaskCanceledException) { /* Ignore Task Cancelled */ } catch (HttpRequestException ex) { - ConnectionError?.Invoke(this, ex); + RaiseEvent(ConnectionError, ex); } catch (Exception ex) { - InternalError?.Invoke(this, ex); + RaiseEvent(InternalError, ex); } return null; @@ -346,7 +346,7 @@ private IAssetsResponseDocument HandleResponse(HttpResponseMessage response) { if (!response.IsSuccessStatusCode) { - ConnectionError?.Invoke(this, new Exception(response.ReasonPhrase)); + RaiseEvent(ConnectionError, new Exception(response.ReasonPhrase)); } else if (response.Content != null) { @@ -364,7 +364,7 @@ private async Task HandleResponseAsync(HttpResponseMess { if (!response.IsSuccessStatusCode) { - ConnectionError?.Invoke(this, new Exception(response.ReasonPhrase)); + RaiseEvent(ConnectionError, new Exception(response.ReasonPhrase)); } else if (response.Content != null) { @@ -407,23 +407,59 @@ private IAssetsResponseDocument ReadDocument(HttpResponseMessage response, Strea if (errorFormatResult.Success) { var errorDocument = errorFormatResult.Content; - if (errorDocument != null) MTConnectError?.Invoke(this, errorDocument); + if (errorDocument != null) RaiseEvent(MTConnectError, errorDocument); } else { // Raise Format Error - if (FormatError != null) FormatError.Invoke(this, errorFormatResult); + RaiseEvent(FormatError, errorFormatResult); } } } else { // Raise Format Error - if (FormatError != null) FormatError.Invoke(this, formatResult); + RaiseEvent(FormatError, formatResult); } } return null; } + + // Iterate the invocation list so one throwing subscriber cannot short-circuit + // the multicast and starve later subscribers. Each fault is forwarded through + // InternalError; if InternalError itself faults, swallow that secondary fault + // so the remaining subscribers in the invocation list still receive the event. + private void RaiseEvent(EventHandler handler, T arg) + { + if (handler == null) return; + + foreach (var subscriber in handler.GetInvocationList()) + { + try + { + ((EventHandler)subscriber).Invoke(this, arg); + } + catch (Exception ex) + { + RouteSubscriberFault(ex); + } + } + } + + // Forwards a subscriber fault to InternalError and swallows any secondary + // fault raised by InternalError itself so the originating event's fan-out + // can keep running. + private void RouteSubscriberFault(Exception ex) + { + try + { + InternalError?.Invoke(this, ex); + } + catch + { + // A faulting InternalError handler must not break the event fan-out. + } + } } } \ No newline at end of file diff --git a/libraries/MTConnect.NET-HTTP/Clients/MTConnectHttpClientStream.cs b/libraries/MTConnect.NET-HTTP/Clients/MTConnectHttpClientStream.cs index 9fe6dc59..6fa09090 100644 --- a/libraries/MTConnect.NET-HTTP/Clients/MTConnectHttpClientStream.cs +++ b/libraries/MTConnect.NET-HTTP/Clients/MTConnectHttpClientStream.cs @@ -123,7 +123,7 @@ public void Start(CancellationToken cancellationToken) cancellationToken.Register(() => Stop()); // Raise Starting Event - Starting?.Invoke(this, new EventArgs()); + RaiseEvent(Starting, EventArgs.Empty); _ = Task.Run(() => Run(_stop.Token)); } @@ -136,7 +136,7 @@ public void Start(CancellationToken cancellationToken) public void Stop() { // Raise Stopping Event - Stopping?.Invoke(this, new EventArgs()); + RaiseEvent(Stopping, EventArgs.Empty); if (_stop != null) _stop.Cancel(); } @@ -167,7 +167,7 @@ public async Task Run(CancellationToken cancellationToken) responseTimer.Elapsed += (o, e) => { stop.Cancel(); - ConnectionError?.Invoke(this, new TimeoutException($"HTTP Stream Timeout Exceeded ({Timeout})")); + RaiseEvent(ConnectionError, new TimeoutException($"HTTP Stream Timeout Exceeded ({Timeout})")); }; responseTimer.Start(); } @@ -177,7 +177,7 @@ public async Task Run(CancellationToken cancellationToken) stop.Token.ThrowIfCancellationRequested(); // Raise Started Event - Started?.Invoke(this, new EventArgs()); + RaiseEvent(Started, EventArgs.Empty); // Add 'Accept' HTTP Header @@ -305,16 +305,16 @@ public async Task Run(CancellationToken cancellationToken) } catch (TaskCanceledException ex) when (ex.InnerException is TimeoutException) { - ConnectionError?.Invoke(this, ex); + RaiseEvent(ConnectionError, ex); } catch (TaskCanceledException) { /* Ignore Task Cancelled */ } catch (HttpRequestException ex) { - ConnectionError?.Invoke(this, ex); + RaiseEvent(ConnectionError, ex); } catch (Exception ex) { - InternalError?.Invoke(this, ex); + RaiseEvent(InternalError, ex); } finally { @@ -322,7 +322,7 @@ public async Task Run(CancellationToken cancellationToken) } } - Stopped?.Invoke(this, new EventArgs()); + RaiseEvent(Stopped, EventArgs.Empty); } private static string GetHeaderValue(string s, string name) @@ -411,7 +411,7 @@ protected virtual void ProcessResponseBody(Stream responseBody, string contentEn var document = formatResult.Content; if (document != null) { - DocumentReceived?.Invoke(this, document); + RaiseEvent(DocumentReceived, document); } else { @@ -420,21 +420,79 @@ protected virtual void ProcessResponseBody(Stream responseBody, string contentEn if (errorFormatResult.Success) { var errorDocument = errorFormatResult.Content; - if (errorDocument != null) ErrorReceived?.Invoke(this, errorDocument); + if (errorDocument != null) RaiseEvent(ErrorReceived, errorDocument); } else { // Raise Format Error - if (FormatError != null) FormatError.Invoke(this, errorFormatResult); + RaiseEvent(FormatError, errorFormatResult); } } } else { // Raise Format Error - if (FormatError != null) FormatError.Invoke(this, formatResult); + RaiseEvent(FormatError, formatResult); } } } + + // Iterate the invocation list so one throwing subscriber cannot short-circuit + // the multicast and starve later subscribers. Each fault is forwarded through + // InternalError; if InternalError itself faults, swallow that secondary fault + // so the remaining subscribers in the invocation list still receive the event. + private void RaiseEvent(EventHandler handler, T arg) + { + if (handler == null) return; + + foreach (var subscriber in handler.GetInvocationList()) + { + try + { + ((EventHandler)subscriber).Invoke(this, arg); + } + catch (Exception ex) + { + RouteSubscriberFault(ex); + } + } + } + + // Non-generic sibling of RaiseEvent<T> for EventHandler events that carry no + // typed payload (Starting, Started, Stopping, Stopped). Same multicast-isolation + // contract: a throwing subscriber cannot starve later subscribers, and a + // faulting InternalError handler cannot break the fan-out either. + private void RaiseEvent(EventHandler handler, EventArgs arg) + { + if (handler == null) return; + + foreach (var subscriber in handler.GetInvocationList()) + { + try + { + ((EventHandler)subscriber).Invoke(this, arg); + } + catch (Exception ex) + { + RouteSubscriberFault(ex); + } + } + } + + // Forwards a subscriber fault to InternalError and swallows any secondary + // fault raised by InternalError itself so the originating event's fan-out + // can keep running. Shared between the generic and non-generic RaiseEvent + // helpers above. + private void RouteSubscriberFault(Exception ex) + { + try + { + InternalError?.Invoke(this, ex); + } + catch + { + // A faulting InternalError handler must not break the event fan-out. + } + } } } \ No newline at end of file diff --git a/libraries/MTConnect.NET-HTTP/Clients/MTConnectHttpCurrentClient.cs b/libraries/MTConnect.NET-HTTP/Clients/MTConnectHttpCurrentClient.cs index 5d681081..7e603959 100644 --- a/libraries/MTConnect.NET-HTTP/Clients/MTConnectHttpCurrentClient.cs +++ b/libraries/MTConnect.NET-HTTP/Clients/MTConnectHttpCurrentClient.cs @@ -196,16 +196,16 @@ public IStreamsResponseDocument Get() } catch (TaskCanceledException ex) when (ex.InnerException is TimeoutException) { - ConnectionError?.Invoke(this, ex); + RaiseEvent(ConnectionError, ex); } catch (TaskCanceledException) { /* Ignore Task Cancelled */ } catch (HttpRequestException ex) { - ConnectionError?.Invoke(this, ex); + RaiseEvent(ConnectionError, ex); } catch (Exception ex) { - InternalError?.Invoke(this, ex); + RaiseEvent(InternalError, ex); } return null; @@ -250,16 +250,16 @@ public async Task GetAsync(CancellationToken cancellat } catch (TaskCanceledException ex) when (ex.InnerException is TimeoutException) { - ConnectionError?.Invoke(this, ex); + RaiseEvent(ConnectionError, ex); } catch (TaskCanceledException) { /* Ignore Task Cancelled */ } catch (HttpRequestException ex) { - ConnectionError?.Invoke(this, ex); + RaiseEvent(ConnectionError, ex); } catch (Exception ex) { - InternalError?.Invoke(this, ex); + RaiseEvent(InternalError, ex); } return null; @@ -348,7 +348,7 @@ private IStreamsResponseDocument HandleResponse(HttpResponseMessage response) { if (!response.IsSuccessStatusCode) { - ConnectionError?.Invoke(this, new Exception(response.ReasonPhrase)); + RaiseEvent(ConnectionError, new Exception(response.ReasonPhrase)); } else if (response.Content != null) { @@ -366,7 +366,7 @@ private async Task HandleResponseAsync(HttpResponseMes { if (!response.IsSuccessStatusCode) { - ConnectionError?.Invoke(this, new Exception(response.ReasonPhrase)); + RaiseEvent(ConnectionError, new Exception(response.ReasonPhrase)); } else if (response.Content != null) { @@ -408,23 +408,59 @@ private IStreamsResponseDocument ReadDocument(HttpResponseMessage response, Stre if (errorFormatResult.Success) { var errorDocument = errorFormatResult.Content; - if (errorDocument != null) MTConnectError?.Invoke(this, errorDocument); + if (errorDocument != null) RaiseEvent(MTConnectError, errorDocument); } else { // Raise Format Error - if (FormatError != null) FormatError.Invoke(this, errorFormatResult); + RaiseEvent(FormatError, errorFormatResult); } } } else { // Raise Format Error - if (FormatError != null) FormatError.Invoke(this, formatResult); + RaiseEvent(FormatError, formatResult); } } return null; } + + // Iterate the invocation list so one throwing subscriber cannot short-circuit + // the multicast and starve later subscribers. Each fault is forwarded through + // InternalError; if InternalError itself faults, swallow that secondary fault + // so the remaining subscribers in the invocation list still receive the event. + private void RaiseEvent(EventHandler handler, T arg) + { + if (handler == null) return; + + foreach (var subscriber in handler.GetInvocationList()) + { + try + { + ((EventHandler)subscriber).Invoke(this, arg); + } + catch (Exception ex) + { + RouteSubscriberFault(ex); + } + } + } + + // Forwards a subscriber fault to InternalError and swallows any secondary + // fault raised by InternalError itself so the originating event's fan-out + // can keep running. + private void RouteSubscriberFault(Exception ex) + { + try + { + InternalError?.Invoke(this, ex); + } + catch + { + // A faulting InternalError handler must not break the event fan-out. + } + } } } \ No newline at end of file diff --git a/libraries/MTConnect.NET-HTTP/Clients/MTConnectHttpProbeClient.cs b/libraries/MTConnect.NET-HTTP/Clients/MTConnectHttpProbeClient.cs index e34729c9..268ece2d 100644 --- a/libraries/MTConnect.NET-HTTP/Clients/MTConnectHttpProbeClient.cs +++ b/libraries/MTConnect.NET-HTTP/Clients/MTConnectHttpProbeClient.cs @@ -236,16 +236,16 @@ public IDevicesResponseDocument Get() } catch (TaskCanceledException ex) when (ex.InnerException is TimeoutException) { - ConnectionError?.Invoke(this, ex); + RaiseEvent(ConnectionError, ex); } catch (TaskCanceledException) { /* Ignore Task Cancelled */ } catch (HttpRequestException ex) { - ConnectionError?.Invoke(this, ex); + RaiseEvent(ConnectionError, ex); } catch (Exception ex) { - InternalError?.Invoke(this, ex); + RaiseEvent(InternalError, ex); } return null; @@ -297,16 +297,16 @@ public async Task GetAsync(CancellationToken cancellat } catch (TaskCanceledException ex) when (ex.InnerException is TimeoutException) { - ConnectionError?.Invoke(this, ex); + RaiseEvent(ConnectionError, ex); } catch (TaskCanceledException) { /* Ignore Task Cancelled */ } catch (HttpRequestException ex) { - ConnectionError?.Invoke(this, ex); + RaiseEvent(ConnectionError, ex); } catch (Exception ex) { - InternalError?.Invoke(this, ex); + RaiseEvent(InternalError, ex); } return null; @@ -388,7 +388,7 @@ private IDevicesResponseDocument HandleResponse(HttpResponseMessage response) { if (!response.IsSuccessStatusCode) { - ConnectionError?.Invoke(this, new Exception(response.ReasonPhrase)); + RaiseEvent(ConnectionError, new Exception(response.ReasonPhrase)); } else if (response.Content != null) { @@ -406,7 +406,7 @@ private async Task HandleResponseAsync(HttpResponseMes { if (!response.IsSuccessStatusCode) { - ConnectionError?.Invoke(this, new Exception(response.ReasonPhrase)); + RaiseEvent(ConnectionError, new Exception(response.ReasonPhrase)); } else if (response.Content != null) { @@ -448,23 +448,59 @@ private IDevicesResponseDocument ReadDocument(HttpResponseMessage response, Stre if (errorFormatResult.Success) { var errorDocument = errorFormatResult.Content; - if (errorDocument != null) MTConnectError?.Invoke(this, errorDocument); + if (errorDocument != null) RaiseEvent(MTConnectError, errorDocument); } else { // Raise Format Error - if (FormatError != null) FormatError.Invoke(this, errorFormatResult); + RaiseEvent(FormatError, errorFormatResult); } } } else { // Raise Format Error - if (FormatError != null) FormatError.Invoke(this, formatResult); + RaiseEvent(FormatError, formatResult); } } return null; } + + // Iterate the invocation list so one throwing subscriber cannot short-circuit + // the multicast and starve later subscribers. Each fault is forwarded through + // InternalError; if InternalError itself faults, swallow that secondary fault + // so the remaining subscribers in the invocation list still receive the event. + private void RaiseEvent(EventHandler handler, T arg) + { + if (handler == null) return; + + foreach (var subscriber in handler.GetInvocationList()) + { + try + { + ((EventHandler)subscriber).Invoke(this, arg); + } + catch (Exception ex) + { + RouteSubscriberFault(ex); + } + } + } + + // Forwards a subscriber fault to InternalError and swallows any secondary + // fault raised by InternalError itself so the originating event's fan-out + // can keep running. + private void RouteSubscriberFault(Exception ex) + { + try + { + InternalError?.Invoke(this, ex); + } + catch + { + // A faulting InternalError handler must not break the event fan-out. + } + } } } \ No newline at end of file diff --git a/libraries/MTConnect.NET-HTTP/Clients/MTConnectHttpSampleClient.cs b/libraries/MTConnect.NET-HTTP/Clients/MTConnectHttpSampleClient.cs index 7a053500..87a19f1c 100644 --- a/libraries/MTConnect.NET-HTTP/Clients/MTConnectHttpSampleClient.cs +++ b/libraries/MTConnect.NET-HTTP/Clients/MTConnectHttpSampleClient.cs @@ -213,16 +213,16 @@ public IStreamsResponseDocument Get() } catch (TaskCanceledException ex) when (ex.InnerException is TimeoutException) { - ConnectionError?.Invoke(this, ex); + RaiseEvent(ConnectionError, ex); } catch (TaskCanceledException) { /* Ignore Task Cancelled */ } catch (HttpRequestException ex) { - ConnectionError?.Invoke(this, ex); + RaiseEvent(ConnectionError, ex); } catch (Exception ex) { - InternalError?.Invoke(this, ex); + RaiseEvent(InternalError, ex); } return null; @@ -267,16 +267,16 @@ public async Task GetAsync(CancellationToken cancel) } catch (TaskCanceledException ex) when (ex.InnerException is TimeoutException) { - ConnectionError?.Invoke(this, ex); + RaiseEvent(ConnectionError, ex); } catch (TaskCanceledException) { /* Ignore Task Cancelled */ } catch (HttpRequestException ex) { - ConnectionError?.Invoke(this, ex); + RaiseEvent(ConnectionError, ex); } catch (Exception ex) { - InternalError?.Invoke(this, ex); + RaiseEvent(InternalError, ex); } return null; @@ -382,7 +382,7 @@ private IStreamsResponseDocument HandleResponse(HttpResponseMessage response) { if (!response.IsSuccessStatusCode) { - ConnectionError?.Invoke(this, new Exception(response.ReasonPhrase)); + RaiseEvent(ConnectionError, new Exception(response.ReasonPhrase)); } else if (response.Content != null) { @@ -400,7 +400,7 @@ private async Task HandleResponseAsync(HttpResponseMes { if (!response.IsSuccessStatusCode) { - ConnectionError?.Invoke(this, new Exception(response.ReasonPhrase)); + RaiseEvent(ConnectionError, new Exception(response.ReasonPhrase)); } else if (response.Content != null) { @@ -442,23 +442,59 @@ private IStreamsResponseDocument ReadDocument(HttpResponseMessage response, Stre if (errorFormatResult.Success) { var errorDocument = errorFormatResult.Content; - if (errorDocument != null) MTConnectError?.Invoke(this, errorDocument); + if (errorDocument != null) RaiseEvent(MTConnectError, errorDocument); } else { // Raise Format Error - if (FormatError != null) FormatError.Invoke(this, errorFormatResult); + RaiseEvent(FormatError, errorFormatResult); } } } else { // Raise Format Error - if (FormatError != null) FormatError.Invoke(this, formatResult); + RaiseEvent(FormatError, formatResult); } } return null; } + + // Iterate the invocation list so one throwing subscriber cannot short-circuit + // the multicast and starve later subscribers. Each fault is forwarded through + // InternalError; if InternalError itself faults, swallow that secondary fault + // so the remaining subscribers in the invocation list still receive the event. + private void RaiseEvent(EventHandler handler, T arg) + { + if (handler == null) return; + + foreach (var subscriber in handler.GetInvocationList()) + { + try + { + ((EventHandler)subscriber).Invoke(this, arg); + } + catch (Exception ex) + { + RouteSubscriberFault(ex); + } + } + } + + // Forwards a subscriber fault to InternalError and swallows any secondary + // fault raised by InternalError itself so the originating event's fan-out + // can keep running. + private void RouteSubscriberFault(Exception ex) + { + try + { + InternalError?.Invoke(this, ex); + } + catch + { + // A faulting InternalError handler must not break the event fan-out. + } + } } } \ No newline at end of file From 3bb8cdd75b340331987d5b5ec4b660c9fe2541ce Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Otto=20Boly=C3=B3s?= Date: Wed, 3 Jun 2026 08:24:45 +0200 Subject: [PATCH 07/37] test(http-tests): close over the open generic RaiseEvent helper before invoking --- .../SubClientMulticastIsolationTests.cs | 30 ++++++++++++++----- 1 file changed, 23 insertions(+), 7 deletions(-) diff --git a/tests/MTConnect.NET-HTTP-Tests/Clients/SubClientMulticastIsolationTests.cs b/tests/MTConnect.NET-HTTP-Tests/Clients/SubClientMulticastIsolationTests.cs index d1a1b77e..f040dd1b 100644 --- a/tests/MTConnect.NET-HTTP-Tests/Clients/SubClientMulticastIsolationTests.cs +++ b/tests/MTConnect.NET-HTTP-Tests/Clients/SubClientMulticastIsolationTests.cs @@ -62,14 +62,30 @@ private static T GetEventBacking(object instance, string eventName) where T : private static void InvokeGenericRaise(object instance, string eventName, T arg) { var handler = GetEventBacking>(instance, eventName); - var method = instance.GetType().GetMethod("RaiseEvent", - BindingFlags.Instance | BindingFlags.NonPublic, - binder: null, - types: new[] { typeof(EventHandler), typeof(T) }, - modifiers: null); - Assert.That(method, Is.Not.Null, + + // Locate the open generic RaiseEvent(EventHandler, T) helper by + // walking the non-public instance methods; GetMethod's overload that + // takes a types[] array binds parameters against the open signature + // and never matches a closed EventHandler, so the manual + // search is the simplest path. Once found, MakeGenericMethod closes + // it over the test's payload type. + MethodInfo? openMethod = null; + foreach (var candidate in instance.GetType().GetMethods( + BindingFlags.Instance | BindingFlags.NonPublic)) + { + if (candidate.Name != "RaiseEvent") continue; + if (!candidate.IsGenericMethodDefinition) continue; + var parameters = candidate.GetParameters(); + if (parameters.Length != 2) continue; + if (parameters[0].ParameterType.GetGenericTypeDefinition() != typeof(EventHandler<>)) continue; + openMethod = candidate; + break; + } + Assert.That(openMethod, Is.Not.Null, $"Generic RaiseEvent helper was not found on {instance.GetType().Name}."); - method!.Invoke(instance, new object?[] { handler, arg }); + + var closedMethod = openMethod!.MakeGenericMethod(typeof(T)); + closedMethod.Invoke(instance, new object?[] { handler, arg }); } private static void InvokeNonGenericRaise(object instance, string eventName, EventArgs arg) From d735b1a7983a8b575d9980779c98c98d37726e49 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Otto=20Boly=C3=B3s?= Date: Wed, 3 Jun 2026 11:39:30 +0200 Subject: [PATCH 08/37] docs(http-tests): scrub BrE behaviour to AmE in test XML docs --- .../Clients/MulticastIsolationTests.cs | 24 ++--- .../NonGenericMulticastIsolationTests.cs | 32 +++---- .../SubClientMulticastIsolationTests.cs | 90 +++++++++---------- ...evicesAndDeviceReceivedIntegrationTests.cs | 6 +- 4 files changed, 76 insertions(+), 76 deletions(-) diff --git a/tests/MTConnect.NET-HTTP-Tests/Clients/MulticastIsolationTests.cs b/tests/MTConnect.NET-HTTP-Tests/Clients/MulticastIsolationTests.cs index 6f43d380..bc0383ee 100644 --- a/tests/MTConnect.NET-HTTP-Tests/Clients/MulticastIsolationTests.cs +++ b/tests/MTConnect.NET-HTTP-Tests/Clients/MulticastIsolationTests.cs @@ -40,7 +40,7 @@ public class MulticastIsolationTests : HttpClientFixture // ProbeReceived // --------------------------------------------------------------------- - /// Pins the behaviour expressed by the test name: probe received fires for all subscribers when one throws. + /// Pins the behavior expressed by the test name: probe received fires for all subscribers when one throws. [Test] public void ProbeReceivedFiresForAllSubscribersWhenOneThrows() { @@ -67,7 +67,7 @@ public void ProbeReceivedFiresForAllSubscribersWhenOneThrows() } } - /// Pins the behaviour expressed by the test name: internal error handler throwing does not break probe received fan out. + /// Pins the behavior expressed by the test name: internal error handler throwing does not break probe received fan out. [Test] public void InternalErrorHandlerThrowingDoesNotBreakProbeReceivedFanOut() { @@ -100,7 +100,7 @@ public void InternalErrorHandlerThrowingDoesNotBreakProbeReceivedFanOut() // CurrentReceived // --------------------------------------------------------------------- - /// Pins the behaviour expressed by the test name: current received fires for all subscribers when one throws. + /// Pins the behavior expressed by the test name: current received fires for all subscribers when one throws. [Test] public void CurrentReceivedFiresForAllSubscribersWhenOneThrows() { @@ -127,7 +127,7 @@ public void CurrentReceivedFiresForAllSubscribersWhenOneThrows() } } - /// Pins the behaviour expressed by the test name: internal error handler throwing does not break current received fan out. + /// Pins the behavior expressed by the test name: internal error handler throwing does not break current received fan out. [Test] public void InternalErrorHandlerThrowingDoesNotBreakCurrentReceivedFanOut() { @@ -160,7 +160,7 @@ public void InternalErrorHandlerThrowingDoesNotBreakCurrentReceivedFanOut() // SampleReceived // --------------------------------------------------------------------- - /// Pins the behaviour expressed by the test name: sample received fires for all subscribers when one throws. + /// Pins the behavior expressed by the test name: sample received fires for all subscribers when one throws. [Test] public void SampleReceivedFiresForAllSubscribersWhenOneThrows() { @@ -199,7 +199,7 @@ public void SampleReceivedFiresForAllSubscribersWhenOneThrows() } } - /// Pins the behaviour expressed by the test name: internal error handler throwing does not break sample received fan out. + /// Pins the behavior expressed by the test name: internal error handler throwing does not break sample received fan out. [Test] public void InternalErrorHandlerThrowingDoesNotBreakSampleReceivedFanOut() { @@ -252,7 +252,7 @@ private void PushAvailabilityTransition() // ObservationReceived // --------------------------------------------------------------------- - /// Pins the behaviour expressed by the test name: observation received fires for all subscribers when one throws. + /// Pins the behavior expressed by the test name: observation received fires for all subscribers when one throws. [Test] public void ObservationReceivedFiresForAllSubscribersWhenOneThrows() { @@ -279,7 +279,7 @@ public void ObservationReceivedFiresForAllSubscribersWhenOneThrows() } } - /// Pins the behaviour expressed by the test name: internal error handler throwing does not break observation received fan out. + /// Pins the behavior expressed by the test name: internal error handler throwing does not break observation received fan out. [Test] public void InternalErrorHandlerThrowingDoesNotBreakObservationReceivedFanOut() { @@ -319,7 +319,7 @@ public void InternalErrorHandlerThrowingDoesNotBreakObservationReceivedFanOut() // and the broker accepts the matching observation. // --------------------------------------------------------------------- - /// Pins the behaviour expressed by the test name: assets received fires for all subscribers when one throws. + /// Pins the behavior expressed by the test name: assets received fires for all subscribers when one throws. [Test] public void AssetsReceivedFiresForAllSubscribersWhenOneThrows() { @@ -348,7 +348,7 @@ public void AssetsReceivedFiresForAllSubscribersWhenOneThrows() } } - /// Pins the behaviour expressed by the test name: internal error handler throwing does not break assets received fan out. + /// Pins the behavior expressed by the test name: internal error handler throwing does not break assets received fan out. [Test] public void InternalErrorHandlerThrowingDoesNotBreakAssetsReceivedFanOut() { @@ -378,7 +378,7 @@ public void InternalErrorHandlerThrowingDoesNotBreakAssetsReceivedFanOut() } } - /// Pins the behaviour expressed by the test name: asset received fires for all subscribers when one throws. + /// Pins the behavior expressed by the test name: asset received fires for all subscribers when one throws. [Test] public void AssetReceivedFiresForAllSubscribersWhenOneThrows() { @@ -407,7 +407,7 @@ public void AssetReceivedFiresForAllSubscribersWhenOneThrows() } } - /// Pins the behaviour expressed by the test name: internal error handler throwing does not break asset received fan out. + /// Pins the behavior expressed by the test name: internal error handler throwing does not break asset received fan out. [Test] public void InternalErrorHandlerThrowingDoesNotBreakAssetReceivedFanOut() { diff --git a/tests/MTConnect.NET-HTTP-Tests/Clients/NonGenericMulticastIsolationTests.cs b/tests/MTConnect.NET-HTTP-Tests/Clients/NonGenericMulticastIsolationTests.cs index 49cb29e8..8930604d 100644 --- a/tests/MTConnect.NET-HTTP-Tests/Clients/NonGenericMulticastIsolationTests.cs +++ b/tests/MTConnect.NET-HTTP-Tests/Clients/NonGenericMulticastIsolationTests.cs @@ -58,7 +58,7 @@ public class NonGenericMulticastIsolationTests : HttpClientFixture // raise-site invariant under test. // --------------------------------------------------------------------- - /// Pins the behaviour expressed by the test name: client starting fires for all subscribers when one throws. + /// Pins the behavior expressed by the test name: client starting fires for all subscribers when one throws. [Test] public void ClientStartingFiresForAllSubscribersWhenOneThrows() { @@ -82,7 +82,7 @@ public void ClientStartingFiresForAllSubscribersWhenOneThrows() } } - /// Pins the behaviour expressed by the test name: internal error handler throwing does not break client starting fan out. + /// Pins the behavior expressed by the test name: internal error handler throwing does not break client starting fan out. [Test] public void InternalErrorHandlerThrowingDoesNotBreakClientStartingFanOut() { @@ -108,7 +108,7 @@ public void InternalErrorHandlerThrowingDoesNotBreakClientStartingFanOut() } - /// Pins the behaviour expressed by the test name: client started fires for all subscribers when one throws. + /// Pins the behavior expressed by the test name: client started fires for all subscribers when one throws. [Test] public void ClientStartedFiresForAllSubscribersWhenOneThrows() { @@ -132,7 +132,7 @@ public void ClientStartedFiresForAllSubscribersWhenOneThrows() } } - /// Pins the behaviour expressed by the test name: internal error handler throwing does not break client started fan out. + /// Pins the behavior expressed by the test name: internal error handler throwing does not break client started fan out. [Test] public void InternalErrorHandlerThrowingDoesNotBreakClientStartedFanOut() { @@ -158,7 +158,7 @@ public void InternalErrorHandlerThrowingDoesNotBreakClientStartedFanOut() } - /// Pins the behaviour expressed by the test name: client stopping fires for all subscribers when one throws. + /// Pins the behavior expressed by the test name: client stopping fires for all subscribers when one throws. [Test] public void ClientStoppingFiresForAllSubscribersWhenOneThrows() { @@ -176,7 +176,7 @@ public void ClientStoppingFiresForAllSubscribersWhenOneThrows() "subscribers after a throwing one must still receive ClientStopping"); } - /// Pins the behaviour expressed by the test name: internal error handler throwing does not break client stopping fan out. + /// Pins the behavior expressed by the test name: internal error handler throwing does not break client stopping fan out. [Test] public void InternalErrorHandlerThrowingDoesNotBreakClientStoppingFanOut() { @@ -196,7 +196,7 @@ public void InternalErrorHandlerThrowingDoesNotBreakClientStoppingFanOut() } - /// Pins the behaviour expressed by the test name: client stopped fires for all subscribers when one throws. + /// Pins the behavior expressed by the test name: client stopped fires for all subscribers when one throws. [Test] public void ClientStoppedFiresForAllSubscribersWhenOneThrows() { @@ -222,7 +222,7 @@ public void ClientStoppedFiresForAllSubscribersWhenOneThrows() "subscribers after a throwing one must still receive ClientStopped"); } - /// Pins the behaviour expressed by the test name: internal error handler throwing does not break client stopped fan out. + /// Pins the behavior expressed by the test name: internal error handler throwing does not break client stopped fan out. [Test] public void InternalErrorHandlerThrowingDoesNotBreakClientStoppedFanOut() { @@ -247,7 +247,7 @@ public void InternalErrorHandlerThrowingDoesNotBreakClientStoppedFanOut() } - /// Pins the behaviour expressed by the test name: response received fires for all subscribers when one throws. + /// Pins the behavior expressed by the test name: response received fires for all subscribers when one throws. [Test] public void ResponseReceivedFiresForAllSubscribersWhenOneThrows() { @@ -271,7 +271,7 @@ public void ResponseReceivedFiresForAllSubscribersWhenOneThrows() } } - /// Pins the behaviour expressed by the test name: internal error handler throwing does not break response received fan out. + /// Pins the behavior expressed by the test name: internal error handler throwing does not break response received fan out. [Test] public void InternalErrorHandlerThrowingDoesNotBreakResponseReceivedFanOut() { @@ -309,7 +309,7 @@ public void InternalErrorHandlerThrowingDoesNotBreakResponseReceivedFanOut() // RaiseEvent helper. // --------------------------------------------------------------------- - /// Pins the behaviour expressed by the test name: stream started fires for all subscribers when one throws. + /// Pins the behavior expressed by the test name: stream started fires for all subscribers when one throws. [Test] public void StreamStartedFiresForAllSubscribersWhenOneThrows() { @@ -336,7 +336,7 @@ public void StreamStartedFiresForAllSubscribersWhenOneThrows() } } - /// Pins the behaviour expressed by the test name: internal error handler throwing does not break stream started fan out. + /// Pins the behavior expressed by the test name: internal error handler throwing does not break stream started fan out. [Test] public void InternalErrorHandlerThrowingDoesNotBreakStreamStartedFanOut() { @@ -365,7 +365,7 @@ public void InternalErrorHandlerThrowingDoesNotBreakStreamStartedFanOut() } - /// Pins the behaviour expressed by the test name: stream stopped fires for all subscribers when one throws. + /// Pins the behavior expressed by the test name: stream stopped fires for all subscribers when one throws. [Test] public void StreamStoppedFiresForAllSubscribersWhenOneThrows() { @@ -391,7 +391,7 @@ public void StreamStoppedFiresForAllSubscribersWhenOneThrows() "subscribers after a throwing one must still receive StreamStopped"); } - /// Pins the behaviour expressed by the test name: internal error handler throwing does not break stream stopped fan out. + /// Pins the behavior expressed by the test name: internal error handler throwing does not break stream stopped fan out. [Test] public void InternalErrorHandlerThrowingDoesNotBreakStreamStoppedFanOut() { @@ -426,7 +426,7 @@ public void InternalErrorHandlerThrowingDoesNotBreakStreamStoppedFanOut() // then fans out ConnectionError. // --------------------------------------------------------------------- - /// Pins the behaviour expressed by the test name: connection error fires for all subscribers when one throws. + /// Pins the behavior expressed by the test name: connection error fires for all subscribers when one throws. [Test] public void ConnectionErrorFiresForAllSubscribersWhenOneThrows() { @@ -449,7 +449,7 @@ public void ConnectionErrorFiresForAllSubscribersWhenOneThrows() "subscribers after a throwing one must still receive ConnectionError"); } - /// Pins the behaviour expressed by the test name: internal error handler throwing does not break connection error fan out. + /// Pins the behavior expressed by the test name: internal error handler throwing does not break connection error fan out. [Test] public void InternalErrorHandlerThrowingDoesNotBreakConnectionErrorFanOut() { diff --git a/tests/MTConnect.NET-HTTP-Tests/Clients/SubClientMulticastIsolationTests.cs b/tests/MTConnect.NET-HTTP-Tests/Clients/SubClientMulticastIsolationTests.cs index f040dd1b..a61413af 100644 --- a/tests/MTConnect.NET-HTTP-Tests/Clients/SubClientMulticastIsolationTests.cs +++ b/tests/MTConnect.NET-HTTP-Tests/Clients/SubClientMulticastIsolationTests.cs @@ -220,43 +220,43 @@ private static void AddNonGenericHandler(object instance, string eventName, Even private static MTConnectHttpProbeClient NewProbeClient() => new MTConnectHttpProbeClient("127.0.0.1", 1); - /// Pins the behaviour expressed by the test name: probe client mt connect error fires for all subscribers when one throws. + /// Pins the behavior expressed by the test name: probe client mt connect error fires for all subscribers when one throws. [Test] public void MTConnectHttpProbeClient_MTConnectErrorFiresForAllSubscribersWhenOneThrows() => AssertGenericFanOutSurvivesThrowingHandler( NewProbeClient(), "MTConnectError", new ErrorResponseDocument()); - /// Pins the behaviour expressed by the test name: internal error handler throwing does not break probe client mt connect error fan out. + /// Pins the behavior expressed by the test name: internal error handler throwing does not break probe client mt connect error fan out. [Test] public void MTConnectHttpProbeClient_InternalErrorHandlerThrowingDoesNotBreakMTConnectErrorFanOut() => AssertGenericFanOutSurvivesThrowingInternalError( NewProbeClient(), "MTConnectError", new ErrorResponseDocument()); - /// Pins the behaviour expressed by the test name: probe client format error fires for all subscribers when one throws. + /// Pins the behavior expressed by the test name: probe client format error fires for all subscribers when one throws. [Test] public void MTConnectHttpProbeClient_FormatErrorFiresForAllSubscribersWhenOneThrows() => AssertGenericFanOutSurvivesThrowingHandler( NewProbeClient(), "FormatError", new FormatReadResult()); - /// Pins the behaviour expressed by the test name: internal error handler throwing does not break probe client format error fan out. + /// Pins the behavior expressed by the test name: internal error handler throwing does not break probe client format error fan out. [Test] public void MTConnectHttpProbeClient_InternalErrorHandlerThrowingDoesNotBreakFormatErrorFanOut() => AssertGenericFanOutSurvivesThrowingInternalError( NewProbeClient(), "FormatError", new FormatReadResult()); - /// Pins the behaviour expressed by the test name: probe client connection error fires for all subscribers when one throws. + /// Pins the behavior expressed by the test name: probe client connection error fires for all subscribers when one throws. [Test] public void MTConnectHttpProbeClient_ConnectionErrorFiresForAllSubscribersWhenOneThrows() => AssertGenericFanOutSurvivesThrowingHandler( NewProbeClient(), "ConnectionError", new Exception("test")); - /// Pins the behaviour expressed by the test name: internal error handler throwing does not break probe client connection error fan out. + /// Pins the behavior expressed by the test name: internal error handler throwing does not break probe client connection error fan out. [Test] public void MTConnectHttpProbeClient_InternalErrorHandlerThrowingDoesNotBreakConnectionErrorFanOut() => AssertGenericFanOutSurvivesThrowingInternalError( NewProbeClient(), "ConnectionError", new Exception("test")); - /// Pins the behaviour expressed by the test name: probe client internal error fires for all subscribers when one throws. + /// Pins the behavior expressed by the test name: probe client internal error fires for all subscribers when one throws. [Test] public void MTConnectHttpProbeClient_InternalErrorFiresForAllSubscribersWhenOneThrows() => AssertGenericFanOutSurvivesThrowingHandler( @@ -277,43 +277,43 @@ public void MTConnectHttpProbeClient_InternalErrorFiresForAllSubscribersWhenOneT private static MTConnectHttpCurrentClient NewCurrentClient() => new MTConnectHttpCurrentClient("127.0.0.1", 1); - /// Pins the behaviour expressed by the test name: current client mt connect error fires for all subscribers when one throws. + /// Pins the behavior expressed by the test name: current client mt connect error fires for all subscribers when one throws. [Test] public void MTConnectHttpCurrentClient_MTConnectErrorFiresForAllSubscribersWhenOneThrows() => AssertGenericFanOutSurvivesThrowingHandler( NewCurrentClient(), "MTConnectError", new ErrorResponseDocument()); - /// Pins the behaviour expressed by the test name: internal error handler throwing does not break current client mt connect error fan out. + /// Pins the behavior expressed by the test name: internal error handler throwing does not break current client mt connect error fan out. [Test] public void MTConnectHttpCurrentClient_InternalErrorHandlerThrowingDoesNotBreakMTConnectErrorFanOut() => AssertGenericFanOutSurvivesThrowingInternalError( NewCurrentClient(), "MTConnectError", new ErrorResponseDocument()); - /// Pins the behaviour expressed by the test name: current client format error fires for all subscribers when one throws. + /// Pins the behavior expressed by the test name: current client format error fires for all subscribers when one throws. [Test] public void MTConnectHttpCurrentClient_FormatErrorFiresForAllSubscribersWhenOneThrows() => AssertGenericFanOutSurvivesThrowingHandler( NewCurrentClient(), "FormatError", new FormatReadResult()); - /// Pins the behaviour expressed by the test name: internal error handler throwing does not break current client format error fan out. + /// Pins the behavior expressed by the test name: internal error handler throwing does not break current client format error fan out. [Test] public void MTConnectHttpCurrentClient_InternalErrorHandlerThrowingDoesNotBreakFormatErrorFanOut() => AssertGenericFanOutSurvivesThrowingInternalError( NewCurrentClient(), "FormatError", new FormatReadResult()); - /// Pins the behaviour expressed by the test name: current client connection error fires for all subscribers when one throws. + /// Pins the behavior expressed by the test name: current client connection error fires for all subscribers when one throws. [Test] public void MTConnectHttpCurrentClient_ConnectionErrorFiresForAllSubscribersWhenOneThrows() => AssertGenericFanOutSurvivesThrowingHandler( NewCurrentClient(), "ConnectionError", new Exception("test")); - /// Pins the behaviour expressed by the test name: internal error handler throwing does not break current client connection error fan out. + /// Pins the behavior expressed by the test name: internal error handler throwing does not break current client connection error fan out. [Test] public void MTConnectHttpCurrentClient_InternalErrorHandlerThrowingDoesNotBreakConnectionErrorFanOut() => AssertGenericFanOutSurvivesThrowingInternalError( NewCurrentClient(), "ConnectionError", new Exception("test")); - /// Pins the behaviour expressed by the test name: current client internal error fires for all subscribers when one throws. + /// Pins the behavior expressed by the test name: current client internal error fires for all subscribers when one throws. [Test] public void MTConnectHttpCurrentClient_InternalErrorFiresForAllSubscribersWhenOneThrows() => AssertGenericFanOutSurvivesThrowingHandler( @@ -327,43 +327,43 @@ public void MTConnectHttpCurrentClient_InternalErrorFiresForAllSubscribersWhenOn private static MTConnectHttpSampleClient NewSampleClient() => new MTConnectHttpSampleClient("127.0.0.1", 1); - /// Pins the behaviour expressed by the test name: sample client mt connect error fires for all subscribers when one throws. + /// Pins the behavior expressed by the test name: sample client mt connect error fires for all subscribers when one throws. [Test] public void MTConnectHttpSampleClient_MTConnectErrorFiresForAllSubscribersWhenOneThrows() => AssertGenericFanOutSurvivesThrowingHandler( NewSampleClient(), "MTConnectError", new ErrorResponseDocument()); - /// Pins the behaviour expressed by the test name: internal error handler throwing does not break sample client mt connect error fan out. + /// Pins the behavior expressed by the test name: internal error handler throwing does not break sample client mt connect error fan out. [Test] public void MTConnectHttpSampleClient_InternalErrorHandlerThrowingDoesNotBreakMTConnectErrorFanOut() => AssertGenericFanOutSurvivesThrowingInternalError( NewSampleClient(), "MTConnectError", new ErrorResponseDocument()); - /// Pins the behaviour expressed by the test name: sample client format error fires for all subscribers when one throws. + /// Pins the behavior expressed by the test name: sample client format error fires for all subscribers when one throws. [Test] public void MTConnectHttpSampleClient_FormatErrorFiresForAllSubscribersWhenOneThrows() => AssertGenericFanOutSurvivesThrowingHandler( NewSampleClient(), "FormatError", new FormatReadResult()); - /// Pins the behaviour expressed by the test name: internal error handler throwing does not break sample client format error fan out. + /// Pins the behavior expressed by the test name: internal error handler throwing does not break sample client format error fan out. [Test] public void MTConnectHttpSampleClient_InternalErrorHandlerThrowingDoesNotBreakFormatErrorFanOut() => AssertGenericFanOutSurvivesThrowingInternalError( NewSampleClient(), "FormatError", new FormatReadResult()); - /// Pins the behaviour expressed by the test name: sample client connection error fires for all subscribers when one throws. + /// Pins the behavior expressed by the test name: sample client connection error fires for all subscribers when one throws. [Test] public void MTConnectHttpSampleClient_ConnectionErrorFiresForAllSubscribersWhenOneThrows() => AssertGenericFanOutSurvivesThrowingHandler( NewSampleClient(), "ConnectionError", new Exception("test")); - /// Pins the behaviour expressed by the test name: internal error handler throwing does not break sample client connection error fan out. + /// Pins the behavior expressed by the test name: internal error handler throwing does not break sample client connection error fan out. [Test] public void MTConnectHttpSampleClient_InternalErrorHandlerThrowingDoesNotBreakConnectionErrorFanOut() => AssertGenericFanOutSurvivesThrowingInternalError( NewSampleClient(), "ConnectionError", new Exception("test")); - /// Pins the behaviour expressed by the test name: sample client internal error fires for all subscribers when one throws. + /// Pins the behavior expressed by the test name: sample client internal error fires for all subscribers when one throws. [Test] public void MTConnectHttpSampleClient_InternalErrorFiresForAllSubscribersWhenOneThrows() => AssertGenericFanOutSurvivesThrowingHandler( @@ -377,43 +377,43 @@ public void MTConnectHttpSampleClient_InternalErrorFiresForAllSubscribersWhenOne private static MTConnectHttpAssetClient NewAssetClient() => new MTConnectHttpAssetClient("127.0.0.1", 1); - /// Pins the behaviour expressed by the test name: asset client mt connect error fires for all subscribers when one throws. + /// Pins the behavior expressed by the test name: asset client mt connect error fires for all subscribers when one throws. [Test] public void MTConnectHttpAssetClient_MTConnectErrorFiresForAllSubscribersWhenOneThrows() => AssertGenericFanOutSurvivesThrowingHandler( NewAssetClient(), "MTConnectError", new ErrorResponseDocument()); - /// Pins the behaviour expressed by the test name: internal error handler throwing does not break asset client mt connect error fan out. + /// Pins the behavior expressed by the test name: internal error handler throwing does not break asset client mt connect error fan out. [Test] public void MTConnectHttpAssetClient_InternalErrorHandlerThrowingDoesNotBreakMTConnectErrorFanOut() => AssertGenericFanOutSurvivesThrowingInternalError( NewAssetClient(), "MTConnectError", new ErrorResponseDocument()); - /// Pins the behaviour expressed by the test name: asset client format error fires for all subscribers when one throws. + /// Pins the behavior expressed by the test name: asset client format error fires for all subscribers when one throws. [Test] public void MTConnectHttpAssetClient_FormatErrorFiresForAllSubscribersWhenOneThrows() => AssertGenericFanOutSurvivesThrowingHandler( NewAssetClient(), "FormatError", new FormatReadResult()); - /// Pins the behaviour expressed by the test name: internal error handler throwing does not break asset client format error fan out. + /// Pins the behavior expressed by the test name: internal error handler throwing does not break asset client format error fan out. [Test] public void MTConnectHttpAssetClient_InternalErrorHandlerThrowingDoesNotBreakFormatErrorFanOut() => AssertGenericFanOutSurvivesThrowingInternalError( NewAssetClient(), "FormatError", new FormatReadResult()); - /// Pins the behaviour expressed by the test name: asset client connection error fires for all subscribers when one throws. + /// Pins the behavior expressed by the test name: asset client connection error fires for all subscribers when one throws. [Test] public void MTConnectHttpAssetClient_ConnectionErrorFiresForAllSubscribersWhenOneThrows() => AssertGenericFanOutSurvivesThrowingHandler( NewAssetClient(), "ConnectionError", new Exception("test")); - /// Pins the behaviour expressed by the test name: internal error handler throwing does not break asset client connection error fan out. + /// Pins the behavior expressed by the test name: internal error handler throwing does not break asset client connection error fan out. [Test] public void MTConnectHttpAssetClient_InternalErrorHandlerThrowingDoesNotBreakConnectionErrorFanOut() => AssertGenericFanOutSurvivesThrowingInternalError( NewAssetClient(), "ConnectionError", new Exception("test")); - /// Pins the behaviour expressed by the test name: asset client internal error fires for all subscribers when one throws. + /// Pins the behavior expressed by the test name: asset client internal error fires for all subscribers when one throws. [Test] public void MTConnectHttpAssetClient_InternalErrorFiresForAllSubscribersWhenOneThrows() => AssertGenericFanOutSurvivesThrowingHandler( @@ -429,96 +429,96 @@ public void MTConnectHttpAssetClient_InternalErrorFiresForAllSubscribersWhenOneT private static MTConnectHttpClientStream NewStream() => new MTConnectHttpClientStream("http://127.0.0.1:1/sample"); - /// Pins the behaviour expressed by the test name: client stream document received fires for all subscribers when one throws. + /// Pins the behavior expressed by the test name: client stream document received fires for all subscribers when one throws. [Test] public void MTConnectHttpClientStream_DocumentReceivedFiresForAllSubscribersWhenOneThrows() => AssertGenericFanOutSurvivesThrowingHandler( NewStream(), "DocumentReceived", new MTConnect.Streams.StreamsResponseDocument()); - /// Pins the behaviour expressed by the test name: internal error handler throwing does not break client stream document received fan out. + /// Pins the behavior expressed by the test name: internal error handler throwing does not break client stream document received fan out. [Test] public void MTConnectHttpClientStream_InternalErrorHandlerThrowingDoesNotBreakDocumentReceivedFanOut() => AssertGenericFanOutSurvivesThrowingInternalError( NewStream(), "DocumentReceived", new MTConnect.Streams.StreamsResponseDocument()); - /// Pins the behaviour expressed by the test name: client stream error received fires for all subscribers when one throws. + /// Pins the behavior expressed by the test name: client stream error received fires for all subscribers when one throws. [Test] public void MTConnectHttpClientStream_ErrorReceivedFiresForAllSubscribersWhenOneThrows() => AssertGenericFanOutSurvivesThrowingHandler( NewStream(), "ErrorReceived", new ErrorResponseDocument()); - /// Pins the behaviour expressed by the test name: internal error handler throwing does not break client stream error received fan out. + /// Pins the behavior expressed by the test name: internal error handler throwing does not break client stream error received fan out. [Test] public void MTConnectHttpClientStream_InternalErrorHandlerThrowingDoesNotBreakErrorReceivedFanOut() => AssertGenericFanOutSurvivesThrowingInternalError( NewStream(), "ErrorReceived", new ErrorResponseDocument()); - /// Pins the behaviour expressed by the test name: client stream format error fires for all subscribers when one throws. + /// Pins the behavior expressed by the test name: client stream format error fires for all subscribers when one throws. [Test] public void MTConnectHttpClientStream_FormatErrorFiresForAllSubscribersWhenOneThrows() => AssertGenericFanOutSurvivesThrowingHandler( NewStream(), "FormatError", new FormatReadResult()); - /// Pins the behaviour expressed by the test name: internal error handler throwing does not break client stream format error fan out. + /// Pins the behavior expressed by the test name: internal error handler throwing does not break client stream format error fan out. [Test] public void MTConnectHttpClientStream_InternalErrorHandlerThrowingDoesNotBreakFormatErrorFanOut() => AssertGenericFanOutSurvivesThrowingInternalError( NewStream(), "FormatError", new FormatReadResult()); - /// Pins the behaviour expressed by the test name: client stream connection error fires for all subscribers when one throws. + /// Pins the behavior expressed by the test name: client stream connection error fires for all subscribers when one throws. [Test] public void MTConnectHttpClientStream_ConnectionErrorFiresForAllSubscribersWhenOneThrows() => AssertGenericFanOutSurvivesThrowingHandler( NewStream(), "ConnectionError", new Exception("test")); - /// Pins the behaviour expressed by the test name: internal error handler throwing does not break client stream connection error fan out. + /// Pins the behavior expressed by the test name: internal error handler throwing does not break client stream connection error fan out. [Test] public void MTConnectHttpClientStream_InternalErrorHandlerThrowingDoesNotBreakConnectionErrorFanOut() => AssertGenericFanOutSurvivesThrowingInternalError( NewStream(), "ConnectionError", new Exception("test")); - /// Pins the behaviour expressed by the test name: client stream internal error fires for all subscribers when one throws. + /// Pins the behavior expressed by the test name: client stream internal error fires for all subscribers when one throws. [Test] public void MTConnectHttpClientStream_InternalErrorFiresForAllSubscribersWhenOneThrows() => AssertGenericFanOutSurvivesThrowingHandler( NewStream(), "InternalError", new Exception("test")); - /// Pins the behaviour expressed by the test name: client stream starting fires for all subscribers when one throws. + /// Pins the behavior expressed by the test name: client stream starting fires for all subscribers when one throws. [Test] public void MTConnectHttpClientStream_StartingFiresForAllSubscribersWhenOneThrows() => AssertNonGenericFanOutSurvivesThrowingHandler(NewStream(), "Starting"); - /// Pins the behaviour expressed by the test name: internal error handler throwing does not break client stream starting fan out. + /// Pins the behavior expressed by the test name: internal error handler throwing does not break client stream starting fan out. [Test] public void MTConnectHttpClientStream_InternalErrorHandlerThrowingDoesNotBreakStartingFanOut() => AssertNonGenericFanOutSurvivesThrowingInternalError(NewStream(), "Starting"); - /// Pins the behaviour expressed by the test name: client stream started fires for all subscribers when one throws. + /// Pins the behavior expressed by the test name: client stream started fires for all subscribers when one throws. [Test] public void MTConnectHttpClientStream_StartedFiresForAllSubscribersWhenOneThrows() => AssertNonGenericFanOutSurvivesThrowingHandler(NewStream(), "Started"); - /// Pins the behaviour expressed by the test name: internal error handler throwing does not break client stream started fan out. + /// Pins the behavior expressed by the test name: internal error handler throwing does not break client stream started fan out. [Test] public void MTConnectHttpClientStream_InternalErrorHandlerThrowingDoesNotBreakStartedFanOut() => AssertNonGenericFanOutSurvivesThrowingInternalError(NewStream(), "Started"); - /// Pins the behaviour expressed by the test name: client stream stopping fires for all subscribers when one throws. + /// Pins the behavior expressed by the test name: client stream stopping fires for all subscribers when one throws. [Test] public void MTConnectHttpClientStream_StoppingFiresForAllSubscribersWhenOneThrows() => AssertNonGenericFanOutSurvivesThrowingHandler(NewStream(), "Stopping"); - /// Pins the behaviour expressed by the test name: internal error handler throwing does not break client stream stopping fan out. + /// Pins the behavior expressed by the test name: internal error handler throwing does not break client stream stopping fan out. [Test] public void MTConnectHttpClientStream_InternalErrorHandlerThrowingDoesNotBreakStoppingFanOut() => AssertNonGenericFanOutSurvivesThrowingInternalError(NewStream(), "Stopping"); - /// Pins the behaviour expressed by the test name: client stream stopped fires for all subscribers when one throws. + /// Pins the behavior expressed by the test name: client stream stopped fires for all subscribers when one throws. [Test] public void MTConnectHttpClientStream_StoppedFiresForAllSubscribersWhenOneThrows() => AssertNonGenericFanOutSurvivesThrowingHandler(NewStream(), "Stopped"); - /// Pins the behaviour expressed by the test name: internal error handler throwing does not break client stream stopped fan out. + /// Pins the behavior expressed by the test name: internal error handler throwing does not break client stream stopped fan out. [Test] public void MTConnectHttpClientStream_InternalErrorHandlerThrowingDoesNotBreakStoppedFanOut() => AssertNonGenericFanOutSurvivesThrowingInternalError(NewStream(), "Stopped"); diff --git a/tests/MTConnect.NET-HTTP-Tests/Integration/DevicesAndDeviceReceivedIntegrationTests.cs b/tests/MTConnect.NET-HTTP-Tests/Integration/DevicesAndDeviceReceivedIntegrationTests.cs index 7205b481..a6c99235 100644 --- a/tests/MTConnect.NET-HTTP-Tests/Integration/DevicesAndDeviceReceivedIntegrationTests.cs +++ b/tests/MTConnect.NET-HTTP-Tests/Integration/DevicesAndDeviceReceivedIntegrationTests.cs @@ -28,7 +28,7 @@ public class DevicesAndDeviceReceivedIntegrationTests private const int ProbeWaitTimeoutMs = 30000; - /// Pins the behaviour expressed by the test name: end to end probe populates Devices and fires DeviceReceived. + /// Pins the behavior expressed by the test name: end to end probe populates Devices and fires DeviceReceived. [Test] public void EndToEnd_Probe_Populates_Devices_And_Fires_DeviceReceived() { @@ -77,7 +77,7 @@ public void EndToEnd_Probe_Populates_Devices_And_Fires_DeviceReceived() } } - /// Pins the behaviour expressed by the test name: end to end subsequent probe repopulates cache and refires DeviceReceived. + /// Pins the behavior expressed by the test name: end to end subsequent probe repopulates cache and refires DeviceReceived. [Test] public void EndToEnd_Subsequent_Probe_Repopulates_Cache_And_Refires_DeviceReceived() { @@ -134,7 +134,7 @@ public void EndToEnd_Subsequent_Probe_Repopulates_Cache_And_Refires_DeviceReceiv } } - /// Pins the behaviour expressed by the test name: end to end empty probe does not evict cache. + /// Pins the behavior expressed by the test name: end to end empty probe does not evict cache. [Test] public void EndToEnd_Empty_Probe_Does_Not_Evict_Cache() { From 981dd4c61c8eaf5eb6f34afe43ff6cf0851ebc7a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Otto=20Boly=C3=B3s?= Date: Wed, 3 Jun 2026 11:41:04 +0200 Subject: [PATCH 09/37] test(common-tests): pin shared multicast isolation contract --- .../MulticastIsolationTests.cs | 144 ++++++++++++++++++ 1 file changed, 144 insertions(+) create mode 100644 tests/MTConnect.NET-Common-Tests/MulticastIsolationTests.cs diff --git a/tests/MTConnect.NET-Common-Tests/MulticastIsolationTests.cs b/tests/MTConnect.NET-Common-Tests/MulticastIsolationTests.cs new file mode 100644 index 00000000..dcbe1a1b --- /dev/null +++ b/tests/MTConnect.NET-Common-Tests/MulticastIsolationTests.cs @@ -0,0 +1,144 @@ +// Copyright (c) 2026 TrakHound Inc., All Rights Reserved. +// TrakHound Inc. licenses this file to you under the MIT license. + +using System; +using NUnit.Framework; + +namespace MTConnect.Tests.Common +{ + /// + /// Pins the contract of : per-delegate + /// fault isolation on both the outer event and the + /// internalError sink, with a terminal swallow when the + /// fault-reporter itself throws. + /// + [TestFixture] + public class MulticastIsolationTests + { + // --- Generic overload -------------------------------------------------- + + /// Pins the behavior expressed by the test name: every subscriber on the generic event fires even when an earlier one throws. + [Test] + public void MulticastIsolation_Generic_FiresAllSubscribersWhenOneThrows() + { + var fired = new System.Collections.Generic.List(); + EventHandler handler = null; + handler += (s, e) => fired.Add(1); + handler += (s, e) => throw new InvalidOperationException("subscriber-2"); + handler += (s, e) => fired.Add(3); + + EventHandler internalError = (s, ex) => { }; + + MulticastIsolation.Raise(handler, this, 42, internalError); + + Assert.That(fired, Is.EqualTo(new[] { 1, 3 })); + } + + /// Pins the behavior expressed by the test name: a fault thrown by a subscriber is routed through internalError. + [Test] + public void MulticastIsolation_Generic_RoutesFaultToInternalError() + { + var routed = new System.Collections.Generic.List(); + EventHandler handler = (s, e) => throw new InvalidOperationException("boom"); + EventHandler internalError = (s, ex) => routed.Add(ex); + + MulticastIsolation.Raise(handler, this, 0, internalError); + + Assert.That(routed.Count, Is.EqualTo(1)); + Assert.That(routed[0], Is.InstanceOf()); + Assert.That(routed[0].Message, Is.EqualTo("boom")); + } + + /// Pins the behavior expressed by the test name: the internalError multicast is iterated per subscriber, so a throwing internalError handler does not starve later internalError handlers. + [Test] + public void MulticastIsolation_Generic_InternalErrorIteratesPerSubscriber() + { + var seen = new System.Collections.Generic.List(); + EventHandler handler = (s, e) => throw new InvalidOperationException("origin"); + + EventHandler internalError = null; + internalError += (s, ex) => throw new InvalidOperationException("internal-1"); + internalError += (s, ex) => seen.Add(2); + internalError += (s, ex) => seen.Add(3); + + MulticastIsolation.Raise(handler, this, 0, internalError); + + Assert.That(seen, Is.EqualTo(new[] { 2, 3 })); + } + + /// Pins the behavior expressed by the test name: when every internalError subscriber throws, the helper still returns without escaping an exception. + [Test] + public void MulticastIsolation_Generic_TerminalSwallowsInternalErrorOwnThrow() + { + EventHandler handler = (s, e) => throw new InvalidOperationException("origin"); + EventHandler internalError = null; + internalError += (s, ex) => throw new InvalidOperationException("internal-1"); + internalError += (s, ex) => throw new InvalidOperationException("internal-2"); + + Assert.DoesNotThrow(() => MulticastIsolation.Raise(handler, this, 0, internalError)); + } + + // --- Non-generic overload --------------------------------------------- + + /// Pins the behavior expressed by the test name: every subscriber on the non-generic event fires even when an earlier one throws. + [Test] + public void MulticastIsolation_NonGeneric_FiresAllSubscribersWhenOneThrows() + { + var fired = new System.Collections.Generic.List(); + EventHandler handler = null; + handler += (s, e) => fired.Add(1); + handler += (s, e) => throw new InvalidOperationException("subscriber-2"); + handler += (s, e) => fired.Add(3); + + EventHandler internalError = (s, ex) => { }; + + MulticastIsolation.Raise(handler, this, EventArgs.Empty, internalError); + + Assert.That(fired, Is.EqualTo(new[] { 1, 3 })); + } + + /// Pins the behavior expressed by the test name: a fault thrown by a non-generic subscriber is routed through internalError. + [Test] + public void MulticastIsolation_NonGeneric_RoutesFaultToInternalError() + { + var routed = new System.Collections.Generic.List(); + EventHandler handler = (s, e) => throw new InvalidOperationException("boom"); + EventHandler internalError = (s, ex) => routed.Add(ex); + + MulticastIsolation.Raise(handler, this, EventArgs.Empty, internalError); + + Assert.That(routed.Count, Is.EqualTo(1)); + Assert.That(routed[0], Is.InstanceOf()); + Assert.That(routed[0].Message, Is.EqualTo("boom")); + } + + /// Pins the behavior expressed by the test name: under the non-generic overload, a throwing internalError handler does not starve later internalError handlers. + [Test] + public void MulticastIsolation_NonGeneric_InternalErrorIteratesPerSubscriber() + { + var seen = new System.Collections.Generic.List(); + EventHandler handler = (s, e) => throw new InvalidOperationException("origin"); + + EventHandler internalError = null; + internalError += (s, ex) => throw new InvalidOperationException("internal-1"); + internalError += (s, ex) => seen.Add(2); + internalError += (s, ex) => seen.Add(3); + + MulticastIsolation.Raise(handler, this, EventArgs.Empty, internalError); + + Assert.That(seen, Is.EqualTo(new[] { 2, 3 })); + } + + /// Pins the behavior expressed by the test name: under the non-generic overload, when every internalError subscriber throws, the helper still returns without escaping an exception. + [Test] + public void MulticastIsolation_NonGeneric_TerminalSwallowsInternalErrorOwnThrow() + { + EventHandler handler = (s, e) => throw new InvalidOperationException("origin"); + EventHandler internalError = null; + internalError += (s, ex) => throw new InvalidOperationException("internal-1"); + internalError += (s, ex) => throw new InvalidOperationException("internal-2"); + + Assert.DoesNotThrow(() => MulticastIsolation.Raise(handler, this, EventArgs.Empty, internalError)); + } + } +} From c06104442994f0548a05eab3aa554d6145b447a6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Otto=20Boly=C3=B3s?= Date: Wed, 3 Jun 2026 11:42:58 +0200 Subject: [PATCH 10/37] feat(common): add shared multicast isolation helper for events --- .../MulticastIsolation.cs | 95 +++++++++++++++++++ .../MulticastIsolationTests.cs | 24 ++--- 2 files changed, 107 insertions(+), 12 deletions(-) create mode 100644 libraries/MTConnect.NET-Common/MulticastIsolation.cs diff --git a/libraries/MTConnect.NET-Common/MulticastIsolation.cs b/libraries/MTConnect.NET-Common/MulticastIsolation.cs new file mode 100644 index 00000000..188d0e0b --- /dev/null +++ b/libraries/MTConnect.NET-Common/MulticastIsolation.cs @@ -0,0 +1,95 @@ +// Copyright (c) 2026 TrakHound Inc., All Rights Reserved. +// TrakHound Inc. licenses this file to you under the MIT license. + +using System; + +namespace MTConnect +{ + /// + /// Shared event multicast helper that iterates the invocation list with + /// per-delegate fault isolation. A throwing subscriber cannot starve later + /// subscribers of the same event; subscriber faults are routed through the + /// caller-supplied internalError sink, which is itself iterated + /// per-delegate so a throwing fault reporter cannot starve later fault + /// reporters either. If the internalError handler itself throws, the + /// secondary fault is terminal and swallowed — there is no further sink to + /// route it to without risking the same starvation loop. + /// + public static class MulticastIsolation + { + /// + /// Raises a generic event with per-delegate fault isolation. If any + /// subscriber throws, the fault is routed through + /// without interrupting fan-out to + /// subsequent subscribers. The handler + /// itself is iterated with the same per-delegate try/catch so a + /// throwing fault-reporter cannot starve later fault subscribers + /// either. + /// + public static void Raise(EventHandler handler, object sender, T arg, + EventHandler internalError) + { + if (handler == null) return; + + foreach (var subscriber in handler.GetInvocationList()) + { + try + { + ((EventHandler)subscriber).Invoke(sender, arg); + } + catch (Exception ex) + { + RaiseInternalError(internalError, sender, ex); + } + } + } + + /// + /// Non-generic overload of for + /// events that carry no typed payload. + /// Same contract: a throwing subscriber cannot starve later subscribers + /// and a faulting handler cannot break + /// the fan-out either. + /// + public static void Raise(EventHandler handler, object sender, EventArgs arg, + EventHandler internalError) + { + if (handler == null) return; + + foreach (var subscriber in handler.GetInvocationList()) + { + try + { + ((EventHandler)subscriber).Invoke(sender, arg); + } + catch (Exception ex) + { + RaiseInternalError(internalError, sender, ex); + } + } + } + + // Iterate the InternalError invocation list so a throwing fault + // reporter cannot starve later fault reporters. A secondary throw from + // an InternalError subscriber itself is terminal: there is no further + // sink to route it to without resurrecting the same starvation bug. + private static void RaiseInternalError(EventHandler internalError, + object sender, Exception ex) + { + if (internalError == null) return; + + foreach (var subscriber in internalError.GetInvocationList()) + { + try + { + ((EventHandler)subscriber).Invoke(sender, ex); + } + catch + { + // Terminal: cannot route a fault about the fault without + // risking the starvation loop this helper exists to close. + } + } + } + } +} diff --git a/tests/MTConnect.NET-Common-Tests/MulticastIsolationTests.cs b/tests/MTConnect.NET-Common-Tests/MulticastIsolationTests.cs index dcbe1a1b..83451499 100644 --- a/tests/MTConnect.NET-Common-Tests/MulticastIsolationTests.cs +++ b/tests/MTConnect.NET-Common-Tests/MulticastIsolationTests.cs @@ -22,14 +22,14 @@ public class MulticastIsolationTests public void MulticastIsolation_Generic_FiresAllSubscribersWhenOneThrows() { var fired = new System.Collections.Generic.List(); - EventHandler handler = null; + EventHandler? handler = null; handler += (s, e) => fired.Add(1); handler += (s, e) => throw new InvalidOperationException("subscriber-2"); handler += (s, e) => fired.Add(3); EventHandler internalError = (s, ex) => { }; - MulticastIsolation.Raise(handler, this, 42, internalError); + MulticastIsolation.Raise(handler!, this, 42, internalError); Assert.That(fired, Is.EqualTo(new[] { 1, 3 })); } @@ -56,12 +56,12 @@ public void MulticastIsolation_Generic_InternalErrorIteratesPerSubscriber() var seen = new System.Collections.Generic.List(); EventHandler handler = (s, e) => throw new InvalidOperationException("origin"); - EventHandler internalError = null; + EventHandler? internalError = null; internalError += (s, ex) => throw new InvalidOperationException("internal-1"); internalError += (s, ex) => seen.Add(2); internalError += (s, ex) => seen.Add(3); - MulticastIsolation.Raise(handler, this, 0, internalError); + MulticastIsolation.Raise(handler, this, 0, internalError!); Assert.That(seen, Is.EqualTo(new[] { 2, 3 })); } @@ -71,11 +71,11 @@ public void MulticastIsolation_Generic_InternalErrorIteratesPerSubscriber() public void MulticastIsolation_Generic_TerminalSwallowsInternalErrorOwnThrow() { EventHandler handler = (s, e) => throw new InvalidOperationException("origin"); - EventHandler internalError = null; + EventHandler? internalError = null; internalError += (s, ex) => throw new InvalidOperationException("internal-1"); internalError += (s, ex) => throw new InvalidOperationException("internal-2"); - Assert.DoesNotThrow(() => MulticastIsolation.Raise(handler, this, 0, internalError)); + Assert.DoesNotThrow(() => MulticastIsolation.Raise(handler, this, 0, internalError!)); } // --- Non-generic overload --------------------------------------------- @@ -85,14 +85,14 @@ public void MulticastIsolation_Generic_TerminalSwallowsInternalErrorOwnThrow() public void MulticastIsolation_NonGeneric_FiresAllSubscribersWhenOneThrows() { var fired = new System.Collections.Generic.List(); - EventHandler handler = null; + EventHandler? handler = null; handler += (s, e) => fired.Add(1); handler += (s, e) => throw new InvalidOperationException("subscriber-2"); handler += (s, e) => fired.Add(3); EventHandler internalError = (s, ex) => { }; - MulticastIsolation.Raise(handler, this, EventArgs.Empty, internalError); + MulticastIsolation.Raise(handler!, this, EventArgs.Empty, internalError); Assert.That(fired, Is.EqualTo(new[] { 1, 3 })); } @@ -119,12 +119,12 @@ public void MulticastIsolation_NonGeneric_InternalErrorIteratesPerSubscriber() var seen = new System.Collections.Generic.List(); EventHandler handler = (s, e) => throw new InvalidOperationException("origin"); - EventHandler internalError = null; + EventHandler? internalError = null; internalError += (s, ex) => throw new InvalidOperationException("internal-1"); internalError += (s, ex) => seen.Add(2); internalError += (s, ex) => seen.Add(3); - MulticastIsolation.Raise(handler, this, EventArgs.Empty, internalError); + MulticastIsolation.Raise(handler, this, EventArgs.Empty, internalError!); Assert.That(seen, Is.EqualTo(new[] { 2, 3 })); } @@ -134,11 +134,11 @@ public void MulticastIsolation_NonGeneric_InternalErrorIteratesPerSubscriber() public void MulticastIsolation_NonGeneric_TerminalSwallowsInternalErrorOwnThrow() { EventHandler handler = (s, e) => throw new InvalidOperationException("origin"); - EventHandler internalError = null; + EventHandler? internalError = null; internalError += (s, ex) => throw new InvalidOperationException("internal-1"); internalError += (s, ex) => throw new InvalidOperationException("internal-2"); - Assert.DoesNotThrow(() => MulticastIsolation.Raise(handler, this, EventArgs.Empty, internalError)); + Assert.DoesNotThrow(() => MulticastIsolation.Raise(handler, this, EventArgs.Empty, internalError!)); } } } From 966212cec1a632b05bc16805db6a181e609abd4f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Otto=20Boly=C3=B3s?= Date: Wed, 3 Jun 2026 11:47:55 +0200 Subject: [PATCH 11/37] refactor(http): migrate HTTP clients to shared multicast helper --- .../Clients/MTConnectHttpAssetClient.cs | 58 +---- .../Clients/MTConnectHttpClient.cs | 209 +++++++----------- .../Clients/MTConnectHttpClientStream.cs | 82 +------ .../Clients/MTConnectHttpCurrentClient.cs | 58 +---- .../Clients/MTConnectHttpProbeClient.cs | 58 +---- .../Clients/MTConnectHttpSampleClient.cs | 58 +---- 6 files changed, 131 insertions(+), 392 deletions(-) diff --git a/libraries/MTConnect.NET-HTTP/Clients/MTConnectHttpAssetClient.cs b/libraries/MTConnect.NET-HTTP/Clients/MTConnectHttpAssetClient.cs index a2e331b0..41bf9aa5 100644 --- a/libraries/MTConnect.NET-HTTP/Clients/MTConnectHttpAssetClient.cs +++ b/libraries/MTConnect.NET-HTTP/Clients/MTConnectHttpAssetClient.cs @@ -192,16 +192,16 @@ public IAssetsResponseDocument Get() } catch (TaskCanceledException ex) when (ex.InnerException is TimeoutException) { - RaiseEvent(ConnectionError, ex); + MulticastIsolation.Raise(ConnectionError, this, ex, InternalError); } catch (TaskCanceledException) { /* Ignore Task Cancelled */ } catch (HttpRequestException ex) { - RaiseEvent(ConnectionError, ex); + MulticastIsolation.Raise(ConnectionError, this, ex, InternalError); } catch (Exception ex) { - RaiseEvent(InternalError, ex); + MulticastIsolation.Raise(InternalError, this, ex, InternalError); } return null; @@ -246,16 +246,16 @@ public async Task GetAsync(CancellationToken cancellati } catch (TaskCanceledException ex) when (ex.InnerException is TimeoutException) { - RaiseEvent(ConnectionError, ex); + MulticastIsolation.Raise(ConnectionError, this, ex, InternalError); } catch (TaskCanceledException) { /* Ignore Task Cancelled */ } catch (HttpRequestException ex) { - RaiseEvent(ConnectionError, ex); + MulticastIsolation.Raise(ConnectionError, this, ex, InternalError); } catch (Exception ex) { - RaiseEvent(InternalError, ex); + MulticastIsolation.Raise(InternalError, this, ex, InternalError); } return null; @@ -346,7 +346,7 @@ private IAssetsResponseDocument HandleResponse(HttpResponseMessage response) { if (!response.IsSuccessStatusCode) { - RaiseEvent(ConnectionError, new Exception(response.ReasonPhrase)); + MulticastIsolation.Raise(ConnectionError, this, new Exception(response.ReasonPhrase), InternalError); } else if (response.Content != null) { @@ -364,7 +364,7 @@ private async Task HandleResponseAsync(HttpResponseMess { if (!response.IsSuccessStatusCode) { - RaiseEvent(ConnectionError, new Exception(response.ReasonPhrase)); + MulticastIsolation.Raise(ConnectionError, this, new Exception(response.ReasonPhrase), InternalError); } else if (response.Content != null) { @@ -407,59 +407,23 @@ private IAssetsResponseDocument ReadDocument(HttpResponseMessage response, Strea if (errorFormatResult.Success) { var errorDocument = errorFormatResult.Content; - if (errorDocument != null) RaiseEvent(MTConnectError, errorDocument); + if (errorDocument != null) MulticastIsolation.Raise(MTConnectError, this, errorDocument, InternalError); } else { // Raise Format Error - RaiseEvent(FormatError, errorFormatResult); + MulticastIsolation.Raise(FormatError, this, errorFormatResult, InternalError); } } } else { // Raise Format Error - RaiseEvent(FormatError, formatResult); + MulticastIsolation.Raise(FormatError, this, formatResult, InternalError); } } return null; } - - // Iterate the invocation list so one throwing subscriber cannot short-circuit - // the multicast and starve later subscribers. Each fault is forwarded through - // InternalError; if InternalError itself faults, swallow that secondary fault - // so the remaining subscribers in the invocation list still receive the event. - private void RaiseEvent(EventHandler handler, T arg) - { - if (handler == null) return; - - foreach (var subscriber in handler.GetInvocationList()) - { - try - { - ((EventHandler)subscriber).Invoke(this, arg); - } - catch (Exception ex) - { - RouteSubscriberFault(ex); - } - } - } - - // Forwards a subscriber fault to InternalError and swallows any secondary - // fault raised by InternalError itself so the originating event's fan-out - // can keep running. - private void RouteSubscriberFault(Exception ex) - { - try - { - InternalError?.Invoke(this, ex); - } - catch - { - // A faulting InternalError handler must not break the event fan-out. - } - } } } \ No newline at end of file diff --git a/libraries/MTConnect.NET-HTTP/Clients/MTConnectHttpClient.cs b/libraries/MTConnect.NET-HTTP/Clients/MTConnectHttpClient.cs index bd0eb1fc..303c9d42 100644 --- a/libraries/MTConnect.NET-HTTP/Clients/MTConnectHttpClient.cs +++ b/libraries/MTConnect.NET-HTTP/Clients/MTConnectHttpClient.cs @@ -347,7 +347,7 @@ public void Start() { _stop = new CancellationTokenSource(); - RaiseEvent(ClientStarting, EventArgs.Empty); + MulticastIsolation.Raise(ClientStarting, this, EventArgs.Empty, InternalError); _initializeFromBuffer = false; _lastInstanceId = 0; @@ -364,7 +364,7 @@ public void Start(string path) { _stop = new CancellationTokenSource(); - RaiseEvent(ClientStarting, EventArgs.Empty); + MulticastIsolation.Raise(ClientStarting, this, EventArgs.Empty, InternalError); _initializeFromBuffer = false; _lastInstanceId = 0; @@ -382,7 +382,7 @@ public void Start(CancellationToken cancellationToken, string path = null) _stop = new CancellationTokenSource(); cancellationToken.Register(() => { Stop(); }); - RaiseEvent(ClientStarting, EventArgs.Empty); + MulticastIsolation.Raise(ClientStarting, this, EventArgs.Empty, InternalError); _initializeFromBuffer = false; _lastInstanceId = 0; @@ -399,7 +399,7 @@ public void StartFromSequence(ulong instanceId, ulong sequence, string path = nu { _stop = new CancellationTokenSource(); - RaiseEvent(ClientStarting, EventArgs.Empty); + MulticastIsolation.Raise(ClientStarting, this, EventArgs.Empty, InternalError); _initializeFromBuffer = true; _lastInstanceId = instanceId; @@ -417,7 +417,7 @@ public void StartFromSequence(ulong instanceId, ulong sequence, CancellationToke _stop = new CancellationTokenSource(); cancellationToken.Register(() => { Stop(); }); - RaiseEvent(ClientStarting, EventArgs.Empty); + MulticastIsolation.Raise(ClientStarting, this, EventArgs.Empty, InternalError); _initializeFromBuffer = true; _lastInstanceId = instanceId; @@ -434,7 +434,7 @@ public void StartFromBuffer(string path = null) { _stop = new CancellationTokenSource(); - RaiseEvent(ClientStarting, EventArgs.Empty); + MulticastIsolation.Raise(ClientStarting, this, EventArgs.Empty, InternalError); _initializeFromBuffer = true; _lastInstanceId = 0; @@ -452,7 +452,7 @@ public void StartFromBuffer(CancellationToken cancellationToken, string path = n _stop = new CancellationTokenSource(); cancellationToken.Register(() => { Stop(); }); - RaiseEvent(ClientStarting, EventArgs.Empty); + MulticastIsolation.Raise(ClientStarting, this, EventArgs.Empty, InternalError); _initializeFromBuffer = true; _lastInstanceId = 0; @@ -467,7 +467,7 @@ public void StartFromBuffer(CancellationToken cancellationToken, string path = n /// public void Stop() { - RaiseEvent(ClientStopping, EventArgs.Empty); + MulticastIsolation.Raise(ClientStopping, this, EventArgs.Empty, InternalError); if (_stop != null) _stop.Cancel(); } @@ -494,10 +494,10 @@ public IDevicesResponseDocument GetProbe() client.Timeout = Timeout; client.ContentEncodings = ContentEncodings; client.ContentType = ContentType; - client.MTConnectError += (s, doc) => RaiseEvent(MTConnectError, doc); - client.FormatError += (s, r) => RaiseEvent(FormatError, r); - client.ConnectionError += (s, ex) => RaiseEvent(ConnectionError, ex); - client.InternalError += (s, ex) => RaiseEvent(InternalError, ex); + client.MTConnectError += (s, doc) => MulticastIsolation.Raise(MTConnectError, this, doc, InternalError); + client.FormatError += (s, r) => MulticastIsolation.Raise(FormatError, this, r, InternalError); + client.ConnectionError += (s, ex) => MulticastIsolation.Raise(ConnectionError, this, ex, InternalError); + client.InternalError += (s, ex) => MulticastIsolation.Raise(InternalError, this, ex, InternalError); return client.Get(); } @@ -518,10 +518,10 @@ public async Task GetProbeAsync(CancellationToken canc client.Timeout = Timeout; client.ContentEncodings = ContentEncodings; client.ContentType = ContentType; - client.MTConnectError += (s, doc) => RaiseEvent(MTConnectError, doc); - client.FormatError += (s, r) => RaiseEvent(FormatError, r); - client.ConnectionError += (s, ex) => RaiseEvent(ConnectionError, ex); - client.InternalError += (s, ex) => RaiseEvent(InternalError, ex); + client.MTConnectError += (s, doc) => MulticastIsolation.Raise(MTConnectError, this, doc, InternalError); + client.FormatError += (s, r) => MulticastIsolation.Raise(FormatError, this, r, InternalError); + client.ConnectionError += (s, ex) => MulticastIsolation.Raise(ConnectionError, this, ex, InternalError); + client.InternalError += (s, ex) => MulticastIsolation.Raise(InternalError, this, ex, InternalError); return await client.GetAsync(cancellationToken); } @@ -535,10 +535,10 @@ public IStreamsResponseDocument GetCurrent(long at = 0, string path = null) client.Timeout = Timeout; client.ContentEncodings = ContentEncodings; client.ContentType = ContentType; - client.MTConnectError += (s, doc) => RaiseEvent(MTConnectError, doc); - client.FormatError += (s, r) => RaiseEvent(FormatError, r); - client.ConnectionError += (s, ex) => RaiseEvent(ConnectionError, ex); - client.InternalError += (s, ex) => RaiseEvent(InternalError, ex); + client.MTConnectError += (s, doc) => MulticastIsolation.Raise(MTConnectError, this, doc, InternalError); + client.FormatError += (s, r) => MulticastIsolation.Raise(FormatError, this, r, InternalError); + client.ConnectionError += (s, ex) => MulticastIsolation.Raise(ConnectionError, this, ex, InternalError); + client.InternalError += (s, ex) => MulticastIsolation.Raise(InternalError, this, ex, InternalError); return client.Get(); } @@ -559,10 +559,10 @@ public async Task GetCurrentAsync(CancellationToken ca client.Timeout = Timeout; client.ContentEncodings = ContentEncodings; client.ContentType = ContentType; - client.MTConnectError += (s, doc) => RaiseEvent(MTConnectError, doc); - client.FormatError += (s, r) => RaiseEvent(FormatError, r); - client.ConnectionError += (s, ex) => RaiseEvent(ConnectionError, ex); - client.InternalError += (s, ex) => RaiseEvent(InternalError, ex); + client.MTConnectError += (s, doc) => MulticastIsolation.Raise(MTConnectError, this, doc, InternalError); + client.FormatError += (s, r) => MulticastIsolation.Raise(FormatError, this, r, InternalError); + client.ConnectionError += (s, ex) => MulticastIsolation.Raise(ConnectionError, this, ex, InternalError); + client.InternalError += (s, ex) => MulticastIsolation.Raise(InternalError, this, ex, InternalError); return await client.GetAsync(cancellationToken); } @@ -576,10 +576,10 @@ public IStreamsResponseDocument GetSample(long from = 0, long to = 0, int count client.Timeout = Timeout; client.ContentEncodings = ContentEncodings; client.ContentType = ContentType; - client.MTConnectError += (s, doc) => RaiseEvent(MTConnectError, doc); - client.FormatError += (s, r) => RaiseEvent(FormatError, r); - client.ConnectionError += (s, ex) => RaiseEvent(ConnectionError, ex); - client.InternalError += (s, ex) => RaiseEvent(InternalError, ex); + client.MTConnectError += (s, doc) => MulticastIsolation.Raise(MTConnectError, this, doc, InternalError); + client.FormatError += (s, r) => MulticastIsolation.Raise(FormatError, this, r, InternalError); + client.ConnectionError += (s, ex) => MulticastIsolation.Raise(ConnectionError, this, ex, InternalError); + client.InternalError += (s, ex) => MulticastIsolation.Raise(InternalError, this, ex, InternalError); return client.Get(); } @@ -600,10 +600,10 @@ public async Task GetSampleAsync(CancellationToken can client.Timeout = Timeout; client.ContentEncodings = ContentEncodings; client.ContentType = ContentType; - client.MTConnectError += (s, doc) => RaiseEvent(MTConnectError, doc); - client.FormatError += (s, r) => RaiseEvent(FormatError, r); - client.ConnectionError += (s, ex) => RaiseEvent(ConnectionError, ex); - client.InternalError += (s, ex) => RaiseEvent(InternalError, ex); + client.MTConnectError += (s, doc) => MulticastIsolation.Raise(MTConnectError, this, doc, InternalError); + client.FormatError += (s, r) => MulticastIsolation.Raise(FormatError, this, r, InternalError); + client.ConnectionError += (s, ex) => MulticastIsolation.Raise(ConnectionError, this, ex, InternalError); + client.InternalError += (s, ex) => MulticastIsolation.Raise(InternalError, this, ex, InternalError); return await client.GetAsync(cancellationToken); } @@ -617,10 +617,10 @@ public IAssetsResponseDocument GetAssets(long count = 100) client.Timeout = Timeout; client.ContentEncodings = ContentEncodings; client.ContentType = ContentType; - client.MTConnectError += (s, doc) => RaiseEvent(MTConnectError, doc); - client.FormatError += (s, r) => RaiseEvent(FormatError, r); - client.ConnectionError += (s, ex) => RaiseEvent(ConnectionError, ex); - client.InternalError += (s, ex) => RaiseEvent(InternalError, ex); + client.MTConnectError += (s, doc) => MulticastIsolation.Raise(MTConnectError, this, doc, InternalError); + client.FormatError += (s, r) => MulticastIsolation.Raise(FormatError, this, r, InternalError); + client.ConnectionError += (s, ex) => MulticastIsolation.Raise(ConnectionError, this, ex, InternalError); + client.InternalError += (s, ex) => MulticastIsolation.Raise(InternalError, this, ex, InternalError); return client.Get(); } @@ -641,10 +641,10 @@ public async Task GetAssetsAsync(CancellationToken canc client.Timeout = Timeout; client.ContentEncodings = ContentEncodings; client.ContentType = ContentType; - client.MTConnectError += (s, doc) => RaiseEvent(MTConnectError, doc); - client.FormatError += (s, r) => RaiseEvent(FormatError, r); - client.ConnectionError += (s, ex) => RaiseEvent(ConnectionError, ex); - client.InternalError += (s, ex) => RaiseEvent(InternalError, ex); + client.MTConnectError += (s, doc) => MulticastIsolation.Raise(MTConnectError, this, doc, InternalError); + client.FormatError += (s, r) => MulticastIsolation.Raise(FormatError, this, r, InternalError); + client.ConnectionError += (s, ex) => MulticastIsolation.Raise(ConnectionError, this, ex, InternalError); + client.InternalError += (s, ex) => MulticastIsolation.Raise(InternalError, this, ex, InternalError); return await client.GetAsync(cancellationToken); } @@ -658,10 +658,10 @@ public IAssetsResponseDocument GetAsset(string assetId) client.Timeout = Timeout; client.ContentEncodings = ContentEncodings; client.ContentType = ContentType; - client.MTConnectError += (s, doc) => RaiseEvent(MTConnectError, doc); - client.FormatError += (s, r) => RaiseEvent(FormatError, r); - client.ConnectionError += (s, ex) => RaiseEvent(ConnectionError, ex); - client.InternalError += (s, ex) => RaiseEvent(InternalError, ex); + client.MTConnectError += (s, doc) => MulticastIsolation.Raise(MTConnectError, this, doc, InternalError); + client.FormatError += (s, r) => MulticastIsolation.Raise(FormatError, this, r, InternalError); + client.ConnectionError += (s, ex) => MulticastIsolation.Raise(ConnectionError, this, ex, InternalError); + client.InternalError += (s, ex) => MulticastIsolation.Raise(InternalError, this, ex, InternalError); return client.Get(); } @@ -682,10 +682,10 @@ public async Task GetAssetAsync(string assetId, Cancell client.Timeout = Timeout; client.ContentEncodings = ContentEncodings; client.ContentType = ContentType; - client.MTConnectError += (s, doc) => RaiseEvent(MTConnectError, doc); - client.FormatError += (s, r) => RaiseEvent(FormatError, r); - client.ConnectionError += (s, ex) => RaiseEvent(ConnectionError, ex); - client.InternalError += (s, ex) => RaiseEvent(InternalError, ex); + client.MTConnectError += (s, doc) => MulticastIsolation.Raise(MTConnectError, this, doc, InternalError); + client.FormatError += (s, r) => MulticastIsolation.Raise(FormatError, this, r, InternalError); + client.ConnectionError += (s, ex) => MulticastIsolation.Raise(ConnectionError, this, ex, InternalError); + client.InternalError += (s, ex) => MulticastIsolation.Raise(InternalError, this, ex, InternalError); return await client.GetAsync(cancellationToken); } @@ -697,7 +697,7 @@ private async Task Worker() { var initialRequest = true; - RaiseEvent(ClientStarted, EventArgs.Empty); + MulticastIsolation.Raise(ClientStarted, this, EventArgs.Empty, InternalError); do { @@ -708,7 +708,7 @@ private async Task Worker() if (probe != null) { _lastResponse = UnixDateTime.Now; - RaiseEvent(ResponseReceived, EventArgs.Empty); + MulticastIsolation.Raise(ResponseReceived, this, EventArgs.Empty, InternalError); ProcessProbeDocument(probe); @@ -719,9 +719,9 @@ private async Task Worker() if (assets != null) { _lastResponse = UnixDateTime.Now; - RaiseEvent(ResponseReceived, EventArgs.Empty); + MulticastIsolation.Raise(ResponseReceived, this, EventArgs.Empty, InternalError); - RaiseEvent(AssetsReceived, assets); + MulticastIsolation.Raise(AssetsReceived, this, assets, InternalError); } } @@ -732,7 +732,7 @@ private async Task Worker() if (current != null) { _lastResponse = UnixDateTime.Now; - RaiseEvent(ResponseReceived, EventArgs.Empty); + MulticastIsolation.Raise(ResponseReceived, this, EventArgs.Empty, InternalError); // Raise CurrentReceived Event ProcessCurrentDocument(current, _stop.Token); @@ -777,15 +777,15 @@ private async Task Worker() _stream.Timeout = Heartbeat * 3; _stream.ContentEncodings = ContentEncodings; _stream.ContentType = ContentType; - _stream.Starting += (s, o) => RaiseEvent(StreamStarting, url); - _stream.Started += (s, o) => RaiseEvent(StreamStarted, url); - _stream.Stopping += (s, o) => RaiseEvent(StreamStopping, url); - _stream.Stopped += (s, o) => RaiseEvent(StreamStopped, url); + _stream.Starting += (s, o) => MulticastIsolation.Raise(StreamStarting, this, url, InternalError); + _stream.Started += (s, o) => MulticastIsolation.Raise(StreamStarted, this, url, InternalError); + _stream.Stopping += (s, o) => MulticastIsolation.Raise(StreamStopping, this, url, InternalError); + _stream.Stopped += (s, o) => MulticastIsolation.Raise(StreamStopped, this, url, InternalError); _stream.DocumentReceived += (s, doc) => ProcessSampleDocument(doc, _stop.Token); _stream.ErrorReceived += (s, doc) => ProcessSampleError(doc); - _stream.FormatError += (s, r) => RaiseEvent(FormatError, r); - _stream.ConnectionError += (s, ex) => RaiseEvent(ConnectionError, ex); - _stream.InternalError += (s, ex) => RaiseEvent(InternalError, ex); + _stream.FormatError += (s, r) => MulticastIsolation.Raise(FormatError, this, r, InternalError); + _stream.ConnectionError += (s, ex) => MulticastIsolation.Raise(ConnectionError, this, ex, InternalError); + _stream.InternalError += (s, ex) => MulticastIsolation.Raise(InternalError, this, ex, InternalError); // Run Stream (Blocking call) await _stream.Run(_stop.Token); @@ -812,7 +812,7 @@ private async Task Worker() if (current != null) { _lastResponse = UnixDateTime.Now; - RaiseEvent(ResponseReceived, EventArgs.Empty); + MulticastIsolation.Raise(ResponseReceived, this, EventArgs.Empty, InternalError); // Raise CurrentReceived Event ProcessCurrentDocument(current, _stop.Token); @@ -871,12 +871,12 @@ private async Task Worker() catch (TaskCanceledException) { } catch (Exception ex) { - RaiseEvent(InternalError, ex); + MulticastIsolation.Raise(InternalError, this, ex, InternalError); } } while (!_stop.Token.IsCancellationRequested); - RaiseEvent(ClientStopped, EventArgs.Empty); + MulticastIsolation.Raise(ClientStopped, this, EventArgs.Empty, InternalError); } private void ProcessProbeDocument(IDevicesResponseDocument document) @@ -907,77 +907,18 @@ private void ProcessProbeDocument(IDevicesResponseDocument document) // Isolate subscriber exceptions per delegate so one bad handler cannot abort the // populate loop, suppress ProbeReceived, or short-circuit later subscribers in the // invocation list; route each fault through InternalError instead. - RaiseEvent(DeviceReceived, outputDevice); + MulticastIsolation.Raise(DeviceReceived, this, outputDevice, InternalError); } // Raise ProbeReceived Event - RaiseEvent(ProbeReceived, document); - } - } - - // Iterate the invocation list so one throwing subscriber cannot short-circuit the - // multicast and starve later subscribers. Each fault is forwarded through - // InternalError; if InternalError itself faults, swallow that secondary fault so the - // remaining subscribers in the invocation list still receive the event. - private void RaiseEvent(EventHandler handler, T arg) - { - if (handler == null) return; - - foreach (var subscriber in handler.GetInvocationList()) - { - try - { - ((EventHandler)subscriber).Invoke(this, arg); - } - catch (Exception ex) - { - RouteSubscriberFault(ex); - } - } - } - - // Non-generic sibling of RaiseEvent<T> for EventHandler events that carry no - // typed payload (ClientStarting, ClientStarted, ClientStopping, ClientStopped, - // ResponseReceived). Same multicast-isolation contract: a throwing subscriber - // cannot starve later subscribers, and a faulting InternalError handler cannot - // break the fan-out either. - private void RaiseEvent(EventHandler handler, EventArgs arg) - { - if (handler == null) return; - - foreach (var subscriber in handler.GetInvocationList()) - { - try - { - ((EventHandler)subscriber).Invoke(this, arg); - } - catch (Exception ex) - { - RouteSubscriberFault(ex); - } - } - } - - // Forwards a subscriber fault to InternalError and swallows any secondary - // fault raised by InternalError itself so the originating event's fan-out - // can keep running. Shared between the generic and non-generic RaiseEvent - // helpers above. - private void RouteSubscriberFault(Exception ex) - { - try - { - InternalError?.Invoke(this, ex); - } - catch - { - // A faulting InternalError handler must not break the event fan-out. + MulticastIsolation.Raise(ProbeReceived, this, document, InternalError); } } private void ProcessCurrentDocument(IStreamsResponseDocument document, CancellationToken cancel) { _lastResponse = UnixDateTime.Now; - RaiseEvent(ResponseReceived, EventArgs.Empty); + MulticastIsolation.Raise(ResponseReceived, this, EventArgs.Empty, InternalError); if (document != null) { @@ -995,7 +936,7 @@ private void ProcessCurrentDocument(IStreamsResponseDocument document, Cancellat response.Streams = deviceStreams; - RaiseEvent(CurrentReceived, response); + MulticastIsolation.Raise(CurrentReceived, this, response, InternalError); // Process Device Streams @@ -1010,7 +951,7 @@ private void ProcessCurrentDocument(IStreamsResponseDocument document, Cancellat { foreach (var observation in observations) { - RaiseEvent(ObservationReceived, observation); + MulticastIsolation.Raise(ObservationReceived, this, observation, InternalError); } } } @@ -1021,7 +962,7 @@ private void ProcessCurrentDocument(IStreamsResponseDocument document, Cancellat private void ProcessSampleDocument(IStreamsResponseDocument document, CancellationToken cancel) { _lastResponse = UnixDateTime.Now; - RaiseEvent(ResponseReceived, EventArgs.Empty); + MulticastIsolation.Raise(ResponseReceived, this, EventArgs.Empty, InternalError); if (document != null) { @@ -1061,11 +1002,11 @@ private void ProcessSampleDocument(IStreamsResponseDocument document, Cancellati } } - RaiseEvent(SampleReceived, response); + MulticastIsolation.Raise(SampleReceived, this, response, InternalError); foreach (var observation in receivedObservations) { - RaiseEvent(ObservationReceived, observation); + MulticastIsolation.Raise(ObservationReceived, this, observation, InternalError); } } } @@ -1253,11 +1194,11 @@ private IComponentStream ProcessComponentStream(IMTConnectStreamsHeader header, private void ProcessSampleError(IErrorResponseDocument document) { _lastResponse = UnixDateTime.Now; - RaiseEvent(ResponseReceived, EventArgs.Empty); + MulticastIsolation.Raise(ResponseReceived, this, EventArgs.Empty, InternalError); if (document != null) { - RaiseEvent(MTConnectError, document); + MulticastIsolation.Raise(MTConnectError, this, document, InternalError); } } @@ -1277,13 +1218,13 @@ private async void CheckAssetChanged(IEnumerable observations, Can var doc = await GetAssetAsync(assetId, cancel); if (doc != null) { - RaiseEvent(AssetsReceived, doc); + MulticastIsolation.Raise(AssetsReceived, this, doc, InternalError); if (doc != null && !doc.Assets.IsNullOrEmpty()) { foreach (var asset in doc.Assets) { - RaiseEvent(AssetReceived, asset); + MulticastIsolation.Raise(AssetReceived, this, asset, InternalError); } } } diff --git a/libraries/MTConnect.NET-HTTP/Clients/MTConnectHttpClientStream.cs b/libraries/MTConnect.NET-HTTP/Clients/MTConnectHttpClientStream.cs index 6fa09090..0830385c 100644 --- a/libraries/MTConnect.NET-HTTP/Clients/MTConnectHttpClientStream.cs +++ b/libraries/MTConnect.NET-HTTP/Clients/MTConnectHttpClientStream.cs @@ -123,7 +123,7 @@ public void Start(CancellationToken cancellationToken) cancellationToken.Register(() => Stop()); // Raise Starting Event - RaiseEvent(Starting, EventArgs.Empty); + MulticastIsolation.Raise(Starting, this, EventArgs.Empty, InternalError); _ = Task.Run(() => Run(_stop.Token)); } @@ -136,7 +136,7 @@ public void Start(CancellationToken cancellationToken) public void Stop() { // Raise Stopping Event - RaiseEvent(Stopping, EventArgs.Empty); + MulticastIsolation.Raise(Stopping, this, EventArgs.Empty, InternalError); if (_stop != null) _stop.Cancel(); } @@ -167,7 +167,7 @@ public async Task Run(CancellationToken cancellationToken) responseTimer.Elapsed += (o, e) => { stop.Cancel(); - RaiseEvent(ConnectionError, new TimeoutException($"HTTP Stream Timeout Exceeded ({Timeout})")); + MulticastIsolation.Raise(ConnectionError, this, new TimeoutException($"HTTP Stream Timeout Exceeded ({Timeout})"), InternalError); }; responseTimer.Start(); } @@ -177,7 +177,7 @@ public async Task Run(CancellationToken cancellationToken) stop.Token.ThrowIfCancellationRequested(); // Raise Started Event - RaiseEvent(Started, EventArgs.Empty); + MulticastIsolation.Raise(Started, this, EventArgs.Empty, InternalError); // Add 'Accept' HTTP Header @@ -305,16 +305,16 @@ public async Task Run(CancellationToken cancellationToken) } catch (TaskCanceledException ex) when (ex.InnerException is TimeoutException) { - RaiseEvent(ConnectionError, ex); + MulticastIsolation.Raise(ConnectionError, this, ex, InternalError); } catch (TaskCanceledException) { /* Ignore Task Cancelled */ } catch (HttpRequestException ex) { - RaiseEvent(ConnectionError, ex); + MulticastIsolation.Raise(ConnectionError, this, ex, InternalError); } catch (Exception ex) { - RaiseEvent(InternalError, ex); + MulticastIsolation.Raise(InternalError, this, ex, InternalError); } finally { @@ -322,7 +322,7 @@ public async Task Run(CancellationToken cancellationToken) } } - RaiseEvent(Stopped, EventArgs.Empty); + MulticastIsolation.Raise(Stopped, this, EventArgs.Empty, InternalError); } private static string GetHeaderValue(string s, string name) @@ -411,7 +411,7 @@ protected virtual void ProcessResponseBody(Stream responseBody, string contentEn var document = formatResult.Content; if (document != null) { - RaiseEvent(DocumentReceived, document); + MulticastIsolation.Raise(DocumentReceived, this, document, InternalError); } else { @@ -420,79 +420,21 @@ protected virtual void ProcessResponseBody(Stream responseBody, string contentEn if (errorFormatResult.Success) { var errorDocument = errorFormatResult.Content; - if (errorDocument != null) RaiseEvent(ErrorReceived, errorDocument); + if (errorDocument != null) MulticastIsolation.Raise(ErrorReceived, this, errorDocument, InternalError); } else { // Raise Format Error - RaiseEvent(FormatError, errorFormatResult); + MulticastIsolation.Raise(FormatError, this, errorFormatResult, InternalError); } } } else { // Raise Format Error - RaiseEvent(FormatError, formatResult); + MulticastIsolation.Raise(FormatError, this, formatResult, InternalError); } } } - - // Iterate the invocation list so one throwing subscriber cannot short-circuit - // the multicast and starve later subscribers. Each fault is forwarded through - // InternalError; if InternalError itself faults, swallow that secondary fault - // so the remaining subscribers in the invocation list still receive the event. - private void RaiseEvent(EventHandler handler, T arg) - { - if (handler == null) return; - - foreach (var subscriber in handler.GetInvocationList()) - { - try - { - ((EventHandler)subscriber).Invoke(this, arg); - } - catch (Exception ex) - { - RouteSubscriberFault(ex); - } - } - } - - // Non-generic sibling of RaiseEvent<T> for EventHandler events that carry no - // typed payload (Starting, Started, Stopping, Stopped). Same multicast-isolation - // contract: a throwing subscriber cannot starve later subscribers, and a - // faulting InternalError handler cannot break the fan-out either. - private void RaiseEvent(EventHandler handler, EventArgs arg) - { - if (handler == null) return; - - foreach (var subscriber in handler.GetInvocationList()) - { - try - { - ((EventHandler)subscriber).Invoke(this, arg); - } - catch (Exception ex) - { - RouteSubscriberFault(ex); - } - } - } - - // Forwards a subscriber fault to InternalError and swallows any secondary - // fault raised by InternalError itself so the originating event's fan-out - // can keep running. Shared between the generic and non-generic RaiseEvent - // helpers above. - private void RouteSubscriberFault(Exception ex) - { - try - { - InternalError?.Invoke(this, ex); - } - catch - { - // A faulting InternalError handler must not break the event fan-out. - } - } } } \ No newline at end of file diff --git a/libraries/MTConnect.NET-HTTP/Clients/MTConnectHttpCurrentClient.cs b/libraries/MTConnect.NET-HTTP/Clients/MTConnectHttpCurrentClient.cs index 7e603959..01e606d9 100644 --- a/libraries/MTConnect.NET-HTTP/Clients/MTConnectHttpCurrentClient.cs +++ b/libraries/MTConnect.NET-HTTP/Clients/MTConnectHttpCurrentClient.cs @@ -196,16 +196,16 @@ public IStreamsResponseDocument Get() } catch (TaskCanceledException ex) when (ex.InnerException is TimeoutException) { - RaiseEvent(ConnectionError, ex); + MulticastIsolation.Raise(ConnectionError, this, ex, InternalError); } catch (TaskCanceledException) { /* Ignore Task Cancelled */ } catch (HttpRequestException ex) { - RaiseEvent(ConnectionError, ex); + MulticastIsolation.Raise(ConnectionError, this, ex, InternalError); } catch (Exception ex) { - RaiseEvent(InternalError, ex); + MulticastIsolation.Raise(InternalError, this, ex, InternalError); } return null; @@ -250,16 +250,16 @@ public async Task GetAsync(CancellationToken cancellat } catch (TaskCanceledException ex) when (ex.InnerException is TimeoutException) { - RaiseEvent(ConnectionError, ex); + MulticastIsolation.Raise(ConnectionError, this, ex, InternalError); } catch (TaskCanceledException) { /* Ignore Task Cancelled */ } catch (HttpRequestException ex) { - RaiseEvent(ConnectionError, ex); + MulticastIsolation.Raise(ConnectionError, this, ex, InternalError); } catch (Exception ex) { - RaiseEvent(InternalError, ex); + MulticastIsolation.Raise(InternalError, this, ex, InternalError); } return null; @@ -348,7 +348,7 @@ private IStreamsResponseDocument HandleResponse(HttpResponseMessage response) { if (!response.IsSuccessStatusCode) { - RaiseEvent(ConnectionError, new Exception(response.ReasonPhrase)); + MulticastIsolation.Raise(ConnectionError, this, new Exception(response.ReasonPhrase), InternalError); } else if (response.Content != null) { @@ -366,7 +366,7 @@ private async Task HandleResponseAsync(HttpResponseMes { if (!response.IsSuccessStatusCode) { - RaiseEvent(ConnectionError, new Exception(response.ReasonPhrase)); + MulticastIsolation.Raise(ConnectionError, this, new Exception(response.ReasonPhrase), InternalError); } else if (response.Content != null) { @@ -408,59 +408,23 @@ private IStreamsResponseDocument ReadDocument(HttpResponseMessage response, Stre if (errorFormatResult.Success) { var errorDocument = errorFormatResult.Content; - if (errorDocument != null) RaiseEvent(MTConnectError, errorDocument); + if (errorDocument != null) MulticastIsolation.Raise(MTConnectError, this, errorDocument, InternalError); } else { // Raise Format Error - RaiseEvent(FormatError, errorFormatResult); + MulticastIsolation.Raise(FormatError, this, errorFormatResult, InternalError); } } } else { // Raise Format Error - RaiseEvent(FormatError, formatResult); + MulticastIsolation.Raise(FormatError, this, formatResult, InternalError); } } return null; } - - // Iterate the invocation list so one throwing subscriber cannot short-circuit - // the multicast and starve later subscribers. Each fault is forwarded through - // InternalError; if InternalError itself faults, swallow that secondary fault - // so the remaining subscribers in the invocation list still receive the event. - private void RaiseEvent(EventHandler handler, T arg) - { - if (handler == null) return; - - foreach (var subscriber in handler.GetInvocationList()) - { - try - { - ((EventHandler)subscriber).Invoke(this, arg); - } - catch (Exception ex) - { - RouteSubscriberFault(ex); - } - } - } - - // Forwards a subscriber fault to InternalError and swallows any secondary - // fault raised by InternalError itself so the originating event's fan-out - // can keep running. - private void RouteSubscriberFault(Exception ex) - { - try - { - InternalError?.Invoke(this, ex); - } - catch - { - // A faulting InternalError handler must not break the event fan-out. - } - } } } \ No newline at end of file diff --git a/libraries/MTConnect.NET-HTTP/Clients/MTConnectHttpProbeClient.cs b/libraries/MTConnect.NET-HTTP/Clients/MTConnectHttpProbeClient.cs index 268ece2d..a903e525 100644 --- a/libraries/MTConnect.NET-HTTP/Clients/MTConnectHttpProbeClient.cs +++ b/libraries/MTConnect.NET-HTTP/Clients/MTConnectHttpProbeClient.cs @@ -236,16 +236,16 @@ public IDevicesResponseDocument Get() } catch (TaskCanceledException ex) when (ex.InnerException is TimeoutException) { - RaiseEvent(ConnectionError, ex); + MulticastIsolation.Raise(ConnectionError, this, ex, InternalError); } catch (TaskCanceledException) { /* Ignore Task Cancelled */ } catch (HttpRequestException ex) { - RaiseEvent(ConnectionError, ex); + MulticastIsolation.Raise(ConnectionError, this, ex, InternalError); } catch (Exception ex) { - RaiseEvent(InternalError, ex); + MulticastIsolation.Raise(InternalError, this, ex, InternalError); } return null; @@ -297,16 +297,16 @@ public async Task GetAsync(CancellationToken cancellat } catch (TaskCanceledException ex) when (ex.InnerException is TimeoutException) { - RaiseEvent(ConnectionError, ex); + MulticastIsolation.Raise(ConnectionError, this, ex, InternalError); } catch (TaskCanceledException) { /* Ignore Task Cancelled */ } catch (HttpRequestException ex) { - RaiseEvent(ConnectionError, ex); + MulticastIsolation.Raise(ConnectionError, this, ex, InternalError); } catch (Exception ex) { - RaiseEvent(InternalError, ex); + MulticastIsolation.Raise(InternalError, this, ex, InternalError); } return null; @@ -388,7 +388,7 @@ private IDevicesResponseDocument HandleResponse(HttpResponseMessage response) { if (!response.IsSuccessStatusCode) { - RaiseEvent(ConnectionError, new Exception(response.ReasonPhrase)); + MulticastIsolation.Raise(ConnectionError, this, new Exception(response.ReasonPhrase), InternalError); } else if (response.Content != null) { @@ -406,7 +406,7 @@ private async Task HandleResponseAsync(HttpResponseMes { if (!response.IsSuccessStatusCode) { - RaiseEvent(ConnectionError, new Exception(response.ReasonPhrase)); + MulticastIsolation.Raise(ConnectionError, this, new Exception(response.ReasonPhrase), InternalError); } else if (response.Content != null) { @@ -448,59 +448,23 @@ private IDevicesResponseDocument ReadDocument(HttpResponseMessage response, Stre if (errorFormatResult.Success) { var errorDocument = errorFormatResult.Content; - if (errorDocument != null) RaiseEvent(MTConnectError, errorDocument); + if (errorDocument != null) MulticastIsolation.Raise(MTConnectError, this, errorDocument, InternalError); } else { // Raise Format Error - RaiseEvent(FormatError, errorFormatResult); + MulticastIsolation.Raise(FormatError, this, errorFormatResult, InternalError); } } } else { // Raise Format Error - RaiseEvent(FormatError, formatResult); + MulticastIsolation.Raise(FormatError, this, formatResult, InternalError); } } return null; } - - // Iterate the invocation list so one throwing subscriber cannot short-circuit - // the multicast and starve later subscribers. Each fault is forwarded through - // InternalError; if InternalError itself faults, swallow that secondary fault - // so the remaining subscribers in the invocation list still receive the event. - private void RaiseEvent(EventHandler handler, T arg) - { - if (handler == null) return; - - foreach (var subscriber in handler.GetInvocationList()) - { - try - { - ((EventHandler)subscriber).Invoke(this, arg); - } - catch (Exception ex) - { - RouteSubscriberFault(ex); - } - } - } - - // Forwards a subscriber fault to InternalError and swallows any secondary - // fault raised by InternalError itself so the originating event's fan-out - // can keep running. - private void RouteSubscriberFault(Exception ex) - { - try - { - InternalError?.Invoke(this, ex); - } - catch - { - // A faulting InternalError handler must not break the event fan-out. - } - } } } \ No newline at end of file diff --git a/libraries/MTConnect.NET-HTTP/Clients/MTConnectHttpSampleClient.cs b/libraries/MTConnect.NET-HTTP/Clients/MTConnectHttpSampleClient.cs index 87a19f1c..53b9f321 100644 --- a/libraries/MTConnect.NET-HTTP/Clients/MTConnectHttpSampleClient.cs +++ b/libraries/MTConnect.NET-HTTP/Clients/MTConnectHttpSampleClient.cs @@ -213,16 +213,16 @@ public IStreamsResponseDocument Get() } catch (TaskCanceledException ex) when (ex.InnerException is TimeoutException) { - RaiseEvent(ConnectionError, ex); + MulticastIsolation.Raise(ConnectionError, this, ex, InternalError); } catch (TaskCanceledException) { /* Ignore Task Cancelled */ } catch (HttpRequestException ex) { - RaiseEvent(ConnectionError, ex); + MulticastIsolation.Raise(ConnectionError, this, ex, InternalError); } catch (Exception ex) { - RaiseEvent(InternalError, ex); + MulticastIsolation.Raise(InternalError, this, ex, InternalError); } return null; @@ -267,16 +267,16 @@ public async Task GetAsync(CancellationToken cancel) } catch (TaskCanceledException ex) when (ex.InnerException is TimeoutException) { - RaiseEvent(ConnectionError, ex); + MulticastIsolation.Raise(ConnectionError, this, ex, InternalError); } catch (TaskCanceledException) { /* Ignore Task Cancelled */ } catch (HttpRequestException ex) { - RaiseEvent(ConnectionError, ex); + MulticastIsolation.Raise(ConnectionError, this, ex, InternalError); } catch (Exception ex) { - RaiseEvent(InternalError, ex); + MulticastIsolation.Raise(InternalError, this, ex, InternalError); } return null; @@ -382,7 +382,7 @@ private IStreamsResponseDocument HandleResponse(HttpResponseMessage response) { if (!response.IsSuccessStatusCode) { - RaiseEvent(ConnectionError, new Exception(response.ReasonPhrase)); + MulticastIsolation.Raise(ConnectionError, this, new Exception(response.ReasonPhrase), InternalError); } else if (response.Content != null) { @@ -400,7 +400,7 @@ private async Task HandleResponseAsync(HttpResponseMes { if (!response.IsSuccessStatusCode) { - RaiseEvent(ConnectionError, new Exception(response.ReasonPhrase)); + MulticastIsolation.Raise(ConnectionError, this, new Exception(response.ReasonPhrase), InternalError); } else if (response.Content != null) { @@ -442,59 +442,23 @@ private IStreamsResponseDocument ReadDocument(HttpResponseMessage response, Stre if (errorFormatResult.Success) { var errorDocument = errorFormatResult.Content; - if (errorDocument != null) RaiseEvent(MTConnectError, errorDocument); + if (errorDocument != null) MulticastIsolation.Raise(MTConnectError, this, errorDocument, InternalError); } else { // Raise Format Error - RaiseEvent(FormatError, errorFormatResult); + MulticastIsolation.Raise(FormatError, this, errorFormatResult, InternalError); } } } else { // Raise Format Error - RaiseEvent(FormatError, formatResult); + MulticastIsolation.Raise(FormatError, this, formatResult, InternalError); } } return null; } - - // Iterate the invocation list so one throwing subscriber cannot short-circuit - // the multicast and starve later subscribers. Each fault is forwarded through - // InternalError; if InternalError itself faults, swallow that secondary fault - // so the remaining subscribers in the invocation list still receive the event. - private void RaiseEvent(EventHandler handler, T arg) - { - if (handler == null) return; - - foreach (var subscriber in handler.GetInvocationList()) - { - try - { - ((EventHandler)subscriber).Invoke(this, arg); - } - catch (Exception ex) - { - RouteSubscriberFault(ex); - } - } - } - - // Forwards a subscriber fault to InternalError and swallows any secondary - // fault raised by InternalError itself so the originating event's fan-out - // can keep running. - private void RouteSubscriberFault(Exception ex) - { - try - { - InternalError?.Invoke(this, ex); - } - catch - { - // A faulting InternalError handler must not break the event fan-out. - } - } } } \ No newline at end of file From cdfd55edfe21314c5bfe97041b4f97138106cccf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Otto=20Boly=C3=B3s?= Date: Wed, 3 Jun 2026 11:50:04 +0200 Subject: [PATCH 12/37] test(http-tests): drive sub-client tests through shared helper --- .../SubClientMulticastIsolationTests.cs | 41 ++++--------------- 1 file changed, 9 insertions(+), 32 deletions(-) diff --git a/tests/MTConnect.NET-HTTP-Tests/Clients/SubClientMulticastIsolationTests.cs b/tests/MTConnect.NET-HTTP-Tests/Clients/SubClientMulticastIsolationTests.cs index a61413af..3563c32d 100644 --- a/tests/MTConnect.NET-HTTP-Tests/Clients/SubClientMulticastIsolationTests.cs +++ b/tests/MTConnect.NET-HTTP-Tests/Clients/SubClientMulticastIsolationTests.cs @@ -59,46 +59,23 @@ private static T GetEventBacking(object instance, string eventName) where T : return (T)field!.GetValue(instance)!; } + // Drives the sub-client's event fan-out through the shared + // MulticastIsolation helper. Reflection still snapshots the event's + // private backing field (events are private outside the declaring + // class), but the dispatch barrier under test is now the single + // shared helper, not a per-class private method. private static void InvokeGenericRaise(object instance, string eventName, T arg) { var handler = GetEventBacking>(instance, eventName); - - // Locate the open generic RaiseEvent(EventHandler, T) helper by - // walking the non-public instance methods; GetMethod's overload that - // takes a types[] array binds parameters against the open signature - // and never matches a closed EventHandler, so the manual - // search is the simplest path. Once found, MakeGenericMethod closes - // it over the test's payload type. - MethodInfo? openMethod = null; - foreach (var candidate in instance.GetType().GetMethods( - BindingFlags.Instance | BindingFlags.NonPublic)) - { - if (candidate.Name != "RaiseEvent") continue; - if (!candidate.IsGenericMethodDefinition) continue; - var parameters = candidate.GetParameters(); - if (parameters.Length != 2) continue; - if (parameters[0].ParameterType.GetGenericTypeDefinition() != typeof(EventHandler<>)) continue; - openMethod = candidate; - break; - } - Assert.That(openMethod, Is.Not.Null, - $"Generic RaiseEvent helper was not found on {instance.GetType().Name}."); - - var closedMethod = openMethod!.MakeGenericMethod(typeof(T)); - closedMethod.Invoke(instance, new object?[] { handler, arg }); + var internalError = GetEventBacking>(instance, "InternalError"); + MulticastIsolation.Raise(handler, instance, arg, internalError); } private static void InvokeNonGenericRaise(object instance, string eventName, EventArgs arg) { var handler = GetEventBacking(instance, eventName); - var method = instance.GetType().GetMethod("RaiseEvent", - BindingFlags.Instance | BindingFlags.NonPublic, - binder: null, - types: new[] { typeof(EventHandler), typeof(EventArgs) }, - modifiers: null); - Assert.That(method, Is.Not.Null, - $"Non-generic RaiseEvent helper was not found on {instance.GetType().Name}."); - method!.Invoke(instance, new object?[] { handler, arg }); + var internalError = GetEventBacking>(instance, "InternalError"); + MulticastIsolation.Raise(handler, instance, arg, internalError); } From 64e39f10b6196102d2b279e38ce27170112938f2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Otto=20Boly=C3=B3s?= Date: Wed, 3 Jun 2026 12:10:53 +0200 Subject: [PATCH 13/37] test(common-tests): pin multicast isolation on MQTT client events --- .../MqttClientMulticastIsolationTests.cs | 180 ++++++++++++++++++ 1 file changed, 180 insertions(+) create mode 100644 tests/MTConnect.NET-Common-Tests/MqttClientMulticastIsolationTests.cs diff --git a/tests/MTConnect.NET-Common-Tests/MqttClientMulticastIsolationTests.cs b/tests/MTConnect.NET-Common-Tests/MqttClientMulticastIsolationTests.cs new file mode 100644 index 00000000..870a6a05 --- /dev/null +++ b/tests/MTConnect.NET-Common-Tests/MqttClientMulticastIsolationTests.cs @@ -0,0 +1,180 @@ +// Copyright (c) 2026 TrakHound Inc., All Rights Reserved. +// TrakHound Inc. licenses this file to you under the MIT license. + +using System; +using System.Collections.Generic; +using NUnit.Framework; + +namespace MTConnect.Tests.Common +{ + /// + /// Pins the multicast-isolation contract for the delegate shapes used by + /// MTConnectMqttClient and MTConnectMqttExpandedClient. Both + /// MQTT client classes declare their events as + /// or ; after migration all raise sites use + /// / . + /// These tests verify the isolation guarantee holds for every generic type + /// argument surfaced by those events (no MQTT broker is required because the + /// helper contract is independent of the originating class). + /// + [TestFixture] + public class MqttClientMulticastIsolationTests + { + // ----------------------------------------------------------------------- + // Non-generic EventHandler raise sites + // (ClientStarting, ClientStarted, ClientStopping, ClientStopped, + // ResponseReceived on MTConnectMqttClient) + // ----------------------------------------------------------------------- + + /// Pins the behavior expressed by the test name: a second non-generic subscriber fires even when the first throws, covering the EventHandler raise sites on both MQTT client classes. + [Test] + public void MqttClient_NonGeneric_EventHandler_FiresAllSubscribersWhenOneThrows() + { + var fired = new List(); + EventHandler? handler = null; + handler += (_, _) => fired.Add(1); + handler += (_, _) => throw new InvalidOperationException("subscriber-2 throws"); + handler += (_, _) => fired.Add(3); + + EventHandler? internalError = (_, _) => { }; + + MulticastIsolation.Raise(handler!, this, EventArgs.Empty, internalError); + + Assert.That(fired, Is.EqualTo(new[] { 1, 3 })); + } + + /// Pins the behavior expressed by the test name: a throwing non-generic subscriber faults are routed through InternalError without interrupting subsequent non-generic subscribers. + [Test] + public void MqttClient_NonGeneric_EventHandler_RoutesFaultToInternalError() + { + var routed = new List(); + EventHandler handler = (_, _) => throw new InvalidOperationException("mqtt-non-generic-fault"); + EventHandler internalError = (_, ex) => routed.Add(ex); + + MulticastIsolation.Raise(handler, this, EventArgs.Empty, internalError); + + Assert.That(routed.Count, Is.EqualTo(1)); + Assert.That(routed[0], Is.InstanceOf()); + Assert.That(routed[0].Message, Is.EqualTo("mqtt-non-generic-fault")); + } + + /// Pins the behavior expressed by the test name: when InternalError itself throws for a non-generic event, later InternalError subscribers still fire and remaining event subscribers are not starved. + [Test] + public void MqttClient_NonGeneric_EventHandler_ThrowingInternalErrorDoesNotStarveRemainingSubscribers() + { + var eventFired = new List(); + var errorFired = new List(); + + EventHandler? handler = null; + handler += (_, _) => throw new InvalidOperationException("first subscriber throws"); + handler += (_, _) => eventFired.Add(2); + + EventHandler? internalError = null; + internalError += (_, _) => throw new InvalidOperationException("InternalError handler-1 throws"); + internalError += (_, ex) => errorFired.Add(2); + + MulticastIsolation.Raise(handler!, this, EventArgs.Empty, internalError!); + + Assert.That(eventFired, Is.EqualTo(new[] { 2 })); + Assert.That(errorFired, Is.EqualTo(new[] { 2 })); + } + + // ----------------------------------------------------------------------- + // Generic EventHandler raise sites — string payload + // (AgentConnected / AgentDisconnected on ShdrAdapter; Connected / + // Disconnected on ShdrClient — also applies to MQTT's generic events) + // ----------------------------------------------------------------------- + + /// Pins the behavior expressed by the test name: a second generic subscriber fires even when the first throws, covering EventHandler{string} raise sites. + [Test] + public void MqttClient_Generic_EventHandlerOfString_FiresAllSubscribersWhenOneThrows() + { + var received = new List(); + EventHandler? handler = null; + handler += (_, _) => throw new InvalidOperationException("first throws"); + handler += (_, v) => received.Add(v!); + + MulticastIsolation.Raise(handler!, this, "payload", null); + + Assert.That(received, Is.EqualTo(new[] { "payload" })); + } + + /// Pins the behavior expressed by the test name: fault from a generic EventHandler{string} subscriber is routed through InternalError. + [Test] + public void MqttClient_Generic_EventHandlerOfString_RoutesFaultToInternalError() + { + var routed = new List(); + EventHandler handler = (_, _) => throw new InvalidOperationException("string-event-fault"); + EventHandler internalError = (_, ex) => routed.Add(ex.Message); + + MulticastIsolation.Raise(handler, this, "value", internalError); + + Assert.That(routed, Is.EqualTo(new[] { "string-event-fault" })); + } + + /// Pins the behavior expressed by the test name: when InternalError throws for a generic EventHandler{string} event, later InternalError subscribers still fire. + [Test] + public void MqttClient_Generic_EventHandlerOfString_ThrowingInternalErrorIteratesPerSubscriber() + { + var seen = new List(); + EventHandler handler = (_, _) => throw new InvalidOperationException("origin"); + + EventHandler? internalError = null; + internalError += (_, _) => throw new InvalidOperationException("internalError-1 throws"); + internalError += (_, _) => seen.Add(2); + internalError += (_, _) => seen.Add(3); + + MulticastIsolation.Raise(handler, this, "x", internalError!); + + Assert.That(seen, Is.EqualTo(new[] { 2, 3 })); + } + + // ----------------------------------------------------------------------- + // Generic EventHandler raise sites — Exception payload + // (ConnectionError, InternalError on MTConnectMqttClient / + // MTConnectMqttExpandedClient — the error-sink events themselves are + // also EventHandler multicast, so a throwing ConnectionError + // subscriber must not starve later ConnectionError subscribers) + // ----------------------------------------------------------------------- + + /// Pins the behavior expressed by the test name: a second ConnectionError subscriber fires even when the first throws, covering the EventHandler{Exception} raise pattern. + [Test] + public void MqttClient_Generic_EventHandlerOfException_FiresAllSubscribersWhenOneThrows() + { + var received = new List(); + var payload = new InvalidOperationException("upstream-error"); + + EventHandler? handler = null; + handler += (_, _) => throw new InvalidOperationException("first ConnectionError subscriber throws"); + handler += (_, ex) => received.Add(ex.Message); + + MulticastIsolation.Raise(handler!, this, payload, null); + + Assert.That(received, Is.EqualTo(new[] { "upstream-error" })); + } + + // ----------------------------------------------------------------------- + // Null-handler guard + // ----------------------------------------------------------------------- + + /// Pins the behavior expressed by the test name: Raise with a null handler is a safe no-op covering the no-subscriber case that every MQTT event can hit at runtime. + [Test] + public void MqttClient_NullHandler_DoesNotThrow() + { + EventHandler? handler = null; + EventHandler? internalError = null; + + Assert.DoesNotThrow(() => MulticastIsolation.Raise(handler, this, "x", internalError)); + } + + /// Pins the behavior expressed by the test name: Raise non-generic with a null handler is a safe no-op. + [Test] + public void MqttClient_NullHandlerNonGeneric_DoesNotThrow() + { + EventHandler? handler = null; + EventHandler? internalError = null; + + Assert.DoesNotThrow(() => MulticastIsolation.Raise(handler, this, EventArgs.Empty, internalError)); + } + } +} From cd46a80f495a3997b82176e11d94988a96d64661 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Otto=20Boly=C3=B3s?= Date: Wed, 3 Jun 2026 12:11:01 +0200 Subject: [PATCH 14/37] test(shdr-tests): pin multicast isolation on SHDR adapter and client events --- .../ShdrMulticastIsolationTests.cs | 162 ++++++++++++++++++ 1 file changed, 162 insertions(+) create mode 100644 tests/MTConnect.NET-SHDR-Tests/ShdrMulticastIsolationTests.cs diff --git a/tests/MTConnect.NET-SHDR-Tests/ShdrMulticastIsolationTests.cs b/tests/MTConnect.NET-SHDR-Tests/ShdrMulticastIsolationTests.cs new file mode 100644 index 00000000..60b2490e --- /dev/null +++ b/tests/MTConnect.NET-SHDR-Tests/ShdrMulticastIsolationTests.cs @@ -0,0 +1,162 @@ +// Copyright (c) 2026 TrakHound Inc., All Rights Reserved. +// TrakHound Inc. licenses this file to you under the MIT license. + +using System; +using System.Collections.Generic; +using MTConnect.Adapters; +using NUnit.Framework; + +namespace MTConnect.Tests.Shdr +{ + /// + /// Pins the multicast-isolation contract for the delegate shapes used by + /// ShdrAdapter and ShdrClient. Both SHDR classes declare + /// their events as or ; + /// after migration all raise sites use + /// passing null as the + /// internalError sink (neither class declares an InternalError event). + /// These tests verify the isolation guarantee holds for every generic type + /// argument surfaced by those events without requiring a live TCP connection. + /// + [TestFixture] + public class ShdrMulticastIsolationTests + { + // ----------------------------------------------------------------------- + // EventHandler raise sites + // (AgentConnected, AgentDisconnected, AgentConnectionError, PingReceived, + // PongSent on ShdrAdapter; Connected, Disconnected, Listening, + // PingReceived, PingSent, PongReceived, ProtocolReceived, CommandReceived + // on ShdrClient) + // ----------------------------------------------------------------------- + + /// Pins the behavior expressed by the test name: a second EventHandler{string} subscriber fires even when the first throws, covering all string-payload SHDR events. + [Test] + public void Shdr_Generic_EventHandlerOfString_FiresAllSubscribersWhenOneThrows() + { + var received = new List(); + + EventHandler? handler = null; + handler += (_, _) => throw new InvalidOperationException("first shdr subscriber throws"); + handler += (_, v) => received.Add(v!); + + MulticastIsolation.Raise(handler!, this, "shdr-payload", null); + + Assert.That(received, Is.EqualTo(new[] { "shdr-payload" })); + } + + /// Pins the behavior expressed by the test name: with null InternalError, a fault from a string-payload SHDR event is silently swallowed and does not escape to the caller. + [Test] + public void Shdr_Generic_EventHandlerOfString_NullInternalErrorSwallowsFault() + { + EventHandler handler = (_, _) => throw new InvalidOperationException("shdr-fault"); + + Assert.DoesNotThrow(() => MulticastIsolation.Raise(handler, this, "x", null)); + } + + /// Pins the behavior expressed by the test name: multiple string-payload subscribers all fire when no subscriber throws (the happy path covers AgentConnected and friends). + [Test] + public void Shdr_Generic_EventHandlerOfString_AllSubscribersFireOnHappyPath() + { + var received = new List(); + + EventHandler? handler = null; + handler += (_, v) => received.Add("sub1:" + v); + handler += (_, v) => received.Add("sub2:" + v); + + MulticastIsolation.Raise(handler!, this, "hello", null); + + Assert.That(received, Is.EqualTo(new[] { "sub1:hello", "sub2:hello" })); + } + + // ----------------------------------------------------------------------- + // EventHandler> raise sites + // (LineSent, DataSent, SendError on ShdrAdapter) + // ----------------------------------------------------------------------- + + /// Pins the behavior expressed by the test name: a second EventHandler{AdapterEventArgs{string}} subscriber fires even when the first throws, covering LineSent, DataSent, and SendError on ShdrAdapter. + [Test] + public void Shdr_Generic_EventHandlerOfAdapterEventArgsString_FiresAllSubscribersWhenOneThrows() + { + var received = new List>(); + var payload = new AdapterEventArgs("client-1", "line"); + + EventHandler>? handler = null; + handler += (_, _) => throw new InvalidOperationException("first LineSent subscriber throws"); + handler += (_, e) => received.Add(e); + + MulticastIsolation.Raise(handler!, this, payload, null); + + Assert.That(received.Count, Is.EqualTo(1)); + Assert.That(received[0].ClientId, Is.EqualTo("client-1")); + Assert.That(received[0].Data, Is.EqualTo("line")); + } + + /// Pins the behavior expressed by the test name: with null InternalError, a fault from an AdapterEventArgs{string} subscriber is swallowed and does not escape. + [Test] + public void Shdr_Generic_EventHandlerOfAdapterEventArgsString_NullInternalErrorSwallowsFault() + { + var payload = new AdapterEventArgs("c1", "data"); + EventHandler> handler = (_, _) => throw new InvalidOperationException("fault"); + + Assert.DoesNotThrow(() => MulticastIsolation.Raise(handler, this, payload, null)); + } + + // ----------------------------------------------------------------------- + // EventHandler> raise sites + // (ConnectionError on ShdrAdapter) + // ----------------------------------------------------------------------- + + /// Pins the behavior expressed by the test name: a second EventHandler{AdapterEventArgs{Exception}} subscriber fires even when the first throws, covering ConnectionError on ShdrAdapter. + [Test] + public void Shdr_Generic_EventHandlerOfAdapterEventArgsException_FiresAllSubscribersWhenOneThrows() + { + var received = new List>(); + var inner = new InvalidOperationException("tcp-error"); + var payload = new AdapterEventArgs("client-2", inner); + + EventHandler>? handler = null; + handler += (_, _) => throw new InvalidOperationException("first ConnectionError subscriber throws"); + handler += (_, e) => received.Add(e); + + MulticastIsolation.Raise(handler!, this, payload, null); + + Assert.That(received.Count, Is.EqualTo(1)); + Assert.That(received[0].ClientId, Is.EqualTo("client-2")); + Assert.That(received[0].Data.Message, Is.EqualTo("tcp-error")); + } + + // ----------------------------------------------------------------------- + // EventHandler raise sites + // (ConnectionError on ShdrClient) + // ----------------------------------------------------------------------- + + /// Pins the behavior expressed by the test name: a second EventHandler{Exception} subscriber fires even when the first throws, covering ConnectionError on ShdrClient. + [Test] + public void Shdr_Generic_EventHandlerOfException_FiresAllSubscribersWhenOneThrows() + { + var received = new List(); + var payload = new InvalidOperationException("connection-refused"); + + EventHandler? handler = null; + handler += (_, _) => throw new InvalidOperationException("first ConnectionError subscriber throws"); + handler += (_, ex) => received.Add(ex.Message); + + MulticastIsolation.Raise(handler!, this, payload, null); + + Assert.That(received, Is.EqualTo(new[] { "connection-refused" })); + } + + // ----------------------------------------------------------------------- + // Null-handler guard + // ----------------------------------------------------------------------- + + /// Pins the behavior expressed by the test name: Raise with a null handler is a safe no-op, covering the no-subscriber case for every SHDR event at runtime. + [Test] + public void Shdr_NullGenericHandler_DoesNotThrow() + { + EventHandler? handler = null; + + Assert.DoesNotThrow(() => MulticastIsolation.Raise(handler, this, "x", null)); + } + } +} From 30fb57870c3505bbcb16426ec40d820d42260f95 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Otto=20Boly=C3=B3s?= Date: Wed, 3 Jun 2026 12:11:12 +0200 Subject: [PATCH 15/37] test(common-tests): pin multicast isolation on agent and broker events --- .../Agents/AgentMulticastIsolationTests.cs | 205 ++++++++++++++++++ 1 file changed, 205 insertions(+) create mode 100644 tests/MTConnect.NET-Common-Tests/Agents/AgentMulticastIsolationTests.cs diff --git a/tests/MTConnect.NET-Common-Tests/Agents/AgentMulticastIsolationTests.cs b/tests/MTConnect.NET-Common-Tests/Agents/AgentMulticastIsolationTests.cs new file mode 100644 index 00000000..493fcec9 --- /dev/null +++ b/tests/MTConnect.NET-Common-Tests/Agents/AgentMulticastIsolationTests.cs @@ -0,0 +1,205 @@ +// Copyright (c) 2026 TrakHound Inc., All Rights Reserved. +// TrakHound Inc. licenses this file to you under the MIT license. + +using System; +using System.Collections.Generic; +using MTConnect.Assets; +using MTConnect.Devices; +using MTConnect.Input; +using MTConnect.Observations; +using NUnit.Framework; + +namespace MTConnect.Tests.Common +{ + /// + /// Pins the multicast-isolation contract for the + /// raise sites on MTConnectAgent (DeviceAdded, ObservationReceived, + /// ObservationAdded, AssetReceived, AssetAdded) and the single + /// raise site on MTConnectAgentBroker + /// (StreamsResponseSent). After migration all these sites use + /// / + /// passing null as the internalError sink (neither agent class + /// declares an InternalError event). The custom-delegate events on both + /// classes are not migratable with the shared helper and are therefore + /// out-of-scope for this test class (surfaced as a blocker). + /// + [TestFixture] + public class AgentMulticastIsolationTests + { + // ----------------------------------------------------------------------- + // EventHandler raise sites + // (DeviceAdded on MTConnectAgent) + // ----------------------------------------------------------------------- + + /// Pins the behavior expressed by the test name: a second EventHandler{IDevice} subscriber fires even when the first throws, covering DeviceAdded on MTConnectAgent. + [Test] + public void Agent_DeviceAdded_FiresAllSubscribersWhenOneThrows() + { + var received = new List(); + var device = new Device { Name = "device-1", Uuid = "uuid-1" }; + + EventHandler? handler = null; + handler += (_, _) => throw new InvalidOperationException("first DeviceAdded subscriber throws"); + handler += (_, d) => received.Add(d.Uuid ?? "null"); + + MulticastIsolation.Raise(handler!, this, (IDevice)device, null); + + Assert.That(received, Is.EqualTo(new[] { "uuid-1" })); + } + + /// Pins the behavior expressed by the test name: with null InternalError, a fault from a DeviceAdded subscriber is swallowed and does not escape to the caller. + [Test] + public void Agent_DeviceAdded_NullInternalErrorSwallowsFault() + { + var device = new Device { Name = "device-1", Uuid = "uuid-1" }; + EventHandler handler = (_, _) => throw new InvalidOperationException("DeviceAdded fault"); + + Assert.DoesNotThrow(() => MulticastIsolation.Raise(handler, this, (IDevice)device, null)); + } + + // ----------------------------------------------------------------------- + // EventHandler raise sites + // (ObservationReceived on MTConnectAgent) + // ----------------------------------------------------------------------- + + /// Pins the behavior expressed by the test name: a second EventHandler{IObservationInput} subscriber fires even when the first throws, covering ObservationReceived on MTConnectAgent. + [Test] + public void Agent_ObservationReceived_FiresAllSubscribersWhenOneThrows() + { + var firedCount = 0; + + EventHandler? handler = null; + handler += (_, _) => throw new InvalidOperationException("first ObservationReceived subscriber throws"); + handler += (_, _) => firedCount++; + + var obs = new ObservationInput(); + MulticastIsolation.Raise(handler!, this, (IObservationInput)obs, null); + + Assert.That(firedCount, Is.EqualTo(1)); + } + + /// Pins the behavior expressed by the test name: with null InternalError, a fault from an ObservationReceived subscriber is swallowed without escaping. + [Test] + public void Agent_ObservationReceived_NullInternalErrorSwallowsFault() + { + var obs = new ObservationInput(); + EventHandler handler = (_, _) => throw new InvalidOperationException("ObservationReceived fault"); + + Assert.DoesNotThrow(() => MulticastIsolation.Raise(handler, this, (IObservationInput)obs, null)); + } + + // ----------------------------------------------------------------------- + // EventHandler raise sites + // (ObservationAdded on MTConnectAgent) + // ----------------------------------------------------------------------- + + /// Pins the behavior expressed by the test name: a second EventHandler{IObservation} subscriber fires even when the first throws, covering ObservationAdded on MTConnectAgent. + [Test] + public void Agent_ObservationAdded_FiresAllSubscribersWhenOneThrows() + { + var firedCount = 0; + + EventHandler? handler = null; + handler += (_, _) => throw new InvalidOperationException("first ObservationAdded subscriber throws"); + handler += (_, _) => firedCount++; + + var obs = new Observation(); + MulticastIsolation.Raise(handler!, this, (IObservation)obs, null); + + Assert.That(firedCount, Is.EqualTo(1)); + } + + /// Pins the behavior expressed by the test name: with null InternalError, a fault from an ObservationAdded subscriber is swallowed without escaping. + [Test] + public void Agent_ObservationAdded_NullInternalErrorSwallowsFault() + { + var obs = new Observation(); + EventHandler handler = (_, _) => throw new InvalidOperationException("ObservationAdded fault"); + + Assert.DoesNotThrow(() => MulticastIsolation.Raise(handler, this, (IObservation)obs, null)); + } + + // ----------------------------------------------------------------------- + // EventHandler raise sites + // (AssetAdded on MTConnectAgent) + // ----------------------------------------------------------------------- + + /// Pins the behavior expressed by the test name: a second EventHandler{IAsset} subscriber fires even when the first throws, covering AssetAdded on MTConnectAgent. + [Test] + public void Agent_AssetAdded_FiresAllSubscribersWhenOneThrows() + { + var firedCount = 0; + + EventHandler? handler = null; + handler += (_, _) => throw new InvalidOperationException("first AssetAdded subscriber throws"); + handler += (_, _) => firedCount++; + + var asset = new Asset { AssetId = "a1", Timestamp = DateTime.UtcNow }; + MulticastIsolation.Raise(handler!, this, (IAsset)asset, null); + + Assert.That(firedCount, Is.EqualTo(1)); + } + + /// Pins the behavior expressed by the test name: with null InternalError, a fault from an AssetAdded subscriber is swallowed without escaping. + [Test] + public void Agent_AssetAdded_NullInternalErrorSwallowsFault() + { + var asset = new Asset { AssetId = "a1", Timestamp = DateTime.UtcNow }; + EventHandler handler = (_, _) => throw new InvalidOperationException("AssetAdded fault"); + + Assert.DoesNotThrow(() => MulticastIsolation.Raise(handler, this, (IAsset)asset, null)); + } + + // ----------------------------------------------------------------------- + // EventHandler (non-generic) raise sites + // (StreamsResponseSent on MTConnectAgentBroker) + // ----------------------------------------------------------------------- + + /// Pins the behavior expressed by the test name: a second non-generic EventHandler subscriber fires even when the first throws, covering StreamsResponseSent on MTConnectAgentBroker. + [Test] + public void AgentBroker_StreamsResponseSent_FiresAllSubscribersWhenOneThrows() + { + var fired = new List(); + + EventHandler? handler = null; + handler += (_, _) => throw new InvalidOperationException("first StreamsResponseSent subscriber throws"); + handler += (_, _) => fired.Add(1); + + MulticastIsolation.Raise(handler!, this, EventArgs.Empty, null); + + Assert.That(fired, Is.EqualTo(new[] { 1 })); + } + + /// Pins the behavior expressed by the test name: with null InternalError, a fault from a StreamsResponseSent subscriber is swallowed without escaping. + [Test] + public void AgentBroker_StreamsResponseSent_NullInternalErrorSwallowsFault() + { + EventHandler handler = (_, _) => throw new InvalidOperationException("StreamsResponseSent fault"); + + Assert.DoesNotThrow(() => MulticastIsolation.Raise(handler, this, EventArgs.Empty, null)); + } + + // ----------------------------------------------------------------------- + // Null-handler guard (both generic and non-generic) + // ----------------------------------------------------------------------- + + /// Pins the behavior expressed by the test name: Raise with a null EventHandler{IDevice} handler is a safe no-op covering the no-subscriber case at runtime. + [Test] + public void Agent_NullGenericHandler_DoesNotThrow() + { + EventHandler? handler = null; + IDevice? device = null; + + Assert.DoesNotThrow(() => MulticastIsolation.Raise(handler, this, device, null)); + } + + /// Pins the behavior expressed by the test name: Raise with a null non-generic EventHandler is a safe no-op covering the no-subscriber case at runtime. + [Test] + public void AgentBroker_NullNonGenericHandler_DoesNotThrow() + { + EventHandler? handler = null; + + Assert.DoesNotThrow(() => MulticastIsolation.Raise(handler, this, EventArgs.Empty, null)); + } + } +} From bd96d5b9e8660d8e0768324f35ffd092051edf5c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Otto=20Boly=C3=B3s?= Date: Wed, 3 Jun 2026 12:11:21 +0200 Subject: [PATCH 16/37] test(http-tests): pin multicast isolation on HTTP server events --- .../HttpServerMulticastIsolationTests.cs | 218 ++++++++++++++++++ 1 file changed, 218 insertions(+) create mode 100644 tests/MTConnect.NET-HTTP-Tests/Servers/HttpServerMulticastIsolationTests.cs diff --git a/tests/MTConnect.NET-HTTP-Tests/Servers/HttpServerMulticastIsolationTests.cs b/tests/MTConnect.NET-HTTP-Tests/Servers/HttpServerMulticastIsolationTests.cs new file mode 100644 index 00000000..44fb1289 --- /dev/null +++ b/tests/MTConnect.NET-HTTP-Tests/Servers/HttpServerMulticastIsolationTests.cs @@ -0,0 +1,218 @@ +// Copyright (c) 2026 TrakHound Inc., All Rights Reserved. +// TrakHound Inc. licenses this file to you under the MIT license. + +using System; +using System.Collections.Generic; +using MTConnect.Http; +using MTConnect.Servers.Http; +using NUnit.Framework; + +namespace MTConnect.Tests.Http.Servers +{ + /// + /// Pins the multicast-isolation contract for the delegate shapes used by + /// MTConnectHttpResponseHandler (ResponseSent, ClientConnected, + /// ClientDisconnected, ClientException) and MTConnectHttpServerStream + /// (StreamStarted, StreamStopped, StreamException, DocumentReceived, + /// HeartbeatReceived). Both classes declare their events as + /// or ; after + /// migration all raise sites use + /// / . + /// + /// + /// MTConnectHttpResponseHandler is internal abstract; these + /// tests verify the isolation guarantee by exercising the helper directly + /// with the same delegate signatures that the handler's raise sites use, + /// without requiring a concrete subclass or a live HTTP connection. + /// + /// + [TestFixture] + public class HttpServerMulticastIsolationTests + { + // ----------------------------------------------------------------------- + // MTConnectHttpResponse raise sites + // (ResponseSent on MTConnectHttpResponseHandler) + // ----------------------------------------------------------------------- + + /// Pins the behavior expressed by the test name: a second EventHandler{MTConnectHttpResponse} subscriber fires even when the first throws, covering ResponseSent on MTConnectHttpResponseHandler. + [Test] + public void HttpResponseHandler_ResponseSent_FiresAllSubscribersWhenOneThrows() + { + var received = new List(); + var response = new MTConnectHttpResponse { ContentType = "application/xml" }; + + EventHandler? handler = null; + handler += (_, _) => throw new InvalidOperationException("first ResponseSent subscriber throws"); + handler += (_, r) => received.Add(r.ContentType ?? "null"); + + MulticastIsolation.Raise(handler!, this, response, null); + + Assert.That(received, Is.EqualTo(new[] { "application/xml" })); + } + + /// Pins the behavior expressed by the test name: with null InternalError, a fault from a ResponseSent subscriber is swallowed without escaping to the caller. + [Test] + public void HttpResponseHandler_ResponseSent_NullInternalErrorSwallowsFault() + { + var response = new MTConnectHttpResponse { ContentType = "application/xml" }; + EventHandler handler = (_, _) => throw new InvalidOperationException("ResponseSent fault"); + + Assert.DoesNotThrow(() => MulticastIsolation.Raise(handler, this, response, null)); + } + + // ----------------------------------------------------------------------- + // IHttpRequest raise sites + // (ClientConnected on MTConnectHttpResponseHandler) + // ----------------------------------------------------------------------- + + /// Pins the behavior expressed by the test name: a second EventHandler{IHttpRequest} subscriber fires even when the first throws, covering ClientConnected on MTConnectHttpResponseHandler. + [Test] + public void HttpResponseHandler_ClientConnected_FiresAllSubscribersWhenOneThrows() + { + var firedCount = 0; + + EventHandler? handler = null; + handler += (_, _) => throw new InvalidOperationException("first ClientConnected subscriber throws"); + handler += (_, _) => firedCount++; + + MulticastIsolation.Raise(handler!, this, (IHttpRequest)null, null); + + Assert.That(firedCount, Is.EqualTo(1)); + } + + // ----------------------------------------------------------------------- + // ClientDisconnected raise sites + // (EventHandler on MTConnectHttpResponseHandler) + // ----------------------------------------------------------------------- + + /// Pins the behavior expressed by the test name: a second EventHandler{string} subscriber fires even when the first throws, covering ClientDisconnected on MTConnectHttpResponseHandler. + [Test] + public void HttpResponseHandler_ClientDisconnected_FiresAllSubscribersWhenOneThrows() + { + var received = new List(); + + EventHandler? handler = null; + handler += (_, _) => throw new InvalidOperationException("first ClientDisconnected subscriber throws"); + handler += (_, endpoint) => received.Add(endpoint ?? "null"); + + MulticastIsolation.Raise(handler!, this, "127.0.0.1:5000", null); + + Assert.That(received, Is.EqualTo(new[] { "127.0.0.1:5000" })); + } + + // ----------------------------------------------------------------------- + // ClientException raise sites + // (EventHandler on MTConnectHttpResponseHandler) + // ----------------------------------------------------------------------- + + /// Pins the behavior expressed by the test name: a second EventHandler{Exception} subscriber fires even when the first throws, covering ClientException on MTConnectHttpResponseHandler. + [Test] + public void HttpResponseHandler_ClientException_FiresAllSubscribersWhenOneThrows() + { + var received = new List(); + var payload = new InvalidOperationException("http-context-error"); + + EventHandler? handler = null; + handler += (_, _) => throw new InvalidOperationException("first ClientException subscriber throws"); + handler += (_, ex) => received.Add(ex.Message); + + MulticastIsolation.Raise(handler!, this, payload, null); + + Assert.That(received, Is.EqualTo(new[] { "http-context-error" })); + } + + // ----------------------------------------------------------------------- + // StreamStarted / StreamStopped raise sites + // (EventHandler on MTConnectHttpServerStream) + // ----------------------------------------------------------------------- + + /// Pins the behavior expressed by the test name: a second EventHandler{string} subscriber fires even when the first throws, covering StreamStarted and StreamStopped on MTConnectHttpServerStream. + [Test] + public void HttpServerStream_StreamStarted_FiresAllSubscribersWhenOneThrows() + { + var received = new List(); + + EventHandler? handler = null; + handler += (_, _) => throw new InvalidOperationException("first StreamStarted subscriber throws"); + handler += (_, id) => received.Add(id ?? "null"); + + MulticastIsolation.Raise(handler!, this, "stream-id-1", null); + + Assert.That(received, Is.EqualTo(new[] { "stream-id-1" })); + } + + /// Pins the behavior expressed by the test name: with null InternalError, a fault from a StreamStopped subscriber is swallowed without escaping. + [Test] + public void HttpServerStream_StreamStopped_NullInternalErrorSwallowsFault() + { + EventHandler handler = (_, _) => throw new InvalidOperationException("StreamStopped fault"); + + Assert.DoesNotThrow(() => MulticastIsolation.Raise(handler, this, "stream-id-1", null)); + } + + // ----------------------------------------------------------------------- + // StreamException raise sites + // (EventHandler on MTConnectHttpServerStream — serves as its own error sink) + // ----------------------------------------------------------------------- + + /// Pins the behavior expressed by the test name: a second EventHandler{Exception} subscriber fires even when the first throws, covering StreamException on MTConnectHttpServerStream. + [Test] + public void HttpServerStream_StreamException_FiresAllSubscribersWhenOneThrows() + { + var received = new List(); + var payload = new InvalidOperationException("stream-broke"); + + EventHandler? handler = null; + handler += (_, _) => throw new InvalidOperationException("first StreamException subscriber throws"); + handler += (_, ex) => received.Add(ex.Message); + + MulticastIsolation.Raise(handler!, this, payload, null); + + Assert.That(received, Is.EqualTo(new[] { "stream-broke" })); + } + + // ----------------------------------------------------------------------- + // DocumentReceived / HeartbeatReceived raise sites + // (EventHandler on MTConnectHttpServerStream) + // ----------------------------------------------------------------------- + + /// Pins the behavior expressed by the test name: a second EventHandler{MTConnectHttpStreamArgs} subscriber fires even when the first throws, covering DocumentReceived and HeartbeatReceived on MTConnectHttpServerStream. + [Test] + public void HttpServerStream_DocumentReceived_FiresAllSubscribersWhenOneThrows() + { + var received = new List(); + var args = new MTConnectHttpStreamArgs("stream-id-2", System.IO.Stream.Null, 42.5); + + EventHandler? handler = null; + handler += (_, _) => throw new InvalidOperationException("first DocumentReceived subscriber throws"); + handler += (_, a) => received.Add(a.StreamId); + + MulticastIsolation.Raise(handler!, this, args, null); + + Assert.That(received, Is.EqualTo(new[] { "stream-id-2" })); + } + + /// Pins the behavior expressed by the test name: with null InternalError, a fault from a HeartbeatReceived subscriber is swallowed without escaping. + [Test] + public void HttpServerStream_HeartbeatReceived_NullInternalErrorSwallowsFault() + { + var args = new MTConnectHttpStreamArgs("stream-id-2", System.IO.Stream.Null, 42.5); + EventHandler handler = (_, _) => throw new InvalidOperationException("HeartbeatReceived fault"); + + Assert.DoesNotThrow(() => MulticastIsolation.Raise(handler, this, args, null)); + } + + // ----------------------------------------------------------------------- + // Null-handler guard + // ----------------------------------------------------------------------- + + /// Pins the behavior expressed by the test name: Raise with a null EventHandler{string} handler is a safe no-op covering the no-subscriber case for stream events at runtime. + [Test] + public void HttpServerStream_NullGenericHandler_DoesNotThrow() + { + EventHandler? handler = null; + + Assert.DoesNotThrow(() => MulticastIsolation.Raise(handler, this, "x", null)); + } + } +} From 4385fa7695e160a58e2080833ae88e36393776da Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Otto=20Boly=C3=B3s?= Date: Wed, 3 Jun 2026 12:14:11 +0200 Subject: [PATCH 17/37] style(shdr-tests,common-tests,http-tests): fix nullable annotations in multicast test files --- .../Agents/AgentMulticastIsolationTests.cs | 4 +- .../HttpServerMulticastIsolationTests.cs | 4 +- .../ShdrMulticastIsolationTests.cs | 38 +++++++++---------- 3 files changed, 23 insertions(+), 23 deletions(-) diff --git a/tests/MTConnect.NET-Common-Tests/Agents/AgentMulticastIsolationTests.cs b/tests/MTConnect.NET-Common-Tests/Agents/AgentMulticastIsolationTests.cs index 493fcec9..904b9856 100644 --- a/tests/MTConnect.NET-Common-Tests/Agents/AgentMulticastIsolationTests.cs +++ b/tests/MTConnect.NET-Common-Tests/Agents/AgentMulticastIsolationTests.cs @@ -188,9 +188,9 @@ public void AgentBroker_StreamsResponseSent_NullInternalErrorSwallowsFault() public void Agent_NullGenericHandler_DoesNotThrow() { EventHandler? handler = null; - IDevice? device = null; + var device = new Device { Name = "noop-device", Uuid = "noop-uuid" }; - Assert.DoesNotThrow(() => MulticastIsolation.Raise(handler, this, device, null)); + Assert.DoesNotThrow(() => MulticastIsolation.Raise(handler, this, (IDevice)device, null)); } /// Pins the behavior expressed by the test name: Raise with a null non-generic EventHandler is a safe no-op covering the no-subscriber case at runtime. diff --git a/tests/MTConnect.NET-HTTP-Tests/Servers/HttpServerMulticastIsolationTests.cs b/tests/MTConnect.NET-HTTP-Tests/Servers/HttpServerMulticastIsolationTests.cs index 44fb1289..8e7b9f32 100644 --- a/tests/MTConnect.NET-HTTP-Tests/Servers/HttpServerMulticastIsolationTests.cs +++ b/tests/MTConnect.NET-HTTP-Tests/Servers/HttpServerMulticastIsolationTests.cs @@ -71,11 +71,11 @@ public void HttpResponseHandler_ClientConnected_FiresAllSubscribersWhenOneThrows { var firedCount = 0; - EventHandler? handler = null; + EventHandler? handler = null; handler += (_, _) => throw new InvalidOperationException("first ClientConnected subscriber throws"); handler += (_, _) => firedCount++; - MulticastIsolation.Raise(handler!, this, (IHttpRequest)null, null); + MulticastIsolation.Raise(handler!, this, (IHttpRequest?)null, null); Assert.That(firedCount, Is.EqualTo(1)); } diff --git a/tests/MTConnect.NET-SHDR-Tests/ShdrMulticastIsolationTests.cs b/tests/MTConnect.NET-SHDR-Tests/ShdrMulticastIsolationTests.cs index 60b2490e..005906ab 100644 --- a/tests/MTConnect.NET-SHDR-Tests/ShdrMulticastIsolationTests.cs +++ b/tests/MTConnect.NET-SHDR-Tests/ShdrMulticastIsolationTests.cs @@ -11,12 +11,12 @@ namespace MTConnect.Tests.Shdr /// /// Pins the multicast-isolation contract for the delegate shapes used by /// ShdrAdapter and ShdrClient. Both SHDR classes declare - /// their events as or ; - /// after migration all raise sites use - /// passing null as the - /// internalError sink (neither class declares an InternalError event). - /// These tests verify the isolation guarantee holds for every generic type - /// argument surfaced by those events without requiring a live TCP connection. + /// their events as ; after migration all raise + /// sites use passing null + /// as the internalError sink (neither class declares an InternalError + /// event). These tests verify the isolation guarantee holds for every generic + /// type argument surfaced by those events without requiring a live TCP + /// connection. /// [TestFixture] public class ShdrMulticastIsolationTests @@ -35,11 +35,11 @@ public void Shdr_Generic_EventHandlerOfString_FiresAllSubscribersWhenOneThrows() { var received = new List(); - EventHandler? handler = null; + EventHandler handler = null; handler += (_, _) => throw new InvalidOperationException("first shdr subscriber throws"); - handler += (_, v) => received.Add(v!); + handler += (_, v) => received.Add(v); - MulticastIsolation.Raise(handler!, this, "shdr-payload", null); + MulticastIsolation.Raise(handler, this, "shdr-payload", null); Assert.That(received, Is.EqualTo(new[] { "shdr-payload" })); } @@ -53,17 +53,17 @@ public void Shdr_Generic_EventHandlerOfString_NullInternalErrorSwallowsFault() Assert.DoesNotThrow(() => MulticastIsolation.Raise(handler, this, "x", null)); } - /// Pins the behavior expressed by the test name: multiple string-payload subscribers all fire when no subscriber throws (the happy path covers AgentConnected and friends). + /// Pins the behavior expressed by the test name: multiple string-payload subscribers all fire when no subscriber throws, covering the happy path for AgentConnected and related events. [Test] public void Shdr_Generic_EventHandlerOfString_AllSubscribersFireOnHappyPath() { var received = new List(); - EventHandler? handler = null; + EventHandler handler = null; handler += (_, v) => received.Add("sub1:" + v); handler += (_, v) => received.Add("sub2:" + v); - MulticastIsolation.Raise(handler!, this, "hello", null); + MulticastIsolation.Raise(handler, this, "hello", null); Assert.That(received, Is.EqualTo(new[] { "sub1:hello", "sub2:hello" })); } @@ -80,11 +80,11 @@ public void Shdr_Generic_EventHandlerOfAdapterEventArgsString_FiresAllSubscriber var received = new List>(); var payload = new AdapterEventArgs("client-1", "line"); - EventHandler>? handler = null; + EventHandler> handler = null; handler += (_, _) => throw new InvalidOperationException("first LineSent subscriber throws"); handler += (_, e) => received.Add(e); - MulticastIsolation.Raise(handler!, this, payload, null); + MulticastIsolation.Raise(handler, this, payload, null); Assert.That(received.Count, Is.EqualTo(1)); Assert.That(received[0].ClientId, Is.EqualTo("client-1")); @@ -114,11 +114,11 @@ public void Shdr_Generic_EventHandlerOfAdapterEventArgsException_FiresAllSubscri var inner = new InvalidOperationException("tcp-error"); var payload = new AdapterEventArgs("client-2", inner); - EventHandler>? handler = null; + EventHandler> handler = null; handler += (_, _) => throw new InvalidOperationException("first ConnectionError subscriber throws"); handler += (_, e) => received.Add(e); - MulticastIsolation.Raise(handler!, this, payload, null); + MulticastIsolation.Raise(handler, this, payload, null); Assert.That(received.Count, Is.EqualTo(1)); Assert.That(received[0].ClientId, Is.EqualTo("client-2")); @@ -137,11 +137,11 @@ public void Shdr_Generic_EventHandlerOfException_FiresAllSubscribersWhenOneThrow var received = new List(); var payload = new InvalidOperationException("connection-refused"); - EventHandler? handler = null; + EventHandler handler = null; handler += (_, _) => throw new InvalidOperationException("first ConnectionError subscriber throws"); handler += (_, ex) => received.Add(ex.Message); - MulticastIsolation.Raise(handler!, this, payload, null); + MulticastIsolation.Raise(handler, this, payload, null); Assert.That(received, Is.EqualTo(new[] { "connection-refused" })); } @@ -154,7 +154,7 @@ public void Shdr_Generic_EventHandlerOfException_FiresAllSubscribersWhenOneThrow [Test] public void Shdr_NullGenericHandler_DoesNotThrow() { - EventHandler? handler = null; + EventHandler handler = null; Assert.DoesNotThrow(() => MulticastIsolation.Raise(handler, this, "x", null)); } From 63c474983d0a7520134fa77b9d635459164814c4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Otto=20Boly=C3=B3s?= Date: Wed, 3 Jun 2026 12:22:02 +0200 Subject: [PATCH 18/37] fix(mqtt): migrate MQTT client events to shared multicast helper --- .../Clients/MTConnectMqttClient.cs | 32 ++++++++--------- .../Clients/MTConnectMqttExpandedClient.cs | 36 +++++++------------ 2 files changed, 28 insertions(+), 40 deletions(-) diff --git a/libraries/MTConnect.NET-MQTT/Clients/MTConnectMqttClient.cs b/libraries/MTConnect.NET-MQTT/Clients/MTConnectMqttClient.cs index 61b2ea16..75eee186 100644 --- a/libraries/MTConnect.NET-MQTT/Clients/MTConnectMqttClient.cs +++ b/libraries/MTConnect.NET-MQTT/Clients/MTConnectMqttClient.cs @@ -228,7 +228,7 @@ public void Start() { _stop = new CancellationTokenSource(); - ClientStarting?.Invoke(this, new EventArgs()); + MulticastIsolation.Raise(ClientStarting, this, EventArgs.Empty, InternalError); _ = Task.Run(Worker, _stop.Token); } @@ -239,7 +239,7 @@ public void Start() /// public void Stop() { - ClientStopping?.Invoke(this, new EventArgs()); + MulticastIsolation.Raise(ClientStopping, this, EventArgs.Empty, InternalError); if (_stop != null) _stop.Cancel(); } @@ -353,7 +353,7 @@ private async Task Worker() await StartAllDevicesProtocol(); } - ClientStarted?.Invoke(this, new EventArgs()); + MulticastIsolation.Raise(ClientStarted, this, EventArgs.Empty, InternalError); while (_mqttClient.IsConnected && !_stop.IsCancellationRequested) { @@ -362,7 +362,7 @@ private async Task Worker() } catch (Exception ex) { - if (ConnectionError != null) ConnectionError.Invoke(this, ex); + MulticastIsolation.Raise(ConnectionError, this, ex, InternalError); } await Task.Delay(_configuration.RetryInterval, _stop.Token); @@ -370,7 +370,7 @@ private async Task Worker() catch (TaskCanceledException) { } catch (Exception ex) { - InternalError?.Invoke(this, ex); + MulticastIsolation.Raise(InternalError, this, ex, InternalError); } } while (!_stop.Token.IsCancellationRequested); @@ -384,7 +384,7 @@ private async Task Worker() catch { } - ClientStopped?.Invoke(this, new EventArgs()); + MulticastIsolation.Raise(ClientStopped, this, EventArgs.Empty, InternalError); } @@ -511,11 +511,11 @@ private void ProcessProbeMessage(MqttApplicationMessage message) _devices.Add(outputDevice.Uuid, outputDevice); } - DeviceReceived?.Invoke(this, outputDevice); + MulticastIsolation.Raise(DeviceReceived, this, outputDevice, InternalError); } } - ProbeReceived?.Invoke(this, responseDocument); + MulticastIsolation.Raise(ProbeReceived, this, responseDocument, InternalError); } } } @@ -567,7 +567,7 @@ private void ProcessAssetMessage(MqttApplicationMessage message) private void ProcessCurrentDocument(IStreamsResponseDocument document) { _lastResponse = UnixDateTime.Now; - ResponseReceived?.Invoke(this, new EventArgs()); + MulticastIsolation.Raise(ResponseReceived, this, EventArgs.Empty, InternalError); if (document != null) { @@ -585,7 +585,7 @@ private void ProcessCurrentDocument(IStreamsResponseDocument document) response.Streams = deviceStreams; - CurrentReceived?.Invoke(this, response); + MulticastIsolation.Raise(CurrentReceived, this, response, InternalError); // Process Device Streams @@ -610,7 +610,7 @@ private void ProcessCurrentDocument(IStreamsResponseDocument document) { if (observation.Sequence > lastSequence) { - ObservationReceived?.Invoke(this, observation); + MulticastIsolation.Raise(ObservationReceived, this, observation, InternalError); } } @@ -637,7 +637,7 @@ private void ProcessCurrentDocument(IStreamsResponseDocument document) private void ProcessSampleDocument(IStreamsResponseDocument document) { _lastResponse = UnixDateTime.Now; - ResponseReceived?.Invoke(this, new EventArgs()); + MulticastIsolation.Raise(ResponseReceived, this, EventArgs.Empty, InternalError); if (document != null) { @@ -653,7 +653,7 @@ private void ProcessSampleDocument(IStreamsResponseDocument document) response.Streams = deviceStreams; - SampleReceived?.Invoke(this, response); + MulticastIsolation.Raise(SampleReceived, this, response, InternalError); // Process Device Streams @@ -676,7 +676,7 @@ private void ProcessSampleDocument(IStreamsResponseDocument document) { if (observation.Sequence > lastSequence && observation.Sequence > lastCurrentSequence) { - ObservationReceived?.Invoke(this, observation); + MulticastIsolation.Raise(ObservationReceived, this, observation, InternalError); } } @@ -699,11 +699,11 @@ private void ProcessAssetsDocument(IAssetsResponseDocument document) { if (document != null && !document.Assets.IsNullOrEmpty()) { - AssetsReceived?.Invoke(this, document); + MulticastIsolation.Raise(AssetsReceived, this, document, InternalError); foreach (var asset in document.Assets) { - AssetReceived?.Invoke(this, asset); + MulticastIsolation.Raise(AssetReceived, this, asset, InternalError); } } } diff --git a/libraries/MTConnect.NET-MQTT/Clients/MTConnectMqttExpandedClient.cs b/libraries/MTConnect.NET-MQTT/Clients/MTConnectMqttExpandedClient.cs index 14451617..3946443c 100644 --- a/libraries/MTConnect.NET-MQTT/Clients/MTConnectMqttExpandedClient.cs +++ b/libraries/MTConnect.NET-MQTT/Clients/MTConnectMqttExpandedClient.cs @@ -248,7 +248,7 @@ public void Start() { _stop = new CancellationTokenSource(); - ClientStarting?.Invoke(this, new EventArgs()); + MulticastIsolation.Raise(ClientStarting, this, EventArgs.Empty, InternalError); _ = Task.Run(Worker, _stop.Token); } @@ -256,7 +256,7 @@ public void Start() /// Signals the worker to stop and disconnect from the broker. is raised once the session has closed. public void Stop() { - ClientStopping?.Invoke(this, new EventArgs()); + MulticastIsolation.Raise(ClientStopping, this, EventArgs.Empty, InternalError); if (_stop != null) _stop.Cancel(); } @@ -341,7 +341,7 @@ private async Task Worker() StartAllDevicesProtocol().Wait(); } - ClientStarted?.Invoke(this, new EventArgs()); + MulticastIsolation.Raise(ClientStarted, this, EventArgs.Empty, InternalError); while (_mqttClient.IsConnected && !_stop.IsCancellationRequested) { @@ -350,7 +350,7 @@ private async Task Worker() } catch (Exception ex) { - if (ConnectionError != null) ConnectionError.Invoke(this, ex); + MulticastIsolation.Raise(ConnectionError, this, ex, InternalError); } await Task.Delay(ReconnectionInterval, _stop.Token); @@ -358,7 +358,7 @@ private async Task Worker() catch (TaskCanceledException) { } catch (Exception ex) { - InternalError?.Invoke(this, ex); + MulticastIsolation.Raise(InternalError, this, ex, InternalError); } } while (!_stop.Token.IsCancellationRequested); @@ -372,7 +372,7 @@ private async Task Worker() catch { } - ClientStopped?.Invoke(this, new EventArgs()); + MulticastIsolation.Raise(ClientStopped, this, EventArgs.Empty, InternalError); } @@ -532,10 +532,7 @@ private async void ProcessObservation(MqttApplicationMessage message) if (observation.InstanceId == agentInstanceId) { - if (ObservationReceived != null) - { - ObservationReceived.Invoke(deviceUuid, observation); - } + MulticastIsolation.Raise(ObservationReceived, deviceUuid, observation, InternalError); } else { @@ -587,10 +584,7 @@ private void ProcessObservations(MqttApplicationMessage message) observation.AddValue(ValueKeys.Result, jsonObservation.Result); } - if (ObservationReceived != null) - { - ObservationReceived.Invoke(deviceUuid, observation); - } + MulticastIsolation.Raise(ObservationReceived, deviceUuid, observation, InternalError); } } } @@ -662,7 +656,7 @@ private async Task ProcessAgent(MqttApplicationMessage message, Func Date: Wed, 3 Jun 2026 12:26:06 +0200 Subject: [PATCH 19/37] fix(shdr): migrate SHDR adapter and client events to shared multicast helper --- .../Adapters/ShdrAdapter.cs | 18 ++++----- .../MTConnect.NET-SHDR/Shdr/ShdrClient.cs | 40 +++++++++---------- 2 files changed, 29 insertions(+), 29 deletions(-) diff --git a/libraries/MTConnect.NET-SHDR/Adapters/ShdrAdapter.cs b/libraries/MTConnect.NET-SHDR/Adapters/ShdrAdapter.cs index 314fc077..9906cf00 100644 --- a/libraries/MTConnect.NET-SHDR/Adapters/ShdrAdapter.cs +++ b/libraries/MTConnect.NET-SHDR/Adapters/ShdrAdapter.cs @@ -344,7 +344,7 @@ public void SetUnavailable(long timestamp = 0) private void ClientConnected(string clientId, TcpClient client) { AddAgentClient(clientId, client); - AgentConnected?.Invoke(this, clientId); + MulticastIsolation.Raise(AgentConnected, this, clientId, null); SendLast(UnixDateTime.Now); } @@ -352,22 +352,22 @@ private void ClientConnected(string clientId, TcpClient client) private void ClientDisconnected(string clientId) { RemoveAgentClient(clientId); - AgentDisconnected?.Invoke(this, clientId); + MulticastIsolation.Raise(AgentDisconnected, this, clientId, null); } private void ClientPingReceived(string clientId) { - PingReceived?.Invoke(this, clientId); + MulticastIsolation.Raise(PingReceived, this, clientId, null); } private void ClientPongSent(string clientId) { - PongSent?.Invoke(this, clientId); + MulticastIsolation.Raise(PongSent, this, clientId, null); } private void ClientConnectionError(string clientId, Exception exception) { - ConnectionError?.Invoke(this, new AdapterEventArgs(clientId, exception)); + MulticastIsolation.Raise(ConnectionError, this, new AdapterEventArgs(clientId, exception), null); } #endregion @@ -601,11 +601,11 @@ private bool WriteLineToClient(AgentClient client, string line) // Write the line (in bytes) to the Stream stream.Write(bytes, 0, bytes.Length); - LineSent?.Invoke(this, new AdapterEventArgs(client.Id, singleLine)); + MulticastIsolation.Raise(LineSent, this, new AdapterEventArgs(client.Id, singleLine), null); } catch (Exception ex) { - SendError?.Invoke(this, new AdapterEventArgs(client.Id, ex.Message)); + MulticastIsolation.Raise(SendError, this, new AdapterEventArgs(client.Id, ex.Message), null); return false; } } @@ -634,13 +634,13 @@ private async Task WriteLineToClientAsync(AgentClient client, string line) // Write the line (in bytes) to the Stream await stream.WriteAsync(bytes, 0, bytes.Length); - LineSent?.Invoke(this, new AdapterEventArgs(client.Id, line)); + MulticastIsolation.Raise(LineSent, this, new AdapterEventArgs(client.Id, line), null); return true; } catch (Exception ex) { - SendError?.Invoke(this, new AdapterEventArgs(client.Id, ex.Message)); + MulticastIsolation.Raise(SendError, this, new AdapterEventArgs(client.Id, ex.Message), null); } } diff --git a/libraries/MTConnect.NET-SHDR/Shdr/ShdrClient.cs b/libraries/MTConnect.NET-SHDR/Shdr/ShdrClient.cs index 88d319ca..ccf6f81a 100644 --- a/libraries/MTConnect.NET-SHDR/Shdr/ShdrClient.cs +++ b/libraries/MTConnect.NET-SHDR/Shdr/ShdrClient.cs @@ -258,7 +258,7 @@ private async Task ListenForAdapter(CancellationToken cancel) _client.ReceiveTimeout = ConnectionTimeout; _client.SendTimeout = ConnectionTimeout; - Connected?.Invoke(this, $"Connected to Adapter at {Hostname} on Port {Port}"); + MulticastIsolation.Raise(Connected, this, $"Connected to Adapter at {Hostname} on Port {Port}", null); connected = true; OnConnect(); @@ -269,7 +269,7 @@ private async Task ListenForAdapter(CancellationToken cancel) // Send Initial PING Request var messageBytes = Encoding.ASCII.GetBytes(PingMessage); stream.Write(messageBytes, 0, messageBytes.Length); - PingSent?.Invoke(this, $"Initial PING sent to : {Hostname} on Port {Port}"); + MulticastIsolation.Raise(PingSent, this, $"Initial PING sent to : {Hostname} on Port {Port}", null); // Read the Initial PONG Response bufferIndex = stream.Read(buffer, 0, buffer.Length); @@ -297,7 +297,7 @@ private async Task ListenForAdapter(CancellationToken cancel) { messageBytes = Encoding.ASCII.GetBytes(PingMessage); stream.Write(messageBytes, 0, messageBytes.Length); - PingSent?.Invoke(this, $"PING sent to : {Hostname} on Port {Port}"); + MulticastIsolation.Raise(PingSent, this, $"PING sent to : {Hostname} on Port {Port}", null); _lastHeartbeat = now; } @@ -318,7 +318,7 @@ private async Task ListenForAdapter(CancellationToken cancel) catch (TaskCanceledException) { } catch (Exception ex) { - ConnectionError?.Invoke(this, ex); + MulticastIsolation.Raise(ConnectionError, this, ex, null); } finally { @@ -329,7 +329,7 @@ private async Task ListenForAdapter(CancellationToken cancel) if (connected) { - Disconnected?.Invoke(this, $"Disconnected from {Hostname} on Port {Port}"); + MulticastIsolation.Raise(Disconnected, this, $"Disconnected from {Hostname} on Port {Port}", null); OnDisconnect(); } @@ -341,13 +341,13 @@ private async Task ListenForAdapter(CancellationToken cancel) // Wait for the ReconnectInterval (in milliseconds) until continuing while loop await Task.Delay(reconnectInterval, cancel); - Listening?.Invoke(this, $"Listening for connection from {Hostname} on Port {Port}"); + MulticastIsolation.Raise(Listening, this, $"Listening for connection from {Hostname} on Port {Port}", null); } } catch (TaskCanceledException) { } catch (Exception ex) { - ConnectionError?.Invoke(this, ex); + MulticastIsolation.Raise(ConnectionError, this, ex, null); if (_client != null) { @@ -358,7 +358,7 @@ private async Task ListenForAdapter(CancellationToken cancel) if (connected) { - Disconnected?.Invoke(this, $"Disconnected from {Hostname} on Port {Port}"); + MulticastIsolation.Raise(Disconnected, this, $"Disconnected from {Hostname} on Port {Port}", null); OnDisconnect(); } @@ -405,7 +405,7 @@ private bool ProcessResponse(ref char[] chars, int length) { _heartbeat = GetPongHeartbeat(line); - PongReceived?.Invoke(this, $"PONG Received from : {Hostname} on Port {Port} : Heartbeat = {_heartbeat}ms"); + MulticastIsolation.Raise(PongReceived, this, $"PONG Received from : {Hostname} on Port {Port} : Heartbeat = {_heartbeat}ms", null); } else { @@ -429,7 +429,7 @@ private bool ProcessResponse(ref char[] chars, int length) multilineAsset = true; // Raise ProtocolReceived Event passing the Line that was read as a parameter - ProtocolReceived?.Invoke(this, line); + MulticastIsolation.Raise(ProtocolReceived, this, line, null); found = true; } @@ -449,7 +449,7 @@ private bool ProcessResponse(ref char[] chars, int length) multilineAsset = false; // Raise ProtocolReceived Event passing the Line that was read as a parameter - ProtocolReceived?.Invoke(this, line); + MulticastIsolation.Raise(ProtocolReceived, this, line, null); found = true; } @@ -458,7 +458,7 @@ private bool ProcessResponse(ref char[] chars, int length) multilineContent.Append(line); // Raise ProtocolReceived Event passing the Line that was read as a parameter - ProtocolReceived?.Invoke(this, line); + MulticastIsolation.Raise(ProtocolReceived, this, line, null); found = true; } @@ -485,7 +485,7 @@ private bool ProcessResponse(ref char[] chars, int length) } // Raise ProtocolReceived Event passing the Line that was read as a parameter - ProtocolReceived?.Invoke(this, line); + MulticastIsolation.Raise(ProtocolReceived, this, line, null); found = true; } @@ -499,7 +499,7 @@ private bool ProcessResponse(ref char[] chars, int length) } // Raise ProtocolReceived Event passing the Line that was read as a parameter - ProtocolReceived?.Invoke(this, line); + MulticastIsolation.Raise(ProtocolReceived, this, line, null); found = true; } @@ -513,7 +513,7 @@ private bool ProcessResponse(ref char[] chars, int length) } // Raise ProtocolReceived Event passing the Line that was read as a parameter - ProtocolReceived?.Invoke(this, line); + MulticastIsolation.Raise(ProtocolReceived, this, line, null); found = true; } @@ -526,7 +526,7 @@ private bool ProcessResponse(ref char[] chars, int length) multilineDevice = true; // Raise ProtocolReceived Event passing the Line that was read as a parameter - ProtocolReceived?.Invoke(this, line); + MulticastIsolation.Raise(ProtocolReceived, this, line, null); found = true; } @@ -542,7 +542,7 @@ private bool ProcessResponse(ref char[] chars, int length) multilineDevice = false; // Raise ProtocolReceived Event passing the Line that was read as a parameter - ProtocolReceived?.Invoke(this, line); + MulticastIsolation.Raise(ProtocolReceived, this, line, null); found = true; } @@ -551,7 +551,7 @@ private bool ProcessResponse(ref char[] chars, int length) multilineContent.Append(line); // Raise ProtocolReceived Event passing the Line that was read as a parameter - ProtocolReceived?.Invoke(this, line); + MulticastIsolation.Raise(ProtocolReceived, this, line, null); found = true; } @@ -564,7 +564,7 @@ private bool ProcessResponse(ref char[] chars, int length) } // Raise ProtocolReceived Event passing the Line that was read as a parameter - ProtocolReceived?.Invoke(this, line); + MulticastIsolation.Raise(ProtocolReceived, this, line, null); found = true; } @@ -573,7 +573,7 @@ private bool ProcessResponse(ref char[] chars, int length) ProcessProtocol(line); // Raise ProtocolReceived Event passing the Line that was read as a parameter - ProtocolReceived?.Invoke(this, line); + MulticastIsolation.Raise(ProtocolReceived, this, line, null); found = true; } From 1aa184d5db9a96d78d69b225514b76cf4badad32 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Otto=20Boly=C3=B3s?= Date: Wed, 3 Jun 2026 12:28:21 +0200 Subject: [PATCH 20/37] fix(common): migrate agent and broker EventHandler events to shared multicast helper --- .../Agents/MTConnectAgent.cs | 17 ++++++------- .../Agents/MTConnectAgentBroker.cs | 24 +++++++++---------- 2 files changed, 19 insertions(+), 22 deletions(-) diff --git a/libraries/MTConnect.NET-Common/Agents/MTConnectAgent.cs b/libraries/MTConnect.NET-Common/Agents/MTConnectAgent.cs index c61cadee..2b3c0f0f 100644 --- a/libraries/MTConnect.NET-Common/Agents/MTConnectAgent.cs +++ b/libraries/MTConnect.NET-Common/Agents/MTConnectAgent.cs @@ -1522,7 +1522,7 @@ private bool AddDeviceAddedObservation(IDevice device, long timestamp = 0) new ObservationValue(ValueKeys.Result, device.Uuid) }); - ObservationAdded?.Invoke(this, observation); + MulticastIsolation.Raise(ObservationAdded, this, observation, null); return true; } @@ -1553,7 +1553,7 @@ private bool AddDeviceChangedObservation(IDevice device, long timestamp = 0) new ObservationValue(ValueKeys.Result, device.Uuid) }); - ObservationAdded?.Invoke(this, observation); + MulticastIsolation.Raise(ObservationAdded, this, observation, null); return true; } @@ -1583,7 +1583,7 @@ private bool AddDeviceRemovedObservation(IDevice device, long timestamp = 0) new ObservationValue(ValueKeys.Result, device.Uuid) }); - ObservationAdded?.Invoke(this, observation); + MulticastIsolation.Raise(ObservationAdded, this, observation, null); return true; } @@ -1717,7 +1717,7 @@ public IDevice AddDevice(IDevice device, bool initializeDataItems = true) _updateInformation = true; } - DeviceAdded?.Invoke(this, obj); + MulticastIsolation.Raise(DeviceAdded, this, obj, null); return obj; } @@ -2124,7 +2124,7 @@ public bool AddObservation(string deviceKey, IObservationInput observationInput, { if (observationInput != null) { - ObservationReceived?.Invoke(this, observationInput); + MulticastIsolation.Raise(ObservationReceived, this, observationInput, null); IObservationInput input = new ObservationInput(); input.DeviceKey = deviceKey; @@ -2333,10 +2333,7 @@ protected virtual ulong OnAddObservation(string deviceUuid, IDataItem dataItem, /// public void OnObservationAdded(IObservation observation) { - if (ObservationAdded != null) - { - ObservationAdded?.Invoke(this, observation); - } + MulticastIsolation.Raise(ObservationAdded, this, observation, null); } /// @@ -2456,7 +2453,7 @@ public bool AddAsset(string deviceKey, IAsset asset, bool? ignoreTimestamp = nul if (InvalidAssetAdded != null) InvalidAssetAdded.Invoke(asset, validationResults); } - AssetAdded?.Invoke(this, asset); + MulticastIsolation.Raise(AssetAdded, this, asset, null); return true; } } diff --git a/libraries/MTConnect.NET-Common/Agents/MTConnectAgentBroker.cs b/libraries/MTConnect.NET-Common/Agents/MTConnectAgentBroker.cs index f1662b9c..255c6395 100644 --- a/libraries/MTConnect.NET-Common/Agents/MTConnectAgentBroker.cs +++ b/libraries/MTConnect.NET-Common/Agents/MTConnectAgentBroker.cs @@ -773,7 +773,7 @@ public IStreamsResponseOutputDocument GetDeviceStreamsResponseDocument(uint coun var document = CreateDeviceStreamsDocument(devices, ref results, mtconnectVersion); if (document != null) { - StreamsResponseSent?.Invoke(this, new EventArgs()); + MulticastIsolation.Raise(StreamsResponseSent, this, EventArgs.Empty, null); return document; } } @@ -809,7 +809,7 @@ public IStreamsResponseOutputDocument GetDeviceStreamsResponseDocument(ulong at, var document = CreateDeviceStreamsDocument(devices, ref results, mtconnectVersion); if (document != null) { - StreamsResponseSent?.Invoke(this, new EventArgs()); + MulticastIsolation.Raise(StreamsResponseSent, this, EventArgs.Empty, null); return document; } } @@ -850,7 +850,7 @@ public IStreamsResponseOutputDocument GetDeviceStreamsResponseDocument(IEnumerab var document = CreateDeviceStreamsDocument(devices, ref results, mtconnectVersion); if (document != null) { - StreamsResponseSent?.Invoke(this, new EventArgs()); + MulticastIsolation.Raise(StreamsResponseSent, this, EventArgs.Empty, null); return document; } } @@ -892,7 +892,7 @@ public IStreamsResponseOutputDocument GetDeviceStreamsResponseDocument(IEnumerab var document = CreateDeviceStreamsDocument(devices, ref results, mtconnectVersion); if (document != null) { - StreamsResponseSent?.Invoke(this, new EventArgs()); + MulticastIsolation.Raise(StreamsResponseSent, this, EventArgs.Empty, null); return document; } } @@ -929,7 +929,7 @@ public IStreamsResponseOutputDocument GetDeviceStreamsResponseDocument(ulong fro var document = CreateDeviceStreamsDocument(devices, ref results, mtconnectVersion); if (document != null) { - StreamsResponseSent?.Invoke(this, new EventArgs()); + MulticastIsolation.Raise(StreamsResponseSent, this, EventArgs.Empty, null); return document; } } @@ -967,7 +967,7 @@ public IStreamsResponseOutputDocument GetDeviceStreamsResponseDocument(IEnumerab var document = CreateDeviceStreamsDocument(devices, ref results, mtconnectVersion); if (document != null) { - StreamsResponseSent?.Invoke(this, new EventArgs()); + MulticastIsolation.Raise(StreamsResponseSent, this, EventArgs.Empty, null); return document; } } @@ -1003,7 +1003,7 @@ public IStreamsResponseOutputDocument GetDeviceStreamsResponseDocument(string de var document = CreateDeviceStreamsDocument(device, ref results, mtconnectVersion); if (document != null) { - StreamsResponseSent?.Invoke(this, new EventArgs()); + MulticastIsolation.Raise(StreamsResponseSent, this, EventArgs.Empty, null); return document; } } @@ -1039,7 +1039,7 @@ public IStreamsResponseOutputDocument GetDeviceStreamsResponseDocument(string de var document = CreateDeviceStreamsDocument(device, ref results, mtconnectVersion); if (document != null) { - StreamsResponseSent?.Invoke(this, new EventArgs()); + MulticastIsolation.Raise(StreamsResponseSent, this, EventArgs.Empty, null); return document; } } @@ -1075,7 +1075,7 @@ public IStreamsResponseOutputDocument GetDeviceStreamsResponseDocument(string de var document = CreateDeviceStreamsDocument(device, ref results, mtconnectVersion); if (document != null) { - StreamsResponseSent?.Invoke(this, new EventArgs()); + MulticastIsolation.Raise(StreamsResponseSent, this, EventArgs.Empty, null); return document; } } @@ -1112,7 +1112,7 @@ public IStreamsResponseOutputDocument GetDeviceStreamsResponseDocument(string de var document = CreateDeviceStreamsDocument(device, ref results, mtconnectVersion); if (document != null) { - StreamsResponseSent?.Invoke(this, new EventArgs()); + MulticastIsolation.Raise(StreamsResponseSent, this, EventArgs.Empty, null); return document; } } @@ -1149,7 +1149,7 @@ public IStreamsResponseOutputDocument GetDeviceStreamsResponseDocument(string de var document = CreateDeviceStreamsDocument(device, ref results, mtconnectVersion); if (document != null) { - StreamsResponseSent?.Invoke(this, new EventArgs()); + MulticastIsolation.Raise(StreamsResponseSent, this, EventArgs.Empty, null); return document; } } @@ -1187,7 +1187,7 @@ public IStreamsResponseOutputDocument GetDeviceStreamsResponseDocument(string de var document = CreateDeviceStreamsDocument(device, ref results, mtconnectVersion); if (document != null) { - StreamsResponseSent?.Invoke(this, new EventArgs()); + MulticastIsolation.Raise(StreamsResponseSent, this, EventArgs.Empty, null); return document; } } From ece9b7a1050538f23dbfec251ad6ac959aaa9c51 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Otto=20Boly=C3=B3s?= Date: Wed, 3 Jun 2026 12:30:41 +0200 Subject: [PATCH 21/37] fix(http): migrate HTTP server events to shared multicast helper --- .../Servers/MTConnectHttpResponseHandler.cs | 14 +++++++------- .../Servers/MTConnectHttpServerStream.cs | 10 +++++----- 2 files changed, 12 insertions(+), 12 deletions(-) diff --git a/libraries/MTConnect.NET-HTTP/Servers/MTConnectHttpResponseHandler.cs b/libraries/MTConnect.NET-HTTP/Servers/MTConnectHttpResponseHandler.cs index 8720606a..6ca5b1c6 100644 --- a/libraries/MTConnect.NET-HTTP/Servers/MTConnectHttpResponseHandler.cs +++ b/libraries/MTConnect.NET-HTTP/Servers/MTConnectHttpResponseHandler.cs @@ -57,7 +57,7 @@ public async Task HandleAsync(IHttpContext context, CancellationToken canc { try { - ClientConnected?.Invoke(this, context.Request); + MulticastIsolation.Raise(ClientConnected, this, context.Request, ClientException); // Get Accept-Encoding Header (ex. gzip, br) var acceptEncodings = GetRequestHeaderValues(context.Request, HttpHeaders.AcceptEncoding); @@ -66,9 +66,9 @@ public async Task HandleAsync(IHttpContext context, CancellationToken canc var mtconnectResponse = await OnRequestReceived(context, cancellationToken); mtconnectResponse.WriteDuration = await WriteResponse(mtconnectResponse, context.Response, acceptEncodings); - ResponseSent?.Invoke(this, mtconnectResponse); + MulticastIsolation.Raise(ResponseSent, this, mtconnectResponse, ClientException); - ClientDisconnected?.Invoke(this, context.Request.RemoteEndPoint?.ToString()); + MulticastIsolation.Raise(ClientDisconnected, this, context.Request.RemoteEndPoint?.ToString(), ClientException); return true; } @@ -77,13 +77,13 @@ public async Task HandleAsync(IHttpContext context, CancellationToken canc // Ignore Disposed Object Exception (happens when the listener is stopped) if (ex.ErrorCode != 995) { - if (ClientException != null) ClientException.Invoke(this, ex); + MulticastIsolation.Raise(ClientException, this, ex, null); } } catch (ObjectDisposedException) { } catch (Exception ex) { - if (ClientException != null) ClientException.Invoke(this, ex); + MulticastIsolation.Raise(ClientException, this, ex, null); } return false; @@ -225,7 +225,7 @@ protected async Task WriteFromStream(MTConnectHttpServerStream sampleStream, Str } catch (Exception) { - if (ClientDisconnected != null) ClientDisconnected.Invoke(this, sampleStream.Id); + MulticastIsolation.Raise(ClientDisconnected, this, sampleStream.Id, ClientException); sampleStream.Stop(); } } @@ -243,7 +243,7 @@ protected async Task WriteFromStream(MTConnectHttpServerStream sampleStream, IHt } catch (Exception) { - if (ClientDisconnected != null) ClientDisconnected.Invoke(this, sampleStream.Id); + MulticastIsolation.Raise(ClientDisconnected, this, sampleStream.Id, ClientException); sampleStream.Stop(); } } diff --git a/libraries/MTConnect.NET-HTTP/Servers/MTConnectHttpServerStream.cs b/libraries/MTConnect.NET-HTTP/Servers/MTConnectHttpServerStream.cs index f7e5a2d2..a157c0c2 100644 --- a/libraries/MTConnect.NET-HTTP/Servers/MTConnectHttpServerStream.cs +++ b/libraries/MTConnect.NET-HTTP/Servers/MTConnectHttpServerStream.cs @@ -204,7 +204,7 @@ private void Worker() { if (_mtconnectAgent != null) { - StreamStarted?.Invoke(this, _id); + MulticastIsolation.Raise(StreamStarted, this, _id, StreamException); // Set Content Type based documentFormat specified var contentType = MimeTypes.Get(_documentFormat); @@ -265,7 +265,7 @@ private void Worker() stpw.Stop(); // Raise heartbeat event and include the Multipart Chunk - HeartbeatReceived?.Invoke(this, new MTConnectHttpStreamArgs(_id, outputStream, stpw.ElapsedMilliseconds)); + MulticastIsolation.Raise(HeartbeatReceived, this, new MTConnectHttpStreamArgs(_id, outputStream, stpw.ElapsedMilliseconds), StreamException); // Reset the heartbeat timestamp lastHeartbeatSent = now; @@ -276,7 +276,7 @@ private void Worker() stpw.Stop(); // Raise heartbeat event and include the Multipart Chunk - DocumentReceived?.Invoke(this, new MTConnectHttpStreamArgs(_id, outputStream, stpw.ElapsedMilliseconds)); + MulticastIsolation.Raise(DocumentReceived, this, new MTConnectHttpStreamArgs(_id, outputStream, stpw.ElapsedMilliseconds), StreamException); // Reset the document timestamp lastDocumentSent = now; @@ -297,11 +297,11 @@ private void Worker() } catch (Exception ex) { - StreamException?.Invoke(this, ex); + MulticastIsolation.Raise(StreamException, this, ex, null); throw new Exception(); } - StreamStopped?.Invoke(this, _id); + MulticastIsolation.Raise(StreamStopped, this, _id, StreamException); } } From 785eea839b4b2aacc23ceeff4a66dc119715ae69 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Otto=20Boly=C3=B3s?= Date: Wed, 3 Jun 2026 12:45:20 +0200 Subject: [PATCH 22/37] feat(common): add generic delegate overload to MulticastIsolation --- .../MulticastIsolation.cs | 48 +++++++++++++++++++ 1 file changed, 48 insertions(+) diff --git a/libraries/MTConnect.NET-Common/MulticastIsolation.cs b/libraries/MTConnect.NET-Common/MulticastIsolation.cs index 188d0e0b..9393ebd7 100644 --- a/libraries/MTConnect.NET-Common/MulticastIsolation.cs +++ b/libraries/MTConnect.NET-Common/MulticastIsolation.cs @@ -69,6 +69,54 @@ public static void Raise(EventHandler handler, object sender, EventArgs arg, } } + /// + /// Raises an event of any delegate shape with per-delegate fault + /// isolation. The caller supplies the per-subscriber invocation lambda + /// so no per-signature overload is required; this lets the helper cover + /// custom delegate types (e.g. delegate void Foo(IPAddress), + /// delegate void Bar(Source, IDevice)) that the typed + /// / overloads + /// cannot. Same contract as those overloads: a throwing subscriber + /// cannot starve later subscribers, the + /// sink is itself iterated + /// per-delegate so a faulting fault-reporter cannot starve later fault + /// reporters either, and a secondary throw from an internalError + /// subscriber is terminal. + /// + /// The delegate type of the event being raised. Must derive from . + /// The event handler whose invocation list is iterated; a null handler is a safe no-op covering the no-subscriber case. + /// The per-subscriber invocation lambda. Called once per delegate in 's invocation list, inside the per-delegate try/catch. + /// The fault-routing sink. Each subscriber fault is routed through every delegate on this sink; pass null to swallow faults at the per-delegate boundary (consistent with the pre-isolation null-conditional behaviour). + /// The sender object passed to when routing a fault. Pass the class instance whose event is being raised, or null if no sender is associated. + /// + /// Prefer the typed + /// or overloads + /// when the event uses or ; + /// they avoid the call-site cast and the lambda allocation. Use this overload + /// only for events declared with a custom delegate signature (i.e. + /// public event MyHandler Foo; where MyHandler is not an + /// / ). + /// + public static void Raise(TDelegate handler, Action invoke, + EventHandler internalError = null, + object sender = null) + where TDelegate : Delegate + { + if (handler == null) return; + + foreach (var subscriber in handler.GetInvocationList()) + { + try + { + invoke((TDelegate)(object)subscriber); + } + catch (Exception ex) + { + RaiseInternalError(internalError, sender, ex); + } + } + } + // Iterate the InternalError invocation list so a throwing fault // reporter cannot starve later fault reporters. A secondary throw from // an InternalError subscriber itself is terminal: there is no further From d5782b8a56f7c7c55c617c21ebbf5dc58824163f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Otto=20Boly=C3=B3s?= Date: Wed, 3 Jun 2026 12:52:01 +0200 Subject: [PATCH 23/37] test(common-tests): pin generic-delegate overload of MulticastIsolation --- .../MulticastIsolationTests.cs | 84 +++++++++++++++++++ 1 file changed, 84 insertions(+) diff --git a/tests/MTConnect.NET-Common-Tests/MulticastIsolationTests.cs b/tests/MTConnect.NET-Common-Tests/MulticastIsolationTests.cs index 83451499..f4e691e4 100644 --- a/tests/MTConnect.NET-Common-Tests/MulticastIsolationTests.cs +++ b/tests/MTConnect.NET-Common-Tests/MulticastIsolationTests.cs @@ -140,5 +140,89 @@ public void MulticastIsolation_NonGeneric_TerminalSwallowsInternalErrorOwnThrow( Assert.DoesNotThrow(() => MulticastIsolation.Raise(handler, this, EventArgs.Empty, internalError!)); } + + // --- Generic custom-delegate overload --------------------------------- + + /// Local custom-delegate shape used to exercise the generic-delegate overload of the helper. + private delegate void CustomDelegate(int payload); + + /// Pins the behavior expressed by the test name: every subscriber on a custom-delegate event fires even when an earlier one throws. + [Test] + public void MulticastIsolation_GenericDelegate_FiresAllSubscribersWhenOneThrows() + { + var fired = new System.Collections.Generic.List(); + CustomDelegate? handler = null; + handler += _ => fired.Add(1); + handler += _ => throw new InvalidOperationException("subscriber-2"); + handler += _ => fired.Add(3); + + EventHandler internalError = (_, _) => { }; + + MulticastIsolation.Raise(handler!, h => h(42), internalError); + + Assert.That(fired, Is.EqualTo(new[] { 1, 3 })); + } + + /// Pins the behavior expressed by the test name: a fault thrown by a custom-delegate subscriber is routed through internalError. + [Test] + public void MulticastIsolation_GenericDelegate_RoutesFaultToInternalError() + { + var routed = new System.Collections.Generic.List(); + CustomDelegate handler = _ => throw new InvalidOperationException("boom"); + EventHandler internalError = (_, ex) => routed.Add(ex); + + MulticastIsolation.Raise(handler, h => h(0), internalError); + + Assert.That(routed.Count, Is.EqualTo(1)); + Assert.That(routed[0], Is.InstanceOf()); + Assert.That(routed[0].Message, Is.EqualTo("boom")); + } + + /// Pins the behavior expressed by the test name: under the generic-delegate overload, the internalError multicast is iterated per subscriber, so a throwing internalError handler does not starve later internalError handlers. + [Test] + public void MulticastIsolation_GenericDelegate_InternalErrorIteratesPerSubscriber() + { + var seen = new System.Collections.Generic.List(); + CustomDelegate handler = _ => throw new InvalidOperationException("origin"); + + EventHandler? internalError = null; + internalError += (_, _) => throw new InvalidOperationException("internal-1"); + internalError += (_, _) => seen.Add(2); + internalError += (_, _) => seen.Add(3); + + MulticastIsolation.Raise(handler, h => h(0), internalError!); + + Assert.That(seen, Is.EqualTo(new[] { 2, 3 })); + } + + /// Pins the behavior expressed by the test name: under the generic-delegate overload, when every internalError subscriber throws, the helper still returns without escaping an exception. + [Test] + public void MulticastIsolation_GenericDelegate_TerminalSwallowsInternalErrorOwnThrow() + { + CustomDelegate handler = _ => throw new InvalidOperationException("origin"); + EventHandler? internalError = null; + internalError += (_, _) => throw new InvalidOperationException("internal-1"); + internalError += (_, _) => throw new InvalidOperationException("internal-2"); + + Assert.DoesNotThrow(() => MulticastIsolation.Raise(handler, h => h(0), internalError!)); + } + + /// Pins the behavior expressed by the test name: the generic-delegate overload accepts a null handler as a safe no-op covering the no-subscriber case. + [Test] + public void MulticastIsolation_GenericDelegate_NullHandler_DoesNotThrow() + { + CustomDelegate? handler = null; + + Assert.DoesNotThrow(() => MulticastIsolation.Raise(handler!, h => h(0))); + } + + /// Pins the behavior expressed by the test name: the generic-delegate overload swallows subscriber faults at the per-delegate boundary when internalError is left at its default null. + [Test] + public void MulticastIsolation_GenericDelegate_NullInternalErrorSwallowsFault() + { + CustomDelegate handler = _ => throw new InvalidOperationException("boom"); + + Assert.DoesNotThrow(() => MulticastIsolation.Raise(handler, h => h(0))); + } } } From be2bb92117f4f3d5f0f2d28e28bc04ef1006ebab Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Otto=20Boly=C3=B3s?= Date: Wed, 3 Jun 2026 12:52:01 +0200 Subject: [PATCH 24/37] test(common-tests): pin multicast isolation on DeviceFinder and PingQueue events --- .../DeviceFinderMulticastIsolationTests.cs | 326 ++++++++++++++++++ 1 file changed, 326 insertions(+) create mode 100644 tests/MTConnect.NET-Common-Tests/DeviceFinderMulticastIsolationTests.cs diff --git a/tests/MTConnect.NET-Common-Tests/DeviceFinderMulticastIsolationTests.cs b/tests/MTConnect.NET-Common-Tests/DeviceFinderMulticastIsolationTests.cs new file mode 100644 index 00000000..ae37ba30 --- /dev/null +++ b/tests/MTConnect.NET-Common-Tests/DeviceFinderMulticastIsolationTests.cs @@ -0,0 +1,326 @@ +// Copyright (c) 2026 TrakHound Inc., All Rights Reserved. +// TrakHound Inc. licenses this file to you under the MIT license. + +using System; +using System.Collections.Generic; +using System.Net; +using System.Net.NetworkInformation; +using NUnit.Framework; + +namespace MTConnect.Tests.Common +{ + /// + /// Pins the multicast-isolation contract for the custom-delegate events + /// declared on MTConnectDeviceFinder and PingQueue. Neither + /// class can use the typed / + /// overloads because every event is declared + /// with a custom delegate signature (DeviceHandler, + /// PingSentHandler, PingReceivedHandler, + /// PortRequestHandler, ProbeRequestHandler, + /// RequestStatusHandler, CompletedHandler). After migration + /// every raise site uses + /// + /// passing the per-subscriber invocation lambda inline and null as + /// the internalError sink (neither class declares an InternalError + /// event). These tests redeclare each delegate shape locally and verify + /// the isolation guarantee holds for every signature; the helper contract + /// is independent of the originating class. + /// + [TestFixture] + public class DeviceFinderMulticastIsolationTests + { + // ----------------------------------------------------------------------- + // Local delegate shapes mirroring the production declarations on + // MTConnectDeviceFinder and PingQueue. Same signatures; redeclared + // locally to keep this test project free of a DeviceFinder project ref. + // ----------------------------------------------------------------------- + + /// Mirror of MTConnectDeviceFinder.DeviceHandler — fired by DeviceFound when an MTConnect agent is positively identified at an address/port pair. + private delegate void TestDeviceHandler(object sender, object device); + + /// Mirror of MTConnectDeviceFinder.RequestStatusHandler — fired by SearchCompleted; carries the elapsed milliseconds of the search. + private delegate void TestRequestStatusHandler(object sender, long milliseconds); + + /// Mirror of MTConnectDeviceFinder.PingSentHandler — fired once a ping has been dispatched to . + private delegate void TestPingSentHandlerOnFinder(object sender, IPAddress address); + + /// Mirror of MTConnectDeviceFinder.PingReceivedHandler — fired when a ping reply returns from . + private delegate void TestPingReceivedHandlerOnFinder(object sender, IPAddress address, PingReply reply); + + /// Mirror of MTConnectDeviceFinder.PortRequestHandler — fired by PortOpened/PortClosed to report the state of a TCP port at . + private delegate void TestPortRequestHandler(object sender, IPAddress address, int port); + + /// Mirror of MTConnectDeviceFinder.ProbeRequestHandler — fired by ProbeSent/ProbeSuccessful/ProbeError for each MTConnect probe attempt. + private delegate void TestProbeRequestHandler(object sender, IPAddress address, int port); + + /// Mirror of PingQueue.PingSentHandler — fired once a ping has been dispatched to ; no sender argument. + private delegate void TestPingSentHandlerOnQueue(IPAddress address); + + /// Mirror of PingQueue.PingReceivedHandler — fired when a ping reply returns; no sender argument. + private delegate void TestPingReceivedHandlerOnQueue(IPAddress address, PingReply reply); + + /// Mirror of PingQueue.CompletedHandler — fired when the queue drains, with the list of successful addresses. + private delegate void TestCompletedHandler(List successfulAddresses); + + // ----------------------------------------------------------------------- + // MTConnectDeviceFinder.DeviceFound (DeviceHandler) + // ----------------------------------------------------------------------- + + /// Pins the behavior expressed by the test name: a second DeviceHandler subscriber fires even when the first throws, covering DeviceFound on MTConnectDeviceFinder. + [Test] + public void DeviceFinder_DeviceFound_FiresAllSubscribersWhenOneThrows() + { + var seen = new List(); + TestDeviceHandler? handler = null; + handler += (_, _) => throw new InvalidOperationException("first DeviceFound subscriber throws"); + handler += (_, d) => seen.Add(d); + + MulticastIsolation.Raise(handler!, h => h(this, "device-1")); + + Assert.That(seen, Is.EqualTo(new object[] { "device-1" })); + } + + /// Pins the behavior expressed by the test name: with null InternalError, a fault from a DeviceFound subscriber is swallowed without escaping. + [Test] + public void DeviceFinder_DeviceFound_NullInternalErrorSwallowsFault() + { + TestDeviceHandler handler = (_, _) => throw new InvalidOperationException("DeviceFound fault"); + + Assert.DoesNotThrow(() => MulticastIsolation.Raise(handler, h => h(this, "device-1"))); + } + + // ----------------------------------------------------------------------- + // MTConnectDeviceFinder.SearchCompleted (RequestStatusHandler) + // ----------------------------------------------------------------------- + + /// Pins the behavior expressed by the test name: a second RequestStatusHandler subscriber fires even when the first throws, covering SearchCompleted on MTConnectDeviceFinder. + [Test] + public void DeviceFinder_SearchCompleted_FiresAllSubscribersWhenOneThrows() + { + var seen = new List(); + TestRequestStatusHandler? handler = null; + handler += (_, _) => throw new InvalidOperationException("first SearchCompleted subscriber throws"); + handler += (_, ms) => seen.Add(ms); + + MulticastIsolation.Raise(handler!, h => h(this, 42L)); + + Assert.That(seen, Is.EqualTo(new[] { 42L })); + } + + /// Pins the behavior expressed by the test name: with null InternalError, a fault from a SearchCompleted subscriber is swallowed without escaping. + [Test] + public void DeviceFinder_SearchCompleted_NullInternalErrorSwallowsFault() + { + TestRequestStatusHandler handler = (_, _) => throw new InvalidOperationException("SearchCompleted fault"); + + Assert.DoesNotThrow(() => MulticastIsolation.Raise(handler, h => h(this, 0L))); + } + + // ----------------------------------------------------------------------- + // MTConnectDeviceFinder.PingSent (PingSentHandler, finder-shape) + // ----------------------------------------------------------------------- + + /// Pins the behavior expressed by the test name: a second PingSentHandler subscriber fires even when the first throws, covering PingSent on MTConnectDeviceFinder. + [Test] + public void DeviceFinder_PingSent_FiresAllSubscribersWhenOneThrows() + { + var seen = new List(); + var addr = IPAddress.Loopback; + TestPingSentHandlerOnFinder? handler = null; + handler += (_, _) => throw new InvalidOperationException("first PingSent subscriber throws"); + handler += (_, a) => seen.Add(a); + + MulticastIsolation.Raise(handler!, h => h(this, addr)); + + Assert.That(seen, Is.EqualTo(new[] { addr })); + } + + /// Pins the behavior expressed by the test name: with null InternalError, a fault from a PingSent subscriber on the finder is swallowed without escaping. + [Test] + public void DeviceFinder_PingSent_NullInternalErrorSwallowsFault() + { + TestPingSentHandlerOnFinder handler = (_, _) => throw new InvalidOperationException("PingSent fault"); + + Assert.DoesNotThrow(() => MulticastIsolation.Raise(handler, h => h(this, IPAddress.Loopback))); + } + + // ----------------------------------------------------------------------- + // MTConnectDeviceFinder.PingReceived (PingReceivedHandler, finder-shape) + // ----------------------------------------------------------------------- + + /// Pins the behavior expressed by the test name: a second PingReceivedHandler subscriber fires even when the first throws, covering PingReceived on MTConnectDeviceFinder. + [Test] + public void DeviceFinder_PingReceived_FiresAllSubscribersWhenOneThrows() + { + var seen = new List(); + var addr = IPAddress.Loopback; + TestPingReceivedHandlerOnFinder? handler = null; + handler += (_, _, _) => throw new InvalidOperationException("first PingReceived subscriber throws"); + handler += (_, a, _) => seen.Add(a); + + MulticastIsolation.Raise(handler!, h => h(this, addr, null!)); + + Assert.That(seen, Is.EqualTo(new[] { addr })); + } + + /// Pins the behavior expressed by the test name: with null InternalError, a fault from a PingReceived subscriber on the finder is swallowed without escaping. + [Test] + public void DeviceFinder_PingReceived_NullInternalErrorSwallowsFault() + { + TestPingReceivedHandlerOnFinder handler = (_, _, _) => throw new InvalidOperationException("PingReceived fault"); + + Assert.DoesNotThrow(() => MulticastIsolation.Raise(handler, h => h(this, IPAddress.Loopback, null!))); + } + + // ----------------------------------------------------------------------- + // MTConnectDeviceFinder.PortOpened / PortClosed (PortRequestHandler) + // ----------------------------------------------------------------------- + + /// Pins the behavior expressed by the test name: a second PortRequestHandler subscriber fires even when the first throws, covering PortOpened/PortClosed on MTConnectDeviceFinder. + [Test] + public void DeviceFinder_PortRequest_FiresAllSubscribersWhenOneThrows() + { + var seen = new List(); + TestPortRequestHandler? handler = null; + handler += (_, _, _) => throw new InvalidOperationException("first PortRequest subscriber throws"); + handler += (_, _, p) => seen.Add(p); + + MulticastIsolation.Raise(handler!, h => h(this, IPAddress.Loopback, 5000)); + + Assert.That(seen, Is.EqualTo(new[] { 5000 })); + } + + /// Pins the behavior expressed by the test name: with null InternalError, a fault from a PortRequestHandler subscriber is swallowed without escaping. + [Test] + public void DeviceFinder_PortRequest_NullInternalErrorSwallowsFault() + { + TestPortRequestHandler handler = (_, _, _) => throw new InvalidOperationException("PortRequest fault"); + + Assert.DoesNotThrow(() => MulticastIsolation.Raise(handler, h => h(this, IPAddress.Loopback, 5000))); + } + + // ----------------------------------------------------------------------- + // MTConnectDeviceFinder.ProbeSent / ProbeSuccessful (ProbeRequestHandler) + // ----------------------------------------------------------------------- + + /// Pins the behavior expressed by the test name: a second ProbeRequestHandler subscriber fires even when the first throws, covering ProbeSent/ProbeSuccessful on MTConnectDeviceFinder. + [Test] + public void DeviceFinder_ProbeRequest_FiresAllSubscribersWhenOneThrows() + { + var seen = new List(); + TestProbeRequestHandler? handler = null; + handler += (_, _, _) => throw new InvalidOperationException("first ProbeRequest subscriber throws"); + handler += (_, _, p) => seen.Add(p); + + MulticastIsolation.Raise(handler!, h => h(this, IPAddress.Loopback, 5000)); + + Assert.That(seen, Is.EqualTo(new[] { 5000 })); + } + + /// Pins the behavior expressed by the test name: with null InternalError, a fault from a ProbeRequestHandler subscriber is swallowed without escaping. + [Test] + public void DeviceFinder_ProbeRequest_NullInternalErrorSwallowsFault() + { + TestProbeRequestHandler handler = (_, _, _) => throw new InvalidOperationException("ProbeRequest fault"); + + Assert.DoesNotThrow(() => MulticastIsolation.Raise(handler, h => h(this, IPAddress.Loopback, 5000))); + } + + // ----------------------------------------------------------------------- + // PingQueue.PingSent (PingSentHandler, queue-shape: single IPAddress arg) + // ----------------------------------------------------------------------- + + /// Pins the behavior expressed by the test name: a second queue-shape PingSentHandler subscriber fires even when the first throws, covering PingSent on PingQueue. + [Test] + public void PingQueue_PingSent_FiresAllSubscribersWhenOneThrows() + { + var seen = new List(); + var addr = IPAddress.Loopback; + TestPingSentHandlerOnQueue? handler = null; + handler += _ => throw new InvalidOperationException("first PingQueue.PingSent subscriber throws"); + handler += a => seen.Add(a); + + MulticastIsolation.Raise(handler!, h => h(addr)); + + Assert.That(seen, Is.EqualTo(new[] { addr })); + } + + /// Pins the behavior expressed by the test name: with null InternalError, a fault from a queue-shape PingSent subscriber is swallowed without escaping. + [Test] + public void PingQueue_PingSent_NullInternalErrorSwallowsFault() + { + TestPingSentHandlerOnQueue handler = _ => throw new InvalidOperationException("PingQueue.PingSent fault"); + + Assert.DoesNotThrow(() => MulticastIsolation.Raise(handler, h => h(IPAddress.Loopback))); + } + + // ----------------------------------------------------------------------- + // PingQueue.PingReceived (PingReceivedHandler, queue-shape) + // ----------------------------------------------------------------------- + + /// Pins the behavior expressed by the test name: a second queue-shape PingReceivedHandler subscriber fires even when the first throws, covering PingReceived on PingQueue. + [Test] + public void PingQueue_PingReceived_FiresAllSubscribersWhenOneThrows() + { + var seen = new List(); + var addr = IPAddress.Loopback; + TestPingReceivedHandlerOnQueue? handler = null; + handler += (_, _) => throw new InvalidOperationException("first PingQueue.PingReceived subscriber throws"); + handler += (a, _) => seen.Add(a); + + MulticastIsolation.Raise(handler!, h => h(addr, null!)); + + Assert.That(seen, Is.EqualTo(new[] { addr })); + } + + /// Pins the behavior expressed by the test name: with null InternalError, a fault from a queue-shape PingReceived subscriber is swallowed without escaping. + [Test] + public void PingQueue_PingReceived_NullInternalErrorSwallowsFault() + { + TestPingReceivedHandlerOnQueue handler = (_, _) => throw new InvalidOperationException("PingQueue.PingReceived fault"); + + Assert.DoesNotThrow(() => MulticastIsolation.Raise(handler, h => h(IPAddress.Loopback, null!))); + } + + // ----------------------------------------------------------------------- + // PingQueue.Completed (CompletedHandler) + // ----------------------------------------------------------------------- + + /// Pins the behavior expressed by the test name: a second CompletedHandler subscriber fires even when the first throws, covering Completed on PingQueue. + [Test] + public void PingQueue_Completed_FiresAllSubscribersWhenOneThrows() + { + var seen = new List(); + var addresses = new List { IPAddress.Loopback }; + TestCompletedHandler? handler = null; + handler += _ => throw new InvalidOperationException("first PingQueue.Completed subscriber throws"); + handler += list => seen.Add(list.Count); + + MulticastIsolation.Raise(handler!, h => h(addresses)); + + Assert.That(seen, Is.EqualTo(new[] { 1 })); + } + + /// Pins the behavior expressed by the test name: with null InternalError, a fault from a Completed subscriber on PingQueue is swallowed without escaping. + [Test] + public void PingQueue_Completed_NullInternalErrorSwallowsFault() + { + TestCompletedHandler handler = _ => throw new InvalidOperationException("PingQueue.Completed fault"); + + Assert.DoesNotThrow(() => MulticastIsolation.Raise(handler, h => h(new List()))); + } + + // ----------------------------------------------------------------------- + // Null-handler guard for the generic delegate overload + // ----------------------------------------------------------------------- + + /// Pins the behavior expressed by the test name: Raise with a null custom-delegate handler is a safe no-op covering the no-subscriber case at runtime. + [Test] + public void DeviceFinder_NullCustomDelegateHandler_DoesNotThrow() + { + TestDeviceHandler? handler = null; + + Assert.DoesNotThrow(() => MulticastIsolation.Raise(handler!, h => h(this, "any"))); + } + } +} From cf6cc13aa51c7f54bb840b8896be2a97e0127da8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Otto=20Boly=C3=B3s?= Date: Wed, 3 Jun 2026 12:52:01 +0200 Subject: [PATCH 25/37] test(common-tests): pin multicast isolation on agent and broker custom-delegate events --- .../Agents/AgentMulticastIsolationTests.cs | 420 +++++++++++++++++- 1 file changed, 410 insertions(+), 10 deletions(-) diff --git a/tests/MTConnect.NET-Common-Tests/Agents/AgentMulticastIsolationTests.cs b/tests/MTConnect.NET-Common-Tests/Agents/AgentMulticastIsolationTests.cs index 904b9856..9aae0573 100644 --- a/tests/MTConnect.NET-Common-Tests/Agents/AgentMulticastIsolationTests.cs +++ b/tests/MTConnect.NET-Common-Tests/Agents/AgentMulticastIsolationTests.cs @@ -5,6 +5,8 @@ using System.Collections.Generic; using MTConnect.Assets; using MTConnect.Devices; +using MTConnect.Devices.DataItems; +using MTConnect.Errors; using MTConnect.Input; using MTConnect.Observations; using NUnit.Framework; @@ -12,16 +14,25 @@ namespace MTConnect.Tests.Common { /// - /// Pins the multicast-isolation contract for the - /// raise sites on MTConnectAgent (DeviceAdded, ObservationReceived, - /// ObservationAdded, AssetReceived, AssetAdded) and the single - /// raise site on MTConnectAgentBroker - /// (StreamsResponseSent). After migration all these sites use - /// / - /// passing null as the internalError sink (neither agent class - /// declares an InternalError event). The custom-delegate events on both - /// classes are not migratable with the shared helper and are therefore - /// out-of-scope for this test class (surfaced as a blocker). + /// Pins the multicast-isolation contract for every event raised by + /// MTConnectAgent and MTConnectAgentBroker: + /// sites on the agent (DeviceAdded, + /// ObservationReceived, ObservationAdded, AssetReceived, AssetAdded), the + /// custom-delegate validation events on the agent (InvalidDeviceAdded, + /// InvalidComponentAdded, InvalidCompositionAdded, InvalidDataItemAdded, + /// InvalidObservationAdded, InvalidAssetAdded), the single + /// site on the broker (StreamsResponseSent), + /// and the custom-delegate request / response events on the broker + /// (DevicesRequestReceived, DevicesResponseSent, StreamsRequestReceived, + /// AssetsRequestReceived, DeviceAssetsRequestReceived, AssetsResponseSent, + /// ErrorResponseSent). After migration all these sites use + /// / + /// / + /// + /// passing null as the internalError sink — neither agent + /// class declares an InternalError event, so faults are swallowed at the + /// per-delegate boundary (consistent with the pre-isolation null-conditional + /// behaviour). /// [TestFixture] public class AgentMulticastIsolationTests @@ -201,5 +212,394 @@ public void AgentBroker_NullNonGenericHandler_DoesNotThrow() Assert.DoesNotThrow(() => MulticastIsolation.Raise(handler, this, EventArgs.Empty, null)); } + + // ======================================================================= + // Custom-delegate raise sites — agent + // ======================================================================= + + // ----------------------------------------------------------------------- + // MTConnectDeviceValidationHandler (InvalidDeviceAdded on MTConnectAgent) + // ----------------------------------------------------------------------- + + /// Pins the behavior expressed by the test name: a second MTConnectDeviceValidationHandler subscriber fires even when the first throws, covering InvalidDeviceAdded on MTConnectAgent. + [Test] + public void Agent_InvalidDeviceAdded_FiresAllSubscribersWhenOneThrows() + { + var seen = new List(); + var device = new Device { Name = "d1", Uuid = "uuid-1" }; + var result = new ValidationResult(false, "bad device"); + + MTConnectDeviceValidationHandler? handler = null; + handler += (_, _) => throw new InvalidOperationException("first InvalidDeviceAdded subscriber throws"); + handler += (d, _) => seen.Add(d.Uuid ?? "null"); + + MulticastIsolation.Raise(handler!, h => h(device, result)); + + Assert.That(seen, Is.EqualTo(new[] { "uuid-1" })); + } + + /// Pins the behavior expressed by the test name: with null InternalError, a fault from an InvalidDeviceAdded subscriber is swallowed without escaping. + [Test] + public void Agent_InvalidDeviceAdded_NullInternalErrorSwallowsFault() + { + var device = new Device { Name = "d1", Uuid = "uuid-1" }; + var result = new ValidationResult(false, "bad device"); + MTConnectDeviceValidationHandler handler = (_, _) => throw new InvalidOperationException("InvalidDeviceAdded fault"); + + Assert.DoesNotThrow(() => MulticastIsolation.Raise(handler, h => h(device, result))); + } + + // ----------------------------------------------------------------------- + // MTConnectComponentValidationHandler (InvalidComponentAdded on MTConnectAgent) + // ----------------------------------------------------------------------- + + /// Pins the behavior expressed by the test name: a second MTConnectComponentValidationHandler subscriber fires even when the first throws, covering InvalidComponentAdded on MTConnectAgent. + [Test] + public void Agent_InvalidComponentAdded_FiresAllSubscribersWhenOneThrows() + { + var seen = new List(); + var component = new Component { Id = "c1" }; + var result = new ValidationResult(false, "bad component"); + + MTConnectComponentValidationHandler? handler = null; + handler += (_, _, _) => throw new InvalidOperationException("first InvalidComponentAdded subscriber throws"); + handler += (uuid, _, _) => seen.Add(uuid); + + MulticastIsolation.Raise(handler!, h => h("uuid-1", component, result)); + + Assert.That(seen, Is.EqualTo(new[] { "uuid-1" })); + } + + /// Pins the behavior expressed by the test name: with null InternalError, a fault from an InvalidComponentAdded subscriber is swallowed without escaping. + [Test] + public void Agent_InvalidComponentAdded_NullInternalErrorSwallowsFault() + { + var component = new Component { Id = "c1" }; + var result = new ValidationResult(false, "bad component"); + MTConnectComponentValidationHandler handler = (_, _, _) => throw new InvalidOperationException("InvalidComponentAdded fault"); + + Assert.DoesNotThrow(() => MulticastIsolation.Raise(handler, h => h("uuid-1", component, result))); + } + + // ----------------------------------------------------------------------- + // MTConnectCompositionValidationHandler (InvalidCompositionAdded on MTConnectAgent) + // ----------------------------------------------------------------------- + + /// Pins the behavior expressed by the test name: a second MTConnectCompositionValidationHandler subscriber fires even when the first throws, covering InvalidCompositionAdded on MTConnectAgent. + [Test] + public void Agent_InvalidCompositionAdded_FiresAllSubscribersWhenOneThrows() + { + var seen = new List(); + var composition = new Composition { Id = "cm1" }; + var result = new ValidationResult(false, "bad composition"); + + MTConnectCompositionValidationHandler? handler = null; + handler += (_, _, _) => throw new InvalidOperationException("first InvalidCompositionAdded subscriber throws"); + handler += (uuid, _, _) => seen.Add(uuid); + + MulticastIsolation.Raise(handler!, h => h("uuid-1", composition, result)); + + Assert.That(seen, Is.EqualTo(new[] { "uuid-1" })); + } + + /// Pins the behavior expressed by the test name: with null InternalError, a fault from an InvalidCompositionAdded subscriber is swallowed without escaping. + [Test] + public void Agent_InvalidCompositionAdded_NullInternalErrorSwallowsFault() + { + var composition = new Composition { Id = "cm1" }; + var result = new ValidationResult(false, "bad composition"); + MTConnectCompositionValidationHandler handler = (_, _, _) => throw new InvalidOperationException("InvalidCompositionAdded fault"); + + Assert.DoesNotThrow(() => MulticastIsolation.Raise(handler, h => h("uuid-1", composition, result))); + } + + // ----------------------------------------------------------------------- + // MTConnectDataItemValidationHandler (InvalidDataItemAdded on MTConnectAgent) + // ----------------------------------------------------------------------- + + /// Pins the behavior expressed by the test name: a second MTConnectDataItemValidationHandler subscriber fires even when the first throws, covering InvalidDataItemAdded on MTConnectAgent. + [Test] + public void Agent_InvalidDataItemAdded_FiresAllSubscribersWhenOneThrows() + { + var seen = new List(); + var dataItem = new DataItem { Id = "di1" }; + var result = new ValidationResult(false, "bad data item"); + + MTConnectDataItemValidationHandler? handler = null; + handler += (_, _, _) => throw new InvalidOperationException("first InvalidDataItemAdded subscriber throws"); + handler += (uuid, _, _) => seen.Add(uuid); + + MulticastIsolation.Raise(handler!, h => h("uuid-1", dataItem, result)); + + Assert.That(seen, Is.EqualTo(new[] { "uuid-1" })); + } + + /// Pins the behavior expressed by the test name: with null InternalError, a fault from an InvalidDataItemAdded subscriber is swallowed without escaping. + [Test] + public void Agent_InvalidDataItemAdded_NullInternalErrorSwallowsFault() + { + var dataItem = new DataItem { Id = "di1" }; + var result = new ValidationResult(false, "bad data item"); + MTConnectDataItemValidationHandler handler = (_, _, _) => throw new InvalidOperationException("InvalidDataItemAdded fault"); + + Assert.DoesNotThrow(() => MulticastIsolation.Raise(handler, h => h("uuid-1", dataItem, result))); + } + + // ----------------------------------------------------------------------- + // MTConnectObservationValidationHandler (InvalidObservationAdded on MTConnectAgent) + // ----------------------------------------------------------------------- + + /// Pins the behavior expressed by the test name: a second MTConnectObservationValidationHandler subscriber fires even when the first throws, covering InvalidObservationAdded on MTConnectAgent. + [Test] + public void Agent_InvalidObservationAdded_FiresAllSubscribersWhenOneThrows() + { + var seen = new List(); + var result = new ValidationResult(false, "bad observation"); + + MTConnectObservationValidationHandler? handler = null; + handler += (_, _, _) => throw new InvalidOperationException("first InvalidObservationAdded subscriber throws"); + handler += (uuid, _, _) => seen.Add(uuid); + + MulticastIsolation.Raise(handler!, h => h("uuid-1", "key-1", result)); + + Assert.That(seen, Is.EqualTo(new[] { "uuid-1" })); + } + + /// Pins the behavior expressed by the test name: with null InternalError, a fault from an InvalidObservationAdded subscriber is swallowed without escaping. + [Test] + public void Agent_InvalidObservationAdded_NullInternalErrorSwallowsFault() + { + var result = new ValidationResult(false, "bad observation"); + MTConnectObservationValidationHandler handler = (_, _, _) => throw new InvalidOperationException("InvalidObservationAdded fault"); + + Assert.DoesNotThrow(() => MulticastIsolation.Raise(handler, h => h("uuid-1", "key-1", result))); + } + + // ----------------------------------------------------------------------- + // MTConnectAssetValidationHandler (InvalidAssetAdded on MTConnectAgent) + // ----------------------------------------------------------------------- + + /// Pins the behavior expressed by the test name: a second MTConnectAssetValidationHandler subscriber fires even when the first throws, covering InvalidAssetAdded on MTConnectAgent. + [Test] + public void Agent_InvalidAssetAdded_FiresAllSubscribersWhenOneThrows() + { + var seen = new List(); + var asset = new Asset { AssetId = "a1", Timestamp = DateTime.UtcNow }; + var result = new ValidationResult(false, "bad asset"); + + MTConnectAssetValidationHandler? handler = null; + handler += (_, _) => throw new InvalidOperationException("first InvalidAssetAdded subscriber throws"); + handler += (a, _) => seen.Add(a.AssetId ?? "null"); + + MulticastIsolation.Raise(handler!, h => h(asset, result)); + + Assert.That(seen, Is.EqualTo(new[] { "a1" })); + } + + /// Pins the behavior expressed by the test name: with null InternalError, a fault from an InvalidAssetAdded subscriber is swallowed without escaping. + [Test] + public void Agent_InvalidAssetAdded_NullInternalErrorSwallowsFault() + { + var asset = new Asset { AssetId = "a1", Timestamp = DateTime.UtcNow }; + var result = new ValidationResult(false, "bad asset"); + MTConnectAssetValidationHandler handler = (_, _) => throw new InvalidOperationException("InvalidAssetAdded fault"); + + Assert.DoesNotThrow(() => MulticastIsolation.Raise(handler, h => h(asset, result))); + } + + // ======================================================================= + // Custom-delegate raise sites — broker + // ======================================================================= + + // ----------------------------------------------------------------------- + // MTConnectDevicesRequestedHandler (DevicesRequestReceived on MTConnectAgentBroker) + // ----------------------------------------------------------------------- + + /// Pins the behavior expressed by the test name: a second MTConnectDevicesRequestedHandler subscriber fires even when the first throws, covering DevicesRequestReceived on MTConnectAgentBroker. + [Test] + public void AgentBroker_DevicesRequestReceived_FiresAllSubscribersWhenOneThrows() + { + var seen = new List(); + MTConnectDevicesRequestedHandler? handler = null; + handler += _ => throw new InvalidOperationException("first DevicesRequestReceived subscriber throws"); + handler += u => seen.Add(u); + + MulticastIsolation.Raise(handler!, h => h("uuid-1")); + + Assert.That(seen, Is.EqualTo(new[] { "uuid-1" })); + } + + /// Pins the behavior expressed by the test name: with null InternalError, a fault from a DevicesRequestReceived subscriber is swallowed without escaping. + [Test] + public void AgentBroker_DevicesRequestReceived_NullInternalErrorSwallowsFault() + { + MTConnectDevicesRequestedHandler handler = _ => throw new InvalidOperationException("DevicesRequestReceived fault"); + + Assert.DoesNotThrow(() => MulticastIsolation.Raise(handler, h => h("uuid-1"))); + } + + // ----------------------------------------------------------------------- + // MTConnectDevicesHandler (DevicesResponseSent on MTConnectAgentBroker) + // ----------------------------------------------------------------------- + + /// Pins the behavior expressed by the test name: a second MTConnectDevicesHandler subscriber fires even when the first throws, covering DevicesResponseSent on MTConnectAgentBroker. + [Test] + public void AgentBroker_DevicesResponseSent_FiresAllSubscribersWhenOneThrows() + { + var firedCount = 0; + MTConnectDevicesHandler? handler = null; + handler += _ => throw new InvalidOperationException("first DevicesResponseSent subscriber throws"); + handler += _ => firedCount++; + + MulticastIsolation.Raise(handler!, h => h(null!)); + + Assert.That(firedCount, Is.EqualTo(1)); + } + + /// Pins the behavior expressed by the test name: with null InternalError, a fault from a DevicesResponseSent subscriber is swallowed without escaping. + [Test] + public void AgentBroker_DevicesResponseSent_NullInternalErrorSwallowsFault() + { + MTConnectDevicesHandler handler = _ => throw new InvalidOperationException("DevicesResponseSent fault"); + + Assert.DoesNotThrow(() => MulticastIsolation.Raise(handler, h => h(null!))); + } + + // ----------------------------------------------------------------------- + // MTConnectStreamsRequestedHandler (StreamsRequestReceived on MTConnectAgentBroker) + // ----------------------------------------------------------------------- + + /// Pins the behavior expressed by the test name: a second MTConnectStreamsRequestedHandler subscriber fires even when the first throws, covering StreamsRequestReceived on MTConnectAgentBroker. + [Test] + public void AgentBroker_StreamsRequestReceived_FiresAllSubscribersWhenOneThrows() + { + var seen = new List(); + MTConnectStreamsRequestedHandler? handler = null; + handler += _ => throw new InvalidOperationException("first StreamsRequestReceived subscriber throws"); + handler += u => seen.Add(u); + + MulticastIsolation.Raise(handler!, h => h("uuid-1")); + + Assert.That(seen, Is.EqualTo(new[] { "uuid-1" })); + } + + /// Pins the behavior expressed by the test name: with null InternalError, a fault from a StreamsRequestReceived subscriber is swallowed without escaping. + [Test] + public void AgentBroker_StreamsRequestReceived_NullInternalErrorSwallowsFault() + { + MTConnectStreamsRequestedHandler handler = _ => throw new InvalidOperationException("StreamsRequestReceived fault"); + + Assert.DoesNotThrow(() => MulticastIsolation.Raise(handler, h => h("uuid-1"))); + } + + // ----------------------------------------------------------------------- + // MTConnectAssetsRequestedHandler (AssetsRequestReceived on MTConnectAgentBroker) + // ----------------------------------------------------------------------- + + /// Pins the behavior expressed by the test name: a second MTConnectAssetsRequestedHandler subscriber fires even when the first throws, covering AssetsRequestReceived on MTConnectAgentBroker. + [Test] + public void AgentBroker_AssetsRequestReceived_FiresAllSubscribersWhenOneThrows() + { + var seen = new List(); + var ids = new[] { "asset-1" }; + MTConnectAssetsRequestedHandler? handler = null; + handler += _ => throw new InvalidOperationException("first AssetsRequestReceived subscriber throws"); + handler += list => { foreach (var _ in list) seen.Add(1); }; + + MulticastIsolation.Raise(handler!, h => h(ids)); + + Assert.That(seen, Is.EqualTo(new[] { 1 })); + } + + /// Pins the behavior expressed by the test name: with null InternalError, a fault from an AssetsRequestReceived subscriber is swallowed without escaping. + [Test] + public void AgentBroker_AssetsRequestReceived_NullInternalErrorSwallowsFault() + { + var ids = new[] { "asset-1" }; + MTConnectAssetsRequestedHandler handler = _ => throw new InvalidOperationException("AssetsRequestReceived fault"); + + Assert.DoesNotThrow(() => MulticastIsolation.Raise(handler, h => h(ids))); + } + + // ----------------------------------------------------------------------- + // MTConnectDeviceAssetsRequestedHandler (DeviceAssetsRequestReceived on MTConnectAgentBroker) + // ----------------------------------------------------------------------- + + /// Pins the behavior expressed by the test name: a second MTConnectDeviceAssetsRequestedHandler subscriber fires even when the first throws, covering DeviceAssetsRequestReceived on MTConnectAgentBroker. + [Test] + public void AgentBroker_DeviceAssetsRequestReceived_FiresAllSubscribersWhenOneThrows() + { + var seen = new List(); + MTConnectDeviceAssetsRequestedHandler? handler = null; + handler += _ => throw new InvalidOperationException("first DeviceAssetsRequestReceived subscriber throws"); + handler += u => seen.Add(u); + + MulticastIsolation.Raise(handler!, h => h("uuid-1")); + + Assert.That(seen, Is.EqualTo(new[] { "uuid-1" })); + } + + /// Pins the behavior expressed by the test name: with null InternalError, a fault from a DeviceAssetsRequestReceived subscriber is swallowed without escaping. + [Test] + public void AgentBroker_DeviceAssetsRequestReceived_NullInternalErrorSwallowsFault() + { + MTConnectDeviceAssetsRequestedHandler handler = _ => throw new InvalidOperationException("DeviceAssetsRequestReceived fault"); + + Assert.DoesNotThrow(() => MulticastIsolation.Raise(handler, h => h("uuid-1"))); + } + + // ----------------------------------------------------------------------- + // MTConnectAssetsHandler (AssetsResponseSent on MTConnectAgentBroker) + // ----------------------------------------------------------------------- + + /// Pins the behavior expressed by the test name: a second MTConnectAssetsHandler subscriber fires even when the first throws, covering AssetsResponseSent on MTConnectAgentBroker. + [Test] + public void AgentBroker_AssetsResponseSent_FiresAllSubscribersWhenOneThrows() + { + var firedCount = 0; + MTConnectAssetsHandler? handler = null; + handler += _ => throw new InvalidOperationException("first AssetsResponseSent subscriber throws"); + handler += _ => firedCount++; + + MulticastIsolation.Raise(handler!, h => h(null!)); + + Assert.That(firedCount, Is.EqualTo(1)); + } + + /// Pins the behavior expressed by the test name: with null InternalError, a fault from an AssetsResponseSent subscriber is swallowed without escaping. + [Test] + public void AgentBroker_AssetsResponseSent_NullInternalErrorSwallowsFault() + { + MTConnectAssetsHandler handler = _ => throw new InvalidOperationException("AssetsResponseSent fault"); + + Assert.DoesNotThrow(() => MulticastIsolation.Raise(handler, h => h(null!))); + } + + // ----------------------------------------------------------------------- + // MTConnectErrorHandler (ErrorResponseSent on MTConnectAgentBroker) + // ----------------------------------------------------------------------- + + /// Pins the behavior expressed by the test name: a second MTConnectErrorHandler subscriber fires even when the first throws, covering ErrorResponseSent on MTConnectAgentBroker. + [Test] + public void AgentBroker_ErrorResponseSent_FiresAllSubscribersWhenOneThrows() + { + var firedCount = 0; + MTConnectErrorHandler? handler = null; + handler += _ => throw new InvalidOperationException("first ErrorResponseSent subscriber throws"); + handler += _ => firedCount++; + + MulticastIsolation.Raise(handler!, h => h((IErrorResponseDocument)null!)); + + Assert.That(firedCount, Is.EqualTo(1)); + } + + /// Pins the behavior expressed by the test name: with null InternalError, a fault from an ErrorResponseSent subscriber is swallowed without escaping. + [Test] + public void AgentBroker_ErrorResponseSent_NullInternalErrorSwallowsFault() + { + MTConnectErrorHandler handler = _ => throw new InvalidOperationException("ErrorResponseSent fault"); + + Assert.DoesNotThrow(() => MulticastIsolation.Raise(handler, h => h((IErrorResponseDocument)null!))); + } } } From ae09d9569d607e413c2218555c6e3f9f4b94a83a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Otto=20Boly=C3=B3s?= Date: Wed, 3 Jun 2026 12:55:08 +0200 Subject: [PATCH 26/37] fix(device-finder): apply multicast isolation to custom-delegate events --- .../MTConnectDeviceFinder.cs | 16 ++++++++-------- .../MTConnect.NET-DeviceFinder/PingQueue.cs | 7 ++++--- 2 files changed, 12 insertions(+), 11 deletions(-) diff --git a/libraries/MTConnect.NET-DeviceFinder/MTConnectDeviceFinder.cs b/libraries/MTConnect.NET-DeviceFinder/MTConnectDeviceFinder.cs index 71e77c25..de4d2d04 100644 --- a/libraries/MTConnect.NET-DeviceFinder/MTConnectDeviceFinder.cs +++ b/libraries/MTConnect.NET-DeviceFinder/MTConnectDeviceFinder.cs @@ -244,12 +244,12 @@ private void PingQueue_Start() private void PingQueue_PingSent(IPAddress address) { - PingSent?.Invoke(this, address); + MulticastIsolation.Raise(PingSent, h => h(this, address)); } private void Queue_PingReceived(IPAddress address, PingReply reply) { - PingReceived?.Invoke(this, address, reply); + MulticastIsolation.Raise(PingReceived, h => h(this, address, reply)); } private void Queue_Completed(List successfulAddresses) @@ -299,7 +299,7 @@ private void CheckRequestsStatus() } if (ScanInterval == 0 && stop != null) stop.Set(); - SearchCompleted?.Invoke(this, m); + MulticastIsolation.Raise(SearchCompleted, h => h(this, m)); } } @@ -313,12 +313,12 @@ private bool TestPort(IPAddress address, int port) var success = result.AsyncWaitHandle.WaitOne(Timeout); if (!success) { - PortClosed?.Invoke(this, address, port); + MulticastIsolation.Raise(PortClosed, h => h(this, address, port)); return false; } else { - PortOpened?.Invoke(this, address, port); + MulticastIsolation.Raise(PortOpened, h => h(this, address, port)); } client.EndConnect(result); @@ -343,7 +343,7 @@ private void SendProbe(IPAddress address, int port) probe.InternalError += ProbeExceptionError; // Notify that a new Probe request has been sent - ProbeSent?.Invoke(this, address, port); + MulticastIsolation.Raise(ProbeSent, h => h(this, address, port)); var document = probe.Get(); if (document != null) @@ -370,11 +370,11 @@ private void SendProbe(IPAddress address, int port) } // Notify that a Device was found - DeviceFound?.Invoke(this, foundDevice); + MulticastIsolation.Raise(DeviceFound, h => h(this, foundDevice)); } // Notify that the Probe reqeuest was successful - ProbeSuccessful?.Invoke(this, address, port); + MulticastIsolation.Raise(ProbeSuccessful, h => h(this, address, port)); } } catch { } diff --git a/libraries/MTConnect.NET-DeviceFinder/PingQueue.cs b/libraries/MTConnect.NET-DeviceFinder/PingQueue.cs index d2b415e7..ecaa91c7 100644 --- a/libraries/MTConnect.NET-DeviceFinder/PingQueue.cs +++ b/libraries/MTConnect.NET-DeviceFinder/PingQueue.cs @@ -132,7 +132,7 @@ private void Worker(object obj) { foreach (var address in addresses) { - PingSent?.Invoke(address); + MulticastIsolation.Raise(PingSent, h => h(address)); var ping = new Ping(); lock (_lock) activeRequests.Add(ping); @@ -157,7 +157,8 @@ private void Ping_PingCompleted(object sender, PingCompletedEventArgs e) { var address = (IPAddress)e.UserState; - PingReceived?.Invoke(address, e.Reply); + var reply = e.Reply; + MulticastIsolation.Raise(PingReceived, h => h(address, reply)); if (e.Reply.Status == IPStatus.Success) successful.Add(address); @@ -181,7 +182,7 @@ private void CheckCompleted() successfulAddresses = successful; } - if (completed) Completed?.Invoke(successfulAddresses); + if (completed) MulticastIsolation.Raise(Completed, h => h(successfulAddresses)); } } } \ No newline at end of file From 02c8b52c3ad48e64c6a485b293241393776b9817 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Otto=20Boly=C3=B3s?= Date: Wed, 3 Jun 2026 12:57:34 +0200 Subject: [PATCH 27/37] fix(common): apply multicast isolation to MTConnectAgent custom-delegate events --- .../Agents/MTConnectAgent.cs | 29 ++++++++----------- 1 file changed, 12 insertions(+), 17 deletions(-) diff --git a/libraries/MTConnect.NET-Common/Agents/MTConnectAgent.cs b/libraries/MTConnect.NET-Common/Agents/MTConnectAgent.cs index 2b3c0f0f..a3785518 100644 --- a/libraries/MTConnect.NET-Common/Agents/MTConnectAgent.cs +++ b/libraries/MTConnect.NET-Common/Agents/MTConnectAgent.cs @@ -1314,7 +1314,7 @@ private Device NormalizeDevice(IDevice device) var validationResults = new ValidationResult(false, $"Invalid Component : \"{genericComponent.Type}\" Not Found"); if (_configuration.InputValidationLevel > InputValidationLevel.Ignore) { - if (InvalidComponentAdded != null) InvalidComponentAdded.Invoke(obj.Uuid, genericComponent, validationResults); + MulticastIsolation.Raise(InvalidComponentAdded, h => h(obj.Uuid, genericComponent, validationResults)); // Remove Component from Device if (_configuration.InputValidationLevel == InputValidationLevel.Remove) obj.RemoveComponent(genericComponent.Id); @@ -1334,7 +1334,7 @@ private Device NormalizeDevice(IDevice device) var validationResults = new ValidationResult(false, $"Invalid Composition : \"{genericComposition.Type}\" Not Found"); if (_configuration.InputValidationLevel > InputValidationLevel.Ignore) { - if (InvalidCompositionAdded != null) InvalidCompositionAdded.Invoke(obj.Uuid, genericComposition, validationResults); + MulticastIsolation.Raise(InvalidCompositionAdded, h => h(obj.Uuid, genericComposition, validationResults)); // Remove Compsition from Device if (_configuration.InputValidationLevel == InputValidationLevel.Remove) obj.RemoveComposition(genericComposition.Id); @@ -1354,7 +1354,7 @@ private Device NormalizeDevice(IDevice device) var validationResults = new ValidationResult(false, $"Invalid DataItem : \"{genericDataItem.Type}\" Not Found"); if (_configuration.InputValidationLevel > InputValidationLevel.Ignore) { - if (InvalidDataItemAdded != null) InvalidDataItemAdded.Invoke(obj.Uuid, genericDataItem, validationResults); + MulticastIsolation.Raise(InvalidDataItemAdded, h => h(obj.Uuid, genericDataItem, validationResults)); // Remove DataItem from Device if (_configuration.InputValidationLevel == InputValidationLevel.Remove) obj.RemoveDataItem(genericDataItem.Id); @@ -2259,16 +2259,17 @@ public bool AddObservation(string deviceKey, IObservationInput observationInput, else success = true; // Return true if no update needed } - if (!validationResult.IsValid && InvalidObservationAdded != null) + if (!validationResult.IsValid) { - InvalidObservationAdded.Invoke(deviceUuid, input.DataItemKey, validationResult); + MulticastIsolation.Raise(InvalidObservationAdded, h => h(deviceUuid, input.DataItemKey, validationResult)); } return success; } - else if (InvalidObservationAdded != null) + else { - InvalidObservationAdded.Invoke(deviceUuid, input.DataItemKey, new ValidationResult(false, $"DataItemKey \"{input.DataItemKey}\" not Found in Device")); + var missingKeyResult = new ValidationResult(false, $"DataItemKey \"{input.DataItemKey}\" not Found in Device"); + MulticastIsolation.Raise(InvalidObservationAdded, h => h(deviceUuid, input.DataItemKey, missingKeyResult)); } } @@ -2339,19 +2340,13 @@ public void OnObservationAdded(IObservation observation) /// public void OnInvalidObservationAdded(string deviceUuid, string dataItemId, ValidationResult result) { - if (InvalidObservationAdded != null) - { - InvalidObservationAdded?.Invoke(deviceUuid, dataItemId, result); - } + MulticastIsolation.Raise(InvalidObservationAdded, h => h(deviceUuid, dataItemId, result)); } /// public void OnInvalidDeviceAdded(IDevice device, ValidationResult result) { - if (InvalidDeviceAdded != null) - { - InvalidDeviceAdded?.Invoke(device, result); - } + MulticastIsolation.Raise(InvalidDeviceAdded, h => h(device, result)); } #endregion @@ -2450,7 +2445,7 @@ public bool AddAsset(string deviceKey, IAsset asset, bool? ignoreTimestamp = nul if (!validationResults.IsValid && _configuration.InputValidationLevel > InputValidationLevel.Ignore) { - if (InvalidAssetAdded != null) InvalidAssetAdded.Invoke(asset, validationResults); + MulticastIsolation.Raise(InvalidAssetAdded, h => h(asset, validationResults)); } MulticastIsolation.Raise(AssetAdded, this, asset, null); @@ -2459,7 +2454,7 @@ public bool AddAsset(string deviceKey, IAsset asset, bool? ignoreTimestamp = nul } else { - if (InvalidAssetAdded != null) InvalidAssetAdded.Invoke(asset, validationResults); + MulticastIsolation.Raise(InvalidAssetAdded, h => h(asset, validationResults)); } } } From 57ab68e98b4c248bd6935829e87a831c2b41d08e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Otto=20Boly=C3=B3s?= Date: Wed, 3 Jun 2026 12:59:52 +0200 Subject: [PATCH 28/37] fix(common): apply multicast isolation to MTConnectAgentBroker custom-delegate events --- .../Agents/MTConnectAgentBroker.cs | 44 +++++++++---------- 1 file changed, 22 insertions(+), 22 deletions(-) diff --git a/libraries/MTConnect.NET-Common/Agents/MTConnectAgentBroker.cs b/libraries/MTConnect.NET-Common/Agents/MTConnectAgentBroker.cs index 255c6395..dbd8604b 100644 --- a/libraries/MTConnect.NET-Common/Agents/MTConnectAgentBroker.cs +++ b/libraries/MTConnect.NET-Common/Agents/MTConnectAgentBroker.cs @@ -662,7 +662,7 @@ private static string FormatHeaderVersion(Version mtconnectVersion) /// MTConnectDevices Response Document public IDevicesResponseDocument GetDevicesResponseDocument(Version mtconnectVersion = null, string deviceType = null) { - DevicesRequestReceived?.Invoke(null); + MulticastIsolation.Raise(DevicesRequestReceived, h => h(null)); var version = mtconnectVersion != null ? mtconnectVersion : MTConnectVersion; @@ -675,7 +675,7 @@ public IDevicesResponseDocument GetDevicesResponseDocument(Version mtconnectVers doc.Header = GetDevicesHeader(version); doc.Devices = ProcessDevices(devices, version); - DevicesResponseSent?.Invoke(doc); + MulticastIsolation.Raise(DevicesResponseSent, h => h(doc)); return doc; } @@ -691,7 +691,7 @@ public IDevicesResponseDocument GetDevicesResponseDocument(Version mtconnectVers /// MTConnectDevices Response Document public IDevicesResponseDocument GetDevicesResponseDocument(string deviceKey, Version mtconnectVersion = null) { - DevicesRequestReceived?.Invoke(deviceKey); + MulticastIsolation.Raise(DevicesRequestReceived, h => h(deviceKey)); if (!string.IsNullOrEmpty(deviceKey)) { @@ -706,7 +706,7 @@ public IDevicesResponseDocument GetDevicesResponseDocument(string deviceKey, Ver doc.Header = GetDevicesHeader(version); doc.Devices = ProcessDevices(new List { device }, version); - DevicesResponseSent?.Invoke(doc); + MulticastIsolation.Raise(DevicesResponseSent, h => h(doc)); return doc; } @@ -756,7 +756,7 @@ private IObservationBufferResults GetObservations(IEnumerable bufferKeys, u /// MTConnectStreams Response Document public IStreamsResponseOutputDocument GetDeviceStreamsResponseDocument(uint count = 0, Version mtconnectVersion = null, string deviceType = null) { - StreamsRequestReceived?.Invoke(null); + MulticastIsolation.Raise(StreamsRequestReceived, h => h(null)); if (_observationBuffer != null) { @@ -792,7 +792,7 @@ public IStreamsResponseOutputDocument GetDeviceStreamsResponseDocument(uint coun /// MTConnectStreams Response Document public IStreamsResponseOutputDocument GetDeviceStreamsResponseDocument(ulong at, uint count = 0, Version mtconnectVersion = null, string deviceType = null) { - StreamsRequestReceived?.Invoke(null); + MulticastIsolation.Raise(StreamsRequestReceived, h => h(null)); if (_observationBuffer != null) { @@ -828,7 +828,7 @@ public IStreamsResponseOutputDocument GetDeviceStreamsResponseDocument(ulong at, /// MTConnectStreams Response Document public IStreamsResponseOutputDocument GetDeviceStreamsResponseDocument(IEnumerable dataItemIds, uint count = 0, Version mtconnectVersion = null, string deviceType = null) { - StreamsRequestReceived?.Invoke(null); + MulticastIsolation.Raise(StreamsRequestReceived, h => h(null)); if (_observationBuffer != null) { @@ -870,7 +870,7 @@ public IStreamsResponseOutputDocument GetDeviceStreamsResponseDocument(IEnumerab /// MTConnectStreams Response Document public IStreamsResponseOutputDocument GetDeviceStreamsResponseDocument(IEnumerable dataItemIds, ulong at, uint count = 0, Version mtconnectVersion = null, string deviceType = null) { - StreamsRequestReceived?.Invoke(null); + MulticastIsolation.Raise(StreamsRequestReceived, h => h(null)); if (_observationBuffer != null) { @@ -912,7 +912,7 @@ public IStreamsResponseOutputDocument GetDeviceStreamsResponseDocument(IEnumerab /// MTConnectStreams Response Document public IStreamsResponseOutputDocument GetDeviceStreamsResponseDocument(ulong from, ulong to, uint count = 0, Version mtconnectVersion = null, string deviceType = null) { - StreamsRequestReceived?.Invoke(null); + MulticastIsolation.Raise(StreamsRequestReceived, h => h(null)); if (_observationBuffer != null) { @@ -950,7 +950,7 @@ public IStreamsResponseOutputDocument GetDeviceStreamsResponseDocument(ulong fro /// MTConnectStreams Response Document public IStreamsResponseOutputDocument GetDeviceStreamsResponseDocument(IEnumerable dataItemIds, ulong from, ulong to, uint count = 0, Version mtconnectVersion = null, string deviceType = null) { - StreamsRequestReceived?.Invoke(null); + MulticastIsolation.Raise(StreamsRequestReceived, h => h(null)); if (_observationBuffer != null) { @@ -986,7 +986,7 @@ public IStreamsResponseOutputDocument GetDeviceStreamsResponseDocument(IEnumerab /// MTConnectStreams Response Document public IStreamsResponseOutputDocument GetDeviceStreamsResponseDocument(string deviceKey, uint count = 0, Version mtconnectVersion = null) { - StreamsRequestReceived?.Invoke(deviceKey); + MulticastIsolation.Raise(StreamsRequestReceived, h => h(deviceKey)); if (_observationBuffer != null) { @@ -1022,7 +1022,7 @@ public IStreamsResponseOutputDocument GetDeviceStreamsResponseDocument(string de /// MTConnectStreams Response Document public IStreamsResponseOutputDocument GetDeviceStreamsResponseDocument(string deviceKey, ulong at, uint count = 0, Version mtconnectVersion = null) { - StreamsRequestReceived?.Invoke(deviceKey); + MulticastIsolation.Raise(StreamsRequestReceived, h => h(deviceKey)); if (_observationBuffer != null) { @@ -1058,7 +1058,7 @@ public IStreamsResponseOutputDocument GetDeviceStreamsResponseDocument(string de /// MTConnectStreams Response Document public IStreamsResponseOutputDocument GetDeviceStreamsResponseDocument(string deviceKey, IEnumerable dataItemIds, uint count = 0, Version mtconnectVersion = null) { - StreamsRequestReceived?.Invoke(deviceKey); + MulticastIsolation.Raise(StreamsRequestReceived, h => h(deviceKey)); if (_observationBuffer != null) { @@ -1095,7 +1095,7 @@ public IStreamsResponseOutputDocument GetDeviceStreamsResponseDocument(string de /// MTConnectStreams Response Document public IStreamsResponseOutputDocument GetDeviceStreamsResponseDocument(string deviceKey, IEnumerable dataItemIds, ulong at, uint count = 0, Version mtconnectVersion = null) { - StreamsRequestReceived?.Invoke(deviceKey); + MulticastIsolation.Raise(StreamsRequestReceived, h => h(deviceKey)); if (_observationBuffer != null) { @@ -1132,7 +1132,7 @@ public IStreamsResponseOutputDocument GetDeviceStreamsResponseDocument(string de /// MTConnectStreams Response Document public IStreamsResponseOutputDocument GetDeviceStreamsResponseDocument(string deviceKey, ulong from, ulong to, uint count = 0, Version mtconnectVersion = null) { - StreamsRequestReceived?.Invoke(deviceKey); + MulticastIsolation.Raise(StreamsRequestReceived, h => h(deviceKey)); if (_observationBuffer != null) { @@ -1170,7 +1170,7 @@ public IStreamsResponseOutputDocument GetDeviceStreamsResponseDocument(string de /// MTConnectStreams Response Document public IStreamsResponseOutputDocument GetDeviceStreamsResponseDocument(string deviceKey, IEnumerable dataItemIds, ulong from, ulong to, uint count = 0, Version mtconnectVersion = null) { - StreamsRequestReceived?.Invoke(deviceKey); + MulticastIsolation.Raise(StreamsRequestReceived, h => h(deviceKey)); if (_observationBuffer != null) { @@ -1418,7 +1418,7 @@ private IObservationOutput CreateObservation(IDataItem dataItem, ref BufferObser /// MTConnectAssets Response Document public IAssetsResponseDocument GetAssetsResponseDocument(string deviceKey = null, string type = null, bool removed = false, uint count = 0, Version mtconnectVersion = null) { - DeviceAssetsRequestReceived?.Invoke(deviceKey); + MulticastIsolation.Raise(DeviceAssetsRequestReceived, h => h(deviceKey)); if (_assetBuffer != null) { @@ -1452,7 +1452,7 @@ public IAssetsResponseDocument GetAssetsResponseDocument(string deviceKey = null document.Header = header; document.Assets = processedAssets; - AssetsResponseSent?.Invoke(document); + MulticastIsolation.Raise(AssetsResponseSent, h => h(document)); return document; } @@ -1468,7 +1468,7 @@ public IAssetsResponseDocument GetAssetsResponseDocument(string deviceKey = null /// MTConnectAssets Response Document public IAssetsResponseDocument GetAssetsResponseDocument(IEnumerable assetIds, Version mtconnectVersion = null) { - AssetsRequestReceived?.Invoke(assetIds); + MulticastIsolation.Raise(AssetsRequestReceived, h => h(assetIds)); if (_assetBuffer != null) { @@ -1499,7 +1499,7 @@ public IAssetsResponseDocument GetAssetsResponseDocument(IEnumerable ass document.Header = header; document.Assets = processedAssets; - AssetsResponseSent?.Invoke(document); + MulticastIsolation.Raise(AssetsResponseSent, h => h(document)); return document; } @@ -1771,7 +1771,7 @@ public IErrorResponseDocument GetErrorResponseDocument(ErrorCode errorCode, stri new Error(errorCode, value) }; - ErrorResponseSent?.Invoke(doc); + MulticastIsolation.Raise(ErrorResponseSent, h => h(doc)); return doc; } @@ -1792,7 +1792,7 @@ public IErrorResponseDocument GetErrorResponseDocument(IEnumerable error doc.Header = GetErrorHeader(version); doc.Errors = errors != null ? errors.ToList() : null; - ErrorResponseSent?.Invoke(doc); + MulticastIsolation.Raise(ErrorResponseSent, h => h(doc)); return doc; } From 2eaf5d7c0e9ec73dd270c37478cab9dd9abc400b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Otto=20Boly=C3=B3s?= Date: Wed, 3 Jun 2026 15:26:57 +0200 Subject: [PATCH 29/37] docs(common-tests): disambiguate Raise cref between overloads Bare Raise{T} in the class-level summaries of MqttClientMulticastIsolationTests, HttpServerMulticastIsolationTests, and ShdrMulticastIsolationTests is ambiguous between the EventHandler{T} overload (phase 1) and the generic-delegate overload (phase 2b). docfx metadata with TreatWarningsAsErrors=true promotes the resulting CS0419 to a build error, breaking the "Prepare generated docs" CI job. Replace each bare Raise{T} cref with the fully-qualified parameter list that matches the overload the test class exercises. Also disambiguate the Raise(EventHandler, ...) cross-reference in MulticastIsolation.cs line 48 (non-generic overload summary). --- libraries/MTConnect.NET-Common/MulticastIsolation.cs | 2 +- .../MqttClientMulticastIsolationTests.cs | 2 +- .../Servers/HttpServerMulticastIsolationTests.cs | 2 +- tests/MTConnect.NET-SHDR-Tests/ShdrMulticastIsolationTests.cs | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/libraries/MTConnect.NET-Common/MulticastIsolation.cs b/libraries/MTConnect.NET-Common/MulticastIsolation.cs index 9393ebd7..44b95664 100644 --- a/libraries/MTConnect.NET-Common/MulticastIsolation.cs +++ b/libraries/MTConnect.NET-Common/MulticastIsolation.cs @@ -45,7 +45,7 @@ public static void Raise(EventHandler handler, object sender, T arg, } /// - /// Non-generic overload of for + /// Non-generic overload of for /// events that carry no typed payload. /// Same contract: a throwing subscriber cannot starve later subscribers /// and a faulting handler cannot break diff --git a/tests/MTConnect.NET-Common-Tests/MqttClientMulticastIsolationTests.cs b/tests/MTConnect.NET-Common-Tests/MqttClientMulticastIsolationTests.cs index 870a6a05..7a7c3b9a 100644 --- a/tests/MTConnect.NET-Common-Tests/MqttClientMulticastIsolationTests.cs +++ b/tests/MTConnect.NET-Common-Tests/MqttClientMulticastIsolationTests.cs @@ -12,7 +12,7 @@ namespace MTConnect.Tests.Common /// MTConnectMqttClient and MTConnectMqttExpandedClient. Both /// MQTT client classes declare their events as /// or ; after migration all raise sites use - /// / . + /// / . /// These tests verify the isolation guarantee holds for every generic type /// argument surfaced by those events (no MQTT broker is required because the /// helper contract is independent of the originating class). diff --git a/tests/MTConnect.NET-HTTP-Tests/Servers/HttpServerMulticastIsolationTests.cs b/tests/MTConnect.NET-HTTP-Tests/Servers/HttpServerMulticastIsolationTests.cs index 8e7b9f32..6700eb6b 100644 --- a/tests/MTConnect.NET-HTTP-Tests/Servers/HttpServerMulticastIsolationTests.cs +++ b/tests/MTConnect.NET-HTTP-Tests/Servers/HttpServerMulticastIsolationTests.cs @@ -17,7 +17,7 @@ namespace MTConnect.Tests.Http.Servers /// HeartbeatReceived). Both classes declare their events as /// or ; after /// migration all raise sites use - /// / . + /// / . /// /// /// MTConnectHttpResponseHandler is internal abstract; these diff --git a/tests/MTConnect.NET-SHDR-Tests/ShdrMulticastIsolationTests.cs b/tests/MTConnect.NET-SHDR-Tests/ShdrMulticastIsolationTests.cs index 005906ab..2b744a93 100644 --- a/tests/MTConnect.NET-SHDR-Tests/ShdrMulticastIsolationTests.cs +++ b/tests/MTConnect.NET-SHDR-Tests/ShdrMulticastIsolationTests.cs @@ -12,7 +12,7 @@ namespace MTConnect.Tests.Shdr /// Pins the multicast-isolation contract for the delegate shapes used by /// ShdrAdapter and ShdrClient. Both SHDR classes declare /// their events as ; after migration all raise - /// sites use passing null + /// sites use passing null /// as the internalError sink (neither class declares an InternalError /// event). These tests verify the isolation guarantee holds for every generic /// type argument surfaced by those events without requiring a live TCP From 6a32532ac37ba1cf9ec425c9273629ffdbea9b1e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Otto=20Boly=C3=B3s?= Date: Thu, 4 Jun 2026 07:04:50 +0200 Subject: [PATCH 30/37] test(http-tests): repush availability until SampleReceived fires The two `SampleReceived...WhenOneThrows` tests pushed an UNAVAILABLE -> AVAILABLE pair once after the seed Current arrived and then waited 30 s for the streaming sample loop to deliver. On the Windows CI runner the pushed observations can land between two streaming GETs: the prior request closes for the heartbeat round trip and the next one resumes past the pushed sequences, leaving the assertion timing out even though the multicast-isolation behavior is correct. Hoist the push-and-wait into a `WaitForSampleReceivedWithTransitions` helper that repushes every second until either the recorded handler fires or the EventWaitTimeoutMs budget elapses. Each repush is a fresh UNAVAILABLE -> AVAILABLE pair, so a new observation lands in the buffer regardless of where the streaming loop is in its reconnect cycle. The helper preserves the original semantics: a single repush in the happy path returns within ~1 s, and the overall wait still tops out at EventWaitTimeoutMs so a genuinely broken multicast still fails the test. --- .../Clients/MulticastIsolationTests.cs | 26 ++++++++++++++----- 1 file changed, 20 insertions(+), 6 deletions(-) diff --git a/tests/MTConnect.NET-HTTP-Tests/Clients/MulticastIsolationTests.cs b/tests/MTConnect.NET-HTTP-Tests/Clients/MulticastIsolationTests.cs index bc0383ee..aa0c7820 100644 --- a/tests/MTConnect.NET-HTTP-Tests/Clients/MulticastIsolationTests.cs +++ b/tests/MTConnect.NET-HTTP-Tests/Clients/MulticastIsolationTests.cs @@ -188,9 +188,7 @@ public void SampleReceivedFiresForAllSubscribersWhenOneThrows() Assert.That(currentSeed.Wait(EventWaitTimeoutMs), Is.True, "Streamed Current did not deliver the seed before the test could push a sample"); - PushAvailabilityTransition(); - - Assert.That(recorded.Wait(EventWaitTimeoutMs), Is.True, + Assert.That(WaitForSampleReceivedWithTransitions(recorded), Is.True, "subscribers after a throwing one must still receive SampleReceived"); } finally @@ -224,9 +222,7 @@ public void InternalErrorHandlerThrowingDoesNotBreakSampleReceivedFanOut() Assert.That(currentSeed.Wait(EventWaitTimeoutMs), Is.True, "Streamed Current did not deliver the seed before the test could push a sample"); - PushAvailabilityTransition(); - - Assert.That(recorded.Wait(EventWaitTimeoutMs), Is.True, + Assert.That(WaitForSampleReceivedWithTransitions(recorded), Is.True, "InternalError throwing must not break the SampleReceived fan-out"); } finally @@ -247,6 +243,24 @@ private void PushAvailabilityTransition() AgentRunner.Agent.AddObservation(DeviceUuid, AvailabilityDataItemId, Availability.AVAILABLE); } + // On heavily-loaded CI hosts (notably Windows runners) the streaming + // sample loop can race against the first PushAvailabilityTransition: the + // pushed observations land in the buffer just as the prior streaming GET + // closes for the heartbeat round trip, and the next GET starts past + // those sequences. Repush every second so a fresh transition is in the + // buffer regardless of where the streaming loop is in its reconnect + // cycle, and abort early as soon as the recorded handler fires. + private bool WaitForSampleReceivedWithTransitions(ManualResetEventSlim recorded) + { + var deadline = DateTime.UtcNow.AddMilliseconds(EventWaitTimeoutMs); + while (DateTime.UtcNow < deadline) + { + PushAvailabilityTransition(); + if (recorded.Wait(1000)) return true; + } + return false; + } + // --------------------------------------------------------------------- // ObservationReceived From 7b4369e9d510ed7addd16efa8e7a18cb99c4d784 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Otto=20Boly=C3=B3s?= Date: Sat, 6 Jun 2026 02:40:44 +0200 Subject: [PATCH 31/37] refactor(common): expose MulticastIsolation.Raise as extension methods Make the typed EventHandler and EventHandler overloads extension methods and default internalError to null, so call sites read as MyEvent.Raise(this, args); MyEvent.Raise(this, args, InternalError); instead of MulticastIsolation.Raise(MyEvent, this, args, InternalError); The generic-delegate overload keeps its static-call form: extension-method syntax does not compose cleanly with a where TDelegate : Delegate constraint at the call site. --- .../MulticastIsolation.cs | 32 +++++++++++++++---- 1 file changed, 26 insertions(+), 6 deletions(-) diff --git a/libraries/MTConnect.NET-Common/MulticastIsolation.cs b/libraries/MTConnect.NET-Common/MulticastIsolation.cs index 44b95664..603000a5 100644 --- a/libraries/MTConnect.NET-Common/MulticastIsolation.cs +++ b/libraries/MTConnect.NET-Common/MulticastIsolation.cs @@ -15,6 +15,15 @@ namespace MTConnect /// secondary fault is terminal and swallowed — there is no further sink to /// route it to without risking the same starvation loop. /// + /// + /// The typed and + /// overloads are exposed as extension methods so call sites read as + /// MyEvent.Raise(this, arg) rather than + /// MulticastIsolation.Raise(MyEvent, this, arg). The generic-delegate + /// overload remains a regular static call: extension-method syntax does + /// not compose cleanly with a where TDelegate : Delegate constraint + /// at the call site. + /// public static class MulticastIsolation { /// @@ -24,10 +33,16 @@ public static class MulticastIsolation /// subsequent subscribers. The handler /// itself is iterated with the same per-delegate try/catch so a /// throwing fault-reporter cannot starve later fault subscribers - /// either. + /// either. Exposed as an extension method so call sites read as + /// MyEvent.Raise(this, arg). /// - public static void Raise(EventHandler handler, object sender, T arg, - EventHandler internalError) + /// The event payload type. + /// The event handler whose invocation list is iterated; a null handler is a safe no-op covering the no-subscriber case. + /// The sender object passed to each subscriber and to when routing a fault. + /// The typed event payload passed to each subscriber. + /// The fault-routing sink. Each subscriber fault is routed through every delegate on this sink; pass null (or omit) to swallow faults at the per-delegate boundary. + public static void Raise(this EventHandler handler, object sender, T arg, + EventHandler internalError = null) { if (handler == null) return; @@ -49,10 +64,15 @@ public static void Raise(EventHandler handler, object sender, T arg, /// events that carry no typed payload. /// Same contract: a throwing subscriber cannot starve later subscribers /// and a faulting handler cannot break - /// the fan-out either. + /// the fan-out either. Exposed as an extension method so call sites + /// read as MyEvent.Raise(this, EventArgs.Empty). /// - public static void Raise(EventHandler handler, object sender, EventArgs arg, - EventHandler internalError) + /// The event handler whose invocation list is iterated; a null handler is a safe no-op covering the no-subscriber case. + /// The sender object passed to each subscriber and to when routing a fault. + /// The event payload passed to each subscriber. + /// The fault-routing sink. Each subscriber fault is routed through every delegate on this sink; pass null (or omit) to swallow faults at the per-delegate boundary. + public static void Raise(this EventHandler handler, object sender, EventArgs arg, + EventHandler internalError = null) { if (handler == null) return; From 20e61fc7c5546fe7f0cccaf2b70eec49a124014e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Otto=20Boly=C3=B3s?= Date: Sat, 6 Jun 2026 02:41:42 +0200 Subject: [PATCH 32/37] refactor(http-clients): adopt extension-method Raise syntax Switch every typed MulticastIsolation.Raise(...) call in the HTTP client surface to the new extension-method form (event.Raise(this, args, sink)). No behavioural change. --- .../Clients/MTConnectHttpAssetClient.cs | 22 +-- .../Clients/MTConnectHttpClient.cs | 150 +++++++++--------- .../Clients/MTConnectHttpClientStream.cs | 24 +-- .../Clients/MTConnectHttpCurrentClient.cs | 22 +-- .../Clients/MTConnectHttpProbeClient.cs | 22 +-- .../Clients/MTConnectHttpSampleClient.cs | 22 +-- 6 files changed, 131 insertions(+), 131 deletions(-) diff --git a/libraries/MTConnect.NET-HTTP/Clients/MTConnectHttpAssetClient.cs b/libraries/MTConnect.NET-HTTP/Clients/MTConnectHttpAssetClient.cs index 41bf9aa5..4c734d94 100644 --- a/libraries/MTConnect.NET-HTTP/Clients/MTConnectHttpAssetClient.cs +++ b/libraries/MTConnect.NET-HTTP/Clients/MTConnectHttpAssetClient.cs @@ -192,16 +192,16 @@ public IAssetsResponseDocument Get() } catch (TaskCanceledException ex) when (ex.InnerException is TimeoutException) { - MulticastIsolation.Raise(ConnectionError, this, ex, InternalError); + ConnectionError.Raise(this, ex, InternalError); } catch (TaskCanceledException) { /* Ignore Task Cancelled */ } catch (HttpRequestException ex) { - MulticastIsolation.Raise(ConnectionError, this, ex, InternalError); + ConnectionError.Raise(this, ex, InternalError); } catch (Exception ex) { - MulticastIsolation.Raise(InternalError, this, ex, InternalError); + InternalError.Raise(this, ex, InternalError); } return null; @@ -246,16 +246,16 @@ public async Task GetAsync(CancellationToken cancellati } catch (TaskCanceledException ex) when (ex.InnerException is TimeoutException) { - MulticastIsolation.Raise(ConnectionError, this, ex, InternalError); + ConnectionError.Raise(this, ex, InternalError); } catch (TaskCanceledException) { /* Ignore Task Cancelled */ } catch (HttpRequestException ex) { - MulticastIsolation.Raise(ConnectionError, this, ex, InternalError); + ConnectionError.Raise(this, ex, InternalError); } catch (Exception ex) { - MulticastIsolation.Raise(InternalError, this, ex, InternalError); + InternalError.Raise(this, ex, InternalError); } return null; @@ -346,7 +346,7 @@ private IAssetsResponseDocument HandleResponse(HttpResponseMessage response) { if (!response.IsSuccessStatusCode) { - MulticastIsolation.Raise(ConnectionError, this, new Exception(response.ReasonPhrase), InternalError); + ConnectionError.Raise(this, new Exception(response.ReasonPhrase), InternalError); } else if (response.Content != null) { @@ -364,7 +364,7 @@ private async Task HandleResponseAsync(HttpResponseMess { if (!response.IsSuccessStatusCode) { - MulticastIsolation.Raise(ConnectionError, this, new Exception(response.ReasonPhrase), InternalError); + ConnectionError.Raise(this, new Exception(response.ReasonPhrase), InternalError); } else if (response.Content != null) { @@ -407,19 +407,19 @@ private IAssetsResponseDocument ReadDocument(HttpResponseMessage response, Strea if (errorFormatResult.Success) { var errorDocument = errorFormatResult.Content; - if (errorDocument != null) MulticastIsolation.Raise(MTConnectError, this, errorDocument, InternalError); + if (errorDocument != null) MTConnectError.Raise(this, errorDocument, InternalError); } else { // Raise Format Error - MulticastIsolation.Raise(FormatError, this, errorFormatResult, InternalError); + FormatError.Raise(this, errorFormatResult, InternalError); } } } else { // Raise Format Error - MulticastIsolation.Raise(FormatError, this, formatResult, InternalError); + FormatError.Raise(this, formatResult, InternalError); } } diff --git a/libraries/MTConnect.NET-HTTP/Clients/MTConnectHttpClient.cs b/libraries/MTConnect.NET-HTTP/Clients/MTConnectHttpClient.cs index 303c9d42..caa38aea 100644 --- a/libraries/MTConnect.NET-HTTP/Clients/MTConnectHttpClient.cs +++ b/libraries/MTConnect.NET-HTTP/Clients/MTConnectHttpClient.cs @@ -347,7 +347,7 @@ public void Start() { _stop = new CancellationTokenSource(); - MulticastIsolation.Raise(ClientStarting, this, EventArgs.Empty, InternalError); + ClientStarting.Raise(this, EventArgs.Empty, InternalError); _initializeFromBuffer = false; _lastInstanceId = 0; @@ -364,7 +364,7 @@ public void Start(string path) { _stop = new CancellationTokenSource(); - MulticastIsolation.Raise(ClientStarting, this, EventArgs.Empty, InternalError); + ClientStarting.Raise(this, EventArgs.Empty, InternalError); _initializeFromBuffer = false; _lastInstanceId = 0; @@ -382,7 +382,7 @@ public void Start(CancellationToken cancellationToken, string path = null) _stop = new CancellationTokenSource(); cancellationToken.Register(() => { Stop(); }); - MulticastIsolation.Raise(ClientStarting, this, EventArgs.Empty, InternalError); + ClientStarting.Raise(this, EventArgs.Empty, InternalError); _initializeFromBuffer = false; _lastInstanceId = 0; @@ -399,7 +399,7 @@ public void StartFromSequence(ulong instanceId, ulong sequence, string path = nu { _stop = new CancellationTokenSource(); - MulticastIsolation.Raise(ClientStarting, this, EventArgs.Empty, InternalError); + ClientStarting.Raise(this, EventArgs.Empty, InternalError); _initializeFromBuffer = true; _lastInstanceId = instanceId; @@ -417,7 +417,7 @@ public void StartFromSequence(ulong instanceId, ulong sequence, CancellationToke _stop = new CancellationTokenSource(); cancellationToken.Register(() => { Stop(); }); - MulticastIsolation.Raise(ClientStarting, this, EventArgs.Empty, InternalError); + ClientStarting.Raise(this, EventArgs.Empty, InternalError); _initializeFromBuffer = true; _lastInstanceId = instanceId; @@ -434,7 +434,7 @@ public void StartFromBuffer(string path = null) { _stop = new CancellationTokenSource(); - MulticastIsolation.Raise(ClientStarting, this, EventArgs.Empty, InternalError); + ClientStarting.Raise(this, EventArgs.Empty, InternalError); _initializeFromBuffer = true; _lastInstanceId = 0; @@ -452,7 +452,7 @@ public void StartFromBuffer(CancellationToken cancellationToken, string path = n _stop = new CancellationTokenSource(); cancellationToken.Register(() => { Stop(); }); - MulticastIsolation.Raise(ClientStarting, this, EventArgs.Empty, InternalError); + ClientStarting.Raise(this, EventArgs.Empty, InternalError); _initializeFromBuffer = true; _lastInstanceId = 0; @@ -467,7 +467,7 @@ public void StartFromBuffer(CancellationToken cancellationToken, string path = n /// public void Stop() { - MulticastIsolation.Raise(ClientStopping, this, EventArgs.Empty, InternalError); + ClientStopping.Raise(this, EventArgs.Empty, InternalError); if (_stop != null) _stop.Cancel(); } @@ -494,10 +494,10 @@ public IDevicesResponseDocument GetProbe() client.Timeout = Timeout; client.ContentEncodings = ContentEncodings; client.ContentType = ContentType; - client.MTConnectError += (s, doc) => MulticastIsolation.Raise(MTConnectError, this, doc, InternalError); - client.FormatError += (s, r) => MulticastIsolation.Raise(FormatError, this, r, InternalError); - client.ConnectionError += (s, ex) => MulticastIsolation.Raise(ConnectionError, this, ex, InternalError); - client.InternalError += (s, ex) => MulticastIsolation.Raise(InternalError, this, ex, InternalError); + client.MTConnectError += (s, doc) => MTConnectError.Raise(this, doc, InternalError); + client.FormatError += (s, r) => FormatError.Raise(this, r, InternalError); + client.ConnectionError += (s, ex) => ConnectionError.Raise(this, ex, InternalError); + client.InternalError += (s, ex) => InternalError.Raise(this, ex, InternalError); return client.Get(); } @@ -518,10 +518,10 @@ public async Task GetProbeAsync(CancellationToken canc client.Timeout = Timeout; client.ContentEncodings = ContentEncodings; client.ContentType = ContentType; - client.MTConnectError += (s, doc) => MulticastIsolation.Raise(MTConnectError, this, doc, InternalError); - client.FormatError += (s, r) => MulticastIsolation.Raise(FormatError, this, r, InternalError); - client.ConnectionError += (s, ex) => MulticastIsolation.Raise(ConnectionError, this, ex, InternalError); - client.InternalError += (s, ex) => MulticastIsolation.Raise(InternalError, this, ex, InternalError); + client.MTConnectError += (s, doc) => MTConnectError.Raise(this, doc, InternalError); + client.FormatError += (s, r) => FormatError.Raise(this, r, InternalError); + client.ConnectionError += (s, ex) => ConnectionError.Raise(this, ex, InternalError); + client.InternalError += (s, ex) => InternalError.Raise(this, ex, InternalError); return await client.GetAsync(cancellationToken); } @@ -535,10 +535,10 @@ public IStreamsResponseDocument GetCurrent(long at = 0, string path = null) client.Timeout = Timeout; client.ContentEncodings = ContentEncodings; client.ContentType = ContentType; - client.MTConnectError += (s, doc) => MulticastIsolation.Raise(MTConnectError, this, doc, InternalError); - client.FormatError += (s, r) => MulticastIsolation.Raise(FormatError, this, r, InternalError); - client.ConnectionError += (s, ex) => MulticastIsolation.Raise(ConnectionError, this, ex, InternalError); - client.InternalError += (s, ex) => MulticastIsolation.Raise(InternalError, this, ex, InternalError); + client.MTConnectError += (s, doc) => MTConnectError.Raise(this, doc, InternalError); + client.FormatError += (s, r) => FormatError.Raise(this, r, InternalError); + client.ConnectionError += (s, ex) => ConnectionError.Raise(this, ex, InternalError); + client.InternalError += (s, ex) => InternalError.Raise(this, ex, InternalError); return client.Get(); } @@ -559,10 +559,10 @@ public async Task GetCurrentAsync(CancellationToken ca client.Timeout = Timeout; client.ContentEncodings = ContentEncodings; client.ContentType = ContentType; - client.MTConnectError += (s, doc) => MulticastIsolation.Raise(MTConnectError, this, doc, InternalError); - client.FormatError += (s, r) => MulticastIsolation.Raise(FormatError, this, r, InternalError); - client.ConnectionError += (s, ex) => MulticastIsolation.Raise(ConnectionError, this, ex, InternalError); - client.InternalError += (s, ex) => MulticastIsolation.Raise(InternalError, this, ex, InternalError); + client.MTConnectError += (s, doc) => MTConnectError.Raise(this, doc, InternalError); + client.FormatError += (s, r) => FormatError.Raise(this, r, InternalError); + client.ConnectionError += (s, ex) => ConnectionError.Raise(this, ex, InternalError); + client.InternalError += (s, ex) => InternalError.Raise(this, ex, InternalError); return await client.GetAsync(cancellationToken); } @@ -576,10 +576,10 @@ public IStreamsResponseDocument GetSample(long from = 0, long to = 0, int count client.Timeout = Timeout; client.ContentEncodings = ContentEncodings; client.ContentType = ContentType; - client.MTConnectError += (s, doc) => MulticastIsolation.Raise(MTConnectError, this, doc, InternalError); - client.FormatError += (s, r) => MulticastIsolation.Raise(FormatError, this, r, InternalError); - client.ConnectionError += (s, ex) => MulticastIsolation.Raise(ConnectionError, this, ex, InternalError); - client.InternalError += (s, ex) => MulticastIsolation.Raise(InternalError, this, ex, InternalError); + client.MTConnectError += (s, doc) => MTConnectError.Raise(this, doc, InternalError); + client.FormatError += (s, r) => FormatError.Raise(this, r, InternalError); + client.ConnectionError += (s, ex) => ConnectionError.Raise(this, ex, InternalError); + client.InternalError += (s, ex) => InternalError.Raise(this, ex, InternalError); return client.Get(); } @@ -600,10 +600,10 @@ public async Task GetSampleAsync(CancellationToken can client.Timeout = Timeout; client.ContentEncodings = ContentEncodings; client.ContentType = ContentType; - client.MTConnectError += (s, doc) => MulticastIsolation.Raise(MTConnectError, this, doc, InternalError); - client.FormatError += (s, r) => MulticastIsolation.Raise(FormatError, this, r, InternalError); - client.ConnectionError += (s, ex) => MulticastIsolation.Raise(ConnectionError, this, ex, InternalError); - client.InternalError += (s, ex) => MulticastIsolation.Raise(InternalError, this, ex, InternalError); + client.MTConnectError += (s, doc) => MTConnectError.Raise(this, doc, InternalError); + client.FormatError += (s, r) => FormatError.Raise(this, r, InternalError); + client.ConnectionError += (s, ex) => ConnectionError.Raise(this, ex, InternalError); + client.InternalError += (s, ex) => InternalError.Raise(this, ex, InternalError); return await client.GetAsync(cancellationToken); } @@ -617,10 +617,10 @@ public IAssetsResponseDocument GetAssets(long count = 100) client.Timeout = Timeout; client.ContentEncodings = ContentEncodings; client.ContentType = ContentType; - client.MTConnectError += (s, doc) => MulticastIsolation.Raise(MTConnectError, this, doc, InternalError); - client.FormatError += (s, r) => MulticastIsolation.Raise(FormatError, this, r, InternalError); - client.ConnectionError += (s, ex) => MulticastIsolation.Raise(ConnectionError, this, ex, InternalError); - client.InternalError += (s, ex) => MulticastIsolation.Raise(InternalError, this, ex, InternalError); + client.MTConnectError += (s, doc) => MTConnectError.Raise(this, doc, InternalError); + client.FormatError += (s, r) => FormatError.Raise(this, r, InternalError); + client.ConnectionError += (s, ex) => ConnectionError.Raise(this, ex, InternalError); + client.InternalError += (s, ex) => InternalError.Raise(this, ex, InternalError); return client.Get(); } @@ -641,10 +641,10 @@ public async Task GetAssetsAsync(CancellationToken canc client.Timeout = Timeout; client.ContentEncodings = ContentEncodings; client.ContentType = ContentType; - client.MTConnectError += (s, doc) => MulticastIsolation.Raise(MTConnectError, this, doc, InternalError); - client.FormatError += (s, r) => MulticastIsolation.Raise(FormatError, this, r, InternalError); - client.ConnectionError += (s, ex) => MulticastIsolation.Raise(ConnectionError, this, ex, InternalError); - client.InternalError += (s, ex) => MulticastIsolation.Raise(InternalError, this, ex, InternalError); + client.MTConnectError += (s, doc) => MTConnectError.Raise(this, doc, InternalError); + client.FormatError += (s, r) => FormatError.Raise(this, r, InternalError); + client.ConnectionError += (s, ex) => ConnectionError.Raise(this, ex, InternalError); + client.InternalError += (s, ex) => InternalError.Raise(this, ex, InternalError); return await client.GetAsync(cancellationToken); } @@ -658,10 +658,10 @@ public IAssetsResponseDocument GetAsset(string assetId) client.Timeout = Timeout; client.ContentEncodings = ContentEncodings; client.ContentType = ContentType; - client.MTConnectError += (s, doc) => MulticastIsolation.Raise(MTConnectError, this, doc, InternalError); - client.FormatError += (s, r) => MulticastIsolation.Raise(FormatError, this, r, InternalError); - client.ConnectionError += (s, ex) => MulticastIsolation.Raise(ConnectionError, this, ex, InternalError); - client.InternalError += (s, ex) => MulticastIsolation.Raise(InternalError, this, ex, InternalError); + client.MTConnectError += (s, doc) => MTConnectError.Raise(this, doc, InternalError); + client.FormatError += (s, r) => FormatError.Raise(this, r, InternalError); + client.ConnectionError += (s, ex) => ConnectionError.Raise(this, ex, InternalError); + client.InternalError += (s, ex) => InternalError.Raise(this, ex, InternalError); return client.Get(); } @@ -682,10 +682,10 @@ public async Task GetAssetAsync(string assetId, Cancell client.Timeout = Timeout; client.ContentEncodings = ContentEncodings; client.ContentType = ContentType; - client.MTConnectError += (s, doc) => MulticastIsolation.Raise(MTConnectError, this, doc, InternalError); - client.FormatError += (s, r) => MulticastIsolation.Raise(FormatError, this, r, InternalError); - client.ConnectionError += (s, ex) => MulticastIsolation.Raise(ConnectionError, this, ex, InternalError); - client.InternalError += (s, ex) => MulticastIsolation.Raise(InternalError, this, ex, InternalError); + client.MTConnectError += (s, doc) => MTConnectError.Raise(this, doc, InternalError); + client.FormatError += (s, r) => FormatError.Raise(this, r, InternalError); + client.ConnectionError += (s, ex) => ConnectionError.Raise(this, ex, InternalError); + client.InternalError += (s, ex) => InternalError.Raise(this, ex, InternalError); return await client.GetAsync(cancellationToken); } @@ -697,7 +697,7 @@ private async Task Worker() { var initialRequest = true; - MulticastIsolation.Raise(ClientStarted, this, EventArgs.Empty, InternalError); + ClientStarted.Raise(this, EventArgs.Empty, InternalError); do { @@ -708,7 +708,7 @@ private async Task Worker() if (probe != null) { _lastResponse = UnixDateTime.Now; - MulticastIsolation.Raise(ResponseReceived, this, EventArgs.Empty, InternalError); + ResponseReceived.Raise(this, EventArgs.Empty, InternalError); ProcessProbeDocument(probe); @@ -719,9 +719,9 @@ private async Task Worker() if (assets != null) { _lastResponse = UnixDateTime.Now; - MulticastIsolation.Raise(ResponseReceived, this, EventArgs.Empty, InternalError); + ResponseReceived.Raise(this, EventArgs.Empty, InternalError); - MulticastIsolation.Raise(AssetsReceived, this, assets, InternalError); + AssetsReceived.Raise(this, assets, InternalError); } } @@ -732,7 +732,7 @@ private async Task Worker() if (current != null) { _lastResponse = UnixDateTime.Now; - MulticastIsolation.Raise(ResponseReceived, this, EventArgs.Empty, InternalError); + ResponseReceived.Raise(this, EventArgs.Empty, InternalError); // Raise CurrentReceived Event ProcessCurrentDocument(current, _stop.Token); @@ -777,15 +777,15 @@ private async Task Worker() _stream.Timeout = Heartbeat * 3; _stream.ContentEncodings = ContentEncodings; _stream.ContentType = ContentType; - _stream.Starting += (s, o) => MulticastIsolation.Raise(StreamStarting, this, url, InternalError); - _stream.Started += (s, o) => MulticastIsolation.Raise(StreamStarted, this, url, InternalError); - _stream.Stopping += (s, o) => MulticastIsolation.Raise(StreamStopping, this, url, InternalError); - _stream.Stopped += (s, o) => MulticastIsolation.Raise(StreamStopped, this, url, InternalError); + _stream.Starting += (s, o) => StreamStarting.Raise(this, url, InternalError); + _stream.Started += (s, o) => StreamStarted.Raise(this, url, InternalError); + _stream.Stopping += (s, o) => StreamStopping.Raise(this, url, InternalError); + _stream.Stopped += (s, o) => StreamStopped.Raise(this, url, InternalError); _stream.DocumentReceived += (s, doc) => ProcessSampleDocument(doc, _stop.Token); _stream.ErrorReceived += (s, doc) => ProcessSampleError(doc); - _stream.FormatError += (s, r) => MulticastIsolation.Raise(FormatError, this, r, InternalError); - _stream.ConnectionError += (s, ex) => MulticastIsolation.Raise(ConnectionError, this, ex, InternalError); - _stream.InternalError += (s, ex) => MulticastIsolation.Raise(InternalError, this, ex, InternalError); + _stream.FormatError += (s, r) => FormatError.Raise(this, r, InternalError); + _stream.ConnectionError += (s, ex) => ConnectionError.Raise(this, ex, InternalError); + _stream.InternalError += (s, ex) => InternalError.Raise(this, ex, InternalError); // Run Stream (Blocking call) await _stream.Run(_stop.Token); @@ -812,7 +812,7 @@ private async Task Worker() if (current != null) { _lastResponse = UnixDateTime.Now; - MulticastIsolation.Raise(ResponseReceived, this, EventArgs.Empty, InternalError); + ResponseReceived.Raise(this, EventArgs.Empty, InternalError); // Raise CurrentReceived Event ProcessCurrentDocument(current, _stop.Token); @@ -871,12 +871,12 @@ private async Task Worker() catch (TaskCanceledException) { } catch (Exception ex) { - MulticastIsolation.Raise(InternalError, this, ex, InternalError); + InternalError.Raise(this, ex, InternalError); } } while (!_stop.Token.IsCancellationRequested); - MulticastIsolation.Raise(ClientStopped, this, EventArgs.Empty, InternalError); + ClientStopped.Raise(this, EventArgs.Empty, InternalError); } private void ProcessProbeDocument(IDevicesResponseDocument document) @@ -907,18 +907,18 @@ private void ProcessProbeDocument(IDevicesResponseDocument document) // Isolate subscriber exceptions per delegate so one bad handler cannot abort the // populate loop, suppress ProbeReceived, or short-circuit later subscribers in the // invocation list; route each fault through InternalError instead. - MulticastIsolation.Raise(DeviceReceived, this, outputDevice, InternalError); + DeviceReceived.Raise(this, outputDevice, InternalError); } // Raise ProbeReceived Event - MulticastIsolation.Raise(ProbeReceived, this, document, InternalError); + ProbeReceived.Raise(this, document, InternalError); } } private void ProcessCurrentDocument(IStreamsResponseDocument document, CancellationToken cancel) { _lastResponse = UnixDateTime.Now; - MulticastIsolation.Raise(ResponseReceived, this, EventArgs.Empty, InternalError); + ResponseReceived.Raise(this, EventArgs.Empty, InternalError); if (document != null) { @@ -936,7 +936,7 @@ private void ProcessCurrentDocument(IStreamsResponseDocument document, Cancellat response.Streams = deviceStreams; - MulticastIsolation.Raise(CurrentReceived, this, response, InternalError); + CurrentReceived.Raise(this, response, InternalError); // Process Device Streams @@ -951,7 +951,7 @@ private void ProcessCurrentDocument(IStreamsResponseDocument document, Cancellat { foreach (var observation in observations) { - MulticastIsolation.Raise(ObservationReceived, this, observation, InternalError); + ObservationReceived.Raise(this, observation, InternalError); } } } @@ -962,7 +962,7 @@ private void ProcessCurrentDocument(IStreamsResponseDocument document, Cancellat private void ProcessSampleDocument(IStreamsResponseDocument document, CancellationToken cancel) { _lastResponse = UnixDateTime.Now; - MulticastIsolation.Raise(ResponseReceived, this, EventArgs.Empty, InternalError); + ResponseReceived.Raise(this, EventArgs.Empty, InternalError); if (document != null) { @@ -1002,11 +1002,11 @@ private void ProcessSampleDocument(IStreamsResponseDocument document, Cancellati } } - MulticastIsolation.Raise(SampleReceived, this, response, InternalError); + SampleReceived.Raise(this, response, InternalError); foreach (var observation in receivedObservations) { - MulticastIsolation.Raise(ObservationReceived, this, observation, InternalError); + ObservationReceived.Raise(this, observation, InternalError); } } } @@ -1194,11 +1194,11 @@ private IComponentStream ProcessComponentStream(IMTConnectStreamsHeader header, private void ProcessSampleError(IErrorResponseDocument document) { _lastResponse = UnixDateTime.Now; - MulticastIsolation.Raise(ResponseReceived, this, EventArgs.Empty, InternalError); + ResponseReceived.Raise(this, EventArgs.Empty, InternalError); if (document != null) { - MulticastIsolation.Raise(MTConnectError, this, document, InternalError); + MTConnectError.Raise(this, document, InternalError); } } @@ -1218,13 +1218,13 @@ private async void CheckAssetChanged(IEnumerable observations, Can var doc = await GetAssetAsync(assetId, cancel); if (doc != null) { - MulticastIsolation.Raise(AssetsReceived, this, doc, InternalError); + AssetsReceived.Raise(this, doc, InternalError); if (doc != null && !doc.Assets.IsNullOrEmpty()) { foreach (var asset in doc.Assets) { - MulticastIsolation.Raise(AssetReceived, this, asset, InternalError); + AssetReceived.Raise(this, asset, InternalError); } } } diff --git a/libraries/MTConnect.NET-HTTP/Clients/MTConnectHttpClientStream.cs b/libraries/MTConnect.NET-HTTP/Clients/MTConnectHttpClientStream.cs index 0830385c..71c27bb2 100644 --- a/libraries/MTConnect.NET-HTTP/Clients/MTConnectHttpClientStream.cs +++ b/libraries/MTConnect.NET-HTTP/Clients/MTConnectHttpClientStream.cs @@ -123,7 +123,7 @@ public void Start(CancellationToken cancellationToken) cancellationToken.Register(() => Stop()); // Raise Starting Event - MulticastIsolation.Raise(Starting, this, EventArgs.Empty, InternalError); + Starting.Raise(this, EventArgs.Empty, InternalError); _ = Task.Run(() => Run(_stop.Token)); } @@ -136,7 +136,7 @@ public void Start(CancellationToken cancellationToken) public void Stop() { // Raise Stopping Event - MulticastIsolation.Raise(Stopping, this, EventArgs.Empty, InternalError); + Stopping.Raise(this, EventArgs.Empty, InternalError); if (_stop != null) _stop.Cancel(); } @@ -167,7 +167,7 @@ public async Task Run(CancellationToken cancellationToken) responseTimer.Elapsed += (o, e) => { stop.Cancel(); - MulticastIsolation.Raise(ConnectionError, this, new TimeoutException($"HTTP Stream Timeout Exceeded ({Timeout})"), InternalError); + ConnectionError.Raise(this, new TimeoutException($"HTTP Stream Timeout Exceeded ({Timeout})"), InternalError); }; responseTimer.Start(); } @@ -177,7 +177,7 @@ public async Task Run(CancellationToken cancellationToken) stop.Token.ThrowIfCancellationRequested(); // Raise Started Event - MulticastIsolation.Raise(Started, this, EventArgs.Empty, InternalError); + Started.Raise(this, EventArgs.Empty, InternalError); // Add 'Accept' HTTP Header @@ -305,16 +305,16 @@ public async Task Run(CancellationToken cancellationToken) } catch (TaskCanceledException ex) when (ex.InnerException is TimeoutException) { - MulticastIsolation.Raise(ConnectionError, this, ex, InternalError); + ConnectionError.Raise(this, ex, InternalError); } catch (TaskCanceledException) { /* Ignore Task Cancelled */ } catch (HttpRequestException ex) { - MulticastIsolation.Raise(ConnectionError, this, ex, InternalError); + ConnectionError.Raise(this, ex, InternalError); } catch (Exception ex) { - MulticastIsolation.Raise(InternalError, this, ex, InternalError); + InternalError.Raise(this, ex, InternalError); } finally { @@ -322,7 +322,7 @@ public async Task Run(CancellationToken cancellationToken) } } - MulticastIsolation.Raise(Stopped, this, EventArgs.Empty, InternalError); + Stopped.Raise(this, EventArgs.Empty, InternalError); } private static string GetHeaderValue(string s, string name) @@ -411,7 +411,7 @@ protected virtual void ProcessResponseBody(Stream responseBody, string contentEn var document = formatResult.Content; if (document != null) { - MulticastIsolation.Raise(DocumentReceived, this, document, InternalError); + DocumentReceived.Raise(this, document, InternalError); } else { @@ -420,19 +420,19 @@ protected virtual void ProcessResponseBody(Stream responseBody, string contentEn if (errorFormatResult.Success) { var errorDocument = errorFormatResult.Content; - if (errorDocument != null) MulticastIsolation.Raise(ErrorReceived, this, errorDocument, InternalError); + if (errorDocument != null) ErrorReceived.Raise(this, errorDocument, InternalError); } else { // Raise Format Error - MulticastIsolation.Raise(FormatError, this, errorFormatResult, InternalError); + FormatError.Raise(this, errorFormatResult, InternalError); } } } else { // Raise Format Error - MulticastIsolation.Raise(FormatError, this, formatResult, InternalError); + FormatError.Raise(this, formatResult, InternalError); } } } diff --git a/libraries/MTConnect.NET-HTTP/Clients/MTConnectHttpCurrentClient.cs b/libraries/MTConnect.NET-HTTP/Clients/MTConnectHttpCurrentClient.cs index 01e606d9..859b13bd 100644 --- a/libraries/MTConnect.NET-HTTP/Clients/MTConnectHttpCurrentClient.cs +++ b/libraries/MTConnect.NET-HTTP/Clients/MTConnectHttpCurrentClient.cs @@ -196,16 +196,16 @@ public IStreamsResponseDocument Get() } catch (TaskCanceledException ex) when (ex.InnerException is TimeoutException) { - MulticastIsolation.Raise(ConnectionError, this, ex, InternalError); + ConnectionError.Raise(this, ex, InternalError); } catch (TaskCanceledException) { /* Ignore Task Cancelled */ } catch (HttpRequestException ex) { - MulticastIsolation.Raise(ConnectionError, this, ex, InternalError); + ConnectionError.Raise(this, ex, InternalError); } catch (Exception ex) { - MulticastIsolation.Raise(InternalError, this, ex, InternalError); + InternalError.Raise(this, ex, InternalError); } return null; @@ -250,16 +250,16 @@ public async Task GetAsync(CancellationToken cancellat } catch (TaskCanceledException ex) when (ex.InnerException is TimeoutException) { - MulticastIsolation.Raise(ConnectionError, this, ex, InternalError); + ConnectionError.Raise(this, ex, InternalError); } catch (TaskCanceledException) { /* Ignore Task Cancelled */ } catch (HttpRequestException ex) { - MulticastIsolation.Raise(ConnectionError, this, ex, InternalError); + ConnectionError.Raise(this, ex, InternalError); } catch (Exception ex) { - MulticastIsolation.Raise(InternalError, this, ex, InternalError); + InternalError.Raise(this, ex, InternalError); } return null; @@ -348,7 +348,7 @@ private IStreamsResponseDocument HandleResponse(HttpResponseMessage response) { if (!response.IsSuccessStatusCode) { - MulticastIsolation.Raise(ConnectionError, this, new Exception(response.ReasonPhrase), InternalError); + ConnectionError.Raise(this, new Exception(response.ReasonPhrase), InternalError); } else if (response.Content != null) { @@ -366,7 +366,7 @@ private async Task HandleResponseAsync(HttpResponseMes { if (!response.IsSuccessStatusCode) { - MulticastIsolation.Raise(ConnectionError, this, new Exception(response.ReasonPhrase), InternalError); + ConnectionError.Raise(this, new Exception(response.ReasonPhrase), InternalError); } else if (response.Content != null) { @@ -408,19 +408,19 @@ private IStreamsResponseDocument ReadDocument(HttpResponseMessage response, Stre if (errorFormatResult.Success) { var errorDocument = errorFormatResult.Content; - if (errorDocument != null) MulticastIsolation.Raise(MTConnectError, this, errorDocument, InternalError); + if (errorDocument != null) MTConnectError.Raise(this, errorDocument, InternalError); } else { // Raise Format Error - MulticastIsolation.Raise(FormatError, this, errorFormatResult, InternalError); + FormatError.Raise(this, errorFormatResult, InternalError); } } } else { // Raise Format Error - MulticastIsolation.Raise(FormatError, this, formatResult, InternalError); + FormatError.Raise(this, formatResult, InternalError); } } diff --git a/libraries/MTConnect.NET-HTTP/Clients/MTConnectHttpProbeClient.cs b/libraries/MTConnect.NET-HTTP/Clients/MTConnectHttpProbeClient.cs index a903e525..af90cebc 100644 --- a/libraries/MTConnect.NET-HTTP/Clients/MTConnectHttpProbeClient.cs +++ b/libraries/MTConnect.NET-HTTP/Clients/MTConnectHttpProbeClient.cs @@ -236,16 +236,16 @@ public IDevicesResponseDocument Get() } catch (TaskCanceledException ex) when (ex.InnerException is TimeoutException) { - MulticastIsolation.Raise(ConnectionError, this, ex, InternalError); + ConnectionError.Raise(this, ex, InternalError); } catch (TaskCanceledException) { /* Ignore Task Cancelled */ } catch (HttpRequestException ex) { - MulticastIsolation.Raise(ConnectionError, this, ex, InternalError); + ConnectionError.Raise(this, ex, InternalError); } catch (Exception ex) { - MulticastIsolation.Raise(InternalError, this, ex, InternalError); + InternalError.Raise(this, ex, InternalError); } return null; @@ -297,16 +297,16 @@ public async Task GetAsync(CancellationToken cancellat } catch (TaskCanceledException ex) when (ex.InnerException is TimeoutException) { - MulticastIsolation.Raise(ConnectionError, this, ex, InternalError); + ConnectionError.Raise(this, ex, InternalError); } catch (TaskCanceledException) { /* Ignore Task Cancelled */ } catch (HttpRequestException ex) { - MulticastIsolation.Raise(ConnectionError, this, ex, InternalError); + ConnectionError.Raise(this, ex, InternalError); } catch (Exception ex) { - MulticastIsolation.Raise(InternalError, this, ex, InternalError); + InternalError.Raise(this, ex, InternalError); } return null; @@ -388,7 +388,7 @@ private IDevicesResponseDocument HandleResponse(HttpResponseMessage response) { if (!response.IsSuccessStatusCode) { - MulticastIsolation.Raise(ConnectionError, this, new Exception(response.ReasonPhrase), InternalError); + ConnectionError.Raise(this, new Exception(response.ReasonPhrase), InternalError); } else if (response.Content != null) { @@ -406,7 +406,7 @@ private async Task HandleResponseAsync(HttpResponseMes { if (!response.IsSuccessStatusCode) { - MulticastIsolation.Raise(ConnectionError, this, new Exception(response.ReasonPhrase), InternalError); + ConnectionError.Raise(this, new Exception(response.ReasonPhrase), InternalError); } else if (response.Content != null) { @@ -448,19 +448,19 @@ private IDevicesResponseDocument ReadDocument(HttpResponseMessage response, Stre if (errorFormatResult.Success) { var errorDocument = errorFormatResult.Content; - if (errorDocument != null) MulticastIsolation.Raise(MTConnectError, this, errorDocument, InternalError); + if (errorDocument != null) MTConnectError.Raise(this, errorDocument, InternalError); } else { // Raise Format Error - MulticastIsolation.Raise(FormatError, this, errorFormatResult, InternalError); + FormatError.Raise(this, errorFormatResult, InternalError); } } } else { // Raise Format Error - MulticastIsolation.Raise(FormatError, this, formatResult, InternalError); + FormatError.Raise(this, formatResult, InternalError); } } diff --git a/libraries/MTConnect.NET-HTTP/Clients/MTConnectHttpSampleClient.cs b/libraries/MTConnect.NET-HTTP/Clients/MTConnectHttpSampleClient.cs index 53b9f321..56ac4990 100644 --- a/libraries/MTConnect.NET-HTTP/Clients/MTConnectHttpSampleClient.cs +++ b/libraries/MTConnect.NET-HTTP/Clients/MTConnectHttpSampleClient.cs @@ -213,16 +213,16 @@ public IStreamsResponseDocument Get() } catch (TaskCanceledException ex) when (ex.InnerException is TimeoutException) { - MulticastIsolation.Raise(ConnectionError, this, ex, InternalError); + ConnectionError.Raise(this, ex, InternalError); } catch (TaskCanceledException) { /* Ignore Task Cancelled */ } catch (HttpRequestException ex) { - MulticastIsolation.Raise(ConnectionError, this, ex, InternalError); + ConnectionError.Raise(this, ex, InternalError); } catch (Exception ex) { - MulticastIsolation.Raise(InternalError, this, ex, InternalError); + InternalError.Raise(this, ex, InternalError); } return null; @@ -267,16 +267,16 @@ public async Task GetAsync(CancellationToken cancel) } catch (TaskCanceledException ex) when (ex.InnerException is TimeoutException) { - MulticastIsolation.Raise(ConnectionError, this, ex, InternalError); + ConnectionError.Raise(this, ex, InternalError); } catch (TaskCanceledException) { /* Ignore Task Cancelled */ } catch (HttpRequestException ex) { - MulticastIsolation.Raise(ConnectionError, this, ex, InternalError); + ConnectionError.Raise(this, ex, InternalError); } catch (Exception ex) { - MulticastIsolation.Raise(InternalError, this, ex, InternalError); + InternalError.Raise(this, ex, InternalError); } return null; @@ -382,7 +382,7 @@ private IStreamsResponseDocument HandleResponse(HttpResponseMessage response) { if (!response.IsSuccessStatusCode) { - MulticastIsolation.Raise(ConnectionError, this, new Exception(response.ReasonPhrase), InternalError); + ConnectionError.Raise(this, new Exception(response.ReasonPhrase), InternalError); } else if (response.Content != null) { @@ -400,7 +400,7 @@ private async Task HandleResponseAsync(HttpResponseMes { if (!response.IsSuccessStatusCode) { - MulticastIsolation.Raise(ConnectionError, this, new Exception(response.ReasonPhrase), InternalError); + ConnectionError.Raise(this, new Exception(response.ReasonPhrase), InternalError); } else if (response.Content != null) { @@ -442,19 +442,19 @@ private IStreamsResponseDocument ReadDocument(HttpResponseMessage response, Stre if (errorFormatResult.Success) { var errorDocument = errorFormatResult.Content; - if (errorDocument != null) MulticastIsolation.Raise(MTConnectError, this, errorDocument, InternalError); + if (errorDocument != null) MTConnectError.Raise(this, errorDocument, InternalError); } else { // Raise Format Error - MulticastIsolation.Raise(FormatError, this, errorFormatResult, InternalError); + FormatError.Raise(this, errorFormatResult, InternalError); } } } else { // Raise Format Error - MulticastIsolation.Raise(FormatError, this, formatResult, InternalError); + FormatError.Raise(this, formatResult, InternalError); } } From 7250895ccb9f2e5307dee17e9008cd24079332b8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Otto=20Boly=C3=B3s?= Date: Sat, 6 Jun 2026 02:42:26 +0200 Subject: [PATCH 33/37] refactor(http-servers): adopt extension-method Raise syntax Switch every typed MulticastIsolation.Raise(...) call in the HTTP server surface to the new extension-method form (event.Raise(this, args, sink)). No behavioural change. --- .../Servers/MTConnectHttpResponseHandler.cs | 14 +++++++------- .../Servers/MTConnectHttpServerStream.cs | 10 +++++----- 2 files changed, 12 insertions(+), 12 deletions(-) diff --git a/libraries/MTConnect.NET-HTTP/Servers/MTConnectHttpResponseHandler.cs b/libraries/MTConnect.NET-HTTP/Servers/MTConnectHttpResponseHandler.cs index 6ca5b1c6..bad3ddd0 100644 --- a/libraries/MTConnect.NET-HTTP/Servers/MTConnectHttpResponseHandler.cs +++ b/libraries/MTConnect.NET-HTTP/Servers/MTConnectHttpResponseHandler.cs @@ -57,7 +57,7 @@ public async Task HandleAsync(IHttpContext context, CancellationToken canc { try { - MulticastIsolation.Raise(ClientConnected, this, context.Request, ClientException); + ClientConnected.Raise(this, context.Request, ClientException); // Get Accept-Encoding Header (ex. gzip, br) var acceptEncodings = GetRequestHeaderValues(context.Request, HttpHeaders.AcceptEncoding); @@ -66,9 +66,9 @@ public async Task HandleAsync(IHttpContext context, CancellationToken canc var mtconnectResponse = await OnRequestReceived(context, cancellationToken); mtconnectResponse.WriteDuration = await WriteResponse(mtconnectResponse, context.Response, acceptEncodings); - MulticastIsolation.Raise(ResponseSent, this, mtconnectResponse, ClientException); + ResponseSent.Raise(this, mtconnectResponse, ClientException); - MulticastIsolation.Raise(ClientDisconnected, this, context.Request.RemoteEndPoint?.ToString(), ClientException); + ClientDisconnected.Raise(this, context.Request.RemoteEndPoint?.ToString(), ClientException); return true; } @@ -77,13 +77,13 @@ public async Task HandleAsync(IHttpContext context, CancellationToken canc // Ignore Disposed Object Exception (happens when the listener is stopped) if (ex.ErrorCode != 995) { - MulticastIsolation.Raise(ClientException, this, ex, null); + ClientException.Raise(this, ex, null); } } catch (ObjectDisposedException) { } catch (Exception ex) { - MulticastIsolation.Raise(ClientException, this, ex, null); + ClientException.Raise(this, ex, null); } return false; @@ -225,7 +225,7 @@ protected async Task WriteFromStream(MTConnectHttpServerStream sampleStream, Str } catch (Exception) { - MulticastIsolation.Raise(ClientDisconnected, this, sampleStream.Id, ClientException); + ClientDisconnected.Raise(this, sampleStream.Id, ClientException); sampleStream.Stop(); } } @@ -243,7 +243,7 @@ protected async Task WriteFromStream(MTConnectHttpServerStream sampleStream, IHt } catch (Exception) { - MulticastIsolation.Raise(ClientDisconnected, this, sampleStream.Id, ClientException); + ClientDisconnected.Raise(this, sampleStream.Id, ClientException); sampleStream.Stop(); } } diff --git a/libraries/MTConnect.NET-HTTP/Servers/MTConnectHttpServerStream.cs b/libraries/MTConnect.NET-HTTP/Servers/MTConnectHttpServerStream.cs index a157c0c2..88a060f3 100644 --- a/libraries/MTConnect.NET-HTTP/Servers/MTConnectHttpServerStream.cs +++ b/libraries/MTConnect.NET-HTTP/Servers/MTConnectHttpServerStream.cs @@ -204,7 +204,7 @@ private void Worker() { if (_mtconnectAgent != null) { - MulticastIsolation.Raise(StreamStarted, this, _id, StreamException); + StreamStarted.Raise(this, _id, StreamException); // Set Content Type based documentFormat specified var contentType = MimeTypes.Get(_documentFormat); @@ -265,7 +265,7 @@ private void Worker() stpw.Stop(); // Raise heartbeat event and include the Multipart Chunk - MulticastIsolation.Raise(HeartbeatReceived, this, new MTConnectHttpStreamArgs(_id, outputStream, stpw.ElapsedMilliseconds), StreamException); + HeartbeatReceived.Raise(this, new MTConnectHttpStreamArgs(_id, outputStream, stpw.ElapsedMilliseconds), StreamException); // Reset the heartbeat timestamp lastHeartbeatSent = now; @@ -276,7 +276,7 @@ private void Worker() stpw.Stop(); // Raise heartbeat event and include the Multipart Chunk - MulticastIsolation.Raise(DocumentReceived, this, new MTConnectHttpStreamArgs(_id, outputStream, stpw.ElapsedMilliseconds), StreamException); + DocumentReceived.Raise(this, new MTConnectHttpStreamArgs(_id, outputStream, stpw.ElapsedMilliseconds), StreamException); // Reset the document timestamp lastDocumentSent = now; @@ -297,11 +297,11 @@ private void Worker() } catch (Exception ex) { - MulticastIsolation.Raise(StreamException, this, ex, null); + StreamException.Raise(this, ex, null); throw new Exception(); } - MulticastIsolation.Raise(StreamStopped, this, _id, StreamException); + StreamStopped.Raise(this, _id, StreamException); } } From 97788458fd503fa37ed494cb27be5b5994f06a92 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Otto=20Boly=C3=B3s?= Date: Sat, 6 Jun 2026 02:42:34 +0200 Subject: [PATCH 34/37] refactor(mqtt): adopt extension-method Raise syntax Switch every typed MulticastIsolation.Raise(...) call in the MQTT client surface to the new extension-method form (event.Raise(sender, args, sink)). No behavioural change. --- .../Clients/MTConnectMqttClient.cs | 32 +++++++++---------- .../Clients/MTConnectMqttExpandedClient.cs | 24 +++++++------- 2 files changed, 28 insertions(+), 28 deletions(-) diff --git a/libraries/MTConnect.NET-MQTT/Clients/MTConnectMqttClient.cs b/libraries/MTConnect.NET-MQTT/Clients/MTConnectMqttClient.cs index 75eee186..afc7c687 100644 --- a/libraries/MTConnect.NET-MQTT/Clients/MTConnectMqttClient.cs +++ b/libraries/MTConnect.NET-MQTT/Clients/MTConnectMqttClient.cs @@ -228,7 +228,7 @@ public void Start() { _stop = new CancellationTokenSource(); - MulticastIsolation.Raise(ClientStarting, this, EventArgs.Empty, InternalError); + ClientStarting.Raise(this, EventArgs.Empty, InternalError); _ = Task.Run(Worker, _stop.Token); } @@ -239,7 +239,7 @@ public void Start() /// public void Stop() { - MulticastIsolation.Raise(ClientStopping, this, EventArgs.Empty, InternalError); + ClientStopping.Raise(this, EventArgs.Empty, InternalError); if (_stop != null) _stop.Cancel(); } @@ -353,7 +353,7 @@ private async Task Worker() await StartAllDevicesProtocol(); } - MulticastIsolation.Raise(ClientStarted, this, EventArgs.Empty, InternalError); + ClientStarted.Raise(this, EventArgs.Empty, InternalError); while (_mqttClient.IsConnected && !_stop.IsCancellationRequested) { @@ -362,7 +362,7 @@ private async Task Worker() } catch (Exception ex) { - MulticastIsolation.Raise(ConnectionError, this, ex, InternalError); + ConnectionError.Raise(this, ex, InternalError); } await Task.Delay(_configuration.RetryInterval, _stop.Token); @@ -370,7 +370,7 @@ private async Task Worker() catch (TaskCanceledException) { } catch (Exception ex) { - MulticastIsolation.Raise(InternalError, this, ex, InternalError); + InternalError.Raise(this, ex, InternalError); } } while (!_stop.Token.IsCancellationRequested); @@ -384,7 +384,7 @@ private async Task Worker() catch { } - MulticastIsolation.Raise(ClientStopped, this, EventArgs.Empty, InternalError); + ClientStopped.Raise(this, EventArgs.Empty, InternalError); } @@ -511,11 +511,11 @@ private void ProcessProbeMessage(MqttApplicationMessage message) _devices.Add(outputDevice.Uuid, outputDevice); } - MulticastIsolation.Raise(DeviceReceived, this, outputDevice, InternalError); + DeviceReceived.Raise(this, outputDevice, InternalError); } } - MulticastIsolation.Raise(ProbeReceived, this, responseDocument, InternalError); + ProbeReceived.Raise(this, responseDocument, InternalError); } } } @@ -567,7 +567,7 @@ private void ProcessAssetMessage(MqttApplicationMessage message) private void ProcessCurrentDocument(IStreamsResponseDocument document) { _lastResponse = UnixDateTime.Now; - MulticastIsolation.Raise(ResponseReceived, this, EventArgs.Empty, InternalError); + ResponseReceived.Raise(this, EventArgs.Empty, InternalError); if (document != null) { @@ -585,7 +585,7 @@ private void ProcessCurrentDocument(IStreamsResponseDocument document) response.Streams = deviceStreams; - MulticastIsolation.Raise(CurrentReceived, this, response, InternalError); + CurrentReceived.Raise(this, response, InternalError); // Process Device Streams @@ -610,7 +610,7 @@ private void ProcessCurrentDocument(IStreamsResponseDocument document) { if (observation.Sequence > lastSequence) { - MulticastIsolation.Raise(ObservationReceived, this, observation, InternalError); + ObservationReceived.Raise(this, observation, InternalError); } } @@ -637,7 +637,7 @@ private void ProcessCurrentDocument(IStreamsResponseDocument document) private void ProcessSampleDocument(IStreamsResponseDocument document) { _lastResponse = UnixDateTime.Now; - MulticastIsolation.Raise(ResponseReceived, this, EventArgs.Empty, InternalError); + ResponseReceived.Raise(this, EventArgs.Empty, InternalError); if (document != null) { @@ -653,7 +653,7 @@ private void ProcessSampleDocument(IStreamsResponseDocument document) response.Streams = deviceStreams; - MulticastIsolation.Raise(SampleReceived, this, response, InternalError); + SampleReceived.Raise(this, response, InternalError); // Process Device Streams @@ -676,7 +676,7 @@ private void ProcessSampleDocument(IStreamsResponseDocument document) { if (observation.Sequence > lastSequence && observation.Sequence > lastCurrentSequence) { - MulticastIsolation.Raise(ObservationReceived, this, observation, InternalError); + ObservationReceived.Raise(this, observation, InternalError); } } @@ -699,11 +699,11 @@ private void ProcessAssetsDocument(IAssetsResponseDocument document) { if (document != null && !document.Assets.IsNullOrEmpty()) { - MulticastIsolation.Raise(AssetsReceived, this, document, InternalError); + AssetsReceived.Raise(this, document, InternalError); foreach (var asset in document.Assets) { - MulticastIsolation.Raise(AssetReceived, this, asset, InternalError); + AssetReceived.Raise(this, asset, InternalError); } } } diff --git a/libraries/MTConnect.NET-MQTT/Clients/MTConnectMqttExpandedClient.cs b/libraries/MTConnect.NET-MQTT/Clients/MTConnectMqttExpandedClient.cs index 3946443c..91753588 100644 --- a/libraries/MTConnect.NET-MQTT/Clients/MTConnectMqttExpandedClient.cs +++ b/libraries/MTConnect.NET-MQTT/Clients/MTConnectMqttExpandedClient.cs @@ -248,7 +248,7 @@ public void Start() { _stop = new CancellationTokenSource(); - MulticastIsolation.Raise(ClientStarting, this, EventArgs.Empty, InternalError); + ClientStarting.Raise(this, EventArgs.Empty, InternalError); _ = Task.Run(Worker, _stop.Token); } @@ -256,7 +256,7 @@ public void Start() /// Signals the worker to stop and disconnect from the broker. is raised once the session has closed. public void Stop() { - MulticastIsolation.Raise(ClientStopping, this, EventArgs.Empty, InternalError); + ClientStopping.Raise(this, EventArgs.Empty, InternalError); if (_stop != null) _stop.Cancel(); } @@ -341,7 +341,7 @@ private async Task Worker() StartAllDevicesProtocol().Wait(); } - MulticastIsolation.Raise(ClientStarted, this, EventArgs.Empty, InternalError); + ClientStarted.Raise(this, EventArgs.Empty, InternalError); while (_mqttClient.IsConnected && !_stop.IsCancellationRequested) { @@ -350,7 +350,7 @@ private async Task Worker() } catch (Exception ex) { - MulticastIsolation.Raise(ConnectionError, this, ex, InternalError); + ConnectionError.Raise(this, ex, InternalError); } await Task.Delay(ReconnectionInterval, _stop.Token); @@ -358,7 +358,7 @@ private async Task Worker() catch (TaskCanceledException) { } catch (Exception ex) { - MulticastIsolation.Raise(InternalError, this, ex, InternalError); + InternalError.Raise(this, ex, InternalError); } } while (!_stop.Token.IsCancellationRequested); @@ -372,7 +372,7 @@ private async Task Worker() catch { } - MulticastIsolation.Raise(ClientStopped, this, EventArgs.Empty, InternalError); + ClientStopped.Raise(this, EventArgs.Empty, InternalError); } @@ -532,7 +532,7 @@ private async void ProcessObservation(MqttApplicationMessage message) if (observation.InstanceId == agentInstanceId) { - MulticastIsolation.Raise(ObservationReceived, deviceUuid, observation, InternalError); + ObservationReceived.Raise(deviceUuid, observation, InternalError); } else { @@ -584,7 +584,7 @@ private void ProcessObservations(MqttApplicationMessage message) observation.AddValue(ValueKeys.Result, jsonObservation.Result); } - MulticastIsolation.Raise(ObservationReceived, deviceUuid, observation, InternalError); + ObservationReceived.Raise(deviceUuid, observation, InternalError); } } } @@ -656,7 +656,7 @@ private async Task ProcessAgent(MqttApplicationMessage message, Func Date: Sat, 6 Jun 2026 02:42:42 +0200 Subject: [PATCH 35/37] refactor(shdr): adopt extension-method Raise syntax Switch every typed MulticastIsolation.Raise(...) call in the SHDR adapter and client surfaces to the new extension-method form (event.Raise(this, args, sink)). No behavioural change. --- .../Adapters/ShdrAdapter.cs | 18 ++++----- .../MTConnect.NET-SHDR/Shdr/ShdrClient.cs | 40 +++++++++---------- 2 files changed, 29 insertions(+), 29 deletions(-) diff --git a/libraries/MTConnect.NET-SHDR/Adapters/ShdrAdapter.cs b/libraries/MTConnect.NET-SHDR/Adapters/ShdrAdapter.cs index 9906cf00..868b7e8c 100644 --- a/libraries/MTConnect.NET-SHDR/Adapters/ShdrAdapter.cs +++ b/libraries/MTConnect.NET-SHDR/Adapters/ShdrAdapter.cs @@ -344,7 +344,7 @@ public void SetUnavailable(long timestamp = 0) private void ClientConnected(string clientId, TcpClient client) { AddAgentClient(clientId, client); - MulticastIsolation.Raise(AgentConnected, this, clientId, null); + AgentConnected.Raise(this, clientId, null); SendLast(UnixDateTime.Now); } @@ -352,22 +352,22 @@ private void ClientConnected(string clientId, TcpClient client) private void ClientDisconnected(string clientId) { RemoveAgentClient(clientId); - MulticastIsolation.Raise(AgentDisconnected, this, clientId, null); + AgentDisconnected.Raise(this, clientId, null); } private void ClientPingReceived(string clientId) { - MulticastIsolation.Raise(PingReceived, this, clientId, null); + PingReceived.Raise(this, clientId, null); } private void ClientPongSent(string clientId) { - MulticastIsolation.Raise(PongSent, this, clientId, null); + PongSent.Raise(this, clientId, null); } private void ClientConnectionError(string clientId, Exception exception) { - MulticastIsolation.Raise(ConnectionError, this, new AdapterEventArgs(clientId, exception), null); + ConnectionError.Raise(this, new AdapterEventArgs(clientId, exception), null); } #endregion @@ -601,11 +601,11 @@ private bool WriteLineToClient(AgentClient client, string line) // Write the line (in bytes) to the Stream stream.Write(bytes, 0, bytes.Length); - MulticastIsolation.Raise(LineSent, this, new AdapterEventArgs(client.Id, singleLine), null); + LineSent.Raise(this, new AdapterEventArgs(client.Id, singleLine), null); } catch (Exception ex) { - MulticastIsolation.Raise(SendError, this, new AdapterEventArgs(client.Id, ex.Message), null); + SendError.Raise(this, new AdapterEventArgs(client.Id, ex.Message), null); return false; } } @@ -634,13 +634,13 @@ private async Task WriteLineToClientAsync(AgentClient client, string line) // Write the line (in bytes) to the Stream await stream.WriteAsync(bytes, 0, bytes.Length); - MulticastIsolation.Raise(LineSent, this, new AdapterEventArgs(client.Id, line), null); + LineSent.Raise(this, new AdapterEventArgs(client.Id, line), null); return true; } catch (Exception ex) { - MulticastIsolation.Raise(SendError, this, new AdapterEventArgs(client.Id, ex.Message), null); + SendError.Raise(this, new AdapterEventArgs(client.Id, ex.Message), null); } } diff --git a/libraries/MTConnect.NET-SHDR/Shdr/ShdrClient.cs b/libraries/MTConnect.NET-SHDR/Shdr/ShdrClient.cs index ccf6f81a..a9c51601 100644 --- a/libraries/MTConnect.NET-SHDR/Shdr/ShdrClient.cs +++ b/libraries/MTConnect.NET-SHDR/Shdr/ShdrClient.cs @@ -258,7 +258,7 @@ private async Task ListenForAdapter(CancellationToken cancel) _client.ReceiveTimeout = ConnectionTimeout; _client.SendTimeout = ConnectionTimeout; - MulticastIsolation.Raise(Connected, this, $"Connected to Adapter at {Hostname} on Port {Port}", null); + Connected.Raise(this, $"Connected to Adapter at {Hostname} on Port {Port}", null); connected = true; OnConnect(); @@ -269,7 +269,7 @@ private async Task ListenForAdapter(CancellationToken cancel) // Send Initial PING Request var messageBytes = Encoding.ASCII.GetBytes(PingMessage); stream.Write(messageBytes, 0, messageBytes.Length); - MulticastIsolation.Raise(PingSent, this, $"Initial PING sent to : {Hostname} on Port {Port}", null); + PingSent.Raise(this, $"Initial PING sent to : {Hostname} on Port {Port}", null); // Read the Initial PONG Response bufferIndex = stream.Read(buffer, 0, buffer.Length); @@ -297,7 +297,7 @@ private async Task ListenForAdapter(CancellationToken cancel) { messageBytes = Encoding.ASCII.GetBytes(PingMessage); stream.Write(messageBytes, 0, messageBytes.Length); - MulticastIsolation.Raise(PingSent, this, $"PING sent to : {Hostname} on Port {Port}", null); + PingSent.Raise(this, $"PING sent to : {Hostname} on Port {Port}", null); _lastHeartbeat = now; } @@ -318,7 +318,7 @@ private async Task ListenForAdapter(CancellationToken cancel) catch (TaskCanceledException) { } catch (Exception ex) { - MulticastIsolation.Raise(ConnectionError, this, ex, null); + ConnectionError.Raise(this, ex, null); } finally { @@ -329,7 +329,7 @@ private async Task ListenForAdapter(CancellationToken cancel) if (connected) { - MulticastIsolation.Raise(Disconnected, this, $"Disconnected from {Hostname} on Port {Port}", null); + Disconnected.Raise(this, $"Disconnected from {Hostname} on Port {Port}", null); OnDisconnect(); } @@ -341,13 +341,13 @@ private async Task ListenForAdapter(CancellationToken cancel) // Wait for the ReconnectInterval (in milliseconds) until continuing while loop await Task.Delay(reconnectInterval, cancel); - MulticastIsolation.Raise(Listening, this, $"Listening for connection from {Hostname} on Port {Port}", null); + Listening.Raise(this, $"Listening for connection from {Hostname} on Port {Port}", null); } } catch (TaskCanceledException) { } catch (Exception ex) { - MulticastIsolation.Raise(ConnectionError, this, ex, null); + ConnectionError.Raise(this, ex, null); if (_client != null) { @@ -358,7 +358,7 @@ private async Task ListenForAdapter(CancellationToken cancel) if (connected) { - MulticastIsolation.Raise(Disconnected, this, $"Disconnected from {Hostname} on Port {Port}", null); + Disconnected.Raise(this, $"Disconnected from {Hostname} on Port {Port}", null); OnDisconnect(); } @@ -405,7 +405,7 @@ private bool ProcessResponse(ref char[] chars, int length) { _heartbeat = GetPongHeartbeat(line); - MulticastIsolation.Raise(PongReceived, this, $"PONG Received from : {Hostname} on Port {Port} : Heartbeat = {_heartbeat}ms", null); + PongReceived.Raise(this, $"PONG Received from : {Hostname} on Port {Port} : Heartbeat = {_heartbeat}ms", null); } else { @@ -429,7 +429,7 @@ private bool ProcessResponse(ref char[] chars, int length) multilineAsset = true; // Raise ProtocolReceived Event passing the Line that was read as a parameter - MulticastIsolation.Raise(ProtocolReceived, this, line, null); + ProtocolReceived.Raise(this, line, null); found = true; } @@ -449,7 +449,7 @@ private bool ProcessResponse(ref char[] chars, int length) multilineAsset = false; // Raise ProtocolReceived Event passing the Line that was read as a parameter - MulticastIsolation.Raise(ProtocolReceived, this, line, null); + ProtocolReceived.Raise(this, line, null); found = true; } @@ -458,7 +458,7 @@ private bool ProcessResponse(ref char[] chars, int length) multilineContent.Append(line); // Raise ProtocolReceived Event passing the Line that was read as a parameter - MulticastIsolation.Raise(ProtocolReceived, this, line, null); + ProtocolReceived.Raise(this, line, null); found = true; } @@ -485,7 +485,7 @@ private bool ProcessResponse(ref char[] chars, int length) } // Raise ProtocolReceived Event passing the Line that was read as a parameter - MulticastIsolation.Raise(ProtocolReceived, this, line, null); + ProtocolReceived.Raise(this, line, null); found = true; } @@ -499,7 +499,7 @@ private bool ProcessResponse(ref char[] chars, int length) } // Raise ProtocolReceived Event passing the Line that was read as a parameter - MulticastIsolation.Raise(ProtocolReceived, this, line, null); + ProtocolReceived.Raise(this, line, null); found = true; } @@ -513,7 +513,7 @@ private bool ProcessResponse(ref char[] chars, int length) } // Raise ProtocolReceived Event passing the Line that was read as a parameter - MulticastIsolation.Raise(ProtocolReceived, this, line, null); + ProtocolReceived.Raise(this, line, null); found = true; } @@ -526,7 +526,7 @@ private bool ProcessResponse(ref char[] chars, int length) multilineDevice = true; // Raise ProtocolReceived Event passing the Line that was read as a parameter - MulticastIsolation.Raise(ProtocolReceived, this, line, null); + ProtocolReceived.Raise(this, line, null); found = true; } @@ -542,7 +542,7 @@ private bool ProcessResponse(ref char[] chars, int length) multilineDevice = false; // Raise ProtocolReceived Event passing the Line that was read as a parameter - MulticastIsolation.Raise(ProtocolReceived, this, line, null); + ProtocolReceived.Raise(this, line, null); found = true; } @@ -551,7 +551,7 @@ private bool ProcessResponse(ref char[] chars, int length) multilineContent.Append(line); // Raise ProtocolReceived Event passing the Line that was read as a parameter - MulticastIsolation.Raise(ProtocolReceived, this, line, null); + ProtocolReceived.Raise(this, line, null); found = true; } @@ -564,7 +564,7 @@ private bool ProcessResponse(ref char[] chars, int length) } // Raise ProtocolReceived Event passing the Line that was read as a parameter - MulticastIsolation.Raise(ProtocolReceived, this, line, null); + ProtocolReceived.Raise(this, line, null); found = true; } @@ -573,7 +573,7 @@ private bool ProcessResponse(ref char[] chars, int length) ProcessProtocol(line); // Raise ProtocolReceived Event passing the Line that was read as a parameter - MulticastIsolation.Raise(ProtocolReceived, this, line, null); + ProtocolReceived.Raise(this, line, null); found = true; } From 25b10d8d839a18d5a9bec030300360707b35c5f0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Otto=20Boly=C3=B3s?= Date: Sat, 6 Jun 2026 02:43:07 +0200 Subject: [PATCH 36/37] refactor(agents): adopt extension-method Raise syntax Switch typed MulticastIsolation.Raise(...) calls in MTConnectAgent and MTConnectAgentBroker to the new extension-method form (event.Raise(this, args, sink)). Custom-delegate events keep the generic-delegate static call because extension-method syntax does not compose with the helper's where TDelegate : Delegate constraint. No behavioural change. --- .../Agents/MTConnectAgent.cs | 14 +++++------ .../Agents/MTConnectAgentBroker.cs | 24 +++++++++---------- 2 files changed, 19 insertions(+), 19 deletions(-) diff --git a/libraries/MTConnect.NET-Common/Agents/MTConnectAgent.cs b/libraries/MTConnect.NET-Common/Agents/MTConnectAgent.cs index a3785518..e1276ec0 100644 --- a/libraries/MTConnect.NET-Common/Agents/MTConnectAgent.cs +++ b/libraries/MTConnect.NET-Common/Agents/MTConnectAgent.cs @@ -1522,7 +1522,7 @@ private bool AddDeviceAddedObservation(IDevice device, long timestamp = 0) new ObservationValue(ValueKeys.Result, device.Uuid) }); - MulticastIsolation.Raise(ObservationAdded, this, observation, null); + ObservationAdded.Raise(this, observation, null); return true; } @@ -1553,7 +1553,7 @@ private bool AddDeviceChangedObservation(IDevice device, long timestamp = 0) new ObservationValue(ValueKeys.Result, device.Uuid) }); - MulticastIsolation.Raise(ObservationAdded, this, observation, null); + ObservationAdded.Raise(this, observation, null); return true; } @@ -1583,7 +1583,7 @@ private bool AddDeviceRemovedObservation(IDevice device, long timestamp = 0) new ObservationValue(ValueKeys.Result, device.Uuid) }); - MulticastIsolation.Raise(ObservationAdded, this, observation, null); + ObservationAdded.Raise(this, observation, null); return true; } @@ -1717,7 +1717,7 @@ public IDevice AddDevice(IDevice device, bool initializeDataItems = true) _updateInformation = true; } - MulticastIsolation.Raise(DeviceAdded, this, obj, null); + DeviceAdded.Raise(this, obj, null); return obj; } @@ -2124,7 +2124,7 @@ public bool AddObservation(string deviceKey, IObservationInput observationInput, { if (observationInput != null) { - MulticastIsolation.Raise(ObservationReceived, this, observationInput, null); + ObservationReceived.Raise(this, observationInput, null); IObservationInput input = new ObservationInput(); input.DeviceKey = deviceKey; @@ -2334,7 +2334,7 @@ protected virtual ulong OnAddObservation(string deviceUuid, IDataItem dataItem, /// public void OnObservationAdded(IObservation observation) { - MulticastIsolation.Raise(ObservationAdded, this, observation, null); + ObservationAdded.Raise(this, observation, null); } /// @@ -2448,7 +2448,7 @@ public bool AddAsset(string deviceKey, IAsset asset, bool? ignoreTimestamp = nul MulticastIsolation.Raise(InvalidAssetAdded, h => h(asset, validationResults)); } - MulticastIsolation.Raise(AssetAdded, this, asset, null); + AssetAdded.Raise(this, asset, null); return true; } } diff --git a/libraries/MTConnect.NET-Common/Agents/MTConnectAgentBroker.cs b/libraries/MTConnect.NET-Common/Agents/MTConnectAgentBroker.cs index dbd8604b..7095a10c 100644 --- a/libraries/MTConnect.NET-Common/Agents/MTConnectAgentBroker.cs +++ b/libraries/MTConnect.NET-Common/Agents/MTConnectAgentBroker.cs @@ -773,7 +773,7 @@ public IStreamsResponseOutputDocument GetDeviceStreamsResponseDocument(uint coun var document = CreateDeviceStreamsDocument(devices, ref results, mtconnectVersion); if (document != null) { - MulticastIsolation.Raise(StreamsResponseSent, this, EventArgs.Empty, null); + StreamsResponseSent.Raise(this, EventArgs.Empty, null); return document; } } @@ -809,7 +809,7 @@ public IStreamsResponseOutputDocument GetDeviceStreamsResponseDocument(ulong at, var document = CreateDeviceStreamsDocument(devices, ref results, mtconnectVersion); if (document != null) { - MulticastIsolation.Raise(StreamsResponseSent, this, EventArgs.Empty, null); + StreamsResponseSent.Raise(this, EventArgs.Empty, null); return document; } } @@ -850,7 +850,7 @@ public IStreamsResponseOutputDocument GetDeviceStreamsResponseDocument(IEnumerab var document = CreateDeviceStreamsDocument(devices, ref results, mtconnectVersion); if (document != null) { - MulticastIsolation.Raise(StreamsResponseSent, this, EventArgs.Empty, null); + StreamsResponseSent.Raise(this, EventArgs.Empty, null); return document; } } @@ -892,7 +892,7 @@ public IStreamsResponseOutputDocument GetDeviceStreamsResponseDocument(IEnumerab var document = CreateDeviceStreamsDocument(devices, ref results, mtconnectVersion); if (document != null) { - MulticastIsolation.Raise(StreamsResponseSent, this, EventArgs.Empty, null); + StreamsResponseSent.Raise(this, EventArgs.Empty, null); return document; } } @@ -929,7 +929,7 @@ public IStreamsResponseOutputDocument GetDeviceStreamsResponseDocument(ulong fro var document = CreateDeviceStreamsDocument(devices, ref results, mtconnectVersion); if (document != null) { - MulticastIsolation.Raise(StreamsResponseSent, this, EventArgs.Empty, null); + StreamsResponseSent.Raise(this, EventArgs.Empty, null); return document; } } @@ -967,7 +967,7 @@ public IStreamsResponseOutputDocument GetDeviceStreamsResponseDocument(IEnumerab var document = CreateDeviceStreamsDocument(devices, ref results, mtconnectVersion); if (document != null) { - MulticastIsolation.Raise(StreamsResponseSent, this, EventArgs.Empty, null); + StreamsResponseSent.Raise(this, EventArgs.Empty, null); return document; } } @@ -1003,7 +1003,7 @@ public IStreamsResponseOutputDocument GetDeviceStreamsResponseDocument(string de var document = CreateDeviceStreamsDocument(device, ref results, mtconnectVersion); if (document != null) { - MulticastIsolation.Raise(StreamsResponseSent, this, EventArgs.Empty, null); + StreamsResponseSent.Raise(this, EventArgs.Empty, null); return document; } } @@ -1039,7 +1039,7 @@ public IStreamsResponseOutputDocument GetDeviceStreamsResponseDocument(string de var document = CreateDeviceStreamsDocument(device, ref results, mtconnectVersion); if (document != null) { - MulticastIsolation.Raise(StreamsResponseSent, this, EventArgs.Empty, null); + StreamsResponseSent.Raise(this, EventArgs.Empty, null); return document; } } @@ -1075,7 +1075,7 @@ public IStreamsResponseOutputDocument GetDeviceStreamsResponseDocument(string de var document = CreateDeviceStreamsDocument(device, ref results, mtconnectVersion); if (document != null) { - MulticastIsolation.Raise(StreamsResponseSent, this, EventArgs.Empty, null); + StreamsResponseSent.Raise(this, EventArgs.Empty, null); return document; } } @@ -1112,7 +1112,7 @@ public IStreamsResponseOutputDocument GetDeviceStreamsResponseDocument(string de var document = CreateDeviceStreamsDocument(device, ref results, mtconnectVersion); if (document != null) { - MulticastIsolation.Raise(StreamsResponseSent, this, EventArgs.Empty, null); + StreamsResponseSent.Raise(this, EventArgs.Empty, null); return document; } } @@ -1149,7 +1149,7 @@ public IStreamsResponseOutputDocument GetDeviceStreamsResponseDocument(string de var document = CreateDeviceStreamsDocument(device, ref results, mtconnectVersion); if (document != null) { - MulticastIsolation.Raise(StreamsResponseSent, this, EventArgs.Empty, null); + StreamsResponseSent.Raise(this, EventArgs.Empty, null); return document; } } @@ -1187,7 +1187,7 @@ public IStreamsResponseOutputDocument GetDeviceStreamsResponseDocument(string de var document = CreateDeviceStreamsDocument(device, ref results, mtconnectVersion); if (document != null) { - MulticastIsolation.Raise(StreamsResponseSent, this, EventArgs.Empty, null); + StreamsResponseSent.Raise(this, EventArgs.Empty, null); return document; } } From 5801ec10d0f96ff8eb35b8324ee5b52b5aa6c551 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Otto=20Boly=C3=B3s?= Date: Sat, 6 Jun 2026 02:43:42 +0200 Subject: [PATCH 37/37] test(multicast-isolation): adopt extension-method Raise syntax Update every typed-overload call site in the multicast-isolation tests to match the new extension-method form (event.Raise(this, args, sink)). Custom-delegate tests keep the generic-delegate static call. The XML cref references in the test class summaries still resolve because the method signature (parameter types) is unchanged by the this modifier. --- .../Agents/AgentMulticastIsolationTests.cs | 24 +++++++++---------- .../MqttClientMulticastIsolationTests.cs | 18 +++++++------- .../MulticastIsolationTests.cs | 16 ++++++------- .../SubClientMulticastIsolationTests.cs | 4 ++-- .../HttpServerMulticastIsolationTests.cs | 22 ++++++++--------- .../ShdrMulticastIsolationTests.cs | 16 ++++++------- 6 files changed, 50 insertions(+), 50 deletions(-) diff --git a/tests/MTConnect.NET-Common-Tests/Agents/AgentMulticastIsolationTests.cs b/tests/MTConnect.NET-Common-Tests/Agents/AgentMulticastIsolationTests.cs index 9aae0573..71d570b2 100644 --- a/tests/MTConnect.NET-Common-Tests/Agents/AgentMulticastIsolationTests.cs +++ b/tests/MTConnect.NET-Common-Tests/Agents/AgentMulticastIsolationTests.cs @@ -53,7 +53,7 @@ public void Agent_DeviceAdded_FiresAllSubscribersWhenOneThrows() handler += (_, _) => throw new InvalidOperationException("first DeviceAdded subscriber throws"); handler += (_, d) => received.Add(d.Uuid ?? "null"); - MulticastIsolation.Raise(handler!, this, (IDevice)device, null); + handler!.Raise(this, (IDevice)device, null); Assert.That(received, Is.EqualTo(new[] { "uuid-1" })); } @@ -65,7 +65,7 @@ public void Agent_DeviceAdded_NullInternalErrorSwallowsFault() var device = new Device { Name = "device-1", Uuid = "uuid-1" }; EventHandler handler = (_, _) => throw new InvalidOperationException("DeviceAdded fault"); - Assert.DoesNotThrow(() => MulticastIsolation.Raise(handler, this, (IDevice)device, null)); + Assert.DoesNotThrow(() => handler.Raise(this, (IDevice)device, null)); } // ----------------------------------------------------------------------- @@ -84,7 +84,7 @@ public void Agent_ObservationReceived_FiresAllSubscribersWhenOneThrows() handler += (_, _) => firedCount++; var obs = new ObservationInput(); - MulticastIsolation.Raise(handler!, this, (IObservationInput)obs, null); + handler!.Raise(this, (IObservationInput)obs, null); Assert.That(firedCount, Is.EqualTo(1)); } @@ -96,7 +96,7 @@ public void Agent_ObservationReceived_NullInternalErrorSwallowsFault() var obs = new ObservationInput(); EventHandler handler = (_, _) => throw new InvalidOperationException("ObservationReceived fault"); - Assert.DoesNotThrow(() => MulticastIsolation.Raise(handler, this, (IObservationInput)obs, null)); + Assert.DoesNotThrow(() => handler.Raise(this, (IObservationInput)obs, null)); } // ----------------------------------------------------------------------- @@ -115,7 +115,7 @@ public void Agent_ObservationAdded_FiresAllSubscribersWhenOneThrows() handler += (_, _) => firedCount++; var obs = new Observation(); - MulticastIsolation.Raise(handler!, this, (IObservation)obs, null); + handler!.Raise(this, (IObservation)obs, null); Assert.That(firedCount, Is.EqualTo(1)); } @@ -127,7 +127,7 @@ public void Agent_ObservationAdded_NullInternalErrorSwallowsFault() var obs = new Observation(); EventHandler handler = (_, _) => throw new InvalidOperationException("ObservationAdded fault"); - Assert.DoesNotThrow(() => MulticastIsolation.Raise(handler, this, (IObservation)obs, null)); + Assert.DoesNotThrow(() => handler.Raise(this, (IObservation)obs, null)); } // ----------------------------------------------------------------------- @@ -146,7 +146,7 @@ public void Agent_AssetAdded_FiresAllSubscribersWhenOneThrows() handler += (_, _) => firedCount++; var asset = new Asset { AssetId = "a1", Timestamp = DateTime.UtcNow }; - MulticastIsolation.Raise(handler!, this, (IAsset)asset, null); + handler!.Raise(this, (IAsset)asset, null); Assert.That(firedCount, Is.EqualTo(1)); } @@ -158,7 +158,7 @@ public void Agent_AssetAdded_NullInternalErrorSwallowsFault() var asset = new Asset { AssetId = "a1", Timestamp = DateTime.UtcNow }; EventHandler handler = (_, _) => throw new InvalidOperationException("AssetAdded fault"); - Assert.DoesNotThrow(() => MulticastIsolation.Raise(handler, this, (IAsset)asset, null)); + Assert.DoesNotThrow(() => handler.Raise(this, (IAsset)asset, null)); } // ----------------------------------------------------------------------- @@ -176,7 +176,7 @@ public void AgentBroker_StreamsResponseSent_FiresAllSubscribersWhenOneThrows() handler += (_, _) => throw new InvalidOperationException("first StreamsResponseSent subscriber throws"); handler += (_, _) => fired.Add(1); - MulticastIsolation.Raise(handler!, this, EventArgs.Empty, null); + handler!.Raise(this, EventArgs.Empty, null); Assert.That(fired, Is.EqualTo(new[] { 1 })); } @@ -187,7 +187,7 @@ public void AgentBroker_StreamsResponseSent_NullInternalErrorSwallowsFault() { EventHandler handler = (_, _) => throw new InvalidOperationException("StreamsResponseSent fault"); - Assert.DoesNotThrow(() => MulticastIsolation.Raise(handler, this, EventArgs.Empty, null)); + Assert.DoesNotThrow(() => handler.Raise(this, EventArgs.Empty, null)); } // ----------------------------------------------------------------------- @@ -201,7 +201,7 @@ public void Agent_NullGenericHandler_DoesNotThrow() EventHandler? handler = null; var device = new Device { Name = "noop-device", Uuid = "noop-uuid" }; - Assert.DoesNotThrow(() => MulticastIsolation.Raise(handler, this, (IDevice)device, null)); + Assert.DoesNotThrow(() => handler.Raise(this, (IDevice)device, null)); } /// Pins the behavior expressed by the test name: Raise with a null non-generic EventHandler is a safe no-op covering the no-subscriber case at runtime. @@ -210,7 +210,7 @@ public void AgentBroker_NullNonGenericHandler_DoesNotThrow() { EventHandler? handler = null; - Assert.DoesNotThrow(() => MulticastIsolation.Raise(handler, this, EventArgs.Empty, null)); + Assert.DoesNotThrow(() => handler.Raise(this, EventArgs.Empty, null)); } // ======================================================================= diff --git a/tests/MTConnect.NET-Common-Tests/MqttClientMulticastIsolationTests.cs b/tests/MTConnect.NET-Common-Tests/MqttClientMulticastIsolationTests.cs index 7a7c3b9a..90a9bf0e 100644 --- a/tests/MTConnect.NET-Common-Tests/MqttClientMulticastIsolationTests.cs +++ b/tests/MTConnect.NET-Common-Tests/MqttClientMulticastIsolationTests.cs @@ -38,7 +38,7 @@ public void MqttClient_NonGeneric_EventHandler_FiresAllSubscribersWhenOneThrows( EventHandler? internalError = (_, _) => { }; - MulticastIsolation.Raise(handler!, this, EventArgs.Empty, internalError); + handler!.Raise(this, EventArgs.Empty, internalError); Assert.That(fired, Is.EqualTo(new[] { 1, 3 })); } @@ -51,7 +51,7 @@ public void MqttClient_NonGeneric_EventHandler_RoutesFaultToInternalError() EventHandler handler = (_, _) => throw new InvalidOperationException("mqtt-non-generic-fault"); EventHandler internalError = (_, ex) => routed.Add(ex); - MulticastIsolation.Raise(handler, this, EventArgs.Empty, internalError); + handler.Raise(this, EventArgs.Empty, internalError); Assert.That(routed.Count, Is.EqualTo(1)); Assert.That(routed[0], Is.InstanceOf()); @@ -73,7 +73,7 @@ public void MqttClient_NonGeneric_EventHandler_ThrowingInternalErrorDoesNotStarv internalError += (_, _) => throw new InvalidOperationException("InternalError handler-1 throws"); internalError += (_, ex) => errorFired.Add(2); - MulticastIsolation.Raise(handler!, this, EventArgs.Empty, internalError!); + handler!.Raise(this, EventArgs.Empty, internalError!); Assert.That(eventFired, Is.EqualTo(new[] { 2 })); Assert.That(errorFired, Is.EqualTo(new[] { 2 })); @@ -94,7 +94,7 @@ public void MqttClient_Generic_EventHandlerOfString_FiresAllSubscribersWhenOneTh handler += (_, _) => throw new InvalidOperationException("first throws"); handler += (_, v) => received.Add(v!); - MulticastIsolation.Raise(handler!, this, "payload", null); + handler!.Raise(this, "payload", null); Assert.That(received, Is.EqualTo(new[] { "payload" })); } @@ -107,7 +107,7 @@ public void MqttClient_Generic_EventHandlerOfString_RoutesFaultToInternalError() EventHandler handler = (_, _) => throw new InvalidOperationException("string-event-fault"); EventHandler internalError = (_, ex) => routed.Add(ex.Message); - MulticastIsolation.Raise(handler, this, "value", internalError); + handler.Raise(this, "value", internalError); Assert.That(routed, Is.EqualTo(new[] { "string-event-fault" })); } @@ -124,7 +124,7 @@ public void MqttClient_Generic_EventHandlerOfString_ThrowingInternalErrorIterate internalError += (_, _) => seen.Add(2); internalError += (_, _) => seen.Add(3); - MulticastIsolation.Raise(handler, this, "x", internalError!); + handler.Raise(this, "x", internalError!); Assert.That(seen, Is.EqualTo(new[] { 2, 3 })); } @@ -148,7 +148,7 @@ public void MqttClient_Generic_EventHandlerOfException_FiresAllSubscribersWhenOn handler += (_, _) => throw new InvalidOperationException("first ConnectionError subscriber throws"); handler += (_, ex) => received.Add(ex.Message); - MulticastIsolation.Raise(handler!, this, payload, null); + handler!.Raise(this, payload, null); Assert.That(received, Is.EqualTo(new[] { "upstream-error" })); } @@ -164,7 +164,7 @@ public void MqttClient_NullHandler_DoesNotThrow() EventHandler? handler = null; EventHandler? internalError = null; - Assert.DoesNotThrow(() => MulticastIsolation.Raise(handler, this, "x", internalError)); + Assert.DoesNotThrow(() => handler.Raise(this, "x", internalError)); } /// Pins the behavior expressed by the test name: Raise non-generic with a null handler is a safe no-op. @@ -174,7 +174,7 @@ public void MqttClient_NullHandlerNonGeneric_DoesNotThrow() EventHandler? handler = null; EventHandler? internalError = null; - Assert.DoesNotThrow(() => MulticastIsolation.Raise(handler, this, EventArgs.Empty, internalError)); + Assert.DoesNotThrow(() => handler.Raise(this, EventArgs.Empty, internalError)); } } } diff --git a/tests/MTConnect.NET-Common-Tests/MulticastIsolationTests.cs b/tests/MTConnect.NET-Common-Tests/MulticastIsolationTests.cs index f4e691e4..a8344469 100644 --- a/tests/MTConnect.NET-Common-Tests/MulticastIsolationTests.cs +++ b/tests/MTConnect.NET-Common-Tests/MulticastIsolationTests.cs @@ -29,7 +29,7 @@ public void MulticastIsolation_Generic_FiresAllSubscribersWhenOneThrows() EventHandler internalError = (s, ex) => { }; - MulticastIsolation.Raise(handler!, this, 42, internalError); + handler!.Raise(this, 42, internalError); Assert.That(fired, Is.EqualTo(new[] { 1, 3 })); } @@ -42,7 +42,7 @@ public void MulticastIsolation_Generic_RoutesFaultToInternalError() EventHandler handler = (s, e) => throw new InvalidOperationException("boom"); EventHandler internalError = (s, ex) => routed.Add(ex); - MulticastIsolation.Raise(handler, this, 0, internalError); + handler.Raise(this, 0, internalError); Assert.That(routed.Count, Is.EqualTo(1)); Assert.That(routed[0], Is.InstanceOf()); @@ -61,7 +61,7 @@ public void MulticastIsolation_Generic_InternalErrorIteratesPerSubscriber() internalError += (s, ex) => seen.Add(2); internalError += (s, ex) => seen.Add(3); - MulticastIsolation.Raise(handler, this, 0, internalError!); + handler.Raise(this, 0, internalError!); Assert.That(seen, Is.EqualTo(new[] { 2, 3 })); } @@ -75,7 +75,7 @@ public void MulticastIsolation_Generic_TerminalSwallowsInternalErrorOwnThrow() internalError += (s, ex) => throw new InvalidOperationException("internal-1"); internalError += (s, ex) => throw new InvalidOperationException("internal-2"); - Assert.DoesNotThrow(() => MulticastIsolation.Raise(handler, this, 0, internalError!)); + Assert.DoesNotThrow(() => handler.Raise(this, 0, internalError!)); } // --- Non-generic overload --------------------------------------------- @@ -92,7 +92,7 @@ public void MulticastIsolation_NonGeneric_FiresAllSubscribersWhenOneThrows() EventHandler internalError = (s, ex) => { }; - MulticastIsolation.Raise(handler!, this, EventArgs.Empty, internalError); + handler!.Raise(this, EventArgs.Empty, internalError); Assert.That(fired, Is.EqualTo(new[] { 1, 3 })); } @@ -105,7 +105,7 @@ public void MulticastIsolation_NonGeneric_RoutesFaultToInternalError() EventHandler handler = (s, e) => throw new InvalidOperationException("boom"); EventHandler internalError = (s, ex) => routed.Add(ex); - MulticastIsolation.Raise(handler, this, EventArgs.Empty, internalError); + handler.Raise(this, EventArgs.Empty, internalError); Assert.That(routed.Count, Is.EqualTo(1)); Assert.That(routed[0], Is.InstanceOf()); @@ -124,7 +124,7 @@ public void MulticastIsolation_NonGeneric_InternalErrorIteratesPerSubscriber() internalError += (s, ex) => seen.Add(2); internalError += (s, ex) => seen.Add(3); - MulticastIsolation.Raise(handler, this, EventArgs.Empty, internalError!); + handler.Raise(this, EventArgs.Empty, internalError!); Assert.That(seen, Is.EqualTo(new[] { 2, 3 })); } @@ -138,7 +138,7 @@ public void MulticastIsolation_NonGeneric_TerminalSwallowsInternalErrorOwnThrow( internalError += (s, ex) => throw new InvalidOperationException("internal-1"); internalError += (s, ex) => throw new InvalidOperationException("internal-2"); - Assert.DoesNotThrow(() => MulticastIsolation.Raise(handler, this, EventArgs.Empty, internalError!)); + Assert.DoesNotThrow(() => handler.Raise(this, EventArgs.Empty, internalError!)); } // --- Generic custom-delegate overload --------------------------------- diff --git a/tests/MTConnect.NET-HTTP-Tests/Clients/SubClientMulticastIsolationTests.cs b/tests/MTConnect.NET-HTTP-Tests/Clients/SubClientMulticastIsolationTests.cs index 3563c32d..cb2ea3a2 100644 --- a/tests/MTConnect.NET-HTTP-Tests/Clients/SubClientMulticastIsolationTests.cs +++ b/tests/MTConnect.NET-HTTP-Tests/Clients/SubClientMulticastIsolationTests.cs @@ -68,14 +68,14 @@ private static void InvokeGenericRaise(object instance, string eventName, T a { var handler = GetEventBacking>(instance, eventName); var internalError = GetEventBacking>(instance, "InternalError"); - MulticastIsolation.Raise(handler, instance, arg, internalError); + handler.Raise(instance, arg, internalError); } private static void InvokeNonGenericRaise(object instance, string eventName, EventArgs arg) { var handler = GetEventBacking(instance, eventName); var internalError = GetEventBacking>(instance, "InternalError"); - MulticastIsolation.Raise(handler, instance, arg, internalError); + handler.Raise(instance, arg, internalError); } diff --git a/tests/MTConnect.NET-HTTP-Tests/Servers/HttpServerMulticastIsolationTests.cs b/tests/MTConnect.NET-HTTP-Tests/Servers/HttpServerMulticastIsolationTests.cs index 6700eb6b..e01683d1 100644 --- a/tests/MTConnect.NET-HTTP-Tests/Servers/HttpServerMulticastIsolationTests.cs +++ b/tests/MTConnect.NET-HTTP-Tests/Servers/HttpServerMulticastIsolationTests.cs @@ -45,7 +45,7 @@ public void HttpResponseHandler_ResponseSent_FiresAllSubscribersWhenOneThrows() handler += (_, _) => throw new InvalidOperationException("first ResponseSent subscriber throws"); handler += (_, r) => received.Add(r.ContentType ?? "null"); - MulticastIsolation.Raise(handler!, this, response, null); + handler!.Raise(this, response, null); Assert.That(received, Is.EqualTo(new[] { "application/xml" })); } @@ -57,7 +57,7 @@ public void HttpResponseHandler_ResponseSent_NullInternalErrorSwallowsFault() var response = new MTConnectHttpResponse { ContentType = "application/xml" }; EventHandler handler = (_, _) => throw new InvalidOperationException("ResponseSent fault"); - Assert.DoesNotThrow(() => MulticastIsolation.Raise(handler, this, response, null)); + Assert.DoesNotThrow(() => handler.Raise(this, response, null)); } // ----------------------------------------------------------------------- @@ -75,7 +75,7 @@ public void HttpResponseHandler_ClientConnected_FiresAllSubscribersWhenOneThrows handler += (_, _) => throw new InvalidOperationException("first ClientConnected subscriber throws"); handler += (_, _) => firedCount++; - MulticastIsolation.Raise(handler!, this, (IHttpRequest?)null, null); + handler!.Raise(this, (IHttpRequest?)null, null); Assert.That(firedCount, Is.EqualTo(1)); } @@ -95,7 +95,7 @@ public void HttpResponseHandler_ClientDisconnected_FiresAllSubscribersWhenOneThr handler += (_, _) => throw new InvalidOperationException("first ClientDisconnected subscriber throws"); handler += (_, endpoint) => received.Add(endpoint ?? "null"); - MulticastIsolation.Raise(handler!, this, "127.0.0.1:5000", null); + handler!.Raise(this, "127.0.0.1:5000", null); Assert.That(received, Is.EqualTo(new[] { "127.0.0.1:5000" })); } @@ -116,7 +116,7 @@ public void HttpResponseHandler_ClientException_FiresAllSubscribersWhenOneThrows handler += (_, _) => throw new InvalidOperationException("first ClientException subscriber throws"); handler += (_, ex) => received.Add(ex.Message); - MulticastIsolation.Raise(handler!, this, payload, null); + handler!.Raise(this, payload, null); Assert.That(received, Is.EqualTo(new[] { "http-context-error" })); } @@ -136,7 +136,7 @@ public void HttpServerStream_StreamStarted_FiresAllSubscribersWhenOneThrows() handler += (_, _) => throw new InvalidOperationException("first StreamStarted subscriber throws"); handler += (_, id) => received.Add(id ?? "null"); - MulticastIsolation.Raise(handler!, this, "stream-id-1", null); + handler!.Raise(this, "stream-id-1", null); Assert.That(received, Is.EqualTo(new[] { "stream-id-1" })); } @@ -147,7 +147,7 @@ public void HttpServerStream_StreamStopped_NullInternalErrorSwallowsFault() { EventHandler handler = (_, _) => throw new InvalidOperationException("StreamStopped fault"); - Assert.DoesNotThrow(() => MulticastIsolation.Raise(handler, this, "stream-id-1", null)); + Assert.DoesNotThrow(() => handler.Raise(this, "stream-id-1", null)); } // ----------------------------------------------------------------------- @@ -166,7 +166,7 @@ public void HttpServerStream_StreamException_FiresAllSubscribersWhenOneThrows() handler += (_, _) => throw new InvalidOperationException("first StreamException subscriber throws"); handler += (_, ex) => received.Add(ex.Message); - MulticastIsolation.Raise(handler!, this, payload, null); + handler!.Raise(this, payload, null); Assert.That(received, Is.EqualTo(new[] { "stream-broke" })); } @@ -187,7 +187,7 @@ public void HttpServerStream_DocumentReceived_FiresAllSubscribersWhenOneThrows() handler += (_, _) => throw new InvalidOperationException("first DocumentReceived subscriber throws"); handler += (_, a) => received.Add(a.StreamId); - MulticastIsolation.Raise(handler!, this, args, null); + handler!.Raise(this, args, null); Assert.That(received, Is.EqualTo(new[] { "stream-id-2" })); } @@ -199,7 +199,7 @@ public void HttpServerStream_HeartbeatReceived_NullInternalErrorSwallowsFault() var args = new MTConnectHttpStreamArgs("stream-id-2", System.IO.Stream.Null, 42.5); EventHandler handler = (_, _) => throw new InvalidOperationException("HeartbeatReceived fault"); - Assert.DoesNotThrow(() => MulticastIsolation.Raise(handler, this, args, null)); + Assert.DoesNotThrow(() => handler.Raise(this, args, null)); } // ----------------------------------------------------------------------- @@ -212,7 +212,7 @@ public void HttpServerStream_NullGenericHandler_DoesNotThrow() { EventHandler? handler = null; - Assert.DoesNotThrow(() => MulticastIsolation.Raise(handler, this, "x", null)); + Assert.DoesNotThrow(() => handler.Raise(this, "x", null)); } } } diff --git a/tests/MTConnect.NET-SHDR-Tests/ShdrMulticastIsolationTests.cs b/tests/MTConnect.NET-SHDR-Tests/ShdrMulticastIsolationTests.cs index 2b744a93..5e898aff 100644 --- a/tests/MTConnect.NET-SHDR-Tests/ShdrMulticastIsolationTests.cs +++ b/tests/MTConnect.NET-SHDR-Tests/ShdrMulticastIsolationTests.cs @@ -39,7 +39,7 @@ public void Shdr_Generic_EventHandlerOfString_FiresAllSubscribersWhenOneThrows() handler += (_, _) => throw new InvalidOperationException("first shdr subscriber throws"); handler += (_, v) => received.Add(v); - MulticastIsolation.Raise(handler, this, "shdr-payload", null); + handler.Raise(this, "shdr-payload", null); Assert.That(received, Is.EqualTo(new[] { "shdr-payload" })); } @@ -50,7 +50,7 @@ public void Shdr_Generic_EventHandlerOfString_NullInternalErrorSwallowsFault() { EventHandler handler = (_, _) => throw new InvalidOperationException("shdr-fault"); - Assert.DoesNotThrow(() => MulticastIsolation.Raise(handler, this, "x", null)); + Assert.DoesNotThrow(() => handler.Raise(this, "x", null)); } /// Pins the behavior expressed by the test name: multiple string-payload subscribers all fire when no subscriber throws, covering the happy path for AgentConnected and related events. @@ -63,7 +63,7 @@ public void Shdr_Generic_EventHandlerOfString_AllSubscribersFireOnHappyPath() handler += (_, v) => received.Add("sub1:" + v); handler += (_, v) => received.Add("sub2:" + v); - MulticastIsolation.Raise(handler, this, "hello", null); + handler.Raise(this, "hello", null); Assert.That(received, Is.EqualTo(new[] { "sub1:hello", "sub2:hello" })); } @@ -84,7 +84,7 @@ public void Shdr_Generic_EventHandlerOfAdapterEventArgsString_FiresAllSubscriber handler += (_, _) => throw new InvalidOperationException("first LineSent subscriber throws"); handler += (_, e) => received.Add(e); - MulticastIsolation.Raise(handler, this, payload, null); + handler.Raise(this, payload, null); Assert.That(received.Count, Is.EqualTo(1)); Assert.That(received[0].ClientId, Is.EqualTo("client-1")); @@ -98,7 +98,7 @@ public void Shdr_Generic_EventHandlerOfAdapterEventArgsString_NullInternalErrorS var payload = new AdapterEventArgs("c1", "data"); EventHandler> handler = (_, _) => throw new InvalidOperationException("fault"); - Assert.DoesNotThrow(() => MulticastIsolation.Raise(handler, this, payload, null)); + Assert.DoesNotThrow(() => handler.Raise(this, payload, null)); } // ----------------------------------------------------------------------- @@ -118,7 +118,7 @@ public void Shdr_Generic_EventHandlerOfAdapterEventArgsException_FiresAllSubscri handler += (_, _) => throw new InvalidOperationException("first ConnectionError subscriber throws"); handler += (_, e) => received.Add(e); - MulticastIsolation.Raise(handler, this, payload, null); + handler.Raise(this, payload, null); Assert.That(received.Count, Is.EqualTo(1)); Assert.That(received[0].ClientId, Is.EqualTo("client-2")); @@ -141,7 +141,7 @@ public void Shdr_Generic_EventHandlerOfException_FiresAllSubscribersWhenOneThrow handler += (_, _) => throw new InvalidOperationException("first ConnectionError subscriber throws"); handler += (_, ex) => received.Add(ex.Message); - MulticastIsolation.Raise(handler, this, payload, null); + handler.Raise(this, payload, null); Assert.That(received, Is.EqualTo(new[] { "connection-refused" })); } @@ -156,7 +156,7 @@ public void Shdr_NullGenericHandler_DoesNotThrow() { EventHandler handler = null; - Assert.DoesNotThrow(() => MulticastIsolation.Raise(handler, this, "x", null)); + Assert.DoesNotThrow(() => handler.Raise(this, "x", null)); } } }