Skip to content

Commit 286796f

Browse files
committed
Fixing several doc issues.
Fixes #486 Fixes #756 Fixes #1311 Fixes #2307 Fixes #2511 Fixes #2731 Fixes #2823
1 parent 81b7481 commit 286796f

File tree

14 files changed

+252
-10
lines changed

14 files changed

+252
-10
lines changed
Loading
Loading
Loading

entity-framework/core/modeling/inheritance.md

+1-1
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,7 @@ By default, EF maps the inheritance using the *table-per-hierarchy* (TPH) patter
3131

3232
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).
3333

34-
![image](_static/inheritance-tph-data.png)
34+
![Screenshot of the results of querying the Blog entity hierarchy using table-per-hierarchy pattern](_static/inheritance-tph-data.png)
3535

3636
You can configure the name and type of the discriminator column and the values that are used to identify each type in the hierarchy:
3737

entity-framework/core/modeling/owned-entities.md

+12
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,10 @@ If the `ShippingAddress` property is private in the `Order` type, you can use th
3131

3232
[!code-csharp[OwnsOneString](../../../samples/core/Modeling/OwnedEntities/OwnedEntityContext.cs?name=OwnsOneString)]
3333

34+
The model above is mapped to the following database schema:
35+
36+
![Sceenshot of the database model for entity containing owned reference](_static/owned-entities-ownsone.png)
37+
3438
See the [full sample project](https://github.com/dotnet/EntityFramework.Docs/tree/master/samples/core/Modeling/OwnedEntities) for more context.
3539

3640
> [!TIP]
@@ -63,6 +67,10 @@ To configure a different primary key call `HasKey`.
6367

6468
[!code-csharp[OwnsMany](../../../samples/core/Modeling/OwnedEntities/OwnedEntityContext.cs?name=OwnsMany)]
6569

70+
The model above is mapped to the following database schema:
71+
72+
![Sceenshot of the database model for entity containing owned collection](_static/owned-entities-ownsmany.png)
73+
6674
## Mapping owned types with table splitting
6775

6876
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
114122

115123
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).
116124

125+
The model above is mapped to the following database schema:
126+
127+
![Sceenshot of the database model for entity containing nested owned references](_static/owned-entities-nested.png)
128+
117129
## Storing owned types in separate tables
118130

119131
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`:

entity-framework/core/querying/complex-query-operators.md

+11-1
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ Language Integrated Query (LINQ) contains many complex operators, which combine
1414
1515
## Join
1616

17-
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.
17+
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.
1818

1919
[!code-csharp[Main](../../../samples/core/Querying/ComplexQuery/Program.cs#Join)]
2020

@@ -24,6 +24,16 @@ FROM [PersonPhoto] AS [p0]
2424
INNER JOIN [Person] AS [p] ON [p0].[PersonPhotoId] = [p].[PhotoId]
2525
```
2626

27+
Further, if the key selectors are anonymous types, EF Core generates a join condition to compare equality component-wise.
28+
29+
[!code-csharp[Main](../../../samples/core/Querying/ComplexQuery/Program.cs#JoinComposite)]
30+
31+
```sql
32+
SELECT [p].[PersonId], [p].[Name], [p].[PhotoId], [p0].[PersonPhotoId], [p0].[Caption], [p0].[Photo]
33+
FROM [PersonPhoto] AS [p0]
34+
INNER JOIN [Person] AS [p] ON ([p0].[PersonPhotoId] = [p].[PhotoId] AND ([p0].[Caption] = N'SN'))
35+
```
36+
2737
## GroupJoin
2838

2939
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<Post>`. 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.

entity-framework/core/querying/filters.md

+17
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,23 @@ The predicate expressions passed to the `HasQueryFilter` calls will now automati
4141

4242
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.
4343

44+
To illustrate this configure query filters in `OnModelCreating` in the following way:
45+
[!code-csharp[Main](../../../samples/core/Querying/QueryFilters/FilteredBloggingContextRequired.cs#NavigationInFilter)]
46+
47+
Next, query for all `Blog` entities:
48+
[!code-csharp[Main](../../../samples/core/Querying/QueryFilters/FilteredBloggingContextRequired.cs#QueriesNavigation)]
49+
50+
This query produces the following SQL, which applies query filters defined for both `Blog` and `Post` entities:
51+
52+
```sql
53+
SELECT [b].[BlogId], [b].[Name], [b].[Url]
54+
FROM [Blogs] AS [b]
55+
WHERE (
56+
SELECT COUNT(*)
57+
FROM [Posts] AS [p]
58+
WHERE ([p].[Title] LIKE N'%fish%') AND ([b].[BlogId] = [p].[BlogId])) > 0
59+
```
60+
4461
> [!NOTE]
4562
> 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.
4663

entity-framework/core/querying/related-data/eager.md

+4-1
Original file line numberDiff line numberDiff line change
@@ -46,7 +46,7 @@ You may want to include multiple related entities for one of the entities that i
4646
> [!NOTE]
4747
> This feature is introduced in EF Core 5.0.
4848
49-
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.
49+
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.
5050

5151
Supported operations are: `Where`, `OrderBy`, `OrderByDescending`, `ThenBy`, `ThenByDescending`, `Skip`, and `Take`.
5252

@@ -74,6 +74,9 @@ var orders = context.Orders.Where(o => o.Id > 1000).ToList();
7474
var filtered = context.Customers.Include(c => c.Orders.Where(o => o.Id > 5000)).ToList();
7575
```
7676

77+
> [!NOTE]
78+
> 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.
79+
7780
## Include on derived types
7881

7982
You can include related data from navigation defined only on a derived type using `Include` and `ThenInclude`.

entity-framework/core/what-is-new/ef-core-5.0/breaking-changes.md

+89
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,8 @@ The following API and behavior changes have the potential to break existing appl
2929
| [IndexBuilder.HasName is now obsolete](#index-obsolete) | Low |
3030
| [A pluarlizer is now included for scaffolding reverse engineered models](#pluralizer) | Low |
3131
| [INavigationBase replaces INavigation in some APIs to support skip navigations](#inavigationbase) | Low |
32+
| [Some queries with correlated collection that also use `Distinct` or `GroupBy` are no longer supported](#collection-distinct-groupby) | Low |
33+
| [Using a collection of Queryable type in projection is not supported](#queryable-projection) | Low |
3234

3335
## Medium-impact changes
3436

@@ -450,3 +452,90 @@ Most of the functionality between normal and skip navigations is the same. Howev
450452
#### Mitigations
451453

452454
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.
455+
456+
<a name="collection-distinct-groupby"></a>
457+
458+
### Some queries with correlated collection that also use `Distinct` or `GroupBy` are no longer supported
459+
460+
[Tracking Issue #15873](https://github.com/dotnet/efcore/issues/15873)
461+
462+
**Old behavior**
463+
464+
Previously, queries involving correlated collections followed by `GroupBy`, as well as some queries using `Distinct` we allowed to execute.
465+
466+
GroupBy example:
467+
468+
```csharp
469+
context.Parents
470+
.Select(p => p.Children
471+
.GroupBy(c => c.School)
472+
.Select(g => g.Key))
473+
```
474+
475+
`Distinct` example - specifically `Distinct` queries where inner collection projection doesn't contain the primary key:
476+
477+
```csharp
478+
context.Parents
479+
.Select(p => p.Children
480+
.Select(c => c.School)
481+
.Distinct())
482+
```
483+
484+
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.
485+
486+
**New behavior**
487+
488+
These queries are no loger suppored. Exception is thrown indicating that we don't have enough information to correctly build the results.
489+
490+
**Why**
491+
492+
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.
493+
494+
**Mitigations**
495+
496+
Rewrite the query to not use `GroupBy` or `Distinct` operations on the inner collection, and perform these operations on the client instead.
497+
498+
```csharp
499+
context.Parents
500+
.Select(p => p.Children.Select(c => c.School))
501+
.ToList()
502+
.Select(x => x.GroupBy(c => c).Select(g => g.Key))
503+
```
504+
505+
```csharp
506+
context.Parents
507+
.Select(p => p.Children.Select(c => c.School))
508+
.ToList()
509+
.Select(x => x.Distinct())
510+
```
511+
512+
<a name="queryable-projection"></a>
513+
514+
### Using a collection of Queryable type in projection is not supported
515+
516+
[Tracking Issue #16314](https://github.com/dotnet/efcore/issues/16314)
517+
518+
**Old behavior**
519+
520+
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<T>` constructor:
521+
522+
```csharp
523+
context.Blogs
524+
.Select(b => new List<Post>(context.Posts.Where(p => p.BlogId == b.Id)))
525+
```
526+
527+
**New behavior**
528+
529+
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.
530+
531+
**Why**
532+
533+
We can't materialize an object of a Queryable type, so they would automatically be created using `List<T>` 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.
534+
535+
**Mitigations**
536+
537+
Add `ToList()` call after the Queryable object in the projection:
538+
539+
```csharp
540+
context.Blogs.Select(b => context.Posts.Where(p => p.BlogId == b.Id).ToList())
541+
```

samples/core/Querying/ComplexQuery/Program.cs

+11
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,17 @@ on photo.PersonPhotoId equals person.PhotoId
1717
#endregion
1818
}
1919

20+
using (var context = new BloggingContext())
21+
{
22+
#region JoinComposite
23+
var query = from photo in context.Set<PersonPhoto>()
24+
join person in context.Set<Person>()
25+
on new { Id = (int?)photo.PersonPhotoId, Caption = photo.Caption }
26+
equals new { Id = person.PhotoId, Caption = "SN" }
27+
select new { person, photo };
28+
#endregion
29+
}
30+
2031
using (var context = new BloggingContext())
2132
{
2233
#region GroupJoin

samples/core/Querying/QueryFilters/FilteredBloggingContextRequired.cs

+8-4
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,14 @@ protected override void OnModelCreating(ModelBuilder modelBuilder)
3434
modelBuilder.Entity<Blog>().HasQueryFilter(b => b.Url.Contains("fish"));
3535
#endregion
3636
}
37+
else if (setup == "NavigationInFilter")
38+
{
39+
#region NavigationInFilter
40+
modelBuilder.Entity<Blog>().HasMany(b => b.Posts).WithOne(p => p.Blog);
41+
modelBuilder.Entity<Blog>().HasQueryFilter(b => b.Posts.Count > 0);
42+
modelBuilder.Entity<Post>().HasQueryFilter(p => p.Title.Contains("fish"));
43+
#endregion
44+
}
3745
else
3846
{
3947
// 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)
4452
modelBuilder.Entity<Post>().HasQueryFilter(p => p.Blog.Url.Contains("fish"));
4553
#endregion
4654
}
47-
48-
49-
5055
}
5156
}
52-
5357
}

samples/core/Querying/QueryFilters/Program.cs

+63
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ static void Main(string[] args)
1212
QueryFiltersBasicExample();
1313
QueryFiltersWithNavigationsExample();
1414
QueryFiltersWithRequiredNavigationExample();
15+
QueryFiltersUsingNavigationExample();
1516
}
1617

1718
static void QueryFiltersBasicExample()
@@ -260,5 +261,67 @@ private static void QueryFiltersWithRequiredNavigationExample()
260261
}
261262
}
262263
}
264+
265+
private static void QueryFiltersUsingNavigationExample()
266+
{
267+
using (var db = new FilteredBloggingContextRequired())
268+
{
269+
db.Database.EnsureDeleted();
270+
db.Database.EnsureCreated();
271+
272+
#region SeedDataNavigation
273+
db.Blogs.Add(
274+
new Blog
275+
{
276+
Url = "http://sample.com/blogs/fish",
277+
Posts = new List<Post>
278+
{
279+
new Post { Title = "Fish care 101" },
280+
new Post { Title = "Caring for tropical fish" },
281+
new Post { Title = "Types of ornamental fish" }
282+
}
283+
});
284+
285+
db.Blogs.Add(
286+
new Blog
287+
{
288+
Url = "http://sample.com/blogs/cats",
289+
Posts = new List<Post>
290+
{
291+
new Post { Title = "Cat care 101" },
292+
new Post { Title = "Caring for tropical cats" },
293+
new Post { Title = "Types of ornamental cats" }
294+
}
295+
});
296+
297+
db.Blogs.Add(
298+
new Blog
299+
{
300+
Url = "http://sample.com/blogs/catfish",
301+
Posts = new List<Post>
302+
{
303+
new Post { Title = "Catfish care 101" },
304+
new Post { Title = "History of the catfish name" }
305+
}
306+
});
307+
#endregion
308+
309+
db.SaveChanges();
310+
}
311+
312+
Console.WriteLine("Query filters using navigations demo");
313+
using (var db = new FilteredBloggingContextRequired())
314+
{
315+
#region QueriesNavigation
316+
var filteredBlogs = db.Blogs.ToList();
317+
#endregion
318+
var filteredBlogsInclude = db.Blogs.Include(b => b.Posts).ToList();
319+
if (filteredBlogs.Count == 2 && filteredBlogsInclude.Count == 2)
320+
{
321+
Console.WriteLine("Blogs without any Posts are also filtered out. Posts must contain 'fish' in title.");
322+
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.");
323+
}
324+
}
325+
}
263326
}
264327
}

samples/core/Querying/RawSQL/BloggingContext.cs

+7
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,13 @@ protected override void OnModelCreating(ModelBuilder modelBuilder)
1212
.HasData(
1313
new Blog { BlogId = 1, Url = @"https://devblogs.microsoft.com/dotnet", Rating = 5 },
1414
new Blog { BlogId = 2, Url = @"https://mytravelblog.com/", Rating = 4 });
15+
16+
modelBuilder.Entity<Post>()
17+
.HasData(
18+
new Post { PostId = 1, BlogId = 1, Title = "What's new", Content = "Lorem ipsum dolor sit amet", Rating = 5 },
19+
new Post { PostId = 2, BlogId = 2, Title = "Around the World in Eighty Days", Content = "consectetur adipiscing elit", Rating = 5 },
20+
new Post { PostId = 3, BlogId = 2, Title = "Glamping *is* the way", Content = "sed do eiusmod tempor incididunt", Rating = 4 },
21+
new Post { PostId = 4, BlogId = 2, Title = "Travel in the time of pandemic", Content = "ut labore et dolore magna aliqua", Rating = 3 });
1522
}
1623

1724
protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)

0 commit comments

Comments
 (0)