Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How to pass Array to OData function in ASP.NET Web API implementation?

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)
like image 201
Eugene D. Gubenkov Avatar asked Oct 29 '14 15:10

Eugene D. Gubenkov


People also ask

How do I pass an array to API?

First step is to build the list and assign to a global (let's use $list). Probably would read the source (could be a file, db query, xml/json from an inbound api call), build a loop using While() and append values to $list (like $list = $list + ",'"+sourcelist[count]+"'").

How do I enable OData in Web API?

In Solution Explorer, right click on Models folder > Add > Class > Name your class. Now, we are going to create a Controller. Right click on the Controllers folder > Add > Controller> selecting Web API 2 OData v3 Controller with actions, using Entity Framework > click Add.

What is the use of $expand in OData?

OData expand functionality can be used to query related data. For example, to get the Course data for each Enrollment entity, include ?$ expand=course at the end of the request path: This tutorial uses Postman to test the web API.


2 Answers

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

like image 136
MHollis Avatar answered Sep 22 '22 23:09

MHollis


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

like image 25
Matthew Merryfull Avatar answered Sep 23 '22 23:09

Matthew Merryfull