diff --git a/AyCode.Core/Serializers/Binaries/AcBinarySerializerOptions.cs b/AyCode.Core/Serializers/Binaries/AcBinarySerializerOptions.cs index 63cdf6e..2b5f664 100644 --- a/AyCode.Core/Serializers/Binaries/AcBinarySerializerOptions.cs +++ b/AyCode.Core/Serializers/Binaries/AcBinarySerializerOptions.cs @@ -1,5 +1,4 @@ using System.Runtime.CompilerServices; -using AyCode.Core.Extensions; using AyCode.Core.Serializers.Jsons; namespace AyCode.Core.Serializers.Binaries; @@ -71,7 +70,7 @@ public sealed class AcBinarySerializerOptions : AcSerializerOptions /// Auto-enabled when IsWasm is true, can be overridden. /// Default: follows IsWasm setting /// - public bool UseStringCaching { get; init; } = DetectedIsWasm; + public bool UseStringCaching { get; private init; } = DetectedIsWasm; /// /// Maximum string length to cache when UseStringCaching is enabled. @@ -219,13 +218,13 @@ internal static class BinaryTypeCode /// Check if type code represents a reference (string or object). /// [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static bool IsReference(byte code) => code == StringInterned || code == ObjectRef; + public static bool IsReference(byte code) => code is StringInterned or ObjectRef; /// /// Check if type code is a FixStr (short string with length encoded in type code). /// [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static bool IsFixStr(byte code) => code >= FixStrBase && code <= FixStrMax; + public static bool IsFixStr(byte code) => code is >= FixStrBase and <= FixStrMax; /// /// Decode FixStr length from type code. @@ -243,7 +242,7 @@ internal static class BinaryTypeCode /// Check if byte length can be encoded as FixStr. /// [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static bool CanEncodeAsFixStr(int byteLength) => byteLength >= 0 && byteLength <= 31; + public static bool CanEncodeAsFixStr(int byteLength) => byteLength is >= 0 and <= 31; /// /// Check if type code is a tiny int (single byte int32 encoding). @@ -265,7 +264,7 @@ internal static class BinaryTypeCode public static bool TryEncodeTinyInt(int value, out byte code) { // Range: -16 to 47 (64 values total, fitting in 192-255) - if (value >= -16 && value <= 47) + if (value is >= -16 and <= 47) { code = (byte)(value + 16 + Int32Tiny); return true; diff --git a/AyCode.Services.Server.Tests/SignalRs/SignalRClientToHubTest.cs b/AyCode.Services.Server.Tests/SignalRs/SignalRClientToHubTest.cs index c365b47..2c89899 100644 --- a/AyCode.Services.Server.Tests/SignalRs/SignalRClientToHubTest.cs +++ b/AyCode.Services.Server.Tests/SignalRs/SignalRClientToHubTest.cs @@ -995,6 +995,54 @@ public abstract class SignalRClientToHubTestBase } #endregion + + #region Default Parameter Tests + + /// + /// Tests calling a method with two parameters where the second has a default value, + /// but only passing the first parameter. + /// Expected: The default value (true) should be used for the optional parameter. + /// + [TestMethod] + public async Task Post_TwoParamsWithDefault_CalledWithOneParam_UsesDefaultValue() + { + // Call with only one parameter - the second parameter should use its default value (true) + var result = await _client.PostDataAsync(TestSignalRTags.TwoParamsWithDefault, 42); + + Assert.IsNotNull(result, "Result should not be null"); + Assert.AreEqual("Required: 42, Optional: True", result, + "The optional parameter should use its default value (true)"); + } + + /// + /// Tests calling a method with two parameters where the second has a default value, + /// passing both parameters explicitly. + /// Expected: The explicitly passed value (false) should override the default. + /// + [TestMethod] + public async Task Post_TwoParamsWithDefault_CalledWithBothParams_UsesExplicitValue() + { + // Call with both parameters - explicitly set optional to false + var result = await _client.PostAsync(TestSignalRTags.TwoParamsWithDefault, [42, false]); + + Assert.IsNotNull(result, "Result should not be null"); + Assert.AreEqual("Required: 42, Optional: False", result, + "The optional parameter should use the explicitly passed value (false)"); + } + + /// + /// Tests calling with both parameters set to true explicitly. + /// + [TestMethod] + public async Task Post_TwoParamsWithDefault_CalledWithBothParamsTrue_UsesExplicitValue() + { + var result = await _client.PostAsync(TestSignalRTags.TwoParamsWithDefault, [100, true]); + + Assert.IsNotNull(result, "Result should not be null"); + Assert.AreEqual("Required: 100, Optional: True", result); + } + + #endregion } /// diff --git a/AyCode.Services.Server.Tests/SignalRs/TestSignalRService2.cs b/AyCode.Services.Server.Tests/SignalRs/TestSignalRService2.cs index a073cf4..a3b66f8 100644 --- a/AyCode.Services.Server.Tests/SignalRs/TestSignalRService2.cs +++ b/AyCode.Services.Server.Tests/SignalRs/TestSignalRService2.cs @@ -538,4 +538,18 @@ public class TestSignalRService2 } #endregion + + #region Default Parameter Tests + + /// + /// Method with two parameters where the second has a default value. + /// Tests if the SignalR infrastructure correctly handles optional parameters. + /// + [SignalR(TestSignalRTags.TwoParamsWithDefault)] + public string HandleTwoParamsWithDefault(int requiredParam, bool optionalParam = true) + { + return $"Required: {requiredParam}, Optional: {optionalParam}"; + } + + #endregion } diff --git a/AyCode.Services.Server.Tests/SignalRs/TestSignalRTags.cs b/AyCode.Services.Server.Tests/SignalRs/TestSignalRTags.cs index 51f0b11..3a33f96 100644 --- a/AyCode.Services.Server.Tests/SignalRs/TestSignalRTags.cs +++ b/AyCode.Services.Server.Tests/SignalRs/TestSignalRTags.cs @@ -91,4 +91,7 @@ public abstract class TestSignalRTags : AcSignalRTags // StockTaking production bug reproduction public const int GetStockTakings = 400; + + // Default parameter tests + public const int TwoParamsWithDefault = 410; } diff --git a/AyCode.Services.Server/SignalRs/AcWebSignalRHubBase.cs b/AyCode.Services.Server/SignalRs/AcWebSignalRHubBase.cs index 4c66a6b..ddd4945 100644 --- a/AyCode.Services.Server/SignalRs/AcWebSignalRHubBase.cs +++ b/AyCode.Services.Server/SignalRs/AcWebSignalRHubBase.cs @@ -298,10 +298,7 @@ public abstract class AcWebSignalRHubBase(IConfiguration var methodName = $"{methodsByDeclaringObject.InstanceObject.GetType().Name}.{methodInfoModel.MethodInfo.Name}"; var paramValues = DeserializeParameters(message, methodInfoModel, tagName, methodName); - if (paramValues == null) - Logger.Debug($"Found dynamic method for the tag! method: {methodName}(); {tagName}"); - else - Logger.Debug($"Found dynamic method for the tag! method: {methodName}({string.Join(", ", methodInfoModel.ParamInfos.Select(x => x.Name))}); {tagName}"); + Logger.Debug(paramValues == null ? $"Found dynamic method for the tag! method: {methodName}(); {tagName}" : $"Found dynamic method for the tag! method: {methodName}({string.Join(", ", methodInfoModel.ParamInfos.Select(x => x.Name))}); {tagName}"); responseData = methodInfoModel.MethodInfo.InvokeMethod(methodsByDeclaringObject.InstanceObject, paramValues); @@ -322,47 +319,69 @@ public abstract class AcWebSignalRHubBase(IConfiguration /// /// Deserializes parameters from the message based on method signature. - /// Uses Binary serialization for message wrapper. + /// Supports optional parameters with default values. /// private static object[]? DeserializeParameters(byte[]? message, AcMethodInfoModel methodInfoModel, string tagName, string methodName) { - if (methodInfoModel.ParamInfos is not { Length: > 0 }) + var paramInfos = methodInfoModel.ParamInfos; + if (paramInfos is not { Length: > 0 }) return null; if (message is null or { Length: 0 }) - throw new ArgumentException($"Message is null or empty but method '{methodName}' requires {methodInfoModel.ParamInfos.Length} parameter(s); {tagName}"); + throw new ArgumentException($"Message is null or empty but method '{methodName}' requires parameters; {tagName}"); - var paramValues = new object[methodInfoModel.ParamInfos.Length]; - var firstParamType = methodInfoModel.ParamInfos[0].ParameterType; - - // First, try to deserialize as SignalPostJsonMessage to get raw PostDataJson var msgBase = SignalRSerializationHelper.DeserializeFromBinary(message); - if (msgBase?.PostDataJson == null || string.IsNullOrEmpty(msgBase.PostDataJson)) - { + if (string.IsNullOrEmpty(msgBase?.PostDataJson)) throw new ArgumentException($"Failed to deserialize message for method '{methodName}'; {tagName}"); - } var json = msgBase.PostDataJson; + var paramValues = new object?[paramInfos.Length]; - // Check if it's an IdMessage format (contains "Ids" property) + // IdMessage format: multiple parameters as JSON array if (json.Contains("\"Ids\"")) { - // Parse as IdMessage - each Id is a JSON string for a parameter var idMessage = json.JsonTo(); - if (idMessage?.Ids != null && idMessage.Ids.Count > 0) + var providedCount = idMessage?.Ids?.Count ?? 0; + + for (var i = 0; i < paramInfos.Length; i++) { - for (var i = 0; i < idMessage.Ids.Count && i < methodInfoModel.ParamInfos.Length; i++) + var param = paramInfos[i]; + + if (i < providedCount) { - var paramType = methodInfoModel.ParamInfos[i].ParameterType; - paramValues[i] = AcJsonDeserializer.Deserialize(idMessage.Ids[i], paramType)!; + paramValues[i] = AcJsonDeserializer.Deserialize(idMessage!.Ids![i], param.ParameterType); + } + else if (param.HasDefaultValue) + { + paramValues[i] = param.DefaultValue; + } + else + { + throw new ArgumentException($"Missing required parameter '{param.Name}' for method '{methodName}'; {tagName}"); + } + } + } + else + { + // Single complex object format + paramValues[0] = json.JsonTo(paramInfos[0].ParameterType); + + // Fill remaining parameters with defaults + for (var i = 1; i < paramInfos.Length; i++) + { + var param = paramInfos[i]; + if (param.HasDefaultValue) + { + paramValues[i] = param.DefaultValue; + } + else + { + throw new ArgumentException($"Missing required parameter '{param.Name}' for method '{methodName}'; {tagName}"); } - return paramValues; } } - // Single complex object - deserialize directly from PostDataJson - paramValues[0] = json.JsonTo(firstParamType)!; - return paramValues; + return paramValues!; } ///