Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
46 changes: 35 additions & 11 deletions pages/configuration.md
Original file line number Diff line number Diff line change
@@ -1,9 +1,8 @@
<!--
This file was generate by MarkdownSnippets.
Source File: \pages\configuration.source.md
Source File: /pages/configuration.source.md
To change this file edit the source file and then re-run the generation using either the dotnet global tool (https://github.com/SimonCropp/MarkdownSnippets#githubmarkdownsnippets) or using the api (https://github.com/SimonCropp/MarkdownSnippets#running-as-a-unit-test).
-->

# 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.
Expand Down Expand Up @@ -209,7 +208,7 @@ public class GraphQlController :
JObject variables,
CancellationToken cancellation)
{
var executionOptions = new ExecutionOptions
var options = new ExecutionOptions
{
Schema = schema,
Query = query,
Expand All @@ -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)
{
Expand Down Expand Up @@ -252,7 +250,7 @@ public class GraphQlController :
}
}
```
<sup>[snippet source](/src/SampleWeb/GraphQlController.cs#L11-L103)</sup>
<sup>[snippet source](/src/SampleWeb/GraphQlController.cs#L11-L102)</sup>
<!-- endsnippet -->

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).
Expand All @@ -275,7 +273,7 @@ public class Query :
return dataContext.Companies;
});
```
<sup>[snippet source](/src/SampleWeb/Query.cs#L5-L21)</sup>
<sup>[snippet source](/src/SampleWeb/Query.cs#L6-L22)</sup>
<!-- endsnippet -->


Expand Down Expand Up @@ -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()
{
Expand Down Expand Up @@ -482,7 +507,7 @@ subscription {
}
}
```
<sup>[snippet source](/src/SampleWeb.Tests/GraphQlControllerTests.cs#L12-L211)</sup>
<sup>[snippet source](/src/SampleWeb.Tests/GraphQlControllerTests.cs#L12-L238)</sup>
<!-- endsnippet -->


Expand All @@ -501,8 +526,7 @@ public static async Task<ExecutionResult> 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)
Expand All @@ -518,5 +542,5 @@ public static async Task<ExecutionResult> ExecuteWithErrorCheck(this IDocumentEx
return executionResult;
}
```
<sup>[snippet source](/src/GraphQL.EntityFramework/GraphQlExtensions.cs#L9-L32)</sup>
<sup>[snippet source](/src/GraphQL.EntityFramework/GraphQlExtensions.cs#L9-L31)</sup>
<!-- endsnippet -->
64 changes: 46 additions & 18 deletions pages/defining-graphs.md
Original file line number Diff line number Diff line change
@@ -1,12 +1,10 @@
<!--
This file was generate by MarkdownSnippets.
Source File: \pages\defining-graphs.source.md
Source File: /pages/defining-graphs.source.md
To change this file edit the source file and then re-run the generation using either the dotnet global tool (https://github.com/SimonCropp/MarkdownSnippets#githubmarkdownsnippets) or using the api (https://github.com/SimonCropp/MarkdownSnippets#running-as-a-unit-test).
-->

# Defining Graphs


## Includes and Navigation properties.

Entity Framework has the concept of [Navigation Properties](https://docs.microsoft.com/en-us/ef/core/modeling/relationships):
Expand All @@ -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

Expand All @@ -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<string>` 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<string>` 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<TSource>` 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 -->
Expand Down Expand Up @@ -87,7 +83,6 @@ public class Query :

`AddSingleField` will result in a single matching being found and returned. This approach uses [`IQueryable<T>.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 -->
Expand All @@ -113,13 +108,10 @@ public class CompanyGraph :
<sup>[snippet source](/src/Snippets/TypedGraph.cs#L7-L27)</sup>
<!-- endsnippet -->


## Connections


### Root Query


#### Graph Type

<!-- snippet: ConnectionRootQuery -->
Expand All @@ -143,7 +135,6 @@ public class Query :
<sup>[snippet source](/src/Snippets/ConnectionRootQuery.cs#L6-L24)</sup>
<!-- endsnippet -->


#### Request

```graphql
Expand Down Expand Up @@ -171,7 +162,6 @@ public class Query :
}
```


#### Response

```js
Expand Down Expand Up @@ -217,7 +207,6 @@ public class Query :
}
```


### Typed Graph

<!-- snippet: ConnectionTypedGraph -->
Expand All @@ -237,8 +226,8 @@ public class CompanyGraph :
<sup>[snippet source](/src/Snippets/ConnectionTypedGraph.cs#L7-L21)</sup>
<!-- endsnippet -->


## Enums

```csharp
public class DayOfTheWeekGraph : EnumerationGraphType<DayOfTheWeek>
{
Expand All @@ -248,11 +237,50 @@ public class DayOfTheWeekGraph : EnumerationGraphType<DayOfTheWeek>
```cs
public class ExampleGraph : ObjectGraphType<Example>
{
public ExampleGraph()
public ExampleGraph()
{
Field(x => x.DayOfTheWeek, type: typeof(DayOfTheWeekGraph));
}
}
```

- [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<T>.BuildPredicate(whereExpression)`

```cs
Field<ListGraphType<EmployeeSummaryGraph>>(
name: "employeeSummary",
arguments: new QueryArguments(
new QueryArgument<ListGraphType<WhereExpressionGraph>> { Name = "where" }
),
resolve: context =>
{
var dataContext = (MyDataContext) context.UserContext;
IQueryable<Employee> query = dataContext.Employees;

if (context.HasArgument("where"))
{
var whereExpressions = context.GetArgument<List<WhereExpression>>("where");
foreach (var whereExpression in whereExpressions)
{
var predicate = ExpressionBuilder<Employee>.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;
});
```
61 changes: 45 additions & 16 deletions pages/defining-graphs.source.md
Original file line number Diff line number Diff line change
@@ -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):
Expand All @@ -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

Expand All @@ -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<string>` 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<string>` 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<TSource>` 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
Expand All @@ -55,23 +52,18 @@ snippet: rootQuery

`AddSingleField` will result in a single matching being found and returned. This approach uses [`IQueryable<T>.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
Expand Down Expand Up @@ -99,7 +91,6 @@ snippet: ConnectionRootQuery
}
```


#### Response

```js
Expand Down Expand Up @@ -145,13 +136,12 @@ snippet: ConnectionRootQuery
}
```


### Typed Graph

snippet: ConnectionTypedGraph


## Enums

```csharp
public class DayOfTheWeekGraph : EnumerationGraphType<DayOfTheWeek>
{
Expand All @@ -161,11 +151,50 @@ public class DayOfTheWeekGraph : EnumerationGraphType<DayOfTheWeek>
```cs
public class ExampleGraph : ObjectGraphType<Example>
{
public ExampleGraph()
public ExampleGraph()
{
Field(x => x.DayOfTheWeek, type: typeof(DayOfTheWeekGraph));
}
}
```

- [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<T>.BuildPredicate(whereExpression)`

```cs
Field<ListGraphType<EmployeeSummaryGraph>>(
name: "employeeSummary",
arguments: new QueryArguments(
new QueryArgument<ListGraphType<WhereExpressionGraph>> { Name = "where" }
),
resolve: context =>
{
var dataContext = (MyDataContext) context.UserContext;
IQueryable<Employee> query = dataContext.Employees;

if (context.HasArgument("where"))
{
var whereExpressions = context.GetArgument<List<WhereExpression>>("where");
foreach (var whereExpression in whereExpressions)
{
var predicate = ExpressionBuilder<Employee>.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;
});
```
Loading