Skip to content

Commit

Permalink
Fixing several doc issues.
Browse files Browse the repository at this point in the history
Fixes #486
Fixes #756
Fixes #1311
Fixes #2307
Fixes #2511
Fixes #2731
Fixes #2823
  • Loading branch information
maumar committed Nov 6, 2020
1 parent 1f71a8e commit 2fe7ca2
Show file tree
Hide file tree
Showing 13 changed files with 251 additions and 9 deletions.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
12 changes: 12 additions & 0 deletions entity-framework/core/modeling/owned-entities.md
Original file line number Diff line number Diff line change
Expand Up @@ -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]
Expand Down Expand Up @@ -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).
Expand Down Expand Up @@ -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`:
Expand Down
12 changes: 11 additions & 1 deletion entity-framework/core/querying/complex-query-operators.md
Original file line number Diff line number Diff line change
Expand Up @@ -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)]

Expand All @@ -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<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.
Expand Down
17 changes: 17 additions & 0 deletions entity-framework/core/querying/filters.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
5 changes: 4 additions & 1 deletion entity-framework/core/querying/related-data/eager.md
Original file line number Diff line number Diff line change
Expand Up @@ -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`.

Expand Down Expand Up @@ -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`.
Expand Down
89 changes: 89 additions & 0 deletions entity-framework/core/what-is-new/ef-core-5.0/breaking-changes.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down Expand Up @@ -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`.

<a name="collection-distinct-groupby"></a>

### 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())
```

<a name="queryable-projection"></a>

### 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<T>` constructor:

```csharp
context.Blogs
.Select(b => new List<Post>(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<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.

**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())
```
11 changes: 11 additions & 0 deletions samples/core/Querying/ComplexQuery/Program.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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<PersonPhoto>()
join person in context.Set<Person>()
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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,14 @@ protected override void OnModelCreating(ModelBuilder modelBuilder)
modelBuilder.Entity<Blog>().HasQueryFilter(b => b.Url.Contains("fish"));
#endregion
}
else if (setup == "NavigationInFilter")
{
#region NavigationInFilter
modelBuilder.Entity<Blog>().HasMany(b => b.Posts).WithOne(p => p.Blog);
modelBuilder.Entity<Blog>().HasQueryFilter(b => b.Posts.Count > 0);
modelBuilder.Entity<Post>().HasQueryFilter(p => p.Title.Contains("fish"));
#endregion
}
else
{
// The relationship is still required but there is a matching filter configured on dependent side too,
Expand All @@ -44,10 +52,6 @@ protected override void OnModelCreating(ModelBuilder modelBuilder)
modelBuilder.Entity<Post>().HasQueryFilter(p => p.Blog.Url.Contains("fish"));
#endregion
}



}
}

}
63 changes: 63 additions & 0 deletions samples/core/Querying/QueryFilters/Program.cs
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ static void Main(string[] args)
QueryFiltersBasicExample();
QueryFiltersWithNavigationsExample();
QueryFiltersWithRequiredNavigationExample();
QueryFiltersUsingNavigationExample();
}

static void QueryFiltersBasicExample()
Expand Down Expand Up @@ -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<Post>
{
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<Post>
{
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<Post>
{
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.");
}
}
}
}
}
7 changes: 7 additions & 0 deletions samples/core/Querying/RawSQL/BloggingContext.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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<Post>()
.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)
Expand Down
32 changes: 29 additions & 3 deletions samples/core/Querying/RawSQL/Program.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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})")
Expand All @@ -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})")
Expand All @@ -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})")
Expand Down

0 comments on commit 2fe7ca2

Please sign in to comment.