diff options
author | raghuramn <ranadimi@microsoft.com> | 2012-09-06 21:36:52 +0400 |
---|---|---|
committer | raghuramn <ranadimi@microsoft.com> | 2012-10-05 05:47:14 +0400 |
commit | 504b0aa28c37ee1d7940e789d28e3020297be42a (patch) | |
tree | d76fad336b1937b65a348a02ccdb9d2059b606f9 | |
parent | 5c7bb4a4fc34b67ca3648afa569b035efe46892c (diff) |
Issue 325: Add parameter binder for supporting odata literal format.
Primitives in OData url's have a different representation than the normal
webapi way. For example, Guid's are represented as guid'0000-00....' .
default webapi model binding fails for these url's.
This commit adds a ODataModelBinderProvider that can deal with such urls.
11 files changed, 719 insertions, 157 deletions
diff --git a/src/System.Web.Http.OData/OData/Formatter/Deserialization/ODataEntryDeserializer.cs b/src/System.Web.Http.OData/OData/Formatter/Deserialization/ODataEntryDeserializer.cs index 9c34ef9d..85d807de 100644 --- a/src/System.Web.Http.OData/OData/Formatter/Deserialization/ODataEntryDeserializer.cs +++ b/src/System.Web.Http.OData/OData/Formatter/Deserialization/ODataEntryDeserializer.cs @@ -2,15 +2,11 @@ using System.Collections; using System.Collections.Generic; -using System.ComponentModel.DataAnnotations; -using System.Data.Linq; using System.Diagnostics.CodeAnalysis; using System.Diagnostics.Contracts; -using System.Globalization; using System.Linq; using System.Reflection; using System.Web.Http.OData.Properties; -using System.Xml.Linq; using Microsoft.Data.Edm; using Microsoft.Data.Edm.Library; using Microsoft.Data.OData; @@ -124,7 +120,7 @@ namespace System.Web.Http.OData.Formatter.Deserialization if (propertyKind == EdmTypeKind.Primitive) { - value = ConvertPrimitiveValue(value, GetPropertyType(resource, propertyName, isDelta), propertyName, resource.GetType().FullName); + value = EdmPrimitiveHelpers.ConvertPrimitiveValue(value, GetPropertyType(resource, propertyName, isDelta)); } SetProperty(resource, propertyName, isDelta, value); @@ -191,82 +187,6 @@ namespace System.Web.Http.OData.Formatter.Deserialization } } - internal static object ConvertPrimitiveValue(object value, Type type, string propertyName, string typeName) - { - Contract.Assert(value != null); - Contract.Assert(type != null); - - // if value is of the same type nothing to do here. - if (value.GetType() == type || value.GetType() == Nullable.GetUnderlyingType(type)) - { - return value; - } - - string str = value as string; - - if (type == typeof(char)) - { - if (str == null || str.Length != 1) - { - throw new ValidationException(Error.Format(SRResources.PropertyMustBeStringLengthOne, propertyName, typeName)); - } - - return str[0]; - } - else if (type == typeof(char?)) - { - if (str == null || str.Length > 1) - { - throw new ValidationException(Error.Format(SRResources.PropertyMustBeStringMaxLengthOne, propertyName, typeName)); - } - - return str.Length > 0 ? str[0] : (char?)null; - } - else if (type == typeof(char[])) - { - if (str == null) - { - throw new ValidationException(Error.Format(SRResources.PropertyMustBeString, propertyName, typeName)); - } - - return str.ToCharArray(); - } - else if (type == typeof(Binary)) - { - return new Binary((byte[])value); - } - else if (type == typeof(XElement)) - { - if (str == null) - { - throw new ValidationException(Error.Format(SRResources.PropertyMustBeString, propertyName, typeName)); - } - - return XElement.Parse(str); - } - else - { - type = Nullable.GetUnderlyingType(type) ?? type; - if (type.IsEnum) - { - if (str == null) - { - throw new ValidationException(Error.Format(SRResources.PropertyMustBeString, propertyName, typeName)); - } - - return Enum.Parse(type, str); - } - else - { - Contract.Assert(type == typeof(uint) || type == typeof(ushort) || type == typeof(ulong)); - - // Note that we are not casting the return value to nullable<T> as even if we do it - // CLR would unbox it back to T. - return Convert.ChangeType(value, type, CultureInfo.InvariantCulture); - } - } - } - private static object ConvertComplexValue(ODataComplexValue complexValue, ref IEdmTypeReference propertyType, ODataDeserializerProvider deserializerProvider, ODataDeserializerContext readContext) { IEdmComplexTypeReference edmComplexType; diff --git a/src/System.Web.Http.OData/OData/Formatter/EdmPrimitiveHelpers.cs b/src/System.Web.Http.OData/OData/Formatter/EdmPrimitiveHelpers.cs new file mode 100644 index 00000000..7da42631 --- /dev/null +++ b/src/System.Web.Http.OData/OData/Formatter/EdmPrimitiveHelpers.cs @@ -0,0 +1,90 @@ +// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved. See License.txt in the project root for license information. + +using System.ComponentModel.DataAnnotations; +using System.Data.Linq; +using System.Diagnostics.Contracts; +using System.Globalization; +using System.Web.Http.OData.Properties; +using System.Xml.Linq; + +namespace System.Web.Http.OData.Formatter +{ + internal static class EdmPrimitiveHelpers + { + public static object ConvertPrimitiveValue(object value, Type type) + { + Contract.Assert(value != null); + Contract.Assert(type != null); + + // if value is of the same type nothing to do here. + if (value.GetType() == type || value.GetType() == Nullable.GetUnderlyingType(type)) + { + return value; + } + + string str = value as string; + + if (type == typeof(char)) + { + if (str == null || str.Length != 1) + { + throw new ValidationException(Error.Format(SRResources.PropertyMustBeStringLengthOne)); + } + + return str[0]; + } + else if (type == typeof(char?)) + { + if (str == null || str.Length > 1) + { + throw new ValidationException(Error.Format(SRResources.PropertyMustBeStringMaxLengthOne)); + } + + return str.Length > 0 ? str[0] : (char?)null; + } + else if (type == typeof(char[])) + { + if (str == null) + { + throw new ValidationException(Error.Format(SRResources.PropertyMustBeString)); + } + + return str.ToCharArray(); + } + else if (type == typeof(Binary)) + { + return new Binary((byte[])value); + } + else if (type == typeof(XElement)) + { + if (str == null) + { + throw new ValidationException(Error.Format(SRResources.PropertyMustBeString)); + } + + return XElement.Parse(str); + } + else + { + type = Nullable.GetUnderlyingType(type) ?? type; + if (type.IsEnum) + { + if (str == null) + { + throw new ValidationException(Error.Format(SRResources.PropertyMustBeString)); + } + + return Enum.Parse(type, str); + } + else + { + Contract.Assert(type == typeof(uint) || type == typeof(ushort) || type == typeof(ulong)); + + // Note that we are not casting the return value to nullable<T> as even if we do it + // CLR would unbox it back to T. + return Convert.ChangeType(value, type, CultureInfo.InvariantCulture); + } + } + } + } +} diff --git a/src/System.Web.Http.OData/OData/Formatter/ODataModelBinderProvider.cs b/src/System.Web.Http.OData/OData/Formatter/ODataModelBinderProvider.cs new file mode 100644 index 00000000..abb6c569 --- /dev/null +++ b/src/System.Web.Http.OData/OData/Formatter/ODataModelBinderProvider.cs @@ -0,0 +1,115 @@ +// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved. See License.txt in the project root for license information. + +using System.ComponentModel.DataAnnotations; +using System.Diagnostics.CodeAnalysis; +using System.Globalization; +using System.Web.Http.Controllers; +using System.Web.Http.ModelBinding; +using System.Web.Http.OData.Properties; +using System.Web.Http.ValueProviders; +using Microsoft.Data.OData; +using Microsoft.Data.OData.Query; + +namespace System.Web.Http.OData.Formatter +{ + /// <summary> + /// Provides a <see cref="IModelBinder"/> for EDM primitive types. + /// </summary> + public class ODataModelBinderProvider : ModelBinderProvider + { + public override IModelBinder GetBinder(HttpConfiguration configuration, Type modelType) + { + if (configuration == null) + { + throw Error.ArgumentNull("configuration"); + } + + if (modelType == null) + { + throw Error.ArgumentNull("modelType"); + } + + if (EdmLibHelpers.GetEdmPrimitiveTypeOrNull(modelType) != null) + { + return new ODataModelBinder(); + } + + return null; + } + + internal class ODataModelBinder : IModelBinder + { + [SuppressMessage("Microsoft.Design", "CA1031:DoNotCatchGeneralExceptionTypes", Justification = "We don't want to fail in model binding.")] + public bool BindModel(HttpActionContext actionContext, ModelBindingContext bindingContext) + { + if (bindingContext == null) + { + throw Error.ArgumentNull("bindingContext"); + } + + if (bindingContext.ModelMetadata == null) + { + throw Error.Argument("bindingContext", SRResources.ModelBinderUtil_ModelMetadataCannotBeNull); + } + + ValueProviderResult value = bindingContext.ValueProvider.GetValue(bindingContext.ModelName); + if (value == null) + { + return false; + } + bindingContext.ModelState.SetModelValue(bindingContext.ModelName, value); + + try + { + string valueString = value.RawValue as string; + object model = ConvertTo(valueString, bindingContext.ModelType); + bindingContext.Model = model; + return true; + } + catch (ODataException ex) + { + bindingContext.ModelState.AddModelError(bindingContext.ModelName, ex.Message); + return false; + } + catch (ValidationException ex) + { + bindingContext.ModelState.AddModelError(bindingContext.ModelName, Error.Format(SRResources.ValueIsInvalid, value.RawValue, ex.Message)); + return false; + } + catch (FormatException ex) + { + bindingContext.ModelState.AddModelError(bindingContext.ModelName, Error.Format(SRResources.ValueIsInvalid, value.RawValue, ex.Message)); + return false; + } + catch (Exception e) + { + bindingContext.ModelState.AddModelError(bindingContext.ModelName, e); + return false; + } + } + + internal static object ConvertTo(string valueString, Type type) + { + if (valueString == null) + { + return null; + } + + object value = ODataUriUtils.ConvertFromUriLiteral(valueString, ODataVersion.V3); + + bool isNonStandardEdmPrimitive; + EdmLibHelpers.IsNonstandardEdmPrimitive(type, out isNonStandardEdmPrimitive); + + if (isNonStandardEdmPrimitive) + { + return EdmPrimitiveHelpers.ConvertPrimitiveValue(value, type); + } + else + { + type = Nullable.GetUnderlyingType(type) ?? type; + return Convert.ChangeType(value, type, CultureInfo.InvariantCulture); + } + } + } + } +} diff --git a/src/System.Web.Http.OData/Properties/SRResources.Designer.cs b/src/System.Web.Http.OData/Properties/SRResources.Designer.cs index 16269bdf..e661c268 100644 --- a/src/System.Web.Http.OData/Properties/SRResources.Designer.cs +++ b/src/System.Web.Http.OData/Properties/SRResources.Designer.cs @@ -538,6 +538,15 @@ namespace System.Web.Http.OData.Properties { } /// <summary> + /// Looks up a localized string similar to The binding context cannot have a null ModelMetadata.. + /// </summary> + internal static string ModelBinderUtil_ModelMetadataCannotBeNull { + get { + return ResourceManager.GetString("ModelBinderUtil_ModelMetadataCannotBeNull", resourceCulture); + } + } + + /// <summary> /// Looks up a localized string similar to More than one Procedure called '{0}' was found. Try using the other RemoveProcedure override.. /// </summary> internal static string MoreThanOneProcedureFound { @@ -763,7 +772,7 @@ namespace System.Web.Http.OData.Properties { } /// <summary> - /// Looks up a localized string similar to The property '{0}' on type '{1}' must be a string.. + /// Looks up a localized string similar to The value must be a string.. /// </summary> internal static string PropertyMustBeString { get { @@ -772,7 +781,7 @@ namespace System.Web.Http.OData.Properties { } /// <summary> - /// Looks up a localized string similar to The property '{0}' on type '{1}' must be a string with a length of 1.. + /// Looks up a localized string similar to The value must be a string with a length of 1.. /// </summary> internal static string PropertyMustBeStringLengthOne { get { @@ -781,7 +790,7 @@ namespace System.Web.Http.OData.Properties { } /// <summary> - /// Looks up a localized string similar to The property '{0}' on type '{1}' must be a string with a maximum length of 1.. + /// Looks up a localized string similar to The value must be a string with a maximum length of 1.. /// </summary> internal static string PropertyMustBeStringMaxLengthOne { get { @@ -952,6 +961,15 @@ namespace System.Web.Http.OData.Properties { } /// <summary> + /// Looks up a localized string similar to The value '{0}' is invalid. {1}. + /// </summary> + internal static string ValueIsInvalid { + get { + return ResourceManager.GetString("ValueIsInvalid", resourceCulture); + } + } + + /// <summary> /// Looks up a localized string similar to {0} does not support WriteObjectInline.. /// </summary> internal static string WriteObjectInlineNotSupported { diff --git a/src/System.Web.Http.OData/Properties/SRResources.resx b/src/System.Web.Http.OData/Properties/SRResources.resx index 462b533f..a501622f 100644 --- a/src/System.Web.Http.OData/Properties/SRResources.resx +++ b/src/System.Web.Http.OData/Properties/SRResources.resx @@ -337,13 +337,13 @@ <value>Cannot apply PATCH to navigation property '{0}' on entity type '{1}'.</value> </data> <data name="PropertyMustBeString" xml:space="preserve"> - <value>The property '{0}' on type '{1}' must be a string.</value> + <value>The value must be a string.</value> </data> <data name="PropertyMustBeStringLengthOne" xml:space="preserve"> - <value>The property '{0}' on type '{1}' must be a string with a length of 1.</value> + <value>The value must be a string with a length of 1.</value> </data> <data name="PropertyMustBeStringMaxLengthOne" xml:space="preserve"> - <value>The property '{0}' on type '{1}' must be a string with a maximum length of 1.</value> + <value>The value must be a string with a maximum length of 1.</value> </data> <data name="ArgumentMustBeOfType" xml:space="preserve"> <value>The argument must be of type '{0}'.</value> @@ -423,4 +423,10 @@ <data name="ResultLimitMustBePositive" xml:space="preserve"> <value>The result limit must be a positive number.</value> </data> + <data name="ValueIsInvalid" xml:space="preserve"> + <value>The value '{0}' is invalid. {1}</value> + </data> + <data name="ModelBinderUtil_ModelMetadataCannotBeNull" xml:space="preserve"> + <value>The binding context cannot have a null ModelMetadata.</value> + </data> </root>
\ No newline at end of file diff --git a/src/System.Web.Http.OData/System.Web.Http.OData.csproj b/src/System.Web.Http.OData/System.Web.Http.OData.csproj index e61d613b..54bf0feb 100644 --- a/src/System.Web.Http.OData/System.Web.Http.OData.csproj +++ b/src/System.Web.Http.OData/System.Web.Http.OData.csproj @@ -174,10 +174,12 @@ <Compile Include="OData\Formatter\Deserialization\ODataNavigationLinkAnnotation.cs" /> <Compile Include="OData\Formatter\Deserialization\ODataPrimitiveDeserializer.cs" /> <Compile Include="OData\FeedContext.cs" /> + <Compile Include="OData\Formatter\EdmPrimitiveHelpers.cs" /> <Compile Include="OData\Formatter\EdmTypeReferenceEqualityComparer.cs" /> <Compile Include="OData\Formatter\ODataFormatterParameterBinding.cs" /> <Compile Include="OData\Formatter\PatchKeyMode.cs" /> <Compile Include="OData\Formatter\PatchKeyModeHelper.cs" /> + <Compile Include="OData\Formatter\ODataModelBinderProvider.cs" /> <Compile Include="OData\Formatter\Serialization\ODataErrorSerializer.cs" /> <Compile Include="OData\Formatter\Serialization\ODataMetadataSerializer.cs" /> <Compile Include="OData\IDeltaOfTEntityType.cs" /> diff --git a/test/System.Web.Http.OData.Test/OData/Formatter/Deserialization/ODataEntryDeserializerTests.cs b/test/System.Web.Http.OData.Test/OData/Formatter/Deserialization/ODataEntryDeserializerTests.cs index 01a41dc1..fe64f145 100644 --- a/test/System.Web.Http.OData.Test/OData/Formatter/Deserialization/ODataEntryDeserializerTests.cs +++ b/test/System.Web.Http.OData.Test/OData/Formatter/Deserialization/ODataEntryDeserializerTests.cs @@ -1,82 +1,14 @@ // Copyright (c) Microsoft Open Technologies, Inc. All rights reserved. See License.txt in the project root for license information. -using System.ComponentModel.DataAnnotations; -using System.Data.Linq; -using System.Xml.Linq; using Microsoft.Data.Edm.Library; using Microsoft.Data.OData; using Microsoft.TestCommon; -using Microsoft.TestCommon.Types; using Moq; namespace System.Web.Http.OData.Formatter.Deserialization { public class ODataEntryDeserializerTests { - public static TheoryDataSet<object, object, Type> ConvertPrimitiveValue_NonStandardPrimitives_Data - { - get - { - return new TheoryDataSet<object, object, Type> - { - { "1", (char)'1', typeof(char) }, - { "1", (char?)'1', typeof(char?) }, - { "123", (char[]) new char[] {'1', '2', '3' }, typeof(char[]) }, - { (int)1 , (ushort)1, typeof(ushort)}, - { (int?)1, (ushort?)1, typeof(ushort?) }, - { (long)1, (uint)1, typeof(uint) }, - { (long?)1, (uint?)1, typeof(uint?) }, - { (long)1 , (ulong)1, typeof(ulong)}, - { (long?)1 ,(ulong?)1, typeof(ulong?)}, - //(Stream) new MemoryStream(new byte[] { 1 }), // TODO: Enable once we have support for streams - { "<element xmlns=\"namespace\" />" ,(XElement) new XElement(XName.Get("element","namespace")), typeof(XElement)}, - { new byte[] {1}, new Binary(new byte[] {1}), typeof(Binary)}, - - // Enums - { "Second", SimpleEnum.Second, typeof(SimpleEnum) }, - { "Second", SimpleEnum.Second, typeof(SimpleEnum?) }, - { "ThirdLong" , LongEnum.ThirdLong, typeof(LongEnum) }, - { "ThirdLong" , LongEnum.ThirdLong, typeof(LongEnum?) }, - { "One, Four" , FlagsEnum.One | FlagsEnum.Four, typeof(FlagsEnum) }, - { "One, Four" , FlagsEnum.One | FlagsEnum.Four, typeof(FlagsEnum?) } - }; - } - } - - [Theory] - [PropertyData("ConvertPrimitiveValue_NonStandardPrimitives_Data")] - public void ConvertPrimitiveValue_NonStandardPrimitives(object valueToConvert, object result, Type conversionType) - { - Assert.Equal(result.GetType(), ODataEntryDeserializer.ConvertPrimitiveValue(valueToConvert, conversionType, "", "").GetType()); - Assert.Equal(result.ToString(), ODataEntryDeserializer.ConvertPrimitiveValue(valueToConvert, conversionType, "", "").ToString()); - } - - [Theory] - [InlineData("123")] - [InlineData("")] - public void ConvertPrimitiveValueToChar_Throws(string input) - { - Assert.Throws<ValidationException>( - () => ODataEntryDeserializer.ConvertPrimitiveValue(input, typeof(char), "property", "type"), - "The property 'property' on type 'type' must be a string with a length of 1."); - } - - [Fact] - public void ConvertPrimitiveValueToNullableChar_Throws() - { - Assert.Throws<ValidationException>( - () => ODataEntryDeserializer.ConvertPrimitiveValue("123", typeof(char?), "property", "type"), - "The property 'property' on type 'type' must be a string with a maximum length of 1."); - } - - [Fact] - public void ConvertPrimitiveValueToXElement_Throws_IfInputIsNotString() - { - Assert.Throws<ValidationException>( - () => ODataEntryDeserializer.ConvertPrimitiveValue(123, typeof(XElement), "property", "type"), - "The property 'property' on type 'type' must be a string."); - } - [Theory] [InlineData("Property", true, typeof(int))] [InlineData("Property", false, typeof(int))] diff --git a/test/System.Web.Http.OData.Test/OData/Formatter/EdmPrimitiveHelpersTest.cs b/test/System.Web.Http.OData.Test/OData/Formatter/EdmPrimitiveHelpersTest.cs new file mode 100644 index 00000000..e0bbfd48 --- /dev/null +++ b/test/System.Web.Http.OData.Test/OData/Formatter/EdmPrimitiveHelpersTest.cs @@ -0,0 +1,77 @@ +// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved. See License.txt in the project root for license information. + +using System.ComponentModel.DataAnnotations; +using System.Data.Linq; +using System.Xml.Linq; +using Microsoft.TestCommon; +using Microsoft.TestCommon.Types; + +namespace System.Web.Http.OData.Formatter +{ + public class EdmPrimitiveHelpersTest + { + public static TheoryDataSet<object, object, Type> ConvertPrimitiveValue_NonStandardPrimitives_Data + { + get + { + return new TheoryDataSet<object, object, Type> + { + { "1", (char)'1', typeof(char) }, + { "1", (char?)'1', typeof(char?) }, + { "123", (char[]) new char[] {'1', '2', '3' }, typeof(char[]) }, + { (int)1 , (ushort)1, typeof(ushort)}, + { (int?)1, (ushort?)1, typeof(ushort?) }, + { (long)1, (uint)1, typeof(uint) }, + { (long?)1, (uint?)1, typeof(uint?) }, + { (long)1 , (ulong)1, typeof(ulong)}, + { (long?)1 ,(ulong?)1, typeof(ulong?)}, + //(Stream) new MemoryStream(new byte[] { 1 }), // TODO: Enable once we have support for streams + { "<element xmlns=\"namespace\" />" ,(XElement) new XElement(XName.Get("element","namespace")), typeof(XElement)}, + { new byte[] {1}, new Binary(new byte[] {1}), typeof(Binary)}, + + // Enums + { "Second", SimpleEnum.Second, typeof(SimpleEnum) }, + { "Second", SimpleEnum.Second, typeof(SimpleEnum?) }, + { "ThirdLong" , LongEnum.ThirdLong, typeof(LongEnum) }, + { "ThirdLong" , LongEnum.ThirdLong, typeof(LongEnum?) }, + { "One, Four" , FlagsEnum.One | FlagsEnum.Four, typeof(FlagsEnum) }, + { "One, Four" , FlagsEnum.One | FlagsEnum.Four, typeof(FlagsEnum?) } + }; + } + } + + [Theory] + [PropertyData("ConvertPrimitiveValue_NonStandardPrimitives_Data")] + public void ConvertPrimitiveValue_NonStandardPrimitives(object valueToConvert, object result, Type conversionType) + { + Assert.Equal(result.GetType(), EdmPrimitiveHelpers.ConvertPrimitiveValue(valueToConvert, conversionType).GetType()); + Assert.Equal(result.ToString(), EdmPrimitiveHelpers.ConvertPrimitiveValue(valueToConvert, conversionType).ToString()); + } + + [Theory] + [InlineData("123")] + [InlineData("")] + public void ConvertPrimitiveValueToChar_Throws(string input) + { + Assert.Throws<ValidationException>( + () => EdmPrimitiveHelpers.ConvertPrimitiveValue(input, typeof(char)), + "The value must be a string with a length of 1."); + } + + [Fact] + public void ConvertPrimitiveValueToNullableChar_Throws() + { + Assert.Throws<ValidationException>( + () => EdmPrimitiveHelpers.ConvertPrimitiveValue("123", typeof(char?)), + "The value must be a string with a maximum length of 1."); + } + + [Fact] + public void ConvertPrimitiveValueToXElement_Throws_IfInputIsNotString() + { + Assert.Throws<ValidationException>( + () => EdmPrimitiveHelpers.ConvertPrimitiveValue(123, typeof(XElement)), + "The value must be a string."); + } + } +} diff --git a/test/System.Web.Http.OData.Test/OData/Formatter/ODataModelBinderProviderTest.cs b/test/System.Web.Http.OData.Test/OData/Formatter/ODataModelBinderProviderTest.cs new file mode 100644 index 00000000..ae8e235d --- /dev/null +++ b/test/System.Web.Http.OData.Test/OData/Formatter/ODataModelBinderProviderTest.cs @@ -0,0 +1,401 @@ +// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved. See License.txt in the project root for license information. + +using System.Collections.Generic; +using System.Globalization; +using System.Linq; +using System.Net; +using System.Net.Http; +using System.Web.Http.Controllers; +using System.Web.Http.ModelBinding; +using System.Web.Http.Routing; +using System.Web.Http.ValueProviders; +using Microsoft.Data.OData; +using Microsoft.Data.OData.Query; +using Microsoft.TestCommon; +using Microsoft.TestCommon.Types; + +namespace System.Web.Http.OData.Formatter +{ + public class ODataModelBinderProviderTest + { + private HttpConfiguration _configuration; + private HttpServer _server; + private HttpClient _client; + + public ODataModelBinderProviderTest() + { + _configuration = new HttpConfiguration(); + _configuration.Services.Replace(typeof(ModelBinderProvider), new ODataModelBinderProvider()); + + _configuration.Routes.MapHttpRoute("default_multiple_keys", "{controller}/{action}({key1}={value1},{key2}={value2})"); + _configuration.Routes.MapHttpRoute("default", "{controller}/{action}({id})"); + + _server = new HttpServer(_configuration); + _client = new HttpClient(_server); + } + + public static TheoryDataSet<object, string> ODataModelBinderProvider_Works_TestData + { + get + { + return new TheoryDataSet<object, string> + { + { true, "GetBool" }, + { (short)123, "GetInt16"}, + { (short)123, "GetUInt16"}, + { (int)123, "GetInt32" }, + { (int)123, "GetUInt32" }, + { (long)123, "GetInt64" }, + { (long)123, "GetUInt64" }, + { (byte)1, "GetByte" }, + { "123", "GetString" }, + { Guid.Empty, "GetGuid" }, + { DateTime.Now, "GetDateTime" }, + { TimeSpan.FromTicks(424242), "GetTimeSpan" }, + { DateTimeOffset.MaxValue, "GetDateTimeOffset" }, + { float.NaN, "GetFloat" }, + { decimal.MaxValue, "GetDecimal" }, + // { double.NaN, "GetDouble" } // doesn't work with uri parser. + { SimpleEnum.First.ToString(), "GetEnum" }, + { (FlagsEnum.One | FlagsEnum.Two).ToString(), "GetFlagsEnum" } + }; + } + } + + public static TheoryDataSet<object, string> ODataModelBinderProvider_Throws_TestData + { + get + { + return new TheoryDataSet<object, string> + { + { "123", "GetBool" }, + { 123, "GetDateTime" }, + { "abc", "GetInt32" }, + { "abc", "GetEnum" }, + { "abc", "GetGuid" }, + { "abc", "GetByte" }, + { "abc", "GetFloat" }, + { "abc", "GetDouble" }, + { "abc", "GetDecimal" }, + { "abc", "GetDateTime" }, + { "abc", "GetTimeSpan" }, + { "abc", "GetDateTimeOffset" }, + { -1, "GetUInt16"}, + { -1, "GetUInt32" }, + { -1, "GetUInt64"}, + }; + } + } + + public static TheoryDataSet<string, string, string> ODataModelBinderProvider_ModelStateErrors_InvalidODataRepresentations_TestData + { + get + { + return new TheoryDataSet<string, string, string> + { + { "abc", "GetNullableBool", "Expected literal type token but found token 'abc'." }, + { "datetime'123'", "GetNullableDateTime", "Unrecognized 'Edm.DateTime' literal 'datetime'123'' at '0' in 'datetime'123''." } + }; + } + } + + public static TheoryDataSet<string, string, string> ODataModelBinderProvider_ModelStateErrors_InvalidConversions_TestData + { + get + { + return new TheoryDataSet<string, string, string> + { + { "'abc'", "GetNullableChar", "The value ''abc'' is invalid. The value must be a string with a maximum length of 1." }, + { "'abc'", "GetDefaultChar", "The value ''abc'' is invalid. The value must be a string with a length of 1." }, + { "-123", "GetDefaultUInt", "Value was either too large or too small for a UInt32." } + }; + } + } + + [Fact] + public void GetBinder_ThrowsArgumentNull_configuration() + { + ODataModelBinderProvider binderProvider = new ODataModelBinderProvider(); + + Assert.ThrowsArgumentNull( + () => binderProvider.GetBinder(configuration: null, modelType: typeof(int)), + "configuration"); + } + + [Fact] + public void GetBinder_ThrowsArgumentNull_modelType() + { + ODataModelBinderProvider binderProvider = new ODataModelBinderProvider(); + + Assert.ThrowsArgumentNull( + () => binderProvider.GetBinder(new HttpConfiguration(), modelType: null), + "modelType"); + } + + [Theory] + [PropertyData("ODataModelBinderProvider_Works_TestData")] + public void ODataModelBinderProvider_Works(object value, string action) + { + string url = String.Format("http://localhost/ODataModelBinderProviderTest/{0}({1})", action, Uri.EscapeDataString(ODataUriUtils.ConvertToUriLiteral(value, ODataVersion.V3))); + HttpResponseMessage response = _client.GetAsync(url).Result; + response.EnsureSuccessStatusCode(); + Assert.Equal( + value, + response.Content.ReadAsAsync(value.GetType(), _configuration.Formatters).Result); + } + + [Theory] + [PropertyData("ODataModelBinderProvider_Throws_TestData")] + public void ODataModelBinderProvider_Throws(object value, string action) + { + string url = String.Format("http://localhost/ODataModelBinderProviderThrowsTest/{0}({1})", action, Uri.EscapeDataString(ODataUriUtils.ConvertToUriLiteral(value, ODataVersion.V3))); + HttpResponseMessage response = _client.GetAsync(url).Result; + + Assert.Equal(HttpStatusCode.BadRequest, response.StatusCode); + } + + [Theory] + [PropertyData("ODataModelBinderProvider_ModelStateErrors_InvalidODataRepresentations_TestData")] + public void ODataModelBinderProvider_ModelStateErrors_InvalidODataRepresentations(string value, string action, string error) + { + string url = String.Format("http://localhost/ODataModelBinderProviderThrowsTest/{0}({1})", action, Uri.EscapeDataString(value)); + HttpResponseMessage response = _client.GetAsync(url).Result; + + response.EnsureSuccessStatusCode(); + Assert.Equal( + response.Content.ReadAsAsync<string[]>().Result, + new[] { error }); + } + + [Theory] + [PropertyData("ODataModelBinderProvider_ModelStateErrors_InvalidConversions_TestData")] + public void ODataModelBinderProvider_ModelStateErrors_InvalidConversions(string value, string action, string error) + { + string url = String.Format("http://localhost/ODataModelBinderProviderThrowsTest/{0}({1})", action, Uri.EscapeDataString(value)); + HttpResponseMessage response = _client.GetAsync(url).Result; + + response.EnsureSuccessStatusCode(); + Assert.Equal( + response.Content.ReadAsAsync<string[]>().Result, + new[] { error }); + } + + [Fact] + public void TestMultipleKeys() + { + string url = String.Format( + "http://localhost/ODataModeBinderMultipleKeys/GetMultipleKeys(name={0},model={1})", + Uri.EscapeDataString(ODataUriUtils.ConvertToUriLiteral("name", ODataVersion.V3)), + Uri.EscapeDataString(ODataUriUtils.ConvertToUriLiteral(2009, ODataVersion.V3))); + + HttpResponseMessage response = _client.GetAsync(url).Result; + + response.EnsureSuccessStatusCode(); + Assert.Equal( + "name-2009", + response.Content.ReadAsAsync<string>().Result); + } + } + + public class ODataKeyAttribute : ModelBinderAttribute + { + public override IEnumerable<ValueProviderFactory> GetValueProviderFactories(HttpConfiguration configuration) + { + return new[] { new ODataKeysValueProviderFactory() }; + } + + internal class ODataKeysValueProviderFactory : ValueProviderFactory + { + public override IValueProvider GetValueProvider(HttpActionContext actionContext) + { + return new ODataKeysValueProvider(actionContext.ControllerContext.RouteData); + } + + private class ODataKeysValueProvider : IValueProvider + { + private IHttpRouteData _routeData; + + public ODataKeysValueProvider(IHttpRouteData routedata) + { + _routeData = routedata; + } + + public bool ContainsPrefix(string prefix) + { + throw new NotImplementedException(); + } + + public ValueProviderResult GetValue(string key) + { + IEnumerable<KeyValuePair<string, object>> match = _routeData.Values.Where(kvp => kvp.Value.Equals(key) && kvp.Key.StartsWith("key")); + if (match.Count() == 1) + { + KeyValuePair<string, object> data = match.First(); + int index = Int32.Parse(data.Key.Replace("key", String.Empty)); + object value = _routeData.Values[String.Format("value{0}", index)]; + return new ValueProviderResult(value, value.ToString(), CultureInfo.InvariantCulture); + } + + return null; + } + } + } + } + + public class ODataModelBinderProviderTestController : ApiController + { + HttpResponseException _exception = new HttpResponseException(HttpStatusCode.NotImplemented); + + public bool GetBool(bool id) + { + ThrowIfInsideThrowsController(); + return id; + } + + public byte GetByte(byte id) + { + ThrowIfInsideThrowsController(); + return id; + } + + public short GetInt16(short id) + { + ThrowIfInsideThrowsController(); + return id; + } + + public ushort GetUInt16(ushort id) + { + ThrowIfInsideThrowsController(); + return id; + } + + public int GetInt32(int id) + { + ThrowIfInsideThrowsController(); + return id; + } + + public uint GetUInt32(uint id) + { + ThrowIfInsideThrowsController(); + return id; + } + + public long GetInt64(long id) + { + ThrowIfInsideThrowsController(); + return id; + } + + public ulong GetUInt64(ulong id) + { + ThrowIfInsideThrowsController(); + return id; + } + + public string GetString(string id) + { + ThrowIfInsideThrowsController(); + return id; + } + + public Guid GetGuid(Guid id) + { + ThrowIfInsideThrowsController(); + return id; + } + + public DateTime GetDateTime(DateTime id) + { + ThrowIfInsideThrowsController(); + return id; + } + + public TimeSpan GetTimeSpan(TimeSpan id) + { + ThrowIfInsideThrowsController(); + return id; + } + + public DateTimeOffset GetDateTimeOffset(DateTimeOffset id) + { + ThrowIfInsideThrowsController(); + return id; + } + + public float GetFloat(float id) + { + ThrowIfInsideThrowsController(); + return id; + } + + public double GetDouble(double id) + { + ThrowIfInsideThrowsController(); + return id; + } + + public decimal GetDecimal(decimal id) + { + ThrowIfInsideThrowsController(); + return id; + } + + public string GetEnum(SimpleEnum id) + { + ThrowIfInsideThrowsController(); + return id.ToString(); + } + + public string GetFlagsEnum(FlagsEnum id) + { + ThrowIfInsideThrowsController(); + return id.ToString(); + } + + private void ThrowIfInsideThrowsController() + { + if (Request.GetRouteData().Values["Controller"].Equals("ODataModelBinderProviderThrowsTest")) + { + throw new HttpResponseException(HttpStatusCode.NotImplemented); + } + } + } + + public class ODataModelBinderProviderThrowsTestController : ODataModelBinderProviderTestController + { + public IEnumerable<string> GetNullableBool(bool? id) + { + return ModelState["id"].Errors.Select(e => e.ErrorMessage); + } + + public IEnumerable<string> GetNullableDateTime(DateTime? id) + { + return ModelState["id"].Errors.Select(e => e.ErrorMessage); + } + + public IEnumerable<string> GetNullableChar(char? id) + { + return ModelState["id"].Errors.Select(e => e.ErrorMessage); + } + + public IEnumerable<string> GetDefaultChar(char id = 'a') + { + return ModelState["id"].Errors.Select(e => e.ErrorMessage); + } + + public IEnumerable<string> GetDefaultUInt(uint id = 0) + { + return ModelState["id"].Errors.Select(e => e.Exception.Message); + } + } + + public class ODataModeBinderMultipleKeysController : ApiController + { + public string GetMultipleKeys([ODataKey]string name, [ODataKey]int model) + { + return name + "-" + model; + } + } +} diff --git a/test/System.Web.Http.OData.Test/OData/Formatter/Serialization/ODataPrimitiveSerializerTests.cs b/test/System.Web.Http.OData.Test/OData/Formatter/Serialization/ODataPrimitiveSerializerTests.cs index dbfcf9c0..2601a2f3 100644 --- a/test/System.Web.Http.OData.Test/OData/Formatter/Serialization/ODataPrimitiveSerializerTests.cs +++ b/test/System.Web.Http.OData.Test/OData/Formatter/Serialization/ODataPrimitiveSerializerTests.cs @@ -4,7 +4,6 @@ using System.Collections.Generic; using System.Data.Linq; using System.IO; using System.Linq; -using System.Web.Http.OData.Formatter.Deserialization; using System.Xml.Linq; using Microsoft.Data.Edm; using Microsoft.Data.Edm.Library; @@ -20,7 +19,7 @@ namespace System.Web.Http.OData.Formatter.Serialization { get { - return ODataEntryDeserializerTests + return EdmPrimitiveHelpersTest .ConvertPrimitiveValue_NonStandardPrimitives_Data .Select(data => new[] { data[1], data[0] }); } diff --git a/test/System.Web.Http.OData.Test/System.Web.Http.OData.Test.csproj b/test/System.Web.Http.OData.Test/System.Web.Http.OData.Test.csproj index 64dc71a1..93fc186c 100644 --- a/test/System.Web.Http.OData.Test/System.Web.Http.OData.Test.csproj +++ b/test/System.Web.Http.OData.Test/System.Web.Http.OData.Test.csproj @@ -111,6 +111,8 @@ <Compile Include="OData\Formatter\ODataActionTests.cs" /> <Compile Include="OData\Formatter\InheritanceTests.cs" /> <Compile Include="OData\Formatter\Deserialization\ODataActionPayloadDeserializerTest.cs" /> + <Compile Include="OData\Formatter\EdmPrimitiveHelpersTest.cs" /> + <Compile Include="OData\Formatter\ODataModelBinderProviderTest.cs" /> <Compile Include="OData\Formatter\PartialTrustTest.cs" /> <Compile Include="OData\Builder\EdmTypeConfigurationExtensionsTest.cs" /> <Compile Include="OData\Builder\TestModels\InheritanceModels.cs" /> |