From a76e483f757c35a382e6396b1d16e1778e0f60f0 Mon Sep 17 00:00:00 2001 From: Mathew Charles Date: Sat, 15 Oct 2016 10:56:37 -0700 Subject: [PATCH] Extending struct data binding to support nullable types --- Settings.StyleCop | 1 + .../Bindings/Data/StructDataBinding.cs | 15 +++-- .../Data/StructDataBindingProvider.cs | 4 +- .../Bindings/ObjectToTypeConverterFactory.cs | 2 +- .../Bindings/StructOutputConverter.cs | 5 +- .../TypeUtility.cs | 20 +++++- .../Bindings/Data/DataBindingProviderTests.cs | 65 +++++++++++++++++++ .../TypeUtilityTests.cs | 33 ++++++++++ .../WebJobs.Host.UnitTests.csproj | 1 + 9 files changed, 136 insertions(+), 10 deletions(-) create mode 100644 test/Microsoft.Azure.WebJobs.Host.UnitTests/TypeUtilityTests.cs diff --git a/Settings.StyleCop b/Settings.StyleCop index f98112d9..4818eee7 100644 --- a/Settings.StyleCop +++ b/Settings.StyleCop @@ -10,6 +10,7 @@ Guid linq mockable + Nullable Nullables odata Queryable diff --git a/src/Microsoft.Azure.WebJobs.Host/Bindings/Data/StructDataBinding.cs b/src/Microsoft.Azure.WebJobs.Host/Bindings/Data/StructDataBinding.cs index 7bd249c3..ad946b36 100644 --- a/src/Microsoft.Azure.WebJobs.Host/Bindings/Data/StructDataBinding.cs +++ b/src/Microsoft.Azure.WebJobs.Host/Bindings/Data/StructDataBinding.cs @@ -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 { + /// + /// Handles value types (structs) as well as nullable types. + /// internal class StructDataBinding : IBinding - where TBindingData : struct { private static readonly IObjectToTypeConverter Converter = ObjectToTypeConverterFactory.CreateForStruct(); + private readonly bool _isNullable; private readonly string _parameterName; private readonly IArgumentBinding _argumentBinding; public StructDataBinding(string parameterName, IArgumentBinding 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; diff --git a/src/Microsoft.Azure.WebJobs.Host/Bindings/Data/StructDataBindingProvider.cs b/src/Microsoft.Azure.WebJobs.Host/Bindings/Data/StructDataBindingProvider.cs index 813d1b56..74d41e0d 100644 --- a/src/Microsoft.Azure.WebJobs.Host/Bindings/Data/StructDataBindingProvider.cs +++ b/src/Microsoft.Azure.WebJobs.Host/Bindings/Data/StructDataBindingProvider.cs @@ -8,8 +8,10 @@ using Microsoft.Azure.WebJobs.Host.Converters; namespace Microsoft.Azure.WebJobs.Host.Bindings.Data { + /// + /// Handles value types (structs) as well as nullable types. + /// internal class StructDataBindingProvider : IBindingProvider - where TBindingData : struct { private static readonly IDataArgumentBindingProvider InnerProvider = new CompositeArgumentBindingProvider( diff --git a/src/Microsoft.Azure.WebJobs.Host/Bindings/ObjectToTypeConverterFactory.cs b/src/Microsoft.Azure.WebJobs.Host/Bindings/ObjectToTypeConverterFactory.cs index 75de9a46..a39f5270 100644 --- a/src/Microsoft.Azure.WebJobs.Host/Bindings/ObjectToTypeConverterFactory.cs +++ b/src/Microsoft.Azure.WebJobs.Host/Bindings/ObjectToTypeConverterFactory.cs @@ -14,7 +14,7 @@ namespace Microsoft.Azure.WebJobs.Host.Bindings return Create(identityConverter); } - public static IObjectToTypeConverter CreateForStruct() where TOutput : struct + public static IObjectToTypeConverter CreateForStruct() { IObjectToTypeConverter identityConverter = new StructOutputConverter(new IdentityConverter()); diff --git a/src/Microsoft.Azure.WebJobs.Host/Bindings/StructOutputConverter.cs b/src/Microsoft.Azure.WebJobs.Host/Bindings/StructOutputConverter.cs index efff38f6..b6efa96d 100644 --- a/src/Microsoft.Azure.WebJobs.Host/Bindings/StructOutputConverter.cs +++ b/src/Microsoft.Azure.WebJobs.Host/Bindings/StructOutputConverter.cs @@ -6,18 +6,19 @@ using Microsoft.Azure.WebJobs.Host.Converters; namespace Microsoft.Azure.WebJobs.Host.Bindings { internal class StructOutputConverter : IObjectToTypeConverter - where TInput : struct { + private readonly bool _isNullable; private readonly IConverter _innerConverter; public StructOutputConverter(IConverter 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; diff --git a/src/Microsoft.Azure.WebJobs.Host/TypeUtility.cs b/src/Microsoft.Azure.WebJobs.Host/TypeUtility.cs index d25b9ae2..adabaa58 100644 --- a/src/Microsoft.Azure.WebJobs.Host/TypeUtility.cs +++ b/src/Microsoft.Azure.WebJobs.Host/TypeUtility.cs @@ -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<>); + } + /// /// Walk from the parameter up to the containing type, looking for an instance /// of the specified attribute type, returning it if found. diff --git a/test/Microsoft.Azure.WebJobs.Host.UnitTests/Bindings/Data/DataBindingProviderTests.cs b/test/Microsoft.Azure.WebJobs.Host.UnitTests/Bindings/Data/DataBindingProviderTests.cs index 210c61d8..13d27380 100644 --- a/test/Microsoft.Azure.WebJobs.Host.UnitTests/Bindings/Data/DataBindingProviderTests.cs +++ b/test/Microsoft.Azure.WebJobs.Host.UnitTests/Bindings/Data/DataBindingProviderTests.cs @@ -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 + { + { "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 + { + { "p", "123" } + }; + var bindingContext = new BindingContext(valueBindingContext, bindingData); + var ex = await Assert.ThrowsAsync(async () => + { + await binding.BindAsync(bindingContext); + }); + Assert.Equal("Binding data for 'p' is not of expected type Nullable.", ex.Message); + } + [Fact] public void Create_ReturnsNull_IfByRefParameter() { diff --git a/test/Microsoft.Azure.WebJobs.Host.UnitTests/TypeUtilityTests.cs b/test/Microsoft.Azure.WebJobs.Host.UnitTests/TypeUtilityTests.cs new file mode 100644 index 00000000..a30ae0fc --- /dev/null +++ b/test/Microsoft.Azure.WebJobs.Host.UnitTests/TypeUtilityTests.cs @@ -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), 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")] + [InlineData(typeof(Nullable), "Nullable")] + public void GetFriendlyName(Type type, string expected) + { + Assert.Equal(expected, TypeUtility.GetFriendlyName(type)); + } + } +} diff --git a/test/Microsoft.Azure.WebJobs.Host.UnitTests/WebJobs.Host.UnitTests.csproj b/test/Microsoft.Azure.WebJobs.Host.UnitTests/WebJobs.Host.UnitTests.csproj index ff44f03c..67ab2a61 100644 --- a/test/Microsoft.Azure.WebJobs.Host.UnitTests/WebJobs.Host.UnitTests.csproj +++ b/test/Microsoft.Azure.WebJobs.Host.UnitTests/WebJobs.Host.UnitTests.csproj @@ -224,6 +224,7 @@ +