Skip to content

Commit 1b4c390

Browse files
authored
Support handling function return value from worker process (#46158)
Fix Azure/azure-functions-dotnet-worker#1496 The reason to add .NET 6 and .NET 8 as TFM: The worker function return value is a Newtonsoft JObject if it's an object in the worker. Since .NET Core Frameworks, the default SignalR JSON protocol uses System.Text.Json library and can't serialize Newtonsoft JObject. Therefore, we need to wrap the JObject to a System.Text.Json serializable object with a customized JSON converter. The customized JSON converter first converts the Newtonsoft JObject to JSON with Newtonsoft library, then writes the raw JSON object with System.Text.Json writer. For .NET Standard 2.0, it's not possible to write a raw JSON with System.Text.Json writer, therefore, we need to add .NET 6 and .NET 8 as TFM to write the raw JSON.
1 parent 6b1dccb commit 1b4c390

12 files changed

+689
-23
lines changed

sdk/signalr/Microsoft.Azure.WebJobs.Extensions.SignalRService/CHANGELOG.md

Lines changed: 2 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,9 @@
11
# Release History
22

3-
## 1.15.0-beta.1 (Unreleased)
4-
5-
### Features Added
6-
7-
### Breaking Changes
3+
## 1.15.0 (2024-10-14)
84

95
### Bugs Fixed
10-
11-
### Other Changes
6+
* Fixed the issue that the function return value from isolated-worker process is not handled correctly.
127

138
## 1.14.0 (2024-05-24)
149

sdk/signalr/Microsoft.Azure.WebJobs.Extensions.SignalRService/api/Microsoft.Azure.WebJobs.Extensions.SignalRService.net6.0.cs

Lines changed: 302 additions & 0 deletions
Large diffs are not rendered by default.

sdk/signalr/Microsoft.Azure.WebJobs.Extensions.SignalRService/api/Microsoft.Azure.WebJobs.Extensions.SignalRService.net8.0.cs

Lines changed: 302 additions & 0 deletions
Large diffs are not rendered by default.

sdk/signalr/Microsoft.Azure.WebJobs.Extensions.SignalRService/api/Microsoft.Azure.WebJobs.Extensions.SignalRService.netstandard2.0.cs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -254,8 +254,8 @@ public partial class SignalRParameterAttribute : System.Attribute
254254
{
255255
public SignalRParameterAttribute() { }
256256
}
257-
[Microsoft.Azure.WebJobs.Description.BindingAttribute]
258-
[System.AttributeUsageAttribute(System.AttributeTargets.Parameter | System.AttributeTargets.ReturnValue)]
257+
[Microsoft.Azure.WebJobs.Description.BindingAttribute(TriggerHandlesReturnValue=true)]
258+
[System.AttributeUsageAttribute(System.AttributeTargets.Parameter)]
259259
public partial class SignalRTriggerAttribute : System.Attribute
260260
{
261261
public SignalRTriggerAttribute() { }

sdk/signalr/Microsoft.Azure.WebJobs.Extensions.SignalRService/src/Config/SignalRConfigProvider.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -71,7 +71,7 @@ public void Initialize(ExtensionConfigContext context)
7171
// Trigger binding rule
7272
var triggerBindingRule = context.AddBindingRule<SignalRTriggerAttribute>();
7373
triggerBindingRule.AddConverter<InvocationContext, JObject>(JObject.FromObject);
74-
triggerBindingRule.BindToTrigger<InvocationContext>(new SignalRTriggerBindingProvider(_dispatcher, _nameResolver, _serviceManagerStore, webhookException));
74+
triggerBindingRule.BindToTrigger(new SignalRTriggerBindingProvider(_dispatcher, _nameResolver, _serviceManagerStore, webhookException));
7575

7676
// Non-trigger binding rule
7777
var signalRConnectionInfoAttributeRule = context.AddBindingRule<SignalRConnectionInfoAttribute>();

sdk/signalr/Microsoft.Azure.WebJobs.Extensions.SignalRService/src/Microsoft.Azure.WebJobs.Extensions.SignalRService.csproj

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,10 @@
11
<Project Sdk="Microsoft.NET.Sdk">
22

33
<PropertyGroup>
4-
<TargetFrameworks>$(RequiredTargetFrameworks)</TargetFrameworks>
4+
<TargetFrameworks>$(RequiredTargetFrameworks);net6.0;net8.0</TargetFrameworks>
55
<PackageId>Microsoft.Azure.WebJobs.Extensions.SignalRService</PackageId>
6-
<Version>1.15.0-beta.1</Version>
6+
<Version>1.15.0</Version>
77
<!--The ApiCompatVersion is managed automatically and should not generally be modified manually.-->
8-
<ApiCompatVersion>1.14.0</ApiCompatVersion>
98
<SignAssembly>true</SignAssembly>
109
<IsExtensionClientLibrary>true</IsExtensionClientLibrary>
1110
<NoWarn>$(NoWarn);CS1591;AZC0107;</NoWarn>

sdk/signalr/Microsoft.Azure.WebJobs.Extensions.SignalRService/src/TriggerBindings/Executor/SignalRInvocationMethodExecutor.cs

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
using System.Threading.Tasks;
88

99
using Microsoft.AspNetCore.SignalR.Protocol;
10+
using Newtonsoft.Json.Linq;
1011
using InvocationMessage = Microsoft.Azure.SignalR.Serverless.Protocols.InvocationMessage;
1112

1213
namespace Microsoft.Azure.WebJobs.Extensions.SignalRService
@@ -52,7 +53,7 @@ public override async Task<HttpResponseMessage> ExecuteAsync(HttpRequestMessage
5253
else
5354
{
5455
var result = await tcs.Task.ConfigureAwait(false);
55-
completionMessage = CompletionMessage.WithResult(message.InvocationId, result);
56+
completionMessage = CompletionMessage.WithResult(message.InvocationId, ToSafeSerializationType(result));
5657
response = new HttpResponseMessage(HttpStatusCode.OK);
5758
}
5859
}
@@ -68,6 +69,18 @@ public override async Task<HttpResponseMessage> ExecuteAsync(HttpRequestMessage
6869
return response;
6970
}
7071

72+
private static object ToSafeSerializationType(object result)
73+
{
74+
if (result is JToken jtoken)
75+
{
76+
return new JTokenWrapper(jtoken);
77+
}
78+
else
79+
{
80+
return result;
81+
}
82+
}
83+
7184
private static void AssertConsistency(InvocationContext context, InvocationMessage message)
7285
{
7386
if (!string.Equals(context.Event, message.Target, StringComparison.OrdinalIgnoreCase))

sdk/signalr/Microsoft.Azure.WebJobs.Extensions.SignalRService/src/TriggerBindings/SignalRTriggerAttribute.cs

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,8 +10,10 @@ namespace Microsoft.Azure.WebJobs.Extensions.SignalRService
1010
/// <summary>
1111
/// Attribute used to mark a function that should be triggered by messages sent from SignalR clients.
1212
/// </summary>
13-
[AttributeUsage(AttributeTargets.ReturnValue | AttributeTargets.Parameter)]
14-
[Binding]
13+
[AttributeUsage(AttributeTargets.Parameter)]
14+
#pragma warning disable CS0618 // Type or member is obsolete
15+
[Binding(TriggerHandlesReturnValue = true)]
16+
#pragma warning restore CS0618 // Type or member is obsolete
1517
public class SignalRTriggerAttribute : Attribute
1618
{
1719
/// <summary>

sdk/signalr/Microsoft.Azure.WebJobs.Extensions.SignalRService/src/TriggerBindings/SignalRTriggerBinding.cs

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717
using Microsoft.Azure.WebJobs.Host.Protocols;
1818
using Microsoft.Azure.WebJobs.Host.Triggers;
1919
using Microsoft.Extensions.Options;
20+
using Newtonsoft.Json;
2021
using Newtonsoft.Json.Linq;
2122

2223
namespace Microsoft.Azure.WebJobs.Extensions.SignalRService
@@ -201,6 +202,11 @@ public Task<object> GetValueAsync()
201202
{
202203
return Task.FromResult<object>(JObject.FromObject(_value));
203204
}
205+
// Isolated worker model
206+
else if (_parameter.ParameterType == typeof(string))
207+
{
208+
return Task.FromResult(JsonConvert.SerializeObject(_value) as object);
209+
}
204210

205211
return Task.FromResult<object>(null);
206212
}
@@ -210,7 +216,7 @@ public string ToInvokeString()
210216
return _value.ToString();
211217
}
212218

213-
public Type Type => _parameter.GetType();
219+
public Type Type => _parameter.ParameterType;
214220

215221
// No use here
216222
public Task SetValueAsync(object value, CancellationToken cancellationToken)
Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
// Copyright (c) Microsoft Corporation. All rights reserved.
2+
// Licensed under the MIT License.
3+
4+
using System;
5+
using System.Text.Json;
6+
using Microsoft.AspNetCore.SignalR.Protocol;
7+
using Newtonsoft.Json;
8+
using Newtonsoft.Json.Linq;
9+
10+
namespace Microsoft.Azure.WebJobs.Extensions.SignalRService;
11+
12+
/// <summary>
13+
/// Helps to make the <see cref="JToken"/> object correctly seralized in <see cref="JsonHubProtocol"/> that using System.Text.Json internally.
14+
/// <para>Since .Net Core 3.0, the <see cref="JsonHubProtocol"/> uses System.Text.Json library for JSON (de)serialization, which cannot handle <see cref="JToken"/> correctly. However, in isolated worker model, if a SignalR trigger function returns an object, then the SignalR extensions in host process gets a <see cref="JToken"/> object. We need to make sure the <see cref="JToken"/> object serialized correctly in the <see cref="CompletionMessage"/>.</para>
15+
/// </summary>
16+
17+
[System.Text.Json.Serialization.JsonConverter(typeof(JTokenWrapperJsonConverter))]
18+
internal class JTokenWrapper
19+
{
20+
public JTokenWrapper(JToken value)
21+
{
22+
Value = value;
23+
}
24+
25+
public JToken Value { get; }
26+
}
27+
28+
internal class JTokenWrapperJsonConverter : System.Text.Json.Serialization.JsonConverter<JTokenWrapper>
29+
{
30+
public override JTokenWrapper Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
31+
{
32+
throw new NotImplementedException();
33+
}
34+
35+
public override void Write(Utf8JsonWriter writer, JTokenWrapper value, JsonSerializerOptions options)
36+
{
37+
#if NET6_0_OR_GREATER
38+
var jsonString = JsonConvert.SerializeObject(value.Value);
39+
writer.WriteRawValue(jsonString);
40+
#elif NETSTANDARD2_0
41+
// No need to implement.
42+
// First of all, the SignalR extensions for host process always run on .NET 6 or greater runtime when this class is first written.
43+
// Even if somehow the extensions run on .NET Framework, the JsonHubProtocol would use Newtonsoft.Json for serialization and this class would not be used.
44+
throw new NotImplementedException("Serializing Newtonsoft.Json.JsonToken with System.Text.Json is not implemented. ");
45+
#endif
46+
}
47+
}

0 commit comments

Comments
 (0)