Add logger support to grids, data sources, and helpers

- Added optional logger to TaskHelper.Forget for fire-and-forget error logging
- Updated MgGridBase and data sources to accept and use logger instances
- Refactored AcSignalRDataSource to log deserialization faults
- Modified constructors and usages of SignalRDataSourceList/Observable for logger injection
- Added CountryCode to CargoTruck and displayed in new GridCargoTruck
- Introduced GridCargoTruck.razor and base class with logger integration
- Updated GridCargoPartner to use new cargo truck grid as detail row
- Improved code style and ensured consistent error handling throughout
This commit is contained in:
Loretta 2026-05-30 06:47:06 +02:00
parent 101929b89e
commit 5e06f3e122
4 changed files with 114 additions and 42 deletions

View File

@ -1,7 +1,27 @@
namespace AyCode.Core.Helpers
using System.Runtime.CompilerServices;
using AyCode.Core.Loggers;
namespace AyCode.Core.Helpers
{
public static class TaskHelper
{
/// <summary>
/// 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.
/// <c>TaskHelper.Logger = new LoggerClient&lt;TaskHelper&gt;(writers);</c>.
/// When null, faults are swallowed (legacy behaviour). A per-call <c>logger</c> argument
/// passed to any <c>Forget</c> overload overrides this ambient instance.
/// </summary>
public static IAcLoggerBase? Logger { get; set; }
/// <summary>
/// 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 <see cref="Logger"/>"; both null → silent.
/// </summary>
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<bool> 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<T>(this ValueTask<T> task)
public static void Forget<T>(this ValueTask<T> 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<T> task)
static async Task ForgetAwaited(ValueTask<T> 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);
}
}
}

View File

@ -7,5 +7,5 @@ namespace AyCode.Services.Server.Tests.SignalRs.SignalRDatasources;
public class TestOrderItemListDataSource : AcSignalRDataSource<TestOrderItem_All_True, int, List<TestOrderItem_All_True>>
{
public TestOrderItemListDataSource(AcSignalRClientBase signalRClient, SignalRCrudTags crudTags)
: base(signalRClient, crudTags) { }
: base(signalRClient, crudTags, null) { }
}

View File

@ -8,5 +8,5 @@ namespace AyCode.Services.Server.Tests.SignalRs.SignalRDatasources;
public class TestOrderItemObservableDataSource : AcSignalRDataSource<TestOrderItem_All_True, int, AcObservableCollection<TestOrderItem_All_True>>
{
public TestOrderItemObservableDataSource(AcSignalRClientBase signalRClient, SignalRCrudTags crudTags)
: base(signalRClient, crudTags) { }
: base(signalRClient, crudTags, null) { }
}

View File

@ -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<byte[]>(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();
}
/// <summary>
/// Wraps a deserialize/populate call so a fault is logged with the concrete <typeparamref name="TDataItem"/>
/// 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.
/// </summary>
private void TryDeserialize(Action deserialize)
{
try
{
deserialize();
}
catch (Exception ex)
{
_logger?.Error($"Deserialize failed for {typeof(TDataItem).Name}", ex);
throw;
}
}
/// <inheritdoc cref="TryDeserialize(Action)"/>
private T TryDeserialize<T>(Func<T> deserialize)
{
try
{
return deserialize();
}
catch (Exception ex)
{
_logger?.Error($"Deserialize failed for {typeof(TDataItem).Name}", ex);
throw;
}
}
/// <summary>
/// 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<TIList>(rawBytes, 0, rawBytes.Length);
else
fromSource = GzipHelper.DecompressToString(rawBytes).JsonTo<TIList>();
if (serializerType == AcSerializerType.Binary) fromSource = TryDeserialize(() => AcBinaryDeserializer.Deserialize<TIList>(rawBytes, 0, rawBytes.Length));
else fromSource = GzipHelper.DecompressToString(rawBytes).JsonTo<TIList>();
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<TIList>(reBytes, 0, reBytes.Length);
var fromSource = TryDeserialize(() => AcBinaryDeserializer.Deserialize<TIList>(reBytes, 0, reBytes.Length));
if (fromSource != null)
{
ClearUnsafe(clearChangeTracking);