19

The specification of OData V4 states that it MUST be possible: https://issues.oasis-open.org/browse/ODATA-636.

"Complex types and arrays can only be passed to functions through parameter aliases"

When I'm trying to pass an array with OData parameter aliases an exception occurs.

/TestEntities/NS.TestFunction(ArrayHere=@p)?@p=[1,2,3]

Results in:

Unable to cast object of type 'EdmValidCoreModelPrimitiveType' to type 'Microsoft.OData.Edm.IEdmStructuredType

The interesting thing is that the metadata document is correctly composed for such cases:

<Function Name="TestFunction" IsBound="true">
  <Parameter Name="bindingParameter" Type="Collection(NS.TestEntity)"/>
  <Parameter Name="ArrayHere" Type="System.Int32[]"/>
  <ReturnType Type="Collection(NS.TestEntity)"/>
</Function>

Is it possible with ASP.NET MVC Web API 2 OData to pass an array to OData function in query string?

UPDATE:

Here is the code to build the EDM model and the controller.

 var builder = new ODataConventionModelBuilder();

 builder.Namespace = "NS";

 builder.EntitySet<TestEntity>("TestEntities");

 builder.EntityType<TestEntity>().Collection
    .Function("TestFunction")
    .ReturnsCollectionFromEntitySet<TestEntity>("TestEntities")
    .Parameter<int[]>("ArrayHere");

Controller:

public class TestEntitiesController : ODataController
{
    public IEnumerable<TestEntity> TestFunction(int[] arrayHere)
    {
        throw new NotImplementedException();
    }
}

Marking parameter with [FromODataUri] does not solve the problem.

UPDATE 2:

Here is the stack trace:

