Skip to content

Commit a76e483

Browse files
committed
Extending struct data binding to support nullable types
1 parent f24b44d commit a76e483

File tree

9 files changed

+136
-10
lines changed

9 files changed

+136
-10
lines changed

Settings.StyleCop

+1
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010
<Value>Guid</Value>
1111
<Value>linq</Value>
1212
<Value>mockable</Value>
13+
<Value>Nullable</Value>
1314
<Value>Nullables</Value>
1415
<Value>odata</Value>
1516
<Value>Queryable</Value>

src/Microsoft.Azure.WebJobs.Host/Bindings/Data/StructDataBinding.cs

+10-5
Original file line numberDiff line numberDiff line change
@@ -3,23 +3,28 @@
33

44
using System;
55
using System.Collections.Generic;
6+
using System.Globalization;
67
using System.Threading.Tasks;
78
using Microsoft.Azure.WebJobs.Host.Converters;
89
using Microsoft.Azure.WebJobs.Host.Protocols;
910

1011
namespace Microsoft.Azure.WebJobs.Host.Bindings.Data
1112
{
13+
/// <summary>
14+
/// Handles value types (structs) as well as nullable types.
15+
/// </summary>
1216
internal class StructDataBinding<TBindingData> : IBinding
13-
where TBindingData : struct
1417
{
1518
private static readonly IObjectToTypeConverter<TBindingData> Converter =
1619
ObjectToTypeConverterFactory.CreateForStruct<TBindingData>();
1720

21+
private readonly bool _isNullable;
1822
private readonly string _parameterName;
1923
private readonly IArgumentBinding<TBindingData> _argumentBinding;
2024

2125
public StructDataBinding(string parameterName, IArgumentBinding<TBindingData> argumentBinding)
2226
{
27+
_isNullable = TypeUtility.IsNullable(typeof(TBindingData));
2328
_parameterName = parameterName;
2429
_argumentBinding = argumentBinding;
2530
}
@@ -40,7 +45,7 @@ public Task<IValueProvider> BindAsync(object value, ValueBindingContext context)
4045

4146
if (!Converter.TryConvert(value, out typedValue))
4247
{
43-
throw new InvalidOperationException("Unable to convert value to " + typeof(TBindingData).Name + ".");
48+
throw new InvalidOperationException("Unable to convert value to " + TypeUtility.GetFriendlyName(typeof(TBindingData)) + ".");
4449
}
4550

4651
return BindAsync(typedValue, context);
@@ -63,10 +68,10 @@ public Task<IValueProvider> BindAsync(BindingContext context)
6368

6469
object untypedValue = bindingData[_parameterName];
6570

66-
if (!(untypedValue is TBindingData))
71+
if (!(untypedValue is TBindingData) && !(untypedValue == null && _isNullable))
6772
{
68-
throw new InvalidOperationException("Binding data for '" + _parameterName +
69-
"' is not of expected type " + typeof(TBindingData).Name + ".");
73+
throw new InvalidOperationException(
74+
string.Format(CultureInfo.InvariantCulture, "Binding data for '{0}' is not of expected type {1}.", _parameterName, TypeUtility.GetFriendlyName(typeof(TBindingData))));
7075
}
7176

7277
TBindingData typedValue = (TBindingData)untypedValue;

src/Microsoft.Azure.WebJobs.Host/Bindings/Data/StructDataBindingProvider.cs

+3-1
Original file line numberDiff line numberDiff line change
@@ -8,8 +8,10 @@
88

99
namespace Microsoft.Azure.WebJobs.Host.Bindings.Data
1010
{
11+
/// <summary>
12+
/// Handles value types (structs) as well as nullable types.
13+
/// </summary>
1114
internal class StructDataBindingProvider<TBindingData> : IBindingProvider
12-
where TBindingData : struct
1315
{
1416
private static readonly IDataArgumentBindingProvider<TBindingData> InnerProvider =
1517
new CompositeArgumentBindingProvider<TBindingData>(

src/Microsoft.Azure.WebJobs.Host/Bindings/ObjectToTypeConverterFactory.cs

+1-1
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ public static IObjectToTypeConverter<TOutput> CreateForClass<TOutput>() where TO
1414
return Create(identityConverter);
1515
}
1616

17-
public static IObjectToTypeConverter<TOutput> CreateForStruct<TOutput>() where TOutput : struct
17+
public static IObjectToTypeConverter<TOutput> CreateForStruct<TOutput>()
1818
{
1919
IObjectToTypeConverter<TOutput> identityConverter =
2020
new StructOutputConverter<TOutput, TOutput>(new IdentityConverter<TOutput>());

src/Microsoft.Azure.WebJobs.Host/Bindings/StructOutputConverter.cs

+3-2
Original file line numberDiff line numberDiff line change
@@ -6,18 +6,19 @@
66
namespace Microsoft.Azure.WebJobs.Host.Bindings
77
{
88
internal class StructOutputConverter<TInput, TOutput> : IObjectToTypeConverter<TOutput>
9-
where TInput : struct
109
{
10+
private readonly bool _isNullable;
1111
private readonly IConverter<TInput, TOutput> _innerConverter;
1212

1313
public StructOutputConverter(IConverter<TInput, TOutput> innerConverter)
1414
{
15+
_isNullable = TypeUtility.IsNullable(typeof(TInput));
1516
_innerConverter = innerConverter;
1617
}
1718

1819
public bool TryConvert(object input, out TOutput output)
1920
{
20-
if (!(input is TInput))
21+
if (!(input is TInput) && !(input == null && _isNullable))
2122
{
2223
output = default(TOutput);
2324
return false;

src/Microsoft.Azure.WebJobs.Host/TypeUtility.cs

+19-1
Original file line numberDiff line numberDiff line change
@@ -2,12 +2,30 @@
22
// Licensed under the MIT License. See License.txt in the project root for license information.
33

44
using System;
5+
using System.Globalization;
56
using System.Reflection;
67

78
namespace Microsoft.Azure.WebJobs.Host
89
{
910
internal static class TypeUtility
10-
{
11+
{
12+
internal static string GetFriendlyName(Type type)
13+
{
14+
if (TypeUtility.IsNullable(type))
15+
{
16+
return string.Format(CultureInfo.InvariantCulture, "Nullable<{0}>", type.GetGenericArguments()[0].Name);
17+
}
18+
else
19+
{
20+
return type.Name;
21+
}
22+
}
23+
24+
internal static bool IsNullable(Type type)
25+
{
26+
return type.IsGenericType && type.GetGenericTypeDefinition() == typeof(Nullable<>);
27+
}
28+
1129
/// <summary>
1230
/// Walk from the parameter up to the containing type, looking for an instance
1331
/// of the specified attribute type, returning it if found.

test/Microsoft.Azure.WebJobs.Host.UnitTests/Bindings/Data/DataBindingProviderTests.cs

+65
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
using System.Collections.Generic;
66
using System.Reflection;
77
using System.Threading;
8+
using System.Threading.Tasks;
89
using Microsoft.Azure.WebJobs.Host.Bindings;
910
using Microsoft.Azure.WebJobs.Host.Bindings.Data;
1011
using Xunit;
@@ -13,6 +14,70 @@ namespace Microsoft.Azure.WebJobs.Host.UnitTests.Bindings.Data
1314
{
1415
public class DataBindingProviderTests
1516
{
17+
[Fact]
18+
public async Task Create_HandlesNullableTypes()
19+
{
20+
// Arrange
21+
IBindingProvider product = new DataBindingProvider();
22+
23+
string parameterName = "p";
24+
Type parameterType = typeof(int?);
25+
BindingProviderContext context = CreateBindingContext(parameterName, parameterType);
26+
27+
// Act
28+
IBinding binding = await product.TryCreateAsync(context);
29+
30+
// Assert
31+
Assert.NotNull(binding);
32+
33+
var functionBindingContext = new FunctionBindingContext(Guid.NewGuid(), CancellationToken.None, null);
34+
var valueBindingContext = new ValueBindingContext(functionBindingContext, CancellationToken.None);
35+
var bindingData = new Dictionary<string, object>
36+
{
37+
{ "p", 123 }
38+
};
39+
var bindingContext = new BindingContext(valueBindingContext, bindingData);
40+
var valueProvider = await binding.BindAsync(bindingContext);
41+
var value = valueProvider.GetValue();
42+
Assert.Equal(123, value);
43+
44+
bindingData["p"] = null;
45+
bindingContext = new BindingContext(valueBindingContext, bindingData);
46+
valueProvider = await binding.BindAsync(bindingContext);
47+
value = valueProvider.GetValue();
48+
Assert.Null(value);
49+
}
50+
51+
[Fact]
52+
public async Task Create_NullableTypeMismatch_ThrowsExpectedError()
53+
{
54+
// Arrange
55+
IBindingProvider product = new DataBindingProvider();
56+
57+
string parameterName = "p";
58+
Type parameterType = typeof(int?);
59+
BindingProviderContext context = CreateBindingContext(parameterName, parameterType);
60+
61+
// Act
62+
IBinding binding = await product.TryCreateAsync(context);
63+
64+
// Assert
65+
Assert.NotNull(binding);
66+
67+
var functionBindingContext = new FunctionBindingContext(Guid.NewGuid(), CancellationToken.None, null);
68+
var valueBindingContext = new ValueBindingContext(functionBindingContext, CancellationToken.None);
69+
var bindingData = new Dictionary<string, object>
70+
{
71+
{ "p", "123" }
72+
};
73+
var bindingContext = new BindingContext(valueBindingContext, bindingData);
74+
var ex = await Assert.ThrowsAsync<InvalidOperationException>(async () =>
75+
{
76+
await binding.BindAsync(bindingContext);
77+
});
78+
Assert.Equal("Binding data for 'p' is not of expected type Nullable<Int32>.", ex.Message);
79+
}
80+
1681
[Fact]
1782
public void Create_ReturnsNull_IfByRefParameter()
1883
{
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
// Copyright (c) .NET Foundation. All rights reserved.
2+
// Licensed under the MIT License. See License.txt in the project root for license information.
3+
4+
using System;
5+
using Xunit;
6+
7+
namespace Microsoft.Azure.WebJobs.Host.UnitTests
8+
{
9+
public class TypeUtilityTests
10+
{
11+
[Theory]
12+
[InlineData(typeof(TypeUtilityTests), false)]
13+
[InlineData(typeof(string), false)]
14+
[InlineData(typeof(int), false)]
15+
[InlineData(typeof(int?), true)]
16+
[InlineData(typeof(Nullable<int>), true)]
17+
public void IsNullable_ReturnsExpectedResult(Type type, bool expected)
18+
{
19+
Assert.Equal(expected, TypeUtility.IsNullable(type));
20+
}
21+
22+
[Theory]
23+
[InlineData(typeof(TypeUtilityTests), "TypeUtilityTests")]
24+
[InlineData(typeof(string), "String")]
25+
[InlineData(typeof(int), "Int32")]
26+
[InlineData(typeof(int?), "Nullable<Int32>")]
27+
[InlineData(typeof(Nullable<int>), "Nullable<Int32>")]
28+
public void GetFriendlyName(Type type, string expected)
29+
{
30+
Assert.Equal(expected, TypeUtility.GetFriendlyName(type));
31+
}
32+
}
33+
}

test/Microsoft.Azure.WebJobs.Host.UnitTests/WebJobs.Host.UnitTests.csproj

+1
Original file line numberDiff line numberDiff line change
@@ -224,6 +224,7 @@
224224
<Compile Include="TestJobHostContextFactory.cs" />
225225
<Compile Include="TestJobHostConfiguration.cs" />
226226
<Compile Include="Timers\RandomizedExponentialBackoffStrategyTests.cs" />
227+
<Compile Include="TypeUtilityTests.cs" />
227228
<Compile Include="WebJobsShutdownWatcherTests.cs" />
228229
<Compile Include="Blobs\BlobPathSourceTests.cs" />
229230
<Compile Include="Blobs\BlobPathTests.cs" />

0 commit comments

Comments
 (0)