Skip to content
Merged
Show file tree
Hide file tree
Changes from 4 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 @@ -5,6 +5,9 @@
using System.Collections.Generic;
using System.Collections.Immutable;
using System.Linq;
using System.Reflection;
using System.Runtime.CompilerServices;
using System.Runtime.Serialization;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.Azure.Functions.Worker.Converters;
Expand Down Expand Up @@ -97,6 +100,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.DefaultValue is not null)
Comment thread
kshyju marked this conversation as resolved.
Outdated
{
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
31 changes: 31 additions & 0 deletions src/DotNetWorker.Core/Definition/FunctionParameter.cs
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,17 @@ 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="defaultValue">Default value of the parameter.</param>
public FunctionParameter(string name, Type type, object? defaultValue)
: this(name, type, defaultValue, ImmutableDictionary<string, object>.Empty)
{
}

/// <summary>
/// Creates an instance of the <see cref="FunctionParameter"/> class.
/// </summary>
Expand All @@ -35,6 +46,21 @@ 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="defaultValue">Default value of the parameter.</param>
/// <param name="properties">The properties of the parameter.</param>
public FunctionParameter(string name, Type type, object? defaultValue, IReadOnlyDictionary<string, object> properties)
{
Name = name ?? throw new ArgumentNullException(nameof(name));
Type = type ?? throw new ArgumentNullException(nameof(type));
DefaultValue = defaultValue ?? default;
Properties = properties ?? throw new ArgumentNullException(nameof(properties));
}

/// <summary>
/// Gets the parameter name.
/// </summary>
Expand All @@ -45,6 +71,11 @@ public FunctionParameter(string name, Type type, IReadOnlyDictionary<string, obj
/// </summary>
public Type Type { 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! : null, GetAdditionalPropertiesDictionary(p)))
.ToImmutableArray();
}

Expand Down
69 changes: 69 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,73 @@ 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), defaultValue: 100)
},
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);
}

[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;
}
}
}