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

Add expression support for abstract properties #30232

Closed
amyboose opened this issue Feb 8, 2023 · 4 comments
Closed

Add expression support for abstract properties #30232

amyboose opened this issue Feb 8, 2023 · 4 comments
Labels
closed-out-of-scope This is not something that will be fixed/implemented and the issue is closed. customer-reported

Comments

@amyboose
Copy link

amyboose commented Feb 8, 2023

I have some problem using inheritance and EF Core.

My code:

using Microsoft.EntityFrameworkCore;
using System.ComponentModel.DataAnnotations.Schema;

namespace EfCore;
public class Program
{
    public static async Task Main(params string[] args)
    {
        IHost host = Host.CreateDefaultBuilder()
        .ConfigureServices(services =>
        {
            services.AddDbContext<MyContext>(builder =>
            {
                builder.UseSqlServer("Server=localhost,7438;Database=testdb;TrustServerCertificate=True;User Id=sa;Password=RMfL3Tx%bZ5b;");
            });
        })
        .Build();

        using var scope = host.Services.CreateScope();
        var provider = scope.ServiceProvider;
        var context = provider.GetRequiredService<MyContext>();

        //Example 1
        List<Blog> nonEmptyBlogs = context.Blogs
            .Where(x => x.Posts.Count > 1)
            .ToList();

        //Exmaple 2, please comment the first example
        DateTimeOffset dtNow = DateTimeOffset.UtcNow.AddDays(-5);
        List<Post> posts = context.Posts
            .Where(x => x.Blog.PublishedAt > dtNow)
            .ToList();
    }
}

public abstract class Blog
{
    public int Id { get; set; }
    public DateTimeOffset PublishedAt { get; set; }
    [NotMapped]
    public abstract IReadOnlyList<Post> Posts { get; }
}

public abstract class Post
{
    public int Id { get; set; }
    public int BlogId { get; set; }
    [NotMapped]
    public abstract Blog Blog { get; }
}

public class AnimalBlog : Blog
{
    public List<AnimalPost> AnimalPosts { get; set; } = null!;
    public override IReadOnlyList<Post> Posts => AnimalPosts;
}

public class HouseBlog : Blog
{
    public List<HousePost> HousePosts { get; set; } = null!;
    public override IReadOnlyList<Post> Posts => HousePosts;
}

public class AnimalPost : Post
{
    public AnimalBlog AnimalBlog { get; set; } = null!;

    public override Blog Blog => AnimalBlog;
}

public class HousePost : Post
{
    public HouseBlog HouseBlog { get; set; } = null!;
    public override Blog Blog => HouseBlog;
}

public class MyContext : DbContext
{
    public MyContext(DbContextOptions options) : base(options) { }

    public DbSet<Blog> Blogs { get; set; }
    public DbSet<Post> Posts { get; set; }
    public DbSet<AnimalBlog> AnimalBlogs { get; set; }
    public DbSet<AnimalPost> AnimalPosts { get; set; }
    public DbSet<HouseBlog> HouseBlogs { get; set; }
    public DbSet<HousePost> HousePosts { get; set; }

    protected override void OnModelCreating(ModelBuilder modelBuilder)
    {
        modelBuilder.Entity<AnimalBlog>()
            .HasMany(x => x.AnimalPosts)
            .WithOne(x => x.AnimalBlog)
            .HasForeignKey(x => x.BlogId);

        modelBuilder.Entity<HouseBlog>()
            .HasMany(x => x.HousePosts)
            .WithOne(x => x.HouseBlog)
            .HasForeignKey(x => x.BlogId);
    }
}

But first and second examples throw an exception:

