Skip to content

Commit

Permalink
Migrated Schema Middleware (#2307)
Browse files Browse the repository at this point in the history
  • Loading branch information
michaelstaib committed Sep 10, 2020
1 parent 4776afc commit 538a94e
Show file tree
Hide file tree
Showing 5 changed files with 236 additions and 6 deletions.
Expand Up @@ -25,18 +25,20 @@ public static class HttpEndpointRouteBuilderExtensions
throw new ArgumentNullException(nameof(endpointRouteBuilder));
}

IApplicationBuilder applicationBuilder =
IApplicationBuilder requestPipeline =
endpointRouteBuilder.CreateApplicationBuilder();

applicationBuilder.UseMiddleware<WebSocketSubscriptionMiddleware>(
requestPipeline.UseMiddleware<WebSocketSubscriptionMiddleware>(
schemaName.HasValue ? schemaName : Schema.DefaultName);
applicationBuilder.UseMiddleware<HttpPostMiddleware>(
requestPipeline.UseMiddleware<HttpPostMiddleware>(
schemaName.HasValue ? schemaName : Schema.DefaultName);
applicationBuilder.UseMiddleware<HttpGetMiddleware>(
requestPipeline.UseMiddleware<HttpGetSchemaMiddleware>(
schemaName.HasValue ? schemaName : Schema.DefaultName);
requestPipeline.UseMiddleware<HttpGetMiddleware>(
schemaName.HasValue ? schemaName : Schema.DefaultName);

return endpointRouteBuilder
.Map(pattern, applicationBuilder.Build())
.Map(pattern, requestPipeline.Build())
.WithDisplayName("Hot Chocolate GraphQL Pipeline");
}
}
Expand Down
@@ -0,0 +1,62 @@
using System.IO;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Http;
using HotChocolate.AspNetCore.Utilities;
using HotChocolate.Execution;
using HttpRequestDelegate = Microsoft.AspNetCore.Http.RequestDelegate;

namespace HotChocolate.AspNetCore
{
public class HttpGetSchemaMiddleware : MiddlewareBase
{

public HttpGetSchemaMiddleware(
HttpRequestDelegate next,
IRequestExecutorResolver executorResolver,
IHttpResultSerializer resultSerializer,
NameString schemaName)
: base(next, executorResolver, resultSerializer, schemaName)
{
}

public async Task InvokeAsync(HttpContext context)
{
if (HttpMethods.IsGet(context.Request.Method) &&
context.Request.Query.ContainsKey("SDL"))
{
await HandleRequestAsync(context);
}
else
{
// if the request is not a get request or if the content type is not correct
// we will just invoke the next middleware and do nothing.
await NextAsync(context);
}
}

private async Task HandleRequestAsync(HttpContext context)
{
IRequestExecutor requestExecutor = await GetExecutorAsync(context.RequestAborted);

string fileName =
requestExecutor.Schema.Name.IsEmpty ||
requestExecutor.Schema.Name.Equals(Schema.DefaultName)
? "schema.graphql"
: requestExecutor.Schema.Name + ".schema.graphql";

context.Response.ContentType = ContentType.GraphQL;
context.Response.Headers.Add(
"Content-Disposition",
new[] { $"attachment; filename=\"{fileName}\"" });

await using var memoryStream = new MemoryStream();
await using var streamWriter = new StreamWriter(memoryStream);

SchemaSerializer.Serialize(requestExecutor.Schema, streamWriter);
await streamWriter.FlushAsync().ConfigureAwait(false);

memoryStream.Seek(0, SeekOrigin.Begin);
await memoryStream.CopyToAsync(context.Response.Body).ConfigureAwait(false);
}
}
}
@@ -0,0 +1,35 @@
using System.Net;
using System.Net.Http;
using System.Threading.Tasks;
using HotChocolate.AspNetCore.Utilities;
using Microsoft.AspNetCore.TestHost;
using Snapshooter.Xunit;
using Xunit;

namespace HotChocolate.AspNetCore
{
public class HttpGetSchemaMiddlewareTests : ServerTestBase
{
public HttpGetSchemaMiddlewareTests(TestServerFactory serverFactory)
: base(serverFactory)
{
}

[Fact]
public async Task Download_GraphQL_SDL()
{
// arrange
TestServer server = CreateStarWarsServer();
var url = TestServerExtensions.CreateUrl("/graphql?sdl");
var request = new HttpRequestMessage(HttpMethod.Get, url);

// act
HttpResponseMessage response = await server.CreateClient().SendAsync(request);

// assert
Assert.Equal(HttpStatusCode.OK, response.StatusCode);
var result = await response.Content.ReadAsStringAsync();
result.MatchSnapshot();
}
}
}
Expand Up @@ -31,7 +31,7 @@ public ServerTestBase(TestServerFactory serverFactory)
app => app
.UseWebSockets()
.UseRouting()
.UseEndpoints(endpoints =>
.UseEndpoints(endpoints =>
{
endpoints.MapGraphQL(pattern);
endpoints.MapGraphQL("evict", "evict");
Expand Down
@@ -0,0 +1,131 @@
schema {
query: Query
mutation: Mutation
subscription: Subscription
}

interface Character {
id: ID!
name: String
friends(first: PaginationAmount after: String last: PaginationAmount before: String): CharacterConnection
appearsIn: [Episode]
height(unit: Unit): Float
}

"A connection to a list of items."
type CharacterConnection {
"Information to aid in pagination."
pageInfo: PageInfo!
"A list of edges."
edges: [CharacterEdge!]
"A flattened list of the nodes."
nodes: [Character]
totalCount: Int!
}

"An edge in a connection."
type CharacterEdge {
"A cursor for use in pagination."
cursor: String!
"The item at the end of the edge."
node: Character
}

type Droid implements Character {
id: ID!
appearsIn: [Episode]
friends(first: PaginationAmount after: String last: PaginationAmount before: String): CharacterConnection
height(unit: Unit): Float
name: String
primaryFunction: String
}

type Human implements Character {
id: ID!
appearsIn: [Episode]
friends(first: PaginationAmount after: String last: PaginationAmount before: String): CharacterConnection
otherHuman: Human
height(unit: Unit): Float
name: String
homePlanet: String
}

type Mutation {
createReview(episode: Episode! review: ReviewInput!): Review!
}

"Information about pagination in a connection."
type PageInfo {
"Indicates whether more edges exist following the set defined by the clients arguments."
hasNextPage: Boolean!
"Indicates whether more edges exist prior the set defined by the clients arguments."
hasPreviousPage: Boolean!
"When paginating backwards, the cursor to continue."
startCursor: String
"When paginating forwards, the cursor to continue."
endCursor: String
}

type Query {
hero(episode: Episode! = NEW_HOPE): Character
heroes(episodes: [Episode!]): [Character!]
character(characterIds: [String]): [Character!]!
search(text: String): [SearchResult]
human(id: String): Human
droid(id: String): Droid
time: Long!
evict: Boolean!
}

type Review {
stars: Int!
commentary: String
}

type Starship {
id: String
name: String
length: Float!
}

type Subscription {
onReview(episode: Episode!): Review!
}

union SearchResult = Starship | Human | Droid

input ReviewInput {
stars: Int!
commentary: String
}

enum Episode {
NEW_HOPE
EMPIRE
JEDI
}

enum Unit {
FOOT
METERS
}

"The `Boolean` scalar type represents `true` or `false`."
scalar Boolean

"The `Float` scalar type represents signed double-precision fractional values as specified by [IEEE 754](http:\/\/en.wikipedia.org\/wiki\/IEEE_floating_point)."
scalar Float

"The `ID` scalar type represents a unique identifier, often used to refetch an object or as key for a cache. The ID type appears in a JSON response as a String; however, it is not intended to be human-readable. When expected as an input type, any string (such as `\"4\"`) or integer (such as `4`) input value will be accepted as an ID."
scalar ID

"The `Int` scalar type represents non-fractional signed whole numeric values. Int can represent values between -(2^31) and 2^31 - 1."
scalar Int

"The `Long` scalar type represents non-fractional signed whole 64-bit numeric values. Long can represent values between -(2^63) and 2^63 - 1."
scalar Long

scalar PaginationAmount

"The `String` scalar type represents textual data, represented as UTF-8 character sequences. The String type is most often used by GraphQL to represent free-form human-readable text."
scalar String

0 comments on commit 538a94e

Please sign in to comment.