diff --git a/.vscode/settings.json b/.vscode/settings.json index 197a12fa7c..732fa54219 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -10,9 +10,13 @@ "XML" ], "cSpell.words": [ + "LINQ", "mitigations", "navigations", + "parameterizable", + "queryable", "savepoint", - "savepoints" + "savepoints", + "subquery" ] } diff --git a/entity-framework/core/querying/client-eval.md b/entity-framework/core/querying/client-eval.md index 230ddbd346..400b1d3450 100644 --- a/entity-framework/core/querying/client-eval.md +++ b/entity-framework/core/querying/client-eval.md @@ -13,21 +13,21 @@ As a general rule, Entity Framework Core attempts to evaluate a query on the ser > Prior to version 3.0, Entity Framework Core supported client evaluation anywhere in the query. For more information, see the [previous versions section](#previous-versions). > [!TIP] -> You can view this article's [sample](https://github.com/dotnet/EntityFramework.Docs/tree/master/samples/core/Querying) on GitHub. +> You can view this article's [sample](https://github.com/dotnet/EntityFramework.Docs/tree/master/samples/core/Querying/ClientEvaluation) on GitHub. ## Client evaluation in the top-level projection In the following example, a helper method is used to standardize URLs for blogs, which are returned from a SQL Server database. Since the SQL Server provider has no insight into how this method is implemented, it isn't possible to translate it into SQL. All other aspects of the query are evaluated in the database, but passing the returned `URL` through this method is done on the client. -[!code-csharp[Main](../../../samples/core/Querying/ClientEval/Sample.cs#ClientProjection)] +[!code-csharp[Main](../../../samples/core/Querying/ClientEvaluation/Program.cs#ClientProjection)] -[!code-csharp[Main](../../../samples/core/Querying/ClientEval/Sample.cs#ClientMethod)] +[!code-csharp[Main](../../../samples/core/Querying/ClientEvaluation/Program.cs#ClientMethod)] ## Unsupported client evaluation While client evaluation is useful, it can result in poor performance sometimes. Consider the following query, in which the helper method is now used in a where filter. Because the filter can't be applied in the database, all the data needs to be pulled into memory to apply the filter on the client. Based on the filter and the amount of data on the server, client evaluation could result in poor performance. So Entity Framework Core blocks such client evaluation and throws a runtime exception. -[!code-csharp[Main](../../../samples/core/Querying/ClientEval/Sample.cs#ClientWhere)] +[!code-csharp[Main](../../../samples/core/Querying/ClientEvaluation/Program.cs#ClientWhere)] ## Explicit client evaluation @@ -38,7 +38,7 @@ You may need to force into client evaluation explicitly in certain cases like fo In such cases, you can explicitly opt into client evaluation by calling methods like `AsEnumerable` or `ToList` (`AsAsyncEnumerable` or `ToListAsync` for async). By using `AsEnumerable` you would be streaming the results, but using `ToList` would cause buffering by creating a list, which also takes additional memory. Though if you're enumerating multiple times, then storing results in a list helps more since there's only one query to the database. Depending on the particular usage, you should evaluate which method is more useful for the case. -[!code-csharp[Main](../../../samples/core/Querying/ClientEval/Sample.cs#ExplicitClientEval)] +[!code-csharp[Main](../../../samples/core/Querying/ClientEvaluation/Program.cs#ExplicitClientEvaluation)] ## Potential memory leak in client evaluation diff --git a/entity-framework/core/querying/complex-query-operators.md b/entity-framework/core/querying/complex-query-operators.md index f8444fbff8..f97796058f 100644 --- a/entity-framework/core/querying/complex-query-operators.md +++ b/entity-framework/core/querying/complex-query-operators.md @@ -10,13 +10,13 @@ uid: core/querying/complex-query-operators Language Integrated Query (LINQ) contains many complex operators, which combine multiple data sources or does complex processing. Not all LINQ operators have suitable translations on the server side. Sometimes, a query in one form translates to the server but if written in a different form doesn't translate even if the result is the same. This page describes some of the complex operators and their supported variations. In future releases, we may recognize more patterns and add their corresponding translations. It's also important to keep in mind that translation support varies between providers. A particular query, which is translated in SqlServer, may not work for SQLite databases. > [!TIP] -> You can view this article's [sample](https://github.com/dotnet/EntityFramework.Docs/tree/master/samples/core/Querying) on GitHub. +> You can view this article's [sample](https://github.com/dotnet/EntityFramework.Docs/tree/master/samples/core/Querying/ComplexQuery) on GitHub. ## 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. -[!code-csharp[Main](../../../samples/core/Querying/ComplexQuery/Sample.cs#Join)] +[!code-csharp[Main](../../../samples/core/Querying/ComplexQuery/Program.cs#Join)] ```SQL SELECT [p].[PersonId], [p].[Name], [p].[PhotoId], [p0].[PersonPhotoId], [p0].[Caption], [p0].[Photo] @@ -28,9 +28,9 @@ INNER JOIN [Person] AS [p] ON [p0].[PersonPhotoId] = [p].[PhotoId] 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. -[!code-csharp[Main](../../../samples/core/Querying/ComplexQuery/Sample.cs#GroupJoin)] +[!code-csharp[Main](../../../samples/core/Querying/ComplexQuery/Program.cs#GroupJoin)] -[!code-csharp[Main](../../../samples/core/Querying/ComplexQuery/Sample.cs#GroupJoinComposed)] +[!code-csharp[Main](../../../samples/core/Querying/ComplexQuery/Program.cs#GroupJoinComposed)] ## SelectMany @@ -40,7 +40,7 @@ The LINQ SelectMany operator allows you to enumerate over a collection selector When the collection selector isn't referencing anything from the outer source, the result is a cartesian product of both data sources. It translates to `CROSS JOIN` in relational databases. -[!code-csharp[Main](../../../samples/core/Querying/ComplexQuery/Sample.cs#SelectManyConvertedToCrossJoin)] +[!code-csharp[Main](../../../samples/core/Querying/ComplexQuery/Program.cs#SelectManyConvertedToCrossJoin)] ```SQL SELECT [b].[BlogId], [b].[OwnerId], [b].[Rating], [b].[Url], [p].[PostId], [p].[AuthorId], [p].[BlogId], [p].[Content], [p].[Rating], [p].[Title] @@ -52,7 +52,7 @@ CROSS JOIN [Posts] AS [p] When the collection selector has a where clause, which references the outer element, then EF Core translates it to a database join and uses the predicate as the join condition. Normally this case arises when using collection navigation on the outer element as the collection selector. If the collection is empty for an outer element, then no results would be generated for that outer element. But if `DefaultIfEmpty` is applied on the collection selector then the outer element will be connected with a default value of the inner element. Because of this distinction, this kind of queries translates to `INNER JOIN` in the absence of `DefaultIfEmpty` and `LEFT JOIN` when `DefaultIfEmpty` is applied. -[!code-csharp[Main](../../../samples/core/Querying/ComplexQuery/Sample.cs#SelectManyConvertedToJoin)] +[!code-csharp[Main](../../../samples/core/Querying/ComplexQuery/Program.cs#SelectManyConvertedToJoin)] ```SQL SELECT [b].[BlogId], [b].[OwnerId], [b].[Rating], [b].[Url], [p].[PostId], [p].[AuthorId], [p].[BlogId], [p].[Content], [p].[Rating], [p].[Title] @@ -68,7 +68,7 @@ LEFT JOIN [Posts] AS [p] ON [b].[BlogId] = [p].[BlogId] When the collection selector references the outer element, which isn't in a where clause (as the case above), it doesn't translate to a database join. That's why we need to evaluate the collection selector for each outer element. It translates to `APPLY` operations in many relational databases. If the collection is empty for an outer element, then no results would be generated for that outer element. But if `DefaultIfEmpty` is applied on the collection selector then the outer element will be connected with a default value of the inner element. Because of this distinction, this kind of queries translates to `CROSS APPLY` in the absence of `DefaultIfEmpty` and `OUTER APPLY` when `DefaultIfEmpty` is applied. Certain databases like SQLite don't support `APPLY` operators so this kind of query may not be translated. -[!code-csharp[Main](../../../samples/core/Querying/ComplexQuery/Sample.cs#SelectManyConvertedToApply)] +[!code-csharp[Main](../../../samples/core/Querying/ComplexQuery/Program.cs#SelectManyConvertedToApply)] ```SQL SELECT [b].[BlogId], [b].[OwnerId], [b].[Rating], [b].[Url], ([b].[Url] + N'=>') + [p].[Title] AS [p] @@ -84,7 +84,7 @@ OUTER APPLY [Posts] AS [p] LINQ GroupBy operators create a result of type `IGrouping` where `TKey` and `TElement` could be any arbitrary type. Furthermore, `IGrouping` implements `IEnumerable`, which means you can compose over it using any LINQ operator after the grouping. Since no database structure can represent an `IGrouping`, GroupBy operators have no translation in most cases. When an aggregate operator is applied to each group, which returns a scalar, it can be translated to SQL `GROUP BY` in relational databases. The SQL `GROUP BY` is restrictive too. It requires you to group only by scalar values. The projection can only contain grouping key columns or any aggregate applied over a column. EF Core identifies this pattern and translates it to the server, as in the following example: -[!code-csharp[Main](../../../samples/core/Querying/ComplexQuery/Sample.cs#GroupBy)] +[!code-csharp[Main](../../../samples/core/Querying/ComplexQuery/Program.cs#GroupBy)] ```SQL SELECT [p].[AuthorId] AS [Key], COUNT(*) AS [Count] @@ -94,7 +94,7 @@ GROUP BY [p].[AuthorId] EF Core also translates queries where an aggregate operator on the grouping appears in a Where or OrderBy (or other ordering) LINQ operator. It uses `HAVING` clause in SQL for the where clause. The part of the query before applying the GroupBy operator can be any complex query as long as it can be translated to server. Furthermore, once you apply aggregate operators on a grouping query to remove groupings from the resulting source, you can compose on top of it like any other query. -[!code-csharp[Main](../../../samples/core/Querying/ComplexQuery/Sample.cs#GroupByFilter)] +[!code-csharp[Main](../../../samples/core/Querying/ComplexQuery/Program.cs#GroupByFilter)] ```SQL SELECT [p].[AuthorId] AS [Key], COUNT(*) AS [Count] @@ -117,7 +117,7 @@ The aggregate operators EF Core supports are as follows While Left Join isn't a LINQ operator, relational databases have the concept of a Left Join which is frequently used in queries. A particular pattern in LINQ queries gives the same result as a `LEFT JOIN` on the server. EF Core identifies such patterns and generates the equivalent `LEFT JOIN` on the server side. The pattern involves creating a GroupJoin between both the data sources and then flattening out the grouping by using the SelectMany operator with DefaultIfEmpty on the grouping source to match null when the inner doesn't have a related element. The following example shows what that pattern looks like and what it generates. -[!code-csharp[Main](../../../samples/core/Querying/ComplexQuery/Sample.cs#LeftJoin)] +[!code-csharp[Main](../../../samples/core/Querying/ComplexQuery/Program.cs#LeftJoin)] ```SQL SELECT [b].[BlogId], [b].[OwnerId], [b].[Rating], [b].[Url], [p].[PostId], [p].[AuthorId], [p].[BlogId], [p].[Content], [p].[Rating], [p].[Title] diff --git a/entity-framework/core/querying/filters.md b/entity-framework/core/querying/filters.md index 9b1e199b57..1535bf5619 100644 --- a/entity-framework/core/querying/filters.md +++ b/entity-framework/core/querying/filters.md @@ -7,38 +7,35 @@ uid: core/querying/filters --- # Global Query Filters -> [!NOTE] -> This feature was introduced in EF Core 2.0. - -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: +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: -* **Soft delete** - An Entity Type defines an *IsDeleted* property. -* **Multi-tenancy** - An Entity Type defines a *TenantId* property. +* **Soft delete** - An Entity Type defines an `IsDeleted` property. +* **Multi-tenancy** - An Entity Type defines a `TenantId` property. ## Example The following example shows how to use Global Query Filters to implement multi-tenancy and soft-delete query behaviors in a simple blogging model. > [!TIP] -> You can view a [multi-tenancy sample](https://github.com/dotnet/EntityFramework.Docs/tree/master/samples/core/QueryFilters) and [samples using navigations](https://github.com/dotnet/EntityFramework.Docs/tree/master/samples/core/QueryFiltersNavigations) on GitHub. +> You can view this article's [sample](https://github.com/dotnet/EntityFramework.Docs/tree/master/samples/core/Querying/QueryFilters) on GitHub. First, define the entities: -[!code-csharp[Main](../../../samples/core/QueryFilters/Program.cs#Entities)] +[!code-csharp[Main](../../../samples/core/Querying/QueryFilters/Entities.cs#Entities)] -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. +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. -Next, configure the query filters in _OnModelCreating_ using the `HasQueryFilter` API. +Next, configure the query filters in `OnModelCreating` using the `HasQueryFilter` API. -[!code-csharp[Main](../../../samples/core/QueryFilters/Program.cs#Configuration)] +[!code-csharp[Main](../../../samples/core/Querying/QueryFilters/BloggingContext.cs#FilterConfiguration)] -The predicate expressions passed to the _HasQueryFilter_ calls will now automatically be applied to any LINQ queries for those types. +The predicate expressions passed to the `HasQueryFilter` calls will now automatically be applied to any LINQ queries for those types. > [!TIP] > 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). > [!NOTE] -> 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-)). +> 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-)). ## Use of navigations @@ -54,33 +51,27 @@ You can also use navigations in defining global query filters. Using navigations 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. -To illustrate the problem, we can use the `Blog` and `Post` entities specified above and the following _OnModelCreating_ method: +To illustrate the problem, we can use the `Blog` and `Post` entities specified above and the following `OnModelCreating` method: -```csharp -protected override void OnModelCreating(ModelBuilder modelBuilder) -{ - modelBuilder.Entity().HasMany(b => b.Posts).WithOne(p => p.Blog).IsRequired(); - modelBuilder.Entity().HasQueryFilter(b => b.Url.Contains("fish")); -} -``` +[!code-csharp[Main](../../../samples/core/Querying/QueryFilters/BloggingContext.cs#IncorrectFilter)] The model can be seeded with the following data: -[!code-csharp[Main](../../../samples/core/QueryFiltersNavigations/Program.cs#SeedData)] +[!code-csharp[Main](../../../samples/core/Querying/QueryFilters/Program.cs#SeedData)] The problem can be observed when executing two queries: -[!code-csharp[Main](../../../samples/core/QueryFiltersNavigations/Program.cs#Queries)] +[!code-csharp[Main](../../../samples/core/Querying/QueryFilters/Program.cs#Queries)] -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: +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: ```SQL SELECT [p].[PostId], [p].[BlogId], [p].[Content], [p].[IsDeleted], [p].[Title], [t].[BlogId], [t].[Name], [t].[Url] -FROM [Post] AS [p] +FROM [Posts] AS [p] INNER JOIN ( SELECT [b].[BlogId], [b].[Name], [b].[Url] FROM [Blogs] AS [b] - WHERE CHARINDEX(N'fish', [b].[Url]) > 0 + WHERE [b].[Url] LIKE N'%fish%' ) AS [t] ON [p].[BlogId] = [t].[BlogId] ``` @@ -89,29 +80,16 @@ Use of the `INNER JOIN` filters out all `Post`s whose related `Blog`s have been It can be addressed by using optional navigation instead of required. This way the first query stays the same as before, however the second query will now generate `LEFT JOIN` and return 6 results. -```csharp -protected override void OnModelCreating(ModelBuilder modelBuilder) -{ - modelBuilder.Entity().HasMany(b => b.Posts).WithOne(p => p.Blog).IsRequired(false); - modelBuilder.Entity().HasQueryFilter(b => b.Url.Contains("fish")); -} -``` +[!code-csharp[Main](../../../samples/core/Querying/QueryFilters/BloggingContext.cs#OptionalNavigation)] Alternative approach is to specify consistent filters on both `Blog` and `Post` entities. 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. -```csharp -protected override void OnModelCreating(ModelBuilder modelBuilder) -{ - modelBuilder.Entity().HasMany(b => b.Posts).WithOne(p => p.Blog).IsRequired(); - modelBuilder.Entity().HasQueryFilter(b => b.Url.Contains("fish")); - modelBuilder.Entity().HasQueryFilter(p => p.Blog.Url.Contains("fish")); -} -``` +[!code-csharp[Main](../../../samples/core/Querying/QueryFilters/BloggingContext.cs#MatchingFilters)] ## Disabling Filters -Filters may be disabled for individual LINQ queries by using the `IgnoreQueryFilters()` operator. +Filters may be disabled for individual LINQ queries by using the operator. [!code-csharp[Main](../../../samples/core/QueryFilters/Program.cs#IgnoreFilters)] diff --git a/entity-framework/core/querying/index.md b/entity-framework/core/querying/index.md index ed92d58bcd..6721a8897f 100644 --- a/entity-framework/core/querying/index.md +++ b/entity-framework/core/querying/index.md @@ -10,21 +10,21 @@ uid: core/querying/index Entity Framework Core uses Language Integrated Query (LINQ) to query data from the database. LINQ allows you to use C# (or your .NET language of choice) to write strongly typed queries. It uses your derived context and entity classes to reference database objects. EF Core passes a representation of the LINQ query to the database provider. Database providers in turn translate it to database-specific query language (for example, SQL for a relational database). > [!TIP] -> You can view this article's [sample](https://github.com/dotnet/EntityFramework.Docs/tree/master/samples/core/Querying) on GitHub. +> You can view this article's [sample](https://github.com/dotnet/EntityFramework.Docs/tree/master/samples/core/Querying/Overview) on GitHub. The following snippets show a few examples of how to achieve common tasks with Entity Framework Core. ## Loading all data -[!code-csharp[Main](../../../samples/core/Querying/Basics/Sample.cs#LoadingAllData)] +[!code-csharp[Main](../../../samples/core/Querying/Overview/Program.cs#LoadingAllData)] ## Loading a single entity -[!code-csharp[Main](../../../samples/core/Querying/Basics/Sample.cs#LoadingSingleEntity)] +[!code-csharp[Main](../../../samples/core/Querying/Overview/Program.cs#LoadingSingleEntity)] ## Filtering -[!code-csharp[Main](../../../samples/core/Querying/Basics/Sample.cs#Filtering)] +[!code-csharp[Main](../../../samples/core/Querying/Overview/Program.cs#Filtering)] ## Further readings diff --git a/entity-framework/core/querying/raw-sql.md b/entity-framework/core/querying/raw-sql.md index 97c95dd761..e11bd77462 100644 --- a/entity-framework/core/querying/raw-sql.md +++ b/entity-framework/core/querying/raw-sql.md @@ -10,17 +10,17 @@ uid: core/querying/raw-sql Entity Framework Core allows you to drop down to raw SQL queries when working with a relational database. Raw SQL queries are useful if the query you want can't be expressed using LINQ. Raw SQL queries are also used if using a LINQ query is resulting in an inefficient SQL query. Raw SQL queries can return regular entity types or [keyless entity types](xref:core/modeling/keyless-entity-types) that are part of your model. > [!TIP] -> You can view this article's [sample](https://github.com/dotnet/EntityFramework.Docs/tree/master/samples/core/Querying/) on GitHub. +> You can view this article's [sample](https://github.com/dotnet/EntityFramework.Docs/tree/master/samples/core/Querying/RawSQL) on GitHub. ## Basic raw SQL queries You can use the `FromSqlRaw` extension method to begin a LINQ query based on a raw SQL query. `FromSqlRaw` can only be used on query roots, that is directly on the `DbSet<>`. -[!code-csharp[Main](../../../samples/core/Querying/RawSQL/Sample.cs#FromSqlRaw)] +[!code-csharp[Main](../../../samples/core/Querying/RawSQL/Program.cs#FromSqlRaw)] Raw SQL queries can be used to execute a stored procedure. -[!code-csharp[Main](../../../samples/core/Querying/RawSQL/Sample.cs#FromSqlRawStoredProcedure)] +[!code-csharp[Main](../../../samples/core/Querying/RawSQL/Program.cs#FromSqlRawStoredProcedure)] ## Passing parameters @@ -33,22 +33,22 @@ Raw SQL queries can be used to execute a stored procedure. The following example passes a single parameter to a stored procedure by including a parameter placeholder in the SQL query string and providing an additional argument. While this syntax may look like `String.Format` syntax, the supplied value is wrapped in a `DbParameter` and the generated parameter name inserted where the `{0}` placeholder was specified. -[!code-csharp[Main](../../../samples/core/Querying/RawSQL/Sample.cs#FromSqlRawStoredProcedureParameter)] +[!code-csharp[Main](../../../samples/core/Querying/RawSQL/Program.cs#FromSqlRawStoredProcedureParameter)] `FromSqlInterpolated` is similar to `FromSqlRaw` but allows you to use string interpolation syntax. Just like `FromSqlRaw`, `FromSqlInterpolated` can only be used on query roots. As with the previous example, the value is converted to a `DbParameter` and isn't vulnerable to SQL injection. > [!NOTE] > Prior to version 3.0, `FromSqlRaw` and `FromSqlInterpolated` were two overloads named `FromSql`. For more information, see the [previous versions section](#previous-versions). -[!code-csharp[Main](../../../samples/core/Querying/RawSQL/Sample.cs#FromSqlInterpolatedStoredProcedureParameter)] +[!code-csharp[Main](../../../samples/core/Querying/RawSQL/Program.cs#FromSqlInterpolatedStoredProcedureParameter)] You can also construct a DbParameter and supply it as a parameter value. Since a regular SQL parameter placeholder is used, rather than a string placeholder, `FromSqlRaw` can be safely used: -[!code-csharp[Main](../../../samples/core/Querying/RawSQL/Sample.cs#FromSqlRawStoredProcedureSqlParameter)] +[!code-csharp[Main](../../../samples/core/Querying/RawSQL/Program.cs#FromSqlRawStoredProcedureSqlParameter)] `FromSqlRaw` allows you to use named parameters in the SQL query string, which is useful when a stored procedure has optional parameters: -[!code-csharp[Main](../../../samples/core/Querying/RawSQL/Sample.cs#FromSqlRawStoredProcedureNamedSqlParameter)] +[!code-csharp[Main](../../../samples/core/Querying/RawSQL/Program.cs#FromSqlRawStoredProcedureNamedSqlParameter)] > [!NOTE] > **Parameter Ordering** @@ -58,7 +58,7 @@ You can also construct a DbParameter and supply it as a parameter value. Since a You can compose on top of the initial raw SQL query using LINQ operators. EF Core will treat it as subquery and compose over it in the database. The following example uses a raw SQL query that selects from a Table-Valued Function (TVF). And then composes on it using LINQ to do filtering and sorting. -[!code-csharp[Main](../../../samples/core/Querying/RawSQL/Sample.cs#FromSqlInterpolatedComposed)] +[!code-csharp[Main](../../../samples/core/Querying/RawSQL/Program.cs#FromSqlInterpolatedComposed)] Above query generates following SQL: @@ -75,7 +75,7 @@ ORDER BY [b].[Rating] DESC The `Include` method can be used to include related data, just like with any other LINQ query: -[!code-csharp[Main](../../../samples/core/Querying/RawSQL/Sample.cs#FromSqlInterpolatedInclude)] +[!code-csharp[Main](../../../samples/core/Querying/RawSQL/Program.cs#FromSqlInterpolatedInclude)] Composing with LINQ requires your raw SQL query to be composable since EF Core will treat the supplied SQL as a subquery. SQL queries that can be composed on begin with the `SELECT` keyword. Further, SQL passed shouldn't contain any characters or options that aren't valid on a subquery, such as: @@ -91,7 +91,7 @@ Queries that use the `FromSqlRaw` or `FromSqlInterpolated` methods follow the ex The following example uses a raw SQL query that selects from a Table-Valued Function (TVF), then disables change tracking with the call to `AsNoTracking`: -[!code-csharp[Main](../../../samples/core/Querying/RawSQL/Sample.cs#FromSqlInterpolatedAsNoTracking)] +[!code-csharp[Main](../../../samples/core/Querying/RawSQL/Program.cs#FromSqlInterpolatedAsNoTracking)] ## Limitations diff --git a/entity-framework/core/querying/related-data/eager.md b/entity-framework/core/querying/related-data/eager.md index 3b9138c70f..477eda5d7b 100644 --- a/entity-framework/core/querying/related-data/eager.md +++ b/entity-framework/core/querying/related-data/eager.md @@ -11,32 +11,32 @@ uid: core/querying/related-data/eager You can use the `Include` method to specify related data to be included in query results. In the following example, the blogs that are returned in the results will have their `Posts` property populated with the related posts. -[!code-csharp[Main](../../../../samples/core/Querying/RelatedData/Sample.cs#SingleInclude)] +[!code-csharp[Main](../../../../samples/core/Querying/RelatedData/Program.cs#SingleInclude)] > [!TIP] > Entity Framework Core will automatically fix-up navigation properties to any other entities that were previously loaded into the context instance. So even if you don't explicitly include the data for a navigation property, the property may still be populated if some or all of the related entities were previously loaded. You can include related data from multiple relationships in a single query. -[!code-csharp[Main](../../../../samples/core/Querying/RelatedData/Sample.cs#MultipleIncludes)] +[!code-csharp[Main](../../../../samples/core/Querying/RelatedData/Program.cs#MultipleIncludes)] ## Including multiple levels You can drill down through relationships to include multiple levels of related data using the `ThenInclude` method. The following example loads all blogs, their related posts, and the author of each post. -[!code-csharp[Main](../../../../samples/core/Querying/RelatedData/Sample.cs#SingleThenInclude)] +[!code-csharp[Main](../../../../samples/core/Querying/RelatedData/Program.cs#SingleThenInclude)] You can chain multiple calls to `ThenInclude` to continue including further levels of related data. -[!code-csharp[Main](../../../../samples/core/Querying/RelatedData/Sample.cs#MultipleThenIncludes)] +[!code-csharp[Main](../../../../samples/core/Querying/RelatedData/Program.cs#MultipleThenIncludes)] You can combine all of the calls to include related data from multiple levels and multiple roots in the same query. -[!code-csharp[Main](../../../../samples/core/Querying/RelatedData/Sample.cs#IncludeTree)] +[!code-csharp[Main](../../../../samples/core/Querying/RelatedData/Program.cs#IncludeTree)] You may want to include multiple related entities for one of the entities that is being included. For example, when querying `Blogs`, you include `Posts` and then want to include both the `Author` and `Tags` of the `Posts`. To include both, you need to specify each include path starting at the root. For example, `Blog -> Posts -> Author` and `Blog -> Posts -> Tags`. It doesn't mean you'll get redundant joins; in most cases, EF will combine the joins when generating SQL. -[!code-csharp[Main](../../../../samples/core/Querying/RelatedData/Sample.cs#MultipleLeafIncludes)] +[!code-csharp[Main](../../../../samples/core/Querying/RelatedData/Program.cs#MultipleLeafIncludes)] ## Single and split queries @@ -60,7 +60,7 @@ If a typical blog has multiple related posts, rows for these posts will duplicat EF allows you to specify that a given LINQ query should be *split* into multiple SQL queries. Instead of JOINs, split queries perform an additional SQL query for each included one-to-many navigation: -[!code-csharp[Main](../../../../samples/core/Querying/RelatedData/Sample.cs?name=AsSplitQuery&highlight=5)] +[!code-csharp[Main](../../../../samples/core/Querying/RelatedData/Program.cs?name=AsSplitQuery&highlight=5)] It will produce the following SQL: @@ -86,7 +86,7 @@ You can also configure split queries as the default for your application's conte When split queries are configured as the default, it is still possible to configure specific queries to execute as single queries: -[!code-csharp[Main](../../../../samples/core/Querying/RelatedData/Sample.cs?name=AsSingleQuery&highlight=5)] +[!code-csharp[Main](../../../../samples/core/Querying/RelatedData/Program.cs?name=AsSingleQuery&highlight=5)] If the query splitting mode isn't explicitly specified - neither globally nor on the query - and EF Core detects that a single query loads multiple collection includes, a warning is emitted to draw attention to the potential resulting performance issues. Setting the query mode to SingleQuery will cause the warning not to be generated. @@ -111,15 +111,15 @@ Supported operations are: `Where`, `OrderBy`, `OrderByDescending`, `ThenBy`, `Th Such operations should be applied on the collection navigation in the lambda passed to the Include method, as shown in example below: -[!code-csharp[Main](../../../../samples/core/Querying/RelatedData/Sample.cs#FilteredInclude)] +[!code-csharp[Main](../../../../samples/core/Querying/RelatedData/Program.cs#FilteredInclude)] Each included navigation allows only one unique set of filter operations. In cases where multiple Include operations are applied for a given collection navigation (`blog.Posts` in the examples below), filter operations can only be specified on one of them: -[!code-csharp[Main](../../../../samples/core/Querying/RelatedData/Sample.cs#MultipleLeafIncludesFiltered1)] +[!code-csharp[Main](../../../../samples/core/Querying/RelatedData/Program.cs#MultipleLeafIncludesFiltered1)] Instead, identical operations can be applied for each navigation that is included multiple times: -[!code-csharp[Main](../../../../samples/core/Querying/RelatedData/Sample.cs#MultipleLeafIncludesFiltered2)] +[!code-csharp[Main](../../../../samples/core/Querying/RelatedData/Program.cs#MultipleLeafIncludesFiltered2)] > [!CAUTION] > In case of tracking queries, results of Filtered Include may be unexpected due to [navigation fixup](xref:core/querying/tracking). All relevant entities that have been queried for previously and have been stored in the Change Tracker will be present in the results of Filtered Include query, even if they don't meet the requirements of the filter. Consider using `NoTracking` queries or re-create the DbContext when using Filtered Include in those situations. @@ -190,4 +190,3 @@ Contents of `School` navigation of all People who are Students can be eagerly lo ```csharp context.People.Include("School").ToList() ``` - diff --git a/entity-framework/core/querying/related-data/explicit.md b/entity-framework/core/querying/related-data/explicit.md index f0194998b4..75182ed922 100644 --- a/entity-framework/core/querying/related-data/explicit.md +++ b/entity-framework/core/querying/related-data/explicit.md @@ -11,7 +11,7 @@ uid: core/querying/related-data/explicit You can explicitly load a navigation property via the `DbContext.Entry(...)` API. -[!code-csharp[Main](../../../../samples/core/Querying/RelatedData/Sample.cs#Eager)] +[!code-csharp[Main](../../../../samples/core/Querying/RelatedData/Program.cs#Eager)] You can also explicitly load a navigation property by executing a separate query that returns the related entities. If change tracking is enabled, then when query materializes an entity, EF Core will automatically set the navigation properties of the newly loaded entity to refer to any entities already loaded, and set the navigation properties of the already-loaded entities to refer to the newly loaded entity. @@ -21,8 +21,8 @@ You can also get a LINQ query that represents the contents of a navigation prope It allows you to apply additional operators over the query. For example applying an aggregate operator over the related entities without loading them into memory. -[!code-csharp[Main](../../../../samples/core/Querying/RelatedData/Sample.cs#NavQueryAggregate)] +[!code-csharp[Main](../../../../samples/core/Querying/RelatedData/Program.cs#NavQueryAggregate)] You can also filter which related entities are loaded into memory. -[!code-csharp[Main](../../../../samples/core/Querying/RelatedData/Sample.cs#NavQueryFiltered)] +[!code-csharp[Main](../../../../samples/core/Querying/RelatedData/Program.cs#NavQueryFiltered)] diff --git a/entity-framework/core/querying/related-data/index.md b/entity-framework/core/querying/related-data/index.md index dfab63f8af..228c01debd 100644 --- a/entity-framework/core/querying/related-data/index.md +++ b/entity-framework/core/querying/related-data/index.md @@ -14,4 +14,4 @@ Entity Framework Core allows you to use the navigation properties in your model * **[Lazy loading](xref:core/querying/related-data/lazy)** means that the related data is transparently loaded from the database when the navigation property is accessed. > [!TIP] -> You can view the [samples](https://github.com/dotnet/EntityFramework.Docs/tree/master/samples/core/Querying) under this section on GitHub. +> You can view the [samples](https://github.com/dotnet/EntityFramework.Docs/tree/master/samples/core/Querying/RelatedData) under this section on GitHub. diff --git a/entity-framework/core/querying/tags.md b/entity-framework/core/querying/tags.md index 8d227a50d5..d4f89e2ae5 100644 --- a/entity-framework/core/querying/tags.md +++ b/entity-framework/core/querying/tags.md @@ -1,90 +1,71 @@ --- title: Query Tags - EF Core description: Using query tags to help identify specific queries in log messages emitted by Entity Framework Core -author: roji +author: smitpatel ms.date: 11/14/2018 uid: core/querying/tags --- # Query tags -> [!NOTE] -> This feature is new in EF Core 2.2. - -This feature helps correlate LINQ queries in code with generated SQL queries captured in logs. +Query tags help correlate LINQ queries in code with generated SQL queries captured in logs. You annotate a LINQ query using the new `TagWith()` method: -``` csharp - var nearestFriends = - (from f in context.Friends.TagWith("This is my spatial query!") - orderby f.Location.Distance(myLocation) descending - select f).Take(5).ToList(); -``` +> [!TIP] +> You can view this article's [sample](https://github.com/dotnet/EntityFramework.Docs/tree/master/samples/core/Querying/Tags) on GitHub. + +[!code-csharp[Main](../../../samples/core/Querying/Tags/Program.cs#BasicQueryTag)] This LINQ query is translated to the following SQL statement: -``` sql +```sql -- This is my spatial query! -SELECT TOP(@__p_1) [f].[Name], [f].[Location] -FROM [Friends] AS [f] -ORDER BY [f].[Location].STDistance(@__myLocation_0) DESC +SELECT TOP(@__p_1) [p].[Id], [p].[Location] +FROM [People] AS [p] +ORDER BY [p].[Location].STDistance(@__myLocation_0) DESC ``` It's possible to call `TagWith()` many times on the same query. Query tags are cumulative. For example, given the following methods: -``` csharp -IQueryable GetNearestFriends(Point myLocation) => - from f in context.Friends.TagWith("GetNearestFriends") - orderby f.Location.Distance(myLocation) descending - select f; - -IQueryable Limit(IQueryable source, int limit) => - source.TagWith("Limit").Take(limit); -``` +[!code-csharp[Main](../../../samples/core/Querying/Tags/Program.cs#QueryableMethods)] The following query: -``` csharp -var results = Limit(GetNearestFriends(myLocation), 25).ToList(); -``` +[!code-csharp[Main](../../../samples/core/Querying/Tags/Program.cs#ChainedQueryTags)] Translates to: -``` sql --- GetNearestFriends +```sql +-- GetNearestPeople -- Limit -SELECT TOP(@__p_1) [f].[Name], [f].[Location] -FROM [Friends] AS [f] -ORDER BY [f].[Location].STDistance(@__myLocation_0) DESC +SELECT TOP(@__p_1) [p].[Id], [p].[Location] +FROM [People] AS [p] +ORDER BY [p].[Location].STDistance(@__myLocation_0) DESC ``` It's also possible to use multi-line strings as query tags. For example: -``` csharp -var results = Limit(GetNearestFriends(myLocation), 25).TagWith( -@"This is a multi-line -string").ToList(); -``` +[!code-csharp[Main](../../../samples/core/Querying/Tags/Program.cs#MultilineQueryTag)] Produces the following SQL: -``` sql --- GetNearestFriends +```sql +-- GetNearestPeople -- Limit -- This is a multi-line -- string -SELECT TOP(@__p_1) [f].[Name], [f].[Location] -FROM [Friends] AS [f] -ORDER BY [f].[Location].STDistance(@__myLocation_0) DESC +SELECT TOP(@__p_1) [p].[Id], [p].[Location] +FROM [People] AS [p] +ORDER BY [p].[Location].STDistance(@__myLocation_0) DESC ``` ## Known limitations diff --git a/entity-framework/core/querying/tracking.md b/entity-framework/core/querying/tracking.md index 511b207242..3196a925d1 100644 --- a/entity-framework/core/querying/tracking.md +++ b/entity-framework/core/querying/tracking.md @@ -13,23 +13,23 @@ Tracking behavior controls if Entity Framework Core will keep information about > [Keyless entity types](xref:core/modeling/keyless-entity-types) are never tracked. Wherever this article mentions entity types, it refers to entity types which have a key defined. > [!TIP] -> You can view this article's [sample](https://github.com/dotnet/EntityFramework.Docs/tree/master/samples/core/Querying) on GitHub. +> You can view this article's [sample](https://github.com/dotnet/EntityFramework.Docs/tree/master/samples/core/Querying/Tracking) on GitHub. ## Tracking queries By default, queries that return entity types are tracking. Which means you can make changes to those entity instances and have those changes persisted by `SaveChanges()`. In the following example, the change to the blogs rating will be detected and persisted to the database during `SaveChanges()`. -[!code-csharp[Main](../../../samples/core/Querying/Tracking/Sample.cs#Tracking)] +[!code-csharp[Main](../../../samples/core/Querying/Tracking/Program.cs#Tracking)] ## No-tracking queries No tracking queries are useful when the results are used in a read-only scenario. They're quicker to execute because there's no need to set up the change tracking information. If you don't need to update the entities retrieved from the database, then a no-tracking query should be used. You can swap an individual query to be no-tracking. -[!code-csharp[Main](../../../samples/core/Querying/Tracking/Sample.cs#NoTracking)] +[!code-csharp[Main](../../../samples/core/Querying/Tracking/Program.cs#NoTracking)] You can also change the default tracking behavior at the context instance level: -[!code-csharp[Main](../../../samples/core/Querying/Tracking/Sample.cs#ContextDefaultTrackingBehavior)] +[!code-csharp[Main](../../../samples/core/Querying/Tracking/Program.cs#ContextDefaultTrackingBehavior)] ## Identity resolution @@ -39,21 +39,21 @@ Since a tracking query uses the change tracker, EF Core will do identity resolut Even if the result type of the query isn't an entity type, EF Core will still track entity types contained in the result by default. In the following query, which returns an anonymous type, the instances of `Blog` in the result set will be tracked. -[!code-csharp[Main](../../../samples/core/Querying/Tracking/Sample.cs#CustomProjection1)] +[!code-csharp[Main](../../../samples/core/Querying/Tracking/Program.cs#CustomProjection1)] If the result set contains entity types coming out from LINQ composition, EF Core will track them. -[!code-csharp[Main](../../../samples/core/Querying/Tracking/Sample.cs#CustomProjection2)] +[!code-csharp[Main](../../../samples/core/Querying/Tracking/Program.cs#CustomProjection2)] If the result set doesn't contain any entity types, then no tracking is done. In the following query, we return an anonymous type with some of the values from the entity (but no instances of the actual entity type). There are no tracked entities coming out of the query. -[!code-csharp[Main](../../../samples/core/Querying/Tracking/Sample.cs#CustomProjection3)] +[!code-csharp[Main](../../../samples/core/Querying/Tracking/Program.cs#CustomProjection3)] EF Core supports doing client evaluation in the top-level projection. If EF Core materializes an entity instance for client evaluation, it will be tracked. Here, since we're passing `blog` entities to the client method `StandardizeURL`, EF Core will track the blog instances too. -[!code-csharp[Main](../../../samples/core/Querying/Tracking/Sample.cs#ClientProjection)] +[!code-csharp[Main](../../../samples/core/Querying/Tracking/Program.cs#ClientProjection)] -[!code-csharp[Main](../../../samples/core/Querying/Tracking/Sample.cs#ClientMethod)] +[!code-csharp[Main](../../../samples/core/Querying/Tracking/Program.cs#ClientMethod)] EF Core doesn't track the keyless entity instances contained in the result. But EF Core tracks all the other instances of entity types with key according to rules above. @@ -65,10 +65,10 @@ Before version 3.0, EF Core had some differences in how tracking was done. Notab - As explained in [Client vs Server Evaluation](xref:core/querying/client-eval) page, EF Core supported client evaluation in any part of the query before version 3.0. Client evaluation caused materialization of entities, which weren't part of the result. So EF Core analyzed the result to detect what to track. This design had certain differences as follows: - Client evaluation in the projection, which caused materialization but didn't return the materialized entity instance wasn't tracked. The following example didn't track `blog` entities. - [!code-csharp[Main](../../../samples/core/Querying/Tracking/Sample.cs#ClientProjection)] + [!code-csharp[Main](../../../samples/core/Querying/Tracking/Program.cs#ClientProjection)] - EF Core didn't track the objects coming out of LINQ composition in certain cases. The following example didn't track `Post`. - [!code-csharp[Main](../../../samples/core/Querying/Tracking/Sample.cs#CustomProjection2)] + [!code-csharp[Main](../../../samples/core/Querying/Tracking/Program.cs#CustomProjection2)] - Whenever query results contained keyless entity types, the whole query was made non-tracking. That means that entity types with keys, which are in result weren't being tracked either. - EF Core did identity resolution in no-tracking query. It used weak references to keep track of entities that had already been returned. So if a result set contained the same entity multiples times, you would get the same instance for each occurrence. Though if a previous result with the same identity went out of scope and got garbage collected, EF Core returned a new instance. diff --git a/samples/core/QueryFilters/Program.cs b/samples/core/QueryFilters/Program.cs deleted file mode 100644 index 22412470e9..0000000000 --- a/samples/core/QueryFilters/Program.cs +++ /dev/null @@ -1,205 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using Microsoft.EntityFrameworkCore; -using Microsoft.Extensions.Logging; - -#pragma warning disable 169 - -namespace Samples -{ - public class Program - { - private static void Main() - { - SetupDatabase(); - - using (var db = new BloggingContext("Diego")) - { - var blogs = db.Blogs - .Include(b => b.Posts) - .ToList(); - - foreach (var blog in blogs) - { - Console.WriteLine( - $"{blog.Url.PadRight(33)} [Tenant: {db.Entry(blog).Property("_tenantId").CurrentValue}]"); - - foreach (var post in blog.Posts) - { - Console.WriteLine($" - {post.Title.PadRight(30)} [IsDeleted: {post.IsDeleted}]"); - } - - Console.WriteLine(); - } - - #region IgnoreFilters - blogs = db.Blogs - .Include(b => b.Posts) - .IgnoreQueryFilters() - .ToList(); - #endregion - - foreach (var blog in blogs) - { - Console.WriteLine( - $"{blog.Url.PadRight(33)} [Tenant: {db.Entry(blog).Property("_tenantId").CurrentValue}]"); - - foreach (var post in blog.Posts) - { - Console.WriteLine($" - {post.Title.PadRight(30)} [IsDeleted: {post.IsDeleted}]"); - } - - Console.WriteLine(); - } - } - } - - private static void SetupDatabase() - { - using (var db = new BloggingContext("diego")) - { - db.Database.EnsureDeleted(); - if (db.Database.EnsureCreated()) - { - 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.SaveChanges(); - - using (var andrewDb = new BloggingContext("andrew")) - { - andrewDb.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" } - } - }); - - andrewDb.SaveChanges(); - } - - db.Posts - .Where( - p => p.Title == "Caring for tropical fish" - || p.Title == "Cat care 101") - .ToList() - .ForEach(p => db.Posts.Remove(p)); - - db.SaveChanges(); - } - } - } - } - - public class BloggingContext : DbContext - { - private static readonly ILoggerFactory _loggerFactory - = LoggerFactory.Create( - builder => - { - builder - .AddFilter((category, level) => - level == LogLevel.Information - && category.EndsWith("Connection", StringComparison.Ordinal)) - .AddConsole(); - }); - - private readonly string _tenantId; - - public BloggingContext(string tenant) - { - _tenantId = tenant; - } - - public DbSet Blogs { get; set; } - public DbSet Posts { get; set; } - - protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder) - { - optionsBuilder - .UseSqlServer( - @"Server=(localdb)\mssqllocaldb;Database=Demo.QueryFilters;Trusted_Connection=True;ConnectRetryCount=0;") - .UseLoggerFactory(_loggerFactory); - } - - #region Configuration - protected override void OnModelCreating(ModelBuilder modelBuilder) - { - modelBuilder.Entity().Property("_tenantId").HasColumnName("TenantId"); - - // Configure entity filters - modelBuilder.Entity().HasQueryFilter(b => EF.Property(b, "_tenantId") == _tenantId); - modelBuilder.Entity().HasQueryFilter(p => !p.IsDeleted); - } - #endregion - - public override int SaveChanges() - { - ChangeTracker.DetectChanges(); - - foreach (var item in ChangeTracker.Entries().Where( - e => - e.State == EntityState.Added && e.Metadata.GetProperties().Any(p => p.Name == "_tenantId"))) - { - item.CurrentValues["_tenantId"] = _tenantId; - } - - foreach (var item in ChangeTracker.Entries().Where(e => e.State == EntityState.Deleted)) - { - item.State = EntityState.Modified; - item.CurrentValues["IsDeleted"] = true; - } - - return base.SaveChanges(); - } - } - - #region Entities - public class Blog - { - private string _tenantId; - - public int BlogId { get; set; } - public string Name { get; set; } - public string Url { get; set; } - - public List Posts { get; set; } - } - - public class Post - { - public int PostId { get; set; } - public string Title { get; set; } - public string Content { get; set; } - public bool IsDeleted { get; set; } - - public Blog Blog { get; set; } - } - #endregion -} diff --git a/samples/core/QueryFilters/README.md b/samples/core/QueryFilters/README.md deleted file mode 100644 index 7ac93bfe15..0000000000 --- a/samples/core/QueryFilters/README.md +++ /dev/null @@ -1,7 +0,0 @@ -# Query Filters - -This sample demonstrates Query Filters. A simple Blog and Post model is created and two query filters are defined: - -1) The Blog entity is configured with a multi-tenancy pattern. The query filter is based on a `_tenantId` field on the context. -2) The Post entity is set up for soft-delete, with an explicit `IsDeleted` property. - diff --git a/samples/core/QueryFiltersNavigations/Program.cs b/samples/core/QueryFiltersNavigations/Program.cs deleted file mode 100644 index 7d5c531637..0000000000 --- a/samples/core/QueryFiltersNavigations/Program.cs +++ /dev/null @@ -1,310 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using Microsoft.EntityFrameworkCore; -using Microsoft.Extensions.Logging; - -namespace Samples -{ - public class Program - { - private static void Main() - { - QueryFiltersWithNavigationsExample(); - - QueryFiltersWithRequiredNavigationExample(); - } - - private static void QueryFiltersWithNavigationsExample() - { - SetupDatabase(); - - using (var animalContext = new AnimalContext()) - { - Console.WriteLine("*****************"); - Console.WriteLine("* Animal lovers *"); - Console.WriteLine("*****************"); - - // Jamie and Paul are filtered out. - // Paul doesn't own any pets. Jamie owns Puffy, but her pet has been filtered out. - var animalLovers = animalContext.People.ToList(); - DisplayResults(animalLovers); - - Console.WriteLine("**************************************************"); - Console.WriteLine("* Animal lovers and their pets - filters enabled *"); - Console.WriteLine("**************************************************"); - - // Jamie and Paul are filtered out. - // Paul doesn't own any pets. Jamie owns Puffy, but her pet has been filtered out. - // Simba's favorite toy has also been filtered out. - // Puffy is filtered out so he doesn't show up as Hati's friend. - var ownersAndTheirPets = animalContext.People - .Include(p => p.Pets) - .ThenInclude(p => ((Dog)p).FavoriteToy) - .ToList(); - - DisplayResults(ownersAndTheirPets); - - Console.WriteLine("*********************************************************"); - Console.WriteLine("* Animal lovers and their pets - query filters disabled *"); - Console.WriteLine("*********************************************************"); - - var ownersAndTheirPetsUnfiltered = animalContext.People - .IgnoreQueryFilters() - .Include(p => p.Pets) - .ThenInclude(p => ((Dog)p).FavoriteToy) - .ToList(); - - DisplayResults(ownersAndTheirPetsUnfiltered); - } - } - - private static void SetupDatabase() - { - using (var animalContext = new AnimalContext()) - { - if (animalContext.Database.EnsureCreated()) - { - var janice = new Person { Name = "Janice" }; - var jamie = new Person { Name = "Jamie" }; - var cesar = new Person { Name = "Cesar" }; - var paul = new Person { Name = "Paul" }; - var dominic = new Person { Name = "Dominic" }; - - var kibbles = new Cat { Name = "Kibbles", PrefersCardboardBoxes = false, Owner = janice }; - var sammy = new Cat { Name = "Sammy", PrefersCardboardBoxes = true, Owner = janice }; - var puffy = new Cat { Name = "Puffy", PrefersCardboardBoxes = true, Owner = jamie }; - var hati = new Dog { Name = "Hati", FavoriteToy = new Toy { Name = "Squeeky duck" }, Owner = dominic, FriendsWith = puffy }; - var simba = new Dog { Name = "Simba", FavoriteToy = new Toy { Name = "Bone" }, Owner = cesar, FriendsWith = sammy }; - puffy.Tolerates = hati; - sammy.Tolerates = simba; - - animalContext.People.AddRange(janice, jamie, cesar, paul, dominic); - animalContext.Animals.AddRange(kibbles, sammy, puffy, hati, simba); - animalContext.SaveChanges(); - } - } - } - - private static void DisplayResults(List people) - { - foreach (var person in people) - { - Console.WriteLine($"{person.Name}"); - if (person.Pets != null) - { - foreach (var pet in person.Pets) - { - Console.Write($" - {pet.Name} [{pet.GetType().Name}] "); - if (pet is Cat cat) - { - Console.Write($"| Prefers cardboard boxes: {(cat.PrefersCardboardBoxes ? "Yes" : "No")} "); - Console.WriteLine($"| Tolerates: {(cat.Tolerates != null ? cat.Tolerates.Name : "No one")}"); - } - else if (pet is Dog dog) - { - Console.Write($"| Favorite toy: {(dog.FavoriteToy != null ? dog.FavoriteToy.Name : "None")} "); - Console.WriteLine($"| Friend: {(dog.FriendsWith != null ? dog.FriendsWith.Name : "The Owner")}"); - } - } - } - - Console.WriteLine(); - } - } - - private static void QueryFiltersWithRequiredNavigationExample() - { - using (var db = new FilteredBloggingContextRequired()) - { - db.Database.EnsureDeleted(); - db.Database.EnsureCreated(); - - #region SeedData - 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" } - } - }); - #endregion - - db.SaveChanges(); - } - - Console.WriteLine("Use of required navigations to access entity with query filter demo"); - using (var db = new FilteredBloggingContextRequired()) - { - - #region Queries - var allPosts = db.Posts.ToList(); - var allPostsWithBlogsIncluded = db.Posts.Include(p => p.Blog).ToList(); - #endregion - - if (allPosts.Count == allPostsWithBlogsIncluded.Count) - { - Console.WriteLine($"Query filters set up correctly. Result count for both queries: {allPosts.Count}."); - } - else - { - Console.WriteLine("Unexpected discrepancy due to query filters and required navigations interaction."); - Console.WriteLine($"All posts count: {allPosts.Count}."); - Console.WriteLine($"All posts with blogs included count: {allPostsWithBlogsIncluded.Count}."); - } - } - } - } - - #region QueryFiltersWithNavigations model - public class AnimalContext : DbContext - { - private static readonly ILoggerFactory _loggerFactory - = LoggerFactory.Create( - builder => - { - builder - .AddFilter((category, level) => - level == LogLevel.Information - && category.EndsWith("Connection", StringComparison.Ordinal)) - .AddConsole(); - }); - - public DbSet People { get; set; } - public DbSet Animals { get; set; } - public DbSet Toys { get; set; } - - protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder) - { - optionsBuilder - .UseSqlServer( - @"Server=(localdb)\mssqllocaldb;Database=Demo.QueryFiltersNavigations;Trusted_Connection=True;ConnectRetryCount=0;") - .UseLoggerFactory(_loggerFactory); - } - - protected override void OnModelCreating(ModelBuilder modelBuilder) - { - modelBuilder.Entity().HasOne(c => c.Tolerates).WithOne(d => d.FriendsWith).HasForeignKey(c => c.ToleratesId); - modelBuilder.Entity().HasOne(d => d.FavoriteToy).WithOne(t => t.BelongsTo).HasForeignKey(d => d.BelongsToId); - - modelBuilder.Entity().HasQueryFilter(p => p.Pets.Count > 0); - modelBuilder.Entity().HasQueryFilter(a => !a.Name.StartsWith("P")); - modelBuilder.Entity().HasQueryFilter(a => a.Name.Length > 5); - - // invalid - cycle in query filter definitions - //modelBuilder.Entity().HasQueryFilter(a => a.Owner.Name != "John"); - } - } - - public class Person - { - public int Id { get; set; } - public string Name { get; set; } - public List Pets { get; set; } - } - - public abstract class Animal - { - public int Id { get; set; } - public string Name { get; set; } - public Person Owner { get; set; } - } - - public class Cat : Animal - { - public bool PrefersCardboardBoxes { get; set; } - - public int? ToleratesId { get; set; } - - public Dog Tolerates { get; set; } - } - - public class Dog : Animal - { - public Toy FavoriteToy { get; set; } - public Cat FriendsWith { get; set; } - } - - public class Toy - { - public int Id { get; set; } - public string Name { get; set; } - public int? BelongsToId { get; set; } - public Dog BelongsTo { get; set; } - } - - #endregion - - #region QueryFiltersWithRequiredNavigation model - - public class Blog - { - public int BlogId { get; set; } - public string Name { get; set; } - public string Url { get; set; } - - public List Posts { get; set; } - } - - public class Post - { - public int PostId { get; set; } - public string Title { get; set; } - public string Content { get; set; } - public bool IsDeleted { get; set; } - - public Blog Blog { get; set; } - } - - public class FilteredBloggingContextRequired : DbContext - { - public DbSet Blogs { get; set; } - public DbSet Posts { get; set; } - - protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder) - { - optionsBuilder - .UseSqlServer( - @"Server=(localdb)\mssqllocaldb;Database=Demo.QueryFiltersRequiredNavigations;Trusted_Connection=True;ConnectRetryCount=0;"); - } - - //incorrect setup - required navigation used to reference entity that has query filter defined, but no query filter for the entity on the other side of the navigation - //protected override void OnModelCreating(ModelBuilder modelBuilder) - //{ - // modelBuilder.Entity().HasMany(b => b.Posts).WithOne(p => p.Blog).IsRequired(); - // modelBuilder.Entity().HasQueryFilter(b => b.Url.Contains("fish")); - //} - - // correct setup #1 - optional navigation used to reference entity that has query filter defined - protected override void OnModelCreating(ModelBuilder modelBuilder) - { - modelBuilder.Entity().HasMany(b => b.Posts).WithOne(p => p.Blog).IsRequired(false); - modelBuilder.Entity().HasQueryFilter(b => b.Url.Contains("fish")); - } - - // correct setup #2 - required navigation used and query filters are applied for entities on both sides of the navigation, making sure results are consistent - //protected override void OnModelCreating(ModelBuilder modelBuilder) - //{ - // modelBuilder.Entity().HasMany(b => b.Posts).WithOne(p => p.Blog).IsRequired(); - // modelBuilder.Entity().HasQueryFilter(b => b.Url.Contains("fish")); - // modelBuilder.Entity().HasQueryFilter(p => p.Blog.Url.Contains("fish")); - //} - } - #endregion -} diff --git a/samples/core/Querying/ClientEvaluation/Blog.cs b/samples/core/Querying/ClientEvaluation/Blog.cs new file mode 100644 index 0000000000..7e2f9299e7 --- /dev/null +++ b/samples/core/Querying/ClientEvaluation/Blog.cs @@ -0,0 +1,9 @@ +namespace EFQuerying.ClientEvaluation +{ + public class Blog + { + public int BlogId { get; set; } + public string Url { get; set; } + public int? Rating { get; set; } + } +} diff --git a/samples/core/Querying/ClientEvaluation/BloggingContext.cs b/samples/core/Querying/ClientEvaluation/BloggingContext.cs new file mode 100644 index 0000000000..82fb21e568 --- /dev/null +++ b/samples/core/Querying/ClientEvaluation/BloggingContext.cs @@ -0,0 +1,22 @@ +using Microsoft.EntityFrameworkCore; + +namespace EFQuerying.ClientEvaluation +{ + public class BloggingContext : DbContext + { + public DbSet Blogs { get; set; } + + protected override void OnModelCreating(ModelBuilder modelBuilder) + { + modelBuilder.Entity() + .HasData( + new Blog { BlogId = 1, Url = @"https://devblogs.microsoft.com/dotnet", Rating = 5 }, + new Blog { BlogId = 2, Url = @"https://mytravelblog.com/", Rating = 4 }); + } + + protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder) + { + optionsBuilder.UseSqlServer(@"Server=(localdb)\mssqllocaldb;Database=EFQuerying.ClientEvaluation;Trusted_Connection=True;ConnectRetryCount=0"); + } + } +} diff --git a/samples/core/Querying/ClientEvaluation/ClientEvaluation.csproj b/samples/core/Querying/ClientEvaluation/ClientEvaluation.csproj new file mode 100644 index 0000000000..31a67d7e95 --- /dev/null +++ b/samples/core/Querying/ClientEvaluation/ClientEvaluation.csproj @@ -0,0 +1,15 @@ + + + + Exe + netcoreapp3.1 + EFQuerying.ClientEvaluation + EFQuerying.ClientEvaluation + + + + + + + + diff --git a/samples/core/Querying/ClientEval/Sample.cs b/samples/core/Querying/ClientEvaluation/Program.cs similarity index 89% rename from samples/core/Querying/ClientEval/Sample.cs rename to samples/core/Querying/ClientEvaluation/Program.cs index 442b427e61..ed45104efd 100644 --- a/samples/core/Querying/ClientEval/Sample.cs +++ b/samples/core/Querying/ClientEvaluation/Program.cs @@ -1,10 +1,9 @@ -using Microsoft.EntityFrameworkCore; -using System; +using System; using System.Linq; -namespace EFQuerying.ClientEval +namespace EFQuerying.ClientEvaluation { - public class Sample + class Program { #region ClientMethod public static string StandardizeUrl(string url) @@ -20,7 +19,7 @@ public static string StandardizeUrl(string url) } #endregion - public static void Run() + static void Main(string[] args) { using (var context = new BloggingContext()) { @@ -54,7 +53,7 @@ public static void Run() using (var context = new BloggingContext()) { - #region ExplicitClientEval + #region ExplicitClientEvaluation var blogs = context.Blogs .AsEnumerable() .Where(blog => StandardizeUrl(blog.Url).Contains("dotnet")) diff --git a/samples/core/Querying/ComplexQuery/Blog.cs b/samples/core/Querying/ComplexQuery/Blog.cs new file mode 100644 index 0000000000..da408b0440 --- /dev/null +++ b/samples/core/Querying/ComplexQuery/Blog.cs @@ -0,0 +1,16 @@ +using System.Collections.Generic; + +namespace EFQuerying.ComplexQuery +{ + public class Blog + { + public int BlogId { get; set; } + public string Url { get; set; } + public int? Rating { get; set; } + + public List Posts { get; set; } + + public int OwnerId { get; set; } + public Person Owner { get; set; } + } +} diff --git a/samples/core/Querying/ComplexQuery/BloggingContext.cs b/samples/core/Querying/ComplexQuery/BloggingContext.cs new file mode 100644 index 0000000000..6a054319c4 --- /dev/null +++ b/samples/core/Querying/ComplexQuery/BloggingContext.cs @@ -0,0 +1,73 @@ +using Microsoft.EntityFrameworkCore; + +namespace EFQuerying.ComplexQuery +{ + public class BloggingContext : DbContext + { + public DbSet Blogs { get; set; } + public DbSet Posts { get; set; } + + protected override void OnModelCreating(ModelBuilder modelBuilder) + { + modelBuilder.Entity() + .HasMany(b => b.Posts) + .WithOne(p => p.Blog) + .OnDelete(DeleteBehavior.NoAction); + + modelBuilder.Entity() + .HasOne(pt => pt.Post) + .WithMany(p => p.Tags) + .HasForeignKey(pt => pt.PostId); + + modelBuilder.Entity() + .HasOne(pt => pt.Tag) + .WithMany(t => t.Posts) + .HasForeignKey(pt => pt.TagId); + + modelBuilder.Entity() + .HasData( + new Blog { BlogId = 1, Url = @"https://devblogs.microsoft.com/dotnet", Rating = 5, OwnerId = 1, }, + new Blog { BlogId = 2, Url = @"https://mytravelblog.com/", Rating = 4, OwnerId = 3 }); + + modelBuilder.Entity() + .HasData( + new Post { PostId = 1, BlogId = 1, Title = "What's new", Content = "Lorem ipsum dolor sit amet", Rating = 5, AuthorId = 1 }, + new Post { PostId = 2, BlogId = 2, Title = "Around the World in Eighty Days", Content = "consectetur adipiscing elit", Rating = 5, AuthorId = 2 }, + new Post { PostId = 3, BlogId = 2, Title = "Glamping *is* the way", Content = "sed do eiusmod tempor incididunt", Rating = 4, AuthorId = 3 }, + new Post { PostId = 4, BlogId = 2, Title = "Travel in the time of pandemic", Content = "ut labore et dolore magna aliqua", Rating = 3, AuthorId = 3 }); + + modelBuilder.Entity() + .HasData( + new Person { PersonId = 1, Name = "Dotnet Blog Admin", PhotoId = 1 }, + new Person { PersonId = 2, Name = "Phileas Fogg", PhotoId = 2 }, + new Person { PersonId = 3, Name = "Jane Doe", PhotoId = 3 }); + + modelBuilder.Entity() + .HasData( + new PersonPhoto { PersonPhotoId = 1, Caption = "SN", Photo = new byte[] { 0x00, 0x01 } }, + new PersonPhoto { PersonPhotoId = 2, Caption = "PF", Photo = new byte[] { 0x01, 0x02, 0x03 } }, + new PersonPhoto { PersonPhotoId = 3, Caption = "JD", Photo = new byte[] { 0x01, 0x01, 0x01 } }); + + modelBuilder.Entity() + .HasData( + new Tag { TagId = "general" }, + new Tag { TagId = "classic" }, + new Tag { TagId = "opinion" }, + new Tag { TagId = "informative" }); + + modelBuilder.Entity() + .HasData( + new PostTag { PostTagId = 1, PostId = 1, TagId = "general" }, + new PostTag { PostTagId = 2, PostId = 1, TagId = "informative" }, + new PostTag { PostTagId = 3, PostId = 2, TagId = "classic" }, + new PostTag { PostTagId = 4, PostId = 3, TagId = "opinion" }, + new PostTag { PostTagId = 5, PostId = 4, TagId = "opinion" }, + new PostTag { PostTagId = 6, PostId = 4, TagId = "informative" }); + } + + protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder) + { + optionsBuilder.UseSqlServer(@"Server=(localdb)\mssqllocaldb;Database=EFQuerying.ComplexQuery;Trusted_Connection=True;ConnectRetryCount=0"); + } + } +} diff --git a/samples/core/Querying/ComplexQuery/ComplexQuery.csproj b/samples/core/Querying/ComplexQuery/ComplexQuery.csproj new file mode 100644 index 0000000000..f414ae8577 --- /dev/null +++ b/samples/core/Querying/ComplexQuery/ComplexQuery.csproj @@ -0,0 +1,15 @@ + + + + Exe + netcoreapp3.1 + EFQuerying.ComplexQuery + EFQuerying.ComplexQuery + + + + + + + + diff --git a/samples/core/Querying/ComplexQuery/Person.cs b/samples/core/Querying/ComplexQuery/Person.cs new file mode 100644 index 0000000000..e315e37b40 --- /dev/null +++ b/samples/core/Querying/ComplexQuery/Person.cs @@ -0,0 +1,16 @@ +using System.Collections.Generic; + +namespace EFQuerying.ComplexQuery +{ + public class Person + { + public int PersonId { get; set; } + public string Name { get; set; } + + public List AuthoredPosts { get; set; } + public List OwnedBlogs { get; set; } + + public int? PhotoId { get; set; } + public PersonPhoto Photo { get; set; } + } +} diff --git a/samples/core/Querying/PersonPhoto.cs b/samples/core/Querying/ComplexQuery/PersonPhoto.cs similarity index 79% rename from samples/core/Querying/PersonPhoto.cs rename to samples/core/Querying/ComplexQuery/PersonPhoto.cs index 59602ab6f9..dee8fd9472 100644 --- a/samples/core/Querying/PersonPhoto.cs +++ b/samples/core/Querying/ComplexQuery/PersonPhoto.cs @@ -1,6 +1,4 @@ -using System.Collections.Generic; - -namespace EFQuerying +namespace EFQuerying.ComplexQuery { public class PersonPhoto { diff --git a/samples/core/Querying/ComplexQuery/Post.cs b/samples/core/Querying/ComplexQuery/Post.cs new file mode 100644 index 0000000000..64d0dee663 --- /dev/null +++ b/samples/core/Querying/ComplexQuery/Post.cs @@ -0,0 +1,20 @@ +using System.Collections.Generic; + +namespace EFQuerying.ComplexQuery +{ + public class Post + { + public int PostId { get; set; } + public string Title { get; set; } + public string Content { get; set; } + public int Rating { get; set; } + + public int BlogId { get; set; } + public Blog Blog { get; set; } + + public int AuthorId { get; set; } + public Person Author { get; set; } + + public List Tags { get; set; } + } +} diff --git a/samples/core/Querying/PostTag.cs b/samples/core/Querying/ComplexQuery/PostTag.cs similarity index 63% rename from samples/core/Querying/PostTag.cs rename to samples/core/Querying/ComplexQuery/PostTag.cs index 6557f3c342..dea68e7d44 100644 --- a/samples/core/Querying/PostTag.cs +++ b/samples/core/Querying/ComplexQuery/PostTag.cs @@ -1,10 +1,4 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.Threading.Tasks; - -namespace EFQuerying +namespace EFQuerying.ComplexQuery { public class PostTag { diff --git a/samples/core/Querying/ComplexQuery/Sample.cs b/samples/core/Querying/ComplexQuery/Program.cs similarity index 97% rename from samples/core/Querying/ComplexQuery/Sample.cs rename to samples/core/Querying/ComplexQuery/Program.cs index d8438eaf58..2ca855b4f3 100644 --- a/samples/core/Querying/ComplexQuery/Sample.cs +++ b/samples/core/Querying/ComplexQuery/Program.cs @@ -1,13 +1,11 @@ -using System; -using System.Collections.Generic; +using System.Collections.Generic; using System.Linq; -using System.Text; namespace EFQuerying.ComplexQuery { - public class Sample + class Program { - public static void Run() + static void Main(string[] args) { using (var context = new BloggingContext()) { diff --git a/samples/core/Querying/ComplexQuery/Tag.cs b/samples/core/Querying/ComplexQuery/Tag.cs new file mode 100644 index 0000000000..bf7ce0774c --- /dev/null +++ b/samples/core/Querying/ComplexQuery/Tag.cs @@ -0,0 +1,11 @@ +using System.Collections.Generic; + +namespace EFQuerying.ComplexQuery +{ + public class Tag + { + public string TagId { get; set; } + + public List Posts { get; set; } + } +} diff --git a/samples/core/Querying/Overview/Blog.cs b/samples/core/Querying/Overview/Blog.cs new file mode 100644 index 0000000000..3f581809cd --- /dev/null +++ b/samples/core/Querying/Overview/Blog.cs @@ -0,0 +1,9 @@ +namespace EFQuerying.Overview +{ + public class Blog + { + public int BlogId { get; set; } + public string Url { get; set; } + public int? Rating { get; set; } + } +} diff --git a/samples/core/Querying/Overview/BloggingContext.cs b/samples/core/Querying/Overview/BloggingContext.cs new file mode 100644 index 0000000000..57b3467e40 --- /dev/null +++ b/samples/core/Querying/Overview/BloggingContext.cs @@ -0,0 +1,22 @@ +using Microsoft.EntityFrameworkCore; + +namespace EFQuerying.Overview +{ + public class BloggingContext : DbContext + { + public DbSet Blogs { get; set; } + + protected override void OnModelCreating(ModelBuilder modelBuilder) + { + modelBuilder.Entity() + .HasData( + new Blog { BlogId = 1, Url = @"https://devblogs.microsoft.com/dotnet", Rating = 5 }, + new Blog { BlogId = 2, Url = @"https://mytravelblog.com/", Rating = 4 }); + } + + protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder) + { + optionsBuilder.UseSqlServer(@"Server=(localdb)\mssqllocaldb;Database=EFQuerying.Overview;Trusted_Connection=True;ConnectRetryCount=0"); + } + } +} diff --git a/samples/core/QueryFiltersNavigations/QueryFiltersNavigations.csproj b/samples/core/Querying/Overview/Overview.csproj similarity index 50% rename from samples/core/QueryFiltersNavigations/QueryFiltersNavigations.csproj rename to samples/core/Querying/Overview/Overview.csproj index 7b61c0829b..d24d33f9f7 100644 --- a/samples/core/QueryFiltersNavigations/QueryFiltersNavigations.csproj +++ b/samples/core/Querying/Overview/Overview.csproj @@ -1,11 +1,15 @@  + Exe netcoreapp3.1 - Samples + EFQuerying.Overview + EFQuerying.Overview + - - + + + diff --git a/samples/core/Querying/Basics/Sample.cs b/samples/core/Querying/Overview/Program.cs similarity index 89% rename from samples/core/Querying/Basics/Sample.cs rename to samples/core/Querying/Overview/Program.cs index 8b59f3a47e..0430e0d573 100644 --- a/samples/core/Querying/Basics/Sample.cs +++ b/samples/core/Querying/Overview/Program.cs @@ -1,10 +1,10 @@ using System.Linq; -namespace EFQuerying.Basics +namespace EFQuerying.Overview { - public class Sample + class Program { - public static void Run() + static void Main(string[] args) { #region LoadingAllData using (var context = new BloggingContext()) diff --git a/samples/core/Querying/Program.cs b/samples/core/Querying/Program.cs deleted file mode 100644 index 9cba3b0c87..0000000000 --- a/samples/core/Querying/Program.cs +++ /dev/null @@ -1,15 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.Threading.Tasks; - -namespace EFQuerying -{ - class Program - { - static void Main(string[] args) - { - } - } -} diff --git a/samples/core/Querying/QueryFilters/AnimalContext.cs b/samples/core/Querying/QueryFilters/AnimalContext.cs new file mode 100644 index 0000000000..8579805d90 --- /dev/null +++ b/samples/core/Querying/QueryFilters/AnimalContext.cs @@ -0,0 +1,32 @@ +using Microsoft.EntityFrameworkCore; + +namespace EFQuerying.QueryFilters +{ + public class AnimalContext : DbContext + { + public DbSet People { get; set; } + public DbSet Animals { get; set; } + public DbSet Toys { get; set; } + + protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder) + { + optionsBuilder + .UseSqlServer( + @"Server=(localdb)\mssqllocaldb;Database=Querying.QueryFilters.Animals;Trusted_Connection=True;ConnectRetryCount=0;"); + } + + protected override void OnModelCreating(ModelBuilder modelBuilder) + { + modelBuilder.Entity().HasOne(c => c.Tolerates).WithOne(d => d.FriendsWith).HasForeignKey(c => c.ToleratesId); + modelBuilder.Entity().HasOne(d => d.FavoriteToy).WithOne(t => t.BelongsTo).HasForeignKey(d => d.BelongsToId); + + modelBuilder.Entity().HasQueryFilter(p => p.Pets.Count > 0); + modelBuilder.Entity().HasQueryFilter(a => !a.Name.StartsWith("P")); + modelBuilder.Entity().HasQueryFilter(a => a.Name.Length > 5); + + // Invalid query filter configuration as it causes cycles in query filters + //modelBuilder.Entity().HasQueryFilter(a => a.Owner.Name != "John"); + } + } + +} diff --git a/samples/core/Querying/QueryFilters/Blog.cs b/samples/core/Querying/QueryFilters/Blog.cs new file mode 100644 index 0000000000..68ef83dc70 --- /dev/null +++ b/samples/core/Querying/QueryFilters/Blog.cs @@ -0,0 +1,64 @@ +using System.Collections.Generic; + +namespace EFQuerying.QueryFilters +{ + #region Entities + public class Blog + { + private string _tenantId; + + public int BlogId { get; set; } + public string Name { get; set; } + public string Url { get; set; } + + public List Posts { get; set; } + } + + public class Post + { + public int PostId { get; set; } + public string Title { get; set; } + public string Content { get; set; } + public bool IsDeleted { get; set; } + + public Blog Blog { get; set; } + } + #endregion + + public class Person + { + public int Id { get; set; } + public string Name { get; set; } + public List Pets { get; set; } + } + + public abstract class Animal + { + public int Id { get; set; } + public string Name { get; set; } + public Person Owner { get; set; } + } + + public class Cat : Animal + { + public bool PrefersCardboardBoxes { get; set; } + + public int? ToleratesId { get; set; } + + public Dog Tolerates { get; set; } + } + + public class Dog : Animal + { + public Toy FavoriteToy { get; set; } + public Cat FriendsWith { get; set; } + } + + public class Toy + { + public int Id { get; set; } + public string Name { get; set; } + public int? BelongsToId { get; set; } + public Dog BelongsTo { get; set; } + } +} diff --git a/samples/core/Querying/QueryFilters/BloggingContext.cs b/samples/core/Querying/QueryFilters/BloggingContext.cs new file mode 100644 index 0000000000..ac5f842465 --- /dev/null +++ b/samples/core/Querying/QueryFilters/BloggingContext.cs @@ -0,0 +1,56 @@ +using Microsoft.EntityFrameworkCore; +using System.Linq; + +namespace EFQuerying.QueryFilters +{ + public class BloggingContext : DbContext + { + private readonly string _tenantId; + + public BloggingContext(string tenant) + { + _tenantId = tenant; + } + + public DbSet Blogs { get; set; } + public DbSet Posts { get; set; } + + protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder) + { + optionsBuilder + .UseSqlServer( + @"Server=(localdb)\mssqllocaldb;Database=Querying.QueryFilters.Blogging;Trusted_Connection=True;ConnectRetryCount=0;"); + } + + protected override void OnModelCreating(ModelBuilder modelBuilder) + { + modelBuilder.Entity().Property("_tenantId").HasColumnName("TenantId"); + + // Configure entity filters + #region FilterConfiguration + modelBuilder.Entity().HasQueryFilter(b => EF.Property(b, "_tenantId") == _tenantId); + modelBuilder.Entity().HasQueryFilter(p => !p.IsDeleted); + #endregion + } + + public override int SaveChanges() + { + ChangeTracker.DetectChanges(); + + foreach (var item in ChangeTracker.Entries().Where( + e => + e.State == EntityState.Added && e.Metadata.GetProperties().Any(p => p.Name == "_tenantId"))) + { + item.CurrentValues["_tenantId"] = _tenantId; + } + + foreach (var item in ChangeTracker.Entries().Where(e => e.State == EntityState.Deleted)) + { + item.State = EntityState.Modified; + item.CurrentValues["IsDeleted"] = true; + } + + return base.SaveChanges(); + } + } +} diff --git a/samples/core/Querying/QueryFilters/FilteredBloggingContextRequired.cs b/samples/core/Querying/QueryFilters/FilteredBloggingContextRequired.cs new file mode 100644 index 0000000000..82f953b01b --- /dev/null +++ b/samples/core/Querying/QueryFilters/FilteredBloggingContextRequired.cs @@ -0,0 +1,53 @@ +using Microsoft.EntityFrameworkCore; + +namespace EFQuerying.QueryFilters +{ + public class FilteredBloggingContextRequired : DbContext + { + public DbSet Blogs { get; set; } + public DbSet Posts { get; set; } + + protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder) + { + optionsBuilder + .UseSqlServer( + @"Server=(localdb)\mssqllocaldb;Database=Querying.QueryFilters.BloggingRequired;Trusted_Connection=True;ConnectRetryCount=0;"); + } + + protected override void OnModelCreating(ModelBuilder modelBuilder) + { + var setup = "OptionalNav"; + if (setup == "Faulty") + { + // Incorrect setup - Required navigation used to reference entity that has query filter defined, + // but no query filter for the entity on the other side of the navigation. + #region IncorrectFilter + modelBuilder.Entity().HasMany(b => b.Posts).WithOne(p => p.Blog).IsRequired(); + modelBuilder.Entity().HasQueryFilter(b => b.Url.Contains("fish")); + #endregion + } + else if (setup == "OptionalNav") + { + // The relationship is marked as optional so dependent can exist even if principal is filtered out. + #region OptionalNavigation + modelBuilder.Entity().HasMany(b => b.Posts).WithOne(p => p.Blog).IsRequired(false); + modelBuilder.Entity().HasQueryFilter(b => b.Url.Contains("fish")); + #endregion + } + else + { + // The relationship is still required but there is a matching filter configured on dependent side too, + // which matches principal side. So if principal is filtered out, the dependent would also be. + #region MatchingFilters + modelBuilder.Entity().HasMany(b => b.Posts).WithOne(p => p.Blog).IsRequired(); + modelBuilder.Entity().HasQueryFilter(b => b.Url.Contains("fish")); + 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 new file mode 100644 index 0000000000..efefe631b4 --- /dev/null +++ b/samples/core/Querying/QueryFilters/Program.cs @@ -0,0 +1,264 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using Microsoft.EntityFrameworkCore; + +namespace EFQuerying.QueryFilters +{ + class Program + { + static void Main(string[] args) + { + QueryFiltersBasicExample(); + QueryFiltersWithNavigationsExample(); + QueryFiltersWithRequiredNavigationExample(); + } + + static void QueryFiltersBasicExample() + { + using (var db = new BloggingContext("diego")) + { + if (db.Database.EnsureCreated()) + { + 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.SaveChanges(); + + using (var andrewDb = new BloggingContext("andrew")) + { + andrewDb.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" } + } + }); + + andrewDb.SaveChanges(); + } + + db.Posts + .Where( + p => p.Title == "Caring for tropical fish" + || p.Title == "Cat care 101") + .ToList() + .ForEach(p => db.Posts.Remove(p)); + + db.SaveChanges(); + } + } + + using (var db = new BloggingContext("Diego")) + { + var blogs = db.Blogs + .Include(b => b.Posts) + .ToList(); + + foreach (var blog in blogs) + { + Console.WriteLine( + $"{blog.Url,-33} [Tenant: {db.Entry(blog).Property("_tenantId").CurrentValue}]"); + + foreach (var post in blog.Posts) + { + Console.WriteLine($" - {post.Title,-30} [IsDeleted: {post.IsDeleted}]"); + } + + Console.WriteLine(); + } + + #region IgnoreFilters + blogs = db.Blogs + .Include(b => b.Posts) + .IgnoreQueryFilters() + .ToList(); + #endregion + + foreach (var blog in blogs) + { + Console.WriteLine( + $"{blog.Url,-33} [Tenant: {db.Entry(blog).Property("_tenantId").CurrentValue}]"); + + foreach (var post in blog.Posts) + { + Console.WriteLine($" - {post.Title,-30} [IsDeleted: {post.IsDeleted}]"); + } + } + } + } + + private static void QueryFiltersWithNavigationsExample() + { + using (var animalContext = new AnimalContext()) + { + animalContext.Database.EnsureDeleted(); + animalContext.Database.EnsureCreated(); + + var janice = new Person { Name = "Janice" }; + var jamie = new Person { Name = "Jamie" }; + var cesar = new Person { Name = "Cesar" }; + var paul = new Person { Name = "Paul" }; + var dominic = new Person { Name = "Dominic" }; + + var kibbles = new Cat { Name = "Kibbles", PrefersCardboardBoxes = false, Owner = janice }; + var sammy = new Cat { Name = "Sammy", PrefersCardboardBoxes = true, Owner = janice }; + var puffy = new Cat { Name = "Puffy", PrefersCardboardBoxes = true, Owner = jamie }; + var hati = new Dog { Name = "Hati", FavoriteToy = new Toy { Name = "Squeeky duck" }, Owner = dominic, FriendsWith = puffy }; + var simba = new Dog { Name = "Simba", FavoriteToy = new Toy { Name = "Bone" }, Owner = cesar, FriendsWith = sammy }; + puffy.Tolerates = hati; + sammy.Tolerates = simba; + + animalContext.People.AddRange(janice, jamie, cesar, paul, dominic); + animalContext.Animals.AddRange(kibbles, sammy, puffy, hati, simba); + animalContext.SaveChanges(); + } + + using (var animalContext = new AnimalContext()) + { + Console.WriteLine("*****************"); + Console.WriteLine("* Animal lovers *"); + Console.WriteLine("*****************"); + + // Jamie and Paul are filtered out. + // Paul doesn't own any pets. Jamie owns Puffy, but her pet has been filtered out. + var animalLovers = animalContext.People.ToList(); + DisplayResults(animalLovers); + + Console.WriteLine("**************************************************"); + Console.WriteLine("* Animal lovers and their pets - filters enabled *"); + Console.WriteLine("**************************************************"); + + // Jamie and Paul are filtered out. + // Paul doesn't own any pets. Jamie owns Puffy, but her pet has been filtered out. + // Simba's favorite toy has also been filtered out. + // Puffy is filtered out so he doesn't show up as Hati's friend. + var ownersAndTheirPets = animalContext.People + .Include(p => p.Pets) + .ThenInclude(p => ((Dog)p).FavoriteToy) + .ToList(); + + DisplayResults(ownersAndTheirPets); + + Console.WriteLine("*********************************************************"); + Console.WriteLine("* Animal lovers and their pets - query filters disabled *"); + Console.WriteLine("*********************************************************"); + + var ownersAndTheirPetsUnfiltered = animalContext.People + .IgnoreQueryFilters() + .Include(p => p.Pets) + .ThenInclude(p => ((Dog)p).FavoriteToy) + .ToList(); + + DisplayResults(ownersAndTheirPetsUnfiltered); + } + + static void DisplayResults(List people) + { + foreach (var person in people) + { + Console.WriteLine($"{person.Name}"); + if (person.Pets != null) + { + foreach (var pet in person.Pets) + { + Console.Write($" - {pet.Name} [{pet.GetType().Name}] "); + if (pet is Cat cat) + { + Console.Write($"| Prefers cardboard boxes: {(cat.PrefersCardboardBoxes ? "Yes" : "No")} "); + Console.WriteLine($"| Tolerates: {(cat.Tolerates != null ? cat.Tolerates.Name : "No one")}"); + } + else if (pet is Dog dog) + { + Console.Write($"| Favorite toy: {(dog.FavoriteToy != null ? dog.FavoriteToy.Name : "None")} "); + Console.WriteLine($"| Friend: {(dog.FriendsWith != null ? dog.FriendsWith.Name : "The Owner")}"); + } + } + } + } + } + } + + private static void QueryFiltersWithRequiredNavigationExample() + { + using (var db = new FilteredBloggingContextRequired()) + { + db.Database.EnsureDeleted(); + db.Database.EnsureCreated(); + + #region SeedData + 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" } + } + }); + #endregion + + db.SaveChanges(); + } + + Console.WriteLine("Use of required navigations to access entity with query filter demo"); + using (var db = new FilteredBloggingContextRequired()) + { + + #region Queries + var allPosts = db.Posts.ToList(); + var allPostsWithBlogsIncluded = db.Posts.Include(p => p.Blog).ToList(); + #endregion + + if (allPosts.Count == allPostsWithBlogsIncluded.Count) + { + Console.WriteLine($"Query filters set up correctly. Result count for both queries: {allPosts.Count}."); + } + else + { + Console.WriteLine("Unexpected discrepancy due to query filters and required navigations interaction."); + Console.WriteLine($"All posts count: {allPosts.Count}."); + Console.WriteLine($"All posts with blogs included count: {allPostsWithBlogsIncluded.Count}."); + } + } + } + } +} diff --git a/samples/core/Querying/QueryFilters/QueryFilters.csproj b/samples/core/Querying/QueryFilters/QueryFilters.csproj new file mode 100644 index 0000000000..8c78314652 --- /dev/null +++ b/samples/core/Querying/QueryFilters/QueryFilters.csproj @@ -0,0 +1,15 @@ + + + + Exe + netcoreapp3.1 + EFQuerying.QueryFilters + EFQuerying.QueryFilters + + + + + + + + diff --git a/samples/core/Querying/RawSQL/Blog.cs b/samples/core/Querying/RawSQL/Blog.cs new file mode 100644 index 0000000000..fa1245dc56 --- /dev/null +++ b/samples/core/Querying/RawSQL/Blog.cs @@ -0,0 +1,9 @@ +namespace EFQuerying.RawSQL +{ + public class Blog + { + public int BlogId { get; set; } + public string Url { get; set; } + public int? Rating { get; set; } + } +} diff --git a/samples/core/Querying/RawSQL/BloggingContext.cs b/samples/core/Querying/RawSQL/BloggingContext.cs new file mode 100644 index 0000000000..75a50dc98e --- /dev/null +++ b/samples/core/Querying/RawSQL/BloggingContext.cs @@ -0,0 +1,22 @@ +using Microsoft.EntityFrameworkCore; + +namespace EFQuerying.RawSQL +{ + public class BloggingContext : DbContext + { + public DbSet Blogs { get; set; } + + protected override void OnModelCreating(ModelBuilder modelBuilder) + { + modelBuilder.Entity() + .HasData( + new Blog { BlogId = 1, Url = @"https://devblogs.microsoft.com/dotnet", Rating = 5 }, + new Blog { BlogId = 2, Url = @"https://mytravelblog.com/", Rating = 4 }); + } + + protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder) + { + optionsBuilder.UseSqlServer(@"Server=(localdb)\mssqllocaldb;Database=EFQuerying.RawSQL;Trusted_Connection=True;ConnectRetryCount=0"); + } + } +} diff --git a/samples/core/Querying/RawSQL/Post.cs b/samples/core/Querying/RawSQL/Post.cs new file mode 100644 index 0000000000..41bee508bd --- /dev/null +++ b/samples/core/Querying/RawSQL/Post.cs @@ -0,0 +1,13 @@ +namespace EFQuerying.RawSQL +{ + public class Post + { + public int PostId { get; set; } + public string Title { get; set; } + public string Content { get; set; } + public int Rating { get; set; } + + public int BlogId { get; set; } + public Blog Blog { get; set; } + } +} diff --git a/samples/core/Querying/RawSQL/Sample.cs b/samples/core/Querying/RawSQL/Program.cs similarity index 96% rename from samples/core/Querying/RawSQL/Sample.cs rename to samples/core/Querying/RawSQL/Program.cs index fd94d0c70d..dc31badf5d 100644 --- a/samples/core/Querying/RawSQL/Sample.cs +++ b/samples/core/Querying/RawSQL/Program.cs @@ -1,12 +1,12 @@ -using System.Linq; +using Microsoft.Data.SqlClient; using Microsoft.EntityFrameworkCore; -using Microsoft.Data.SqlClient; +using System.Linq; namespace EFQuerying.RawSQL { - public class Sample + class Program { - public static void Run() + static void Main(string[] args) { using (var context = new BloggingContext()) { diff --git a/samples/core/Querying/Querying.csproj b/samples/core/Querying/RawSQL/RawSQL.csproj similarity index 79% rename from samples/core/Querying/Querying.csproj rename to samples/core/Querying/RawSQL/RawSQL.csproj index dab0384204..b99a44e1c4 100644 --- a/samples/core/Querying/Querying.csproj +++ b/samples/core/Querying/RawSQL/RawSQL.csproj @@ -3,8 +3,8 @@ Exe netcoreapp3.1 - EFQuerying - EFQuerying + EFQuerying.RawSQL + EFQuerying.RawSQL diff --git a/samples/core/Querying/Blog.cs b/samples/core/Querying/RelatedData/Blog.cs similarity index 90% rename from samples/core/Querying/Blog.cs rename to samples/core/Querying/RelatedData/Blog.cs index a987f6a20e..4d193215aa 100644 --- a/samples/core/Querying/Blog.cs +++ b/samples/core/Querying/RelatedData/Blog.cs @@ -1,6 +1,6 @@ using System.Collections.Generic; -namespace EFQuerying +namespace EFQuerying.RelatedData { public class Blog { diff --git a/samples/core/Querying/BloggingContext.cs b/samples/core/Querying/RelatedData/BloggingContext.cs similarity index 96% rename from samples/core/Querying/BloggingContext.cs rename to samples/core/Querying/RelatedData/BloggingContext.cs index e63791c502..7a2e923719 100644 --- a/samples/core/Querying/BloggingContext.cs +++ b/samples/core/Querying/RelatedData/BloggingContext.cs @@ -1,6 +1,6 @@ using Microsoft.EntityFrameworkCore; -namespace EFQuerying +namespace EFQuerying.RelatedData { public class BloggingContext : DbContext { @@ -67,7 +67,7 @@ protected override void OnModelCreating(ModelBuilder modelBuilder) protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder) { - optionsBuilder.UseSqlServer(@"Server=(localdb)\mssqllocaldb;Database=EFQuerying;Trusted_Connection=True;ConnectRetryCount=0"); + optionsBuilder.UseSqlServer(@"Server=(localdb)\mssqllocaldb;Database=EFQuerying.ComplexQuery;Trusted_Connection=True;ConnectRetryCount=0"); } } } diff --git a/samples/core/Querying/Person.cs b/samples/core/Querying/RelatedData/Person.cs similarity index 91% rename from samples/core/Querying/Person.cs rename to samples/core/Querying/RelatedData/Person.cs index 620aa8da24..1579bd3714 100644 --- a/samples/core/Querying/Person.cs +++ b/samples/core/Querying/RelatedData/Person.cs @@ -1,6 +1,6 @@ using System.Collections.Generic; -namespace EFQuerying +namespace EFQuerying.RelatedData { public class Person { diff --git a/samples/core/Querying/RelatedData/PersonPhoto.cs b/samples/core/Querying/RelatedData/PersonPhoto.cs new file mode 100644 index 0000000000..91394d0b5e --- /dev/null +++ b/samples/core/Querying/RelatedData/PersonPhoto.cs @@ -0,0 +1,11 @@ +namespace EFQuerying.RelatedData +{ + public class PersonPhoto + { + public int PersonPhotoId { get; set; } + public string Caption { get; set; } + public byte[] Photo { get; set; } + + public Person Person { get; set; } + } +} diff --git a/samples/core/Querying/Post.cs b/samples/core/Querying/RelatedData/Post.cs similarity index 93% rename from samples/core/Querying/Post.cs rename to samples/core/Querying/RelatedData/Post.cs index f4a4e28672..5f30b6e6b9 100644 --- a/samples/core/Querying/Post.cs +++ b/samples/core/Querying/RelatedData/Post.cs @@ -1,6 +1,6 @@ using System.Collections.Generic; -namespace EFQuerying +namespace EFQuerying.RelatedData { public class Post { diff --git a/samples/core/Querying/RelatedData/PostTag.cs b/samples/core/Querying/RelatedData/PostTag.cs new file mode 100644 index 0000000000..05a848366f --- /dev/null +++ b/samples/core/Querying/RelatedData/PostTag.cs @@ -0,0 +1,13 @@ +namespace EFQuerying.RelatedData +{ + public class PostTag + { + public int PostTagId { get; set; } + + public int PostId { get; set; } + public Post Post { get; set; } + + public string TagId { get; set; } + public Tag Tag { get; set; } + } +} diff --git a/samples/core/Querying/RelatedData/Sample.cs b/samples/core/Querying/RelatedData/Program.cs similarity index 97% rename from samples/core/Querying/RelatedData/Sample.cs rename to samples/core/Querying/RelatedData/Program.cs index b90c98fd74..fc75d07660 100644 --- a/samples/core/Querying/RelatedData/Sample.cs +++ b/samples/core/Querying/RelatedData/Program.cs @@ -1,11 +1,15 @@ using Microsoft.EntityFrameworkCore; +using System; +using System.Collections.Generic; using System.Linq; +using System.Text; +using System.Threading.Tasks; namespace EFQuerying.RelatedData { - public class Sample + class Program { - public static void Run() + static void Main(string[] args) { #region SingleInclude using (var context = new BloggingContext()) diff --git a/samples/core/Querying/RelatedData/RelatedData.csproj b/samples/core/Querying/RelatedData/RelatedData.csproj new file mode 100644 index 0000000000..b54d6852e2 --- /dev/null +++ b/samples/core/Querying/RelatedData/RelatedData.csproj @@ -0,0 +1,15 @@ + + + + Exe + netcoreapp3.1 + EFQuerying.RelatedData + EFQuerying.RelatedData + + + + + + + + diff --git a/samples/core/Querying/RelatedData/Tag.cs b/samples/core/Querying/RelatedData/Tag.cs new file mode 100644 index 0000000000..3b0069bb2b --- /dev/null +++ b/samples/core/Querying/RelatedData/Tag.cs @@ -0,0 +1,11 @@ +using System.Collections.Generic; + +namespace EFQuerying.RelatedData +{ + public class Tag + { + public string TagId { get; set; } + + public List Posts { get; set; } + } +} diff --git a/samples/core/Querying/RelatedData/ThrowOnIgnoredInclude/BloggingContext.cs b/samples/core/Querying/RelatedData/ThrowOnIgnoredInclude/BloggingContext.cs deleted file mode 100644 index ca1c92c848..0000000000 --- a/samples/core/Querying/RelatedData/ThrowOnIgnoredInclude/BloggingContext.cs +++ /dev/null @@ -1,19 +0,0 @@ -using Microsoft.EntityFrameworkCore; -using Microsoft.EntityFrameworkCore.Diagnostics; - -namespace EFQuerying.RelatedData.ThrowOnIgnoredInclude -{ - public class BloggingContext : DbContext - { - public DbSet Blogs { get; set; } - public DbSet Posts { get; set; } - - #region OnConfiguring - protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder) - { - optionsBuilder - .UseSqlServer(@"Server=(localdb)\mssqllocaldb;Database=EFQuerying;Trusted_Connection=True;ConnectRetryCount=0"); - } - #endregion - } -} diff --git a/samples/core/Querying/Tag.cs b/samples/core/Querying/Tag.cs deleted file mode 100644 index a07689df28..0000000000 --- a/samples/core/Querying/Tag.cs +++ /dev/null @@ -1,15 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.Threading.Tasks; - -namespace EFQuerying -{ - public class Tag - { - public string TagId { get; set; } - - public List Posts { get; set; } - } -} diff --git a/samples/core/Querying/Tags/Program.cs b/samples/core/Querying/Tags/Program.cs new file mode 100644 index 0000000000..bc78f7f20e --- /dev/null +++ b/samples/core/Querying/Tags/Program.cs @@ -0,0 +1,63 @@ +using Microsoft.EntityFrameworkCore; +using NetTopologySuite.Geometries; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace EFQuerying.Tags +{ + class Program + { + static void Main(string[] args) + { + using (var context = new SpatialContext()) + { + context.Database.EnsureDeleted(); + context.Database.EnsureCreated(); + } + + using (var context = new SpatialContext()) + { + #region BasicQueryTag + var myLocation = new Point(1, 2); + var nearestPeople = (from f in context.People.TagWith("This is my spatial query!") + orderby f.Location.Distance(myLocation) descending + select f).Take(5).ToList(); + #endregion + } + + using (var context = new SpatialContext()) + { + #region ChainedQueryTags + var results = Limit(GetNearestPeople(context, new Point(1, 2)), 25).ToList(); + #endregion + } + + using (var context = new SpatialContext()) + { + #region MultilineQueryTag + var results = Limit(GetNearestPeople(context, new Point(1, 2)), 25).TagWith( +@"This is a multi-line +string").ToList(); + #endregion + } + } + + #region QueryableMethods + static IQueryable GetNearestPeople(SpatialContext context, Point myLocation) + => from f in context.People.TagWith("GetNearestPeople") + orderby f.Location.Distance(myLocation) descending + select f; + + static IQueryable Limit(IQueryable source, int limit) => source.TagWith("Limit").Take(limit); + #endregion + } + + public class Person + { + public int Id { get; set; } + public Point Location { get; set; } + } +} diff --git a/samples/core/Querying/Tags/SpatialContext.cs b/samples/core/Querying/Tags/SpatialContext.cs new file mode 100644 index 0000000000..afd99a3130 --- /dev/null +++ b/samples/core/Querying/Tags/SpatialContext.cs @@ -0,0 +1,29 @@ +using Microsoft.EntityFrameworkCore; +using NetTopologySuite.Geometries; + +namespace EFQuerying.Tags +{ + public class SpatialContext : DbContext + { + public DbSet People { get; set; } + + protected override void OnModelCreating(ModelBuilder modelBuilder) + { + modelBuilder.Entity(b => + { + b.Property(e => e.Location).HasColumnType("geometry"); + + b.HasData( + new Person { Id = 1, Location = new Point(0, 1) }, + new Person { Id = 2, Location = new Point(2, 1) }, + new Person { Id = 3, Location = new Point(4, 5) }); + }); + } + + protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder) + { + optionsBuilder.UseSqlServer(@"Server=(localdb)\mssqllocaldb;Database=EFQuerying.Tags;Trusted_Connection=True;ConnectRetryCount=0", + b => b.UseNetTopologySuite()); + } + } +} diff --git a/samples/core/Querying/Tags/Tags.csproj b/samples/core/Querying/Tags/Tags.csproj new file mode 100644 index 0000000000..e9bb90235c --- /dev/null +++ b/samples/core/Querying/Tags/Tags.csproj @@ -0,0 +1,15 @@ + + + + Exe + netcoreapp3.1 + EFQuerying.Tags + EFQuerying.Tags + + + + + + + + diff --git a/samples/core/Querying/Tracking/Blog.cs b/samples/core/Querying/Tracking/Blog.cs new file mode 100644 index 0000000000..1b64671975 --- /dev/null +++ b/samples/core/Querying/Tracking/Blog.cs @@ -0,0 +1,12 @@ +using System.Collections.Generic; + +namespace EFQuerying.Tracking +{ + public class Blog + { + public int BlogId { get; set; } + public string Url { get; set; } + public int? Rating { get; set; } + public List Posts { get; set; } + } +} diff --git a/samples/core/Querying/Tracking/BloggingContext.cs b/samples/core/Querying/Tracking/BloggingContext.cs new file mode 100644 index 0000000000..ff07924527 --- /dev/null +++ b/samples/core/Querying/Tracking/BloggingContext.cs @@ -0,0 +1,22 @@ +using Microsoft.EntityFrameworkCore; + +namespace EFQuerying.Tracking +{ + public class BloggingContext : DbContext + { + public DbSet Blogs { get; set; } + + protected override void OnModelCreating(ModelBuilder modelBuilder) + { + modelBuilder.Entity() + .HasData( + new Blog { BlogId = 1, Url = @"https://devblogs.microsoft.com/dotnet", Rating = 5 }, + new Blog { BlogId = 2, Url = @"https://mytravelblog.com/", Rating = 4 }); + } + + protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder) + { + optionsBuilder.UseSqlServer(@"Server=(localdb)\mssqllocaldb;Database=EFQuerying.Tracking;Trusted_Connection=True;ConnectRetryCount=0"); + } + } +} diff --git a/samples/core/Querying/Tracking/Post.cs b/samples/core/Querying/Tracking/Post.cs new file mode 100644 index 0000000000..6c8eb49abd --- /dev/null +++ b/samples/core/Querying/Tracking/Post.cs @@ -0,0 +1,13 @@ +namespace EFQuerying.Tracking +{ + public class Post + { + public int PostId { get; set; } + public string Title { get; set; } + public string Content { get; set; } + public int Rating { get; set; } + + public int BlogId { get; set; } + public Blog Blog { get; set; } + } +} diff --git a/samples/core/Querying/Tracking/Sample.cs b/samples/core/Querying/Tracking/Program.cs similarity index 95% rename from samples/core/Querying/Tracking/Sample.cs rename to samples/core/Querying/Tracking/Program.cs index 4c4f0fb95d..7f8cb07a92 100644 --- a/samples/core/Querying/Tracking/Sample.cs +++ b/samples/core/Querying/Tracking/Program.cs @@ -1,11 +1,15 @@ using Microsoft.EntityFrameworkCore; +using System; +using System.Collections.Generic; using System.Linq; +using System.Text; +using System.Threading.Tasks; namespace EFQuerying.Tracking { - public class Sample + class Program { - public static void Run() + static void Main(string[] args) { using (var context = new BloggingContext()) { diff --git a/samples/core/QueryFilters/QueryFilters.csproj b/samples/core/Querying/Tracking/Tracking.csproj similarity index 50% rename from samples/core/QueryFilters/QueryFilters.csproj rename to samples/core/Querying/Tracking/Tracking.csproj index 7b61c0829b..bdf24a2b7c 100644 --- a/samples/core/QueryFilters/QueryFilters.csproj +++ b/samples/core/Querying/Tracking/Tracking.csproj @@ -1,11 +1,15 @@  + Exe netcoreapp3.1 - Samples + EFQuerying.Tracking + EFQuerying.Tracking + - - + + + diff --git a/samples/core/Samples.sln b/samples/core/Samples.sln index e07555128a..9859d3f133 100644 --- a/samples/core/Samples.sln +++ b/samples/core/Samples.sln @@ -23,8 +23,6 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "BusinessLogic", "Miscellane EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Logging", "Miscellaneous\Logging\Logging\Logging.csproj", "{D3A5391D-9351-44D6-8122-2821C6E4BD62}" EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Querying", "Querying\Querying.csproj", "{7DEB577A-52B8-4475-A109-689FD515AEEA}" -EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "ConnectionResiliency", "Miscellaneous\ConnectionResiliency\ConnectionResiliency.csproj", "{73D58479-A7E6-4867-8A73-0E07E96C6117}" EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "DynamicModel", "Modeling\DynamicModel\DynamicModel.csproj", "{70D00673-084C-40B7-B0F1-67498593F8EE}" @@ -33,10 +31,6 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "DataSeeding", "Modeling\Dat EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "OwnedEntities", "Modeling\OwnedEntities\OwnedEntities.csproj", "{802E31AD-2F1E-41A1-A662-5929E2626601}" EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "QueryFilters", "QueryFilters\QueryFilters.csproj", "{F96E15B3-C203-4DDC-BA7B-D33EB60B85CC}" -EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "CompiledQueries", "CompiledQueries\CompiledQueries.csproj", "{B5A76E8B-79B5-4B10-B9B8-F2A6883FE821}" -EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "DbContextPooling", "DbContextPooling\DbContextPooling.csproj", "{EFA3A7DD-F76C-4DA9-A657-D08E78E7E397}" EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "KeylessEntityTypes", "KeylessEntityTypes\KeylessEntityTypes.csproj", "{BB293879-4741-4D7A-87E8-3B9727B43A19}" @@ -59,8 +53,6 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Tests", "Miscellaneous\Test EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "SharedDatabaseTests", "Miscellaneous\Testing\ItemsWebApi\SharedDatabaseTests\SharedDatabaseTests.csproj", "{34C237C8-DD12-4C14-9B15-B7F85C218CDB}" EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "QueryFiltersNavigations", "QueryFiltersNavigations\QueryFiltersNavigations.csproj", "{CDB06F3E-A3D8-4DF8-AB32-71BF9B2EEC29}" -EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Collations", "Miscellaneous\Collations\Collations.csproj", "{62C86664-49F4-4C59-A2EC-1D70D85149D9}" EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Async", "Miscellaneous\Async\Async.csproj", "{1DA2B6AD-F71A-4224-92EB-3D0EE6E68BF4}" @@ -73,6 +65,24 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "SqlServer", "Spatial\SqlSer EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Projections", "Spatial\Projections\Projections.csproj", "{59588FC9-0A4F-4903-84B0-A2CB2EE6F9AC}" EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Querying", "Querying", "{1AD64707-0BE0-48B0-A803-916FF96DCB4F}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Overview", "Querying\Overview\Overview.csproj", "{524AE4A6-A15F-466C-AED3-CDF3209E4394}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "ClientEvaluation", "Querying\ClientEvaluation\ClientEvaluation.csproj", "{AF8B71E6-454F-4B66-98EE-2128B415EC45}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Tracking", "Querying\Tracking\Tracking.csproj", "{CCD901FE-20C8-4DC0-AD9C-8B8333EE277B}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "RawSQL", "Querying\RawSQL\RawSQL.csproj", "{63B6747C-470D-4561-A3A2-3C024747BE3B}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "ComplexQuery", "Querying\ComplexQuery\ComplexQuery.csproj", "{AD0BF60E-B46C-4181-BF43-D68801E5D259}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Tags", "Querying\Tags\Tags.csproj", "{2343A153-5832-41B9-A1FF-D80D3107A1DA}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "QueryFilters", "Querying\QueryFilters\QueryFilters.csproj", "{17A82259-1B8A-4199-A564-AC250F51360E}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "RelatedData", "Querying\RelatedData\RelatedData.csproj", "{CE9050E2-48AE-4CC4-8CCB-E19EE3102C0A}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -107,10 +117,6 @@ Global {D3A5391D-9351-44D6-8122-2821C6E4BD62}.Debug|Any CPU.Build.0 = Debug|Any CPU {D3A5391D-9351-44D6-8122-2821C6E4BD62}.Release|Any CPU.ActiveCfg = Release|Any CPU {D3A5391D-9351-44D6-8122-2821C6E4BD62}.Release|Any CPU.Build.0 = Release|Any CPU - {7DEB577A-52B8-4475-A109-689FD515AEEA}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {7DEB577A-52B8-4475-A109-689FD515AEEA}.Debug|Any CPU.Build.0 = Debug|Any CPU - {7DEB577A-52B8-4475-A109-689FD515AEEA}.Release|Any CPU.ActiveCfg = Release|Any CPU - {7DEB577A-52B8-4475-A109-689FD515AEEA}.Release|Any CPU.Build.0 = Release|Any CPU {73D58479-A7E6-4867-8A73-0E07E96C6117}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {73D58479-A7E6-4867-8A73-0E07E96C6117}.Debug|Any CPU.Build.0 = Debug|Any CPU {73D58479-A7E6-4867-8A73-0E07E96C6117}.Release|Any CPU.ActiveCfg = Release|Any CPU @@ -127,14 +133,6 @@ Global {802E31AD-2F1E-41A1-A662-5929E2626601}.Debug|Any CPU.Build.0 = Debug|Any CPU {802E31AD-2F1E-41A1-A662-5929E2626601}.Release|Any CPU.ActiveCfg = Release|Any CPU {802E31AD-2F1E-41A1-A662-5929E2626601}.Release|Any CPU.Build.0 = Release|Any CPU - {F96E15B3-C203-4DDC-BA7B-D33EB60B85CC}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {F96E15B3-C203-4DDC-BA7B-D33EB60B85CC}.Debug|Any CPU.Build.0 = Debug|Any CPU - {F96E15B3-C203-4DDC-BA7B-D33EB60B85CC}.Release|Any CPU.ActiveCfg = Release|Any CPU - {F96E15B3-C203-4DDC-BA7B-D33EB60B85CC}.Release|Any CPU.Build.0 = Release|Any CPU - {B5A76E8B-79B5-4B10-B9B8-F2A6883FE821}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {B5A76E8B-79B5-4B10-B9B8-F2A6883FE821}.Debug|Any CPU.Build.0 = Debug|Any CPU - {B5A76E8B-79B5-4B10-B9B8-F2A6883FE821}.Release|Any CPU.ActiveCfg = Release|Any CPU - {B5A76E8B-79B5-4B10-B9B8-F2A6883FE821}.Release|Any CPU.Build.0 = Release|Any CPU {EFA3A7DD-F76C-4DA9-A657-D08E78E7E397}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {EFA3A7DD-F76C-4DA9-A657-D08E78E7E397}.Debug|Any CPU.Build.0 = Debug|Any CPU {EFA3A7DD-F76C-4DA9-A657-D08E78E7E397}.Release|Any CPU.ActiveCfg = Release|Any CPU @@ -175,10 +173,6 @@ Global {34C237C8-DD12-4C14-9B15-B7F85C218CDB}.Debug|Any CPU.Build.0 = Debug|Any CPU {34C237C8-DD12-4C14-9B15-B7F85C218CDB}.Release|Any CPU.ActiveCfg = Release|Any CPU {34C237C8-DD12-4C14-9B15-B7F85C218CDB}.Release|Any CPU.Build.0 = Release|Any CPU - {CDB06F3E-A3D8-4DF8-AB32-71BF9B2EEC29}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {CDB06F3E-A3D8-4DF8-AB32-71BF9B2EEC29}.Debug|Any CPU.Build.0 = Debug|Any CPU - {CDB06F3E-A3D8-4DF8-AB32-71BF9B2EEC29}.Release|Any CPU.ActiveCfg = Release|Any CPU - {CDB06F3E-A3D8-4DF8-AB32-71BF9B2EEC29}.Release|Any CPU.Build.0 = Release|Any CPU {62C86664-49F4-4C59-A2EC-1D70D85149D9}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {62C86664-49F4-4C59-A2EC-1D70D85149D9}.Debug|Any CPU.Build.0 = Debug|Any CPU {62C86664-49F4-4C59-A2EC-1D70D85149D9}.Release|Any CPU.ActiveCfg = Release|Any CPU @@ -199,6 +193,38 @@ Global {59588FC9-0A4F-4903-84B0-A2CB2EE6F9AC}.Debug|Any CPU.Build.0 = Debug|Any CPU {59588FC9-0A4F-4903-84B0-A2CB2EE6F9AC}.Release|Any CPU.ActiveCfg = Release|Any CPU {59588FC9-0A4F-4903-84B0-A2CB2EE6F9AC}.Release|Any CPU.Build.0 = Release|Any CPU + {524AE4A6-A15F-466C-AED3-CDF3209E4394}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {524AE4A6-A15F-466C-AED3-CDF3209E4394}.Debug|Any CPU.Build.0 = Debug|Any CPU + {524AE4A6-A15F-466C-AED3-CDF3209E4394}.Release|Any CPU.ActiveCfg = Release|Any CPU + {524AE4A6-A15F-466C-AED3-CDF3209E4394}.Release|Any CPU.Build.0 = Release|Any CPU + {AF8B71E6-454F-4B66-98EE-2128B415EC45}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {AF8B71E6-454F-4B66-98EE-2128B415EC45}.Debug|Any CPU.Build.0 = Debug|Any CPU + {AF8B71E6-454F-4B66-98EE-2128B415EC45}.Release|Any CPU.ActiveCfg = Release|Any CPU + {AF8B71E6-454F-4B66-98EE-2128B415EC45}.Release|Any CPU.Build.0 = Release|Any CPU + {CCD901FE-20C8-4DC0-AD9C-8B8333EE277B}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {CCD901FE-20C8-4DC0-AD9C-8B8333EE277B}.Debug|Any CPU.Build.0 = Debug|Any CPU + {CCD901FE-20C8-4DC0-AD9C-8B8333EE277B}.Release|Any CPU.ActiveCfg = Release|Any CPU + {CCD901FE-20C8-4DC0-AD9C-8B8333EE277B}.Release|Any CPU.Build.0 = Release|Any CPU + {63B6747C-470D-4561-A3A2-3C024747BE3B}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {63B6747C-470D-4561-A3A2-3C024747BE3B}.Debug|Any CPU.Build.0 = Debug|Any CPU + {63B6747C-470D-4561-A3A2-3C024747BE3B}.Release|Any CPU.ActiveCfg = Release|Any CPU + {63B6747C-470D-4561-A3A2-3C024747BE3B}.Release|Any CPU.Build.0 = Release|Any CPU + {AD0BF60E-B46C-4181-BF43-D68801E5D259}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {AD0BF60E-B46C-4181-BF43-D68801E5D259}.Debug|Any CPU.Build.0 = Debug|Any CPU + {AD0BF60E-B46C-4181-BF43-D68801E5D259}.Release|Any CPU.ActiveCfg = Release|Any CPU + {AD0BF60E-B46C-4181-BF43-D68801E5D259}.Release|Any CPU.Build.0 = Release|Any CPU + {2343A153-5832-41B9-A1FF-D80D3107A1DA}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {2343A153-5832-41B9-A1FF-D80D3107A1DA}.Debug|Any CPU.Build.0 = Debug|Any CPU + {2343A153-5832-41B9-A1FF-D80D3107A1DA}.Release|Any CPU.ActiveCfg = Release|Any CPU + {2343A153-5832-41B9-A1FF-D80D3107A1DA}.Release|Any CPU.Build.0 = Release|Any CPU + {17A82259-1B8A-4199-A564-AC250F51360E}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {17A82259-1B8A-4199-A564-AC250F51360E}.Debug|Any CPU.Build.0 = Debug|Any CPU + {17A82259-1B8A-4199-A564-AC250F51360E}.Release|Any CPU.ActiveCfg = Release|Any CPU + {17A82259-1B8A-4199-A564-AC250F51360E}.Release|Any CPU.Build.0 = Release|Any CPU + {CE9050E2-48AE-4CC4-8CCB-E19EE3102C0A}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {CE9050E2-48AE-4CC4-8CCB-E19EE3102C0A}.Debug|Any CPU.Build.0 = Debug|Any CPU + {CE9050E2-48AE-4CC4-8CCB-E19EE3102C0A}.Release|Any CPU.ActiveCfg = Release|Any CPU + {CE9050E2-48AE-4CC4-8CCB-E19EE3102C0A}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -226,6 +252,14 @@ Global {70E581C3-38BB-46CC-9063-ADF9F2B76570} = {85AFD7F1-6943-40FE-B8EC-AA9DBB42CCA6} {849357D0-85B3-4326-9D42-AD5A09E9613C} = {B3714D90-F595-4644-8018-ADE19D66B853} {59588FC9-0A4F-4903-84B0-A2CB2EE6F9AC} = {B3714D90-F595-4644-8018-ADE19D66B853} + {524AE4A6-A15F-466C-AED3-CDF3209E4394} = {1AD64707-0BE0-48B0-A803-916FF96DCB4F} + {AF8B71E6-454F-4B66-98EE-2128B415EC45} = {1AD64707-0BE0-48B0-A803-916FF96DCB4F} + {CCD901FE-20C8-4DC0-AD9C-8B8333EE277B} = {1AD64707-0BE0-48B0-A803-916FF96DCB4F} + {63B6747C-470D-4561-A3A2-3C024747BE3B} = {1AD64707-0BE0-48B0-A803-916FF96DCB4F} + {AD0BF60E-B46C-4181-BF43-D68801E5D259} = {1AD64707-0BE0-48B0-A803-916FF96DCB4F} + {2343A153-5832-41B9-A1FF-D80D3107A1DA} = {1AD64707-0BE0-48B0-A803-916FF96DCB4F} + {17A82259-1B8A-4199-A564-AC250F51360E} = {1AD64707-0BE0-48B0-A803-916FF96DCB4F} + {CE9050E2-48AE-4CC4-8CCB-E19EE3102C0A} = {1AD64707-0BE0-48B0-A803-916FF96DCB4F} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {20C98D35-54EF-46A6-8F3B-1855C1AE4F70}