Modifying ODataQueryOptions on the fly

ODATA provides several methods to validate the query options a client can specify. However it is not too easy to modify them, which might become necessary when we only want to allow certain options in relation to one another. In this article I will show how this can be done on a per request level or via an attribute.

The main problem is that all of the ODATA validation options we can override or extend (such as EnableQueryAttribute.ValidateQuery) only jump in after the request has been processed and data is about to leave the controller method. However two approaches are possible:

  1. Replacing ODataQueryOptions inside the respective Controller method
  2. Replacing ODataQueryOptions inside HttpActionContext.ActionArguments

Replacing ODataQueryOptions inside the respective Controller method

A typical ODATA method for returning an entity set has a signature similar to this:

public IHttpActionResult Get(ODataQueryOptions<TEntity> queryOptions);

Instantiating ODataQueryOptions (generic or non-generic) requires an ODataQueryContext and an HttpRequestMessage. Both of these are available as properties on the original ODataQueryOptions. So in order to override our query options we only have to split the incoming RequestUri and create new options from it. In the following example will strip off any $expand options that contain a property called Details.

public static ODataQueryOptions RemoveExpandDetails(ODataQueryOptions queryOptions)
{
  const char QUERY_OPTIONS_PREFIX = '?';
  const char QUERY_OPTIONS_SEPARATOR = '&';
  const char QUERY_OPTIONS_DELIMITER = '=';
  const string QUERY_OPTION_EXPAND = "$expand";
  const char QUERY_OPTION_EXPAND_DELIMITER = ',';

  if (!queryOptions.SelectExpand.RawExpand.Contains(nameof(PublicEntity.Details)))
  {
    return queryOptions;
  }

  var uri = queryOptions.Request.RequestUri;
  var queryParameters = uri.Query
    .TrimStart(QUERY_OPTIONS_PREFIX)
    .Split(QUERY_OPTIONS_SEPARATOR)
    .ToDictionary(e => e.Split(QUERY_OPTIONS_DELIMITER).FirstOrDefault(), e => e.Split(QUERY_OPTIONS_DELIMITER).LastOrDefault());
  var newQueryOptions = new StringBuilder();
  queryParameters.ForEach(e =>
  {
    if (e.Key != QUERY_OPTION_EXPAND)
    {
      newQueryOptions.Append(QUERY_OPTIONS_SEPARATOR);
      newQueryOptions.Append(e.Key);
      newQueryOptions.Append(QUERY_OPTIONS_DELIMITER);
      newQueryOptions.Append(e.Value);
      return;
    }

    var value = e.Value.Replace(nameof(PublicEntity.Details), string.Empty).Trim(QUERY_OPTION_EXPAND_DELIMITER);
    if (string.IsNullOrWhiteSpace(value))
    {
      return;
    }

    newQueryOptions.Append(QUERY_OPTIONS_SEPARATOR);
    newQueryOptions.Append(e.Key);
    newQueryOptions.Append(QUERY_OPTIONS_DELIMITER);
    newQueryOptions.Append(value);
  });
  queryOptions.Request.RequestUri =
    new Uri($"{uri.Scheme}://{uri.Authority}{uri.AbsolutePath}{QUERY_OPTIONS_PREFIX}{newQueryOptions}");

  var newOdataQueryOptions = (ODataQueryOptions) Activator.CreateInstance(queryOptions.GetType(), queryOptions.Context, queryOptions.Request);

  return newOdataQueryOptions;
}

Now can call this method in our Get method:

public IHttpActionResult Get(ODataQueryOptions<TEntity> queryOptions)
{
  QueryOptionsUtilities.RemoveExpandDetails(queryOptions);

  // ... 
}

Replacing ODataQueryOptions inside HttpActionContext.ActionArguments

Instead of rewriting query options in every requeest we can also create a custom EnableQuery attribute and apply it to our controller methods. The problem here is that the ValidateQuery method which contains a parameter with ODataQueryOptions is only called after the request has been processed by the underlying LINQ provider. However, the very first method called in our query attribute is OnActionExecuting and contains an HttpActionContext. Inside this context we can access ActionArguments which contain the query options available under the queryOptions key.
With this we can extract the options an re-attach our modified options:

public class AppclusiveEnableQueryAttribute : EnableQueryAttribute
{
  private const string QUERY_OPTIONS_KEY = "queryOptions";

  public override void OnActionExecuting(HttpActionContext actionContext)
  {
    Contract.Requires(actionContext.ActionArguments.ContainsKey(QUERY_OPTIONS_KEY));

    var queryOptions = (ODataQueryOptions) actionContext.ActionArguments[QUERY_OPTIONS_KEY];
    queryOptions = QueryOptionsUtilities.RemoveExpandDetails(queryOptions);
    actionContext.ActionArguments[QUERY_OPTIONS_KEY] = queryOptions;

    base.OnActionExecuting(actionContext);
  }
}

Now with these two options we can modify incoming query options in a very granular way and modify them on the fly.

[UPDATE]

A complete version with more flexibility of invocation can be found here: https://gist.github.com/dfch/12697689ed0ed1c855828acb85294342

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s

%d bloggers like this: