diff --git a/src/FSharp.Data.GraphQL.Server.AspNetCore/Giraffe/HttpContext.fs b/src/FSharp.Data.GraphQL.Server.AspNetCore/Giraffe/HttpContext.fs index 1f8e59460..9a5492301 100644 --- a/src/FSharp.Data.GraphQL.Server.AspNetCore/Giraffe/HttpContext.fs +++ b/src/FSharp.Data.GraphQL.Server.AspNetCore/Giraffe/HttpContext.fs @@ -8,6 +8,7 @@ open System.Runtime.CompilerServices open System.Text.Json open Microsoft.AspNetCore.Http open Microsoft.Extensions.DependencyInjection +open Microsoft.Extensions.Options open FSharp.Core open FsToolkit.ErrorHandling @@ -26,7 +27,7 @@ type HttpContext with /// [] member ctx.TryBindJsonAsync<'T>(expectedJson) = taskResult { - let serializerOptions = ctx.RequestServices.GetRequiredService().SerializerOptions + let serializerOptions = ctx.RequestServices.GetRequiredService>().Value.SerializerOptions let request = ctx.Request try diff --git a/src/FSharp.Data.GraphQL.Server.AspNetCore/Giraffe/HttpHandlers.fs b/src/FSharp.Data.GraphQL.Server.AspNetCore/Giraffe/HttpHandlers.fs index f8e1370cf..0ceafc718 100644 --- a/src/FSharp.Data.GraphQL.Server.AspNetCore/Giraffe/HttpHandlers.fs +++ b/src/FSharp.Data.GraphQL.Server.AspNetCore/Giraffe/HttpHandlers.fs @@ -8,6 +8,7 @@ open System.Threading.Tasks open Microsoft.AspNetCore.Http open Microsoft.Extensions.DependencyInjection open Microsoft.Extensions.Logging +open Microsoft.Extensions.Options open FsToolkit.ErrorHandling open Giraffe @@ -20,6 +21,8 @@ type HttpHandler = HttpFunc -> HttpContext -> HttpFuncResult module HttpHandlers = + let [] internal IdentedOptionsName = "Idented" + let rec private moduleType = getModuleType <@ moduleType @> let ofTaskIResult ctx (taskRes: Task) : HttpFuncResult = task { @@ -38,12 +41,12 @@ module HttpHandlers = let logger = sp.CreateLogger moduleType - let options = sp.GetRequiredService>() + let options = sp.GetRequiredService>>() let toResponse { DocumentId = documentId; Content = content; Metadata = metadata } = let serializeIdented value = - let jsonSerializerOptions = options.GetSerializerOptionsIdented() + let jsonSerializerOptions = options.Get(IdentedOptionsName).SerializerOptions JsonSerializer.Serialize(value, jsonSerializerOptions) match content with @@ -243,7 +246,7 @@ module HttpHandlers = variables |> Option.iter (fun v -> logger.LogTrace($"GraphQL variables:{Environment.NewLine}{{variables}}", v)) - let root = options.RootFactory ctx + let root = options.CurrentValue.RootFactory ctx let! result = Async.StartAsTask( @@ -259,7 +262,7 @@ module HttpHandlers = Task.FromResult None else taskResult { - let executor = options.SchemaExecutor + let executor = options.CurrentValue.SchemaExecutor match! checkOperationType ctx with | IntrospectionQuery optionalAstDocument -> return! executeIntrospectionQuery executor optionalAstDocument | OperationQuery content -> return! executeOperation executor content diff --git a/src/FSharp.Data.GraphQL.Server.AspNetCore/GraphQLOptions.fs b/src/FSharp.Data.GraphQL.Server.AspNetCore/GraphQLOptions.fs index 773414dc5..75ac60f50 100644 --- a/src/FSharp.Data.GraphQL.Server.AspNetCore/GraphQLOptions.fs +++ b/src/FSharp.Data.GraphQL.Server.AspNetCore/GraphQLOptions.fs @@ -6,12 +6,12 @@ open System.Text.Json open System.Threading.Tasks open Microsoft.AspNetCore.Http -type PingHandler = IServiceProvider -> JsonDocument option -> Task +type PingHandler = IServiceProvider -> JsonDocument voption -> Task type GraphQLTransportWSOptions = { EndpointUrl : string ConnectionInitTimeoutInMs : int - CustomPingHandler : PingHandler option + CustomPingHandler : PingHandler voption } type IGraphQLOptions = diff --git a/src/FSharp.Data.GraphQL.Server.AspNetCore/GraphQLWebsocketMiddleware.fs b/src/FSharp.Data.GraphQL.Server.AspNetCore/GraphQLWebsocketMiddleware.fs index 5cc7239b2..3c6a9f834 100644 --- a/src/FSharp.Data.GraphQL.Server.AspNetCore/GraphQLWebsocketMiddleware.fs +++ b/src/FSharp.Data.GraphQL.Server.AspNetCore/GraphQLWebsocketMiddleware.fs @@ -10,6 +10,7 @@ open System.Threading.Tasks open Microsoft.AspNetCore.Http open Microsoft.Extensions.Hosting open Microsoft.Extensions.Logging +open Microsoft.Extensions.Options open FsToolkit.ErrorHandling open FSharp.Data.GraphQL @@ -22,18 +23,24 @@ type GraphQLWebSocketMiddleware<'Root> applicationLifetime : IHostApplicationLifetime, serviceProvider : IServiceProvider, logger : ILogger>, - options : GraphQLOptions<'Root> + options : IOptions> ) = + let options = options.Value + let serializerOptions = options.SerializerOptions + let pingHandler = options.WebsocketOptions.CustomPingHandler + let endpointUrl = PathString options.WebsocketOptions.EndpointUrl + let connectionInitTimeout = options.WebsocketOptions.ConnectionInitTimeoutInMs + let serializeServerMessage (jsonSerializerOptions : JsonSerializerOptions) (serverMessage : ServerMessage) = task { let raw = match serverMessage with - | ConnectionAck -> { Id = None; Type = "connection_ack"; Payload = None } - | ServerPing -> { Id = None; Type = "ping"; Payload = None } - | ServerPong p -> { Id = None; Type = "pong"; Payload = p |> Option.map CustomResponse } - | Next (id, payload) -> { Id = Some id; Type = "next"; Payload = Some <| ExecutionResult payload } - | Complete id -> { Id = Some id; Type = "complete"; Payload = None } - | Error (id, errMsgs) -> { Id = Some id; Type = "error"; Payload = Some <| ErrorMessages errMsgs } + | ConnectionAck -> { Id = ValueNone; Type = "connection_ack"; Payload = ValueNone } + | ServerPing -> { Id = ValueNone; Type = "ping"; Payload = ValueNone } + | 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 } return JsonSerializer.Serialize (raw, jsonSerializerOptions) } @@ -83,10 +90,10 @@ type GraphQLWebSocketMiddleware<'Root> |> Array.ofSeq |> System.Text.Encoding.UTF8.GetString if String.IsNullOrWhiteSpace message then - return None + return ValueNone else let! result = message |> deserializeClientMessage serializerOptions - return Some result + return ValueSome result } let sendMessageViaSocket (jsonSerializerOptions) (socket : WebSocket) (message : ServerMessage) : Task = task { @@ -137,15 +144,7 @@ type GraphQLWebSocketMiddleware<'Root> let tryToGracefullyCloseSocketWithDefaultBehavior = tryToGracefullyCloseSocket (WebSocketCloseStatus.NormalClosure, "Normal Closure") - let handleMessages - (cancellationToken : CancellationToken) - (httpContext : HttpContext) - (serializerOptions : JsonSerializerOptions) - (executor : Executor<'Root>) - (root : HttpContext -> 'Root) - (pingHandler : PingHandler option) - (socket : WebSocket) - = + let handleMessages (cancellationToken : CancellationToken) (httpContext : HttpContext) (socket : WebSocket) : Task = let subscriptions = new Dictionary () // ----------> // Helpers --> @@ -204,8 +203,8 @@ type GraphQLWebSocketMiddleware<'Root> let getStrAddendumOfOptionalPayload optionalPayload = optionalPayload - |> Option.map (fun payloadStr -> $" with payload: %A{payloadStr}") - |> Option.defaultWith (fun () -> "") + |> ValueOption.map (fun payloadStr -> $" with payload: %A{payloadStr}") + |> ValueOption.defaultWith (fun () -> "") let logMsgReceivedWithOptionalPayload optionalPayload (msgAsStr : string) = logger.LogTrace ("{message}{messageaddendum}", msgAsStr, (optionalPayload |> getStrAddendumOfOptionalPayload)) @@ -226,13 +225,13 @@ type GraphQLWebSocketMiddleware<'Root> let! receivedMessage = rcv () match receivedMessage with | Result.Error failureMsgs -> - "InvalidMessage" |> logMsgReceivedWithOptionalPayload None + "InvalidMessage" |> logMsgReceivedWithOptionalPayload ValueNone match failureMsgs with | InvalidMessage (code, explanation) -> do! socket.CloseAsync (enum code, explanation, CancellationToken.None) | Ok maybeMsg -> match maybeMsg with - | None -> logger.LogTrace ("Websocket socket received empty message! (socket state = {socketstate})", socket.State) - | Some msg -> + | ValueNone -> logger.LogTrace ("Websocket socket received empty message! (socket state = {socketstate})", socket.State) + | ValueSome msg -> match msg with | ConnectionInit p -> "ConnectionInit" |> logMsgReceivedWithOptionalPayload p @@ -245,10 +244,10 @@ type GraphQLWebSocketMiddleware<'Root> | ClientPing p -> "ClientPing" |> logMsgReceivedWithOptionalPayload p match pingHandler with - | Some func -> + | ValueSome func -> let! customP = p |> func serviceProvider do! ServerPong customP |> sendMsg - | None -> do! ServerPong p |> sendMsg + | ValueNone -> do! ServerPong p |> sendMsg | ClientPong p -> "ClientPong" |> logMsgReceivedWithOptionalPayload p | Subscribe (id, query) -> "Subscribe" |> logMsgWithIdReceived id @@ -262,7 +261,8 @@ type GraphQLWebSocketMiddleware<'Root> else let variables = query.Variables |> Skippable.toOption let! planExecutionResult = - executor.AsyncExecute (query.Query, root (httpContext), ?variables = variables) + let root = options.RootFactory httpContext + options.SchemaExecutor.AsyncExecute (query.Query, root, ?variables = variables) do! planExecutionResult |> applyPlanExecutionResult id socket | ClientComplete id -> "ClientComplete" |> logMsgWithIdReceived id @@ -282,14 +282,10 @@ type GraphQLWebSocketMiddleware<'Root> // <-- Main // <-------- - let waitForConnectionInitAndRespondToClient - (serializerOptions : JsonSerializerOptions) - (connectionInitTimeoutInMs : int) - (socket : WebSocket) - : TaskResult = - taskResult { + let waitForConnectionInitAndRespondToClient (socket : WebSocket) : TaskResult = + task { let timerTokenSource = new CancellationTokenSource () - timerTokenSource.CancelAfter (connectionInitTimeoutInMs) + timerTokenSource.CancelAfter connectionInitTimeout let detonationRegistration = timerTokenSource.Token.Register (fun _ -> socket @@ -302,14 +298,14 @@ type GraphQLWebSocketMiddleware<'Root> logger.LogDebug ("Waiting for ConnectionInit...") let! receivedMessage = receiveMessageViaSocket (CancellationToken.None) serializerOptions socket match receivedMessage with - | Ok (Some (ConnectionInit _)) -> + | Ok (ValueSome (ConnectionInit _)) -> logger.LogDebug ("Valid connection_init received! Responding with ACK!") detonationRegistration.Unregister () |> ignore do! ConnectionAck |> sendMessageViaSocket serializerOptions socket return true - | Ok (Some (Subscribe _)) -> + | Ok (ValueSome (Subscribe _)) -> do! socket |> tryToGracefullyCloseSocket (enum CustomWebSocketStatus.Unauthorized, "Unauthorized") @@ -327,46 +323,30 @@ type GraphQLWebSocketMiddleware<'Root> ) if (not timerTokenSource.Token.IsCancellationRequested) then if connectionInitSucceeded then - return () + return Ok () else - return! - Result.Error - <| "ConnectionInit failed (not because of timeout)" + return Result.Error ("ConnectionInit failed (not because of timeout)") else - return! Result.Error <| "ConnectionInit timeout" + return Result.Error <| "ConnectionInit timeout" } member __.InvokeAsync (ctx : HttpContext) = task { - if not (ctx.Request.Path = PathString (options.WebsocketOptions.EndpointUrl)) then + if not (ctx.Request.Path = endpointUrl) then do! next.Invoke (ctx) else if ctx.WebSockets.IsWebSocketRequest then use! socket = ctx.WebSockets.AcceptWebSocketAsync ("graphql-transport-ws") let! connectionInitResult = - socket - |> waitForConnectionInitAndRespondToClient options.SerializerOptions options.WebsocketOptions.ConnectionInitTimeoutInMs + socket |> waitForConnectionInitAndRespondToClient match connectionInitResult with - | Result.Error errMsg -> logger.LogWarning ("{warningmsg}", ($"%A{errMsg}")) + | Result.Error errMsg -> logger.LogWarning ("{warningmsg}", errMsg) | Ok _ -> let longRunningCancellationToken = (CancellationTokenSource .CreateLinkedTokenSource(ctx.RequestAborted, applicationLifetime.ApplicationStopping) .Token) - longRunningCancellationToken.Register (fun _ -> - socket - |> tryToGracefullyCloseSocketWithDefaultBehavior - |> Async.AwaitTask - |> Async.RunSynchronously) - |> ignore - let safe_HandleMessages = handleMessages longRunningCancellationToken + longRunningCancellationToken.Register (fun _ -> (socket |> tryToGracefullyCloseSocketWithDefaultBehavior).Wait()) |> ignore try - do! - socket - |> safe_HandleMessages - ctx - options.SerializerOptions - options.SchemaExecutor - options.RootFactory - options.WebsocketOptions.CustomPingHandler + do! socket |> handleMessages longRunningCancellationToken ctx with ex -> logger.LogError (ex, "Cannot handle Websocket message.") else diff --git a/src/FSharp.Data.GraphQL.Server.AspNetCore/Messages.fs b/src/FSharp.Data.GraphQL.Server.AspNetCore/Messages.fs index 82a649f50..93db7cd40 100644 --- a/src/FSharp.Data.GraphQL.Server.AspNetCore/Messages.fs +++ b/src/FSharp.Data.GraphQL.Server.AspNetCore/Messages.fs @@ -11,19 +11,19 @@ type SubscriptionUnsubscriber = IDisposable type OnUnsubscribeAction = SubscriptionId -> unit type SubscriptionsDict = IDictionary -type RawMessage = { Id : string option; Type : string; Payload : JsonDocument option } +type RawMessage = { Id : string voption; Type : string; Payload : JsonDocument voption } type ServerRawPayload = | ExecutionResult of Output | ErrorMessages of NameValueLookup list | CustomResponse of JsonDocument -type RawServerMessage = { Id : string option; Type : string; Payload : ServerRawPayload option } +type RawServerMessage = { Id : string voption; Type : string; Payload : ServerRawPayload voption } type ClientMessage = - | ConnectionInit of payload : JsonDocument option - | ClientPing of payload : JsonDocument option - | ClientPong of payload : JsonDocument option + | ConnectionInit of payload : JsonDocument voption + | ClientPing of payload : JsonDocument voption + | ClientPong of payload : JsonDocument voption | Subscribe of id : string * query : GQLRequestContent | ClientComplete of id : string @@ -32,7 +32,7 @@ type ClientMessageProtocolFailure = InvalidMessage of code : int * explanation : type ServerMessage = | ConnectionAck | ServerPing - | ServerPong of JsonDocument option + | ServerPong of JsonDocument voption | Next of id : string * payload : Output | Error of id : string * err : NameValueLookup list | Complete of id : string diff --git a/src/FSharp.Data.GraphQL.Server.AspNetCore/Serialization/JsonConverters.fs b/src/FSharp.Data.GraphQL.Server.AspNetCore/Serialization/JsonConverters.fs index cd2390945..36380b233 100644 --- a/src/FSharp.Data.GraphQL.Server.AspNetCore/Serialization/JsonConverters.fs +++ b/src/FSharp.Data.GraphQL.Server.AspNetCore/Serialization/JsonConverters.fs @@ -27,9 +27,9 @@ type ClientMessageConverter () = let getOptionalString (reader : byref) = if reader.TokenType.Equals (JsonTokenType.Null) then - None + ValueNone else - Some (reader.GetString ()) + ValueSome (reader.GetString ()) let readPropertyValueAsAString (propertyName : string) (reader : byref) = if reader.Read () then @@ -40,20 +40,20 @@ type ClientMessageConverter () = let requireId (raw : RawMessage) : Result = match raw.Id with - | Some s -> Ok s - | None -> + | ValueSome s -> Ok s + | ValueNone -> invalidMsg <| "Property \"id\" is required for this message but was not present." let requireSubscribePayload (serializerOptions : JsonSerializerOptions) - (payload : JsonDocument option) + (payload : JsonDocument voption) : Result = match payload with - | None -> + | ValueNone -> invalidMsg <| "Payload is required for this message, but none was present." - | Some p -> + | ValueSome p -> try JsonSerializer.Deserialize(p, serializerOptions) |> Ok with @@ -65,20 +65,20 @@ type ClientMessageConverter () = if not (reader.TokenType.Equals (JsonTokenType.StartObject)) then raise (new JsonException ($"reader's first token was not \"%A{JsonTokenType.StartObject}\", but \"%A{reader.TokenType}\"")) else - let mutable id : string option = None - let mutable theType : string option = None - let mutable payload : JsonDocument option = None + let mutable id : string voption = ValueNone + let mutable theType : string voption = ValueNone + let mutable payload : JsonDocument voption = ValueNone while reader.Read () && (not (reader.TokenType.Equals (JsonTokenType.EndObject))) do match reader.GetString () with | "id" -> id <- readPropertyValueAsAString "id" &reader | "type" -> theType <- readPropertyValueAsAString "type" &reader - | "payload" -> payload <- Some <| JsonDocument.ParseValue (&reader) + | "payload" -> payload <- ValueSome <| JsonDocument.ParseValue (&reader) | other -> raiseInvalidMsg <| $"Unknown property \"%s{other}\"" match theType with - | None -> raiseInvalidMsg "Property \"type\" is missing" - | Some msgType -> { Id = id; Type = msgType; Payload = payload } + | ValueNone -> raiseInvalidMsg "Property \"type\" is missing" + | ValueSome msgType -> { Id = id; Type = msgType; Payload = payload } override __.Read (reader : byref, typeToConvert : Type, options : JsonSerializerOptions) : ClientMessage = let raw = readRawMessage (&reader, options) @@ -119,12 +119,12 @@ type RawServerMessageConverter () = writer.WriteStartObject () writer.WriteString ("type", value.Type) match value.Id with - | None -> () - | Some id -> writer.WriteString ("id", id) + | ValueNone -> () + | ValueSome id -> writer.WriteString ("id", id) match value.Payload with - | None -> () - | Some serverRawPayload -> + | ValueNone -> () + | ValueSome serverRawPayload -> match serverRawPayload with | ExecutionResult output -> writer.WritePropertyName ("payload") diff --git a/src/FSharp.Data.GraphQL.Server.AspNetCore/StartupExtensions.fs b/src/FSharp.Data.GraphQL.Server.AspNetCore/StartupExtensions.fs index 9c3e98f6f..d7f73db41 100644 --- a/src/FSharp.Data.GraphQL.Server.AspNetCore/StartupExtensions.fs +++ b/src/FSharp.Data.GraphQL.Server.AspNetCore/StartupExtensions.fs @@ -7,6 +7,7 @@ open Microsoft.AspNetCore.Builder open Microsoft.AspNetCore.Http open Microsoft.Extensions.DependencyInjection open FSharp.Data.GraphQL +open Microsoft.Extensions.Options [] module ServiceCollectionExtensions = @@ -18,7 +19,7 @@ module ServiceCollectionExtensions = WebsocketOptions = { EndpointUrl = endpointUrl ConnectionInitTimeoutInMs = 3000 - CustomPingHandler = None + CustomPingHandler = ValueNone } } @@ -51,8 +52,18 @@ module ServiceCollectionExtensions = Json.configureDefaultSerializerOptions Seq.empty o.SerializerOptions ) ) - .AddSingleton>(options) - .AddSingleton>(fun sp -> sp.GetRequiredService>()) + .AddSingleton>>( + { new IOptionsFactory> with + member this.Create name = options + } + ) + .Configure>(Giraffe.HttpHandlers.IdentedOptionsName, (fun o -> o.SerializerOptions.WriteIndented <- true)) + .AddSingleton>(fun sp -> + { new IOptionsFactory with + member this.Create name = + sp.GetRequiredService>>().Get(name) + } + ) [] module ApplicationBuilderExtensions = diff --git a/tests/FSharp.Data.GraphQL.Tests/AspNetCore/SerializationTests.fs b/tests/FSharp.Data.GraphQL.Tests/AspNetCore/SerializationTests.fs index db0406049..05bcab916 100644 --- a/tests/FSharp.Data.GraphQL.Tests/AspNetCore/SerializationTests.fs +++ b/tests/FSharp.Data.GraphQL.Tests/AspNetCore/SerializationTests.fs @@ -17,7 +17,7 @@ let ``Deserializes ConnectionInit correctly`` () = let result = JsonSerializer.Deserialize (input, serializerOptions) match result with - | ConnectionInit None -> () // <-- expected + | ConnectionInit ValueNone -> () // <-- expected | other -> Assert.Fail ($"unexpected actual value: '%A{other}'") [] @@ -39,7 +39,7 @@ let ``Deserializes ClientPing correctly`` () = let result = JsonSerializer.Deserialize (input, serializerOptions) match result with - | ClientPing None -> () // <-- expected + | ClientPing ValueNone -> () // <-- expected | other -> Assert.Fail ($"unexpected actual value '%A{other}'") [] @@ -61,7 +61,7 @@ let ``Deserializes ClientPong correctly`` () = let result = JsonSerializer.Deserialize (input, serializerOptions) match result with - | ClientPong None -> () // <-- expected + | ClientPong ValueNone -> () // <-- expected | other -> Assert.Fail ($"unexpected actual value: '%A{other}'") []