AspNetCoreOData icon indicating copy to clipboard operation
AspNetCoreOData copied to clipboard

Executing OData filter server side does not work

Open nathanvj opened this issue 1 year ago • 11 comments

Assemblies affected I'm using AutoMapper.AspNetCore.OData.EFCore 4.0.0 with Microsoft.AspNetCore.OData 8.0.6.

Describe the bug My use case is as follows: In the frontend application the user can build a complex OData filter which we then save in our database. An AWS lambda function then needs to query the dataset using this saved filter (for example: (assigneeId in ('8201014','3038351')) and (user/status eq 'active'))

I've been looking into instantiating ODataQueryOptions like described here, but it doesn't seem to work.

This is what I have right now. The filter is added to the query collection of the request, but somehow they aren't applied to the ODataQueryOptions (filter is still null).

    public async Task<IQueryable<TContract>> GetQueryableFromString<TEntity, TContract>(
        string entitySetName,
        string odataQuery,
        Expression<Func<TEntity, bool>> filter = null,
        QueryStrategy queryStrategy = QueryStrategy.AllFilters)
             where TEntity : class
             where TContract : class
    {
        IEdmModel model = ODataEdmModelProvider.BuildEdmModel();
        IEdmEntitySet entitySet = model.EntityContainer.FindEntitySet(entitySetName);
        if (entitySet is null)
        {
            throw new Exception();
        }

        Type clrType = typeof(TContract);
        List<ODataPathSegment> segments = new() 
        { 
            new EntitySetSegment(entitySet),
        };
        ODataPath path = new(segments);
        ODataQueryContext context = new(model, clrType, path);

        HttpRequest request = new DefaultHttpContext().Request;
        Dictionary<string, StringValues> dictionary = new()
        {
            { "filter", new StringValues(odataQuery) }
        };
        request.Query = new QueryCollection(dictionary);
        ODataQueryOptions<TContract> options = new(context, request);

        DbSet<TEntity> set = _dbSetProvider.DbSetOf<TEntity>();

        IQueryable<TEntity> query = filter is null ? set.AsQueryable() : set.Where(filter);
        
        return await query.GetQueryAsync(_mapper, options, _querySettings);
    }

Reproduce steps N/A

Data Model N/A

EDM (CSDL) Model N/A

Request/Response N/A

Expected behavior N/A

Screenshots N/A

Additional context N/A

nathanvj avatar Mar 03 '23 23:03 nathanvj

{ "filter", new StringValues(odataQuery) } should not it be $filter?

Airex avatar Mar 03 '23 23:03 Airex

I had such little faith it was gonna work (because I spend a lot of time even coming this far) that I didn't scan for obvious mistakes like that. But you just saved me. It does now work. Very glad. Thank you.

I still feel like there should be an easier way to do this instead instead of instantiating a HttpRequest, but it will suffice for now.

nathanvj avatar Mar 04 '23 00:03 nathanvj

I still feel like there should be an easier way to do this instead instead of instantiating a HttpRequest, but it will suffice for now.

There will be, eventually. Microsoft is working on a so-called "next gen OData" that is supposed to be transport-agnostic. One of the main features there is that you can apply queries like that without an HttpContext at all.

You can find more details here:

  • https://github.com/OData/OData.Neo

julealgon avatar Mar 06 '23 13:03 julealgon

In addition to OData Neo mentioned above, we're also looking at breakout out the query options from the existing stack; please stay tuned!

lisicase avatar Mar 12 '23 23:03 lisicase

Exciting! Is there an ETA? And will it be hard to migrate?

I have one more question, that is related to the "issue" I described above. As you can see in my code I'm using AutoMapper.AspNetCore.OData.EFCore 4.0.0, as to not expose my entities.

When I'm calling query.GetQueryAsync() I'm passing in my mapping configuration (_mapper). However I'm running into two issues:

  • When I enable ExplicitExpansion in the mapping configuration (.ForAllMembers(e => e.ExplicitExpansion())), the query can not compile if the filter uses a child entity. For example if the filter is (user/status eq 'active'), it won't work because the user is null in the projection (I assume).

  • When I set ExplicitExpansion to false, the query does work, and when I call query.Count() for example it returns the correct count (based on the filters). However when I call query.ToList(), the query never completes and my application memory keeps increasing. I assume there is some recursive projection going on, but I really don't know to be honest.

I would be grateful if anyone has any input on how to approach this. If needed I can try to reproduce it in a small test project tomorrow and share it.

nathanvj avatar Mar 13 '23 00:03 nathanvj

I have one more question, that is related to the "issue" I described above. As you can see in my code I'm using AutoMapper.AspNetCore.OData.EFCore 4.0.0, as to not expose my entities. ... I would be grateful if anyone has any input on how to approach this. If needed I can try to reproduce it in a small test project tomorrow and share it.

@nathanvj for these, I'd suggest asking the question to Jimmy over at the appropriate AutoMapper repo, or in stackoverflow by marking it with the "AutoMapper" tag.

julealgon avatar Mar 13 '23 12:03 julealgon

Exciting! Is there an ETA? And will it be hard to migrate?

@nathanvj We're currently investigating this as we're considering other changes for the upcoming release of OData Library (ODL) 8.0, so although I don't have an ETA for you at the moment, it'll likely follow a similar timeline. Similarly goes with details on migration since our approach itself is still under development, but although this would be a breaking change (as would the other ODL 8.0 changes), we're trying to design this in a way that doesn't require significant adjustment.

It's also possible that we may reach out in the future -- here and/or in the GitHub discussions -- for feedback after an internal design review.

lisicase avatar Mar 29 '23 23:03 lisicase

Hello! It looks like what you're really looking for is Restier, a Microsoft service library built on OData that provides interceptors for filtering data.

The samples under SRC will help you understand how it works. Happy to help any way I can.

robertmclaws avatar Mar 30 '23 18:03 robertmclaws

Sorry to reply to this older thread, but I've ran into another issue when trying to use the $search parameter server side, like this:

if (!string.IsNullOrWhiteSpace(search))
{
     dictionary.Add("$search", new StringValues("\"" + search + "\""));
}

It gives me the exception:

Unable to cast object of type 'System.Linq.Expressions.ConstantExpression' to type 'System.Linq.Expressions.MethodCallExpression'.

I've compared the ODataQueryOptions object when simply calling the controller endpoint with the one instantiate by myself and it seems that the reason this exception is thrown, is because the property ODataQueryOptions.Search.Context.RequestContainer is null. However - this is a readonly property.

Does anyone have any suggestions on how I might be able to fix this?

nathanvj avatar Jan 02 '24 23:01 nathanvj

@nathanvj , are you attempting to call one controller action from another controller action? If so, I'd suggest not doing that and instead extracting the logic you want to call and call that logic from both places.

If that's not what you are doing, it would be nice if you could provide a small repro.

julealgon avatar Jan 16 '24 21:01 julealgon

@nathanvj , I am having the same issue. Did you manage to fix it?

diegomodolo avatar May 22 '24 19:05 diegomodolo