Skip to content

Commit ea38da8

Browse files
authored
Documentation for preview6 (#5047)
1 parent 3463369 commit ea38da8

File tree

15 files changed

+554
-582
lines changed

15 files changed

+554
-582
lines changed

entity-framework/core/providers/sql-server/functions.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -127,6 +127,7 @@ dateOnly.Day | DATEPART(day, @dat
127127
dateOnly.DayOfYear | DATEPART(dayofyear, @dateOnly) | EF Core 8.0
128128
dateOnly.Month | DATEPART(month, @dateOnly) | EF Core 8.0
129129
dateOnly.Year | DATEPART(year, @dateOnly) | EF Core 8.0
130+
dateOnly.DayNumber | DATEDIFF(day, '0001-01-01', @dateOnly) | EF Core 10.0
130131
EF.Functions.AtTimeZone(dateTime, timeZone) | @dateTime AT TIME ZONE @timeZone | EF Core 7.0
131132
EF.Functions.DateDiffDay(start, end) | DATEDIFF(day, @start, @end)
132133
EF.Functions.DateDiffHour(start, end) | DATEDIFF(hour, @start, @end)

entity-framework/core/providers/sqlite/functions.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -72,6 +72,7 @@ dateOnly.DayOfYear | strftime('%j', @dateOnly)
7272
DateOnly.FromDateTime(dateTime) | date(@dateTime) | EF Core 8.0
7373
dateOnly.Month | strftime('%m', @dateOnly)
7474
dateOnly.Year | strftime('%Y', @dateOnly)
75+
dateOnly.DayNumber | CAST(julianday(@dateOnly) - julianday('0001-01-01') AS INTEGER) | EF Core 10.0
7576
DateTime.Now | datetime('now', 'localtime')
7677
DateTime.Today | datetime('now', 'localtime', 'start of day')
7778
DateTime.UtcNow | datetime('now')

entity-framework/core/querying/filters.md

Lines changed: 92 additions & 53 deletions
Original file line numberDiff line numberDiff line change
@@ -7,83 +7,115 @@ uid: core/querying/filters
77
---
88
# Global Query Filters
99

10-
Global query filters are LINQ query predicates applied to Entity Types in the metadata model (usually in `OnModelCreating`). A query predicate is a boolean expression typically passed to the LINQ `Where` query operator. EF Core applies such filters automatically to any LINQ queries involving those Entity Types. EF Core also applies them to Entity Types, referenced indirectly through use of Include or navigation property. Some common applications of this feature are:
10+
Global query filters allow attaching a filter to an entity type and having that filter applied whenever a query on that entity type is executed; think of them as an additional LINQ `Where` operator that's added whenever the entity type is queried. Such filters are useful in a variety of cases.
1111

12-
* **Soft delete** - An Entity Type defines an `IsDeleted` property.
13-
* **Multi-tenancy** - An Entity Type defines a `TenantId` property.
12+
> [!TIP]
13+
> You can view this article's [sample](https://github.com/dotnet/EntityFramework.Docs/tree/live/samples/core/Querying/QueryFilters) on GitHub.
1414
15-
## Example
15+
## Basic example - soft deletion
1616

17-
The following example shows how to use Global Query Filters to implement multi-tenancy and soft-delete query behaviors in a simple blogging model.
17+
In some scenarios, rather than deleting a row from the database, it's preferable to instead set an `IsDeleted` flag to mark the row as deleted; this pattern is called *soft deletion*. Soft deletion allows rows to be undeleted if needed, or to preserve an audit trail where deleted rows are still accessible. Global query filters can be used to filter out soft-deleted rows by default, while still allowing you to access them in specific places by disabling the filter for a specific query.
1818

19-
> [!TIP]
20-
> You can view this article's [sample](https://github.com/dotnet/EntityFramework.Docs/tree/live/samples/core/Querying/QueryFilters) on GitHub.
19+
To enable soft deletion, let's add an `IsDeleted` property to our Blog type:
2120

22-
> [!NOTE]
23-
> Multi-tenancy is used here as a simple example. There is also an article with comprehensive guidance for [multi-tenancy in EF Core applications](xref:core/miscellaneous/multitenancy).
21+
[!code-csharp[Main](../../../samples/core/Querying/QueryFilters/SoftDeletion.cs#Blog)]
2422

25-
First, define the entities:
23+
We now set up a global query filter, using the <xref:Microsoft.EntityFrameworkCore.Metadata.Builders.EntityTypeBuilder`1.HasQueryFilter*> API in `OnModelCreating`:
2624

27-
[!code-csharp[Main](../../../samples/core/Querying/QueryFilters/Entities.cs#Entities)]
25+
[!code-csharp[Main](../../../samples/core/Querying/QueryFilters/SoftDeletion.cs#FilterConfiguration)]
2826

29-
Note the declaration of a `_tenantId` field on the `Blog` entity. This field will be used to associate each Blog instance with a specific tenant. Also defined is an `IsDeleted` property on the `Post` entity type. This property is used to keep track of whether a post instance has been "soft-deleted". That is, the instance is marked as deleted without physically removing the underlying data.
27+
We can now query our `Blog` entities as usual; the configured filter will ensure that all queries will - by default - filter out all instances where `IsDeleted` is true.
3028

31-
Next, configure the query filters in `OnModelCreating` using the `HasQueryFilter` API.
29+
Note that at this point, you must manually set `IsDeleted` in order to soft-delete an entity. For a more end-to-end solution, you can override your context type's `SaveChangesAsync` method to add logic which goes over all entities which the user deleted, and changes them to be modified instead, setting the `IsDeleted` property to true:
3230

33-
[!code-csharp[Main](../../../samples/core/Querying/QueryFilters/BloggingContext.cs#FilterConfiguration)]
31+
[!code-csharp[Main](../../../samples/core/Querying/QueryFilters/SoftDeletion.cs#SaveChangesAsyncOverride)]
3432

35-
The predicate expressions passed to the `HasQueryFilter` calls will now automatically be applied to any LINQ queries for those types.
33+
This allows you to use EF APIs that delete an entity instance as usual and have them get soft-deleted instead.
3634

37-
> [!TIP]
38-
> Note the use of a DbContext instance level field: `_tenantId` used to set the current tenant. Model-level filters will use the value from the correct context instance (that is, the instance that is executing the query).
35+
## Using context data - multi-tenancy
3936

40-
> [!NOTE]
41-
> It is currently not possible to define multiple query filters on the same entity - only the last one will be applied. However, you can define a single filter with multiple conditions using the logical `AND` operator ([`&&` in C#](/dotnet/csharp/language-reference/operators/boolean-logical-operators#conditional-logical-and-operator-)).
37+
Another mainstream scenario for global query filters is *multi-tenancy*, where your application stores data belonging to different users in the same table. In such cases, there's usually a *tenant ID* column which associates the row to a specific tenant, and global query filters can be used to automatically filter for the rows of the current tenant. This provides strong tenant isolation for your queries by default, removing the need to think of filtering for the tenant in each and every query.
4238

43-
## Use of navigations
39+
Unlike with soft deletion, multi-tenancy requires knowing the *current* tenant ID; this value is usually determined e.g. when the user authenticates over the web. For EF's purposes, the tenant ID must be available on the context instance, so that the global query filter can refer to it and use it when querying. Let's accept a `tenantId` parameter in our context type's constructor, and reference that from our filter:
4440

45-
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.
41+
```c#
42+
public class MultitenancyContext(string tenantId) : DbContext
43+
{
44+
protected override void OnModelCreating(ModelBuilder modelBuilder)
45+
{
46+
modelBuilder.Entity<Blog>().HasQueryFilter(b => b.TenantId == tenantId);
47+
}
48+
}
49+
```
50+
51+
This forces anyone constructing a context to specify its associated tenant ID, and ensures that only `Blog` entities with that ID are returned from queries by default.
4652

47-
To illustrate this configure query filters in `OnModelCreating` in the following way:
48-
[!code-csharp[Main](../../../samples/core/Querying/QueryFilters/FilteredBloggingContextRequired.cs#NavigationInFilter)]
53+
> [!NOTE]
54+
> This sample only showed basic multi-tenancy concepts needed in order to demonstrate global query filters. For more information on multi-tenancy and EF, see [multi-tenancy in EF Core applications](xref:core/miscellaneous/multitenancy).
4955
50-
Next, query for all `Blog` entities:
51-
[!code-csharp[Main](../../../samples/core/Querying/QueryFilters/Program.cs#QueriesNavigation)]
56+
## Using multiple query filters
5257

53-
This query produces the following SQL, which applies query filters defined for both `Blog` and `Post` entities:
58+
Calling <xref:Microsoft.EntityFrameworkCore.Metadata.Builders.EntityTypeBuilder`1.HasQueryFilter*> with a simple filter overwrites any previous filter, so multiple filters **cannot** be defined on the same entity type in this way:
5459

55-
```sql
56-
SELECT [b].[BlogId], [b].[Name], [b].[Url]
57-
FROM [Blogs] AS [b]
58-
WHERE (
59-
SELECT COUNT(*)
60-
FROM [Posts] AS [p]
61-
WHERE ([p].[Title] LIKE N'%fish%') AND ([b].[BlogId] = [p].[BlogId])) > 0
60+
```c#
61+
modelBuilder.Entity<Blog>().HasQueryFilter(b => !b.IsDeleted);
62+
// The following overwrites the previous query filter:
63+
modelBuilder.Entity<Blog>().HasQueryFilter(b => b.TenantId == tenantId);
6264
```
6365

66+
### [EF 10+](#tab/ef10)
67+
6468
> [!NOTE]
65-
> 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.
69+
> This feature is being introduced in EF Core 10.0 (in preview).
70+
71+
In order to define multiple query filters on the same entity type, they must be *named*:
72+
73+
[!code-csharp[Main](../../../samples/core/Querying/QueryFilters/NamedFilters.cs#FilterConfiguration)]
6674

67-
## Accessing entity with query filter using required navigation
75+
This allows you to manage each filter separately, including selectively disabling one but not the other.
76+
77+
### [Older versions](#tab/older)
78+
79+
Prior to EF 10, you can attach multiple filters to an entity type by calling <xref:Microsoft.EntityFrameworkCore.Metadata.Builders.EntityTypeBuilder`1.HasQueryFilter*> once and combining your filters using the `&&` operator:
80+
81+
```c#
82+
modelBuilder.Entity<Blog>().HasQueryFilter(b => !b.IsDeleted && b.TenantId == tenantId);
83+
```
84+
85+
This unfortunately does not allow to selectively disable a single filter.
86+
87+
***
88+
89+
## Disabling filters
90+
91+
Filters may be disabled for individual LINQ queries by using the <xref:Microsoft.EntityFrameworkCore.EntityFrameworkQueryableExtensions.IgnoreQueryFilters*> operator:
92+
93+
[!code-csharp[Main](../../../samples/core/Querying/QueryFilters/SoftDeletion.cs#DisableFilter)]
94+
95+
If multiple named filters are configured, this disables all of them. To selectively disable specific filters (starting with EF 10), pass the list of filter names to be disabled:
96+
97+
[!code-csharp[Main](../../../samples/core/Querying/QueryFilters/NamedFilters.cs#DisableSoftDeletionFilter)]
98+
99+
## Query filters and required navigations
68100

69101
> [!CAUTION]
70102
> Using required navigation to access entity which has global query filter defined may lead to unexpected results.
71103
72-
Required navigation expects the related entity to always be present. If necessary related entity is filtered out by the query filter, the parent entity wouldn't be in result either. So you may get fewer elements than expected in result.
104+
Required navigations in EF imply that the related entity is always present. Since inner joins may be used to fetch related entities, if a required related entity is filtered out by the query filter, the parent entity may get filtered out as well. This can result in unexpectedly retrieving fewer elements than expected.
73105

74-
To illustrate the problem, we can use the `Blog` and `Post` entities specified above and the following `OnModelCreating` method:
106+
To illustrate the problem, we can use `Blog` and `Post` entities and configure them as follows:
75107

76-
[!code-csharp[Main](../../../samples/core/Querying/QueryFilters/FilteredBloggingContextRequired.cs#IncorrectFilter)]
108+
[!code-csharp[Main](../../../samples/core/Querying/QueryFilters/QueryFiltersAndRequiredNavigations.cs#IncorrectFilter)]
77109

78110
The model can be seeded with the following data:
79111

80-
[!code-csharp[Main](../../../samples/core/Querying/QueryFilters/Program.cs#SeedData)]
112+
[!code-csharp[Main](../../../samples/core/Querying/QueryFilters/QueryFiltersAndRequiredNavigations.cs#SeedData)]
81113

82-
The problem can be observed when executing two queries:
114+
The problem can be observed when executing the following two queries:
83115

84-
[!code-csharp[Main](../../../samples/core/Querying/QueryFilters/Program.cs#Queries)]
116+
[!code-csharp[Main](../../../samples/core/Querying/QueryFilters/QueryFiltersAndRequiredNavigations.cs#Queries)]
85117

86-
With above setup, the first query returns all 6 `Post`s, however the second query only returns 3. This mismatch happens because `Include` method in the second query loads the related `Blog` entities. Since the navigation between `Blog` and `Post` is required, EF Core uses `INNER JOIN` when constructing the query:
118+
With the above setup, the first query returns all 6 `Post` instances, but the second query returns only 3. This mismatch occurs because the `Include` method in the second query loads the related `Blog` entities. Since the navigation between `Blog` and `Post` is required, EF Core uses `INNER JOIN` when constructing the query:
87119

88120
```sql
89121
SELECT [p].[PostId], [p].[BlogId], [p].[Content], [p].[IsDeleted], [p].[Title], [t].[BlogId], [t].[Name], [t].[Url]
@@ -95,26 +127,33 @@ INNER JOIN (
95127
) AS [t] ON [p].[BlogId] = [t].[BlogId]
96128
```
97129

98-
Use of the `INNER JOIN` filters out all `Post`s whose related `Blog`s have been removed by a global query filter.
130+
Use of the `INNER JOIN` filters out all `Post` rows whose related `Blog` rows have been filtered out by a query filter. This problem can be addressed by configuring the navigation as optional navigation instead of required, causing EF to generate a `LEFT JOIN` instead of an `INNER JOIN`:
99131

100-
It can be addressed by using optional navigation instead of required.
101-
This way the first query stays the same as before, however the second query will now generate `LEFT JOIN` and return 6 results.
132+
[!code-csharp[Main](../../../samples/core/Querying/QueryFilters/QueryFiltersAndRequiredNavigations.cs#OptionalNavigation)]
102133

103-
[!code-csharp[Main](../../../samples/core/Querying/QueryFilters/FilteredBloggingContextRequired.cs#OptionalNavigation)]
134+
An alternative approach is to specify consistent filters on both `Blog` and `Post` entity types; once matching filters are applied to both `Blog` and `Post`, `Post` rows that could end up in unexpected state are removed and both queries return 3 results.
104135

105-
Alternative approach is to specify consistent filters on both `Blog` and `Post` entities.
106-
This way matching filters are applied to both `Blog` and `Post`. `Post`s that could end up in unexpected state are removed and both queries return 3 results.
136+
[!code-csharp[Main](../../../samples/core/Querying/QueryFilters/QueryFiltersAndRequiredNavigations.cs#MatchingFilters)]
107137

108-
[!code-csharp[Main](../../../samples/core/Querying/QueryFilters/FilteredBloggingContextRequired.cs#MatchingFilters)]
138+
## Query filters and IEntityTypeConfiguration
109139

110-
## Disabling Filters
140+
If your query filter needs to access a tenant ID or similar contextual information, <xref:Microsoft.EntityFrameworkCore.IEntityTypeConfiguration`1> can pose an additional complication as unlike with `OnModelCreating`, there's no instance of your context type readily available to reference from the query filter. As a workaround, add a dummy context to your configuration type and reference that as follows:
111141

112-
Filters may be disabled for individual LINQ queries by using the <xref:Microsoft.EntityFrameworkCore.EntityFrameworkQueryableExtensions.IgnoreQueryFilters*> operator.
142+
```c#
143+
private sealed class CustomerEntityConfiguration : IEntityTypeConfiguration<Customer>
144+
{
145+
private readonly SomeDbContext _context == null!;
113146

114-
[!code-csharp[Main](../../../samples/core/Querying/QueryFilters/Program.cs#IgnoreFilters)]
147+
public void Configure(EntityTypeBuilder<Customer> builder)
148+
{
149+
builder.HasQueryFilter(d => d.TenantId == _context.TenantId);
150+
}
151+
}
152+
```
115153

116154
## Limitations
117155

118156
Global query filters have the following limitations:
119157

120-
* Filters can only be defined for the root Entity Type of an inheritance hierarchy.
158+
* Filters can only be defined for the root entity type of an inheritance hierarchy.
159+
* 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.

entity-framework/core/what-is-new/ef-core-10.0/whatsnew.md

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,22 @@ EF10 requires the .NET 10 SDK to build and requires the .NET 10 runtime to run.
1919

2020
<a name="cosmos"></a>
2121

22+
## Named query filters
23+
24+
EF's [global query filters](xref:core/querying/filters) feature has long enabled users to configuring filters to entity types which apply to all queries by default. This has simplified implementing common patterns and scenarios such as soft deletion, multitenancy and others. However, up to now EF has only supported a single query filter per entity type, making it difficult to have multiple filters and selectively disabling only some of them in specific queries.
25+
26+
EF 10 introduces *named query filters*, which allow attaching names to query filter and managing each one separately:
27+
28+
[!code-csharp[Main](../../../../samples/core/Querying/QueryFilters/NamedFilters.cs#FilterConfiguration)]
29+
30+
This notably allows disabling only certain filters in a specific LINQ query:
31+
32+
[!code-csharp[Main](../../../../samples/core/Querying/QueryFilters/NamedFilters.cs#DisableSoftDeletionFilter)]
33+
34+
For more information on named query filters, see the [documentation](xref:core/querying/filters).
35+
36+
This feature was contributed by [@bittola](https://github.com/bittola).
37+
2238
## Azure Cosmos DB for NoSQL
2339

2440
<a name="full-text-search-support"></a>
@@ -130,7 +146,8 @@ See [#12793](https://github.com/dotnet/efcore/issues/12793) and [#35367](https:/
130146

131147
### Other query improvements
132148

133-
- Translate DateOnly.ToDateTime(timeOnly) ([#35194](https://github.com/dotnet/efcore/pull/35194), contributed by [@mseada94](https://github.com/mseada94)).
149+
- Translate [DateOnly.ToDateTime()](/dotnet/api/system.dateonly.todatetime) ([#35194](https://github.com/dotnet/efcore/pull/35194), contributed by [@mseada94](https://github.com/mseada94)).
150+
- Translate [DateOnly.DayNumber](/dotnet/api/system.dateonly.daynumber) and `DayNumber` subtraction for SQL Server and SQLite ([#36183](https://github.com/dotnet/efcore/issues/36183)).
134151
- Optimize multiple consecutive `LIMIT`s ([#35384](https://github.com/dotnet/efcore/pull/35384), contributed by [@ranma42](https://github.com/ranma42)).
135152
- Optimize use of `Count` operation on `ICollection<T>` ([#35381](https://github.com/dotnet/efcore/pull/35381), contributed by [@ChrisJollyAU](https://github.com/ChrisJollyAU)).
136153
- Optimize `MIN`/`MAX` over `DISTINCT` ([#34699](https://github.com/dotnet/efcore/pull/34699), contributed by [@ranma42](https://github.com/ranma42)).

samples/core/NuGet.config

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
<?xml version="1.0" encoding="utf-8"?>
2+
<configuration>
3+
<packageSources>
4+
<add key="nuget.org" value="https://api.nuget.org/v3/index.json" />
5+
<add key="dotnet10" value="https://pkgs.dev.azure.com/dnceng/public/_packaging/dotnet10/nuget/v3/index.json" />
6+
</packageSources>
7+
</configuration>

0 commit comments

Comments
 (0)