Azure / azure-functions-dotnet-worker

Azure Functions out-of-process .NET language worker
MIT License
414 stars 181 forks source link

Bug: Null Result Issue with SignalRTrigger Functions in .NET8 Upgrade #2651

Open Furrman opened 1 month ago

Furrman commented 1 month ago

What version of .NET does your existing project use?

.NET 6

What version of .NET are you attempting to target?

.NET 8

Description

Hello,

I wanted to bring up an issue with the SignalRTrigger solution. My team and I are using this approach in .NET6 for our project to trigger some asynchronous operations. In response, we are returning a custom object indicating whether the operation was successful. However, in .NET8, our SignalRTrigger functions consistently return a null result in the server response, regardless of our efforts to fix it. It is worth mentioning that during the .NET8 upgrade, we are migrating our functions to isolated mode.

Expected

Function with SignalR trigger and return type will send custom response in result field to client.

Actual

Function with SignalR trigger is sending null result to client.

Examples

Click me Here is our server-side code in .NET6: ``` [SignalRHub(path: "")] public partial class MainHub : ServerlessHub { ... [FunctionName(nameof(Echo))] public async Task Echo([SignalRTrigger()] InvocationContext invocationContext, string name, string message) { await Clients.Client(invocationContext.ConnectionId).Echo(name, message); return new SignalRHubActionRequest { SignalRHubActionStatus = SignalRHubActionStatus.Accepted }; } ... } public class SignalRHubActionRequest { public string Id { get; set; } public Status Status { get; set; } } public enum Status { Success, Failure } ``` The only change in .NET8 solution for this code snippet is the Echo function (in the SignalRTrigger attribute): ``` [Function(nameof(Echo))] public async Task Echo([SignalRTrigger(nameof(MainHub), CATEGORY, nameof(Echo), "name", "message")] SignalRInvocationContext invocationContext, string name, string message) { await Clients.Client(invocationContext.ConnectionId).Echo(name, $"async : {message}"); return new SignalRHubActionRequest { SignalRHubActionStatus = SignalRHubActionStatus.Accepted }; } ``` This is how we registered SignalR in .NET6: ``` builder.Services.Configure(o => { var settings = new JsonSerializerSettings() { NullValueHandling = NullValueHandling.Ignore, ContractResolver = new CamelCasePropertyNamesContractResolver() }; settings.Converters.Add(new Newtonsoft.Json.Converters.StringEnumConverter()); o.JsonObjectSerializer = new NewtonsoftJsonObjectSerializer(settings); }); ``` This is our registration in .NET8: ``` services.AddSignalR() .AddNewtonsoftJsonProtocol(options => { options.PayloadSerializerSettings = new JsonSerializerSettings { NullValueHandling = NullValueHandling.Ignore, ContractResolver = new CamelCasePropertyNamesContractResolver() }; options.PayloadSerializerSettings.Converters.Add(new Newtonsoft.Json.Converters.StringEnumConverter()); }); ``` And this is our C# SignalR client code: ``` await using var connection = new HubConnectionBuilder() .WithAutomaticReconnect() .WithUrl(uriTime, options => { options.AccessTokenProvider = () => Task.FromResult(accessToken); }) .AddNewtonsoftJsonProtocol(options => { options.PayloadSerializerSettings = new JsonSerializerSettings { NullValueHandling = NullValueHandling.Ignore, ContractResolver = new CamelCasePropertyNamesContractResolver() }; options.PayloadSerializerSettings.Converters.Add(new Newtonsoft.Json.Converters.StringEnumConverter()); }); await connection.StartAsync(); var result01 = await connection.InvokeAsync("Echo", name, message); ``` We inspected the request and observed that the server side is returning null in the result property, regardless of what we attempt to send (e.g., a string instead of SignalRHubActionRequest or changing serializer to default or json one). We confirmed this by using tools like ngrok and Fiddler. ![image](https://github.com/user-attachments/assets/194faf0d-87b8-41f5-a982-b8e2a3ac27c8) ![image](https://github.com/user-attachments/assets/329b42b2-b768-4034-bf79-16897d2cb080) We tried several approaches, with the most promising one being the use of the SignalROutput attribute to bind the output: [SignalROutput(HubName = nameof(MainHub), ConnectionStringSetting = "AzureSignalRConnectionString")] ![image](https://github.com/user-attachments/assets/afdf694c-59fb-48bd-985e-18c9a4cd3d48) ![image](https://github.com/user-attachments/assets/e659490b-2394-48d7-9b3f-66dbd4fb5e9e) This resulted in a server-side error. When we changed the return type to string while using this SignalROutput attribute, we encountered a different error. ![image](https://github.com/user-attachments/assets/17d78815-6dc0-421f-93ce-c6e164aa7256) Ultimately, we downloaded the latest codebase from your repository and used your samples, specifically the Extensions solution, and experienced the same null result issue. This indicates that the problem lies within your library. We are unsure of the cause of these varying errors, but based on these examples, it appears you can return error objects. This implies you should be able to return an object in the result property as well. We initially thought this functionality was removed, but the errors suggest otherwise. Additionally, your documentation does not indicate that the feature to return objects from a SignalRTrigger function was removed. Therefore, we are requesting your assistance and investigation into this issue. Thank you. ### Project configuration and dependencies Azure function app ``` net8.0 v4 Exe <_FunctionsSkipCleanOutput>true True enable ``` Client app: ``` Exe net8.0 enable enable ```

Link to a repository that reproduces the issue

https://github.com/Azure/azure-functions-dotnet-worker/tree/main/samples/Extensions/SignalR

waqarzafar commented 1 month ago

I am having same issue as well so looks like SignalR has not caught up with recent .NET8 upgrades

RedChris commented 1 month ago

I was just going to post something similar, Im also seeing this issue.

Furrman commented 1 month ago

I have did investigation on your source code and looks like you are passing the return object to the Sytem.Threading.Channels library in GrpcWorker:ProcessRequestCoreAsync() to send it over to the client side. I cannot go further but seems something there is ignoring our return object. It can be wrong assigment or anything, but it is outside of my reach. There is one more thing that got my attention that in your main branch, in DotNetWorker.Grpc project, you do not have setup .net8 dedicated packages - instead you refer to System.Threading.Channels to 6.0.0 version in your .NETStandard references. I checked and the newest package version is 8.0.0 which has high probability it is the version prepared for .NET8. Can it be related to this issue? Maybe updating reference would fix the problem we are facing? At last I can add, that our codebase with working solution was using Microsoft.Azure.WebJobs.Extensions.SignalRService package instead (for in-process mode in function app).

Furrman commented 1 month ago

Can someone from admins add label potential-bug and need-attention?

nickjd331 commented 3 weeks ago

Hi,

Has anyone seen any updates on this issue being investigated?

This is also impacting my live project and preventing me from upgrading to .NET8, so I'm eager for this to be resolved asap.

Thanks.

satvu commented 2 days ago

@Y-Sindo Is this something you can help look into?

Y-Sindo commented 14 hours ago

@satvu This is a known issue, see #1496 . Please see #1496 to track the progress.