diff --git a/FruitBank.Common.Server/Services/SignalRs/DevAdminSignalRhub.cs b/FruitBank.Common.Server/Services/SignalRs/DevAdminSignalRhub.cs index e4c1fab..f30502b 100644 --- a/FruitBank.Common.Server/Services/SignalRs/DevAdminSignalRhub.cs +++ b/FruitBank.Common.Server/Services/SignalRs/DevAdminSignalRhub.cs @@ -35,6 +35,7 @@ public class DevAdminSignalRHub : AcWebSignalRHubWithSessionBase logWriters) : base(configuration, new Logger(logWriters.ToArray())) { + EnableBinaryDiagnostics = FruitBankConstClient.SignalRSerializerDiagnosticLog; SerializerOptions = new AcBinarySerializerOptions(); //SerializerOptions = new AcJsonSerializerOptions(); diff --git a/FruitBank.Common/FruitBankConstClient.cs b/FruitBank.Common/FruitBankConstClient.cs index ecd408a..07bac4e 100644 --- a/FruitBank.Common/FruitBankConstClient.cs +++ b/FruitBank.Common/FruitBankConstClient.cs @@ -22,6 +22,7 @@ public static class FruitBankConstClient public static string DefaultHubName = "fbHub"; public static string LoggerHubName = "loggerHub"; + public static bool SignalRSerializerDiagnosticLog = false; public static long SignalRKeepAliveIntervalSecond = 60; public static long SignarlRTimeoutIntervalSecond = 180; diff --git a/FruitBankHybrid.Shared.Tests/OrderClientTests.cs b/FruitBankHybrid.Shared.Tests/OrderClientTests.cs index 1f3f9a0..37dc2e3 100644 --- a/FruitBankHybrid.Shared.Tests/OrderClientTests.cs +++ b/FruitBankHybrid.Shared.Tests/OrderClientTests.cs @@ -37,7 +37,7 @@ public sealed class OrderClientTests [TestMethod] public async Task GetAllStockTakings() { - var stockTakings = await _signalRClient.GetStockTakings(true); + var stockTakings = await _signalRClient.GetStockTakings(false); Assert.IsNotNull(stockTakings); Assert.IsTrue(stockTakings.Count != 0); diff --git a/FruitBankHybrid.Shared.Tests/StockTakingSerializerTests.cs b/FruitBankHybrid.Shared.Tests/StockTakingSerializerTests.cs new file mode 100644 index 0000000..e8dba09 --- /dev/null +++ b/FruitBankHybrid.Shared.Tests/StockTakingSerializerTests.cs @@ -0,0 +1,267 @@ +using AyCode.Core.Extensions; +using AyCode.Core.Serializers.Binaries; +using FruitBank.Common.Entities; +using System.Reflection; + +namespace FruitBankHybrid.Shared.Tests; + +[TestClass] +public class StockTakingSerializerTests +{ + [TestMethod] + public void StockTaking_WithNullItems_RoundTrip() + { + var stockTaking = new StockTaking + { + 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 + }; + + // Log StockTaking properties + Console.WriteLine("=== StockTaking Properties ==="); + var stProps = typeof(StockTaking).GetProperties(BindingFlags.Public | BindingFlags.Instance) + .Where(p => p.CanRead && p.GetIndexParameters().Length == 0) + .ToArray(); + for (int i = 0; i < stProps.Length; i++) + Console.WriteLine($" [{i}] {stProps[i].Name} : {stProps[i].PropertyType.Name} (from: {stProps[i].DeclaringType?.Name})"); + + // Log StockTakingItem properties + Console.WriteLine("\n=== StockTakingItem Properties ==="); + var stiProps = typeof(StockTakingItem).GetProperties(BindingFlags.Public | BindingFlags.Instance) + .Where(p => p.CanRead && p.GetIndexParameters().Length == 0) + .ToArray(); + for (int i = 0; i < stiProps.Length; i++) + { + var hasIgnore = Attribute.IsDefined(stiProps[i], typeof(Newtonsoft.Json.JsonIgnoreAttribute)) || + Attribute.IsDefined(stiProps[i], typeof(System.Text.Json.Serialization.JsonIgnoreAttribute)); + Console.WriteLine($" [{i}] {stiProps[i].Name} : {stiProps[i].PropertyType.Name}{(hasIgnore ? " [IGNORED]" : "")}"); + } + + var binary = stockTaking.ToBinary(); + Console.WriteLine($"\nBinary length: {binary.Length}"); + + // Parse header + var pos = 0; + var version = binary[pos++]; + var marker = binary[pos++]; + Console.WriteLine($"Version: {version}, Marker: 0x{marker:X2}"); + + if ((marker & 0x10) != 0) + { + var propCount = binary[pos++]; + Console.WriteLine($"\n=== HEADER ({propCount} properties) ==="); + 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($" [{i}]: '{propName}'"); + } + } + + // Deserialize + try + { + var result = binary.BinaryTo(); + + Console.WriteLine($"\n=== RESULT ==="); + Console.WriteLine($"Id: {result?.Id}"); + Console.WriteLine($"Creator: {result?.Creator}"); + Console.WriteLine($"Created: {result?.Created}"); + + Assert.IsNotNull(result); + Assert.AreEqual(1, result.Id, "Id mismatch"); + Assert.AreEqual(6, result.Creator, $"Creator should be 6, got {result.Creator}"); + Assert.AreEqual(stockTaking.Created, result.Created, $"Created mismatch"); + } + catch (Exception ex) + { + Console.WriteLine($"\n=== ERROR ==="); + Console.WriteLine(ex.Message); + throw; + } + } + + [TestMethod] + public void ListOfStockTaking_WithNullItems_RoundTrip() + { + var stockTakings = new List + { + new() { Id = 1, StartDateTime = DateTime.UtcNow, IsClosed = false, Creator = 6, Created = DateTime.UtcNow, Modified = DateTime.UtcNow, StockTakingItems = null }, + new() { Id = 2, StartDateTime = DateTime.UtcNow, IsClosed = true, Creator = 12, Created = DateTime.UtcNow, Modified = DateTime.UtcNow, StockTakingItems = null } + }; + + var binary = stockTakings.ToBinary(); + Console.WriteLine($"Binary length: {binary.Length}"); + + var result = binary.BinaryTo>(); + + Assert.IsNotNull(result); + Assert.AreEqual(2, result.Count); + Assert.AreEqual(6, result[0].Creator, $"First Creator should be 6, got {result[0].Creator}"); + Assert.AreEqual(12, result[1].Creator, $"Second Creator should be 12, got {result[1].Creator}"); + } + + [TestMethod] + public void StockTaking_DebugBodyProperties() + { + var stockTaking = new StockTaking + { + 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(); + + // Parse header + var pos = 0; + var version = binary[pos++]; + var marker = binary[pos++]; + + var propertyNames = new List(); + if ((marker & 0x10) != 0) + { + var propCount = (int)binary[pos++]; // Simplified VarUInt read + for (int i = 0; i < propCount; i++) + { + var strLen = (int)binary[pos++]; + var propName = System.Text.Encoding.UTF8.GetString(binary, pos, strLen); + pos += strLen; + propertyNames.Add(propName); + } + } + + // Parse body + Console.WriteLine($"\n=== BODY (starts at position {pos}) ==="); + var objectMarker = binary[pos++]; + Console.WriteLine($"Object marker: 0x{objectMarker:X2}"); + + // Read ref ID (VarInt) + var refIdByte = binary[pos]; + if ((refIdByte & 0x80) == 0) pos++; + else pos += 2; + + // Read property count + var bodyPropCount = binary[pos++]; + Console.WriteLine($"Body property count: {bodyPropCount}"); + + for (int i = 0; i < bodyPropCount; i++) + { + var propIndex = binary[pos++]; + var propName = propIndex < propertyNames.Count ? propertyNames[propIndex] : $"UNKNOWN({propIndex})"; + var valueType = binary[pos]; + + string valueInfo; + if (valueType == 0x14) // DateTime + { + valueInfo = "DateTime"; + pos += 10; + } + else if (valueType >= 0xD0) // TinyInt + { + var tinyValue = valueType - 0xD0; + valueInfo = $"TinyInt({tinyValue})"; + pos += 1; + } + else if (valueType == 0x03) + { + valueInfo = "False"; + pos += 1; + } + else if (valueType == 0x02) + { + valueInfo = "True"; + pos += 1; + } + else + { + valueInfo = $"0x{valueType:X2}"; + break; + } + + Console.WriteLine($" Body[{i}]: index={propIndex} -> '{propName}' = {valueInfo}"); + } + } + + [TestMethod] + public void StockTaking_ExactProductionData_RoundTrip() + { + var stockTakings = new List + { + new() + { + Id = 7, + StartDateTime = new DateTime(2025, 12, 3, 8, 55, 43, 539, DateTimeKind.Utc), + IsClosed = false, + 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 = null + }, + new() + { + 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 = null + }, + new() + { + 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 = null + }, + new() + { + 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 = null + }, + new() + { + 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 = null + } + }; + + var binary = stockTakings.ToBinary(); + Console.WriteLine($"Binary length: {binary.Length}"); + + var result = binary.BinaryTo>(); + + Assert.IsNotNull(result); + Assert.AreEqual(5, result.Count); + + foreach (var item in result) + { + Assert.AreEqual(6, item.Creator, + $"StockTaking Id={item.Id}: Creator should be 6, got {item.Creator}"); + } + } +} diff --git a/FruitBankHybrid.Shared/Services/SignalRs/FruitBankSignalRClient.cs b/FruitBankHybrid.Shared/Services/SignalRs/FruitBankSignalRClient.cs index a39f9c6..63fc5a5 100644 --- a/FruitBankHybrid.Shared/Services/SignalRs/FruitBankSignalRClient.cs +++ b/FruitBankHybrid.Shared/Services/SignalRs/FruitBankSignalRClient.cs @@ -33,7 +33,7 @@ namespace FruitBankHybrid.Shared.Services.SignalRs // .WithStatefulReconnect() // .WithKeepAliveInterval(TimeSpan.FromSeconds(60)) // .WithServerTimeout(TimeSpan.FromSeconds(120)) - + EnableBinaryDiagnostics = FruitBankConstClient.SignalRSerializerDiagnosticLog; ConstHelper.NameByValue(0); } diff --git a/FruitBankHybrid.Web.Client/Program.cs b/FruitBankHybrid.Web.Client/Program.cs index f524ea8..98dde71 100644 --- a/FruitBankHybrid.Web.Client/Program.cs +++ b/FruitBankHybrid.Web.Client/Program.cs @@ -1,4 +1,6 @@ using AyCode.Core.Loggers; +using AyCode.Services.SignalRs; +using FruitBank.Common; using FruitBank.Common.Loggers; using FruitBank.Common.Models; using FruitBank.Common.Services; @@ -29,4 +31,11 @@ builder.Services.AddSingleton(); builder.Services.AddSingleton(); +#if DEBUG +if (FruitBankConstClient.SignalRSerializerDiagnosticLog) +{ + SignalResponseDataMessage.DiagnosticLogger = message => { Console.WriteLine(message); }; +} +#endif + await builder.Build().RunAsync();