at Microsoft.OData.Core.UriParser.TypePromotionUtils.CanConvertTo(SingleValueNode sourceNodeOrNull, IEdmTypeReference sourceReference, IEdmTypeReference targetReference)
at Microsoft.OData.Core.UriParser.Parsers.MetadataBindingUtils.ConvertToTypeIfNeeded(SingleValueNode source, IEdmTypeReference targetTypeReference)
at Microsoft.OData.Core.UriParser.Parsers.FunctionCallBinder.BindSegmentParameters(ODataUriParserConfiguration configuration, IEdmOperation functionOrOpertion, ICollection`1 segmentParameterTokens)
at Microsoft.OData.Core.UriParser.Parsers.ODataPathParser.TryBindingParametersAndMatchingOperation(String identifier, String parenthesisExpression, IEdmType bindingType, ODataUriParserConfiguration configuration, ICollection`1& boundParameters, IEdmOperation& matchingOperation)
at Microsoft.OData.Core.UriParser.Parsers.ODataPathParser.TryCreateSegmentForOperation(ODataPathSegment previousSegment, String identifier, String parenthesisExpression)
at Microsoft.OData.Core.UriParser.Parsers.ODataPathParser.CreateNextSegment(String text)
at Microsoft.OData.Core.UriParser.Parsers.ODataPathParser.ParsePath(ICollection`1 segments)
at Microsoft.OData.Core.UriParser.Parsers.ODataPathFactory.BindPath(ICollection`1 segments, ODataUriParserConfiguration configuration)
at Microsoft.OData.Core.UriParser.ODataUriParser.ParsePathImplementation()
at Microsoft.OData.Core.UriParser.ODataUriParser.Initialize()
at System.Web.OData.Routing.DefaultODataPathHandler.Parse(IEdmModel model, String serviceRoot, String odataPath, Boolean enableUriTemplateParsing)
at System.Web.OData.Routing.DefaultODataPathHandler.Parse(IEdmModel model, String serviceRoot, String odataPath)
at System.Web.OData.Routing.ODataPathRouteConstraint.Match(HttpRequestMessage request, IHttpRoute route, String parameterName, IDictionary`2 values, HttpRouteDirection routeDirection)
at System.Web.Http.Routing.HttpRoute.ProcessConstraint(HttpRequestMessage request, Object constraint, String parameterName, HttpRouteValueDictionary values, HttpRouteDirection routeDirection)
at System.Web.Http.Routing.HttpRoute.ProcessConstraints(HttpRequestMessage request, HttpRouteValueDictionary values, HttpRouteDirection routeDirection)
at System.Web.Http.Routing.HttpRoute.GetRouteData(String virtualPathRoot, HttpRequestMessage request)
at System.Web.Http.WebHost.Routing.HttpWebRoute.GetRouteData(HttpContextBase httpContext)
3
  • Can you post the code that is throwing the exception? Commented Oct 29, 2014 at 15:45
  • Interesting question, I would really be happy to see the code you used to add the function to the $metadata and the function implementation. Commented Oct 29, 2014 at 19:32
  • I have code that is similar but uses CollectionParameter<int>() instead of Parameter<int[]>(). It does not throw an exception but the value passed to the custom function in the controller is always null (whether I define it as int[] or IEnumerable<in> or ICollection<int>, etc.). Interestingly, Microsoft's OData client builder creates code that puts the JSON as the parameter value rather than using a parameter alias. However, the OData routing code fails to find the controller for a function call formatted like that. Commented Apr 17, 2015 at 13:13

2 Answers 2

21

Assuming you're using OData V4 you need to use CollectionParameter when registering the function and you're missing the [FromODataUri] on the arrayHere parameter. Also, try it with IEnumerable<int> instead of an array.

var builder = new ODataConventionModelBuilder();

 builder.Namespace = "NS";

 builder.EntitySet<TestEntity>("TestEntities");

 builder.EntityType<TestEntity>().Collection
    .Function("TestFunction")
    .ReturnsCollectionFromEntitySet<TestEntity>("TestEntities")
    .CollectionParameter<int>("ArrayHere");

With the function in the controller like this...

[HttpGet]
public async Task<IHttpActionResult> TestFunction([FromODataUri] IEnumerable<int> ArrayHere)
{
    // Do stuff
}

Now you can make a request such as...

http://yourRestService/API/TestEntities/NS.TestFunction(ArrayHere=[1,2,3])

As a note, you can also accept an array of complex types as well. You'll have to url encode the json for the array and use a parameter alias so you'll end up with something like this...

 builder.EntityType<TestEntity>().Collection
    .Function("TestFunction2")
    .ReturnsCollectionFromEntitySet<TestEntity>("TestEntities")
    .CollectionParameter<person>("ArrayHere");

and

[HttpGet]
public async Task<IHttpActionResult> TestFunction2([FromODataUri] IEnumerable<person> ArrayHere)
{
    // Do stuff
}

http://yourRestService/API/TestEntities/NS.TestFunction2(ArrayHere=@ArrayData)?@ArrayData=%5B%7B%22FirstName%22%3A%22Bob%22%2C+%22LastName%22%3A%22Dole%22%7D%2C%7B%22FirstName%22%3A%22Bill%22%2C+%22LastName%22%3A%22Clinton%22%7D%5D

Sign up to request clarification or add additional context in comments.

6 Comments

Great! CollectionParameter! I was trying to use Parameter<int[]>(). I'd like to clarify that values to such parameters are enclosed in brackets in the URL: NS.TestFunction9(IntArrayHere=[11,12,13,14,15])
I have voted up, it's really useful. But it's must important to use a correct namespace. So, for expample, if your OData controller has a namespace 'CK.Common.Planogram.Controllers' you must use 'CK.Common.Planogram' as namespace in the WebApi.config. A function without correct namespace will be return 404/406. Be carefull.
+1 Shows in depth usage for Functions where other top response focused on Actions only. The encoded HTTP was hard to read, and not entirely useful because we tend to code in environments where the request will be correctly encoded for us, so thats probably why you got the down vote. Perhaps showing the swagger doc for such a function might be helpful too
Check here , ODataConventionModelBuilder doesn't have EntityType method , then how to use ? Is there a clear tutorial to post collection to Odata action ?
I am getting: HTTP Error 404.0 - Not Found The resource you are looking for has been removed, had its name changed, or is temporarily unavailable.
|
10

I don't know which version of OData you're currently using, but here's a solution I've implemented which works for v4 and passing in simple arrays to an OData Action Method

In your WebConfig.cs

ODataModelBuilder builder = new ODataConventionModelBuilder();
builder.EntitySet<TestEntity>("TestEntities");

builder.Namespace = "NS";

builder.EntityType<TestEntity>()
        .Action("TestAction")
    .CollectionParameter<string>("ArrayHere");    

And your controller method here

[HttpPost]
public async Task<IHttpActionResult> TestAction([FromODataUri] int key, ODataActionParameters parameters)
{
    // Do your thing here..
    var invites = parameters["ArrayHere"] as IEnumerable<string>;
}

The resulting API endpoint request should look something like the following:

POST http://localhost/odata/TestEntities(1)/NS.TestAction HTTP/1.1
Content-type: application/json
Host: localhost:49255
Content-Length: 37

{"ArrayHere":["hello", "world"]}    

I went down this particular problem a few times and couldn't make it work for an OData function. I dug a little bit deeper and found the CollectionParameter<>() method which sorted everything out. I didn't go back and try it with a Function as I didn't really see the need. I hope this helps you out

1 Comment

Thanks for the answer, but the question is about passing array to OData Function (as part of query string), not an Action (as request payload) which worked for me also.

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.