Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Issue with NeinLinq.EntityFramework, Linq.Count() method #39

Closed
TheHACKATHON opened this issue Apr 19, 2023 · 6 comments
Closed

Issue with NeinLinq.EntityFramework, Linq.Count() method #39

TheHACKATHON opened this issue Apr 19, 2023 · 6 comments
Labels

Comments

@TheHACKATHON
Copy link

TheHACKATHON commented Apr 19, 2023

Hi, I have a problem with the Count method in NeinLinq.EntityFramework

Framework: net4.8
EntityFramework: 6.4.4
NeinLinq: 6.1.1
NeinLinq.EntityFramework: 6.1.1

The code looks like this:
I have a query provider:

public interface IQueryProvider<DbContext, TParam, TResult>
{
    [InjectLambda]
    IQueryable<TResult> GetQuery(DbContext context, TParam query);
}

public class GetTodoItemsQueryProvider : IQueryProvider<ApplicationDbContext, int, TodoItem>
{
    [InjectLambda(nameof(GetQueryExpression))]
    public IQueryable<TodoItem> GetQuery(ApplicationDbContext context, int query)
    {
        throw new NotImplementedException();
    }

    public Expression<Func<ApplicationDbContext, int, IQueryable<TodoItem>>> GetQueryExpression()
    {
        return (context, query) => from todoItem in context.Set<TodoItem>()
            where todoItem.ListId == query
            select todoItem;
    }
}

and code which query database and calls this query provider

[HttpGet("test")]
public ActionResult Test()
{
    List<TodoItem> result = (from list in _dbContext.Set<TodoList>().ToDbInjectable()
        let item = _queryProvider.GetQuery(_dbContext, list.Id).FirstOrDefault()
        select item).ToList();

    int count = (from list in _dbContext.Set<TodoList>().ToDbInjectable()
        let item = _queryProvider.GetQuery(_dbContext, list.Id).FirstOrDefault()
        select item).Count();

  return true;
}

the first request(with ToList) works without any problems
on second request I got an exception

LINQ to Entities does not recognize the method 'System.Data.Entity.DbSet`1[TodoList] Set[TodoList]()' method, and this method cannot be translated into a store expression.

Same code on NeinLinq.EntityFrameworkCore works fine, pls help

@axelheer
Copy link
Owner

That's weird, since the resulting query expressions look almost the same...

List query:

.Call System.Linq.Queryable.Select(
    .Call System.Linq.Queryable.Select(
        .Call .Constant<System.Data.Entity.Core.Objects.ObjectQuery`1[NeinLinq.Tests.TodoList]>(System.Data.Entity.Core.Objects.ObjectQuery`1[NeinLinq.Tests.TodoList]).MergeAs(.Constant<System.Data.Entity.Core.Objects.MergeOption>(AppendOnly))
        ,
        '(.Lambda #Lambda1<System.Func`2[NeinLinq.Tests.TodoList,<>f__AnonymousType3`2[NeinLinq.Tests.TodoList,NeinLinq.Tests.TodoItem]]>))
    ,
    '(.Lambda #Lambda2<System.Func`2[<>f__AnonymousType3`2[NeinLinq.Tests.TodoList,NeinLinq.Tests.TodoItem],NeinLinq.Tests.TodoItem]>))

.Lambda #Lambda1<System.Func`2[NeinLinq.Tests.TodoList,<>f__AnonymousType3`2[NeinLinq.Tests.TodoList,NeinLinq.Tests.TodoItem]]>(NeinLinq.Tests.TodoList $list)
{
    .New <>f__AnonymousType3`2[NeinLinq.Tests.TodoList,NeinLinq.Tests.TodoItem](
        $list,
        .Call System.Linq.Queryable.FirstOrDefault(.Call System.Linq.Queryable.Where(
                .Call (.Constant<NeinLinq.Tests.SomeTest>(NeinLinq.Tests.SomeTest)._dbContext).Set(),
                '(.Lambda #Lambda3<System.Func`2[NeinLinq.Tests.TodoItem,System.Boolean]>))))
}

