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
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@

<ItemGroup>
<Compile Include="Helpers.fs" />
<Compile Include="RequestExecutionContext.fs" />
<Compile Include="GraphQLOptions.fs" />
<Compile Include="GraphQLSubscriptionsManagement.fs" />
<Compile Include="GraphQLWebsocketMiddleware.fs" />
Expand Down
26 changes: 16 additions & 10 deletions src/FSharp.Data.GraphQL.Server.AspNetCore/GraphQLRequestHandler.fs
Original file line number Diff line number Diff line change
Expand Up @@ -16,28 +16,34 @@ open FSharp.Data.GraphQL.Server
open FSharp.Data.GraphQL.Shared

type DefaultGraphQLRequestHandler<'Root>
/// <summary>
/// Handles GraphQL requests using a provided root schema.
/// </summary>
/// <param name="httpContextAccessor">The accessor to the current HTTP context.</param>
/// <param name="options">The options monitor for GraphQL options.</param>
/// <param name="logger">The logger to log messages.</param>
(
/// The accessor to the current HTTP context
httpContextAccessor : IHttpContextAccessor,
/// The options monitor for GraphQL options
options : IOptionsMonitor<GraphQLOptions<'Root>>,
/// The logger to log messages
logger : ILogger<DefaultGraphQLRequestHandler<'Root>>
) =
inherit GraphQLRequestHandler<'Root> (httpContextAccessor, options, logger)

/// Provides logic to parse and execute GraphQL request
and [<AbstractClass>] GraphQLRequestHandler<'Root>
/// <summary>
/// Provides logic to parse and execute GraphQL requests.
/// </summary>
/// <param name="httpContextAccessor">The accessor to the current HTTP context.</param>
/// <param name="options">The options monitor for GraphQL options.</param>
/// <param name="logger">The logger to log messages.</param>
(
/// The accessor to the current HTTP context
httpContextAccessor : IHttpContextAccessor,
/// The options monitor for GraphQL options
options : IOptionsMonitor<GraphQLOptions<'Root>>,
/// The logger to log messages
logger : ILogger
) =

let ctx = httpContextAccessor.HttpContext
let getInputContext() = (HttpContextRequestExecutionContext ctx) :> IInputExecutionContext

let toResponse { DocumentId = documentId; Content = content; Metadata = metadata } =

Expand Down Expand Up @@ -142,8 +148,8 @@ and [<AbstractClass>] GraphQLRequestHandler<'Root>
let executeIntrospectionQuery (executor : Executor<_>) (ast : Ast.Document voption) : Task<IResult> = task {
let! result =
match ast with
| ValueNone -> executor.AsyncExecute IntrospectionQuery.Definition
| ValueSome ast -> executor.AsyncExecute ast
| ValueNone -> executor.AsyncExecute (IntrospectionQuery.Definition, getInputContext)
| ValueSome ast -> executor.AsyncExecute (ast, getInputContext)

let response = result |> toResponse
return (TypedResults.Ok response) :> IResult
Expand Down Expand Up @@ -228,7 +234,7 @@ and [<AbstractClass>] GraphQLRequestHandler<'Root>

let! result =
Async.StartImmediateAsTask (
executor.AsyncExecute (content.Ast, root, ?variables = variables, ?operationName = operationName),
executor.AsyncExecute (content.Ast, getInputContext, root, ?variables = variables, ?operationName = operationName),
cancellationToken = ctx.RequestAborted
)

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@ type GraphQLWebSocketMiddleware<'Root>
| ServerPong p -> { Id = ValueNone; Type = "pong"; Payload = p |> ValueOption.map CustomResponse }
| Next (id, payload) -> { Id = ValueSome id; Type = "next"; Payload = ValueSome <| ExecutionResult payload }
| Complete id -> { Id = ValueSome id; Type = "complete"; Payload = ValueNone }
| Error (id, errMsgs) -> { Id = ValueSome id; Type = "error"; Payload = ValueSome <| ErrorMessages errMsgs }
| Error (id, errMessages) -> { Id = ValueSome id; Type = "error"; Payload = ValueSome <| ErrorMessages errMessages }
return JsonSerializer.Serialize (raw, jsonSerializerOptions)
}

