Skip to content
Merged
Show file tree
Hide file tree
Changes from all 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
4 changes: 2 additions & 2 deletions release_notes.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,8 @@

### Microsoft.Azure.Functions.Worker.Core <version>

- <entry>
- Adding optional parameter support (#1868)

### Microsoft.Azure.Functions.Worker.Grpc <version>

- <entry>
- Adding optional parameter support (#1868)
Original file line number Diff line number Diff line change
Expand Up @@ -97,6 +97,21 @@ public async ValueTask<FunctionInputBindingResult> BindFunctionInputAsync(Functi
errors.Add(
$"Cannot convert input parameter '{param.Name}' to type '{param.Type.FullName}' from type '{source.GetType().FullName}'. Error:{bindingResult.Error}");
}
else if (bindingResult.Status == ConversionStatus.Unhandled)
{
// If still unhandled after going through all converters,check an explicit default value was provided for the parameter.
if (param.HasDefaultValue)
{
parameterValues[i] = param.DefaultValue;
}
else
{
// We could not find a value for this param. should throw.
errors ??= new List<string>();
errors.Add(
$"Could not populate the value for '{param.Name}' parameter. Consider updating the parameter with a default value.");
}
}
}

// found errors
Expand Down
39 changes: 39 additions & 0 deletions src/DotNetWorker.Core/Definition/FunctionParameter.cs
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,18 @@ public FunctionParameter(string name, Type type)
{
}

/// <summary>
/// Creates an instance of the <see cref="FunctionParameter"/> class.
/// </summary>
/// <param name="name">The parameter name.</param>
/// <param name="type">The <see cref="System.Type"/> of the parameter.</param>
/// <param name="hasDefaultValue">Value that indicates whether the parameter has a default value.</param>
/// <param name="defaultValue">Default value of the parameter.</param>
public FunctionParameter(string name, Type type, bool hasDefaultValue, object? defaultValue)
: this(name, type, hasDefaultValue, defaultValue, ImmutableDictionary<string, object>.Empty)
{
}

/// <summary>
/// Creates an instance of the <see cref="FunctionParameter"/> class.
/// </summary>
Expand All @@ -35,6 +47,23 @@ public FunctionParameter(string name, Type type, IReadOnlyDictionary<string, obj
Properties = properties ?? throw new ArgumentNullException(nameof(properties));
}

/// <summary>
/// Creates an instance of the <see cref="FunctionParameter"/> class.
/// </summary>
/// <param name="name">The parameter name.</param>
/// <param name="type">The <see cref="System.Type"/> of the parameter.</param>
/// <param name="hasDefaultValue">Value that indicates whether the parameter has a default value.</param>
/// <param name="defaultValue">Default value of the parameter.</param>
/// <param name="properties">The properties of the parameter.</param>
public FunctionParameter(string name, Type type, bool hasDefaultValue, object? defaultValue, IReadOnlyDictionary<string, object> properties)
{
Name = name ?? throw new ArgumentNullException(nameof(name));
Type = type ?? throw new ArgumentNullException(nameof(type));
DefaultValue = defaultValue ?? default;
HasDefaultValue = hasDefaultValue;
Properties = properties ?? throw new ArgumentNullException(nameof(properties));
}

/// <summary>
/// Gets the parameter name.
/// </summary>
Expand All @@ -45,6 +74,16 @@ public FunctionParameter(string name, Type type, IReadOnlyDictionary<string, obj
/// </summary>
public Type Type { get; }

/// <summary>
/// Gets a value that indicates whether this parameter has a default value.
/// </summary>
public bool HasDefaultValue { get; }

/// <summary>
/// Gets the default value of the parameter if exists, else null.
/// </summary>
public object? DefaultValue { get; }

/// <summary>
/// A dictionary holding properties of this parameter.
/// </summary>
Expand Down
2 changes: 1 addition & 1 deletion src/DotNetWorker.Grpc/Definition/GrpcFunctionDefinition.cs
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,7 @@ public GrpcFunctionDefinition(FunctionLoadRequest loadRequest, IMethodInfoLocato
Parameters = methodInfoLocator.GetMethod(PathToAssembly, EntryPoint)
.GetParameters()
.Where(p => p.Name != null)
.Select(p => new FunctionParameter(p.Name!, p.ParameterType, GetAdditionalPropertiesDictionary(p)))
.Select(p => new FunctionParameter(p.Name!, p.ParameterType, p.HasDefaultValue, p.DefaultValue, GetAdditionalPropertiesDictionary(p)))
.ToImmutableArray();
}

Expand Down
72 changes: 72 additions & 0 deletions test/DotNetWorkerTests/DefaultModelBindingFeatureTests.cs
Original file line number Diff line number Diff line change
@@ -1,10 +1,12 @@
using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Linq;
using System.Text.Json;
using System.Threading;
using Azure.Core.Serialization;
using Microsoft.Azure.Functions.Worker.Context.Features;
using Microsoft.Azure.Functions.Worker.Http;
using Microsoft.Azure.Functions.Worker.Tests.Features;
using Microsoft.Extensions.DependencyInjection;
using Xunit;
Expand Down Expand Up @@ -59,6 +61,76 @@ public async void BindFunctionInputAsync_Populates_ParametersUsingConverters()
Assert.Equal("0ab4800e-1308-4e9f-be5f-4372717e68eb", guid.ToString());
}

[Fact]
public async void BindFunctionInputAsync_Populates_Parameter_Using_DefaultValue_When_CouldNot_Populate_From_InputData()
{
var features = new InvocationFeatures(Enumerable.Empty<IInvocationFeatureProvider>());
var httpFunctionDefinition = new TestFunctionDefinition(parameters: new FunctionParameter[]
{
new("req", typeof(HttpRequestData)),
new("fooId", typeof(int), true, defaultValue: 100),
new("bar", typeof(string), true, defaultValue: null)
},
inputBindings: new Dictionary<string, BindingMetadata>
{
{ "req", new TestBindingMetadata("req","httpTrigger",BindingDirection.In) }
}); ;
features.Set<FunctionDefinition>(httpFunctionDefinition);

var functionContext = new TestFunctionContext(httpFunctionDefinition, invocation: null, CancellationToken.None, serviceProvider: _serviceProvider, features: features);
var grpcHttpReq = new GrpcHttpRequestData(TestUtility.CreateRpcHttp(), functionContext);
var functionBindings = new TestFunctionBindingsFeature
{
InputData = new ReadOnlyDictionary<string, object>(new Dictionary<string, object> { { "req", grpcHttpReq } })
};
features.Set<IFunctionBindingsFeature>(functionBindings);
features.Set(_serviceProvider.GetService<IInputConversionFeature>());

// Act
var bindingResult = await _functionInputBindingFeature.BindFunctionInputAsync(functionContext);
var parameterValuesArray = bindingResult.Values;

// Assert
var httpReqData = TestUtility.AssertIsTypeAndConvert<HttpRequestData>(parameterValuesArray[0]);
Assert.NotNull(httpReqData);
var fooId = TestUtility.AssertIsTypeAndConvert<int>(parameterValuesArray[1]);
Assert.Equal(100, fooId);
var bar = parameterValuesArray[2];
Assert.Null(bar);
}

[Fact]
public async void BindFunctionInputAsync_Throws_When_Explicit_OptionalParametersValueNotPresent()
{
// 'fooId' is a parameter defined for the function without a default value
// and 'InputData' does not have a corresponding entry for that we could use to populate that parameter.

IInvocationFeatures features = new InvocationFeatures(Enumerable.Empty<IInvocationFeatureProvider>());
var httpFunctionDefinition = new TestFunctionDefinition(parameters: new FunctionParameter[]
{
new("req", typeof(HttpRequestData)),
new("fooId", typeof(int))
},
inputBindings: new Dictionary<string, BindingMetadata>
{
{ "req", new TestBindingMetadata("req","httpTrigger",BindingDirection.In) }
}); ;
features.Set<FunctionDefinition>(httpFunctionDefinition);

var functionContext = new TestFunctionContext(httpFunctionDefinition, invocation: null, CancellationToken.None, serviceProvider: _serviceProvider, features: features);
var grpcHttpReq = new GrpcHttpRequestData(TestUtility.CreateRpcHttp(), functionContext);
var functionBindings = new TestFunctionBindingsFeature
{
InputData = new ReadOnlyDictionary<string, object>(new Dictionary<string, object> { { "req", grpcHttpReq } })
};
features.Set<IFunctionBindingsFeature>(functionBindings);
features.Set(_serviceProvider.GetService<IInputConversionFeature>());

// Assert
var exception = await Assert.ThrowsAsync<FunctionInputConverterException>(async () => await _functionInputBindingFeature.BindFunctionInputAsync(functionContext));
Assert.Equal("Error converting 1 input parameters for Function 'TestName': Could not populate the value for 'fooId' parameter. Consider updating the parameter with a default value.", exception.Message);
}

[Fact]
public async void BindFunctionInputAsync_Returns_Cached_Value_When_Called_SecondTime()
{
Expand Down
18 changes: 3 additions & 15 deletions test/DotNetWorkerTests/FunctionContextHttpExtensionTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -85,7 +85,7 @@ public async Task GetHttpRequestDataAsync_Works_For_Http_Invocation()
_features.Set<FunctionDefinition>(httpFunctionDefinition);

_defaultFunctionContext = new DefaultFunctionContext(_serviceScopeFactory, _features, CancellationToken.None);
var grpcHttpReq = new GrpcHttpRequestData(CreateRpcHttp(), _defaultFunctionContext);
var grpcHttpReq = new GrpcHttpRequestData(TestUtility.CreateRpcHttp(), _defaultFunctionContext);
var functionBindings = new TestFunctionBindingsFeature
{
InputData = new ReadOnlyDictionary<string, object>(new Dictionary<string, object> { { "req", grpcHttpReq } })
Expand Down Expand Up @@ -118,7 +118,7 @@ public void GetHttpResponseData_Works_For_Http_Invocation()
_features.Set<FunctionDefinition>(new TestFunctionDefinition());

_defaultFunctionContext = new DefaultFunctionContext(_serviceScopeFactory, _features, CancellationToken.None);
var grpcHttpReq = new GrpcHttpRequestData(CreateRpcHttp(), _defaultFunctionContext);
var grpcHttpReq = new GrpcHttpRequestData(TestUtility.CreateRpcHttp(), _defaultFunctionContext);
var functionBindings = new TestFunctionBindingsFeature
{
InputData = new ReadOnlyDictionary<string, object>(new Dictionary<string, object> { { "req", grpcHttpReq } }),
Expand Down Expand Up @@ -153,7 +153,7 @@ public void GetHttpResponseData_Works_For_Http_Invocation_POCO_OutputBinding_Pro
}));

_defaultFunctionContext = new DefaultFunctionContext(_serviceScopeFactory, _features, CancellationToken.None);
var grpcHttpReq = new GrpcHttpRequestData(CreateRpcHttp(), _defaultFunctionContext);
var grpcHttpReq = new GrpcHttpRequestData(TestUtility.CreateRpcHttp(), _defaultFunctionContext);
var functionBindings = new TestFunctionBindingsFeature
{
InputData = new ReadOnlyDictionary<string, object>(new Dictionary<string, object> { { "req", grpcHttpReq } })
Expand All @@ -179,17 +179,5 @@ public void GetHttpResponseData_Works_For_Http_Invocation_POCO_OutputBinding_Pro
HttpResponseData actual2 = _defaultFunctionContext.GetHttpResponseData();
Assert.True(actual2.Headers.First(a => a.Key == "X-Foo-Id").Value.Any());
}

private RpcHttp CreateRpcHttp()
{
var rpcHttp = new RpcHttp
{
Url = "https://m.sn"
};
rpcHttp.NullableHeaders["Accept-Encoding"] = new NullableString() { Value = "gzip, deflate" };
rpcHttp.NullableHeaders["Cookie"] = new NullableString() { Value = "theme=light; x-token=foo" };

return rpcHttp;
}
}
}
12 changes: 12 additions & 0 deletions test/DotNetWorkerTests/TestUtility.cs
Original file line number Diff line number Diff line change
Expand Up @@ -87,5 +87,17 @@ public static InvocationRequest CreateInvocationRequestWithNullRetryContext(stri
RetryContext = null
};
}

internal static RpcHttp CreateRpcHttp()
{
var rpcHttp = new RpcHttp
{
Url = "https://m.sn"
};
rpcHttp.NullableHeaders["Accept-Encoding"] = new NullableString() { Value = "gzip, deflate" };
rpcHttp.NullableHeaders["Cookie"] = new NullableString() { Value = "theme=light; x-token=foo" };

return rpcHttp;
}
}
}