diff --git a/libraries/MTConnect.NET-Common/Agents/MTConnectAgent.cs b/libraries/MTConnect.NET-Common/Agents/MTConnectAgent.cs
index c61cadeec..e1276ec00 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);
@@ -1522,7 +1522,7 @@ private bool AddDeviceAddedObservation(IDevice device, long timestamp = 0)
new ObservationValue(ValueKeys.Result, device.Uuid)
});
- ObservationAdded?.Invoke(this, observation);
+ 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)
});
- ObservationAdded?.Invoke(this, observation);
+ 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)
});
- ObservationAdded?.Invoke(this, observation);
+ ObservationAdded.Raise(this, observation, null);
return true;
}
@@ -1717,7 +1717,7 @@ public IDevice AddDevice(IDevice device, bool initializeDataItems = true)
_updateInformation = true;
}
- DeviceAdded?.Invoke(this, obj);
+ DeviceAdded.Raise(this, obj, null);
return obj;
}
@@ -2124,7 +2124,7 @@ public bool AddObservation(string deviceKey, IObservationInput observationInput,
{
if (observationInput != null)
{
- ObservationReceived?.Invoke(this, observationInput);
+ ObservationReceived.Raise(this, observationInput, null);
IObservationInput input = new ObservationInput();
input.DeviceKey = deviceKey;
@@ -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));
}
}
@@ -2333,28 +2334,19 @@ protected virtual ulong OnAddObservation(string deviceUuid, IDataItem dataItem,
///
public void OnObservationAdded(IObservation observation)
{
- if (ObservationAdded != null)
- {
- ObservationAdded?.Invoke(this, observation);
- }
+ ObservationAdded.Raise(this, observation, null);
}
///
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
@@ -2453,16 +2445,16 @@ 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));
}
- AssetAdded?.Invoke(this, asset);
+ AssetAdded.Raise(this, asset, null);
return true;
}
}
else
{
- if (InvalidAssetAdded != null) InvalidAssetAdded.Invoke(asset, validationResults);
+ MulticastIsolation.Raise(InvalidAssetAdded, h => h(asset, validationResults));
}
}
}
diff --git a/libraries/MTConnect.NET-Common/Agents/MTConnectAgentBroker.cs b/libraries/MTConnect.NET-Common/Agents/MTConnectAgentBroker.cs
index f1662b9c9..7095a10cb 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)
{
@@ -773,7 +773,7 @@ public IStreamsResponseOutputDocument GetDeviceStreamsResponseDocument(uint coun
var document = CreateDeviceStreamsDocument(devices, ref results, mtconnectVersion);
if (document != null)
{
- StreamsResponseSent?.Invoke(this, new EventArgs());
+ StreamsResponseSent.Raise(this, EventArgs.Empty, null);
return document;
}
}
@@ -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)
{
@@ -809,7 +809,7 @@ public IStreamsResponseOutputDocument GetDeviceStreamsResponseDocument(ulong at,
var document = CreateDeviceStreamsDocument(devices, ref results, mtconnectVersion);
if (document != null)
{
- StreamsResponseSent?.Invoke(this, new EventArgs());
+ StreamsResponseSent.Raise(this, EventArgs.Empty, null);
return document;
}
}
@@ -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)
{
@@ -850,7 +850,7 @@ public IStreamsResponseOutputDocument GetDeviceStreamsResponseDocument(IEnumerab
var document = CreateDeviceStreamsDocument(devices, ref results, mtconnectVersion);
if (document != null)
{
- StreamsResponseSent?.Invoke(this, new EventArgs());
+ StreamsResponseSent.Raise(this, EventArgs.Empty, null);
return document;
}
}
@@ -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)
{
@@ -892,7 +892,7 @@ public IStreamsResponseOutputDocument GetDeviceStreamsResponseDocument(IEnumerab
var document = CreateDeviceStreamsDocument(devices, ref results, mtconnectVersion);
if (document != null)
{
- StreamsResponseSent?.Invoke(this, new EventArgs());
+ StreamsResponseSent.Raise(this, EventArgs.Empty, null);
return document;
}
}
@@ -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)
{
@@ -929,7 +929,7 @@ public IStreamsResponseOutputDocument GetDeviceStreamsResponseDocument(ulong fro
var document = CreateDeviceStreamsDocument(devices, ref results, mtconnectVersion);
if (document != null)
{
- StreamsResponseSent?.Invoke(this, new EventArgs());
+ StreamsResponseSent.Raise(this, EventArgs.Empty, null);
return document;
}
}
@@ -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)
{
@@ -967,7 +967,7 @@ public IStreamsResponseOutputDocument GetDeviceStreamsResponseDocument(IEnumerab
var document = CreateDeviceStreamsDocument(devices, ref results, mtconnectVersion);
if (document != null)
{
- StreamsResponseSent?.Invoke(this, new EventArgs());
+ StreamsResponseSent.Raise(this, EventArgs.Empty, null);
return document;
}
}
@@ -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)
{
@@ -1003,7 +1003,7 @@ public IStreamsResponseOutputDocument GetDeviceStreamsResponseDocument(string de
var document = CreateDeviceStreamsDocument(device, ref results, mtconnectVersion);
if (document != null)
{
- StreamsResponseSent?.Invoke(this, new EventArgs());
+ StreamsResponseSent.Raise(this, EventArgs.Empty, null);
return document;
}
}
@@ -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)
{
@@ -1039,7 +1039,7 @@ public IStreamsResponseOutputDocument GetDeviceStreamsResponseDocument(string de
var document = CreateDeviceStreamsDocument(device, ref results, mtconnectVersion);
if (document != null)
{
- StreamsResponseSent?.Invoke(this, new EventArgs());
+ StreamsResponseSent.Raise(this, EventArgs.Empty, null);
return document;
}
}
@@ -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)
{
@@ -1075,7 +1075,7 @@ public IStreamsResponseOutputDocument GetDeviceStreamsResponseDocument(string de
var document = CreateDeviceStreamsDocument(device, ref results, mtconnectVersion);
if (document != null)
{
- StreamsResponseSent?.Invoke(this, new EventArgs());
+ StreamsResponseSent.Raise(this, EventArgs.Empty, null);
return document;
}
}
@@ -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)
{
@@ -1112,7 +1112,7 @@ public IStreamsResponseOutputDocument GetDeviceStreamsResponseDocument(string de
var document = CreateDeviceStreamsDocument(device, ref results, mtconnectVersion);
if (document != null)
{
- StreamsResponseSent?.Invoke(this, new EventArgs());
+ StreamsResponseSent.Raise(this, EventArgs.Empty, null);
return document;
}
}
@@ -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)
{
@@ -1149,7 +1149,7 @@ public IStreamsResponseOutputDocument GetDeviceStreamsResponseDocument(string de
var document = CreateDeviceStreamsDocument(device, ref results, mtconnectVersion);
if (document != null)
{
- StreamsResponseSent?.Invoke(this, new EventArgs());
+ StreamsResponseSent.Raise(this, EventArgs.Empty, null);
return document;
}
}
@@ -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)
{
@@ -1187,7 +1187,7 @@ public IStreamsResponseOutputDocument GetDeviceStreamsResponseDocument(string de
var document = CreateDeviceStreamsDocument(device, ref results, mtconnectVersion);
if (document != null)
{
- StreamsResponseSent?.Invoke(this, new EventArgs());
+ StreamsResponseSent.Raise(this, EventArgs.Empty, null);
return document;
}
}
@@ -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;
}
diff --git a/libraries/MTConnect.NET-Common/MulticastIsolation.cs b/libraries/MTConnect.NET-Common/MulticastIsolation.cs
new file mode 100644
index 000000000..603000a52
--- /dev/null
+++ b/libraries/MTConnect.NET-Common/MulticastIsolation.cs
@@ -0,0 +1,163 @@
+// 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.
+ ///
+ ///
+ /// 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
+ {
+ ///
+ /// 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. Exposed as an extension method so call sites read as
+ /// MyEvent.Raise(this, arg).
+ ///
+ /// 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;
+
+ 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. Exposed as an extension method so call sites
+ /// read as MyEvent.Raise(this, EventArgs.Empty).
+ ///
+ /// 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;
+
+ foreach (var subscriber in handler.GetInvocationList())
+ {
+ try
+ {
+ ((EventHandler)subscriber).Invoke(sender, arg);
+ }
+ catch (Exception ex)
+ {
+ RaiseInternalError(internalError, sender, ex);
+ }
+ }
+ }
+
+ ///
+ /// 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
+ // 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/libraries/MTConnect.NET-DeviceFinder/MTConnectDeviceFinder.cs b/libraries/MTConnect.NET-DeviceFinder/MTConnectDeviceFinder.cs
index 71e77c25f..de4d2d049 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 d2b415e7a..ecaa91c7f 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
diff --git a/libraries/MTConnect.NET-HTTP/Clients/MTConnectHttpAssetClient.cs b/libraries/MTConnect.NET-HTTP/Clients/MTConnectHttpAssetClient.cs
index e8484adb9..4c734d946 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);
+ ConnectionError.Raise(this, ex, InternalError);
}
catch (TaskCanceledException) { /* Ignore Task Cancelled */ }
catch (HttpRequestException ex)
{
- ConnectionError?.Invoke(this, ex);
+ ConnectionError.Raise(this, ex, InternalError);
}
catch (Exception ex)
{
- InternalError?.Invoke(this, ex);
+ 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)
{
- ConnectionError?.Invoke(this, ex);
+ ConnectionError.Raise(this, ex, InternalError);
}
catch (TaskCanceledException) { /* Ignore Task Cancelled */ }
catch (HttpRequestException ex)
{
- ConnectionError?.Invoke(this, ex);
+ ConnectionError.Raise(this, ex, InternalError);
}
catch (Exception ex)
{
- InternalError?.Invoke(this, ex);
+ InternalError.Raise(this, ex, InternalError);
}
return null;
@@ -346,7 +346,7 @@ private IAssetsResponseDocument HandleResponse(HttpResponseMessage response)
{
if (!response.IsSuccessStatusCode)
{
- ConnectionError?.Invoke(this, new Exception(response.ReasonPhrase));
+ 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)
{
- ConnectionError?.Invoke(this, new Exception(response.ReasonPhrase));
+ 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) MTConnectError?.Invoke(this, errorDocument);
+ if (errorDocument != null) MTConnectError.Raise(this, errorDocument, InternalError);
}
else
{
// Raise Format Error
- if (FormatError != null) FormatError.Invoke(this, errorFormatResult);
+ FormatError.Raise(this, errorFormatResult, InternalError);
}
}
}
else
{
// Raise Format Error
- if (FormatError != null) FormatError.Invoke(this, formatResult);
+ FormatError.Raise(this, formatResult, InternalError);
}
}
diff --git a/libraries/MTConnect.NET-HTTP/Clients/MTConnectHttpClient.cs b/libraries/MTConnect.NET-HTTP/Clients/MTConnectHttpClient.cs
index 13a029cc9..caa38aeaa 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());
+ ClientStarting.Raise(this, EventArgs.Empty, InternalError);
_initializeFromBuffer = false;
_lastInstanceId = 0;
@@ -364,7 +364,7 @@ public void Start(string path)
{
_stop = new CancellationTokenSource();
- ClientStarting?.Invoke(this, new EventArgs());
+ 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(); });
- ClientStarting?.Invoke(this, new EventArgs());
+ 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();
- ClientStarting?.Invoke(this, new EventArgs());
+ 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(); });
- ClientStarting?.Invoke(this, new EventArgs());
+ ClientStarting.Raise(this, EventArgs.Empty, InternalError);
_initializeFromBuffer = true;
_lastInstanceId = instanceId;
@@ -434,7 +434,7 @@ public void StartFromBuffer(string path = null)
{
_stop = new CancellationTokenSource();
- ClientStarting?.Invoke(this, new EventArgs());
+ 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(); });
- ClientStarting?.Invoke(this, new EventArgs());
+ 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()
{
- ClientStopping?.Invoke(this, new EventArgs());
+ 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) => 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) => 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) => 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) => 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) => 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) => 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) => 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) => 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) => 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) => 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) => 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) => 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) => 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) => 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) => 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) => 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) => 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) => 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) => 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) => 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;
- ClientStarted?.Invoke(this, new EventArgs());
+ ClientStarted.Raise(this, EventArgs.Empty, InternalError);
do
{
@@ -708,7 +708,7 @@ private async Task Worker()
if (probe != null)
{
_lastResponse = UnixDateTime.Now;
- ResponseReceived?.Invoke(this, new EventArgs());
+ ResponseReceived.Raise(this, EventArgs.Empty, InternalError);
ProcessProbeDocument(probe);
@@ -719,9 +719,9 @@ private async Task Worker()
if (assets != null)
{
_lastResponse = UnixDateTime.Now;
- ResponseReceived?.Invoke(this, new EventArgs());
+ ResponseReceived.Raise(this, EventArgs.Empty, InternalError);
- AssetsReceived?.Invoke(this, assets);
+ AssetsReceived.Raise(this, assets, InternalError);
}
}
@@ -732,7 +732,7 @@ private async Task Worker()
if (current != null)
{
_lastResponse = UnixDateTime.Now;
- ResponseReceived?.Invoke(this, new EventArgs());
+ 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) => 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) => 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) => FormatError?.Invoke(this, r);
- _stream.ConnectionError += (s, ex) => ConnectionError?.Invoke(this, ex);
- _stream.InternalError += (s, ex) => InternalError?.Invoke(this, ex);
+ _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;
- ResponseReceived?.Invoke(this, new EventArgs());
+ 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)
{
- InternalError?.Invoke(this, ex);
+ InternalError.Raise(this, ex, InternalError);
}
} while (!_stop.Token.IsCancellationRequested);
- ClientStopped?.Invoke(this, new EventArgs());
+ ClientStopped.Raise(this, EventArgs.Empty, InternalError);
}
private void ProcessProbeDocument(IDevicesResponseDocument document)
@@ -907,47 +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.
- RaiseDeviceReceived(outputDevice);
+ DeviceReceived.Raise(this, outputDevice, InternalError);
}
// Raise ProbeReceived Event
- ProbeReceived?.Invoke(this, 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)
- {
- var handler = DeviceReceived;
- if (handler == null) return;
-
- foreach (var subscriber in handler.GetInvocationList())
- {
- try
- {
- ((EventHandler)subscriber).Invoke(this, device);
- }
- catch (Exception ex)
- {
- try
- {
- InternalError?.Invoke(this, ex);
- }
- catch
- {
- // A faulting InternalError handler must not break DeviceReceived fan-out.
- }
- }
+ ProbeReceived.Raise(this, document, InternalError);
}
}
private void ProcessCurrentDocument(IStreamsResponseDocument document, CancellationToken cancel)
{
_lastResponse = UnixDateTime.Now;
- ResponseReceived?.Invoke(this, new EventArgs());
+ ResponseReceived.Raise(this, EventArgs.Empty, InternalError);
if (document != null)
{
@@ -965,7 +936,7 @@ private void ProcessCurrentDocument(IStreamsResponseDocument document, Cancellat
response.Streams = deviceStreams;
- CurrentReceived?.Invoke(this, response);
+ CurrentReceived.Raise(this, response, InternalError);
// Process Device Streams
@@ -980,7 +951,7 @@ private void ProcessCurrentDocument(IStreamsResponseDocument document, Cancellat
{
foreach (var observation in observations)
{
- ObservationReceived?.Invoke(this, observation);
+ ObservationReceived.Raise(this, observation, InternalError);
}
}
}
@@ -991,7 +962,7 @@ private void ProcessCurrentDocument(IStreamsResponseDocument document, Cancellat
private void ProcessSampleDocument(IStreamsResponseDocument document, CancellationToken cancel)
{
_lastResponse = UnixDateTime.Now;
- ResponseReceived?.Invoke(this, new EventArgs());
+ ResponseReceived.Raise(this, EventArgs.Empty, InternalError);
if (document != null)
{
@@ -1031,11 +1002,11 @@ private void ProcessSampleDocument(IStreamsResponseDocument document, Cancellati
}
}
- SampleReceived?.Invoke(this, response);
+ SampleReceived.Raise(this, response, InternalError);
foreach (var observation in receivedObservations)
{
- ObservationReceived?.Invoke(this, observation);
+ ObservationReceived.Raise(this, observation, InternalError);
}
}
}
@@ -1223,11 +1194,11 @@ private IComponentStream ProcessComponentStream(IMTConnectStreamsHeader header,
private void ProcessSampleError(IErrorResponseDocument document)
{
_lastResponse = UnixDateTime.Now;
- ResponseReceived?.Invoke(this, new EventArgs());
+ ResponseReceived.Raise(this, EventArgs.Empty, InternalError);
if (document != null)
{
- MTConnectError?.Invoke(this, document);
+ MTConnectError.Raise(this, document, InternalError);
}
}
@@ -1247,13 +1218,13 @@ private async void CheckAssetChanged(IEnumerable observations, Can
var doc = await GetAssetAsync(assetId, cancel);
if (doc != null)
{
- AssetsReceived?.Invoke(this, doc);
+ AssetsReceived.Raise(this, doc, InternalError);
if (doc != null && !doc.Assets.IsNullOrEmpty())
{
foreach (var asset in doc.Assets)
{
- AssetReceived?.Invoke(this, asset);
+ AssetReceived.Raise(this, asset, InternalError);
}
}
}
diff --git a/libraries/MTConnect.NET-HTTP/Clients/MTConnectHttpClientStream.cs b/libraries/MTConnect.NET-HTTP/Clients/MTConnectHttpClientStream.cs
index 9fe6dc59e..71c27bb25 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());
+ 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
- Stopping?.Invoke(this, new EventArgs());
+ 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();
- ConnectionError?.Invoke(this, new TimeoutException($"HTTP Stream Timeout Exceeded ({Timeout})"));
+ 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
- Started?.Invoke(this, new EventArgs());
+ 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)
{
- ConnectionError?.Invoke(this, ex);
+ ConnectionError.Raise(this, ex, InternalError);
}
catch (TaskCanceledException) { /* Ignore Task Cancelled */ }
catch (HttpRequestException ex)
{
- ConnectionError?.Invoke(this, ex);
+ ConnectionError.Raise(this, ex, InternalError);
}
catch (Exception ex)
{
- InternalError?.Invoke(this, ex);
+ InternalError.Raise(this, ex, InternalError);
}
finally
{
@@ -322,7 +322,7 @@ public async Task Run(CancellationToken cancellationToken)
}
}
- Stopped?.Invoke(this, new EventArgs());
+ 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)
{
- DocumentReceived?.Invoke(this, document);
+ 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) ErrorReceived?.Invoke(this, errorDocument);
+ if (errorDocument != null) ErrorReceived.Raise(this, errorDocument, InternalError);
}
else
{
// Raise Format Error
- if (FormatError != null) FormatError.Invoke(this, errorFormatResult);
+ FormatError.Raise(this, errorFormatResult, InternalError);
}
}
}
else
{
// Raise Format Error
- if (FormatError != null) FormatError.Invoke(this, formatResult);
+ FormatError.Raise(this, formatResult, InternalError);
}
}
}
diff --git a/libraries/MTConnect.NET-HTTP/Clients/MTConnectHttpCurrentClient.cs b/libraries/MTConnect.NET-HTTP/Clients/MTConnectHttpCurrentClient.cs
index 5d681081c..859b13bd6 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);
+ ConnectionError.Raise(this, ex, InternalError);
}
catch (TaskCanceledException) { /* Ignore Task Cancelled */ }
catch (HttpRequestException ex)
{
- ConnectionError?.Invoke(this, ex);
+ ConnectionError.Raise(this, ex, InternalError);
}
catch (Exception ex)
{
- InternalError?.Invoke(this, ex);
+ 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)
{
- ConnectionError?.Invoke(this, ex);
+ ConnectionError.Raise(this, ex, InternalError);
}
catch (TaskCanceledException) { /* Ignore Task Cancelled */ }
catch (HttpRequestException ex)
{
- ConnectionError?.Invoke(this, ex);
+ ConnectionError.Raise(this, ex, InternalError);
}
catch (Exception ex)
{
- InternalError?.Invoke(this, ex);
+ InternalError.Raise(this, ex, InternalError);
}
return null;
@@ -348,7 +348,7 @@ private IStreamsResponseDocument HandleResponse(HttpResponseMessage response)
{
if (!response.IsSuccessStatusCode)
{
- ConnectionError?.Invoke(this, new Exception(response.ReasonPhrase));
+ 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)
{
- ConnectionError?.Invoke(this, new Exception(response.ReasonPhrase));
+ 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) MTConnectError?.Invoke(this, errorDocument);
+ if (errorDocument != null) MTConnectError.Raise(this, errorDocument, InternalError);
}
else
{
// Raise Format Error
- if (FormatError != null) FormatError.Invoke(this, errorFormatResult);
+ FormatError.Raise(this, errorFormatResult, InternalError);
}
}
}
else
{
// Raise Format Error
- if (FormatError != null) FormatError.Invoke(this, formatResult);
+ FormatError.Raise(this, formatResult, InternalError);
}
}
diff --git a/libraries/MTConnect.NET-HTTP/Clients/MTConnectHttpProbeClient.cs b/libraries/MTConnect.NET-HTTP/Clients/MTConnectHttpProbeClient.cs
index e34729c91..af90cebc4 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);
+ ConnectionError.Raise(this, ex, InternalError);
}
catch (TaskCanceledException) { /* Ignore Task Cancelled */ }
catch (HttpRequestException ex)
{
- ConnectionError?.Invoke(this, ex);
+ ConnectionError.Raise(this, ex, InternalError);
}
catch (Exception ex)
{
- InternalError?.Invoke(this, ex);
+ 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)
{
- ConnectionError?.Invoke(this, ex);
+ ConnectionError.Raise(this, ex, InternalError);
}
catch (TaskCanceledException) { /* Ignore Task Cancelled */ }
catch (HttpRequestException ex)
{
- ConnectionError?.Invoke(this, ex);
+ ConnectionError.Raise(this, ex, InternalError);
}
catch (Exception ex)
{
- InternalError?.Invoke(this, ex);
+ InternalError.Raise(this, ex, InternalError);
}
return null;
@@ -388,7 +388,7 @@ private IDevicesResponseDocument HandleResponse(HttpResponseMessage response)
{
if (!response.IsSuccessStatusCode)
{
- ConnectionError?.Invoke(this, new Exception(response.ReasonPhrase));
+ 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)
{
- ConnectionError?.Invoke(this, new Exception(response.ReasonPhrase));
+ 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) MTConnectError?.Invoke(this, errorDocument);
+ if (errorDocument != null) MTConnectError.Raise(this, errorDocument, InternalError);
}
else
{
// Raise Format Error
- if (FormatError != null) FormatError.Invoke(this, errorFormatResult);
+ FormatError.Raise(this, errorFormatResult, InternalError);
}
}
}
else
{
// Raise Format Error
- if (FormatError != null) FormatError.Invoke(this, formatResult);
+ FormatError.Raise(this, formatResult, InternalError);
}
}
diff --git a/libraries/MTConnect.NET-HTTP/Clients/MTConnectHttpSampleClient.cs b/libraries/MTConnect.NET-HTTP/Clients/MTConnectHttpSampleClient.cs
index 7a0535003..56ac4990c 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);
+ ConnectionError.Raise(this, ex, InternalError);
}
catch (TaskCanceledException) { /* Ignore Task Cancelled */ }
catch (HttpRequestException ex)
{
- ConnectionError?.Invoke(this, ex);
+ ConnectionError.Raise(this, ex, InternalError);
}
catch (Exception ex)
{
- InternalError?.Invoke(this, ex);
+ 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)
{
- ConnectionError?.Invoke(this, ex);
+ ConnectionError.Raise(this, ex, InternalError);
}
catch (TaskCanceledException) { /* Ignore Task Cancelled */ }
catch (HttpRequestException ex)
{
- ConnectionError?.Invoke(this, ex);
+ ConnectionError.Raise(this, ex, InternalError);
}
catch (Exception ex)
{
- InternalError?.Invoke(this, ex);
+ InternalError.Raise(this, ex, InternalError);
}
return null;
@@ -382,7 +382,7 @@ private IStreamsResponseDocument HandleResponse(HttpResponseMessage response)
{
if (!response.IsSuccessStatusCode)
{
- ConnectionError?.Invoke(this, new Exception(response.ReasonPhrase));
+ 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)
{
- ConnectionError?.Invoke(this, new Exception(response.ReasonPhrase));
+ 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) MTConnectError?.Invoke(this, errorDocument);
+ if (errorDocument != null) MTConnectError.Raise(this, errorDocument, InternalError);
}
else
{
// Raise Format Error
- if (FormatError != null) FormatError.Invoke(this, errorFormatResult);
+ FormatError.Raise(this, errorFormatResult, InternalError);
}
}
}
else
{
// Raise Format Error
- if (FormatError != null) FormatError.Invoke(this, formatResult);
+ FormatError.Raise(this, formatResult, InternalError);
}
}
diff --git a/libraries/MTConnect.NET-HTTP/Servers/MTConnectHttpResponseHandler.cs b/libraries/MTConnect.NET-HTTP/Servers/MTConnectHttpResponseHandler.cs
index 8720606af..bad3ddd00 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);
+ 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);
- ResponseSent?.Invoke(this, mtconnectResponse);
+ ResponseSent.Raise(this, mtconnectResponse, ClientException);
- ClientDisconnected?.Invoke(this, context.Request.RemoteEndPoint?.ToString());
+ 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)
{
- if (ClientException != null) ClientException.Invoke(this, ex);
+ ClientException.Raise(this, ex, null);
}
}
catch (ObjectDisposedException) { }
catch (Exception ex)
{
- if (ClientException != null) ClientException.Invoke(this, ex);
+ ClientException.Raise(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);
+ ClientDisconnected.Raise(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);
+ 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 f7e5a2d27..88a060f39 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);
+ 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
- HeartbeatReceived?.Invoke(this, new MTConnectHttpStreamArgs(_id, outputStream, stpw.ElapsedMilliseconds));
+ 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
- DocumentReceived?.Invoke(this, new MTConnectHttpStreamArgs(_id, outputStream, stpw.ElapsedMilliseconds));
+ 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)
{
- StreamException?.Invoke(this, ex);
+ StreamException.Raise(this, ex, null);
throw new Exception();
}
- StreamStopped?.Invoke(this, _id);
+ StreamStopped.Raise(this, _id, StreamException);
}
}
diff --git a/libraries/MTConnect.NET-MQTT/Clients/MTConnectMqttClient.cs b/libraries/MTConnect.NET-MQTT/Clients/MTConnectMqttClient.cs
index 61b2ea16b..afc7c6872 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());
+ ClientStarting.Raise(this, EventArgs.Empty, InternalError);
_ = Task.Run(Worker, _stop.Token);
}
@@ -239,7 +239,7 @@ public void Start()
///
public void Stop()
{
- ClientStopping?.Invoke(this, new EventArgs());
+ ClientStopping.Raise(this, EventArgs.Empty, InternalError);
if (_stop != null) _stop.Cancel();
}
@@ -353,7 +353,7 @@ private async Task Worker()
await StartAllDevicesProtocol();
}
- ClientStarted?.Invoke(this, new EventArgs());
+ ClientStarted.Raise(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);
+ 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)
{
- InternalError?.Invoke(this, ex);
+ InternalError.Raise(this, ex, InternalError);
}
} while (!_stop.Token.IsCancellationRequested);
@@ -384,7 +384,7 @@ private async Task Worker()
catch { }
- ClientStopped?.Invoke(this, new EventArgs());
+ ClientStopped.Raise(this, EventArgs.Empty, InternalError);
}
@@ -511,11 +511,11 @@ private void ProcessProbeMessage(MqttApplicationMessage message)
_devices.Add(outputDevice.Uuid, outputDevice);
}
- DeviceReceived?.Invoke(this, outputDevice);
+ DeviceReceived.Raise(this, outputDevice, InternalError);
}
}
- ProbeReceived?.Invoke(this, responseDocument);
+ ProbeReceived.Raise(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());
+ ResponseReceived.Raise(this, EventArgs.Empty, InternalError);
if (document != null)
{
@@ -585,7 +585,7 @@ private void ProcessCurrentDocument(IStreamsResponseDocument document)
response.Streams = deviceStreams;
- CurrentReceived?.Invoke(this, response);
+ CurrentReceived.Raise(this, response, InternalError);
// Process Device Streams
@@ -610,7 +610,7 @@ private void ProcessCurrentDocument(IStreamsResponseDocument document)
{
if (observation.Sequence > lastSequence)
{
- ObservationReceived?.Invoke(this, observation);
+ ObservationReceived.Raise(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());
+ ResponseReceived.Raise(this, EventArgs.Empty, InternalError);
if (document != null)
{
@@ -653,7 +653,7 @@ private void ProcessSampleDocument(IStreamsResponseDocument document)
response.Streams = deviceStreams;
- SampleReceived?.Invoke(this, response);
+ SampleReceived.Raise(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);
+ ObservationReceived.Raise(this, observation, InternalError);
}
}
@@ -699,11 +699,11 @@ private void ProcessAssetsDocument(IAssetsResponseDocument document)
{
if (document != null && !document.Assets.IsNullOrEmpty())
{
- AssetsReceived?.Invoke(this, document);
+ AssetsReceived.Raise(this, document, InternalError);
foreach (var asset in document.Assets)
{
- AssetReceived?.Invoke(this, asset);
+ AssetReceived.Raise(this, asset, InternalError);
}
}
}
diff --git a/libraries/MTConnect.NET-MQTT/Clients/MTConnectMqttExpandedClient.cs b/libraries/MTConnect.NET-MQTT/Clients/MTConnectMqttExpandedClient.cs
index 144516178..917535888 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());
+ 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()
{
- ClientStopping?.Invoke(this, new EventArgs());
+ ClientStopping.Raise(this, EventArgs.Empty, InternalError);
if (_stop != null) _stop.Cancel();
}
@@ -341,7 +341,7 @@ private async Task Worker()
StartAllDevicesProtocol().Wait();
}
- ClientStarted?.Invoke(this, new EventArgs());
+ ClientStarted.Raise(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);
+ ConnectionError.Raise(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);
+ InternalError.Raise(this, ex, InternalError);
}
} while (!_stop.Token.IsCancellationRequested);
@@ -372,7 +372,7 @@ private async Task Worker()
catch { }
- ClientStopped?.Invoke(this, new EventArgs());
+ ClientStopped.Raise(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);
- }
+ ObservationReceived.Raise(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);
- }
+ ObservationReceived.Raise(deviceUuid, observation, InternalError);
}
}
}
@@ -662,7 +656,7 @@ private async Task ProcessAgent(MqttApplicationMessage message, Func(clientId, exception));
+ 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);
- LineSent?.Invoke(this, new AdapterEventArgs(client.Id, singleLine));
+ LineSent.Raise(this, new AdapterEventArgs(client.Id, singleLine), null);
}
catch (Exception ex)
{
- SendError?.Invoke(this, new AdapterEventArgs(client.Id, ex.Message));
+ 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);
- LineSent?.Invoke(this, new AdapterEventArgs(client.Id, line));
+ LineSent.Raise(this, new AdapterEventArgs(client.Id, line), null);
return true;
}
catch (Exception ex)
{
- SendError?.Invoke(this, new AdapterEventArgs(client.Id, ex.Message));
+ 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 88d319caa..a9c516012 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}");
+ 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);
- PingSent?.Invoke(this, $"Initial PING sent to : {Hostname} on Port {Port}");
+ 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);
- PingSent?.Invoke(this, $"PING sent to : {Hostname} on Port {Port}");
+ 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)
{
- ConnectionError?.Invoke(this, ex);
+ ConnectionError.Raise(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}");
+ 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);
- Listening?.Invoke(this, $"Listening for connection from {Hostname} on Port {Port}");
+ Listening.Raise(this, $"Listening for connection from {Hostname} on Port {Port}", null);
}
}
catch (TaskCanceledException) { }
catch (Exception ex)
{
- ConnectionError?.Invoke(this, ex);
+ ConnectionError.Raise(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}");
+ 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);
- PongReceived?.Invoke(this, $"PONG Received from : {Hostname} on Port {Port} : Heartbeat = {_heartbeat}ms");
+ 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
- ProtocolReceived?.Invoke(this, line);
+ 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
- ProtocolReceived?.Invoke(this, line);
+ 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
- ProtocolReceived?.Invoke(this, line);
+ 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
- ProtocolReceived?.Invoke(this, line);
+ 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
- ProtocolReceived?.Invoke(this, line);
+ 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
- ProtocolReceived?.Invoke(this, line);
+ 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
- ProtocolReceived?.Invoke(this, line);
+ 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
- ProtocolReceived?.Invoke(this, line);
+ 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
- ProtocolReceived?.Invoke(this, line);
+ 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
- ProtocolReceived?.Invoke(this, line);
+ 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
- ProtocolReceived?.Invoke(this, line);
+ ProtocolReceived.Raise(this, line, null);
found = true;
}
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 000000000..71d570b2f
--- /dev/null
+++ b/tests/MTConnect.NET-Common-Tests/Agents/AgentMulticastIsolationTests.cs
@@ -0,0 +1,605 @@
+// 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.Devices.DataItems;
+using MTConnect.Errors;
+using MTConnect.Input;
+using MTConnect.Observations;
+using NUnit.Framework;
+
+namespace MTConnect.Tests.Common
+{
+ ///
+ /// 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
+ {
+ // -----------------------------------------------------------------------
+ // 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");
+
+ handler!.Raise(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(() => handler.Raise(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();
+ handler!.Raise(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(() => handler.Raise(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();
+ handler!.Raise(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(() => handler.Raise(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 };
+ handler!.Raise(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(() => handler.Raise(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);
+
+ handler!.Raise(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(() => handler.Raise(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;
+ var device = new Device { Name = "noop-device", Uuid = "noop-uuid" };
+
+ 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.
+ [Test]
+ public void AgentBroker_NullNonGenericHandler_DoesNotThrow()
+ {
+ EventHandler? handler = null;
+
+ Assert.DoesNotThrow(() => handler.Raise(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!)));
+ }
+ }
+}
diff --git a/tests/MTConnect.NET-Common-Tests/DeviceFinderMulticastIsolationTests.cs b/tests/MTConnect.NET-Common-Tests/DeviceFinderMulticastIsolationTests.cs
new file mode 100644
index 000000000..ae37ba301
--- /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