Unify SignalR response serialization (JSON/Binary/Brotli)
Major refactor: all SignalR responses now use a single unified `SignalResponseDataMessage` type with binary payloads. JSON responses are Brotli-compressed for efficiency. Removed legacy JSON/Binary response types and MessagePack server-to-client serialization. Updated all serialization extensions for zero-allocation binary ops. Refactored client/server/data source/test code to use new message and serialization model. Improved deserialization robustness for primitives. Modernized and streamlined test suite.
This commit is contained in:
parent
60238952d8
commit
ac6735ebd8
|
|
@ -0,0 +1,114 @@
|
|||
using System.IO.Compression;
|
||||
using System.Text;
|
||||
|
||||
namespace AyCode.Core.Compression;
|
||||
|
||||
/// <summary>
|
||||
/// Brotli compression/decompression helper for SignalR message transport.
|
||||
/// Used when JSON serializer is configured to reduce payload size.
|
||||
/// </summary>
|
||||
public static class BrotliHelper
|
||||
{
|
||||
/// <summary>
|
||||
/// Compresses a string using Brotli compression.
|
||||
/// </summary>
|
||||
/// <param name="text">The text to compress.</param>
|
||||
/// <param name="compressionLevel">Compression level (default: Optimal).</param>
|
||||
/// <returns>Compressed byte array.</returns>
|
||||
public static byte[] Compress(string text, CompressionLevel compressionLevel = CompressionLevel.Optimal)
|
||||
{
|
||||
if (string.IsNullOrEmpty(text))
|
||||
return [];
|
||||
|
||||
var bytes = Encoding.UTF8.GetBytes(text);
|
||||
return Compress(bytes, compressionLevel);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Compresses a byte array using Brotli compression.
|
||||
/// </summary>
|
||||
/// <param name="data">The data to compress.</param>
|
||||
/// <param name="compressionLevel">Compression level (default: Optimal).</param>
|
||||
/// <returns>Compressed byte array.</returns>
|
||||
public static byte[] Compress(byte[] data, CompressionLevel compressionLevel = CompressionLevel.Optimal)
|
||||
{
|
||||
if (data == null || data.Length == 0)
|
||||
return [];
|
||||
|
||||
using var outputStream = new MemoryStream();
|
||||
using (var brotliStream = new BrotliStream(outputStream, compressionLevel, leaveOpen: true))
|
||||
{
|
||||
brotliStream.Write(data, 0, data.Length);
|
||||
}
|
||||
|
||||
return outputStream.ToArray();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Decompresses Brotli-compressed data to a string.
|
||||
/// </summary>
|
||||
/// <param name="compressedData">The compressed data.</param>
|
||||
/// <returns>Decompressed string.</returns>
|
||||
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 Brotli-compressed data to a byte array.
|
||||
/// </summary>
|
||||
/// <param name="compressedData">The compressed data.</param>
|
||||
/// <returns>Decompressed byte array.</returns>
|
||||
public static byte[] Decompress(byte[] compressedData)
|
||||
{
|
||||
if (compressedData == null || compressedData.Length == 0)
|
||||
return [];
|
||||
|
||||
using var inputStream = new MemoryStream(compressedData);
|
||||
using var brotliStream = new BrotliStream(inputStream, CompressionMode.Decompress);
|
||||
using var outputStream = new MemoryStream();
|
||||
|
||||
brotliStream.CopyTo(outputStream);
|
||||
return outputStream.ToArray();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Checks if the data appears to be Brotli compressed.
|
||||
/// Brotli doesn't have a magic number, so we use a heuristic approach.
|
||||
/// </summary>
|
||||
/// <param name="data">The data to check.</param>
|
||||
/// <returns>True if the data might be Brotli compressed.</returns>
|
||||
public static bool IsBrotliCompressed(byte[] data)
|
||||
{
|
||||
if (data == null || data.Length < 4)
|
||||
return false;
|
||||
|
||||
// Brotli doesn't have a magic header like gzip (0x1F 0x8B)
|
||||
// We check if it's NOT valid UTF-8 JSON (starts with { or [)
|
||||
// and try to decompress
|
||||
var firstByte = data[0];
|
||||
|
||||
// If it starts with '{' (0x7B) or '[' (0x5B), it's likely uncompressed JSON
|
||||
if (firstByte == 0x7B || firstByte == 0x5B)
|
||||
return false;
|
||||
|
||||
// Try to decompress - if it fails, it's not Brotli
|
||||
try
|
||||
{
|
||||
using var inputStream = new MemoryStream(data);
|
||||
using var brotliStream = new BrotliStream(inputStream, CompressionMode.Decompress);
|
||||
|
||||
// Try to read first byte
|
||||
var buffer = new byte[1];
|
||||
return brotliStream.Read(buffer, 0, 1) >= 0;
|
||||
}
|
||||
catch
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1323,12 +1323,24 @@ public static class AcJsonDeserializer
|
|||
case TypeCode.Decimal: result = decimal.Parse(json, CultureInfo.InvariantCulture); return true;
|
||||
case TypeCode.Single: result = float.Parse(json, CultureInfo.InvariantCulture); return true;
|
||||
case TypeCode.String:
|
||||
// If already unwrapped (no quotes), return as-is; otherwise parse JSON
|
||||
if (json.Length == 0 || json[0] != '"')
|
||||
{
|
||||
result = json;
|
||||
return true;
|
||||
}
|
||||
using (var doc = JsonDocument.Parse(json))
|
||||
{
|
||||
result = doc.RootElement.GetString();
|
||||
return true;
|
||||
}
|
||||
case TypeCode.DateTime:
|
||||
// If already unwrapped (no quotes), parse directly; otherwise use JSON parser
|
||||
if (json.Length == 0 || json[0] != '"')
|
||||
{
|
||||
result = DateTime.Parse(json, CultureInfo.InvariantCulture, System.Globalization.DateTimeStyles.RoundtripKind);
|
||||
return true;
|
||||
}
|
||||
using (var doc = JsonDocument.Parse(json))
|
||||
{
|
||||
result = doc.RootElement.GetDateTime();
|
||||
|
|
@ -1341,6 +1353,11 @@ public static class AcJsonDeserializer
|
|||
case TypeCode.UInt64: result = ulong.Parse(json, CultureInfo.InvariantCulture); return true;
|
||||
case TypeCode.SByte: result = sbyte.Parse(json, CultureInfo.InvariantCulture); return true;
|
||||
case TypeCode.Char:
|
||||
if (json.Length == 0 || json[0] != '"')
|
||||
{
|
||||
result = json.Length > 0 ? json[0] : '\0';
|
||||
return true;
|
||||
}
|
||||
using (var doc = JsonDocument.Parse(json))
|
||||
{
|
||||
var s = doc.RootElement.GetString();
|
||||
|
|
@ -1351,6 +1368,12 @@ public static class AcJsonDeserializer
|
|||
|
||||
if (ReferenceEquals(type, GuidType))
|
||||
{
|
||||
// If already unwrapped (no quotes), parse directly
|
||||
if (json.Length == 0 || json[0] != '"')
|
||||
{
|
||||
result = Guid.Parse(json);
|
||||
return true;
|
||||
}
|
||||
using var doc = JsonDocument.Parse(json);
|
||||
result = doc.RootElement.GetGuid();
|
||||
return true;
|
||||
|
|
@ -1358,6 +1381,12 @@ public static class AcJsonDeserializer
|
|||
|
||||
if (ReferenceEquals(type, DateTimeOffsetType))
|
||||
{
|
||||
// If already unwrapped (no quotes), parse directly
|
||||
if (json.Length == 0 || json[0] != '"')
|
||||
{
|
||||
result = DateTimeOffset.Parse(json, CultureInfo.InvariantCulture, System.Globalization.DateTimeStyles.RoundtripKind);
|
||||
return true;
|
||||
}
|
||||
using var doc = JsonDocument.Parse(json);
|
||||
result = doc.RootElement.GetDateTimeOffset();
|
||||
return true;
|
||||
|
|
@ -1365,6 +1394,12 @@ public static class AcJsonDeserializer
|
|||
|
||||
if (ReferenceEquals(type, TimeSpanType))
|
||||
{
|
||||
// If already unwrapped (no quotes), parse directly
|
||||
if (json.Length == 0 || json[0] != '"')
|
||||
{
|
||||
result = TimeSpan.Parse(json, CultureInfo.InvariantCulture);
|
||||
return true;
|
||||
}
|
||||
using var doc = JsonDocument.Parse(json);
|
||||
result = TimeSpan.Parse(doc.RootElement.GetString()!, CultureInfo.InvariantCulture);
|
||||
return true;
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
using System.Collections.Concurrent;
|
||||
using System.Buffers;
|
||||
using System.Collections.Concurrent;
|
||||
using System.Reflection;
|
||||
using System.Runtime.CompilerServices;
|
||||
using System.Runtime.Serialization;
|
||||
|
|
@ -337,6 +338,8 @@ public static class SerializeObjectExtensions
|
|||
Formatting = Formatting.None,
|
||||
};
|
||||
|
||||
#region JSON Serialization
|
||||
|
||||
/// <summary>
|
||||
/// Serialize object to JSON string with default options.
|
||||
/// </summary>
|
||||
|
|
@ -414,29 +417,9 @@ public static class SerializeObjectExtensions
|
|||
AcJsonDeserializer.Populate(json, target, options);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Clone object via JSON serialization with default options.
|
||||
/// </summary>
|
||||
public static TDestination? CloneTo<TDestination>(this object? src) where TDestination : class
|
||||
=> src?.ToJson().JsonTo<TDestination>();
|
||||
#endregion
|
||||
|
||||
/// <summary>
|
||||
/// Clone object via JSON serialization with specified options.
|
||||
/// </summary>
|
||||
public static TDestination? CloneTo<TDestination>(this object? src, AcJsonSerializerOptions options) where TDestination : class
|
||||
=> src?.ToJson(options).JsonTo<TDestination>(options);
|
||||
|
||||
/// <summary>
|
||||
/// Copy object properties to target via JSON with default options.
|
||||
/// </summary>
|
||||
public static void CopyTo(this object? src, object target)
|
||||
=> src?.ToJson().JsonTo(target);
|
||||
|
||||
/// <summary>
|
||||
/// Copy object properties to target via JSON with specified options.
|
||||
/// </summary>
|
||||
public static void CopyTo(this object? src, object target, AcJsonSerializerOptions options)
|
||||
=> src?.ToJson(options).JsonTo(target, options);
|
||||
#region MessagePack
|
||||
|
||||
public static byte[] ToMessagePack(this object message, MessagePackSerializerOptions options)
|
||||
=> MessagePackSerializer.Serialize(message, options);
|
||||
|
|
@ -444,6 +427,9 @@ public static class SerializeObjectExtensions
|
|||
public static T MessagePackTo<T>(this byte[] message, MessagePackSerializerOptions options)
|
||||
=> MessagePackSerializer.Deserialize<T>(message, options);
|
||||
|
||||
#endregion
|
||||
|
||||
#region Any (JSON or Binary based on options)
|
||||
|
||||
public static object ToAny<T>(this T source, AcSerializerOptions options)
|
||||
{
|
||||
|
|
@ -488,37 +474,17 @@ public static class SerializeObjectExtensions
|
|||
public static void AnyToMerge<T>(this object data, T target, AcSerializerOptions options) where T : class
|
||||
{
|
||||
if (options.SerializerType == AcSerializerType.Json)
|
||||
((string)data).JsonTo(target, (AcJsonSerializerOptions)options); // JSON always merges
|
||||
((string)data).JsonTo(target, (AcJsonSerializerOptions)options);
|
||||
else
|
||||
((byte[])data).BinaryToMerge(target);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Clone object via serialization based on options.
|
||||
/// </summary>
|
||||
public static T? CloneToAny<T>(this T source, AcSerializerOptions options) where T : class
|
||||
{
|
||||
if (options.SerializerType == AcSerializerType.Json)
|
||||
return source.CloneTo<T>((AcJsonSerializerOptions)options);
|
||||
return source.BinaryCloneTo();
|
||||
}
|
||||
#endregion
|
||||
|
||||
/// <summary>
|
||||
/// Copy object properties to target via serialization based on options.
|
||||
/// </summary>
|
||||
public static void CopyToAny<T>(this T source, T target, AcSerializerOptions options) where T : class
|
||||
{
|
||||
if (options.SerializerType == AcSerializerType.Json)
|
||||
source.CopyTo(target, (AcJsonSerializerOptions)options);
|
||||
else
|
||||
source.BinaryCopyTo(target);
|
||||
}
|
||||
|
||||
#region Binary Serialization Extension Methods
|
||||
#region Binary Serialization
|
||||
|
||||
/// <summary>
|
||||
/// Serialize object to binary byte array with default options.
|
||||
/// Significantly faster than JSON, especially for large data in WASM.
|
||||
/// </summary>
|
||||
public static byte[] ToBinary<T>(this T source) => AcBinarySerializer.Serialize(source);
|
||||
|
||||
|
|
@ -529,29 +495,83 @@ public static class SerializeObjectExtensions
|
|||
=> AcBinarySerializer.Serialize(source, options);
|
||||
|
||||
/// <summary>
|
||||
/// Deserialize binary data to object with default options.
|
||||
/// Serialize object directly to an IBufferWriter for zero-copy scenarios.
|
||||
/// </summary>
|
||||
public static void ToBinary<T>(this T source, IBufferWriter<byte> writer)
|
||||
=> AcBinarySerializer.Serialize(source, writer, AcBinarySerializerOptions.Default);
|
||||
|
||||
/// <summary>
|
||||
/// Serialize object directly to an IBufferWriter with specified options.
|
||||
/// </summary>
|
||||
public static void ToBinary<T>(this T source, IBufferWriter<byte> writer, AcBinarySerializerOptions options)
|
||||
=> AcBinarySerializer.Serialize(source, writer, options);
|
||||
|
||||
/// <summary>
|
||||
/// Get the serialized binary size without allocating the final array.
|
||||
/// </summary>
|
||||
public static int GetBinarySize<T>(this T source)
|
||||
=> AcBinarySerializer.GetSerializedSize(source, AcBinarySerializerOptions.Default);
|
||||
|
||||
/// <summary>
|
||||
/// Get the serialized binary size with specified options.
|
||||
/// </summary>
|
||||
public static int GetBinarySize<T>(this T source, AcBinarySerializerOptions options)
|
||||
=> AcBinarySerializer.GetSerializedSize(source, options);
|
||||
|
||||
/// <summary>
|
||||
/// Deserialize binary data to object.
|
||||
/// </summary>
|
||||
public static T? BinaryTo<T>(this byte[] data)
|
||||
=> AcBinaryDeserializer.Deserialize<T>(data);
|
||||
|
||||
/// <summary>
|
||||
/// Deserialize binary data to object.
|
||||
/// Deserialize binary data from ReadOnlySpan.
|
||||
/// </summary>
|
||||
public static T? BinaryTo<T>(this ReadOnlySpan<byte> data)
|
||||
=> AcBinaryDeserializer.Deserialize<T>(data);
|
||||
|
||||
/// <summary>
|
||||
/// Deserialize binary data from ReadOnlyMemory.
|
||||
/// </summary>
|
||||
public static T? BinaryTo<T>(this ReadOnlyMemory<byte> data)
|
||||
=> AcBinaryDeserializer.Deserialize<T>(data.Span);
|
||||
|
||||
/// <summary>
|
||||
/// Deserialize binary data to specified type.
|
||||
/// </summary>
|
||||
public static object? BinaryTo(this byte[] data, Type targetType)
|
||||
=> AcBinaryDeserializer.Deserialize(data.AsSpan(), targetType);
|
||||
|
||||
/// <summary>
|
||||
/// Deserialize binary data from ReadOnlySpan to specified type.
|
||||
/// </summary>
|
||||
public static object? BinaryTo(this ReadOnlySpan<byte> data, Type targetType)
|
||||
=> AcBinaryDeserializer.Deserialize(data, targetType);
|
||||
|
||||
/// <summary>
|
||||
/// Deserialize binary data from ReadOnlyMemory to specified type.
|
||||
/// </summary>
|
||||
public static object? BinaryTo(this ReadOnlyMemory<byte> data, Type targetType)
|
||||
=> AcBinaryDeserializer.Deserialize(data.Span, targetType);
|
||||
|
||||
/// <summary>
|
||||
/// Populate existing object from binary data.
|
||||
/// </summary>
|
||||
public static void BinaryTo<T>(this byte[] data, T target) where T : class
|
||||
=> AcBinaryDeserializer.Populate(data, target);
|
||||
|
||||
/// <summary>
|
||||
/// Populate existing object from binary ReadOnlySpan.
|
||||
/// </summary>
|
||||
public static void BinaryTo<T>(this ReadOnlySpan<byte> data, T target) where T : class
|
||||
=> AcBinaryDeserializer.Populate(data, target);
|
||||
|
||||
/// <summary>
|
||||
/// Populate existing object from binary ReadOnlyMemory.
|
||||
/// </summary>
|
||||
public static void BinaryTo<T>(this ReadOnlyMemory<byte> data, T target) where T : class
|
||||
=> AcBinaryDeserializer.Populate(data.Span, target);
|
||||
|
||||
/// <summary>
|
||||
/// Populate existing object from binary data with merge semantics for IId collections.
|
||||
/// </summary>
|
||||
|
|
@ -559,16 +579,46 @@ public static class SerializeObjectExtensions
|
|||
=> AcBinaryDeserializer.PopulateMerge(data.AsSpan(), target);
|
||||
|
||||
/// <summary>
|
||||
/// Clone object via binary serialization (faster than JSON clone).
|
||||
/// Populate existing object from binary ReadOnlySpan with merge semantics.
|
||||
/// </summary>
|
||||
public static T? BinaryCloneTo<T>(this T source) where T : class
|
||||
=> source?.ToBinary().BinaryTo<T>();
|
||||
public static void BinaryToMerge<T>(this ReadOnlySpan<byte> data, T target) where T : class
|
||||
=> AcBinaryDeserializer.PopulateMerge(data, target);
|
||||
|
||||
/// <summary>
|
||||
/// Copy object properties to target via binary serialization.
|
||||
/// Populate existing object from binary ReadOnlyMemory with merge semantics.
|
||||
/// </summary>
|
||||
public static void BinaryCopyTo<T>(this T source, T target) where T : class
|
||||
=> source?.ToBinary().BinaryTo(target);
|
||||
public static void BinaryToMerge<T>(this ReadOnlyMemory<byte> data, T target) where T : class
|
||||
=> AcBinaryDeserializer.PopulateMerge(data.Span, target);
|
||||
|
||||
#endregion
|
||||
|
||||
#region Clone and Copy (Binary-based, zero intermediate allocation)
|
||||
|
||||
/// <summary>
|
||||
/// Clone object via binary serialization (zero intermediate byte[] allocation).
|
||||
/// Uses ArrayBufferWriter to serialize directly into a buffer, then deserializes from the span.
|
||||
/// </summary>
|
||||
public static TDestination? CloneTo<TDestination>(this object? src) where TDestination : class
|
||||
{
|
||||
if (src == null) return null;
|
||||
|
||||
var buffer = new ArrayBufferWriter<byte>(256);
|
||||
AcBinarySerializer.Serialize(src, buffer, AcBinarySerializerOptions.Default);
|
||||
return AcBinaryDeserializer.Deserialize<TDestination>(buffer.WrittenSpan);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Copy object properties to target via binary serialization (zero intermediate byte[] allocation).
|
||||
/// Uses ArrayBufferWriter to serialize directly into a buffer, then populates target from the span.
|
||||
/// </summary>
|
||||
public static void CopyTo(this object? src, object target)
|
||||
{
|
||||
if (src == null) return;
|
||||
|
||||
var buffer = new ArrayBufferWriter<byte>(256);
|
||||
AcBinarySerializer.Serialize(src, buffer, AcBinarySerializerOptions.Default);
|
||||
AcBinaryDeserializer.Populate(buffer.WrittenSpan, target);
|
||||
}
|
||||
|
||||
#endregion
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,1301 +0,0 @@
|
|||
using AyCode.Core.Enums;
|
||||
using AyCode.Core.Extensions;
|
||||
using AyCode.Core.Helpers;
|
||||
using AyCode.Core.Interfaces;
|
||||
using AyCode.Core.Loggers;
|
||||
using AyCode.Services.SignalRs;
|
||||
using AyCode.Services.Server.SignalRs;
|
||||
using System.Collections.Concurrent;
|
||||
using System.Runtime.CompilerServices;
|
||||
|
||||
namespace AyCode.Services.Server.Tests.SignalRs;
|
||||
|
||||
#region Test Models
|
||||
|
||||
public class TestDataItem : IId<int>
|
||||
{
|
||||
public int Id { get; set; }
|
||||
public string Name { get; set; } = string.Empty;
|
||||
public int Value { get; set; }
|
||||
|
||||
public TestDataItem() { }
|
||||
public TestDataItem(int id, string name, int value = 0)
|
||||
{
|
||||
Id = id;
|
||||
Name = name;
|
||||
Value = value;
|
||||
}
|
||||
|
||||
public override string ToString() => $"TestDataItem[{Id}, {Name}, {Value}]";
|
||||
}
|
||||
|
||||
public class TestDataSource : AcSignalRDataSource<TestDataItem, int, List<TestDataItem>>
|
||||
{
|
||||
public TestDataSource(AcSignalRClientBase signalRClient, SignalRCrudTags signalRCrudTags, object[]? contextIds = null)
|
||||
: base(signalRClient, signalRCrudTags, contextIds)
|
||||
{
|
||||
}
|
||||
}
|
||||
|
||||
public class TestObservableDataSource : AcSignalRDataSource<TestDataItem, int, AcObservableCollection<TestDataItem>>
|
||||
{
|
||||
public TestObservableDataSource(AcSignalRClientBase signalRClient, SignalRCrudTags signalRCrudTags, object[]? contextIds = null)
|
||||
: base(signalRClient, signalRCrudTags, contextIds)
|
||||
{
|
||||
}
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Mock SignalR Client
|
||||
|
||||
/// <summary>
|
||||
/// Mock SignalR client for testing AcSignalRDataSource without actual network calls.
|
||||
/// Uses the test constructor to avoid real HubConnection.
|
||||
/// </summary>
|
||||
public class MockSignalRClient : AcSignalRClientBase
|
||||
{
|
||||
private readonly ConcurrentDictionary<int, Func<object?, object?>> _responseHandlers = new();
|
||||
private readonly ConcurrentBag<(int MessageTag, object? Data, DateTime Timestamp)> _sentMessages = new();
|
||||
private static int _idCounter;
|
||||
|
||||
public IReadOnlyList<(int MessageTag, object? Data, DateTime Timestamp)> SentMessages
|
||||
=> _sentMessages.OrderBy(x => x.Timestamp).ToList();
|
||||
|
||||
public int GetSentMessageCount(int messageTag)
|
||||
=> _sentMessages.Count(m => m.MessageTag == messageTag);
|
||||
|
||||
public static int NextId() => Interlocked.Increment(ref _idCounter);
|
||||
public static void ResetIdCounter() => _idCounter = 0;
|
||||
|
||||
/// <summary>
|
||||
/// Uses test constructor - no real HubConnection created.
|
||||
/// </summary>
|
||||
public MockSignalRClient() : base(new MockLogger())
|
||||
{
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Setup a static response for a specific message tag
|
||||
/// </summary>
|
||||
public void SetupResponse<TResponse>(int messageTag, TResponse response)
|
||||
{
|
||||
_responseHandlers[messageTag] = _ => response;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Setup a dynamic response based on the request
|
||||
/// </summary>
|
||||
public void SetupResponse<TRequest, TResponse>(int messageTag, Func<TRequest?, TResponse> responseFactory)
|
||||
{
|
||||
_responseHandlers[messageTag] = req => responseFactory((TRequest?)req);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Setup a response that returns the posted data (echo)
|
||||
/// </summary>
|
||||
public void SetupEchoResponse<T>(int messageTag) where T : class
|
||||
{
|
||||
_responseHandlers[messageTag] = req => req;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Clear all response handlers
|
||||
/// </summary>
|
||||
public void ClearResponses()
|
||||
{
|
||||
_responseHandlers.Clear();
|
||||
}
|
||||
|
||||
public override Task<TResponseData?> GetAllAsync<TResponseData>(int messageTag, object[]? contextParams) where TResponseData : default
|
||||
{
|
||||
_sentMessages.Add((messageTag, contextParams, DateTime.UtcNow));
|
||||
|
||||
if (_responseHandlers.TryGetValue(messageTag, out var handler))
|
||||
{
|
||||
return Task.FromResult((TResponseData?)handler(contextParams));
|
||||
}
|
||||
return Task.FromResult(default(TResponseData));
|
||||
}
|
||||
|
||||
public override Task GetAllAsync<TResponseData>(int messageTag, Func<ISignalResponseMessage<TResponseData?>, Task> responseCallback, object[]? contextParams) where TResponseData : default
|
||||
{
|
||||
_sentMessages.Add((messageTag, contextParams, DateTime.UtcNow));
|
||||
|
||||
if (_responseHandlers.TryGetValue(messageTag, out var handler))
|
||||
{
|
||||
var response = (TResponseData?)handler(contextParams);
|
||||
var responseJson = response?.ToJson();
|
||||
ISignalResponseMessage<TResponseData?> message = new SignalResponseMessage<TResponseData?>(messageTag, SignalResponseStatus.Success, responseJson);
|
||||
return responseCallback(message);
|
||||
}
|
||||
|
||||
ISignalResponseMessage<TResponseData?> errorMessage = new SignalResponseMessage<TResponseData?>(messageTag, SignalResponseStatus.Error, (string?)null);
|
||||
return responseCallback(errorMessage);
|
||||
}
|
||||
|
||||
public override Task<TResponseData?> GetByIdAsync<TResponseData>(int messageTag, object id) where TResponseData : default
|
||||
{
|
||||
_sentMessages.Add((messageTag, id, DateTime.UtcNow));
|
||||
|
||||
if (_responseHandlers.TryGetValue(messageTag, out var handler))
|
||||
{
|
||||
return Task.FromResult((TResponseData?)handler(id));
|
||||
}
|
||||
return Task.FromResult(default(TResponseData));
|
||||
}
|
||||
|
||||
public override Task<TPostData?> PostDataAsync<TPostData>(int messageTag, TPostData postData) where TPostData : class
|
||||
{
|
||||
_sentMessages.Add((messageTag, postData, DateTime.UtcNow));
|
||||
|
||||
if (_responseHandlers.TryGetValue(messageTag, out var handler))
|
||||
{
|
||||
return Task.FromResult((TPostData?)handler(postData));
|
||||
}
|
||||
// Default: echo back the posted data
|
||||
return Task.FromResult<TPostData?>(postData);
|
||||
}
|
||||
|
||||
public override Task PostDataAsync<TPostData>(int messageTag, TPostData postData, Func<ISignalResponseMessage<TPostData?>, Task> responseCallback) where TPostData : default
|
||||
{
|
||||
_sentMessages.Add((messageTag, postData, DateTime.UtcNow));
|
||||
|
||||
if (_responseHandlers.TryGetValue(messageTag, out var handler))
|
||||
{
|
||||
var response = (TPostData?)handler(postData);
|
||||
var responseJson = response?.ToJson();
|
||||
ISignalResponseMessage<TPostData?> message = new SignalResponseMessage<TPostData?>(messageTag, SignalResponseStatus.Success, responseJson);
|
||||
return responseCallback(message);
|
||||
}
|
||||
|
||||
// Default: echo back the posted data
|
||||
ISignalResponseMessage<TPostData?> successMessage = new SignalResponseMessage<TPostData?>(messageTag, SignalResponseStatus.Success, postData?.ToJson());
|
||||
return responseCallback(successMessage);
|
||||
}
|
||||
|
||||
protected override Task MessageReceived(int messageTag, byte[] messageBytes)
|
||||
{
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Silent logger for testing - does not require appsettings.json
|
||||
/// </summary>
|
||||
public class MockLogger : AcLoggerBase
|
||||
{
|
||||
private readonly List<string> _logs = new();
|
||||
public IReadOnlyList<string> Logs => _logs;
|
||||
|
||||
public MockLogger() : base(AppType.TestUnit, LogLevel.Error, "MockLogger")
|
||||
{
|
||||
}
|
||||
|
||||
public override void Detail(string? text, string? categoryName = null, [CallerMemberName] string? memberName = null)
|
||||
=> _logs.Add($"DETAIL: {text}");
|
||||
|
||||
public override void Debug(string? text, string? categoryName = null, [CallerMemberName] string? memberName = null)
|
||||
=> _logs.Add($"DEBUG: {text}");
|
||||
|
||||
public override void Info(string? text, string? categoryName = null, [CallerMemberName] string? memberName = null)
|
||||
=> _logs.Add($"INFO: {text}");
|
||||
|
||||
public override void Warning(string? text, string? categoryName = null, [CallerMemberName] string? memberName = null)
|
||||
=> _logs.Add($"WARN: {text}");
|
||||
|
||||
public override void Error(string? text, Exception? ex = null, string? categoryName = null, [CallerMemberName] string? memberName = null)
|
||||
=> _logs.Add($"ERROR: {text}");
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
[TestClass]
|
||||
public class AcSignalRDataSourceTests
|
||||
{
|
||||
private const int GetAllTag = 100;
|
||||
private const int GetItemTag = 101;
|
||||
private const int AddTag = 102;
|
||||
private const int UpdateTag = 103;
|
||||
private const int RemoveTag = 104;
|
||||
|
||||
private MockSignalRClient _mockClient = null!;
|
||||
private SignalRCrudTags _crudTags = null!;
|
||||
private TestDataSource _dataSource = null!;
|
||||
|
||||
[TestInitialize]
|
||||
public void Setup()
|
||||
{
|
||||
MockSignalRClient.ResetIdCounter();
|
||||
_mockClient = new MockSignalRClient();
|
||||
_crudTags = new SignalRCrudTags(GetAllTag, GetItemTag, AddTag, UpdateTag, RemoveTag);
|
||||
_dataSource = new TestDataSource(_mockClient, _crudTags);
|
||||
}
|
||||
|
||||
#region Basic CRUD Tests
|
||||
|
||||
[TestMethod]
|
||||
public void Add_ValidItem_AddsToCollection()
|
||||
{
|
||||
// Arrange
|
||||
var item = new TestDataItem(MockSignalRClient.NextId(), "Test Item");
|
||||
|
||||
// Act
|
||||
_dataSource.Add(item);
|
||||
|
||||
// Assert
|
||||
Assert.AreEqual(1, _dataSource.Count);
|
||||
Assert.IsTrue(_dataSource.Contains(item));
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void Add_ItemWithDefaultId_ThrowsArgumentNullException()
|
||||
{
|
||||
// Arrange
|
||||
var item = new TestDataItem(0, "Invalid Item"); // 0 is default for int
|
||||
|
||||
// Act & Assert
|
||||
try
|
||||
{
|
||||
_dataSource.Add(item);
|
||||
Assert.Fail("Expected ArgumentNullException was not thrown");
|
||||
}
|
||||
catch (ArgumentNullException)
|
||||
{
|
||||
// Expected
|
||||
}
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void Add_DuplicateItem_ThrowsArgumentException()
|
||||
{
|
||||
// Arrange
|
||||
var id = MockSignalRClient.NextId();
|
||||
var item1 = new TestDataItem(id, "Item 1");
|
||||
var item2 = new TestDataItem(id, "Item 2");
|
||||
_dataSource.Add(item1);
|
||||
|
||||
// Act & Assert
|
||||
try
|
||||
{
|
||||
_dataSource.Add(item2);
|
||||
Assert.Fail("Expected ArgumentException was not thrown");
|
||||
}
|
||||
catch (ArgumentException)
|
||||
{
|
||||
// Expected
|
||||
}
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void Remove_ExistingItem_RemovesFromCollection()
|
||||
{
|
||||
// Arrange
|
||||
var item = new TestDataItem(MockSignalRClient.NextId(), "Test Item");
|
||||
_dataSource.Add(item);
|
||||
|
||||
// Act
|
||||
var result = _dataSource.Remove(item);
|
||||
|
||||
// Assert
|
||||
Assert.IsTrue(result);
|
||||
Assert.AreEqual(0, _dataSource.Count);
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void Remove_NonExistingItem_ReturnsFalse()
|
||||
{
|
||||
// Arrange
|
||||
var item = new TestDataItem(MockSignalRClient.NextId(), "Test Item");
|
||||
|
||||
// Act
|
||||
var result = _dataSource.Remove(item);
|
||||
|
||||
// Assert
|
||||
Assert.IsFalse(result);
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void Indexer_ValidIndex_ReturnsItem()
|
||||
{
|
||||
// Arrange
|
||||
var item = new TestDataItem(MockSignalRClient.NextId(), "Test Item");
|
||||
_dataSource.Add(item);
|
||||
|
||||
// Act
|
||||
var result = _dataSource[0];
|
||||
|
||||
// Assert
|
||||
Assert.AreEqual(item.Id, result.Id);
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void Indexer_InvalidIndex_ThrowsArgumentOutOfRangeException()
|
||||
{
|
||||
// Act & Assert
|
||||
try
|
||||
{
|
||||
var _ = _dataSource[0];
|
||||
Assert.Fail("Expected ArgumentOutOfRangeException was not thrown");
|
||||
}
|
||||
catch (ArgumentOutOfRangeException)
|
||||
{
|
||||
// Expected
|
||||
}
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void Insert_ValidIndexAndItem_InsertsAtCorrectPosition()
|
||||
{
|
||||
// Arrange
|
||||
var item1 = new TestDataItem(MockSignalRClient.NextId(), "Item 1");
|
||||
var item2 = new TestDataItem(MockSignalRClient.NextId(), "Item 2");
|
||||
var item3 = new TestDataItem(MockSignalRClient.NextId(), "Item 3");
|
||||
|
||||
_dataSource.Add(item1);
|
||||
_dataSource.Add(item3);
|
||||
|
||||
// Act
|
||||
_dataSource.Insert(1, item2);
|
||||
|
||||
// Assert
|
||||
Assert.AreEqual(3, _dataSource.Count);
|
||||
Assert.AreEqual(item2.Id, _dataSource[1].Id);
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void Clear_WithItems_RemovesAllItems()
|
||||
{
|
||||
// Arrange
|
||||
_dataSource.Add(new TestDataItem(MockSignalRClient.NextId(), "Item 1"));
|
||||
_dataSource.Add(new TestDataItem(MockSignalRClient.NextId(), "Item 2"));
|
||||
|
||||
// Act
|
||||
_dataSource.Clear();
|
||||
|
||||
// Assert
|
||||
Assert.AreEqual(0, _dataSource.Count);
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void TryGetValue_ExistingId_ReturnsTrue()
|
||||
{
|
||||
// Arrange
|
||||
var id = MockSignalRClient.NextId();
|
||||
var item = new TestDataItem(id, "Test Item");
|
||||
_dataSource.Add(item);
|
||||
|
||||
// Act
|
||||
var result = _dataSource.TryGetValue(id, out var foundItem);
|
||||
|
||||
// Assert
|
||||
Assert.IsTrue(result);
|
||||
Assert.IsNotNull(foundItem);
|
||||
Assert.AreEqual(id, foundItem.Id);
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void TryGetValue_NonExistingId_ReturnsFalse()
|
||||
{
|
||||
// Act
|
||||
var result = _dataSource.TryGetValue(999, out var foundItem);
|
||||
|
||||
// Assert
|
||||
Assert.IsFalse(result);
|
||||
Assert.IsNull(foundItem);
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void RemoveAt_ValidIndex_RemovesItem()
|
||||
{
|
||||
// Arrange
|
||||
var item1 = new TestDataItem(MockSignalRClient.NextId(), "Item 1");
|
||||
var item2 = new TestDataItem(MockSignalRClient.NextId(), "Item 2");
|
||||
_dataSource.Add(item1);
|
||||
_dataSource.Add(item2);
|
||||
|
||||
// Act
|
||||
_dataSource.RemoveAt(0);
|
||||
|
||||
// Assert
|
||||
Assert.AreEqual(1, _dataSource.Count);
|
||||
Assert.AreEqual(item2.Id, _dataSource[0].Id);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Tracking Tests
|
||||
|
||||
[TestMethod]
|
||||
public void Add_CreatesTrackingItem_WithAddState()
|
||||
{
|
||||
// Arrange
|
||||
var item = new TestDataItem(MockSignalRClient.NextId(), "Test Item");
|
||||
|
||||
// Act
|
||||
_dataSource.Add(item);
|
||||
|
||||
// Assert
|
||||
var trackingItems = _dataSource.GetTrackingItems();
|
||||
Assert.AreEqual(1, trackingItems.Count);
|
||||
Assert.AreEqual(TrackingState.Add, trackingItems[0].TrackingState);
|
||||
Assert.AreEqual(item.Id, trackingItems[0].CurrentValue.Id);
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void Remove_AfterAdd_RemovesTrackingItem()
|
||||
{
|
||||
// Arrange
|
||||
var item = new TestDataItem(MockSignalRClient.NextId(), "Test Item");
|
||||
_dataSource.Add(item);
|
||||
|
||||
// Act
|
||||
_dataSource.Remove(item);
|
||||
|
||||
// Assert - Tracking should be empty because Add followed by Remove cancels out
|
||||
var trackingItems = _dataSource.GetTrackingItems();
|
||||
Assert.AreEqual(0, trackingItems.Count);
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void TryGetTrackingItem_ExistingItem_ReturnsTrue()
|
||||
{
|
||||
// Arrange
|
||||
var item = new TestDataItem(MockSignalRClient.NextId(), "Test Item");
|
||||
_dataSource.Add(item);
|
||||
|
||||
// Act
|
||||
var result = _dataSource.TryGetTrackingItem(item.Id, out var trackingItem);
|
||||
|
||||
// Assert
|
||||
Assert.IsTrue(result);
|
||||
Assert.IsNotNull(trackingItem);
|
||||
Assert.AreEqual(TrackingState.Add, trackingItem.TrackingState);
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void SetTrackingStateToUpdate_AddsUpdateTracking()
|
||||
{
|
||||
// Arrange - Load data without tracking
|
||||
var item = new TestDataItem(MockSignalRClient.NextId(), "Test Item", 1);
|
||||
var innerList = _dataSource.GetReferenceInnerList();
|
||||
innerList.Add(item);
|
||||
|
||||
// Act
|
||||
_dataSource.SetTrackingStateToUpdate(item);
|
||||
|
||||
// Assert
|
||||
Assert.IsTrue(_dataSource.TryGetTrackingItem(item.Id, out var trackingItem));
|
||||
Assert.AreEqual(TrackingState.Update, trackingItem!.TrackingState);
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void SetTrackingStateToUpdate_DoesNotChangeAddState()
|
||||
{
|
||||
// Arrange
|
||||
var item = new TestDataItem(MockSignalRClient.NextId(), "Test Item");
|
||||
_dataSource.Add(item);
|
||||
|
||||
// Act
|
||||
_dataSource.SetTrackingStateToUpdate(item);
|
||||
|
||||
// Assert - Should still be Add, not Update
|
||||
Assert.IsTrue(_dataSource.TryGetTrackingItem(item.Id, out var trackingItem));
|
||||
Assert.AreEqual(TrackingState.Add, trackingItem!.TrackingState);
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void TryRollbackItem_RollsBackAddedItem()
|
||||
{
|
||||
// Arrange
|
||||
var item = new TestDataItem(MockSignalRClient.NextId(), "Test Item");
|
||||
_dataSource.Add(item);
|
||||
|
||||
// Act
|
||||
var result = _dataSource.TryRollbackItem(item.Id, out var originalValue);
|
||||
|
||||
// Assert
|
||||
Assert.IsTrue(result);
|
||||
Assert.IsNull(originalValue); // Added items have no original value
|
||||
Assert.AreEqual(0, _dataSource.Count);
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void Rollback_RollsBackAllChanges()
|
||||
{
|
||||
// Arrange
|
||||
var item1 = new TestDataItem(MockSignalRClient.NextId(), "Item 1");
|
||||
var item2 = new TestDataItem(MockSignalRClient.NextId(), "Item 2");
|
||||
_dataSource.Add(item1);
|
||||
_dataSource.Add(item2);
|
||||
|
||||
// Act
|
||||
_dataSource.Rollback();
|
||||
|
||||
// Assert
|
||||
Assert.AreEqual(0, _dataSource.Count);
|
||||
Assert.AreEqual(0, _dataSource.GetTrackingItems().Count);
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void Clear_WithClearChangeTrackingFalse_KeepsTrackingItems()
|
||||
{
|
||||
// Arrange
|
||||
var item = new TestDataItem(MockSignalRClient.NextId(), "Test Item");
|
||||
_dataSource.Add(item);
|
||||
|
||||
// Act
|
||||
_dataSource.Clear(clearChangeTracking: false);
|
||||
|
||||
// Assert
|
||||
Assert.AreEqual(0, _dataSource.Count);
|
||||
Assert.AreEqual(1, _dataSource.GetTrackingItems().Count);
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void Clear_WithClearChangeTrackingTrue_RemovesTrackingItems()
|
||||
{
|
||||
// Arrange
|
||||
var item = new TestDataItem(MockSignalRClient.NextId(), "Test Item");
|
||||
_dataSource.Add(item);
|
||||
|
||||
// Act
|
||||
_dataSource.Clear(clearChangeTracking: true);
|
||||
|
||||
// Assert
|
||||
Assert.AreEqual(0, _dataSource.Count);
|
||||
Assert.AreEqual(0, _dataSource.GetTrackingItems().Count);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Async Save Tests
|
||||
|
||||
[TestMethod]
|
||||
public async Task Add_WithAutoSave_CallsSignalRClient()
|
||||
{
|
||||
// Arrange
|
||||
var item = new TestDataItem(MockSignalRClient.NextId(), "Test Item");
|
||||
_mockClient.SetupEchoResponse<TestDataItem>(AddTag);
|
||||
|
||||
// Act
|
||||
var result = await _dataSource.Add(item, autoSave: true);
|
||||
|
||||
// Assert
|
||||
Assert.IsNotNull(result);
|
||||
Assert.AreEqual(item.Id, result.Id);
|
||||
Assert.AreEqual(1, _mockClient.GetSentMessageCount(AddTag));
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public async Task Add_WithAutoSaveFalse_DoesNotCallSignalR()
|
||||
{
|
||||
// Arrange
|
||||
var item = new TestDataItem(MockSignalRClient.NextId(), "Test Item");
|
||||
|
||||
// Act
|
||||
var result = await _dataSource.Add(item, autoSave: false);
|
||||
|
||||
// Assert
|
||||
Assert.IsNotNull(result);
|
||||
Assert.AreEqual(0, _mockClient.GetSentMessageCount(AddTag));
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public async Task Remove_WithAutoSave_CallsSignalRClient()
|
||||
{
|
||||
// Arrange
|
||||
var item = new TestDataItem(MockSignalRClient.NextId(), "Test Item");
|
||||
_dataSource.Add(item);
|
||||
_mockClient.SetupEchoResponse<TestDataItem>(RemoveTag);
|
||||
|
||||
// Act
|
||||
var result = await _dataSource.Remove(item, autoSave: true);
|
||||
|
||||
// Assert
|
||||
Assert.IsTrue(result);
|
||||
Assert.AreEqual(1, _mockClient.GetSentMessageCount(RemoveTag));
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public async Task Update_WithAutoSave_CallsSignalRClient()
|
||||
{
|
||||
// Arrange
|
||||
var id = MockSignalRClient.NextId();
|
||||
var item = new TestDataItem(id, "Test Item", 1);
|
||||
_dataSource.Add(item);
|
||||
|
||||
var updatedItem = new TestDataItem(id, "Updated Item", 2);
|
||||
_mockClient.SetupEchoResponse<TestDataItem>(UpdateTag);
|
||||
|
||||
// Act
|
||||
var result = await _dataSource.Update(updatedItem, autoSave: true);
|
||||
|
||||
// Assert
|
||||
Assert.IsNotNull(result);
|
||||
Assert.AreEqual(1, _mockClient.GetSentMessageCount(UpdateTag));
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public async Task AddOrUpdate_NewItem_AddsItem()
|
||||
{
|
||||
// Arrange
|
||||
var item = new TestDataItem(MockSignalRClient.NextId(), "New Item");
|
||||
_mockClient.SetupEchoResponse<TestDataItem>(AddTag);
|
||||
|
||||
// Act
|
||||
var result = await _dataSource.AddOrUpdate(item, autoSave: true);
|
||||
|
||||
// Assert
|
||||
Assert.AreEqual(1, _dataSource.Count);
|
||||
Assert.AreEqual(1, _mockClient.GetSentMessageCount(AddTag));
|
||||
Assert.AreEqual(0, _mockClient.GetSentMessageCount(UpdateTag));
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public async Task AddOrUpdate_ExistingItem_UpdatesItem()
|
||||
{
|
||||
// Arrange
|
||||
var id = MockSignalRClient.NextId();
|
||||
var item = new TestDataItem(id, "Original", 1);
|
||||
_dataSource.Add(item);
|
||||
|
||||
var updatedItem = new TestDataItem(id, "Updated", 2);
|
||||
_mockClient.SetupEchoResponse<TestDataItem>(UpdateTag);
|
||||
|
||||
// Act
|
||||
var result = await _dataSource.AddOrUpdate(updatedItem, autoSave: true);
|
||||
|
||||
// Assert
|
||||
Assert.AreEqual(1, _dataSource.Count);
|
||||
Assert.AreEqual(1, _mockClient.GetSentMessageCount(UpdateTag));
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public async Task SaveChanges_SavesAllTrackedItems()
|
||||
{
|
||||
// Arrange
|
||||
var item1 = new TestDataItem(MockSignalRClient.NextId(), "Item 1");
|
||||
var item2 = new TestDataItem(MockSignalRClient.NextId(), "Item 2");
|
||||
_dataSource.Add(item1);
|
||||
_dataSource.Add(item2);
|
||||
|
||||
_mockClient.SetupEchoResponse<TestDataItem>(AddTag);
|
||||
|
||||
// Act
|
||||
var unsavedItems = await _dataSource.SaveChanges();
|
||||
|
||||
// Assert
|
||||
Assert.AreEqual(0, unsavedItems.Count);
|
||||
Assert.AreEqual(2, _mockClient.GetSentMessageCount(AddTag));
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public async Task SaveChangesAsync_SavesAllTrackedItems()
|
||||
{
|
||||
// Arrange
|
||||
var item1 = new TestDataItem(MockSignalRClient.NextId(), "Item 1");
|
||||
var item2 = new TestDataItem(MockSignalRClient.NextId(), "Item 2");
|
||||
_dataSource.Add(item1);
|
||||
_dataSource.Add(item2);
|
||||
|
||||
_mockClient.SetupEchoResponse<TestDataItem>(AddTag);
|
||||
|
||||
// Act
|
||||
await _dataSource.SaveChangesAsync();
|
||||
|
||||
// Assert
|
||||
Assert.AreEqual(0, _dataSource.GetTrackingItems().Count);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Load Tests
|
||||
|
||||
[TestMethod]
|
||||
public async Task LoadDataSource_LoadsItemsFromSignalR()
|
||||
{
|
||||
// Arrange
|
||||
var items = new List<TestDataItem>
|
||||
{
|
||||
new(MockSignalRClient.NextId(), "Item 1"),
|
||||
new(MockSignalRClient.NextId(), "Item 2"),
|
||||
new(MockSignalRClient.NextId(), "Item 3")
|
||||
};
|
||||
_mockClient.SetupResponse(GetAllTag, items);
|
||||
|
||||
// Act
|
||||
await _dataSource.LoadDataSource();
|
||||
|
||||
// Assert
|
||||
Assert.AreEqual(3, _dataSource.Count);
|
||||
Assert.AreEqual(0, _dataSource.GetTrackingItems().Count); // No tracking for loaded items
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public async Task LoadItem_LoadsSingleItemFromSignalR()
|
||||
{
|
||||
// Arrange
|
||||
var id = MockSignalRClient.NextId();
|
||||
var item = new TestDataItem(id, "Loaded Item");
|
||||
_mockClient.SetupResponse(GetItemTag, item);
|
||||
|
||||
// Act
|
||||
var result = await _dataSource.LoadItem(id);
|
||||
|
||||
// Assert
|
||||
Assert.IsNotNull(result);
|
||||
Assert.AreEqual(id, result.Id);
|
||||
Assert.AreEqual(1, _dataSource.Count);
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public async Task LoadItem_ReturnsNullForNonExisting()
|
||||
{
|
||||
// Arrange
|
||||
_mockClient.SetupResponse<TestDataItem?>(GetItemTag, null);
|
||||
|
||||
// Act
|
||||
var result = await _dataSource.LoadItem(999);
|
||||
|
||||
// Assert
|
||||
Assert.IsNull(result);
|
||||
Assert.AreEqual(0, _dataSource.Count);
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public async Task LoadDataSource_FromList_CopiesItems()
|
||||
{
|
||||
// Arrange
|
||||
var sourceList = new List<TestDataItem>
|
||||
{
|
||||
new(MockSignalRClient.NextId(), "Item 1"),
|
||||
new(MockSignalRClient.NextId(), "Item 2")
|
||||
};
|
||||
|
||||
// Act
|
||||
await _dataSource.LoadDataSource(sourceList);
|
||||
|
||||
// Assert
|
||||
Assert.AreEqual(2, _dataSource.Count);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Thread Safety Tests
|
||||
|
||||
[TestMethod]
|
||||
public async Task ConcurrentAdds_AreThreadSafe()
|
||||
{
|
||||
// Arrange
|
||||
var tasks = new List<Task>();
|
||||
var itemCount = 100;
|
||||
|
||||
// Act
|
||||
for (int i = 0; i < itemCount; i++)
|
||||
{
|
||||
var item = new TestDataItem(MockSignalRClient.NextId(), $"Item {i}");
|
||||
tasks.Add(Task.Run(() => _dataSource.Add(item)));
|
||||
}
|
||||
|
||||
await Task.WhenAll(tasks);
|
||||
|
||||
// Assert
|
||||
Assert.AreEqual(itemCount, _dataSource.Count);
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public async Task ConcurrentReadsAndWrites_AreThreadSafe()
|
||||
{
|
||||
// Arrange - Pre-populate with items
|
||||
for (int i = 0; i < 50; i++)
|
||||
{
|
||||
_dataSource.Add(new TestDataItem(MockSignalRClient.NextId(), $"Initial Item {i}"));
|
||||
}
|
||||
|
||||
var tasks = new List<Task>();
|
||||
var readCount = 0;
|
||||
var writeCount = 0;
|
||||
|
||||
// Act - Concurrent reads
|
||||
for (int i = 0; i < 100; i++)
|
||||
{
|
||||
tasks.Add(Task.Run(() =>
|
||||
{
|
||||
var count = _dataSource.Count;
|
||||
Interlocked.Increment(ref readCount);
|
||||
}));
|
||||
}
|
||||
|
||||
// Act - Concurrent writes
|
||||
for (int i = 0; i < 50; i++)
|
||||
{
|
||||
var item = new TestDataItem(MockSignalRClient.NextId(), $"New Item {i}");
|
||||
tasks.Add(Task.Run(() =>
|
||||
{
|
||||
_dataSource.Add(item);
|
||||
Interlocked.Increment(ref writeCount);
|
||||
}));
|
||||
}
|
||||
|
||||
await Task.WhenAll(tasks);
|
||||
|
||||
// Assert
|
||||
Assert.AreEqual(100, readCount);
|
||||
Assert.AreEqual(50, writeCount);
|
||||
Assert.AreEqual(100, _dataSource.Count); // 50 original + 50 new
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public async Task ConcurrentAsyncOperations_AreThreadSafe()
|
||||
{
|
||||
// Arrange
|
||||
_mockClient.SetupEchoResponse<TestDataItem>(AddTag);
|
||||
|
||||
var tasks = new List<Task<TestDataItem>>();
|
||||
|
||||
// Act
|
||||
for (int i = 0; i < 20; i++)
|
||||
{
|
||||
var item = new TestDataItem(MockSignalRClient.NextId(), $"Item {i}");
|
||||
tasks.Add(_dataSource.Add(item, autoSave: true));
|
||||
}
|
||||
|
||||
var results = await Task.WhenAll(tasks);
|
||||
|
||||
// Assert
|
||||
Assert.AreEqual(20, results.Length);
|
||||
Assert.AreEqual(20, _dataSource.Count);
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void GetEnumerator_ReturnsCopy_SafeForModification()
|
||||
{
|
||||
// Arrange
|
||||
for (int i = 0; i < 10; i++)
|
||||
{
|
||||
_dataSource.Add(new TestDataItem(MockSignalRClient.NextId(), $"Item {i}"));
|
||||
}
|
||||
|
||||
// Act & Assert - Should not throw even when modifying during enumeration
|
||||
var enumeratedItems = new List<TestDataItem>();
|
||||
foreach (var item in _dataSource)
|
||||
{
|
||||
enumeratedItems.Add(item);
|
||||
if (enumeratedItems.Count == 5)
|
||||
{
|
||||
// This would throw if we're not using a copy
|
||||
_dataSource.Add(new TestDataItem(MockSignalRClient.NextId(), "New Item"));
|
||||
}
|
||||
}
|
||||
|
||||
Assert.AreEqual(10, enumeratedItems.Count); // Original count
|
||||
Assert.AreEqual(11, _dataSource.Count); // After modification
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public async Task ConcurrentRemoves_AreThreadSafe()
|
||||
{
|
||||
// Arrange
|
||||
var items = Enumerable.Range(0, 50)
|
||||
.Select(i => new TestDataItem(MockSignalRClient.NextId(), $"Item {i}"))
|
||||
.ToList();
|
||||
|
||||
foreach (var item in items)
|
||||
{
|
||||
_dataSource.Add(item);
|
||||
}
|
||||
|
||||
// Act
|
||||
var tasks = items.Select(item => Task.Run(() => _dataSource.Remove(item))).ToList();
|
||||
await Task.WhenAll(tasks);
|
||||
|
||||
// Assert
|
||||
Assert.AreEqual(0, _dataSource.Count);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Observable Collection Tests
|
||||
|
||||
[TestMethod]
|
||||
public void WithObservableCollection_BeginEndUpdate_SuppressesNotifications()
|
||||
{
|
||||
// Arrange
|
||||
var observableDataSource = new TestObservableDataSource(_mockClient, _crudTags);
|
||||
var innerList = observableDataSource.GetReferenceInnerList();
|
||||
var notificationCount = 0;
|
||||
|
||||
innerList.CollectionChanged += (s, e) => notificationCount++;
|
||||
|
||||
// Act
|
||||
innerList.BeginUpdate();
|
||||
for (int i = 0; i < 10; i++)
|
||||
{
|
||||
innerList.Add(new TestDataItem(MockSignalRClient.NextId(), $"Item {i}"));
|
||||
}
|
||||
innerList.EndUpdate();
|
||||
|
||||
// Assert - Should only have 1 notification (Reset) instead of 10
|
||||
Assert.AreEqual(1, notificationCount);
|
||||
Assert.AreEqual(10, innerList.Count);
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void WithObservableCollection_NestedUpdates_OnlyFiresOnce()
|
||||
{
|
||||
// Arrange
|
||||
var observableDataSource = new TestObservableDataSource(_mockClient, _crudTags);
|
||||
var innerList = observableDataSource.GetReferenceInnerList();
|
||||
var notificationCount = 0;
|
||||
|
||||
innerList.CollectionChanged += (s, e) => notificationCount++;
|
||||
|
||||
// Act - Nested BeginUpdate/EndUpdate
|
||||
innerList.BeginUpdate();
|
||||
innerList.BeginUpdate();
|
||||
innerList.Add(new TestDataItem(MockSignalRClient.NextId(), "Item 1"));
|
||||
innerList.EndUpdate(); // Inner - should not fire
|
||||
innerList.Add(new TestDataItem(MockSignalRClient.NextId(), "Item 2"));
|
||||
innerList.EndUpdate(); // Outer - should fire once
|
||||
|
||||
// Assert
|
||||
Assert.AreEqual(1, notificationCount);
|
||||
Assert.AreEqual(2, innerList.Count);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Edge Cases
|
||||
|
||||
[TestMethod]
|
||||
public void AddRange_AddsMultipleItems()
|
||||
{
|
||||
// Arrange
|
||||
var items = new List<TestDataItem>
|
||||
{
|
||||
new(MockSignalRClient.NextId(), "Item 1"),
|
||||
new(MockSignalRClient.NextId(), "Item 2"),
|
||||
new(MockSignalRClient.NextId(), "Item 3")
|
||||
};
|
||||
|
||||
// Act
|
||||
_dataSource.AddRange(items);
|
||||
|
||||
// Assert
|
||||
Assert.AreEqual(3, _dataSource.Count);
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void IndexOf_ReturnsCorrectIndex()
|
||||
{
|
||||
// Arrange
|
||||
var item1 = new TestDataItem(MockSignalRClient.NextId(), "Item 1");
|
||||
var item2 = new TestDataItem(MockSignalRClient.NextId(), "Item 2");
|
||||
_dataSource.Add(item1);
|
||||
_dataSource.Add(item2);
|
||||
|
||||
// Act
|
||||
var index = _dataSource.IndexOf(item2);
|
||||
|
||||
// Assert
|
||||
Assert.AreEqual(1, index);
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void IndexOf_NonExisting_ReturnsMinusOne()
|
||||
{
|
||||
// Arrange
|
||||
var item = new TestDataItem(MockSignalRClient.NextId(), "Item");
|
||||
|
||||
// Act
|
||||
var index = _dataSource.IndexOf(item);
|
||||
|
||||
// Assert
|
||||
Assert.AreEqual(-1, index);
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void TryRemove_RemovesAndReturnsItem()
|
||||
{
|
||||
// Arrange
|
||||
var item = new TestDataItem(MockSignalRClient.NextId(), "Test Item");
|
||||
_dataSource.Add(item);
|
||||
|
||||
// Act
|
||||
var result = _dataSource.TryRemove(item.Id, out var removedItem);
|
||||
|
||||
// Assert
|
||||
Assert.IsTrue(result);
|
||||
Assert.IsNotNull(removedItem);
|
||||
Assert.AreEqual(item.Id, removedItem.Id);
|
||||
Assert.AreEqual(0, _dataSource.Count);
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void TryGetIndex_ExistingId_ReturnsTrueWithIndex()
|
||||
{
|
||||
// Arrange
|
||||
var item = new TestDataItem(MockSignalRClient.NextId(), "Test Item");
|
||||
_dataSource.Add(item);
|
||||
|
||||
// Act
|
||||
var result = _dataSource.TryGetIndex(item.Id, out var index);
|
||||
|
||||
// Assert
|
||||
Assert.IsTrue(result);
|
||||
Assert.AreEqual(0, index);
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void CopyTo_CopiesItemsToArray()
|
||||
{
|
||||
// Arrange
|
||||
var item1 = new TestDataItem(MockSignalRClient.NextId(), "Item 1");
|
||||
var item2 = new TestDataItem(MockSignalRClient.NextId(), "Item 2");
|
||||
_dataSource.Add(item1);
|
||||
_dataSource.Add(item2);
|
||||
|
||||
var array = new TestDataItem[2];
|
||||
|
||||
// Act
|
||||
_dataSource.CopyTo(array);
|
||||
|
||||
// Assert
|
||||
Assert.AreEqual(item1.Id, array[0].Id);
|
||||
Assert.AreEqual(item2.Id, array[1].Id);
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void SetWorkingReferenceList_SetsNewInnerList()
|
||||
{
|
||||
// Arrange
|
||||
var newList = new List<TestDataItem>
|
||||
{
|
||||
new(MockSignalRClient.NextId(), "Item 1"),
|
||||
new(MockSignalRClient.NextId(), "Item 2")
|
||||
};
|
||||
|
||||
// Act
|
||||
_dataSource.SetWorkingReferenceList(newList);
|
||||
|
||||
// Assert
|
||||
Assert.IsTrue(_dataSource.HasWorkingReferenceList);
|
||||
Assert.AreEqual(2, _dataSource.Count);
|
||||
Assert.AreSame(newList, _dataSource.GetReferenceInnerList());
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void SetWorkingReferenceList_WithNull_DoesNothing()
|
||||
{
|
||||
// Act
|
||||
_dataSource.SetWorkingReferenceList(null);
|
||||
|
||||
// Assert
|
||||
Assert.IsFalse(_dataSource.HasWorkingReferenceList);
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void AsReadOnly_ReturnsReadOnlyCollection()
|
||||
{
|
||||
// Arrange
|
||||
_dataSource.Add(new TestDataItem(MockSignalRClient.NextId(), "Item"));
|
||||
|
||||
// Act
|
||||
var readOnly = _dataSource.AsReadOnly();
|
||||
|
||||
// Assert
|
||||
Assert.IsNotNull(readOnly);
|
||||
Assert.AreEqual(1, readOnly.Count);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Event Tests
|
||||
|
||||
[TestMethod]
|
||||
public async Task OnDataSourceLoaded_IsCalledAfterLoad()
|
||||
{
|
||||
// Arrange
|
||||
var loadedEventCalled = false;
|
||||
_dataSource.OnDataSourceLoaded = () =>
|
||||
{
|
||||
loadedEventCalled = true;
|
||||
return Task.CompletedTask;
|
||||
};
|
||||
|
||||
_mockClient.SetupResponse(GetAllTag, new List<TestDataItem>());
|
||||
|
||||
// Act
|
||||
await _dataSource.LoadDataSource();
|
||||
|
||||
// Assert
|
||||
Assert.IsTrue(loadedEventCalled);
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public async Task OnDataSourceItemChanged_IsCalledAfterLoadItem()
|
||||
{
|
||||
// Arrange
|
||||
TestDataItem? changedItem = null;
|
||||
TrackingState? changedState = null;
|
||||
|
||||
_dataSource.OnDataSourceItemChanged = args =>
|
||||
{
|
||||
changedItem = args.Item;
|
||||
changedState = args.TrackingState;
|
||||
return Task.CompletedTask;
|
||||
};
|
||||
|
||||
var id = MockSignalRClient.NextId();
|
||||
var item = new TestDataItem(id, "Test Item");
|
||||
_mockClient.SetupResponse(GetItemTag, item);
|
||||
|
||||
// Act
|
||||
await _dataSource.LoadItem(id);
|
||||
|
||||
// Assert
|
||||
Assert.IsNotNull(changedItem);
|
||||
Assert.AreEqual(id, changedItem.Id);
|
||||
Assert.AreEqual(TrackingState.Get, changedState);
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public async Task IsSyncing_IsTrue_DuringSaveChanges()
|
||||
{
|
||||
// Arrange
|
||||
var wasSyncing = false;
|
||||
var item = new TestDataItem(MockSignalRClient.NextId(), "Test Item");
|
||||
_dataSource.Add(item);
|
||||
|
||||
_mockClient.SetupResponse<TestDataItem, TestDataItem>(AddTag, req =>
|
||||
{
|
||||
wasSyncing = _dataSource.IsSyncing;
|
||||
return req!;
|
||||
});
|
||||
|
||||
// Act
|
||||
await _dataSource.SaveChanges();
|
||||
|
||||
// Assert
|
||||
Assert.IsTrue(wasSyncing);
|
||||
Assert.IsFalse(_dataSource.IsSyncing); // Should be false after completion
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public async Task OnSyncingStateChanged_FiresCorrectly()
|
||||
{
|
||||
// Arrange
|
||||
var syncStates = new List<bool>();
|
||||
_dataSource.OnSyncingStateChanged += state => syncStates.Add(state);
|
||||
|
||||
var item = new TestDataItem(MockSignalRClient.NextId(), "Test Item");
|
||||
_dataSource.Add(item);
|
||||
_mockClient.SetupEchoResponse<TestDataItem>(AddTag);
|
||||
|
||||
// Act
|
||||
await _dataSource.SaveChanges();
|
||||
|
||||
// Assert
|
||||
Assert.AreEqual(2, syncStates.Count);
|
||||
Assert.IsTrue(syncStates[0]); // Started
|
||||
Assert.IsFalse(syncStates[1]); // Ended
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region IList Interface Tests
|
||||
|
||||
[TestMethod]
|
||||
public void IList_Add_AddsItem()
|
||||
{
|
||||
// Arrange
|
||||
var item = new TestDataItem(MockSignalRClient.NextId(), "Test");
|
||||
var list = (System.Collections.IList)_dataSource;
|
||||
|
||||
// Act
|
||||
var index = list.Add(item);
|
||||
|
||||
// Assert
|
||||
Assert.AreEqual(0, index);
|
||||
Assert.AreEqual(1, _dataSource.Count);
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void IList_Contains_ReturnsCorrectly()
|
||||
{
|
||||
// Arrange
|
||||
var item = new TestDataItem(MockSignalRClient.NextId(), "Test");
|
||||
_dataSource.Add(item);
|
||||
var list = (System.Collections.IList)_dataSource;
|
||||
|
||||
// Act & Assert
|
||||
Assert.IsTrue(list.Contains(item));
|
||||
Assert.IsFalse(list.Contains(new TestDataItem(MockSignalRClient.NextId(), "Other")));
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void IList_IndexOf_ReturnsCorrectIndex()
|
||||
{
|
||||
// Arrange
|
||||
var item = new TestDataItem(MockSignalRClient.NextId(), "Test");
|
||||
_dataSource.Add(item);
|
||||
var list = (System.Collections.IList)_dataSource;
|
||||
|
||||
// Act
|
||||
var index = list.IndexOf(item);
|
||||
|
||||
// Assert
|
||||
Assert.AreEqual(0, index);
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void IList_Remove_RemovesItem()
|
||||
{
|
||||
// Arrange
|
||||
var item = new TestDataItem(MockSignalRClient.NextId(), "Test");
|
||||
_dataSource.Add(item);
|
||||
var list = (System.Collections.IList)_dataSource;
|
||||
|
||||
// Act
|
||||
list.Remove(item);
|
||||
|
||||
// Assert
|
||||
Assert.AreEqual(0, _dataSource.Count);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Context and Filter Tests
|
||||
|
||||
[TestMethod]
|
||||
public async Task LoadDataSource_WithContextIds_PassesContextToSignalR()
|
||||
{
|
||||
// Arrange
|
||||
var contextIds = new object[] { 123, "SomeFilter" };
|
||||
var dataSourceWithContext = new TestDataSource(_mockClient, _crudTags, contextIds);
|
||||
_mockClient.SetupResponse(GetAllTag, new List<TestDataItem>());
|
||||
|
||||
// Act
|
||||
await dataSourceWithContext.LoadDataSource();
|
||||
|
||||
// Assert
|
||||
Assert.AreEqual(1, _mockClient.GetSentMessageCount(GetAllTag));
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public async Task LoadDataSource_WithFilterText_PassesFilterToSignalR()
|
||||
{
|
||||
// Arrange
|
||||
_dataSource.FilterText = "MyFilter";
|
||||
_mockClient.SetupResponse(GetAllTag, new List<TestDataItem>());
|
||||
|
||||
// Act
|
||||
await _dataSource.LoadDataSource();
|
||||
|
||||
// Assert
|
||||
Assert.AreEqual(1, _mockClient.GetSentMessageCount(GetAllTag));
|
||||
}
|
||||
|
||||
#endregion
|
||||
}
|
||||
|
|
@ -1,1067 +0,0 @@
|
|||
using AyCode.Core.Tests.TestModels;
|
||||
using AyCode.Core.Extensions;
|
||||
using AyCode.Services.SignalRs;
|
||||
using MessagePack;
|
||||
|
||||
namespace AyCode.Services.Server.Tests.SignalRs;
|
||||
|
||||
/// <summary>
|
||||
/// Base class for ProcessOnReceiveMessage tests.
|
||||
/// Tests for AcWebSignalRHubBase.ProcessOnReceiveMessage.
|
||||
/// Uses shared DTOs from AyCode.Core.Tests.TestModels.
|
||||
/// Derived classes specify the serializer type (JSON or Binary).
|
||||
/// </summary>
|
||||
public abstract class ProcessOnReceiveMessageTestsBase
|
||||
{
|
||||
protected abstract AcSerializerType SerializerType { get; }
|
||||
|
||||
protected TestableSignalRHub _hub = null!;
|
||||
protected TestSignalRService _service = null!;
|
||||
|
||||
[TestInitialize]
|
||||
public void Setup()
|
||||
{
|
||||
_hub = new TestableSignalRHub();
|
||||
_service = new TestSignalRService();
|
||||
_hub.SetSerializerType(SerializerType);
|
||||
_hub.RegisterService(_service);
|
||||
}
|
||||
|
||||
[TestCleanup]
|
||||
public void Cleanup()
|
||||
{
|
||||
_hub.Reset();
|
||||
_service.Reset();
|
||||
}
|
||||
|
||||
#region Single Primitive Parameter Tests
|
||||
|
||||
[TestMethod]
|
||||
public async Task ProcessOnReceiveMessage_SingleInt_DeserializesCorrectly()
|
||||
{
|
||||
// Arrange
|
||||
var message = SignalRTestHelper.CreateSinglePrimitiveMessage(42);
|
||||
|
||||
// Act
|
||||
await _hub.InvokeProcessOnReceiveMessage(TestSignalRTags.SingleIntParam, message);
|
||||
|
||||
// Assert
|
||||
Assert.IsTrue(_service.SingleIntMethodCalled);
|
||||
Assert.AreEqual(42, _service.ReceivedInt);
|
||||
Assert.AreEqual(1, _hub.SentMessages.Count);
|
||||
SignalRTestHelper.AssertSuccessResponse(_hub.SentMessages[0], TestSignalRTags.SingleIntParam);
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public async Task ProcessOnReceiveMessage_BoolTrue_DeserializesCorrectly()
|
||||
{
|
||||
// Arrange
|
||||
var message = SignalRTestHelper.CreateSinglePrimitiveMessage(true);
|
||||
|
||||
// Act
|
||||
await _hub.InvokeProcessOnReceiveMessage(TestSignalRTags.BoolParam, message);
|
||||
|
||||
// Assert
|
||||
Assert.IsTrue(_service.BoolMethodCalled);
|
||||
Assert.IsTrue(_service.ReceivedBool);
|
||||
|
||||
var responseData = SignalRTestHelper.GetResponseData<bool>(_hub.SentMessages[0]);
|
||||
Assert.IsTrue(responseData, "Response should be true");
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public async Task ProcessOnReceiveMessage_BoolFalse_DeserializesCorrectly()
|
||||
{
|
||||
// Arrange
|
||||
var message = SignalRTestHelper.CreateSinglePrimitiveMessage(false);
|
||||
|
||||
// Act
|
||||
await _hub.InvokeProcessOnReceiveMessage(TestSignalRTags.BoolParam, message);
|
||||
|
||||
// Assert
|
||||
Assert.IsTrue(_service.BoolMethodCalled);
|
||||
Assert.IsFalse(_service.ReceivedBool);
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public async Task ProcessOnReceiveMessage_String_DeserializesCorrectly()
|
||||
{
|
||||
// Arrange
|
||||
var message = SignalRTestHelper.CreateSinglePrimitiveMessage("Hello SignalR!");
|
||||
|
||||
// Act
|
||||
await _hub.InvokeProcessOnReceiveMessage(TestSignalRTags.StringParam, message);
|
||||
|
||||
// Assert
|
||||
Assert.IsTrue(_service.StringMethodCalled);
|
||||
Assert.AreEqual("Hello SignalR!", _service.ReceivedString);
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public async Task ProcessOnReceiveMessage_Guid_DeserializesCorrectly()
|
||||
{
|
||||
// Arrange
|
||||
var testGuid = Guid.NewGuid();
|
||||
var message = SignalRTestHelper.CreateSinglePrimitiveMessage(testGuid);
|
||||
|
||||
// Act
|
||||
await _hub.InvokeProcessOnReceiveMessage(TestSignalRTags.GuidParam, message);
|
||||
|
||||
// Assert
|
||||
Assert.IsTrue(_service.GuidMethodCalled);
|
||||
Assert.AreEqual(testGuid, _service.ReceivedGuid);
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public async Task ProcessOnReceiveMessage_Enum_DeserializesCorrectly()
|
||||
{
|
||||
// Arrange - using shared TestStatus enum
|
||||
var message = SignalRTestHelper.CreateSinglePrimitiveMessage(TestStatus.Active);
|
||||
|
||||
// Act
|
||||
await _hub.InvokeProcessOnReceiveMessage(TestSignalRTags.EnumParam, message);
|
||||
|
||||
// Assert
|
||||
Assert.IsTrue(_service.EnumMethodCalled);
|
||||
Assert.AreEqual(TestStatus.Active, _service.ReceivedEnum);
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public async Task ProcessOnReceiveMessage_Decimal_DeserializesCorrectly()
|
||||
{
|
||||
// Arrange
|
||||
var message = SignalRTestHelper.CreateSinglePrimitiveMessage(123.456m);
|
||||
|
||||
// Act
|
||||
await _hub.InvokeProcessOnReceiveMessage(TestSignalRTags.DecimalParam, message);
|
||||
|
||||
// Assert
|
||||
Assert.IsTrue(_service.DecimalMethodCalled);
|
||||
Assert.AreEqual(123.456m, _service.ReceivedDecimal);
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public async Task ProcessOnReceiveMessage_DateTime_DeserializesCorrectly()
|
||||
{
|
||||
// Arrange
|
||||
var testDate = new DateTime(2024, 12, 25, 12, 30, 45, DateTimeKind.Utc);
|
||||
var message = SignalRTestHelper.CreateSinglePrimitiveMessage(testDate);
|
||||
|
||||
// Act
|
||||
await _hub.InvokeProcessOnReceiveMessage(TestSignalRTags.DateTimeParam, message);
|
||||
|
||||
// Assert
|
||||
Assert.IsTrue(_service.DateTimeMethodCalled);
|
||||
Assert.AreEqual(testDate, _service.ReceivedDateTime);
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public async Task ProcessOnReceiveMessage_Double_DeserializesCorrectly()
|
||||
{
|
||||
// Arrange
|
||||
var message = SignalRTestHelper.CreateSinglePrimitiveMessage(3.14159265359);
|
||||
|
||||
// Act
|
||||
await _hub.InvokeProcessOnReceiveMessage(TestSignalRTags.DoubleParam, message);
|
||||
|
||||
// Assert
|
||||
Assert.IsTrue(_service.DoubleMethodCalled);
|
||||
Assert.IsNotNull(_service.ReceivedDouble);
|
||||
Assert.IsTrue(Math.Abs(_service.ReceivedDouble.Value - 3.14159265359) < 0.0000000001);
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public async Task ProcessOnReceiveMessage_Long_DeserializesCorrectly()
|
||||
{
|
||||
// Arrange
|
||||
var message = SignalRTestHelper.CreateSinglePrimitiveMessage(9223372036854775807L);
|
||||
|
||||
// Act
|
||||
await _hub.InvokeProcessOnReceiveMessage(TestSignalRTags.LongParam, message);
|
||||
|
||||
// Assert
|
||||
Assert.IsTrue(_service.LongMethodCalled);
|
||||
Assert.AreEqual(9223372036854775807L, _service.ReceivedLong);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Multiple Primitive Parameters Tests
|
||||
|
||||
[TestMethod]
|
||||
public async Task ProcessOnReceiveMessage_TwoInts_DeserializesCorrectly()
|
||||
{
|
||||
// Arrange
|
||||
var message = SignalRTestHelper.CreatePrimitiveParamsMessage(10, 20);
|
||||
|
||||
// Act
|
||||
await _hub.InvokeProcessOnReceiveMessage(TestSignalRTags.TwoIntParams, message);
|
||||
|
||||
// Assert
|
||||
Assert.IsTrue(_service.TwoIntMethodCalled);
|
||||
Assert.AreEqual((10, 20), _service.ReceivedTwoInts);
|
||||
|
||||
var responseData = SignalRTestHelper.GetResponseData<int>(_hub.SentMessages[0]);
|
||||
Assert.AreEqual(30, responseData, "Sum should be 30");
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public async Task ProcessOnReceiveMessage_MultipleTypes_DeserializesCorrectly()
|
||||
{
|
||||
// Arrange
|
||||
var message = SignalRTestHelper.CreatePrimitiveParamsMessage(true, "test", 123);
|
||||
|
||||
// Act
|
||||
await _hub.InvokeProcessOnReceiveMessage(TestSignalRTags.MultipleTypesParams, message);
|
||||
|
||||
// Assert
|
||||
Assert.IsTrue(_service.MultipleTypesMethodCalled);
|
||||
Assert.AreEqual((true, "test", 123), _service.ReceivedMultipleTypes);
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public async Task ProcessOnReceiveMessage_FiveParams_DeserializesCorrectly()
|
||||
{
|
||||
// Arrange
|
||||
var testGuid = Guid.NewGuid();
|
||||
var message = SignalRTestHelper.CreatePrimitiveParamsMessage(42, "hello", true, testGuid, 99.99m);
|
||||
|
||||
// Act
|
||||
await _hub.InvokeProcessOnReceiveMessage(TestSignalRTags.FiveParams, message);
|
||||
|
||||
// Assert
|
||||
Assert.IsTrue(_service.FiveParamsMethodCalled);
|
||||
Assert.IsNotNull(_service.ReceivedFiveParams);
|
||||
Assert.AreEqual(42, _service.ReceivedFiveParams.Value.Item1);
|
||||
Assert.AreEqual("hello", _service.ReceivedFiveParams.Value.Item2);
|
||||
Assert.AreEqual(true, _service.ReceivedFiveParams.Value.Item3);
|
||||
Assert.AreEqual(testGuid, _service.ReceivedFiveParams.Value.Item4);
|
||||
Assert.AreEqual(99.99m, _service.ReceivedFiveParams.Value.Item5);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Complex Object Tests (using shared DTOs)
|
||||
|
||||
[TestMethod]
|
||||
public async Task ProcessOnReceiveMessage_TestOrderItem_DeserializesCorrectly()
|
||||
{
|
||||
// Arrange - using shared TestOrderItem from Core.Tests
|
||||
var item = new TestOrderItem
|
||||
{
|
||||
Id = 1,
|
||||
ProductName = "Test Product",
|
||||
Quantity = 10,
|
||||
UnitPrice = 99.99m,
|
||||
Status = TestStatus.Active
|
||||
};
|
||||
var message = SignalRTestHelper.CreateComplexObjectMessage(item);
|
||||
|
||||
// Act
|
||||
await _hub.InvokeProcessOnReceiveMessage(TestSignalRTags.TestOrderItemParam, message);
|
||||
|
||||
// Assert
|
||||
Assert.IsTrue(_service.TestOrderItemMethodCalled);
|
||||
Assert.IsNotNull(_service.ReceivedTestOrderItem);
|
||||
Assert.AreEqual(1, _service.ReceivedTestOrderItem.Id);
|
||||
Assert.AreEqual("Test Product", _service.ReceivedTestOrderItem.ProductName);
|
||||
Assert.AreEqual(10, _service.ReceivedTestOrderItem.Quantity);
|
||||
Assert.AreEqual(99.99m, _service.ReceivedTestOrderItem.UnitPrice);
|
||||
Assert.AreEqual(TestStatus.Active, _service.ReceivedTestOrderItem.Status);
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public async Task ProcessOnReceiveMessage_TestOrder_WithNestedItems_DeserializesCorrectly()
|
||||
{
|
||||
// Arrange - using shared TestOrder with nested items
|
||||
var order = new TestOrder
|
||||
{
|
||||
Id = 100,
|
||||
OrderNumber = "ORD-001",
|
||||
Status = TestStatus.Active,
|
||||
TotalAmount = 500.00m,
|
||||
Items =
|
||||
[
|
||||
new TestOrderItem { Id = 1, ProductName = "Item A", Quantity = 2 },
|
||||
new TestOrderItem { Id = 2, ProductName = "Item B", Quantity = 3 }
|
||||
]
|
||||
};
|
||||
var message = SignalRTestHelper.CreateComplexObjectMessage(order);
|
||||
|
||||
// Act
|
||||
await _hub.InvokeProcessOnReceiveMessage(TestSignalRTags.TestOrderParam, message);
|
||||
|
||||
// Assert
|
||||
Assert.IsTrue(_service.TestOrderMethodCalled);
|
||||
Assert.IsNotNull(_service.ReceivedTestOrder);
|
||||
Assert.AreEqual(100, _service.ReceivedTestOrder.Id);
|
||||
Assert.AreEqual("ORD-001", _service.ReceivedTestOrder.OrderNumber);
|
||||
Assert.AreEqual(2, _service.ReceivedTestOrder.Items.Count);
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public async Task ProcessOnReceiveMessage_SharedTag_IIdType_DeserializesCorrectly()
|
||||
{
|
||||
// Arrange - using shared SharedTag (IId type)
|
||||
var tag = new SharedTag
|
||||
{
|
||||
Id = 1,
|
||||
Name = "Important",
|
||||
Color = "#FF0000",
|
||||
Priority = 1,
|
||||
IsActive = true
|
||||
};
|
||||
var message = SignalRTestHelper.CreateComplexObjectMessage(tag);
|
||||
|
||||
// Act
|
||||
await _hub.InvokeProcessOnReceiveMessage(TestSignalRTags.SharedTagParam, message);
|
||||
|
||||
// Assert
|
||||
Assert.IsTrue(_service.SharedTagMethodCalled);
|
||||
Assert.IsNotNull(_service.ReceivedSharedTag);
|
||||
Assert.AreEqual(1, _service.ReceivedSharedTag.Id);
|
||||
Assert.AreEqual("Important", _service.ReceivedSharedTag.Name);
|
||||
Assert.AreEqual("#FF0000", _service.ReceivedSharedTag.Color);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region No Parameters Tests
|
||||
|
||||
[TestMethod]
|
||||
public async Task ProcessOnReceiveMessage_NoParams_InvokesMethod()
|
||||
{
|
||||
// Arrange
|
||||
var message = SignalRTestHelper.CreateEmptyMessage();
|
||||
|
||||
// Act
|
||||
await _hub.InvokeProcessOnReceiveMessage(TestSignalRTags.NoParams, message);
|
||||
|
||||
// Assert
|
||||
Assert.IsTrue(_service.NoParamsMethodCalled);
|
||||
SignalRTestHelper.AssertSuccessResponse(_hub.SentMessages[0], TestSignalRTags.NoParams);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Simple Collection Parameter Tests
|
||||
|
||||
[TestMethod]
|
||||
public async Task ProcessOnReceiveMessage_IntArray_DeserializesCorrectly()
|
||||
{
|
||||
// Arrange - Arrays are complex objects (not ValueType), use CreateComplexObjectMessage
|
||||
var values = new[] { 1, 2, 3, 4, 5 };
|
||||
var message = SignalRTestHelper.CreateComplexObjectMessage(values);
|
||||
|
||||
// Act
|
||||
await _hub.InvokeProcessOnReceiveMessage(TestSignalRTags.IntArrayParam, message);
|
||||
|
||||
// Assert
|
||||
Assert.IsTrue(_service.IntArrayMethodCalled);
|
||||
Assert.IsNotNull(_service.ReceivedIntArray);
|
||||
CollectionAssert.AreEqual(values, _service.ReceivedIntArray);
|
||||
|
||||
// Verify response (doubled values)
|
||||
var responseData = SignalRTestHelper.GetResponseData<int[]>(_hub.SentMessages[0]);
|
||||
Assert.IsNotNull(responseData);
|
||||
CollectionAssert.AreEqual(new[] { 2, 4, 6, 8, 10 }, responseData);
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public async Task ProcessOnReceiveMessage_GuidArray_DeserializesCorrectly()
|
||||
{
|
||||
// Arrange
|
||||
var guids = new[] { Guid.NewGuid(), Guid.NewGuid(), Guid.NewGuid() };
|
||||
var message = SignalRTestHelper.CreateComplexObjectMessage(guids);
|
||||
|
||||
// Act
|
||||
await _hub.InvokeProcessOnReceiveMessage(TestSignalRTags.GuidArrayParam, message);
|
||||
|
||||
// Assert
|
||||
Assert.IsTrue(_service.GuidArrayMethodCalled);
|
||||
Assert.IsNotNull(_service.ReceivedGuidArray);
|
||||
Assert.AreEqual(3, _service.ReceivedGuidArray.Length);
|
||||
CollectionAssert.AreEqual(guids, _service.ReceivedGuidArray);
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public async Task ProcessOnReceiveMessage_StringList_DeserializesCorrectly()
|
||||
{
|
||||
// Arrange
|
||||
var items = new List<string> { "apple", "banana", "cherry" };
|
||||
var message = SignalRTestHelper.CreateComplexObjectMessage(items);
|
||||
|
||||
// Act
|
||||
await _hub.InvokeProcessOnReceiveMessage(TestSignalRTags.StringListParam, message);
|
||||
|
||||
// Assert
|
||||
Assert.IsTrue(_service.StringListMethodCalled);
|
||||
Assert.IsNotNull(_service.ReceivedStringList);
|
||||
CollectionAssert.AreEqual(items, _service.ReceivedStringList);
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public async Task ProcessOnReceiveMessage_IntList_DeserializesCorrectly()
|
||||
{
|
||||
// Arrange
|
||||
var numbers = new List<int> { 10, 20, 30 };
|
||||
var message = SignalRTestHelper.CreateComplexObjectMessage(numbers);
|
||||
|
||||
// Act
|
||||
await _hub.InvokeProcessOnReceiveMessage(TestSignalRTags.IntListParam, message);
|
||||
|
||||
// Assert
|
||||
Assert.IsTrue(_service.IntListMethodCalled);
|
||||
Assert.IsNotNull(_service.ReceivedIntList);
|
||||
CollectionAssert.AreEqual(numbers, _service.ReceivedIntList);
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public async Task ProcessOnReceiveMessage_BoolArray_DeserializesCorrectly()
|
||||
{
|
||||
// Arrange
|
||||
var flags = new[] { true, false, true, true };
|
||||
var message = SignalRTestHelper.CreateComplexObjectMessage(flags);
|
||||
|
||||
// Act
|
||||
await _hub.InvokeProcessOnReceiveMessage(TestSignalRTags.BoolArrayParam, message);
|
||||
|
||||
// Assert
|
||||
Assert.IsTrue(_service.BoolArrayMethodCalled);
|
||||
Assert.IsNotNull(_service.ReceivedBoolArray);
|
||||
CollectionAssert.AreEqual(flags, _service.ReceivedBoolArray);
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public async Task ProcessOnReceiveMessage_EmptyIntArray_DeserializesCorrectly()
|
||||
{
|
||||
// Arrange
|
||||
var values = Array.Empty<int>();
|
||||
var message = SignalRTestHelper.CreateComplexObjectMessage(values);
|
||||
|
||||
// Act
|
||||
await _hub.InvokeProcessOnReceiveMessage(TestSignalRTags.IntArrayParam, message);
|
||||
|
||||
// Assert
|
||||
Assert.IsTrue(_service.IntArrayMethodCalled);
|
||||
Assert.IsNotNull(_service.ReceivedIntArray);
|
||||
Assert.AreEqual(0, _service.ReceivedIntArray.Length);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Complex Collection Parameter Tests
|
||||
|
||||
[TestMethod]
|
||||
public async Task ProcessOnReceiveMessage_TestOrderItemList_DeserializesCorrectly()
|
||||
{
|
||||
// Arrange - using shared TestOrderItem list
|
||||
var items = new List<TestOrderItem>
|
||||
{
|
||||
new() { Id = 1, ProductName = "First", Quantity = 1 },
|
||||
new() { Id = 2, ProductName = "Second", Quantity = 2 },
|
||||
new() { Id = 3, ProductName = "Third", Quantity = 3 }
|
||||
};
|
||||
var message = SignalRTestHelper.CreateComplexObjectMessage(items);
|
||||
|
||||
// Act
|
||||
await _hub.InvokeProcessOnReceiveMessage(TestSignalRTags.TestOrderItemListParam, message);
|
||||
|
||||
// Assert
|
||||
Assert.IsTrue(_service.TestOrderItemListMethodCalled);
|
||||
Assert.AreEqual(3, _service.ReceivedTestOrderItemList?.Count);
|
||||
Assert.AreEqual("First", _service.ReceivedTestOrderItemList?[0].ProductName);
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public async Task ProcessOnReceiveMessage_NestedList_DeserializesCorrectly()
|
||||
{
|
||||
// Arrange
|
||||
var nestedList = new List<List<int>>
|
||||
{
|
||||
new() { 1, 2, 3 },
|
||||
new() { 4, 5 },
|
||||
new() { 6, 7, 8, 9 }
|
||||
};
|
||||
var message = SignalRTestHelper.CreateComplexObjectMessage(nestedList);
|
||||
|
||||
// Act
|
||||
await _hub.InvokeProcessOnReceiveMessage(TestSignalRTags.NestedListParam, message);
|
||||
|
||||
// Assert
|
||||
Assert.IsTrue(_service.NestedListMethodCalled);
|
||||
Assert.IsNotNull(_service.ReceivedNestedList);
|
||||
Assert.AreEqual(3, _service.ReceivedNestedList.Count);
|
||||
CollectionAssert.AreEqual(new List<int> { 1, 2, 3 }, _service.ReceivedNestedList[0]);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Mixed Parameter Tests (Primitive + Complex)
|
||||
|
||||
[TestMethod]
|
||||
public async Task ProcessOnReceiveMessage_IntAndDto_DeserializesCorrectly()
|
||||
{
|
||||
// Arrange
|
||||
var item = new TestOrderItem { Id = 10, ProductName = "Test", Quantity = 5 };
|
||||
var message = SignalRTestHelper.CreatePrimitiveParamsMessage(42, item);
|
||||
|
||||
// Act
|
||||
await _hub.InvokeProcessOnReceiveMessage(TestSignalRTags.IntAndDtoParam, message);
|
||||
|
||||
// Assert
|
||||
Assert.IsTrue(_service.IntAndDtoMethodCalled);
|
||||
Assert.IsNotNull(_service.ReceivedIntAndDto);
|
||||
Assert.AreEqual(42, _service.ReceivedIntAndDto.Value.Item1);
|
||||
Assert.AreEqual("Test", _service.ReceivedIntAndDto.Value.Item2?.ProductName);
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public async Task ProcessOnReceiveMessage_DtoAndList_DeserializesCorrectly()
|
||||
{
|
||||
// Arrange
|
||||
var item = new TestOrderItem { Id = 1, ProductName = "Test", Quantity = 1 };
|
||||
var numbers = new List<int> { 1, 2, 3 };
|
||||
var message = SignalRTestHelper.CreatePrimitiveParamsMessage(item, numbers);
|
||||
|
||||
// Act
|
||||
await _hub.InvokeProcessOnReceiveMessage(TestSignalRTags.DtoAndListParam, message);
|
||||
|
||||
// Assert
|
||||
Assert.IsTrue(_service.DtoAndListMethodCalled);
|
||||
Assert.IsNotNull(_service.ReceivedDtoAndList);
|
||||
Assert.AreEqual("Test", _service.ReceivedDtoAndList.Value.Item1?.ProductName);
|
||||
CollectionAssert.AreEqual(numbers, _service.ReceivedDtoAndList.Value.Item2);
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public async Task ProcessOnReceiveMessage_ThreeComplexParams_DeserializesCorrectly()
|
||||
{
|
||||
// Arrange
|
||||
var item = new TestOrderItem { Id = 1, ProductName = "Product", Quantity = 1 };
|
||||
var tags = new List<string> { "tag1", "tag2" };
|
||||
var sharedTag = new SharedTag { Id = 1, Name = "Shared" };
|
||||
var message = SignalRTestHelper.CreatePrimitiveParamsMessage(item, tags, sharedTag);
|
||||
|
||||
// Act
|
||||
await _hub.InvokeProcessOnReceiveMessage(TestSignalRTags.ThreeComplexParams, message);
|
||||
|
||||
// Assert
|
||||
Assert.IsTrue(_service.ThreeComplexParamsMethodCalled);
|
||||
Assert.IsNotNull(_service.ReceivedThreeComplexParams);
|
||||
Assert.AreEqual("Product", _service.ReceivedThreeComplexParams.Value.Item1?.ProductName);
|
||||
Assert.AreEqual(2, _service.ReceivedThreeComplexParams.Value.Item2?.Count);
|
||||
Assert.AreEqual("Shared", _service.ReceivedThreeComplexParams.Value.Item3?.Name);
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public async Task ProcessOnReceiveMessage_MixedWithArray_DeserializesCorrectly()
|
||||
{
|
||||
// Arrange - Multiple params: bool, int[], string
|
||||
// When ParamCount > 1, uses IdMessage format regardless of types
|
||||
var flag = true;
|
||||
var numbers = new[] { 1, 2, 3 };
|
||||
var text = "hello";
|
||||
var message = SignalRTestHelper.CreatePrimitiveParamsMessage(flag, numbers, text);
|
||||
|
||||
// Act
|
||||
await _hub.InvokeProcessOnReceiveMessage(TestSignalRTags.MixedWithArrayParam, message);
|
||||
|
||||
// Assert
|
||||
Assert.IsTrue(_service.MixedWithArrayMethodCalled);
|
||||
Assert.IsNotNull(_service.ReceivedMixedWithArray);
|
||||
Assert.AreEqual(true, _service.ReceivedMixedWithArray.Value.Item1);
|
||||
CollectionAssert.AreEqual(numbers, _service.ReceivedMixedWithArray.Value.Item2);
|
||||
Assert.AreEqual("hello", _service.ReceivedMixedWithArray.Value.Item3);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Error Handling Tests
|
||||
|
||||
[TestMethod]
|
||||
public async Task ProcessOnReceiveMessage_UnknownTag_InvokesNotFoundCallback()
|
||||
{
|
||||
// Arrange
|
||||
const int unknownTag = 9999;
|
||||
var message = SignalRTestHelper.CreateEmptyMessage();
|
||||
|
||||
// Act
|
||||
await _hub.InvokeProcessOnReceiveMessage(unknownTag, message);
|
||||
|
||||
// Assert
|
||||
Assert.IsTrue(_hub.WasNotFoundCallbackInvoked);
|
||||
Assert.IsNotNull(_hub.NotFoundTagName);
|
||||
|
||||
// Should still send error response
|
||||
Assert.AreEqual(1, _hub.SentMessages.Count);
|
||||
SignalRTestHelper.AssertErrorResponse(_hub.SentMessages[0], unknownTag);
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public async Task ProcessOnReceiveMessage_MethodThrows_ReturnsErrorResponse()
|
||||
{
|
||||
// Arrange
|
||||
var message = SignalRTestHelper.CreateEmptyMessage();
|
||||
|
||||
// Act
|
||||
await _hub.InvokeProcessOnReceiveMessage(TestSignalRTags.ThrowsException, message);
|
||||
|
||||
// Assert
|
||||
Assert.AreEqual(1, _hub.SentMessages.Count);
|
||||
SignalRTestHelper.AssertErrorResponse(_hub.SentMessages[0], TestSignalRTags.ThrowsException);
|
||||
Assert.IsTrue(_hub.Logger.HasErrorLogs, "Should have logged error");
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public async Task ProcessOnReceiveMessage_EmptyMessage_LogsWarning()
|
||||
{
|
||||
// Arrange
|
||||
byte[] emptyMessage = [];
|
||||
|
||||
|
||||
// Act
|
||||
await _hub.InvokeProcessOnReceiveMessage(TestSignalRTags.NoParams, emptyMessage);
|
||||
|
||||
// Assert
|
||||
Assert.IsTrue(_hub.Logger.HasWarningLogs, "Should have warning for empty message");
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Response Tests
|
||||
|
||||
[TestMethod]
|
||||
public async Task ProcessOnReceiveMessage_Success_SendsToCaller()
|
||||
{
|
||||
// Arrange
|
||||
var message = SignalRTestHelper.CreateSinglePrimitiveMessage(42);
|
||||
|
||||
// Act
|
||||
await _hub.InvokeProcessOnReceiveMessage(TestSignalRTags.SingleIntParam, message);
|
||||
|
||||
// Assert
|
||||
Assert.AreEqual(1, _hub.SentMessages.Count);
|
||||
Assert.AreEqual(SendTarget.Caller, _hub.SentMessages[0].Target);
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public async Task ProcessOnReceiveMessage_WithRequestId_IncludesRequestIdInResponse()
|
||||
{
|
||||
// Arrange
|
||||
var message = SignalRTestHelper.CreateSinglePrimitiveMessage(42);
|
||||
const int requestId = 12345;
|
||||
|
||||
// Act
|
||||
await _hub.InvokeProcessOnReceiveMessage(TestSignalRTags.SingleIntParam, message, requestId);
|
||||
|
||||
// Assert
|
||||
Assert.AreEqual(requestId, _hub.SentMessages[0].RequestId);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Empty/Null Message Edge Cases
|
||||
|
||||
[TestMethod]
|
||||
public async Task ProcessOnReceiveMessage_EmptyBytes_WithNoParams_Succeeds()
|
||||
{
|
||||
// Arrange - empty byte array for method with no parameters
|
||||
byte[] emptyMessage = [];
|
||||
|
||||
// Act
|
||||
await _hub.InvokeProcessOnReceiveMessage(TestSignalRTags.NoParams, emptyMessage);
|
||||
|
||||
// Assert - should succeed because method has no params
|
||||
Assert.IsTrue(_service.NoParamsMethodCalled);
|
||||
Assert.IsTrue(_hub.Logger.HasWarningLogs, "Should have warning for empty message");
|
||||
SignalRTestHelper.AssertSuccessResponse(_hub.SentMessages[0], TestSignalRTags.NoParams);
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public async Task ProcessOnReceiveMessage_EmptyBytes_WithIntParam_ReturnsError()
|
||||
{
|
||||
// Arrange - empty byte array for method expecting int parameter
|
||||
byte[] emptyMessage = [];
|
||||
|
||||
// Act
|
||||
await _hub.InvokeProcessOnReceiveMessage(TestSignalRTags.SingleIntParam, emptyMessage);
|
||||
|
||||
// Assert - should return error response, not crash
|
||||
Assert.IsFalse(_service.SingleIntMethodCalled, "Method should not be called with empty message");
|
||||
Assert.AreEqual(1, _hub.SentMessages.Count);
|
||||
SignalRTestHelper.AssertErrorResponse(_hub.SentMessages[0], TestSignalRTags.SingleIntParam);
|
||||
Assert.IsTrue(_hub.Logger.HasErrorLogs || _hub.Logger.HasWarningLogs);
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public async Task ProcessOnReceiveMessage_EmptyBytes_WithComplexParam_ReturnsError()
|
||||
{
|
||||
// Arrange - empty byte array for method expecting TestOrderItem parameter
|
||||
byte[] emptyMessage = [];
|
||||
|
||||
// Act
|
||||
await _hub.InvokeProcessOnReceiveMessage(TestSignalRTags.TestOrderItemParam, emptyMessage);
|
||||
|
||||
// Assert - should return error response, not crash
|
||||
Assert.IsFalse(_service.TestOrderItemMethodCalled, "Method should not be called with empty message");
|
||||
Assert.AreEqual(1, _hub.SentMessages.Count);
|
||||
SignalRTestHelper.AssertErrorResponse(_hub.SentMessages[0], TestSignalRTags.TestOrderItemParam);
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public async Task ProcessOnReceiveMessage_NullMessage_WithNoParams_Succeeds()
|
||||
{
|
||||
// Arrange - null message for method with no parameters
|
||||
byte[]? nullMessage = null;
|
||||
|
||||
// Act
|
||||
await _hub.InvokeProcessOnReceiveMessage(TestSignalRTags.NoParams, nullMessage);
|
||||
|
||||
// Assert - should succeed because method has no params
|
||||
Assert.IsTrue(_service.NoParamsMethodCalled);
|
||||
SignalRTestHelper.AssertSuccessResponse(_hub.SentMessages[0], TestSignalRTags.NoParams);
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public async Task ProcessOnReceiveMessage_NullMessage_WithIntParam_ReturnsError()
|
||||
{
|
||||
// Arrange - null message for method expecting int parameter
|
||||
byte[]? nullMessage = null;
|
||||
|
||||
// Act
|
||||
await _hub.InvokeProcessOnReceiveMessage(TestSignalRTags.SingleIntParam, nullMessage);
|
||||
|
||||
// Assert - should return error response, not crash
|
||||
Assert.IsFalse(_service.SingleIntMethodCalled, "Method should not be called with null message");
|
||||
Assert.AreEqual(1, _hub.SentMessages.Count);
|
||||
SignalRTestHelper.AssertErrorResponse(_hub.SentMessages[0], TestSignalRTags.SingleIntParam);
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public async Task ProcessOnReceiveMessage_EmptyBytes_WithMultipleParams_ReturnsError()
|
||||
{
|
||||
// Arrange - empty byte array for method expecting multiple parameters
|
||||
byte[] emptyMessage = [];
|
||||
|
||||
// Act
|
||||
await _hub.InvokeProcessOnReceiveMessage(TestSignalRTags.TwoIntParams, emptyMessage);
|
||||
|
||||
// Assert - should return error response, not crash
|
||||
Assert.IsFalse(_service.TwoIntMethodCalled, "Method should not be called with empty message");
|
||||
Assert.AreEqual(1, _hub.SentMessages.Count);
|
||||
SignalRTestHelper.AssertErrorResponse(_hub.SentMessages[0], TestSignalRTags.TwoIntParams);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Extended Array Parameter Deserialization Tests
|
||||
|
||||
[TestMethod]
|
||||
public async Task ProcessOnReceiveMessage_LongArray_DeserializesCorrectly()
|
||||
{
|
||||
// Arrange
|
||||
var values = new[] { 1L, 9223372036854775807L, -9223372036854775808L, 0L };
|
||||
var message = SignalRTestHelper.CreateComplexObjectMessage(values);
|
||||
|
||||
// Act
|
||||
await _hub.InvokeProcessOnReceiveMessage(TestSignalRTags.LongArrayParam, message);
|
||||
|
||||
// Assert
|
||||
Assert.IsTrue(_service.LongArrayMethodCalled);
|
||||
Assert.IsNotNull(_service.ReceivedLongArray);
|
||||
Assert.AreEqual(4, _service.ReceivedLongArray.Length);
|
||||
Assert.AreEqual(9223372036854775807L, _service.ReceivedLongArray[1]);
|
||||
Assert.AreEqual(-9223372036854775808L, _service.ReceivedLongArray[2]);
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public async Task ProcessOnReceiveMessage_DecimalArray_DeserializesCorrectly()
|
||||
{
|
||||
// Arrange
|
||||
var values = new[] { 0.01m, 99.99m, 123456.789m, -999.99m };
|
||||
var message = SignalRTestHelper.CreateComplexObjectMessage(values);
|
||||
|
||||
// Act
|
||||
await _hub.InvokeProcessOnReceiveMessage(TestSignalRTags.DecimalArrayParam, message);
|
||||
|
||||
// Assert
|
||||
Assert.IsTrue(_service.DecimalArrayMethodCalled);
|
||||
Assert.IsNotNull(_service.ReceivedDecimalArray);
|
||||
Assert.AreEqual(4, _service.ReceivedDecimalArray.Length);
|
||||
Assert.AreEqual(0.01m, _service.ReceivedDecimalArray[0]);
|
||||
Assert.AreEqual(99.99m, _service.ReceivedDecimalArray[1]);
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public async Task ProcessOnReceiveMessage_DateTimeArray_DeserializesCorrectly()
|
||||
{
|
||||
// Arrange
|
||||
var values = new[]
|
||||
{
|
||||
new DateTime(2024, 1, 1, 0, 0, 0, DateTimeKind.Utc),
|
||||
new DateTime(2024, 12, 31, 23, 59, 59, DateTimeKind.Utc),
|
||||
DateTime.MinValue,
|
||||
new DateTime(2000, 6, 15, 12, 30, 45, DateTimeKind.Utc)
|
||||
};
|
||||
var message = SignalRTestHelper.CreateComplexObjectMessage(values);
|
||||
|
||||
// Act
|
||||
await _hub.InvokeProcessOnReceiveMessage(TestSignalRTags.DateTimeArrayParam, message);
|
||||
|
||||
// Assert
|
||||
Assert.IsTrue(_service.DateTimeArrayMethodCalled);
|
||||
Assert.IsNotNull(_service.ReceivedDateTimeArray);
|
||||
Assert.AreEqual(4, _service.ReceivedDateTimeArray.Length);
|
||||
Assert.AreEqual(2024, _service.ReceivedDateTimeArray[0].Year);
|
||||
Assert.AreEqual(12, _service.ReceivedDateTimeArray[1].Month);
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public async Task ProcessOnReceiveMessage_EnumArray_DeserializesCorrectly()
|
||||
{
|
||||
// Arrange - using shared TestStatus enum
|
||||
var values = new[] { TestStatus.Pending, TestStatus.Active, TestStatus.Processing, TestStatus.Shipped };
|
||||
var message = SignalRTestHelper.CreateComplexObjectMessage(values);
|
||||
|
||||
// Act
|
||||
await _hub.InvokeProcessOnReceiveMessage(TestSignalRTags.EnumArrayParam, message);
|
||||
|
||||
// Assert
|
||||
Assert.IsTrue(_service.EnumArrayMethodCalled);
|
||||
Assert.IsNotNull(_service.ReceivedEnumArray);
|
||||
Assert.AreEqual(4, _service.ReceivedEnumArray.Length);
|
||||
Assert.AreEqual(TestStatus.Pending, _service.ReceivedEnumArray[0]);
|
||||
Assert.AreEqual(TestStatus.Shipped, _service.ReceivedEnumArray[3]);
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public async Task ProcessOnReceiveMessage_DoubleArray_DeserializesCorrectly()
|
||||
{
|
||||
// Arrange
|
||||
var values = new[] { 3.14159265359, -273.15, 0.0, double.MaxValue, double.MinValue };
|
||||
var message = SignalRTestHelper.CreateComplexObjectMessage(values);
|
||||
|
||||
// Act
|
||||
await _hub.InvokeProcessOnReceiveMessage(TestSignalRTags.DoubleArrayParam, message);
|
||||
|
||||
// Assert
|
||||
Assert.IsTrue(_service.DoubleArrayMethodCalled);
|
||||
Assert.IsNotNull(_service.ReceivedDoubleArray);
|
||||
Assert.AreEqual(5, _service.ReceivedDoubleArray.Length);
|
||||
Assert.IsTrue(Math.Abs(_service.ReceivedDoubleArray[0] - 3.14159265359) < 0.0000001);
|
||||
Assert.AreEqual(-273.15, _service.ReceivedDoubleArray[1]);
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public async Task ProcessOnReceiveMessage_SharedTagArray_ComplexObjectArray_DeserializesCorrectly()
|
||||
{
|
||||
// Arrange - array of IId<int> complex objects
|
||||
var tags = new[]
|
||||
{
|
||||
new SharedTag { Id = 1, Name = "Tag1", Color = "#FF0000", Priority = 1 },
|
||||
new SharedTag { Id = 2, Name = "Tag2", Color = "#00FF00", Priority = 2 },
|
||||
new SharedTag { Id = 3, Name = "Tag3", Color = "#0000FF", Priority = 3 }
|
||||
};
|
||||
var message = SignalRTestHelper.CreateComplexObjectMessage(tags);
|
||||
|
||||
// Act
|
||||
await _hub.InvokeProcessOnReceiveMessage(TestSignalRTags.SharedTagArrayParam, message);
|
||||
|
||||
// Assert
|
||||
Assert.IsTrue(_service.SharedTagArrayMethodCalled);
|
||||
Assert.IsNotNull(_service.ReceivedSharedTagArray);
|
||||
Assert.AreEqual(3, _service.ReceivedSharedTagArray.Length);
|
||||
Assert.AreEqual("Tag1", _service.ReceivedSharedTagArray[0].Name);
|
||||
Assert.AreEqual("#00FF00", _service.ReceivedSharedTagArray[1].Color);
|
||||
Assert.AreEqual(3, _service.ReceivedSharedTagArray[2].Priority);
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public async Task ProcessOnReceiveMessage_Dictionary_DeserializesCorrectly()
|
||||
{
|
||||
// Arrange
|
||||
var dict = new Dictionary<string, int>
|
||||
{
|
||||
{ "apple", 1 },
|
||||
{ "banana", 2 },
|
||||
{ "cherry", 3 }
|
||||
};
|
||||
var message = SignalRTestHelper.CreateComplexObjectMessage(dict);
|
||||
|
||||
// Act
|
||||
await _hub.InvokeProcessOnReceiveMessage(TestSignalRTags.DictionaryParam, message);
|
||||
|
||||
// Assert
|
||||
Assert.IsTrue(_service.DictionaryMethodCalled);
|
||||
Assert.IsNotNull(_service.ReceivedDictionary);
|
||||
Assert.AreEqual(3, _service.ReceivedDictionary.Count);
|
||||
Assert.AreEqual(1, _service.ReceivedDictionary["apple"]);
|
||||
Assert.AreEqual(2, _service.ReceivedDictionary["banana"]);
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public async Task ProcessOnReceiveMessage_EmptyGuidArray_DeserializesCorrectly()
|
||||
{
|
||||
// Arrange - edge case: empty Guid array
|
||||
var guids = Array.Empty<Guid>();
|
||||
var message = SignalRTestHelper.CreateComplexObjectMessage(guids);
|
||||
|
||||
// Act
|
||||
await _hub.InvokeProcessOnReceiveMessage(TestSignalRTags.GuidArrayParam, message);
|
||||
|
||||
// Assert
|
||||
Assert.IsTrue(_service.GuidArrayMethodCalled);
|
||||
Assert.IsNotNull(_service.ReceivedGuidArray);
|
||||
Assert.AreEqual(0, _service.ReceivedGuidArray.Length);
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public async Task ProcessOnReceiveMessage_SingleElementArray_DeserializesCorrectly()
|
||||
{
|
||||
// Arrange - edge case: single element array
|
||||
var values = new[] { 42 };
|
||||
var message = SignalRTestHelper.CreateComplexObjectMessage(values);
|
||||
|
||||
// Act
|
||||
await _hub.InvokeProcessOnReceiveMessage(TestSignalRTags.IntArrayParam, message);
|
||||
|
||||
// Assert
|
||||
Assert.IsTrue(_service.IntArrayMethodCalled);
|
||||
Assert.IsNotNull(_service.ReceivedIntArray);
|
||||
Assert.AreEqual(1, _service.ReceivedIntArray.Length);
|
||||
Assert.AreEqual(42, _service.ReceivedIntArray[0]);
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public async Task ProcessOnReceiveMessage_LargeArray_DeserializesCorrectly()
|
||||
{
|
||||
// Arrange - performance edge case: large array
|
||||
var values = Enumerable.Range(1, 1000).ToArray();
|
||||
var message = SignalRTestHelper.CreateComplexObjectMessage(values);
|
||||
|
||||
// Act
|
||||
await _hub.InvokeProcessOnReceiveMessage(TestSignalRTags.IntArrayParam, message);
|
||||
|
||||
// Assert
|
||||
Assert.IsTrue(_service.IntArrayMethodCalled);
|
||||
Assert.IsNotNull(_service.ReceivedIntArray);
|
||||
Assert.AreEqual(1000, _service.ReceivedIntArray.Length);
|
||||
Assert.AreEqual(1, _service.ReceivedIntArray[0]);
|
||||
Assert.AreEqual(1000, _service.ReceivedIntArray[999]);
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public async Task ProcessOnReceiveMessage_ComplexObjectListWithNestedCollections_DeserializesCorrectly()
|
||||
{
|
||||
// Arrange - TestOrder contains nested Items list
|
||||
var orders = new List<TestOrderItem>
|
||||
{
|
||||
new() { Id = 1, ProductName = "Product A", Quantity = 10, UnitPrice = 9.99m, Status = TestStatus.Active },
|
||||
new() { Id = 2, ProductName = "Product B", Quantity = 5, UnitPrice = 19.99m, Status = TestStatus.Processing },
|
||||
new() { Id = 3, ProductName = "Product C", Quantity = 1, UnitPrice = 99.99m, Status = TestStatus.Shipped }
|
||||
};
|
||||
var message = SignalRTestHelper.CreateComplexObjectMessage(orders);
|
||||
|
||||
// Act
|
||||
await _hub.InvokeProcessOnReceiveMessage(TestSignalRTags.TestOrderItemListParam, message);
|
||||
|
||||
// Assert
|
||||
Assert.IsTrue(_service.TestOrderItemListMethodCalled);
|
||||
Assert.IsNotNull(_service.ReceivedTestOrderItemList);
|
||||
Assert.AreEqual(3, _service.ReceivedTestOrderItemList.Count);
|
||||
Assert.AreEqual("Product A", _service.ReceivedTestOrderItemList[0].ProductName);
|
||||
Assert.AreEqual(19.99m, _service.ReceivedTestOrderItemList[1].UnitPrice);
|
||||
Assert.AreEqual(TestStatus.Shipped, _service.ReceivedTestOrderItemList[2].Status);
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public async Task ProcessOnReceiveMessage_StringArrayWithSpecialChars_DeserializesCorrectly()
|
||||
{
|
||||
// Arrange - strings with special characters
|
||||
var values = new List<string>
|
||||
{
|
||||
"normal",
|
||||
"with spaces",
|
||||
"with\"quotes\"",
|
||||
"with\nnewline",
|
||||
"with\ttab",
|
||||
"unicode: áéíóú",
|
||||
"" // empty string
|
||||
};
|
||||
var message = SignalRTestHelper.CreateComplexObjectMessage(values);
|
||||
|
||||
// Act
|
||||
await _hub.InvokeProcessOnReceiveMessage(TestSignalRTags.StringListParam, message);
|
||||
|
||||
// Assert
|
||||
Assert.IsTrue(_service.StringListMethodCalled);
|
||||
Assert.IsNotNull(_service.ReceivedStringList);
|
||||
Assert.AreEqual(7, _service.ReceivedStringList.Count);
|
||||
Assert.AreEqual("normal", _service.ReceivedStringList[0]);
|
||||
Assert.AreEqual("with spaces", _service.ReceivedStringList[1]);
|
||||
Assert.AreEqual("unicode: áéíóú", _service.ReceivedStringList[5]);
|
||||
Assert.AreEqual("", _service.ReceivedStringList[6]);
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public async Task ProcessOnReceiveMessage_GuidArrayWithEmptyGuid_DeserializesCorrectly()
|
||||
{
|
||||
// Arrange - includes Guid.Empty
|
||||
var guids = new[] { Guid.NewGuid(), Guid.Empty, Guid.NewGuid() };
|
||||
var message = SignalRTestHelper.CreateComplexObjectMessage(guids);
|
||||
|
||||
// Act
|
||||
await _hub.InvokeProcessOnReceiveMessage(TestSignalRTags.GuidArrayParam, message);
|
||||
|
||||
// Assert
|
||||
Assert.IsTrue(_service.GuidArrayMethodCalled);
|
||||
Assert.IsNotNull(_service.ReceivedGuidArray);
|
||||
Assert.AreEqual(3, _service.ReceivedGuidArray.Length);
|
||||
Assert.AreEqual(Guid.Empty, _service.ReceivedGuidArray[1]);
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void CreateComplexObjectMessage_Dictionary_ProducesCorrectJson()
|
||||
{
|
||||
// Arrange
|
||||
var dict = new Dictionary<string, int>
|
||||
{
|
||||
{ "apple", 1 },
|
||||
{ "banana", 2 },
|
||||
{ "cherry", 3 }
|
||||
};
|
||||
|
||||
// Act
|
||||
var message = SignalRTestHelper.CreateComplexObjectMessage(dict);
|
||||
var deserialized = MessagePackSerializer.Deserialize<SignalPostJsonDataMessage<object>>(
|
||||
message, MessagePackSerializerOptions.Standard);
|
||||
|
||||
// Assert
|
||||
Assert.IsNotNull(deserialized.PostDataJson, "PostDataJson should not be null");
|
||||
Assert.IsTrue(deserialized.PostDataJson.Contains("apple"),
|
||||
$"PostDataJson should contain 'apple'. Actual: {deserialized.PostDataJson}");
|
||||
Assert.IsTrue(deserialized.PostDataJson.StartsWith("{"),
|
||||
$"PostDataJson should start with {{. Actual first char: {deserialized.PostDataJson[0]}");
|
||||
}
|
||||
|
||||
#endregion
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Runs all ProcessOnReceiveMessage tests with JSON serialization.
|
||||
/// </summary>
|
||||
[TestClass]
|
||||
public class ProcessOnReceiveMessageTests_Json : ProcessOnReceiveMessageTestsBase
|
||||
{
|
||||
protected override AcSerializerType SerializerType => AcSerializerType.Json;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Runs all ProcessOnReceiveMessage tests with Binary serialization.
|
||||
/// </summary>
|
||||
[TestClass]
|
||||
public class ProcessOnReceiveMessageTests_Binary : ProcessOnReceiveMessageTestsBase
|
||||
{
|
||||
protected override AcSerializerType SerializerType => AcSerializerType.Binary;
|
||||
}
|
||||
|
|
@ -0,0 +1,172 @@
|
|||
using AyCode.Core.Enums;
|
||||
using AyCode.Core.Extensions;
|
||||
using AyCode.Core.Helpers;
|
||||
using AyCode.Core.Tests.TestModels;
|
||||
using AyCode.Services.Server.SignalRs;
|
||||
using AyCode.Services.SignalRs;
|
||||
|
||||
namespace AyCode.Services.Server.Tests.SignalRs;
|
||||
|
||||
#region DataSource Implementations
|
||||
|
||||
public class TestOrderItemListDataSource : AcSignalRDataSource<TestOrderItem, int, List<TestOrderItem>>
|
||||
{
|
||||
public TestOrderItemListDataSource(AcSignalRClientBase signalRClient, SignalRCrudTags crudTags)
|
||||
: base(signalRClient, crudTags) { }
|
||||
}
|
||||
|
||||
public class TestOrderItemObservableDataSource : AcSignalRDataSource<TestOrderItem, int, AcObservableCollection<TestOrderItem>>
|
||||
{
|
||||
public TestOrderItemObservableDataSource(AcSignalRClientBase signalRClient, SignalRCrudTags crudTags)
|
||||
: base(signalRClient, crudTags) { }
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Abstract Test Base
|
||||
|
||||
/// <summary>
|
||||
/// Base class for SignalR DataSource tests.
|
||||
/// Derived classes specify the serializer type and collection type.
|
||||
/// </summary>
|
||||
/// <typeparam name="TDataSource">The concrete DataSource type</typeparam>
|
||||
/// <typeparam name="TIList">The inner list type (List or AcObservableCollection)</typeparam>
|
||||
public abstract class SignalRDataSourceTestBase<TDataSource, TIList>
|
||||
where TDataSource : AcSignalRDataSource<TestOrderItem, int, TIList>
|
||||
where TIList : class, IList<TestOrderItem>
|
||||
{
|
||||
protected abstract AcSerializerOptions SerializerOption { get; }
|
||||
protected abstract TDataSource CreateDataSource(TestableSignalRClient2 client, SignalRCrudTags crudTags);
|
||||
|
||||
protected TestLogger _logger = null!;
|
||||
protected TestableSignalRHub2 _hub = null!;
|
||||
protected TestableSignalRClient2 _client = null!;
|
||||
protected TestSignalRService2 _service = null!;
|
||||
protected SignalRCrudTags _crudTags = null!;
|
||||
|
||||
[TestInitialize]
|
||||
public void Setup()
|
||||
{
|
||||
_logger = new TestLogger();
|
||||
_hub = new TestableSignalRHub2();
|
||||
_service = new TestSignalRService2();
|
||||
_client = new TestableSignalRClient2(_hub, _logger);
|
||||
|
||||
_hub.SetSerializerType(SerializerOption);
|
||||
_hub.RegisterService(_service, _client);
|
||||
|
||||
_crudTags = new SignalRCrudTags(
|
||||
TestSignalRTags.DataSourceGetAll,
|
||||
TestSignalRTags.DataSourceGetItem,
|
||||
TestSignalRTags.DataSourceAdd,
|
||||
TestSignalRTags.DataSourceUpdate,
|
||||
TestSignalRTags.DataSourceRemove
|
||||
);
|
||||
}
|
||||
|
||||
#region Load Tests
|
||||
|
||||
[TestMethod]
|
||||
public async Task LoadDataSource_ReturnsAllItems()
|
||||
{
|
||||
var dataSource = CreateDataSource(_client, _crudTags);
|
||||
|
||||
await dataSource.LoadDataSource();
|
||||
|
||||
Assert.AreEqual(3, dataSource.Count);
|
||||
Assert.AreEqual("Product A", dataSource[0].ProductName);
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public async Task LoadItem_ReturnsSingleItem()
|
||||
{
|
||||
var dataSource = CreateDataSource(_client, _crudTags);
|
||||
|
||||
var result = await dataSource.LoadItem(2);
|
||||
|
||||
Assert.IsNotNull(result);
|
||||
Assert.AreEqual(2, result.Id);
|
||||
Assert.AreEqual("Product B", result.ProductName);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Add Tests
|
||||
|
||||
[TestMethod]
|
||||
public async Task Add_WithAutoSave_AddsItem()
|
||||
{
|
||||
var dataSource = CreateDataSource(_client, _crudTags);
|
||||
var newItem = new TestOrderItem { Id = 100, ProductName = "New Product", Quantity = 5, UnitPrice = 50m };
|
||||
|
||||
var result = await dataSource.Add(newItem, autoSave: true);
|
||||
|
||||
Assert.AreEqual(1, dataSource.Count);
|
||||
Assert.AreEqual("New Product", result.ProductName);
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void Add_WithoutAutoSave_AddsToTrackingOnly()
|
||||
{
|
||||
var dataSource = CreateDataSource(_client, _crudTags);
|
||||
var newItem = new TestOrderItem { Id = 100, ProductName = "New Product" };
|
||||
|
||||
dataSource.Add(newItem);
|
||||
|
||||
Assert.AreEqual(1, dataSource.Count);
|
||||
Assert.AreEqual(1, dataSource.GetTrackingItems().Count);
|
||||
Assert.AreEqual(TrackingState.Add, dataSource.GetTrackingItems()[0].TrackingState);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region SaveChanges Tests
|
||||
|
||||
[TestMethod]
|
||||
public async Task SaveChanges_SavesTrackedItems()
|
||||
{
|
||||
var dataSource = CreateDataSource(_client, _crudTags);
|
||||
dataSource.Add(new TestOrderItem { Id = 101, ProductName = "Item 1" });
|
||||
dataSource.Add(new TestOrderItem { Id = 102, ProductName = "Item 2" });
|
||||
|
||||
var unsaved = await dataSource.SaveChanges();
|
||||
|
||||
Assert.AreEqual(0, unsaved.Count);
|
||||
Assert.AreEqual(0, dataSource.GetTrackingItems().Count);
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public async Task SaveChangesAsync_ClearsTracking()
|
||||
{
|
||||
var dataSource = CreateDataSource(_client, _crudTags);
|
||||
dataSource.Add(new TestOrderItem { Id = 103, ProductName = "Item 3" });
|
||||
|
||||
await dataSource.SaveChangesAsync();
|
||||
|
||||
Assert.AreEqual(0, dataSource.GetTrackingItems().Count);
|
||||
}
|
||||
|
||||
#endregion
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region DataSources
|
||||
|
||||
[TestClass]
|
||||
public class SignalRDataSourceTests_List : SignalRDataSourceTestBase<TestOrderItemListDataSource, List<TestOrderItem>>
|
||||
{
|
||||
protected override AcSerializerOptions SerializerOption => AcBinarySerializerOptions.Default;
|
||||
protected override TestOrderItemListDataSource CreateDataSource(TestableSignalRClient2 client, SignalRCrudTags crudTags)
|
||||
=> new(client, crudTags);
|
||||
}
|
||||
|
||||
[TestClass]
|
||||
public class SignalRDataSourceTests_Observable : SignalRDataSourceTestBase<TestOrderItemObservableDataSource, AcObservableCollection<TestOrderItem>>
|
||||
{
|
||||
protected override AcSerializerOptions SerializerOption => AcBinarySerializerOptions.Default;
|
||||
|
||||
protected override TestOrderItemObservableDataSource CreateDataSource(TestableSignalRClient2 client, SignalRCrudTags crudTags)
|
||||
=> new(client, crudTags);
|
||||
}
|
||||
#endregion
|
||||
|
|
@ -5,18 +5,39 @@ using MessagePack.Resolvers;
|
|||
|
||||
namespace AyCode.Services.Server.Tests.SignalRs;
|
||||
|
||||
/// <summary>
|
||||
/// Captured sent message for assertions.
|
||||
/// </summary>
|
||||
public record SentMessage(
|
||||
int MessageTag,
|
||||
ISignalRMessage Message,
|
||||
int? RequestId,
|
||||
SendTarget Target,
|
||||
string? TargetId = null)
|
||||
{
|
||||
public SignalResponseDataMessage? AsDataResponse => Message as SignalResponseDataMessage;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Target of the sent message.
|
||||
/// </summary>
|
||||
public enum SendTarget
|
||||
{
|
||||
Caller,
|
||||
Client,
|
||||
Others,
|
||||
All,
|
||||
User,
|
||||
Group
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Helper methods for creating SignalR test messages.
|
||||
/// Uses the production SignalR types for compatibility with the actual client/server code.
|
||||
/// </summary>
|
||||
public static class SignalRTestHelper
|
||||
{
|
||||
private static readonly MessagePackSerializerOptions MessagePackOptions = ContractlessStandardResolver.Options;
|
||||
|
||||
/// <summary>
|
||||
/// Creates a MessagePack message for parameters using IdMessage format.
|
||||
/// Each parameter is serialized directly as JSON (no array wrapping).
|
||||
/// </summary>
|
||||
public static byte[] CreatePrimitiveParamsMessage(params object[] values)
|
||||
{
|
||||
var idMessage = new IdMessage(values);
|
||||
|
|
@ -24,18 +45,9 @@ public static class SignalRTestHelper
|
|||
return MessagePackSerializer.Serialize(postMessage, MessagePackOptions);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates a MessagePack message for a single primitive parameter.
|
||||
/// </summary>
|
||||
public static byte[] CreateSinglePrimitiveMessage<T>(T value) where T : notnull
|
||||
{
|
||||
return CreatePrimitiveParamsMessage(value);
|
||||
}
|
||||
=> CreatePrimitiveParamsMessage(value);
|
||||
|
||||
/// <summary>
|
||||
/// Creates a MessagePack message for a complex object parameter.
|
||||
/// Uses PostDataJson pattern for single complex objects.
|
||||
/// </summary>
|
||||
public static byte[] CreateComplexObjectMessage<T>(T obj)
|
||||
{
|
||||
var json = obj.ToJson();
|
||||
|
|
@ -43,73 +55,39 @@ public static class SignalRTestHelper
|
|||
return MessagePackSerializer.Serialize(postMessage, MessagePackOptions);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates an empty MessagePack message for parameterless methods.
|
||||
/// </summary>
|
||||
public static byte[] CreateEmptyMessage()
|
||||
{
|
||||
var postMessage = new SignalPostJsonDataMessage<object>();
|
||||
return MessagePackSerializer.Serialize(postMessage, MessagePackOptions);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Deserialize a SignalResponseJsonMessage from the captured SentMessage.
|
||||
/// </summary>
|
||||
public static T? GetResponseData<T>(SentMessage sentMessage)
|
||||
{
|
||||
if (sentMessage.Message is SignalResponseJsonMessage jsonResponse && jsonResponse.ResponseData != null)
|
||||
{
|
||||
return jsonResponse.ResponseData.JsonTo<T>();
|
||||
}
|
||||
|
||||
if (sentMessage.Message is SignalResponseBinaryMessage binaryResponse && binaryResponse.ResponseData != null)
|
||||
{
|
||||
return binaryResponse.ResponseData.BinaryTo<T>();
|
||||
}
|
||||
if (sentMessage.Message is SignalResponseDataMessage dataResponse && dataResponse.ResponseDataBin != null)
|
||||
return dataResponse.GetResponseData<T>();
|
||||
|
||||
return default;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the response status from either JSON or Binary message.
|
||||
/// </summary>
|
||||
private static SignalResponseStatus? GetResponseStatus(ISignalRMessage message)
|
||||
{
|
||||
return message switch
|
||||
{
|
||||
SignalResponseJsonMessage jsonMsg => jsonMsg.Status,
|
||||
SignalResponseBinaryMessage binaryMsg => binaryMsg.Status,
|
||||
_ => null
|
||||
};
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Assert that a response was successful.
|
||||
/// </summary>
|
||||
public static void AssertSuccessResponse(SentMessage sentMessage, int expectedTag)
|
||||
{
|
||||
var status = GetResponseStatus(sentMessage.Message);
|
||||
if (status == null)
|
||||
throw new AssertFailedException($"Response is not a valid SignalR response message. Type: {sentMessage.Message.GetType().Name}");
|
||||
if (sentMessage.Message is not SignalResponseDataMessage response)
|
||||
throw new AssertFailedException($"Response is not SignalResponseDataMessage. Type: {sentMessage.Message.GetType().Name}");
|
||||
|
||||
if (status != SignalResponseStatus.Success)
|
||||
throw new AssertFailedException($"Expected Success status but got {status}");
|
||||
if (response.Status != SignalResponseStatus.Success)
|
||||
throw new AssertFailedException($"Expected Success status but got {response.Status}");
|
||||
|
||||
if (sentMessage.MessageTag != expectedTag)
|
||||
throw new AssertFailedException($"Expected tag {expectedTag} but got {sentMessage.MessageTag}");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Assert that a response was an error.
|
||||
/// </summary>
|
||||
public static void AssertErrorResponse(SentMessage sentMessage, int expectedTag)
|
||||
{
|
||||
var status = GetResponseStatus(sentMessage.Message);
|
||||
if (status == null)
|
||||
throw new AssertFailedException($"Response is not a valid SignalR response message. Type: {sentMessage.Message.GetType().Name}");
|
||||
if (sentMessage.Message is not SignalResponseDataMessage response)
|
||||
throw new AssertFailedException($"Response is not SignalResponseDataMessage. Type: {sentMessage.Message.GetType().Name}");
|
||||
|
||||
if (status != SignalResponseStatus.Error)
|
||||
throw new AssertFailedException($"Expected Error status but got {status}");
|
||||
if (response.Status != SignalResponseStatus.Error)
|
||||
throw new AssertFailedException($"Expected Error status but got {response.Status}");
|
||||
|
||||
if (sentMessage.MessageTag != expectedTag)
|
||||
throw new AssertFailedException($"Expected tag {expectedTag} but got {sentMessage.MessageTag}");
|
||||
|
|
|
|||
|
|
@ -1,518 +0,0 @@
|
|||
using AyCode.Core.Tests.TestModels;
|
||||
using AyCode.Services.SignalRs;
|
||||
|
||||
namespace AyCode.Services.Server.Tests.SignalRs;
|
||||
|
||||
/// <summary>
|
||||
/// Test service with SignalR-attributed methods for testing ProcessOnReceiveMessage.
|
||||
/// Uses shared DTOs from AyCode.Core.Tests.TestModels.
|
||||
/// </summary>
|
||||
public class TestSignalRService
|
||||
{
|
||||
#region Captured Values for Assertions
|
||||
|
||||
// Primitive captures
|
||||
public bool SingleIntMethodCalled { get; private set; }
|
||||
public int? ReceivedInt { get; private set; }
|
||||
|
||||
public bool TwoIntMethodCalled { get; private set; }
|
||||
public (int A, int B)? ReceivedTwoInts { get; private set; }
|
||||
|
||||
public bool BoolMethodCalled { get; private set; }
|
||||
public bool? ReceivedBool { get; private set; }
|
||||
|
||||
public bool StringMethodCalled { get; private set; }
|
||||
public string? ReceivedString { get; private set; }
|
||||
|
||||
public bool GuidMethodCalled { get; private set; }
|
||||
public Guid? ReceivedGuid { get; private set; }
|
||||
|
||||
public bool EnumMethodCalled { get; private set; }
|
||||
public TestStatus? ReceivedEnum { get; private set; }
|
||||
|
||||
public bool NoParamsMethodCalled { get; private set; }
|
||||
|
||||
public bool MultipleTypesMethodCalled { get; private set; }
|
||||
public (bool, string, int)? ReceivedMultipleTypes { get; private set; }
|
||||
|
||||
// Extended primitives
|
||||
public bool DecimalMethodCalled { get; private set; }
|
||||
public decimal? ReceivedDecimal { get; private set; }
|
||||
|
||||
public bool DateTimeMethodCalled { get; private set; }
|
||||
public DateTime? ReceivedDateTime { get; private set; }
|
||||
|
||||
public bool DoubleMethodCalled { get; private set; }
|
||||
public double? ReceivedDouble { get; private set; }
|
||||
|
||||
public bool LongMethodCalled { get; private set; }
|
||||
public long? ReceivedLong { get; private set; }
|
||||
|
||||
// Complex object captures (using shared DTOs)
|
||||
public bool TestOrderItemMethodCalled { get; private set; }
|
||||
public TestOrderItem? ReceivedTestOrderItem { get; private set; }
|
||||
|
||||
public bool TestOrderMethodCalled { get; private set; }
|
||||
public TestOrder? ReceivedTestOrder { get; private set; }
|
||||
|
||||
public bool SharedTagMethodCalled { get; private set; }
|
||||
public SharedTag? ReceivedSharedTag { get; private set; }
|
||||
|
||||
// Collection captures
|
||||
public bool IntArrayMethodCalled { get; private set; }
|
||||
public int[]? ReceivedIntArray { get; private set; }
|
||||
|
||||
public bool GuidArrayMethodCalled { get; private set; }
|
||||
public Guid[]? ReceivedGuidArray { get; private set; }
|
||||
|
||||
public bool StringListMethodCalled { get; private set; }
|
||||
public List<string>? ReceivedStringList { get; private set; }
|
||||
|
||||
public bool TestOrderItemListMethodCalled { get; private set; }
|
||||
public List<TestOrderItem>? ReceivedTestOrderItemList { get; private set; }
|
||||
|
||||
public bool IntListMethodCalled { get; private set; }
|
||||
public List<int>? ReceivedIntList { get; private set; }
|
||||
|
||||
public bool BoolArrayMethodCalled { get; private set; }
|
||||
public bool[]? ReceivedBoolArray { get; private set; }
|
||||
|
||||
public bool MixedWithArrayMethodCalled { get; private set; }
|
||||
public (bool, int[], string)? ReceivedMixedWithArray { get; private set; }
|
||||
|
||||
public bool NestedListMethodCalled { get; private set; }
|
||||
public List<List<int>>? ReceivedNestedList { get; private set; }
|
||||
|
||||
// Extended array captures for comprehensive testing
|
||||
public bool LongArrayMethodCalled { get; private set; }
|
||||
public long[]? ReceivedLongArray { get; private set; }
|
||||
|
||||
public bool DecimalArrayMethodCalled { get; private set; }
|
||||
public decimal[]? ReceivedDecimalArray { get; private set; }
|
||||
|
||||
public bool DateTimeArrayMethodCalled { get; private set; }
|
||||
public DateTime[]? ReceivedDateTimeArray { get; private set; }
|
||||
|
||||
public bool EnumArrayMethodCalled { get; private set; }
|
||||
public TestStatus[]? ReceivedEnumArray { get; private set; }
|
||||
|
||||
public bool DoubleArrayMethodCalled { get; private set; }
|
||||
public double[]? ReceivedDoubleArray { get; private set; }
|
||||
|
||||
public bool SharedTagArrayMethodCalled { get; private set; }
|
||||
public SharedTag[]? ReceivedSharedTagArray { get; private set; }
|
||||
|
||||
public bool DictionaryMethodCalled { get; private set; }
|
||||
public Dictionary<string, int>? ReceivedDictionary { get; private set; }
|
||||
|
||||
public bool ObjectArrayMethodCalled { get; private set; }
|
||||
public object[]? ReceivedObjectArray { get; private set; }
|
||||
|
||||
// Mixed parameter captures
|
||||
public bool IntAndDtoMethodCalled { get; private set; }
|
||||
public (int, TestOrderItem?)? ReceivedIntAndDto { get; private set; }
|
||||
|
||||
public bool DtoAndListMethodCalled { get; private set; }
|
||||
public (TestOrderItem?, List<int>?)? ReceivedDtoAndList { get; private set; }
|
||||
|
||||
public bool ThreeComplexParamsMethodCalled { get; private set; }
|
||||
public (TestOrderItem?, List<string>?, SharedTag?)? ReceivedThreeComplexParams { get; private set; }
|
||||
|
||||
public bool FiveParamsMethodCalled { get; private set; }
|
||||
public (int, string?, bool, Guid, decimal)? ReceivedFiveParams { get; private set; }
|
||||
|
||||
#endregion
|
||||
|
||||
#region Primitive Parameter Handlers
|
||||
|
||||
[SignalR(TestSignalRTags.SingleIntParam)]
|
||||
public string HandleSingleInt(int value)
|
||||
{
|
||||
SingleIntMethodCalled = true;
|
||||
ReceivedInt = value;
|
||||
return $"Received: {value}";
|
||||
}
|
||||
|
||||
[SignalR(TestSignalRTags.TwoIntParams)]
|
||||
public int HandleTwoInts(int a, int b)
|
||||
{
|
||||
TwoIntMethodCalled = true;
|
||||
ReceivedTwoInts = (a, b);
|
||||
return a + b;
|
||||
}
|
||||
|
||||
[SignalR(TestSignalRTags.BoolParam)]
|
||||
public bool HandleBool(bool loadRelations)
|
||||
{
|
||||
BoolMethodCalled = true;
|
||||
ReceivedBool = loadRelations;
|
||||
return loadRelations;
|
||||
}
|
||||
|
||||
[SignalR(TestSignalRTags.StringParam)]
|
||||
public string HandleString(string text)
|
||||
{
|
||||
StringMethodCalled = true;
|
||||
ReceivedString = text;
|
||||
return $"Echo: {text}";
|
||||
}
|
||||
|
||||
[SignalR(TestSignalRTags.GuidParam)]
|
||||
public Guid HandleGuid(Guid id)
|
||||
{
|
||||
GuidMethodCalled = true;
|
||||
ReceivedGuid = id;
|
||||
return id;
|
||||
}
|
||||
|
||||
[SignalR(TestSignalRTags.EnumParam)]
|
||||
public TestStatus HandleEnum(TestStatus status)
|
||||
{
|
||||
EnumMethodCalled = true;
|
||||
ReceivedEnum = status;
|
||||
return status;
|
||||
}
|
||||
|
||||
[SignalR(TestSignalRTags.NoParams)]
|
||||
public string HandleNoParams()
|
||||
{
|
||||
NoParamsMethodCalled = true;
|
||||
return "OK";
|
||||
}
|
||||
|
||||
[SignalR(TestSignalRTags.MultipleTypesParams)]
|
||||
public string HandleMultipleTypes(bool flag, string text, int number)
|
||||
{
|
||||
MultipleTypesMethodCalled = true;
|
||||
ReceivedMultipleTypes = (flag, text, number);
|
||||
return $"{flag}-{text}-{number}";
|
||||
}
|
||||
|
||||
[SignalR(TestSignalRTags.ThrowsException)]
|
||||
public void HandleThrowsException()
|
||||
{
|
||||
throw new InvalidOperationException("Test exception");
|
||||
}
|
||||
|
||||
[SignalR(TestSignalRTags.DecimalParam)]
|
||||
public decimal HandleDecimal(decimal value)
|
||||
{
|
||||
DecimalMethodCalled = true;
|
||||
ReceivedDecimal = value;
|
||||
return value * 2;
|
||||
}
|
||||
|
||||
[SignalR(TestSignalRTags.DateTimeParam)]
|
||||
public DateTime HandleDateTime(DateTime dateTime)
|
||||
{
|
||||
DateTimeMethodCalled = true;
|
||||
ReceivedDateTime = dateTime;
|
||||
return dateTime;
|
||||
}
|
||||
|
||||
[SignalR(TestSignalRTags.DoubleParam)]
|
||||
public double HandleDouble(double value)
|
||||
{
|
||||
DoubleMethodCalled = true;
|
||||
ReceivedDouble = value;
|
||||
return value;
|
||||
}
|
||||
|
||||
[SignalR(TestSignalRTags.LongParam)]
|
||||
public long HandleLong(long value)
|
||||
{
|
||||
LongMethodCalled = true;
|
||||
ReceivedLong = value;
|
||||
return value;
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Complex Object Handlers (using shared DTOs)
|
||||
|
||||
[SignalR(TestSignalRTags.TestOrderItemParam)]
|
||||
public TestOrderItem HandleTestOrderItem(TestOrderItem item)
|
||||
{
|
||||
TestOrderItemMethodCalled = true;
|
||||
ReceivedTestOrderItem = item;
|
||||
return new TestOrderItem
|
||||
{
|
||||
Id = item.Id,
|
||||
ProductName = $"Processed: {item.ProductName}",
|
||||
Quantity = item.Quantity * 2,
|
||||
UnitPrice = item.UnitPrice
|
||||
};
|
||||
}
|
||||
|
||||
[SignalR(TestSignalRTags.TestOrderParam)]
|
||||
public TestOrder HandleTestOrder(TestOrder order)
|
||||
{
|
||||
TestOrderMethodCalled = true;
|
||||
ReceivedTestOrder = order;
|
||||
return order;
|
||||
}
|
||||
|
||||
[SignalR(TestSignalRTags.SharedTagParam)]
|
||||
public SharedTag HandleSharedTag(SharedTag tag)
|
||||
{
|
||||
SharedTagMethodCalled = true;
|
||||
ReceivedSharedTag = tag;
|
||||
return tag;
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Collection Parameter Handlers
|
||||
|
||||
[SignalR(TestSignalRTags.IntArrayParam)]
|
||||
public int[] HandleIntArray(int[] values)
|
||||
{
|
||||
IntArrayMethodCalled = true;
|
||||
ReceivedIntArray = values;
|
||||
return values.Select(x => x * 2).ToArray();
|
||||
}
|
||||
|
||||
[SignalR(TestSignalRTags.GuidArrayParam)]
|
||||
public Guid[] HandleGuidArray(Guid[] ids)
|
||||
{
|
||||
GuidArrayMethodCalled = true;
|
||||
ReceivedGuidArray = ids;
|
||||
return ids;
|
||||
}
|
||||
|
||||
[SignalR(TestSignalRTags.StringListParam)]
|
||||
public List<string> HandleStringList(List<string> items)
|
||||
{
|
||||
StringListMethodCalled = true;
|
||||
ReceivedStringList = items;
|
||||
return items.Select(x => x.ToUpper()).ToList();
|
||||
}
|
||||
|
||||
[SignalR(TestSignalRTags.TestOrderItemListParam)]
|
||||
public List<TestOrderItem> HandleTestOrderItemList(List<TestOrderItem> items)
|
||||
{
|
||||
TestOrderItemListMethodCalled = true;
|
||||
ReceivedTestOrderItemList = items;
|
||||
return items;
|
||||
}
|
||||
|
||||
[SignalR(TestSignalRTags.IntListParam)]
|
||||
public List<int> HandleIntList(List<int> numbers)
|
||||
{
|
||||
IntListMethodCalled = true;
|
||||
ReceivedIntList = numbers;
|
||||
return numbers.Select(x => x * 2).ToList();
|
||||
}
|
||||
|
||||
[SignalR(TestSignalRTags.BoolArrayParam)]
|
||||
public bool[] HandleBoolArray(bool[] flags)
|
||||
{
|
||||
BoolArrayMethodCalled = true;
|
||||
ReceivedBoolArray = flags;
|
||||
return flags;
|
||||
}
|
||||
|
||||
[SignalR(TestSignalRTags.MixedWithArrayParam)]
|
||||
public string HandleMixedWithArray(bool flag, int[] numbers, string text)
|
||||
{
|
||||
MixedWithArrayMethodCalled = true;
|
||||
ReceivedMixedWithArray = (flag, numbers, text);
|
||||
return $"{flag}-[{string.Join(",", numbers)}]-{text}";
|
||||
}
|
||||
|
||||
[SignalR(TestSignalRTags.NestedListParam)]
|
||||
public List<List<int>> HandleNestedList(List<List<int>> nestedList)
|
||||
{
|
||||
NestedListMethodCalled = true;
|
||||
ReceivedNestedList = nestedList;
|
||||
return nestedList;
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Extended Array Parameter Handlers
|
||||
|
||||
[SignalR(TestSignalRTags.LongArrayParam)]
|
||||
public long[] HandleLongArray(long[] values)
|
||||
{
|
||||
LongArrayMethodCalled = true;
|
||||
ReceivedLongArray = values;
|
||||
return values;
|
||||
}
|
||||
|
||||
[SignalR(TestSignalRTags.DecimalArrayParam)]
|
||||
public decimal[] HandleDecimalArray(decimal[] values)
|
||||
{
|
||||
DecimalArrayMethodCalled = true;
|
||||
ReceivedDecimalArray = values;
|
||||
return values;
|
||||
}
|
||||
|
||||
[SignalR(TestSignalRTags.DateTimeArrayParam)]
|
||||
public DateTime[] HandleDateTimeArray(DateTime[] values)
|
||||
{
|
||||
DateTimeArrayMethodCalled = true;
|
||||
ReceivedDateTimeArray = values;
|
||||
return values;
|
||||
}
|
||||
|
||||
[SignalR(TestSignalRTags.EnumArrayParam)]
|
||||
public TestStatus[] HandleEnumArray(TestStatus[] values)
|
||||
{
|
||||
EnumArrayMethodCalled = true;
|
||||
ReceivedEnumArray = values;
|
||||
return values;
|
||||
}
|
||||
|
||||
[SignalR(TestSignalRTags.DoubleArrayParam)]
|
||||
public double[] HandleDoubleArray(double[] values)
|
||||
{
|
||||
DoubleArrayMethodCalled = true;
|
||||
ReceivedDoubleArray = values;
|
||||
return values;
|
||||
}
|
||||
|
||||
[SignalR(TestSignalRTags.SharedTagArrayParam)]
|
||||
public SharedTag[] HandleSharedTagArray(SharedTag[] tags)
|
||||
{
|
||||
SharedTagArrayMethodCalled = true;
|
||||
ReceivedSharedTagArray = tags;
|
||||
return tags;
|
||||
}
|
||||
|
||||
[SignalR(TestSignalRTags.DictionaryParam)]
|
||||
public Dictionary<string, int> HandleDictionary(Dictionary<string, int> dict)
|
||||
{
|
||||
DictionaryMethodCalled = true;
|
||||
ReceivedDictionary = dict;
|
||||
return dict;
|
||||
}
|
||||
|
||||
[SignalR(TestSignalRTags.ObjectArrayParam)]
|
||||
public object[] HandleObjectArray(object[] values)
|
||||
{
|
||||
ObjectArrayMethodCalled = true;
|
||||
ReceivedObjectArray = values;
|
||||
return values;
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Mixed Parameter Handlers
|
||||
|
||||
[SignalR(TestSignalRTags.IntAndDtoParam)]
|
||||
public string HandleIntAndDto(int id, TestOrderItem item)
|
||||
{
|
||||
IntAndDtoMethodCalled = true;
|
||||
ReceivedIntAndDto = (id, item);
|
||||
return $"{id}-{item?.ProductName}";
|
||||
}
|
||||
|
||||
[SignalR(TestSignalRTags.DtoAndListParam)]
|
||||
public string HandleDtoAndList(TestOrderItem item, List<int> numbers)
|
||||
{
|
||||
DtoAndListMethodCalled = true;
|
||||
ReceivedDtoAndList = (item, numbers);
|
||||
return $"{item?.ProductName}-[{string.Join(",", numbers ?? [])}]";
|
||||
}
|
||||
|
||||
[SignalR(TestSignalRTags.ThreeComplexParams)]
|
||||
public string HandleThreeComplexParams(TestOrderItem item, List<string> tags, SharedTag sharedTag)
|
||||
{
|
||||
ThreeComplexParamsMethodCalled = true;
|
||||
ReceivedThreeComplexParams = (item, tags, sharedTag);
|
||||
return $"{item?.ProductName}-{tags?.Count}-{sharedTag?.Name}";
|
||||
}
|
||||
|
||||
[SignalR(TestSignalRTags.FiveParams)]
|
||||
public string HandleFiveParams(int a, string b, bool c, Guid d, decimal e)
|
||||
{
|
||||
FiveParamsMethodCalled = true;
|
||||
ReceivedFiveParams = (a, b, c, d, e);
|
||||
return $"{a}-{b}-{c}-{d}-{e}";
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
public void Reset()
|
||||
{
|
||||
// Primitive captures
|
||||
SingleIntMethodCalled = false;
|
||||
ReceivedInt = null;
|
||||
TwoIntMethodCalled = false;
|
||||
ReceivedTwoInts = null;
|
||||
BoolMethodCalled = false;
|
||||
ReceivedBool = null;
|
||||
StringMethodCalled = false;
|
||||
ReceivedString = null;
|
||||
GuidMethodCalled = false;
|
||||
ReceivedGuid = null;
|
||||
EnumMethodCalled = false;
|
||||
ReceivedEnum = null;
|
||||
NoParamsMethodCalled = false;
|
||||
MultipleTypesMethodCalled = false;
|
||||
ReceivedMultipleTypes = null;
|
||||
DecimalMethodCalled = false;
|
||||
ReceivedDecimal = null;
|
||||
DateTimeMethodCalled = false;
|
||||
ReceivedDateTime = null;
|
||||
DoubleMethodCalled = false;
|
||||
ReceivedDouble = null;
|
||||
LongMethodCalled = false;
|
||||
ReceivedLong = null;
|
||||
|
||||
// Complex object captures
|
||||
TestOrderItemMethodCalled = false;
|
||||
ReceivedTestOrderItem = null;
|
||||
TestOrderMethodCalled = false;
|
||||
ReceivedTestOrder = null;
|
||||
SharedTagMethodCalled = false;
|
||||
ReceivedSharedTag = null;
|
||||
|
||||
// Collection captures
|
||||
IntArrayMethodCalled = false;
|
||||
ReceivedIntArray = null;
|
||||
GuidArrayMethodCalled = false;
|
||||
ReceivedGuidArray = null;
|
||||
StringListMethodCalled = false;
|
||||
ReceivedStringList = null;
|
||||
TestOrderItemListMethodCalled = false;
|
||||
ReceivedTestOrderItemList = null;
|
||||
IntListMethodCalled = false;
|
||||
ReceivedIntList = null;
|
||||
BoolArrayMethodCalled = false;
|
||||
ReceivedBoolArray = null;
|
||||
MixedWithArrayMethodCalled = false;
|
||||
ReceivedMixedWithArray = null;
|
||||
NestedListMethodCalled = false;
|
||||
ReceivedNestedList = null;
|
||||
|
||||
// Extended array captures
|
||||
LongArrayMethodCalled = false;
|
||||
ReceivedLongArray = null;
|
||||
DecimalArrayMethodCalled = false;
|
||||
ReceivedDecimalArray = null;
|
||||
DateTimeArrayMethodCalled = false;
|
||||
ReceivedDateTimeArray = null;
|
||||
EnumArrayMethodCalled = false;
|
||||
ReceivedEnumArray = null;
|
||||
DoubleArrayMethodCalled = false;
|
||||
ReceivedDoubleArray = null;
|
||||
SharedTagArrayMethodCalled = false;
|
||||
ReceivedSharedTagArray = null;
|
||||
DictionaryMethodCalled = false;
|
||||
ReceivedDictionary = null;
|
||||
ObjectArrayMethodCalled = false;
|
||||
ReceivedObjectArray = null;
|
||||
|
||||
// Mixed parameter captures
|
||||
IntAndDtoMethodCalled = false;
|
||||
ReceivedIntAndDto = null;
|
||||
DtoAndListMethodCalled = false;
|
||||
ReceivedDtoAndList = null;
|
||||
ThreeComplexParamsMethodCalled = false;
|
||||
ReceivedThreeComplexParams = null;
|
||||
FiveParamsMethodCalled = false;
|
||||
ReceivedFiveParams = null;
|
||||
}
|
||||
}
|
||||
|
|
@ -428,4 +428,44 @@ public class TestSignalRService2
|
|||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region DataSource CRUD Tests
|
||||
|
||||
private readonly List<TestOrderItem> _dataSourceItems =
|
||||
[
|
||||
new() { Id = 1, ProductName = "Product A", Quantity = 10, UnitPrice = 100m },
|
||||
new() { Id = 2, ProductName = "Product B", Quantity = 20, UnitPrice = 200m },
|
||||
new() { Id = 3, ProductName = "Product C", Quantity = 30, UnitPrice = 300m }
|
||||
];
|
||||
|
||||
[SignalR(TestSignalRTags.DataSourceGetAll)]
|
||||
public List<TestOrderItem> DataSourceGetAll() => _dataSourceItems.ToList();
|
||||
|
||||
[SignalR(TestSignalRTags.DataSourceGetItem)]
|
||||
public TestOrderItem? DataSourceGetItem(int id) => _dataSourceItems.FirstOrDefault(x => x.Id == id);
|
||||
|
||||
[SignalR(TestSignalRTags.DataSourceAdd)]
|
||||
public TestOrderItem DataSourceAdd(TestOrderItem item)
|
||||
{
|
||||
_dataSourceItems.Add(item);
|
||||
return item;
|
||||
}
|
||||
|
||||
[SignalR(TestSignalRTags.DataSourceUpdate)]
|
||||
public TestOrderItem DataSourceUpdate(TestOrderItem item)
|
||||
{
|
||||
var index = _dataSourceItems.FindIndex(x => x.Id == item.Id);
|
||||
if (index >= 0) _dataSourceItems[index] = item;
|
||||
return item;
|
||||
}
|
||||
|
||||
[SignalR(TestSignalRTags.DataSourceRemove)]
|
||||
public TestOrderItem? DataSourceRemove(TestOrderItem item)
|
||||
{
|
||||
var existing = _dataSourceItems.FirstOrDefault(x => x.Id == item.Id);
|
||||
if (existing != null) _dataSourceItems.Remove(existing);
|
||||
return existing;
|
||||
}
|
||||
|
||||
#endregion
|
||||
}
|
||||
|
|
|
|||
|
|
@ -81,4 +81,11 @@ public abstract class TestSignalRTags : AcSignalRTags
|
|||
public const int PropertyMismatchListParam = 241;
|
||||
public const int PropertyMismatchNestedParam = 242;
|
||||
public const int PropertyMismatchNestedListParam = 243;
|
||||
|
||||
// DataSource CRUD tags
|
||||
public const int DataSourceGetAll = 300;
|
||||
public const int DataSourceGetItem = 301;
|
||||
public const int DataSourceAdd = 302;
|
||||
public const int DataSourceUpdate = 303;
|
||||
public const int DataSourceRemove = 304;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,221 +0,0 @@
|
|||
using System.Security.Claims;
|
||||
using AyCode.Core.Extensions;
|
||||
using AyCode.Core.Tests.TestModels;
|
||||
using AyCode.Models.Server.DynamicMethods;
|
||||
using AyCode.Services.Server.SignalRs;
|
||||
using AyCode.Services.SignalRs;
|
||||
using Microsoft.Extensions.Configuration;
|
||||
|
||||
namespace AyCode.Services.Server.Tests.SignalRs;
|
||||
|
||||
/// <summary>
|
||||
/// Testable SignalR hub that overrides infrastructure dependencies.
|
||||
/// Enables unit testing without SignalR server or mocks.
|
||||
/// </summary>
|
||||
public class TestableSignalRHub : AcWebSignalRHubBase<TestSignalRTags, TestLogger>
|
||||
{
|
||||
#region Captured Data for Assertions
|
||||
|
||||
/// <summary>
|
||||
/// Messages sent via ResponseToCaller or SendMessageToClient
|
||||
/// </summary>
|
||||
public List<SentMessage> SentMessages { get; } = [];
|
||||
|
||||
/// <summary>
|
||||
/// Whether notFoundCallback was invoked
|
||||
/// </summary>
|
||||
public bool WasNotFoundCallbackInvoked { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// The tag name passed to notFoundCallback
|
||||
/// </summary>
|
||||
public string? NotFoundTagName { get; private set; }
|
||||
|
||||
#endregion
|
||||
|
||||
#region Test Configuration
|
||||
|
||||
/// <summary>
|
||||
/// Simulated connection ID
|
||||
/// </summary>
|
||||
public string TestConnectionId { get; set; } = "test-connection-id";
|
||||
|
||||
/// <summary>
|
||||
/// Simulated user identifier
|
||||
/// </summary>
|
||||
public string? TestUserIdentifier { get; set; } = "test-user-id";
|
||||
|
||||
/// <summary>
|
||||
/// Simulated connection aborted state
|
||||
/// </summary>
|
||||
public bool TestIsConnectionAborted { get; set; } = false;
|
||||
|
||||
/// <summary>
|
||||
/// Simulated ClaimsPrincipal (optional)
|
||||
/// </summary>
|
||||
public ClaimsPrincipal? TestUser { get; set; }
|
||||
|
||||
#endregion
|
||||
|
||||
public TestableSignalRHub()
|
||||
: base(new ConfigurationBuilder().Build(), new TestLogger())
|
||||
{
|
||||
}
|
||||
|
||||
public TestableSignalRHub(IConfiguration configuration, TestLogger logger)
|
||||
: base(configuration, logger)
|
||||
{
|
||||
}
|
||||
|
||||
#region Public Test Entry Points
|
||||
|
||||
/// <summary>
|
||||
/// Sets the serializer type for testing (JSON or Binary).
|
||||
/// </summary>
|
||||
public void SetSerializerType(AcSerializerType serializerType)
|
||||
{
|
||||
SerializerOptions = serializerType == AcSerializerType.Binary
|
||||
? new AcBinarySerializerOptions()
|
||||
: new AcJsonSerializerOptions();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Register a service with SignalR-attributed methods
|
||||
/// </summary>
|
||||
public void RegisterService(object service)
|
||||
{
|
||||
DynamicMethodCallModels.Add(new AcDynamicMethodCallModel<SignalRAttribute>(service));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Invoke ProcessOnReceiveMessage for testing
|
||||
/// </summary>
|
||||
public Task InvokeProcessOnReceiveMessage(int messageTag, byte[]? message, int? requestId = null)
|
||||
{
|
||||
return ProcessOnReceiveMessage(messageTag, message, requestId, async tagName =>
|
||||
{
|
||||
WasNotFoundCallbackInvoked = true;
|
||||
NotFoundTagName = tagName;
|
||||
await Task.CompletedTask;
|
||||
});
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Get the logger for assertions
|
||||
/// </summary>
|
||||
public new TestLogger Logger => base.Logger;
|
||||
|
||||
/// <summary>
|
||||
/// Reset captured state for next test
|
||||
/// </summary>
|
||||
public void Reset()
|
||||
{
|
||||
SentMessages.Clear();
|
||||
WasNotFoundCallbackInvoked = false;
|
||||
NotFoundTagName = null;
|
||||
Logger.Clear();
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Overridden Context Accessors
|
||||
|
||||
protected override string GetConnectionId() => TestConnectionId;
|
||||
|
||||
protected override bool IsConnectionAborted() => TestIsConnectionAborted;
|
||||
|
||||
protected override string? GetUserIdentifier() => TestUserIdentifier;
|
||||
|
||||
protected override ClaimsPrincipal? GetUser() => TestUser;
|
||||
|
||||
#endregion
|
||||
|
||||
#region Overridden Response Methods (capture messages for testing)
|
||||
|
||||
protected override Task ResponseToCaller(int messageTag, ISignalRMessage message, int? requestId)
|
||||
{
|
||||
SentMessages.Add(new SentMessage(
|
||||
MessageTag: messageTag,
|
||||
Message: message,
|
||||
RequestId: requestId,
|
||||
Target: SendTarget.Caller
|
||||
));
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
protected override Task SendMessageToClient(IAcSignalRHubItemServer sendTo, int messageTag, ISignalRMessage message, int? requestId = null)
|
||||
{
|
||||
SentMessages.Add(new SentMessage(
|
||||
MessageTag: messageTag,
|
||||
Message: message,
|
||||
RequestId: requestId,
|
||||
Target: SendTarget.Client
|
||||
));
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
protected override Task SendMessageToOthers(int messageTag, object? content)
|
||||
{
|
||||
SentMessages.Add(new SentMessage(
|
||||
MessageTag: messageTag,
|
||||
Message: new SignalResponseJsonMessage(messageTag, SignalResponseStatus.Success, content),
|
||||
RequestId: null,
|
||||
Target: SendTarget.Others
|
||||
));
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
protected override Task SendMessageToAll(int messageTag, object? content)
|
||||
{
|
||||
SentMessages.Add(new SentMessage(
|
||||
MessageTag: messageTag,
|
||||
Message: new SignalResponseJsonMessage(messageTag, SignalResponseStatus.Success, content),
|
||||
RequestId: null,
|
||||
Target: SendTarget.All
|
||||
));
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
protected override Task SendMessageToUserIdInternal(string userId, int messageTag, ISignalRMessage message, int? requestId)
|
||||
{
|
||||
SentMessages.Add(new SentMessage(
|
||||
MessageTag: messageTag,
|
||||
Message: message,
|
||||
RequestId: requestId,
|
||||
Target: SendTarget.User,
|
||||
TargetId: userId
|
||||
));
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
#endregion
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Captured sent message for assertions
|
||||
/// </summary>
|
||||
public record SentMessage(
|
||||
int MessageTag,
|
||||
ISignalRMessage Message,
|
||||
int? RequestId,
|
||||
SendTarget Target,
|
||||
string? TargetId = null)
|
||||
{
|
||||
/// <summary>
|
||||
/// Get the response as SignalResponseJsonMessage for inspection
|
||||
/// </summary>
|
||||
public SignalResponseJsonMessage? AsJsonResponse => Message as SignalResponseJsonMessage;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Target of the sent message
|
||||
/// </summary>
|
||||
public enum SendTarget
|
||||
{
|
||||
Caller,
|
||||
Client,
|
||||
Others,
|
||||
All,
|
||||
User,
|
||||
Group
|
||||
}
|
||||
|
|
@ -275,7 +275,8 @@ namespace AyCode.Services.Server.SignalRs
|
|||
}
|
||||
|
||||
/// <summary>
|
||||
/// GetAllMessageTag - Async callback version with optimized JSON handling
|
||||
/// GetAllMessageTag - Async callback version with optimized direct populate.
|
||||
/// Uses SignalResponseDataMessage to avoid double deserialization.
|
||||
/// </summary>
|
||||
public Task LoadDataSourceAsync(bool clearChangeTracking = true)
|
||||
{
|
||||
|
|
@ -283,44 +284,80 @@ namespace AyCode.Services.Server.SignalRs
|
|||
throw new ArgumentException($"SignalRCrudTags.GetAllMessageTag == SignalRTags.None");
|
||||
|
||||
BeginSync();
|
||||
return SignalRClient.GetAllAsync<TIList>(SignalRCrudTags.GetAllMessageTag, async result =>
|
||||
{
|
||||
try
|
||||
// Request SignalResponseDataMessage directly to avoid deserializing ResponseData
|
||||
return SignalRClient.GetAllAsync<SignalResponseDataMessage>(SignalRCrudTags.GetAllMessageTag, GetContextParams())
|
||||
.ContinueWith(async task =>
|
||||
{
|
||||
if (result.Status != SignalResponseStatus.Success || string.IsNullOrEmpty(result.ResponseDataJson))
|
||||
throw new NullReferenceException($"LoadDataSourceAsync; Status: {result.Status}");
|
||||
try
|
||||
{
|
||||
var response = task.Result;
|
||||
if (response?.Status != SignalResponseStatus.Success || response.ResponseDataBin == null)
|
||||
throw new NullReferenceException($"LoadDataSourceAsync; Status: {response?.Status}");
|
||||
|
||||
await LoadDataSourceFromJson(result.ResponseDataJson, false, false, clearChangeTracking);
|
||||
}
|
||||
finally
|
||||
{
|
||||
EndSync();
|
||||
}
|
||||
}, GetContextParams());
|
||||
await LoadDataSourceFromResponseData(response.ResponseDataBin, response.DataSerializerType,
|
||||
false, false, clearChangeTracking);
|
||||
}
|
||||
finally
|
||||
{
|
||||
EndSync();
|
||||
}
|
||||
}).Unwrap();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Loads data source directly from JSON string, avoiding double deserialization.
|
||||
/// Loads data source directly from ResponseData byte[], avoiding double deserialization.
|
||||
/// </summary>
|
||||
public async Task LoadDataSourceFromJson(string json, bool refreshDataFromDbAsync = false, bool setSourceToWorkingReferenceList = false, bool clearChangeTracking = true)
|
||||
public async Task LoadDataSourceFromResponseData(byte[] responseData, AcSerializerType serializerType,
|
||||
bool refreshDataFromDbAsync = false, bool setSourceToWorkingReferenceList = false, bool clearChangeTracking = true)
|
||||
{
|
||||
await _asyncLock.WaitAsync();
|
||||
try
|
||||
{
|
||||
if (!setSourceToWorkingReferenceList)
|
||||
{
|
||||
if (InnerList is IAcObservableCollection observable)
|
||||
// Direct populate into existing InnerList
|
||||
if (serializerType == AcSerializerType.Binary)
|
||||
{
|
||||
observable.PopulateFromJson(json);
|
||||
if (InnerList is IAcObservableCollection observable)
|
||||
{
|
||||
observable.BeginUpdate();
|
||||
try
|
||||
{
|
||||
responseData.BinaryToMerge(InnerList);
|
||||
}
|
||||
finally
|
||||
{
|
||||
observable.EndUpdate();
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
responseData.BinaryTo(InnerList);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
json.JsonTo(InnerList);
|
||||
// JSON mode
|
||||
var json = System.Text.Encoding.UTF8.GetString(responseData);
|
||||
if (InnerList is IAcObservableCollection observable)
|
||||
{
|
||||
observable.PopulateFromJson(json);
|
||||
}
|
||||
else
|
||||
{
|
||||
json.JsonTo(InnerList);
|
||||
}
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
var fromSource = json.JsonTo<TIList>();
|
||||
// Deserialize to new list and set as reference
|
||||
TIList? fromSource;
|
||||
if (serializerType == AcSerializerType.Binary)
|
||||
fromSource = responseData.BinaryTo<TIList>();
|
||||
else
|
||||
fromSource = System.Text.Encoding.UTF8.GetString(responseData).JsonTo<TIList>();
|
||||
|
||||
if (fromSource != null)
|
||||
{
|
||||
ClearUnsafe(clearChangeTracking);
|
||||
|
|
@ -891,7 +928,7 @@ namespace AyCode.Services.Server.SignalRs
|
|||
private Task<TDataItem> SaveTrackingItemUnsafe(TrackingItem<TDataItem, TId> trackingItem)
|
||||
=> SaveItemUnsafe(trackingItem.CurrentValue, trackingItem.TrackingState);
|
||||
|
||||
private Task SaveTrackingItemUnsafeAsync(TrackingItem<TDataItem, TId> trackingItem)
|
||||
private async Task SaveTrackingItemUnsafeAsync(TrackingItem<TDataItem, TId> trackingItem)
|
||||
=> SaveItemUnsafeAsync(trackingItem.CurrentValue, trackingItem.TrackingState);
|
||||
|
||||
private Task<TDataItem> SaveItemUnsafe(TDataItem item, TrackingState trackingState)
|
||||
|
|
@ -900,19 +937,22 @@ namespace AyCode.Services.Server.SignalRs
|
|||
if (messageTag == AcSignalRTags.None)
|
||||
throw new ArgumentException($"SaveItemUnsafe; messageTag == SignalRTags.None");
|
||||
|
||||
return SignalRClient.PostDataAsync(messageTag, item).ContinueWith(x =>
|
||||
return SignalRClient.PostDataAsync(messageTag, item).ContinueWith(task =>
|
||||
{
|
||||
if (x.Result == null)
|
||||
if (task.Result == null)
|
||||
{
|
||||
if (TryRollbackItem(item.Id, out _)) return item;
|
||||
throw new NullReferenceException($"SaveItemUnsafe; result == null");
|
||||
}
|
||||
|
||||
ProcessSavedResponseItem(x.Result, trackingState, item.Id);
|
||||
return x.Result;
|
||||
});
|
||||
ProcessSavedResponseItem(task.Result, trackingState, item.Id);
|
||||
return task.Result;
|
||||
}, TaskScheduler.Default);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Saves item in background (fire-and-forget friendly). Does not block UI.
|
||||
/// </summary>
|
||||
private Task SaveItemUnsafeAsync(TDataItem item, TrackingState trackingState)
|
||||
{
|
||||
var messageTag = SignalRCrudTags.GetMessageTagByTrackingState(trackingState);
|
||||
|
|
@ -920,14 +960,14 @@ namespace AyCode.Services.Server.SignalRs
|
|||
|
||||
return SignalRClient.PostDataAsync(messageTag, item, response =>
|
||||
{
|
||||
//response.ResponseDataJson
|
||||
if (response.Status != SignalResponseStatus.Success || response.ResponseData == null)
|
||||
if (response.Status != SignalResponseStatus.Success || response.ResponseDataBin == null)
|
||||
{
|
||||
if (TryRollbackItem(item.Id, out _)) return Task.CompletedTask;
|
||||
if (TryRollbackItem(item.Id, out _)) return;
|
||||
throw new NullReferenceException($"SaveItemUnsafeAsync; Status: {response.Status}");
|
||||
}
|
||||
|
||||
return ProcessSavedResponseItem(response.ResponseData, trackingState, item.Id);
|
||||
var resultItem = response.GetResponseData<TDataItem>();
|
||||
ProcessSavedResponseItem(resultItem, trackingState, item.Id);
|
||||
});
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,57 +1,37 @@
|
|||
using AyCode.Core.Extensions;
|
||||
using AyCode.Core.Helpers;
|
||||
using AyCode.Core.Loggers;
|
||||
using AyCode.Services.Loggers;
|
||||
using AyCode.Services.SignalRs;
|
||||
using MessagePack.Resolvers;
|
||||
using Microsoft.AspNetCore.SignalR;
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
||||
namespace AyCode.Services.Server.SignalRs;
|
||||
|
||||
public abstract class AcSignalRSendToClientService<TSignalRHub, TSignalRTags, TLogger>(IHubContext<TSignalRHub, IAcSignalRHubItemServer> signalRHub, IAcLoggerBase logger) //: IAcSignalRHubServer
|
||||
public abstract class AcSignalRSendToClientService<TSignalRHub, TSignalRTags, TLogger>(IHubContext<TSignalRHub, IAcSignalRHubItemServer> signalRHub, IAcLoggerBase logger)
|
||||
where TSignalRHub: Hub<IAcSignalRHubItemServer>, IAcSignalRHubServer where TSignalRTags : AcSignalRTags where TLogger : IAcLoggerBase
|
||||
{
|
||||
protected IAcLoggerBase Logger => logger;
|
||||
|
||||
protected virtual async Task SendMessageToClient(IAcSignalRHubItemServer sendTo, int messageTag, object? content)
|
||||
{
|
||||
var jsonContent = new SignalResponseJsonMessage(messageTag, SignalResponseStatus.Success, content);
|
||||
await SendMessageToClient(sendTo, messageTag, jsonContent, null);
|
||||
var response = new SignalResponseDataMessage(messageTag, SignalResponseStatus.Success, content, AcJsonSerializerOptions.Default);
|
||||
var responseBytes = response.ToBinary();
|
||||
|
||||
Logger.Info($"[{responseBytes.Length / 1024}kb] Server sending to client; {ConstHelper.NameByValue<TSignalRTags>(messageTag)}");
|
||||
await sendTo.OnReceiveMessage(messageTag, responseBytes, null);
|
||||
}
|
||||
|
||||
protected virtual async Task SendMessageToClient(IAcSignalRHubItemServer sendTo, int messageTag, ISignalRMessage message, int? requestId = null)
|
||||
{
|
||||
var sendingDataMessagePack = message.ToMessagePack(ContractlessStandardResolver.Options);
|
||||
public virtual Task SendMessageToAllClients(int messageTag, object? content)
|
||||
=> SendMessageToClient(signalRHub.Clients.All, messageTag, content);
|
||||
|
||||
Logger.Info($"[{(sendingDataMessagePack.Length/1024)}kb] Server sending dataMessagePack to client; {nameof(requestId)}: {requestId}; {ConstHelper.NameByValue<TSignalRTags>(messageTag)}");
|
||||
//Logger.Info($"[{(responseDataMessagePack.Length/1024)}kb] Server sending dataMessagePack to client; {nameof(requestId)}: {requestId}; ConnectionId: {signalRHub.ConnectionId}; {ConstHelper.NameByValue<TSignalRTags>(messageTag)}");
|
||||
public virtual Task SendMessageToConnection(string connectionId, int messageTag, object? content)
|
||||
=> SendMessageToClient(signalRHub.Clients.Client(connectionId), messageTag, content);
|
||||
|
||||
await sendTo.OnReceiveMessage(messageTag, sendingDataMessagePack, requestId);
|
||||
}
|
||||
public virtual Task SendMessageToConnections(IEnumerable<string> connectionIds, int messageTag, object? content)
|
||||
=> SendMessageToClient(signalRHub.Clients.Clients(connectionIds), messageTag, content);
|
||||
|
||||
public virtual async Task SendMessageToAllClients(int messageTag, object? content)
|
||||
{
|
||||
await SendMessageToClient(signalRHub.Clients.All, messageTag, content);
|
||||
}
|
||||
public virtual Task SendMessageToUser(string user, int messageTag, object? content)
|
||||
=> SendMessageToClient(signalRHub.Clients.User(user), messageTag, content);
|
||||
|
||||
public virtual async Task SendMessageToConnection(string connectionId, int messageTag, object? content)
|
||||
{
|
||||
await SendMessageToClient(signalRHub.Clients.Client(connectionId), messageTag, content);
|
||||
}
|
||||
|
||||
public virtual async Task SendMessageToConnections(IEnumerable<string> connectionIds, int messageTag, object? content)
|
||||
{
|
||||
await SendMessageToClient(signalRHub.Clients.Clients(connectionIds), messageTag, content);
|
||||
}
|
||||
|
||||
public virtual async Task SendMessageToUser(string user, int messageTag, object? content)
|
||||
{
|
||||
await SendMessageToClient(signalRHub.Clients.User(user), messageTag, content);
|
||||
}
|
||||
|
||||
public virtual async Task SendMessageToUsers(IEnumerable<string> users, int messageTag, object? content)
|
||||
{
|
||||
await SendMessageToClient(signalRHub.Clients.Users(users), messageTag, content);
|
||||
}
|
||||
public virtual Task SendMessageToUsers(IEnumerable<string> users, int messageTag, object? content)
|
||||
=> SendMessageToClient(signalRHub.Clients.Users(users), messageTag, content);
|
||||
}
|
||||
|
|
@ -1,4 +1,5 @@
|
|||
using System.Security.Claims;
|
||||
using System.Buffers;
|
||||
using System.Security.Claims;
|
||||
using AyCode.Core;
|
||||
using AyCode.Core.Extensions;
|
||||
using AyCode.Core.Helpers;
|
||||
|
|
@ -96,27 +97,22 @@ public abstract class AcWebSignalRHubBase<TSignalRTags, TLogger>(IConfiguration
|
|||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates a response message using the configured serializer (JSON or Binary).
|
||||
/// Creates a response message using the configured serializer.
|
||||
/// Always creates SignalResponseDataMessage which includes the SerializerType.
|
||||
/// </summary>
|
||||
protected virtual ISignalRMessage CreateResponseMessage(int messageTag, SignalResponseStatus status, object? responseData)
|
||||
{
|
||||
if (SerializerOptions.SerializerType == AcSerializerType.Binary)
|
||||
{
|
||||
return new SignalResponseBinaryMessage(messageTag, status, responseData, (AcBinarySerializerOptions)SerializerOptions);
|
||||
}
|
||||
|
||||
return new SignalResponseJsonMessage(messageTag, status, responseData);
|
||||
return new SignalResponseDataMessage(messageTag, status, responseData, SerializerOptions);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the size of the response data for logging purposes.
|
||||
/// </summary>
|
||||
private int GetResponseSize(ISignalRMessage responseMessage)
|
||||
private static int GetResponseSize(ISignalRMessage responseMessage)
|
||||
{
|
||||
return responseMessage switch
|
||||
{
|
||||
SignalResponseJsonMessage jsonMsg => System.Text.Encoding.Unicode.GetByteCount(jsonMsg.ResponseData ?? ""),
|
||||
SignalResponseBinaryMessage binaryMsg => binaryMsg.ResponseData?.Length ?? 0,
|
||||
SignalResponseDataMessage dataMsg => dataMsg.ResponseDataBin?.Length ?? 0,
|
||||
_ => 0
|
||||
};
|
||||
}
|
||||
|
|
@ -246,14 +242,20 @@ public abstract class AcWebSignalRHubBase<TSignalRTags, TLogger>(IConfiguration
|
|||
protected virtual Task SendMessageToAll(int messageTag, object? content)
|
||||
=> SendMessageToClient(Clients.All, messageTag, CreateResponseMessage(messageTag, SignalResponseStatus.Success, content), null);
|
||||
|
||||
/// <summary>
|
||||
/// Sends message to client.
|
||||
/// Both Binary and JSON modes use AcBinarySerializer directly (no MessagePack wrapper).
|
||||
/// </summary>
|
||||
protected virtual async Task SendMessageToClient(IAcSignalRHubItemServer sendTo, int messageTag, ISignalRMessage message, int? requestId = null)
|
||||
{
|
||||
var responseDataMessagePack = message.ToMessagePack(ContractlessStandardResolver.Options);
|
||||
// Both modes use AcBinarySerializer - unified serialization
|
||||
var responseBytes = message.ToBinary();
|
||||
|
||||
var tagName = ConstHelper.NameByValue<TSignalRTags>(messageTag);
|
||||
|
||||
Logger.Debug($"[{responseDataMessagePack.Length / 1024}kb] Server sending message to client; requestId: {requestId}; Aborted: {IsConnectionAborted()}; ConnectionId: {GetConnectionId()}; {tagName}");
|
||||
Logger.Debug($"[{responseBytes.Length / 1024}kb] Server sending message to client; requestId: {requestId}; Aborted: {IsConnectionAborted()}; ConnectionId: {GetConnectionId()}; {tagName}");
|
||||
|
||||
await sendTo.OnReceiveMessage(messageTag, responseDataMessagePack, requestId);
|
||||
await sendTo.OnReceiveMessage(messageTag, responseBytes, requestId);
|
||||
|
||||
Logger.Debug($"Server sent message to client; requestId: {requestId}; ConnectionId: {GetConnectionId()}; {tagName}");
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,1158 +0,0 @@
|
|||
using AyCode.Core.Extensions;
|
||||
using AyCode.Core.Helpers;
|
||||
using AyCode.Core.Tests.TestModels;
|
||||
using AyCode.Services.SignalRs;
|
||||
using MessagePack.Resolvers;
|
||||
using Microsoft.AspNetCore.SignalR.Client;
|
||||
|
||||
namespace AyCode.Services.Tests.SignalRs;
|
||||
|
||||
[TestClass]
|
||||
public class AcSignalRClientBaseTests
|
||||
{
|
||||
private TestableSignalRClient _client = null!;
|
||||
private TestLogger _logger = null!;
|
||||
|
||||
[TestInitialize]
|
||||
public void Setup()
|
||||
{
|
||||
_logger = new TestLogger();
|
||||
_client = new TestableSignalRClient(_logger);
|
||||
_client.TransportSendTimeout = 100; // Short timeout for tests
|
||||
}
|
||||
|
||||
#region Connection State Tests
|
||||
|
||||
[TestMethod]
|
||||
public async Task StartConnection_WhenDisconnected_ConnectsSuccessfully()
|
||||
{
|
||||
_client.SetConnectionState(HubConnectionState.Disconnected);
|
||||
|
||||
await _client.StartConnection();
|
||||
|
||||
Assert.IsTrue(_client.GetPendingRequests().IsEmpty);
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public async Task StopConnection_ClearsState()
|
||||
{
|
||||
_client.RegisterPendingRequest(1, new SignalRRequestModel());
|
||||
|
||||
await _client.StopConnection();
|
||||
|
||||
// Connection should be stopped (no exception)
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region SendMessageToServerAsync Tests
|
||||
|
||||
[TestMethod]
|
||||
public async Task SendMessageToServerAsync_SendsMessage()
|
||||
{
|
||||
await _client.SendMessageToServerAsync(TestClientTags.Ping);
|
||||
|
||||
Assert.AreEqual(1, _client.SentMessages.Count);
|
||||
Assert.AreEqual(TestClientTags.Ping, _client.LastSentMessage?.MessageTag);
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public async Task SendMessageToServerAsync_WithMessage_SerializesCorrectly()
|
||||
{
|
||||
var idMessage = new IdMessage(42);
|
||||
var message = new SignalPostJsonDataMessage<IdMessage>(idMessage);
|
||||
|
||||
await _client.SendMessageToServerAsync(TestClientTags.GetById, message, 1);
|
||||
|
||||
Assert.AreEqual(1, _client.SentMessages.Count);
|
||||
var sent = _client.LastSentMessage;
|
||||
Assert.IsNotNull(sent?.MessageBytes);
|
||||
Assert.AreEqual(1, sent.RequestId);
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public async Task SendMessageToServerAsync_WhenDisconnected_LogsError()
|
||||
{
|
||||
_client.SetConnectionState(HubConnectionState.Disconnected);
|
||||
|
||||
await _client.SendMessageToServerAsync(TestClientTags.Ping, null, 1);
|
||||
|
||||
// Should attempt to connect first, then send
|
||||
Assert.AreEqual(1, _client.SentMessages.Count);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region GetByIdAsync Tests
|
||||
|
||||
[TestMethod]
|
||||
public async Task GetByIdAsync_SendsIdMessageWithSingleId()
|
||||
{
|
||||
_client.SetNextRequestId(100);
|
||||
|
||||
// Start the async operation but don't await the result (it will timeout)
|
||||
_ = Task.Run(() => _client.GetByIdAsync<TestOrderItem>(TestClientTags.GetById, 42));
|
||||
|
||||
// Give it time to send
|
||||
await Task.Delay(50);
|
||||
|
||||
Assert.IsTrue(_client.SentMessages.Count >= 1);
|
||||
var sent = _client.SentMessages.FirstOrDefault(m => m.MessageTag == TestClientTags.GetById);
|
||||
Assert.IsNotNull(sent);
|
||||
Assert.AreEqual(100, sent.RequestId);
|
||||
|
||||
var idMessage = sent.AsIdMessage();
|
||||
Assert.IsNotNull(idMessage);
|
||||
Assert.AreEqual(1, idMessage.Ids.Count);
|
||||
Assert.AreEqual("42", idMessage.Ids[0]); // Direct JSON format
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public async Task GetByIdAsync_WithGuid_SendsCorrectly()
|
||||
{
|
||||
var guid = Guid.Parse("12345678-1234-1234-1234-123456789abc");
|
||||
_client.SetNextRequestId(101);
|
||||
|
||||
_ = Task.Run(() => _client.GetByIdAsync<TestGuidOrder>(TestClientTags.GetById, guid));
|
||||
await Task.Delay(50);
|
||||
|
||||
var sent = _client.SentMessages.FirstOrDefault(m => m.MessageTag == TestClientTags.GetById);
|
||||
Assert.IsNotNull(sent);
|
||||
var idMessage = sent.AsIdMessage();
|
||||
Assert.IsNotNull(idMessage);
|
||||
Assert.IsTrue(idMessage.Ids[0].Contains("12345678"));
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public async Task GetByIdAsync_WithMultipleIds_SendsAllIds()
|
||||
{
|
||||
_client.SetNextRequestId(102);
|
||||
|
||||
var task = _client.GetByIdAsync<List<TestOrderItem>>(TestClientTags.GetById, new object[] { 1, 2, 3 });
|
||||
await TaskHelper.WaitToAsync(() => _client.SentMessages.Count > 0, 1000, 10);
|
||||
|
||||
var idMessage = _client.LastSentMessage?.AsIdMessage();
|
||||
Assert.IsNotNull(idMessage);
|
||||
Assert.AreEqual(3, idMessage.Ids.Count);
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public async Task GetByIdAsync_WithCallback_InvokesCallback()
|
||||
{
|
||||
_client.SetNextRequestId(103);
|
||||
var callbackInvoked = false;
|
||||
TestOrderItem? receivedData = null;
|
||||
|
||||
var task = _client.GetByIdAsync<TestOrderItem>(TestClientTags.GetById, async response =>
|
||||
{
|
||||
callbackInvoked = true;
|
||||
receivedData = response.ResponseData;
|
||||
await Task.CompletedTask;
|
||||
}, 42);
|
||||
|
||||
await Task.Delay(10);
|
||||
|
||||
// Simulate server response
|
||||
var responseItem = new TestOrderItem { Id = 42, ProductName = "Test Product" };
|
||||
await _client.SimulateSuccessResponse(103, TestClientTags.GetById, responseItem);
|
||||
|
||||
await Task.Delay(10);
|
||||
|
||||
Assert.IsTrue(callbackInvoked);
|
||||
Assert.IsNotNull(receivedData);
|
||||
Assert.AreEqual(42, receivedData.Id);
|
||||
Assert.AreEqual("Test Product", receivedData.ProductName);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region GetAllAsync Tests
|
||||
|
||||
[TestMethod]
|
||||
public async Task GetAllAsync_SendsMessageWithoutParams()
|
||||
{
|
||||
_client.SetNextRequestId(200);
|
||||
|
||||
var task = _client.GetAllAsync<List<TestOrderItem>>(TestClientTags.GetAll);
|
||||
await TaskHelper.WaitToAsync(() => _client.SentMessages.Count > 0, 1000, 10);
|
||||
|
||||
Assert.AreEqual(1, _client.SentMessages.Count);
|
||||
Assert.AreEqual(TestClientTags.GetAll, _client.LastSentMessage?.MessageTag);
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public async Task GetAllAsync_WithContextParams_SendsParams()
|
||||
{
|
||||
_client.SetNextRequestId(201);
|
||||
|
||||
var task = _client.GetAllAsync<List<TestOrderItem>>(TestClientTags.GetAll, new object[] { true, "filter" });
|
||||
await TaskHelper.WaitToAsync(() => _client.SentMessages.Count > 0, 1000, 10);
|
||||
|
||||
var idMessage = _client.LastSentMessage?.AsIdMessage();
|
||||
Assert.IsNotNull(idMessage);
|
||||
Assert.AreEqual(2, idMessage.Ids.Count);
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public async Task GetAllAsync_WithEmptyParams_SendsWithoutMessage()
|
||||
{
|
||||
_client.SetNextRequestId(202);
|
||||
|
||||
var task = _client.GetAllAsync<List<TestOrderItem>>(TestClientTags.GetAll, Array.Empty<object>());
|
||||
await TaskHelper.WaitToAsync(() => _client.SentMessages.Count > 0, 1000, 10);
|
||||
|
||||
// Empty params should not create IdMessage
|
||||
Assert.IsNull(_client.LastSentMessage?.MessageBytes);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region PostDataAsync Tests
|
||||
|
||||
[TestMethod]
|
||||
public async Task PostDataAsync_SendsComplexObject()
|
||||
{
|
||||
_client.SetNextRequestId(300);
|
||||
var orderItem = new TestOrderItem { Id = 1, ProductName = "New Product", Quantity = 5 };
|
||||
|
||||
_ = Task.Run(() => _client.PostDataAsync<TestOrderItem, TestOrderItem>(TestClientTags.Create, orderItem));
|
||||
await TaskHelper.WaitToAsync(() => _client.SentMessages.Count > 0, 1000, 10);
|
||||
|
||||
Assert.IsTrue(_client.SentMessages.Count >= 1, $"Expected at least 1 message, got {_client.SentMessages.Count}");
|
||||
var sent = _client.SentMessages.FirstOrDefault(m => m.MessageTag == TestClientTags.Create);
|
||||
Assert.IsNotNull(sent);
|
||||
Assert.IsNotNull(sent.MessageBytes);
|
||||
|
||||
var postData = sent.AsPostData<TestOrderItem>();
|
||||
Assert.IsNotNull(postData);
|
||||
Assert.AreEqual("New Product", postData.ProductName);
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public async Task PostDataAsync_WithNestedObject_SerializesCorrectly()
|
||||
{
|
||||
_client.SetNextRequestId(301);
|
||||
var order = TestDataFactory.CreateOrder(itemCount: 2);
|
||||
|
||||
_ = Task.Run(() => _client.PostDataAsync<TestOrder, TestOrder>(TestClientTags.PostOrder, order));
|
||||
await TaskHelper.WaitToAsync(() => _client.SentMessages.Count > 0, 1000, 10);
|
||||
|
||||
var postData = _client.LastSentMessage?.AsPostData<TestOrder>();
|
||||
Assert.IsNotNull(postData);
|
||||
Assert.AreEqual(2, postData.Items.Count);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region OnReceiveMessage Tests
|
||||
|
||||
[TestMethod]
|
||||
public async Task OnReceiveMessage_WithPendingRequest_SetsResponse()
|
||||
{
|
||||
var requestId = 400;
|
||||
// Register with null callback - response will be stored in ResponseByRequestId
|
||||
_client.RegisterPendingRequest(requestId, new SignalRRequestModel());
|
||||
|
||||
await _client.SimulateSuccessResponse(requestId, TestClientTags.Echo, "Hello");
|
||||
|
||||
// After receiving response, the request should have ResponseByRequestId set
|
||||
// The pending request gets removed after callback handling, but for null callback
|
||||
// it stays with the response set
|
||||
var pending = _client.GetPendingRequests();
|
||||
if (pending.TryGetValue(requestId, out var model))
|
||||
{
|
||||
Assert.IsNotNull(model.ResponseByRequestId);
|
||||
}
|
||||
// If not in pending, it means it was handled (which is also valid)
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public async Task OnReceiveMessage_WithCallback_InvokesCallback()
|
||||
{
|
||||
var requestId = 401;
|
||||
var callbackInvoked = false;
|
||||
string? receivedData = null;
|
||||
|
||||
_client.RegisterPendingRequest(requestId, new SignalRRequestModel(new Action<ISignalResponseMessage<string>>(response =>
|
||||
{
|
||||
callbackInvoked = true;
|
||||
receivedData = response.ResponseData;
|
||||
})));
|
||||
|
||||
await _client.SimulateSuccessResponse(requestId, TestClientTags.Echo, "Hello World");
|
||||
|
||||
Assert.IsTrue(callbackInvoked);
|
||||
Assert.IsNotNull(receivedData);
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public async Task OnReceiveMessage_WithoutPendingRequest_CallsMessageReceived()
|
||||
{
|
||||
var response = new SignalResponseJsonMessage(TestClientTags.GetStatus, SignalResponseStatus.Success, "OK");
|
||||
var bytes = response.ToMessagePack(ContractlessStandardResolver.Options);
|
||||
|
||||
await _client.InvokeOnReceiveMessage(TestClientTags.GetStatus, bytes, null);
|
||||
|
||||
Assert.AreEqual(1, _client.ReceivedMessages.Count);
|
||||
Assert.AreEqual(TestClientTags.GetStatus, _client.ReceivedMessages[0].MessageTag);
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public async Task OnReceiveMessage_WithEmptyBytes_LogsWarning()
|
||||
{
|
||||
await _client.InvokeOnReceiveMessage(TestClientTags.Echo, Array.Empty<byte>(), 999);
|
||||
|
||||
Assert.IsTrue(_logger.HasWarningLogs);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Error Handling Tests
|
||||
|
||||
[TestMethod]
|
||||
public async Task GetByIdAsync_WithErrorResponse_ReturnsDefault()
|
||||
{
|
||||
_client.SetNextRequestId(500);
|
||||
|
||||
var task = Task.Run(async () =>
|
||||
{
|
||||
try
|
||||
{
|
||||
return await _client.GetByIdAsync<TestOrderItem>(TestClientTags.NotFound, 999);
|
||||
}
|
||||
catch
|
||||
{
|
||||
return null;
|
||||
}
|
||||
});
|
||||
|
||||
await Task.Delay(10);
|
||||
await _client.SimulateErrorResponse(500, TestClientTags.NotFound);
|
||||
|
||||
var result = await task;
|
||||
Assert.IsNull(result);
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public async Task SendMessageToServerAsync_WithZeroTag_LogsError()
|
||||
{
|
||||
await _client.SendMessageToServerAsync<string>(0, response => Task.CompletedTask);
|
||||
|
||||
Assert.IsTrue(_logger.HasErrorLogs);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Complex Scenario Tests
|
||||
|
||||
[TestMethod]
|
||||
public async Task MultipleParallelRequests_HandleCorrectly()
|
||||
{
|
||||
_client.SetNextRequestId(600);
|
||||
|
||||
// Start multiple requests
|
||||
var task1 = Task.Run(async () =>
|
||||
{
|
||||
var t = _client.GetByIdAsync<TestOrderItem>(TestClientTags.GetById, 1);
|
||||
await Task.Delay(5);
|
||||
return 600;
|
||||
});
|
||||
|
||||
var task2 = Task.Run(async () =>
|
||||
{
|
||||
var t = _client.GetByIdAsync<TestOrderItem>(TestClientTags.GetById, 2);
|
||||
await Task.Delay(5);
|
||||
return 601;
|
||||
});
|
||||
|
||||
await Task.WhenAll(task1, task2);
|
||||
|
||||
// Both requests should have been sent
|
||||
Assert.AreEqual(2, _client.SentMessages.Count);
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public async Task ConcurrentResponseHandling_ResolvesCorrectRequests()
|
||||
{
|
||||
_client.SetNextRequestId(700);
|
||||
|
||||
// Register two pending requests with callbacks
|
||||
var results = new Dictionary<int, string?>();
|
||||
|
||||
_client.RegisterPendingRequest(700, new SignalRRequestModel(new Action<ISignalResponseMessage<string>>(response =>
|
||||
{
|
||||
results[700] = response.ResponseData;
|
||||
})));
|
||||
|
||||
_client.RegisterPendingRequest(701, new SignalRRequestModel(new Action<ISignalResponseMessage<string>>(response =>
|
||||
{
|
||||
results[701] = response.ResponseData;
|
||||
})));
|
||||
|
||||
// Simulate responses in reverse order
|
||||
await _client.SimulateSuccessResponse(701, TestClientTags.Echo, "Response 701");
|
||||
await _client.SimulateSuccessResponse(700, TestClientTags.Echo, "Response 700");
|
||||
|
||||
// Each request should get its correct response
|
||||
Assert.AreEqual(2, results.Count);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region IdMessage Single Primitive Parameter Tests
|
||||
|
||||
[TestMethod]
|
||||
public void IdMessage_WithInt_SerializesAsNumber()
|
||||
{
|
||||
var idMessage = new IdMessage(42);
|
||||
|
||||
Assert.AreEqual(1, idMessage.Ids.Count);
|
||||
Assert.AreEqual("42", idMessage.Ids[0]);
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void IdMessage_WithLong_SerializesAsNumber()
|
||||
{
|
||||
var idMessage = new IdMessage(9223372036854775807L);
|
||||
|
||||
Assert.AreEqual(1, idMessage.Ids.Count);
|
||||
Assert.AreEqual("9223372036854775807", idMessage.Ids[0]);
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void IdMessage_WithBoolTrue_SerializesCorrectly()
|
||||
{
|
||||
var idMessage = new IdMessage(true);
|
||||
|
||||
Assert.AreEqual(1, idMessage.Ids.Count);
|
||||
Assert.AreEqual("true", idMessage.Ids[0]);
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void IdMessage_WithBoolFalse_SerializesCorrectly()
|
||||
{
|
||||
var idMessage = new IdMessage(false);
|
||||
|
||||
Assert.AreEqual(1, idMessage.Ids.Count);
|
||||
Assert.AreEqual("false", idMessage.Ids[0]);
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void IdMessage_WithString_SerializesAsQuotedString()
|
||||
{
|
||||
var idMessage = new IdMessage("hello");
|
||||
|
||||
Assert.AreEqual(1, idMessage.Ids.Count);
|
||||
Assert.AreEqual("\"hello\"", idMessage.Ids[0]);
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void IdMessage_WithDouble_SerializesAsNumber()
|
||||
{
|
||||
var idMessage = new IdMessage(3.14159);
|
||||
|
||||
Assert.AreEqual(1, idMessage.Ids.Count);
|
||||
Assert.IsTrue(idMessage.Ids[0].StartsWith("3.14"));
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void IdMessage_WithDecimal_SerializesAsNumber()
|
||||
{
|
||||
var idMessage = new IdMessage(99.99m);
|
||||
|
||||
Assert.AreEqual(1, idMessage.Ids.Count);
|
||||
Assert.AreEqual("99.99", idMessage.Ids[0]);
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void IdMessage_WithGuid_SerializesAsQuotedString()
|
||||
{
|
||||
var guid = Guid.Parse("12345678-1234-1234-1234-123456789abc");
|
||||
var idMessage = new IdMessage(guid);
|
||||
|
||||
Assert.AreEqual(1, idMessage.Ids.Count);
|
||||
Assert.IsTrue(idMessage.Ids[0].Contains("12345678"));
|
||||
Assert.IsTrue(idMessage.Ids[0].StartsWith("\"") && idMessage.Ids[0].EndsWith("\""));
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void IdMessage_WithDateTime_SerializesAsQuotedString()
|
||||
{
|
||||
var date = new DateTime(2024, 12, 25, 12, 30, 45, DateTimeKind.Utc);
|
||||
var idMessage = new IdMessage(date);
|
||||
|
||||
Assert.AreEqual(1, idMessage.Ids.Count);
|
||||
Assert.IsTrue(idMessage.Ids[0].Contains("2024"));
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void IdMessage_WithEnum_SerializesAsNumber()
|
||||
{
|
||||
var idMessage = new IdMessage(TestStatus.Processing);
|
||||
|
||||
Assert.AreEqual(1, idMessage.Ids.Count);
|
||||
Assert.AreEqual("2", idMessage.Ids[0]); // Processing = 2
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region IdMessage Multiple Primitive Parameters Tests
|
||||
|
||||
[TestMethod]
|
||||
public void IdMessage_WithTwoInts_SerializesCorrectly()
|
||||
{
|
||||
var idMessage = new IdMessage(new object[] { 10, 20 });
|
||||
|
||||
Assert.AreEqual(2, idMessage.Ids.Count);
|
||||
Assert.AreEqual("10", idMessage.Ids[0]);
|
||||
Assert.AreEqual("20", idMessage.Ids[1]);
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void IdMessage_WithThreeMixedTypes_SerializesCorrectly()
|
||||
{
|
||||
var idMessage = new IdMessage(new object[] { true, "test", 123 });
|
||||
|
||||
Assert.AreEqual(3, idMessage.Ids.Count);
|
||||
Assert.AreEqual("true", idMessage.Ids[0]);
|
||||
Assert.AreEqual("\"test\"", idMessage.Ids[1]);
|
||||
Assert.AreEqual("123", idMessage.Ids[2]);
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void IdMessage_WithFiveParams_SerializesCorrectly()
|
||||
{
|
||||
var testGuid = Guid.NewGuid();
|
||||
var idMessage = new IdMessage(new object[] { 42, "hello", true, testGuid, 99.99m });
|
||||
|
||||
Assert.AreEqual(5, idMessage.Ids.Count);
|
||||
Assert.AreEqual("42", idMessage.Ids[0]);
|
||||
Assert.AreEqual("\"hello\"", idMessage.Ids[1]);
|
||||
Assert.AreEqual("true", idMessage.Ids[2]);
|
||||
Assert.IsTrue(idMessage.Ids[3].Contains(testGuid.ToString()));
|
||||
Assert.AreEqual("99.99", idMessage.Ids[4]);
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void IdMessage_WithIntBoolStringGuidDecimal_AllTypesSerializeCorrectly()
|
||||
{
|
||||
var guid = Guid.Parse("aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee");
|
||||
var idMessage = new IdMessage(new object[] {
|
||||
42, // int
|
||||
true, // bool
|
||||
"text", // string
|
||||
guid, // Guid
|
||||
123.45m // decimal
|
||||
});
|
||||
|
||||
Assert.AreEqual(5, idMessage.Ids.Count);
|
||||
Assert.AreEqual("42", idMessage.Ids[0]);
|
||||
Assert.AreEqual("true", idMessage.Ids[1]);
|
||||
Assert.AreEqual("\"text\"", idMessage.Ids[2]);
|
||||
Assert.IsTrue(idMessage.Ids[3].Contains("aaaaaaaa"));
|
||||
Assert.AreEqual("123.45", idMessage.Ids[4]);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region IdMessage No Parameters Tests
|
||||
|
||||
[TestMethod]
|
||||
public void IdMessage_WithEmptyArray_HasNoIds()
|
||||
{
|
||||
var idMessage = new IdMessage(Array.Empty<object>());
|
||||
|
||||
Assert.AreEqual(0, idMessage.Ids.Count);
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public async Task GetAllAsync_WithNullParams_SendsWithoutMessage()
|
||||
{
|
||||
_client.SetNextRequestId(203);
|
||||
|
||||
object[]? nullParams = null;
|
||||
var task = _client.GetAllAsync<List<TestOrderItem>>(TestClientTags.GetAll, nullParams);
|
||||
await TaskHelper.WaitToAsync(() => _client.SentMessages.Count > 0, 1000, 10);
|
||||
|
||||
// Null params should not create IdMessage
|
||||
Assert.IsNull(_client.LastSentMessage?.MessageBytes);
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public async Task SendMessageToServerAsync_NoParams_SendsWithoutBytes()
|
||||
{
|
||||
await _client.SendMessageToServerAsync(TestClientTags.Ping, null, 1);
|
||||
|
||||
Assert.AreEqual(1, _client.SentMessages.Count);
|
||||
Assert.IsNull(_client.LastSentMessage?.MessageBytes);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region IdMessage Complex Object Tests
|
||||
|
||||
[TestMethod]
|
||||
public void IdMessage_WithComplexObject_SerializesAsJson()
|
||||
{
|
||||
var item = new TestOrderItem { Id = 1, ProductName = "Test", Quantity = 5 };
|
||||
var idMessage = new IdMessage(item);
|
||||
|
||||
Assert.AreEqual(1, idMessage.Ids.Count);
|
||||
Assert.IsTrue(idMessage.Ids[0].Contains("\"ProductName\""));
|
||||
Assert.IsTrue(idMessage.Ids[0].Contains("\"Test\""));
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void IdMessage_WithNestedObject_SerializesCorrectly()
|
||||
{
|
||||
var order = new TestOrder
|
||||
{
|
||||
Id = 100,
|
||||
OrderNumber = "ORD-001",
|
||||
Items = new List<TestOrderItem>
|
||||
{
|
||||
new() { Id = 1, ProductName = "Item A" },
|
||||
new() { Id = 2, ProductName = "Item B" }
|
||||
}
|
||||
};
|
||||
var idMessage = new IdMessage(order);
|
||||
|
||||
Assert.AreEqual(1, idMessage.Ids.Count);
|
||||
Assert.IsTrue(idMessage.Ids[0].Contains("ORD-001"));
|
||||
Assert.IsTrue(idMessage.Ids[0].Contains("Item A"));
|
||||
Assert.IsTrue(idMessage.Ids[0].Contains("Item B"));
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void IdMessage_WithSharedTag_IIdType_SerializesCorrectly()
|
||||
{
|
||||
var tag = new SharedTag { Id = 1, Name = "Important", Color = "#FF0000" };
|
||||
var idMessage = new IdMessage(tag);
|
||||
|
||||
Assert.AreEqual(1, idMessage.Ids.Count);
|
||||
Assert.IsTrue(idMessage.Ids[0].Contains("Important"));
|
||||
Assert.IsTrue(idMessage.Ids[0].Contains("#FF0000"));
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region IdMessage Array/Collection Tests
|
||||
|
||||
[TestMethod]
|
||||
public void IdMessage_WithIntArray_SerializesAsJsonArray()
|
||||
{
|
||||
var values = new[] { 1, 2, 3, 4, 5 };
|
||||
var idMessage = new IdMessage(values);
|
||||
|
||||
Assert.AreEqual(1, idMessage.Ids.Count);
|
||||
Assert.IsTrue(idMessage.Ids[0].Contains("["));
|
||||
Assert.IsTrue(idMessage.Ids[0].Contains("1"));
|
||||
Assert.IsTrue(idMessage.Ids[0].Contains("5"));
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void IdMessage_WithStringList_SerializesAsJsonArray()
|
||||
{
|
||||
var items = new List<string> { "apple", "banana", "cherry" };
|
||||
var idMessage = new IdMessage(items);
|
||||
|
||||
Assert.AreEqual(1, idMessage.Ids.Count);
|
||||
Assert.IsTrue(idMessage.Ids[0].Contains("apple"));
|
||||
Assert.IsTrue(idMessage.Ids[0].Contains("banana"));
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void IdMessage_WithGuidArray_SerializesAsJsonArray()
|
||||
{
|
||||
// Guid[] is treated as a single object and serialized as JSON array
|
||||
// Use new object[] { guids } to pass it as single parameter
|
||||
var guids = new[] { Guid.NewGuid(), Guid.NewGuid() };
|
||||
var idMessage = new IdMessage(new object[] { guids });
|
||||
|
||||
Assert.AreEqual(1, idMessage.Ids.Count);
|
||||
Assert.IsTrue(idMessage.Ids[0].StartsWith("["));
|
||||
Assert.IsTrue(idMessage.Ids[0].EndsWith("]"));
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void IdMessage_WithGuidArrayAsMultipleParams_EnumeratesEachGuid()
|
||||
{
|
||||
// Using IEnumerable<Guid> constructor - each guid becomes separate Id
|
||||
var guids = new[] { Guid.NewGuid(), Guid.NewGuid() };
|
||||
var idMessage = new IdMessage((IEnumerable<Guid>)guids);
|
||||
|
||||
Assert.AreEqual(2, idMessage.Ids.Count);
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void IdMessage_WithEmptyArray_SerializesAsEmptyJsonArray()
|
||||
{
|
||||
var empty = Array.Empty<int>();
|
||||
var idMessage = new IdMessage(empty);
|
||||
|
||||
Assert.AreEqual(1, idMessage.Ids.Count);
|
||||
Assert.AreEqual("[]", idMessage.Ids[0]);
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void IdMessage_WithTestOrderItemList_SerializesAsJsonArray()
|
||||
{
|
||||
var items = new List<TestOrderItem>
|
||||
{
|
||||
new() { Id = 1, ProductName = "First" },
|
||||
new() { Id = 2, ProductName = "Second" }
|
||||
};
|
||||
var idMessage = new IdMessage(items);
|
||||
|
||||
Assert.AreEqual(1, idMessage.Ids.Count);
|
||||
Assert.IsTrue(idMessage.Ids[0].Contains("First"));
|
||||
Assert.IsTrue(idMessage.Ids[0].Contains("Second"));
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region IdMessage Mixed Parameters (Primitive + Complex + Collection) Tests
|
||||
|
||||
[TestMethod]
|
||||
public void IdMessage_WithIntAndDto_SerializesCorrectly()
|
||||
{
|
||||
var item = new TestOrderItem { Id = 10, ProductName = "Test" };
|
||||
var idMessage = new IdMessage(new object[] { 42, item });
|
||||
|
||||
Assert.AreEqual(2, idMessage.Ids.Count);
|
||||
Assert.AreEqual("42", idMessage.Ids[0]);
|
||||
Assert.IsTrue(idMessage.Ids[1].Contains("Test"));
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void IdMessage_WithDtoAndList_SerializesCorrectly()
|
||||
{
|
||||
var item = new TestOrderItem { Id = 1, ProductName = "Product" };
|
||||
var numbers = new List<int> { 1, 2, 3 };
|
||||
var idMessage = new IdMessage(new object[] { item, numbers });
|
||||
|
||||
Assert.AreEqual(2, idMessage.Ids.Count);
|
||||
Assert.IsTrue(idMessage.Ids[0].Contains("Product"));
|
||||
Assert.IsTrue(idMessage.Ids[1].Contains("["));
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void IdMessage_WithBoolArrayString_MixedWithArray_SerializesCorrectly()
|
||||
{
|
||||
var numbers = new[] { 1, 2, 3 };
|
||||
var idMessage = new IdMessage(new object[] { true, numbers, "hello" });
|
||||
|
||||
Assert.AreEqual(3, idMessage.Ids.Count);
|
||||
Assert.AreEqual("true", idMessage.Ids[0]);
|
||||
Assert.IsTrue(idMessage.Ids[1].Contains("[1,2,3]") || idMessage.Ids[1].Contains("[1, 2, 3]"));
|
||||
Assert.AreEqual("\"hello\"", idMessage.Ids[2]);
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void IdMessage_WithThreeComplexParams_SerializesCorrectly()
|
||||
{
|
||||
var item = new TestOrderItem { Id = 1, ProductName = "Product" };
|
||||
var tags = new List<string> { "tag1", "tag2" };
|
||||
var sharedTag = new SharedTag { Id = 1, Name = "Shared" };
|
||||
var idMessage = new IdMessage(new object[] { item, tags, sharedTag });
|
||||
|
||||
Assert.AreEqual(3, idMessage.Ids.Count);
|
||||
Assert.IsTrue(idMessage.Ids[0].Contains("Product"));
|
||||
Assert.IsTrue(idMessage.Ids[1].Contains("tag1"));
|
||||
Assert.IsTrue(idMessage.Ids[2].Contains("Shared"));
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void IdMessage_WithFiveMixedParams_AllTypesSerialize()
|
||||
{
|
||||
var guid = Guid.NewGuid();
|
||||
var item = new TestOrderItem { Id = 5, ProductName = "Mixed" };
|
||||
var numbers = new[] { 10, 20, 30 };
|
||||
|
||||
var idMessage = new IdMessage(new object[] {
|
||||
42, // int
|
||||
guid, // Guid
|
||||
item, // complex object
|
||||
numbers, // array
|
||||
"final" // string
|
||||
});
|
||||
|
||||
Assert.AreEqual(5, idMessage.Ids.Count);
|
||||
Assert.AreEqual("42", idMessage.Ids[0]);
|
||||
Assert.IsTrue(idMessage.Ids[1].Contains(guid.ToString()));
|
||||
Assert.IsTrue(idMessage.Ids[2].Contains("Mixed"));
|
||||
Assert.IsTrue(idMessage.Ids[3].Contains("["));
|
||||
Assert.AreEqual("\"final\"", idMessage.Ids[4]);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region GetAllAsync Parameter Type Tests
|
||||
|
||||
[TestMethod]
|
||||
public async Task GetAllAsync_WithSingleBoolParam_SerializesCorrectly()
|
||||
{
|
||||
_client.SetNextRequestId(210);
|
||||
|
||||
_ = _client.GetAllAsync<List<TestOrderItem>>(TestClientTags.GetAll, new object[] { true });
|
||||
await TaskHelper.WaitToAsync(() => _client.SentMessages.Count > 0, 1000, 10);
|
||||
|
||||
var idMessage = _client.LastSentMessage?.AsIdMessage();
|
||||
Assert.IsNotNull(idMessage);
|
||||
Assert.AreEqual(1, idMessage.Ids.Count);
|
||||
Assert.AreEqual("true", idMessage.Ids[0]);
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public async Task GetAllAsync_WithIntAndString_SerializesCorrectly()
|
||||
{
|
||||
_client.SetNextRequestId(211);
|
||||
|
||||
_ = _client.GetAllAsync<List<TestOrderItem>>(TestClientTags.GetAll, new object[] { 100, "filter" });
|
||||
await TaskHelper.WaitToAsync(() => _client.SentMessages.Count > 0, 1000, 10);
|
||||
|
||||
var idMessage = _client.LastSentMessage?.AsIdMessage();
|
||||
Assert.IsNotNull(idMessage);
|
||||
Assert.AreEqual(2, idMessage.Ids.Count);
|
||||
Assert.AreEqual("100", idMessage.Ids[0]);
|
||||
Assert.AreEqual("\"filter\"", idMessage.Ids[1]);
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public async Task GetAllAsync_WithGuidParam_SerializesCorrectly()
|
||||
{
|
||||
_client.SetNextRequestId(212);
|
||||
var guid = Guid.NewGuid();
|
||||
|
||||
_ = _client.GetAllAsync<TestGuidOrder>(TestClientTags.GetAll, new object[] { guid });
|
||||
await TaskHelper.WaitToAsync(() => _client.SentMessages.Count > 0, 1000, 10);
|
||||
|
||||
var idMessage = _client.LastSentMessage?.AsIdMessage();
|
||||
Assert.IsNotNull(idMessage);
|
||||
Assert.AreEqual(1, idMessage.Ids.Count);
|
||||
Assert.IsTrue(idMessage.Ids[0].Contains(guid.ToString()));
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public async Task GetAllAsync_WithComplexObjectParam_SerializesCorrectly()
|
||||
{
|
||||
_client.SetNextRequestId(213);
|
||||
var filter = new TestOrderItem { Id = 0, ProductName = "SearchFilter" };
|
||||
|
||||
_ = _client.GetAllAsync<List<TestOrderItem>>(TestClientTags.GetAll, new object[] { filter });
|
||||
await TaskHelper.WaitToAsync(() => _client.SentMessages.Count > 0, 1000, 10);
|
||||
|
||||
var idMessage = _client.LastSentMessage?.AsIdMessage();
|
||||
Assert.IsNotNull(idMessage);
|
||||
Assert.AreEqual(1, idMessage.Ids.Count);
|
||||
Assert.IsTrue(idMessage.Ids[0].Contains("SearchFilter"));
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public async Task GetAllAsync_WithArrayParam_SerializesCorrectly()
|
||||
{
|
||||
_client.SetNextRequestId(214);
|
||||
var ids = new[] { 1, 2, 3, 4, 5 };
|
||||
|
||||
_ = _client.GetAllAsync<List<TestOrderItem>>(TestClientTags.GetAll, new object[] { ids });
|
||||
await TaskHelper.WaitToAsync(() => _client.SentMessages.Count > 0, 1000, 10);
|
||||
|
||||
var idMessage = _client.LastSentMessage?.AsIdMessage();
|
||||
Assert.IsNotNull(idMessage);
|
||||
Assert.AreEqual(1, idMessage.Ids.Count);
|
||||
Assert.IsTrue(idMessage.Ids[0].Contains("["));
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public async Task GetAllAsync_WithThreeMixedParams_SerializesCorrectly()
|
||||
{
|
||||
_client.SetNextRequestId(215);
|
||||
var tag = new SharedTag { Id = 1, Name = "Filter" };
|
||||
|
||||
_ = _client.GetAllAsync<List<TestOrderItem>>(TestClientTags.GetAll, new object[] { true, 50, tag });
|
||||
await TaskHelper.WaitToAsync(() => _client.SentMessages.Count > 0, 1000, 10);
|
||||
|
||||
var idMessage = _client.LastSentMessage?.AsIdMessage();
|
||||
Assert.IsNotNull(idMessage);
|
||||
Assert.AreEqual(3, idMessage.Ids.Count);
|
||||
Assert.AreEqual("true", idMessage.Ids[0]);
|
||||
Assert.AreEqual("50", idMessage.Ids[1]);
|
||||
Assert.IsTrue(idMessage.Ids[2].Contains("Filter"));
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region PostDataAsync Parameter Type Tests
|
||||
|
||||
[TestMethod]
|
||||
public async Task PostDataAsync_WithSimpleDto_SerializesCorrectly()
|
||||
{
|
||||
_client.SetNextRequestId(320);
|
||||
var item = new TestOrderItem { Id = 1, ProductName = "Simple", Quantity = 10, UnitPrice = 25.50m };
|
||||
|
||||
_ = Task.Run(() => _client.PostDataAsync<TestOrderItem, TestOrderItem>(TestClientTags.Create, item));
|
||||
await Task.Delay(50);
|
||||
|
||||
var postData = _client.LastSentMessage?.AsPostData<TestOrderItem>();
|
||||
Assert.IsNotNull(postData);
|
||||
Assert.AreEqual("Simple", postData.ProductName);
|
||||
Assert.AreEqual(10, postData.Quantity);
|
||||
Assert.AreEqual(25.50m, postData.UnitPrice);
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public async Task PostDataAsync_WithDtoContainingList_SerializesCorrectly()
|
||||
{
|
||||
_client.SetNextRequestId(321);
|
||||
var order = new TestOrder
|
||||
{
|
||||
Id = 1,
|
||||
OrderNumber = "ORD-TEST",
|
||||
Items = new List<TestOrderItem>
|
||||
{
|
||||
new() { Id = 1, ProductName = "A", Quantity = 1 },
|
||||
new() { Id = 2, ProductName = "B", Quantity = 2 },
|
||||
new() { Id = 3, ProductName = "C", Quantity = 3 }
|
||||
}
|
||||
};
|
||||
|
||||
_ = Task.Run(() => _client.PostDataAsync<TestOrder, TestOrder>(TestClientTags.PostOrder, order));
|
||||
await Task.Delay(50);
|
||||
|
||||
var postData = _client.LastSentMessage?.AsPostData<TestOrder>();
|
||||
Assert.IsNotNull(postData);
|
||||
Assert.AreEqual("ORD-TEST", postData.OrderNumber);
|
||||
Assert.AreEqual(3, postData.Items.Count);
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public async Task PostDataAsync_WithDtoContainingSharedRefs_SerializesCorrectly()
|
||||
{
|
||||
_client.SetNextRequestId(322);
|
||||
var sharedTag = new SharedTag { Id = 1, Name = "Shared" };
|
||||
var order = new TestOrder
|
||||
{
|
||||
Id = 1,
|
||||
OrderNumber = "ORD-SHARED",
|
||||
PrimaryTag = sharedTag,
|
||||
SecondaryTag = sharedTag
|
||||
};
|
||||
|
||||
_ = Task.Run(() => _client.PostDataAsync<TestOrder, TestOrder>(TestClientTags.PostOrder, order));
|
||||
await Task.Delay(50);
|
||||
|
||||
var postData = _client.LastSentMessage?.AsPostData<TestOrder>();
|
||||
Assert.IsNotNull(postData);
|
||||
Assert.IsNotNull(postData.PrimaryTag);
|
||||
Assert.AreEqual("Shared", postData.PrimaryTag.Name);
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public async Task PostDataAsync_WithList_SerializesCorrectly()
|
||||
{
|
||||
_client.SetNextRequestId(323);
|
||||
var items = new List<TestOrderItem>
|
||||
{
|
||||
new() { Id = 1, ProductName = "First" },
|
||||
new() { Id = 2, ProductName = "Second" }
|
||||
};
|
||||
|
||||
_ = Task.Run(() => _client.PostDataAsync<List<TestOrderItem>, List<TestOrderItem>>(TestClientTags.Create, items));
|
||||
await Task.Delay(50);
|
||||
|
||||
var postData = _client.LastSentMessage?.AsPostData<List<TestOrderItem>>();
|
||||
Assert.IsNotNull(postData);
|
||||
Assert.AreEqual(2, postData.Count);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Edge Cases Tests
|
||||
|
||||
[TestMethod]
|
||||
public void IdMessage_WithNullInArray_HandlesGracefully()
|
||||
{
|
||||
// This tests how null values are handled - they should serialize as "null"
|
||||
var idMessage = new IdMessage(new object[] { 42, null!, "text" });
|
||||
|
||||
Assert.AreEqual(3, idMessage.Ids.Count);
|
||||
Assert.AreEqual("42", idMessage.Ids[0]);
|
||||
// null should serialize as "null" JSON literal
|
||||
Assert.AreEqual("null", idMessage.Ids[1]);
|
||||
Assert.AreEqual("\"text\"", idMessage.Ids[2]);
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void IdMessage_WithSpecialCharactersInString_EscapesCorrectly()
|
||||
{
|
||||
var idMessage = new IdMessage("hello \"world\" \n\t\\");
|
||||
|
||||
Assert.AreEqual(1, idMessage.Ids.Count);
|
||||
// Should contain escaped characters
|
||||
Assert.IsTrue(idMessage.Ids[0].Contains("\\\""));
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void IdMessage_WithEmptyString_SerializesAsEmptyQuotedString()
|
||||
{
|
||||
var idMessage = new IdMessage("");
|
||||
|
||||
Assert.AreEqual(1, idMessage.Ids.Count);
|
||||
Assert.AreEqual("\"\"", idMessage.Ids[0]);
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void IdMessage_WithZeroValues_SerializesCorrectly()
|
||||
{
|
||||
var idMessage = new IdMessage(new object[] { 0, 0L, 0.0, 0m });
|
||||
|
||||
Assert.AreEqual(4, idMessage.Ids.Count);
|
||||
Assert.AreEqual("0", idMessage.Ids[0]);
|
||||
Assert.AreEqual("0", idMessage.Ids[1]);
|
||||
Assert.AreEqual("0", idMessage.Ids[2]);
|
||||
Assert.AreEqual("0", idMessage.Ids[3]);
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void IdMessage_WithGuidEmpty_SerializesCorrectly()
|
||||
{
|
||||
var idMessage = new IdMessage(Guid.Empty);
|
||||
|
||||
Assert.AreEqual(1, idMessage.Ids.Count);
|
||||
Assert.IsTrue(idMessage.Ids[0].Contains("00000000-0000-0000-0000-000000000000"));
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region MessagePack Serialization Round-Trip Tests
|
||||
|
||||
[TestMethod]
|
||||
public void SignalPostJsonDataMessage_MessagePackRoundTrip_PreservesPostDataJson()
|
||||
{
|
||||
// Arrange
|
||||
var original = new SignalPostJsonDataMessage<IdMessage>(new IdMessage(42));
|
||||
|
||||
// Act - Serialize to MessagePack
|
||||
var bytes = original.ToMessagePack(ContractlessStandardResolver.Options);
|
||||
|
||||
// Deserialize back
|
||||
var deserialized = bytes.MessagePackTo<SignalPostJsonDataMessage<IdMessage>>(ContractlessStandardResolver.Options);
|
||||
|
||||
// Assert
|
||||
Assert.IsNotNull(deserialized);
|
||||
Assert.IsNotNull(deserialized.PostDataJson, "PostDataJson should not be null after deserialization");
|
||||
Assert.IsFalse(string.IsNullOrEmpty(deserialized.PostDataJson), "PostDataJson should not be empty");
|
||||
|
||||
// Verify PostData can be accessed
|
||||
Assert.IsNotNull(deserialized.PostData);
|
||||
Assert.AreEqual(1, deserialized.PostData.Ids.Count);
|
||||
Assert.AreEqual("42", deserialized.PostData.Ids[0]);
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void SignalPostJsonDataMessage_WithComplexObject_MessagePackRoundTrip()
|
||||
{
|
||||
// Arrange
|
||||
var order = new TestOrderItem { Id = 1, ProductName = "Test", Quantity = 5 };
|
||||
var original = new SignalPostJsonDataMessage<TestOrderItem>(order);
|
||||
|
||||
// Act
|
||||
var bytes = original.ToMessagePack(ContractlessStandardResolver.Options);
|
||||
var deserialized = bytes.MessagePackTo<SignalPostJsonDataMessage<TestOrderItem>>(ContractlessStandardResolver.Options);
|
||||
|
||||
// Assert
|
||||
Assert.IsNotNull(deserialized);
|
||||
Assert.IsNotNull(deserialized.PostDataJson);
|
||||
Assert.IsNotNull(deserialized.PostData);
|
||||
Assert.AreEqual("Test", deserialized.PostData.ProductName);
|
||||
Assert.AreEqual(5, deserialized.PostData.Quantity);
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void SignalPostJsonMessage_BaseClass_MessagePackRoundTrip()
|
||||
{
|
||||
// Test base class directly
|
||||
var original = new SignalPostJsonMessage { PostDataJson = "{\"test\":123}" };
|
||||
|
||||
var bytes = original.ToMessagePack(ContractlessStandardResolver.Options);
|
||||
var deserialized = bytes.MessagePackTo<SignalPostJsonMessage>(ContractlessStandardResolver.Options);
|
||||
|
||||
Assert.IsNotNull(deserialized);
|
||||
Assert.AreEqual("{\"test\":123}", deserialized.PostDataJson);
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void IdMessage_InSignalPostJsonDataMessage_RoundTrip_PreservesAllIds()
|
||||
{
|
||||
// Arrange - Multiple IDs
|
||||
var idMessage = new IdMessage(new object[] { 1, "test", true, Guid.Empty });
|
||||
var original = new SignalPostJsonDataMessage<IdMessage>(idMessage);
|
||||
|
||||
// Act
|
||||
var bytes = original.ToMessagePack(ContractlessStandardResolver.Options);
|
||||
var deserialized = bytes.MessagePackTo<SignalPostJsonDataMessage<IdMessage>>(ContractlessStandardResolver.Options);
|
||||
|
||||
// Assert
|
||||
Assert.IsNotNull(deserialized?.PostData);
|
||||
Assert.AreEqual(4, deserialized.PostData.Ids.Count);
|
||||
Assert.AreEqual("1", deserialized.PostData.Ids[0]);
|
||||
Assert.AreEqual("\"test\"", deserialized.PostData.Ids[1]);
|
||||
Assert.AreEqual("true", deserialized.PostData.Ids[2]);
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void SignalPostJsonDataMessage_DeserializeAsBaseType_WorksCorrectly()
|
||||
{
|
||||
// This simulates what the server does - deserializing as SignalPostJsonMessage (base type)
|
||||
var original = new SignalPostJsonDataMessage<IdMessage>(new IdMessage(42));
|
||||
|
||||
// Serialize as derived type
|
||||
var bytes = original.ToMessagePack(ContractlessStandardResolver.Options);
|
||||
|
||||
// Deserialize as BASE type (like server might do)
|
||||
var deserialized = bytes.MessagePackTo<SignalPostJsonMessage>(ContractlessStandardResolver.Options);
|
||||
|
||||
// Assert - PostDataJson should still be available
|
||||
Assert.IsNotNull(deserialized);
|
||||
Assert.IsNotNull(deserialized.PostDataJson);
|
||||
Assert.IsFalse(string.IsNullOrEmpty(deserialized.PostDataJson));
|
||||
|
||||
// Should be able to manually deserialize the JSON
|
||||
var idMessage = deserialized.PostDataJson.JsonTo<IdMessage>();
|
||||
Assert.IsNotNull(idMessage);
|
||||
Assert.AreEqual(1, idMessage.Ids.Count);
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void SignalPostJsonDataMessage_WithIdMessage_ContainingInt_RoundTrip()
|
||||
{
|
||||
// Arrange - This is exactly what the client does when calling PostDataAsync<int, string>
|
||||
var idMessage = new IdMessage(42);
|
||||
var original = new SignalPostJsonDataMessage<IdMessage>(idMessage);
|
||||
|
||||
// Debug: print what's in PostDataJson
|
||||
Console.WriteLine($"IdMessage.Ids[0]: {idMessage.Ids[0]}");
|
||||
Console.WriteLine($"Original PostDataJson: {original.PostDataJson}");
|
||||
|
||||
// Act - Serialize to MessagePack (what client does)
|
||||
var bytes = original.ToMessagePack(ContractlessStandardResolver.Options);
|
||||
Console.WriteLine($"MessagePack bytes length: {bytes.Length}");
|
||||
|
||||
// Deserialize back (what server does)
|
||||
var deserialized = bytes.MessagePackTo<SignalPostJsonDataMessage<IdMessage>>(ContractlessStandardResolver.Options);
|
||||
|
||||
// Assert
|
||||
Assert.IsNotNull(deserialized);
|
||||
Console.WriteLine($"Deserialized PostDataJson: {deserialized.PostDataJson}");
|
||||
Assert.IsNotNull(deserialized.PostDataJson, "PostDataJson should not be null");
|
||||
Assert.IsFalse(string.IsNullOrEmpty(deserialized.PostDataJson), "PostDataJson should not be empty");
|
||||
|
||||
// This is the key - PostData should be accessible
|
||||
Assert.IsNotNull(deserialized.PostData, "PostData should be deserializable");
|
||||
Console.WriteLine($"Deserialized PostData.Ids.Count: {deserialized.PostData.Ids.Count}");
|
||||
Assert.AreEqual(1, deserialized.PostData.Ids.Count, "Should have 1 Id");
|
||||
Assert.AreEqual("42", deserialized.PostData.Ids[0], "Id should be '42'");
|
||||
}
|
||||
|
||||
#endregion
|
||||
}
|
||||
|
|
@ -10,7 +10,6 @@ public class PostJsonDataMessageTests
|
|||
[TestMethod]
|
||||
public void Debug_CreatePostMessage_ForInt()
|
||||
{
|
||||
// Test what CreatePostMessage produces for an int
|
||||
var message = CreatePostMessageTest(42);
|
||||
|
||||
Console.WriteLine($"Message type: {message.GetType().Name}");
|
||||
|
|
@ -22,11 +21,9 @@ public class PostJsonDataMessageTests
|
|||
Console.WriteLine($"PostData.Ids[0]: {idMsg.PostData.Ids[0]}");
|
||||
}
|
||||
|
||||
// Serialize to MessagePack
|
||||
var bytes = message.ToMessagePack(ContractlessStandardResolver.Options);
|
||||
Console.WriteLine($"MessagePack bytes: {bytes.Length}");
|
||||
|
||||
// Deserialize as server would
|
||||
var deserialized = bytes.MessagePackTo<SignalPostJsonDataMessage<IdMessage>>(ContractlessStandardResolver.Options);
|
||||
Console.WriteLine($"Deserialized PostDataJson: {deserialized.PostDataJson}");
|
||||
Console.WriteLine($"Deserialized PostData type: {deserialized.PostData?.GetType().Name}");
|
||||
|
|
@ -52,68 +49,49 @@ public class PostJsonDataMessageTests
|
|||
return null;
|
||||
}
|
||||
|
||||
// Step 1: Client creates message for int parameter (like PostDataAsync<int, string>)
|
||||
Console.WriteLine("=== Step 1: Client creates message ===");
|
||||
|
||||
var idMessage = new IdMessage(GetValueByType(testValue));
|
||||
Console.WriteLine($"IdMessage.Ids[0]: '{idMessage.Ids[0]}'");
|
||||
|
||||
var clientMessage = new SignalPostJsonDataMessage<IdMessage>(idMessage);
|
||||
Console.WriteLine($"Client PostDataJson: '{clientMessage.PostDataJson}'");
|
||||
|
||||
// Step 2: Serialize to MessagePack (client sends)
|
||||
Console.WriteLine("\n=== Step 2: MessagePack serialization ===");
|
||||
var bytes = clientMessage.ToMessagePack(ContractlessStandardResolver.Options);
|
||||
Console.WriteLine($"MessagePack bytes: {bytes.Length}");
|
||||
|
||||
// Step 3: Server deserializes
|
||||
Console.WriteLine("\n=== Step 3: Server deserializes ===");
|
||||
var serverMessage = bytes.MessagePackTo<SignalPostJsonDataMessage<IdMessage>>(ContractlessStandardResolver.Options);
|
||||
Console.WriteLine($"Server PostDataJson: '{serverMessage.PostDataJson}'");
|
||||
Console.WriteLine($"Server PostData.Ids.Count: {serverMessage.PostData?.Ids.Count}");
|
||||
Console.WriteLine($"Server PostData.Ids[0]: '{serverMessage.PostData?.Ids[0]}'");
|
||||
|
||||
// Step 4: Server deserializes parameter
|
||||
Console.WriteLine("\n=== Step 4: Server deserializes parameter ===");
|
||||
var paramJson = serverMessage.PostData.Ids[0];
|
||||
Console.WriteLine($"Parameter JSON: '{paramJson}'");
|
||||
var paramValue = AcJsonDeserializer.Deserialize(paramJson, testValue.GetType());
|
||||
Console.WriteLine($"Deserialized int value: {paramValue}");
|
||||
Console.WriteLine($"Deserialized value: {paramValue}");
|
||||
|
||||
// Step 5: Service method returns string
|
||||
Console.WriteLine("\n=== Step 5: Service method returns ===");
|
||||
var serviceResult = $"{paramValue}"; // Like HandleSingleInt does
|
||||
var serviceResult = $"{paramValue}";
|
||||
Console.WriteLine($"Service result: '{serviceResult}'");
|
||||
|
||||
// Step 6: Server creates response
|
||||
Console.WriteLine("\n=== Step 6: Server creates response ===");
|
||||
var response = new SignalResponseJsonMessage(100, SignalResponseStatus.Success, serviceResult);
|
||||
Console.WriteLine($"Response.ResponseData: '{response.ResponseData}'");
|
||||
var response = new SignalResponseDataMessage(100, SignalResponseStatus.Success, serviceResult, AcJsonSerializerOptions.Default);
|
||||
Console.WriteLine($"Response created with Binary bytes: {response.ResponseDataBin?.Length ?? 0}");
|
||||
|
||||
// Step 7: Serialize response to MessagePack
|
||||
Console.WriteLine("\n=== Step 7: Response MessagePack ===");
|
||||
var responseBytes = response.ToMessagePack(ContractlessStandardResolver.Options);
|
||||
Console.WriteLine($"Response MessagePack bytes: {responseBytes.Length}");
|
||||
Console.WriteLine("\n=== Step 7: Response Binary ===");
|
||||
var responseBytes = response.ToBinary();
|
||||
Console.WriteLine($"Response Binary bytes: {responseBytes.Length}");
|
||||
|
||||
// Step 8: Client deserializes response
|
||||
Console.WriteLine("\n=== Step 8: Client deserializes response ===");
|
||||
var clientResponse = responseBytes.MessagePackTo<SignalResponseJsonMessage>(ContractlessStandardResolver.Options);
|
||||
Console.WriteLine($"Client ResponseData: '{clientResponse.ResponseData}'");
|
||||
var clientResponse = responseBytes.BinaryTo<SignalResponseDataMessage>();
|
||||
Console.WriteLine($"Client Response Status: {clientResponse?.Status}");
|
||||
|
||||
// Step 9: Client deserializes to target type (string)
|
||||
Console.WriteLine("\n=== Step 9: Client deserializes to string ===");
|
||||
try
|
||||
{
|
||||
var finalResult = clientResponse.ResponseData.JsonTo<string>();
|
||||
|
||||
Console.WriteLine($"Final result: '{finalResult}'");
|
||||
Assert.AreEqual(GetValueByType(testValue).ToString(), finalResult);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Console.WriteLine($"ERROR: {ex.Message}");
|
||||
throw;
|
||||
}
|
||||
var finalResult = clientResponse?.GetResponseData<string>();
|
||||
Console.WriteLine($"Final result: '{finalResult}'");
|
||||
Assert.AreEqual(GetValueByType(testValue).ToString(), finalResult);
|
||||
}
|
||||
|
||||
private static ISignalRMessage CreatePostMessageTest<TPostData>(TPostData postData)
|
||||
|
|
@ -121,9 +99,7 @@ public class PostJsonDataMessageTests
|
|||
var type = typeof(TPostData);
|
||||
|
||||
if (type == typeof(string) || type.IsEnum || type.IsValueType || type == typeof(DateTime))
|
||||
{
|
||||
return new SignalPostJsonDataMessage<IdMessage>(new IdMessage(postData!));
|
||||
}
|
||||
|
||||
return new SignalPostJsonDataMessage<TPostData>(postData);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,30 +0,0 @@
|
|||
using AyCode.Services.SignalRs;
|
||||
|
||||
namespace AyCode.Services.Tests.SignalRs;
|
||||
|
||||
/// <summary>
|
||||
/// SignalR message tags for client testing.
|
||||
/// </summary>
|
||||
public static class TestClientTags
|
||||
{
|
||||
// Basic operations
|
||||
public const int Ping = 1;
|
||||
public const int Echo = 2;
|
||||
public const int GetStatus = 3;
|
||||
|
||||
// CRUD operations
|
||||
public const int GetById = 10;
|
||||
public const int GetAll = 11;
|
||||
public const int Create = 12;
|
||||
public const int Update = 13;
|
||||
public const int Delete = 14;
|
||||
|
||||
// Complex operations
|
||||
public const int GetOrderWithItems = 20;
|
||||
public const int PostOrder = 21;
|
||||
public const int GetMultipleParams = 22;
|
||||
|
||||
// Error scenarios
|
||||
public const int NotFound = 100;
|
||||
public const int ServerError = 101;
|
||||
}
|
||||
|
|
@ -109,8 +109,8 @@ public class TestableSignalRClient : AcSignalRClientBase
|
|||
/// </summary>
|
||||
public Task SimulateServerResponse(int requestId, int messageTag, SignalResponseStatus status, object? data = null)
|
||||
{
|
||||
var response = new SignalResponseJsonMessage(messageTag, status, data);
|
||||
var bytes = response.ToMessagePack(ContractlessStandardResolver.Options);
|
||||
var response = new SignalResponseDataMessage(messageTag, status, data, AcJsonSerializerOptions.Default);
|
||||
var bytes = response.ToBinary();
|
||||
return OnReceiveMessage(messageTag, bytes, requestId);
|
||||
}
|
||||
|
||||
|
|
@ -217,11 +217,11 @@ public record ReceivedClientMessage(int MessageTag, byte[] MessageBytes)
|
|||
/// <summary>
|
||||
/// Deserializes the message as a response.
|
||||
/// </summary>
|
||||
public SignalResponseJsonMessage? AsResponse()
|
||||
public SignalResponseDataMessage? AsResponse()
|
||||
{
|
||||
try
|
||||
{
|
||||
return MessageBytes.MessagePackTo<SignalResponseJsonMessage>(ContractlessStandardResolver.Options);
|
||||
return MessageBytes.BinaryTo<SignalResponseDataMessage>();
|
||||
}
|
||||
catch
|
||||
{
|
||||
|
|
|
|||
|
|
@ -5,11 +5,8 @@ using AyCode.Core.Helpers;
|
|||
using AyCode.Core.Loggers;
|
||||
using AyCode.Interfaces.Entities;
|
||||
using MessagePack.Resolvers;
|
||||
using Microsoft.AspNetCore.Connections;
|
||||
using Microsoft.AspNetCore.Http.Connections;
|
||||
using Microsoft.AspNetCore.SignalR.Client;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using static AyCode.Core.Extensions.JsonUtilities;
|
||||
|
||||
namespace AyCode.Services.SignalRs
|
||||
{
|
||||
|
|
@ -20,7 +17,6 @@ namespace AyCode.Services.SignalRs
|
|||
protected readonly HubConnection? HubConnection;
|
||||
protected readonly AcLoggerBase Logger;
|
||||
|
||||
//protected event Action<int, byte[], int?> OnMessageReceived = null!;
|
||||
protected abstract Task MessageReceived(int messageTag, byte[] messageBytes);
|
||||
|
||||
public int MsDelay = 25;
|
||||
|
|
@ -30,67 +26,30 @@ namespace AyCode.Services.SignalRs
|
|||
public int TransportSendTimeout = 60000;
|
||||
private const string TagsName = "SignalRTags";
|
||||
|
||||
/// <summary>
|
||||
/// Production constructor - creates and starts HubConnection.
|
||||
/// </summary>
|
||||
protected AcSignalRClientBase(string fullHubName, AcLoggerBase logger)
|
||||
{
|
||||
Logger = logger;
|
||||
Logger.Detail(fullHubName);
|
||||
|
||||
//TODO: HubConnectionBuilder constructor!!! - J.
|
||||
HubConnection = new HubConnectionBuilder()
|
||||
//.WithUrl(fullHubName)
|
||||
.WithUrl(fullHubName, HttpTransportType.WebSockets,
|
||||
options =>
|
||||
{
|
||||
options.TransportMaxBufferSize = 30_000_000; //Increasing this value allows the client to receive larger messages. default: 65KB; unlimited: 0;;
|
||||
options.ApplicationMaxBufferSize = 30_000_000; //Increasing this value allows the client to send larger messages. default: 65KB; unlimited: 0;
|
||||
options.CloseTimeout = TimeSpan.FromSeconds(10); //default: 5 sec.
|
||||
options.SkipNegotiation = true; // Skip HTTP negotiation when using WebSockets only
|
||||
|
||||
//options.AccessTokenProvider = null;
|
||||
//options.HttpMessageHandlerFactory = null;
|
||||
//options.Headers["CustomData"] = "value";
|
||||
//options.SkipNegotiation = true;
|
||||
//options.ClientCertificates = new System.Security.Cryptography.X509Certificates.X509CertificateCollection();
|
||||
//options.Cookies = new System.Net.CookieContainer();
|
||||
//options.DefaultTransferFormat = TransferFormat.Text;
|
||||
//options.Credentials = null;
|
||||
//options.Proxy = null;
|
||||
//options.UseDefaultCredentials = true;
|
||||
//options.WebSocketConfiguration = null;
|
||||
//options.WebSocketFactory = null;
|
||||
options.TransportMaxBufferSize = 30_000_000;
|
||||
options.ApplicationMaxBufferSize = 30_000_000;
|
||||
options.CloseTimeout = TimeSpan.FromSeconds(10);
|
||||
options.SkipNegotiation = true;
|
||||
})
|
||||
//.ConfigureLogging(logging =>
|
||||
//{
|
||||
// logging.SetMinimumLevel(Microsoft.Extensions.Logging.LogLevel.Trace);
|
||||
// logging.AddConsole();
|
||||
//})
|
||||
.WithAutomaticReconnect()
|
||||
.WithStatefulReconnect()
|
||||
.WithKeepAliveInterval(TimeSpan.FromSeconds(60))
|
||||
.WithServerTimeout(TimeSpan.FromSeconds(180))
|
||||
//.AddMessagePackProtocol(options => {
|
||||
// options.SerializerOptions = MessagePackSerializerOptions.Standard
|
||||
// .WithResolver(MessagePack.Resolvers.StandardResolver.Instance)
|
||||
// .WithSecurity(MessagePackSecurity.UntrustedData)
|
||||
// .WithCompression(MessagePackCompression.Lz4Block)
|
||||
// .WithCompressionMinLength(256);})
|
||||
.Build();
|
||||
|
||||
HubConnection.Closed += HubConnection_Closed;
|
||||
|
||||
_ = HubConnection.On<int, byte[], int?>(nameof(IAcSignalRHubClient.OnReceiveMessage), OnReceiveMessage);
|
||||
//_ = HubConnection.On<int, int>(nameof(IAcSignalRHubClient.OnRequestMessage), OnRequestMessage);
|
||||
|
||||
//HubConnection.StartAsync().Forget();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Test constructor - allows testing without real HubConnection.
|
||||
/// Override virtual methods to control behavior in tests.
|
||||
/// </summary>
|
||||
protected AcSignalRClientBase(AcLoggerBase logger)
|
||||
{
|
||||
Logger = logger;
|
||||
|
|
@ -99,96 +58,40 @@ namespace AyCode.Services.SignalRs
|
|||
|
||||
private Task HubConnection_Closed(Exception? arg)
|
||||
{
|
||||
if (_responseByRequestId.IsEmpty) Logger.DebugConditional($"Client HubConnection_Closed");
|
||||
if (_responseByRequestId.IsEmpty) Logger.DebugConditional("Client HubConnection_Closed");
|
||||
else Logger.Warning($"Client HubConnection_Closed; {nameof(_responseByRequestId)} count: {_responseByRequestId.Count}");
|
||||
|
||||
ClearPendingRequests();
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
#region Connection State Methods (virtual for testing)
|
||||
#region Connection State Methods
|
||||
|
||||
/// <summary>
|
||||
/// Gets the current connection state. Override in tests.
|
||||
/// </summary>
|
||||
protected virtual HubConnectionState GetConnectionState()
|
||||
=> HubConnection?.State ?? HubConnectionState.Disconnected;
|
||||
|
||||
/// <summary>
|
||||
/// Checks if the connection is connected. Override in tests.
|
||||
/// </summary>
|
||||
protected virtual bool IsConnected()
|
||||
=> GetConnectionState() == HubConnectionState.Connected;
|
||||
|
||||
/// <summary>
|
||||
/// Starts the connection. Override in tests to avoid real connection.
|
||||
/// </summary>
|
||||
protected virtual Task StartConnectionInternal()
|
||||
{
|
||||
if (HubConnection == null) return Task.CompletedTask;
|
||||
return HubConnection.StartAsync();
|
||||
}
|
||||
=> HubConnection?.StartAsync() ?? Task.CompletedTask;
|
||||
|
||||
/// <summary>
|
||||
/// Stops the connection. Override in tests.
|
||||
/// </summary>
|
||||
protected virtual Task StopConnectionInternal()
|
||||
{
|
||||
if (HubConnection == null) return Task.CompletedTask;
|
||||
return HubConnection.StopAsync();
|
||||
}
|
||||
=> HubConnection?.StopAsync() ?? Task.CompletedTask;
|
||||
|
||||
/// <summary>
|
||||
/// Disposes the connection. Override in tests.
|
||||
/// </summary>
|
||||
protected virtual ValueTask DisposeConnectionInternal()
|
||||
{
|
||||
if (HubConnection == null) return ValueTask.CompletedTask;
|
||||
return HubConnection.DisposeAsync();
|
||||
}
|
||||
=> HubConnection?.DisposeAsync() ?? ValueTask.CompletedTask;
|
||||
|
||||
/// <summary>
|
||||
/// Sends a message to the server via HubConnection. Override in tests.
|
||||
/// </summary>
|
||||
protected virtual Task SendToHubAsync(int messageTag, byte[]? messageBytes, int? requestId)
|
||||
{
|
||||
if (HubConnection == null) return Task.CompletedTask;
|
||||
return HubConnection.SendAsync(nameof(IAcSignalRHubClient.OnReceiveMessage), messageTag, messageBytes, requestId);
|
||||
}
|
||||
=> HubConnection?.SendAsync(nameof(IAcSignalRHubClient.OnReceiveMessage), messageTag, messageBytes, requestId) ?? Task.CompletedTask;
|
||||
|
||||
#endregion
|
||||
|
||||
#region Protected Test Helpers
|
||||
|
||||
/// <summary>
|
||||
/// Gets the pending requests dictionary for testing.
|
||||
/// </summary>
|
||||
protected ConcurrentDictionary<int, SignalRRequestModel> GetPendingRequests()
|
||||
=> _responseByRequestId;
|
||||
|
||||
/// <summary>
|
||||
/// Clears all pending requests.
|
||||
/// </summary>
|
||||
protected void ClearPendingRequests()
|
||||
=> _responseByRequestId.Clear();
|
||||
|
||||
/// <summary>
|
||||
/// Registers a pending request for testing.
|
||||
/// </summary>
|
||||
protected void RegisterPendingRequest(int requestId, SignalRRequestModel model)
|
||||
=> _responseByRequestId[requestId] = model;
|
||||
|
||||
/// <summary>
|
||||
/// Simulates receiving a response for testing.
|
||||
/// </summary>
|
||||
protected void SimulateResponse(int requestId, ISignalResponseMessage<string> response)
|
||||
{
|
||||
if (_responseByRequestId.TryGetValue(requestId, out var model))
|
||||
{
|
||||
model.ResponseByRequestId = response;
|
||||
model.ResponseDateTime = DateTime.UtcNow;
|
||||
}
|
||||
}
|
||||
protected ConcurrentDictionary<int, SignalRRequestModel> GetPendingRequests() => _responseByRequestId;
|
||||
protected void ClearPendingRequests() => _responseByRequestId.Clear();
|
||||
protected void RegisterPendingRequest(int requestId, SignalRRequestModel model) => _responseByRequestId[requestId] = model;
|
||||
|
||||
#endregion
|
||||
|
||||
|
|
@ -228,105 +131,82 @@ namespace AyCode.Services.SignalRs
|
|||
}
|
||||
|
||||
#region CRUD
|
||||
public virtual Task<TResponseData?> PostAsync<TResponseData>(int messageTag, object parameter) //where TResponseData : class
|
||||
|
||||
public virtual Task<TResponseData?> PostAsync<TResponseData>(int messageTag, object parameter)
|
||||
=> SendMessageToServerAsync<TResponseData>(messageTag, new SignalPostJsonDataMessage<IdMessage>(new IdMessage(parameter)), GetNextRequestId());
|
||||
|
||||
public virtual Task<TResponseData?> PostAsync<TResponseData>(int messageTag, object[] parameters) //where TResponseData : class
|
||||
public virtual Task<TResponseData?> PostAsync<TResponseData>(int messageTag, object[] parameters)
|
||||
=> SendMessageToServerAsync<TResponseData>(messageTag, new SignalPostJsonDataMessage<IdMessage>(new IdMessage(parameters)), GetNextRequestId());
|
||||
|
||||
public virtual Task<TResponseData?> GetByIdAsync<TResponseData>(int messageTag, object id) //where TResponseData : class
|
||||
public virtual Task<TResponseData?> GetByIdAsync<TResponseData>(int messageTag, object id)
|
||||
=> PostAsync<TResponseData?>(messageTag, id);
|
||||
|
||||
public virtual Task GetByIdAsync<TResponseData>(int messageTag, Func<ISignalResponseMessage<TResponseData?>, Task> responseCallback, object id)
|
||||
=> SendMessageToServerAsync(messageTag, new SignalPostJsonDataMessage<IdMessage>(new IdMessage(id)), responseCallback);
|
||||
|
||||
public virtual Task<TResponseData?> GetByIdAsync<TResponseData>(int messageTag, object[] ids) //where TResponseData : class
|
||||
public virtual Task<TResponseData?> GetByIdAsync<TResponseData>(int messageTag, object[] ids)
|
||||
=> PostAsync<TResponseData?>(messageTag, ids);
|
||||
|
||||
public virtual Task GetByIdAsync<TResponseData>(int messageTag, Func<ISignalResponseMessage<TResponseData?>, Task> responseCallback, object[] ids)
|
||||
=> SendMessageToServerAsync(messageTag, new SignalPostJsonDataMessage<IdMessage>(new IdMessage(ids)), responseCallback);
|
||||
|
||||
public virtual Task<TResponseData?> GetAllAsync<TResponseData>(int messageTag) //where TResponseData : class
|
||||
public virtual Task<TResponseData?> GetAllAsync<TResponseData>(int messageTag)
|
||||
=> SendMessageToServerAsync<TResponseData>(messageTag);
|
||||
public virtual Task GetAllAsync<TResponseData>(int messageTag, Func<ISignalResponseMessage<TResponseData?>, Task> responseCallback)
|
||||
=> SendMessageToServerAsync(messageTag, null, responseCallback);
|
||||
|
||||
public virtual Task GetAllAsync<TResponseData>(int messageTag, Func<ISignalResponseMessage<TResponseData?>, Task> responseCallback, object[]? contextParams)
|
||||
=> SendMessageToServerAsync(messageTag, (contextParams == null || contextParams.Length == 0 ? null : new SignalPostJsonDataMessage<IdMessage>(new IdMessage(contextParams))), responseCallback);
|
||||
public virtual Task<TResponseData?> GetAllAsync<TResponseData>(int messageTag, object[]? contextParams) //where TResponseData : class
|
||||
public virtual Task<TResponseData?> GetAllAsync<TResponseData>(int messageTag, object[]? contextParams)
|
||||
=> SendMessageToServerAsync<TResponseData>(messageTag, contextParams == null || contextParams.Length == 0 ? null : new SignalPostJsonDataMessage<IdMessage>(new IdMessage(contextParams)), GetNextRequestId());
|
||||
|
||||
public virtual Task<TPostData?> PostDataAsync<TPostData>(int messageTag, TPostData postData) where TPostData : class
|
||||
=> SendMessageToServerAsync<TPostData>(messageTag, CreatePostMessage(postData), GetNextRequestId());
|
||||
public virtual Task<TResponseData?> PostDataAsync<TPostData, TResponseData>(int messageTag, TPostData postData) //where TPostData : class where TResponseData : class
|
||||
|
||||
public virtual Task<TResponseData?> PostDataAsync<TPostData, TResponseData>(int messageTag, TPostData postData)
|
||||
=> SendMessageToServerAsync<TResponseData>(messageTag, CreatePostMessage(postData), GetNextRequestId());
|
||||
|
||||
public virtual Task PostDataAsync<TPostData>(int messageTag, TPostData postData, Func<ISignalResponseMessage<TPostData?>, Task> responseCallback) //where TPostData : class
|
||||
=> SendMessageToServerAsync(messageTag, CreatePostMessage(postData), responseCallback);
|
||||
public virtual Task PostDataAsync<TPostData, TResponseData>(int messageTag, TPostData postData, Func<ISignalResponseMessage<TResponseData?>, Task> responseCallback) //where TPostData : class where TResponseData : class
|
||||
=> SendMessageToServerAsync(messageTag, CreatePostMessage(postData), responseCallback);
|
||||
|
||||
/// <summary>
|
||||
/// Creates the appropriate message wrapper for the post data.
|
||||
/// Primitives, strings, enums, and value types are wrapped in IdMessage.
|
||||
/// Complex objects are sent directly in SignalPostJsonDataMessage.
|
||||
/// Posts data and invokes callback with response. Fire-and-forget friendly for background saves.
|
||||
/// </summary>
|
||||
public virtual Task PostDataAsync<TPostData>(int messageTag, TPostData postData, Action<SignalResponseDataMessage> responseCallback)
|
||||
{
|
||||
var requestId = GetNextRequestId();
|
||||
var requestModel = SignalRRequestModelPool.Get(new Action<ISignalResponseMessage>(response =>
|
||||
{
|
||||
if (response is SignalResponseDataMessage dataMsg)
|
||||
responseCallback(dataMsg);
|
||||
else
|
||||
Logger.Error($"PostDataAsync callback received unexpected message type: {response.GetType().Name}");
|
||||
}));
|
||||
|
||||
_responseByRequestId[requestId] = requestModel;
|
||||
return SendMessageToServerAsync(messageTag, CreatePostMessage(postData), requestId);
|
||||
}
|
||||
|
||||
private static ISignalRMessage CreatePostMessage<TPostData>(TPostData postData)
|
||||
{
|
||||
var type = typeof(TPostData);
|
||||
|
||||
// Primitives, strings, enums, and value types should use IdMessage format
|
||||
if (IsPrimitiveOrStringOrEnum(type))
|
||||
{
|
||||
if (type == typeof(string) || type.IsEnum || type.IsValueType)
|
||||
return new SignalPostJsonDataMessage<IdMessage>(new IdMessage(postData!));
|
||||
}
|
||||
|
||||
// Complex objects use direct serialization
|
||||
return new SignalPostJsonDataMessage<TPostData>(postData);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Determines if a type should use IdMessage format (primitives, strings, enums, value types).
|
||||
/// Must match the logic in AcWebSignalRHubBase.IsPrimitiveOrStringOrEnum.
|
||||
/// NOTE: Arrays and collections are NOT included here - they are complex objects for PostDataAsync.
|
||||
/// </summary>
|
||||
private static bool IsPrimitiveOrStringOrEnum(Type type)
|
||||
{
|
||||
return type == typeof(string) ||
|
||||
type.IsEnum ||
|
||||
type.IsValueType ||
|
||||
type == typeof(DateTime);
|
||||
}
|
||||
|
||||
public Task GetAllIntoAsync<TResponseItem>(List<TResponseItem> intoList, int messageTag, object[]? contextParams = null, Action? callback = null) where TResponseItem : IEntityGuid
|
||||
{
|
||||
return GetAllAsync<List<TResponseItem>>(messageTag, response =>
|
||||
return GetAllAsync<List<TResponseItem>>(messageTag, contextParams).ContinueWith(task =>
|
||||
{
|
||||
var logText = $"GetAllIntoAsync<{typeof(TResponseItem).Name}>(); status: {response.Status}; dataCount: {response.ResponseData?.Count}; {ConstHelper.NameByValue(TagsName, messageTag)};";
|
||||
|
||||
var logText = $"GetAllIntoAsync<{typeof(TResponseItem).Name}>(); dataCount: {task.Result?.Count}; {ConstHelper.NameByValue(TagsName, messageTag)};";
|
||||
intoList.Clear();
|
||||
|
||||
if (response.Status == SignalResponseStatus.Success && response.ResponseData != null)
|
||||
if (task.Result != null)
|
||||
{
|
||||
Logger.Debug(logText);
|
||||
intoList.AddRange(response.ResponseData);
|
||||
intoList.AddRange(task.Result);
|
||||
}
|
||||
else Logger.Error(logText);
|
||||
|
||||
callback?.Invoke();
|
||||
return Task.CompletedTask;
|
||||
}, contextParams);
|
||||
}, TaskScheduler.Default);
|
||||
}
|
||||
|
||||
#endregion CRUD
|
||||
#endregion
|
||||
|
||||
public virtual Task<TResponse?> SendMessageToServerAsync<TResponse>(int messageTag) //where TResponse : class
|
||||
public virtual Task<TResponse?> SendMessageToServerAsync<TResponse>(int messageTag)
|
||||
=> SendMessageToServerAsync<TResponse>(messageTag, null, GetNextRequestId());
|
||||
|
||||
public virtual Task<TResponse?> SendMessageToServerAsync<TResponse>(int messageTag, ISignalRMessage? message) //where TResponse : class
|
||||
public virtual Task<TResponse?> SendMessageToServerAsync<TResponse>(int messageTag, ISignalRMessage? message)
|
||||
=> SendMessageToServerAsync<TResponse>(messageTag, message, GetNextRequestId());
|
||||
|
||||
protected virtual async Task<TResponse?> SendMessageToServerAsync<TResponse>(int messageTag, ISignalRMessage? message, int requestId) //where TResponse : class
|
||||
protected virtual async Task<TResponse?> SendMessageToServerAsync<TResponse>(int messageTag, ISignalRMessage? message, int requestId)
|
||||
{
|
||||
Logger.DebugConditional($"Client SendMessageToServerAsync<TResult>; {nameof(requestId)}: {requestId}; {ConstHelper.NameByValue(TagsName, messageTag)}");
|
||||
|
||||
|
|
@ -339,7 +219,7 @@ namespace AyCode.Services.SignalRs
|
|||
try
|
||||
{
|
||||
if (await TaskHelper.WaitToAsync(() => _responseByRequestId[requestId].ResponseByRequestId != null, TransportSendTimeout, MsDelay, MsFirstDelay) &&
|
||||
_responseByRequestId.TryRemove(requestId, out var obj) && obj.ResponseByRequestId is ISignalResponseMessage responseMessage)
|
||||
_responseByRequestId.TryRemove(requestId, out var obj) && obj.ResponseByRequestId is SignalResponseDataMessage responseMessage)
|
||||
{
|
||||
startTime = obj.RequestDateTime;
|
||||
SignalRRequestModelPool.Return(obj);
|
||||
|
|
@ -351,20 +231,15 @@ namespace AyCode.Services.SignalRs
|
|||
return await Task.FromException<TResponse>(new Exception(errorText));
|
||||
}
|
||||
|
||||
var responseData = DeserializeResponseData<TResponse>(responseMessage);
|
||||
var responseData = responseMessage.GetResponseData<TResponse>();
|
||||
|
||||
if (responseData == null && responseMessage.Status == SignalResponseStatus.Success)
|
||||
{
|
||||
// Null response is valid for Success status
|
||||
Logger.Info($"Client received null response. Total: {(DateTime.UtcNow.Subtract(startTime)).TotalMilliseconds} ms! requestId: {requestId}; tag: {messageTag} [{ConstHelper.NameByValue(TagsName, messageTag)}]");
|
||||
return default;
|
||||
}
|
||||
|
||||
var serializerType = responseMessage switch
|
||||
{
|
||||
SignalResponseBinaryMessage => "Binary",
|
||||
_ => "JSON"
|
||||
};
|
||||
var serializerType = responseMessage.DataSerializerType == AcSerializerType.Binary ? "Binary" : "JSON";
|
||||
Logger.Info($"Client deserialized response ({serializerType}). Total: {(DateTime.UtcNow.Subtract(startTime)).TotalMilliseconds} ms! requestId: {requestId}; tag: {messageTag} [{ConstHelper.NameByValue(TagsName, messageTag)}]");
|
||||
return responseData;
|
||||
}
|
||||
|
|
@ -377,62 +252,11 @@ namespace AyCode.Services.SignalRs
|
|||
}
|
||||
|
||||
if (_responseByRequestId.TryRemove(requestId, out var removedModel))
|
||||
{
|
||||
SignalRRequestModelPool.Return(removedModel);
|
||||
}
|
||||
|
||||
return default;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Deserializes response data from either JSON or Binary format.
|
||||
/// Automatically detects the format based on the response message type.
|
||||
/// </summary>
|
||||
private static TResponse? DeserializeResponseData<TResponse>(ISignalResponseMessage responseMessage)
|
||||
{
|
||||
return responseMessage switch
|
||||
{
|
||||
SignalResponseBinaryMessage binaryMsg when binaryMsg.ResponseData != null
|
||||
=> binaryMsg.ResponseData.BinaryTo<TResponse>(),
|
||||
|
||||
SignalResponseJsonMessage jsonMsg when !string.IsNullOrEmpty(jsonMsg.ResponseData)
|
||||
=> jsonMsg.ResponseData.JsonTo<TResponse>(),
|
||||
|
||||
ISignalResponseMessage<string> stringMsg when !string.IsNullOrEmpty(stringMsg.ResponseData)
|
||||
=> stringMsg.ResponseData.JsonTo<TResponse>(),
|
||||
|
||||
_ => default
|
||||
};
|
||||
}
|
||||
|
||||
public virtual Task SendMessageToServerAsync<TResponseData>(int messageTag, Func<ISignalResponseMessage<TResponseData?>, Task> responseCallback)
|
||||
=> SendMessageToServerAsync(messageTag, null, responseCallback);
|
||||
|
||||
public virtual Task SendMessageToServerAsync<TResponseData>(int messageTag, ISignalRMessage? message, Func<ISignalResponseMessage<TResponseData?>, Task> responseCallback)
|
||||
{
|
||||
if (messageTag == 0) Logger.Error($"SendMessageToServerAsync; messageTag == 0");
|
||||
|
||||
var requestId = GetNextRequestId();
|
||||
var requestModel = SignalRRequestModelPool.Get(new Action<ISignalResponseMessage>(responseMessage =>
|
||||
{
|
||||
TResponseData? responseData = default;
|
||||
|
||||
if (responseMessage.Status == SignalResponseStatus.Success)
|
||||
{
|
||||
responseData = DeserializeResponseData<TResponseData>(responseMessage);
|
||||
}
|
||||
else Logger.Error($"Client SendMessageToServerAsync<TResponseData> response error; callback; Status: {responseMessage.Status}; ConnectionState: {GetConnectionState()}; requestId: {requestId}; {ConstHelper.NameByValue(TagsName, messageTag)}");
|
||||
|
||||
responseCallback(new SignalResponseMessage<TResponseData?>(messageTag, responseMessage.Status, responseData));
|
||||
}));
|
||||
|
||||
_responseByRequestId[requestId] = requestModel;
|
||||
|
||||
return SendMessageToServerAsync(messageTag, message, requestId);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the next unique request ID.
|
||||
/// </summary>
|
||||
protected virtual int GetNextRequestId() => AcDomain.NextUniqueInt32;
|
||||
|
||||
public virtual Task OnReceiveMessage(int messageTag, byte[] messageBytes, int? requestId)
|
||||
|
|
@ -443,64 +267,44 @@ namespace AyCode.Services.SignalRs
|
|||
|
||||
try
|
||||
{
|
||||
if (requestId.HasValue && _responseByRequestId.ContainsKey(requestId.Value))
|
||||
if (requestId.HasValue && _responseByRequestId.TryGetValue(requestId.Value, out var requestModel))
|
||||
{
|
||||
var reqId = requestId.Value;
|
||||
requestModel.ResponseDateTime = DateTime.UtcNow;
|
||||
Logger.Debug($"[{requestModel.ResponseDateTime.Subtract(requestModel.RequestDateTime).TotalMilliseconds:N0}ms][{messageBytes.Length / 1024}kb]{logText}");
|
||||
|
||||
_responseByRequestId[reqId].ResponseDateTime = DateTime.UtcNow;
|
||||
Logger.Debug($"[{_responseByRequestId[reqId].ResponseDateTime.Subtract(_responseByRequestId[reqId].RequestDateTime).TotalMilliseconds:N0}ms][{(messageBytes.Length / 1024)}kb]{logText}");
|
||||
var responseMessage = messageBytes.BinaryTo<SignalResponseDataMessage>() ?? new SignalResponseDataMessage();
|
||||
|
||||
var responseMessage = DeserializeResponseMessage(messageBytes);
|
||||
|
||||
switch (_responseByRequestId[reqId].ResponseByRequestId)
|
||||
switch (requestModel.ResponseByRequestId)
|
||||
{
|
||||
case null:
|
||||
_responseByRequestId[reqId].ResponseByRequestId = responseMessage;
|
||||
requestModel.ResponseByRequestId = responseMessage;
|
||||
return Task.CompletedTask;
|
||||
|
||||
case Action<ISignalResponseMessage> messageCallback:
|
||||
if (_responseByRequestId.TryRemove(reqId, out var callbackModel))
|
||||
{
|
||||
SignalRRequestModelPool.Return(callbackModel);
|
||||
}
|
||||
|
||||
messageCallback.Invoke(responseMessage);
|
||||
return Task.CompletedTask;
|
||||
|
||||
// Legacy support for string-based callbacks
|
||||
case Action<ISignalResponseMessage<string>> stringCallback when responseMessage is SignalResponseJsonMessage jsonMsg:
|
||||
if (_responseByRequestId.TryRemove(reqId, out var legacyModel))
|
||||
{
|
||||
SignalRRequestModelPool.Return(legacyModel);
|
||||
}
|
||||
|
||||
stringCallback.Invoke(jsonMsg);
|
||||
return Task.CompletedTask;
|
||||
|
||||
default:
|
||||
Logger.Error($"Client OnReceiveMessage switch; unknown message type: {_responseByRequestId[reqId].ResponseByRequestId?.GetType().Name}; {ConstHelper.NameByValue(TagsName, messageTag)}");
|
||||
Logger.Error($"Client OnReceiveMessage switch; unknown message type: {requestModel.ResponseByRequestId?.GetType().Name}; {ConstHelper.NameByValue(TagsName, messageTag)}");
|
||||
break;
|
||||
}
|
||||
|
||||
if (_responseByRequestId.TryRemove(reqId, out var removedModel))
|
||||
{
|
||||
SignalRRequestModelPool.Return(removedModel);
|
||||
}
|
||||
|
||||
// Request-response hibás eset - ne hívjuk meg a MessageReceived-et
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
// Csak broadcast/notification üzeneteknél hívjuk meg a MessageReceived-et
|
||||
Logger.Info(logText);
|
||||
MessageReceived(messageTag, messageBytes).Forget();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
if (requestId.HasValue && _responseByRequestId.TryRemove(requestId.Value, out var exModel))
|
||||
{
|
||||
SignalRRequestModelPool.Return(exModel);
|
||||
}
|
||||
|
||||
Logger.Error($"Client OnReceiveMessage; ConnectionState: {GetConnectionState()}; requestId: {requestId}; {ex.Message}; {ConstHelper.NameByValue(TagsName, messageTag)}", ex);
|
||||
throw;
|
||||
|
|
@ -508,33 +312,5 @@ namespace AyCode.Services.SignalRs
|
|||
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Deserializes a MessagePack response to the appropriate message type (JSON or Binary).
|
||||
/// Uses DetectSerializerTypeFromBytes to determine the format of the ResponseData.
|
||||
/// </summary>
|
||||
protected virtual ISignalResponseMessage DeserializeResponseMessage(byte[] messageBytes)
|
||||
{
|
||||
// First, try to deserialize as Binary message to check the ResponseData format
|
||||
try
|
||||
{
|
||||
var binaryMsg = messageBytes.MessagePackTo<SignalResponseBinaryMessage>(ContractlessStandardResolver.Options);
|
||||
if (binaryMsg.ResponseData != null && binaryMsg.ResponseData.Length > 0)
|
||||
{
|
||||
// Use the existing utility to detect if ResponseData is Binary format
|
||||
if (DetectSerializerTypeFromBytes(binaryMsg.ResponseData) == AcSerializerType.Binary)
|
||||
{
|
||||
return binaryMsg;
|
||||
}
|
||||
}
|
||||
}
|
||||
catch
|
||||
{
|
||||
// Failed to deserialize as Binary message
|
||||
}
|
||||
|
||||
// Fall back to JSON format
|
||||
return messageBytes.MessagePackTo<SignalResponseJsonMessage>(ContractlessStandardResolver.Options);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,10 +1,9 @@
|
|||
using AyCode.Core.Extensions;
|
||||
using MessagePack;
|
||||
using Newtonsoft.Json.Linq;
|
||||
using System.Text.RegularExpressions;
|
||||
using AyCode.Core.Interfaces;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq.Expressions;
|
||||
using AyCode.Core.Compression;
|
||||
using JsonIgnoreAttribute = Newtonsoft.Json.JsonIgnoreAttribute;
|
||||
using STJIgnore = System.Text.Json.Serialization.JsonIgnoreAttribute;
|
||||
|
||||
namespace AyCode.Services.SignalRs;
|
||||
|
||||
|
|
@ -24,7 +23,6 @@ public class IdMessage
|
|||
/// </summary>
|
||||
public IdMessage(object[] ids)
|
||||
{
|
||||
// Pre-allocate capacity to avoid list resizing
|
||||
Ids = new List<string>(ids.Length);
|
||||
for (var i = 0; i < ids.Length; i++)
|
||||
{
|
||||
|
|
@ -38,7 +36,6 @@ public class IdMessage
|
|||
/// </summary>
|
||||
public IdMessage(object id)
|
||||
{
|
||||
// Pre-allocate for single item
|
||||
Ids = new List<string>(1) { id.ToJson() };
|
||||
}
|
||||
|
||||
|
|
@ -48,7 +45,6 @@ public class IdMessage
|
|||
/// </summary>
|
||||
public IdMessage(IEnumerable<Guid> ids)
|
||||
{
|
||||
// Materialize to array once to get count and avoid multiple enumeration
|
||||
var idsArray = ids as Guid[] ?? ids.ToArray();
|
||||
Ids = new List<string>(idsArray.Length);
|
||||
for (var i = 0; i < idsArray.Length; i++)
|
||||
|
|
@ -57,10 +53,7 @@ public class IdMessage
|
|||
}
|
||||
}
|
||||
|
||||
public override string ToString()
|
||||
{
|
||||
return string.Join("; ", Ids);
|
||||
}
|
||||
public override string ToString() => string.Join("; ", Ids);
|
||||
}
|
||||
|
||||
[MessagePackObject]
|
||||
|
|
@ -69,24 +62,24 @@ public class SignalPostJsonMessage
|
|||
[Key(0)]
|
||||
public string PostDataJson { get; set; } = "";
|
||||
|
||||
public SignalPostJsonMessage()
|
||||
{}
|
||||
public SignalPostJsonMessage() { }
|
||||
protected SignalPostJsonMessage(string postDataJson) => PostDataJson = postDataJson;
|
||||
}
|
||||
|
||||
[MessagePackObject(AllowPrivate = false)]
|
||||
public class SignalPostJsonDataMessage<TPostDataType> : SignalPostJsonMessage, ISignalPostMessage<TPostDataType> //where TPostDataType : class
|
||||
public class SignalPostJsonDataMessage<TPostDataType> : SignalPostJsonMessage, ISignalPostMessage<TPostDataType>
|
||||
{
|
||||
[IgnoreMember]
|
||||
[JsonIgnore]
|
||||
[STJIgnore]
|
||||
private TPostDataType? _postData;
|
||||
|
||||
[IgnoreMember]
|
||||
[JsonIgnore]
|
||||
[STJIgnore]
|
||||
public TPostDataType PostData
|
||||
{
|
||||
get
|
||||
{
|
||||
return _postData ??= PostDataJson.JsonTo<TPostDataType>()!;
|
||||
}
|
||||
get => _postData ??= PostDataJson.JsonTo<TPostDataType>()!;
|
||||
private init
|
||||
{
|
||||
_postData = value;
|
||||
|
|
@ -94,11 +87,9 @@ public class SignalPostJsonDataMessage<TPostDataType> : SignalPostJsonMessage, I
|
|||
}
|
||||
}
|
||||
|
||||
public SignalPostJsonDataMessage() : base()
|
||||
{}
|
||||
public SignalPostJsonDataMessage() : base() { }
|
||||
public SignalPostJsonDataMessage(TPostDataType postData) => PostData = postData;
|
||||
public SignalPostJsonDataMessage(string postDataJson) : base(postDataJson)
|
||||
{}
|
||||
public SignalPostJsonDataMessage(string postDataJson) : base(postDataJson) { }
|
||||
}
|
||||
|
||||
[MessagePackObject]
|
||||
|
|
@ -125,148 +116,7 @@ public interface ISignalRequestMessage<TRequestId> : ISignalRMessage
|
|||
TRequestId Id { get; set; }
|
||||
}
|
||||
|
||||
public interface ISignalRMessage
|
||||
{ }
|
||||
|
||||
|
||||
[MessagePackObject]
|
||||
public sealed class SignalResponseJsonMessage : ISignalResponseMessage<string>
|
||||
{
|
||||
[Key(0)] public int MessageTag { get; set; }
|
||||
|
||||
[Key(1)] public SignalResponseStatus Status { get; set; }
|
||||
|
||||
[Key(2)] public string? ResponseData { get; set; } = null;
|
||||
|
||||
[IgnoreMember]
|
||||
public string? ResponseDataJson => ResponseData;
|
||||
|
||||
public SignalResponseJsonMessage(){}
|
||||
|
||||
public SignalResponseJsonMessage(int messageTag, SignalResponseStatus status)
|
||||
{
|
||||
Status = status;
|
||||
MessageTag = messageTag;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates a response with the given data serialized as JSON.
|
||||
/// If responseData is already a JSON string (starts with { or [), it will be used directly.
|
||||
/// All other data types are serialized to JSON format.
|
||||
/// </summary>
|
||||
public SignalResponseJsonMessage(int messageTag, SignalResponseStatus status, object? responseData) : this(messageTag, status)
|
||||
{
|
||||
if (responseData == null)
|
||||
{
|
||||
ResponseData = null;
|
||||
return;
|
||||
}
|
||||
|
||||
// If responseData is already a JSON string, use it directly
|
||||
if (responseData is string strData)
|
||||
{
|
||||
var trimmed = strData.Trim();
|
||||
if (trimmed.Length > 1 && (trimmed[0] == '{' || trimmed[0] == '[') && (trimmed[^1] == '}' || trimmed[^1] == ']'))
|
||||
{
|
||||
// Already JSON - use directly without re-serialization
|
||||
ResponseData = strData;
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// Serialize to JSON
|
||||
ResponseData = responseData.ToJson();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Signal response message with lazy deserialization support.
|
||||
/// ResponseData is only deserialized on first access and cached.
|
||||
/// Use ResponseDataJson for direct JSON access without deserialization.
|
||||
/// </summary>
|
||||
[MessagePackObject(AllowPrivate = false)]
|
||||
public sealed class SignalResponseMessage<TResponseData> : ISignalResponseMessage<TResponseData>
|
||||
{
|
||||
[IgnoreMember]
|
||||
private TResponseData? _responseData;
|
||||
|
||||
[IgnoreMember]
|
||||
private bool _isDeserialized;
|
||||
|
||||
[Key(0)]
|
||||
public int MessageTag { get; set; }
|
||||
|
||||
[Key(1)]
|
||||
public SignalResponseStatus Status { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Raw JSON string. Use this for direct JSON access without triggering deserialization.
|
||||
/// </summary>
|
||||
[Key(2)]
|
||||
public string? ResponseDataJson { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Deserialized response data. Lazy-loaded on first access.
|
||||
/// </summary>
|
||||
[IgnoreMember]
|
||||
public TResponseData? ResponseData
|
||||
{
|
||||
get
|
||||
{
|
||||
if (!_isDeserialized)
|
||||
{
|
||||
_isDeserialized = true;
|
||||
|
||||
_responseData = ResponseDataJson != null
|
||||
? ResponseDataJson.JsonTo<TResponseData>()
|
||||
: default;
|
||||
}
|
||||
|
||||
return _responseData;
|
||||
}
|
||||
set
|
||||
{
|
||||
_isDeserialized = true;
|
||||
_responseData = value;
|
||||
ResponseDataJson = value?.ToJson();
|
||||
}
|
||||
}
|
||||
|
||||
public SignalResponseMessage()
|
||||
{
|
||||
}
|
||||
|
||||
public SignalResponseMessage(int messageTag, SignalResponseStatus status)
|
||||
{
|
||||
MessageTag = messageTag;
|
||||
Status = status;
|
||||
}
|
||||
|
||||
public SignalResponseMessage(int messageTag, SignalResponseStatus status, TResponseData? responseData)
|
||||
: this(messageTag, status)
|
||||
{
|
||||
ResponseData = responseData;
|
||||
}
|
||||
|
||||
public SignalResponseMessage(int messageTag, SignalResponseStatus status, string? responseDataJson)
|
||||
: this(messageTag, status)
|
||||
{
|
||||
ResponseDataJson = responseDataJson;
|
||||
}
|
||||
}
|
||||
|
||||
public interface ISignalResponseMessage<TResponseData> : ISignalResponseMessage
|
||||
{
|
||||
/// <summary>
|
||||
/// Deserialized response data. May trigger lazy deserialization.
|
||||
/// </summary>
|
||||
TResponseData? ResponseData { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Raw JSON string for direct access without deserialization.
|
||||
/// </summary>
|
||||
string? ResponseDataJson { get; }
|
||||
}
|
||||
public interface ISignalRMessage { }
|
||||
|
||||
public interface ISignalResponseMessage : ISignalRMessage
|
||||
{
|
||||
|
|
@ -281,54 +131,154 @@ public enum SignalResponseStatus : byte
|
|||
}
|
||||
|
||||
/// <summary>
|
||||
/// Signal response message with binary serialized data.
|
||||
/// Used when SerializerOptions.SerializerType == Binary for better performance.
|
||||
/// Signal response message with lazy deserialization support.
|
||||
/// Used for callback-based response handling.
|
||||
/// </summary>
|
||||
[MessagePackObject]
|
||||
public sealed class SignalResponseBinaryMessage : ISignalResponseMessage<byte[]>
|
||||
[MessagePackObject(AllowPrivate = false)]
|
||||
public sealed class SignalResponseMessage<TResponseData> : ISignalResponseMessage
|
||||
{
|
||||
[Key(0)] public int MessageTag { get; set; }
|
||||
|
||||
[Key(1)] public SignalResponseStatus Status { get; set; }
|
||||
|
||||
[Key(2)] public byte[]? ResponseData { get; set; }
|
||||
[IgnoreMember]
|
||||
[JsonIgnore]
|
||||
[STJIgnore]
|
||||
private TResponseData? _responseData;
|
||||
|
||||
[IgnoreMember]
|
||||
public string? ResponseDataJson => ResponseData != null ? Convert.ToBase64String(ResponseData) : null;
|
||||
[IgnoreMember]
|
||||
[JsonIgnore]
|
||||
[STJIgnore]
|
||||
private bool _isDeserialized;
|
||||
|
||||
public SignalResponseBinaryMessage() { }
|
||||
[Key(0)]
|
||||
public int MessageTag { get; set; }
|
||||
|
||||
[Key(1)]
|
||||
public SignalResponseStatus Status { get; set; }
|
||||
|
||||
[Key(2)]
|
||||
public string? ResponseDataJson { get; set; }
|
||||
|
||||
public SignalResponseBinaryMessage(int messageTag, SignalResponseStatus status)
|
||||
[IgnoreMember]
|
||||
[JsonIgnore]
|
||||
[STJIgnore]
|
||||
public TResponseData? ResponseData
|
||||
{
|
||||
Status = status;
|
||||
MessageTag = messageTag;
|
||||
get
|
||||
{
|
||||
if (!_isDeserialized)
|
||||
{
|
||||
_isDeserialized = true;
|
||||
_responseData = ResponseDataJson != null ? ResponseDataJson.JsonTo<TResponseData>() : default;
|
||||
}
|
||||
return _responseData;
|
||||
}
|
||||
set
|
||||
{
|
||||
_isDeserialized = true;
|
||||
_responseData = value;
|
||||
ResponseDataJson = value?.ToJson();
|
||||
}
|
||||
}
|
||||
|
||||
public SignalResponseBinaryMessage(int messageTag, SignalResponseStatus status, object? responseData, AcBinarySerializerOptions? options = null)
|
||||
public SignalResponseMessage() { }
|
||||
public SignalResponseMessage(int messageTag, SignalResponseStatus status)
|
||||
{
|
||||
MessageTag = messageTag;
|
||||
Status = status;
|
||||
}
|
||||
public SignalResponseMessage(int messageTag, SignalResponseStatus status, TResponseData? responseData) : this(messageTag, status)
|
||||
=> ResponseData = responseData;
|
||||
public SignalResponseMessage(int messageTag, SignalResponseStatus status, string? responseDataJson) : this(messageTag, status)
|
||||
=> ResponseDataJson = responseDataJson;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Unified signal response message that supports both JSON and Binary serialization.
|
||||
/// JSON mode uses Brotli compression for reduced payload size.
|
||||
/// Optimized: decompression is performed only once and cached.
|
||||
/// </summary>
|
||||
public sealed class SignalResponseDataMessage : ISignalResponseMessage
|
||||
{
|
||||
public int MessageTag { get; set; }
|
||||
public SignalResponseStatus Status { get; set; }
|
||||
public AcSerializerType DataSerializerType { get; set; }
|
||||
public byte[]? ResponseDataBin { get; set; }
|
||||
|
||||
[JsonIgnore] [STJIgnore] private string? _cachedJson;
|
||||
|
||||
[JsonIgnore] [STJIgnore] private object? _cachedResponseData;
|
||||
|
||||
public SignalResponseDataMessage()
|
||||
{
|
||||
}
|
||||
|
||||
public SignalResponseDataMessage(int messageTag, SignalResponseStatus status)
|
||||
{
|
||||
MessageTag = messageTag;
|
||||
Status = status;
|
||||
}
|
||||
|
||||
public SignalResponseDataMessage(int messageTag, SignalResponseStatus status, object? responseData, AcSerializerOptions serializerOptions)
|
||||
: this(messageTag, status)
|
||||
{
|
||||
DataSerializerType = serializerOptions.SerializerType;
|
||||
if (responseData == null)
|
||||
{
|
||||
ResponseData = null;
|
||||
ResponseDataBin = null;
|
||||
return;
|
||||
}
|
||||
|
||||
// If responseData is already a byte array, use it directly
|
||||
if (responseData is byte[] byteData)
|
||||
if (serializerOptions.SerializerType == AcSerializerType.Binary)
|
||||
{
|
||||
ResponseData = byteData;
|
||||
return;
|
||||
}
|
||||
if (responseData is byte[] byteData)
|
||||
{
|
||||
ResponseDataBin = byteData;
|
||||
return;
|
||||
}
|
||||
|
||||
// Serialize to binary
|
||||
ResponseData = options != null
|
||||
? responseData.ToBinary(options)
|
||||
: responseData.ToBinary();
|
||||
var binaryOptions = serializerOptions as AcBinarySerializerOptions ?? AcBinarySerializerOptions.Default;
|
||||
ResponseDataBin = responseData.ToBinary(binaryOptions);
|
||||
}
|
||||
else
|
||||
{
|
||||
string json;
|
||||
if (responseData is string strData)
|
||||
{
|
||||
var trimmed = strData.Trim();
|
||||
if (trimmed.Length > 1 && (trimmed[0] == '{' || trimmed[0] == '[') && (trimmed[^1] == '}' || trimmed[^1] == ']'))
|
||||
json = strData;
|
||||
else
|
||||
{
|
||||
var jsonOptions = serializerOptions as AcJsonSerializerOptions ?? AcJsonSerializerOptions.Default;
|
||||
json = responseData.ToJson(jsonOptions);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
var jsonOptions = serializerOptions as AcJsonSerializerOptions ?? AcJsonSerializerOptions.Default;
|
||||
json = responseData.ToJson(jsonOptions);
|
||||
}
|
||||
|
||||
ResponseDataBin = BrotliHelper.Compress(json);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Deserializes the ResponseData to the specified type.
|
||||
/// Uses cached decompressed JSON for repeated calls.
|
||||
/// </summary>
|
||||
public T? GetResponseData<T>()
|
||||
{
|
||||
if (_cachedResponseData != null) return (T)_cachedResponseData;
|
||||
if (ResponseDataBin == null) return default;
|
||||
|
||||
if (DataSerializerType == AcSerializerType.Binary) return (T)(_cachedResponseData = ResponseDataBin.BinaryTo<T>());
|
||||
|
||||
_cachedJson ??= BrotliHelper.DecompressToString(ResponseDataBin);
|
||||
|
||||
return (T)(_cachedResponseData = _cachedJson.JsonTo<T>());
|
||||
}
|
||||
}
|
||||
|
||||
public interface IAcSignalRHubClient : IAcSignalRHubBase
|
||||
{
|
||||
Task SendMessageToServerAsync(int messageTag, ISignalRMessage? message, int? requestId );
|
||||
//Task SendRequestToServerAsync(int messageTag, int requestId);
|
||||
Task SendMessageToServerAsync(int messageTag, ISignalRMessage? message, int? requestId);
|
||||
}
|
||||
Loading…
Reference in New Issue