Skip to content

Commit 8f8740f

Browse files
v0.14.0-beta (Performance Tuning) (#28)
* updated benchmark page with new results * adding new log events for dispatch queue * proofing, clarity and typos
1 parent 2df4f06 commit 8f8740f

21 files changed

+354
-322
lines changed

docs/advanced/custom-scalars.md

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -256,8 +256,8 @@ The @specifiedBy directive can be applied to a scalar in all the same ways as ot
256256
GraphQLProviders.ScalarProvider.RegisterCustomScalar(typeof(MoneyScalarType));
257257
services.AddGraphQL(o => {
258258
o.ApplyDirective("@specifiedBy")
259-
.WithArguments("https://myurl.com")
260-
.ToItems(item => item.Name == "Money");
259+
.WithArguments("https://myurl.com")
260+
.ToItems(item => item.Name == "Money");
261261
});
262262

263263
// via the ApplyDirective attribute
@@ -286,8 +286,7 @@ A few points about designing your scalar:
286286
- Scalar types are expected to be thread safe.
287287
- The runtime will pass a new instance of your scalar graph type to each registered schema. It must be declared with a public, parameterless constructor.
288288
- Scalar types should be simple and work in isolation.
289-
- The `ReadOnlySpan<char>` provided to `ILeafValueResolver.Resolve` should be all the data needed to generate a value, there should be no need to perform side effects or fetch additional data.
290-
- If you have a lot of logic to unpack a string, consider using a regular OBJECT graph type instead.
289+
- The `ReadOnlySpan<char>` provided to `ILeafValueResolver.Resolve` should be all the data needed to generate a value, there should be no need to perform side effects or fetch additional data.
291290
- Scalar types should not track any state or depend on any stateful objects.
292291
- `ILeafValueResolver.Resolve` must be **FAST**! Since your resolver is used to construct an initial query plan from a text document, it'll be called orders of magnitude more often than any other method.
293292

docs/advanced/directives.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -43,7 +43,7 @@ All directive action methods must:
4343

4444
- Share the same method signature
4545
- The return type must match exactly
46-
- The input arguments must match exactly in name, casing and declaration order.
46+
- The input arguments must match exactly in type, name, casing and declaration order.
4747
- Return a `IGraphActionResult` or `Task<IGraphActionResult>`
4848

4949
### Action Results

docs/advanced/subscriptions.md

Lines changed: 102 additions & 72 deletions
Large diffs are not rendered by default.

docs/advanced/type-expressions.md

Lines changed: 30 additions & 63 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,14 @@
11
---
22
id: type-expressions
3-
title: Custom Type Expressions
3+
title: Type Expressions
44
sidebar_label: Type Expressions
55
---
66

7-
GraphQL states that when a field returns a value that doesn't conform to the required definition of the field that the value is rejected, converted to null and an error added to the response.
7+
The GraphQL specification states that when a field resolves a value that doesn't conform to the type expression of the field that the value is rejected, converted to null and an error added to the response.
88

9-
GraphQL ASP.NET makes as few assumptions as possible about the data returned from your fields to result in as few errors as possible.
9+
When GraphQL ASP.NET build a schema it makes as few assumptions as possible about the data returned from your fields to result in as few errors as possible.
1010

11-
These assumptions are made:
11+
These assumptions are:
1212

1313
- Fields that return reference types **can be** null
1414
- Fields that return value types **cannot be** null
@@ -49,20 +49,20 @@ query {
4949
</div>
5050
<br/>
5151

52-
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.
52+
This action method could return a `Donut` or return `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.
5353

5454
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.
5555

56-
## Using an Alternate Type Expression
56+
## Field Type Expressions
5757

58-
Most of the time, using the `TypeExpression` property of a field declaration attribute is sufficient to indicate your intentions.
58+
You can add more specificity to your fields by using the `TypeExpression` property of the various field declaration attributes.
5959

6060
```csharp
6161

6262
// Declare that a donut MUST be returned (null is invalid)
6363
// ----
64-
// Schema Syntax: Donut!
65-
[Query("donut", TypeExpression = TypeExpressions.IsNotNull)]
64+
// Final Schema Syntax: Donut!
65+
[Query("donut", TypeExpression = "Type!")]
6666
public Donut RetrieveDonut(string id)
6767
{/*...*/}
6868

@@ -73,8 +73,8 @@ public Donut RetrieveDonut(string id)
7373
// valid: []
7474
// invalid: null
7575
// ----
76-
// Schema Syntax: [Donut]!
77-
[Query("donut", TypeExpression = TypeExpressions.IsNotNullList)]
76+
// Final Schema Syntax: [Donut]!
77+
[Query("donut", TypeExpression = "[Type]!")]
7878
public IEnumerable<Donut> RetrieveDonut(string id)
7979
{/*...*/}
8080

@@ -86,69 +86,36 @@ public IEnumerable<Donut> RetrieveDonut(string id)
8686
// invalid: [donut1, null, donut2]
8787
// invalid: null
8888
// ----
89-
// Schema Syntax: [Donut!]!
90-
[Query("donut", TypeExpression = TypeExpressions.IsNotNull | TypeExpressions.IsNotNullList)]
89+
// Final Schema Syntax: [Donut!]!
90+
[Query("donut", TypeExpression = "[Type!]!")]
9191
public IEnumerable<Donut> RetrieveDonut(string id)
9292
{/*...*/}
9393
```
9494

95-
> The `TypeExpression` property is available on `[Query]`, `[QueryRoot]`, `[Mutation]`, `[MutationRoot]`, `[Subscription]`, `[SubscriptionRoot]` and `[GraphField]`
95+
> The value `Type` used in the examples is arbitrary and can be any valid string. The correct type name for the target schema will be used in its place at runtime.
9696
97-
## Declaring a TypeDefinition
98-
99-
Using the `TypeExpressions` enumeration above is a convenient way to indicate the requirements of a field but in rare occasions you'll need to take it one step further and declare a complete type definition.
100-
101-
Take for instance this type:
97+
<span style="color:pink;">**Warning**: When declared, the runtime will use your `TypeExpression` as law for any field declarations; skipping its internal checks. You can setup a scenario where by you could return data that the runtime could never validate as being correct and GraphQL will happily process it and return an error every time. </span>
10298

10399
```csharp
104-
IEnumerable<IEnumerable<IEnumerable<string>>>
105-
```
106-
107-
The possible scenarios for our data could be endless:
108-
109-
- A list of a list of a list of strings
110-
- A list of a list of a list of strings that can't be null
111-
- A list that returns a list that could be a list or null that contains a list that contains a string
112-
- ...and so on
113-
114-
For this we have declare the full type definition our self as an array of `MetaGraphType`s
115-
116-
```csharp
117-
// Declare that the the method will return a "list of a list of a list of strings" and that any element could be null
118-
// This is equivalent to the defaults applied by GraphQL
119-
// ----
120-
// Schema Syntax: [[[String]]]
121-
[Query("names", TypeDefinition = [MetaGraphTypes.IsList, MetaGraphTypes.IsList, MetaGraphTypes.IsList])]
122-
public IEnumerable<IEnumerable<IEnumerable<string>>> GenerateNames(int seed)
123-
{/*...*/}
124-
125-
// Declare that we will return a "list of a list of a list of strings" and while any list could be null,
126-
// the strings themselves cannot be null
127-
// ----
128-
// Schema Syntax: [[[String!]]]
129-
[Query("names", TypeDefinition = [MetaGraphTypes.IsList, MetaGraphTypes.IsList, MetaGraphTypes.IsList, MetaGraphTypes.IsNotNull])]
130-
public IEnumerable<IEnumerable<IEnumerable<string>>> GenerateNames(int seed)
131-
{/*...*/}
132-
133-
// Declare that the return type is a "list of a list of non-null lists of strings".
134-
// ----
135-
// Schema Syntax: [[[String]!]]
136-
[Query("names", TypeDefinition = [MetaGraphTypes.IsList, MetaGraphTypes.IsList, MetaGraphTypes.IsNotNull, MetaGraphTypes.IsList])]
137-
public IEnumerable<IEnumerable<IEnumerable<string>>> GenerateNames(int seed)
100+
// QUERY EXECUTION ERROR
101+
// GraphQL will attempt to process Donut as an IEnumerable and will fail to resolve every time this
102+
// field is invoked
103+
[Query("donut", TypeExpression ="[Type]")]
104+
public Donut RetrieveDonut(string id)
138105
{/*...*/}
139106
```
140107

141-
> The `TypeDefinition` property is available on `[Query]`, `[QueryRoot]`, `[Mutation]`, `[MutationRoot]`, `[Subscription]`, `[SubscriptionRoot]` and `[GraphField]`
108+
"With great power comes great responsibility" -Uncle Ben
142109

143-
**Warning**: When declared, the runtime will accept your `TypeDefinition` or `TypeExpression` as law. You can setup a scenario where by you could return data that the runtime could never validate and GraphQL will happily process it and cause an error every time. For instance returning a single integer but declaring a `TypeDefinition` of a list of integers or declaring a list of donuts but only returning a single instance.
110+
## Input Argument Type Expressions
111+
112+
Similar to fields, you can use the `TypeExpression` property on `[FromGraphQL]` to add more specificity to your input arguments.
144113

145114
```csharp
146-
// ERROR
147-
// GraphQL will expect an IEnumerable to be returned and will fail every time this
148-
// field is invoked
149-
[Query("donut", TypeDefinition = [MetaGraphTypes.IsList])]
150-
public Donut RetrieveDonut(string id)
115+
// Force the argument "id" to supply a string (it cannot be supplied as null)
116+
// -----------------
117+
// Final Type Expression of the 'id' arg: String!
118+
[Query]
119+
public Donut RetrieveDonut([FromGraphQL(TypeExpression = "Type!")] string id)
151120
{/*...*/}
152-
```
153-
154-
"With great power comes great responsibility" - Uncle Ben
121+
```

docs/assets/benchmarks.png

37.5 KB
Loading

docs/controllers/type-extensions.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -68,7 +68,7 @@ Well that's just plain awful. We've over complicated our bakery model and made i
6868

6969
## The [TypeExtension] Attribute
7070

71-
So what do we do? We've talked in the section on [field paths](./field-paths) about GraphQL maintaining a 1:1 mapping between a field in the graph and a method to retrieve data for it (i.e. its assigned resolver). What prevents us from creating a method to fetch a list of Cake Orders and saying, "Hey, GraphQL! When someone asks for the field `[type]/bakery/orders` call our method instead of a property getter on the `Bakery` class. As it turns out, that is exactly what a `Type Extension` does.
71+
So what do we do? We've talked before about GraphQL maintaining a 1:1 mapping between a field in the graph and a method to retrieve data for it (i.e. its assigned resolver). What prevents us from creating a method to fetch a list of Cake Orders and saying, "Hey, GraphQL! When someone asks for the field `[type]/bakery/orders` call our method instead of a property getter on the `Bakery` class. As it turns out, that is exactly what a `Type Extension` does.
7272

7373
```csharp
7474
// Bakery.cs

docs/controllers/field-paths.md renamed to docs/controllers/virtual-types.md

Lines changed: 34 additions & 45 deletions
Original file line numberDiff line numberDiff line change
@@ -1,72 +1,61 @@
11
---
2-
id: field-paths
2+
id: virtual-types
33
title: Virtual Graph Types
44
sidebar_label: Virtual Graph Types
55
---
66

77
## What is a Virtual Graph Type?
88

9-
When we reason about ASP.NET MVC, routing comes naturally. We define a URL and perform an HTTP request to fetch data.
9+
GraphQL is statically typed. Each field in a query must always resolve to a single graph type known to the schema. This can make query organization rather tedious and adds A LOT of boilerplate code if you wanted to introduce even the slightest complexity to your graph.
1010

11-
```
12-
GET http://homeMadeBakery.local/pastries/donuts/15
13-
```
14-
15-
and we can picture how this might map to an API Controller:
16-
17-
```csharp
18-
[Route("pastries")]
19-
public class PastriesController : Controller
20-
{
21-
[HttpGet("donuts/{id}")]
22-
public Donut RetrieveDonut(int id)
23-
{/* ... */}
24-
}
25-
```
26-
27-
When you startup an MVC or Web API application .NET gathers your configured routes and maintains a map of `endpoint -> action method` in order to hand off an HTTP request to a given method.
28-
29-
OK, Great! Now lets think about this graphQL query:
11+
Let's think about this query:
3012

31-
```javascript
13+
```ruby
3214
query {
33-
pastries {
34-
donut(id: 15){
35-
name
36-
flavor
15+
groceryStore {
16+
bakery {
17+
pastries {
18+
donut(id: 15){
19+
name
20+
flavor
21+
}
22+
}
3723
}
24+
deli {
25+
meats {
26+
beef (id: 23) {
27+
name
28+
cut
29+
}
30+
}
31+
}
3832
}
3933
}
4034
```
4135

42-
We can think about each field in the query as a unique path in our schema, similar to the URL above:
43-
44-
```ruby
45-
[query]
46-
[query]/pastries
47-
[query]/pastries/donut
48-
[type]/donut/name
49-
[type]/donut/flavor
50-
```
51-
52-
In fact, this is how GraphQL ASP.NET knows how to invoke your methods. At startup, a complete map of the query and mutation methods as well as every graph type is translated into a set of field paths. Then, when a request is made of a field, it invokes the method or property associated with that `field path`.
36+
Knowing what we know about GraphQL's requirements, we need to create types for the grocery store, the bakery, pastries, a donut, the deli counter, meats, beef etc. Its a lot of setup for what basically boils down to two methods to retrieve a donut and a cut of beef by their respective ids.
5337

54-
> All action methods, graph type properties and graph type methods must map to a unique field path in your graph schema.
38+
This is where `virtual graph types` come in. Using a templating pattern similar to what we do with REST queries we can create rich graphs with very little boiler plate. Adding a new arm to your graph is as simple as defining a path to it in a controller.
5539

5640
```csharp
57-
[GraphRoute("pastries")]
58-
public class PastriesController : GraphController
41+
[GraphRoute("groceryStore")]
42+
public class GroceryStoreController : GraphController
5943
{
60-
[Query("donut")]
44+
[Query("bakery/pastries/donut")]
6145
public Donut RetrieveDonut(int id)
6246
{/* ...*/}
6347

48+
[Query("deli/meats/beef")]
49+
public Meat RetrieveCutOfBeef(int id)
50+
{/* ...*/}
6451
}
6552
```
6653

67-
This is a different controller than in the top example. We inherit from `GraphController` and use `Query` instead of `Controller` and `HttpGet`, respectively.
54+
Internally, for each encountered path segment (e.g. `bakery`, `meats`), GraphQL generates a `virutal graph type` to fulfill resolver requests for you and act as a pass through to your real code. It does this in concert with your real code and performs a lot of checks at start up to ensure that the combination of your real types as well as virutal types can be put together to form a functional graph. If a collision occurs the server will fail to start.
55+
56+
#### Another Example
6857

69-
You can nest fields as deep as you need in order to create a rich organizational hierarchy to your data. This is best explained by code, take a look at these two controllers:
58+
You can nest fields as deep as you want and spread them across any number of controllers in order to create a rich organizational hierarchy to your data. This is best explained by code, take a look at these two controllers:
7059

7160
```csharp
7261

@@ -143,7 +132,7 @@ With REST, this is probably 4 separate requests or one super contrived request b
143132

144133
## Actions Must Have a Unique Path
145134

146-
As was said above, each field in your object graph must uniquely map to one method (or getter property) in your code.
135+
Each field in your object graph must uniquely map to one method (or getter property) in your code.
147136

148137
Take this example:
149138

@@ -385,7 +374,7 @@ When you start thinking about large object graphs, 100s of controllers and 100s
385374

386375
## Field Path Names
387376

388-
> Each segment of a field path must individually conform to the required naming standards for fields and graph type names.
377+
> Each segment of a virtual field path must individually conform to the required naming standards for fields and graph type names.
389378
390379
In reality this primarily means don't start your fields with a double underscore, `__`, as thats reserved by the introspection system. The complete regex is available in the source code at `Constants.RegExPatterns.NameRegex`.
391380

docs/development/entity-framework.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -99,6 +99,6 @@ public void ConfigureServices(IServiceCollection services)
9999
```
100100
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.
101101

102-
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.
102+
The tradeoff with this method is an increase in processing time since the methods are called in sequence. All other field resolutions would be executed in parallel.
103103

104104
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.

docs/development/unit-testing.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ title: Unit Testing
44
sidebar_label: Unit Testing
55
---
66

7-
GraphQL ASP.NET has more than `2500 unit tests and 91% code coverage`. Much of this is powered by a test component designed to quickly build a configurable, fully mocked server instance to perform a query. It may be helpful to download the code and extend it for harnessing your own controllers.
7+
GraphQL ASP.NET has more than `3000 unit tests and 91% code coverage`. Much of this is powered by a test component designed to quickly build a configurable, fully mocked server instance to perform a query. It may be helpful to download the code and extend it for harnessing your own controllers.
88

99
The `TestServerBuilder<TSchema>` can be found in the `graphql-aspnet-testframework` project of the primary repo and is dependent on `Moq`. As its part of the core library solution you'll want to remove the project reference to `graphql-aspnet` project and instead add a reference to the nuget package.
1010

0 commit comments

Comments
 (0)