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..d53c6586c4 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..4a1734b829 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..5444862b13 Binary files /dev/null and b/entity-framework/core/modeling/_static/owned-entities-ownsone.png differ diff --git a/entity-framework/core/modeling/inheritance.md b/entity-framework/core/modeling/inheritance.md index 0855499026..efd6debfd8 100644 --- a/entity-framework/core/modeling/inheritance.md +++ b/entity-framework/core/modeling/inheritance.md @@ -31,7 +31,7 @@ By default, EF maps the inheritance using the *table-per-hierarchy* (TPH) patter The model above is mapped to the following database schema (note the implicitly-created `Discriminator` column, which identifies which type of `Blog` is stored in each row). -![image](_static/inheritance-tph-data.png) +![Screenshot of the results of querying the Blog entity hierarchy using table-per-hierarchy pattern](_static/inheritance-tph-data.png) You can configure the name and type of the discriminator column and the values that are used to identify each type in the hierarchy: diff --git a/entity-framework/core/modeling/owned-entities.md b/entity-framework/core/modeling/owned-entities.md index d8083a90d0..29516c7403 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: + +![Sceenshot of the database model for entity containing owned reference](_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: + +![Sceenshot of the database model for entity containing owned collection](_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: + +![Sceenshot of the database model for entity containing nested owned references](_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 253334805d..3f0e7006ef 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 @@ -29,6 +29,8 @@ The following API and behavior changes have the potential to break existing appl | [IndexBuilder.HasName is now obsolete](#index-obsolete) | Low | | [A pluarlizer is now included for scaffolding reverse engineered models](#pluralizer) | Low | | [INavigationBase replaces INavigation in some APIs to support skip navigations](#inavigationbase) | 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 @@ -450,3 +452,90 @@ Most of the functionality between normal and skip navigations is the same. Howev #### Mitigations In many cases applications can switch to using the new base interface with no other changes. However, in cases where the navigation is used to access foreign key properties, application code should either be constrained to only normal navigations, or updated to do the appropriate thing for both normal and skip navigations. + + + +### 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})")