OData V4 modify $filter on server side

18,740

Solution 1

Remove [EnableQuery] attribute, your scenario should work, because after using this attribute, OData/WebApi will apply your original query option after you return data in controller, if you already apply in your controller method, then you shouldn't use that attribute.

But if your query option contains $select, those code are not working because the result's type is not Product, we use a wrapper to represent the result of $select, so I suggest you use try this:

Make a customized EnableQueryAttribute

public class MyEnableQueryAttribute : EnableQueryAttribute
{
    public override IQueryable ApplyQuery(IQueryable queryable, ODataQueryOptions queryOptions)
    {
        if (queryOptions.Filter != null)
        {
            queryOptions.ApplyTo(queryable);
            var url = queryOptions.Request.RequestUri.AbsoluteUri;

            url = url.Replace("$filter=Id%20eq%201", "$filter=Id%20eq%202");
            var req = new HttpRequestMessage(HttpMethod.Get, url);

            queryOptions = new ODataQueryOptions(queryOptions.Context, req);
        }

        return queryOptions.ApplyTo(queryable);
    }
}

Use this attribute in your controller method

[MyEnableQueryAttribute]
public IHttpActionResult Get()
{
    return Ok(_products);
}

Hope this can solve your problem, thanks!

Fan.

Solution 2

In response of @Chris Schaller I post my own solution as below:

public class CustomEnableQueryAttribute : EnableQueryAttribute
{
    public override void OnActionExecuting(HttpActionContext actionContext)
    {
        var url = actionContext.Request.RequestUri.OriginalString;

        //change something in original url, 
        //for example change all A charaters to B charaters,
        //consider decoding url using WebUtility.UrlDecode() if necessary
        var newUrl = ModifyUrl(url); 

        actionContext.Request.RequestUri = new Uri(newUrl);
        base.OnActionExecuting(actionContext);
    }
}
Share:
18,740
goroth
Author by

goroth

Updated on June 16, 2022

