Skip to content
Merged
Show file tree
Hide file tree
Changes from 5 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 0 additions & 3 deletions samples/FunctionApp/Program.cs
Original file line number Diff line number Diff line change
@@ -1,9 +1,6 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the MIT License. See License.txt in the project root for license information.

using System;
using System.Diagnostics;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.Azure.Functions.Worker;
using Microsoft.Extensions.DependencyInjection;
Expand Down
76 changes: 43 additions & 33 deletions src/DotNetWorker.Core/Context/Features/IDictionaryExtensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -8,49 +8,59 @@
using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.ComponentModel;

namespace Microsoft.Azure.Functions.Worker
internal static class IDictionaryExtensions
{
internal static class IDictionaryExtensions
internal static bool TryAdd<TKey, TValue>(this IDictionary<TKey, TValue> dictionary, TKey key, TValue value)
{
internal static bool TryAdd<TKey, TValue>(this IDictionary<TKey, TValue> dictionary, TKey key, TValue value)
if (key == null)
{
if (key == null)
{
throw new ArgumentNullException(nameof(key));
}

if (dictionary.ContainsKey(key))
{
return false;
}

dictionary.Add(key, value);
return true;
throw new ArgumentNullException(nameof(key));
}

if (dictionary.ContainsKey(key))
{
return false;
}

dictionary.Add(key, value);
return true;
}
}

internal static class ConcurrentDictionaryExtensions
internal static class ConcurrentDictionaryExtensions
{
public static TValue GetOrAdd<TKey, TValue, TArg>(this ConcurrentDictionary<TKey, TValue> dictionary, TKey key, Func<TKey, TArg, TValue> valueFactory, TArg factoryArgument)
{
public static TValue GetOrAdd<TKey, TValue, TArg>(this ConcurrentDictionary<TKey, TValue> dictionary, TKey key, Func<TKey, TArg, TValue> valueFactory, TArg factoryArgument)
if (dictionary == null)
{
if (dictionary == null)
{
throw new ArgumentNullException(nameof(dictionary));
}

if (key == null)
{
throw new ArgumentNullException(nameof(key));
}

if (valueFactory == null)
{
throw new ArgumentNullException(nameof(valueFactory));
}

return dictionary.GetOrAdd(key, k => valueFactory(k, factoryArgument));
throw new ArgumentNullException(nameof(dictionary));
}

if (key == null)
{
throw new ArgumentNullException(nameof(key));
}

if (valueFactory == null)
{
throw new ArgumentNullException(nameof(valueFactory));
}

return dictionary.GetOrAdd(key, k => valueFactory(k, factoryArgument));
}
}

internal static class KeyValuePairExtensions
{
// Based on https://source.dot.net/#System.Private.CoreLib/KeyValuePair.cs,aa57b8e336bf7f59
[EditorBrowsable(EditorBrowsableState.Never)]
public static void Deconstruct<TKey, TValue>(this KeyValuePair<TKey, TValue> pair, out TKey key, out TValue value)
{
key = pair.Key;
value = pair.Value;
}
}

#endif
44 changes: 41 additions & 3 deletions src/DotNetWorker.Core/Hosting/WorkerOptions.cs
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the MIT License. See License.txt in the project root for license information.

using System.Collections.Generic;
using System.Text.Json;
using Azure.Core.Serialization;

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

/// <summary>
/// Gets the optional worker capabilities.
/// </summary>
public IDictionary<string, string> Capabilities { get; } = new Dictionary<string, string>()
{
// Enable these by default, although they are not strictly required and can be removed
{ "HandlesWorkerTerminateMessage", bool.TrueString },
{ "HandlesInvocationCancelMessage", bool.TrueString }
};

/// <summary>
/// Gets and sets the flag for opting in to unwrapping user-code-thrown
/// exceptions when they are surfaced to the Host.
/// </summary>
public bool EnableUserCodeException { get; set; } = false;

public bool EnableUserCodeException
{
get => GetBoolCapability(nameof(EnableUserCodeException));
set => SetBoolCapability(nameof(EnableUserCodeException), value);
}

/// <summary>
/// Gets or sets a value that determines if empty entries should be included in the function trigger message payload.
/// For example, if a set of entries were sent to a messaging service such as Service Bus or Event Hub and your function
/// app has a Service bus trigger or Event hub trigger, only the non-empty entries from the payload will be sent to the
/// function code as trigger data when this setting value is <see langword="false"/>. When it is <see langword="true"/>,
/// All entries will be sent to the function code as it is. Default value for this setting is <see langword="false"/>.
/// </summary>
public bool IncludeEmptyEntriesInMessagePayload { get; set; }
public bool IncludeEmptyEntriesInMessagePayload
{
get => GetBoolCapability(nameof(IncludeEmptyEntriesInMessagePayload));
set => SetBoolCapability(nameof(IncludeEmptyEntriesInMessagePayload), value);
}

private bool GetBoolCapability(string name)
{
return Capabilities.TryGetValue(name, out string? value) && bool.TryParse(value, out bool b) && b;
}

