using System.Globalization; using AyCode.Core.Tests.TestModels; using AyCode.Services.SignalRs; namespace AyCode.Services.Server.Tests.SignalRs; /// /// Test service with SignalR-attributed methods for testing ProcessOnReceiveMessage. /// Uses shared DTOs from AyCode.Core.Tests.TestModels. /// public class TestSignalRService2 { #region Primitive Parameter Handlers [SignalR(TestSignalRTags.SingleIntParam)] public string HandleSingleInt(int value) { return $"{value}"; } [SignalR(TestSignalRTags.TwoIntParams)] public int HandleTwoInts(int a, int b) { return a + b; } [SignalR(TestSignalRTags.BoolParam)] public bool HandleBool(bool loadRelations) { return loadRelations; } [SignalR(TestSignalRTags.StringParam)] public string HandleString(string text) { return $"Echo: {text}"; } [SignalR(TestSignalRTags.GuidParam)] public Guid HandleGuid(Guid id) { return id; } [SignalR(TestSignalRTags.EnumParam)] public TestStatus HandleEnum(TestStatus status) { return status; } [SignalR(TestSignalRTags.NoParams)] public string HandleNoParams() { return "OK"; } [SignalR(TestSignalRTags.MultipleTypesParams)] public string HandleMultipleTypes(bool flag, string text, int number) { return $"{flag}-{text}-{number}"; } [SignalR(TestSignalRTags.ThrowsException)] public void HandleThrowsException() { throw new InvalidOperationException("Test exception"); } [SignalR(TestSignalRTags.DecimalParam)] public decimal HandleDecimal(decimal value) { return value * 2; } [SignalR(TestSignalRTags.DateTimeParam)] public DateTime HandleDateTime(DateTime dateTime) { return dateTime; } [SignalR(TestSignalRTags.DoubleParam)] public double HandleDouble(double value) { return value; } [SignalR(TestSignalRTags.LongParam)] public long HandleLong(long value) { return value; } #endregion #region Complex Object Handlers (using shared DTOs) [SignalR(TestSignalRTags.TestOrderItemParam)] public TestOrderItem HandleTestOrderItem(TestOrderItem item) { return new TestOrderItem { Id = item.Id, ProductName = $"Processed: {item.ProductName}", Quantity = item.Quantity * 2, UnitPrice = item.UnitPrice * 2, }; } [SignalR(TestSignalRTags.TestOrderParam)] public TestOrder HandleTestOrder(TestOrder order) { return order; } [SignalR(TestSignalRTags.SharedTagParam)] public SharedTag HandleSharedTag(SharedTag tag) { return tag; } #endregion #region Collection Parameter Handlers [SignalR(TestSignalRTags.IntArrayParam)] public int[] HandleIntArray(int[] values) { return values.Select(x => x * 2).ToArray(); } [SignalR(TestSignalRTags.GuidArrayParam)] public Guid[] HandleGuidArray(Guid[] ids) { return ids; } [SignalR(TestSignalRTags.StringListParam)] public List HandleStringList(List items) { return items.Select(x => x.ToUpper()).ToList(); } [SignalR(TestSignalRTags.TestOrderItemListParam)] public List HandleTestOrderItemList(List items) { return items; } [SignalR(TestSignalRTags.IntListParam)] public List HandleIntList(List numbers) { return numbers.Select(x => x * 2).ToList(); } [SignalR(TestSignalRTags.BoolArrayParam)] public bool[] HandleBoolArray(bool[] flags) { return flags; } [SignalR(TestSignalRTags.MixedWithArrayParam)] public string HandleMixedWithArray(bool flag, int[] numbers, string text) { return $"{flag}-[{string.Join(",", numbers)}]-{text}"; } [SignalR(TestSignalRTags.NestedListParam)] public List> HandleNestedList(List> nestedList) { return nestedList; } #endregion #region Extended Array Parameter Handlers [SignalR(TestSignalRTags.LongArrayParam)] public long[] HandleLongArray(long[] values) { return values; } [SignalR(TestSignalRTags.DecimalArrayParam)] public decimal[] HandleDecimalArray(decimal[] values) { return values; } [SignalR(TestSignalRTags.DateTimeArrayParam)] public DateTime[] HandleDateTimeArray(DateTime[] values) { return values; } [SignalR(TestSignalRTags.EnumArrayParam)] public TestStatus[] HandleEnumArray(TestStatus[] values) { return values; } [SignalR(TestSignalRTags.DoubleArrayParam)] public double[] HandleDoubleArray(double[] values) { return values; } [SignalR(TestSignalRTags.SharedTagArrayParam)] public SharedTag[] HandleSharedTagArray(SharedTag[] tags) { return tags; } [SignalR(TestSignalRTags.DictionaryParam)] public Dictionary HandleDictionary(Dictionary dict) { return dict; } [SignalR(TestSignalRTags.ObjectArrayParam)] public object[] HandleObjectArray(object[] values) { return values; } #endregion #region Mixed Parameter Handlers [SignalR(TestSignalRTags.IntAndDtoParam)] public string HandleIntAndDto(int id, TestOrderItem item) { return $"{id}-{item?.ProductName}"; } [SignalR(TestSignalRTags.DtoAndListParam)] public string HandleDtoAndList(TestOrderItem item, List numbers) { return $"{item?.ProductName}-[{string.Join(",", numbers ?? [])}]"; } [SignalR(TestSignalRTags.ThreeComplexParams)] public string HandleThreeComplexParams(TestOrderItem item, List tags, SharedTag sharedTag) { return $"{item?.ProductName}-{tags?.Count}-{sharedTag?.Name}"; } [SignalR(TestSignalRTags.FiveParams)] public Task HandleFiveParams(int a, string b, bool c, Guid d, decimal e) { return Task.FromResult($"{a}-{b}-{c}-{d}-{e.ToString(CultureInfo.InvariantCulture)}"); } #endregion #region Async Task Method Tests [SignalR(TestSignalRTags.AsyncTestOrderItemParam)] public async Task HandleAsyncTestOrderItem(TestOrderItem item) { await Task.Delay(1); // Simulate async work return new TestOrderItem { Id = item.Id, ProductName = $"Async: {item.ProductName}", Quantity = item.Quantity * 3, UnitPrice = item.UnitPrice * 3, }; } [SignalR(TestSignalRTags.AsyncStringParam)] public async Task HandleAsyncString(string input) { await Task.Delay(1); return $"Async: {input}"; } [SignalR(TestSignalRTags.AsyncNoParams)] public async Task HandleAsyncNoParams() { await Task.Delay(1); return "AsyncOK"; } [SignalR(TestSignalRTags.AsyncIntParam)] public async Task HandleAsyncInt(int value) { await Task.Delay(1); return value * 2; } #endregion #region Task.FromResult Tests - Critical for testing non-async methods returning Task // PRODUCTION BUG FIX: These methods test the scenario where a method returns Task // using Task.FromResult() instead of async/await. Such methods do NOT have // AsyncStateMachineAttribute, so the old InvokeMethod implementation would serialize // the Task wrapper instead of awaiting and returning the actual result. /// /// Returns Task without async keyword - uses Task.FromResult(). /// This pattern does NOT have AsyncStateMachineAttribute! /// [SignalR(TestSignalRTags.TaskFromResultStringParam)] public Task HandleTaskFromResultString(string input) { return Task.FromResult($"FromResult: {input}"); } /// /// Returns Task<TestOrderItem> without async keyword. /// CRITICAL: This simulates the exact production bug scenario. /// [SignalR(TestSignalRTags.TaskFromResultTestOrderItemParam)] public Task HandleTaskFromResultTestOrderItem(TestOrderItem item) { return Task.FromResult(new TestOrderItem { Id = item.Id, ProductName = $"FromResult: {item.ProductName}", Quantity = item.Quantity * 2, UnitPrice = item.UnitPrice * 2, }); } /// /// Returns Task<int> without async keyword. /// [SignalR(TestSignalRTags.TaskFromResultIntParam)] public Task HandleTaskFromResultInt(int value) { return Task.FromResult(value * 2); } /// /// Returns non-generic Task using Task.CompletedTask. /// [SignalR(TestSignalRTags.TaskFromResultNoParams)] public Task HandleTaskFromResultNoParams() { return Task.CompletedTask; } #endregion #region Binary Serialization with GenericAttributes Test /// /// Tests Binary serialization with GenericAttributes containing string-stored DateTime values. /// This reproduces the production bug scenario where DateTime values stored as strings /// in GenericAttributes were incorrectly blamed for Binary serialization issues. /// [SignalR(TestSignalRTags.GenericAttributesParam)] public TestDtoWithGenericAttributes HandleGenericAttributes(TestDtoWithGenericAttributes dto) { // Return the same DTO to verify Binary round-trip preserves all values return dto; } /// /// Tests Binary serialization with a list of DTOs containing GenericAttributes. /// This simulates the production scenario with large datasets (e.g., 1834 orders). /// [SignalR(TestSignalRTags.GenericAttributesListParam)] public List HandleGenericAttributesList(List dtos) { return dtos; } #endregion #region Large Dataset / List Tests /// /// Tests Binary serialization with a list of TestOrder objects. /// Used for testing string interning with deeply nested objects. /// [SignalR(TestSignalRTags.TestOrderListParam)] public List HandleTestOrderList(List orders) { return orders; } #endregion #region Property Mismatch Tests (Server has more properties than Client) // Tests for SkipValue string interning bug fix. // In these tests, the server sends DTOs with more properties than the client knows about. // The client's deserializer must skip unknown properties while maintaining string intern table consistency. /// /// Handles server DTO and returns the same DTO. /// Client will deserialize this into ClientCustomerDto (fewer properties). /// [SignalR(TestSignalRTags.PropertyMismatchParam)] public ServerCustomerDto HandlePropertyMismatch(ServerCustomerDto dto) { return dto; } /// /// Handles list of server DTOs. /// Client will deserialize into List<ClientCustomerDto>. /// [SignalR(TestSignalRTags.PropertyMismatchListParam)] public List HandlePropertyMismatchList(List dtos) { return dtos; } /// /// Handles server order with nested customer objects. /// Client will deserialize into ClientOrderSimple (no nested objects). /// [SignalR(TestSignalRTags.PropertyMismatchNestedParam)] public ServerOrderWithExtras HandlePropertyMismatchNested(ServerOrderWithExtras order) { return order; } /// /// Handles list of server orders with nested customer objects. /// Client will deserialize into List<ClientOrderSimple>. /// [SignalR(TestSignalRTags.PropertyMismatchNestedListParam)] public List HandlePropertyMismatchNestedList(List orders) { return orders; } #endregion #region DataSource CRUD Tests private readonly List _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 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 }