.Lambda #Lambda2<System.Func`2[<>f__AnonymousType3`2[NeinLinq.Tests.TodoList,NeinLinq.Tests.TodoItem],NeinLinq.Tests.TodoItem]>(<>f__AnonymousType3`2[NeinLinq.Tests.TodoList,NeinLinq.Tests.TodoItem] $<>h__TransparentIdentifier0)
{
    $<>h__TransparentIdentifier0.item
}

.Lambda #Lambda3<System.Func`2[NeinLinq.Tests.TodoItem,System.Boolean]>(NeinLinq.Tests.TodoItem $todoItem) {
    $todoItem.ListId == $list.Id
}

Count query:

.Call System.Linq.Queryable.Count(.Call System.Linq.Queryable.Select(
        .Call System.Linq.Queryable.Select(
            .Call .Constant<System.Data.Entity.Core.Objects.ObjectQuery`1[NeinLinq.Tests.TodoList]>(System.Data.Entity.Core.Objects.ObjectQuery`1[NeinLinq.Tests.TodoList]).MergeAs(.Constant<System.Data.Entity.Core.Objects.MergeOption>(AppendOnly))
            ,
            '(.Lambda #Lambda1<System.Func`2[NeinLinq.Tests.TodoList,<>f__AnonymousType3`2[NeinLinq.Tests.TodoList,NeinLinq.Tests.TodoItem]]>))
        ,
        '(.Lambda #Lambda2<System.Func`2[<>f__AnonymousType3`2[NeinLinq.Tests.TodoList,NeinLinq.Tests.TodoItem],NeinLinq.Tests.TodoItem]>))
)

.Lambda #Lambda1<System.Func`2[NeinLinq.Tests.TodoList,<>f__AnonymousType3`2[NeinLinq.Tests.TodoList,NeinLinq.Tests.TodoItem]]>(NeinLinq.Tests.TodoList $list)
{
    .New <>f__AnonymousType3`2[NeinLinq.Tests.TodoList,NeinLinq.Tests.TodoItem](
        $list,
        .Call System.Linq.Queryable.FirstOrDefault(.Call System.Linq.Queryable.Where(
                .Call (.Constant<NeinLinq.Tests.SomeTest>(NeinLinq.Tests.SomeTest)._dbContext).Set(),
                '(.Lambda #Lambda3<System.Func`2[NeinLinq.Tests.TodoItem,System.Boolean]>))))
}

.Lambda #Lambda2<System.Func`2[<>f__AnonymousType3`2[NeinLinq.Tests.TodoList,NeinLinq.Tests.TodoItem],NeinLinq.Tests.TodoItem]>(<>f__AnonymousType3`2[NeinLinq.Tests.TodoList,NeinLinq.Tests.TodoItem] $<>h__TransparentIdentifier0)
{
    $<>h__TransparentIdentifier0.item
}

.Lambda #Lambda3<System.Func`2[NeinLinq.Tests.TodoItem,System.Boolean]>(NeinLinq.Tests.TodoItem $todoItem) {
    $todoItem.ListId == $list.Id
}

The processing of these expressions is completely different between Entity Framework and Entity Framework Core. Happily the newer Framework understands this anyway.

Nevertheless, NeinLinq does some "clean-up" of the query's expression, which does handle properties but not method calls at the moment. Thus, this should work for you already:

public class GetTodoItemsQueryProvider : IQueryProvider<ApplicationDbContext, int, TodoItem>
{
    [InjectLambda(nameof(GetQueryExpression))]
    public IQueryable<TodoItem> GetQuery(ApplicationDbContext context, int query)
    {
        throw new NotImplementedException();
    }

    public Expression<Func<ApplicationDbContext, int, IQueryable<TodoItem>>> GetQueryExpression()
    {
        return (context, query) => from todoItem in context.TodoItems
                                   where todoItem.ListId == query
                                   select todoItem;
    }
}

I can dig a little deeper to make context.Set<TodoItem>() work as well. Please let me know, if context.TodoItems is sufficient anyway.

Just for further reference, the test code I used in order to reproduce this:

using System.Data.Entity;
using Xunit;

namespace NeinLinq.Tests;

public class SomeTest
{
    private readonly ApplicationDbContext _dbContext;
    private readonly IQueryProvider<ApplicationDbContext, int, TodoItem> _queryProvider;

