diff --git a/pages/configuration.md b/pages/configuration.md
index 2e8db3dc9..15bed9124 100644
--- a/pages/configuration.md
+++ b/pages/configuration.md
@@ -1,9 +1,8 @@
-
# Configuration
Configuration requires an instance of `Microsoft.EntityFrameworkCore.Metadata.IModel`. It can be extracted from a DbContext instance via the `DbContext.Model` property. Unfortunately EntityFramework conflates configuration with runtime in its API. So `DbContext` is the main API used at runtime, but it also contains the configuration API via the `OnModelCreating` method. As such a DbContext needs to be instantiated and disposed for the purposes of IModel construction. One possible approach is via a static field on the DbContext.
@@ -209,7 +208,7 @@ public class GraphQlController :
JObject variables,
CancellationToken cancellation)
{
- var executionOptions = new ExecutionOptions
+ var options = new ExecutionOptions
{
Schema = schema,
Query = query,
@@ -223,8 +222,7 @@ public class GraphQlController :
#endif
};
- var result = await executer.ExecuteAsync(executionOptions)
- ;
+ var result = await executer.ExecuteAsync(options);
if (result.Errors?.Count > 0)
{
@@ -252,7 +250,7 @@ public class GraphQlController :
}
}
```
-[snippet source](/src/SampleWeb/GraphQlController.cs#L11-L103)
+[snippet source](/src/SampleWeb/GraphQlController.cs#L11-L102)
Note that the instance of the DataContext is passed to the [GraphQL .net User Context](https://graphql-dotnet.github.io/docs/getting-started/user-context).
@@ -275,7 +273,7 @@ public class Query :
return dataContext.Companies;
});
```
-[snippet source](/src/SampleWeb/Query.cs#L5-L21)
+[snippet source](/src/SampleWeb/Query.cs#L6-L22)
@@ -393,6 +391,33 @@ query {
Assert.NotEqual(after.ToString(), page);
}
+ [Fact]
+ public async Task Get_employee_summary()
+ {
+ var query = @"
+query {
+ employeeSummary {
+ companyId
+ averageAge
+ }
+}";
+ var response = await ClientQueryExecutor.ExecuteGet(client, query);
+ response.EnsureSuccessStatusCode();
+ var result = JObject.Parse(await response.Content.ReadAsStringAsync());
+
+ var expected = JObject.FromObject(new
+ {
+ data = new
+ {
+ employeeSummary = new[] {
+ new { companyId = 1, averageAge = 28.0 },
+ new { companyId = 4, averageAge = 34.0 }
+ }
+ }
+ });
+ Assert.Equal(expected.ToString(), result.ToString());
+ }
+
[Fact]
public async Task Post()
{
@@ -482,7 +507,7 @@ subscription {
}
}
```
-[snippet source](/src/SampleWeb.Tests/GraphQlControllerTests.cs#L12-L211)
+[snippet source](/src/SampleWeb.Tests/GraphQlControllerTests.cs#L12-L238)
@@ -501,8 +526,7 @@ public static async Task ExecuteWithErrorCheck(this IDocumentEx
{
Guard.AgainstNull(nameof(documentExecuter), documentExecuter);
Guard.AgainstNull(nameof(executionOptions), executionOptions);
- var executionResult = await documentExecuter.ExecuteAsync(executionOptions)
- ;
+ var executionResult = await documentExecuter.ExecuteAsync(executionOptions);
var errors = executionResult.Errors;
if (errors != null && errors.Count > 0)
@@ -518,5 +542,5 @@ public static async Task ExecuteWithErrorCheck(this IDocumentEx
return executionResult;
}
```
-[snippet source](/src/GraphQL.EntityFramework/GraphQlExtensions.cs#L9-L32)
+[snippet source](/src/GraphQL.EntityFramework/GraphQlExtensions.cs#L9-L31)
diff --git a/pages/defining-graphs.md b/pages/defining-graphs.md
index 408a00d94..b3073e16c 100644
--- a/pages/defining-graphs.md
+++ b/pages/defining-graphs.md
@@ -1,12 +1,10 @@
-
# Defining Graphs
-
## Includes and Navigation properties.
Entity Framework has the concept of [Navigation Properties](https://docs.microsoft.com/en-us/ef/core/modeling/relationships):
@@ -17,9 +15,9 @@ In the context of GraphQL, Root Graph is the entry point to performing the initi
When performing a query there are several approaches to [Loading Related Data](https://docs.microsoft.com/en-us/ef/core/querying/related-data)
- * **Eager loading** means that the related data is loaded from the database as part of the initial query.
- * **Explicit loading** means that the related data is explicitly loaded from the database at a later time.
- * **Lazy loading** means that the related data is transparently loaded from the database when the navigation property is accessed.
+- **Eager loading** means that the related data is loaded from the database as part of the initial query.
+- **Explicit loading** means that the related data is explicitly loaded from the database at a later time.
+- **Lazy loading** means that the related data is transparently loaded from the database when the navigation property is accessed.
Ideally, all navigation properties would be eagerly loaded as part of the root query. However determining what navigation properties to eagerly is difficult in the context of GraphQL. The reason is, given the returned hierarchy of data is dynamically defined by the requesting client, the root query cannot know what properties to include. To work around this GraphQL.EntityFramework interrogates the incoming query to derive the includes. So for example take the following query
@@ -45,14 +43,12 @@ context.Heros
.Include("Friends.Address");
```
-The string for the include is taken from the field name when using `AddNavigationField` or `AddNavigationConnectionField` with the first character upper cased. This value can be overridden using the optional parameter `includeNames` . Note that `includeNames` is an `IEnumerable` so that multiple navigation properties can optionally be included for a single node.
-
+The string for the include is taken from the field name when using `AddNavigationField` or `AddNavigationConnectionField` with the first character upper cased. This value can be overridden using the optional parameter `includeNames` . Note that `includeNames` is an `IEnumerable` so that multiple navigation properties can optionally be included for a single node.
## Fields
Queries in GraphQL.net are defined using the [Fields API](https://graphql-dotnet.github.io/docs/getting-started/introduction#queries). Fields can be mapped to Entity Framework by using `IEfGraphQLService`. `IEfGraphQLService` can be used in either a root query or a nested query via dependency injection. Alternatively the base type `EfObjectGraphType` or `EfObjectGraphType` can be used for root or nested graphs respectively. The below samples all use the base type approach as it results in slightly less code.
-
### Root Query
@@ -87,7 +83,6 @@ public class Query :
`AddSingleField` will result in a single matching being found and returned. This approach uses [`IQueryable.SingleOrDefaultAsync`](https://docs.microsoft.com/en-us/dotnet/api/microsoft.entityframeworkcore.entityframeworkqueryableextensions.singleordefaultasync) as such, if no records are found a null will be returned, and if multiple records match then an exception will be thrown.
-
### Typed Graph
@@ -113,13 +108,10 @@ public class CompanyGraph :
[snippet source](/src/Snippets/TypedGraph.cs#L7-L27)
-
## Connections
-
### Root Query
-
#### Graph Type
@@ -143,7 +135,6 @@ public class Query :
[snippet source](/src/Snippets/ConnectionRootQuery.cs#L6-L24)
-
#### Request
```graphql
@@ -171,7 +162,6 @@ public class Query :
}
```
-
#### Response
```js
@@ -217,7 +207,6 @@ public class Query :
}
```
-
### Typed Graph
@@ -237,8 +226,8 @@ public class CompanyGraph :
[snippet source](/src/Snippets/ConnectionTypedGraph.cs#L7-L21)
-
## Enums
+
```csharp
public class DayOfTheWeekGraph : EnumerationGraphType
{
@@ -248,7 +237,7 @@ public class DayOfTheWeekGraph : EnumerationGraphType
```cs
public class ExampleGraph : ObjectGraphType
{
- public ExampleGraph()
+ public ExampleGraph()
{
Field(x => x.DayOfTheWeek, type: typeof(DayOfTheWeekGraph));
}
@@ -256,3 +245,42 @@ public class ExampleGraph : ObjectGraphType
```
- [GraphQL .NET - Schema Types / Enumerations](https://graphql-dotnet.github.io/docs/getting-started/schema-types/#enumerations)
+
+## Manually Apply `WhereExpression`
+
+In some cases, you may want to use `Field` instead of `AddQueryField`/`AddSingleField`/etc but still would like to use apply the `where` argument. This can be useful when the returned `Graph` type is not for an entity (for example, aggregate results). To support this, you must:
+
+- Add the `WhereExpressionGraph` argument
+- Apply the `where` argument expression using `ExpressionBuilder.BuildPredicate(whereExpression)`
+
+```cs
+Field>(
+ name: "employeeSummary",
+ arguments: new QueryArguments(
+ new QueryArgument> { Name = "where" }
+ ),
+ resolve: context =>
+ {
+ var dataContext = (MyDataContext) context.UserContext;
+ IQueryable query = dataContext.Employees;
+
+ if (context.HasArgument("where"))
+ {
+ var whereExpressions = context.GetArgument>("where");
+ foreach (var whereExpression in whereExpressions)
+ {
+ var predicate = ExpressionBuilder.BuildPredicate(whereExpression);
+ query = query.Where(predicate);
+ }
+ }
+
+ var results = from q in query
+ group q by new { q.CompanyId } into g
+ select new EmployeeSummary {
+ CompanyId = g.Key.CompanyId,
+ AverageAge = g.Average(x => x.Age),
+ };
+
+ return results;
+ });
+```
diff --git a/pages/defining-graphs.source.md b/pages/defining-graphs.source.md
index 21e912007..1a2d72de7 100644
--- a/pages/defining-graphs.source.md
+++ b/pages/defining-graphs.source.md
@@ -1,6 +1,5 @@
# Defining Graphs
-
## Includes and Navigation properties.
Entity Framework has the concept of [Navigation Properties](https://docs.microsoft.com/en-us/ef/core/modeling/relationships):
@@ -11,9 +10,9 @@ In the context of GraphQL, Root Graph is the entry point to performing the initi
When performing a query there are several approaches to [Loading Related Data](https://docs.microsoft.com/en-us/ef/core/querying/related-data)
- * **Eager loading** means that the related data is loaded from the database as part of the initial query.
- * **Explicit loading** means that the related data is explicitly loaded from the database at a later time.
- * **Lazy loading** means that the related data is transparently loaded from the database when the navigation property is accessed.
+- **Eager loading** means that the related data is loaded from the database as part of the initial query.
+- **Explicit loading** means that the related data is explicitly loaded from the database at a later time.
+- **Lazy loading** means that the related data is transparently loaded from the database when the navigation property is accessed.
Ideally, all navigation properties would be eagerly loaded as part of the root query. However determining what navigation properties to eagerly is difficult in the context of GraphQL. The reason is, given the returned hierarchy of data is dynamically defined by the requesting client, the root query cannot know what properties to include. To work around this GraphQL.EntityFramework interrogates the incoming query to derive the includes. So for example take the following query
@@ -39,14 +38,12 @@ context.Heros
.Include("Friends.Address");
```
-The string for the include is taken from the field name when using `AddNavigationField` or `AddNavigationConnectionField` with the first character upper cased. This value can be overridden using the optional parameter `includeNames` . Note that `includeNames` is an `IEnumerable` so that multiple navigation properties can optionally be included for a single node.
-
+The string for the include is taken from the field name when using `AddNavigationField` or `AddNavigationConnectionField` with the first character upper cased. This value can be overridden using the optional parameter `includeNames` . Note that `includeNames` is an `IEnumerable` so that multiple navigation properties can optionally be included for a single node.
## Fields
Queries in GraphQL.net are defined using the [Fields API](https://graphql-dotnet.github.io/docs/getting-started/introduction#queries). Fields can be mapped to Entity Framework by using `IEfGraphQLService`. `IEfGraphQLService` can be used in either a root query or a nested query via dependency injection. Alternatively the base type `EfObjectGraphType` or `EfObjectGraphType` can be used for root or nested graphs respectively. The below samples all use the base type approach as it results in slightly less code.
-
### Root Query
snippet: rootQuery
@@ -55,23 +52,18 @@ snippet: rootQuery
`AddSingleField` will result in a single matching being found and returned. This approach uses [`IQueryable.SingleOrDefaultAsync`](https://docs.microsoft.com/en-us/dotnet/api/microsoft.entityframeworkcore.entityframeworkqueryableextensions.singleordefaultasync) as such, if no records are found a null will be returned, and if multiple records match then an exception will be thrown.
-
### Typed Graph
snippet: typedGraph
-
## Connections
-
### Root Query
-
#### Graph Type
snippet: ConnectionRootQuery
-
#### Request
```graphql
@@ -99,7 +91,6 @@ snippet: ConnectionRootQuery
}
```
-
#### Response
```js
@@ -145,13 +136,12 @@ snippet: ConnectionRootQuery
}
```
-
### Typed Graph
snippet: ConnectionTypedGraph
-
## Enums
+
```csharp
public class DayOfTheWeekGraph : EnumerationGraphType
{
@@ -161,7 +151,7 @@ public class DayOfTheWeekGraph : EnumerationGraphType
```cs
public class ExampleGraph : ObjectGraphType
{
- public ExampleGraph()
+ public ExampleGraph()
{
Field(x => x.DayOfTheWeek, type: typeof(DayOfTheWeekGraph));
}
@@ -169,3 +159,42 @@ public class ExampleGraph : ObjectGraphType
```
- [GraphQL .NET - Schema Types / Enumerations](https://graphql-dotnet.github.io/docs/getting-started/schema-types/#enumerations)
+
+## Manually Apply `WhereExpression`
+
+In some cases, you may want to use `Field` instead of `AddQueryField`/`AddSingleField`/etc but still would like to use apply the `where` argument. This can be useful when the returned `Graph` type is not for an entity (for example, aggregate results). To support this, you must:
+
+- Add the `WhereExpressionGraph` argument
+- Apply the `where` argument expression using `ExpressionBuilder.BuildPredicate(whereExpression)`
+
+```cs
+Field>(
+ name: "employeeSummary",
+ arguments: new QueryArguments(
+ new QueryArgument> { Name = "where" }
+ ),
+ resolve: context =>
+ {
+ var dataContext = (MyDataContext) context.UserContext;
+ IQueryable query = dataContext.Employees;
+
+ if (context.HasArgument("where"))
+ {
+ var whereExpressions = context.GetArgument>("where");
+ foreach (var whereExpression in whereExpressions)
+ {
+ var predicate = ExpressionBuilder.BuildPredicate(whereExpression);
+ query = query.Where(predicate);
+ }
+ }
+
+ var results = from q in query
+ group q by new { q.CompanyId } into g
+ select new EmployeeSummary {
+ CompanyId = g.Key.CompanyId,
+ AverageAge = g.Average(x => x.Age),
+ };
+
+ return results;
+ });
+```
diff --git a/pages/filters.md b/pages/filters.md
index 3582d0c6a..db27068db 100644
--- a/pages/filters.md
+++ b/pages/filters.md
@@ -1,9 +1,8 @@
-
# Filters
Sometimes, in the context of constructing an EF query, it is not possible to know if any given item should be returned in the results. For example when performing authorization where the rules rules are pulled from a different system, and that information does not exist in the database.
diff --git a/pages/query-usage.md b/pages/query-usage.md
index 19986809b..69df8b559 100644
--- a/pages/query-usage.md
+++ b/pages/query-usage.md
@@ -1,10 +1,9 @@
-
# Query Usage
diff --git a/src/GraphQL.EntityFramework/Where/ExpressionBuilder.cs b/src/GraphQL.EntityFramework/Where/ExpressionBuilder.cs
index 90968b0f8..a239b0ad2 100644
--- a/src/GraphQL.EntityFramework/Where/ExpressionBuilder.cs
+++ b/src/GraphQL.EntityFramework/Where/ExpressionBuilder.cs
@@ -3,7 +3,7 @@
using System.Linq.Expressions;
using GraphQL.EntityFramework;
-static class ExpressionBuilder
+public static class ExpressionBuilder
{
public static Expression> BuildPredicate(WhereExpression where)
{
diff --git a/src/GraphQL.EntityFramework/Where/Graphs/WhereExpression.cs b/src/GraphQL.EntityFramework/Where/Graphs/WhereExpression.cs
index afaace5b4..30e792f73 100644
--- a/src/GraphQL.EntityFramework/Where/Graphs/WhereExpression.cs
+++ b/src/GraphQL.EntityFramework/Where/Graphs/WhereExpression.cs
@@ -1,7 +1,7 @@
using System;
using GraphQL.EntityFramework;
-class WhereExpression
+public class WhereExpression
{
public string Path { get; set; }
public Comparison? Comparison { get; set; }
diff --git a/src/GraphQL.EntityFramework/Where/Graphs/WhereExpressionGraph.cs b/src/GraphQL.EntityFramework/Where/Graphs/WhereExpressionGraph.cs
index 8fc495542..49a125217 100644
--- a/src/GraphQL.EntityFramework/Where/Graphs/WhereExpressionGraph.cs
+++ b/src/GraphQL.EntityFramework/Where/Graphs/WhereExpressionGraph.cs
@@ -1,6 +1,6 @@
using GraphQL.Types;
-class WhereExpressionGraph :
+public class WhereExpressionGraph :
InputObjectGraphType
{
public WhereExpressionGraph()
diff --git a/src/SampleWeb.Tests/GraphQlControllerTests.cs b/src/SampleWeb.Tests/GraphQlControllerTests.cs
index 8c7550ebd..5933b26e1 100644
--- a/src/SampleWeb.Tests/GraphQlControllerTests.cs
+++ b/src/SampleWeb.Tests/GraphQlControllerTests.cs
@@ -119,6 +119,33 @@ public async Task Get_companies_paging()
Assert.NotEqual(after.ToString(), page);
}
+ [Fact]
+ public async Task Get_employee_summary()
+ {
+ var query = @"
+query {
+ employeeSummary {
+ companyId
+ averageAge
+ }
+}";
+ var response = await ClientQueryExecutor.ExecuteGet(client, query);
+ response.EnsureSuccessStatusCode();
+ var result = JObject.Parse(await response.Content.ReadAsStringAsync());
+
+ var expected = JObject.FromObject(new
+ {
+ data = new
+ {
+ employeeSummary = new[] {
+ new { companyId = 1, averageAge = 28.0 },
+ new { companyId = 4, averageAge = 34.0 }
+ }
+ }
+ });
+ Assert.Equal(expected.ToString(), result.ToString());
+ }
+
[Fact]
public async Task Post()
{
diff --git a/src/SampleWeb/DataContext/Employee.cs b/src/SampleWeb/DataContext/Employee.cs
index 2e0310676..23b76aa5d 100644
--- a/src/SampleWeb/DataContext/Employee.cs
+++ b/src/SampleWeb/DataContext/Employee.cs
@@ -4,4 +4,5 @@
public int CompanyId { get; set; }
public Company Company { get; set; }
public string Content { get; set; }
+ public int Age { get; set; }
}
\ No newline at end of file
diff --git a/src/SampleWeb/DataContextBuilder.cs b/src/SampleWeb/DataContextBuilder.cs
index c771368ce..a7b6adcfd 100644
--- a/src/SampleWeb/DataContextBuilder.cs
+++ b/src/SampleWeb/DataContextBuilder.cs
@@ -26,13 +26,15 @@ public static MyDataContext BuildDataContext()
{
Id = 2,
CompanyId = company1.Id,
- Content = "Employee1"
+ Content = "Employee1",
+ Age = 25
};
var employee2 = new Employee
{
Id = 3,
CompanyId = company1.Id,
- Content = "Employee2"
+ Content = "Employee2",
+ Age = 31
};
var company2 = new Company
{
@@ -43,7 +45,8 @@ public static MyDataContext BuildDataContext()
{
Id = 5,
CompanyId = company2.Id,
- Content = "Employee4"
+ Content = "Employee4",
+ Age = 34
};
var company3 = new Company
{
diff --git a/src/SampleWeb/Graphs/EmployeeGraph.cs b/src/SampleWeb/Graphs/EmployeeGraph.cs
index 5e58e26ff..471413e4c 100644
--- a/src/SampleWeb/Graphs/EmployeeGraph.cs
+++ b/src/SampleWeb/Graphs/EmployeeGraph.cs
@@ -8,6 +8,7 @@ public EmployeeGraph(IEfGraphQLService graphQlService) :
{
Field(x => x.Id);
Field(x => x.Content);
+ Field(x => x.Age);
AddNavigationField(
name: "company",
resolve: context => context.Source.Company);
diff --git a/src/SampleWeb/Graphs/EmployeeSummaryGraph.cs b/src/SampleWeb/Graphs/EmployeeSummaryGraph.cs
new file mode 100644
index 000000000..f750befa2
--- /dev/null
+++ b/src/SampleWeb/Graphs/EmployeeSummaryGraph.cs
@@ -0,0 +1,18 @@
+using GraphQL.EntityFramework;
+
+public class EmployeeSummaryGraph :
+ EfObjectGraphType
+{
+ public EmployeeSummaryGraph(IEfGraphQLService graphQlService) :
+ base(graphQlService)
+ {
+ Field(x => x.CompanyId);
+ Field(x => x.AverageAge);
+ }
+}
+
+public class EmployeeSummary
+{
+ public int CompanyId { get; set; }
+ public double AverageAge { get; set; }
+}
\ No newline at end of file
diff --git a/src/SampleWeb/Query.cs b/src/SampleWeb/Query.cs
index 90047d857..7c2aac1c7 100644
--- a/src/SampleWeb/Query.cs
+++ b/src/SampleWeb/Query.cs
@@ -1,4 +1,5 @@
-using System.Linq;
+using System.Collections.Generic;
+using System.Linq;
using GraphQL.EntityFramework;
using GraphQL.Types;
@@ -60,5 +61,35 @@ public Query(IEfGraphQLService efGraphQlService) :
var dataContext = (MyDataContext) context.UserContext;
return dataContext.Employees;
});
+
+ Field>(
+ name: "employeeSummary",
+ arguments: new QueryArguments(
+ new QueryArgument> { Name = "where" }
+ ),
+ resolve: context =>
+ {
+ var dataContext = (MyDataContext) context.UserContext;
+ IQueryable query = dataContext.Employees;
+
+ if (context.HasArgument("where"))
+ {
+ var whereExpressions = context.GetArgument>("where");
+ foreach (var whereExpression in whereExpressions)
+ {
+ var predicate = ExpressionBuilder.BuildPredicate(whereExpression);
+ query = query.Where(predicate);
+ }
+ }
+
+ var results = from q in query
+ group q by new { q.CompanyId } into g
+ select new EmployeeSummary {
+ CompanyId = g.Key.CompanyId,
+ AverageAge = g.Average(x => x.Age),
+ };
+
+ return results;
+ });
}
}
\ No newline at end of file