Matias Cicero
Matias Cicero

Reputation: 26331

ASP.NET Web Api complex query parameters

I have a Web Api endpoint which is currently called like this:

http://api.example.com/scenes?creationDate=1440091949

I am trying to support more complex queries such as:

http://api.example.com/scenes?creationDate.lt=1440091949

Note the .lt suffix. This would let the user to list all the scenes in which creationDate is less than (lt) than 1440091949.

For this, I had to create my own mapper function which would map each query parameter to a query model's properties and store each query operation (lt, gt, eq, etc...) in a dictionary of operations:

protected QueryData MapToQueryData(IEnumerable<KeyValuePair<string, string>> queryParameters)
{
     QueryData queryData = new QueryData();

     foreach(var queryParam in queryParameters)
     {
          string[] segments = queryParam.Key.Split('.'); // ["creationDate", "lt"]
          if(!segments.Any()) continue;

          PropertyInfo queryProperty = _reflection.Properties.GetProperty<QueryData>(segments.First()); // "CreationDate" Property
          if(queryProperty == null) continue;

          object value = _reflection.Conversion.ConvertTo(queryParam.Value, property.PropertyType); // Convert 1440091949 to long
          if(value == null) continue;

          _reflection.Properties.SetPropertyValue(queryData, property, value); // Set queryData.CreationDate = 1440091949

          if(segments.Length < 2) continue;

          PropertyInfo mapProperty = _reflection.Properties.GetPropertiesWithAttribute<QueryData, OperationMapAttribute>().FirstOrDefault(); // Identify Property annotated with [OperationMap]
          if(mapProperty == null) continue;

          Dictionary<string, string> operationMap = _reflection.Properties.GetPropertyValue(queryData, mapProperty) as Dictionary<string, string>(); // Get dictionary from Map property
          if(operationMap == null) continue;

          if(!operationMap.ContainsKey(property.Name))
                 operationMap.Add(property.Name, segments.Last()); // "creationDate" -> "lt"
          else
                 operationMap[property.Name] = segments.Last();

          _reflection.Properties.SetPropertyValue(queryData, mapProperty, operationMap); // Set Map property of QueryData to the updated dictionary
     }

     return queryData;
}

I know there exists an automatic mapping provided by ASP.NET Web Api if one decides to use the [FromUri] attribute, but that would work if I had simple query parameters such as creationDate=1440091949, but will not work if I send a query parameter such as creationDate.lt=1440091949.

Is there something built in, in the Web APi engine to handle these types of query parameters? I have seen them on a lot of web services, so they are pretty common for ordering or doing complex queries.

Upvotes: 6

Views: 4683

Answers (3)

user4843530
user4843530

Reputation:

You could put the operator into the parameter value:

http://api.example.com/scenes?creationDateTest=lt.1440091949

then test the beginning part of the string you get for the creationDate parameter to see if it contains a valid operator. Only be careful, I would still parameterize the actual value. So convert this into

string cmd = "select .... from ... where creationdate < @creationDate";

and feed in the 1440091949 as parameter @creationDate.

I just tried this on one of my web forms, and it works fine. You get those parameters as strings anyway, so it works, and it does not even feel like a kludge. And I am oversensitive to anything that smells of kludge.

PS: notice how I changed the name of the parameter in the query string from creationDate to creationDateTest.

Upvotes: 1

Riad Baghbanli
Riad Baghbanli

Reputation: 3319

Will the life on earth end if you have it this way?

http://api.example.com/scenes?cd=1440091949&cdop=lt&ep=45&epop=gt
http://api.example.com/scenes?cd=1440091949&cdop=gt&ep=45&epop=lt
http://api.example.com/scenes?cd=1440091949&cdop=eq&ep=45&epop=eq

No complicated mapping required in this case.

Upvotes: 0

ConfusingBoat
ConfusingBoat

Reputation: 66

Have you tried using something like the OData query syntax?

http://blogs.msdn.com/b/martinkearn/archive/2015/03/10/using-odata-query-syntax-with-web-api.aspx

From the aforementioned...

What is OData query syntax?

OData query syntax is a standard way of querying RESTful APIs using a pre-defined syntax that allows the calling client to define the sort order, filter parameters and pages of data that is returned.

The full syntax can be found in section 5 of the OData Version 4.0 Part 2 specification but here are some quick examples of how the syntax is used:

  • .../host/service/Products?$filter=Name eq 'Milk': Returns all products where the name is equal to 'Milk'
  • .../host/service/Products?$filter=Name eq 'Milk' and Price lt 2.55: Returns all products where the name is equal to 'Milk' and the price is less than 2.55
  • .../host/service/Products?$top=10&$skip10: Returns items 11>21. The $top system query option requests the number of items in the queried collection to be included in the result. The $skip query option requests the number of items in the queried collection that are to be skipped and not included in the result
  • .../host/service/Products?$orderby=Name desc,Id: Returns all products ordered by the Name in descending order, then ordered by Id
  • .../host/service/Products?$filter=Enabled eq true: Returns all products where the Enabled field (which is a boolean) is true
  • .../host/service/Products?$filter=substringof('Martin', Name): Returns all products the Name field contains the text Martin

How to use OData query syntax in ASP.net Web API?

This is where things get really awesome - it is super, super easy to change a regular Web API controller to support OData query syntax - once you know how to do this, you'll never not do it!

If we assume we are starting from a regular Web API project (File > New Project > ASP.net Web Application > Web API), you first need to add a scaffolded controller which you can do by following these steps:

  1. Add a model class (I'm using the typical 'Person' class with Id, First Name, Last Name, Age etc)
  2. Right-click the 'controllers' folder > Add > New Scaffolded item > Web API 2 Controller with actions, using Entity Framework > Model = Person

NOTE: You do not need to choose the 'Web API 2 OData Controller...' option, you can add the query syntax to any controller

Once you've setup your controller, you should end up with a simple controller which has a default GET action that looks a little like this:

// GET: api/People
public IQueryable<Person> GetPeople()
{
    return db.People;
}

This action will simply return all the rows in the 'People' table of the database with no ability to filter, sort etc. To add full OData query support, you need to make a few changes:

  1. Add the Microsoft.Aspnet.OData package via nugget ... simply pop this into you 'Package Manager Console': Install-Package Microsoft.AspNet.Odata
  2. Add this using statement: using System.Web.OData;
  3. Add the[EnableQueryAttribute]attribute to your GetPeople action.
  4. Add AsQueryable(); to your return line so it looks like this: return db.People.AsQueryable();

Your finished code will look something like this:

// GET: api/People
[EnableQueryAttribute]
public IQueryable<Person> GetPeople()
{
    return db.People.AsQueryable();
}

That is all there is to it! .... now you have this setup, you can use the full OData syntax to query sort etc without any extra code.

Upvotes: 5

Related Questions