OData / AspNetCoreOData

ASP.NET Core OData: A server library built upon ODataLib and ASP.NET Core
Other
457 stars 158 forks source link

Executing OData filter server side does not work #852

Open nathanvj opened 1 year ago

nathanvj commented 1 year ago

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

Airex commented 1 year ago

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

nathanvj commented 1 year ago

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.

julealgon commented 1 year ago

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:

lisicase commented 1 year ago

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

nathanvj commented 1 year ago

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:

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.

julealgon commented 1 year ago

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.

lisicase commented 1 year ago

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.

robertmclaws commented 1 year ago

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.

nathanvj commented 10 months ago

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?

julealgon commented 10 months ago

@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.

diegomodolo commented 5 months ago

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