Skip to content

Commit 651e9ad

Browse files
authored
adding ability to manipulate capabilities (#1183)
1 parent 5fc4732 commit 651e9ad

File tree

5 files changed

+131
-55
lines changed

5 files changed

+131
-55
lines changed

samples/FunctionApp/Program.cs

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,6 @@
11
// Copyright (c) .NET Foundation. All rights reserved.
22
// Licensed under the MIT License. See License.txt in the project root for license information.
33

4-
using System;
5-
using System.Diagnostics;
6-
using System.Threading;
74
using System.Threading.Tasks;
85
using Microsoft.Azure.Functions.Worker;
96
using Microsoft.Extensions.DependencyInjection;

src/DotNetWorker.Core/Context/Features/IDictionaryExtensions.cs

Lines changed: 43 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -8,49 +8,59 @@
88
using System;
99
using System.Collections.Concurrent;
1010
using System.Collections.Generic;
11+
using System.ComponentModel;
1112

12-
namespace Microsoft.Azure.Functions.Worker
13+
internal static class IDictionaryExtensions
1314
{
14-
internal static class IDictionaryExtensions
15+
internal static bool TryAdd<TKey, TValue>(this IDictionary<TKey, TValue> dictionary, TKey key, TValue value)
1516
{
16-
internal static bool TryAdd<TKey, TValue>(this IDictionary<TKey, TValue> dictionary, TKey key, TValue value)
17+
if (key == null)
1718
{
18-
if (key == null)
19-
{
20-
throw new ArgumentNullException(nameof(key));
21-
}
22-
23-
if (dictionary.ContainsKey(key))
24-
{
25-
return false;
26-
}
27-
28-
dictionary.Add(key, value);
29-
return true;
19+
throw new ArgumentNullException(nameof(key));
3020
}
21+
22+
if (dictionary.ContainsKey(key))
23+
{
24+
return false;
25+
}
26+
27+
dictionary.Add(key, value);
28+
return true;
3129
}
30+
}
3231

33-
internal static class ConcurrentDictionaryExtensions
32+
internal static class ConcurrentDictionaryExtensions
33+
{
34+
public static TValue GetOrAdd<TKey, TValue, TArg>(this ConcurrentDictionary<TKey, TValue> dictionary, TKey key, Func<TKey, TArg, TValue> valueFactory, TArg factoryArgument)
3435
{
35-
public static TValue GetOrAdd<TKey, TValue, TArg>(this ConcurrentDictionary<TKey, TValue> dictionary, TKey key, Func<TKey, TArg, TValue> valueFactory, TArg factoryArgument)
36+
if (dictionary == null)
3637
{
37-
if (dictionary == null)
38-
{
39-
throw new ArgumentNullException(nameof(dictionary));
40-
}
41-
42-
if (key == null)
43-
{
44-
throw new ArgumentNullException(nameof(key));
45-
}
46-
47-
if (valueFactory == null)
48-
{
49-
throw new ArgumentNullException(nameof(valueFactory));
50-
}
51-
52-
return dictionary.GetOrAdd(key, k => valueFactory(k, factoryArgument));
38+
throw new ArgumentNullException(nameof(dictionary));
5339
}
40+
41+
if (key == null)
42+
{
43+
throw new ArgumentNullException(nameof(key));
44+
}
45+
46+
if (valueFactory == null)
47+
{
48+
throw new ArgumentNullException(nameof(valueFactory));
49+
}
50+
51+
return dictionary.GetOrAdd(key, k => valueFactory(k, factoryArgument));
5452
}
5553
}
54+
55+
internal static class KeyValuePairExtensions
56+
{
57+
// Based on https://source.dot.net/#System.Private.CoreLib/src/libraries/System.Private.CoreLib/src/System/Collections/Generic/KeyValuePair.cs,aa57b8e336bf7f59
58+
[EditorBrowsable(EditorBrowsableState.Never)]
59+
public static void Deconstruct<TKey, TValue>(this KeyValuePair<TKey, TValue> pair, out TKey key, out TValue value)
60+
{
61+
key = pair.Key;
62+
value = pair.Value;
63+
}
64+
}
65+
5666
#endif

src/DotNetWorker.Core/Hosting/WorkerOptions.cs

Lines changed: 41 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
// Copyright (c) .NET Foundation. All rights reserved.
22
// Licensed under the MIT License. See License.txt in the project root for license information.
33

4+
using System.Collections.Generic;
45
using System.Text.Json;
56
using Azure.Core.Serialization;
67

@@ -22,19 +23,56 @@ public class WorkerOptions
2223
/// </summary>
2324
public InputConverterCollection InputConverters { get; } = new InputConverterCollection();
2425

26+
/// <summary>
27+
/// Gets the optional worker capabilities.
28+
/// </summary>
29+
public IDictionary<string, string> Capabilities { get; } = new Dictionary<string, string>()
30+
{
31+
// Enable these by default, although they are not strictly required and can be removed
32+
{ "HandlesWorkerTerminateMessage", bool.TrueString },
33+
{ "HandlesInvocationCancelMessage", bool.TrueString }
34+
};
35+
2536
/// <summary>
2637
/// Gets and sets the flag for opting in to unwrapping user-code-thrown
2738
/// exceptions when they are surfaced to the Host.
2839
/// </summary>
29-
public bool EnableUserCodeException { get; set; } = false;
30-
40+
public bool EnableUserCodeException
41+
{
42+
get => GetBoolCapability(nameof(EnableUserCodeException));
43+
set => SetBoolCapability(nameof(EnableUserCodeException), value);
44+
}
45+
3146
/// <summary>
3247
/// Gets or sets a value that determines if empty entries should be included in the function trigger message payload.
3348
/// For example, if a set of entries were sent to a messaging service such as Service Bus or Event Hub and your function
3449
/// app has a Service bus trigger or Event hub trigger, only the non-empty entries from the payload will be sent to the
3550
/// function code as trigger data when this setting value is <see langword="false"/>. When it is <see langword="true"/>,
3651
/// All entries will be sent to the function code as it is. Default value for this setting is <see langword="false"/>.
3752
/// </summary>
38-
public bool IncludeEmptyEntriesInMessagePayload { get; set; }
53+
public bool IncludeEmptyEntriesInMessagePayload
54+
{
55+
get => GetBoolCapability(nameof(IncludeEmptyEntriesInMessagePayload));
56+
set => SetBoolCapability(nameof(IncludeEmptyEntriesInMessagePayload), value);
57+
}
58+
59+
private bool GetBoolCapability(string name)
60+
{
61+
return Capabilities.TryGetValue(name, out string? value) && bool.TryParse(value, out bool b) && b;
62+
}
63+
64+
// For false values, the host does not expect the capability to exist; there are some cases where this
65+
// will be interpreted as "true" just because the key is there.
66+
private void SetBoolCapability(string name, bool value)
67+
{
68+
if (value)
69+
{
70+
Capabilities[name] = bool.TrueString;
71+
}
72+
else
73+
{
74+
Capabilities.Remove(name);
75+
}
76+
}
3977
}
4078
}

