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

Query: Allow use of AsQueryable method #6132

Closed
colltoaction opened this issue Jul 21, 2016 · 47 comments
Closed

Query: Allow use of AsQueryable method #6132

colltoaction opened this issue Jul 21, 2016 · 47 comments
Labels
closed-fixed The issue has been fixed and is/will be included in the release indicated by the issue milestone. type-bug
Milestone

Comments

@colltoaction
Copy link
Contributor

colltoaction commented Jul 21, 2016

Update

Modifying the description to track support for AsQueryable in linq queries.
AsQueryable method is required when user wants to pass Expression to linq method which is hanging of an navigation property since navigation is of type IEnumerable. With the relinq fix now we can get parsed query with AsQueryable but we are client eval-ing afterwards. This issue is to track work needed on EF Core side to translate query to server.

Query
var query3 = db.Products.Select(p => p.ProductCategories.AsQueryable().Select(pc => pc.Category).Where(Category.IsGenre)).ToList();

QueryExecution:

dbug: Microsoft.EntityFrameworkCore.Query[10104]
      Optimized query model: 
      'from Product p in DbSet<Product>
      select 
          from ProductCategory pc in 
              (from ProductCategory <generated>_1 in DbSet<ProductCategory>
              where Property([p], "ProductId") ?= Property([<generated>_1], "ProductId")
              select [<generated>_1]).AsQueryable()
          join Category pc.Category in DbSet<Category>
          on Property([pc], "CategoryId") equals Property([pc.Category], "CategoryId")
          where [pc.Category].ParentId == (Nullable<Guid>)__Genre_0
          select [pc.Category]'
warn: Microsoft.EntityFrameworkCore.Query[20500]
      The LINQ expression 'join Category pc.Category in value(Microsoft.EntityFrameworkCore.Query.Internal.EntityQueryable`1[EFSampleApp.Category]) on Property([pc], "CategoryId") equals Property([pc.Category], "CategoryId")' could not be translated and will be evaluated locally.
warn: Microsoft.EntityFrameworkCore.Query[20500]
      The LINQ expression 'where ([pc.Category].ParentId == Convert(__Genre_0, Nullable`1))' could not be translated and will be evaluated locally.
dbug: Microsoft.EntityFrameworkCore.Query[10107]
      (QueryContext queryContext) => IEnumerable<IOrderedQueryable<Category>> _InterceptExceptions(
          source: IEnumerable<IOrderedQueryable<Category>> _ShapedQuery(
              queryContext: queryContext, 
              shaperCommandContext: SelectExpression: 
                  SELECT [p].[ProductId]
                  FROM [Commerce].[Product] AS [p], 
              shaper: TypedProjectionShaper<ValueBufferShaper, ValueBuffer, IOrderedQueryable<Category>>), 
          contextType: EFSampleApp.MyContext, 
          logger: DiagnosticsLogger<Query>, 
          queryContext: queryContext)

Original Issue

Hi!

I'm trying to map an existing database to EF Core. Our project has a terrible model where we have to compare the ParentId to a specific Guid to find out the type of a row. E.g. we have the table Categories and each Guid identifies Genre, Mood, etc. (we have a music app).

So I'm trying to write this property in the Category class, but if I do, I'm unable to use Include because it can't be translated:

public virtual bool IsGenre => ParentId == Genre;
private static Guid Genre = Guid.Parse("3CA9FA61-EB62-4480-B476-867F78A9ADB3")

