Add JIT disassembly benchmark and improve string interning
Introduce JitDisassemblyBenchmark for analyzing JIT-generated x64 assembly of AcBinarySerializer hot paths, accessible via --jitasm. Refactor string interning logic to support per-property and string collection interning, adding IsStringCollectionProperty and ScanStringCollection. Update ScanPass and WriteString for finer-grained control. Remove DEBUG-only CurrentPropertyPath in favor of a more robust property tracking approach. Update usage instructions and clean up related code.
This commit is contained in:
parent
bfab7c16b9
commit
896f720109
|
|
@ -0,0 +1,69 @@
|
||||||
|
using AyCode.Core.Serializers.Binaries;
|
||||||
|
using AyCode.Core.Tests.TestModels;
|
||||||
|
using BenchmarkDotNet.Attributes;
|
||||||
|
using BenchmarkDotNet.Diagnosers;
|
||||||
|
using BenchmarkDotNet.Jobs;
|
||||||
|
|
||||||
|
namespace AyCode.Core.Benchmarks;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// JIT disassembly benchmark for AcBinarySerializer hot path analysis.
|
||||||
|
/// Shows actual x64 assembly generated by the JIT to verify inlining decisions.
|
||||||
|
///
|
||||||
|
/// Usage: dotnet run -c Release -- --filter *JitDisassemblyBenchmark*
|
||||||
|
/// Or from Program.cs: --jitasm
|
||||||
|
///
|
||||||
|
/// Output: BenchmarkDotNet artifacts folder contains .asm files with full disassembly.
|
||||||
|
/// Look for:
|
||||||
|
/// - WritePropertyOrSkip / WritePropertyMarkerless: are they inlined or called?
|
||||||
|
/// - WriteInt32 / WriteFloat64Unsafe / etc.: inlined into the caller or separate calls?
|
||||||
|
/// - context parameter passing: register usage (RCX/RDX/R8/R9)
|
||||||
|
/// </summary>
|
||||||
|
[SimpleJob(RuntimeMoniker.Net90)]
|
||||||
|
[DisassemblyDiagnoser(maxDepth: 4, printSource: true, exportGithubMarkdown: true)]
|
||||||
|
[MemoryDiagnoser(displayGenColumns: false)]
|
||||||
|
public class JitDisassemblyBenchmark
|
||||||
|
{
|
||||||
|
private TestOrder _order = null!;
|
||||||
|
private AcBinarySerializerOptions _fastModeOptions = null!;
|
||||||
|
private byte[] _serialized = null!;
|
||||||
|
|
||||||
|
[GlobalSetup]
|
||||||
|
public void Setup()
|
||||||
|
{
|
||||||
|
TestDataFactory.ResetIdCounter();
|
||||||
|
var sharedTag = TestDataFactory.CreateTag("SharedTag");
|
||||||
|
var sharedUser = TestDataFactory.CreateUser("shareduser");
|
||||||
|
|
||||||
|
// Medium data: enough properties to show loop behavior, not too large for disassembly
|
||||||
|
_order = TestDataFactory.CreateOrder(
|
||||||
|
itemCount: 3,
|
||||||
|
palletsPerItem: 3,
|
||||||
|
measurementsPerPallet: 3,
|
||||||
|
pointsPerMeasurement: 4,
|
||||||
|
sharedTag: sharedTag,
|
||||||
|
sharedUser: sharedUser);
|
||||||
|
|
||||||
|
_fastModeOptions = AcBinarySerializerOptions.FastMode;
|
||||||
|
_serialized = AcBinarySerializer.Serialize(_order, _fastModeOptions);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// FastMode serialize — the primary hot path.
|
||||||
|
/// Disassembly will show WriteObject → WritePropertyMarkerless/WritePropertyOrSkip call chain.
|
||||||
|
/// </summary>
|
||||||
|
[Benchmark(Baseline = true)]
|
||||||
|
public byte[] Serialize_FastMode()
|
||||||
|
{
|
||||||
|
return AcBinarySerializer.Serialize(_order, _fastModeOptions);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// FastMode deserialize — for comparison.
|
||||||
|
/// </summary>
|
||||||
|
[Benchmark]
|
||||||
|
public TestOrder Deserialize_FastMode()
|
||||||
|
{
|
||||||
|
return AcBinaryDeserializer.Deserialize<TestOrder>(_serialized, _fastModeOptions);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -124,6 +124,12 @@ namespace AyCode.Benchmark
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (args.Length > 0 && args[0] == "--jitasm")
|
||||||
|
{
|
||||||
|
RunBenchmark<JitDisassemblyBenchmark>(config, benchmarkDir, memDiagDir, "JitDisassemblyBenchmark");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
Console.WriteLine("Usage:");
|
Console.WriteLine("Usage:");
|
||||||
Console.WriteLine(" --quick Quick benchmark with tabular output (AcBinary vs MessagePack)");
|
Console.WriteLine(" --quick Quick benchmark with tabular output (AcBinary vs MessagePack)");
|
||||||
Console.WriteLine(" --test Quick AcBinary test");
|
Console.WriteLine(" --test Quick AcBinary test");
|
||||||
|
|
@ -133,6 +139,7 @@ namespace AyCode.Benchmark
|
||||||
Console.WriteLine(" --complex Complex hierarchy (AcBinary vs JSON)");
|
Console.WriteLine(" --complex Complex hierarchy (AcBinary vs JSON)");
|
||||||
Console.WriteLine(" --msgpack MessagePack comparison");
|
Console.WriteLine(" --msgpack MessagePack comparison");
|
||||||
Console.WriteLine(" --sizes Size comparison only");
|
Console.WriteLine(" --sizes Size comparison only");
|
||||||
|
Console.WriteLine(" --jitasm JIT disassembly analysis (shows actual x64 assembly for hot path)");
|
||||||
Console.WriteLine(" --save-coverage <file> Save coverage file into Test_Benchmark_Results/CoverageReport");
|
Console.WriteLine(" --save-coverage <file> Save coverage file into Test_Benchmark_Results/CoverageReport");
|
||||||
|
|
||||||
if (args.Length == 0)
|
if (args.Length == 0)
|
||||||
|
|
|
||||||
|
|
@ -212,15 +212,15 @@ public static class Program
|
||||||
{
|
{
|
||||||
|
|
||||||
// AcBinary variants
|
// AcBinary variants
|
||||||
new AcBinaryBenchmark(testData.Order, AcBinarySerializerOptions.Default, SerializerAcBinaryDefault),
|
//new AcBinaryBenchmark(testData.Order, AcBinarySerializerOptions.Default, SerializerAcBinaryDefault),
|
||||||
new AcBinaryBenchmark(testData.Order, AcBinarySerializerOptions.WithoutReferenceHandling, SerializerAcBinaryNoRef),
|
//new AcBinaryBenchmark(testData.Order, AcBinarySerializerOptions.WithoutReferenceHandling, SerializerAcBinaryNoRef),
|
||||||
new AcBinaryBenchmark(testData.Order, AcBinarySerializerOptions.FastMode, SerializerAcBinaryFastMode),
|
|
||||||
new AcBinaryBenchmark(testData.Order, new AcBinarySerializerOptions { UseStringInterning = StringInterningMode.None }, SerializerAcBinaryNoIntern),
|
|
||||||
|
|
||||||
//new AcBinaryBenchmark(testData.Order, AcBinarySerializerOptions.FastMode, SerializerAcBinaryDefault),
|
|
||||||
//new AcBinaryBenchmark(testData.Order, AcBinarySerializerOptions.FastMode, SerializerAcBinaryNoRef),
|
|
||||||
//new AcBinaryBenchmark(testData.Order, AcBinarySerializerOptions.FastMode, SerializerAcBinaryFastMode),
|
//new AcBinaryBenchmark(testData.Order, AcBinarySerializerOptions.FastMode, SerializerAcBinaryFastMode),
|
||||||
//new AcBinaryBenchmark(testData.Order, AcBinarySerializerOptions.FastMode, SerializerAcBinaryNoIntern),
|
//new AcBinaryBenchmark(testData.Order, new AcBinarySerializerOptions { UseStringInterning = StringInterningMode.None }, SerializerAcBinaryNoIntern),
|
||||||
|
|
||||||
|
new AcBinaryBenchmark(testData.Order, AcBinarySerializerOptions.FastMode, SerializerAcBinaryDefault),
|
||||||
|
new AcBinaryBenchmark(testData.Order, AcBinarySerializerOptions.FastMode, SerializerAcBinaryNoRef),
|
||||||
|
new AcBinaryBenchmark(testData.Order, AcBinarySerializerOptions.FastMode, SerializerAcBinaryFastMode),
|
||||||
|
new AcBinaryBenchmark(testData.Order, AcBinarySerializerOptions.FastMode, SerializerAcBinaryNoIntern),
|
||||||
|
|
||||||
// AcJson
|
// AcJson
|
||||||
new AcJsonBenchmark(testData.Order, AcJsonSerializerOptions.Default, SerializerAcJsonDefault),
|
new AcJsonBenchmark(testData.Order, AcJsonSerializerOptions.Default, SerializerAcJsonDefault),
|
||||||
|
|
|
||||||
|
|
@ -104,13 +104,14 @@ public static partial class AcBinarySerializer
|
||||||
private int[]? _propertyIndexBuffer;
|
private int[]? _propertyIndexBuffer;
|
||||||
private byte[]? _propertyStateBuffer;
|
private byte[]? _propertyStateBuffer;
|
||||||
|
|
||||||
#if DEBUG
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// DEBUG ONLY: Current property path being serialized (e.g., "Order.Status").
|
/// Current property being serialized. Set in WriteObject property loops.
|
||||||
/// Used for string interning analysis.
|
/// Used by WriteString for per-property interning control (UseStringPropertyInterning).
|
||||||
|
/// null when writing non-property values (arrays, dictionaries, root value).
|
||||||
/// </summary>
|
/// </summary>
|
||||||
internal string? CurrentPropertyPath;
|
//internal BinaryPropertyAccessorBase? CurrentProperty;
|
||||||
|
|
||||||
|
#if DEBUG
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// DEBUG ONLY: Callback invoked when a string is registered for interning.
|
/// DEBUG ONLY: Callback invoked when a string is registered for interning.
|
||||||
/// Parameters: (propertyPath, stringValue)
|
/// Parameters: (propertyPath, stringValue)
|
||||||
|
|
@ -120,7 +121,7 @@ public static partial class AcBinarySerializer
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
// These properties delegate to Options for convenience
|
// These properties delegate to Options for convenience
|
||||||
public bool UseStringInterning => Options.UseStringInterning != StringInterningMode.None;
|
internal bool UseStringInterning => Options.UseStringInterning != StringInterningMode.None;
|
||||||
|
|
||||||
public bool IsValidForInterningString(int strLength)
|
public bool IsValidForInterningString(int strLength)
|
||||||
{
|
{
|
||||||
|
|
|
||||||
|
|
@ -23,6 +23,8 @@ public static partial class AcBinarySerializer
|
||||||
|
|
||||||
var wrapper = context.GetWrapper(type);
|
var wrapper = context.GetWrapper(type);
|
||||||
ScanValue(value, wrapper, context, 0);
|
ScanValue(value, wrapper, context, 0);
|
||||||
|
|
||||||
|
//context.CurrentProperty = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
private static void ScanValue<TOutput>(object? value, TypeMetadataWrapper<BinarySerializeTypeMetadata> wrapper, BinarySerializationContext<TOutput> context, int depth)
|
private static void ScanValue<TOutput>(object? value, TypeMetadataWrapper<BinarySerializeTypeMetadata> wrapper, BinarySerializationContext<TOutput> context, int depth)
|
||||||
|
|
@ -42,13 +44,23 @@ public static partial class AcBinarySerializer
|
||||||
{
|
{
|
||||||
var isStringCollectionElementType = false;
|
var isStringCollectionElementType = false;
|
||||||
|
|
||||||
|
// Per-property interning: use CurrentProperty (set in property loop) when available,
|
||||||
|
// fallback to global UseStringInterning for root-level collections.
|
||||||
|
|
||||||
//TODO: A collection esetén is vizsgálni kéne hogy UseStringPropertyInterning - J.
|
//TODO: A collection esetén is vizsgálni kéne hogy UseStringPropertyInterning - J.
|
||||||
if (metadata.ElementNeedsScan &&
|
if (metadata.ElementNeedsScan &&
|
||||||
!((isStringCollectionElementType = ReferenceEquals(metadata.CollectionElementType, StringType)) && !context.UseStringInterning))
|
!((isStringCollectionElementType = ReferenceEquals(metadata.CollectionElementType, StringType)) && !context.UseStringInterning))
|
||||||
{
|
{
|
||||||
var nextDepth = depth + 1;
|
var nextDepth = depth + 1;
|
||||||
|
|
||||||
|
if (isStringCollectionElementType)
|
||||||
|
{
|
||||||
|
ScanStringCollection(value, context);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
// Pre-cache element wrapper for non-string collections (string fast path in ScanItem doesn't need it)
|
// Pre-cache element wrapper for non-string collections (string fast path in ScanItem doesn't need it)
|
||||||
var elementWrapper = isStringCollectionElementType ? null : context.GetWrapper(metadata.CollectionElementType!);
|
var elementWrapper = context.GetWrapper(metadata.CollectionElementType!);
|
||||||
|
|
||||||
if (value is IList list)
|
if (value is IList list)
|
||||||
{
|
{
|
||||||
|
|
@ -111,9 +123,12 @@ public static partial class AcBinarySerializer
|
||||||
var refProperties = metadata.ReferenceProperties;
|
var refProperties = metadata.ReferenceProperties;
|
||||||
var hasPropertyFilter = context.HasPropertyFilter;
|
var hasPropertyFilter = context.HasPropertyFilter;
|
||||||
var nextDepth2 = depth + 1;
|
var nextDepth2 = depth + 1;
|
||||||
|
|
||||||
for (var i = 0; i < refProperties.Length; i++)
|
for (var i = 0; i < refProperties.Length; i++)
|
||||||
{
|
{
|
||||||
var prop = refProperties[i];
|
var prop = refProperties[i];
|
||||||
|
//context.CurrentProperty = prop;
|
||||||
|
|
||||||
// Must match write pass: filtered properties write PropertySkip (no value) →
|
// Must match write pass: filtered properties write PropertySkip (no value) →
|
||||||
// scanning them would assign CacheIndex for strings/objects never in output
|
// scanning them would assign CacheIndex for strings/objects never in output
|
||||||
if (hasPropertyFilter && !context.ShouldSerializeProperty(value, prop))
|
if (hasPropertyFilter && !context.ShouldSerializeProperty(value, prop))
|
||||||
|
|
@ -128,6 +143,15 @@ public static partial class AcBinarySerializer
|
||||||
if (str2 != null && context.IsValidForInterningString(str2.Length))
|
if (str2 != null && context.IsValidForInterningString(str2.Length))
|
||||||
context.ScanInternString(str2);
|
context.ScanInternString(str2);
|
||||||
}
|
}
|
||||||
|
else if (prop.IsStringCollectionProperty)
|
||||||
|
{
|
||||||
|
// String collection: per-property interning control, no GetWrapper needed
|
||||||
|
if (!prop.UseStringPropertyInterning(context.Options.UseStringInterning)) continue;
|
||||||
|
|
||||||
|
var propValue = prop.GetValue(value);
|
||||||
|
if (propValue != null)
|
||||||
|
ScanStringCollection(propValue, context);
|
||||||
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
// Object property: use pre-cached wrapper, fallback to GetWrapper on miss/polymorphism
|
// Object property: use pre-cached wrapper, fallback to GetWrapper on miss/polymorphism
|
||||||
|
|
@ -147,6 +171,30 @@ public static partial class AcBinarySerializer
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Scans string elements of a collection for interning. Uses IList fast path when available.
|
||||||
|
/// </summary>
|
||||||
|
private static void ScanStringCollection<TOutput>(object collection, BinarySerializationContext<TOutput> context)
|
||||||
|
where TOutput : struct, IBinaryOutputBase
|
||||||
|
{
|
||||||
|
if (collection is IList list)
|
||||||
|
{
|
||||||
|
for (var i = 0; i < list.Count; i++)
|
||||||
|
{
|
||||||
|
if (list[i] is string str && context.IsValidForInterningString(str.Length))
|
||||||
|
context.ScanInternString(str);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else if (collection is IEnumerable enumerable)
|
||||||
|
{
|
||||||
|
foreach (var item in enumerable)
|
||||||
|
{
|
||||||
|
if (item is string str && context.IsValidForInterningString(str.Length))
|
||||||
|
context.ScanInternString(str);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Scans a collection item. Uses pre-cached element wrapper when runtime type matches,
|
/// Scans a collection item. Uses pre-cached element wrapper when runtime type matches,
|
||||||
/// falls back to GetWrapper for polymorphic items.
|
/// falls back to GetWrapper for polymorphic items.
|
||||||
|
|
@ -155,15 +203,9 @@ public static partial class AcBinarySerializer
|
||||||
private static void ScanItem<TOutput>(object item, TypeMetadataWrapper<BinarySerializeTypeMetadata>? elementWrapper, BinarySerializationContext<TOutput> context, int depth)
|
private static void ScanItem<TOutput>(object item, TypeMetadataWrapper<BinarySerializeTypeMetadata>? elementWrapper, BinarySerializationContext<TOutput> context, int depth)
|
||||||
where TOutput : struct, IBinaryOutputBase
|
where TOutput : struct, IBinaryOutputBase
|
||||||
{
|
{
|
||||||
// String fast path — avoid GetWrapper entirely
|
var itemType = item.GetType();
|
||||||
if (item is string str)
|
var itemWrapper = itemType == elementWrapper!.Metadata.SourceType ? elementWrapper : context.GetWrapper(itemType);
|
||||||
{
|
|
||||||
if (context.UseStringInterning && context.IsValidForInterningString(str.Length))
|
|
||||||
context.ScanInternString(str);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
var itemWrapper = item.GetType() == elementWrapper!.Metadata.SourceType ? elementWrapper : context.GetWrapper(item.GetType());
|
|
||||||
ScanValue(item, itemWrapper, context, depth);
|
ScanValue(item, itemWrapper, context, depth);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -840,10 +840,7 @@ public static partial class AcBinarySerializer
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// String interning: only for strings within length range
|
if (context.UseStringInterning && context.IsValidForInterningString(value.Length))// && context.CurrentProperty!.UseStringPropertyInterning(context.Options.UseStringInterning))
|
||||||
// MaxStringInternLength == 0 means no max limit
|
|
||||||
//TODO: A prop.UseStringPropertyInterning-et kéne használni! - J.
|
|
||||||
if (context.UseStringInterning && context.IsValidForInterningString(value.Length))
|
|
||||||
{
|
{
|
||||||
ref var interEntry = ref context.GetInternedStringEntry(value, out bool found);
|
ref var interEntry = ref context.GetInternedStringEntry(value, out bool found);
|
||||||
|
|
||||||
|
|
@ -867,7 +864,9 @@ public static partial class AcBinarySerializer
|
||||||
}
|
}
|
||||||
// CacheIndex < 0 or not found → single occurrence, fall through to FixStr/String path
|
// CacheIndex < 0 or not found → single occurrence, fall through to FixStr/String path
|
||||||
#if DEBUG
|
#if DEBUG
|
||||||
context.OnStringInterned?.Invoke(context.CurrentPropertyPath, value);
|
context.OnStringInterned?.Invoke(
|
||||||
|
context.CurrentProperty != null ? $"{context.CurrentProperty.DeclaringType.Name}.{context.CurrentProperty.Name}" : null,
|
||||||
|
value);
|
||||||
#endif
|
#endif
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -1044,6 +1043,7 @@ public static partial class AcBinarySerializer
|
||||||
for (var i = 0; i < propCount; i++)
|
for (var i = 0; i < propCount; i++)
|
||||||
{
|
{
|
||||||
var prop = properties[i];
|
var prop = properties[i];
|
||||||
|
//context.CurrentProperty = prop;
|
||||||
|
|
||||||
if (prop.ExpectedTypeCode.HasValue)
|
if (prop.ExpectedTypeCode.HasValue)
|
||||||
{
|
{
|
||||||
|
|
@ -1065,6 +1065,7 @@ public static partial class AcBinarySerializer
|
||||||
for (var i = 0; i < propCount; i++)
|
for (var i = 0; i < propCount; i++)
|
||||||
{
|
{
|
||||||
var prop = properties[i];
|
var prop = properties[i];
|
||||||
|
//context.CurrentProperty = prop;
|
||||||
|
|
||||||
if (hasPropertyFilter && !context.ShouldSerializeProperty(value, prop))
|
if (hasPropertyFilter && !context.ShouldSerializeProperty(value, prop))
|
||||||
{
|
{
|
||||||
|
|
@ -1363,9 +1364,6 @@ public static partial class AcBinarySerializer
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
#if DEBUG
|
|
||||||
context.CurrentPropertyPath = $"{prop.DeclaringType.Name}.{prop.Name}";
|
|
||||||
#endif
|
|
||||||
WriteString(value, context);
|
WriteString(value, context);
|
||||||
}
|
}
|
||||||
return;
|
return;
|
||||||
|
|
@ -1384,9 +1382,6 @@ public static partial class AcBinarySerializer
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
#if DEBUG
|
|
||||||
context.CurrentPropertyPath = $"{prop.DeclaringType.Name}.{prop.Name}";
|
|
||||||
#endif
|
|
||||||
var runtimeType = value.GetType();
|
var runtimeType = value.GetType();
|
||||||
var complexIdx = prop.ComplexPropertyIndex;
|
var complexIdx = prop.ComplexPropertyIndex;
|
||||||
if (complexIdx >= 0)
|
if (complexIdx >= 0)
|
||||||
|
|
@ -1584,7 +1579,6 @@ public static partial class AcBinarySerializer
|
||||||
{
|
{
|
||||||
context.WriteByte(boolArray[i] ? BinaryTypeCode.True : BinaryTypeCode.False);
|
context.WriteByte(boolArray[i] ? BinaryTypeCode.True : BinaryTypeCode.False);
|
||||||
}
|
}
|
||||||
context.WriteVarUInt((uint)boolArray.Length);
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -24,6 +24,7 @@ public abstract class BinaryPropertyAccessorBase : PropertyAccessorBase
|
||||||
/// Used to pre-cache TypeMetadataWrapper per property type, eliminating GetWrapper dictionary lookups.
|
/// Used to pre-cache TypeMetadataWrapper per property type, eliminating GetWrapper dictionary lookups.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public int ComplexPropertyIndex { get; internal set; } = -1;
|
public int ComplexPropertyIndex { get; internal set; } = -1;
|
||||||
|
public bool IsStringCollectionProperty { get; }
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Cached [AcStringIntern] attribute value for this property.
|
/// Cached [AcStringIntern] attribute value for this property.
|
||||||
|
|
@ -55,8 +56,10 @@ public abstract class BinaryPropertyAccessorBase : PropertyAccessorBase
|
||||||
protected BinaryPropertyAccessorBase(PropertyInfo prop, Type declaringType)
|
protected BinaryPropertyAccessorBase(PropertyInfo prop, Type declaringType)
|
||||||
: base(prop, declaringType)
|
: base(prop, declaringType)
|
||||||
{
|
{
|
||||||
|
IsStringCollectionProperty = IsStringCollection(prop.PropertyType);
|
||||||
|
|
||||||
// All typed getters are initialized in PropertyAccessorBase
|
// All typed getters are initialized in PropertyAccessorBase
|
||||||
if (AccessorType == PropertyAccessorType.String)
|
if (AccessorType == PropertyAccessorType.String || IsStringCollectionProperty)
|
||||||
{
|
{
|
||||||
// Cache [AcStringIntern] attribute (inherit: true to check base class properties)
|
// Cache [AcStringIntern] attribute (inherit: true to check base class properties)
|
||||||
var internAttr = prop.GetCustomAttribute<AcStringInternAttribute>(inherit: true);
|
var internAttr = prop.GetCustomAttribute<AcStringInternAttribute>(inherit: true);
|
||||||
|
|
@ -72,6 +75,23 @@ public abstract class BinaryPropertyAccessorBase : PropertyAccessorBase
|
||||||
ExpectedTypeCode = ComputeExpectedTypeCode(AccessorType);
|
ExpectedTypeCode = ComputeExpectedTypeCode(AccessorType);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Checks if the property type is a collection with string elements (string[], List<string>, etc.).
|
||||||
|
/// </summary>
|
||||||
|
private static bool IsStringCollection(Type propertyType)
|
||||||
|
{
|
||||||
|
if (propertyType.IsArray)
|
||||||
|
return propertyType.GetElementType() == typeof(string);
|
||||||
|
|
||||||
|
if (propertyType.IsGenericType)
|
||||||
|
{
|
||||||
|
var args = propertyType.GetGenericArguments();
|
||||||
|
return args.Length == 1 && args[0] == typeof(string);
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Maps AccessorType to the BinaryTypeCode that would normally be written as marker.
|
/// Maps AccessorType to the BinaryTypeCode that would normally be written as marker.
|
||||||
/// Returns null for types that always need a stream marker (bool, enum, string, object/nullable).
|
/// Returns null for types that always need a stream marker (bool, enum, string, object/nullable).
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue