diff --git a/entity-framework/core/modeling/_static/owned-entities-nested.png b/entity-framework/core/modeling/_static/owned-entities-nested.png new file mode 100644 index 0000000000..e2a838e25d Binary files /dev/null and b/entity-framework/core/modeling/_static/owned-entities-nested.png differ diff --git a/entity-framework/core/modeling/_static/owned-entities-ownsmany.png b/entity-framework/core/modeling/_static/owned-entities-ownsmany.png new file mode 100644 index 0000000000..eeade04efe Binary files /dev/null and b/entity-framework/core/modeling/_static/owned-entities-ownsmany.png differ diff --git a/entity-framework/core/modeling/_static/owned-entities-ownsone.png b/entity-framework/core/modeling/_static/owned-entities-ownsone.png new file mode 100644 index 0000000000..de6afd8f62 Binary files /dev/null and b/entity-framework/core/modeling/_static/owned-entities-ownsone.png differ diff --git a/entity-framework/core/modeling/owned-entities.md b/entity-framework/core/modeling/owned-entities.md index d8083a90d0..b812adafb7 100644 --- a/entity-framework/core/modeling/owned-entities.md +++ b/entity-framework/core/modeling/owned-entities.md @@ -31,6 +31,10 @@ If the `ShippingAddress` property is private in the `Order` type, you can use th [!code-csharp[OwnsOneString](../../../samples/core/Modeling/OwnedEntities/OwnedEntityContext.cs?name=OwnsOneString)] +The model above is mapped to the following database schema: + +![image](_static/owned-entities-ownsone.png) + See the [full sample project](https://github.com/dotnet/EntityFramework.Docs/tree/master/samples/core/Modeling/OwnedEntities) for more context. > [!TIP] @@ -63,6 +67,10 @@ To configure a different primary key call `HasKey`. [!code-csharp[OwnsMany](../../../samples/core/Modeling/OwnedEntities/OwnedEntityContext.cs?name=OwnsMany)] +The model above is mapped to the following database schema: + +![image](_static/owned-entities-ownsmany.png) + ## Mapping owned types with table splitting When using relational databases, by default reference owned types are mapped to the same table as the owner. This requires splitting the table in two: some columns will be used to store the data of the owner, and some columns will be used to store data of the owned entity. This is a common feature known as [table splitting](xref:core/modeling/table-splitting). @@ -114,6 +122,10 @@ It is also possible to achieve this result using `OwnedAttribute` on both `Order In addition, notice the `Navigation` call. In EFCore 5.0, navigation properties to owned types can be further configured [as for non-owned navigation properties](xref:core/modeling/relationships#configuring-navigation-properties). +The model above is mapped to the following database schema: + +![image](_static/owned-entities-nested.png) + ## Storing owned types in separate tables Also unlike EF6 complex types, owned types can be stored in a separate table from the owner. In order to override the convention that maps an owned type to the same table as the owner, you can simply call `ToTable` and provide a different table name. The following example will map `OrderDetails` and its two addresses to a separate table from `DetailedOrder`: diff --git a/entity-framework/core/querying/complex-query-operators.md b/entity-framework/core/querying/complex-query-operators.md index 9689ba3b07..a3195d852f 100644 --- a/entity-framework/core/querying/complex-query-operators.md +++ b/entity-framework/core/querying/complex-query-operators.md @@ -14,7 +14,7 @@ Language Integrated Query (LINQ) contains many complex operators, which combine ## Join -The LINQ Join operator allows you to connect two data sources based on the key selector for each source, generating a tuple of values when the key matches. It naturally translates to `INNER JOIN` on relational databases. While the LINQ Join has outer and inner key selectors, the database requires a single join condition. So EF Core generates a join condition by comparing the outer key selector to the inner key selector for equality. Further, if the key selectors are anonymous types, EF Core generates a join condition to compare equality component wise. +The LINQ Join operator allows you to connect two data sources based on the key selector for each source, generating a tuple of values when the key matches. It naturally translates to `INNER JOIN` on relational databases. While the LINQ Join has outer and inner key selectors, the database requires a single join condition. So EF Core generates a join condition by comparing the outer key selector to the inner key selector for equality. [!code-csharp[Main](../../../samples/core/Querying/ComplexQuery/Program.cs#Join)] @@ -24,6 +24,16 @@ FROM [PersonPhoto] AS [p0] INNER JOIN [Person] AS [p] ON [p0].[PersonPhotoId] = [p].[PhotoId] ``` +Further, if the key selectors are anonymous types, EF Core generates a join condition to compare equality component-wise. + +[!code-csharp[Main](../../../samples/core/Querying/ComplexQuery/Program.cs#JoinComposite)] + +```sql +SELECT [p].[PersonId], [p].[Name], [p].[PhotoId], [p0].[PersonPhotoId], [p0].[Caption], [p0].[Photo] +FROM [PersonPhoto] AS [p0] +INNER JOIN [Person] AS [p] ON ([p0].[PersonPhotoId] = [p].[PhotoId] AND ([p0].[Caption] = N'SN')) +``` + ## GroupJoin The LINQ GroupJoin operator allows you to connect two data sources similar to Join, but it creates a group of inner values for matching outer elements. Executing a query like the following example generates a result of `Blog` & `IEnumerable`. Since databases (especially relational databases) don't have a way to represent a collection of client-side objects, GroupJoin doesn't translate to the server in many cases. It requires you to get all of the data from the server to do GroupJoin without a special selector (first query below). But if the selector is limiting data being selected then fetching all of the data from the server may cause performance issues (second query below). That's why EF Core doesn't translate GroupJoin. diff --git a/entity-framework/core/querying/filters.md b/entity-framework/core/querying/filters.md index b61c813754..3c61b63c8a 100644 --- a/entity-framework/core/querying/filters.md +++ b/entity-framework/core/querying/filters.md @@ -41,6 +41,23 @@ The predicate expressions passed to the `HasQueryFilter` calls will now automati You can also use navigations in defining global query filters. Using navigations in query filter will cause query filters to be applied recursively. When EF Core expands navigations used in query filters, it will also apply query filters defined on referenced entities. +To illustrate this configure query filters in `OnModelCreating` in the following way: +[!code-csharp[Main](../../../samples/core/Querying/QueryFilters/FilteredBloggingContextRequired.cs#NavigationInFilter)] + +Next, query for all `Blog` entities: +[!code-csharp[Main](../../../samples/core/Querying/QueryFilters/FilteredBloggingContextRequired.cs#QueriesNavigation)] + +This query produces the following SQL, which applies query filters defined for both `Blog` and `Post` entities: + +```sql +SELECT [b].[BlogId], [b].[Name], [b].[Url] +FROM [Blogs] AS [b] +WHERE ( + SELECT COUNT(*) + FROM [Posts] AS [p] + WHERE ([p].[Title] LIKE N'%fish%') AND ([b].[BlogId] = [p].[BlogId])) > 0 +``` + > [!NOTE] > Currently EF Core does not detect cycles in global query filter definitions, so you should be careful when defining them. If specified incorrectly, cycles could lead to infinite loops during query translation. diff --git a/entity-framework/core/querying/related-data/eager.md b/entity-framework/core/querying/related-data/eager.md index 7b36b71fe5..9ce1bf0de0 100644 --- a/entity-framework/core/querying/related-data/eager.md +++ b/entity-framework/core/querying/related-data/eager.md @@ -46,7 +46,7 @@ You may want to include multiple related entities for one of the entities that i > [!NOTE] > This feature is introduced in EF Core 5.0. -When applying Include to load related data, you can apply certain enumerable operations on the included collection navigation, which allows for filtering and sorting of the results. +When applying Include to load related data, you can add certain enumerable operations to the included collection navigation, which allows for filtering and sorting of the results. Supported operations are: `Where`, `OrderBy`, `OrderByDescending`, `ThenBy`, `ThenByDescending`, `Skip`, and `Take`. @@ -74,6 +74,9 @@ var orders = context.Orders.Where(o => o.Id > 1000).ToList(); var filtered = context.Customers.Include(c => c.Orders.Where(o => o.Id > 5000)).ToList(); ``` +> [!NOTE] +> In case of tracking queries, the navigation on which filtered include was applied is considered to be loaded. This means that EF Core will not attempt to re-load it's values using [explicit loading](xref:core/querying/related-data/explicit) or [lazy loading](xref:core/querying/related-data/lazy), even though some elements could still be missing. + ## Include on derived types You can include related data from navigation defined only on a derived type using `Include` and `ThenInclude`. diff --git a/entity-framework/core/what-is-new/ef-core-5.0/breaking-changes.md b/entity-framework/core/what-is-new/ef-core-5.0/breaking-changes.md index 3bee7cf889..869d0d3b23 100644 --- a/entity-framework/core/what-is-new/ef-core-5.0/breaking-changes.md +++ b/entity-framework/core/what-is-new/ef-core-5.0/breaking-changes.md @@ -27,6 +27,8 @@ The following API and behavior changes have the potential to break existing appl | [Provider-specific EF.Functions methods throw for InMemory provider](#no-client-methods) | Low | | [IndexBuilder.HasName is now obsolete](#index-obsolete) | Low | | [A pluarlizer is now included for scaffolding reverse engineered models](#pluralizer) | Low | +| [Some queries with correlated collection that also use `Distinct` or `GroupBy` are no longer supported](#collection-distinct-groupby) | Low | +| [Using a collection of Queryable type in projection is not supported](#queryable-projection) | Low | ## Medium-impact changes @@ -390,3 +392,90 @@ Using plural forms of words for collection properties and singular forms for typ #### Mitigations To disable the pluralizer, use the `--no-pluralize` option on `dotnet ef dbcontext scaffold` or the `-NoPluralize` switch on `Scaffold-DbContext`. + + + +### Some queries with correlated collection that also use `Distinct` or `GroupBy` are no longer supported + +[Tracking Issue #15873](https://github.com/dotnet/efcore/issues/15873) + +**Old behavior** + +Previously, queries involving correlated collections followed by `GroupBy`, as well as some queries using `Distinct` we allowed to execute. + +GroupBy example: + +```csharp +context.Parents + .Select(p => p.Children + .GroupBy(c => c.School) + .Select(g => g.Key)) +``` + +`Distinct` example - specifically `Distinct` queries where inner collection projection doesn't contain the primary key: + +```csharp +context.Parents + .Select(p => p.Children + .Select(c => c.School) + .Distinct()) +``` + +These queries could return incorrect results if the inner collection contained any duplicates, but worked correctly if all the elements in the inner collection were unique. + +**New behavior** + +These queries are no loger suppored. Exception is thrown indicating that we don't have enough information to correctly build the results. + +**Why** + +For correlated collection scenarios we need to know entity's primary key in order to assign collection entities to the correct parent. When inner collection doesn't use `GroupBy` or `Distinct`, the missing primary key can simply be added to the projection. However in case of `GroupBy` and `Distinct` it can't be done because it would change the result of `GroupBy` or `Distinct` operation. + +**Mitigations** + +Rewrite the query to not use `GroupBy` or `Distinct` operations on the inner collection, and perform these operations on the client instead. + +```csharp +context.Parents + .Select(p => p.Children.Select(c => c.School)) + .ToList() + .Select(x => x.GroupBy(c => c).Select(g => g.Key)) +``` + +```csharp +context.Parents + .Select(p => p.Children.Select(c => c.School)) + .ToList() + .Select(x => x.Distinct()) +``` + + + +### Using a collection of Queryable type in projection is not supported + +[Tracking Issue #16314](https://github.com/dotnet/efcore/issues/16314) + +**Old behavior** + +Previously, it was possible to use collection of a Queryable type inside the projection in some cases, for example as an argument to a `List` constructor: + +```csharp +context.Blogs + .Select(b => new List(context.Posts.Where(p => p.BlogId == b.Id))) +``` + +**New behavior** + +These queries are no loger suppored. Exception is thrown indicating that we can't create an object of Queryable type and suggesting how this could be fixed. + +**Why** + +We can't materialize an object of a Queryable type, so they would automatically be created using `List` type instead. This would often cause an exception due to type mismatch which was not very clear and could be surprising to some users. We decided to recognize the pattern and throw a more meaningful exception. + +**Mitigations** + +Add `ToList()` call after the Queryable object in the projection: + +```csharp +context.Blogs.Select(b => context.Posts.Where(p => p.BlogId == b.Id).ToList()) +``` diff --git a/samples/core/Querying/ComplexQuery/Program.cs b/samples/core/Querying/ComplexQuery/Program.cs index 2ca855b4f3..cf09b76d26 100644 --- a/samples/core/Querying/ComplexQuery/Program.cs +++ b/samples/core/Querying/ComplexQuery/Program.cs @@ -17,6 +17,17 @@ on photo.PersonPhotoId equals person.PhotoId #endregion } + using (var context = new BloggingContext()) + { + #region JoinComposite + var query = from photo in context.Set() + join person in context.Set() + on new { Id = (int?)photo.PersonPhotoId, Caption = photo.Caption } + equals new { Id = person.PhotoId, Caption = "SN" } + select new { person, photo }; + #endregion + } + using (var context = new BloggingContext()) { #region GroupJoin diff --git a/samples/core/Querying/QueryFilters/FilteredBloggingContextRequired.cs b/samples/core/Querying/QueryFilters/FilteredBloggingContextRequired.cs index 82f953b01b..b67a20ee8f 100644 --- a/samples/core/Querying/QueryFilters/FilteredBloggingContextRequired.cs +++ b/samples/core/Querying/QueryFilters/FilteredBloggingContextRequired.cs @@ -34,6 +34,14 @@ protected override void OnModelCreating(ModelBuilder modelBuilder) modelBuilder.Entity().HasQueryFilter(b => b.Url.Contains("fish")); #endregion } + else if (setup == "NavigationInFilter") + { + #region NavigationInFilter + modelBuilder.Entity().HasMany(b => b.Posts).WithOne(p => p.Blog); + modelBuilder.Entity().HasQueryFilter(b => b.Posts.Count > 0); + modelBuilder.Entity().HasQueryFilter(p => p.Title.Contains("fish")); + #endregion + } else { // The relationship is still required but there is a matching filter configured on dependent side too, @@ -44,10 +52,6 @@ protected override void OnModelCreating(ModelBuilder modelBuilder) modelBuilder.Entity().HasQueryFilter(p => p.Blog.Url.Contains("fish")); #endregion } - - - } } - } diff --git a/samples/core/Querying/QueryFilters/Program.cs b/samples/core/Querying/QueryFilters/Program.cs index efefe631b4..910e421ff0 100644 --- a/samples/core/Querying/QueryFilters/Program.cs +++ b/samples/core/Querying/QueryFilters/Program.cs @@ -12,6 +12,7 @@ static void Main(string[] args) QueryFiltersBasicExample(); QueryFiltersWithNavigationsExample(); QueryFiltersWithRequiredNavigationExample(); + QueryFiltersUsingNavigationExample(); } static void QueryFiltersBasicExample() @@ -260,5 +261,67 @@ private static void QueryFiltersWithRequiredNavigationExample() } } } + + private static void QueryFiltersUsingNavigationExample() + { + using (var db = new FilteredBloggingContextRequired()) + { + db.Database.EnsureDeleted(); + db.Database.EnsureCreated(); + + #region SeedDataNavigation + db.Blogs.Add( + new Blog + { + Url = "http://sample.com/blogs/fish", + Posts = new List + { + new Post { Title = "Fish care 101" }, + new Post { Title = "Caring for tropical fish" }, + new Post { Title = "Types of ornamental fish" } + } + }); + + db.Blogs.Add( + new Blog + { + Url = "http://sample.com/blogs/cats", + Posts = new List + { + new Post { Title = "Cat care 101" }, + new Post { Title = "Caring for tropical cats" }, + new Post { Title = "Types of ornamental cats" } + } + }); + + db.Blogs.Add( + new Blog + { + Url = "http://sample.com/blogs/catfish", + Posts = new List + { + new Post { Title = "Catfish care 101" }, + new Post { Title = "History of the catfish name" } + } + }); + #endregion + + db.SaveChanges(); + } + + Console.WriteLine("Query filters using navigations demo"); + using (var db = new FilteredBloggingContextRequired()) + { + #region QueriesNavigation + var filteredBlogs = db.Blogs.ToList(); + #endregion + var filteredBlogsInclude = db.Blogs.Include(b => b.Posts).ToList(); + if (filteredBlogs.Count == 2 && filteredBlogsInclude.Count == 2) + { + Console.WriteLine("Blogs without any Posts are also filtered out. Posts must contain 'fish' in title."); + Console.WriteLine("Filters are applied recursively, so Blogs that do have Posts, but those Posts don't contain 'fish' in the title will also be filtered out."); + } + } + } } } diff --git a/samples/core/Querying/RawSQL/BloggingContext.cs b/samples/core/Querying/RawSQL/BloggingContext.cs index 75a50dc98e..03abc620c3 100644 --- a/samples/core/Querying/RawSQL/BloggingContext.cs +++ b/samples/core/Querying/RawSQL/BloggingContext.cs @@ -12,6 +12,13 @@ protected override void OnModelCreating(ModelBuilder modelBuilder) .HasData( new Blog { BlogId = 1, Url = @"https://devblogs.microsoft.com/dotnet", Rating = 5 }, new Blog { BlogId = 2, Url = @"https://mytravelblog.com/", Rating = 4 }); + + modelBuilder.Entity() + .HasData( + new Post { PostId = 1, BlogId = 1, Title = "What's new", Content = "Lorem ipsum dolor sit amet", Rating = 5 }, + new Post { PostId = 2, BlogId = 2, Title = "Around the World in Eighty Days", Content = "consectetur adipiscing elit", Rating = 5 }, + new Post { PostId = 3, BlogId = 2, Title = "Glamping *is* the way", Content = "sed do eiusmod tempor incididunt", Rating = 4 }, + new Post { PostId = 4, BlogId = 2, Title = "Travel in the time of pandemic", Content = "ut labore et dolore magna aliqua", Rating = 3 }); } protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder) diff --git a/samples/core/Querying/RawSQL/Program.cs b/samples/core/Querying/RawSQL/Program.cs index dc31badf5d..ef0a0d4651 100644 --- a/samples/core/Querying/RawSQL/Program.cs +++ b/samples/core/Querying/RawSQL/Program.cs @@ -8,6 +8,32 @@ class Program { static void Main(string[] args) { + using (var context = new BloggingContext()) + { + context.Database.EnsureDeleted(); + context.Database.EnsureCreated(); + + context.Database.ExecuteSqlRaw( + @"create function [dbo].[SearchBlogs] (@searchTerm nvarchar(max)) + returns @found table + ( + BlogId int not null, + Url nvarchar(max), + Rating int + ) + as + begin + insert into @found + select * from dbo.Blogs as b + where exists ( + select 1 + from [Post] as [p] + where ([b].[BlogId] = [p].[BlogId]) and (charindex(@searchTerm, [p].[Title]) > 0)) + + return + end"); + } + using (var context = new BloggingContext()) { #region FromSqlRaw @@ -73,7 +99,7 @@ static void Main(string[] args) using (var context = new BloggingContext()) { #region FromSqlInterpolatedComposed - var searchTerm = ".NET"; + var searchTerm = "Lorem ipsum"; var blogs = context.Blogs .FromSqlInterpolated($"SELECT * FROM dbo.SearchBlogs({searchTerm})") @@ -86,7 +112,7 @@ static void Main(string[] args) using (var context = new BloggingContext()) { #region FromSqlInterpolatedAsNoTracking - var searchTerm = ".NET"; + var searchTerm = "Lorem ipsum"; var blogs = context.Blogs .FromSqlInterpolated($"SELECT * FROM dbo.SearchBlogs({searchTerm})") @@ -98,7 +124,7 @@ static void Main(string[] args) using (var context = new BloggingContext()) { #region FromSqlInterpolatedInclude - var searchTerm = ".NET"; + var searchTerm = "Lorem ipsum"; var blogs = context.Blogs .FromSqlInterpolated($"SELECT * FROM dbo.SearchBlogs({searchTerm})")