diff options
author | Alex James <Alex@base4.net> | 2012-09-20 06:08:40 +0400 |
---|---|---|
committer | Alex James <Alex@base4.net> | 2012-09-27 20:21:40 +0400 |
commit | e6cad76db3e4b60e54418eff1cbcfc14ff8982aa (patch) | |
tree | df387d564427f0e5cb0b0ecd13e4ada7b908d3b6 | |
parent | 6a0c03f9e549966a7f806f8b696ec4cb2ec272e6 (diff) |
ODataActionParameters
- ODataActionParameters class to represent action parameters.
- ODataActionPayloadDeserializer to create ODataActionParameters objects from request body.
- DefaultODataDeserializerProvider changes to dispatch to ODataActionPayloadDeserializer for ODataActionParameters signatures
- Adding Request and Model to ODataDeserializerContext
- Adding IODataActionResolver and DefaultODataActionResolver
- Reworking ODataMediaTypeFormatter to set Request when constructing an ODataDeserializerContext
- Added Request to ODataDeserializerContext so formatters have access to the Request (and things like RequestUri)
- ODataParameterBindingAttribute to ensure access to Request in deserializers.
- Adding unit tests for ODataActionParameters deserialization
- End to end tests
- Fixing Stylecop and FxCop violations
19 files changed, 859 insertions, 2 deletions
diff --git a/src/System.Web.Http.OData/HttpConfigurationExtensions.cs b/src/System.Web.Http.OData/HttpConfigurationExtensions.cs index 9174855a..f0a0b39a 100644 --- a/src/System.Web.Http.OData/HttpConfigurationExtensions.cs +++ b/src/System.Web.Http.OData/HttpConfigurationExtensions.cs @@ -2,6 +2,7 @@ using System.ComponentModel; using System.Linq; +using System.Web.Http.OData; using System.Web.Http.OData.Formatter; using System.Web.Http.OData.Properties; using Microsoft.Data.Edm; @@ -13,6 +14,7 @@ namespace System.Web.Http { private const string EdmModelKey = "MS_EdmModel"; private const string ODataFormatterKey = "MS_ODataFormatter"; + private const string ODataActionResolverKey = "MS_ODataActionResolver"; /// <summary> /// Retrieve the EdmModel from the configuration Properties collection. Null if user has not set it. @@ -130,5 +132,43 @@ namespace System.Web.Http configuration.Formatters.Insert(0, formatter); } } + + /// <summary> + /// Gets the <see cref="IODataActionResolver"/> on the configuration. + /// </summary> + /// <remarks> + /// If not <see cref="IODataActionResolver"/> is configured this returns the <see cref="DefaultODataActionResolver"/> + /// </remarks> + /// <param name="configuration">Configuration o check.</param> + /// <returns>Returns an <see cref="IODataActionResolver"/> for this configuration.</returns> + public static IODataActionResolver GetODataActionResolver(this HttpConfiguration configuration) + { + if (configuration == null) + { + throw Error.ArgumentNull("configuration"); + } + + // returns one if user sets one, null otherwise + object result = configuration.Properties.GetOrAdd(ODataActionResolverKey, new DefaultODataActionResolver()); + return result as IODataActionResolver; + } + + /// <summary> + /// Sets the <see cref="IODataActionResolver"/> on the configuration + /// </summary> + /// <param name="configuration">Configuration to be updated.</param> + /// <param name="resolver">The <see cref="IODataActionResolver"/> this configuration should use.</param> + public static void SetODataActionResolver(this HttpConfiguration configuration, IODataActionResolver resolver) + { + if (configuration == null) + { + throw Error.ArgumentNull("configuration"); + } + if (resolver == null) + { + throw Error.ArgumentNull("resolver"); + } + configuration.Properties[ODataActionResolverKey] = resolver; + } } } diff --git a/src/System.Web.Http.OData/OData/DefaultODataActionResolver.cs b/src/System.Web.Http.OData/OData/DefaultODataActionResolver.cs new file mode 100644 index 00000000..abfd6331 --- /dev/null +++ b/src/System.Web.Http.OData/OData/DefaultODataActionResolver.cs @@ -0,0 +1,63 @@ +// 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.Diagnostics.Contracts; +using System.Linq; +using System.Web.Http.OData.Formatter.Deserialization; +using System.Web.Http.OData.Properties; +using Microsoft.Data.Edm; + +namespace System.Web.Http.OData +{ + /// <summary> + /// A default implementation of an IODataActionResolver + /// </summary> + public class DefaultODataActionResolver : IODataActionResolver + { + public IEdmFunctionImport Resolve(ODataDeserializerContext context) + { + Contract.Assert(context.Request != null); + Contract.Assert(context.Model != null); + + string actionName = null; + string containerName = null; + string nspace = null; + + string lastSegment = context.Request.RequestUri.Segments.Last(); + string[] nameParts = lastSegment.Split('.'); + + IEnumerable<IEdmFunctionImport> matchingActionsQuery = context.Model.EntityContainers().Single().FunctionImports(); + + if (nameParts.Length == 1) + { + actionName = nameParts[0]; + matchingActionsQuery = matchingActionsQuery.Where(f => f.Name == actionName && f.IsSideEffecting == true); + } + else if (nameParts.Length == 2) + { + actionName = nameParts[nameParts.Length - 1]; + containerName = nameParts[nameParts.Length - 2]; + matchingActionsQuery = matchingActionsQuery.Where(f => f.Name == actionName && f.IsSideEffecting == true && f.Container.Name == containerName); + } + else if (nameParts.Length > 2) + { + actionName = nameParts[nameParts.Length - 1]; + containerName = nameParts[nameParts.Length - 2]; + nspace = String.Join(".", nameParts.Take(nameParts.Length - 2)); + matchingActionsQuery = matchingActionsQuery.Where(f => f.Name == actionName && f.IsSideEffecting == true && f.Container.Name == containerName && f.Container.Namespace == nspace); + } + + IEdmFunctionImport[] possibleMatches = matchingActionsQuery.ToArray(); + + if (possibleMatches.Length == 0) + { + throw Error.InvalidOperation(SRResources.ActionNotFound, actionName); + } + if (possibleMatches.Length > 1) + { + throw Error.InvalidOperation(SRResources.ActionResolutionFailed, actionName); + } + return possibleMatches[0]; + } + } +} diff --git a/src/System.Web.Http.OData/OData/Formatter/Deserialization/DefaultODataDeserializerProvider.cs b/src/System.Web.Http.OData/OData/Formatter/Deserialization/DefaultODataDeserializerProvider.cs index d2f048f1..696ec585 100644 --- a/src/System.Web.Http.OData/OData/Formatter/Deserialization/DefaultODataDeserializerProvider.cs +++ b/src/System.Web.Http.OData/OData/Formatter/Deserialization/DefaultODataDeserializerProvider.cs @@ -59,6 +59,11 @@ namespace System.Web.Http.OData.Formatter.Deserialization return new ODataEntityReferenceLinkDeserializer(); } + if (typeof(ODataActionParameters).IsAssignableFrom(type)) + { + return new ODataActionPayloadDeserializer(type, this); + } + return _clrTypeMappingCache.GetOrAdd(type, (t) => { IEdmTypeReference edmType = EdmModel.GetEdmTypeReference(t); diff --git a/src/System.Web.Http.OData/OData/Formatter/Deserialization/ODataActionPayloadDeserializer.cs b/src/System.Web.Http.OData/OData/Formatter/Deserialization/ODataActionPayloadDeserializer.cs new file mode 100644 index 00000000..e2c03bc0 --- /dev/null +++ b/src/System.Web.Http.OData/OData/Formatter/Deserialization/ODataActionPayloadDeserializer.cs @@ -0,0 +1,117 @@ +// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved. See License.txt in the project root for license information. + +using System.Collections; +using System.Collections.Generic; +using System.Diagnostics.Contracts; +using System.Globalization; +using System.Linq; +using Microsoft.Data.Edm; +using Microsoft.Data.OData; + +namespace System.Web.Http.OData.Formatter.Deserialization +{ + internal class ODataActionPayloadDeserializer : ODataDeserializer + { + private ODataDeserializerProvider _provider; + private Type _payloadType; + + public ODataActionPayloadDeserializer(Type payloadType, ODataDeserializerProvider provider) + : base(ODataPayloadKind.Parameter) + { + Contract.Assert(payloadType != null); + Contract.Assert(provider != null); + _payloadType = payloadType; + _provider = provider; + } + + public override object Read(ODataMessageReader messageReader, ODataDeserializerContext readContext) + { + // Create the correct resource type; + ODataActionParameters payload = CreateNewPayload(); + + IEdmFunctionImport action = payload.GetFunctionImport(readContext); + ODataParameterReader reader = messageReader.CreateODataParameterReader(action); + + while (reader.Read()) + { + string parameterName = null; + IEdmFunctionParameter parameter = null; + + switch (reader.State) + { + case ODataParameterReaderState.Value: + parameterName = reader.Name; + parameter = action.Parameters.SingleOrDefault(p => p.Name == parameterName); + // ODataLib protects against this but asserting just in case. + Contract.Assert(parameter != null, String.Format(CultureInfo.InvariantCulture, "Parameter '{0}' not found.", parameterName)); + payload[parameterName] = Convert(reader.Value, parameter.Type, readContext); + break; + + case ODataParameterReaderState.Collection: + parameterName = reader.Name; + parameter = action.Parameters.SingleOrDefault(p => p.Name == parameterName); + // ODataLib protects against this but asserting just in case. + Contract.Assert(parameter != null, String.Format(CultureInfo.InvariantCulture, "Parameter '{0}' not found.", parameterName)); + IEdmCollectionTypeReference collectionType = parameter.Type as IEdmCollectionTypeReference; + Contract.Assert(collectionType != null); + + payload[parameterName] = Convert(reader.CreateCollectionReader(), collectionType, readContext); + break; + + default: + break; + } + } + + return payload; + } + + private ODataActionParameters CreateNewPayload() + { + if (_payloadType == typeof(ODataActionParameters)) + { + return new ODataActionParameters(); + } + else + { + return Activator.CreateInstance(_payloadType, false) as ODataActionParameters; + } + } + + private object Convert(object value, IEdmTypeReference parameterType, ODataDeserializerContext readContext) + { + if (parameterType.IsPrimitive()) + { + return value; + } + else + { + ODataEntryDeserializer deserializer = _provider.GetODataDeserializer(parameterType); + return deserializer.ReadInline(value, readContext); + } + } + + private object Convert(ODataCollectionReader reader, IEdmCollectionTypeReference collectionType, ODataDeserializerContext readContext) + { + IEdmTypeReference elementType = collectionType.ElementType(); + Type clrElementType = EdmLibHelpers.GetClrType(elementType, readContext.Model); + IList list = Activator.CreateInstance(typeof(List<>).MakeGenericType(clrElementType)) as IList; + ODataEntryDeserializer deserializer = _provider.GetODataDeserializer(elementType); + + while (reader.Read()) + { + switch (reader.State) + { + case ODataCollectionReaderState.Value: + object element = Convert(reader.Item, elementType, readContext); + list.Add(element); + break; + + default: + break; + } + } + return list; + } + } +}
\ No newline at end of file diff --git a/src/System.Web.Http.OData/OData/Formatter/Deserialization/ODataDeserializerContext.cs b/src/System.Web.Http.OData/OData/Formatter/Deserialization/ODataDeserializerContext.cs index b8b5c63a..2e17f6e7 100644 --- a/src/System.Web.Http.OData/OData/Formatter/Deserialization/ODataDeserializerContext.cs +++ b/src/System.Web.Http.OData/OData/Formatter/Deserialization/ODataDeserializerContext.cs @@ -1,6 +1,9 @@ // Copyright (c) Microsoft Open Technologies, Inc. All rights reserved. See License.txt in the project root for license information. using System.Diagnostics.Contracts; +using System.Net.Http; +using Microsoft.Data.Edm; + namespace System.Web.Http.OData.Formatter.Deserialization { /// <summary> @@ -37,6 +40,25 @@ namespace System.Web.Http.OData.Formatter.Deserialization } /// <summary> + /// Gets or sets the HttpRequestMessage. + /// The HttpRequestMessage can then be used by ODataDeserializers to learn more about the Request that triggered the deserialization + /// </summary> + public HttpRequestMessage Request + { + get; + set; + } + + /// <summary> + /// Gets or set the EdmModel associated with the Request. + /// </summary> + public IEdmModel Model + { + get; + set; + } + + /// <summary> /// Increments the current reference depth. /// </summary> /// <returns><c>false</c> if the current reference depth is greater than the maximum allowed and <c>false</c> otherwise.</returns> diff --git a/src/System.Web.Http.OData/OData/Formatter/ODataFormatterParameterBinding.cs b/src/System.Web.Http.OData/OData/Formatter/ODataFormatterParameterBinding.cs new file mode 100644 index 00000000..6645d2c4 --- /dev/null +++ b/src/System.Web.Http.OData/OData/Formatter/ODataFormatterParameterBinding.cs @@ -0,0 +1,33 @@ +// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved. See License.txt in the project root for license information. + +using System.Threading; +using System.Threading.Tasks; +using System.Web.Http.Controllers; +using System.Web.Http.Metadata; + +namespace System.Web.Http.OData.Formatter +{ + /// <summary> + /// A special HttpParameterBinding that uses a Per Request formatter instance with access to the Request. + /// <remarks> + /// This class is needed by some of the ODataDeserializers, since they actually need access to more than just the Request body, + /// they also need to interrogate the RequestUri etc. + /// </remarks> + /// </summary> + public class ODataFormatterParameterBinding : HttpParameterBinding + { + private ODataMediaTypeFormatter _formatter; + + public ODataFormatterParameterBinding(HttpParameterDescriptor descriptor, ODataMediaTypeFormatter formatter) + : base(descriptor) + { + _formatter = formatter; + } + + public override Task ExecuteBindingAsync(ModelMetadataProvider metadataProvider, HttpActionContext actionContext, CancellationToken cancellationToken) + { + var formatter = _formatter.GetPerRequestFormatterInstance(Descriptor.ParameterType, actionContext.Request, actionContext.Request.Content.Headers.ContentType); + return Descriptor.BindWithFormatter(new[] { formatter }).ExecuteBindingAsync(metadataProvider, actionContext, cancellationToken); + } + } +} diff --git a/src/System.Web.Http.OData/OData/Formatter/ODataMediaTypeFormatter.cs b/src/System.Web.Http.OData/OData/Formatter/ODataMediaTypeFormatter.cs index 83747e58..e85c50fb 100644 --- a/src/System.Web.Http.OData/OData/Formatter/ODataMediaTypeFormatter.cs +++ b/src/System.Web.Http.OData/OData/Formatter/ODataMediaTypeFormatter.cs @@ -210,7 +210,7 @@ namespace System.Web.Http.OData.Formatter { IODataRequestMessage oDataRequestMessage = new ODataMessageWrapper(readStream, contentHeaders); oDataMessageReader = new ODataMessageReader(oDataRequestMessage, oDataReaderSettings, ODataDeserializerProvider.EdmModel); - ODataDeserializerContext readContext = new ODataDeserializerContext { IsPatchMode = isPatchMode, PatchKeyMode = PatchKeyMode }; + ODataDeserializerContext readContext = new ODataDeserializerContext { IsPatchMode = isPatchMode, PatchKeyMode = PatchKeyMode, Request = Request, Model = Model }; result = deserializer.Read(oDataMessageReader, readContext); } catch (Exception e) diff --git a/src/System.Web.Http.OData/OData/Formatter/ODataParameterBindingAttribute.cs b/src/System.Web.Http.OData/OData/Formatter/ODataParameterBindingAttribute.cs new file mode 100644 index 00000000..7f4b170a --- /dev/null +++ b/src/System.Web.Http.OData/OData/Formatter/ODataParameterBindingAttribute.cs @@ -0,0 +1,18 @@ +// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved. See License.txt in the project root for license information. + +using System.Web.Http.Controllers; + +namespace System.Web.Http.OData.Formatter +{ + /// <summary> + /// This attribute insures that The ODataFormatterParameterBinding is used. + /// </summary> + [AttributeUsage(AttributeTargets.Class | AttributeTargets.Parameter, Inherited = true, AllowMultiple = false)] + public sealed class ODataParameterBindingAttribute : ParameterBindingAttribute + { + public override HttpParameterBinding GetBinding(HttpParameterDescriptor parameter) + { + return new ODataFormatterParameterBinding(parameter, parameter.Configuration.GetODataFormatter()); + } + } +} diff --git a/src/System.Web.Http.OData/OData/Formatter/Serialization/ODataSerializerContext.cs b/src/System.Web.Http.OData/OData/Formatter/Serialization/ODataSerializerContext.cs index 2d7f567b..5323c23f 100644 --- a/src/System.Web.Http.OData/OData/Formatter/Serialization/ODataSerializerContext.cs +++ b/src/System.Web.Http.OData/OData/Formatter/Serialization/ODataSerializerContext.cs @@ -1,5 +1,6 @@ // Copyright (c) Microsoft Open Technologies, Inc. All rights reserved. See License.txt in the project root for license information. +using System.Net.Http; using System.Web.Http.Routing; using Microsoft.Data.Edm; @@ -35,5 +36,11 @@ namespace System.Web.Http.OData.Formatter.Serialization /// and complex types. /// </summary> public string ServiceOperationName { get; set; } + + /// <summary> + /// Gets or sets the HttpRequestMessage. + /// The HttpRequestMessage can then be used by ODataSerializers to learn more about the Request that triggered the serialization + /// </summary> + public HttpRequestMessage Request { get; set; } } } diff --git a/src/System.Web.Http.OData/OData/IODataActionResolver.cs b/src/System.Web.Http.OData/OData/IODataActionResolver.cs new file mode 100644 index 00000000..62a14d12 --- /dev/null +++ b/src/System.Web.Http.OData/OData/IODataActionResolver.cs @@ -0,0 +1,20 @@ +// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved. See License.txt in the project root for license information. + +using System.Web.Http.OData.Formatter.Deserialization; +using Microsoft.Data.Edm; + +namespace System.Web.Http.OData +{ + /// <summary> + /// Resolves an OData Action + /// </summary> + public interface IODataActionResolver + { + /// <summary> + /// Return the matching ODataAction (IEdmFunctionImport) given the request described by the ODataDeserializerContext + /// </summary> + /// <param name="context">The ODataDeserializerContext from which the resolver should use to find the Action</param> + /// <returns>The resolved Action.</returns> + IEdmFunctionImport Resolve(ODataDeserializerContext context); + } +} diff --git a/src/System.Web.Http.OData/OData/ODataActionParameters.cs b/src/System.Web.Http.OData/OData/ODataActionParameters.cs new file mode 100644 index 00000000..e125ce9a --- /dev/null +++ b/src/System.Web.Http.OData/OData/ODataActionParameters.cs @@ -0,0 +1,38 @@ +// 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.Diagnostics.CodeAnalysis; +using System.Diagnostics.Contracts; +using System.Net.Http; +using System.Web.Http.OData.Formatter; +using System.Web.Http.OData.Formatter.Deserialization; +using System.Web.Http.OData.Properties; +using Microsoft.Data.Edm; + +namespace System.Web.Http.OData +{ + /// <summary> + /// ActionPayload holds the Parameter names and values provided by a client in a POST request + /// to invoke a particular Action. The Parameter values are stored in the dictionary keyed using the Parameter name. + /// </summary> + [ODataParameterBinding] + [SuppressMessage("Microsoft.Naming", "CA1710:IdentifiersShouldHaveCorrectSuffix", Justification = "Pending, will remove once class has appropriate base type.")] + [SuppressMessage("Microsoft.Usage", "CA2237:MarkISerializableTypesWithSerializable", Justification = "Pending, will remove once class has appropriate base type.")] + public class ODataActionParameters : Dictionary<string, object> + { + /// <summary> + /// Gets the IEdmFunctionImport that describes the payload. + /// </summary> + public virtual IEdmFunctionImport GetFunctionImport(ODataDeserializerContext context) + { + HttpConfiguration configuration = context.Request.GetConfiguration(); + if (configuration == null) + { + throw Error.InvalidOperation(SRResources.RequestMustContainConfiguration); + } + IODataActionResolver resolver = configuration.GetODataActionResolver(); + Contract.Assert(resolver != null); + return resolver.Resolve(context); + } + } +}
\ No newline at end of file diff --git a/src/System.Web.Http.OData/Properties/SRResources.Designer.cs b/src/System.Web.Http.OData/Properties/SRResources.Designer.cs index c92fb845..0b26e87a 100644 --- a/src/System.Web.Http.OData/Properties/SRResources.Designer.cs +++ b/src/System.Web.Http.OData/Properties/SRResources.Designer.cs @@ -1,7 +1,7 @@ //------------------------------------------------------------------------------ // <auto-generated> // This code was generated by a tool. -// Runtime Version:4.0.30319.18003 +// Runtime Version:4.0.30319.17929 // // Changes to this file may cause incorrect behavior and will be lost if // the code is regenerated. @@ -97,6 +97,24 @@ namespace System.Web.Http.OData.Properties { } /// <summary> + /// Looks up a localized string similar to Action '{0}' not found.. + /// </summary> + internal static string ActionNotFound { + get { + return ResourceManager.GetString("ActionNotFound", resourceCulture); + } + } + + /// <summary> + /// Looks up a localized string similar to Ambiguous request. Multiple action overloads called '{0}' found.. + /// </summary> + internal static string ActionResolutionFailed { + get { + return ResourceManager.GetString("ActionResolutionFailed", resourceCulture); + } + } + + /// <summary> /// Looks up a localized string similar to The argument must be of type '{0}'.. /// </summary> internal static string ArgumentMustBeOfType { diff --git a/src/System.Web.Http.OData/Properties/SRResources.resx b/src/System.Web.Http.OData/Properties/SRResources.resx index 47468f69..99dd6934 100644 --- a/src/System.Web.Http.OData/Properties/SRResources.resx +++ b/src/System.Web.Http.OData/Properties/SRResources.resx @@ -408,4 +408,10 @@ <data name="WriteToStreamAsyncMustHaveRequest" xml:space="preserve"> <value>The OData formatter does not support writing client requests. This formatter instance must have an associated request.</value> </data> + <data name="ActionNotFound" xml:space="preserve"> + <value>Action '{0}' not found.</value> + </data> + <data name="ActionResolutionFailed" xml:space="preserve"> + <value>Ambiguous request. Multiple action overloads called '{0}' found.</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 f183d5d4..5dbc0aef 100644 --- a/src/System.Web.Http.OData/System.Web.Http.OData.csproj +++ b/src/System.Web.Http.OData/System.Web.Http.OData.csproj @@ -104,6 +104,7 @@ <Link>Common\Error.cs</Link> </Compile> <Compile Include="GlobalSuppressions.cs" /> + <Compile Include="OData\ODataActionParameters.cs" /> <Compile Include="OData\Builder\ActionConfiguration.cs" /> <Compile Include="OData\Builder\BindingParameterConfiguration.cs" /> <Compile Include="OData\Builder\CollectionPropertyConfiguration.cs" /> @@ -158,9 +159,12 @@ <Compile Include="OData\Builder\Conventions\EntityTypeConvention.cs" /> <Compile Include="OData\Builder\Conventions\IEdmTypeConvention.cs" /> <Compile Include="OData\Builder\ComplexTypeConfigurationOfTComplexType.cs" /> + <Compile Include="OData\DefaultODataActionResolver.cs" /> <Compile Include="OData\EntityInstanceContextOfTEntityType.cs" /> <Compile Include="OData\EntityInstanceContext.cs" /> <Compile Include="OData\CompiledPropertyAccessor.cs" /> + <Compile Include="OData\Formatter\ODataParameterBindingAttribute.cs" /> + <Compile Include="OData\Formatter\Deserialization\ODataActionPayloadDeserializer.cs" /> <Compile Include="OData\Formatter\Deserialization\ODataCollectionDeserializer.cs" /> <Compile Include="OData\Formatter\Deserialization\ODataEntryAnnotation.cs" /> <Compile Include="OData\Formatter\Deserialization\ODataEntryDeserializerOfTItem.cs" /> @@ -170,11 +174,13 @@ <Compile Include="OData\Formatter\Deserialization\ODataPrimitiveDeserializer.cs" /> <Compile Include="OData\FeedContext.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\Serialization\ODataErrorSerializer.cs" /> <Compile Include="OData\Formatter\Serialization\ODataMetadataSerializer.cs" /> <Compile Include="OData\IDeltaOfTEntityType.cs" /> + <Compile Include="OData\IODataActionResolver.cs" /> <Compile Include="OData\ODataMetadataControllerConfigurationAttribute.cs" /> <Compile Include="OData\ODataResultOfT.cs" /> <Compile Include="OData\Query\Expressions\ClrSafeFunctions.cs" /> diff --git a/test/System.Web.Http.OData.Test/OData/DefaultODataActionResolverTest.cs b/test/System.Web.Http.OData.Test/OData/DefaultODataActionResolverTest.cs new file mode 100644 index 00000000..06b8b5ed --- /dev/null +++ b/test/System.Web.Http.OData.Test/OData/DefaultODataActionResolverTest.cs @@ -0,0 +1,110 @@ +// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved. See License.txt in the project root for license information. + +using System.Linq; +using System.Net.Http; +using System.Web.Http.Hosting; +using System.Web.Http.OData.Builder; +using System.Web.Http.OData.Builder.TestModels; +using System.Web.Http.OData.Formatter.Deserialization; +using Microsoft.Data.Edm; +using Microsoft.TestCommon; + +namespace System.Web.Http.OData +{ + public class DefaultODataActionResolverTest + { + private IEdmModel _model; + + [Theory] + [InlineData("Drive", "http://server/Vehicles(6)/Drive")] + [InlineData("Drive", "http://server/Vehicles(6)/Container.Drive")] + [InlineData("Drive", "http://server/Vehicles(6)/org.odata.Container.Drive")] + [InlineData("Drive", "http://server/service/Vehicles(6)/Drive")] + [InlineData("Drive", "http://server/service/Vehicles(6)/Container.Drive")] + [InlineData("Drive", "http://server/service/Vehicles(6)/org.odata.Container.Drive")] + [InlineData("Drive", "http://server/Vehicles(6)/Container.Car/Drive")] + [InlineData("Drive", "http://server/Vehicles(6)/Container.Car/Container.Drive")] + [InlineData("Drive", "http://server/Vehicles(6)/Container.Car/org.odata.Container.Drive")] + [InlineData("Drive", "http://server/service/Vehicles/Container.Car(6)/Drive")] + [InlineData("Drive", "http://server/service/Vehicles/Container.Car(6)/Container.Drive")] + [InlineData("Drive", "http://server/service/Vehicles/Container.Car(6)/org.odata.Container.Drive")] + public void Can_find_action(string actionName, string url) + { + IODataActionResolver resolver = new DefaultODataActionResolver(); + ODataDeserializerContext context = new ODataDeserializerContext { Request = GetPostRequest(url), Model = GetModel() }; + IEdmFunctionImport action = resolver.Resolve(context); + Assert.NotNull(action); + Assert.Equal(actionName, action.Name); + } + + [Fact(Skip = "Requires improvements in Uri Parser so it can establish type of path segment prior to ActionName")] + public void Can_find_action_overload_using_bindingparameter_type() + { + string url = "http://server/service/Vehicles(8)/Container.Car/Wash"; + IODataActionResolver resolver = new DefaultODataActionResolver(); + ODataDeserializerContext context = new ODataDeserializerContext { Request = GetPostRequest(url), Model = GetModel() }; + IEdmFunctionImport action = resolver.Resolve(context); + Assert.NotNull(action); + Assert.Equal("Car", action.Parameters.First().Name); + } + + [Fact] + public void Throws_InvalidOperation_when_action_not_found() + { + string invalidUrl = "http://server/service/MissingOperation"; + IODataActionResolver resolver = new DefaultODataActionResolver(); + ODataDeserializerContext context = new ODataDeserializerContext { Request = GetPostRequest(invalidUrl), Model = GetModel() }; + Assert.Throws<InvalidOperationException>(() => + { + IEdmFunctionImport action = resolver.Resolve(context); + }, "Action 'MissingOperation' not found."); + } + + [Fact] + public void Throws_InvalidOperation_when_multiple_overloads_found() + { + string invalidUrl = "http://server/service/Vehicles/Container.Car(8)/Park"; + IODataActionResolver resolver = new DefaultODataActionResolver(); + ODataDeserializerContext context = new ODataDeserializerContext { Request = GetPostRequest(invalidUrl), Model = GetModel() }; + InvalidOperationException ioe = Assert.Throws<InvalidOperationException>(() => + { + IEdmFunctionImport action = resolver.Resolve(context); + }, "Ambiguous request. Multiple action overloads called 'Park' found."); + } + + [Fact] + public void Is_Auto_Registered() + { + HttpConfiguration configuration = new HttpConfiguration(); + DefaultODataActionResolver resolver = configuration.GetODataActionResolver() as DefaultODataActionResolver; + Assert.NotNull(resolver); + } + + private IEdmModel GetModel() + { + if (_model == null) + { + ODataModelBuilder builder = new ODataConventionModelBuilder(); + builder.ContainerName = "Container"; + builder.Namespace = "org.odata"; + // Action with no overloads + builder.EntitySet<Vehicle>("Vehicles").EntityType.Action("Drive"); + // Valid overloads of "Wash" bound to different entities + builder.Entity<Motorcycle>().Action("Wash"); + builder.Entity<Car>().Action("Wash"); + // Invalid overloads of "Park" + builder.Entity<Car>().Action("Park"); + builder.Entity<Car>().Action("Park").Parameter<string>("mood"); + _model = builder.GetEdmModel(); + } + return _model; + } + + private static HttpRequestMessage GetPostRequest(string url) + { + HttpRequestMessage request = new HttpRequestMessage(HttpMethod.Post, url); + request.Properties[HttpPropertyKeys.HttpConfigurationKey] = new HttpConfiguration(); + return request; + } + } +} diff --git a/test/System.Web.Http.OData.Test/OData/Formatter/Deserialization/DefaultODataDeserializerProviderTests.cs b/test/System.Web.Http.OData.Test/OData/Formatter/Deserialization/DefaultODataDeserializerProviderTests.cs index 92e66d33..fd12c053 100644 --- a/test/System.Web.Http.OData.Test/OData/Formatter/Deserialization/DefaultODataDeserializerProviderTests.cs +++ b/test/System.Web.Http.OData.Test/OData/Formatter/Deserialization/DefaultODataDeserializerProviderTests.cs @@ -98,5 +98,27 @@ namespace System.Web.Http.OData.Formatter.Deserialization Assert.Same(firstCallDeserializer, secondCallDeserializer); } + + [Fact] + public void GetODataSerializer_ActionPayload() + { + ODataDeserializerProvider deserializerProvider = new DefaultODataDeserializerProvider(_edmModel); + ODataActionPayloadDeserializer basicActionPayload = deserializerProvider.GetODataDeserializer(typeof(ODataActionParameters)) as ODataActionPayloadDeserializer; + + Assert.NotNull(basicActionPayload); + } + + [Fact] + public void GetODataSerializer_Derived_ActionPayload() + { + ODataDeserializerProvider deserializerProvider = new DefaultODataDeserializerProvider(_edmModel); + ODataActionPayloadDeserializer derivedActionPayload = deserializerProvider.GetODataDeserializer(typeof(MyActionPayload)) as ODataActionPayloadDeserializer; + + Assert.NotNull(derivedActionPayload); + } + + public class MyActionPayload : ODataActionParameters + { + } } } diff --git a/test/System.Web.Http.OData.Test/OData/Formatter/Deserialization/ODataActionPayloadDeserializerTest.cs b/test/System.Web.Http.OData.Test/OData/Formatter/Deserialization/ODataActionPayloadDeserializerTest.cs new file mode 100644 index 00000000..36acd8aa --- /dev/null +++ b/test/System.Web.Http.OData.Test/OData/Formatter/Deserialization/ODataActionPayloadDeserializerTest.cs @@ -0,0 +1,211 @@ +// 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.IO; +using System.Linq; +using System.Net.Http; +using System.Web.Http.Hosting; +using System.Web.Http.OData.Builder; +using System.Web.Http.OData.TestCommon.Models; +using Microsoft.Data.Edm; +using Microsoft.Data.OData; +using Microsoft.TestCommon; + +namespace System.Web.Http.OData.Formatter.Deserialization +{ + public class ODataActionPayloadDeserializerTest + { + private IEdmModel _model; + + [Fact] + public void Can_deserialize_payload_with_primitive_parameters() + { + string actionName = "Primitive"; + int quantity = 1; + string productCode = "PCode"; + string body = "{" + string.Format(@" ""Quantity"": {0} , ""ProductCode"": ""{1}"" ", quantity, productCode) + "}"; + + ODataMessageWrapper message = new ODataMessageWrapper(GetStringAsStream(body)); + message.SetHeader("Content-Type", "application/json;odata=verbose"); + + IEdmModel model = GetModel(); + ODataMessageReader reader = new ODataMessageReader(message as IODataRequestMessage, new ODataMessageReaderSettings(), model); + ODataActionPayloadDeserializer deserializer = new ODataActionPayloadDeserializer(typeof(ODataActionParameters), new DefaultODataDeserializerProvider(model)); + string url = "http://server/service/EntitySet(key)/" + actionName; + HttpRequestMessage request = GetPostRequest(url); + + ODataDeserializerContext context = new ODataDeserializerContext { Request = request, Model = model }; + ODataActionParameters payload = deserializer.Read(reader, context) as ODataActionParameters; + + Assert.NotNull(payload); + Assert.Same(model.EntityContainers().Single().FunctionImports().SingleOrDefault(f => f.Name == "Primitive"), payload.GetFunctionImport(context)); + Assert.True(payload.ContainsKey("Quantity")); + Assert.Equal(quantity, payload["Quantity"]); + Assert.True(payload.ContainsKey("ProductCode")); + Assert.Equal(productCode, payload["ProductCode"]); + } + + [Fact] + public void Can_deserialize_payload_with_complex_parameters() + { + string actionName = "Complex"; + string body = @"{ ""Quantity"": 1 , ""Address"": { ""StreetAddress"":""1 Microsoft Way"", ""City"": ""Redmond"", ""State"": ""WA"", ""ZipCode"": 98052 } }"; + + ODataMessageWrapper message = new ODataMessageWrapper(GetStringAsStream(body)); + message.SetHeader("Content-Type", "application/json;odata=verbose"); + IEdmModel model = GetModel(); + ODataMessageReader reader = new ODataMessageReader(message as IODataRequestMessage, new ODataMessageReaderSettings(), model); + + ODataActionPayloadDeserializer deserializer = new ODataActionPayloadDeserializer(typeof(ODataActionParameters), new DefaultODataDeserializerProvider(model)); + string url = "http://server/service/EntitySet(key)/" + actionName; + HttpRequestMessage request = GetPostRequest(url); + ODataDeserializerContext context = new ODataDeserializerContext { Request = request, Model = model }; + ODataActionParameters payload = deserializer.Read(reader, context) as ODataActionParameters; + + Assert.NotNull(payload); + Assert.Same(model.EntityContainers().Single().FunctionImports().SingleOrDefault(f => f.Name == "Complex"), payload.GetFunctionImport(context)); + Assert.True(payload.ContainsKey("Quantity")); + Assert.Equal(1, payload["Quantity"]); + Assert.True(payload.ContainsKey("Address")); + MyAddress address = payload["Address"] as MyAddress; + Assert.NotNull(address); + Assert.Equal("1 Microsoft Way", address.StreetAddress); + Assert.Equal("Redmond", address.City); + Assert.Equal("WA", address.State); + Assert.Equal(98052, address.ZipCode); + } + + [Fact] + public void Can_deserialize_payload_with_primitive_collection_parameters() + { + string actionName = "PrimitiveCollection"; + string body = @"{ ""Name"": ""Avatar"", ""Ratings"": [ 5, 5, 3, 4, 5, 5, 4, 5, 5, 4 ] }"; + int[] expectedRatings = new int[] { 5, 5, 3, 4, 5, 5, 4, 5, 5, 4 }; + ODataMessageWrapper message = new ODataMessageWrapper(GetStringAsStream(body)); + message.SetHeader("Content-Type", "application/json;odata=verbose"); + IEdmModel model = GetModel(); + ODataMessageReader reader = new ODataMessageReader(message as IODataRequestMessage, new ODataMessageReaderSettings(), model); + + ODataActionPayloadDeserializer deserializer = new ODataActionPayloadDeserializer(typeof(ODataActionParameters), new DefaultODataDeserializerProvider(model)); + string url = "http://server/service/EntitySet(key)/" + actionName; + HttpRequestMessage request = GetPostRequest(url); + ODataDeserializerContext context = new ODataDeserializerContext { Request = request, Model = model }; + ODataActionParameters payload = deserializer.Read(reader, context) as ODataActionParameters; + + Assert.NotNull(payload); + Assert.Same(model.EntityContainers().Single().FunctionImports().SingleOrDefault(f => f.Name == "PrimitiveCollection"), payload.GetFunctionImport(context)); + Assert.True(payload.ContainsKey("Name")); + Assert.Equal("Avatar", payload["Name"]); + Assert.True(payload.ContainsKey("Ratings")); + IList<int> ratings = payload["Ratings"] as IList<int>; + Assert.Equal(10, ratings.Count); + Assert.True(expectedRatings.Zip(ratings, (expected, actual) => expected - actual).All(diff => diff == 0)); + } + + [Fact] + public void Can_deserialize_payload_with_complex_collection_parameters() + { + string actionName = "ComplexCollection"; + string body = @"{ ""Name"": ""Microsoft"", ""Addresses"": [ { ""StreetAddress"":""1 Microsoft Way"", ""City"": ""Redmond"", ""State"": ""WA"", ""ZipCode"": 98052 } ] }"; + ODataMessageWrapper message = new ODataMessageWrapper(GetStringAsStream(body)); + message.SetHeader("Content-Type", "application/json;odata=verbose"); + IEdmModel model = GetModel(); + ODataMessageReader reader = new ODataMessageReader(message as IODataRequestMessage, new ODataMessageReaderSettings(), model); + + ODataActionPayloadDeserializer deserializer = new ODataActionPayloadDeserializer(typeof(ODataActionParameters), new DefaultODataDeserializerProvider(model)); + string url = "http://server/service/EntitySet(key)/" + actionName; + HttpRequestMessage request = GetPostRequest(url); + ODataDeserializerContext context = new ODataDeserializerContext { Request = request, Model = model }; + ODataActionParameters payload = deserializer.Read(reader, context) as ODataActionParameters; + + Assert.NotNull(payload); + Assert.True(payload.ContainsKey("Name")); + Assert.Equal("Microsoft", payload["Name"]); + Assert.True(payload.ContainsKey("Addresses")); + IList<MyAddress> addresses = payload["Addresses"] as IList<MyAddress>; + Assert.NotNull(addresses); + Assert.Equal(1, addresses.Count); + MyAddress address = addresses[0]; + Assert.NotNull(address); + Assert.Equal("1 Microsoft Way", address.StreetAddress); + Assert.Equal("Redmond", address.City); + Assert.Equal("WA", address.State); + Assert.Equal(98052, address.ZipCode); + } + + [Fact] + public void Throws_ODataException_when_parameter_not_found() + { + string body = @"{ ""Quantity"": 1 , ""ProductCode"": ""PCode"", ""MissingParameter"": 1 }"; + + ODataMessageWrapper message = new ODataMessageWrapper(GetStringAsStream(body)); + message.SetHeader("Content-Type", "application/json;odata=verbose"); + IEdmModel model = GetModel(); + ODataMessageReader reader = new ODataMessageReader(message as IODataRequestMessage, new ODataMessageReaderSettings(), model); + + ODataActionPayloadDeserializer deserializer = new ODataActionPayloadDeserializer(typeof(ODataActionParameters), new DefaultODataDeserializerProvider(model)); + string url = "http://server/service/EntitySet(key)/Primitive"; + HttpRequestMessage request = GetPostRequest(url); + ODataDeserializerContext context = new ODataDeserializerContext { Request = request, Model = model }; + Assert.Throws<ODataException>(() => + { + ODataActionParameters payload = deserializer.Read(reader, context) as ODataActionParameters; + }, "The parameter 'MissingParameter' in the request payload is not a valid parameter for the function import 'Primitive'."); + } + + private IEdmModel GetModel() + { + if (_model == null) + { + ODataModelBuilder builder = new ODataConventionModelBuilder(); + builder.ContainerName = "C"; + builder.Namespace = "A.B"; + EntityTypeConfiguration<Customer> customer = builder.EntitySet<Customer>("Customers").EntityType; + + ActionConfiguration primitive = customer.Action("Primitive"); + primitive.Parameter<int>("Quantity"); + primitive.Parameter<string>("ProductCode"); + + ActionConfiguration complex = customer.Action("Complex"); + complex.Parameter<int>("Quantity"); + complex.Parameter<MyAddress>("Address"); + + ActionConfiguration primitiveCollection = customer.Action("PrimitiveCollection"); + primitiveCollection.Parameter<string>("Name"); + primitiveCollection.CollectionParameter<int>("Ratings"); + + ActionConfiguration complexCollection = customer.Action("ComplexCollection"); + complexCollection.Parameter<string>("Name"); + complexCollection.CollectionParameter<MyAddress>("Addresses"); + + _model = builder.GetEdmModel(); + } + return _model; + } + + private static HttpRequestMessage GetPostRequest(string url) + { + HttpRequestMessage request = new HttpRequestMessage(HttpMethod.Post, url); + request.Properties[HttpPropertyKeys.HttpConfigurationKey] = new HttpConfiguration(); + return request; + } + + private static Stream GetStringAsStream(string body) + { + Stream stream = new MemoryStream(); + StreamWriter writer = new StreamWriter(stream); + writer.Write(body); + writer.Flush(); + stream.Seek(0, SeekOrigin.Begin); + return stream; + } + } + + public class MyAddress + { + public string StreetAddress { get; set; } + public string City { get; set; } + public string State { get; set; } + public int ZipCode { get; set; } + } +} diff --git a/test/System.Web.Http.OData.Test/OData/Formatter/ODataActionTests.cs b/test/System.Web.Http.OData.Test/OData/Formatter/ODataActionTests.cs new file mode 100644 index 00000000..11f4aa41 --- /dev/null +++ b/test/System.Web.Http.OData.Test/OData/Formatter/ODataActionTests.cs @@ -0,0 +1,118 @@ +// 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.Net.Http; +using System.Net.Http.Headers; +using System.Web.Http.OData.Builder; +using Microsoft.Data.Edm; +using Microsoft.TestCommon; + +namespace System.Web.Http.OData.Formatter +{ + public class ODataActionTests + { + ODataMediaTypeFormatter _formatter; + HttpConfiguration _configuration; + HttpServer _server; + HttpClient _client; + IEdmModel _model; + + public ODataActionTests() + { + _configuration = new HttpConfiguration(); + _model = GetModel(); + _formatter = new ODataMediaTypeFormatter(_model); + _configuration.Formatters.Clear(); + _configuration.SetODataFormatter(_formatter); + + _configuration.Routes.MapHttpRoute("default", "{action}", new { Controller = "ODataActions" }); + _configuration.Routes.MapHttpRoute(ODataRouteNames.GetById, "{controller}({id})"); + _configuration.Routes.MapHttpRoute(ODataRouteNames.Default, "{controller}"); + + _server = new HttpServer(_configuration); + _client = new HttpClient(_server); + } + + [Fact] + public void Can_dispatch_actionPayload_to_action() + { + HttpRequestMessage request = new HttpRequestMessage(HttpMethod.Post, "http://localhost/DoSomething"); + request.Headers.Add("accept", "application/json;odata=verbose"); + string payload = @"{ + ""p1"": 1, + ""p2"": { ""StreetAddress"": ""1 Microsoft Way"", ""City"": ""Redmond"", ""State"": ""WA"", ""ZipCode"": 98052 }, + ""p3"": [ ""one"", ""two"" ], + ""p4"": [ { ""StreetAddress"": ""1 Microsoft Way"", ""City"": ""Redmond"", ""State"": ""WA"", ""ZipCode"": 98052 } ] + }"; + + request.Content = new StringContent(payload); + request.Content.Headers.ContentType = MediaTypeHeaderValue.Parse("application/json;odata=verbose"); + + HttpResponseMessage response = _client.SendAsync(request).Result; + response.EnsureSuccessStatusCode(); + } + + private IEdmModel GetModel() + { + ODataModelBuilder builder = new ODataConventionModelBuilder(); + builder.ContainerName = "Container"; + builder.Namespace = "org.odata"; + EntityTypeConfiguration<Customer> customer = builder.EntitySet<Customer>("Customers").EntityType; + ActionConfiguration action = customer.Action("DoSomething"); + action.Parameter<int>("p1"); + action.Parameter<Address>("p2"); + action.CollectionParameter<string>("p3"); + action.CollectionParameter<Address>("p4"); + return builder.GetEdmModel(); + } + + public class Customer + { + public int ID { get; set; } + public string Name { get; set; } + } + + public class Address + { + public string StreetAddress { get; set; } + public string City { get; set; } + public string State { get; set; } + public int ZipCode { get; set; } + } + } + + public class ODataActionsController : ApiController + { + [HttpPost] + public bool DoSomething(ODataActionParameters parameters) + { + Assert.Equal(1, parameters["p1"]); + ValidateAddress(parameters["p2"] as ODataActionTests.Address); + ValidateNumbers(parameters["p3"] as IList<string>); + ValidateAddresses(parameters["p4"] as IList<ODataActionTests.Address>); + return true; + } + + private void ValidateAddress(ODataActionTests.Address address) + { + Assert.NotNull(address); + Assert.Equal("1 Microsoft Way", address.StreetAddress); + Assert.Equal("Redmond", address.City); + Assert.Equal("WA", address.State); + Assert.Equal(98052, address.ZipCode); + } + private void ValidateNumbers(IList<string> numbers) + { + Assert.NotNull(numbers); + Assert.Equal(2, numbers.Count); + Assert.Equal("one", numbers[0]); + Assert.Equal("two", numbers[1]); + } + private void ValidateAddresses(IList<ODataActionTests.Address> addresses) + { + Assert.NotNull(addresses); + Assert.Equal(1, addresses.Count); + ValidateAddress(addresses[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 f10f9646..e996a3fa 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 @@ -107,7 +107,10 @@ <Compile Include="OData\Builder\ParameterConfigurationTest.cs" /> <Compile Include="OData\Builder\CollectionPropertyConfigurationTest.cs" /> <Compile Include="OData\Builder\TestModels\EnumModel.cs" /> + <Compile Include="OData\DefaultODataActionResolverTest.cs" /> + <Compile Include="OData\Formatter\ODataActionTests.cs" /> <Compile Include="OData\Formatter\InheritanceTests.cs" /> + <Compile Include="OData\Formatter\Deserialization\ODataActionPayloadDeserializerTest.cs" /> <Compile Include="OData\Formatter\PartialTrustTest.cs" /> <Compile Include="OData\Builder\EdmTypeConfigurationExtensionsTest.cs" /> <Compile Include="OData\Builder\TestModels\InheritanceModels.cs" /> |