src/DotNetWorker.Grpc/GrpcWorker.cs

Lines changed: 11 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -163,24 +163,20 @@ internal static WorkerInitResponse WorkerInitRequestHandler(WorkerInitRequest re
163163

164164
response.WorkerMetadata.CustomProperties.Add("Worker.Grpc.Version", typeof(GrpcWorker).Assembly.GetName().Version?.ToString());
165165

166-
response.Capabilities.Add("RpcHttpBodyOnly", bool.TrueString);
167-
response.Capabilities.Add("RawHttpBodyBytes", bool.TrueString);
168-
response.Capabilities.Add("RpcHttpTriggerMetadataRemoved", bool.TrueString);
169-
response.Capabilities.Add("UseNullableValueDictionaryForHttp", bool.TrueString);
170-
response.Capabilities.Add("TypedDataCollection", bool.TrueString);
171-
response.Capabilities.Add("WorkerStatus", bool.TrueString);
172-
response.Capabilities.Add("HandlesWorkerTerminateMessage", bool.TrueString);
173-
response.Capabilities.Add("HandlesInvocationCancelMessage", bool.TrueString);
174-
175-
if (workerOptions.EnableUserCodeException)
166+
// Add additional capabilities defined by WorkerOptions
167+
foreach ((string key, string value) in workerOptions.Capabilities)
176168
{
177-
response.Capabilities.Add("EnableUserCodeException", bool.TrueString);
178-
}
179-
if (workerOptions.IncludeEmptyEntriesInMessagePayload)
180-
{
181-
response.Capabilities.Add("IncludeEmptyEntriesInMessagePayload", bool.TrueString);
169+
response.Capabilities[key] = value;
182170
}
183171

172+
// Add required capabilities; these cannot be modified and will override anything from WorkerOptions
173+
response.Capabilities["RpcHttpBodyOnly"] = bool.TrueString;
174+
response.Capabilities["RawHttpBodyBytes"] = bool.TrueString;
175+
response.Capabilities["RpcHttpTriggerMetadataRemoved"] = bool.TrueString;
176+
response.Capabilities["UseNullableValueDictionaryForHttp"] = bool.TrueString;
177+
response.Capabilities["TypedDataCollection"] = bool.TrueString;
178+
response.Capabilities["WorkerStatus"] = bool.TrueString;
179+
184180
return response;
185181
}
186182

test/DotNetWorkerTests/GrpcWorkerTests.cs

Lines changed: 36 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,10 @@
1616
using Microsoft.Azure.Functions.Worker.Handlers;
1717
using Microsoft.Azure.Functions.Worker.Invocation;
1818
using Microsoft.Azure.Functions.Worker.OutputBindings;
19+
using Microsoft.Extensions.DependencyInjection;
20+
using Microsoft.Extensions.Hosting;
1921
using Microsoft.Extensions.Logging;
22+
using Microsoft.Extensions.Options;
2023
using Moq;
2124
using Xunit;
2225

@@ -41,7 +44,8 @@ public GrpcWorkerTests()
4144

4245
_mockApplication
4346
.Setup(m => m.CreateContext(It.IsAny<IInvocationFeatures>(), It.IsAny<CancellationToken>()))
44-
.Returns((IInvocationFeatures f, CancellationToken ct) => {
47+
.Returns((IInvocationFeatures f, CancellationToken ct) =>
48+
{
4549
_context = new TestFunctionContext(f, ct);
4650
return _context;
4751
});
@@ -184,6 +188,7 @@ public void InitRequest_ReturnsExpectedCapabilities_BasedOnWorkerOptions(
184188
string expectedCapabilityValue = null)
185189
{
186190
var workerOptions = new WorkerOptions();
191+
187192
// Update boolean property values of workerOption based on test input parameters.
188193
workerOptions.GetType().GetProperty(booleanPropertyName)?.SetValue(workerOptions, booleanPropertyValue);
189194

@@ -210,6 +215,36 @@ public void InitRequest_ReturnsExpectedCapabilities_BasedOnWorkerOptions(
210215
}
211216
}
212217

218+
[Fact]
219+
public void WorkerOptions_CanChangeOptionalCapabilities()
220+
{
221+
var host = new HostBuilder()
222+
.ConfigureFunctionsWorkerDefaults((WorkerOptions options) =>
223+
{
224+
options.Capabilities.Remove("HandlesWorkerTerminateMessage");
225+
options.Capabilities.Add("SomeNewCapability", bool.TrueString);
226+
}).Build();
227+
228+
var workerOptions = host.Services.GetService<IOptions<WorkerOptions>>().Value;
229+
var response = GrpcWorker.WorkerInitRequestHandler(new(), workerOptions);
230+
231+
void AssertKeyAndValue(KeyValuePair<string, string> kvp, string expectedKey, string expectedValue)
232+
{
233+
Assert.Same(expectedKey, kvp.Key);
234+
Assert.Same(expectedValue, kvp.Value);
235+
}
236+
237+
Assert.Collection(response.Capabilities.OrderBy(p => p.Key),
238+
c => AssertKeyAndValue(c, "HandlesInvocationCancelMessage", bool.TrueString),
239+
c => AssertKeyAndValue(c, "RawHttpBodyBytes", bool.TrueString),
240+
c => AssertKeyAndValue(c, "RpcHttpBodyOnly", bool.TrueString),
241+
c => AssertKeyAndValue(c, "RpcHttpTriggerMetadataRemoved", bool.TrueString),
242+
c => AssertKeyAndValue(c, "SomeNewCapability", bool.TrueString),
243+
c => AssertKeyAndValue(c, "TypedDataCollection", bool.TrueString),
244+
c => AssertKeyAndValue(c, "UseNullableValueDictionaryForHttp", bool.TrueString),
245+
c => AssertKeyAndValue(c, "WorkerStatus", bool.TrueString));
246+
}
247+
213248
[Fact]
214249
public async Task Invoke_ReturnsSuccess()
215250
{

0 commit comments

Comments
 (0)