10

I have a Web API project which has been used for several years without OData support, just with standard URL params.

I now wish to add OData support to this API, but as the API is not built on a queryable model the intention is to receive the ODataQueryOptions<T> object and pass this down to a repository.

Everything I can find to read about supporting OData either assumes that I have a queryable model or is overly simplistic and simply tells me how to make sense of the ODataQueryOptions object. Consequently I'm unable to get a simple method up and running.

Here's what I currently have.

[Route("test")]
[HttpGet]
[EnableQuery]
public IHttpActionResult Test(ODataQueryOptions<TestOptions> options)
{
    var settings = new ODataValidationSettings {
            AllowedFunctions = AllowedFunctions.None,
            AllowedLogicalOperators = AllowedLogicalOperators.Equal,
            AllowedArithmeticOperators = AllowedArithmeticOperators.None,
            AllowedQueryOptions = AllowedQueryOptions.Filter
        };
    try
    {
        options.Validate(settings);
    }
    catch (ODataException exception)
    {
        return BadRequest(exception.Message);
    }

    var binaryOperator = options.Filter?.FilterClause?.Expression as BinaryOperatorNode;
    if (binaryOperator != null)
    {
        var property = binaryOperator.Left as SingleValuePropertyAccessNode ?? binaryOperator.Right as SingleValuePropertyAccessNode;
        var constant = binaryOperator.Left as ConstantNode ?? binaryOperator.Right as ConstantNode;

        if (property?.Property != null && constant?.Value != null)
        {
            ;
        }
    }

    return Ok();
}

The TestOptions class (in the ODataQueryOptions<TestOptions> param) is currently an empty class:

public class TestOptions
{
}

I've also added

public static class WebApiConfig
{
    public static void Register(HttpConfiguration config)
    {
        // existing code

        config.AddODataQueryFilter();
    }
}

However, upon calling this from a REST API client...

{
"Message": "An error has occurred.",
"ExceptionMessage": "No non-OData HTTP route registered.",
"ExceptionType": "System.InvalidOperationException",
"StackTrace": " ... "
}

What have I missed? I would have thought that I might have to register OData-enabled methods in the global.asax or similar, but the exception implies that the problem is with non-OData methods, but all the other methods still return as expected (i.e., without any OData involvement).

awj
  • 7,482
  • 10
  • 66
  • 120
  • I think that the error message is somewhat misleading here and that you need to register the OData route. Following the instructions here should get you started: https://learn.microsoft.com/en-us/aspnet/web-api/overview/odata-support-in-aspnet-web-api/odata-v4/create-an-odata-v4-endpoint – TomDoesCode Jul 03 '17 at 14:03
  • That example is built upon an EF model and like I said in the OP, I don't have an underlying model. The example registers the OData endpoint with specific details of its model: https://learn.microsoft.com/en-us/aspnet/web-api/overview/odata-support-in-aspnet-web-api/odata-v4/create-an-odata-v4-endpoint#configure-the-odata-endpoint – awj Jul 03 '17 at 14:07
  • OK, you still need to build a model though, even if it isn't through entity framework. Will your model be defined at compile time? I.E will the types and properties that are exposed change? – TomDoesCode Jul 03 '17 at 14:10
  • I don't think those objects and properties will change after compile time, but what I'm looking to achieve is a public interface which meets OData standards but without an underlying model; I simply want the OData library to parse the request and pass a populated ODataQueryOptions object detailing the properties, conditions, sorting, paging, _etc_ params. Is this not possible? This suggests it _is_ possible: http://www.ben-morris.com/parsing-odata-queries-decoupled-data-entities-webapi/ – awj Jul 03 '17 at 14:15
  • That article is suggesting that you don't need to expose `IQueryable` which is correct but you still need to define an OData model. In that article, it is the `Product` class that is the basis for the OData model. For that example, you would need to add the `Product` class to the ODataModelBuilder. In the first link I sent, look at the "Configure the OData Endpoint" section, I think that is the missing link here – TomDoesCode Jul 03 '17 at 14:19

2 Answers2

9

Actually, this works perfectly without EntityDate or any other model setup. You just need a List<Poco.Language> that you can convert with .AsQueryable() and off you go.

[Route(""), HttpGet]
public IHttpActionResult  Get(ODataQueryOptions<Poco.Language> queryOptions)
{          
    return Ok(queryOptions.ApplyTo(_repository.GetAll().AsQueryable()));
}

Above controller can be called with all types of OData query options, normal routes and no setup in the WebApiConfig.

Poco.Language is just a plain C# POCO class.

janw
  • 8,758
  • 11
  • 40
  • 62
Remy
  • 12,555
  • 14
  • 64
  • 104
  • 1
    In case you still having the issue, depending upon the OData library version, also add `config.EnableDependencyInjection();` in your webapi configurations. See https://github.com/OData/WebApi/issues/816 – Chandermani Dec 06 '17 at 15:35
  • Where did you take `Poco.Language` from? I mean it wasn't in the question. The answer seems out of context anyway. – Konrad Aug 17 '18 at 10:19
  • That was just an internal data structure from my company. But the example shows that OData also works without EntityData. – Remy Aug 21 '18 at 15:31
  • Hi Remy what do mean it works without Entity data? I have a similar question, trying to get it working without entity type https://stackoverflow.com/q/62650392/12425844 –  Jul 01 '20 at 00:31
  • If I don't use entity type I get a bad request error etc –  Jul 01 '20 at 00:32
  • Yes, it also works with a normal list too. Obviously a bit limited, but sort and filter work. Example: queryOptions.ApplyTo(auditLogEntries.AsQueryable()) where queryOptions are the OData command from the controller: ODataQueryOptions queryOptions – Remy Jul 02 '20 at 14:20
  • (Late addition :) ) Doesn't this defeat the whole purpose of being Queryable though? If your GetAll() is huge, then you're materializing everything in your repo, and only then filtering it in-memory, instead of passing the filter options down to the (SQL?) query? – oflahero Apr 27 '21 at 10:27
  • With my implementation above, absolutely. We're using it for smaller lists and often we do have them in cache already. – Remy Apr 28 '21 at 07:39
0

Did you add this to your Startup?

public void Configuration(IAppBuilder appBuilder)
        {
            // Set up server configuration
            var config = new HttpConfiguration();
            config.Routes.MapODataRoute(routeName: "OData", routePrefix: "odata", model: GetEdmModel());
            appBuilder.UseWebApi(config);
        }

        private IEdmModel GetEdmModel()
        {
            var modelBuilder = new ODataConventionModelBuilder();
            modelBuilder.EntitySet<Customer>("customer");
            modelBuilder.EntitySet<Order>("order");
            modelBuilder.EntitySet<Customer>("response");
            return modelBuilder.GetEdmModel();
        }

I am trying to do the same thing you are, but with a .Net Core Web Api. I downloaded the samples from https://github.com/OData/ODataSamples and he has a working project that does exactly what we're trying to do. Look at ODataQueryableSample.csproj. It doesn't use EntityFramework either, just creates a list in memory.

Also, I don't think you need both the [EnableQuery] attribute and ODataQueryOptions parameter - the sample gives an example for both and doesn't use them together.

I haven't gotten this working with my project either, but I have seen it working on my PC so I believe its just a configuration thing (for instance, my Startup uses IApplicationBuilder instead of IAppBuilder). You might also try to hit it from a browser to get a better exception message.

BLogan
  • 836
  • 8
  • 10