Expand Down Expand Up @@ -89,9 +89,9 @@ type GraphQLWebSocketMiddleware<'Root>
&& ((segmentResponse = null)
|| (not segmentResponse.EndOfMessage)) do
try
let! r = socket.ReceiveAsync (new ArraySegment<byte> (buffer), cancellationToken)
let! r = socket.ReceiveAsync (ArraySegment<byte>(buffer), cancellationToken)
segmentResponse <- r
completeMessage.AddRange (new ArraySegment<byte> (buffer, 0, r.Count))
completeMessage.AddRange (ArraySegment<byte>(buffer, 0, r.Count))
with :? OperationCanceledException ->
()

Expand All @@ -117,7 +117,7 @@ type GraphQLWebSocketMiddleware<'Root>
else
// TODO: Allocate string only if a debugger is attached
let! serializedMessage = message |> serializeServerMessage jsonSerializerOptions
let segment = new ArraySegment<byte> (System.Text.Encoding.UTF8.GetBytes (serializedMessage))
let segment = ArraySegment<byte>(System.Text.Encoding.UTF8.GetBytes (serializedMessage))
if not (socket.State = WebSocketState.Open) then
logger.LogTrace ($"Ignoring message to be sent via socket, since its state is not '{nameof WebSocketState.Open}', but '{{state}}'", socket.State)
else
Expand Down Expand Up @@ -160,14 +160,15 @@ type GraphQLWebSocketMiddleware<'Root>
tryToGracefullyCloseSocket (WebSocketCloseStatus.NormalClosure, "Normal Closure")

let handleMessages (cancellationToken : CancellationToken) (httpContext : HttpContext) (socket : WebSocket) : Task =
let subscriptions = new Dictionary<SubscriptionId, SubscriptionUnsubscriber * OnUnsubscribeAction> ()
let subscriptions = Dictionary<SubscriptionId, SubscriptionUnsubscriber * OnUnsubscribeAction>()
// ---------->
// Helpers -->
// ---------->
let rcvMsgViaSocket = receiveMessageViaSocket (CancellationToken.None)

let sendMsg = sendMessageViaSocket serializerOptions socket
let rcv () = socket |> rcvMsgViaSocket serializerOptions
let getInputContext() = (HttpContextRequestExecutionContext httpContext) :> IInputExecutionContext

let sendOutput id (output : SubscriptionExecutionResult) =
sendMsg (Next (id, output))
Expand Down Expand Up @@ -234,10 +235,10 @@ type GraphQLWebSocketMiddleware<'Root>
&& socket |> isSocketOpen do
let! receivedMessage = rcv ()
match receivedMessage with
| Result.Error failureMsgs ->
| Result.Error failureMessages ->
nameof InvalidMessage
|> logMsgReceivedWithOptionalPayload ValueNone
match failureMsgs with
match failureMessages with
| InvalidMessage (code, explanation) -> do! socket.CloseAsync (enum code, explanation, CancellationToken.None)
| Ok ValueNone -> logger.LogTrace ("WebSocket received empty message! State = '{socketState}'", socket.State)
| Ok (ValueSome msg) ->
Expand Down Expand Up @@ -274,11 +275,11 @@ type GraphQLWebSocketMiddleware<'Root>
let variables = query.Variables |> Skippable.toOption
let! planExecutionResult =
let root = options.RootFactory httpContext
options.SchemaExecutor.AsyncExecute (query.Query, root, ?variables = variables)
options.SchemaExecutor.AsyncExecute (query.Query, getInputContext, root, ?variables = variables)
do! planExecutionResult |> applyPlanExecutionResult id socket
with ex ->
logger.LogError (ex, "Unexpected error during subscription with id '{id}'", id)
do! sendMsg (Error (id, [new Shared.NameValueLookup ([ ("subscription", "Unexpected error during subscription" :> obj) ])]))
do! sendMsg (Error (id, [NameValueLookup([ ("subscription", "Unexpected error during subscription" :> obj) ])]))
| ClientComplete id ->
"ClientComplete" |> logMsgWithIdReceived id
subscriptions
Expand All @@ -287,7 +288,7 @@ type GraphQLWebSocketMiddleware<'Root>
do! socket |> tryToGracefullyCloseSocketWithDefaultBehavior
with ex ->
logger.LogError (ex, "Cannot handle a message; dropping a websocket connection")
// at this point, only something really weird must have happened.
// At this point, only something really weird must have happened.
// In order to avoid faulty state scenarios and unimagined damages,
// just close the socket without further ado.
do! socket |> tryToGracefullyCloseSocketWithDefaultBehavior
Expand Down Expand Up @@ -344,7 +345,7 @@ type GraphQLWebSocketMiddleware<'Root>
return Result.Error <| "{nameof ConnectionInit} timeout"
}