If I instead do ctx.Categories.Where(c => c.ParentId == Guid.Parse("3CA9FA61-EB62-4480-B476-867F78A9ADB3") it works perfectly.

I'm wondering if there's any way to move this to the Category class so I can avoid copy and pasting for each query I need to write.

I tried to manually create the Expression:

public static Expression<Func<Category, bool>> IsGenre = c => c.ParentId == Genre;

but I get the following error on p.ProductCategories.Select(pc => pc.Category).Where(Category.IsGenre):

Error   CS1929  'IEnumerable<Category>' does not contain a definition for 'Where' and the best extension method overload 'Queryable.Where<Category>(IQueryable<Category>, Expression<Func<Category, bool>>)' requires a receiver of type 'IQueryable<Category>'

With .AsQueryable() I'm able to compile the program but I get This overload of the method 'System.Linq.Queryable.AsQueryable' is currently not supported.

Thanks!

@smitpatel
Copy link
Contributor

I could not reproduce this. Below is the code i used. Can you make modifications in it according to what you are trying to do?

public class Program
{
    public static void Main(string[] args)
    {
        using (var ctx = new MyContext())
        {
            ctx.Database.EnsureDeleted();
            ctx.Database.EnsureCreated();

            var query1 =
                ctx.Categories.Where(c => c.ParentId == Guid.Parse("3CA9FA61-EB62-4480-B476-867F78A9ADB3")).ToList();

            var query2 = ctx.ProductCategories.Select(pc => pc.Category).Where(Category.IsGenre).ToList();

        }
    }
}

public class MyContext : DbContext
{
    public DbSet<ProductCategory> ProductCategories { get; set; }
    public DbSet<Category> Categories { get; set; }

    protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
    {
        optionsBuilder.UseSqlServer(@"Server=(localdb)\mssqllocaldb;Database=_ModelApp;Trusted_Connection=True;");
    }
}

public class ProductCategory
{
    public Guid Id { get; set; }
    public Category Category { get; set; }
}

public class Category
{
    private static Guid Genre = Guid.Parse("3CA9FA61-EB62-4480-B476-867F78A9ADB3");
    public static Expression<Func<Category, bool>> IsGenre = c => c.ParentId == Genre;

    public Guid Id { get; set; }
    public Guid ParentId { get; set; }
    public ProductCategory Parent { get; set; }
}

@colltoaction
Copy link
Contributor Author

This is actually a many-to-many relationship. Please try the code below (and let me know if you need seed data):

using System;
using System.Collections.Generic;
using System.Linq;
using System.Linq.Expressions;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Metadata;

namespace EFIssue
{
    public class Program
    {
        public static void Main(string[] args)
        {
            using (var ctx = new MyContext())
            {
                var query1 =
                    ctx.Categories.Where(c => c.ParentId == Guid.Parse("3CA9FA61-EB62-4480-B476-867F78A9ADB3")).ToList();

                var query2 = ctx.ProductCategories.Select(pc => pc.Category).Where(Category.IsGenre).ToList();

                var query3 = ctx.Products.Select(p =>
                    p.ProductCategories.Select(pc => pc.Category)
                                       .Where(Category.IsGenre))
                    .ToList();
            }
        }
    }

    public class MyContext : DbContext
    {
        public DbSet<Product> Products { get; set; }
        public DbSet<ProductCategory> ProductCategories { get; set; }
        public DbSet<Category> Categories { get; set; }

        protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
        {
            optionsBuilder.UseSqlServer(@"Server=(localdb)\mssqllocaldb;Database=_ModelApp;Trusted_Connection=True;");
        }

        protected override void OnModelCreating(ModelBuilder modelBuilder)
        {
            modelBuilder.Entity<Category>(entity =>
            {
                entity.HasKey(e => e.CategoryId)
                    .HasName("PK__Category__19093A0BEC90FC43");

                entity.ToTable("Category", "Commerce");

                entity.Property(e => e.CategoryId).ValueGeneratedNever();

                entity.Property(e => e.Name).HasMaxLength(255);
            });

            modelBuilder.Entity<Product>(entity =>
            {
                entity.ToTable("Product", "Commerce");

                entity.Property(e => e.ProductId).ValueGeneratedNever();

                entity.Property(e => e.Name).HasMaxLength(255);
            });

            modelBuilder.Entity<ProductCategory>(entity =>
            {
                entity.ToTable("ProductCategory", "Commerce");

                entity.HasIndex(e => new { e.ProductId, e.CategoryId })
                    .HasName("nci_wi_ProductCategory_E47C12BC-D5AC-4997-BCF5-AD7A7AF8751B");

                entity.HasIndex(e => new { e.CategoryId, e.Id, e.ProductId })
                    .HasName("nci_wi_ProductCategory_C39551F634191AFB3482");

                entity.Property(e => e.Id).HasColumnName("id");

                entity.HasOne(e => e.Category)
                    .WithMany(c => c.ProductCategories)
                    .HasForeignKey(e => e.CategoryId)
                    .OnDelete(DeleteBehavior.Cascade)
                    .HasConstraintName("FK__ProductCa__Categ__0D44F85C");

                entity.HasOne(e => e.Product)
                    .WithMany(p => p.ProductCategories)
                    .HasForeignKey(e => e.ProductId)
                    .OnDelete(DeleteBehavior.Cascade)
                    .HasConstraintName("FK__ProductCa__Produ__0C50D423");
            });
        }
    }

    public class Product
    {
        public Product()
        {
            ProductCategories = new HashSet<ProductCategory>();
        }

        public Guid ProductId { get; set; }
        public string Name { get; set; }

        public virtual ICollection<ProductCategory> ProductCategories { get; set; }
    }

    public class ProductCategory
    {
        public Guid CategoryId { get; set; }
        public Guid ProductId { get; set; }
        public int Id { get; set; }

        public virtual Category Category { get; set; }
        public virtual Product Product { get; set; }
    }

    public class Category
    {
        public Guid CategoryId { get; set; }
        public Guid? ParentId { get; set; }
        public string Name { get; set; }

        public virtual ICollection<ProductCategory> ProductCategories { get; set; }

        public static Expression<Func<Category, bool>> IsGenre = c => c.ParentId == Genre;
        // public virtual bool IsGenre => ParentId == Genre;
        private static Guid Genre = Guid.Parse("3CA9FA61-EB62-4480-B476-867F78A9ADB3");
    }
}

@smitpatel
Copy link
Contributor

@tinchou - Thanks for working repro code. Can you tell in words what you are trying to query so I can translate it to linq.

@colltoaction
Copy link
Contributor Author

In my real app I would like to:

  1. Find some products based on some criteria (ctx.Products.Where(...))
  2. Find the categories related to that product (I have to go through the ProductCategories intermediate table since it's a many-to-many relationship)

@smitpatel
Copy link
Contributor

I tried running different queries. The queries in repro code are based criteria on category (rather than product as you mentioned in previous post) but since relationship is many to many, it can be easily written from other way depending on what you want to query.
Query1: Categories based on condition
Query2: ProductCategories based on condition on Category property
Query3: Categories based on condition with data for ProductCategories and products populated.
Query4: List of Products for all ProductCategories of (Category filtered by condition)
Query5: Similary to query you were writing, with forced client eval.

var query1 =
    ctx.Categories.Where(c => c.ParentId == Guid.Parse("3CA9FA61-EB62-4480-B476-867F78A9ADB3")).ToList();

var query2 = ctx.ProductCategories.Select(pc => pc.Category).Where(Category.IsGenre).ToList();

var query3 = ctx.Categories
    .Where(Category.IsGenre)
    .Include(c => c.ProductCategories)
    .ThenInclude(pc => pc.Category)
    .ToList();

var query4 = ctx.Categories.Where(Category.IsGenre)
    .SelectMany(c => c.ProductCategories)
    .Select(pc => pc.Product)
    .ToList();

var query5 = ctx.Products.ToList()
    .Where(p => p.ProductCategories.Select(pc => pc.Category).AsQueryable().Any(Category.IsGenre))
    .ToList();

As you can see you can still get products for categories even with starting from categories db set.
The issue with the query you were writing,
First you need to use Where instead of Select since you want to filter products and not project something from products. And that still would not work without AsQueryable() call since it is Enumerable. The AsQueryable() call is correct compilation wise. But the issue is EF uses Remotion.Linq to parse the query model which is not able to parse AsQueryable() method call yet therefore it throws runtime exception. Doing ToList() on product would mean that Where clause will be evaluated locally using Linq though it would also load whole table from database.
Query4 should be proper way to achieve what you are trying to query.

@anpete

@rowanmiller
Copy link
Contributor

@smitpatel anything to do on the EF side here?

@smitpatel
Copy link
Contributor

From discussions so far, the way user wants to write query is not possible due to limited from re-linq. And there is alternate way to write the query which gives same results.

@divega
Copy link
Contributor

divega commented Jul 25, 2016

I was curious about this one because it ringed a bell so I took a look:

In the past I found it very useful to create repositories that take predicates of the form Expression<Func<T, bool>> in their methods and I also hit the need to call AsQueryable() just to be able to use those predicates in the context of collection nav. props.

It was very handy that AsQueryable() worked in the old EF stack to satisfy this scenario.

I agree with @smitpatel that in some cases it should be possible to change your code to get the same data loaded from the other side, but I don't think that it is true in all cases, e.g. it won't help if the reverse nav. prop. isn't present. It might also be harder to figure out what query to write.

If this is a fundamental issue in re-linq with AsQueryable() I think it might be worth passing the bug to them.

@rowanmiller
Copy link
Contributor

@smitpatel could you pass off a request to re-linq to look at removing this limitation, then you can close this as external.

@colltoaction
Copy link
Contributor Author

Fine with me. Please post here if you open an issue on re-linq so I can keep an eye on it!

@rowanmiller
Copy link
Contributor

BTW re-linq issue tracker is at https://www.re-motion.org/jira/browse/RMLNQ

@smitpatel
Copy link
Contributor

@divega
Copy link
Contributor

divega commented Aug 5, 2016

cc @MichaelKetting as heads up that we would appreciate if https://www.re-motion.org/jira/projects/RMLNQ/issues/RMLNQ-28 got looked at to unblock us from supporting AsQueryable() in EF Core. Thanks!

@MichaelKetting
Copy link
Contributor

MichaelKetting commented Aug 5, 2016

@divega I've taken a quick look at the solution proposed in our Jira ticket and if the custom AsQueryableNode really is all that's needed, you don't need to wait for re-linq to implement this. In EF's QueryCompiler.CreateNodeTypeProvider you already use the extension point for custom node types.

Mind you, I haven't gotten to actually trying the sample from our ticket, but if this works, this would certainly give you the most effective way forward.

BTW, thanks for pointing out that this isn't implemented yet. I've prioritized it for the next release but if the quick fix works, I'd prefer to treat this as nice-to-have.

Edit: Come to think about it, 'most effective' might not be correct since EF just did a 1.0 release and if I fix this in re-linq, tincho could just upgrade re-linq and needn't wait for the next EF release. Interesting thing, this semantic versioning in combination with NuGet...

@igoreso
Copy link

igoreso commented Aug 17, 2017

Is there any update? Or maybe a workaround (that doesn't involve building own EF)?
It doesn't seem like this issue have been worked on on re-linq side in almost a year.

@smitpatel smitpatel reopened this Sep 30, 2017
@smitpatel smitpatel removed this from the 1.1.0-preview1 milestone Sep 30, 2017
@smitpatel
Copy link
Contributor

Reopening the issue and removing from milestone for triage. We need to decide if we want to do something to translate AsQueryable or wait for fix from relinq. The comment above been stale (though may be similar grounds again given we released 2.0 now).

P.S. - if we decide to add support with custom extension, we should file new issue tracking it.

@smitpatel
Copy link
Contributor

@MichaelKetting - Any estimate when fix for https://www.re-motion.org/jira/projects/RMLNQ/issues/RMLNQ-28?filter=allopenissues will be available or should we go ahead and write code on our end as suggested?

@MichaelKetting
Copy link
Contributor

I've got some vacation days coming up the last week of October so that should work out nicely for me having some time on my hands :)

@MichaelKetting
Copy link
Contributor

MichaelKetting commented Jan 21, 2018

@techniq yeah, I just managed to push the AsQueryable feature in 2.2.0-alpha.5 :)

Edit: fixed version number.

@techniq
Copy link

techniq commented Jan 21, 2018 via email

@techniq
Copy link

techniq commented Jan 22, 2018

@MichaelKetting Just a heads up, I added the Remotion.Linq 2.0.0-alpha.5 to my project, which fixed the System.NotSupportedException: Could not parse expression '$it.AsQueryable()': This overload of the method 'System.Linq.Queryable.AsQueryable' is currently not supported. exception, but now the dotnet process exits without any stack trace:

image

I'm not sure if the problem lies within EFCore, OData / ASP.NET Core, or Remotion.Linq. Any thoughts?

@MichaelKetting
Copy link
Contributor

MichaelKetting commented Jan 23, 2018

@techniq Hmm...I've usually seen this with StackOverflowExceptions. You could try to install Remotion.Linq v2.2.0-alpha.4 to check if the problem is related to the AsQueryable-feature. That was the only change in alpha.5, so can do an A-B test very easily. Then there's the option of attaching a debugger and debugging the exceptions. A StackOverflow excpetion should show up in the debugger.

Erm...I just noticed, I may have written "v2.0.0-alpha.5" instead of "v2.2.0-alpha.5" in my previous reply. Could you check that one first?

Edit: we never had a v2.0.0-alpha.5, so at least you couldn't have downloaded the wrong version. Maybe the wrong binding redirect, though.

@techniq
Copy link

techniq commented Jan 23, 2018

@MichaelKetting As you determined, I definitely have the v2.2.0-alpha.5 version. If I remove Remotion.Linq from my project I returned to the System.NotSupportedException exception about AsQueryable not being implemented so it is definitely using your changes (and should indicate the binding redirects are kicking in, at least I think so).

I was using dotnet on Mac but decided to open up Visual Studio on my Windows VM to see if I could get any more insight into the problem and see this unhandled exception mesage at the bottom:

An unhandled exception of type 'System.AccessViolationException' occurred in Microsoft.AspNetCore.OData.dll
Attempted to read or write protected memory. This is often an indication that other memory is corrupt.

image

With that, it appears to be a problem on the ASP.NET Core OData side and I'll log an issue over there (although I'm not for certain if it's not the result of the interplay between EFCore, OData, and Remotion.Linq).

@MichaelKetting
Copy link
Contributor

Thanks for the update. The AccessViolationException sounds definitely strange. My best guesses would either be an undetected API change in the alpha.5 (which would really surprise me if I missed that) or some issue when working with unmanged code during the query execution. But I'll adopt a wait-and-see approch for now :)

@smitpatel smitpatel changed the title How to use a custom property as part of an expression Query: Allow use of AsQueryable method Feb 1, 2018
@smitpatel
Copy link
Contributor

@MichaelKetting - Thanks for the fix. We are now able to get parsed query model. Can you also take a look at #9311. We are at present in preview2 and would soon need RTM version of re-linq to use in our RTM version. (else we would have to revert back to 2.1.1) Can you give an estimate when can RTM version be available?

Removing milestone of this issue to track work needed to get translation to server.

@smitpatel smitpatel removed this from the 2.1.0 milestone Feb 1, 2018
@smitpatel smitpatel removed their assignment Feb 1, 2018
@MichaelKetting
Copy link
Contributor

@smitpatel I can do an RTM build as needed, the question is, should I try to take a shot at the two issues in https://www.re-motion.org/jira/issues/?filter=11440 over the weekend and drop a beta version for you to try first and then an RTM build sometime next week? Or just go with what's already in the bag?

@smitpatel
Copy link
Contributor

@MichaelKetting - Sorry for late response. Sure go ahead with more bug fixes if you want. One week wait is no issue for us. Let me know if you drop another beta version, I can certainly help validate it against EF tests.

@ajcvickers
Copy link
Member

Fix it if it's easy in 2.1, else punt.

@anpete
Copy link
Contributor

anpete commented Feb 5, 2018

Confirmed fixed after we take the new version of relinq.

@MichaelKetting
Copy link
Contributor

Okay, didn't manage the OrderBy with Custom Comparer and the QueryModel Bug for v2.2. Sorry about that, but on the plus side the v2.2 rtm build's just gotten sent off to the build server.

@smitpatel
Copy link
Contributor

Thanks @MichaelKetting

@anpete
Copy link
Contributor

anpete commented Feb 14, 2018

Added test in e2a2b9b

@anpete anpete closed this as completed Feb 14, 2018
@anpete anpete added the closed-fixed The issue has been fixed and is/will be included in the release indicated by the issue milestone. label Feb 14, 2018
@ajcvickers ajcvickers modified the milestones: 2.1.0-preview2, 2.1.0 Nov 11, 2019
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
closed-fixed The issue has been fixed and is/will be included in the release indicated by the issue milestone. type-bug
Projects
None yet
Development

No branches or pull requests

9 participants