Extending struct data binding to support nullable types
This commit is contained in:
Родитель
f24b44df6a
Коммит
a76e483f75
|
@ -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" />
|
||||
|
|
Загрузка…
Ссылка в новой задаче