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!;
}
///