Welcome to mirror list, hosted at ThFree Co, Russian Federation.

github.com/mono/aspnetwebstack.git - Unnamed repository; edit this file 'description' to name the repository.
summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorAlex James <Alex@base4.net>2012-09-20 06:08:40 +0400
committerAlex James <Alex@base4.net>2012-09-27 20:21:40 +0400
commite6cad76db3e4b60e54418eff1cbcfc14ff8982aa (patch)
treedf387d564427f0e5cb0b0ecd13e4ada7b908d3b6
parent6a0c03f9e549966a7f806f8b696ec4cb2ec272e6 (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
-rw-r--r--src/System.Web.Http.OData/HttpConfigurationExtensions.cs40
-rw-r--r--src/System.Web.Http.OData/OData/DefaultODataActionResolver.cs63
-rw-r--r--src/System.Web.Http.OData/OData/Formatter/Deserialization/DefaultODataDeserializerProvider.cs5
-rw-r--r--src/System.Web.Http.OData/OData/Formatter/Deserialization/ODataActionPayloadDeserializer.cs117
-rw-r--r--src/System.Web.Http.OData/OData/Formatter/Deserialization/ODataDeserializerContext.cs22
-rw-r--r--src/System.Web.Http.OData/OData/Formatter/ODataFormatterParameterBinding.cs33
-rw-r--r--src/System.Web.Http.OData/OData/Formatter/ODataMediaTypeFormatter.cs2
-rw-r--r--src/System.Web.Http.OData/OData/Formatter/ODataParameterBindingAttribute.cs18
-rw-r--r--src/System.Web.Http.OData/OData/Formatter/Serialization/ODataSerializerContext.cs7
-rw-r--r--src/System.Web.Http.OData/OData/IODataActionResolver.cs20
-rw-r--r--src/System.Web.Http.OData/OData/ODataActionParameters.cs38
-rw-r--r--src/System.Web.Http.OData/Properties/SRResources.Designer.cs20
-rw-r--r--src/System.Web.Http.OData/Properties/SRResources.resx6
-rw-r--r--src/System.Web.Http.OData/System.Web.Http.OData.csproj6
-rw-r--r--test/System.Web.Http.OData.Test/OData/DefaultODataActionResolverTest.cs110
-rw-r--r--test/System.Web.Http.OData.Test/OData/Formatter/Deserialization/DefaultODataDeserializerProviderTests.cs22
-rw-r--r--test/System.Web.Http.OData.Test/OData/Formatter/Deserialization/ODataActionPayloadDeserializerTest.cs211
-rw-r--r--test/System.Web.Http.OData.Test/OData/Formatter/ODataActionTests.cs118
-rw-r--r--test/System.Web.Http.OData.Test/System.Web.Http.OData.Test.csproj3
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 &apos;{0}&apos; 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 &apos;{0}&apos; 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 &apos;{0}&apos;..
/// </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" />