Comments

  • goroth
    goroth almost 2 years

    I would like to be able to modify the filter inside the controller and then return the data based on the altered filter.

    So for I have an ODataQueryOptions parameter on the server side that I can use to look at the FilterQueryOption.

    Let's assume the filter is something like this "$filter=ID eq -1" but on the server side if I see "-1" for an ID this tells me that the user wants to select all records.

    I tried to change the "$filter=ID eq -1" to "$filter=ID ne -1" which would give me all by setting the Filter.RawValue but this is read only.
    I tried to create a new FilterQueryOption but this requires a ODataQueryContext and a ODataQueryOptionParser which I can't figure out how to create.

    I then tried to set the Filter = Null and then us the ApplyTo which seems to work when I set a break point in the controller and check this on the immediate window but once it leaves the GET method on the controller then it "reverts" back to what was passed in the URL.

    This article talks about doing something very similar "The best way to modify a WebAPI OData QueryOptions.Filter" but once it leaves the controller GET method then it reverts back to the URL query filter.

    UPDATE WITH SAMPLE CODE

    [EnableQuery]
    [HttpGet]
    public IQueryable<Product> GetProducts(ODataQueryOptions<Product> queryOptions)
    {
        if (queryOptions.Filter != null)
        {
            var url = queryOptions.Request.RequestUri.AbsoluteUri;
            string filter = queryOptions.Filter.RawValue;
    
            url = url.Replace("$filter=ID%20eq%201", "$filter=ID%20eq%202");
            var req = new HttpRequestMessage(HttpMethod.Get, url);
    
            queryOptions = new ODataQueryOptions<Product>(queryOptions.Context, req);
        }
    
        IQueryable query = queryOptions.ApplyTo(db.Products.AsQueryable());
        return query as IQueryable<Product>;
    }
    

    Running this code will not return any product this is because the original query in the URL wanted product 1 and I swapped the ID filter of product 1 with product 2.
    Now if I run SQL Profiler, I can see that it added something like "Select * from Product WHERE ID = 1 AND ID = 2".

    BUT if I try the same thing by replacing the $top then it works fine.

    [EnableQuery]
    [HttpGet]
    public IQueryable<Product> GetProducts(ODataQueryOptions<Product> queryOptions)
    {
        if (queryOptions.Top != null)
        {
            var url = queryOptions.Request.RequestUri.AbsoluteUri;
            string filter = queryOptions.Top.RawValue;
    
            url = url.Replace("$top=2", "$top=1");
            var req = new HttpRequestMessage(HttpMethod.Get, url);
    
            queryOptions = new ODataQueryOptions<Product>(queryOptions.Context, req);
        }
    
        IQueryable query = queryOptions.ApplyTo(db.Products.AsQueryable());
        return query as IQueryable<Product>;
    }
    

    END RESULT
    With Microsoft's help. Here is the final output that supports filter, count, and paging.

    using System.Net.Http;
    using System.Web.OData;
    using System.Web.OData.Extensions;
    using System.Web.OData.Query;
    
    /// <summary>
    /// Used to create custom filters, selects, groupings, ordering, etc...
    /// </summary>
    public class CustomEnableQueryAttribute : EnableQueryAttribute
    {
        public override IQueryable ApplyQuery(IQueryable queryable, ODataQueryOptions queryOptions)
        {
            IQueryable result = default(IQueryable);
    
            // get the original request before the alterations
            HttpRequestMessage originalRequest = queryOptions.Request;
    
            // get the original URL before the alterations
            string url = originalRequest.RequestUri.AbsoluteUri;
    
            // rebuild the URL if it contains a specific filter for "ID = 0" to select all records
            if (queryOptions.Filter != null && url.Contains("$filter=ID%20eq%200")) 
            {
                // apply the new filter
                url = url.Replace("$filter=ID%20eq%200", "$filter=ID%20ne%200");
    
                // build a new request for the filter
                HttpRequestMessage req = new HttpRequestMessage(HttpMethod.Get, url);
    
                // reset the query options with the new request
                queryOptions = new ODataQueryOptions(queryOptions.Context, req);
            }
    
            // set a top filter if one was not supplied
            if (queryOptions.Top == null) 
            {
                // apply the query options with the new top filter
                result = queryOptions.ApplyTo(queryable, new ODataQuerySettings { PageSize = 100 });
            } 
            else 
            {
                // apply any pending information that was not previously applied
                result = queryOptions.ApplyTo(queryable);
            }
    
            // add the NextLink if one exists
            if (queryOptions.Request.ODataProperties().NextLink != null) 
            {
                originalRequest.ODataProperties().NextLink = queryOptions.Request.ODataProperties().NextLink;
            }
            // add the TotalCount if one exists
            if (queryOptions.Request.ODataProperties().TotalCount != null) 
            {
                originalRequest.ODataProperties().TotalCount = queryOptions.Request.ODataProperties().TotalCount;
            }
    
            // return all results
            return result;
        }
    }
    
  • goroth
    goroth over 8 years
    That almost worked. It did indeed alter the $filter command on the server but then it also broke the $select command. I can not longer use $select when a filter is applied. I get error "The EDM instance of type 'Product Nullable=True' is missing the property 'X'" Where 'X' is the property name I was trying to select. If I include all the properties in the $select then it seems to be working fine.
  • goroth
    goroth over 8 years
    This works perfect now even with $select after you added the "ApplyTo" right before the queryOptions get re-initialized. Thanks.
  • Afshar Mohebi
    Afshar Mohebi over 7 years
    Solution does not works for me on OData 5.5.1. Changing URL does not affects.
  • Afshar Mohebi
    Afshar Mohebi over 7 years
    I ended using OnActionExecuting. Changed url in this event and this worked for me.
  • Chris Schaller
    Chris Schaller over 7 years
    Hey @afsharm can you post an example of your OnActionExecuting variant as a solution. I am having issues with supporting StringAsEnumResolver as well as modifying the url in ApplyTo. It seems that custom Uri Resolvers are not re-evaluated correctly when we create the new ODataQueryOptions, I'm hoping that OnActionExecuting might work around my issue.
  • Chris Schaller
    Chris Schaller over 7 years
    Thankyou, your solution doesn't mess around with the ODataQueryOptions which is where many of my issues since ODataLib v6 have manifested. If this works I'll edit your solution with an example of ModifyUrl that would traditionally involve altering ODataQueryOptions.
  • Chris Schaller
    Chris Schaller about 7 years
    Thanks again @afsharm I think in most cases OnActionExecuting is a superior location for forcing or modifying OData $filter parameters. This executes before most of the OData Query analysis so you do not need to mess around with creating a new ODataQueryOptions object with a faked request. Disappointed that I didn't find this out much earlier.
  • manojmore
    manojmore over 6 years
    @Afshar- Thanks for your solution.I am new to OData. I have followed your solution, and I have modified the url inside OnActionExecuting as you have done. But inside controller when I see the value for queryOptions.Filter.RawValue, it shows old filter values. But the queryOptions.Request.RequestUri shows the newly modified url. Can you help?
  • Alberto Rechy
    Alberto Rechy about 5 years
    @manojmore I know this is very late, but for anyone looking for an answer to your question, the way to do it is the following (this code goes right before base.OnActionExecuting(actionContext);): var queryOptions = (ODataQueryOptions)actionContext.ActionArguments['options']; queryOptions.Request.RequestUri = newUrl.Uri; var newOdataQueryOptions = (ODataQueryOptions)Activator.CreateInstance(queryOptions.Get‌​Type(), queryOptions.Context, queryOptions.Request); actionContext.ActionArguments['options'] = newOdataQueryOptions;
  • Admin
    Admin over 3 years
    hi, can someone answer this question for create? stackoverflow.com/questions/63825932/…
  • Lukasz S
    Lukasz S over 2 years
    Hi, has anyone done this for asp.net/odata core version?