// For false values, the host does not expect the capability to exist; there are some cases where this
// will be interpreted as "true" just because the key is there.
private void SetBoolCapability(string name, bool value)
{
if (value)
{
Capabilities[name] = bool.TrueString;
}
else
{
Capabilities.Remove(name);
}
}
}
}
26 changes: 11 additions & 15 deletions src/DotNetWorker.Grpc/GrpcWorker.cs
Original file line number Diff line number Diff line change
Expand Up @@ -212,24 +212,20 @@ internal static WorkerInitResponse WorkerInitRequestHandler(WorkerInitRequest re

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

response.Capabilities.Add("RpcHttpBodyOnly", bool.TrueString);
response.Capabilities.Add("RawHttpBodyBytes", bool.TrueString);
response.Capabilities.Add("RpcHttpTriggerMetadataRemoved", bool.TrueString);
response.Capabilities.Add("UseNullableValueDictionaryForHttp", bool.TrueString);
response.Capabilities.Add("TypedDataCollection", bool.TrueString);
response.Capabilities.Add("WorkerStatus", bool.TrueString);
response.Capabilities.Add("HandlesWorkerTerminateMessage", bool.TrueString);
response.Capabilities.Add("HandlesInvocationCancelMessage", bool.TrueString);

if (workerOptions.EnableUserCodeException)
// Add additional capabilities defined by WorkerOptions
foreach ((string key, string value) in workerOptions.Capabilities)
{
response.Capabilities.Add("EnableUserCodeException", bool.TrueString);
}
if (workerOptions.IncludeEmptyEntriesInMessagePayload)
{
response.Capabilities.Add("IncludeEmptyEntriesInMessagePayload", bool.TrueString);
response.Capabilities[key] = value;
}

// Add required capabilities; these cannot be modified and will override anything from WorkerOptions
response.Capabilities["RpcHttpBodyOnly"] = bool.TrueString;
response.Capabilities["RawHttpBodyBytes"] = bool.TrueString;
response.Capabilities["RpcHttpTriggerMetadataRemoved"] = bool.TrueString;
response.Capabilities["UseNullableValueDictionaryForHttp"] = bool.TrueString;
response.Capabilities["TypedDataCollection"] = bool.TrueString;
response.Capabilities["WorkerStatus"] = bool.TrueString;

return response;
}

Expand Down
37 changes: 36 additions & 1 deletion test/DotNetWorkerTests/GrpcWorkerTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,10 @@
using Microsoft.Azure.Functions.Worker.Handlers;
using Microsoft.Azure.Functions.Worker.Invocation;
using Microsoft.Azure.Functions.Worker.OutputBindings;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
using Moq;
using Xunit;

Expand All @@ -40,7 +43,8 @@ public GrpcWorkerTests()

_mockApplication
.Setup(m => m.CreateContext(It.IsAny<IInvocationFeatures>(), It.IsAny<CancellationToken>()))
.Returns((IInvocationFeatures f, CancellationToken ct) => {
.Returns((IInvocationFeatures f, CancellationToken ct) =>
{
_context = new TestFunctionContext(f, ct);
return _context;
});
Expand Down Expand Up @@ -176,6 +180,7 @@ public void InitRequest_ReturnsExpectedCapabilities_BasedOnWorkerOptions(
string expectedCapabilityValue = null)
{
var workerOptions = new WorkerOptions();

// Update boolean property values of workerOption based on test input parameters.
workerOptions.GetType().GetProperty(booleanPropertyName)?.SetValue(workerOptions, booleanPropertyValue);

Expand All @@ -202,6 +207,36 @@ public void InitRequest_ReturnsExpectedCapabilities_BasedOnWorkerOptions(
}
}

[Fact]
public void WorkerOptions_CanChangeOptionalCapabilities()
{
var host = new HostBuilder()
.ConfigureFunctionsWorkerDefaults((WorkerOptions options) =>
{
options.Capabilities.Remove("HandlesWorkerTerminateMessage");
options.Capabilities.Add("SomeNewCapability", bool.TrueString);
}).Build();

var workerOptions = host.Services.GetService<IOptions<WorkerOptions>>().Value;
var response = GrpcWorker.WorkerInitRequestHandler(new(), workerOptions);

void AssertKeyAndValue(KeyValuePair<string, string> kvp, string expectedKey, string expectedValue)
{
Assert.Same(expectedKey, kvp.Key);
Assert.Same(expectedValue, kvp.Value);
}

Assert.Collection(response.Capabilities.OrderBy(p => p.Key),
c => AssertKeyAndValue(c, "HandlesInvocationCancelMessage", bool.TrueString),
c => AssertKeyAndValue(c, "RawHttpBodyBytes", bool.TrueString),
c => AssertKeyAndValue(c, "RpcHttpBodyOnly", bool.TrueString),
c => AssertKeyAndValue(c, "RpcHttpTriggerMetadataRemoved", bool.TrueString),
c => AssertKeyAndValue(c, "SomeNewCapability", bool.TrueString),
c => AssertKeyAndValue(c, "TypedDataCollection", bool.TrueString),
c => AssertKeyAndValue(c, "UseNullableValueDictionaryForHttp", bool.TrueString),
c => AssertKeyAndValue(c, "WorkerStatus", bool.TrueString));
}

[Fact]
public async Task Invoke_ReturnsSuccess()
{
Expand Down