AWS Appsync implementation using GraphQL-client library in .Net

一个人想着一个人 提交于 2021-01-27 19:43:33

问题


I am trying to implement an app sync subscription similar to this python example but in .net https://aws.amazon.com/blogs/mobile/appsync-websockets-python/

I started this using the nuget package GraphQL.Client https://www.nuget.org/packages/GraphQL.Client The execution of Query/Mutation is working fine like given in the readme of https://github.com/graphql-dotnet/graphql-client But subscription is not working.

My code using the GraphQL.Client:

using var graphQLClient = new GraphQLHttpClient("https://<MY-API-PATH>.appsync-realtime-api.<AWS-region>.amazonaws.com/graphql", new NewtonsoftJsonSerializer());

 graphQLClient.HttpClient.DefaultRequestHeaders.Add("host", "<API HOST without https or absolute path and 'realtime-' text in the api address>"); //As given in the python example

graphQLClient.HttpClient.DefaultRequestHeaders.Add("x-api-key", "<API KEY>");
var req= new GraphQLRequest
{
    Query = @"subscription SubscribeToEventComments{ subscribeToEventComments(eventId: 'test'){  content }}",
    Variables = new{}
};

IObservable<GraphQLResponse<Response>> subscriptionStream = graphQLClient.CreateSubscriptionStream<Response>(req, (Exception ex) =>
{
      Console.WriteLine("Error: {0}", ex.ToString());
});

var subscription = subscriptionStream.Subscribe(response =>
{
                Console.WriteLine($"Response'{Newtonsoft.Json.JsonConvert.SerializeObject(response)}' ");
},
ex =>
{
Console.WriteLine("Error{0}", ex.ToString());
});

Its giving the exception "The remote party closed the WebSocket connection without completing the close handshake."

stack trace:

at System.Net.WebSockets.ManagedWebSocket.d__662.MoveNext() at System.Runtime.ExceptionServices.ExceptionDispatchInfo.Throw() at System.Runtime.CompilerServices.TaskAwaiter.ThrowForNonSuccess(Task task) at System.Runtime.CompilerServices.TaskAwaiter.HandleNonSuccessAndDebuggerNotification(Task task) at System.Runtime.CompilerServices.TaskAwaiter1.GetResult() at GraphQL.Client.Http.Websocket.GraphQLHttpWebSocket.d__40.MoveNext() in C:\Users\UserName\Source\repos\graphql-client\src\GraphQL.Client\Websocket\GraphQLHttpWebSocket.cs:line 546

Then I tried without this nuget and using standard websocket

Code without nuget:

static public async Task CallWebsocket()
        {
            try
            {
                _client = new ClientWebSocket();
                _client.Options.AddSubProtocol("graphql-ws");
                _client.Options.SetRequestHeader("host", "<HOST URL without wss but now with 'realtime' text in api url because otherwise we are getting SSL error>");
                _client.Options.SetRequestHeader("x-api-key", "<API KEY>");

                await _client.ConnectAsync(new Uri("https://<MY-APPSYNC_API_PATH>.appsync-realtime-api.<AWS-region>.amazonaws.com/graphql"), CancellationToken.None);
                await SendCommand();
                var docList = await Receive();
            }
            catch(Exception ex)
            {

            }
        }

       static  private async Task SendCommand()
        {
            ArraySegment<byte> outputBuffer = new ArraySegment<byte>(Encoding.UTF8.GetBytes("'query' : 'subscription SubscribeToEventComments{ subscribeToEventComments(eventId: 'test'){  content }}'"));
            await _client.SendAsync(outputBuffer, WebSocketMessageType.Text, true, CancellationToken.None);
        }
        static private async Task<string> Receive()
        {
            var receiveBufferSize = 1536;
            byte[] buffer = new byte[receiveBufferSize];
            var result = await _client.ReceiveAsync(new ArraySegment<byte>(buffer), CancellationToken.None);
            var resultJson = (new UTF8Encoding()).GetString(buffer);
            return resultJson;
        }