System.InvalidOperationException: "The LINQ expression 'DbSet<Blog>() .Where(b => b.Posts.Count > 1)' could not be translated. 
Additional information: Translation of member 'Posts' on entity type 'Blog' failed. 
This commonly occurs when the specified member is unmapped. 
Either rewrite the query in a form that can be translated, or switch to client evaluation explicitly by inserting a call to 'AsEnumerable', 'AsAsyncEnumerable', 'ToList', or 'ToListAsync'. See https://go.microsoft.com/fwlink/?linkid=2101038 for more information."
System.InvalidOperationException: "The LINQ expression 'DbSet<Post>().Where(p => p.Blog.PublishedAt > __dtNow_0)' could not be translated. 
Additional information: Translation of member 'Blog' on entity type 'Post' failed. This commonly occurs when the specified member is unmapped. 
Either rewrite the query in a form that can be translated, or switch to client evaluation explicitly by inserting a call to 'AsEnumerable', 'AsAsyncEnumerable', 'ToList', or 'ToListAsync'. See https://go.microsoft.com/fwlink/?linkid=2101038 for more information."

Blog.Posts and Post.Blog are not a part of EF Core model but technically they are the same as its internal properties.

I would like to use these properties in Expressions without using custom creation of expression trees.

It might look like this:

internal class AnimalBlogConfiguration : IEntityTypeConfiguration<AnimalBlog>
{
    public void Configure(EntityTypeBuilder<AnimalBlog> builder)
    {
        builder
            .MapProperty(p => p.Posts)
            .ToProperty(p => p.AnimalPosts);
    }
}

Also it can be EF Core's internal mechanism to add abstract property mapping to its internal property.
This feature is useful a lot for repositories when parent repository contains a whole logic. For example, BlogRepository can contains a whole logic for all child repositories.

Additional information:

Also I've got endless migration after comment [NotMapped] attributes

EF Core version: 7.0.2
Database provider: SQL Provider
Target framework: NET 7.0
Operating system: Windows 10
IDE: Visual Studio 2022 17.4

@ajcvickers
Copy link
Member

