Refactor serializer tests, fix deserializer bugs, add Gzip
Major overhaul of binary serializer/deserializer tests: split and expand test coverage for primitives, objects, navigation, generics, circular refs, and edge cases. Fix critical bugs in property skipping, string interning, type mismatch diagnostics, nullable assignment, and VarInt decoding. Add WASM-optimized deserialization options with string caching. Switch SignalR compression from Brotli to Gzip and introduce GzipHelper. Add comprehensive StockTaking test models and real-world bug reproductions. Improve diagnostics, test discovery, and add benchmark/utility scripts.
This commit is contained in:
parent
762088caf7
commit
cde2b5e529
|
|
@ -0,0 +1,82 @@
|
|||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset='utf-8'>
|
||||
<title>BenchmarkDotNet Riportok (Dropdown)</title>
|
||||
<style>
|
||||
body { font-family: Segoe UI, Arial, sans-serif; margin: 2em; background: #f8f9fa; }
|
||||
select { font-size: 1.1em; padding: 0.2em; }
|
||||
.report-content { background: #fff; border: 1px solid #ccc; padding: 1em; margin-top: 1em; border-radius: 6px; box-shadow: 0 2px 8px #0001; min-height: 200px; }
|
||||
h1 { font-size: 1.5em; }
|
||||
.filename { color: #888; font-size: 0.95em; }
|
||||
.compare { color: #007700; font-size: 1.1em; margin-bottom: 1em; }
|
||||
.compare.negative { color: #bb2222; }
|
||||
iframe { width: 100%; min-height: 600px; border: none; background: #fff; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<h1>BenchmarkDotNet Riportok</h1>
|
||||
<label for='reportSelect'>Válassz riportot:</label>
|
||||
<select id='reportSelect'>
|
||||
<option value='AyCode.Core.Benchmarks.AcBinaryOptionsDeserializeBenchmark-report_486670772'>AyCode.Core.Benchmarks.AcBinaryOptionsDeserializeBenchmark-report</option>
|
||||
<option value='AyCode.Core.Benchmarks.AcBinaryOptionsDeserializeBenchmark-report_2144380973'>SwitcherRun_20251215T194244_217</option>
|
||||
<option value='AyCode.Core.Benchmarks.MessagePackComparisonBenchmark-report_742792311'>SwitcherRun_20251214T182029_626</option>
|
||||
<option value='AyCode.Core.Benchmarks.MessagePackComparisonBenchmark-report_889027207'>AyCode.Core.Benchmarks.MessagePackComparisonBenchmark-report</option>
|
||||
<option value='AyCode.Core.Benchmarks.AcBinaryVsMessagePackFullBenchmark-report_839282233'>SwitcherRun_20251214T182029_626</option>
|
||||
</select>
|
||||
<div class='filename' id='filename'></div>
|
||||
<div class='compare' id='compare'></div>
|
||||
<div class='report-content'><iframe id='reportFrame'></iframe></div>
|
||||
<script>
|
||||
// Relatív riport fájlok
|
||||
const reports = {
|
||||
'AyCode.Core.Benchmarks.AcBinaryOptionsDeserializeBenchmark-report_486670772': 'Test_Benchmark_Results/Benchmark/results/AyCode.Core.Benchmarks.AcBinaryOptionsDeserializeBenchmark-report.html',
|
||||
'AyCode.Core.Benchmarks.AcBinaryOptionsDeserializeBenchmark-report_2144380973': 'Test_Benchmark_Results/MemDiag/SwitcherRun_20251215T194244_217/results/AyCode.Core.Benchmarks.AcBinaryOptionsDeserializeBenchmark-report.html',
|
||||
'AyCode.Core.Benchmarks.MessagePackComparisonBenchmark-report_742792311': 'AyCode.Benchmark/Test_Benchmark_Results/MemDiag/SwitcherRun_20251214T182029_626/results/AyCode.Core.Benchmarks.MessagePackComparisonBenchmark-report.html',
|
||||
'AyCode.Core.Benchmarks.MessagePackComparisonBenchmark-report_889027207': 'AyCode.Benchmark/Test_Benchmark_Results/Benchmark/results/AyCode.Core.Benchmarks.MessagePackComparisonBenchmark-report.html',
|
||||
'AyCode.Core.Benchmarks.AcBinaryVsMessagePackFullBenchmark-report_839282233': 'AyCode.Benchmark/Test_Benchmark_Results/MemDiag/SwitcherRun_20251214T182029_626/results/AyCode.Core.Benchmarks.AcBinaryVsMessagePackFullBenchmark-report.html',
|
||||
};
|
||||
const means = {
|
||||
'AyCode.Core.Benchmarks.AcBinaryOptionsDeserializeBenchmark-report_486670772': '',
|
||||
'AyCode.Core.Benchmarks.AcBinaryOptionsDeserializeBenchmark-report_2144380973': '',
|
||||
'AyCode.Core.Benchmarks.MessagePackComparisonBenchmark-report_742792311': '',
|
||||
'AyCode.Core.Benchmarks.MessagePackComparisonBenchmark-report_889027207': '',
|
||||
'AyCode.Core.Benchmarks.AcBinaryVsMessagePackFullBenchmark-report_839282233': '',
|
||||
};
|
||||
const select = document.getElementById('reportSelect');
|
||||
const frame = document.getElementById('reportFrame');
|
||||
const filename = document.getElementById('filename');
|
||||
const compare = document.getElementById('compare');
|
||||
function showReport() {
|
||||
const id = select.value;
|
||||
if (reports[id]) {
|
||||
frame.src = reports[id];
|
||||
} else {
|
||||
frame.srcdoc = '<i>Nincs tartalom.</i>';
|
||||
}
|
||||
const opt = select.options[select.selectedIndex];
|
||||
filename.textContent = opt ? opt.text : '';
|
||||
// Összehasonlítás az eggyel korábbival
|
||||
const idx = select.selectedIndex;
|
||||
if (idx < select.options.length - 1) {
|
||||
const prevId = select.options[idx + 1].value;
|
||||
const currMean = parseFloat(means[id].replace(',','.'));
|
||||
const prevMean = parseFloat(means[prevId].replace(',','.'));
|
||||
if (!isNaN(currMean) && !isNaN(prevMean) && prevMean > 0) {
|
||||
const diff = currMean - prevMean;
|
||||
const percent = (diff / prevMean * 100).toFixed(2);
|
||||
const sign = percent > 0 ? '+' : '';
|
||||
compare.textContent = Eltérés az előzőhöz képest: % ( vs );
|
||||
compare.className = 'compare' + (percent > 0 ? ' negative' : '');
|
||||
} else {
|
||||
compare.textContent = '';
|
||||
}
|
||||
} else {
|
||||
compare.textContent = '';
|
||||
}
|
||||
}
|
||||
select.addEventListener('change', showReport);
|
||||
window.onload = showReport;
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
|
|
@ -0,0 +1,52 @@
|
|||
using System.Buffers;
|
||||
using System.Text;
|
||||
using AyCode.Core.Compression;
|
||||
|
||||
namespace AyCode.Core.Tests.Compression;
|
||||
|
||||
[TestClass]
|
||||
public class GzipHelperTests
|
||||
{
|
||||
[TestMethod]
|
||||
public void CompressAndDecompress_StringRoundTrip_Succeeds()
|
||||
{
|
||||
var original = "SignalR payload for gzip";
|
||||
|
||||
var compressed = GzipHelper.Compress(original);
|
||||
var decompressed = GzipHelper.DecompressToString(compressed);
|
||||
|
||||
Assert.IsNotNull(compressed);
|
||||
Assert.AreNotEqual(0, compressed.Length);
|
||||
Assert.AreEqual(original, decompressed);
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void DecompressToRentedBuffer_ReturnsOriginalBytes()
|
||||
{
|
||||
var payload = "{\"message\":\"gzip\"}";
|
||||
var compressed = GzipHelper.Compress(payload);
|
||||
|
||||
var (buffer, length) = GzipHelper.DecompressToRentedBuffer(compressed);
|
||||
try
|
||||
{
|
||||
Assert.IsTrue(length > 0);
|
||||
var text = Encoding.UTF8.GetString(buffer, 0, length);
|
||||
Assert.AreEqual(payload, text);
|
||||
}
|
||||
finally
|
||||
{
|
||||
ArrayPool<byte>.Shared.Return(buffer);
|
||||
}
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void IsGzipCompressed_ReturnsExpectedValues()
|
||||
{
|
||||
var compressed = GzipHelper.Compress("ping");
|
||||
var nonCompressed = Encoding.UTF8.GetBytes("plain text");
|
||||
|
||||
Assert.IsTrue(GzipHelper.IsGzipCompressed(compressed));
|
||||
Assert.IsFalse(GzipHelper.IsGzipCompressed(nonCompressed));
|
||||
Assert.IsFalse(GzipHelper.IsGzipCompressed(Array.Empty<byte>()));
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,124 @@
|
|||
using AyCode.Core.Serializers.Binaries;
|
||||
|
||||
namespace AyCode.Core.Tests.Serialization;
|
||||
|
||||
/// <summary>
|
||||
/// Basic serialization tests for primitive types.
|
||||
/// </summary>
|
||||
[TestClass]
|
||||
public class AcBinarySerializerBasicTests
|
||||
{
|
||||
[TestMethod]
|
||||
public void Serialize_Null_ReturnsSingleNullByte()
|
||||
{
|
||||
var result = AcBinarySerializer.Serialize<object?>(null);
|
||||
Assert.AreEqual(1, result.Length);
|
||||
Assert.AreEqual((byte)0, result[0]);
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void Serialize_Int32_RoundTrip()
|
||||
{
|
||||
var value = 12345;
|
||||
var binary = AcBinarySerializer.Serialize(value);
|
||||
var result = AcBinaryDeserializer.Deserialize<int>(binary);
|
||||
Assert.AreEqual(value, result);
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void Serialize_Int64_RoundTrip()
|
||||
{
|
||||
var value = 123456789012345L;
|
||||
var binary = AcBinarySerializer.Serialize(value);
|
||||
var result = AcBinaryDeserializer.Deserialize<long>(binary);
|
||||
Assert.AreEqual(value, result);
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void Serialize_Double_RoundTrip()
|
||||
{
|
||||
var value = 3.14159265358979;
|
||||
var binary = AcBinarySerializer.Serialize(value);
|
||||
var result = AcBinaryDeserializer.Deserialize<double>(binary);
|
||||
Assert.AreEqual(value, result);
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void Serialize_String_RoundTrip()
|
||||
{
|
||||
var value = "Hello, Binary World!";
|
||||
var binary = AcBinarySerializer.Serialize(value);
|
||||
var result = AcBinaryDeserializer.Deserialize<string>(binary);
|
||||
Assert.AreEqual(value, result);
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void Serialize_Boolean_RoundTrip()
|
||||
{
|
||||
var trueResult = AcBinaryDeserializer.Deserialize<bool>(AcBinarySerializer.Serialize(true));
|
||||
var falseResult = AcBinaryDeserializer.Deserialize<bool>(AcBinarySerializer.Serialize(false));
|
||||
Assert.IsTrue(trueResult);
|
||||
Assert.IsFalse(falseResult);
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void Serialize_DateTime_RoundTrip()
|
||||
{
|
||||
var value = new DateTime(2024, 12, 25, 10, 30, 45, DateTimeKind.Utc);
|
||||
var binary = AcBinarySerializer.Serialize(value);
|
||||
var result = AcBinaryDeserializer.Deserialize<DateTime>(binary);
|
||||
Assert.AreEqual(value, result);
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
[DataRow(DateTimeKind.Unspecified)]
|
||||
[DataRow(DateTimeKind.Utc)]
|
||||
[DataRow(DateTimeKind.Local)]
|
||||
public void Serialize_DateTime_PreservesKind(DateTimeKind kind)
|
||||
{
|
||||
var value = new DateTime(2024, 12, 25, 10, 30, 45, kind);
|
||||
var binary = AcBinarySerializer.Serialize(value);
|
||||
var result = AcBinaryDeserializer.Deserialize<DateTime>(binary);
|
||||
|
||||
Assert.AreEqual(value.Ticks, result.Ticks);
|
||||
Assert.AreEqual(value.Kind, result.Kind);
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void Serialize_Guid_RoundTrip()
|
||||
{
|
||||
var value = Guid.NewGuid();
|
||||
var binary = AcBinarySerializer.Serialize(value);
|
||||
var result = AcBinaryDeserializer.Deserialize<Guid>(binary);
|
||||
Assert.AreEqual(value, result);
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void Serialize_Decimal_RoundTrip()
|
||||
{
|
||||
var value = 123456.789012m;
|
||||
var binary = AcBinarySerializer.Serialize(value);
|
||||
var result = AcBinaryDeserializer.Deserialize<decimal>(binary);
|
||||
Assert.AreEqual(value, result);
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void Serialize_TimeSpan_RoundTrip()
|
||||
{
|
||||
var value = TimeSpan.FromHours(2.5);
|
||||
var binary = AcBinarySerializer.Serialize(value);
|
||||
var result = AcBinaryDeserializer.Deserialize<TimeSpan>(binary);
|
||||
Assert.AreEqual(value, result);
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void Serialize_DateTimeOffset_RoundTrip()
|
||||
{
|
||||
var value = new DateTimeOffset(2024, 12, 25, 10, 30, 45, TimeSpan.FromHours(2));
|
||||
var binary = AcBinarySerializer.Serialize(value);
|
||||
var result = AcBinaryDeserializer.Deserialize<DateTimeOffset>(binary);
|
||||
|
||||
Assert.AreEqual(value.UtcTicks, result.UtcTicks);
|
||||
Assert.AreEqual(value.Offset, result.Offset);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,82 @@
|
|||
using AyCode.Core.Serializers.Binaries;
|
||||
using AyCode.Core.Tests.TestModels;
|
||||
|
||||
namespace AyCode.Core.Tests.Serialization;
|
||||
|
||||
[TestClass]
|
||||
public class AcBinarySerializerBenchmarkTests
|
||||
{
|
||||
[TestMethod]
|
||||
public void Serialize_BenchmarkOrder_RoundTrip()
|
||||
{
|
||||
var order = TestDataFactory.CreateBenchmarkOrder(itemCount: 3, palletsPerItem: 2, measurementsPerPallet: 2, pointsPerMeasurement: 5);
|
||||
|
||||
var binary = AcBinarySerializer.Serialize(order);
|
||||
Assert.IsTrue(binary.Length > 0, "Binary data should not be empty");
|
||||
|
||||
var result = AcBinaryDeserializer.Deserialize<TestOrder>(binary);
|
||||
Assert.IsNotNull(result);
|
||||
Assert.AreEqual(order.Id, result.Id);
|
||||
Assert.AreEqual(order.OrderNumber, result.OrderNumber);
|
||||
Assert.AreEqual(order.Items.Count, result.Items.Count);
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void Serialize_BenchmarkOrder_SmallData_RoundTrip()
|
||||
{
|
||||
var order = TestDataFactory.CreateBenchmarkOrder(itemCount: 1, palletsPerItem: 1, measurementsPerPallet: 1, pointsPerMeasurement: 1);
|
||||
|
||||
var binary = AcBinarySerializer.Serialize(order);
|
||||
Assert.IsTrue(binary.Length > 0);
|
||||
|
||||
var result = AcBinaryDeserializer.Deserialize<TestOrder>(binary);
|
||||
Assert.IsNotNull(result);
|
||||
Assert.AreEqual(order.Id, result.Id);
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void Serialize_BenchmarkOrder_LargeData_RoundTrip()
|
||||
{
|
||||
var order = TestDataFactory.CreateBenchmarkOrder(itemCount: 10, palletsPerItem: 5, measurementsPerPallet: 3, pointsPerMeasurement: 10);
|
||||
|
||||
var binary = AcBinarySerializer.Serialize(order);
|
||||
Assert.IsTrue(binary.Length > 0, "Binary data should not be empty");
|
||||
|
||||
var result = AcBinaryDeserializer.Deserialize<TestOrder>(binary);
|
||||
Assert.IsNotNull(result);
|
||||
Assert.AreEqual(order.Id, result.Id);
|
||||
Assert.AreEqual(order.OrderNumber, result.OrderNumber);
|
||||
Assert.AreEqual(order.Items.Count, result.Items.Count);
|
||||
|
||||
// Verify nested structure
|
||||
for (int i = 0; i < order.Items.Count; i++)
|
||||
{
|
||||
Assert.AreEqual(order.Items[i].Id, result.Items[i].Id);
|
||||
Assert.AreEqual(order.Items[i].Pallets.Count, result.Items[i].Pallets.Count);
|
||||
}
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void Serialize_BenchmarkOrder_WithStringInterning_SmallerThanWithout()
|
||||
{
|
||||
var order = TestDataFactory.CreateBenchmarkOrder(itemCount: 5, palletsPerItem: 3, measurementsPerPallet: 2, pointsPerMeasurement: 5);
|
||||
|
||||
var binaryWithInterning = AcBinarySerializer.Serialize(order, AcBinarySerializerOptions.Default);
|
||||
var binaryWithoutInterning = AcBinarySerializer.Serialize(order, new AcBinarySerializerOptions { UseStringInterning = false });
|
||||
|
||||
// Note: String interning may not always result in smaller size due to header overhead
|
||||
// The primary benefit is for larger datasets with many repeated strings
|
||||
Console.WriteLine($"With interning: {binaryWithInterning.Length}, Without: {binaryWithoutInterning.Length}");
|
||||
|
||||
// Both should deserialize correctly regardless of size
|
||||
var result1 = AcBinaryDeserializer.Deserialize<TestOrder>(binaryWithInterning);
|
||||
var result2 = AcBinaryDeserializer.Deserialize<TestOrder>(binaryWithoutInterning);
|
||||
|
||||
Assert.IsNotNull(result1);
|
||||
Assert.IsNotNull(result2);
|
||||
Assert.AreEqual(order.Id, result1.Id);
|
||||
Assert.AreEqual(order.Id, result2.Id);
|
||||
Assert.AreEqual(order.Items.Count, result1.Items.Count);
|
||||
Assert.AreEqual(order.Items.Count, result2.Items.Count);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,160 @@
|
|||
using AyCode.Core.Extensions;
|
||||
using static AyCode.Core.Tests.Serialization.AcSerializerModels;
|
||||
|
||||
namespace AyCode.Core.Tests.Serialization;
|
||||
|
||||
/// <summary>
|
||||
/// Tests for circular reference handling with back-navigation properties.
|
||||
/// </summary>
|
||||
[TestClass]
|
||||
public class AcBinarySerializerCircularReferenceTests
|
||||
{
|
||||
/// <summary>
|
||||
/// CRITICAL TEST: Circular references with back-navigation properties.
|
||||
/// This simulates the exact production scenario where:
|
||||
/// - StockTaking has StockTakingItems collection
|
||||
/// - StockTakingItem has StockTaking back-reference (circular!)
|
||||
/// - StockTakingItem has Product navigation property
|
||||
/// </summary>
|
||||
[TestMethod]
|
||||
public void Deserialize_CircularReference_ParentChildBackReference()
|
||||
{
|
||||
var parent = new CircularParent
|
||||
{
|
||||
Id = 1,
|
||||
Name = "Parent",
|
||||
Created = new DateTime(2025, 1, 24, 15, 25, 0, DateTimeKind.Utc),
|
||||
Modified = DateTime.UtcNow,
|
||||
Creator = 6,
|
||||
Children = new List<CircularChild>()
|
||||
};
|
||||
|
||||
var child = new CircularChild
|
||||
{
|
||||
Id = 10,
|
||||
ParentId = 1,
|
||||
Name = "Child",
|
||||
Created = DateTime.UtcNow.AddHours(-1),
|
||||
Modified = DateTime.UtcNow,
|
||||
Parent = parent,
|
||||
GrandChildren = new List<CircularGrandChild>()
|
||||
};
|
||||
|
||||
var grandChild = new CircularGrandChild
|
||||
{
|
||||
Id = 100,
|
||||
ChildId = 10,
|
||||
CreatorId = 6,
|
||||
ModifierId = null,
|
||||
Created = DateTime.UtcNow,
|
||||
Modified = DateTime.UtcNow,
|
||||
Child = child
|
||||
};
|
||||
|
||||
child.GrandChildren.Add(grandChild);
|
||||
parent.Children.Add(child);
|
||||
|
||||
var binary = parent.ToBinary();
|
||||
var result = binary.BinaryTo<CircularParent>();
|
||||
|
||||
Assert.IsNotNull(result);
|
||||
Assert.AreEqual(1, result.Id);
|
||||
Assert.AreEqual(6, result.Creator, "Creator should be 6");
|
||||
Assert.AreEqual(parent.Created.Ticks, result.Created.Ticks,
|
||||
$"Created mismatch. Expected: {parent.Created}, Got: {result.Created}");
|
||||
|
||||
Assert.IsNotNull(result.Children);
|
||||
Assert.AreEqual(1, result.Children.Count);
|
||||
|
||||
var resultChild = result.Children[0];
|
||||
Assert.AreEqual(10, resultChild.Id);
|
||||
Assert.AreEqual(resultChild.Created.Ticks, child.Created.Ticks, "Child.Created should match");
|
||||
|
||||
Assert.IsNotNull(resultChild.Parent, "Child.Parent back-reference should be resolved");
|
||||
Assert.AreEqual(1, resultChild.Parent.Id, "Back-reference should point to same parent");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Test list of parents with circular references.
|
||||
/// </summary>
|
||||
[TestMethod]
|
||||
public void Deserialize_ListOfCircularReferences_AllItemsCorrect()
|
||||
{
|
||||
var parents = Enumerable.Range(1, 5).Select(p =>
|
||||
{
|
||||
var parent = new CircularParent
|
||||
{
|
||||
Id = p,
|
||||
Name = $"Parent_{p}",
|
||||
Created = DateTime.UtcNow.AddDays(-p),
|
||||
Modified = DateTime.UtcNow,
|
||||
Creator = p,
|
||||
Children = new List<CircularChild>()
|
||||
};
|
||||
|
||||
for (int c = 1; c <= 2; c++)
|
||||
{
|
||||
var child = new CircularChild
|
||||
{
|
||||
Id = p * 100 + c,
|
||||
ParentId = p,
|
||||
Name = $"Child_{p}_{c}",
|
||||
Created = DateTime.UtcNow.AddHours(-c),
|
||||
Modified = DateTime.UtcNow,
|
||||
Parent = parent,
|
||||
GrandChildren = new List<CircularGrandChild>()
|
||||
};
|
||||
|
||||
for (int g = 1; g <= 2; g++)
|
||||
{
|
||||
child.GrandChildren.Add(new CircularGrandChild
|
||||
{
|
||||
Id = p * 1000 + c * 100 + g,
|
||||
ChildId = child.Id,
|
||||
CreatorId = g % 2 == 0 ? p : null,
|
||||
ModifierId = g % 2 == 1 ? p * 2 : null,
|
||||
Created = DateTime.UtcNow,
|
||||
Modified = DateTime.UtcNow,
|
||||
Child = child
|
||||
});
|
||||
}
|
||||
|
||||
parent.Children.Add(child);
|
||||
}
|
||||
|
||||
return parent;
|
||||
}).ToList();
|
||||
|
||||
var binary = parents.ToBinary();
|
||||
var result = binary.BinaryTo<List<CircularParent>>();
|
||||
|
||||
Assert.IsNotNull(result);
|
||||
Assert.AreEqual(5, result.Count);
|
||||
|
||||
for (int p = 0; p < 5; p++)
|
||||
{
|
||||
var original = parents[p];
|
||||
var deserialized = result[p];
|
||||
|
||||
Assert.AreEqual(original.Id, deserialized.Id, $"Parent[{p}].Id mismatch");
|
||||
Assert.AreEqual(original.Creator, deserialized.Creator, $"Parent[{p}].Creator mismatch");
|
||||
Assert.AreEqual(original.Created.Ticks, deserialized.Created.Ticks,
|
||||
$"Parent[{p}].Created mismatch. Expected: {original.Created}, Got: {deserialized.Created}");
|
||||
|
||||
Assert.IsNotNull(deserialized.Children, $"Parent[{p}].Children is null");
|
||||
Assert.AreEqual(2, deserialized.Children.Count, $"Parent[{p}].Children.Count mismatch");
|
||||
|
||||
for (int c = 0; c < 2; c++)
|
||||
{
|
||||
var origChild = original.Children![c];
|
||||
var deserChild = deserialized.Children[c];
|
||||
|
||||
Assert.AreEqual(origChild.Id, deserChild.Id, $"Parent[{p}].Children[{c}].Id mismatch");
|
||||
Assert.AreEqual(origChild.Created.Ticks, deserChild.Created.Ticks,
|
||||
$"Parent[{p}].Children[{c}].Created mismatch");
|
||||
|
||||
Assert.IsNotNull(deserChild.Parent, $"Parent[{p}].Children[{c}].Parent should not be null");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,188 @@
|
|||
using AyCode.Core.Extensions;
|
||||
using static AyCode.Core.Tests.Serialization.AcSerializerModels;
|
||||
using static AyCode.Core.Tests.Serialization.AcSerializerTestHelper;
|
||||
|
||||
namespace AyCode.Core.Tests.Serialization;
|
||||
|
||||
/// <summary>
|
||||
/// Tests for DateTime type handling and potential type mismatch issues.
|
||||
/// </summary>
|
||||
[TestClass]
|
||||
public class AcBinarySerializerDateTimeTests
|
||||
{
|
||||
[TestMethod]
|
||||
public void Deserialize_DateTimeProperty_FromDifferentPropertyOrder_RoundTrip()
|
||||
{
|
||||
var entity = new TestEntityWithDateTimeAndInt
|
||||
{
|
||||
Id = 42,
|
||||
IntValue = 100,
|
||||
Created = new DateTime(2024, 12, 25, 10, 30, 45, DateTimeKind.Utc),
|
||||
Modified = new DateTime(2024, 12, 26, 11, 45, 30, DateTimeKind.Utc),
|
||||
StatusCode = 5,
|
||||
Name = "TestEntity"
|
||||
};
|
||||
|
||||
var binary = entity.ToBinary();
|
||||
var result = binary.BinaryTo<TestEntityWithDateTimeAndInt>();
|
||||
|
||||
Assert.IsNotNull(result);
|
||||
Assert.AreEqual(42, result.Id);
|
||||
Assert.AreEqual(100, result.IntValue);
|
||||
Assert.AreEqual(entity.Created, result.Created, "Created DateTime should match");
|
||||
Assert.AreEqual(entity.Modified, result.Modified, "Modified DateTime should match");
|
||||
Assert.AreEqual(5, result.StatusCode);
|
||||
Assert.AreEqual("TestEntity", result.Name);
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void Deserialize_ListOfEntitiesWithDateTimeProperties_RoundTrip()
|
||||
{
|
||||
var entities = CreateDateTimeEntities(10);
|
||||
var binary = entities.ToBinary();
|
||||
var result = binary.BinaryTo<List<TestEntityWithDateTimeAndInt>>();
|
||||
|
||||
Assert.IsNotNull(result);
|
||||
Assert.AreEqual(10, result.Count);
|
||||
|
||||
for (int i = 0; i < 10; i++)
|
||||
{
|
||||
var original = entities[i];
|
||||
var deserialized = result[i];
|
||||
|
||||
Assert.AreEqual(original.Id, deserialized.Id, $"Id mismatch at index {i}");
|
||||
Assert.AreEqual(original.IntValue, deserialized.IntValue, $"IntValue mismatch at index {i}");
|
||||
Assert.AreEqual(original.Created.Ticks, deserialized.Created.Ticks, $"Created mismatch at index {i}");
|
||||
Assert.AreEqual(original.Modified.Ticks, deserialized.Modified.Ticks, $"Modified mismatch at index {i}");
|
||||
Assert.AreEqual(original.StatusCode, deserialized.StatusCode, $"StatusCode mismatch at index {i}");
|
||||
Assert.AreEqual(original.Name, deserialized.Name, $"Name mismatch at index {i}");
|
||||
}
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void Deserialize_EntityWithManyIntPropertiesBeforeDateTime_RoundTrip()
|
||||
{
|
||||
var entity = new TestEntityWithManyIntsBeforeDateTime
|
||||
{
|
||||
Id = 1,
|
||||
Value1 = 10,
|
||||
Value2 = 20,
|
||||
Value3 = 30,
|
||||
Value4 = 40,
|
||||
Value5 = 50,
|
||||
FirstDateTime = new DateTime(2024, 1, 15, 10, 0, 0, DateTimeKind.Utc),
|
||||
SecondDateTime = new DateTime(2024, 6, 20, 15, 30, 0, DateTimeKind.Utc),
|
||||
FinalValue = 999
|
||||
};
|
||||
|
||||
var binary = entity.ToBinary();
|
||||
var result = binary.BinaryTo<TestEntityWithManyIntsBeforeDateTime>();
|
||||
|
||||
Assert.IsNotNull(result);
|
||||
Assert.AreEqual(1, result.Id);
|
||||
Assert.AreEqual(10, result.Value1);
|
||||
Assert.AreEqual(20, result.Value2);
|
||||
Assert.AreEqual(30, result.Value3);
|
||||
Assert.AreEqual(40, result.Value4);
|
||||
Assert.AreEqual(50, result.Value5);
|
||||
Assert.AreEqual(entity.FirstDateTime, result.FirstDateTime, "FirstDateTime should match");
|
||||
Assert.AreEqual(entity.SecondDateTime, result.SecondDateTime, "SecondDateTime should match");
|
||||
Assert.AreEqual(999, result.FinalValue);
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void Deserialize_NestedEntityWithDateTimeInChild_RoundTrip()
|
||||
{
|
||||
var parent = new TestParentEntityWithDateTimeChild
|
||||
{
|
||||
ParentId = 1,
|
||||
ParentName = "Parent",
|
||||
Child = new TestEntityWithDateTimeAndInt
|
||||
{
|
||||
Id = 100,
|
||||
IntValue = 200,
|
||||
Created = DateTime.UtcNow.AddDays(-5),
|
||||
Modified = DateTime.UtcNow,
|
||||
StatusCode = 3,
|
||||
Name = "Child"
|
||||
}
|
||||
};
|
||||
|
||||
var binary = parent.ToBinary();
|
||||
var result = binary.BinaryTo<TestParentEntityWithDateTimeChild>();
|
||||
|
||||
Assert.IsNotNull(result);
|
||||
Assert.AreEqual(1, result.ParentId);
|
||||
Assert.AreEqual("Parent", result.ParentName);
|
||||
Assert.IsNotNull(result.Child);
|
||||
Assert.AreEqual(100, result.Child.Id);
|
||||
Assert.AreEqual(200, result.Child.IntValue);
|
||||
Assert.AreEqual(parent.Child.Created.Ticks, result.Child.Created.Ticks, "Child.Created should match");
|
||||
Assert.AreEqual(parent.Child.Modified.Ticks, result.Child.Modified.Ticks, "Child.Modified should match");
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void Deserialize_EntityWithCollectionContainingDateTimeItems_RoundTrip()
|
||||
{
|
||||
var parent = new TestParentWithDateTimeItemCollection
|
||||
{
|
||||
Id = 1,
|
||||
Name = "Parent",
|
||||
Created = DateTime.UtcNow.AddDays(-10),
|
||||
Items = CreateDateTimeEntities(5)
|
||||
};
|
||||
|
||||
var binary = parent.ToBinary();
|
||||
var result = binary.BinaryTo<TestParentWithDateTimeItemCollection>();
|
||||
|
||||
Assert.IsNotNull(result);
|
||||
Assert.AreEqual(1, result.Id);
|
||||
Assert.AreEqual("Parent", result.Name);
|
||||
Assert.AreEqual(parent.Created.Ticks, result.Created.Ticks, "Parent.Created should match");
|
||||
Assert.IsNotNull(result.Items);
|
||||
Assert.AreEqual(5, result.Items.Count);
|
||||
|
||||
for (int i = 0; i < 5; i++)
|
||||
{
|
||||
var originalItem = parent.Items[i];
|
||||
var deserializedItem = result.Items[i];
|
||||
|
||||
Assert.AreEqual(originalItem.Id, deserializedItem.Id, $"Items[{i}].Id should match");
|
||||
Assert.AreEqual(originalItem.IntValue, deserializedItem.IntValue, $"Items[{i}].IntValue should match");
|
||||
Assert.AreEqual(originalItem.Created.Ticks, deserializedItem.Created.Ticks, $"Items[{i}].Created should match");
|
||||
Assert.AreEqual(originalItem.Modified.Ticks, deserializedItem.Modified.Ticks, $"Items[{i}].Modified should match");
|
||||
}
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void Deserialize_ListOfParentEntitiesWithDateTimeChildCollections_RoundTrip()
|
||||
{
|
||||
var parents = CreateParentWithDateTimeItems(3, 3);
|
||||
var binary = parents.ToBinary();
|
||||
var result = binary.BinaryTo<List<TestParentWithDateTimeItemCollection>>();
|
||||
|
||||
Assert.IsNotNull(result);
|
||||
Assert.AreEqual(3, result.Count);
|
||||
|
||||
for (int p = 0; p < 3; p++)
|
||||
{
|
||||
var originalParent = parents[p];
|
||||
var deserializedParent = result[p];
|
||||
|
||||
Assert.AreEqual(originalParent.Id, deserializedParent.Id, $"Parent[{p}].Id should match");
|
||||
Assert.AreEqual(originalParent.Name, deserializedParent.Name, $"Parent[{p}].Name should match");
|
||||
Assert.AreEqual(originalParent.Created.Ticks, deserializedParent.Created.Ticks, $"Parent[{p}].Created should match");
|
||||
Assert.IsNotNull(deserializedParent.Items);
|
||||
Assert.AreEqual(3, deserializedParent.Items.Count, $"Parent[{p}].Items.Count should match");
|
||||
|
||||
for (int i = 0; i < 3; i++)
|
||||
{
|
||||
var originalItem = originalParent.Items![i];
|
||||
var deserializedItem = deserializedParent.Items[i];
|
||||
|
||||
Assert.AreEqual(originalItem.Id, deserializedItem.Id, $"Parent[{p}].Items[{i}].Id should match");
|
||||
Assert.AreEqual(originalItem.Created.Ticks, deserializedItem.Created.Ticks, $"Parent[{p}].Items[{i}].Created should match. Expected: {originalItem.Created}, Got: {deserializedItem.Created}");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,474 @@
|
|||
using AyCode.Core.Extensions;
|
||||
using AyCode.Core.Serializers.Binaries;
|
||||
using System.Reflection;
|
||||
using static AyCode.Core.Tests.Serialization.AcSerializerModels;
|
||||
|
||||
namespace AyCode.Core.Tests.Serialization;
|
||||
|
||||
/// <summary>
|
||||
/// Diagnostic tests to help debug serialization issues.
|
||||
/// </summary>
|
||||
[TestClass]
|
||||
public class AcBinarySerializerDiagnosticTests
|
||||
{
|
||||
/// <summary>
|
||||
/// Diagnostic test to understand the exact binary structure.
|
||||
/// This test outputs the binary bytes to help debug production issues.
|
||||
/// </summary>
|
||||
[TestMethod]
|
||||
public void Diagnostic_StockTaking_BinaryStructure()
|
||||
{
|
||||
var stockTaking = new TestStockTakingWithInheritance
|
||||
{
|
||||
Id = 1,
|
||||
StartDateTime = new DateTime(2025, 1, 24, 10, 0, 0, DateTimeKind.Utc),
|
||||
IsClosed = false,
|
||||
Creator = 6,
|
||||
Created = new DateTime(2025, 1, 24, 15, 25, 0, DateTimeKind.Utc),
|
||||
Modified = new DateTime(2025, 1, 24, 16, 0, 0, DateTimeKind.Utc),
|
||||
StockTakingItems = null
|
||||
};
|
||||
|
||||
var binary = stockTaking.ToBinary();
|
||||
|
||||
var hexDump = string.Join(" ", binary.Select(b => b.ToString("X2")));
|
||||
Console.WriteLine($"Binary length: {binary.Length}");
|
||||
Console.WriteLine($"Binary hex: {hexDump}");
|
||||
|
||||
for (int i = 0; i < binary.Length; i++)
|
||||
{
|
||||
if (binary[i] == 214)
|
||||
{
|
||||
Console.WriteLine($"Found 0xD6 at position {i}");
|
||||
}
|
||||
}
|
||||
|
||||
var result = binary.BinaryTo<TestStockTakingWithInheritance>();
|
||||
|
||||
Assert.IsNotNull(result);
|
||||
Assert.AreEqual(6, result.Creator);
|
||||
Assert.AreEqual(stockTaking.Created.Ticks, result.Created.Ticks);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Test with nested list to ensure proper stream positioning.
|
||||
/// </summary>
|
||||
[TestMethod]
|
||||
public void Diagnostic_StockTaking_WithNestedItems()
|
||||
{
|
||||
var stockTaking = new TestStockTakingWithInheritance
|
||||
{
|
||||
Id = 1,
|
||||
StartDateTime = new DateTime(2025, 1, 24, 10, 0, 0, DateTimeKind.Utc),
|
||||
IsClosed = false,
|
||||
Creator = 6,
|
||||
Created = new DateTime(2025, 1, 24, 15, 25, 0, DateTimeKind.Utc),
|
||||
Modified = new DateTime(2025, 1, 24, 16, 0, 0, DateTimeKind.Utc),
|
||||
StockTakingItems = new List<TestStockTakingItemWithInheritance>
|
||||
{
|
||||
new()
|
||||
{
|
||||
Id = 10,
|
||||
StockTakingId = 1,
|
||||
ProductId = 100,
|
||||
IsMeasured = true,
|
||||
OriginalStockQuantity = 50,
|
||||
MeasuredStockQuantity = 48,
|
||||
Created = new DateTime(2025, 1, 24, 14, 0, 0, DateTimeKind.Utc),
|
||||
Modified = new DateTime(2025, 1, 24, 14, 30, 0, DateTimeKind.Utc),
|
||||
StockTakingItemPallets = null
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
var binary = stockTaking.ToBinary();
|
||||
Console.WriteLine($"Binary length with 1 item: {binary.Length}");
|
||||
|
||||
var result = binary.BinaryTo<TestStockTakingWithInheritance>();
|
||||
|
||||
Assert.IsNotNull(result);
|
||||
Assert.AreEqual(6, result.Creator, "Creator should be 6");
|
||||
Assert.AreEqual(stockTaking.Created.Ticks, result.Created.Ticks,
|
||||
$"Created mismatch. Expected: {stockTaking.Created}, Got: {result.Created}");
|
||||
Assert.IsNotNull(result.StockTakingItems);
|
||||
Assert.AreEqual(1, result.StockTakingItems.Count);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// CRITICAL TEST: Verify property order is consistent.
|
||||
/// This test checks that the reflection-based property order matches
|
||||
/// what's expected for serialization/deserialization.
|
||||
/// </summary>
|
||||
[TestMethod]
|
||||
public void Diagnostic_PropertyOrder_InheritanceHierarchy()
|
||||
{
|
||||
var type = typeof(SimStockTaking);
|
||||
var props = type.GetProperties(BindingFlags.Public | BindingFlags.Instance);
|
||||
|
||||
Console.WriteLine($"Properties of {type.Name} (count: {props.Length}):");
|
||||
for (int i = 0; i < props.Length; i++)
|
||||
{
|
||||
var prop = props[i];
|
||||
Console.WriteLine($" [{i}] {prop.Name} : {prop.PropertyType.Name} (declared in: {prop.DeclaringType?.Name})");
|
||||
}
|
||||
|
||||
// The exact order may vary by platform!
|
||||
// Log it so we can compare server vs client
|
||||
Assert.IsTrue(props.Length >= 7, "Should have at least 7 properties");
|
||||
|
||||
// Check that all expected properties exist
|
||||
var propNames = props.Select(p => p.Name).ToHashSet();
|
||||
Assert.IsTrue(propNames.Contains("Id"), "Should have Id");
|
||||
Assert.IsTrue(propNames.Contains("StartDateTime"), "Should have StartDateTime");
|
||||
Assert.IsTrue(propNames.Contains("IsClosed"), "Should have IsClosed");
|
||||
Assert.IsTrue(propNames.Contains("Creator"), "Should have Creator");
|
||||
Assert.IsTrue(propNames.Contains("Created"), "Should have Created");
|
||||
Assert.IsTrue(propNames.Contains("Modified"), "Should have Modified");
|
||||
Assert.IsTrue(propNames.Contains("StockTakingItems"), "Should have StockTakingItems");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// CRITICAL REGRESSION TEST: Simulates exact production hierarchy.
|
||||
/// StockTaking : MgStockTaking<StockTakingItem> : MgEntityBase : BaseEntity
|
||||
/// </summary>
|
||||
[TestMethod]
|
||||
public void Diagnostic_SimStockTaking_RoundTrip()
|
||||
{
|
||||
var stockTaking = new SimStockTaking
|
||||
{
|
||||
Id = 1,
|
||||
StartDateTime = new DateTime(2025, 1, 24, 10, 0, 0, DateTimeKind.Utc),
|
||||
IsClosed = false,
|
||||
Creator = 6, // The exact value from production error
|
||||
Created = new DateTime(2025, 1, 24, 15, 25, 0, DateTimeKind.Utc),
|
||||
Modified = new DateTime(2025, 1, 24, 16, 0, 0, DateTimeKind.Utc),
|
||||
StockTakingItems = null // loadRelations = false means no items
|
||||
};
|
||||
|
||||
var binary = stockTaking.ToBinary();
|
||||
|
||||
// Log the property names in the header
|
||||
Console.WriteLine($"Binary length: {binary.Length}");
|
||||
|
||||
var result = binary.BinaryTo<SimStockTaking>();
|
||||
|
||||
Assert.IsNotNull(result);
|
||||
Assert.AreEqual(1, result.Id, "Id should be 1");
|
||||
Assert.AreEqual(6, result.Creator, "Creator should be 6 - this is where the bug occurs!");
|
||||
Assert.AreEqual(stockTaking.Created.Ticks, result.Created.Ticks,
|
||||
$"Created mismatch. Expected: {stockTaking.Created}, Got: {result.Created}");
|
||||
Assert.AreEqual(stockTaking.StartDateTime, result.StartDateTime);
|
||||
Assert.IsFalse(result.IsClosed);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Test List of SimStockTaking - exact production scenario.
|
||||
/// </summary>
|
||||
[TestMethod]
|
||||
public void Diagnostic_ListOfSimStockTaking_RoundTrip()
|
||||
{
|
||||
var stockTakings = Enumerable.Range(1, 3).Select(i => new SimStockTaking
|
||||
{
|
||||
Id = i,
|
||||
StartDateTime = DateTime.UtcNow.AddDays(-i),
|
||||
IsClosed = i % 2 == 0,
|
||||
Creator = i,
|
||||
Created = DateTime.UtcNow.AddDays(-i),
|
||||
Modified = DateTime.UtcNow,
|
||||
StockTakingItems = null
|
||||
}).ToList();
|
||||
|
||||
var binary = stockTakings.ToBinary();
|
||||
var result = binary.BinaryTo<List<SimStockTaking>>();
|
||||
|
||||
Assert.IsNotNull(result);
|
||||
Assert.AreEqual(3, result.Count);
|
||||
|
||||
for (int i = 0; i < 3; i++)
|
||||
{
|
||||
var original = stockTakings[i];
|
||||
var deserialized = result[i];
|
||||
|
||||
Assert.AreEqual(original.Id, deserialized.Id, $"[{i}] Id mismatch");
|
||||
Assert.AreEqual(original.Creator, deserialized.Creator, $"[{i}] Creator mismatch");
|
||||
Assert.AreEqual(original.Created.Ticks, deserialized.Created.Ticks,
|
||||
$"[{i}] Created mismatch. Expected: {original.Created}, Got: {deserialized.Created}");
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Diagnostic: Check what PropertyType the reflection returns for generic type parameter.
|
||||
/// </summary>
|
||||
[TestMethod]
|
||||
public void Diagnostic_GenericProperty_ReflectionType()
|
||||
{
|
||||
var parentType = typeof(ConcreteParent);
|
||||
var itemsProp = parentType.GetProperty("Items");
|
||||
|
||||
Assert.IsNotNull(itemsProp);
|
||||
|
||||
var propType = itemsProp.PropertyType;
|
||||
Console.WriteLine($"PropertyType: {propType}");
|
||||
Console.WriteLine($"PropertyType.FullName: {propType.FullName}");
|
||||
Console.WriteLine($"IsGenericType: {propType.IsGenericType}");
|
||||
|
||||
if (propType.IsGenericType)
|
||||
{
|
||||
var args = propType.GetGenericArguments();
|
||||
Console.WriteLine($"GenericArguments.Length: {args.Length}");
|
||||
foreach (var arg in args)
|
||||
{
|
||||
Console.WriteLine($" GenericArgument: {arg.FullName}");
|
||||
}
|
||||
}
|
||||
|
||||
Assert.IsTrue(propType.IsGenericType);
|
||||
var elementType = propType.GetGenericArguments()[0];
|
||||
Assert.AreEqual(typeof(GenericItemImpl), elementType,
|
||||
"Element type should be GenericItemImpl, not IGenericItem");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// CRITICAL BUG REPRODUCTION: StockTakingItems is null (loadRelations = false)
|
||||
/// This test verifies what happens when:
|
||||
/// 1. Metadata header registers ALL properties including StockTakingItems
|
||||
/// 2. Body SKIPS StockTakingItems because it's null
|
||||
/// 3. Deserializer reads the body and must correctly map indices
|
||||
/// </summary>
|
||||
[TestMethod]
|
||||
public void Diagnostic_NullStockTakingItems_VerifyPropertyIndices()
|
||||
{
|
||||
var stockTaking = new SimStockTaking
|
||||
{
|
||||
Id = 1,
|
||||
StartDateTime = new DateTime(2025, 1, 24, 10, 0, 0, DateTimeKind.Utc),
|
||||
IsClosed = false,
|
||||
Creator = 6, // The exact value from production error (becomes TinyInt 0xD6)
|
||||
Created = new DateTime(2025, 1, 24, 15, 25, 0, DateTimeKind.Utc),
|
||||
Modified = new DateTime(2025, 1, 24, 16, 0, 0, DateTimeKind.Utc),
|
||||
StockTakingItems = null // THIS IS THE KEY - loadRelations = false
|
||||
};
|
||||
|
||||
var binary = stockTaking.ToBinary();
|
||||
|
||||
// Log the binary structure
|
||||
Console.WriteLine($"Binary length: {binary.Length}");
|
||||
|
||||
// Parse the header manually to understand structure
|
||||
var pos = 0;
|
||||
var version = binary[pos++];
|
||||
Console.WriteLine($"Version: {version}");
|
||||
|
||||
var marker = binary[pos++];
|
||||
Console.WriteLine($"Marker: 0x{marker:X2}");
|
||||
|
||||
// Read property count from metadata header
|
||||
if ((marker & 0x10) != 0) // HasMetadata flag
|
||||
{
|
||||
var propCount = binary[pos++];
|
||||
Console.WriteLine($"\n=== METADATA HEADER ===");
|
||||
Console.WriteLine($"Property count in header: {propCount}");
|
||||
|
||||
for (int i = 0; i < propCount; i++)
|
||||
{
|
||||
var strLen = binary[pos++];
|
||||
var propName = System.Text.Encoding.UTF8.GetString(binary, pos, strLen);
|
||||
pos += strLen;
|
||||
Console.WriteLine($" Header property [{i}]: '{propName}'");
|
||||
}
|
||||
}
|
||||
|
||||
Console.WriteLine($"\n=== BODY (starts at position {pos}) ===");
|
||||
|
||||
// The body should start with Object marker (0x19)
|
||||
var bodyStart = pos;
|
||||
var objectMarker = binary[pos++];
|
||||
Console.WriteLine($"Object marker: 0x{objectMarker:X2} (expected 0x19 for Object)");
|
||||
|
||||
// Read ref ID (if reference handling is enabled)
|
||||
// VarInt: if top bit is set, continue reading
|
||||
var refIdByte = binary[pos];
|
||||
int refId;
|
||||
if ((refIdByte & 0x80) == 0)
|
||||
{
|
||||
refId = refIdByte;
|
||||
pos++;
|
||||
}
|
||||
else
|
||||
{
|
||||
// Multi-byte VarInt - simplified parsing
|
||||
refId = -1;
|
||||
pos += 2; // Skip for now
|
||||
}
|
||||
Console.WriteLine($"RefId: {refId}");
|
||||
|
||||
// Read property count in body
|
||||
var bodyPropCount = binary[pos++];
|
||||
Console.WriteLine($"Property count in body: {bodyPropCount}");
|
||||
|
||||
Console.WriteLine($"\n=== BODY PROPERTIES ===");
|
||||
for (int i = 0; i < bodyPropCount && pos < binary.Length; i++)
|
||||
{
|
||||
var propIndex = binary[pos++];
|
||||
Console.WriteLine($" Body property [{i}]: index={propIndex}, next bytes: 0x{binary[pos]:X2} 0x{(pos + 1 < binary.Length ? binary[pos + 1] : 0):X2}");
|
||||
|
||||
// Skip the value (simplified - just log)
|
||||
var valueType = binary[pos];
|
||||
if (valueType == 0x14) // DateTime
|
||||
{
|
||||
Console.WriteLine($" -> DateTime (9 bytes)");
|
||||
pos += 10; // type + 9 bytes
|
||||
}
|
||||
else if (valueType >= 0xD0 && valueType <= 0xE7) // TinyInt
|
||||
{
|
||||
var tinyValue = valueType - 0xD0;
|
||||
Console.WriteLine($" -> TinyInt value: {tinyValue}");
|
||||
pos += 1;
|
||||
}
|
||||
else if (valueType == 0x03) // False
|
||||
{
|
||||
Console.WriteLine($" -> Boolean: false");
|
||||
pos += 1;
|
||||
}
|
||||
else if (valueType == 0x02) // True
|
||||
{
|
||||
Console.WriteLine($" -> Boolean: true");
|
||||
pos += 1;
|
||||
}
|
||||
else
|
||||
{
|
||||
Console.WriteLine($" -> Unknown type: 0x{valueType:X2}");
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// Find where 0xD6 (Creator = 6) appears in the body
|
||||
Console.WriteLine($"\n=== 0xD6 OCCURRENCES ===");
|
||||
for (int i = bodyStart; i < binary.Length; i++)
|
||||
{
|
||||
if (binary[i] == 0xD6)
|
||||
{
|
||||
Console.WriteLine($"Found 0xD6 (TinyInt 6 = Creator value) at position {i}");
|
||||
}
|
||||
}
|
||||
|
||||
// Deserialize and verify
|
||||
var result = binary.BinaryTo<SimStockTaking>();
|
||||
|
||||
Assert.IsNotNull(result);
|
||||
Assert.AreEqual(1, result.Id, "Id should be 1");
|
||||
Assert.AreEqual(6, result.Creator,
|
||||
$"Creator should be 6. Got: {result.Creator}. " +
|
||||
$"If this fails with a very large number, it means DateTime bytes were interpreted as int!");
|
||||
Assert.AreEqual(stockTaking.Created, result.Created,
|
||||
$"Created mismatch. Expected: {stockTaking.Created}, Got: {result.Created}. " +
|
||||
$"If Created has wrong value, deserializer read wrong bytes!");
|
||||
Assert.AreEqual(stockTaking.StartDateTime, result.StartDateTime);
|
||||
Assert.IsFalse(result.IsClosed);
|
||||
Assert.IsNull(result.StockTakingItems, "StockTakingItems should remain null");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Test to verify property order consistency between serializer and deserializer.
|
||||
/// </summary>
|
||||
[TestMethod]
|
||||
public void Diagnostic_VerifyPropertyOrderConsistency()
|
||||
{
|
||||
// Get serializer's property order
|
||||
var serializerType = typeof(AcBinarySerializer);
|
||||
var metadataCacheField = serializerType.GetField("TypeMetadataCache",
|
||||
BindingFlags.NonPublic | BindingFlags.Static);
|
||||
|
||||
// Clear cache to force fresh metadata creation
|
||||
// (This helps ensure we're testing the actual order)
|
||||
|
||||
var type = typeof(SimStockTaking);
|
||||
var props = type.GetProperties(BindingFlags.Public | BindingFlags.Instance)
|
||||
.Where(p => p.CanRead && p.GetIndexParameters().Length == 0)
|
||||
.ToArray();
|
||||
|
||||
Console.WriteLine($"Properties of {type.Name} (reflection order):");
|
||||
for (int i = 0; i < props.Length; i++)
|
||||
{
|
||||
var prop = props[i];
|
||||
Console.WriteLine($" [{i}] {prop.Name} : {prop.PropertyType.Name}");
|
||||
}
|
||||
|
||||
// Verify Creator comes BEFORE Created in the reflection order
|
||||
var creatorIndex = Array.FindIndex(props, p => p.Name == "Creator");
|
||||
var createdIndex = Array.FindIndex(props, p => p.Name == "Created");
|
||||
var stockTakingItemsIndex = Array.FindIndex(props, p => p.Name == "StockTakingItems");
|
||||
|
||||
Console.WriteLine($"\nKey indices:");
|
||||
Console.WriteLine($" StockTakingItems: {stockTakingItemsIndex}");
|
||||
Console.WriteLine($" Creator: {creatorIndex}");
|
||||
Console.WriteLine($" Created: {createdIndex}");
|
||||
|
||||
// The bug scenario: if StockTakingItems is skipped during serialization,
|
||||
// but the deserializer still expects it at the original index position,
|
||||
// then Creator (index 3) would be read when expecting StockTakingItems (index 2)
|
||||
// and Created (index 4) would be read when expecting Creator (index 3)
|
||||
|
||||
Assert.IsTrue(stockTakingItemsIndex >= 0, "StockTakingItems should exist");
|
||||
Assert.IsTrue(creatorIndex >= 0, "Creator should exist");
|
||||
Assert.IsTrue(createdIndex >= 0, "Created should exist");
|
||||
|
||||
// In the class definition order:
|
||||
// StockTakingItems comes BEFORE Creator and Created
|
||||
Assert.IsTrue(stockTakingItemsIndex < creatorIndex,
|
||||
"StockTakingItems should come before Creator");
|
||||
Assert.IsTrue(creatorIndex < createdIndex,
|
||||
"Creator should come before Created");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Test multiple StockTakings with null StockTakingItems - exact production scenario.
|
||||
/// </summary>
|
||||
[TestMethod]
|
||||
public void Diagnostic_MultipleStockTakings_NullItems()
|
||||
{
|
||||
var stockTakings = new List<SimStockTaking>
|
||||
{
|
||||
new()
|
||||
{
|
||||
Id = 1,
|
||||
StartDateTime = new DateTime(2025, 1, 24, 10, 0, 0, DateTimeKind.Utc),
|
||||
IsClosed = false,
|
||||
Creator = 6,
|
||||
Created = new DateTime(2025, 1, 24, 15, 25, 0, DateTimeKind.Utc),
|
||||
Modified = new DateTime(2025, 1, 24, 16, 0, 0, DateTimeKind.Utc),
|
||||
StockTakingItems = null
|
||||
},
|
||||
new()
|
||||
{
|
||||
Id = 2,
|
||||
StartDateTime = new DateTime(2025, 1, 23, 9, 0, 0, DateTimeKind.Utc),
|
||||
IsClosed = true,
|
||||
Creator = 12,
|
||||
Created = new DateTime(2025, 1, 23, 14, 0, 0, DateTimeKind.Utc),
|
||||
Modified = new DateTime(2025, 1, 23, 15, 30, 0, DateTimeKind.Utc),
|
||||
StockTakingItems = null
|
||||
}
|
||||
};
|
||||
|
||||
var binary = stockTakings.ToBinary();
|
||||
Console.WriteLine($"Binary length for 2 StockTakings: {binary.Length}");
|
||||
|
||||
var result = binary.BinaryTo<List<SimStockTaking>>();
|
||||
|
||||
Assert.IsNotNull(result);
|
||||
Assert.AreEqual(2, result.Count);
|
||||
|
||||
// First item
|
||||
Assert.AreEqual(1, result[0].Id);
|
||||
Assert.AreEqual(6, result[0].Creator, "First item Creator should be 6");
|
||||
Assert.AreEqual(stockTakings[0].Created, result[0].Created,
|
||||
$"First item Created mismatch. Expected: {stockTakings[0].Created}, Got: {result[0].Created}");
|
||||
|
||||
// Second item
|
||||
Assert.AreEqual(2, result[1].Id);
|
||||
Assert.AreEqual(12, result[1].Creator, "Second item Creator should be 12");
|
||||
Assert.AreEqual(stockTakings[1].Created, result[1].Created,
|
||||
$"Second item Created mismatch. Expected: {stockTakings[1].Created}, Got: {result[1].Created}");
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,161 @@
|
|||
using AyCode.Core.Extensions;
|
||||
using static AyCode.Core.Tests.Serialization.AcSerializerModels;
|
||||
|
||||
namespace AyCode.Core.Tests.Serialization;
|
||||
|
||||
/// <summary>
|
||||
/// Tests for generic type parameter handling in serialization.
|
||||
/// </summary>
|
||||
[TestClass]
|
||||
public class AcBinarySerializerGenericTypeTests
|
||||
{
|
||||
/// <summary>
|
||||
/// CRITICAL REGRESSION TEST: Generic type parameter causing metadata mismatch.
|
||||
///
|
||||
/// The bug pattern:
|
||||
/// 1. Parent class uses generic type parameter: GenericParent<TItem> where TItem : IGenericItem
|
||||
/// 2. RegisterMetadataForType uses GetCollectionElementType which returns TItem (the interface/constraint)
|
||||
/// 3. But serialization uses runtime type (GenericItemImpl) which has MORE properties
|
||||
/// 4. Property indices in metadata table don't match what's being serialized
|
||||
/// 5. Deserialization reads wrong property indices ? type mismatch!
|
||||
/// </summary>
|
||||
[TestMethod]
|
||||
public void Deserialize_GenericTypeParameter_RuntimeTypeHasMoreProperties()
|
||||
{
|
||||
var parent = new ConcreteParent
|
||||
{
|
||||
Id = 1,
|
||||
Name = "Parent",
|
||||
Creator = 6,
|
||||
Created = new DateTime(2025, 1, 24, 15, 25, 0, DateTimeKind.Utc),
|
||||
Modified = DateTime.UtcNow,
|
||||
Items = new List<GenericItemImpl>
|
||||
{
|
||||
new()
|
||||
{
|
||||
Id = 10,
|
||||
Name = "Item1",
|
||||
ExtraInt = 100,
|
||||
Created = DateTime.UtcNow.AddHours(-1),
|
||||
Modified = DateTime.UtcNow,
|
||||
Description = "Description1"
|
||||
},
|
||||
new()
|
||||
{
|
||||
Id = 20,
|
||||
Name = "Item2",
|
||||
ExtraInt = 200,
|
||||
Created = DateTime.UtcNow.AddHours(-2),
|
||||
Modified = DateTime.UtcNow,
|
||||
Description = "Description2"
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
var binary = parent.ToBinary();
|
||||
var result = binary.BinaryTo<ConcreteParent>();
|
||||
|
||||
Assert.IsNotNull(result);
|
||||
Assert.AreEqual(1, result.Id);
|
||||
Assert.AreEqual(6, result.Creator, "Creator should be 6");
|
||||
Assert.AreEqual(parent.Created.Ticks, result.Created.Ticks,
|
||||
$"Created mismatch. Expected: {parent.Created}, Got: {result.Created}");
|
||||
|
||||
Assert.IsNotNull(result.Items);
|
||||
Assert.AreEqual(2, result.Items.Count);
|
||||
|
||||
Assert.AreEqual(10, result.Items[0].Id);
|
||||
Assert.AreEqual(100, result.Items[0].ExtraInt, "ExtraInt should be preserved");
|
||||
Assert.AreEqual(parent.Items[0].Created.Ticks, result.Items[0].Created.Ticks,
|
||||
"Item Created should match");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Test with list of generic parents.
|
||||
/// </summary>
|
||||
[TestMethod]
|
||||
public void Deserialize_ListOfGenericParents_AllItemsCorrect()
|
||||
{
|
||||
var parents = Enumerable.Range(1, 5).Select(p => new ConcreteParent
|
||||
{
|
||||
Id = p,
|
||||
Name = $"Parent_{p}",
|
||||
Creator = p,
|
||||
Created = DateTime.UtcNow.AddDays(-p),
|
||||
Modified = DateTime.UtcNow,
|
||||
Items = Enumerable.Range(1, 3).Select(i => new GenericItemImpl
|
||||
{
|
||||
Id = p * 100 + i,
|
||||
Name = $"Item_{p}_{i}",
|
||||
ExtraInt = p * 10 + i,
|
||||
Created = DateTime.UtcNow.AddHours(-i),
|
||||
Modified = DateTime.UtcNow,
|
||||
Description = $"Desc_{p}_{i}"
|
||||
}).ToList()
|
||||
}).ToList();
|
||||
|
||||
var binary = parents.ToBinary();
|
||||
var result = binary.BinaryTo<List<ConcreteParent>>();
|
||||
|
||||
Assert.IsNotNull(result);
|
||||
Assert.AreEqual(5, result.Count);
|
||||
|
||||
for (int p = 0; p < 5; p++)
|
||||
{
|
||||
var original = parents[p];
|
||||
var deserialized = result[p];
|
||||
|
||||
Assert.AreEqual(original.Id, deserialized.Id, $"Parent[{p}].Id mismatch");
|
||||
Assert.AreEqual(original.Creator, deserialized.Creator, $"Parent[{p}].Creator mismatch");
|
||||
Assert.AreEqual(original.Created.Ticks, deserialized.Created.Ticks,
|
||||
$"Parent[{p}].Created mismatch. Expected: {original.Created}, Got: {deserialized.Created}");
|
||||
|
||||
Assert.IsNotNull(deserialized.Items, $"Parent[{p}].Items is null");
|
||||
Assert.AreEqual(3, deserialized.Items.Count);
|
||||
|
||||
for (int i = 0; i < 3; i++)
|
||||
{
|
||||
var origItem = original.Items![i];
|
||||
var deserItem = deserialized.Items[i];
|
||||
|
||||
Assert.AreEqual(origItem.Id, deserItem.Id, $"Parent[{p}].Items[{i}].Id mismatch");
|
||||
Assert.AreEqual(origItem.ExtraInt, deserItem.ExtraInt,
|
||||
$"Parent[{p}].Items[{i}].ExtraInt mismatch");
|
||||
Assert.AreEqual(origItem.Created.Ticks, deserItem.Created.Ticks,
|
||||
$"Parent[{p}].Items[{i}].Created mismatch");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Diagnostic: Check what PropertyType the reflection returns for generic type parameter.
|
||||
/// </summary>
|
||||
[TestMethod]
|
||||
public void Diagnostic_GenericProperty_ReflectionType()
|
||||
{
|
||||
var parentType = typeof(ConcreteParent);
|
||||
var itemsProp = parentType.GetProperty("Items");
|
||||
|
||||
Assert.IsNotNull(itemsProp);
|
||||
|
||||
var propType = itemsProp.PropertyType;
|
||||
Console.WriteLine($"PropertyType: {propType}");
|
||||
Console.WriteLine($"PropertyType.FullName: {propType.FullName}");
|
||||
Console.WriteLine($"IsGenericType: {propType.IsGenericType}");
|
||||
|
||||
if (propType.IsGenericType)
|
||||
{
|
||||
var args = propType.GetGenericArguments();
|
||||
Console.WriteLine($"GenericArguments.Length: {args.Length}");
|
||||
foreach (var arg in args)
|
||||
{
|
||||
Console.WriteLine($" GenericArgument: {arg.FullName}");
|
||||
}
|
||||
}
|
||||
|
||||
Assert.IsTrue(propType.IsGenericType);
|
||||
var elementType = propType.GetGenericArguments()[0];
|
||||
Assert.AreEqual(typeof(GenericItemImpl), elementType,
|
||||
"Element type should be GenericItemImpl, not IGenericItem");
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,370 @@
|
|||
using AyCode.Core.Extensions;
|
||||
using static AyCode.Core.Tests.Serialization.AcSerializerModels;
|
||||
|
||||
namespace AyCode.Core.Tests.Serialization;
|
||||
|
||||
/// <summary>
|
||||
/// Tests for navigation property serialization issues.
|
||||
///
|
||||
/// CRITICAL BUG REPRODUCTION:
|
||||
/// When a navigation property (like StockTakingItem.Product) is populated,
|
||||
/// the serializer writes properties of the navigation target (Product),
|
||||
/// but these property names were NOT registered in the metadata header!
|
||||
///
|
||||
/// The bug pattern:
|
||||
/// 1. RegisterMetadataForType walks List<StockTakingItem> and registers StockTakingItem properties
|
||||
/// 2. StockTakingItem has a "Product" property of type Product - this property NAME is registered
|
||||
/// 3. BUT Product's own properties (Name, Description, Price, CategoryId) are NOT registered!
|
||||
/// 4. When Product is NOT NULL at runtime, WriteObject writes Product's property indices
|
||||
/// 5. GetPropertyNameIndex returns NEW indices that weren't in the header!
|
||||
/// 6. Deserializer reads property indices that don't exist in its table ? crash/type mismatch
|
||||
/// </summary>
|
||||
[TestClass]
|
||||
public class AcBinarySerializerNavigationPropertyTests
|
||||
{
|
||||
/// <summary>
|
||||
/// CRITICAL REGRESSION TEST: Navigation properties causing metadata mismatch.
|
||||
/// This is the EXACT production scenario:
|
||||
/// - StockTakingItem.Product is populated by the database query
|
||||
/// - Product's properties are serialized with wrong indices
|
||||
/// - Deserializer fails with type mismatch
|
||||
/// </summary>
|
||||
[TestMethod]
|
||||
public void Deserialize_NavigationPropertyPopulated_MetadataIncludesNestedType()
|
||||
{
|
||||
var parent = new ParentWithNavigatingItems
|
||||
{
|
||||
Id = 1,
|
||||
Name = "Parent",
|
||||
Creator = 6, // The exact value from production error
|
||||
Created = new DateTime(2025, 1, 24, 15, 25, 0, DateTimeKind.Utc),
|
||||
Modified = DateTime.UtcNow,
|
||||
Items = new List<ItemWithNavigationProperty>
|
||||
{
|
||||
new()
|
||||
{
|
||||
Id = 10,
|
||||
ParentId = 1,
|
||||
ProductId = 100,
|
||||
IsMeasured = true,
|
||||
Quantity = 50,
|
||||
Created = DateTime.UtcNow.AddHours(-1),
|
||||
Modified = DateTime.UtcNow,
|
||||
// Navigation property IS populated - this is the key!
|
||||
Product = new ProductEntity
|
||||
{
|
||||
Id = 100,
|
||||
Name = "TestProduct",
|
||||
Description = "Product description with long text",
|
||||
Price = 99.99,
|
||||
CategoryId = 5,
|
||||
Created = DateTime.UtcNow.AddDays(-30)
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
var binary = parent.ToBinary();
|
||||
var result = binary.BinaryTo<ParentWithNavigatingItems>();
|
||||
|
||||
Assert.IsNotNull(result);
|
||||
Assert.AreEqual(1, result.Id);
|
||||
Assert.AreEqual(6, result.Creator, "Creator should be 6");
|
||||
Assert.AreEqual(parent.Created.Ticks, result.Created.Ticks,
|
||||
$"Created mismatch. Expected: {parent.Created}, Got: {result.Created}");
|
||||
|
||||
Assert.IsNotNull(result.Items);
|
||||
Assert.AreEqual(1, result.Items.Count);
|
||||
|
||||
var item = result.Items[0];
|
||||
Assert.AreEqual(10, item.Id);
|
||||
Assert.AreEqual(100, item.ProductId);
|
||||
|
||||
// Navigation property should be deserialized correctly
|
||||
Assert.IsNotNull(item.Product, "Product navigation property should not be null");
|
||||
Assert.AreEqual(100, item.Product.Id);
|
||||
Assert.AreEqual("TestProduct", item.Product.Name);
|
||||
Assert.AreEqual(5, item.Product.CategoryId);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Test with multiple items, some with Product populated, some without.
|
||||
/// This creates a mixed scenario where some items have navigation properties.
|
||||
/// </summary>
|
||||
[TestMethod]
|
||||
public void Deserialize_MixedNavigationProperties_AllItemsCorrect()
|
||||
{
|
||||
var parent = new ParentWithNavigatingItems
|
||||
{
|
||||
Id = 1,
|
||||
Name = "Parent",
|
||||
Creator = 6,
|
||||
Created = DateTime.UtcNow,
|
||||
Modified = DateTime.UtcNow,
|
||||
Items = Enumerable.Range(1, 5).Select(i => new ItemWithNavigationProperty
|
||||
{
|
||||
Id = i * 10,
|
||||
ParentId = 1,
|
||||
ProductId = 100 + i,
|
||||
IsMeasured = i % 2 == 0,
|
||||
Quantity = i * 10,
|
||||
Created = DateTime.UtcNow.AddHours(-i),
|
||||
Modified = DateTime.UtcNow,
|
||||
// Only populate Product for even items
|
||||
Product = i % 2 == 0 ? new ProductEntity
|
||||
{
|
||||
Id = 100 + i,
|
||||
Name = $"Product_{i}",
|
||||
Description = $"Description for product {i}",
|
||||
Price = i * 10.5,
|
||||
CategoryId = i % 3,
|
||||
Created = DateTime.UtcNow.AddDays(-i)
|
||||
} : null
|
||||
}).ToList()
|
||||
};
|
||||
|
||||
var binary = parent.ToBinary();
|
||||
var result = binary.BinaryTo<ParentWithNavigatingItems>();
|
||||
|
||||
Assert.IsNotNull(result);
|
||||
Assert.AreEqual(1, result.Id);
|
||||
Assert.AreEqual(6, result.Creator);
|
||||
|
||||
Assert.IsNotNull(result.Items);
|
||||
Assert.AreEqual(5, result.Items.Count);
|
||||
|
||||
for (int i = 1; i <= 5; i++)
|
||||
{
|
||||
var item = result.Items[i - 1];
|
||||
Assert.AreEqual(i * 10, item.Id, $"Item {i} Id mismatch");
|
||||
|
||||
if (i % 2 == 0)
|
||||
{
|
||||
Assert.IsNotNull(item.Product, $"Item {i} should have Product");
|
||||
Assert.AreEqual($"Product_{i}", item.Product.Name);
|
||||
}
|
||||
else
|
||||
{
|
||||
Assert.IsNull(item.Product, $"Item {i} should not have Product");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Test with list of parents, each with items with navigation properties.
|
||||
/// This is the exact production scenario - multiple StockTaking entities
|
||||
/// each with StockTakingItems that have Product navigation properties.
|
||||
/// </summary>
|
||||
[TestMethod]
|
||||
public void Deserialize_ListOfParentsWithNavigationProperties_AllCorrect()
|
||||
{
|
||||
var parents = Enumerable.Range(1, 3).Select(p => new ParentWithNavigatingItems
|
||||
{
|
||||
Id = p,
|
||||
Name = $"Parent_{p}",
|
||||
Creator = p,
|
||||
Created = DateTime.UtcNow.AddDays(-p),
|
||||
Modified = DateTime.UtcNow,
|
||||
Items = Enumerable.Range(1, 2).Select(i => new ItemWithNavigationProperty
|
||||
{
|
||||
Id = p * 100 + i,
|
||||
ParentId = p,
|
||||
ProductId = 1000 + i,
|
||||
IsMeasured = true,
|
||||
Quantity = 10 * i,
|
||||
Created = DateTime.UtcNow.AddHours(-i),
|
||||
Modified = DateTime.UtcNow,
|
||||
Product = new ProductEntity
|
||||
{
|
||||
Id = 1000 + i,
|
||||
Name = $"Product_{p}_{i}",
|
||||
Description = $"Description {p}_{i}",
|
||||
Price = (p * 10) + (i * 1.5),
|
||||
CategoryId = i % 3,
|
||||
Created = DateTime.UtcNow.AddDays(-10)
|
||||
}
|
||||
}).ToList()
|
||||
}).ToList();
|
||||
|
||||
var binary = parents.ToBinary();
|
||||
var result = binary.BinaryTo<List<ParentWithNavigatingItems>>();
|
||||
|
||||
Assert.IsNotNull(result);
|
||||
Assert.AreEqual(3, result.Count);
|
||||
|
||||
for (int p = 0; p < 3; p++)
|
||||
{
|
||||
var parent = result[p];
|
||||
Assert.AreEqual(p + 1, parent.Id, $"Parent[{p}].Id mismatch");
|
||||
Assert.AreEqual(p + 1, parent.Creator, $"Parent[{p}].Creator mismatch");
|
||||
|
||||
Assert.IsNotNull(parent.Items);
|
||||
Assert.AreEqual(2, parent.Items.Count);
|
||||
|
||||
for (int i = 0; i < 2; i++)
|
||||
{
|
||||
var item = parent.Items[i];
|
||||
Assert.IsNotNull(item.Product, $"Parent[{p}].Items[{i}].Product should not be null");
|
||||
Assert.AreEqual($"Product_{p + 1}_{i + 1}", item.Product.Name);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Test deeply nested navigation properties.
|
||||
/// Product has a Category, Category has a Parent, etc.
|
||||
/// </summary>
|
||||
[TestMethod]
|
||||
public void Deserialize_DeeplyNestedNavigationProperties_AllCorrect()
|
||||
{
|
||||
// This tests that the serializer correctly handles navigation properties
|
||||
// even when they are deeply nested (Product -> Category -> Parent)
|
||||
var parent = new ParentWithNavigatingItems
|
||||
{
|
||||
Id = 1,
|
||||
Name = "Parent",
|
||||
Creator = 6,
|
||||
Created = DateTime.UtcNow,
|
||||
Modified = DateTime.UtcNow,
|
||||
Items = new List<ItemWithNavigationProperty>
|
||||
{
|
||||
new()
|
||||
{
|
||||
Id = 10,
|
||||
ParentId = 1,
|
||||
ProductId = 100,
|
||||
IsMeasured = true,
|
||||
Quantity = 50,
|
||||
Created = DateTime.UtcNow,
|
||||
Modified = DateTime.UtcNow,
|
||||
Product = new ProductEntity
|
||||
{
|
||||
Id = 100,
|
||||
Name = "ProductWithDetails",
|
||||
Description = "Very long description that should be interned",
|
||||
Price = 123.45,
|
||||
CategoryId = 10,
|
||||
Created = DateTime.UtcNow.AddMonths(-6)
|
||||
}
|
||||
},
|
||||
new()
|
||||
{
|
||||
Id = 20,
|
||||
ParentId = 1,
|
||||
ProductId = 200,
|
||||
IsMeasured = false,
|
||||
Quantity = 25,
|
||||
Created = DateTime.UtcNow,
|
||||
Modified = DateTime.UtcNow,
|
||||
Product = new ProductEntity
|
||||
{
|
||||
Id = 200,
|
||||
Name = "AnotherProduct",
|
||||
Description = "Another description",
|
||||
Price = 67.89,
|
||||
CategoryId = 20,
|
||||
Created = DateTime.UtcNow.AddMonths(-3)
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
var binary = parent.ToBinary();
|
||||
|
||||
// Log binary size for debugging
|
||||
Console.WriteLine($"Binary size: {binary.Length} bytes");
|
||||
|
||||
var result = binary.BinaryTo<ParentWithNavigatingItems>();
|
||||
|
||||
Assert.IsNotNull(result);
|
||||
Assert.AreEqual(1, result.Id);
|
||||
Assert.AreEqual(6, result.Creator);
|
||||
|
||||
Assert.IsNotNull(result.Items);
|
||||
Assert.AreEqual(2, result.Items.Count);
|
||||
|
||||
// First item
|
||||
Assert.AreEqual(10, result.Items[0].Id);
|
||||
Assert.IsNotNull(result.Items[0].Product);
|
||||
Assert.AreEqual("ProductWithDetails", result.Items[0].Product.Name);
|
||||
Assert.AreEqual(123.45, result.Items[0].Product.Price);
|
||||
|
||||
// Second item
|
||||
Assert.AreEqual(20, result.Items[1].Id);
|
||||
Assert.IsNotNull(result.Items[1].Product);
|
||||
Assert.AreEqual("AnotherProduct", result.Items[1].Product.Name);
|
||||
Assert.AreEqual(67.89, result.Items[1].Product.Price);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Test with same Product instance referenced multiple times.
|
||||
/// This tests the reference handling with navigation properties.
|
||||
/// </summary>
|
||||
[TestMethod]
|
||||
public void Deserialize_SharedNavigationProperty_ReferencesPreserved()
|
||||
{
|
||||
// Create a shared Product that is referenced by multiple items
|
||||
var sharedProduct = new ProductEntity
|
||||
{
|
||||
Id = 999,
|
||||
Name = "SharedProduct",
|
||||
Description = "This product is shared across items",
|
||||
Price = 50.00,
|
||||
CategoryId = 1,
|
||||
Created = DateTime.UtcNow.AddYears(-1)
|
||||
};
|
||||
|
||||
var parent = new ParentWithNavigatingItems
|
||||
{
|
||||
Id = 1,
|
||||
Name = "Parent",
|
||||
Creator = 6,
|
||||
Created = DateTime.UtcNow,
|
||||
Modified = DateTime.UtcNow,
|
||||
Items = new List<ItemWithNavigationProperty>
|
||||
{
|
||||
new()
|
||||
{
|
||||
Id = 10,
|
||||
ParentId = 1,
|
||||
ProductId = 999,
|
||||
IsMeasured = true,
|
||||
Quantity = 50,
|
||||
Created = DateTime.UtcNow,
|
||||
Modified = DateTime.UtcNow,
|
||||
Product = sharedProduct // Same reference
|
||||
},
|
||||
new()
|
||||
{
|
||||
Id = 20,
|
||||
ParentId = 1,
|
||||
ProductId = 999,
|
||||
IsMeasured = false,
|
||||
Quantity = 75,
|
||||
Created = DateTime.UtcNow,
|
||||
Modified = DateTime.UtcNow,
|
||||
Product = sharedProduct // Same reference
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
var binary = parent.ToBinary();
|
||||
var result = binary.BinaryTo<ParentWithNavigatingItems>();
|
||||
|
||||
Assert.IsNotNull(result);
|
||||
Assert.IsNotNull(result.Items);
|
||||
Assert.AreEqual(2, result.Items.Count);
|
||||
|
||||
// Both items should have the same Product values
|
||||
Assert.IsNotNull(result.Items[0].Product);
|
||||
Assert.IsNotNull(result.Items[1].Product);
|
||||
Assert.AreEqual(999, result.Items[0].Product.Id);
|
||||
Assert.AreEqual(999, result.Items[1].Product.Id);
|
||||
Assert.AreEqual("SharedProduct", result.Items[0].Product.Name);
|
||||
Assert.AreEqual("SharedProduct", result.Items[1].Product.Name);
|
||||
|
||||
// With reference handling, they should be the same instance
|
||||
// (This depends on UseReferenceHandling being enabled)
|
||||
Console.WriteLine($"Same instance: {ReferenceEquals(result.Items[0].Product, result.Items[1].Product)}");
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,237 @@
|
|||
using AyCode.Core.Extensions;
|
||||
using static AyCode.Core.Tests.Serialization.AcSerializerModels;
|
||||
using static AyCode.Core.Tests.Serialization.AcSerializerTestHelper;
|
||||
|
||||
namespace AyCode.Core.Tests.Serialization;
|
||||
|
||||
/// <summary>
|
||||
/// Tests for nullable value type serialization.
|
||||
/// </summary>
|
||||
[TestClass]
|
||||
public class AcBinarySerializerNullableTests
|
||||
{
|
||||
[TestMethod]
|
||||
public void Deserialize_NullableIntProperty_WithValue_RoundTrip()
|
||||
{
|
||||
var obj = new TestClassWithNullableProperties { Id = 42, NullableInt = 123, NullableIntNull = null };
|
||||
|
||||
var binary = obj.ToBinary();
|
||||
var result = binary.BinaryTo<TestClassWithNullableProperties>();
|
||||
|
||||
Assert.IsNotNull(result);
|
||||
Assert.AreEqual(42, result.Id);
|
||||
Assert.AreEqual(123, result.NullableInt);
|
||||
Assert.IsNull(result.NullableIntNull);
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void Deserialize_NullableDoubleProperty_WithValue_RoundTrip()
|
||||
{
|
||||
var obj = new TestClassWithNullableProperties { Id = 1, NullableDouble = 3.14159, NullableDoubleNull = null };
|
||||
|
||||
var binary = obj.ToBinary();
|
||||
var result = binary.BinaryTo<TestClassWithNullableProperties>();
|
||||
|
||||
Assert.IsNotNull(result);
|
||||
Assert.AreEqual(3.14159, result.NullableDouble);
|
||||
Assert.IsNull(result.NullableDoubleNull);
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void Deserialize_NullableDateTimeProperty_WithValue_RoundTrip()
|
||||
{
|
||||
var testDate = new DateTime(2024, 12, 25, 10, 30, 45, DateTimeKind.Utc);
|
||||
var obj = new TestClassWithNullableProperties { Id = 1, NullableDateTime = testDate, NullableDateTimeNull = null };
|
||||
|
||||
var binary = obj.ToBinary();
|
||||
var result = binary.BinaryTo<TestClassWithNullableProperties>();
|
||||
|
||||
Assert.IsNotNull(result);
|
||||
Assert.AreEqual(testDate, result.NullableDateTime);
|
||||
Assert.IsNull(result.NullableDateTimeNull);
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void Deserialize_NullableGuidProperty_WithValue_RoundTrip()
|
||||
{
|
||||
var testGuid = Guid.NewGuid();
|
||||
var obj = new TestClassWithNullableProperties { Id = 1, NullableGuid = testGuid, NullableGuidNull = null };
|
||||
|
||||
var binary = obj.ToBinary();
|
||||
var result = binary.BinaryTo<TestClassWithNullableProperties>();
|
||||
|
||||
Assert.IsNotNull(result);
|
||||
Assert.AreEqual(testGuid, result.NullableGuid);
|
||||
Assert.IsNull(result.NullableGuidNull);
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void Deserialize_NullableDecimalProperty_WithValue_RoundTrip()
|
||||
{
|
||||
var obj = new TestClassWithNullableProperties { Id = 1, NullableDecimal = 123456.789m, NullableDecimalNull = null };
|
||||
|
||||
var binary = obj.ToBinary();
|
||||
var result = binary.BinaryTo<TestClassWithNullableProperties>();
|
||||
|
||||
Assert.IsNotNull(result);
|
||||
Assert.AreEqual(123456.789m, result.NullableDecimal);
|
||||
Assert.IsNull(result.NullableDecimalNull);
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void Deserialize_NullableBoolProperty_WithValue_RoundTrip()
|
||||
{
|
||||
var obj = new TestClassWithNullableProperties { Id = 1, NullableBool = true, NullableBoolNull = null };
|
||||
|
||||
var binary = obj.ToBinary();
|
||||
var result = binary.BinaryTo<TestClassWithNullableProperties>();
|
||||
|
||||
Assert.IsNotNull(result);
|
||||
Assert.AreEqual(true, result.NullableBool);
|
||||
Assert.IsNull(result.NullableBoolNull);
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void Deserialize_NullableLongProperty_WithValue_RoundTrip()
|
||||
{
|
||||
var obj = new TestClassWithNullableProperties { Id = 1, NullableLong = 9876543210L, NullableLongNull = null };
|
||||
|
||||
var binary = obj.ToBinary();
|
||||
var result = binary.BinaryTo<TestClassWithNullableProperties>();
|
||||
|
||||
Assert.IsNotNull(result);
|
||||
Assert.AreEqual(9876543210L, result.NullableLong);
|
||||
Assert.IsNull(result.NullableLongNull);
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void Deserialize_AllNullablePropertiesWithValues_RoundTrip()
|
||||
{
|
||||
var testDate = new DateTime(2024, 6, 15, 14, 30, 0, DateTimeKind.Utc);
|
||||
var testGuid = Guid.Parse("12345678-1234-1234-1234-123456789abc");
|
||||
|
||||
var obj = new TestClassWithNullableProperties
|
||||
{
|
||||
Id = 999,
|
||||
NullableInt = int.MaxValue,
|
||||
NullableIntNull = 42,
|
||||
NullableLong = long.MaxValue,
|
||||
NullableLongNull = 100L,
|
||||
NullableDouble = double.MaxValue,
|
||||
NullableDoubleNull = 2.5,
|
||||
NullableDecimal = decimal.MaxValue,
|
||||
NullableDecimalNull = 1.1m,
|
||||
NullableDateTime = testDate,
|
||||
NullableDateTimeNull = DateTime.UtcNow,
|
||||
NullableGuid = testGuid,
|
||||
NullableGuidNull = Guid.NewGuid(),
|
||||
NullableBool = false,
|
||||
NullableBoolNull = true
|
||||
};
|
||||
|
||||
var binary = obj.ToBinary();
|
||||
var result = binary.BinaryTo<TestClassWithNullableProperties>();
|
||||
|
||||
Assert.IsNotNull(result);
|
||||
Assert.AreEqual(obj.Id, result.Id);
|
||||
Assert.AreEqual(obj.NullableInt, result.NullableInt);
|
||||
Assert.AreEqual(obj.NullableIntNull, result.NullableIntNull);
|
||||
Assert.AreEqual(obj.NullableLong, result.NullableLong);
|
||||
Assert.AreEqual(obj.NullableLongNull, result.NullableLongNull);
|
||||
Assert.AreEqual(obj.NullableDouble, result.NullableDouble);
|
||||
Assert.AreEqual(obj.NullableDecimalNull, result.NullableDecimalNull);
|
||||
Assert.AreEqual(obj.NullableDateTime, result.NullableDateTime);
|
||||
Assert.AreEqual(obj.NullableGuid, result.NullableGuid);
|
||||
Assert.AreEqual(obj.NullableBool, result.NullableBool);
|
||||
Assert.AreEqual(obj.NullableBoolNull, result.NullableBoolNull);
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void Deserialize_ObjectWithNestedNullableProperties_RoundTrip()
|
||||
{
|
||||
var obj = new TestParentWithNullableChild
|
||||
{
|
||||
Id = 1,
|
||||
Name = "Parent",
|
||||
Child = new TestClassWithNullableProperties
|
||||
{
|
||||
Id = 2,
|
||||
NullableInt = 100,
|
||||
NullableDouble = 5.5,
|
||||
NullableDateTime = DateTime.UtcNow,
|
||||
NullableGuid = Guid.NewGuid()
|
||||
}
|
||||
};
|
||||
|
||||
var binary = obj.ToBinary();
|
||||
var result = binary.BinaryTo<TestParentWithNullableChild>();
|
||||
|
||||
Assert.IsNotNull(result);
|
||||
Assert.AreEqual(obj.Id, result.Id);
|
||||
Assert.AreEqual(obj.Name, result.Name);
|
||||
Assert.IsNotNull(result.Child);
|
||||
Assert.AreEqual(obj.Child.NullableInt, result.Child.NullableInt);
|
||||
Assert.AreEqual(obj.Child.NullableDouble, result.Child.NullableDouble);
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void Deserialize_ListOfObjectsWithNullableProperties_RoundTrip()
|
||||
{
|
||||
var items = CreateNullablePropertyItems(10);
|
||||
var binary = items.ToBinary();
|
||||
var result = binary.BinaryTo<List<TestClassWithNullableProperties>>();
|
||||
|
||||
Assert.IsNotNull(result);
|
||||
Assert.AreEqual(10, result.Count);
|
||||
|
||||
for (int i = 0; i < 10; i++)
|
||||
{
|
||||
var original = items[i];
|
||||
var deserialized = result[i];
|
||||
|
||||
Assert.AreEqual(original.Id, deserialized.Id, $"Id mismatch at index {i}");
|
||||
Assert.AreEqual(original.NullableInt, deserialized.NullableInt, $"NullableInt mismatch at index {i}");
|
||||
Assert.AreEqual(original.NullableDouble, deserialized.NullableDouble, $"NullableDouble mismatch at index {i}");
|
||||
Assert.AreEqual(original.NullableGuid, deserialized.NullableGuid, $"NullableGuid mismatch at index {i}");
|
||||
}
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void Deserialize_StockTakingLikeHierarchy_WithNullableProperties_RoundTrip()
|
||||
{
|
||||
var stockTaking = CreateStockTaking(2, 2);
|
||||
|
||||
var binary = stockTaking.ToBinary();
|
||||
var result = binary.BinaryTo<TestStockTaking>();
|
||||
|
||||
Assert.IsNotNull(result);
|
||||
Assert.AreEqual(stockTaking.Id, result.Id);
|
||||
Assert.AreEqual(stockTaking.IsClosed, result.IsClosed);
|
||||
Assert.AreEqual(stockTaking.Creator, result.Creator);
|
||||
|
||||
Assert.IsNotNull(result.StockTakingItems);
|
||||
Assert.AreEqual(2, result.StockTakingItems.Count);
|
||||
|
||||
var item0 = result.StockTakingItems[0];
|
||||
Assert.IsNotNull(item0.StockTakingItemPallets);
|
||||
Assert.AreEqual(2, item0.StockTakingItemPallets.Count);
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void Deserialize_ListOfStockTakingLikeEntities_RoundTrip()
|
||||
{
|
||||
var stockTakings = CreateStockTakingList(2, 1, 1);
|
||||
var binary = stockTakings.ToBinary();
|
||||
var result = binary.BinaryTo<List<TestStockTaking>>();
|
||||
|
||||
Assert.IsNotNull(result);
|
||||
Assert.AreEqual(2, result.Count);
|
||||
|
||||
Assert.AreEqual(1, result[0].Id);
|
||||
Assert.IsNotNull(result[0].StockTakingItems);
|
||||
Assert.AreEqual(1, result[0].StockTakingItems.Count);
|
||||
|
||||
Assert.AreEqual(2, result[1].Id);
|
||||
Assert.IsNotNull(result[1].StockTakingItems);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,130 @@
|
|||
using AyCode.Core.Extensions;
|
||||
using static AyCode.Core.Tests.Serialization.AcSerializerModels;
|
||||
|
||||
namespace AyCode.Core.Tests.Serialization;
|
||||
|
||||
/// <summary>
|
||||
/// Tests for object serialization including nested objects, lists, and dictionaries.
|
||||
/// </summary>
|
||||
[TestClass]
|
||||
public class AcBinarySerializerObjectTests
|
||||
{
|
||||
[TestMethod]
|
||||
public void Serialize_SimpleObject_RoundTrip()
|
||||
{
|
||||
var obj = new TestSimpleClass { Id = 42, Name = "Test Object", Value = 3.14, IsActive = true };
|
||||
|
||||
var binary = obj.ToBinary();
|
||||
var result = binary.BinaryTo<TestSimpleClass>();
|
||||
|
||||
Assert.IsNotNull(result);
|
||||
Assert.AreEqual(obj.Id, result.Id);
|
||||
Assert.AreEqual(obj.Name, result.Name);
|
||||
Assert.AreEqual(obj.Value, result.Value);
|
||||
Assert.AreEqual(obj.IsActive, result.IsActive);
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void Serialize_NestedObject_RoundTrip()
|
||||
{
|
||||
var obj = new TestNestedClass
|
||||
{
|
||||
Id = 1,
|
||||
Name = "Parent",
|
||||
Child = new TestSimpleClass { Id = 2, Name = "Child", Value = 2.5, IsActive = true }
|
||||
};
|
||||
|
||||
var binary = obj.ToBinary();
|
||||
var result = binary.BinaryTo<TestNestedClass>();
|
||||
|
||||
Assert.IsNotNull(result);
|
||||
Assert.AreEqual(obj.Id, result.Id);
|
||||
Assert.AreEqual(obj.Name, result.Name);
|
||||
Assert.IsNotNull(result.Child);
|
||||
Assert.AreEqual(obj.Child.Id, result.Child.Id);
|
||||
Assert.AreEqual(obj.Child.Name, result.Child.Name);
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void Serialize_List_RoundTrip()
|
||||
{
|
||||
var list = new List<int> { 1, 2, 3, 4, 5 };
|
||||
var binary = list.ToBinary();
|
||||
var result = binary.BinaryTo<List<int>>();
|
||||
|
||||
Assert.IsNotNull(result);
|
||||
CollectionAssert.AreEqual(list, result);
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void Serialize_ObjectWithList_RoundTrip()
|
||||
{
|
||||
var obj = new TestClassWithList { Id = 1, Items = ["Item1", "Item2", "Item3"] };
|
||||
|
||||
var binary = obj.ToBinary();
|
||||
var result = binary.BinaryTo<TestClassWithList>();
|
||||
|
||||
Assert.IsNotNull(result);
|
||||
Assert.AreEqual(obj.Id, result.Id);
|
||||
Assert.IsNotNull(result.Items);
|
||||
CollectionAssert.AreEqual(obj.Items, result.Items);
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void Serialize_Dictionary_RoundTrip()
|
||||
{
|
||||
var dict = new Dictionary<string, int> { ["one"] = 1, ["two"] = 2, ["three"] = 3 };
|
||||
|
||||
var binary = dict.ToBinary();
|
||||
var result = binary.BinaryTo<Dictionary<string, int>>();
|
||||
|
||||
Assert.IsNotNull(result);
|
||||
Assert.AreEqual(dict.Count, result.Count);
|
||||
foreach (var kvp in dict)
|
||||
{
|
||||
Assert.IsTrue(result.ContainsKey(kvp.Key));
|
||||
Assert.AreEqual(kvp.Value, result[kvp.Key]);
|
||||
}
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void Populate_UpdatesExistingObject()
|
||||
{
|
||||
var target = new TestSimpleClass { Id = 0, Name = "Original" };
|
||||
var source = new TestSimpleClass { Id = 42, Name = "Updated", Value = 3.14 };
|
||||
|
||||
var binary = source.ToBinary();
|
||||
binary.BinaryTo(target);
|
||||
|
||||
Assert.AreEqual(42, target.Id);
|
||||
Assert.AreEqual("Updated", target.Name);
|
||||
Assert.AreEqual(3.14, target.Value);
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void PopulateMerge_MergesNestedObjects()
|
||||
{
|
||||
var target = new TestNestedClass
|
||||
{
|
||||
Id = 1,
|
||||
Name = "Original",
|
||||
Child = new TestSimpleClass { Id = 10, Name = "OriginalChild", Value = 1.0 }
|
||||
};
|
||||
|
||||
var source = new TestNestedClass
|
||||
{
|
||||
Id = 2,
|
||||
Name = "Updated",
|
||||
Child = new TestSimpleClass { Id = 20, Name = "UpdatedChild", Value = 2.0 }
|
||||
};
|
||||
|
||||
var binary = source.ToBinary();
|
||||
binary.BinaryToMerge(target);
|
||||
|
||||
Assert.AreEqual(2, target.Id);
|
||||
Assert.AreEqual("Updated", target.Name);
|
||||
Assert.IsNotNull(target.Child);
|
||||
Assert.AreEqual(20, target.Child.Id);
|
||||
Assert.AreEqual("UpdatedChild", target.Child.Name);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,289 @@
|
|||
using AyCode.Core.Extensions;
|
||||
using AyCode.Core.Serializers.Binaries;
|
||||
using static AyCode.Core.Tests.Serialization.AcSerializerModels;
|
||||
using static AyCode.Core.Tests.Serialization.AcSerializerTestHelper;
|
||||
|
||||
namespace AyCode.Core.Tests.Serialization;
|
||||
|
||||
/// <summary>
|
||||
/// Tests for string interning functionality.
|
||||
/// </summary>
|
||||
[TestClass]
|
||||
public class AcBinarySerializerStringInterningTests
|
||||
{
|
||||
[TestMethod]
|
||||
public void Serialize_RepeatedStrings_UsesInterning()
|
||||
{
|
||||
var obj = new TestClassWithRepeatedStrings
|
||||
{
|
||||
Field1 = "Repeated",
|
||||
Field2 = "Repeated",
|
||||
Field3 = "Repeated",
|
||||
Field4 = "Unique"
|
||||
};
|
||||
|
||||
var binaryWithInterning = AcBinarySerializer.Serialize(obj, AcBinarySerializerOptions.Default);
|
||||
var binaryWithoutInterning = AcBinarySerializer.Serialize(obj,
|
||||
new AcBinarySerializerOptions { UseStringInterning = false });
|
||||
|
||||
Assert.IsTrue(binaryWithInterning.Length < binaryWithoutInterning.Length,
|
||||
$"With interning: {binaryWithInterning.Length}, Without: {binaryWithoutInterning.Length}");
|
||||
|
||||
var result1 = AcBinaryDeserializer.Deserialize<TestClassWithRepeatedStrings>(binaryWithInterning);
|
||||
var result2 = AcBinaryDeserializer.Deserialize<TestClassWithRepeatedStrings>(binaryWithoutInterning);
|
||||
|
||||
Assert.AreEqual(obj.Field1, result1!.Field1);
|
||||
Assert.AreEqual(obj.Field1, result2!.Field1);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// REGRESSION TEST: Comprehensive string interning edge cases.
|
||||
/// </summary>
|
||||
[TestMethod]
|
||||
public void StringInterning_PropertyNames_MustBeRegisteredDuringDeserialization()
|
||||
{
|
||||
var items = Enumerable.Range(0, 10).Select(i => new TestClassWithLongPropertyNames
|
||||
{
|
||||
FirstProperty = $"Value1_{i}",
|
||||
SecondProperty = $"Value2_{i}",
|
||||
ThirdProperty = $"Value3_{i}",
|
||||
FourthProperty = $"Value4_{i}"
|
||||
}).ToList();
|
||||
|
||||
var binary = items.ToBinary();
|
||||
var result = binary.BinaryTo<List<TestClassWithLongPropertyNames>>();
|
||||
|
||||
Assert.IsNotNull(result);
|
||||
Assert.AreEqual(10, result.Count);
|
||||
for (int i = 0; i < 10; i++)
|
||||
{
|
||||
Assert.AreEqual($"Value1_{i}", result[i].FirstProperty);
|
||||
Assert.AreEqual($"Value2_{i}", result[i].SecondProperty);
|
||||
}
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void StringInterning_MixedShortAndLongStrings_HandledCorrectly()
|
||||
{
|
||||
var items = Enumerable.Range(0, 20).Select(i => new TestClassWithMixedStrings
|
||||
{
|
||||
Id = i,
|
||||
ShortName = $"A{i % 3}",
|
||||
LongName = $"LongName_{i % 5}",
|
||||
Description = $"Description_value_{i % 7}",
|
||||
Tag = i % 2 == 0 ? "AB" : "XY"
|
||||
}).ToList();
|
||||
|
||||
var binary = items.ToBinary();
|
||||
var result = binary.BinaryTo<List<TestClassWithMixedStrings>>();
|
||||
|
||||
Assert.IsNotNull(result);
|
||||
Assert.AreEqual(20, result.Count);
|
||||
|
||||
for (int i = 0; i < 20; i++)
|
||||
{
|
||||
Assert.AreEqual(i, result[i].Id);
|
||||
Assert.AreEqual($"A{i % 3}", result[i].ShortName);
|
||||
Assert.AreEqual($"LongName_{i % 5}", result[i].LongName);
|
||||
Assert.AreEqual($"Description_value_{i % 7}", result[i].Description);
|
||||
}
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void StringInterning_DeeplyNestedObjects_MaintainsCorrectOrder()
|
||||
{
|
||||
var root = new TestNestedStructure
|
||||
{
|
||||
RootName = "RootObject",
|
||||
Level1Items = Enumerable.Range(0, 5).Select(i => new TestLevel1
|
||||
{
|
||||
Level1Name = $"Level1_{i}",
|
||||
Level2Items = Enumerable.Range(0, 3).Select(j => new TestLevel2
|
||||
{
|
||||
Level2Name = $"Level2_{i}_{j}",
|
||||
Value = $"Value_{i * 3 + j}"
|
||||
}).ToList()
|
||||
}).ToList()
|
||||
};
|
||||
|
||||
var binary = root.ToBinary();
|
||||
var result = binary.BinaryTo<TestNestedStructure>();
|
||||
|
||||
Assert.IsNotNull(result);
|
||||
Assert.AreEqual("RootObject", result.RootName);
|
||||
Assert.AreEqual(5, result.Level1Items.Count);
|
||||
|
||||
for (int i = 0; i < 5; i++)
|
||||
{
|
||||
Assert.AreEqual($"Level1_{i}", result.Level1Items[i].Level1Name);
|
||||
Assert.AreEqual(3, result.Level1Items[i].Level2Items.Count);
|
||||
|
||||
for (int j = 0; j < 3; j++)
|
||||
{
|
||||
Assert.AreEqual($"Level2_{i}_{j}", result.Level1Items[i].Level2Items[j].Level2Name);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void StringInterning_RepeatedPropertyValuesAcrossObjects_UsesReferences()
|
||||
{
|
||||
var items = Enumerable.Range(0, 50).Select(i => new TestClassWithRepeatedValues
|
||||
{
|
||||
Id = i,
|
||||
Status = i % 3 == 0 ? "Pending" : i % 3 == 1 ? "Processing" : "Completed",
|
||||
Category = i % 5 == 0 ? "CategoryA" : i % 5 == 1 ? "CategoryB" : "CategoryC",
|
||||
Priority = i % 2 == 0 ? "High" : "Low_Priority_Value"
|
||||
}).ToList();
|
||||
|
||||
var binary = items.ToBinary();
|
||||
var result = binary.BinaryTo<List<TestClassWithRepeatedValues>>();
|
||||
|
||||
Assert.IsNotNull(result);
|
||||
Assert.AreEqual(50, result.Count);
|
||||
|
||||
for (int i = 0; i < 50; i++)
|
||||
{
|
||||
Assert.AreEqual(i, result[i].Id);
|
||||
var expectedStatus = i % 3 == 0 ? "Pending" : i % 3 == 1 ? "Processing" : "Completed";
|
||||
Assert.AreEqual(expectedStatus, result[i].Status, $"Status mismatch at index {i}");
|
||||
}
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void StringInterning_ManyUniqueStringsFollowedByRepeats_CorrectIndexLookup()
|
||||
{
|
||||
var items = new List<TestClassWithNameValue>();
|
||||
|
||||
for (int i = 0; i < 30; i++)
|
||||
{
|
||||
items.Add(new TestClassWithNameValue
|
||||
{
|
||||
Name = $"UniqueName_{i:D4}",
|
||||
Value = $"UniqueValue_{i:D4}"
|
||||
});
|
||||
}
|
||||
|
||||
for (int i = 0; i < 20; i++)
|
||||
{
|
||||
items.Add(new TestClassWithNameValue
|
||||
{
|
||||
Name = $"UniqueName_{i % 10:D4}",
|
||||
Value = $"UniqueValue_{(i + 10) % 30:D4}"
|
||||
});
|
||||
}
|
||||
|
||||
var binary = items.ToBinary();
|
||||
var result = binary.BinaryTo<List<TestClassWithNameValue>>();
|
||||
|
||||
Assert.IsNotNull(result);
|
||||
Assert.AreEqual(50, result.Count);
|
||||
|
||||
for (int i = 0; i < 30; i++)
|
||||
{
|
||||
Assert.AreEqual($"UniqueName_{i:D4}", result[i].Name, $"Name mismatch at index {i}");
|
||||
Assert.AreEqual($"UniqueValue_{i:D4}", result[i].Value, $"Value mismatch at index {i}");
|
||||
}
|
||||
|
||||
for (int i = 0; i < 20; i++)
|
||||
{
|
||||
Assert.AreEqual($"UniqueName_{i % 10:D4}", result[30 + i].Name, $"Name mismatch at index {30 + i}");
|
||||
}
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void StringInterning_EmptyAndNullStrings_DoNotAffectInternTable()
|
||||
{
|
||||
var items = new List<TestClassWithNullableStrings>();
|
||||
|
||||
for (int i = 0; i < 25; i++)
|
||||
{
|
||||
items.Add(new TestClassWithNullableStrings
|
||||
{
|
||||
Id = i,
|
||||
RequiredName = $"Required_{i:D3}",
|
||||
OptionalName = i % 3 == 0 ? null : i % 3 == 1 ? "" : $"Optional_{i:D3}",
|
||||
Description = i % 2 == 0 ? $"Description_{i % 5:D3}" : null
|
||||
});
|
||||
}
|
||||
|
||||
var binary = items.ToBinary();
|
||||
var result = binary.BinaryTo<List<TestClassWithNullableStrings>>();
|
||||
|
||||
Assert.IsNotNull(result);
|
||||
Assert.AreEqual(25, result.Count);
|
||||
|
||||
for (int i = 0; i < 25; i++)
|
||||
{
|
||||
Assert.AreEqual(i, result[i].Id);
|
||||
Assert.AreEqual($"Required_{i:D3}", result[i].RequiredName);
|
||||
|
||||
if (i % 3 == 0)
|
||||
{
|
||||
Assert.IsNull(result[i].OptionalName, $"OptionalName at index {i} should be null");
|
||||
}
|
||||
else if (i % 3 == 1)
|
||||
{
|
||||
Assert.IsTrue(string.IsNullOrEmpty(result[i].OptionalName),
|
||||
$"OptionalName at index {i} should be null or empty, got: '{result[i].OptionalName}'");
|
||||
}
|
||||
else
|
||||
{
|
||||
Assert.AreEqual($"Optional_{i:D3}", result[i].OptionalName,
|
||||
$"OptionalName at index {i} mismatch");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void StringInterning_ProductionLikeCustomerDto_RoundTrip()
|
||||
{
|
||||
var customers = CreateCustomerLikeItems(25);
|
||||
var binary = customers.ToBinary();
|
||||
var result = binary.BinaryTo<List<TestCustomerLikeDto>>();
|
||||
|
||||
Assert.IsNotNull(result, "Result should not be null - deserialization failed");
|
||||
Assert.AreEqual(25, result.Count);
|
||||
|
||||
for (int i = 0; i < 25; i++)
|
||||
{
|
||||
Assert.AreEqual(i, result[i].Id, $"Id mismatch at index {i}");
|
||||
Assert.AreEqual($"FirstName_{i % 10}", result[i].FirstName, $"FirstName mismatch at index {i}");
|
||||
Assert.AreEqual($"LastName_{i % 8}", result[i].LastName, $"LastName mismatch at index {i}");
|
||||
Assert.AreEqual($"user{i}@example.com", result[i].Email, $"Email mismatch at index {i}");
|
||||
Assert.AreEqual(4, result[i].Attributes.Count, $"Attributes count mismatch at index {i}");
|
||||
Assert.AreEqual("DateOfReceipt", result[i].Attributes[0].Key);
|
||||
Assert.AreEqual("Priority", result[i].Attributes[1].Key);
|
||||
}
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void StringInterning_LargeListWithHighStringReuse_HandlesCorrectly()
|
||||
{
|
||||
const int itemCount = 150;
|
||||
var items = Enumerable.Range(0, itemCount).Select(i => new TestHighReuseDto
|
||||
{
|
||||
Id = i,
|
||||
CategoryCode = $"CAT_{i % 10:D2}",
|
||||
StatusCode = $"STATUS_{i % 5:D2}",
|
||||
TypeCode = $"TYPE_{i % 3:D2}",
|
||||
PriorityCode = i % 2 == 0 ? "PRIORITY_HIGH" : "PRIORITY_LOW",
|
||||
UniqueField = $"UNIQUE_{i:D4}"
|
||||
}).ToList();
|
||||
|
||||
var binary = items.ToBinary();
|
||||
var result = binary.BinaryTo<List<TestHighReuseDto>>();
|
||||
|
||||
Assert.IsNotNull(result, "Result should not be null");
|
||||
Assert.AreEqual(itemCount, result.Count, $"Expected {itemCount} items");
|
||||
|
||||
for (int i = 0; i < itemCount; i++)
|
||||
{
|
||||
Assert.AreEqual(i, result[i].Id, $"Id mismatch at {i}");
|
||||
Assert.AreEqual($"CAT_{i % 10:D2}", result[i].CategoryCode, $"CategoryCode mismatch at {i}");
|
||||
Assert.AreEqual($"STATUS_{i % 5:D2}", result[i].StatusCode, $"StatusCode mismatch at {i}");
|
||||
Assert.AreEqual($"TYPE_{i % 3:D2}", result[i].TypeCode, $"TypeCode mismatch at {i}");
|
||||
Assert.AreEqual($"UNIQUE_{i:D4}", result[i].UniqueField, $"UniqueField mismatch at {i}");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,789 +0,0 @@
|
|||
using AyCode.Core.Extensions;
|
||||
using AyCode.Core.Serializers.Binaries;
|
||||
using AyCode.Core.Tests.TestModels;
|
||||
|
||||
namespace AyCode.Core.Tests.serialization;
|
||||
|
||||
[TestClass]
|
||||
public class AcBinarySerializerTests
|
||||
{
|
||||
#region Basic Serialization Tests
|
||||
|
||||
[TestMethod]
|
||||
public void Serialize_Null_ReturnsSingleNullByte()
|
||||
{
|
||||
var result = AcBinarySerializer.Serialize<object?>(null);
|
||||
Assert.AreEqual(1, result.Length);
|
||||
Assert.AreEqual((byte)0, result[0]); // BinaryTypeCode.Null = 0
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void Serialize_Int32_RoundTrip()
|
||||
{
|
||||
var value = 12345;
|
||||
var binary = AcBinarySerializer.Serialize(value);
|
||||
var result = AcBinaryDeserializer.Deserialize<int>(binary);
|
||||
Assert.AreEqual(value, result);
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void Serialize_Int64_RoundTrip()
|
||||
{
|
||||
var value = 123456789012345L;
|
||||
var binary = AcBinarySerializer.Serialize(value);
|
||||
var result = AcBinaryDeserializer.Deserialize<long>(binary);
|
||||
Assert.AreEqual(value, result);
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void Serialize_Double_RoundTrip()
|
||||
{
|
||||
var value = 3.14159265358979;
|
||||
var binary = AcBinarySerializer.Serialize(value);
|
||||
var result = AcBinaryDeserializer.Deserialize<double>(binary);
|
||||
Assert.AreEqual(value, result);
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void Serialize_String_RoundTrip()
|
||||
{
|
||||
var value = "Hello, Binary World!";
|
||||
var binary = AcBinarySerializer.Serialize(value);
|
||||
var result = AcBinaryDeserializer.Deserialize<string>(binary);
|
||||
Assert.AreEqual(value, result);
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void Serialize_Boolean_RoundTrip()
|
||||
{
|
||||
var trueResult = AcBinaryDeserializer.Deserialize<bool>(AcBinarySerializer.Serialize(true));
|
||||
var falseResult = AcBinaryDeserializer.Deserialize<bool>(AcBinarySerializer.Serialize(false));
|
||||
Assert.IsTrue(trueResult);
|
||||
Assert.IsFalse(falseResult);
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void Serialize_DateTime_RoundTrip()
|
||||
{
|
||||
var value = new DateTime(2024, 12, 25, 10, 30, 45, DateTimeKind.Utc);
|
||||
var binary = AcBinarySerializer.Serialize(value);
|
||||
var result = AcBinaryDeserializer.Deserialize<DateTime>(binary);
|
||||
Assert.AreEqual(value, result);
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
[DataRow(DateTimeKind.Unspecified)]
|
||||
[DataRow(DateTimeKind.Utc)]
|
||||
[DataRow(DateTimeKind.Local)]
|
||||
public void Serialize_DateTime_PreservesKind(DateTimeKind kind)
|
||||
{
|
||||
var value = new DateTime(2024, 12, 25, 10, 30, 45, kind);
|
||||
var binary = AcBinarySerializer.Serialize(value);
|
||||
var result = AcBinaryDeserializer.Deserialize<DateTime>(binary);
|
||||
|
||||
Assert.AreEqual(value.Ticks, result.Ticks);
|
||||
Assert.AreEqual(value.Kind, result.Kind);
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void Serialize_Guid_RoundTrip()
|
||||
{
|
||||
var value = Guid.NewGuid();
|
||||
var binary = AcBinarySerializer.Serialize(value);
|
||||
var result = AcBinaryDeserializer.Deserialize<Guid>(binary);
|
||||
Assert.AreEqual(value, result);
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void Serialize_Decimal_RoundTrip()
|
||||
{
|
||||
var value = 123456.789012m;
|
||||
var binary = AcBinarySerializer.Serialize(value);
|
||||
var result = AcBinaryDeserializer.Deserialize<decimal>(binary);
|
||||
Assert.AreEqual(value, result);
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void Serialize_TimeSpan_RoundTrip()
|
||||
{
|
||||
var value = TimeSpan.FromHours(2.5);
|
||||
var binary = AcBinarySerializer.Serialize(value);
|
||||
var result = AcBinaryDeserializer.Deserialize<TimeSpan>(binary);
|
||||
Assert.AreEqual(value, result);
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void Serialize_DateTimeOffset_RoundTrip()
|
||||
{
|
||||
var value = new DateTimeOffset(2024, 12, 25, 10, 30, 45, TimeSpan.FromHours(2));
|
||||
var binary = AcBinarySerializer.Serialize(value);
|
||||
var result = AcBinaryDeserializer.Deserialize<DateTimeOffset>(binary);
|
||||
|
||||
// Compare UTC ticks and offset separately since we store UTC ticks
|
||||
Assert.AreEqual(value.UtcTicks, result.UtcTicks);
|
||||
Assert.AreEqual(value.Offset, result.Offset);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Object Serialization Tests
|
||||
|
||||
[TestMethod]
|
||||
public void Serialize_SimpleObject_RoundTrip()
|
||||
{
|
||||
var obj = new TestSimpleClass
|
||||
{
|
||||
Id = 42,
|
||||
Name = "Test Object",
|
||||
Value = 3.14,
|
||||
IsActive = true
|
||||
};
|
||||
|
||||
var binary = obj.ToBinary();
|
||||
var result = binary.BinaryTo<TestSimpleClass>();
|
||||
|
||||
Assert.IsNotNull(result);
|
||||
Assert.AreEqual(obj.Id, result.Id);
|
||||
Assert.AreEqual(obj.Name, result.Name);
|
||||
Assert.AreEqual(obj.Value, result.Value);
|
||||
Assert.AreEqual(obj.IsActive, result.IsActive);
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void Serialize_NestedObject_RoundTrip()
|
||||
{
|
||||
var obj = new TestNestedClass
|
||||
{
|
||||
Id = 1,
|
||||
Name = "Parent",
|
||||
Child = new TestSimpleClass
|
||||
{
|
||||
Id = 2,
|
||||
Name = "Child",
|
||||
Value = 2.5,
|
||||
IsActive = true
|
||||
}
|
||||
};
|
||||
|
||||
var binary = obj.ToBinary();
|
||||
var result = binary.BinaryTo<TestNestedClass>();
|
||||
|
||||
Assert.IsNotNull(result);
|
||||
Assert.AreEqual(obj.Id, result.Id);
|
||||
Assert.AreEqual(obj.Name, result.Name);
|
||||
Assert.IsNotNull(result.Child);
|
||||
Assert.AreEqual(obj.Child.Id, result.Child.Id);
|
||||
Assert.AreEqual(obj.Child.Name, result.Child.Name);
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void Serialize_List_RoundTrip()
|
||||
{
|
||||
var list = new List<int> { 1, 2, 3, 4, 5 };
|
||||
var binary = list.ToBinary();
|
||||
var result = binary.BinaryTo<List<int>>();
|
||||
|
||||
Assert.IsNotNull(result);
|
||||
CollectionAssert.AreEqual(list, result);
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void Serialize_ObjectWithList_RoundTrip()
|
||||
{
|
||||
var obj = new TestClassWithList
|
||||
{
|
||||
Id = 1,
|
||||
Items = new List<string> { "Item1", "Item2", "Item3" }
|
||||
};
|
||||
|
||||
var binary = obj.ToBinary();
|
||||
var result = binary.BinaryTo<TestClassWithList>();
|
||||
|
||||
Assert.IsNotNull(result);
|
||||
Assert.AreEqual(obj.Id, result.Id);
|
||||
Assert.IsNotNull(result.Items);
|
||||
CollectionAssert.AreEqual(obj.Items, result.Items);
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void Serialize_Dictionary_RoundTrip()
|
||||
{
|
||||
var dict = new Dictionary<string, int>
|
||||
{
|
||||
["one"] = 1,
|
||||
["two"] = 2,
|
||||
["three"] = 3
|
||||
};
|
||||
|
||||
var binary = dict.ToBinary();
|
||||
var result = binary.BinaryTo<Dictionary<string, int>>();
|
||||
|
||||
Assert.IsNotNull(result);
|
||||
Assert.AreEqual(dict.Count, result.Count);
|
||||
foreach (var kvp in dict)
|
||||
{
|
||||
Assert.IsTrue(result.ContainsKey(kvp.Key));
|
||||
Assert.AreEqual(kvp.Value, result[kvp.Key]);
|
||||
}
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Populate Tests
|
||||
|
||||
[TestMethod]
|
||||
public void Populate_UpdatesExistingObject()
|
||||
{
|
||||
var target = new TestSimpleClass { Id = 0, Name = "Original" };
|
||||
var source = new TestSimpleClass { Id = 42, Name = "Updated", Value = 3.14 };
|
||||
|
||||
var binary = source.ToBinary();
|
||||
binary.BinaryTo(target);
|
||||
|
||||
Assert.AreEqual(42, target.Id);
|
||||
Assert.AreEqual("Updated", target.Name);
|
||||
Assert.AreEqual(3.14, target.Value);
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void PopulateMerge_MergesNestedObjects()
|
||||
{
|
||||
var target = new TestNestedClass
|
||||
{
|
||||
Id = 1,
|
||||
Name = "Original",
|
||||
Child = new TestSimpleClass { Id = 10, Name = "OriginalChild", Value = 1.0 }
|
||||
};
|
||||
|
||||
var source = new TestNestedClass
|
||||
{
|
||||
Id = 2,
|
||||
Name = "Updated",
|
||||
Child = new TestSimpleClass { Id = 20, Name = "UpdatedChild", Value = 2.0 }
|
||||
};
|
||||
|
||||
var binary = source.ToBinary();
|
||||
binary.BinaryToMerge(target);
|
||||
|
||||
Assert.AreEqual(2, target.Id);
|
||||
Assert.AreEqual("Updated", target.Name);
|
||||
Assert.IsNotNull(target.Child);
|
||||
// Child object should be merged, not replaced
|
||||
Assert.AreEqual(20, target.Child.Id);
|
||||
Assert.AreEqual("UpdatedChild", target.Child.Name);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region String Interning Tests
|
||||
|
||||
[TestMethod]
|
||||
public void Serialize_RepeatedStrings_UsesInterning()
|
||||
{
|
||||
var obj = new TestClassWithRepeatedStrings
|
||||
{
|
||||
Field1 = "Repeated",
|
||||
Field2 = "Repeated",
|
||||
Field3 = "Repeated",
|
||||
Field4 = "Unique"
|
||||
};
|
||||
|
||||
var binaryWithInterning = AcBinarySerializer.Serialize(obj, AcBinarySerializerOptions.Default);
|
||||
var binaryWithoutInterning = AcBinarySerializer.Serialize(obj,
|
||||
new AcBinarySerializerOptions { UseStringInterning = false });
|
||||
|
||||
// With interning should be smaller
|
||||
Assert.IsTrue(binaryWithInterning.Length < binaryWithoutInterning.Length,
|
||||
$"With interning: {binaryWithInterning.Length}, Without: {binaryWithoutInterning.Length}");
|
||||
|
||||
// Both should deserialize correctly
|
||||
var result1 = AcBinaryDeserializer.Deserialize<TestClassWithRepeatedStrings>(binaryWithInterning);
|
||||
var result2 = AcBinaryDeserializer.Deserialize<TestClassWithRepeatedStrings>(binaryWithoutInterning);
|
||||
|
||||
Assert.AreEqual(obj.Field1, result1!.Field1);
|
||||
Assert.AreEqual(obj.Field1, result2!.Field1);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// REGRESSION TEST: Comprehensive string interning edge cases.
|
||||
///
|
||||
/// Production bug pattern: "Invalid interned string index: X. Interned strings count: Y"
|
||||
///
|
||||
/// Root causes identified:
|
||||
/// 1. Property names not being registered in intern table during deserialization
|
||||
/// 2. String values with same length but different content
|
||||
/// 3. Nested objects creating complex interning order
|
||||
/// 4. Collections of objects with repeated property names
|
||||
/// </summary>
|
||||
[TestMethod]
|
||||
public void StringInterning_PropertyNames_MustBeRegisteredDuringDeserialization()
|
||||
{
|
||||
// This test verifies that property names (>= 4 chars) are properly
|
||||
// registered in the intern table during deserialization.
|
||||
// The serializer registers them via WriteString, so deserializer must too.
|
||||
|
||||
var items = Enumerable.Range(0, 10).Select(i => new TestClassWithLongPropertyNames
|
||||
{
|
||||
FirstProperty = $"Value1_{i}",
|
||||
SecondProperty = $"Value2_{i}",
|
||||
ThirdProperty = $"Value3_{i}",
|
||||
FourthProperty = $"Value4_{i}"
|
||||
}).ToList();
|
||||
|
||||
var binary = items.ToBinary();
|
||||
var result = binary.BinaryTo<List<TestClassWithLongPropertyNames>>();
|
||||
|
||||
Assert.IsNotNull(result);
|
||||
Assert.AreEqual(10, result.Count);
|
||||
for (int i = 0; i < 10; i++)
|
||||
{
|
||||
Assert.AreEqual($"Value1_{i}", result[i].FirstProperty);
|
||||
Assert.AreEqual($"Value2_{i}", result[i].SecondProperty);
|
||||
}
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void StringInterning_MixedShortAndLongStrings_HandledCorrectly()
|
||||
{
|
||||
// Short strings (< 4 chars) are NOT interned
|
||||
// Long strings (>= 4 chars) ARE interned
|
||||
// This creates different traversal patterns
|
||||
|
||||
var items = Enumerable.Range(0, 20).Select(i => new TestClassWithMixedStrings
|
||||
{
|
||||
Id = i,
|
||||
ShortName = $"A{i % 3}", // 2-3 chars, NOT interned
|
||||
LongName = $"LongName_{i % 5}", // > 4 chars, interned
|
||||
Description = $"Description_value_{i % 7}", // > 4 chars, interned
|
||||
Tag = i % 2 == 0 ? "AB" : "XY" // 2 chars, NOT interned
|
||||
}).ToList();
|
||||
|
||||
var binary = items.ToBinary();
|
||||
var result = binary.BinaryTo<List<TestClassWithMixedStrings>>();
|
||||
|
||||
Assert.IsNotNull(result);
|
||||
Assert.AreEqual(20, result.Count);
|
||||
|
||||
for (int i = 0; i < 20; i++)
|
||||
{
|
||||
Assert.AreEqual(i, result[i].Id);
|
||||
Assert.AreEqual($"A{i % 3}", result[i].ShortName);
|
||||
Assert.AreEqual($"LongName_{i % 5}", result[i].LongName);
|
||||
Assert.AreEqual($"Description_value_{i % 7}", result[i].Description);
|
||||
}
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void StringInterning_DeeplyNestedObjects_MaintainsCorrectOrder()
|
||||
{
|
||||
// Complex nested structure where property names and values
|
||||
// are interleaved in a specific order
|
||||
|
||||
var root = new TestNestedStructure
|
||||
{
|
||||
RootName = "RootObject",
|
||||
Level1Items = Enumerable.Range(0, 5).Select(i => new TestLevel1
|
||||
{
|
||||
Level1Name = $"Level1_{i}",
|
||||
Level2Items = Enumerable.Range(0, 3).Select(j => new TestLevel2
|
||||
{
|
||||
Level2Name = $"Level2_{i}_{j}",
|
||||
Value = $"Value_{i * 3 + j}"
|
||||
}).ToList()
|
||||
}).ToList()
|
||||
};
|
||||
|
||||
var binary = root.ToBinary();
|
||||
var result = binary.BinaryTo<TestNestedStructure>();
|
||||
|
||||
Assert.IsNotNull(result);
|
||||
Assert.AreEqual("RootObject", result.RootName);
|
||||
Assert.AreEqual(5, result.Level1Items.Count);
|
||||
|
||||
for (int i = 0; i < 5; i++)
|
||||
{
|
||||
Assert.AreEqual($"Level1_{i}", result.Level1Items[i].Level1Name);
|
||||
Assert.AreEqual(3, result.Level1Items[i].Level2Items.Count);
|
||||
|
||||
for (int j = 0; j < 3; j++)
|
||||
{
|
||||
Assert.AreEqual($"Level2_{i}_{j}", result.Level1Items[i].Level2Items[j].Level2Name);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void StringInterning_RepeatedPropertyValuesAcrossObjects_UsesReferences()
|
||||
{
|
||||
// When the same string value appears multiple times,
|
||||
// the serializer writes StringInterned reference instead of the full string.
|
||||
// The deserializer must look up the correct index.
|
||||
|
||||
var items = Enumerable.Range(0, 50).Select(i => new TestClassWithRepeatedValues
|
||||
{
|
||||
Id = i,
|
||||
Status = i % 3 == 0 ? "Pending" : i % 3 == 1 ? "Processing" : "Completed",
|
||||
Category = i % 5 == 0 ? "CategoryA" : i % 5 == 1 ? "CategoryB" : "CategoryC",
|
||||
Priority = i % 2 == 0 ? "High" : "Low_Priority_Value"
|
||||
}).ToList();
|
||||
|
||||
var binary = items.ToBinary();
|
||||
var result = binary.BinaryTo<List<TestClassWithRepeatedValues>>();
|
||||
|
||||
Assert.IsNotNull(result);
|
||||
Assert.AreEqual(50, result.Count);
|
||||
|
||||
for (int i = 0; i < 50; i++)
|
||||
{
|
||||
Assert.AreEqual(i, result[i].Id);
|
||||
var expectedStatus = i % 3 == 0 ? "Pending" : i % 3 == 1 ? "Processing" : "Completed";
|
||||
Assert.AreEqual(expectedStatus, result[i].Status, $"Status mismatch at index {i}");
|
||||
}
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void StringInterning_ManyUniqueStringsFollowedByRepeats_CorrectIndexLookup()
|
||||
{
|
||||
// First create many unique strings (all get registered)
|
||||
// Then repeat some of them (use StringInterned references)
|
||||
// This tests the index calculation
|
||||
|
||||
var items = new List<TestClassWithNameValue>();
|
||||
|
||||
// First 30 items with unique names (all registered as new)
|
||||
for (int i = 0; i < 30; i++)
|
||||
{
|
||||
items.Add(new TestClassWithNameValue
|
||||
{
|
||||
Name = $"UniqueName_{i:D4}",
|
||||
Value = $"UniqueValue_{i:D4}"
|
||||
});
|
||||
}
|
||||
|
||||
// Next 20 items reuse names from first batch (should use StringInterned)
|
||||
for (int i = 0; i < 20; i++)
|
||||
{
|
||||
items.Add(new TestClassWithNameValue
|
||||
{
|
||||
Name = $"UniqueName_{i % 10:D4}", // Reuse first 10 names
|
||||
Value = $"UniqueValue_{(i + 10) % 30:D4}" // Reuse different values
|
||||
});
|
||||
}
|
||||
|
||||
var binary = items.ToBinary();
|
||||
var result = binary.BinaryTo<List<TestClassWithNameValue>>();
|
||||
|
||||
Assert.IsNotNull(result);
|
||||
Assert.AreEqual(50, result.Count);
|
||||
|
||||
// Verify first batch
|
||||
for (int i = 0; i < 30; i++)
|
||||
{
|
||||
Assert.AreEqual($"UniqueName_{i:D4}", result[i].Name, $"Name mismatch at index {i}");
|
||||
Assert.AreEqual($"UniqueValue_{i:D4}", result[i].Value, $"Value mismatch at index {i}");
|
||||
}
|
||||
|
||||
// Verify second batch (reused strings)
|
||||
for (int i = 0; i < 20; i++)
|
||||
{
|
||||
Assert.AreEqual($"UniqueName_{i % 10:D4}", result[30 + i].Name, $"Name mismatch at index {30 + i}");
|
||||
}
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void StringInterning_EmptyAndNullStrings_DoNotAffectInternTable()
|
||||
{
|
||||
// Empty strings use StringEmpty type code
|
||||
// Null strings use Null type code
|
||||
// Neither should affect intern table indices
|
||||
|
||||
var items = new List<TestClassWithNullableStrings>();
|
||||
|
||||
for (int i = 0; i < 25; i++)
|
||||
{
|
||||
items.Add(new TestClassWithNullableStrings
|
||||
{
|
||||
Id = i,
|
||||
RequiredName = $"Required_{i:D3}",
|
||||
OptionalName = i % 3 == 0 ? null : i % 3 == 1 ? "" : $"Optional_{i:D3}",
|
||||
Description = i % 2 == 0 ? $"Description_{i % 5:D3}" : null
|
||||
});
|
||||
}
|
||||
|
||||
var binary = items.ToBinary();
|
||||
var result = binary.BinaryTo<List<TestClassWithNullableStrings>>();
|
||||
|
||||
Assert.IsNotNull(result);
|
||||
Assert.AreEqual(25, result.Count);
|
||||
|
||||
for (int i = 0; i < 25; i++)
|
||||
{
|
||||
Assert.AreEqual(i, result[i].Id);
|
||||
Assert.AreEqual($"Required_{i:D3}", result[i].RequiredName);
|
||||
|
||||
if (i % 3 == 0)
|
||||
{
|
||||
Assert.IsNull(result[i].OptionalName, $"OptionalName at index {i} should be null");
|
||||
}
|
||||
else if (i % 3 == 1)
|
||||
{
|
||||
// Empty string may deserialize as either "" or null depending on implementation
|
||||
Assert.IsTrue(string.IsNullOrEmpty(result[i].OptionalName),
|
||||
$"OptionalName at index {i} should be null or empty, got: '{result[i].OptionalName}'");
|
||||
}
|
||||
else
|
||||
{
|
||||
Assert.AreEqual($"Optional_{i:D3}", result[i].OptionalName,
|
||||
$"OptionalName at index {i} mismatch");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void StringInterning_ProductionLikeCustomerDto_RoundTrip()
|
||||
{
|
||||
// Simulate the CustomerDto structure that causes production issues
|
||||
// Key characteristics:
|
||||
// - Many string properties (FirstName, LastName, Email, Company, etc.)
|
||||
// - GenericAttributes list with repeated Key values
|
||||
// - List of items with common status/category values
|
||||
|
||||
var customers = Enumerable.Range(0, 25).Select(i => new TestCustomerLikeDto
|
||||
{
|
||||
Id = i,
|
||||
FirstName = $"FirstName_{i % 10}", // 10 unique values
|
||||
LastName = $"LastName_{i % 8}", // 8 unique values
|
||||
Email = $"user{i}@example.com", // All unique
|
||||
Company = $"Company_{i % 5}", // 5 unique values
|
||||
Department = i % 3 == 0 ? "Sales" : i % 3 == 1 ? "Engineering" : "Marketing",
|
||||
Role = i % 4 == 0 ? "Admin" : i % 4 == 1 ? "User" : i % 4 == 2 ? "Manager" : "Guest",
|
||||
Status = i % 2 == 0 ? "Active" : "Inactive",
|
||||
Attributes = new List<TestGenericAttribute>
|
||||
{
|
||||
new() { Key = "DateOfReceipt", Value = $"{i % 28 + 1}/24/2025 00:27:00" },
|
||||
new() { Key = "Priority", Value = (i % 5).ToString() },
|
||||
new() { Key = "CustomField1", Value = $"CustomValue_{i % 7}" },
|
||||
new() { Key = "CustomField2", Value = i % 2 == 0 ? "Yes" : "No_Value" }
|
||||
}
|
||||
}).ToList();
|
||||
|
||||
var binary = customers.ToBinary();
|
||||
var result = binary.BinaryTo<List<TestCustomerLikeDto>>();
|
||||
|
||||
Assert.IsNotNull(result, "Result should not be null - deserialization failed");
|
||||
Assert.AreEqual(25, result.Count);
|
||||
|
||||
for (int i = 0; i < 25; i++)
|
||||
{
|
||||
Assert.AreEqual(i, result[i].Id, $"Id mismatch at index {i}");
|
||||
Assert.AreEqual($"FirstName_{i % 10}", result[i].FirstName, $"FirstName mismatch at index {i}");
|
||||
Assert.AreEqual($"LastName_{i % 8}", result[i].LastName, $"LastName mismatch at index {i}");
|
||||
Assert.AreEqual($"user{i}@example.com", result[i].Email, $"Email mismatch at index {i}");
|
||||
Assert.AreEqual(4, result[i].Attributes.Count, $"Attributes count mismatch at index {i}");
|
||||
|
||||
Assert.AreEqual("DateOfReceipt", result[i].Attributes[0].Key);
|
||||
Assert.AreEqual("Priority", result[i].Attributes[1].Key);
|
||||
}
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void StringInterning_LargeListWithHighStringReuse_HandlesCorrectly()
|
||||
{
|
||||
// Large dataset (100+ items) with high string reuse ratio
|
||||
// This is the scenario that triggers production bugs
|
||||
|
||||
const int itemCount = 150;
|
||||
var items = Enumerable.Range(0, itemCount).Select(i => new TestHighReuseDto
|
||||
{
|
||||
Id = i,
|
||||
// Property names are reused 150 times (once per object)
|
||||
CategoryCode = $"CAT_{i % 10:D2}", // 10 unique values, 15x reuse each
|
||||
StatusCode = $"STATUS_{i % 5:D2}", // 5 unique values, 30x reuse each
|
||||
TypeCode = $"TYPE_{i % 3:D2}", // 3 unique values, 50x reuse each
|
||||
PriorityCode = i % 2 == 0 ? "PRIORITY_HIGH" : "PRIORITY_LOW", // 2 values, 75x each
|
||||
UniqueField = $"UNIQUE_{i:D4}" // All unique, no reuse
|
||||
}).ToList();
|
||||
|
||||
var binary = items.ToBinary();
|
||||
var result = binary.BinaryTo<List<TestHighReuseDto>>();
|
||||
|
||||
Assert.IsNotNull(result, "Result should not be null");
|
||||
Assert.AreEqual(itemCount, result.Count, $"Expected {itemCount} items");
|
||||
|
||||
// Verify every item
|
||||
for (int i = 0; i < itemCount; i++)
|
||||
{
|
||||
Assert.AreEqual(i, result[i].Id, $"Id mismatch at {i}");
|
||||
Assert.AreEqual($"CAT_{i % 10:D2}", result[i].CategoryCode, $"CategoryCode mismatch at {i}");
|
||||
Assert.AreEqual($"STATUS_{i % 5:D2}", result[i].StatusCode, $"StatusCode mismatch at {i}");
|
||||
Assert.AreEqual($"TYPE_{i % 3:D2}", result[i].TypeCode, $"TypeCode mismatch at {i}");
|
||||
Assert.AreEqual($"UNIQUE_{i:D4}", result[i].UniqueField, $"UniqueField mismatch at {i}");
|
||||
}
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Test Models
|
||||
|
||||
private class TestSimpleClass
|
||||
{
|
||||
public int Id { get; set; }
|
||||
public string Name { get; set; } = "";
|
||||
public double Value { get; set; }
|
||||
public bool IsActive { get; set; }
|
||||
}
|
||||
|
||||
private class TestNestedClass
|
||||
{
|
||||
public int Id { get; set; }
|
||||
public string Name { get; set; } = "";
|
||||
public TestSimpleClass? Child { get; set; }
|
||||
}
|
||||
|
||||
private class TestClassWithList
|
||||
{
|
||||
public int Id { get; set; }
|
||||
public List<string> Items { get; set; } = new();
|
||||
}
|
||||
|
||||
private class TestClassWithRepeatedStrings
|
||||
{
|
||||
public string Field1 { get; set; } = "";
|
||||
public string Field2 { get; set; } = "";
|
||||
public string Field3 { get; set; } = "";
|
||||
public string Field4 { get; set; } = "";
|
||||
}
|
||||
|
||||
// New test models for string interning edge cases
|
||||
|
||||
private class TestClassWithLongPropertyNames
|
||||
{
|
||||
public string FirstProperty { get; set; } = "";
|
||||
public string SecondProperty { get; set; } = "";
|
||||
public string ThirdProperty { get; set; } = "";
|
||||
public string FourthProperty { get; set; } = "";
|
||||
}
|
||||
|
||||
private class TestClassWithMixedStrings
|
||||
{
|
||||
public int Id { get; set; }
|
||||
public string ShortName { get; set; } = ""; // < 4 chars
|
||||
public string LongName { get; set; } = ""; // >= 4 chars
|
||||
public string Description { get; set; } = ""; // >= 4 chars
|
||||
public string Tag { get; set; } = ""; // < 4 chars
|
||||
}
|
||||
|
||||
private class TestNestedStructure
|
||||
{
|
||||
public string RootName { get; set; } = "";
|
||||
public List<TestLevel1> Level1Items { get; set; } = new();
|
||||
}
|
||||
|
||||
private class TestLevel1
|
||||
{
|
||||
public string Level1Name { get; set; } = "";
|
||||
public List<TestLevel2> Level2Items { get; set; } = new();
|
||||
}
|
||||
|
||||
private class TestLevel2
|
||||
{
|
||||
public string Level2Name { get; set; } = "";
|
||||
public string Value { get; set; } = "";
|
||||
}
|
||||
|
||||
private class TestClassWithRepeatedValues
|
||||
{
|
||||
public int Id { get; set; }
|
||||
public string Status { get; set; } = "";
|
||||
public string Category { get; set; } = "";
|
||||
public string Priority { get; set; } = "";
|
||||
}
|
||||
|
||||
private class TestClassWithNameValue
|
||||
{
|
||||
public string Name { get; set; } = "";
|
||||
public string Value { get; set; } = "";
|
||||
}
|
||||
|
||||
private class TestClassWithNullableStrings
|
||||
{
|
||||
public int Id { get; set; }
|
||||
public string RequiredName { get; set; } = "";
|
||||
public string? OptionalName { get; set; }
|
||||
public string? Description { get; set; }
|
||||
}
|
||||
|
||||
private class TestCustomerLikeDto
|
||||
{
|
||||
public int Id { get; set; }
|
||||
public string FirstName { get; set; } = "";
|
||||
public string LastName { get; set; } = "";
|
||||
public string Email { get; set; } = "";
|
||||
public string Company { get; set; } = "";
|
||||
public string Department { get; set; } = "";
|
||||
public string Role { get; set; } = "";
|
||||
public string Status { get; set; } = "";
|
||||
public List<TestGenericAttribute> Attributes { get; set; } = new();
|
||||
}
|
||||
|
||||
private class TestGenericAttribute
|
||||
{
|
||||
public string Key { get; set; } = "";
|
||||
public string Value { get; set; } = "";
|
||||
}
|
||||
|
||||
private class TestHighReuseDto
|
||||
{
|
||||
public int Id { get; set; }
|
||||
public string CategoryCode { get; set; } = "";
|
||||
public string StatusCode { get; set; } = "";
|
||||
public string TypeCode { get; set; } = "";
|
||||
public string PriorityCode { get; set; } = "";
|
||||
public string UniqueField { get; set; } = "";
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Benchmark Order Tests
|
||||
|
||||
[TestMethod]
|
||||
public void Serialize_BenchmarkOrder_RoundTrip()
|
||||
{
|
||||
// This is the exact same data that causes stack overflow in benchmarks
|
||||
var order = TestDataFactory.CreateBenchmarkOrder(
|
||||
itemCount: 3,
|
||||
palletsPerItem: 2,
|
||||
measurementsPerPallet: 2,
|
||||
pointsPerMeasurement: 5);
|
||||
|
||||
// Should not throw stack overflow
|
||||
var binary = AcBinarySerializer.Serialize(order);
|
||||
Assert.IsTrue(binary.Length > 0, "Binary data should not be empty");
|
||||
|
||||
var result = AcBinaryDeserializer.Deserialize<TestOrder>(binary);
|
||||
Assert.IsNotNull(result);
|
||||
Assert.AreEqual(order.Id, result.Id);
|
||||
Assert.AreEqual(order.OrderNumber, result.OrderNumber);
|
||||
Assert.AreEqual(order.Items.Count, result.Items.Count);
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void Serialize_BenchmarkOrder_SmallData_RoundTrip()
|
||||
{
|
||||
// Smaller test to isolate the issue
|
||||
var order = TestDataFactory.CreateBenchmarkOrder(
|
||||
itemCount: 1,
|
||||
palletsPerItem: 1,
|
||||
measurementsPerPallet: 1,
|
||||
pointsPerMeasurement: 1);
|
||||
|
||||
var binary = AcBinarySerializer.Serialize(order);
|
||||
Assert.IsTrue(binary.Length > 0);
|
||||
|
||||
var result = AcBinaryDeserializer.Deserialize<TestOrder>(binary);
|
||||
Assert.IsNotNull(result);
|
||||
Assert.AreEqual(order.Id, result.Id);
|
||||
}
|
||||
|
||||
#endregion
|
||||
}
|
||||
|
|
@ -0,0 +1,633 @@
|
|||
namespace AyCode.Core.Tests.Serialization;
|
||||
|
||||
/// <summary>
|
||||
/// Test models for binary serializer tests.
|
||||
/// </summary>
|
||||
public static class AcSerializerModels
|
||||
{
|
||||
public class TestSimpleClass
|
||||
{
|
||||
public int Id { get; set; }
|
||||
public string Name { get; set; } = "";
|
||||
public double Value { get; set; }
|
||||
public bool IsActive { get; set; }
|
||||
}
|
||||
|
||||
public class TestNestedClass
|
||||
{
|
||||
public int Id { get; set; }
|
||||
public string Name { get; set; } = "";
|
||||
public TestSimpleClass? Child { get; set; }
|
||||
}
|
||||
|
||||
public class TestClassWithList
|
||||
{
|
||||
public int Id { get; set; }
|
||||
public List<string> Items { get; set; } = new();
|
||||
}
|
||||
|
||||
public class TestClassWithRepeatedStrings
|
||||
{
|
||||
public string Field1 { get; set; } = "";
|
||||
public string Field2 { get; set; } = "";
|
||||
public string Field3 { get; set; } = "";
|
||||
public string Field4 { get; set; } = "";
|
||||
}
|
||||
|
||||
public class TestClassWithLongPropertyNames
|
||||
{
|
||||
public string FirstProperty { get; set; } = "";
|
||||
public string SecondProperty { get; set; } = "";
|
||||
public string ThirdProperty { get; set; } = "";
|
||||
public string FourthProperty { get; set; } = "";
|
||||
}
|
||||
|
||||
public class TestClassWithMixedStrings
|
||||
{
|
||||
public int Id { get; set; }
|
||||
public string ShortName { get; set; } = "";
|
||||
public string LongName { get; set; } = "";
|
||||
public string Description { get; set; } = "";
|
||||
public string Tag { get; set; } = "";
|
||||
}
|
||||
|
||||
public class TestNestedStructure
|
||||
{
|
||||
public string RootName { get; set; } = "";
|
||||
public List<TestLevel1> Level1Items { get; set; } = new();
|
||||
}
|
||||
|
||||
public class TestLevel1
|
||||
{
|
||||
public string Level1Name { get; set; } = "";
|
||||
public List<TestLevel2> Level2Items { get; set; } = new();
|
||||
}
|
||||
|
||||
public class TestLevel2
|
||||
{
|
||||
public string Level2Name { get; set; } = "";
|
||||
public string Value { get; set; } = "";
|
||||
}
|
||||
|
||||
public class TestClassWithRepeatedValues
|
||||
{
|
||||
public int Id { get; set; }
|
||||
public string Status { get; set; } = "";
|
||||
public string Category { get; set; } = "";
|
||||
public string Priority { get; set; } = "";
|
||||
}
|
||||
|
||||
public class TestClassWithNameValue
|
||||
{
|
||||
public string Name { get; set; } = "";
|
||||
public string Value { get; set; } = "";
|
||||
}
|
||||
|
||||
public class TestClassWithNullableStrings
|
||||
{
|
||||
public int Id { get; set; }
|
||||
public string RequiredName { get; set; } = "";
|
||||
public string? OptionalName { get; set; }
|
||||
public string? Description { get; set; }
|
||||
}
|
||||
|
||||
public class TestCustomerLikeDto
|
||||
{
|
||||
public int Id { get; set; }
|
||||
public string FirstName { get; set; } = "";
|
||||
public string LastName { get; set; } = "";
|
||||
public string Email { get; set; } = "";
|
||||
public string Company { get; set; } = "";
|
||||
public string Department { get; set; } = "";
|
||||
public string Role { get; set; } = "";
|
||||
public string Status { get; set; } = "";
|
||||
public List<TestAttribute> Attributes { get; set; } = new();
|
||||
}
|
||||
|
||||
public class TestAttribute
|
||||
{
|
||||
public string Key { get; set; } = "";
|
||||
public string Value { get; set; } = "";
|
||||
}
|
||||
|
||||
public class TestHighReuseDto
|
||||
{
|
||||
public int Id { get; set; }
|
||||
public string CategoryCode { get; set; } = "";
|
||||
public string StatusCode { get; set; } = "";
|
||||
public string TypeCode { get; set; } = "";
|
||||
public string PriorityCode { get; set; } = "";
|
||||
public string UniqueField { get; set; } = "";
|
||||
}
|
||||
|
||||
public class TestClassWithNullableProperties
|
||||
{
|
||||
public int Id { get; set; }
|
||||
|
||||
public int? NullableInt { get; set; }
|
||||
public int? NullableIntNull { get; set; }
|
||||
|
||||
public long? NullableLong { get; set; }
|
||||
public long? NullableLongNull { get; set; }
|
||||
|
||||
public double? NullableDouble { get; set; }
|
||||
public double? NullableDoubleNull { get; set; }
|
||||
|
||||
public decimal? NullableDecimal { get; set; }
|
||||
public decimal? NullableDecimalNull { get; set; }
|
||||
|
||||
public DateTime? NullableDateTime { get; set; }
|
||||
public DateTime? NullableDateTimeNull { get; set; }
|
||||
|
||||
public Guid? NullableGuid { get; set; }
|
||||
public Guid? NullableGuidNull { get; set; }
|
||||
|
||||
public bool? NullableBool { get; set; }
|
||||
public bool? NullableBoolNull { get; set; }
|
||||
}
|
||||
|
||||
public class TestParentWithNullableChild
|
||||
{
|
||||
public int Id { get; set; }
|
||||
public string Name { get; set; } = "";
|
||||
public TestClassWithNullableProperties? Child { get; set; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Test model mimicking StockTaking entity structure.
|
||||
/// /// </summary>
|
||||
public class TestStockTaking
|
||||
{
|
||||
public int Id { get; set; }
|
||||
public DateTime StartDateTime { get; set; }
|
||||
public bool IsClosed { get; set; }
|
||||
public int Creator { get; set; }
|
||||
public DateTime Created { get; set; }
|
||||
public DateTime Modified { get; set; }
|
||||
public List<TestStockTakingItem>? StockTakingItems { get; set; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Test model mimicking StockTakingItem entity structure.
|
||||
/// /// </summary>
|
||||
public class TestStockTakingItem
|
||||
{
|
||||
public int Id { get; set; }
|
||||
public int StockTakingId { get; set; }
|
||||
public int ProductId { get; set; }
|
||||
public bool IsMeasured { get; set; }
|
||||
public int OriginalStockQuantity { get; set; }
|
||||
public int MeasuredStockQuantity { get; set; }
|
||||
public DateTime Created { get; set; }
|
||||
public DateTime Modified { get; set; }
|
||||
public List<TestStockTakingItemPallet>? StockTakingItemPallets { get; set; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Test model mimicking StockTakingItemPallet/MeasuringItemPalletBase entity structure.
|
||||
/// Contains nullable int properties (CreatorId, ModifierId) that caused the production bug.
|
||||
/// </summary>
|
||||
public class TestStockTakingItemPallet
|
||||
{
|
||||
public int Id { get; set; }
|
||||
public int StockTakingItemId { get; set; }
|
||||
public int TrayQuantity { get; set; }
|
||||
public double TareWeight { get; set; }
|
||||
public double PalletWeight { get; set; }
|
||||
public double GrossWeight { get; set; }
|
||||
public bool IsMeasured { get; set; }
|
||||
|
||||
public int? CreatorId { get; set; }
|
||||
public int? ModifierId { get; set; }
|
||||
|
||||
public DateTime Created { get; set; }
|
||||
public DateTime Modified { get; set; }
|
||||
}
|
||||
|
||||
public class TestEntityWithDateTimeAndInt
|
||||
{
|
||||
public int Id { get; set; }
|
||||
public int IntValue { get; set; }
|
||||
public DateTime Created { get; set; }
|
||||
public DateTime Modified { get; set; }
|
||||
public int StatusCode { get; set; }
|
||||
public string Name { get; set; } = "";
|
||||
}
|
||||
|
||||
public class TestEntityWithManyIntsBeforeDateTime
|
||||
{
|
||||
public int Id { get; set; }
|
||||
public int Value1 { get; set; }
|
||||
public int Value2 { get; set; }
|
||||
public int Value3 { get; set; }
|
||||
public int Value4 { get; set; }
|
||||
public int Value5 { get; set; }
|
||||
public DateTime FirstDateTime { get; set; }
|
||||
public DateTime SecondDateTime { get; set; }
|
||||
public int FinalValue { get; set; }
|
||||
}
|
||||
|
||||
public class TestParentEntityWithDateTimeChild
|
||||
{
|
||||
public int ParentId { get; set; }
|
||||
public string ParentName { get; set; } = "";
|
||||
public TestEntityWithDateTimeAndInt? Child { get; set; }
|
||||
}
|
||||
|
||||
public class TestParentWithDateTimeItemCollection
|
||||
{
|
||||
public int Id { get; set; }
|
||||
public string Name { get; set; } = "";
|
||||
public DateTime Created { get; set; }
|
||||
public List<TestEntityWithDateTimeAndInt>? Items { get; set; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Base class to simulate inheritance hierarchy like MgEntityBase.
|
||||
/// Id property is in base class, so reflection may return it last.
|
||||
/// </summary>
|
||||
public abstract class TestEntityBase
|
||||
{
|
||||
public int Id { get; set; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Simulates MgStockTaking structure with inheritance.
|
||||
/// Properties are ordered: StartDateTime, IsClosed, StockTakingItems, Creator, Created, Modified
|
||||
/// Id comes from base class and may appear last in reflection.
|
||||
/// </summary>
|
||||
public class TestStockTakingWithInheritance : TestEntityBase
|
||||
{
|
||||
public DateTime StartDateTime { get; set; }
|
||||
public bool IsClosed { get; set; }
|
||||
public List<TestStockTakingItemWithInheritance>? StockTakingItems { get; set; }
|
||||
public int Creator { get; set; }
|
||||
public DateTime Created { get; set; }
|
||||
public DateTime Modified { get; set; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Simulates StockTakingItem with inheritance.
|
||||
/// </summary>
|
||||
public class TestStockTakingItemWithInheritance : TestEntityBase
|
||||
{
|
||||
public int StockTakingId { get; set; }
|
||||
public int ProductId { get; set; }
|
||||
public bool IsMeasured { get; set; }
|
||||
public int OriginalStockQuantity { get; set; }
|
||||
public int MeasuredStockQuantity { get; set; }
|
||||
public DateTime Created { get; set; }
|
||||
public DateTime Modified { get; set; }
|
||||
public List<TestStockTakingItemPalletWithInheritance>? StockTakingItemPallets { get; set; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Simulates MeasuringItemPalletBase with inheritance and ForeignKey pattern.
|
||||
/// </summary>
|
||||
public class TestStockTakingItemPalletWithInheritance : TestEntityBase
|
||||
{
|
||||
public int StockTakingItemId { get; set; }
|
||||
public int TrayQuantity { get; set; }
|
||||
public double TareWeight { get; set; }
|
||||
public double PalletWeight { get; set; }
|
||||
public double GrossWeight { get; set; }
|
||||
public bool IsMeasured { get; set; }
|
||||
public int? CreatorId { get; set; }
|
||||
public int? ModifierId { get; set; }
|
||||
public DateTime Created { get; set; }
|
||||
public DateTime Modified { get; set; }
|
||||
}
|
||||
|
||||
#region Schema Mismatch Tests
|
||||
|
||||
/// <summary>
|
||||
/// Server-side model with extra properties that client doesn't know about.
|
||||
/// </summary>
|
||||
public class ServerStockTaking
|
||||
{
|
||||
public int Id { get; set; }
|
||||
public DateTime StartDateTime { get; set; }
|
||||
public bool IsClosed { get; set; }
|
||||
public int Creator { get; set; }
|
||||
public DateTime Created { get; set; }
|
||||
public DateTime Modified { get; set; }
|
||||
public List<ServerStockTakingItem>? StockTakingItems { get; set; }
|
||||
|
||||
// Extra properties that client doesn't have
|
||||
public string? ExtraServerProperty1 { get; set; }
|
||||
public int ExtraServerProperty2 { get; set; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Server-side item with Product navigation property.
|
||||
/// </summary>
|
||||
public class ServerStockTakingItem
|
||||
{
|
||||
public int Id { get; set; }
|
||||
public int StockTakingId { get; set; }
|
||||
public int ProductId { get; set; }
|
||||
public bool IsMeasured { get; set; }
|
||||
public int OriginalStockQuantity { get; set; }
|
||||
public int MeasuredStockQuantity { get; set; }
|
||||
public DateTime Created { get; set; }
|
||||
public DateTime Modified { get; set; }
|
||||
|
||||
// Navigation property - this gets serialized on server!
|
||||
public ServerProductDto? Product { get; set; }
|
||||
|
||||
public List<ServerStockTakingItemPallet>? StockTakingItemPallets { get; set; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Server-side product DTO.
|
||||
/// </summary>
|
||||
public class ServerProductDto
|
||||
{
|
||||
public int Id { get; set; }
|
||||
public string Name { get; set; } = "";
|
||||
public string Description { get; set; } = "";
|
||||
public double Price { get; set; }
|
||||
public int CategoryId { get; set; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Server-side pallet with all properties.
|
||||
/// </summary>
|
||||
public class ServerStockTakingItemPallet
|
||||
{
|
||||
public int Id { get; set; }
|
||||
public int StockTakingItemId { get; set; }
|
||||
public int TrayQuantity { get; set; }
|
||||
public double TareWeight { get; set; }
|
||||
public double PalletWeight { get; set; }
|
||||
public double GrossWeight { get; set; }
|
||||
public bool IsMeasured { get; set; }
|
||||
public int? CreatorId { get; set; }
|
||||
public int? ModifierId { get; set; }
|
||||
public DateTime Created { get; set; }
|
||||
public DateTime Modified { get; set; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Client-side model - MISSING some properties that server sends.
|
||||
/// </summary>
|
||||
public class ClientStockTaking
|
||||
{
|
||||
public int Id { get; set; }
|
||||
public DateTime StartDateTime { get; set; }
|
||||
public bool IsClosed { get; set; }
|
||||
public int Creator { get; set; }
|
||||
public DateTime Created { get; set; }
|
||||
public DateTime Modified { get; set; }
|
||||
public List<ClientStockTakingItem>? StockTakingItems { get; set; }
|
||||
// Note: ExtraServerProperty1 and ExtraServerProperty2 are MISSING
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Client-side item - MISSING Product navigation property.
|
||||
/// </summary>
|
||||
public class ClientStockTakingItem
|
||||
{
|
||||
public int Id { get; set; }
|
||||
public int StockTakingId { get; set; }
|
||||
public int ProductId { get; set; }
|
||||
public bool IsMeasured { get; set; }
|
||||
public int OriginalStockQuantity { get; set; }
|
||||
public int MeasuredStockQuantity { get; set; }
|
||||
public DateTime Created { get; set; }
|
||||
public DateTime Modified { get; set; }
|
||||
// Note: Product property is MISSING - server sends it, client skips it
|
||||
public List<ClientStockTakingItemPallet>? StockTakingItemPallets { get; set; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Client-side pallet.
|
||||
/// </summary>
|
||||
public class ClientStockTakingItemPallet
|
||||
{
|
||||
public int Id { get; set; }
|
||||
public int StockTakingItemId { get; set; }
|
||||
public int TrayQuantity { get; set; }
|
||||
public double TareWeight { get; set; }
|
||||
public double PalletWeight { get; set; }
|
||||
public double GrossWeight { get; set; }
|
||||
public bool IsMeasured { get; set; }
|
||||
public int? CreatorId { get; set; }
|
||||
public int? ModifierId { get; set; }
|
||||
public DateTime Created { get; set; }
|
||||
public DateTime Modified { get; set; }
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Circular Reference Test Models
|
||||
|
||||
/// <summary>
|
||||
/// Parent entity with circular reference to child.
|
||||
/// </summary>
|
||||
public class CircularParent
|
||||
{
|
||||
public int Id { get; set; }
|
||||
public string Name { get; set; } = "";
|
||||
public DateTime Created { get; set; }
|
||||
public DateTime Modified { get; set; }
|
||||
public int Creator { get; set; }
|
||||
public List<CircularChild>? Children { get; set; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Child entity with back-reference to parent (circular).
|
||||
/// </summary>
|
||||
public class CircularChild
|
||||
{
|
||||
public int Id { get; set; }
|
||||
public int ParentId { get; set; }
|
||||
public string Name { get; set; } = "";
|
||||
public DateTime Created { get; set; }
|
||||
public DateTime Modified { get; set; }
|
||||
|
||||
// Back-reference to parent - creates circular reference!
|
||||
public CircularParent? Parent { get; set; }
|
||||
|
||||
public List<CircularGrandChild>? GrandChildren { get; set; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Grandchild with back-reference.
|
||||
/// </summary>
|
||||
public class CircularGrandChild
|
||||
{
|
||||
public int Id { get; set; }
|
||||
public int ChildId { get; set; }
|
||||
public int? CreatorId { get; set; }
|
||||
public int? ModifierId { get; set; }
|
||||
public DateTime Created { get; set; }
|
||||
public DateTime Modified { get; set; }
|
||||
|
||||
// Back-reference
|
||||
public CircularChild? Child { get; set; }
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Generic Type Parameter Test Models
|
||||
|
||||
/// <summary>
|
||||
/// Interface with fewer properties than implementation.
|
||||
/// </summary>
|
||||
public interface IGenericItem
|
||||
{
|
||||
int Id { get; set; }
|
||||
string Name { get; set; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Implementation with MORE properties than interface.
|
||||
/// This is the exact pattern of StockTakingItem.
|
||||
/// </summary>
|
||||
public class GenericItemImpl : IGenericItem
|
||||
{
|
||||
public int Id { get; set; }
|
||||
public string Name { get; set; } = "";
|
||||
|
||||
// Extra properties NOT in interface
|
||||
public int ExtraInt { get; set; }
|
||||
public DateTime Created { get; set; }
|
||||
public DateTime Modified { get; set; }
|
||||
public string Description { get; set; } = "";
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Parent class with generic type parameter for items.
|
||||
/// Similar to MgStockTaking<TStockTakingItem>.
|
||||
/// </summary>
|
||||
public class GenericParent<TItem> where TItem : class, IGenericItem
|
||||
{
|
||||
public int Id { get; set; }
|
||||
public string Name { get; set; } = "";
|
||||
public int Creator { get; set; }
|
||||
public DateTime Created { get; set; }
|
||||
public DateTime Modified { get; set; }
|
||||
public List<TItem>? Items { get; set; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Concrete implementation.
|
||||
/// Similar to StockTaking : MgStockTaking<StockTakingItem>.
|
||||
/// </summary>
|
||||
public class ConcreteParent : GenericParent<GenericItemImpl>
|
||||
{
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Navigation Property Test Models
|
||||
|
||||
/// <summary>
|
||||
/// Product entity that is NOT in the parent's generic type hierarchy.
|
||||
/// </summary>
|
||||
public class ProductEntity
|
||||
{
|
||||
public int Id { get; set; }
|
||||
public string Name { get; set; } = "";
|
||||
public string Description { get; set; } = "";
|
||||
public double Price { get; set; }
|
||||
public int CategoryId { get; set; }
|
||||
public DateTime Created { get; set; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Item entity with navigation property to Product.
|
||||
/// This simulates StockTakingItem.Product.
|
||||
/// </summary>
|
||||
public class ItemWithNavigationProperty
|
||||
{
|
||||
public int Id { get; set; }
|
||||
public int ParentId { get; set; }
|
||||
public int ProductId { get; set; }
|
||||
public bool IsMeasured { get; set; }
|
||||
public int Quantity { get; set; }
|
||||
public DateTime Created { get; set; }
|
||||
public DateTime Modified { get; set; }
|
||||
|
||||
// Navigation property - NOT in parent's generic type!
|
||||
// When populated, its properties need to be in metadata table too!
|
||||
public ProductEntity? Product { get; set; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Parent entity with items that have navigation properties.
|
||||
/// This simulates StockTaking with StockTakingItems that have Product.
|
||||
/// </summary>
|
||||
public class ParentWithNavigatingItems
|
||||
{
|
||||
public int Id { get; set; }
|
||||
public string Name { get; set; } = "";
|
||||
public int Creator { get; set; }
|
||||
public DateTime Created { get; set; }
|
||||
public DateTime Modified { get; set; }
|
||||
public List<ItemWithNavigationProperty>? Items { get; set; }
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Property Order Test Models
|
||||
|
||||
/// <summary>
|
||||
/// Simulates NopCommerce BaseEntity.
|
||||
/// </summary>
|
||||
public abstract class NopBaseEntity
|
||||
{
|
||||
public int Id { get; set; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Simulates MgEntityBase : BaseEntity.
|
||||
/// </summary>
|
||||
public abstract class SimMgEntityBase : NopBaseEntity
|
||||
{
|
||||
public override string ToString() => $"{GetType().Name}; Id: {Id}";
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Simulates MgStockTaking with exact property order from production.
|
||||
/// CRITICAL: Property order in code may differ from reflection order!
|
||||
/// </summary>
|
||||
public abstract class SimMgStockTaking<TStockTakingItem> : SimMgEntityBase
|
||||
where TStockTakingItem : class
|
||||
{
|
||||
public DateTime StartDateTime { get; set; }
|
||||
public bool IsClosed { get; set; }
|
||||
public List<TStockTakingItem>? StockTakingItems { get; set; }
|
||||
public int Creator { get; set; }
|
||||
public DateTime Created { get; set; }
|
||||
public DateTime Modified { get; set; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Simulates the concrete StockTaking class.
|
||||
/// </summary>
|
||||
public class SimStockTaking : SimMgStockTaking<SimStockTakingItem>
|
||||
{
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Simulates StockTakingItem with navigation properties.
|
||||
/// </summary>
|
||||
public class SimStockTakingItem : SimMgEntityBase
|
||||
{
|
||||
public int StockTakingId { get; set; }
|
||||
public int ProductId { get; set; }
|
||||
public bool IsMeasured { get; set; }
|
||||
public int OriginalStockQuantity { get; set; }
|
||||
public int MeasuredStockQuantity { get; set; }
|
||||
|
||||
// Navigation property back to parent (circular!)
|
||||
public SimStockTaking? StockTaking { get; set; }
|
||||
|
||||
public DateTime Created { get; set; }
|
||||
public DateTime Modified { get; set; }
|
||||
}
|
||||
|
||||
#endregion
|
||||
}
|
||||
|
|
@ -0,0 +1,251 @@
|
|||
using static AyCode.Core.Tests.Serialization.AcSerializerModels;
|
||||
|
||||
namespace AyCode.Core.Tests.Serialization;
|
||||
|
||||
/// <summary>
|
||||
/// Helper methods for creating test data in serializer tests.
|
||||
/// </summary>
|
||||
public static class AcSerializerTestHelper
|
||||
{
|
||||
public static List<TestClassWithLongPropertyNames> CreateLongPropertyNameItems(int count)
|
||||
{
|
||||
return Enumerable.Range(0, count).Select(i => new TestClassWithLongPropertyNames
|
||||
{
|
||||
FirstProperty = $"Value1_{i}",
|
||||
SecondProperty = $"Value2_{i}",
|
||||
ThirdProperty = $"Value3_{i}",
|
||||
FourthProperty = $"Value4_{i}"
|
||||
}).ToList();
|
||||
}
|
||||
|
||||
public static List<TestClassWithMixedStrings> CreateMixedStringItems(int count)
|
||||
{
|
||||
return Enumerable.Range(0, count).Select(i => new TestClassWithMixedStrings
|
||||
{
|
||||
Id = i,
|
||||
ShortName = $"A{i % 3}",
|
||||
LongName = $"LongName_{i % 5}",
|
||||
Description = $"Description_value_{i % 7}",
|
||||
Tag = i % 2 == 0 ? "AB" : "XY"
|
||||
}).ToList();
|
||||
}
|
||||
|
||||
public static TestNestedStructure CreateNestedStructure(int level1Count, int level2Count)
|
||||
{
|
||||
return new TestNestedStructure
|
||||
{
|
||||
RootName = "RootObject",
|
||||
Level1Items = Enumerable.Range(0, level1Count).Select(i => new TestLevel1
|
||||
{
|
||||
Level1Name = $"Level1_{i}",
|
||||
Level2Items = Enumerable.Range(0, level2Count).Select(j => new TestLevel2
|
||||
{
|
||||
Level2Name = $"Level2_{i}_{j}",
|
||||
Value = $"Value_{i * level2Count + j}"
|
||||
}).ToList()
|
||||
}).ToList()
|
||||
};
|
||||
}
|
||||
|
||||
public static List<TestClassWithRepeatedValues> CreateRepeatedValueItems(int count)
|
||||
{
|
||||
return Enumerable.Range(0, count).Select(i => new TestClassWithRepeatedValues
|
||||
{
|
||||
Id = i,
|
||||
Status = i % 3 == 0 ? "Pending" : i % 3 == 1 ? "Processing" : "Completed",
|
||||
Category = i % 5 == 0 ? "CategoryA" : i % 5 == 1 ? "CategoryB" : "CategoryC",
|
||||
Priority = i % 2 == 0 ? "High" : "Low_Priority_Value"
|
||||
}).ToList();
|
||||
}
|
||||
|
||||
public static List<TestClassWithNameValue> CreateNameValueItems(int uniqueCount, int reuseCount)
|
||||
{
|
||||
var items = new List<TestClassWithNameValue>();
|
||||
|
||||
for (int i = 0; i < uniqueCount; i++)
|
||||
{
|
||||
items.Add(new TestClassWithNameValue
|
||||
{
|
||||
Name = $"UniqueName_{i:D4}",
|
||||
Value = $"UniqueValue_{i:D4}"
|
||||
});
|
||||
}
|
||||
|
||||
for (int i = 0; i < reuseCount; i++)
|
||||
{
|
||||
items.Add(new TestClassWithNameValue
|
||||
{
|
||||
Name = $"UniqueName_{i % 10:D4}",
|
||||
Value = $"UniqueValue_{(i + 10) % uniqueCount:D4}"
|
||||
});
|
||||
}
|
||||
|
||||
return items;
|
||||
}
|
||||
|
||||
public static List<TestClassWithNullableStrings> CreateNullableStringItems(int count)
|
||||
{
|
||||
return Enumerable.Range(0, count).Select(i => new TestClassWithNullableStrings
|
||||
{
|
||||
Id = i,
|
||||
RequiredName = $"Required_{i:D3}",
|
||||
OptionalName = i % 3 == 0 ? null : i % 3 == 1 ? "" : $"Optional_{i:D3}",
|
||||
Description = i % 2 == 0 ? $"Description_{i % 5:D3}" : null
|
||||
}).ToList();
|
||||
}
|
||||
|
||||
public static List<TestCustomerLikeDto> CreateCustomerLikeItems(int count)
|
||||
{
|
||||
return Enumerable.Range(0, count).Select(i => new TestCustomerLikeDto
|
||||
{
|
||||
Id = i,
|
||||
FirstName = $"FirstName_{i % 10}",
|
||||
LastName = $"LastName_{i % 8}",
|
||||
Email = $"user{i}@example.com",
|
||||
Company = $"Company_{i % 5}",
|
||||
Department = i % 3 == 0 ? "Sales" : i % 3 == 1 ? "Engineering" : "Marketing",
|
||||
Role = i % 4 == 0 ? "Admin" : i % 4 == 1 ? "User" : i % 4 == 2 ? "Manager" : "Guest",
|
||||
Status = i % 2 == 0 ? "Active" : "Inactive",
|
||||
Attributes =
|
||||
[
|
||||
new() { Key = "DateOfReceipt", Value = $"{i % 28 + 1}/24/2025 00:27:00" },
|
||||
new() { Key = "Priority", Value = (i % 5).ToString() },
|
||||
new() { Key = "CustomField1", Value = $"CustomValue_{i % 7}" },
|
||||
new() { Key = "CustomField2", Value = i % 2 == 0 ? "Yes" : "No_Value" }
|
||||
]
|
||||
}).ToList();
|
||||
}
|
||||
|
||||
public static List<TestHighReuseDto> CreateHighReuseItems(int count)
|
||||
{
|
||||
return Enumerable.Range(0, count).Select(i => new TestHighReuseDto
|
||||
{
|
||||
Id = i,
|
||||
CategoryCode = $"CAT_{i % 10:D2}",
|
||||
StatusCode = $"STATUS_{i % 5:D2}",
|
||||
TypeCode = $"TYPE_{i % 3:D2}",
|
||||
PriorityCode = i % 2 == 0 ? "PRIORITY_HIGH" : "PRIORITY_LOW",
|
||||
UniqueField = $"UNIQUE_{i:D4}"
|
||||
}).ToList();
|
||||
}
|
||||
|
||||
public static List<TestClassWithNullableProperties> CreateNullablePropertyItems(int count)
|
||||
{
|
||||
return Enumerable.Range(1, count).Select(i => new TestClassWithNullableProperties
|
||||
{
|
||||
Id = i,
|
||||
NullableInt = i % 3 == 0 ? null : i * 10,
|
||||
NullableDouble = i % 2 == 0 ? null : i * 1.5,
|
||||
NullableDateTime = i % 4 == 0 ? null : DateTime.UtcNow.AddDays(-i),
|
||||
NullableGuid = i % 5 == 0 ? null : Guid.NewGuid()
|
||||
}).ToList();
|
||||
}
|
||||
|
||||
public static TestStockTaking CreateStockTaking(int itemCount = 2, int palletCount = 2)
|
||||
{
|
||||
return new TestStockTaking
|
||||
{
|
||||
Id = 1,
|
||||
StartDateTime = DateTime.UtcNow,
|
||||
IsClosed = false,
|
||||
Creator = 100,
|
||||
Created = DateTime.UtcNow.AddHours(-2),
|
||||
Modified = DateTime.UtcNow,
|
||||
StockTakingItems = Enumerable.Range(1, itemCount).Select(i => new TestStockTakingItem
|
||||
{
|
||||
Id = i * 10,
|
||||
StockTakingId = 1,
|
||||
ProductId = 500 + i,
|
||||
IsMeasured = i % 2 == 0,
|
||||
OriginalStockQuantity = 50 * i,
|
||||
MeasuredStockQuantity = 48 * i,
|
||||
Created = DateTime.UtcNow.AddHours(-i),
|
||||
Modified = DateTime.UtcNow,
|
||||
StockTakingItemPallets = Enumerable.Range(1, palletCount).Select(p => new TestStockTakingItemPallet
|
||||
{
|
||||
Id = i * 100 + p,
|
||||
StockTakingItemId = i * 10,
|
||||
TrayQuantity = p + 2,
|
||||
TareWeight = p * 0.5,
|
||||
PalletWeight = p * 10.0,
|
||||
GrossWeight = p * 50.0,
|
||||
IsMeasured = p % 2 == 0,
|
||||
CreatorId = p % 2 == 0 ? 100 : null,
|
||||
ModifierId = p % 2 == 1 ? 200 : null,
|
||||
Created = DateTime.UtcNow,
|
||||
Modified = DateTime.UtcNow
|
||||
}).ToList()
|
||||
}).ToList()
|
||||
};
|
||||
}
|
||||
|
||||
public static List<TestStockTaking> CreateStockTakingList(int count, int itemsPerStock = 1, int palletsPerItem = 1)
|
||||
{
|
||||
return Enumerable.Range(1, count).Select(s => new TestStockTaking
|
||||
{
|
||||
Id = s,
|
||||
StartDateTime = DateTime.UtcNow.AddDays(-s),
|
||||
IsClosed = s % 2 == 0,
|
||||
Creator = s,
|
||||
Created = DateTime.UtcNow.AddDays(-s),
|
||||
Modified = DateTime.UtcNow,
|
||||
StockTakingItems = Enumerable.Range(1, itemsPerStock).Select(i => new TestStockTakingItem
|
||||
{
|
||||
Id = s * 100 + i,
|
||||
StockTakingId = s,
|
||||
ProductId = 1000 * s + i,
|
||||
IsMeasured = i % 2 == 0,
|
||||
OriginalStockQuantity = 10 * i,
|
||||
MeasuredStockQuantity = 10 * i,
|
||||
Created = DateTime.UtcNow,
|
||||
Modified = DateTime.UtcNow,
|
||||
StockTakingItemPallets = Enumerable.Range(1, palletsPerItem).Select(p => new TestStockTakingItemPallet
|
||||
{
|
||||
Id = s * 1000 + i * 100 + p,
|
||||
StockTakingItemId = s * 100 + i,
|
||||
CreatorId = p % 2 == 0 ? s * 10 : null,
|
||||
ModifierId = p % 2 == 1 ? s * 20 : null,
|
||||
TrayQuantity = p,
|
||||
TareWeight = p * 1.0,
|
||||
PalletWeight = p * 10.0,
|
||||
GrossWeight = p * 50.0,
|
||||
IsMeasured = p % 2 == 0,
|
||||
Created = DateTime.UtcNow,
|
||||
Modified = DateTime.UtcNow
|
||||
}).ToList()
|
||||
}).ToList()
|
||||
}).ToList();
|
||||
}
|
||||
|
||||
public static List<TestEntityWithDateTimeAndInt> CreateDateTimeEntities(int count)
|
||||
{
|
||||
return Enumerable.Range(1, count).Select(i => new TestEntityWithDateTimeAndInt
|
||||
{
|
||||
Id = i,
|
||||
IntValue = i * 10,
|
||||
Created = DateTime.UtcNow.AddDays(-i),
|
||||
Modified = DateTime.UtcNow.AddHours(-i),
|
||||
StatusCode = i % 5,
|
||||
Name = $"Entity_{i}"
|
||||
}).ToList();
|
||||
}
|
||||
|
||||
public static List<TestParentWithDateTimeItemCollection> CreateParentWithDateTimeItems(int parentCount, int itemsPerParent)
|
||||
{
|
||||
return Enumerable.Range(1, parentCount).Select(p => new TestParentWithDateTimeItemCollection
|
||||
{
|
||||
Id = p,
|
||||
Name = $"Parent_{p}",
|
||||
Created = DateTime.UtcNow.AddDays(-p * 10),
|
||||
Items = Enumerable.Range(1, itemsPerParent).Select(i => new TestEntityWithDateTimeAndInt
|
||||
{
|
||||
Id = p * 100 + i,
|
||||
IntValue = i * 10,
|
||||
Created = DateTime.UtcNow.AddDays(-i),
|
||||
Modified = DateTime.UtcNow.AddHours(-i),
|
||||
StatusCode = i % 3,
|
||||
Name = $"Parent{p}_Item_{i}"
|
||||
}).ToList()
|
||||
}).ToList();
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,389 @@
|
|||
using AyCode.Interfaces.Entities;
|
||||
using AyCode.Interfaces.TimeStampInfo;
|
||||
using Newtonsoft.Json;
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
using System.ComponentModel.DataAnnotations.Schema;
|
||||
using System.Linq.Expressions;
|
||||
using static System.Net.Mime.MediaTypeNames;
|
||||
|
||||
namespace AyCode.Core.Tests.Serialization;
|
||||
|
||||
|
||||
public abstract partial class BaseEntity : IBaseEntity
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets or sets the entity identifier
|
||||
/// </summary>
|
||||
public int Id { get; set; }
|
||||
|
||||
public override string ToString()
|
||||
{
|
||||
return $"{GetType().Name} [Id: {Id}]";
|
||||
}
|
||||
}
|
||||
|
||||
public interface IBaseEntity //: IEntityInt
|
||||
{
|
||||
public int Id { get; set; }
|
||||
}
|
||||
|
||||
public abstract class MgEntityBase : BaseEntity, IEntityInt
|
||||
{
|
||||
public override string ToString()
|
||||
{
|
||||
return $"{GetType().Name}; Id: {Id}";
|
||||
}
|
||||
}
|
||||
public interface IMgStockTaking : IEntityInt, ITimeStampInfo
|
||||
{
|
||||
DateTime StartDateTime { get; set; }
|
||||
bool IsClosed { get; set; }
|
||||
bool IsReadyForClose();
|
||||
}
|
||||
|
||||
public abstract class MgStockTaking<TStockTakingItem> : MgEntityBase, IMgStockTaking
|
||||
where TStockTakingItem : class, IMgStockTakingItem
|
||||
{
|
||||
public DateTime StartDateTime { get; set; }
|
||||
public bool IsClosed { get; set; }
|
||||
|
||||
public abstract bool IsReadyForClose();
|
||||
|
||||
public List<TStockTakingItem>? StockTakingItems { get; set; }
|
||||
|
||||
public int Creator { get; set; }
|
||||
public DateTime Created { get; set; }
|
||||
public DateTime Modified { get; set; }
|
||||
}
|
||||
|
||||
public interface IMgStockTakingItem : IEntityInt, ITimeStampInfo
|
||||
{
|
||||
int StockTakingId { get; set; }
|
||||
int ProductId { get; set; }
|
||||
bool IsMeasured { get; set; }
|
||||
int OriginalStockQuantity { get; set; }
|
||||
int MeasuredStockQuantity { get; set; }
|
||||
}
|
||||
|
||||
public abstract class MgStockTakingItem<TStockTaking, TProduct> : MgEntityBase, IMgStockTakingItem
|
||||
where TStockTaking : class, IMgStockTaking
|
||||
where TProduct : class, IMgProductDto
|
||||
{
|
||||
public int StockTakingId { get; set; }
|
||||
public int ProductId { get; set; }
|
||||
public bool IsMeasured { get; set; }
|
||||
public int OriginalStockQuantity { get; set; }
|
||||
public int MeasuredStockQuantity { get; set; }
|
||||
|
||||
public TStockTaking? StockTaking { get; set; }
|
||||
|
||||
public TProduct? Product { get; set; }
|
||||
|
||||
public DateTime Created { get; set; }
|
||||
public DateTime Modified { get; set; }
|
||||
}
|
||||
|
||||
|
||||
public class StockTaking : MgStockTaking<StockTakingItem>
|
||||
{
|
||||
public override bool IsReadyForClose()
|
||||
{
|
||||
if (StockTakingItems == null || StockTakingItems.Count == 0) return false;
|
||||
return StockTakingItems
|
||||
.Where(stockTakingItem => stockTakingItem is { IsRequiredForMeasuring: true, IsInvalid: false })
|
||||
.All(x => x.IsMeasured);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
public class StockTakingItem : MgStockTakingItem<StockTaking, ProductDto>
|
||||
{
|
||||
public bool IsMeasurable { get; set; }
|
||||
|
||||
public double OriginalNetWeight { get; set; }
|
||||
|
||||
public double MeasuredNetWeight { get; set; }
|
||||
|
||||
public int InProcessOrdersQuantity { get; set; }
|
||||
|
||||
[NotMapped, JsonIgnore, System.Text.Json.Serialization.JsonIgnore]
|
||||
public int TotalOriginalQuantity => OriginalStockQuantity + InProcessOrdersQuantity;
|
||||
|
||||
[NotMapped, JsonIgnore, System.Text.Json.Serialization.JsonIgnore]
|
||||
public int QuantityDiff => IsMeasured ? MeasuredStockQuantity - TotalOriginalQuantity : 0;
|
||||
|
||||
[NotMapped, JsonIgnore, System.Text.Json.Serialization.JsonIgnore]
|
||||
public double NetWeightDiff => IsMeasurable && IsMeasured ? double.Round(MeasuredNetWeight - OriginalNetWeight, 1) : 0d;
|
||||
|
||||
public List<StockTakingItemPallet>? StockTakingItemPallets { get; set; }
|
||||
|
||||
[NotMapped, JsonIgnore, System.Text.Json.Serialization.JsonIgnore]
|
||||
public bool IsRequiredForMeasuring => !IsInvalid && (TotalOriginalQuantity != 0 || OriginalNetWeight != 0);
|
||||
|
||||
[NotMapped, JsonIgnore, System.Text.Json.Serialization.JsonIgnore]
|
||||
public bool IsInvalid => TotalOriginalQuantity < 0;
|
||||
|
||||
[NotMapped, JsonIgnore, System.Text.Json.Serialization.JsonIgnore]
|
||||
public string DisplayText
|
||||
{
|
||||
get
|
||||
{
|
||||
if (IsInvalid) return $"[HIBA] {Product!.Name}";
|
||||
if (IsMeasured) return $"[KÉSZ] {Product!.Name}";
|
||||
return IsRequiredForMeasuring ? $"[KÖT] {Product!.Name}" : $"{Product!.Name}";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public class StockTakingItemPallet : MeasuringItemPalletBase
|
||||
{
|
||||
public int StockTakingItemId
|
||||
{
|
||||
get => ForeignItemId;
|
||||
set => ForeignItemId = value;
|
||||
}
|
||||
|
||||
public StockTakingItem? StockTakingItem { get; set; }
|
||||
|
||||
public override double CalculateNetWeight() => base.CalculateNetWeight();
|
||||
|
||||
public override bool IsValidSafeMeasuringValues()
|
||||
{
|
||||
return StockTakingItemId > 0 && TrayQuantity >= 0 && TareWeight >= 0 && PalletWeight >= 0 && NetWeight >= 0 && GrossWeight >= 0;
|
||||
}
|
||||
|
||||
public override bool IsValidMeasuringValues(bool isMeasurable)
|
||||
{
|
||||
return StockTakingItemId > 0 && TrayQuantity >= 0 &&
|
||||
((!isMeasurable && NetWeight == 0 && GrossWeight == 0 && PalletWeight == 0 && TareWeight == 0)
|
||||
|| (isMeasurable && NetWeight >= 0 && GrossWeight >= 0 && PalletWeight >= 0 && TareWeight >= 0));
|
||||
}
|
||||
|
||||
public override void SetParentPropToNull() => StockTakingItem = null;
|
||||
}
|
||||
|
||||
public abstract class MeasuringItemPalletBase : MgEntityBase
|
||||
{
|
||||
private double _palletWeight;
|
||||
private double _grossWeight;
|
||||
private double _tareWeight;
|
||||
|
||||
protected int ForeignItemId;
|
||||
|
||||
public int ForeignKey => ForeignItemId;
|
||||
|
||||
public int TrayQuantity { get; set; }
|
||||
|
||||
public double TareWeight
|
||||
{
|
||||
get => _tareWeight;
|
||||
set => _tareWeight = double.Round(value, 1);
|
||||
}
|
||||
|
||||
public double PalletWeight
|
||||
{
|
||||
get => _palletWeight;
|
||||
set => _palletWeight = double.Round(value, 0);
|
||||
}
|
||||
|
||||
[System.ComponentModel.DataAnnotations.Schema.NotMapped, JsonIgnore, System.Text.Json.Serialization.JsonIgnore]
|
||||
public double NetWeight
|
||||
{
|
||||
get => CalculateNetWeight();
|
||||
set => throw new Exception($"MeasuringItemPalletBase.NetWeight not set");
|
||||
}
|
||||
|
||||
public double GrossWeight
|
||||
{
|
||||
get => _grossWeight;
|
||||
set => _grossWeight = double.Round(value, 1);
|
||||
}
|
||||
|
||||
public bool IsMeasured { get; set; }
|
||||
|
||||
public int? CreatorId { get; set; }
|
||||
public int? ModifierId { get; set; }
|
||||
|
||||
public DateTime Created { get; set; }
|
||||
public DateTime Modified { get; set; }
|
||||
|
||||
public abstract void SetParentPropToNull();
|
||||
public void SetForeignKey(int foreignKey) => ForeignItemId = foreignKey;
|
||||
public virtual double CalculateNetWeight() => double.Round(GrossWeight - PalletWeight - (TareWeight * TrayQuantity), 1);
|
||||
|
||||
public virtual bool IsValidSafeMeasuringValues()
|
||||
{
|
||||
return TrayQuantity > 0 && TareWeight >= 0 && PalletWeight >= 0 && NetWeight >= 0 && GrossWeight >= 0;
|
||||
}
|
||||
|
||||
public virtual bool IsValidMeasuringValues(bool isMeasurable)
|
||||
{
|
||||
return TrayQuantity > 0 &&
|
||||
((!isMeasurable && NetWeight == 0 && GrossWeight == 0 && PalletWeight == 0 && TareWeight == 0)
|
||||
|| (isMeasurable && NetWeight > 0 && GrossWeight > 0 && PalletWeight >= 0 && TareWeight >= 0));
|
||||
}
|
||||
|
||||
public bool IsMeasuredAndValid(bool isMeasurable)
|
||||
{
|
||||
return Id > 0 && IsMeasured && IsValidMeasuringValues(isMeasurable);
|
||||
}
|
||||
|
||||
public virtual void SetupCustomItemPalletMeauringValues(bool isMeasurable)
|
||||
{
|
||||
if (!isMeasurable)
|
||||
{
|
||||
TareWeight = 0;
|
||||
PalletWeight = 0;
|
||||
GrossWeight = 0;
|
||||
}
|
||||
IsMeasured = IsValidMeasuringValues(isMeasurable);
|
||||
}
|
||||
|
||||
public override string ToString()
|
||||
{
|
||||
return $"{base.ToString()} [ForeignItemId: {ForeignItemId}; IsMeasured: {IsMeasured}; PalletWeight: {PalletWeight}; TareWeight: {TareWeight}; Quantity: {TrayQuantity}; NetWeight: {NetWeight}; GrossWeight: {GrossWeight}]";
|
||||
}
|
||||
}
|
||||
|
||||
public interface IMgProductDto : IEntityInt
|
||||
{
|
||||
int ProductTypeId { get; set; }
|
||||
int ParentGroupedProductId { get; set; }
|
||||
|
||||
string Name { get; set; }
|
||||
string ShortDescription { get; set; }
|
||||
string FullDescription { get; set; }
|
||||
|
||||
int WarehouseId { get; set; }
|
||||
decimal Price { get; set; }
|
||||
int StockQuantity { get; set; }
|
||||
decimal ProductCost { get; set; }
|
||||
|
||||
decimal Weight { get; set; }
|
||||
decimal Length { get; set; }
|
||||
decimal Width { get; set; }
|
||||
decimal Height { get; set; }
|
||||
}
|
||||
|
||||
public class ProductDto : MgProductDto
|
||||
{
|
||||
public List<GenericAttributeDto> GenericAttributes { get; set; }
|
||||
|
||||
public ProductDto() :base()
|
||||
{ }
|
||||
public ProductDto(int productId) : base(productId)
|
||||
{ }
|
||||
//public ProductDto(Product product) : base(product)
|
||||
//{ }
|
||||
|
||||
[NotMapped, JsonIgnore, System.Text.Json.Serialization.JsonIgnore]
|
||||
public int AvailableQuantity => StockQuantity;
|
||||
|
||||
public bool HasMeasuringValues() => Id > 0;
|
||||
}
|
||||
|
||||
public abstract class MgProductDto : MgEntityBase, /*Product,*/ IMgProductDto//IModelDtoBase<Product>//, IDiscountSupported<DiscountProductMapping>
|
||||
{
|
||||
//public int Id { get; set; }
|
||||
public int ProductTypeId { get; set; }
|
||||
public int ParentGroupedProductId { get; set; }
|
||||
|
||||
public string Name { get; set; }
|
||||
public string ShortDescription { get; set; }
|
||||
public string FullDescription { get; set; }
|
||||
|
||||
public int WarehouseId { get; set; }
|
||||
public decimal Price { get; set; }
|
||||
public int StockQuantity { get; set; }
|
||||
public decimal ProductCost { get; set; }
|
||||
|
||||
public decimal Weight { get; set; }
|
||||
public decimal Length { get; set; }
|
||||
public decimal Width { get; set; }
|
||||
public decimal Height { get; set; }
|
||||
|
||||
public bool Deleted { get; set; }
|
||||
|
||||
public bool SubjectToAcl { get; set; }
|
||||
public bool LimitedToStores { get; set; }
|
||||
|
||||
protected MgProductDto() :base()
|
||||
{ }
|
||||
|
||||
protected MgProductDto(int productId)
|
||||
{
|
||||
Id = productId;
|
||||
}
|
||||
}
|
||||
|
||||
public class GenericAttributeDto : MgGenericAttributeDto
|
||||
{
|
||||
|
||||
}
|
||||
|
||||
public abstract class MgGenericAttributeDto : GenericAttribute
|
||||
{
|
||||
public GenericAttribute CreateMainEntity()
|
||||
{
|
||||
var mainEntity = Activator.CreateInstance<GenericAttribute>();
|
||||
CopyDtoValuesToEntity(mainEntity);
|
||||
|
||||
mainEntity.CreatedOrUpdatedDateUTC = DateTime.UtcNow;
|
||||
return mainEntity;
|
||||
}
|
||||
|
||||
public void CopyDtoValuesToEntity(GenericAttribute entity)
|
||||
{
|
||||
entity.Id = Id;
|
||||
entity.Key = Key;
|
||||
entity.Value = Value;
|
||||
entity.EntityId = EntityId;
|
||||
entity.KeyGroup = KeyGroup;
|
||||
entity.StoreId = StoreId;
|
||||
entity.CreatedOrUpdatedDateUTC = CreatedOrUpdatedDateUTC;
|
||||
}
|
||||
|
||||
public void CopyEntityValuesToDto(GenericAttribute entity)
|
||||
{
|
||||
Id = entity.Id;
|
||||
Key = entity.Key;
|
||||
Value = entity.Value;
|
||||
EntityId = entity.EntityId;
|
||||
KeyGroup = entity.KeyGroup;
|
||||
StoreId = entity.StoreId;
|
||||
CreatedOrUpdatedDateUTC = entity.CreatedOrUpdatedDateUTC;
|
||||
}
|
||||
}
|
||||
|
||||
public partial class GenericAttribute : BaseEntity
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets or sets the entity identifier
|
||||
/// </summary>
|
||||
public int EntityId { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the key group
|
||||
/// </summary>
|
||||
public string KeyGroup { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the key
|
||||
/// </summary>
|
||||
public string Key { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the value
|
||||
/// </summary>
|
||||
public string Value { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the store identifier
|
||||
/// </summary>
|
||||
public int StoreId { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the created or updated date
|
||||
/// </summary>
|
||||
public DateTime? CreatedOrUpdatedDateUTC { get; set; }
|
||||
}
|
||||
|
|
@ -0,0 +1,187 @@
|
|||
using System.Buffers;
|
||||
using System.IO.Compression;
|
||||
using System.Runtime.CompilerServices;
|
||||
using System.Text;
|
||||
|
||||
namespace AyCode.Core.Compression;
|
||||
|
||||
/// <summary>
|
||||
/// GZip compression/decompression helper for SignalR message transport.
|
||||
/// Matches BrotliHelper API to make switching effortless.
|
||||
/// </summary>
|
||||
public static class GzipHelper
|
||||
{
|
||||
private const int DefaultBufferSize = 4096;
|
||||
private const int MaxStackAllocSize = 1024;
|
||||
|
||||
#region Compression
|
||||
|
||||
/// <summary>
|
||||
/// Compresses a string using GZip compression with pooled buffers.
|
||||
/// </summary>
|
||||
public static byte[] Compress(string text, CompressionLevel compressionLevel = CompressionLevel.Optimal)
|
||||
{
|
||||
if (string.IsNullOrEmpty(text))
|
||||
return [];
|
||||
|
||||
var maxByteCount = Encoding.UTF8.GetMaxByteCount(text.Length);
|
||||
|
||||
if (maxByteCount <= MaxStackAllocSize)
|
||||
{
|
||||
Span<byte> utf8Bytes = stackalloc byte[maxByteCount];
|
||||
var actualLength = Encoding.UTF8.GetBytes(text.AsSpan(), utf8Bytes);
|
||||
return CompressSpan(utf8Bytes[..actualLength], compressionLevel);
|
||||
}
|
||||
|
||||
var rentedBuffer = ArrayPool<byte>.Shared.Rent(maxByteCount);
|
||||
try
|
||||
{
|
||||
var actualLength = Encoding.UTF8.GetBytes(text.AsSpan(), rentedBuffer);
|
||||
return CompressSpan(rentedBuffer.AsSpan(0, actualLength), compressionLevel);
|
||||
}
|
||||
finally
|
||||
{
|
||||
ArrayPool<byte>.Shared.Return(rentedBuffer);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Compresses a byte array using GZip compression.
|
||||
/// </summary>
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public static byte[] Compress(byte[] data, CompressionLevel compressionLevel = CompressionLevel.Optimal)
|
||||
=> data == null || data.Length == 0 ? [] : CompressSpan(data.AsSpan(), compressionLevel);
|
||||
|
||||
/// <summary>
|
||||
/// Compresses a ReadOnlySpan using GZip compression with pooled output buffer.
|
||||
/// </summary>
|
||||
public static byte[] CompressSpan(ReadOnlySpan<byte> data, CompressionLevel compressionLevel = CompressionLevel.Optimal)
|
||||
{
|
||||
if (data.IsEmpty)
|
||||
return [];
|
||||
|
||||
using var outputStream = new MemoryStream();
|
||||
using (var gzipStream = new GZipStream(outputStream, compressionLevel, leaveOpen: true))
|
||||
{
|
||||
gzipStream.Write(data);
|
||||
}
|
||||
|
||||
return outputStream.ToArray();
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Decompression
|
||||
|
||||
/// <summary>
|
||||
/// Decompresses GZip-compressed data to a string.
|
||||
/// </summary>
|
||||
public static string DecompressToString(byte[] compressedData)
|
||||
{
|
||||
if (compressedData == null || compressedData.Length == 0)
|
||||
return string.Empty;
|
||||
|
||||
var decompressedBytes = Decompress(compressedData);
|
||||
return Encoding.UTF8.GetString(decompressedBytes);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Decompresses GZip-compressed data to a byte array.
|
||||
/// </summary>
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public static byte[] Decompress(byte[] compressedData)
|
||||
{
|
||||
if (compressedData == null || compressedData.Length == 0)
|
||||
return [];
|
||||
|
||||
return DecompressCore(compressedData);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Decompresses GZip-compressed data from a ReadOnlySpan.
|
||||
/// </summary>
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public static byte[] DecompressSpan(ReadOnlySpan<byte> compressedData)
|
||||
{
|
||||
if (compressedData.IsEmpty)
|
||||
return [];
|
||||
|
||||
return DecompressCore(compressedData.ToArray());
|
||||
}
|
||||
|
||||
private static byte[] DecompressCore(byte[] compressedData)
|
||||
{
|
||||
using var inputStream = new MemoryStream(compressedData, writable: false);
|
||||
using var gzipStream = new GZipStream(inputStream, CompressionMode.Decompress);
|
||||
using var outputStream = new MemoryStream();
|
||||
|
||||
gzipStream.CopyTo(outputStream);
|
||||
return outputStream.ToArray();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Decompresses to a rented buffer. Caller must return the buffer to ArrayPool.
|
||||
/// Returns the actual decompressed length.
|
||||
/// </summary>
|
||||
public static (byte[] Buffer, int Length) DecompressToRentedBuffer(ReadOnlySpan<byte> compressedData)
|
||||
{
|
||||
if (compressedData.IsEmpty)
|
||||
return ([], 0);
|
||||
|
||||
var estimatedSize = Math.Max(compressedData.Length * 4, DefaultBufferSize);
|
||||
var outputBuffer = ArrayPool<byte>.Shared.Rent(estimatedSize);
|
||||
|
||||
using var inputStream = new MemoryStream(compressedData.ToArray(), writable: false);
|
||||
using var gzipStream = new GZipStream(inputStream, CompressionMode.Decompress);
|
||||
|
||||
var totalRead = 0;
|
||||
int bytesRead;
|
||||
|
||||
while ((bytesRead = gzipStream.Read(outputBuffer.AsSpan(totalRead))) > 0)
|
||||
{
|
||||
totalRead += bytesRead;
|
||||
|
||||
if (totalRead >= outputBuffer.Length - DefaultBufferSize)
|
||||
{
|
||||
var newBuffer = ArrayPool<byte>.Shared.Rent(outputBuffer.Length * 2);
|
||||
outputBuffer.AsSpan(0, totalRead).CopyTo(newBuffer);
|
||||
ArrayPool<byte>.Shared.Return(outputBuffer);
|
||||
outputBuffer = newBuffer;
|
||||
}
|
||||
}
|
||||
|
||||
return (outputBuffer, totalRead);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Utility
|
||||
|
||||
/// <summary>
|
||||
/// Checks if the data appears to be GZip compressed.
|
||||
/// </summary>
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public static bool IsGzipCompressed(byte[] data)
|
||||
{
|
||||
if (data == null || data.Length < 2)
|
||||
return false;
|
||||
|
||||
// 0x1F, 0x8B are the fixed gzip header bytes
|
||||
if (data[0] != 0x1F || data[1] != 0x8B)
|
||||
return false;
|
||||
|
||||
try
|
||||
{
|
||||
using var inputStream = new MemoryStream(data, 0, Math.Min(data.Length, 64), writable: false);
|
||||
using var gzipStream = new GZipStream(inputStream, CompressionMode.Decompress);
|
||||
Span<byte> buffer = stackalloc byte[1];
|
||||
return gzipStream.Read(buffer) >= 0;
|
||||
}
|
||||
catch
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
#endregion
|
||||
}
|
||||
|
|
@ -16,7 +16,10 @@ public static partial class AcBinaryDeserializer
|
|||
private List<string>? _internedStrings;
|
||||
private List<string>? _propertyNames;
|
||||
private Dictionary<int, object>? _objectReferences;
|
||||
private Dictionary<int, string>? _stringCache;
|
||||
private readonly byte _minStringInternLength;
|
||||
private readonly bool _useStringCaching;
|
||||
private readonly int _maxCachedStringLength;
|
||||
|
||||
public bool HasMetadata { get; private set; }
|
||||
public bool HasReferenceHandling { get; private set; }
|
||||
|
|
@ -27,17 +30,25 @@ public static partial class AcBinaryDeserializer
|
|||
public byte MinStringInternLength => _minStringInternLength;
|
||||
|
||||
public BinaryDeserializationContext(ReadOnlySpan<byte> data)
|
||||
: this(data, AcBinarySerializerOptions.Default)
|
||||
{
|
||||
}
|
||||
|
||||
public BinaryDeserializationContext(ReadOnlySpan<byte> data, AcBinarySerializerOptions options)
|
||||
{
|
||||
_buffer = data;
|
||||
_position = 0;
|
||||
_internedStrings = null;
|
||||
_propertyNames = null;
|
||||
_objectReferences = null;
|
||||
_stringCache = null;
|
||||
HasMetadata = false;
|
||||
HasReferenceHandling = false;
|
||||
IsMergeMode = false;
|
||||
RemoveOrphanedItems = false;
|
||||
_minStringInternLength = AcBinarySerializerOptions.Default.MinStringInternLength;
|
||||
_minStringInternLength = options.MinStringInternLength;
|
||||
_useStringCaching = options.UseStringCaching;
|
||||
_maxCachedStringLength = options.MaxCachedStringLength;
|
||||
}
|
||||
|
||||
public void ReadHeader()
|
||||
|
|
@ -229,17 +240,51 @@ public static partial class AcBinaryDeserializer
|
|||
return value;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Optimized VarInt reader with fast path for 1-2 byte values (most common case).
|
||||
/// Uses ZigZag decoding to handle signed integers.
|
||||
/// </summary>
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public int ReadVarInt()
|
||||
{
|
||||
var raw = ReadVarUInt();
|
||||
var temp = (int)raw;
|
||||
var value = (temp >> 1) ^ -(temp & 1);
|
||||
// ZigZag decode: handle full uint range before casting to int
|
||||
// This correctly handles values like int.MaxValue which encode to uint > int.MaxValue
|
||||
var value = (int)(raw >> 1) ^ -(int)(raw & 1);
|
||||
return value;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Optimized VarUInt reader with fast path for 1-2 byte values.
|
||||
/// Most VarInts in real data are small (property indices, array lengths, etc.)
|
||||
/// </summary>
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public uint ReadVarUInt()
|
||||
{
|
||||
// Fast path: single byte (0-127) - ~70% of cases
|
||||
var b0 = _buffer[_position];
|
||||
if ((b0 & 0x80) == 0)
|
||||
{
|
||||
_position++;
|
||||
return b0;
|
||||
}
|
||||
|
||||
// Fast path: two bytes (128-16383) - ~25% of cases
|
||||
if (_position + 1 < _buffer.Length)
|
||||
{
|
||||
var b1 = _buffer[_position + 1];
|
||||
if ((b1 & 0x80) == 0)
|
||||
{
|
||||
_position += 2;
|
||||
return (uint)(b0 & 0x7F) | ((uint)b1 << 7);
|
||||
}
|
||||
}
|
||||
|
||||
// Slow path: 3+ bytes - ~5% of cases
|
||||
return ReadVarUIntSlow();
|
||||
}
|
||||
|
||||
private uint ReadVarUIntSlow()
|
||||
{
|
||||
uint value = 0;
|
||||
var shift = 0;
|
||||
|
|
@ -309,6 +354,9 @@ public static partial class AcBinaryDeserializer
|
|||
return result;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Read UTF8 string with optional caching for WASM optimization.
|
||||
/// </summary>
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public string ReadStringUtf8(int length)
|
||||
{
|
||||
|
|
@ -318,11 +366,67 @@ public static partial class AcBinaryDeserializer
|
|||
}
|
||||
|
||||
EnsureAvailable(length);
|
||||
|
||||
// WASM optimization: cache short strings to reduce allocations
|
||||
if (_useStringCaching && length <= _maxCachedStringLength)
|
||||
{
|
||||
return ReadStringUtf8Cached(length);
|
||||
}
|
||||
|
||||
var value = Utf8NoBom.GetString(_buffer.Slice(_position, length));
|
||||
_position += length;
|
||||
return value;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Read string with caching - reduces allocations in WASM.
|
||||
/// </summary>
|
||||
private string ReadStringUtf8Cached(int length)
|
||||
{
|
||||
// Create hash from position and first few bytes for fast lookup
|
||||
var slice = _buffer.Slice(_position, length);
|
||||
var hash = ComputeStringHash(slice);
|
||||
|
||||
_stringCache ??= new Dictionary<int, string>(128);
|
||||
|
||||
if (_stringCache.TryGetValue(hash, out var cached))
|
||||
{
|
||||
// Verify it's actually the same string (hash collision check)
|
||||
if (cached.Length == length)
|
||||
{
|
||||
_position += length;
|
||||
return cached;
|
||||
}
|
||||
}
|
||||
|
||||
var value = Utf8NoBom.GetString(slice);
|
||||
_stringCache[hash] = value;
|
||||
_position += length;
|
||||
return value;
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
private static int ComputeStringHash(ReadOnlySpan<byte> data)
|
||||
{
|
||||
// Fast hash using first bytes and length
|
||||
var hash = data.Length;
|
||||
if (data.Length >= 4)
|
||||
{
|
||||
hash = HashCode.Combine(hash,
|
||||
MemoryMarshal.Read<int>(data));
|
||||
}
|
||||
else if (data.Length >= 2)
|
||||
{
|
||||
hash = HashCode.Combine(hash,
|
||||
MemoryMarshal.Read<short>(data));
|
||||
}
|
||||
else if (data.Length == 1)
|
||||
{
|
||||
hash = HashCode.Combine(hash, data[0]);
|
||||
}
|
||||
return hash;
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public void Skip(int count)
|
||||
{
|
||||
|
|
|
|||
|
|
@ -124,7 +124,38 @@ public static partial class AcBinaryDeserializer
|
|||
public object? GetValue(object target) => _getter(target);
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public void SetValue(object target, object? value) => _setter(target, value);
|
||||
public void SetValue(object target, object? value)
|
||||
{
|
||||
try
|
||||
{
|
||||
_setter(target, value);
|
||||
}
|
||||
catch (InvalidCastException ex)
|
||||
{
|
||||
var valueType = value?.GetType().FullName ?? "null";
|
||||
var propType = PropertyType.FullName;
|
||||
var targetTypeName = target.GetType().FullName;
|
||||
var underlyingType = Nullable.GetUnderlyingType(PropertyType);
|
||||
|
||||
// Get actual value info for debugging
|
||||
var valueInfo = value switch
|
||||
{
|
||||
null => "null",
|
||||
int i => $"int:{i}",
|
||||
long l => $"long:{l}",
|
||||
DateTime dt => $"DateTime:{dt:O}",
|
||||
string s => $"string:'{s}'",
|
||||
_ => $"{value.GetType().Name}:{value}"
|
||||
};
|
||||
|
||||
throw new InvalidCastException(
|
||||
$"Cannot set property '{Name}' on type '{targetTypeName}' - " +
|
||||
$"PropertyType: '{propType}', ValueType: '{valueType}', Value: {valueInfo}, " +
|
||||
$"IsNullable: {underlyingType != null}, UnderlyingType: {underlyingType?.FullName ?? "N/A"}, " +
|
||||
$"IsValueType: {PropertyType.IsValueType}, IsCollection: {IsCollection}, IsComplexType: {IsComplexType}",
|
||||
ex);
|
||||
}
|
||||
}
|
||||
|
||||
private static bool IsCollectionType(Type type)
|
||||
{
|
||||
|
|
@ -154,8 +185,30 @@ public static partial class AcBinaryDeserializer
|
|||
var valueParam = Expression.Parameter(typeof(object), "value");
|
||||
|
||||
var castTarget = Expression.Convert(targetParam, property.DeclaringType!);
|
||||
var castValue = Expression.Convert(valueParam, property.PropertyType);
|
||||
var propertyAccess = Expression.Property(castTarget, property);
|
||||
|
||||
Expression castValue;
|
||||
var propertyType = property.PropertyType;
|
||||
var underlyingType = Nullable.GetUnderlyingType(propertyType);
|
||||
|
||||
if (underlyingType != null)
|
||||
{
|
||||
// Nullable value type: first unbox to underlying type, then convert to nullable
|
||||
// This handles cases where we receive int but need to set int?
|
||||
var unboxed = Expression.Unbox(valueParam, underlyingType);
|
||||
castValue = Expression.Convert(unboxed, propertyType);
|
||||
}
|
||||
else if (propertyType.IsValueType)
|
||||
{
|
||||
// Non-nullable value type: use Unbox for proper unboxing
|
||||
castValue = Expression.Unbox(valueParam, propertyType);
|
||||
}
|
||||
else
|
||||
{
|
||||
// Reference type: use TypeAs for safe casting (returns null if incompatible)
|
||||
castValue = Expression.TypeAs(valueParam, propertyType);
|
||||
}
|
||||
|
||||
var assign = Expression.Assign(propertyAccess, castValue);
|
||||
return Expression.Lambda<Action<object, object?>>(assign, targetParam, valueParam).Compile();
|
||||
}
|
||||
|
|
|
|||
|
|
@ -110,18 +110,29 @@ public static partial class AcBinaryDeserializer
|
|||
/// Deserialize binary data to object of type T.
|
||||
/// </summary>
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public static T? Deserialize<T>(byte[] data) => Deserialize<T>(data.AsSpan());
|
||||
public static T? Deserialize<T>(byte[] data) => Deserialize<T>(data.AsSpan(), AcBinarySerializerOptions.Default);
|
||||
|
||||
/// <summary>
|
||||
/// Deserialize binary data to object of type T with options.
|
||||
/// </summary>
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public static T? Deserialize<T>(byte[] data, AcBinarySerializerOptions options) => Deserialize<T>(data.AsSpan(), options);
|
||||
|
||||
/// <summary>
|
||||
/// Deserialize binary data to object of type T.
|
||||
/// </summary>
|
||||
public static T? Deserialize<T>(ReadOnlySpan<byte> data)
|
||||
public static T? Deserialize<T>(ReadOnlySpan<byte> data) => Deserialize<T>(data, AcBinarySerializerOptions.Default);
|
||||
|
||||
/// <summary>
|
||||
/// Deserialize binary data to object of type T with options.
|
||||
/// </summary>
|
||||
public static T? Deserialize<T>(ReadOnlySpan<byte> data, AcBinarySerializerOptions options)
|
||||
{
|
||||
if (data.Length == 0) return default;
|
||||
if (data.Length == 1 && data[0] == BinaryTypeCode.Null) return default;
|
||||
|
||||
var targetType = typeof(T);
|
||||
var context = new BinaryDeserializationContext(data);
|
||||
var context = new BinaryDeserializationContext(data, options);
|
||||
|
||||
try
|
||||
{
|
||||
|
|
@ -144,12 +155,18 @@ public static partial class AcBinaryDeserializer
|
|||
/// <summary>
|
||||
/// Deserialize binary data to specified type.
|
||||
/// </summary>
|
||||
public static object? Deserialize(ReadOnlySpan<byte> data, Type targetType)
|
||||
public static object? Deserialize(ReadOnlySpan<byte> data, Type targetType)
|
||||
=> Deserialize(data, targetType, AcBinarySerializerOptions.Default);
|
||||
|
||||
/// <summary>
|
||||
/// Deserialize binary data to specified type with options.
|
||||
/// </summary>
|
||||
public static object? Deserialize(ReadOnlySpan<byte> data, Type targetType, AcBinarySerializerOptions options)
|
||||
{
|
||||
if (data.Length == 0) return null;
|
||||
if (data.Length == 1 && data[0] == BinaryTypeCode.Null) return null;
|
||||
|
||||
var context = new BinaryDeserializationContext(data);
|
||||
var context = new BinaryDeserializationContext(data, options);
|
||||
|
||||
try
|
||||
{
|
||||
|
|
@ -172,19 +189,31 @@ public static partial class AcBinaryDeserializer
|
|||
/// Populate existing object from binary data.
|
||||
/// </summary>
|
||||
public static void Populate<T>(byte[] data, T target) where T : class
|
||||
=> Populate(data.AsSpan(), target);
|
||||
=> Populate(data.AsSpan(), target, AcBinarySerializerOptions.Default);
|
||||
|
||||
/// <summary>
|
||||
/// Populate existing object from binary data with options.
|
||||
/// </summary>
|
||||
public static void Populate<T>(byte[] data, T target, AcBinarySerializerOptions options) where T : class
|
||||
=> Populate(data.AsSpan(), target, options);
|
||||
|
||||
/// <summary>
|
||||
/// Populate existing object from binary data.
|
||||
/// </summary>
|
||||
public static void Populate<T>(ReadOnlySpan<byte> data, T target) where T : class
|
||||
=> Populate(data, target, AcBinarySerializerOptions.Default);
|
||||
|
||||
/// <summary>
|
||||
/// Populate existing object from binary data with options.
|
||||
/// </summary>
|
||||
public static void Populate<T>(ReadOnlySpan<byte> data, T target, AcBinarySerializerOptions options) where T : class
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(target);
|
||||
if (data.Length == 0) return;
|
||||
if (data.Length == 1 && data[0] == BinaryTypeCode.Null) return;
|
||||
|
||||
var targetType = target.GetType();
|
||||
var context = new BinaryDeserializationContext(data);
|
||||
var context = new BinaryDeserializationContext(data, options);
|
||||
|
||||
try
|
||||
{
|
||||
|
|
@ -224,7 +253,7 @@ public static partial class AcBinaryDeserializer
|
|||
/// Populate with merge semantics for IId collections.
|
||||
/// </summary>
|
||||
public static void PopulateMerge<T>(ReadOnlySpan<byte> data, T target) where T : class
|
||||
=> PopulateMerge(data, target, null);
|
||||
=> PopulateMerge(data, target, AcBinarySerializerOptions.Default);
|
||||
|
||||
/// <summary>
|
||||
/// Populate with merge semantics for IId collections.
|
||||
|
|
@ -239,11 +268,12 @@ public static partial class AcBinaryDeserializer
|
|||
if (data.Length == 0) return;
|
||||
if (data.Length == 1 && data[0] == BinaryTypeCode.Null) return;
|
||||
|
||||
var opts = options ?? AcBinarySerializerOptions.Default;
|
||||
var targetType = target.GetType();
|
||||
var context = new BinaryDeserializationContext(data)
|
||||
var context = new BinaryDeserializationContext(data, opts)
|
||||
{
|
||||
IsMergeMode = true,
|
||||
RemoveOrphanedItems = options?.RemoveOrphanedItems ?? false
|
||||
RemoveOrphanedItems = opts.RemoveOrphanedItems
|
||||
};
|
||||
|
||||
try
|
||||
|
|
@ -489,9 +519,11 @@ public static partial class AcBinaryDeserializer
|
|||
{
|
||||
var propertyCount = (int)context.ReadVarUInt();
|
||||
var nextDepth = depth + 1;
|
||||
var targetTypeName = target.GetType().Name;
|
||||
|
||||
for (int i = 0; i < propertyCount; i++)
|
||||
{
|
||||
var propertyNameStartPosition = context.Position;
|
||||
string propertyName;
|
||||
if (context.HasMetadata)
|
||||
{
|
||||
|
|
@ -513,10 +545,21 @@ public static partial class AcBinaryDeserializer
|
|||
{
|
||||
propertyName = string.Empty;
|
||||
}
|
||||
else if (typeCode == BinaryTypeCode.StringInternNew)
|
||||
{
|
||||
propertyName = ReadAndRegisterInternedString(ref context);
|
||||
}
|
||||
else if (BinaryTypeCode.IsFixStr(typeCode))
|
||||
{
|
||||
// FixStr: short string with length encoded in type code
|
||||
var length = BinaryTypeCode.DecodeFixStrLength(typeCode);
|
||||
propertyName = length == 0 ? string.Empty : context.ReadStringUtf8(length);
|
||||
}
|
||||
else
|
||||
{
|
||||
throw new AcBinaryDeserializationException(
|
||||
$"Expected string for property name, got: {typeCode}",
|
||||
$"Expected string for property name, got: {typeCode} (0x{typeCode:X2}) at position {propertyNameStartPosition}. " +
|
||||
$"Target: {targetTypeName}, PropertyIndex: {i}/{propertyCount}, Depth: {depth}",
|
||||
context.Position, target.GetType());
|
||||
}
|
||||
}
|
||||
|
|
@ -556,8 +599,27 @@ public static partial class AcBinaryDeserializer
|
|||
}
|
||||
|
||||
// Default: read value and set (for primitives, strings, null cases)
|
||||
var value = ReadValue(ref context, propInfo.PropertyType, nextDepth);
|
||||
propInfo.SetValue(target, value);
|
||||
var positionBeforeRead = context.Position;
|
||||
try
|
||||
{
|
||||
var value = ReadValue(ref context, propInfo.PropertyType, nextDepth);
|
||||
propInfo.SetValue(target, value);
|
||||
}
|
||||
catch (InvalidCastException ex)
|
||||
{
|
||||
// Add context about which property and what byte code was at the read position
|
||||
throw new AcBinaryDeserializationException(
|
||||
$"Type mismatch for property '{propertyName}' (index {i}/{propertyCount}) on '{targetTypeName}'. " +
|
||||
$"Expected type: '{propInfo.PropertyType.FullName}'. " +
|
||||
$"PeekCode before read: {peekCode} (0x{peekCode:X2}). " +
|
||||
$"Position before read: {positionBeforeRead}, current: {context.Position}. " +
|
||||
$"Depth: {depth}. " +
|
||||
$"All target properties: [{string.Join(", ", metadata.PropertiesArray.Select(p => $"{p.Name}:{p.PropertyType.Name}"))}]. " +
|
||||
$"Error: {ex.Message}",
|
||||
positionBeforeRead,
|
||||
propInfo.PropertyType,
|
||||
ex);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -621,10 +683,19 @@ public static partial class AcBinaryDeserializer
|
|||
{
|
||||
propertyName = string.Empty;
|
||||
}
|
||||
else if (typeCode == BinaryTypeCode.StringInternNew)
|
||||
{
|
||||
propertyName = ReadAndRegisterInternedString(ref context);
|
||||
}
|
||||
else if (BinaryTypeCode.IsFixStr(typeCode))
|
||||
{
|
||||
var length = BinaryTypeCode.DecodeFixStrLength(typeCode);
|
||||
propertyName = length == 0 ? string.Empty : context.ReadStringUtf8(length);
|
||||
}
|
||||
else
|
||||
{
|
||||
throw new AcBinaryDeserializationException(
|
||||
$"Expected string for property name, got: {typeCode}",
|
||||
$"Expected string for property name, got: {typeCode} (0x{typeCode:X2})",
|
||||
context.Position, targetType);
|
||||
}
|
||||
}
|
||||
|
|
@ -1314,6 +1385,18 @@ public static partial class AcBinaryDeserializer
|
|||
// Just read the index, no registration needed
|
||||
context.ReadVarUInt();
|
||||
}
|
||||
else if (nameCode == BinaryTypeCode.StringInternNew)
|
||||
{
|
||||
// New interned string - must register even when skipping!
|
||||
SkipAndRegisterInternedString(ref context);
|
||||
}
|
||||
else if (BinaryTypeCode.IsFixStr(nameCode))
|
||||
{
|
||||
// FixStr: short string, just skip the bytes
|
||||
var length = BinaryTypeCode.DecodeFixStrLength(nameCode);
|
||||
if (length > 0)
|
||||
context.Skip(length);
|
||||
}
|
||||
// StringEmpty doesn't need any action
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -10,6 +10,11 @@ namespace AyCode.Core.Serializers.Binaries;
|
|||
/// </summary>
|
||||
public sealed class AcBinarySerializerOptions : AcSerializerOptions
|
||||
{
|
||||
/// <summary>
|
||||
/// Cached platform detection - true if running in WebAssembly/Browser environment.
|
||||
/// </summary>
|
||||
private static readonly bool DetectedIsWasm = OperatingSystem.IsBrowser();
|
||||
|
||||
public override AcSerializerType SerializerType { get; init; } = AcSerializerType.Binary;
|
||||
|
||||
/// <summary>
|
||||
|
|
@ -44,6 +49,37 @@ public sealed class AcBinarySerializerOptions : AcSerializerOptions
|
|||
UseReferenceHandling = false
|
||||
};
|
||||
|
||||
/// <summary>
|
||||
/// Options optimized for WASM environment with string caching enabled.
|
||||
/// </summary>
|
||||
public static readonly AcBinarySerializerOptions WasmOptimized = new()
|
||||
{
|
||||
IsWasm = true,
|
||||
UseStringCaching = true
|
||||
};
|
||||
|
||||
/// <summary>
|
||||
/// Whether running in WebAssembly/Browser environment.
|
||||
/// When true, enables WASM-specific optimizations like string caching.
|
||||
/// Default: auto-detected via OperatingSystem.IsBrowser()
|
||||
/// </summary>
|
||||
public bool IsWasm { get; init; } = DetectedIsWasm;
|
||||
|
||||
/// <summary>
|
||||
/// Whether to cache short strings during deserialization to reduce allocations.
|
||||
/// Most beneficial in WASM where GC is expensive.
|
||||
/// Auto-enabled when IsWasm is true, can be overridden.
|
||||
/// Default: follows IsWasm setting
|
||||
/// </summary>
|
||||
public bool UseStringCaching { get; init; } = DetectedIsWasm;
|
||||
|
||||
/// <summary>
|
||||
/// Maximum string length to cache when UseStringCaching is enabled.
|
||||
/// Longer strings are not cached to avoid memory bloat.
|
||||
/// Default: 64 characters
|
||||
/// </summary>
|
||||
public int MaxCachedStringLength { get; init; } = 64;
|
||||
|
||||
/// <summary>
|
||||
/// Whether to include metadata header with property names.
|
||||
/// When enabled, property names are stored once and referenced by index.
|
||||
|
|
|
|||
|
|
@ -5,6 +5,7 @@ using AyCode.Core.Tests.TestModels;
|
|||
using AyCode.Services.Server.SignalRs;
|
||||
using AyCode.Services.SignalRs;
|
||||
using MessagePack.Resolvers;
|
||||
using AyCode.Core.Tests.Serialization;
|
||||
|
||||
namespace AyCode.Services.Server.Tests.SignalRs;
|
||||
|
||||
|
|
@ -928,185 +929,68 @@ public abstract class SignalRClientToHubTestBase
|
|||
|
||||
#endregion
|
||||
|
||||
#region Property Mismatch Tests (Server has more properties than Client - tests SkipValue)
|
||||
|
||||
#region StockTaking Production Bug Reproduction
|
||||
|
||||
/// <summary>
|
||||
/// REGRESSION TEST: Tests the case where server sends a DTO with more properties than the client knows about.
|
||||
/// Bug: "Invalid interned string index: 15. Interned strings count: 12"
|
||||
/// Root cause: When deserializing, unknown properties are skipped via SkipValue(), but the skipped
|
||||
/// string values were not being registered in the intern table, causing index mismatch for later StringInterned references.
|
||||
/// CRITICAL PRODUCTION BUG TEST: Reproduces the exact GetStockTakings(false) scenario.
|
||||
/// Bug: "Type mismatch for property 'Created' (index 3/5) on 'StockTaking'"
|
||||
///
|
||||
/// This test simulates the production bug where CustomerDto had properties on server
|
||||
/// that the client didn't have defined.
|
||||
/// Root cause hypothesis: When IsClosed=false, the serializer skips it (default value optimization),
|
||||
/// but the first item has IsClosed=false while others have IsClosed=true.
|
||||
/// This may cause property index mismatch between server and client.
|
||||
/// Uses the REAL StockTaking model from StockTakingTestModels.cs
|
||||
/// </summary>
|
||||
[TestMethod]
|
||||
public async Task PropertyMismatch_ServerHasMoreProperties_DeserializesCorrectly()
|
||||
public async Task GetStockTakings_WithNullItems_RoundTrip()
|
||||
{
|
||||
// Arrange: Create "server" DTO with many properties
|
||||
var serverDto = new ServerCustomerDto
|
||||
{
|
||||
Id = 1,
|
||||
FirstName = "John",
|
||||
LastName = "Smith",
|
||||
Email = "john.smith@example.com",
|
||||
Phone = "+1-555-1234",
|
||||
Address = "123 Main Street",
|
||||
City = "New York",
|
||||
Country = "USA",
|
||||
PostalCode = "10001",
|
||||
Company = "Acme Corp",
|
||||
Department = "Engineering",
|
||||
Notes = "VIP customer with special requirements",
|
||||
Status = TestStatus.Active,
|
||||
IsVerified = true,
|
||||
LoginCount = 42,
|
||||
Balance = 1234.56m
|
||||
};
|
||||
// Simulate production: loadRelations = false
|
||||
var result = await _client.PostDataAsync<bool, List<StockTaking>>(
|
||||
TestSignalRTags.GetStockTakings, false);
|
||||
|
||||
// Act: Send server DTO, receive client DTO (fewer properties)
|
||||
// This simulates the real bug scenario
|
||||
var result = await _client.PostDataAsync<ServerCustomerDto, ClientCustomerDto>(
|
||||
TestSignalRTags.PropertyMismatchParam, serverDto);
|
||||
|
||||
// Assert: Client should receive only the properties it knows about
|
||||
Assert.IsNotNull(result, "Result should not be null - deserialization should succeed even with unknown properties");
|
||||
Assert.AreEqual(1, result.Id);
|
||||
Assert.AreEqual("John", result.FirstName);
|
||||
Assert.AreEqual("Smith", result.LastName);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// REGRESSION TEST: Tests a list of DTOs with property mismatch.
|
||||
/// This more closely simulates the production bug with GetMeasuringUsers returning List<CustomerDto>.
|
||||
/// </summary>
|
||||
[TestMethod]
|
||||
public async Task PropertyMismatch_ListOfDtos_WithManyProperties_DeserializesCorrectly()
|
||||
{
|
||||
// Arrange: Create list of "server" DTOs with many string properties
|
||||
var serverDtos = Enumerable.Range(0, 25).Select(i => new ServerCustomerDto
|
||||
{
|
||||
Id = i,
|
||||
FirstName = $"FirstName_{i % 10}", // 10 unique values (will be interned)
|
||||
LastName = $"LastName_{i % 8}", // 8 unique values
|
||||
Email = $"user{i}@example.com",
|
||||
Phone = $"+1-555-{i:D4}",
|
||||
Address = $"Address_{i % 5}", // 5 unique values
|
||||
City = i % 3 == 0 ? "New York" : i % 3 == 1 ? "Los Angeles" : "Chicago",
|
||||
Country = "USA",
|
||||
PostalCode = $"{10000 + i}",
|
||||
Company = $"Company_{i % 6}", // 6 unique values
|
||||
Department = i % 4 == 0 ? "Engineering" : i % 4 == 1 ? "Sales" : i % 4 == 2 ? "Marketing" : "Support",
|
||||
Notes = $"Notes for customer {i}",
|
||||
Status = (TestStatus)(i % 5),
|
||||
IsVerified = i % 2 == 0,
|
||||
LoginCount = i * 10,
|
||||
Balance = i * 100.50m
|
||||
}).ToList();
|
||||
|
||||
// Act: Send list of server DTOs, receive list of client DTOs
|
||||
var result = await _client.PostDataAsync<List<ServerCustomerDto>, List<ClientCustomerDto>>(
|
||||
TestSignalRTags.PropertyMismatchListParam, serverDtos);
|
||||
|
||||
// Assert
|
||||
Assert.IsNotNull(result, "Result should not be null");
|
||||
Assert.AreEqual(serverDtos.Count, result.Count, $"Expected {serverDtos.Count} items");
|
||||
Assert.AreEqual(5, result.Count, "Should have 5 StockTakings from production DB");
|
||||
|
||||
for (int i = 0; i < serverDtos.Count; i++)
|
||||
// Verify first item (IsClosed = false - this is where the bug occurs!)
|
||||
var first = result[0];
|
||||
Assert.AreEqual(7, first.Id, "First Id should be 7");
|
||||
Assert.AreEqual(6, first.Creator,
|
||||
$"First Creator should be 6, got {first.Creator}. " +
|
||||
"If this is a very large number, DateTime bytes were interpreted as int!");
|
||||
Assert.IsFalse(first.IsClosed, "First IsClosed should be false");
|
||||
Assert.IsNull(first.StockTakingItems, "StockTakingItems should be null when loadRelations=false");
|
||||
|
||||
// Verify second item (IsClosed = true)
|
||||
var second = result[1];
|
||||
Assert.AreEqual(6, second.Id, "Second Id should be 6");
|
||||
Assert.AreEqual(6, second.Creator, "Second Creator should be 6");
|
||||
Assert.IsTrue(second.IsClosed, "Second IsClosed should be true");
|
||||
|
||||
// Verify all items have correct Creator = 6
|
||||
foreach (var item in result)
|
||||
{
|
||||
Assert.AreEqual(serverDtos[i].Id, result[i].Id, $"Id mismatch at index {i}");
|
||||
Assert.AreEqual(serverDtos[i].FirstName, result[i].FirstName,
|
||||
$"FirstName mismatch at index {i}: expected '{serverDtos[i].FirstName}', got '{result[i].FirstName}'");
|
||||
Assert.AreEqual(serverDtos[i].LastName, result[i].LastName,
|
||||
$"LastName mismatch at index {i}: expected '{serverDtos[i].LastName}', got '{result[i].LastName}'");
|
||||
Assert.AreEqual(6, item.Creator,
|
||||
$"StockTaking Id={item.Id}: Creator should be 6, got {item.Creator}");
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// REGRESSION TEST: Tests nested objects being skipped when client doesn't know about them.
|
||||
/// Test with loadRelations = true (StockTakingItems is empty list, not null).
|
||||
/// </summary>
|
||||
[TestMethod]
|
||||
public async Task PropertyMismatch_NestedObjectsSkipped_DeserializesCorrectly()
|
||||
public async Task GetStockTakings_WithEmptyItems_RoundTrip()
|
||||
{
|
||||
// Arrange: Server order with nested customer object
|
||||
var serverOrder = new ServerOrderWithExtras
|
||||
var result = await _client.PostDataAsync<bool, List<StockTaking>>(
|
||||
TestSignalRTags.GetStockTakings, true);
|
||||
|
||||
Assert.IsNotNull(result, "Result should not be null");
|
||||
Assert.AreEqual(5, result.Count, "Should have 5 StockTakings");
|
||||
|
||||
foreach (var item in result)
|
||||
{
|
||||
Id = 100,
|
||||
OrderNumber = "ORD-2024-001",
|
||||
TotalAmount = 999.99m,
|
||||
Customer = new ServerCustomerDto
|
||||
{
|
||||
Id = 1,
|
||||
FirstName = "John",
|
||||
LastName = "Doe",
|
||||
Email = "john@example.com",
|
||||
Phone = "+1-555-0001"
|
||||
},
|
||||
RelatedCustomers =
|
||||
[
|
||||
new ServerCustomerDto { Id = 2, FirstName = "Jane", LastName = "Smith", Email = "jane@example.com" },
|
||||
new ServerCustomerDto { Id = 3, FirstName = "Bob", LastName = "Wilson", Email = "bob@example.com" }
|
||||
],
|
||||
InternalNotes = "Priority processing required",
|
||||
ProcessingCode = "RUSH-001"
|
||||
};
|
||||
|
||||
// Act: Send server order, receive simplified client order
|
||||
var result = await _client.PostDataAsync<ServerOrderWithExtras, ClientOrderSimple>(
|
||||
TestSignalRTags.PropertyMismatchNestedParam, serverOrder);
|
||||
|
||||
// Assert: Client should receive only basic order info
|
||||
Assert.IsNotNull(result);
|
||||
Assert.AreEqual(100, result.Id);
|
||||
Assert.AreEqual("ORD-2024-001", result.OrderNumber);
|
||||
Assert.AreEqual(999.99m, result.TotalAmount);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// REGRESSION TEST: Large list with nested objects being skipped.
|
||||
/// This is the most comprehensive test for the SkipValue string interning bug.
|
||||
/// </summary>
|
||||
[TestMethod]
|
||||
public async Task PropertyMismatch_LargeListWithNestedObjects_DeserializesCorrectly()
|
||||
{
|
||||
// Arrange: Create 50 orders with nested customers
|
||||
var serverOrders = Enumerable.Range(0, 50).Select(i => new ServerOrderWithExtras
|
||||
{
|
||||
Id = i,
|
||||
OrderNumber = $"ORD-{i:D4}",
|
||||
TotalAmount = i * 100.50m,
|
||||
Customer = new ServerCustomerDto
|
||||
{
|
||||
Id = i * 100,
|
||||
FirstName = $"Customer_{i % 10}",
|
||||
LastName = $"LastName_{i % 8}",
|
||||
Email = $"customer{i}@example.com",
|
||||
Company = $"Company_{i % 5}"
|
||||
},
|
||||
RelatedCustomers = Enumerable.Range(0, i % 3 + 1).Select(j => new ServerCustomerDto
|
||||
{
|
||||
Id = i * 100 + j,
|
||||
FirstName = $"Related_{j}",
|
||||
LastName = $"Contact_{i % 4}",
|
||||
Email = $"related{i}_{j}@example.com"
|
||||
}).ToList(),
|
||||
InternalNotes = $"Notes for order {i}",
|
||||
ProcessingCode = $"CODE-{i % 10}"
|
||||
}).ToList();
|
||||
|
||||
// Act
|
||||
var result = await _client.PostDataAsync<List<ServerOrderWithExtras>, List<ClientOrderSimple>>(
|
||||
TestSignalRTags.PropertyMismatchNestedListParam, serverOrders);
|
||||
|
||||
// Assert
|
||||
Assert.IsNotNull(result, "Result should not be null - SkipValue should correctly handle unknown nested objects");
|
||||
Assert.AreEqual(serverOrders.Count, result.Count);
|
||||
|
||||
for (int i = 0; i < serverOrders.Count; i++)
|
||||
{
|
||||
Assert.AreEqual(serverOrders[i].Id, result[i].Id, $"Id mismatch at index {i}");
|
||||
Assert.AreEqual(serverOrders[i].OrderNumber, result[i].OrderNumber,
|
||||
$"OrderNumber mismatch at index {i}: expected '{serverOrders[i].OrderNumber}', got '{result[i].OrderNumber}'");
|
||||
Assert.AreEqual(serverOrders[i].TotalAmount, result[i].TotalAmount, $"TotalAmount mismatch at index {i}");
|
||||
Assert.AreEqual(6, item.Creator,
|
||||
$"StockTaking Id={item.Id}: Creator should be 6, got {item.Creator}");
|
||||
Assert.IsNotNull(item.StockTakingItems,
|
||||
$"StockTaking Id={item.Id}: StockTakingItems should not be null when loadRelations=true");
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -22,5 +22,142 @@ public class SignalRDataSourceTests_List_Binary : SignalRDataSourceTestBase<Test
|
|||
public override async Task LoadDataSource_InvokesOnDataSourceLoaded() => await base.LoadDataSource_InvokesOnDataSourceLoaded();
|
||||
[TestMethod]
|
||||
public override async Task LoadDataSource_MultipleCalls_RefreshesData() => await base.LoadDataSource_MultipleCalls_RefreshesData();
|
||||
// ... (repeat for all other test methods)
|
||||
[TestMethod]
|
||||
public override async Task LoadDataSourceAsync_LoadsDataViaCallback() => await base.LoadDataSourceAsync_LoadsDataViaCallback();
|
||||
[TestMethod]
|
||||
public override async Task LoadItem_ReturnsSingleItem() => await base.LoadItem_ReturnsSingleItem();
|
||||
[TestMethod]
|
||||
public override async Task LoadItem_AddsToDataSource_WhenNotExists() => await base.LoadItem_AddsToDataSource_WhenNotExists();
|
||||
[TestMethod]
|
||||
public override async Task LoadItem_UpdatesExisting_WhenAlreadyLoaded() => await base.LoadItem_UpdatesExisting_WhenAlreadyLoaded();
|
||||
[TestMethod]
|
||||
public override async Task LoadItem_InvokesOnDataSourceItemChanged() => await base.LoadItem_InvokesOnDataSourceItemChanged();
|
||||
[TestMethod]
|
||||
public override async Task LoadItem_ReturnsNull_WhenNotFound() => await base.LoadItem_ReturnsNull_WhenNotFound();
|
||||
[TestMethod]
|
||||
public override async Task Add_WithAutoSave_AddsItem() => await base.Add_WithAutoSave_AddsItem();
|
||||
[TestMethod]
|
||||
public override void Add_WithoutAutoSave_AddsToTrackingOnly() => base.Add_WithoutAutoSave_AddsToTrackingOnly();
|
||||
[TestMethod]
|
||||
public override void Add_DuplicateId_ThrowsException() => base.Add_DuplicateId_ThrowsException();
|
||||
[TestMethod]
|
||||
public override void Add_DefaultId_ThrowsException() => base.Add_DefaultId_ThrowsException();
|
||||
[TestMethod]
|
||||
public override void AddRange_AddsMultipleItems() => base.AddRange_AddsMultipleItems();
|
||||
[TestMethod]
|
||||
public override async Task AddOrUpdate_AddsNew_WhenNotExists() => await base.AddOrUpdate_AddsNew_WhenNotExists();
|
||||
[TestMethod]
|
||||
public override async Task AddOrUpdate_UpdatesExisting_WhenExists() => await base.AddOrUpdate_UpdatesExisting_WhenExists();
|
||||
[TestMethod]
|
||||
public override void Insert_AtIndex_InsertsCorrectly() => base.Insert_AtIndex_InsertsCorrectly();
|
||||
[TestMethod]
|
||||
public override async Task Insert_WithAutoSave_SavesImmediately() => await base.Insert_WithAutoSave_SavesImmediately();
|
||||
[TestMethod]
|
||||
public override async Task Update_ByIndex_UpdatesCorrectly() => await base.Update_ByIndex_UpdatesCorrectly();
|
||||
[TestMethod]
|
||||
public override async Task Update_ByItem_UpdatesCorrectly() => await base.Update_ByItem_UpdatesCorrectly();
|
||||
[TestMethod]
|
||||
public override void Indexer_Set_TracksUpdate() => base.Indexer_Set_TracksUpdate();
|
||||
[TestMethod]
|
||||
public override async Task Remove_ById_RemovesItem() => await base.Remove_ById_RemovesItem();
|
||||
[TestMethod]
|
||||
public override async Task Remove_ByItem_RemovesItem() => await base.Remove_ByItem_RemovesItem();
|
||||
[TestMethod]
|
||||
public override void Remove_WithoutAutoSave_TracksRemoval() => base.Remove_WithoutAutoSave_TracksRemoval();
|
||||
[TestMethod]
|
||||
public override void RemoveAt_RemovesCorrectItem() => base.RemoveAt_RemovesCorrectItem();
|
||||
[TestMethod]
|
||||
public override async Task RemoveAt_WithAutoSave_SavesImmediately() => await base.RemoveAt_WithAutoSave_SavesImmediately();
|
||||
[TestMethod]
|
||||
public override void TryRemove_ReturnsTrue_WhenExists() => base.TryRemove_ReturnsTrue_WhenExists();
|
||||
[TestMethod]
|
||||
public override void TryRemove_ReturnsFalse_WhenNotExists() => base.TryRemove_ReturnsFalse_WhenNotExists();
|
||||
[TestMethod]
|
||||
public override async Task SaveChanges_SavesTrackedItems() => await base.SaveChanges_SavesTrackedItems();
|
||||
[TestMethod]
|
||||
public override async Task SaveChangesAsync_ClearsTracking() => await base.SaveChangesAsync_ClearsTracking();
|
||||
[TestMethod]
|
||||
public override async Task SaveItem_ById_SavesSpecificItem() => await base.SaveItem_ById_SavesSpecificItem();
|
||||
[TestMethod]
|
||||
public override async Task SaveItem_WithTrackingState_SavesCorrectly() => await base.SaveItem_WithTrackingState_SavesCorrectly();
|
||||
[TestMethod]
|
||||
public override void SetTrackingStateToUpdate_MarksItemForUpdate() => base.SetTrackingStateToUpdate_MarksItemForUpdate();
|
||||
[TestMethod]
|
||||
public override void SetTrackingStateToUpdate_DoesNotChangeAddState() => base.SetTrackingStateToUpdate_DoesNotChangeAddState();
|
||||
[TestMethod]
|
||||
public override void TryGetTrackingItem_ReturnsTrue_WhenTracked() => base.TryGetTrackingItem_ReturnsTrue_WhenTracked();
|
||||
[TestMethod]
|
||||
public override void TryGetTrackingItem_ReturnsFalse_WhenNotTracked() => base.TryGetTrackingItem_ReturnsFalse_WhenNotTracked();
|
||||
[TestMethod]
|
||||
public override void TryRollbackItem_RevertsAddedItem() => base.TryRollbackItem_RevertsAddedItem();
|
||||
[TestMethod]
|
||||
public override async Task TryRollbackItem_RevertsUpdatedItem() => await base.TryRollbackItem_RevertsUpdatedItem();
|
||||
[TestMethod]
|
||||
public override void Rollback_RevertsAllChanges() => base.Rollback_RevertsAllChanges();
|
||||
[TestMethod]
|
||||
public override async Task Count_ReturnsCorrectValue() => await base.Count_ReturnsCorrectValue();
|
||||
[TestMethod]
|
||||
public override void Clear_RemovesAllItems() => base.Clear_RemovesAllItems();
|
||||
[TestMethod]
|
||||
public override void Clear_WithoutClearingTracking_PreservesTracking() => base.Clear_WithoutClearingTracking_PreservesTracking();
|
||||
[TestMethod]
|
||||
public override async Task Contains_ReturnsTrue_WhenItemExists() => await base.Contains_ReturnsTrue_WhenItemExists();
|
||||
[TestMethod]
|
||||
public override void Contains_ReturnsFalse_WhenItemNotExists() => base.Contains_ReturnsFalse_WhenItemNotExists();
|
||||
[TestMethod]
|
||||
public override async Task IndexOf_ReturnsCorrectIndex() => await base.IndexOf_ReturnsCorrectIndex();
|
||||
[TestMethod]
|
||||
public override void IndexOf_ById_ReturnsCorrectIndex() => base.IndexOf_ById_ReturnsCorrectIndex();
|
||||
[TestMethod]
|
||||
public override void TryGetIndex_ReturnsTrue_WhenExists() => base.TryGetIndex_ReturnsTrue_WhenExists();
|
||||
[TestMethod]
|
||||
public override async Task TryGetValue_ReturnsItem_WhenExists() => await base.TryGetValue_ReturnsItem_WhenExists();
|
||||
[TestMethod]
|
||||
public override void TryGetValue_ReturnsFalse_WhenNotExists() => base.TryGetValue_ReturnsFalse_WhenNotExists();
|
||||
[TestMethod]
|
||||
public override async Task CopyTo_CopiesAllItems() => await base.CopyTo_CopiesAllItems();
|
||||
[TestMethod]
|
||||
public override async Task GetEnumerator_EnumeratesAllItems() => await base.GetEnumerator_EnumeratesAllItems();
|
||||
[TestMethod]
|
||||
public override async Task AsReadOnly_ReturnsReadOnlyCollection() => await base.AsReadOnly_ReturnsReadOnlyCollection();
|
||||
[TestMethod]
|
||||
public override async Task SetWorkingReferenceList_SetsNewInnerList() => await base.SetWorkingReferenceList_SetsNewInnerList();
|
||||
[TestMethod]
|
||||
public override async Task GetReferenceInnerList_ReturnsInnerList() => await base.GetReferenceInnerList_ReturnsInnerList();
|
||||
[TestMethod]
|
||||
public override void IsSyncing_IsFalse_Initially() => base.IsSyncing_IsFalse_Initially();
|
||||
[TestMethod]
|
||||
public override async Task OnSyncingStateChanged_Fires_DuringLoad() => await base.OnSyncingStateChanged_Fires_DuringLoad();
|
||||
[TestMethod]
|
||||
public override void ContextIds_CanBeSetAndRetrieved() => base.ContextIds_CanBeSetAndRetrieved();
|
||||
[TestMethod]
|
||||
public override void FilterText_CanBeSetAndRetrieved() => base.FilterText_CanBeSetAndRetrieved();
|
||||
[TestMethod]
|
||||
public override void IList_Add_ReturnsCorrectIndex() => base.IList_Add_ReturnsCorrectIndex();
|
||||
[TestMethod]
|
||||
public override void IList_Contains_WorksCorrectly() => base.IList_Contains_WorksCorrectly();
|
||||
[TestMethod]
|
||||
public override void IList_IndexOf_WorksCorrectly() => base.IList_IndexOf_WorksCorrectly();
|
||||
[TestMethod]
|
||||
public override void IList_Insert_WorksCorrectly() => base.IList_Insert_WorksCorrectly();
|
||||
[TestMethod]
|
||||
public override void IList_Remove_WorksCorrectly() => base.IList_Remove_WorksCorrectly();
|
||||
[TestMethod]
|
||||
public override void IList_Indexer_GetSet_WorksCorrectly() => base.IList_Indexer_GetSet_WorksCorrectly();
|
||||
[TestMethod]
|
||||
public override void ICollection_CopyTo_WorksCorrectly() => base.ICollection_CopyTo_WorksCorrectly();
|
||||
[TestMethod]
|
||||
public override void IsSynchronized_ReturnsTrue() => base.IsSynchronized_ReturnsTrue();
|
||||
[TestMethod]
|
||||
public override void SyncRoot_IsNotNull() => base.SyncRoot_IsNotNull();
|
||||
[TestMethod]
|
||||
public override void IsFixedSize_ReturnsFalse() => base.IsFixedSize_ReturnsFalse();
|
||||
[TestMethod]
|
||||
public override void IsReadOnly_ReturnsFalse() => base.IsReadOnly_ReturnsFalse();
|
||||
[TestMethod]
|
||||
public override async Task Indexer_OutOfRange_ThrowsException() => await base.Indexer_OutOfRange_ThrowsException();
|
||||
[TestMethod]
|
||||
public override void Add_ThenRemove_ClearsTracking() => base.Add_ThenRemove_ClearsTracking();
|
||||
[TestMethod]
|
||||
public override async Task ComplexWorkflow_AddUpdateRemoveSave() => await base.ComplexWorkflow_AddUpdateRemoveSave();
|
||||
}
|
||||
|
|
@ -11,4 +11,153 @@ public class SignalRDataSourceTests_List_Binary_NoRef : SignalRDataSourceTestBas
|
|||
protected override AcSerializerOptions SerializerOption => new AcBinarySerializerOptions { UseReferenceHandling = false };
|
||||
protected override TestOrderItemListDataSource CreateDataSource(TestableSignalRClient2 client, SignalRCrudTags crudTags)
|
||||
=> new(client, crudTags);
|
||||
|
||||
[TestMethod]
|
||||
public override async Task LoadDataSource_ReturnsAllItems() => await base.LoadDataSource_ReturnsAllItems();
|
||||
[TestMethod]
|
||||
public override async Task LoadDataSource_ClearsChangeTracking_ByDefault() => await base.LoadDataSource_ClearsChangeTracking_ByDefault();
|
||||
[TestMethod]
|
||||
public override async Task LoadDataSource_PreservesChangeTracking_WhenFalse() => await base.LoadDataSource_PreservesChangeTracking_WhenFalse();
|
||||
[TestMethod]
|
||||
public override async Task LoadDataSource_InvokesOnDataSourceLoaded() => await base.LoadDataSource_InvokesOnDataSourceLoaded();
|
||||
[TestMethod]
|
||||
public override async Task LoadDataSource_MultipleCalls_RefreshesData() => await base.LoadDataSource_MultipleCalls_RefreshesData();
|
||||
[TestMethod]
|
||||
public override async Task LoadDataSourceAsync_LoadsDataViaCallback() => await base.LoadDataSourceAsync_LoadsDataViaCallback();
|
||||
[TestMethod]
|
||||
public override async Task LoadItem_ReturnsSingleItem() => await base.LoadItem_ReturnsSingleItem();
|
||||
[TestMethod]
|
||||
public override async Task LoadItem_AddsToDataSource_WhenNotExists() => await base.LoadItem_AddsToDataSource_WhenNotExists();
|
||||
[TestMethod]
|
||||
public override async Task LoadItem_UpdatesExisting_WhenAlreadyLoaded() => await base.LoadItem_UpdatesExisting_WhenAlreadyLoaded();
|
||||
[TestMethod]
|
||||
public override async Task LoadItem_InvokesOnDataSourceItemChanged() => await base.LoadItem_InvokesOnDataSourceItemChanged();
|
||||
[TestMethod]
|
||||
public override async Task LoadItem_ReturnsNull_WhenNotFound() => await base.LoadItem_ReturnsNull_WhenNotFound();
|
||||
[TestMethod]
|
||||
public override async Task Add_WithAutoSave_AddsItem() => await base.Add_WithAutoSave_AddsItem();
|
||||
[TestMethod]
|
||||
public override void Add_WithoutAutoSave_AddsToTrackingOnly() => base.Add_WithoutAutoSave_AddsToTrackingOnly();
|
||||
[TestMethod]
|
||||
public override void Add_DuplicateId_ThrowsException() => base.Add_DuplicateId_ThrowsException();
|
||||
[TestMethod]
|
||||
public override void Add_DefaultId_ThrowsException() => base.Add_DefaultId_ThrowsException();
|
||||
[TestMethod]
|
||||
public override void AddRange_AddsMultipleItems() => base.AddRange_AddsMultipleItems();
|
||||
[TestMethod]
|
||||
public override async Task AddOrUpdate_AddsNew_WhenNotExists() => await base.AddOrUpdate_AddsNew_WhenNotExists();
|
||||
[TestMethod]
|
||||
public override async Task AddOrUpdate_UpdatesExisting_WhenExists() => await base.AddOrUpdate_UpdatesExisting_WhenExists();
|
||||
[TestMethod]
|
||||
public override void Insert_AtIndex_InsertsCorrectly() => base.Insert_AtIndex_InsertsCorrectly();
|
||||
[TestMethod]
|
||||
public override async Task Insert_WithAutoSave_SavesImmediately() => await base.Insert_WithAutoSave_SavesImmediately();
|
||||
[TestMethod]
|
||||
public override async Task Update_ByIndex_UpdatesCorrectly() => await base.Update_ByIndex_UpdatesCorrectly();
|
||||
[TestMethod]
|
||||
public override async Task Update_ByItem_UpdatesCorrectly() => await base.Update_ByItem_UpdatesCorrectly();
|
||||
[TestMethod]
|
||||
public override void Indexer_Set_TracksUpdate() => base.Indexer_Set_TracksUpdate();
|
||||
[TestMethod]
|
||||
public override async Task Remove_ById_RemovesItem() => await base.Remove_ById_RemovesItem();
|
||||
[TestMethod]
|
||||
public override async Task Remove_ByItem_RemovesItem() => await base.Remove_ByItem_RemovesItem();
|
||||
[TestMethod]
|
||||
public override void Remove_WithoutAutoSave_TracksRemoval() => base.Remove_WithoutAutoSave_TracksRemoval();
|
||||
[TestMethod]
|
||||
public override void RemoveAt_RemovesCorrectItem() => base.RemoveAt_RemovesCorrectItem();
|
||||
[TestMethod]
|
||||
public override async Task RemoveAt_WithAutoSave_SavesImmediately() => await base.RemoveAt_WithAutoSave_SavesImmediately();
|
||||
[TestMethod]
|
||||
public override void TryRemove_ReturnsTrue_WhenExists() => base.TryRemove_ReturnsTrue_WhenExists();
|
||||
[TestMethod]
|
||||
public override void TryRemove_ReturnsFalse_WhenNotExists() => base.TryRemove_ReturnsFalse_WhenNotExists();
|
||||
[TestMethod]
|
||||
public override async Task SaveChanges_SavesTrackedItems() => await base.SaveChanges_SavesTrackedItems();
|
||||
[TestMethod]
|
||||
public override async Task SaveChangesAsync_ClearsTracking() => await base.SaveChangesAsync_ClearsTracking();
|
||||
[TestMethod]
|
||||
public override async Task SaveItem_ById_SavesSpecificItem() => await base.SaveItem_ById_SavesSpecificItem();
|
||||
[TestMethod]
|
||||
public override async Task SaveItem_WithTrackingState_SavesCorrectly() => await base.SaveItem_WithTrackingState_SavesCorrectly();
|
||||
[TestMethod]
|
||||
public override void SetTrackingStateToUpdate_MarksItemForUpdate() => base.SetTrackingStateToUpdate_MarksItemForUpdate();
|
||||
[TestMethod]
|
||||
public override void SetTrackingStateToUpdate_DoesNotChangeAddState() => base.SetTrackingStateToUpdate_DoesNotChangeAddState();
|
||||
[TestMethod]
|
||||
public override void TryGetTrackingItem_ReturnsTrue_WhenTracked() => base.TryGetTrackingItem_ReturnsTrue_WhenTracked();
|
||||
[TestMethod]
|
||||
public override void TryGetTrackingItem_ReturnsFalse_WhenNotTracked() => base.TryGetTrackingItem_ReturnsFalse_WhenNotTracked();
|
||||
[TestMethod]
|
||||
public override void TryRollbackItem_RevertsAddedItem() => base.TryRollbackItem_RevertsAddedItem();
|
||||
[TestMethod]
|
||||
public override async Task TryRollbackItem_RevertsUpdatedItem() => await base.TryRollbackItem_RevertsUpdatedItem();
|
||||
[TestMethod]
|
||||
public override void Rollback_RevertsAllChanges() => base.Rollback_RevertsAllChanges();
|
||||
[TestMethod]
|
||||
public override async Task Count_ReturnsCorrectValue() => await base.Count_ReturnsCorrectValue();
|
||||
[TestMethod]
|
||||
public override void Clear_RemovesAllItems() => base.Clear_RemovesAllItems();
|
||||
[TestMethod]
|
||||
public override void Clear_WithoutClearingTracking_PreservesTracking() => base.Clear_WithoutClearingTracking_PreservesTracking();
|
||||
[TestMethod]
|
||||
public override async Task Contains_ReturnsTrue_WhenItemExists() => await base.Contains_ReturnsTrue_WhenItemExists();
|
||||
[TestMethod]
|
||||
public override void Contains_ReturnsFalse_WhenItemNotExists() => base.Contains_ReturnsFalse_WhenItemNotExists();
|
||||
[TestMethod]
|
||||
public override async Task IndexOf_ReturnsCorrectIndex() => await base.IndexOf_ReturnsCorrectIndex();
|
||||
[TestMethod]
|
||||
public override void IndexOf_ById_ReturnsCorrectIndex() => base.IndexOf_ById_ReturnsCorrectIndex();
|
||||
[TestMethod]
|
||||
public override void TryGetIndex_ReturnsTrue_WhenExists() => base.TryGetIndex_ReturnsTrue_WhenExists();
|
||||
[TestMethod]
|
||||
public override async Task TryGetValue_ReturnsItem_WhenExists() => await base.TryGetValue_ReturnsItem_WhenExists();
|
||||
[TestMethod]
|
||||
public override void TryGetValue_ReturnsFalse_WhenNotExists() => base.TryGetValue_ReturnsFalse_WhenNotExists();
|
||||
[TestMethod]
|
||||
public override async Task CopyTo_CopiesAllItems() => await base.CopyTo_CopiesAllItems();
|
||||
[TestMethod]
|
||||
public override async Task GetEnumerator_EnumeratesAllItems() => await base.GetEnumerator_EnumeratesAllItems();
|
||||
[TestMethod]
|
||||
public override async Task AsReadOnly_ReturnsReadOnlyCollection() => await base.AsReadOnly_ReturnsReadOnlyCollection();
|
||||
[TestMethod]
|
||||
public override async Task SetWorkingReferenceList_SetsNewInnerList() => await base.SetWorkingReferenceList_SetsNewInnerList();
|
||||
[TestMethod]
|
||||
public override async Task GetReferenceInnerList_ReturnsInnerList() => await base.GetReferenceInnerList_ReturnsInnerList();
|
||||
[TestMethod]
|
||||
public override void IsSyncing_IsFalse_Initially() => base.IsSyncing_IsFalse_Initially();
|
||||
[TestMethod]
|
||||
public override async Task OnSyncingStateChanged_Fires_DuringLoad() => await base.OnSyncingStateChanged_Fires_DuringLoad();
|
||||
[TestMethod]
|
||||
public override void ContextIds_CanBeSetAndRetrieved() => base.ContextIds_CanBeSetAndRetrieved();
|
||||
[TestMethod]
|
||||
public override void FilterText_CanBeSetAndRetrieved() => base.FilterText_CanBeSetAndRetrieved();
|
||||
[TestMethod]
|
||||
public override void IList_Add_ReturnsCorrectIndex() => base.IList_Add_ReturnsCorrectIndex();
|
||||
[TestMethod]
|
||||
public override void IList_Contains_WorksCorrectly() => base.IList_Contains_WorksCorrectly();
|
||||
[TestMethod]
|
||||
public override void IList_IndexOf_WorksCorrectly() => base.IList_IndexOf_WorksCorrectly();
|
||||
[TestMethod]
|
||||
public override void IList_Insert_WorksCorrectly() => base.IList_Insert_WorksCorrectly();
|
||||
[TestMethod]
|
||||
public override void IList_Remove_WorksCorrectly() => base.IList_Remove_WorksCorrectly();
|
||||
[TestMethod]
|
||||
public override void IList_Indexer_GetSet_WorksCorrectly() => base.IList_Indexer_GetSet_WorksCorrectly();
|
||||
[TestMethod]
|
||||
public override void ICollection_CopyTo_WorksCorrectly() => base.ICollection_CopyTo_WorksCorrectly();
|
||||
[TestMethod]
|
||||
public override void IsSynchronized_ReturnsTrue() => base.IsSynchronized_ReturnsTrue();
|
||||
[TestMethod]
|
||||
public override void SyncRoot_IsNotNull() => base.SyncRoot_IsNotNull();
|
||||
[TestMethod]
|
||||
public override void IsFixedSize_ReturnsFalse() => base.IsFixedSize_ReturnsFalse();
|
||||
[TestMethod]
|
||||
public override void IsReadOnly_ReturnsFalse() => base.IsReadOnly_ReturnsFalse();
|
||||
[TestMethod]
|
||||
public override async Task Indexer_OutOfRange_ThrowsException() => await base.Indexer_OutOfRange_ThrowsException();
|
||||
[TestMethod]
|
||||
public override void Add_ThenRemove_ClearsTracking() => base.Add_ThenRemove_ClearsTracking();
|
||||
[TestMethod]
|
||||
public override async Task ComplexWorkflow_AddUpdateRemoveSave() => await base.ComplexWorkflow_AddUpdateRemoveSave();
|
||||
}
|
||||
|
|
@ -1,3 +1,4 @@
|
|||
using AyCode.Core.Serializers.Binaries;
|
||||
using AyCode.Core.Serializers.Jsons;
|
||||
using AyCode.Core.Tests.TestModels;
|
||||
using AyCode.Services.SignalRs;
|
||||
|
|
@ -10,4 +11,153 @@ public class SignalRDataSourceTests_List_Json : SignalRDataSourceTestBase<TestOr
|
|||
protected override AcSerializerOptions SerializerOption => new AcJsonSerializerOptions();
|
||||
protected override TestOrderItemListDataSource CreateDataSource(TestableSignalRClient2 client, SignalRCrudTags crudTags)
|
||||
=> new(client, crudTags);
|
||||
|
||||
[TestMethod]
|
||||
public override async Task LoadDataSource_ReturnsAllItems() => await base.LoadDataSource_ReturnsAllItems();
|
||||
[TestMethod]
|
||||
public override async Task LoadDataSource_ClearsChangeTracking_ByDefault() => await base.LoadDataSource_ClearsChangeTracking_ByDefault();
|
||||
[TestMethod]
|
||||
public override async Task LoadDataSource_PreservesChangeTracking_WhenFalse() => await base.LoadDataSource_PreservesChangeTracking_WhenFalse();
|
||||
[TestMethod]
|
||||
public override async Task LoadDataSource_InvokesOnDataSourceLoaded() => await base.LoadDataSource_InvokesOnDataSourceLoaded();
|
||||
[TestMethod]
|
||||
public override async Task LoadDataSource_MultipleCalls_RefreshesData() => await base.LoadDataSource_MultipleCalls_RefreshesData();
|
||||
[TestMethod]
|
||||
public override async Task LoadDataSourceAsync_LoadsDataViaCallback() => await base.LoadDataSourceAsync_LoadsDataViaCallback();
|
||||
[TestMethod]
|
||||
public override async Task LoadItem_ReturnsSingleItem() => await base.LoadItem_ReturnsSingleItem();
|
||||
[TestMethod]
|
||||
public override async Task LoadItem_AddsToDataSource_WhenNotExists() => await base.LoadItem_AddsToDataSource_WhenNotExists();
|
||||
[TestMethod]
|
||||
public override async Task LoadItem_UpdatesExisting_WhenAlreadyLoaded() => await base.LoadItem_UpdatesExisting_WhenAlreadyLoaded();
|
||||
[TestMethod]
|
||||
public override async Task LoadItem_InvokesOnDataSourceItemChanged() => await base.LoadItem_InvokesOnDataSourceItemChanged();
|
||||
[TestMethod]
|
||||
public override async Task LoadItem_ReturnsNull_WhenNotFound() => await base.LoadItem_ReturnsNull_WhenNotFound();
|
||||
[TestMethod]
|
||||
public override async Task Add_WithAutoSave_AddsItem() => await base.Add_WithAutoSave_AddsItem();
|
||||
[TestMethod]
|
||||
public override void Add_WithoutAutoSave_AddsToTrackingOnly() => base.Add_WithoutAutoSave_AddsToTrackingOnly();
|
||||
[TestMethod]
|
||||
public override void Add_DuplicateId_ThrowsException() => base.Add_DuplicateId_ThrowsException();
|
||||
[TestMethod]
|
||||
public override void Add_DefaultId_ThrowsException() => base.Add_DefaultId_ThrowsException();
|
||||
[TestMethod]
|
||||
public override void AddRange_AddsMultipleItems() => base.AddRange_AddsMultipleItems();
|
||||
[TestMethod]
|
||||
public override async Task AddOrUpdate_AddsNew_WhenNotExists() => await base.AddOrUpdate_AddsNew_WhenNotExists();
|
||||
[TestMethod]
|
||||
public override async Task AddOrUpdate_UpdatesExisting_WhenExists() => await base.AddOrUpdate_UpdatesExisting_WhenExists();
|
||||
[TestMethod]
|
||||
public override void Insert_AtIndex_InsertsCorrectly() => base.Insert_AtIndex_InsertsCorrectly();
|
||||
[TestMethod]
|
||||
public override async Task Insert_WithAutoSave_SavesImmediately() => await base.Insert_WithAutoSave_SavesImmediately();
|
||||
[TestMethod]
|
||||
public override async Task Update_ByIndex_UpdatesCorrectly() => await base.Update_ByIndex_UpdatesCorrectly();
|
||||
[TestMethod]
|
||||
public override async Task Update_ByItem_UpdatesCorrectly() => await base.Update_ByItem_UpdatesCorrectly();
|
||||
[TestMethod]
|
||||
public override void Indexer_Set_TracksUpdate() => base.Indexer_Set_TracksUpdate();
|
||||
[TestMethod]
|
||||
public override async Task Remove_ById_RemovesItem() => await base.Remove_ById_RemovesItem();
|
||||
[TestMethod]
|
||||
public override async Task Remove_ByItem_RemovesItem() => await base.Remove_ByItem_RemovesItem();
|
||||
[TestMethod]
|
||||
public override void Remove_WithoutAutoSave_TracksRemoval() => base.Remove_WithoutAutoSave_TracksRemoval();
|
||||
[TestMethod]
|
||||
public override void RemoveAt_RemovesCorrectItem() => base.RemoveAt_RemovesCorrectItem();
|
||||
[TestMethod]
|
||||
public override async Task RemoveAt_WithAutoSave_SavesImmediately() => await base.RemoveAt_WithAutoSave_SavesImmediately();
|
||||
[TestMethod]
|
||||
public override void TryRemove_ReturnsTrue_WhenExists() => base.TryRemove_ReturnsTrue_WhenExists();
|
||||
[TestMethod]
|
||||
public override void TryRemove_ReturnsFalse_WhenNotExists() => base.TryRemove_ReturnsFalse_WhenNotExists();
|
||||
[TestMethod]
|
||||
public override async Task SaveChanges_SavesTrackedItems() => await base.SaveChanges_SavesTrackedItems();
|
||||
[TestMethod]
|
||||
public override async Task SaveChangesAsync_ClearsTracking() => await base.SaveChangesAsync_ClearsTracking();
|
||||
[TestMethod]
|
||||
public override async Task SaveItem_ById_SavesSpecificItem() => await base.SaveItem_ById_SavesSpecificItem();
|
||||
[TestMethod]
|
||||
public override async Task SaveItem_WithTrackingState_SavesCorrectly() => await base.SaveItem_WithTrackingState_SavesCorrectly();
|
||||
[TestMethod]
|
||||
public override void SetTrackingStateToUpdate_MarksItemForUpdate() => base.SetTrackingStateToUpdate_MarksItemForUpdate();
|
||||
[TestMethod]
|
||||
public override void SetTrackingStateToUpdate_DoesNotChangeAddState() => base.SetTrackingStateToUpdate_DoesNotChangeAddState();
|
||||
[TestMethod]
|
||||
public override void TryGetTrackingItem_ReturnsTrue_WhenTracked() => base.TryGetTrackingItem_ReturnsTrue_WhenTracked();
|
||||
[TestMethod]
|
||||
public override void TryGetTrackingItem_ReturnsFalse_WhenNotTracked() => base.TryGetTrackingItem_ReturnsFalse_WhenNotTracked();
|
||||
[TestMethod]
|
||||
public override void TryRollbackItem_RevertsAddedItem() => base.TryRollbackItem_RevertsAddedItem();
|
||||
[TestMethod]
|
||||
public override async Task TryRollbackItem_RevertsUpdatedItem() => await base.TryRollbackItem_RevertsUpdatedItem();
|
||||
[TestMethod]
|
||||
public override void Rollback_RevertsAllChanges() => base.Rollback_RevertsAllChanges();
|
||||
[TestMethod]
|
||||
public override async Task Count_ReturnsCorrectValue() => await base.Count_ReturnsCorrectValue();
|
||||
[TestMethod]
|
||||
public override void Clear_RemovesAllItems() => base.Clear_RemovesAllItems();
|
||||
[TestMethod]
|
||||
public override void Clear_WithoutClearingTracking_PreservesTracking() => base.Clear_WithoutClearingTracking_PreservesTracking();
|
||||
[TestMethod]
|
||||
public override async Task Contains_ReturnsTrue_WhenItemExists() => await base.Contains_ReturnsTrue_WhenItemExists();
|
||||
[TestMethod]
|
||||
public override void Contains_ReturnsFalse_WhenItemNotExists() => base.Contains_ReturnsFalse_WhenItemNotExists();
|
||||
[TestMethod]
|
||||
public override async Task IndexOf_ReturnsCorrectIndex() => await base.IndexOf_ReturnsCorrectIndex();
|
||||
[TestMethod]
|
||||
public override void IndexOf_ById_ReturnsCorrectIndex() => base.IndexOf_ById_ReturnsCorrectIndex();
|
||||
[TestMethod]
|
||||
public override void TryGetIndex_ReturnsTrue_WhenExists() => base.TryGetIndex_ReturnsTrue_WhenExists();
|
||||
[TestMethod]
|
||||
public override async Task TryGetValue_ReturnsItem_WhenExists() => await base.TryGetValue_ReturnsItem_WhenExists();
|
||||
[TestMethod]
|
||||
public override void TryGetValue_ReturnsFalse_WhenNotExists() => base.TryGetValue_ReturnsFalse_WhenNotExists();
|
||||
[TestMethod]
|
||||
public override async Task CopyTo_CopiesAllItems() => await base.CopyTo_CopiesAllItems();
|
||||
[TestMethod]
|
||||
public override async Task GetEnumerator_EnumeratesAllItems() => await base.GetEnumerator_EnumeratesAllItems();
|
||||
[TestMethod]
|
||||
public override async Task AsReadOnly_ReturnsReadOnlyCollection() => await base.AsReadOnly_ReturnsReadOnlyCollection();
|
||||
[TestMethod]
|
||||
public override async Task SetWorkingReferenceList_SetsNewInnerList() => await base.SetWorkingReferenceList_SetsNewInnerList();
|
||||
[TestMethod]
|
||||
public override async Task GetReferenceInnerList_ReturnsInnerList() => await base.GetReferenceInnerList_ReturnsInnerList();
|
||||
[TestMethod]
|
||||
public override void IsSyncing_IsFalse_Initially() => base.IsSyncing_IsFalse_Initially();
|
||||
[TestMethod]
|
||||
public override async Task OnSyncingStateChanged_Fires_DuringLoad() => await base.OnSyncingStateChanged_Fires_DuringLoad();
|
||||
[TestMethod]
|
||||
public override void ContextIds_CanBeSetAndRetrieved() => base.ContextIds_CanBeSetAndRetrieved();
|
||||
[TestMethod]
|
||||
public override void FilterText_CanBeSetAndRetrieved() => base.FilterText_CanBeSetAndRetrieved();
|
||||
[TestMethod]
|
||||
public override void IList_Add_ReturnsCorrectIndex() => base.IList_Add_ReturnsCorrectIndex();
|
||||
[TestMethod]
|
||||
public override void IList_Contains_WorksCorrectly() => base.IList_Contains_WorksCorrectly();
|
||||
[TestMethod]
|
||||
public override void IList_IndexOf_WorksCorrectly() => base.IList_IndexOf_WorksCorrectly();
|
||||
[TestMethod]
|
||||
public override void IList_Insert_WorksCorrectly() => base.IList_Insert_WorksCorrectly();
|
||||
[TestMethod]
|
||||
public override void IList_Remove_WorksCorrectly() => base.IList_Remove_WorksCorrectly();
|
||||
[TestMethod]
|
||||
public override void IList_Indexer_GetSet_WorksCorrectly() => base.IList_Indexer_GetSet_WorksCorrectly();
|
||||
[TestMethod]
|
||||
public override void ICollection_CopyTo_WorksCorrectly() => base.ICollection_CopyTo_WorksCorrectly();
|
||||
[TestMethod]
|
||||
public override void IsSynchronized_ReturnsTrue() => base.IsSynchronized_ReturnsTrue();
|
||||
[TestMethod]
|
||||
public override void SyncRoot_IsNotNull() => base.SyncRoot_IsNotNull();
|
||||
[TestMethod]
|
||||
public override void IsFixedSize_ReturnsFalse() => base.IsFixedSize_ReturnsFalse();
|
||||
[TestMethod]
|
||||
public override void IsReadOnly_ReturnsFalse() => base.IsReadOnly_ReturnsFalse();
|
||||
[TestMethod]
|
||||
public override async Task Indexer_OutOfRange_ThrowsException() => await base.Indexer_OutOfRange_ThrowsException();
|
||||
[TestMethod]
|
||||
public override void Add_ThenRemove_ClearsTracking() => base.Add_ThenRemove_ClearsTracking();
|
||||
[TestMethod]
|
||||
public override async Task ComplexWorkflow_AddUpdateRemoveSave() => await base.ComplexWorkflow_AddUpdateRemoveSave();
|
||||
}
|
||||
|
|
@ -12,4 +12,153 @@ public class SignalRDataSourceTests_Observable_Binary : SignalRDataSourceTestBas
|
|||
protected override AcSerializerOptions SerializerOption => new AcBinarySerializerOptions();
|
||||
protected override TestOrderItemObservableDataSource CreateDataSource(TestableSignalRClient2 client, SignalRCrudTags crudTags)
|
||||
=> new(client, crudTags);
|
||||
|
||||
[TestMethod]
|
||||
public override async Task LoadDataSource_ReturnsAllItems() => await base.LoadDataSource_ReturnsAllItems();
|
||||
[TestMethod]
|
||||
public override async Task LoadDataSource_ClearsChangeTracking_ByDefault() => await base.LoadDataSource_ClearsChangeTracking_ByDefault();
|
||||
[TestMethod]
|
||||
public override async Task LoadDataSource_PreservesChangeTracking_WhenFalse() => await base.LoadDataSource_PreservesChangeTracking_WhenFalse();
|
||||
[TestMethod]
|
||||
public override async Task LoadDataSource_InvokesOnDataSourceLoaded() => await base.LoadDataSource_InvokesOnDataSourceLoaded();
|
||||
[TestMethod]
|
||||
public override async Task LoadDataSource_MultipleCalls_RefreshesData() => await base.LoadDataSource_MultipleCalls_RefreshesData();
|
||||
[TestMethod]
|
||||
public override async Task LoadDataSourceAsync_LoadsDataViaCallback() => await base.LoadDataSourceAsync_LoadsDataViaCallback();
|
||||
[TestMethod]
|
||||
public override async Task LoadItem_ReturnsSingleItem() => await base.LoadItem_ReturnsSingleItem();
|
||||
[TestMethod]
|
||||
public override async Task LoadItem_AddsToDataSource_WhenNotExists() => await base.LoadItem_AddsToDataSource_WhenNotExists();
|
||||
[TestMethod]
|
||||
public override async Task LoadItem_UpdatesExisting_WhenAlreadyLoaded() => await base.LoadItem_UpdatesExisting_WhenAlreadyLoaded();
|
||||
[TestMethod]
|
||||
public override async Task LoadItem_InvokesOnDataSourceItemChanged() => await base.LoadItem_InvokesOnDataSourceItemChanged();
|
||||
[TestMethod]
|
||||
public override async Task LoadItem_ReturnsNull_WhenNotFound() => await base.LoadItem_ReturnsNull_WhenNotFound();
|
||||
[TestMethod]
|
||||
public override async Task Add_WithAutoSave_AddsItem() => await base.Add_WithAutoSave_AddsItem();
|
||||
[TestMethod]
|
||||
public override void Add_WithoutAutoSave_AddsToTrackingOnly() => base.Add_WithoutAutoSave_AddsToTrackingOnly();
|
||||
[TestMethod]
|
||||
public override void Add_DuplicateId_ThrowsException() => base.Add_DuplicateId_ThrowsException();
|
||||
[TestMethod]
|
||||
public override void Add_DefaultId_ThrowsException() => base.Add_DefaultId_ThrowsException();
|
||||
[TestMethod]
|
||||
public override void AddRange_AddsMultipleItems() => base.AddRange_AddsMultipleItems();
|
||||
[TestMethod]
|
||||
public override async Task AddOrUpdate_AddsNew_WhenNotExists() => await base.AddOrUpdate_AddsNew_WhenNotExists();
|
||||
[TestMethod]
|
||||
public override async Task AddOrUpdate_UpdatesExisting_WhenExists() => await base.AddOrUpdate_UpdatesExisting_WhenExists();
|
||||
[TestMethod]
|
||||
public override void Insert_AtIndex_InsertsCorrectly() => base.Insert_AtIndex_InsertsCorrectly();
|
||||
[TestMethod]
|
||||
public override async Task Insert_WithAutoSave_SavesImmediately() => await base.Insert_WithAutoSave_SavesImmediately();
|
||||
[TestMethod]
|
||||
public override async Task Update_ByIndex_UpdatesCorrectly() => await base.Update_ByIndex_UpdatesCorrectly();
|
||||
[TestMethod]
|
||||
public override async Task Update_ByItem_UpdatesCorrectly() => await base.Update_ByItem_UpdatesCorrectly();
|
||||
[TestMethod]
|
||||
public override void Indexer_Set_TracksUpdate() => base.Indexer_Set_TracksUpdate();
|
||||
[TestMethod]
|
||||
public override async Task Remove_ById_RemovesItem() => await base.Remove_ById_RemovesItem();
|
||||
[TestMethod]
|
||||
public override async Task Remove_ByItem_RemovesItem() => await base.Remove_ByItem_RemovesItem();
|
||||
[TestMethod]
|
||||
public override void Remove_WithoutAutoSave_TracksRemoval() => base.Remove_WithoutAutoSave_TracksRemoval();
|
||||
[TestMethod]
|
||||
public override void RemoveAt_RemovesCorrectItem() => base.RemoveAt_RemovesCorrectItem();
|
||||
[TestMethod]
|
||||
public override async Task RemoveAt_WithAutoSave_SavesImmediately() => await base.RemoveAt_WithAutoSave_SavesImmediately();
|
||||
[TestMethod]
|
||||
public override void TryRemove_ReturnsTrue_WhenExists() => base.TryRemove_ReturnsTrue_WhenExists();
|
||||
[TestMethod]
|
||||
public override void TryRemove_ReturnsFalse_WhenNotExists() => base.TryRemove_ReturnsFalse_WhenNotExists();
|
||||
[TestMethod]
|
||||
public override async Task SaveChanges_SavesTrackedItems() => await base.SaveChanges_SavesTrackedItems();
|
||||
[TestMethod]
|
||||
public override async Task SaveChangesAsync_ClearsTracking() => await base.SaveChangesAsync_ClearsTracking();
|
||||
[TestMethod]
|
||||
public override async Task SaveItem_ById_SavesSpecificItem() => await base.SaveItem_ById_SavesSpecificItem();
|
||||
[TestMethod]
|
||||
public override async Task SaveItem_WithTrackingState_SavesCorrectly() => await base.SaveItem_WithTrackingState_SavesCorrectly();
|
||||
[TestMethod]
|
||||
public override void SetTrackingStateToUpdate_MarksItemForUpdate() => base.SetTrackingStateToUpdate_MarksItemForUpdate();
|
||||
[TestMethod]
|
||||
public override void SetTrackingStateToUpdate_DoesNotChangeAddState() => base.SetTrackingStateToUpdate_DoesNotChangeAddState();
|
||||
[TestMethod]
|
||||
public override void TryGetTrackingItem_ReturnsTrue_WhenTracked() => base.TryGetTrackingItem_ReturnsTrue_WhenTracked();
|
||||
[TestMethod]
|
||||
public override void TryGetTrackingItem_ReturnsFalse_WhenNotTracked() => base.TryGetTrackingItem_ReturnsFalse_WhenNotTracked();
|
||||
[TestMethod]
|
||||
public override void TryRollbackItem_RevertsAddedItem() => base.TryRollbackItem_RevertsAddedItem();
|
||||
[TestMethod]
|
||||
public override async Task TryRollbackItem_RevertsUpdatedItem() => await base.TryRollbackItem_RevertsUpdatedItem();
|
||||
[TestMethod]
|
||||
public override void Rollback_RevertsAllChanges() => base.Rollback_RevertsAllChanges();
|
||||
[TestMethod]
|
||||
public override async Task Count_ReturnsCorrectValue() => await base.Count_ReturnsCorrectValue();
|
||||
[TestMethod]
|
||||
public override void Clear_RemovesAllItems() => base.Clear_RemovesAllItems();
|
||||
[TestMethod]
|
||||
public override void Clear_WithoutClearingTracking_PreservesTracking() => base.Clear_WithoutClearingTracking_PreservesTracking();
|
||||
[TestMethod]
|
||||
public override async Task Contains_ReturnsTrue_WhenItemExists() => await base.Contains_ReturnsTrue_WhenItemExists();
|
||||
[TestMethod]
|
||||
public override void Contains_ReturnsFalse_WhenItemNotExists() => base.Contains_ReturnsFalse_WhenItemNotExists();
|
||||
[TestMethod]
|
||||
public override async Task IndexOf_ReturnsCorrectIndex() => await base.IndexOf_ReturnsCorrectIndex();
|
||||
[TestMethod]
|
||||
public override void IndexOf_ById_ReturnsCorrectIndex() => base.IndexOf_ById_ReturnsCorrectIndex();
|
||||
[TestMethod]
|
||||
public override void TryGetIndex_ReturnsTrue_WhenExists() => base.TryGetIndex_ReturnsTrue_WhenExists();
|
||||
[TestMethod]
|
||||
public override async Task TryGetValue_ReturnsItem_WhenExists() => await base.TryGetValue_ReturnsItem_WhenExists();
|
||||
[TestMethod]
|
||||
public override void TryGetValue_ReturnsFalse_WhenNotExists() => base.TryGetValue_ReturnsFalse_WhenNotExists();
|
||||
[TestMethod]
|
||||
public override async Task CopyTo_CopiesAllItems() => await base.CopyTo_CopiesAllItems();
|
||||
[TestMethod]
|
||||
public override async Task GetEnumerator_EnumeratesAllItems() => await base.GetEnumerator_EnumeratesAllItems();
|
||||
[TestMethod]
|
||||
public override async Task AsReadOnly_ReturnsReadOnlyCollection() => await base.AsReadOnly_ReturnsReadOnlyCollection();
|
||||
[TestMethod]
|
||||
public override async Task SetWorkingReferenceList_SetsNewInnerList() => await base.SetWorkingReferenceList_SetsNewInnerList();
|
||||
[TestMethod]
|
||||
public override async Task GetReferenceInnerList_ReturnsInnerList() => await base.GetReferenceInnerList_ReturnsInnerList();
|
||||
[TestMethod]
|
||||
public override void IsSyncing_IsFalse_Initially() => base.IsSyncing_IsFalse_Initially();
|
||||
[TestMethod]
|
||||
public override async Task OnSyncingStateChanged_Fires_DuringLoad() => await base.OnSyncingStateChanged_Fires_DuringLoad();
|
||||
[TestMethod]
|
||||
public override void ContextIds_CanBeSetAndRetrieved() => base.ContextIds_CanBeSetAndRetrieved();
|
||||
[TestMethod]
|
||||
public override void FilterText_CanBeSetAndRetrieved() => base.FilterText_CanBeSetAndRetrieved();
|
||||
[TestMethod]
|
||||
public override void IList_Add_ReturnsCorrectIndex() => base.IList_Add_ReturnsCorrectIndex();
|
||||
[TestMethod]
|
||||
public override void IList_Contains_WorksCorrectly() => base.IList_Contains_WorksCorrectly();
|
||||
[TestMethod]
|
||||
public override void IList_IndexOf_WorksCorrectly() => base.IList_IndexOf_WorksCorrectly();
|
||||
[TestMethod]
|
||||
public override void IList_Insert_WorksCorrectly() => base.IList_Insert_WorksCorrectly();
|
||||
[TestMethod]
|
||||
public override void IList_Remove_WorksCorrectly() => base.IList_Remove_WorksCorrectly();
|
||||
[TestMethod]
|
||||
public override void IList_Indexer_GetSet_WorksCorrectly() => base.IList_Indexer_GetSet_WorksCorrectly();
|
||||
[TestMethod]
|
||||
public override void ICollection_CopyTo_WorksCorrectly() => base.ICollection_CopyTo_WorksCorrectly();
|
||||
[TestMethod]
|
||||
public override void IsSynchronized_ReturnsTrue() => base.IsSynchronized_ReturnsTrue();
|
||||
[TestMethod]
|
||||
public override void SyncRoot_IsNotNull() => base.SyncRoot_IsNotNull();
|
||||
[TestMethod]
|
||||
public override void IsFixedSize_ReturnsFalse() => base.IsFixedSize_ReturnsFalse();
|
||||
[TestMethod]
|
||||
public override void IsReadOnly_ReturnsFalse() => base.IsReadOnly_ReturnsFalse();
|
||||
[TestMethod]
|
||||
public override async Task Indexer_OutOfRange_ThrowsException() => await base.Indexer_OutOfRange_ThrowsException();
|
||||
[TestMethod]
|
||||
public override void Add_ThenRemove_ClearsTracking() => base.Add_ThenRemove_ClearsTracking();
|
||||
[TestMethod]
|
||||
public override async Task ComplexWorkflow_AddUpdateRemoveSave() => await base.ComplexWorkflow_AddUpdateRemoveSave();
|
||||
}
|
||||
|
|
@ -11,4 +11,153 @@ public class SignalRDataSourceTests_Observable_Json : SignalRDataSourceTestBase<
|
|||
protected override AcSerializerOptions SerializerOption => new AcJsonSerializerOptions();
|
||||
protected override TestOrderItemObservableDataSource CreateDataSource(TestableSignalRClient2 client, SignalRCrudTags crudTags)
|
||||
=> new(client, crudTags);
|
||||
|
||||
[TestMethod]
|
||||
public override async Task LoadDataSource_ReturnsAllItems() => await base.LoadDataSource_ReturnsAllItems();
|
||||
[TestMethod]
|
||||
public override async Task LoadDataSource_ClearsChangeTracking_ByDefault() => await base.LoadDataSource_ClearsChangeTracking_ByDefault();
|
||||
[TestMethod]
|
||||
public override async Task LoadDataSource_PreservesChangeTracking_WhenFalse() => await base.LoadDataSource_PreservesChangeTracking_WhenFalse();
|
||||
[TestMethod]
|
||||
public override async Task LoadDataSource_InvokesOnDataSourceLoaded() => await base.LoadDataSource_InvokesOnDataSourceLoaded();
|
||||
[TestMethod]
|
||||
public override async Task LoadDataSource_MultipleCalls_RefreshesData() => await base.LoadDataSource_MultipleCalls_RefreshesData();
|
||||
[TestMethod]
|
||||
public override async Task LoadDataSourceAsync_LoadsDataViaCallback() => await base.LoadDataSourceAsync_LoadsDataViaCallback();
|
||||
[TestMethod]
|
||||
public override async Task LoadItem_ReturnsSingleItem() => await base.LoadItem_ReturnsSingleItem();
|
||||
[TestMethod]
|
||||
public override async Task LoadItem_AddsToDataSource_WhenNotExists() => await base.LoadItem_AddsToDataSource_WhenNotExists();
|
||||
[TestMethod]
|
||||
public override async Task LoadItem_UpdatesExisting_WhenAlreadyLoaded() => await base.LoadItem_UpdatesExisting_WhenAlreadyLoaded();
|
||||
[TestMethod]
|
||||
public override async Task LoadItem_InvokesOnDataSourceItemChanged() => await base.LoadItem_InvokesOnDataSourceItemChanged();
|
||||
[TestMethod]
|
||||
public override async Task LoadItem_ReturnsNull_WhenNotFound() => await base.LoadItem_ReturnsNull_WhenNotFound();
|
||||
[TestMethod]
|
||||
public override async Task Add_WithAutoSave_AddsItem() => await base.Add_WithAutoSave_AddsItem();
|
||||
[TestMethod]
|
||||
public override void Add_WithoutAutoSave_AddsToTrackingOnly() => base.Add_WithoutAutoSave_AddsToTrackingOnly();
|
||||
[TestMethod]
|
||||
public override void Add_DuplicateId_ThrowsException() => base.Add_DuplicateId_ThrowsException();
|
||||
[TestMethod]
|
||||
public override void Add_DefaultId_ThrowsException() => base.Add_DefaultId_ThrowsException();
|
||||
[TestMethod]
|
||||
public override void AddRange_AddsMultipleItems() => base.AddRange_AddsMultipleItems();
|
||||
[TestMethod]
|
||||
public override async Task AddOrUpdate_AddsNew_WhenNotExists() => await base.AddOrUpdate_AddsNew_WhenNotExists();
|
||||
[TestMethod]
|
||||
public override async Task AddOrUpdate_UpdatesExisting_WhenExists() => await base.AddOrUpdate_UpdatesExisting_WhenExists();
|
||||
[TestMethod]
|
||||
public override void Insert_AtIndex_InsertsCorrectly() => base.Insert_AtIndex_InsertsCorrectly();
|
||||
[TestMethod]
|
||||
public override async Task Insert_WithAutoSave_SavesImmediately() => await base.Insert_WithAutoSave_SavesImmediately();
|
||||
[TestMethod]
|
||||
public override async Task Update_ByIndex_UpdatesCorrectly() => await base.Update_ByIndex_UpdatesCorrectly();
|
||||
[TestMethod]
|
||||
public override async Task Update_ByItem_UpdatesCorrectly() => await base.Update_ByItem_UpdatesCorrectly();
|
||||
[TestMethod]
|
||||
public override void Indexer_Set_TracksUpdate() => base.Indexer_Set_TracksUpdate();
|
||||
[TestMethod]
|
||||
public override async Task Remove_ById_RemovesItem() => await base.Remove_ById_RemovesItem();
|
||||
[TestMethod]
|
||||
public override async Task Remove_ByItem_RemovesItem() => await base.Remove_ByItem_RemovesItem();
|
||||
[TestMethod]
|
||||
public override void Remove_WithoutAutoSave_TracksRemoval() => base.Remove_WithoutAutoSave_TracksRemoval();
|
||||
[TestMethod]
|
||||
public override void RemoveAt_RemovesCorrectItem() => base.RemoveAt_RemovesCorrectItem();
|
||||
[TestMethod]
|
||||
public override async Task RemoveAt_WithAutoSave_SavesImmediately() => await base.RemoveAt_WithAutoSave_SavesImmediately();
|
||||
[TestMethod]
|
||||
public override void TryRemove_ReturnsTrue_WhenExists() => base.TryRemove_ReturnsTrue_WhenExists();
|
||||
[TestMethod]
|
||||
public override void TryRemove_ReturnsFalse_WhenNotExists() => base.TryRemove_ReturnsFalse_WhenNotExists();
|
||||
[TestMethod]
|
||||
public override async Task SaveChanges_SavesTrackedItems() => await base.SaveChanges_SavesTrackedItems();
|
||||
[TestMethod]
|
||||
public override async Task SaveChangesAsync_ClearsTracking() => await base.SaveChangesAsync_ClearsTracking();
|
||||
[TestMethod]
|
||||
public override async Task SaveItem_ById_SavesSpecificItem() => await base.SaveItem_ById_SavesSpecificItem();
|
||||
[TestMethod]
|
||||
public override async Task SaveItem_WithTrackingState_SavesCorrectly() => await base.SaveItem_WithTrackingState_SavesCorrectly();
|
||||
[TestMethod]
|
||||
public override void SetTrackingStateToUpdate_MarksItemForUpdate() => base.SetTrackingStateToUpdate_MarksItemForUpdate();
|
||||
[TestMethod]
|
||||
public override void SetTrackingStateToUpdate_DoesNotChangeAddState() => base.SetTrackingStateToUpdate_DoesNotChangeAddState();
|
||||
[TestMethod]
|
||||
public override void TryGetTrackingItem_ReturnsTrue_WhenTracked() => base.TryGetTrackingItem_ReturnsTrue_WhenTracked();
|
||||
[TestMethod]
|
||||
public override void TryGetTrackingItem_ReturnsFalse_WhenNotTracked() => base.TryGetTrackingItem_ReturnsFalse_WhenNotTracked();
|
||||
[TestMethod]
|
||||
public override void TryRollbackItem_RevertsAddedItem() => base.TryRollbackItem_RevertsAddedItem();
|
||||
[TestMethod]
|
||||
public override async Task TryRollbackItem_RevertsUpdatedItem() => await base.TryRollbackItem_RevertsUpdatedItem();
|
||||
[TestMethod]
|
||||
public override void Rollback_RevertsAllChanges() => base.Rollback_RevertsAllChanges();
|
||||
[TestMethod]
|
||||
public override async Task Count_ReturnsCorrectValue() => await base.Count_ReturnsCorrectValue();
|
||||
[TestMethod]
|
||||
public override void Clear_RemovesAllItems() => base.Clear_RemovesAllItems();
|
||||
[TestMethod]
|
||||
public override void Clear_WithoutClearingTracking_PreservesTracking() => base.Clear_WithoutClearingTracking_PreservesTracking();
|
||||
[TestMethod]
|
||||
public override async Task Contains_ReturnsTrue_WhenItemExists() => await base.Contains_ReturnsTrue_WhenItemExists();
|
||||
[TestMethod]
|
||||
public override void Contains_ReturnsFalse_WhenItemNotExists() => base.Contains_ReturnsFalse_WhenItemNotExists();
|
||||
[TestMethod]
|
||||
public override async Task IndexOf_ReturnsCorrectIndex() => await base.IndexOf_ReturnsCorrectIndex();
|
||||
[TestMethod]
|
||||
public override void IndexOf_ById_ReturnsCorrectIndex() => base.IndexOf_ById_ReturnsCorrectIndex();
|
||||
[TestMethod]
|
||||
public override void TryGetIndex_ReturnsTrue_WhenExists() => base.TryGetIndex_ReturnsTrue_WhenExists();
|
||||
[TestMethod]
|
||||
public override async Task TryGetValue_ReturnsItem_WhenExists() => await base.TryGetValue_ReturnsItem_WhenExists();
|
||||
[TestMethod]
|
||||
public override void TryGetValue_ReturnsFalse_WhenNotExists() => base.TryGetValue_ReturnsFalse_WhenNotExists();
|
||||
[TestMethod]
|
||||
public override async Task CopyTo_CopiesAllItems() => await base.CopyTo_CopiesAllItems();
|
||||
[TestMethod]
|
||||
public override async Task GetEnumerator_EnumeratesAllItems() => await base.GetEnumerator_EnumeratesAllItems();
|
||||
[TestMethod]
|
||||
public override async Task AsReadOnly_ReturnsReadOnlyCollection() => await base.AsReadOnly_ReturnsReadOnlyCollection();
|
||||
[TestMethod]
|
||||
public override async Task SetWorkingReferenceList_SetsNewInnerList() => await base.SetWorkingReferenceList_SetsNewInnerList();
|
||||
[TestMethod]
|
||||
public override async Task GetReferenceInnerList_ReturnsInnerList() => await base.GetReferenceInnerList_ReturnsInnerList();
|
||||
[TestMethod]
|
||||
public override void IsSyncing_IsFalse_Initially() => base.IsSyncing_IsFalse_Initially();
|
||||
[TestMethod]
|
||||
public override async Task OnSyncingStateChanged_Fires_DuringLoad() => await base.OnSyncingStateChanged_Fires_DuringLoad();
|
||||
[TestMethod]
|
||||
public override void ContextIds_CanBeSetAndRetrieved() => base.ContextIds_CanBeSetAndRetrieved();
|
||||
[TestMethod]
|
||||
public override void FilterText_CanBeSetAndRetrieved() => base.FilterText_CanBeSetAndRetrieved();
|
||||
[TestMethod]
|
||||
public override void IList_Add_ReturnsCorrectIndex() => base.IList_Add_ReturnsCorrectIndex();
|
||||
[TestMethod]
|
||||
public override void IList_Contains_WorksCorrectly() => base.IList_Contains_WorksCorrectly();
|
||||
[TestMethod]
|
||||
public override void IList_IndexOf_WorksCorrectly() => base.IList_IndexOf_WorksCorrectly();
|
||||
[TestMethod]
|
||||
public override void IList_Insert_WorksCorrectly() => base.IList_Insert_WorksCorrectly();
|
||||
[TestMethod]
|
||||
public override void IList_Remove_WorksCorrectly() => base.IList_Remove_WorksCorrectly();
|
||||
[TestMethod]
|
||||
public override void IList_Indexer_GetSet_WorksCorrectly() => base.IList_Indexer_GetSet_WorksCorrectly();
|
||||
[TestMethod]
|
||||
public override void ICollection_CopyTo_WorksCorrectly() => base.ICollection_CopyTo_WorksCorrectly();
|
||||
[TestMethod]
|
||||
public override void IsSynchronized_ReturnsTrue() => base.IsSynchronized_ReturnsTrue();
|
||||
[TestMethod]
|
||||
public override void SyncRoot_IsNotNull() => base.SyncRoot_IsNotNull();
|
||||
[TestMethod]
|
||||
public override void IsFixedSize_ReturnsFalse() => base.IsFixedSize_ReturnsFalse();
|
||||
[TestMethod]
|
||||
public override void IsReadOnly_ReturnsFalse() => base.IsReadOnly_ReturnsFalse();
|
||||
[TestMethod]
|
||||
public override async Task Indexer_OutOfRange_ThrowsException() => await base.Indexer_OutOfRange_ThrowsException();
|
||||
[TestMethod]
|
||||
public override void Add_ThenRemove_ClearsTracking() => base.Add_ThenRemove_ClearsTracking();
|
||||
[TestMethod]
|
||||
public override async Task ComplexWorkflow_AddUpdateRemoveSave() => await base.ComplexWorkflow_AddUpdateRemoveSave();
|
||||
}
|
||||
|
|
@ -1,6 +1,8 @@
|
|||
using System.Globalization;
|
||||
using AyCode.Core.Tests.TestModels;
|
||||
using AyCode.Services.SignalRs;
|
||||
using static AyCode.Core.Tests.Serialization.AcSerializerModels;
|
||||
using AyCode.Core.Tests.Serialization;
|
||||
|
||||
namespace AyCode.Services.Server.Tests.SignalRs;
|
||||
|
||||
|
|
@ -468,4 +470,72 @@ public class TestSignalRService2
|
|||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region StockTaking Production Bug Reproduction
|
||||
|
||||
/// <summary>
|
||||
/// Simulates the exact production scenario from FruitBank GetStockTakings(false).
|
||||
/// Returns data from actual database records to reproduce the bug.
|
||||
/// Uses the REAL StockTaking model from StockTakingTestModels.cs
|
||||
/// </summary>
|
||||
[SignalR(TestSignalRTags.GetStockTakings)]
|
||||
public List<StockTaking> GetStockTakings(bool loadRelations)
|
||||
{
|
||||
// Exact data from production database:
|
||||
return
|
||||
[
|
||||
new StockTaking
|
||||
{
|
||||
Id = 7,
|
||||
StartDateTime = new DateTime(2025, 12, 3, 8, 55, 43, 539, DateTimeKind.Utc),
|
||||
IsClosed = false, // This is the key - IsClosed=false gets skipped by serializer!
|
||||
Creator = 6,
|
||||
Created = new DateTime(2025, 12, 3, 7, 55, 43, 571, DateTimeKind.Utc),
|
||||
Modified = new DateTime(2025, 12, 3, 7, 55, 43, 571, DateTimeKind.Utc),
|
||||
StockTakingItems = loadRelations ? [] : null
|
||||
},
|
||||
new StockTaking
|
||||
{
|
||||
Id = 6,
|
||||
StartDateTime = new DateTime(2025, 12, 2, 8, 21, 26, 439, DateTimeKind.Utc),
|
||||
IsClosed = true,
|
||||
Creator = 6,
|
||||
Created = new DateTime(2025, 12, 2, 7, 21, 26, 468, DateTimeKind.Utc),
|
||||
Modified = new DateTime(2025, 12, 2, 7, 21, 26, 468, DateTimeKind.Utc),
|
||||
StockTakingItems = loadRelations ? [] : null
|
||||
},
|
||||
new StockTaking
|
||||
{
|
||||
Id = 3,
|
||||
StartDateTime = new DateTime(2025, 11, 30, 14, 1, 55, 663, DateTimeKind.Utc),
|
||||
IsClosed = true,
|
||||
Creator = 6,
|
||||
Created = new DateTime(2025, 11, 30, 13, 1, 55, 692, DateTimeKind.Utc),
|
||||
Modified = new DateTime(2025, 11, 30, 13, 1, 55, 692, DateTimeKind.Utc),
|
||||
StockTakingItems = loadRelations ? [] : null
|
||||
},
|
||||
new StockTaking
|
||||
{
|
||||
Id = 2,
|
||||
StartDateTime = new DateTime(2025, 11, 30, 8, 20, 2, 182, DateTimeKind.Utc),
|
||||
IsClosed = true,
|
||||
Creator = 6,
|
||||
Created = new DateTime(2025, 11, 30, 7, 20, 3, 331, DateTimeKind.Utc),
|
||||
Modified = new DateTime(2025, 11, 30, 7, 20, 3, 331, DateTimeKind.Utc),
|
||||
StockTakingItems = loadRelations ? [] : null
|
||||
},
|
||||
new StockTaking
|
||||
{
|
||||
Id = 1,
|
||||
StartDateTime = new DateTime(2025, 11, 30, 8, 18, 59, 693, DateTimeKind.Utc),
|
||||
IsClosed = true,
|
||||
Creator = 6,
|
||||
Created = new DateTime(2025, 11, 30, 7, 19, 1, 849, DateTimeKind.Utc),
|
||||
Modified = new DateTime(2025, 11, 30, 7, 19, 1, 877, DateTimeKind.Utc),
|
||||
StockTakingItems = loadRelations ? [] : null
|
||||
}
|
||||
];
|
||||
}
|
||||
|
||||
#endregion
|
||||
}
|
||||
|
|
|
|||
|
|
@ -88,4 +88,7 @@ public abstract class TestSignalRTags : AcSignalRTags
|
|||
public const int DataSourceAdd = 302;
|
||||
public const int DataSourceUpdate = 303;
|
||||
public const int DataSourceRemove = 304;
|
||||
|
||||
// StockTaking production bug reproduction
|
||||
public const int GetStockTakings = 400;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -106,7 +106,7 @@ public static class SignalRSerializationHelper
|
|||
public static byte[] SerializeToCompressedJson<T>(T value, AcJsonSerializerOptions? options = null)
|
||||
{
|
||||
var json = value.ToJson(options ?? AcJsonSerializerOptions.Default);
|
||||
return BrotliHelper.Compress(json);
|
||||
return GzipHelper.Compress(json);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
|
@ -115,7 +115,7 @@ public static class SignalRSerializationHelper
|
|||
/// </summary>
|
||||
public static T? DeserializeFromCompressedJson<T>(byte[] compressedData)
|
||||
{
|
||||
var (buffer, length) = BrotliHelper.DecompressToRentedBuffer(compressedData.AsSpan());
|
||||
var (buffer, length) = GzipHelper.DecompressToRentedBuffer(compressedData.AsSpan());
|
||||
try
|
||||
{
|
||||
return AcJsonDeserializer.Deserialize<T>(new ReadOnlySpan<byte>(buffer, 0, length));
|
||||
|
|
@ -133,7 +133,7 @@ public static class SignalRSerializationHelper
|
|||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public static (byte[] Buffer, int Length) DecompressToRentedBuffer(byte[] compressedData)
|
||||
{
|
||||
return BrotliHelper.DecompressToRentedBuffer(compressedData.AsSpan());
|
||||
return GzipHelper.DecompressToRentedBuffer(compressedData.AsSpan());
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
|
@ -181,7 +181,7 @@ public static class SignalRSerializationHelper
|
|||
json = responseData.ToJson(jsonOptions);
|
||||
}
|
||||
|
||||
return BrotliHelper.Compress(json);
|
||||
return GzipHelper.Compress(json);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
|
|
|||
|
|
@ -0,0 +1,7 @@
|
|||
@echo off
|
||||
setlocal
|
||||
set ROOT=h:\Applications\Aycode\Source\AyCode.Core
|
||||
set COUNT=5
|
||||
powershell -ExecutionPolicy Bypass -File "%ROOT%\MergeBenchmarksHtmlDropdown.ps1" -maxCount %COUNT%
|
||||
endlocal
|
||||
pause
|
||||
|
|
@ -0,0 +1,114 @@
|
|||
param(
|
||||
[int]$maxCount = 5
|
||||
)
|
||||
|
||||
$root = "h:\Applications\Aycode\Source\AyCode.Core"
|
||||
$output = Join-Path $root "AllBenchmarksDropdown.html"
|
||||
|
||||
if (Test-Path $output) { Remove-Item $output }
|
||||
|
||||
$htmlFiles = Get-ChildItem -Path $root -Recurse -Filter "*-report.html" | Sort-Object LastWriteTime -Descending | Select-Object -First $maxCount
|
||||
|
||||
@"
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset='utf-8'>
|
||||
<title>BenchmarkDotNet Riportok (Dropdown)</title>
|
||||
<style>
|
||||
body { font-family: Segoe UI, Arial, sans-serif; margin: 2em; background: #f8f9fa; }
|
||||
select { font-size: 1.1em; padding: 0.2em; }
|
||||
.report-content { background: #fff; border: 1px solid #ccc; padding: 1em; margin-top: 1em; border-radius: 6px; box-shadow: 0 2px 8px #0001; min-height: 200px; }
|
||||
h1 { font-size: 1.5em; }
|
||||
.filename { color: #888; font-size: 0.95em; }
|
||||
.compare { color: #007700; font-size: 1.1em; margin-bottom: 1em; }
|
||||
.compare.negative { color: #bb2222; }
|
||||
iframe { width: 100%; min-height: 600px; border: none; background: #fff; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<h1>BenchmarkDotNet Riportok</h1>
|
||||
<label for='reportSelect'>Válassz riportot:</label>
|
||||
<select id='reportSelect'>
|
||||
"@ | Set-Content -Encoding UTF8 -Path $output
|
||||
|
||||
$reports = @()
|
||||
foreach ($file in $htmlFiles) {
|
||||
$id = ([System.IO.Path]::GetFileNameWithoutExtension($file.Name) + "_" + [Math]::Abs($file.FullName.GetHashCode()))
|
||||
$runName = ($file.FullName -replace "^.*SwitcherRun_([\w\dT]+).*", 'SwitcherRun_$1')
|
||||
if ($runName -eq $file.FullName) { $runName = $file.BaseName }
|
||||
$relPath = $file.FullName.Substring($root.Length + 1).Replace("\", "/")
|
||||
$reports += @{ Id = $id; Title = $runName; Path = $relPath }
|
||||
Add-Content -Encoding UTF8 -Path $output -Value " <option value='$id'>$runName</option>"
|
||||
}
|
||||
|
||||
@"
|
||||
</select>
|
||||
<div class='filename' id='filename'></div>
|
||||
<div class='compare' id='compare'></div>
|
||||
<div class='report-content'><iframe id='reportFrame'></iframe></div>
|
||||
<script>
|
||||
// Relatív riport fájlok
|
||||
const reports = {
|
||||
"@ | Add-Content -Encoding UTF8 -Path $output
|
||||
|
||||
$means = @{}
|
||||
foreach ($r in $reports) {
|
||||
Add-Content -Encoding UTF8 -Path $output -Value " '$($r.Id)': '$($r.Path)',"
|
||||
$html = Get-Content (Join-Path $root $r.Path) -Raw
|
||||
$mean = [regex]::Match($html, '(?<=<td>Mean</td><td>)[0-9.,]+').Value
|
||||
$means[$r.Id] = $mean
|
||||
}
|
||||
|
||||
@"
|
||||
};
|
||||
const means = {
|
||||
"@ | Add-Content -Encoding UTF8 -Path $output
|
||||
|
||||
foreach ($r in $reports) {
|
||||
$mean = $means[$r.Id]
|
||||
Add-Content -Encoding UTF8 -Path $output -Value " '$($r.Id)': '$mean',"
|
||||
}
|
||||
|
||||
@"
|
||||
};
|
||||
const select = document.getElementById('reportSelect');
|
||||
const frame = document.getElementById('reportFrame');
|
||||
const filename = document.getElementById('filename');
|
||||
const compare = document.getElementById('compare');
|
||||
function showReport() {
|
||||
const id = select.value;
|
||||
if (reports[id]) {
|
||||
frame.src = reports[id];
|
||||
} else {
|
||||
frame.srcdoc = '<i>Nincs tartalom.</i>';
|
||||
}
|
||||
const opt = select.options[select.selectedIndex];
|
||||
filename.textContent = opt ? opt.text : '';
|
||||
// Összehasonlítás az eggyel korábbival
|
||||
const idx = select.selectedIndex;
|
||||
if (idx < select.options.length - 1) {
|
||||
const prevId = select.options[idx + 1].value;
|
||||
const currMean = parseFloat(means[id].replace(',','.'));
|
||||
const prevMean = parseFloat(means[prevId].replace(',','.'));
|
||||
if (!isNaN(currMean) && !isNaN(prevMean) && prevMean > 0) {
|
||||
const diff = currMean - prevMean;
|
||||
const percent = (diff / prevMean * 100).toFixed(2);
|
||||
const sign = percent > 0 ? '+' : '';
|
||||
compare.textContent = `Eltérés az elõzõhöz képest: ${sign}${percent}% (${currMean} vs ${prevMean})`;
|
||||
compare.className = 'compare' + (percent > 0 ? ' negative' : '');
|
||||
} else {
|
||||
compare.textContent = '';
|
||||
}
|
||||
} else {
|
||||
compare.textContent = '';
|
||||
}
|
||||
}
|
||||
select.addEventListener('change', showReport);
|
||||
window.onload = showReport;
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
"@ | Add-Content -Encoding UTF8 -Path $output
|
||||
|
||||
Write-Host "Összefûzve: $output"
|
||||
|
|
@ -0,0 +1,6 @@
|
|||
dotnet run -c Release --project AyCode.Benchmark
|
||||
|
||||
echo.
|
||||
pause
|
||||
endlocal
|
||||
exit /b %EXITCODE%
|
||||
Loading…
Reference in New Issue