I am getting below exception:

Inner exception: "An established connection was aborted by the software in your host machine."

Inner exception message: "Unable to read data from the transport connection: An established connection was aborted by the software in your host machine.."

Message: "The remote party closed the WebSocket connection without completing the close handshake."

Could anyone please help with the correct implementation.


回答1:


Nuget won't work out of the box with AppSync subscriptions, so you will need to write your own client code for that, like you attempted in the second (non-nuget) example.

Now, for the second example, take a second look at the python example referenced in your question. There are several steps that are not included in your code. I will enumerate the required steps and try to port them to C# from the python code (note that I don't have a C# environment at hand so there might be syntax errors, but this code should be pretty close to what you need)

Step 0 - AppSync Endpoints

Assume the result of invoking aws appsync get-graphql-api --api-id example123456 for your API is:

{
    "graphqlApi": {
        "name": "myNewRealTimeGraphQL-API",
        "authenticationType": "<API_KEY>",
        "tags": {},
        "apiId": "example123456",
        "uris": {
            "GRAPHQL": "https://abc.appsync-api.us-west-2.amazonaws.com/graphql",
            "REALTIME": "wss://abc.appsync-realtime-api.us-west-2.amazonaws.com/graphql"
        },
        "arn": "arn:aws:appsync:us-west-2: xxxxxxxxxxxx:apis/xxxxxxxxxxxx"
    }
}

Step 1 - Build the connection URL

Step 2 - Connect to WebSocket Endpoint

This includes sending a connection_init message as per the protocol mentioned in the python article

Step 3 - Wait for connection_ack as per protocol

Again, this is as per protocol

Step 4 - Register subscription

Step 5 - Send mutation

This step is not in this response, but can be done through the AWS console

Step 6 - Wait for "data" messages

These are the real-time events sent by AppSync

Step 7 - Deregister subscription

Step 8 - Disconnect

// These are declared at the same level as your _client

// This comes from the graphqlApi.uris.GRAPHQL in step 0, set as a var here for clarity
_gqlHost  = "abc.appsync-api.us-west-2.amazonaws.com";

// This comes from the graphqlApi.uris.REALTIME in step 0, set as a var here for clarity
_realtimeUri = "wss://abc.appsync-realtime-api.us-west-2.amazonaws.com/graphql";

_apiKey = "<API KEY>";

static public async Task CallWebsocket()
{
    
    // Step 1
    // This is JSON needed by the server, it will be converted to base64
    // (note: might be better to use something like Json.NET for this task)
    var header = var test = $@"{{
        ""host"":""{_gqlHost}"",
        ""x-api-key"": ""{_apiKey}""
    }}";

    // Now we need to encode the previous JSON to base64
    var headerB64 = System.Convert.ToBase64String(
        System.Text.Encoding.UTF8.GetBytes(header));

    UriBuilder connectionUriBuilder = new UriBuilder(_realtimeUri);
    connectionUriBuilder.Query = $"header={headerB64}&payload=e30=";
    
    try
    {
        _client = new ClientWebSocket();
        _client.Options.AddSubProtocol("graphql-ws");

        // Step 2
        await _client.ConnectAsync(connectionUriBuilder.Uri), CancellationToken.None);
        // Step 3
        await SendConnectionInit();
        await Receive();
    }
    catch(Exception ex)
    {

    }
}

static  private async Task SendConnectionInit()
{
    ArraySegment<byte> outputBuffer = new ArraySegment<byte>(Encoding.UTF8.GetBytes(@"{""type"": ""connection_init""}"));
    await _client.SendAsync(outputBuffer, WebSocketMessageType.Text, true, CancellationToken.None);
}

