Extending struct data binding to support nullable types

This commit is contained in:
Mathew Charles 2016-10-15 10:56:37 -07:00
Родитель f24b44df6a
Коммит a76e483f75
9 изменённых файлов: 136 добавлений и 10 удалений

Просмотреть файл

@ -10,6 +10,7 @@
<Value>Guid</Value>
<Value>linq</Value>
<Value>mockable</Value>
<Value>Nullable</Value>
<Value>Nullables</Value>
<Value>odata</Value>
<Value>Queryable</Value>

Просмотреть файл

@ -3,23 +3,28 @@
using System;
using System.Collections.Generic;
using System.Globalization;
using System.Threading.Tasks;
using Microsoft.Azure.WebJobs.Host.Converters;
using Microsoft.Azure.WebJobs.Host.Protocols;
namespace Microsoft.Azure.WebJobs.Host.Bindings.Data
{
/// <summary>
/// Handles value types (structs) as well as nullable types.
/// </summary>
internal class StructDataBinding<TBindingData> : IBinding
where TBindingData : struct
{
private static readonly IObjectToTypeConverter<TBindingData> Converter =
ObjectToTypeConverterFactory.CreateForStruct<TBindingData>();
private readonly bool _isNullable;
private readonly string _parameterName;
private readonly IArgumentBinding<TBindingData> _argumentBinding;
public StructDataBinding(string parameterName, IArgumentBinding<TBindingData> argumentBinding)
{
_isNullable = TypeUtility.IsNullable(typeof(TBindingData));
_parameterName = parameterName;
_argumentBinding = argumentBinding;
}
@ -40,7 +45,7 @@ namespace Microsoft.Azure.WebJobs.Host.Bindings.Data
if (!Converter.TryConvert(value, out typedValue))
{
throw new InvalidOperationException("Unable to convert value to " + typeof(TBindingData).Name + ".");
throw new InvalidOperationException("Unable to convert value to " + TypeUtility.GetFriendlyName(typeof(TBindingData)) + ".");
}
return BindAsync(typedValue, context);
@ -63,10 +68,10 @@ namespace Microsoft.Azure.WebJobs.Host.Bindings.Data
object untypedValue = bindingData[_parameterName];
if (!(untypedValue is TBindingData))
if (!(untypedValue is TBindingData) && !(untypedValue == null && _isNullable))
{
throw new InvalidOperationException("Binding data for '" + _parameterName +
"' is not of expected type " + typeof(TBindingData).Name + ".");
throw new InvalidOperationException(
string.Format(CultureInfo.InvariantCulture, "Binding data for '{0}' is not of expected type {1}.", _parameterName, TypeUtility.GetFriendlyName(typeof(TBindingData))));
}
TBindingData typedValue = (TBindingData)untypedValue;

Просмотреть файл

@ -8,8 +8,10 @@ using Microsoft.Azure.WebJobs.Host.Converters;
namespace Microsoft.Azure.WebJobs.Host.Bindings.Data
{
/// <summary>
/// Handles value types (structs) as well as nullable types.
/// </summary>
internal class StructDataBindingProvider<TBindingData> : IBindingProvider
where TBindingData : struct
{
private static readonly IDataArgumentBindingProvider<TBindingData> InnerProvider =
new CompositeArgumentBindingProvider<TBindingData>(

Просмотреть файл

@ -14,7 +14,7 @@ namespace Microsoft.Azure.WebJobs.Host.Bindings
return Create(identityConverter);
}
public static IObjectToTypeConverter<TOutput> CreateForStruct<TOutput>() where TOutput : struct
public static IObjectToTypeConverter<TOutput> CreateForStruct<TOutput>()
{
IObjectToTypeConverter<TOutput> identityConverter =
new StructOutputConverter<TOutput, TOutput>(new IdentityConverter<TOutput>());

Просмотреть файл

@ -6,18 +6,19 @@ using Microsoft.Azure.WebJobs.Host.Converters;
namespace Microsoft.Azure.WebJobs.Host.Bindings
{
internal class StructOutputConverter<TInput, TOutput> : IObjectToTypeConverter<TOutput>
where TInput : struct
{
private readonly bool _isNullable;
private readonly IConverter<TInput, TOutput> _innerConverter;
public StructOutputConverter(IConverter<TInput, TOutput> innerConverter)
{
_isNullable = TypeUtility.IsNullable(typeof(TInput));
_innerConverter = innerConverter;
}
public bool TryConvert(object input, out TOutput output)
{
if (!(input is TInput))
if (!(input is TInput) && !(input == null && _isNullable))
{
output = default(TOutput);
return false;

Просмотреть файл

@ -2,12 +2,30 @@
// Licensed under the MIT License. See License.txt in the project root for license information.
using System;
using System.Globalization;
using System.Reflection;
namespace Microsoft.Azure.WebJobs.Host
{
internal static class TypeUtility
{
{
internal static string GetFriendlyName(Type type)
{
if (TypeUtility.IsNullable(type))
{
return string.Format(CultureInfo.InvariantCulture, "Nullable<{0}>", type.GetGenericArguments()[0].Name);
}
else
{
return type.Name;
}
}
internal static bool IsNullable(Type type)
{
return type.IsGenericType && type.GetGenericTypeDefinition() == typeof(Nullable<>);
}
/// <summary>
/// Walk from the parameter up to the containing type, looking for an instance
/// of the specified attribute type, returning it if found.

Просмотреть файл

@ -5,6 +5,7 @@ using System;
using System.Collections.Generic;
using System.Reflection;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.Azure.WebJobs.Host.Bindings;
using Microsoft.Azure.WebJobs.Host.Bindings.Data;
using Xunit;
@ -13,6 +14,70 @@ namespace Microsoft.Azure.WebJobs.Host.UnitTests.Bindings.Data
{
public class DataBindingProviderTests
{
[Fact]
public async Task Create_HandlesNullableTypes()
{
// Arrange
IBindingProvider product = new DataBindingProvider();
string parameterName = "p";
Type parameterType = typeof(int?);
BindingProviderContext context = CreateBindingContext(parameterName, parameterType);
// Act
IBinding binding = await product.TryCreateAsync(context);
// Assert
Assert.NotNull(binding);
var functionBindingContext = new FunctionBindingContext(Guid.NewGuid(), CancellationToken.None, null);
var valueBindingContext = new ValueBindingContext(functionBindingContext, CancellationToken.None);
var bindingData = new Dictionary<string, object>
{
{ "p", 123 }
};
var bindingContext = new BindingContext(valueBindingContext, bindingData);
var valueProvider = await binding.BindAsync(bindingContext);
var value = valueProvider.GetValue();
Assert.Equal(123, value);
bindingData["p"] = null;
bindingContext = new BindingContext(valueBindingContext, bindingData);
valueProvider = await binding.BindAsync(bindingContext);
value = valueProvider.GetValue();
Assert.Null(value);
}
[Fact]
public async Task Create_NullableTypeMismatch_ThrowsExpectedError()
{
// Arrange
IBindingProvider product = new DataBindingProvider();
string parameterName = "p";
Type parameterType = typeof(int?);
BindingProviderContext context = CreateBindingContext(parameterName, parameterType);
// Act
IBinding binding = await product.TryCreateAsync(context);
// Assert
Assert.NotNull(binding);
var functionBindingContext = new FunctionBindingContext(Guid.NewGuid(), CancellationToken.None, null);
var valueBindingContext = new ValueBindingContext(functionBindingContext, CancellationToken.None);
var bindingData = new Dictionary<string, object>
{
{ "p", "123" }
};
var bindingContext = new BindingContext(valueBindingContext, bindingData);
var ex = await Assert.ThrowsAsync<InvalidOperationException>(async () =>
{
await binding.BindAsync(bindingContext);
});
Assert.Equal("Binding data for 'p' is not of expected type Nullable<Int32>.", ex.Message);
}
[Fact]
public void Create_ReturnsNull_IfByRefParameter()
{

Просмотреть файл

@ -0,0 +1,33 @@
// 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 Xunit;
namespace Microsoft.Azure.WebJobs.Host.UnitTests
{
public class TypeUtilityTests
{
[Theory]
[InlineData(typeof(TypeUtilityTests), false)]
[InlineData(typeof(string), false)]
[InlineData(typeof(int), false)]
[InlineData(typeof(int?), true)]
[InlineData(typeof(Nullable<int>), true)]
public void IsNullable_ReturnsExpectedResult(Type type, bool expected)
{
Assert.Equal(expected, TypeUtility.IsNullable(type));
}
[Theory]
[InlineData(typeof(TypeUtilityTests), "TypeUtilityTests")]
[InlineData(typeof(string), "String")]
[InlineData(typeof(int), "Int32")]
[InlineData(typeof(int?), "Nullable<Int32>")]
[InlineData(typeof(Nullable<int>), "Nullable<Int32>")]
public void GetFriendlyName(Type type, string expected)
{
Assert.Equal(expected, TypeUtility.GetFriendlyName(type));
}
}
}

Просмотреть файл

@ -224,6 +224,7 @@
<Compile Include="TestJobHostContextFactory.cs" />
<Compile Include="TestJobHostConfiguration.cs" />
<Compile Include="Timers\RandomizedExponentialBackoffStrategyTests.cs" />
<Compile Include="TypeUtilityTests.cs" />
<Compile Include="WebJobsShutdownWatcherTests.cs" />
<Compile Include="Blobs\BlobPathSourceTests.cs" />
<Compile Include="Blobs\BlobPathTests.cs" />