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
152 changes: 79 additions & 73 deletions docs/advanced/directives.md

Large diffs are not rendered by default.

161 changes: 137 additions & 24 deletions docs/advanced/subscriptions.md

Large diffs are not rendered by default.

6 changes: 3 additions & 3 deletions docs/advanced/type-expressions.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ These assumptions are made:

- Fields that return reference types **can be** null
- Fields that return value types **cannot be** null
- Fields that return Nullable value types (e.g. `int?`) **can be** be null
- Fields that return Nullable value types (e.g. `int?`) **can be** be null.
- When a field returns an object that implements `IEnumerable<TType>` it will be presented to GraphQL as a "list of `TType`".

Basically, if your method is able to return a value...then its valid as far as GraphQL is concerned.
Expand Down Expand Up @@ -49,7 +49,7 @@ query {
</div>
<br/>

This action method could return a `Donut` or return `null`. But should the `donut` field allow a null value? The code certainly does and the rules above say fields that return a reference type can be null...but that's not what's important. Its ultimately your decision to decide if a null donut is allowed, not the C# compiler and not the assumptions made by the library.
This action method could return a `Donut` or returns `null`. But should the donut field, from a GraphQL perspective, allow a null return value? The code certainly does and the rules above say fields that return a reference type can be null...but that's not what's important. Its ultimately your decision to decide if a "null donut" is allowed, not the C# compiler and not the assumptions made by the library.

On one hand, if a null value is returned, regardless of it being valid, the _outcome_ of the field is the same. When we return a null no child fields are processed. On the other hand, if null is not allowed we need to tell someone, let them know its nulled out not because it simply _is_ null but because a schema violation occurred.

Expand All @@ -59,7 +59,7 @@ Most of the time, using the `TypeExpression` property of a field declaration att

```csharp

// Declare that an MUST be returned (null is invalid)
// Declare that a donut MUST be returned (null is invalid)
// ----
// Schema Syntax: Donut!
[Query("donut", TypeExpression = TypeExpressions.IsNotNull)]
Expand Down
Binary file not shown.
Binary file not shown.
Binary file added docs/assets/2022-10-subscription-server.pdf
Binary file not shown.
15 changes: 7 additions & 8 deletions docs/controllers/actions.md
Original file line number Diff line number Diff line change
Expand Up @@ -562,12 +562,12 @@ public class BakeryController : GraphController

```javascript
query {
searchDonuts(searchParams:
name: "jelly*"
filled: true
dayOld: false){
id
name
searchDonuts(
name: "jelly*"
filled: true
dayOld: false){
id
name
}
}
```
Expand Down Expand Up @@ -609,8 +609,7 @@ public class DonutSearchParams
public class BakeryController : GraphController
{
[QueryRoot]
public IEnumerable<Donut>
SearchDonuts(DonutSearchParams searchParams)
public IEnumerable<Donut> SearchDonuts(DonutSearchParams searchParams)
{/* ... */}
}

Expand Down
27 changes: 20 additions & 7 deletions docs/controllers/authorization.md
Original file line number Diff line number Diff line change
Expand Up @@ -80,7 +80,7 @@ public class BakeryController : GraphController

## Use of IAuthorizationService

Under the hood, GraphQL taps into your `IServiceProvider` to obtain a reference to the `IAuthorizationService` that gets created when you configure `.AddAuthorization()` for policy enforcement rules. Take a look at the [Field Authorization](https://github.com/graphql-aspnet/graphql-aspnet/blob/master/src/graphql-aspnet/Middleware/FieldSecurity/Components/FieldAuthorizationMiddleware.cs) Middleware Component for the full picture.
Under the hood, GraphQL taps into your `IServiceProvider` to obtain a reference to the `IAuthorizationService` that gets created when you configure `.AddAuthorization()` for policy enforcement rules. Take a look at the [Schema Item Authorization Pipeline](https://github.com/graphql-aspnet/graphql-aspnet/tree/master/src/graphql-aspnet/Middleware/SchemaItemSecurity) for the full picture.

## When does Authorization Occur?

Expand All @@ -90,21 +90,32 @@ _The Default "per field" Authorization workflow_

---

In the diagram above we can see that user authorization in GraphQL ASP.NET makes use of the result from [ASP.NET's security pipeline](https://docs.microsoft.com/en-us/aspnet/core/security/authorization/introduction) but makes no attempt to interact with it. Whether you use Kerberos tokens, OAUTH2, username/password, API tokens or if you support 2-factor authentication or one-time-use passwords, GraphQL doesn't care. The entirety of your authentication and authorization pipeline is executed by GraphQL, no special arrangements or configuration is needed.
In the diagram above we can see that user authorization in GraphQL ASP.NET makes use of the result from [ASP.NET's security pipeline](https://docs.microsoft.com/en-us/aspnet/core/security/authorization/introduction). Whether you use Kerberos tokens, oauth2, username/password, API tokens or if you support 2-factor authentication or one-time-use passwords, GraphQL doesn't care. The entirety of your authentication and authorization scheme is executed by GraphQL, no special arrangements or configuration is needed.

> GraphQL ASP.NET draws from your configured authentication/authorization solution.

Field requests are passed through a [pipeline](../reference/how-it-works#middleware-pipelines) where field authorization is enforced as a middleware component before the data is queried (i.e. before the resolver is invoked). Should a requestor not be authorized for a given field a `null` value is resolved and a message added to the response.
Execution directives and field resolutions are passed through a [pipeline](../reference/how-it-works#middleware-pipelines) where authorization is enforced as a series of middleware components before the respective handlers are invoked. Should a requestor not be authorized for a given schema item an action is taken:

Null propagation rules still apply to unauthorized fields meaning if the field cannot accept a null value its propagated up the field chain potentially nulling out a parent or parent of a parent depending on your schema.

By default, a single unauthorized result does not necessarily kill an entire query, it depends on the structure of your object graph. When a field request is terminated any down-stream child fields are discarded immediately but sibling fields or unrelated ancestors continue to execute as normal.
## Field Authorizations

If a requestor is not authorized to a requested field a value of `null` is used as the resolved value and an error message is recorded to the query results.

Null propagation rules still apply to unauthorized fields meaning if the field cannot accept a null value, its propagated up the field chain potentially nulling out a parent or "parent of a parent" depending on your schema.

By default, a single unauthorized field result does not necessarily kill an entire query, it depends on the structure of your object graph and the query being executed. When a field request is terminated any down-stream child fields are discarded immediately but sibling fields or unrelated ancestors continue to execute as normal.

Since this authorization occurs "per field" and not "per controller action" its possible to define the same security chain for POCO properties. This allows you to effectively deny access, by policy, to a single property of an instantiated object. Performing security checks for every field of data (especially in parent/child scenarios) has a performance cost though, especially for larger data sets. For most scenarios enforcing security at the controller level is sufficient.

## Authorization Failures are Obfuscated
### Field Authorization Failures are Obfuscated

When GraphQL denies a requestor access to a field a message naming the field path is added to the response. This message is generic on purpose; `"Access denied to field '[query]/bakery/donuts'"`. To view more targeted reasons, such as specific policy failures, you'll need to expose exceptions on the request or turn on [logging](../logging/structured-logging). GraphQL automatically raises the `FieldSecurityChallengeCompleted` log event at a `Warning` level when a security check fails.

When GraphQL denies a requestor access to a field a message naming the field path is added to the response. This message is generic on purpose, `"Access denied to field '[query]/bakery/donuts'."`. To view more targeted reasons, such as policy failures, you'll need to expose exceptions on the request or turn on [logging](../logging/structured-logging). GraphQL automatically raises the `FieldSecurityChallengeCompleted` log event at a `Warning` level when a security check fails.
## Execution Directives Authorizations

Execution directives are applied to the _query document_ before a query plan is created and it is the query plan that determines what field resolvers should be called. As a result, execution directives have the potential to alter the document structure and change how a query might be resolved. Because of this, not executing a query directive has the potential to change (or not change) the expected query to be different than what the requestor asked for.

Therefore, if an execution directive fails authorization the query is rejected and not executed. The called will receive an error message as part of the response indicating the unauthorized directive. Like field authorization failures, the message is obfuscated and contains only a generic message. You'll need to expose exception on the request or turn on logging to see additional details.

## Authorization Methods

Expand All @@ -126,3 +137,5 @@ public void ConfigureServices(IServiceCollection services)
});
}
```

>Regardless of the authorization method chosen, **execution directives** are ALWAYS evaluated with a "per request" method. If a single execution directive fails, the whole query is dicarded.
10 changes: 5 additions & 5 deletions docs/controllers/type-extensions.md
Original file line number Diff line number Diff line change
Expand Up @@ -121,25 +121,25 @@ query {

When you declare a type extension it will only be invoked in context of the type being extended.

When we return a field of data from a property, an instance of the object must to exist in order to retrieve the property value. The same is true for a `type extension` except that instead of calling a property getter on the instance we're handing the entire instance to our method and letting it figure out what it needs to do with the data to resolve the field.
When we return a value from a property, an instance of an object must exist in order to supply that value. That is to say if you want the `Name` property of a bakery, you need a bakery instance to retrieve it from. The same is true for a `type extension` except that instead of calling a property getter on the instance, graphql hands the entire object to our method and lets us figure out what needs to happen to resolve the field.

GraphQL senses the type being extended and finds a method parameter to match. It captures that parameter, hides it from the object graph and supplies it with the result of the parent field, in this case the resolution of field `bakery(id: 5)`.
GraphQL inspects the type being extended and finds a parameter on the method to match it. It captures that parameter, hides it from the object graph, and fills it with the result of the parent field, in this case the resolution of field `bakery(id: 5)`.

This is immensely scalable:

- There are no wasted cycles fetching `CakeOrders` unless the requestor specifically asks for them.
- We have full access to [type expression validation](../advanced/type-expressions) and [model validation](./model-state) for our other method parameters.
- Since its a controller action we have full access to graph action results and can return `this.Ok()`, `this.Error()` etc. to give a rich developer experience.
- [Field Authorization](./authorization) is also wired up for us.
- [Field Security](./authorization) is also wired up for us.
- The bakery model is greatly simplified.

#### Can every field be a type extension?

Theoretically, yes. But take a moment and think about performance. For basic objects with few dozen properties which is faster:

- One database query to retrieve 24 columns of a single record then only use six in a graph result.
- Six separate database queries, one for each 10 character string value requested.
- Six separate database queries, one for each string value requested.

Type extensions shine in parent-child relationships when preloading data is a concern but be careful not to go isolating every graph field just to avoid retrieving data unless absolutely necessary. Retrieving a few extra bytes of string data is negligible compared to querying a database 20 times. Your REST APIs likely do it as well and they even transmit that data down the wire to the client and the client has to discard it.
Type extensions shine in parent-child relationships when preloading data is a concern but be careful not to go isolating every graph field just to avoid retrieving data. Fetching a few extra bytes from a database is negligible compared to querying a database 20 individual times. Your REST APIs likely do it as well and they even transmit that data down the wire to the client and the client has to discard it.

It comes down to your use case. There are times when it makes sense to query data separately using type extensions and times when preloading whole objects is better. For many applications, once you've deployed to production, the queries being executed are finite. Design your model objects and extensions to be performant in the ways your data is being requested, not in the ways it _could be_ requested.
17 changes: 0 additions & 17 deletions docs/development/debugging.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,20 +21,3 @@ public void ConfigureServices(IServiceCollection services)
});
}
```

## Increase the Query Timeout

GraphQL will automatically abandon long running queries to prevent a resource drain. It may be helpful to up this timeout length in development. By default the timeout is `1 minute`.

```csharp
// Startup.cs
public void ConfigureServices(IServiceCollection services)
{
// Extending the default query timeout can help
// during extended debug sessions
services.AddGraphQL(options =>
{
options.ExecutionOptions.QueryTimeout = TimeSpan.FromMinutes(30);
});
}
```
10 changes: 5 additions & 5 deletions docs/development/entity-framework.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ public void ConfigureServices(IServiceCollection services)
});
}
```
This default registration adds the `DbContext` to the DI container is as a `Scoped` service. Meaning one instance is generated per Http request. However, consider the following controller and query:
This default registration adds the `DbContext` to the DI container is as a `Scoped` service. Meaning one instance is generated per Http request. However, consider the following graph controller and query:


<div class="sideBySideCode hljs">
Expand Down Expand Up @@ -57,13 +57,13 @@ query {
</div>
<br/>

The `FoodController` contains two action methods both of which are executed by the query. While the controller itself is registered with the DI container as transient the `DbContext` is not, it is shared between the controller instances. This can result in an exception being thrown :
The `FoodController` contains two action methods both of which are executed by the query. This means two instances of the controller are needed, once for each field resolution, since they are executed in parallel. While the controller itself is registered with the DI container as transient the `DbContext` is not, it is shared between the controller instances. This can result in an exception being thrown :

![Ef Core Error](../assets/ef-core-error.png)

This is caused by graphql attempting to execute both controller actions simultaneously. Ef Core will reject multiple active queries. There are a few ways to handle this and each comes with its own trade offs:

## Register DbContext as transient
## Register DbContext as Transient

One way to correct this problem is to register your DbContext
as a transient object.
Expand Down Expand Up @@ -99,6 +99,6 @@ public void ConfigureServices(IServiceCollection services)
```
This will instruct graphql to execute each encountered controller action one after the other. Your scoped `DbContext` would then be able to process the queries without issue.

The tradeoff with this method is a minor decrease in processing time since the queries are called in sequence. All other field resolutions would be executed in parallel.
The tradeoff with this method is a decrease in processing time since the queries are called in sequence. All other field resolutions would be executed in parallel.

If your application has other resources or services that are not thread safe it can be beneficial to isolate the other resolver types as well. You can add them to the ResolverIsolation configuration option as needed.
If your application has other resources or services that may have similar restrictions, it can be beneficial to isolate the other resolver types as well. You can add them to the ResolverIsolation configuration option as needed.
2 changes: 1 addition & 1 deletion docs/introduction/made-for-aspnet-developers.md
Original file line number Diff line number Diff line change
Expand Up @@ -75,7 +75,7 @@ Also, if you are integrating into an existing project, you'll find a lot of util

## Scoped Dependency Injection

Services are injected into graph controllers in the same manner as MVC controllers and with the same scope resolution as the HTTP request. Yes, your HTTP request level Entity Framework `DbContext` will be carried forward through all field resolutions.
Services are injected into graph controllers in the same manner as MVC controllers and with the same scope resolution as the HTTP request.

## User Authorization

Expand Down
Loading