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: Include doesn't work with SelectMany #6451

Closed
Bartmax opened this issue Sep 1, 2016 · 13 comments
Closed

Query: Include doesn't work with SelectMany #6451

Bartmax opened this issue Sep 1, 2016 · 13 comments
Assignees
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

@Bartmax
Copy link

Bartmax commented Sep 1, 2016

Steps to reproduce

return context.SiteBanners.Include(sb => sb.Layers).ThenInclude(l => l.SiteBannerFile)
    .SelectMany(b => b.Layers).Include(l=>l.SiteBannerFile).FirstOrDefaultAsync(l => l.FileId == id);

The issue

this does populate the SiteBannerLayer entity but not the SiteBannerFile.
SiteBannerFile is null.

In the example above I used Include everywhere just in case.

Of course this works:

var banners = await context.SiteBanners.Include(sb => sb.Layers).ThenInclude(l => l.SiteBannerFile).ToListAsync();
return banners.SelectMany(b => b.Layers).FirstOrDefault(l => l.FileId == id);

but that's an inefficient query

Further technical details

EF Core version: 1.0
Operating system: Windows 10
Visual Studio version: 2015

Other details about my project setup:
Here are the entities involved.

public class SiteBanner
{
    public SiteBanner()
    {
        Layers = new List<SiteBannerLayer>();
    }
    public int SiteBannerId { get; set; }

    public string Title { get; set; }

    public ICollection<SiteBannerLayer> Layers { get; set; }
}
public class SiteBannerLayer : ISortableFile
{
    SiteBannerLayer()
    {/*EF*/ }
    public SiteBannerLayer(FileData file, int order = 0)
    {
        if (file == null)
        {
            throw new ArgumentNullException(nameof(file));
        }
        SiteBannerFile = new SiteBannerFile(file);
        Order = order;
    }

    public int SiteBannerLayerId { get; set; }

    [Required]
    public SiteBannerFile SiteBannerFile { get; set; }

    public int Order { get; set; }

    public IFileData File => SiteBannerFile;

    public Guid FileId => SiteBannerFile.SiteBannerFileId;

}
public class SiteBannerFile : IFileData
{
    private const string folder = "SiteBanner";
    public SiteBannerFile() { } /*EF*/
    public SiteBannerFile(FileData fileData)
    {
        SiteBannerFileId = fileData.FileDataId;
        Filename = fileData.Filename;
        OriginalFilename = fileData.OriginalFilename;
        ContentType = fileData.ContentType;
    }

    [Key]
    public Guid SiteBannerFileId { get; private set; }

    [Required]
    public string ContentType { get; set; }

    [Required]
    public string Filename { get; set; }

    public static string Folder => folder;

    public string OriginalFilename { get; set; }

    string IFileData.Folder => folder;

    Guid IFileData.FileDataId => SiteBannerFileId;

}
@maumar
Copy link
Contributor

maumar commented Sep 1, 2016

@Bartmax could you also provide listing for DbContext (especially the OnModelCreating) and IFileData?

@Bartmax
Copy link
Author

Bartmax commented Sep 1, 2016

I have no code OnModelCreating code regarding any included entities.

Here's FileData and IFileData

public interface IFileData
{
    Guid FileDataId { get; }
    string Folder { get; }
    string Filename { get; }
    string ContentType { get; }
}

public class FileData : IFileData
{
    protected FileData() { }
    protected FileData(FileData file) : 
        this(file.FileDataId, file.Folder, file.Filename, file.OriginalFilename, file.ContentType)
    {

    }

    public FileData(Guid id, string folder, string filename, string originalFilename, string contentType)
    {
        if (id == default(Guid)) throw new ArgumentNullException(nameof(id));
        if (string.IsNullOrWhiteSpace(filename)) throw new ArgumentNullException(nameof(filename));
        if (folder == null) throw new ArgumentNullException(nameof(folder));
        FileDataId = id;
        Folder = folder;
        Filename = filename;
        OriginalFilename = originalFilename;
        ContentType = contentType;
    }
    public Guid FileDataId { get; }
    public string Folder { get; }
    public string Filename { get; }
    public string OriginalFilename { get;  }
    public string ContentType { get;  }
}

@divega divega added this to the 1.1.0 milestone Sep 2, 2016
@maumar
Copy link
Contributor

maumar commented Sep 2, 2016