static  private async Task SendSubscription()
{
    // This detail is important, note that the subscription is a stringified JSON that will be embeded in the "data" field below
    var subscription = $@"{{\""query\"": \""subscription SubscribeToEventComments{{ subscribeToEventComments{{ content }} }}\"", \""variables\"": {{}} }}";
    
    var register = $@"{{
            ""id"": ""<SUB_ID>"",
            ""payload"": {{
                ""data"": ""{subscription}"",
                ""extensions"": {{
                    ""authorization"": {{
                        ""host"": ""{_gqlHost}"",
                        ""x-api-key"":""{_apiKey}""
                    }}
                }}
            }},
            ""type"": ""start""
        }}";
        
    // The output should look like below, note again the "data" field contains a stringified JSON that represents the subscription 
    /*
    {
        "id": "<SUB_ID>",
        "payload": {
            "data": "{\"query\": \"subscription SubscribeToEventComments{ subscribeToEventComments{ content}}\", \"variables\": {} }",
            "extensions": {
                "authorization": {
                    "host": "abc.appsync-api.us-west-2.amazonaws.com",
                    "x-api-key":"<API KEY>"
                }
            }
        },
        "type": "start"
    }
    */

    ArraySegment<byte> outputBuffer = new ArraySegment<byte>(Encoding.UTF8.GetBytes(register));
    await _client.SendAsync(outputBuffer, WebSocketMessageType.Text, true, CancellationToken.None);
}

static  private async Task Deregister()
{
    var deregister = $@"{{
                            ""type"": ""stop"",
                            ""id"": ""<SUB_ID>""
                        }}"
    ArraySegment<byte> outputBuffer = new ArraySegment<byte>(Encoding.UTF8.GetBytes(deregister));
    await _client.SendAsync(outputBuffer, WebSocketMessageType.Text, true, CancellationToken.None);
}

static private async Task Receive()
{
    while (_socket.State == WebSocketState.Open)
    {
        ArraySegment<Byte> buffer = new ArraySegment<byte>(new Byte[8192]);
        WebSocketReceiveResult result= null;
        using (var ms = new MemoryStream())
        {
            // This loop is needed because the server might send chunks of data that need to be assembled by the client
            // see: https://stackoverflow.com/questions/23773407/a-websockets-receiveasync-method-does-not-await-the-entire-message
            do
            {
                result = await socket.ReceiveAsync(buffer, CancellationToken.None);
                ms.Write(buffer.Array, buffer.Offset, result.Count);
            }
            while (!result.EndOfMessage);

            ms.Seek(0, SeekOrigin.Begin);

            using (var reader = new StreamReader(ms, Encoding.UTF8))
            {
                // convert stream to string
                var message = reader.ReadToEnd();
                Console.WriteLine(message)
                // quick and dirty way to check response
                if (message.Contains("connection_ack"))
                {
                    // Step 4
                    await SendSubscription();
                } else if (message.Contains("data"))  // Step 6
                {
                    // Step 7 
                    await Deregister();
                    // Step 8
                    await _client.CloseOutputAsync(WebSocketCloseStatus.NormalClosure, string.Empty, CancellationToken.None);
                }
            }
        }
    }
}




回答2:


For others who face same issues, I have created a nuget package now. https://www.nuget.org/packages/DotNetCSharp.AWS.AppSync.Client/1.1.1 You can use it as below.

//Create Client Specify eithen APIKey or AuthToken
var Client = new AppSyncClient("<Appsync URL>", new AuthOptions()
{
// APIKey = "<API Key>",
AuthToken = "<JWT Token>"
});

//To Subscribe an query
Guid newId = Guid.NewGuid();
await Client.CreateSubscriptionAsync<Message>(new QueryOptions()
{
Query = "subscription <Subscription Query>",
SubscriptionId = newId
},
(data) =>
{

});

//To unsubscribe an subscription
await Client.UnSubscribe(newId);

//To close the websocket
await Client.Close();


来源:https://stackoverflow.com/questions/62111313/aws-appsync-implementation-using-graphql-client-library-in-net

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!