I'm updating an EF6.x project to EF Core 3.1. Decided to go back to basics and follow the example of how to set up relationships from scratch again.
According to the official Microsoft documentation, EF Core Relationship Examples, I translated the examples into a console app below:
using Microsoft.EntityFrameworkCore;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
namespace BlogPostsExample
{
class Program
{
async static Task Main(string[] args)
{
// SQL Running in a Docker container - update as required
var conString = "data source=localhost,14330;initial catalog=BlogsDb;persist security info=True;user id=sa;password=<Your super secure SA password>;MultipleActiveResultSets=True;App=EntityFramework;";
var ctx = new MyContext(conString);
await ctx.Database.EnsureCreatedAsync();
var result = await ctx.Posts.GroupBy(p => p.Blog).ToArrayAsync();
}
}
class MyContext : DbContext
{
private readonly string _connectionString;
public MyContext(string connectionString)
{
_connectionString = connectionString;
}
protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
{
base.OnConfiguring(optionsBuilder);
if (!optionsBuilder.IsConfigured)
{
optionsBuilder
.UseSqlServer(_connectionString);
}
}
public DbSet<Blog> Blogs { get; set; }
public DbSet<Post> Posts { get; set; }
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder.Entity<Post>()
.HasOne(p => p.Blog)
.WithMany(b => b.Posts)
.HasForeignKey(p => p.BlogId) //Tried with and without these keys defined.
.HasPrincipalKey(b => b.BlogId);
}
}
public class Blog
{
public int BlogId { get; set; }
public string Url { get; set; }
public List<Post> Posts { get; set; }
}
public class Post
{
public int PostId { get; set; }
public string Title { get; set; }
public string Content { get; set; }
public int BlogId { get; set; }
public Blog Blog { get; set; }
}
}
There is no data in the DB. EF Core fails to convert
ctx.Posts.GroupBy(p => p.Blog)
to a store query. This appears to me to be the simplest example of a GroupBy you could try.
When you run this code you get the following exception:
System.InvalidOperationException: 'The LINQ expression 'DbSet<Post>
.Join(
outer: DbSet<Blog>,
inner: p => EF.Property<Nullable<int>>(p, "BlogId"),
outerKeySelector: b => EF.Property<Nullable<int>>(b, "BlogId"),
innerKeySelector: (o, i) => new TransparentIdentifier<Post, Blog>(
Outer = o,
Inner = i
))
.GroupBy(
source: p => p.Inner,
keySelector: p => p.Outer)' could not be translated. Either rewrite the query in a form that can be translated, or switch to client evaluation explicitly by inserting a call to either AsEnumerable(), AsAsyncEnumerable(), ToList(), or ToListAsync(). See https://go.microsoft.com/fwlink/?linkid=2101038 for more information.'
The only way to get this to work is to add something like AsEnumerable() before the GroupBy
.
This is clearly not great from a performance point of view, it turns the group by operation into a client side operation where you really want to be doing grouping on the server side.
Have I missed something glaringly obvious? I struggle to believe that EF Core can't do the simplest group by that EF Framework has been doing since day 1. This seems like a fundamental requirement of any data driven app? (Or any app with a modicum of data!)
Update:
Adding a property, such as the Primary Key for the Blog in question makes no difference.
Update 2:
If you follow this JetBrains article, you can do this:
var ctx = new EntertainmentDbContext(conString);
await ctx.Database.EnsureCreatedAsync();
var dataTask = ctx
.Ratings
.GroupBy(x => x.Source)
.Select(x => new {Source = x.Key, Count = x.Count()})
.OrderByDescending(x => x.Count)
.ToListAsync();
var data = await dataTask;
But NOT this:
var ctx = new EntertainmentDbContext(conString);
await ctx.Database.EnsureCreatedAsync();
var dataTask = ctx
.Ratings
.GroupBy(x => x.Source)
// .Select(x => new {Source = x.Key, Count = x.Count()})
// .OrderByDescending(x => x.Count)
.ToListAsync();
var data = await dataTask;
It only works with with an aggregating function, eg Count as above.
Something similar in SQL works
SELECT COUNT(R.Id), R.Source
FROM
[EntertainmentDb].[dbo].[Ratings] R
GROUP BY R.Source
But, removing the aggregating function, COUNT does not, you receive messages similar to:
Column 'EntertainmentDb.dbo.Ratings.Id' is invalid in the select list because it is not contained in either an aggregate function or the GROUP BY clause.
So it looks like I am trying to ask EF Core a question that I cannot ask in TSQL
One of the biggest reasons not to use Entity Framework Core is that your application needs the fastest possible data access. Some applications do a lot of heavy data operations with very high-performance demands, but usually business applications don't have that high of a performance demand.
EF Core 6.0 itself is 31% faster executing queries. Heap allocations have been reduced by 43%.
Lazy loading means that the related data is transparently loaded from the database when the navigation property is accessed.
Earlier EF/EF core automatically converted to client-side query evaluation when server-side evaluation was not possible.
Grouping by a key without select is not something supported by SQL and would always have been a client-side operation.
With EF 3.0+, they made it explicit on which query should run on server or on client. Technically it is better to explicitly know which query will run on server and what will run on client rather than a framework deciding it on our behalf.
You can read more about it here: https://docs.microsoft.com/en-us/ef/core/what-is-new/ef-core-3.x/breaking-changes#linq-queries-are-no-longer-evaluated-on-the-client
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With