Verified this is a bug. Simplified repro:

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

                var bf11 = new SiteBannerFile();
                var bf12 = new SiteBannerFile();
                var l11 = new SiteBannerLayer { SiteBannerFile = bf11 };
                var l12 = new SiteBannerLayer { SiteBannerFile = bf12 };
                var b1 = new SiteBanner { Layers = new List<SiteBannerLayer> { l11, l12, } };

                ctx.SiteBanners.AddRange(b1);
                ctx.SiteBannerLayers.AddRange(l11, l12);
                ctx.SiteBannerFiles.AddRange(bf11, bf12);
                ctx.SaveChanges();
            }

            using (var ctx = new MyContext())
            {
                var query = ctx.SiteBanners.Include(sb => sb.Layers).ThenInclude(l => l.SiteBannerFile)
                    .SelectMany(b => b.Layers).ToList();
            }
        }
    }

    public class MyContext : DbContext
    {
        public DbSet<SiteBanner> SiteBanners { get; set; }
        public DbSet<SiteBannerLayer> SiteBannerLayers { get; set; }
        public DbSet<SiteBannerFile> SiteBannerFiles { get; set; }

        protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
        {
            optionsBuilder.UseSqlServer("Server=.;Database=Repro6451;Trusted_Connection=True;MultipleActiveResultSets=true");
        }
    }

    public class SiteBanner
    {
        public int SiteBannerId { get; set; }

        public ICollection<SiteBannerLayer> Layers { get; set; }
    }
    public class SiteBannerLayer
    {
        public int SiteBannerLayerId { get; set; }

        [Required]
        public SiteBannerFile SiteBannerFile { get; set; }
    }

    public class SiteBannerFile
    {
        public int SiteBannerFileId { get; set; }
    }

@maumar maumar changed the title can't get a navigation property. Query :: Include doesn't work with SelectMany Sep 2, 2016
@maumar
Copy link
Contributor

maumar commented Sep 2, 2016

@Bartmax as a workaround I would suggest running two queries, one retrieves the entities you are interested in, and the second one does the proper include:

var query1 = context.SiteBanners.SelectMany(b => b.Layers).Where(l => l.FieldId == id).Select(l => l.SiteBannerLayerId).FirstOrDefault();

var query2 = context.SiteBannerLayers.Include(l=>l.SiteBannerFile).Where(l => l.SiteBannerLayerId == query1);

maumar added a commit that referenced this issue Oct 12, 2016
Problem was that we were not able to match query source we were trying to include, if it came from the SelectMany since it has different structure than just regular filter/projection etc.
Fix is to compensate for the SelectMany case when constructing IncludeSpecification by using the correct query source.
@divega divega removed this from the 1.1.0-preview1 milestone Oct 17, 2016
@divega
Copy link
Contributor

divega commented Oct 17, 2016

Clearing the milestone because at this point the fix does not seem to fit in 1.1.0-preview1. We can discuss the appropriate milestone in triage.

@rowanmiller rowanmiller added this to the 1.2.0 milestone Oct 17, 2016
maumar added a commit that referenced this issue Oct 21, 2016
Problem was that in several places we transformed query model but we were not updating the (include) annotations accordingly. This resulted in annotations going out-of-sync with the actual query model which then resulted in us not being able to find the right query source to run include on.

Fix is to update query annotations where necessary (nav rewrite, select many rewrite, join elimination).

There was additional problem with string based include - we were always assuming that the include operator was called directly on a DbSet, so string based include didn't work for cases like:

customers.SelectMany(c => c.Orders).Include("...");
orders.Select(o => o.Customer).Include("...");

Fix is to add additional metadata to the IncludeResultOperator that describes the path from the initial query source (i.e. DbSet) to the actual include method. Also refactored expression based include in similar fashion - split old NavigationPropertyPath into two expressions: PathFromQuerySource (the same as with string based include) and lambda describing properties that are being included.
maumar added a commit that referenced this issue Oct 24, 2016
Problem was that in several places we transformed query model but we were not updating the (include) annotations accordingly. This resulted in annotations going out-of-sync with the actual query model which then resulted in us not being able to find the right query source to run include on.

Fix is to update query annotations where necessary (nav rewrite, select many rewrite, join elimination).

There was additional problem with string based include - we were always assuming that the include operator was called directly on a DbSet, so string based include didn't work for cases like:

customers.SelectMany(c => c.Orders).Include("...");
orders.Select(o => o.Customer).Include("...");

Fix is to add additional metadata to the IncludeResultOperator that describes the path from the initial query source (i.e. DbSet) to the actual include method. Also refactored expression based include - split old NavigationPropertyPath into two:
- expression from going from query source to the start of include chain
- list of navigation paths that are to be included.

Effectively both lambda and string based include use the same logic now.

Also fixed a minor bug where string based include was not working on navigations declared on derived types.
maumar added a commit that referenced this issue Oct 24, 2016
Problem was that in several places we transformed query model but we were not updating the (include) annotations accordingly. This resulted in annotations going out-of-sync with the actual query model which then resulted in us not being able to find the right query source to run include on.

