diff --git a/AyCode.Core/Helpers/TaskHelper.cs b/AyCode.Core/Helpers/TaskHelper.cs
index 4cc1ac2..3aa3dbb 100644
--- a/AyCode.Core/Helpers/TaskHelper.cs
+++ b/AyCode.Core/Helpers/TaskHelper.cs
@@ -1,7 +1,27 @@
-namespace AyCode.Core.Helpers
+using System.Runtime.CompilerServices;
+using AyCode.Core.Loggers;
+
+namespace AyCode.Core.Helpers
{
public static class TaskHelper
{
+ ///
+ /// Optional ambient logger for fire-and-forget faults. Set once at startup (after the DI
+ /// container is built) in each host's entry point, e.g.
+ /// TaskHelper.Logger = new LoggerClient<TaskHelper>(writers);.
+ /// When null, faults are swallowed (legacy behaviour). A per-call logger argument
+ /// passed to any Forget overload overrides this ambient instance.
+ ///
+ public static IAcLoggerBase? Logger { get; set; }
+
+ ///
+ /// Logs a fire-and-forget fault. Normal cancellation is filtered out by the callers,
+ /// so anything reaching here is a real fault worth recording. Resolves the logger as
+ /// "explicit per-call argument, else ambient "; both null → silent.
+ ///
+ private static void LogForgetFault(Exception ex, IAcLoggerBase? logger, string caller, int line)
+ => (logger ?? Logger)?.Error($"Fire-and-forget faulted @ {caller}:{line}", ex);
+
public static bool WaitTo(Func predicate, int msTimeout = 10000, int msDelay = 5, int msFirstDelay = 0)
=> WaitToAsync(predicate, msTimeout, msDelay, msFirstDelay).GetAwaiter().GetResult();
@@ -48,56 +68,68 @@
return predicate();
}
- public static void Forget(this Task task)
+ public static void Forget(this Task task, IAcLoggerBase? logger = null, [CallerMemberName] string caller = "", [CallerLineNumber] int line = 0)
{
- if (!task.IsCompleted || task.IsFaulted)
- _ = ForgetAwaited(task);
+ if (!task.IsCompleted || task.IsFaulted) _ = ForgetAwaited(task, logger, caller, line);
+ return;
- static async Task ForgetAwaited(Task task)
+ static async Task ForgetAwaited(Task task, IAcLoggerBase? logger, string caller, int line)
{
try
{
await task.ConfigureAwait(false);
}
- catch
+ catch (OperationCanceledException)
{
- // Swallow exception - fire and forget semantics
+ // Normal cancellation — not a fault.
+ }
+ catch (Exception ex)
+ {
+ LogForgetFault(ex, logger, caller, line);
}
}
}
- public static void Forget(this ValueTask task)
+ public static void Forget(this ValueTask task, IAcLoggerBase? logger = null, [CallerMemberName] string caller = "", [CallerLineNumber] int line = 0)
{
- if (!task.IsCompleted || task.IsFaulted)
- _ = ForgetAwaited(task);
+ if (!task.IsCompleted || task.IsFaulted) _ = ForgetAwaited(task, logger, caller, line);
+ return;
- static async Task ForgetAwaited(ValueTask task)
+ static async Task ForgetAwaited(ValueTask task, IAcLoggerBase? logger, string caller, int line)
{
try
{
await task.ConfigureAwait(false);
}
- catch
+ catch (OperationCanceledException)
{
- // Swallow exception - fire and forget semantics
+ // Normal cancellation — not a fault.
+ }
+ catch (Exception ex)
+ {
+ LogForgetFault(ex, logger, caller, line);
}
}
}
- public static void Forget(this ValueTask task)
+ public static void Forget(this ValueTask task, IAcLoggerBase? logger = null, [CallerMemberName] string caller = "", [CallerLineNumber] int line = 0)
{
- if (!task.IsCompleted || task.IsFaulted)
- _ = ForgetAwaited(task);
+ if (!task.IsCompleted || task.IsFaulted) _ = ForgetAwaited(task, logger, caller, line);
+ return;
- static async Task ForgetAwaited(ValueTask task)
+ static async Task ForgetAwaited(ValueTask task, IAcLoggerBase? logger, string caller, int line)
{
try
{
await task.ConfigureAwait(false);
}
- catch
+ catch (OperationCanceledException)
{
- // Swallow exception - fire and forget semantics
+ // Normal cancellation — not a fault.
+ }
+ catch (Exception ex)
+ {
+ LogForgetFault(ex, logger, caller, line);
}
}
}
diff --git a/AyCode.Services.Server.Tests/SignalRs/SignalRDatasources/TestOrderItemListDataSource.cs b/AyCode.Services.Server.Tests/SignalRs/SignalRDatasources/TestOrderItemListDataSource.cs
index 0c0a7b1..e9672e3 100644
--- a/AyCode.Services.Server.Tests/SignalRs/SignalRDatasources/TestOrderItemListDataSource.cs
+++ b/AyCode.Services.Server.Tests/SignalRs/SignalRDatasources/TestOrderItemListDataSource.cs
@@ -7,5 +7,5 @@ namespace AyCode.Services.Server.Tests.SignalRs.SignalRDatasources;
public class TestOrderItemListDataSource : AcSignalRDataSource>
{
public TestOrderItemListDataSource(AcSignalRClientBase signalRClient, SignalRCrudTags crudTags)
- : base(signalRClient, crudTags) { }
+ : base(signalRClient, crudTags, null) { }
}
\ No newline at end of file
diff --git a/AyCode.Services.Server.Tests/SignalRs/SignalRDatasources/TestOrderItemObservableDataSource.cs b/AyCode.Services.Server.Tests/SignalRs/SignalRDatasources/TestOrderItemObservableDataSource.cs
index dff2ddf..a9e2c81 100644
--- a/AyCode.Services.Server.Tests/SignalRs/SignalRDatasources/TestOrderItemObservableDataSource.cs
+++ b/AyCode.Services.Server.Tests/SignalRs/SignalRDatasources/TestOrderItemObservableDataSource.cs
@@ -8,5 +8,5 @@ namespace AyCode.Services.Server.Tests.SignalRs.SignalRDatasources;
public class TestOrderItemObservableDataSource : AcSignalRDataSource>
{
public TestOrderItemObservableDataSource(AcSignalRClientBase signalRClient, SignalRCrudTags crudTags)
- : base(signalRClient, crudTags) { }
+ : base(signalRClient, crudTags, null) { }
}
\ No newline at end of file
diff --git a/AyCode.Services.Server/SignalRs/AcSignalRDataSource.cs b/AyCode.Services.Server/SignalRs/AcSignalRDataSource.cs
index 008d2be..c7ffdc3 100644
--- a/AyCode.Services.Server/SignalRs/AcSignalRDataSource.cs
+++ b/AyCode.Services.Server/SignalRs/AcSignalRDataSource.cs
@@ -1,15 +1,17 @@
-using AyCode.Core.Enums;
+using AyCode.Core.Compression;
+using AyCode.Core.Enums;
using AyCode.Core.Extensions;
using AyCode.Core.Helpers;
using AyCode.Core.Interfaces;
+using AyCode.Core.Loggers;
+using AyCode.Core.Serializers;
+using AyCode.Core.Serializers.Binaries;
using AyCode.Services.SignalRs;
+using Castle.Core.Logging;
using System.Collections;
using System.Collections.ObjectModel;
using System.Diagnostics;
using System.Diagnostics.CodeAnalysis;
-using AyCode.Core.Compression;
-using AyCode.Core.Serializers;
-using AyCode.Core.Serializers.Binaries;
namespace AyCode.Services.Server.SignalRs
{
@@ -218,8 +220,12 @@ namespace AyCode.Services.Server.SignalRs
protected int FindIndexInnerListUnsafe(TId id) => InnerList.FindIndex(x => IdEquals(x.Id, id));
protected TDataItem? FirstOrDefaultInnerListUnsafe(TId id) => InnerList.FirstOrDefault(x => IdEquals(x.Id, id));
- public AcSignalRDataSource(AcSignalRClientBase signalRClient, SignalRCrudTags signalRCrudTags, object[]? contextIds = null)
+ private IAcLoggerBase? _logger;
+
+ public AcSignalRDataSource(AcSignalRClientBase signalRClient, SignalRCrudTags signalRCrudTags, IAcLoggerBase? logger, object[]? contextIds = null)
{
+ _logger = logger;
+
ContextIds = contextIds;
SignalRCrudTags = signalRCrudTags;
SignalRClient = signalRClient;
@@ -277,8 +283,7 @@ namespace AyCode.Services.Server.SignalRs
var rawBytes = await SignalRClient.GetAllAsync(SignalRCrudTags.GetAllMessageTag, GetContextParams())
?? throw new NullReferenceException("LoadDataSource; null response");
- await LoadDataSourceFromResponseData(rawBytes, SerializerType,
- false, false, clearChangeTracking);
+ await LoadDataSourceFromResponseData(rawBytes, SerializerType, false, false, clearChangeTracking);
}
finally
{
@@ -301,11 +306,9 @@ namespace AyCode.Services.Server.SignalRs
{
try
{
- var rawBytes = await responseTask
- ?? throw new NullReferenceException("LoadDataSourceAsync; null response");
+ var rawBytes = await responseTask ?? throw new NullReferenceException("LoadDataSourceAsync; null response");
- await LoadDataSourceFromResponseData(rawBytes, SerializerType,
- false, false, clearChangeTracking);
+ await LoadDataSourceFromResponseData(rawBytes, SerializerType, false, false, clearChangeTracking);
}
finally
{
@@ -314,6 +317,39 @@ namespace AyCode.Services.Server.SignalRs
}).Unwrap();
}
+ ///
+ /// Wraps a deserialize/populate call so a fault is logged with the concrete
+ /// type before it propagates. Placed directly around the deserialize call (inside any BeginUpdate/EndUpdate
+ /// scope, before EndUpdate runs) so the genuine root exception is captured even if EndUpdate's NotifyReset
+ /// throws a secondary exception that would otherwise overwrite it. Rethrows — callers' behaviour is unchanged.
+ ///
+ private void TryDeserialize(Action deserialize)
+ {
+ try
+ {
+ deserialize();
+ }
+ catch (Exception ex)
+ {
+ _logger?.Error($"Deserialize failed for {typeof(TDataItem).Name}", ex);
+ throw;
+ }
+ }
+
+ ///
+ private T TryDeserialize(Func deserialize)
+ {
+ try
+ {
+ return deserialize();
+ }
+ catch (Exception ex)
+ {
+ _logger?.Error($"Deserialize failed for {typeof(TDataItem).Name}", ex);
+ throw;
+ }
+ }
+
///
/// Loads data source from response data.
/// responseData is either a typed object (protocol deserialized) or byte[] (raw path).
@@ -336,7 +372,7 @@ namespace AyCode.Services.Server.SignalRs
observable.BeginUpdate();
try
{
- AcBinaryDeserializer.PopulateMerge(rawBytes, 0, rawBytes.Length, InnerList);
+ TryDeserialize(() => AcBinaryDeserializer.PopulateMerge(rawBytes, 0, rawBytes.Length, InnerList));
}
finally
{
@@ -345,7 +381,7 @@ namespace AyCode.Services.Server.SignalRs
}
else
{
- AcBinaryDeserializer.Populate(rawBytes, 0, rawBytes.Length, InnerList);
+ TryDeserialize(() => AcBinaryDeserializer.Populate(rawBytes, 0, rawBytes.Length, InnerList));
}
}
else
@@ -364,10 +400,8 @@ namespace AyCode.Services.Server.SignalRs
else
{
TIList? fromSource;
- if (serializerType == AcSerializerType.Binary)
- fromSource = AcBinaryDeserializer.Deserialize(rawBytes, 0, rawBytes.Length);
- else
- fromSource = GzipHelper.DecompressToString(rawBytes).JsonTo();
+ if (serializerType == AcSerializerType.Binary) fromSource = TryDeserialize(() => AcBinaryDeserializer.Deserialize(rawBytes, 0, rawBytes.Length));
+ else fromSource = GzipHelper.DecompressToString(rawBytes).JsonTo();
if (fromSource != null)
{
@@ -417,17 +451,23 @@ namespace AyCode.Services.Server.SignalRs
if (InnerList is IAcObservableCollection observable2)
{
observable2.BeginUpdate();
- try { AcBinaryDeserializer.PopulateMerge(reBytes, 0, reBytes.Length, InnerList); }
- finally { observable2.EndUpdate(); }
+ try
+ {
+ TryDeserialize(() => AcBinaryDeserializer.PopulateMerge(reBytes, 0, reBytes.Length, InnerList));
+ }
+ finally
+ {
+ observable2.EndUpdate();
+ }
}
else
{
- AcBinaryDeserializer.Populate(reBytes, 0, reBytes.Length, InnerList);
+ TryDeserialize(() => AcBinaryDeserializer.Populate(reBytes, 0, reBytes.Length, InnerList));
}
}
else
{
- var fromSource = AcBinaryDeserializer.Deserialize(reBytes, 0, reBytes.Length);
+ var fromSource = TryDeserialize(() => AcBinaryDeserializer.Deserialize(reBytes, 0, reBytes.Length));
if (fromSource != null)
{
ClearUnsafe(clearChangeTracking);