    public SomeTest()
    {
        _dbContext = new ApplicationDbContext("Data Source=(localdb)\\MSSQLLocalDB;Initial Catalog=SomeTest;Integrated Security=true;");
        _dbContext.TodoLists.Add(
            new TodoList
            {
                Items =
                {
                    new TodoItem()
                }
            }
        );
        _dbContext.SaveChanges();

        _queryProvider = new GetTodoItemsQueryProvider();
    }

    [Fact]
    public void Test_WithLambdaInjection()
    {
        var query = from list in _dbContext.Set<TodoList>().ToDbInjectable()
                    let item = _queryProvider.GetQuery(_dbContext, list.Id).FirstOrDefault()
                    select item;

        var result = query.ToList();

        Assert.NotEmpty(result);

        int count = query.Count();

        Assert.NotEqual(0, count);
    }

    [Fact]
    public void Test_WithoutLambdaInjection()
    {
        var query = from list in _dbContext.Set<TodoList>()
                    let item = (from todoItem in _dbContext.Set<TodoItem>()
                                where todoItem.ListId == list.Id
                                select todoItem).FirstOrDefault()
                    select item;

        var result = query.ToList();

        Assert.NotEmpty(result);

        int count = query.Count();

        Assert.NotEqual(0, count);
    }
}

public class TodoList
{
    public int Id { get; set; }

    public virtual ICollection<TodoItem> Items { get; } = new List<TodoItem>();
}

public class TodoItem
{
    public int Id { get; set; }

    public int ListId { get; set; }

    public virtual TodoList? List { get; set; }
}

public class ApplicationDbContext : DbContext
{
    public DbSet<TodoList> TodoLists { get; }

    public DbSet<TodoItem> TodoItems { get; }

    public ApplicationDbContext(string nameOrConnectionString) : base(nameOrConnectionString)
    {
        TodoLists = Set<TodoList>();
        TodoItems = Set<TodoItem>();
    }
}

public interface IQueryProvider<DbContext, TParam, TResult>
{
    [InjectLambda]
    IQueryable<TResult> GetQuery(DbContext context, TParam query);
}

public class GetTodoItemsQueryProvider : IQueryProvider<ApplicationDbContext, int, TodoItem>
{
    [InjectLambda(nameof(GetQueryExpression))]
    public IQueryable<TodoItem> GetQuery(ApplicationDbContext context, int query)
    {
        throw new NotImplementedException();
    }

    public Expression<Func<ApplicationDbContext, int, IQueryable<TodoItem>>> GetQueryExpression()
    {
        return (context, query) => from todoItem in context.TodoItems
                                   where todoItem.ListId == query
                                   select todoItem;
    }
}

@axelheer axelheer added the bug label Apr 25, 2023
@TheHACKATHON
Copy link
Author

TheHACKATHON commented Apr 25, 2023

Thanks for your research.
сontext.TodoItems works as a workaround for now, but I would like to use Set<>, because in most code we inject abstract DbContext and don't have access to properties like TodoItems, and unit tests now based on Set method

@axelheer
Copy link
Owner

Okay, after some further digging, it seems, that Entity Framework needs some processing, in order to understand, what you are doing here. This is currently implemented by an internal EF class DbQueryVisitor, which I now call using some Reflection.

I'm not quite happy with this "Hack", but please try Version 6.2.0 as it will be available as a Preview here:
https://github.com/axelheer/nein-linq/pkgs/nuget/NeinLinq.EntityFramework

For implementation details: 66817a2

@TheHACKATHON
Copy link
Author

For some reason, I can't see this prerelease on NuGet and can't install it via command
NU1102: Unable to find package NeinLinq.EntityFramework with version (>= 6.2.0-preview.186)

@axelheer
Copy link
Owner

axelheer commented May 1, 2023

It isn't on NuGet, the previews are on GitHub only.

See Installing a package for details

@TheHACKATHON
Copy link
Author

Thank you. I checked, for my cases the problem is fixed

@axelheer axelheer closed this as completed May 1, 2023
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Projects
None yet
Development

No branches or pull requests

2 participants