Fix is to update query annotations where necessary (nav rewrite, select many rewrite, join elimination).

There was additional problem with string based include - we were always assuming that the include operator was called directly on a DbSet, so string based include didn't work for cases like:

customers.SelectMany(c => c.Orders).Include("...");
orders.Select(o => o.Customer).Include("...");

Fix is to add additional metadata to the IncludeResultOperator that describes the path from the initial query source (i.e. DbSet) to the actual include method. Also refactored expression based include - split old NavigationPropertyPath into two:
- expression from going from query source to the start of include chain
- list of navigation paths that are to be included.

Effectively both lambda and string based include use the same logic now.

Also fixed a minor bug where string based include was not working on navigations declared on derived types.
maumar added a commit that referenced this issue Oct 25, 2016
Problem was that in several places we transformed query model but we were not updating the (include) annotations accordingly. This resulted in annotations going out-of-sync with the actual query model which then resulted in us not being able to find the right query source to run include on.

Fix is to update query annotations where necessary (nav rewrite, select many rewrite, join elimination).

There was additional problem with string based include - we were always assuming that the include operator was called directly on a DbSet, so string based include didn't work for cases like:

customers.SelectMany(c => c.Orders).Include("...");
orders.Select(o => o.Customer).Include("...");

Fix is to add additional metadata to the IncludeResultOperator that describes the path from the initial query source (i.e. DbSet) to the actual include method. Also refactored expression based include - split old NavigationPropertyPath into two:
- expression from going from query source to the start of include chain
- list of navigation paths that are to be included.

Effectively both lambda and string based include use the same logic now.

Also fixed a minor bug where string based include was not working on navigations declared on derived types.
@maumar
Copy link
Contributor

maumar commented Oct 25, 2016

Fixed in 93ecfec

@maumar maumar closed this as completed Oct 25, 2016
@maumar maumar added closed-fixed The issue has been fixed and is/will be included in the release indicated by the issue milestone. and removed type-bug labels Oct 25, 2016
smitpatel pushed a commit that referenced this issue Nov 8, 2016
Problem was that in several places we transformed query model but we were not updating the (include) annotations accordingly. This resulted in annotations going out-of-sync with the actual query model which then resulted in us not being able to find the right query source to run include on.

Fix is to update query annotations where necessary (nav rewrite, select many rewrite, join elimination).

There was additional problem with string based include - we were always assuming that the include operator was called directly on a DbSet, so string based include didn't work for cases like:

customers.SelectMany(c => c.Orders).Include("...");
orders.Select(o => o.Customer).Include("...");

Fix is to add additional metadata to the IncludeResultOperator that describes the path from the initial query source (i.e. DbSet) to the actual include method. Also refactored expression based include - split old NavigationPropertyPath into two:
- expression from going from query source to the start of include chain
- list of navigation paths that are to be included.

Effectively both lambda and string based include use the same logic now.

Also fixed a minor bug where string based include was not working on navigations declared on derived types.
@ajcvickers ajcvickers changed the title Query :: Include doesn't work with SelectMany Query: Include doesn't work with SelectMany May 9, 2017
@divega divega added closed-fixed The issue has been fixed and is/will be included in the release indicated by the issue milestone. and removed closed-fixed The issue has been fixed and is/will be included in the release indicated by the issue milestone. labels May 10, 2017
@michaelaird
Copy link

Any chance this fix will get ported back to EF 6?

@ajcvickers
Copy link
Contributor

@michaelaird EF Core is an entirely different codebase than EF6, so any change to fix a similar issue in EF6 would be a different change, not just a port of this fix. It's very unlikely that this will change in EF6.

@michaelaird
Copy link

michaelaird commented Dec 15, 2017 via email

@hieplam
Copy link

hieplam commented Jan 4, 2018

Hi, does this bug fixed, could you give me the version of ef core where this bug was fixed.

@ajcvickers
Copy link
Contributor

@hieplam In the labels there is a "closed fixed" tag. This means that it has been fixed. The milestone indicates which release contains the fix--in this case 2.0.0-preview1.

@Tbd19
Copy link

Tbd19 commented May 7, 2019

Can this issue be reopened for version 2.2.4
In maumar reproduction please insert the code
Console.WriteLine(query.Count(s => s.SiteBannerFile != null) + " " + query.Count);
The two numbers should be equal but they are not. The first one is 0 and the second one is 2.

@smitpatel
Copy link
Contributor

@Tbd19 - It's entirely different bug. Please file a new issue with repro code and version of EF core you are using.

@ajcvickers ajcvickers modified the milestones: 2.0.0-preview1, 2.0.0 Oct 15, 2022
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