Switch to net9.0; improve AcBinary diagnostics & chunk fallback

- Change target framework to net9.0 in AyCode.Core.targets.
- Add DEBUG-only property access diagnostics to AcBinarySerializer for better error reporting.
- Update AcBinaryHubProtocol to dispose chunk state and retry normal parse on unknown bytes, improving resilience after serialization failures.
- Update comments to clarify new logic and rationale.
This commit is contained in:
Loretta 2026-05-22 23:37:44 +02:00
parent c2a22e5215
commit e2b96b4148
3 changed files with 37 additions and 7 deletions

View File

@ -7,7 +7,7 @@
<!--<GitBranch>$([System.IO.File]::ReadAlltext('$(MsBuildThisFileDirectory)\.git\HEAD').Replace('ref: refs/heads/', '').Trim())</GitBranch> <!--<GitBranch>$([System.IO.File]::ReadAlltext('$(MsBuildThisFileDirectory)\.git\HEAD').Replace('ref: refs/heads/', '').Trim())</GitBranch>
<_ProjectName>$(GitBranch)</_ProjectName>--> <_ProjectName>$(GitBranch)</_ProjectName>-->
<TargetFramework Condition="'$(TargetFramework)' == ''">net10.0</TargetFramework> <TargetFramework Condition="'$(TargetFramework)' == ''">net9.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings> <ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable> <Nullable>enable</Nullable>

View File

@ -6,6 +6,7 @@ using System.Collections.Concurrent;
using System.Diagnostics; using System.Diagnostics;
using System.Diagnostics.CodeAnalysis; using System.Diagnostics.CodeAnalysis;
using System.Linq.Expressions; using System.Linq.Expressions;
using System.Reflection;
using System.Runtime.CompilerServices; using System.Runtime.CompilerServices;
using System.Runtime.InteropServices; using System.Runtime.InteropServices;
using System.Text; using System.Text;
@ -1965,7 +1966,12 @@ public static partial class AcBinarySerializer
// Use pre-cached wrapper, fallback to GetWrapper on miss/polymorphism // Use pre-cached wrapper, fallback to GetWrapper on miss/polymorphism
// Set interning eligibility for string collection elements // Set interning eligibility for string collection elements
context.StringInternEligible = prop.IsStringCollectionProperty && prop.UseStringPropertyInterning(context.InternBit); context.StringInternEligible = prop.IsStringCollectionProperty && prop.UseStringPropertyInterning(context.InternBit);
#if DEBUG
var value = GetPropertyValueWithDiagnostics(obj, prop);
#else
var value = prop.GetValue(obj); var value = prop.GetValue(obj);
#endif
// SKIP marker only for null (reference types) // SKIP marker only for null (reference types)
// Empty string, empty collections, etc. are valid values and must be written! // Empty string, empty collections, etc. are valid values and must be written!
@ -2004,6 +2010,26 @@ public static partial class AcBinarySerializer
} }
} }
[MethodImpl(MethodImplOptions.NoInlining)]
private static object? GetPropertyValueWithDiagnostics(object source, BinaryPropertyAccessor property)
{
try
{
return property.GetValue(source);
}
catch (Exception ex) when (ex is NullReferenceException || ex is TargetInvocationException)
{
var sourceType = source.GetType().FullName ?? source.GetType().Name;
var propertyName = property.Name;
var declaredType = property.PropertyType?.FullName ?? "<unknown>";
throw new InvalidOperationException(
$"AcBinary runtime property write failed at {sourceType}.{propertyName} (declaredType={declaredType}, accessor={property.AccessorType}). " +
"This usually indicates a null-sensitive property getter or malformed source data.",
ex);
}
}
#endregion #endregion
#region Specialized Array Writers #region Specialized Array Writers

View File

@ -964,11 +964,12 @@ public class AcBinaryHubProtocol : IHubProtocol
return true; return true;
} }
// Unknown byte in chunk mode — break out (shouldn't happen). // Unknown byte in chunk mode.
// Note: AsyncPipeReaderInput's WritePos/ReadPos are private, so the previous diagnostic // Real-world case: server-side serialization fails after CHUNK_START was sent, then SignalR
// fields are unavailable here. Enable AsyncPipeReaderInput.DiagnosticLog (DEBUG-only) // emits a normal framed CloseMessage. The first byte here is then the little-endian payload
// for deeper instrumentation when investigating framing-state corruption. // length (e.g. 114), not [201]/[202]. If we keep chunk state, subsequent parses keep failing
_logger?.LogWarning("TryParseChunkData unknown byte {FirstByte} in chunk mode, breaking. " + // with the same warning. Instead, tear down chunk mode and re-parse as normal framed message.
_logger?.LogWarning("TryParseChunkData unknown byte {FirstByte} in chunk mode, falling back to normal parse. " +
"binderHash={BinderHash} inputLength={InputLength} " + "binderHash={BinderHash} inputLength={InputLength} " +
"state: streamedArgType={TargetType} deserTaskStatus={TaskStatus} chunkFrameBytesConsumed={ChunkFrameBytesConsumed}", "state: streamedArgType={TargetType} deserTaskStatus={TaskStatus} chunkFrameBytesConsumed={ChunkFrameBytesConsumed}",
firstByte, firstByte,
@ -977,7 +978,10 @@ public class AcBinaryHubProtocol : IHubProtocol
state.StreamedArgType.Name, state.StreamedArgType.Name,
state.DeserTask?.Status.ToString() ?? "null", state.DeserTask?.Status.ToString() ?? "null",
state.ChunkFrameBytesConsumed); state.ChunkFrameBytesConsumed);
break;
state.Input.Dispose();
_chunkStates.Remove(binder);
return TryParseMessage(ref input, binder, out message);
} }
return false; return false;