member __.InvokeAsync (ctx : HttpContext) = task {
member _.InvokeAsync (ctx : HttpContext) = task {
if not (ctx.Request.Path = endpointUrl) then
do! next.Invoke (ctx)
else if ctx.WebSockets.IsWebSocketRequest then
Expand Down
2 changes: 0 additions & 2 deletions src/FSharp.Data.GraphQL.Server.AspNetCore/Helpers.fs
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@ namespace FSharp.Data.GraphQL.Server.AspNetCore
open System
open System.Text


[<AutoOpen>]
module Helpers =

Expand Down Expand Up @@ -48,4 +47,3 @@ module ReflectionHelpers =
| PropertyGet (_, propertyInfo, _) -> propertyInfo.DeclaringType
| FieldGet (_, fieldInfo) -> fieldInfo.DeclaringType
| _ -> failwith "Expression is no property."

21 changes: 17 additions & 4 deletions src/FSharp.Data.GraphQL.Server.AspNetCore/HttpContext.fs
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ type HttpContext with
/// </summary>
/// <typeparam name="'T">Type to deserialize to</typeparam>
/// <returns>
/// Retruns a <see cref="System.Threading.Tasks.Task{T}"/>Deserialized object or
/// Returns a <see cref="System.Threading.Tasks.Task{T}"/>Deserialized object or
/// <see cref="ProblemDetails">ProblemDetails</see> as <see cref="IResult">IResult</see>
/// if a body could not be deserialized.
/// </returns>
Expand All @@ -31,10 +31,23 @@ type HttpContext with
let request = ctx.Request

try
if not request.Body.CanSeek then
request.EnableBuffering()
let! jsonStream =
task {
if request.HasFormContentType then
let! form = request.ReadFormAsync(ctx.RequestAborted)
match form.TryGetValue("operations") with
| true, values when values.Count > 0 ->
let bytes = System.Text.Encoding.UTF8.GetBytes(values[0])
return new MemoryStream(bytes) :> Stream
| _ ->
return request.Body
else
if not request.Body.CanSeek then
request.EnableBuffering()
return request.Body
}

return! JsonSerializer.DeserializeAsync<'T>(request.Body, serializerOptions, ctx.RequestAborted)
return! JsonSerializer.DeserializeAsync<'T>(jsonStream, serializerOptions, ctx.RequestAborted)
Comment on lines +34 to +50
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This looks strange to put string back to stream

Suggested change
let! jsonStream =
task {
if request.HasFormContentType then
let! form = request.ReadFormAsync(ctx.RequestAborted)
match form.TryGetValue("operations") with
| true, values when values.Count > 0 ->
let bytes = System.Text.Encoding.UTF8.GetBytes(values[0])
return new MemoryStream(bytes) :> Stream
| _ ->
return request.Body
else
if not request.Body.CanSeek then
request.EnableBuffering()
return request.Body
}
return! JsonSerializer.DeserializeAsync<'T>(request.Body, serializerOptions, ctx.RequestAborted)
return! JsonSerializer.DeserializeAsync<'T>(jsonStream, serializerOptions, ctx.RequestAborted)
if request.HasFormContentType then
let! form = request.ReadFormAsync (ctx.RequestAborted)
match form.TryGetValue "operations" with
| true, values when values.Count > 0 ->
return JsonSerializer.Deserialize<'T>(values[0], serializerOptions, ctx.RequestAborted)
| _ ->
return! JsonSerializer.DeserializeAsync<'T>(request.Body, serializerOptions, ctx.RequestAborted)
else
if not request.Body.CanSeek then
request.EnableBuffering()
return! JsonSerializer.DeserializeAsync<'T>(request.Body, serializerOptions, ctx.RequestAborted)

with :? JsonException ->
let body = request.Body
body.Seek(0, SeekOrigin.Begin) |> ignore
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
namespace FSharp.Data.GraphQL.Server.AspNetCore

open FSharp.Data.GraphQL
open Microsoft.AspNetCore.Http

type HttpContextRequestExecutionContext (httpContext : HttpContext) =

interface IInputExecutionContext with

member this.GetFile(key) =
if not httpContext.Request.HasFormContentType then
Error "Request does not have form content type"
else
let form = httpContext.Request.Form
match (form.Files |> Seq.vtryFind (fun f -> f.Name = key)) with
| ValueSome file -> Ok (file.OpenReadStream())
| ValueNone -> Error $"File with key '{key}' not found"

Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ namespace FSharp.Data.GraphQL.Server.Middleware

open System.Collections.Generic
open System.Collections.Immutable
open FSharp.Data.GraphQL.Shared
open FsToolkit.ErrorHandling

open FSharp.Data.GraphQL
Expand All @@ -11,7 +12,7 @@ open FSharp.Data.GraphQL.Types

type internal QueryWeightMiddleware(threshold : float, reportToMetadata : bool) =

let middleware (threshold : float) (ctx : ExecutionContext) (next : ExecutionContext -> AsyncVal<GQLExecutionResult>) =
let middleware (threshold : float) (inputContext : InputExecutionContextProvider) (ctx : ExecutionContext) (next : ExecutionContext -> AsyncVal<GQLExecutionResult>) =
let measureThreshold (threshold : float) (fields : ExecutionInfo list) =
let getWeight f =
if f.ParentDef = upcast ctx.ExecutionPlan.RootDef
Expand Down Expand Up @@ -72,7 +73,7 @@ type internal ObjectListFilterMiddleware<'ObjectType, 'ListType>(reportToMetadat
let compileMiddleware (ctx : SchemaCompileContext) (next : SchemaCompileContext -> unit) =
let modifyFields (object : ObjectDef<'ObjectType>) (fields : FieldDef<'ObjectType> seq) =
let args = [ Define.Input("filter", Nullable ObjectListFilterType) ]
let fields = fields |> Seq.map (fun x -> x.WithArgs(args)) |> List.ofSeq
let fields = fields |> Seq.map _.WithArgs(args) |> List.ofSeq
object.WithFields(fields)
let typesWithListFields =
ctx.TypeMap.GetTypesWithListFields<'ObjectType, 'ListType>()
Expand All @@ -85,15 +86,15 @@ type internal ObjectListFilterMiddleware<'ObjectType, 'ListType>(reportToMetadat
ctx.TypeMap.AddTypes(modifiedTypes, overwrite = true)
next ctx

let reportMiddleware (ctx : ExecutionContext) (next : ExecutionContext -> AsyncVal<GQLExecutionResult>) =
let reportMiddleware (inputContext : InputExecutionContextProvider) (ctx : ExecutionContext) (next : ExecutionContext -> AsyncVal<GQLExecutionResult>) =
let rec collectArgs (path: obj list) (acc : KeyValuePair<obj list, ObjectListFilter> list) (fields : ExecutionInfo list) =
let fieldArgs currentPath field =
let filterResults =
field.Ast.Arguments
|> Seq.map (fun x ->
match x.Name, x.Value with
| "filter", (VariableName variableName) -> Ok (ValueSome (ctx.Variables[variableName] :?> ObjectListFilter))
| "filter", inlineConstant -> ObjectListFilterType.CoerceInput (InlineConstant inlineConstant) ctx.Variables |> Result.map ValueOption.ofObj
| "filter", inlineConstant -> ObjectListFilterType.CoerceInput inputContext (InlineConstant inlineConstant) ctx.Variables |> Result.map ValueOption.ofObj
| _ -> Ok ValueNone)
|> Seq.toList
match filterResults |> splitSeqErrorsList with
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -167,7 +167,7 @@ let ObjectListFilterType : InputCustomDefinition<ObjectListFilter> = {
Some
"The `Filter` scalar type represents a filter on one or more fields of an object in an object list. The filter is represented by a JSON object where the fields are the complemented by specific suffixes to represent a query."
CoerceInput =
(fun input variables ->
(fun _ input variables ->
match input with
| InlineConstant c ->
(coerceObjectListFilterInput variables c)
Expand Down
Loading