@amyboose Do you want to be able to use both queries using Blog (e.g. .Where(x => x.Blog.PublishedAt > dtNow) and queries using AnimalBlog/HouseBlog (e.g. .Where(x => x.AnimalBlog.PublishedAt > dtNow)?

@amyboose
Copy link
Author

amyboose commented Feb 17, 2023

@amyboose Do you want to be able to use both queries using Blog (e.g. .Where(x => x.Blog.PublishedAt > dtNow) and queries using AnimalBlog/HouseBlog (e.g. .Where(x => x.AnimalBlog.PublishedAt > dtNow)?

Yes, I want both.

It will be so useful for many cases using inheritance and repository pattern.

I've changed a bit my code to show some hypothetical case. Main changes in repositories:

using Microsoft.EntityFrameworkCore;
using System.ComponentModel.DataAnnotations.Schema;

namespace EfCore;
public class Program
{
    public static async Task Main(params string[] args)
    {
        IHost host = Host.CreateDefaultBuilder()
        .ConfigureServices(services =>
        {
            services.AddDbContext<MyContext>(builder =>
            {
                builder.UseSqlServer("Server=localhost,7438;Database=testdb;TrustServerCertificate=True;User Id=sa;Password=RMfL3Tx%bZ5b;");
            });

            services.AddScoped(typeof(BlogRepository<>));
            services.AddScoped<AnimalBlogRepository>();
        })
        .Build();

        using var scope = host.Services.CreateScope();
        var provider = scope.ServiceProvider;
        var blogRepository = provider.GetRequiredService<BlogRepository<Blog>>();
        var animalBlogRepository = provider.GetRequiredService<AnimalBlogRepository>();

        //Example 1
        List<Blog> nonEmptyBlogs = await blogRepository.GetNonEmptyBlogsWithPosts();

        //Exmaple 2, please comment the first example
        List<AnimalBlog> blogsAboutDogs = await animalBlogRepository.GetBlogsByAnimal("dog");
    }
}

public abstract class Blog
{
    public int Id { get; set; }
    public DateTimeOffset PublishedAt { get; set; }
    [NotMapped]
    public abstract IReadOnlyList<Post> Posts { get; }
}

public abstract class Post
{
    public int Id { get; set; }
    public int BlogId { get; set; }
    [NotMapped]
    public abstract Blog Blog { get; }
}

public class AnimalBlog : Blog
{
    public List<AnimalPost> AnimalPosts { get; set; } = null!;
    public override IReadOnlyList<Post> Posts => AnimalPosts;
}

public class HouseBlog : Blog
{
    public List<HousePost> HousePosts { get; set; } = null!;
    public override IReadOnlyList<Post> Posts => HousePosts;
}

public class AnimalPost : Post
{
    public AnimalBlog AnimalBlog { get; set; } = null!;
    public List<string> AnimalTags { get; set; } = null!;
    public override Blog Blog => AnimalBlog;
}

public class HousePost : Post
{
    public HouseBlog HouseBlog { get; set; } = null!;
    public override Blog Blog => HouseBlog;
}

public class MyContext : DbContext
{
    public MyContext(DbContextOptions options) : base(options) { }

    public DbSet<Blog> Blogs { get; set; }
    public DbSet<Post> Posts { get; set; }
    public DbSet<AnimalBlog> AnimalBlogs { get; set; }
    public DbSet<AnimalPost> AnimalPosts { get; set; }
    public DbSet<HouseBlog> HouseBlogs { get; set; }
    public DbSet<HousePost> HousePosts { get; set; }

    protected override void OnModelCreating(ModelBuilder modelBuilder)
    {
        modelBuilder.Entity<AnimalBlog>()
            .HasMany(x => x.AnimalPosts)
            .WithOne(x => x.AnimalBlog)
            .HasForeignKey(x => x.BlogId);

        modelBuilder.Entity<AnimalPost>()
            .OwnsOne(p => p.AnimalTags)
            .ToJson();

        modelBuilder.Entity<HouseBlog>()
            .HasMany(x => x.HousePosts)
            .WithOne(x => x.HouseBlog)
            .HasForeignKey(x => x.BlogId);
    }
}

public class BlogRepository<TBlog> where TBlog : Blog
{
    protected readonly MyContext _context;

    public BlogRepository(MyContext context)
    {
        _context = context;
    }

    public Task<List<TBlog>> GetNonEmptyBlogsWithPosts()
    {
        return _context.Set<TBlog>()
            .Where(x => x.Posts.Count > 0)
            .Include(x => x.Posts)
            .ToListAsync();
    }
}

public class AnimalBlogRepository : BlogRepository<AnimalBlog>
{
    public AnimalBlogRepository(MyContext context) : base(context) { }

    public Task<List<AnimalBlog>> GetBlogsByAnimal(string animalTag)
    {
        return _context.Set<AnimalBlog>()
            .Where(x => x.AnimalPosts.Any(post => post.AnimalTags.Any(tag => tag == animalTag)))
            .Include(x => x.AnimalPosts)
            .ToListAsync();
    }
}

Also you can see that I've added Include function which can be useful too

.Include(x => x.Posts)

The exmple throw exceptions

I have real cases but it will be too long example to show it.

@ajcvickers
Copy link
Member

Yes, I want both.

That's not currently supported by EF Core; I'll talk to the team about whether we might consider it for the future.

@ajcvickers
Copy link
Member

Notes from triage:

  • Multiple sets of navigation properties backed by the same relationship is not something we plan to introduce first-class support for. Keeping these navigations in sync, and what to do when they get out of sync is not trivial, and could get very confusing.
  • On the other hand, support for abstracted collections, as tracked by Rich collection support: Dictionary, Lookup, HashSet, etc. #2919 could potentially be used in the future to manage this on the application side, where the application would be responsible for understanding what interaction and syncing is needed between the navigations.

@ajcvickers ajcvickers closed this as not planned Won't fix, can't repro, duplicate, stale Feb 23, 2023
@ajcvickers ajcvickers added closed-out-of-scope This is not something that will be fixed/implemented and the issue is closed. and removed type-enhancement labels Feb 23, 2023
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
closed-out-of-scope This is not something that will be fixed/implemented and the issue is closed. customer-reported
Projects
None yet
Development

No branches or pull requests

2 participants