348 lines
14 KiB
C#
348 lines
14 KiB
C#
// Copyright (c) .NET Foundation. All rights reserved.
|
|
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
|
|
|
|
using System;
|
|
using System.Collections.Generic;
|
|
using System.IO;
|
|
using System.Runtime.ExceptionServices;
|
|
using Microsoft.AspNetCore.SignalR.Internal.Formatters;
|
|
using Newtonsoft.Json;
|
|
using Newtonsoft.Json.Linq;
|
|
using Newtonsoft.Json.Serialization;
|
|
|
|
namespace Microsoft.AspNetCore.SignalR.Internal.Protocol
|
|
{
|
|
public class JsonHubProtocol : IHubProtocol
|
|
{
|
|
private const string ResultPropertyName = "result";
|
|
private const string ItemPropertyName = "item";
|
|
private const string InvocationIdPropertyName = "invocationId";
|
|
private const string TypePropertyName = "type";
|
|
private const string ErrorPropertyName = "error";
|
|
private const string TargetPropertyName = "target";
|
|
private const string NonBlockingPropertyName = "nonBlocking";
|
|
private const string ArgumentsPropertyName = "arguments";
|
|
|
|
private const int InvocationMessageType = 1;
|
|
private const int ResultMessageType = 2;
|
|
private const int CompletionMessageType = 3;
|
|
private const int StreamInvocationMessageType = 4;
|
|
private const int CancelInvocationMessageType = 5;
|
|
|
|
// ONLY to be used for application payloads (args, return values, etc.)
|
|
private JsonSerializer _payloadSerializer;
|
|
|
|
/// <summary>
|
|
/// Creates an instance of the <see cref="JsonHubProtocol"/> using the default <see cref="JsonSerializer"/>
|
|
/// to serialize application payloads (arguments, results, etc.). The serialization of the outer protocol can
|
|
/// NOT be changed using this serializer.
|
|
/// </summary>
|
|
public JsonHubProtocol()
|
|
: this(JsonSerializer.Create(CreateDefaultSerializerSettings()))
|
|
{ }
|
|
|
|
/// <summary>
|
|
/// Creates an instance of the <see cref="JsonHubProtocol"/> using the specified <see cref="JsonSerializer"/>
|
|
/// to serialize application payloads (arguments, results, etc.). The serialization of the outer protocol can
|
|
/// NOT be changed using this serializer.
|
|
/// </summary>
|
|
/// <param name="payloadSerializer">The <see cref="JsonSerializer"/> to use to serialize application payloads (arguments, results, etc.).</param>
|
|
public JsonHubProtocol(JsonSerializer payloadSerializer)
|
|
{
|
|
if (payloadSerializer == null)
|
|
{
|
|
throw new ArgumentNullException(nameof(payloadSerializer));
|
|
}
|
|
|
|
_payloadSerializer = payloadSerializer;
|
|
}
|
|
|
|
public string Name => "json";
|
|
|
|
public ProtocolType Type => ProtocolType.Text;
|
|
|
|
public bool TryParseMessages(ReadOnlyMemory<byte> input, IInvocationBinder binder, out IList<HubMessage> messages)
|
|
{
|
|
messages = new List<HubMessage>();
|
|
|
|
while (TextMessageParser.TryParseMessage(ref input, out var payload))
|
|
{
|
|
// TODO: Need a span-native JSON parser!
|
|
using (var memoryStream = new MemoryStream(payload.ToArray()))
|
|
{
|
|
messages.Add(ParseMessage(memoryStream, binder));
|
|
}
|
|
}
|
|
|
|
return messages.Count > 0;
|
|
}
|
|
|
|
public void WriteMessage(HubMessage message, Stream output)
|
|
{
|
|
using (var memoryStream = new MemoryStream())
|
|
{
|
|
WriteMessageCore(message, memoryStream);
|
|
memoryStream.Flush();
|
|
|
|
TextMessageFormatter.WriteMessage(memoryStream.ToArray(), output);
|
|
}
|
|
}
|
|
|
|
private HubMessage ParseMessage(Stream input, IInvocationBinder binder)
|
|
{
|
|
using (var reader = new JsonTextReader(new StreamReader(input)))
|
|
{
|
|
try
|
|
{
|
|
// PERF: Could probably use the JsonTextReader directly for better perf and fewer allocations
|
|
var token = JToken.ReadFrom(reader);
|
|
|
|
if (token == null || token.Type != JTokenType.Object)
|
|
{
|
|
throw new FormatException($"Unexpected JSON Token Type '{token?.Type}'. Expected a JSON Object.");
|
|
}
|
|
|
|
var json = (JObject)token;
|
|
|
|
// Determine the type of the message
|
|
var type = JsonUtils.GetRequiredProperty<int>(json, TypePropertyName, JTokenType.Integer);
|
|
switch (type)
|
|
{
|
|
case InvocationMessageType:
|
|
return BindInvocationMessage(json, binder);
|
|
case StreamInvocationMessageType:
|
|
return BindStreamInvocationMessage(json, binder);
|
|
case ResultMessageType:
|
|
return BindResultMessage(json, binder);
|
|
case CompletionMessageType:
|
|
return BindCompletionMessage(json, binder);
|
|
case CancelInvocationMessageType:
|
|
return BindCancelInvocationMessage(json);
|
|
default:
|
|
throw new FormatException($"Unknown message type: {type}");
|
|
}
|
|
}
|
|
catch (JsonReaderException jrex)
|
|
{
|
|
throw new FormatException("Error reading JSON.", jrex);
|
|
}
|
|
}
|
|
}
|
|
|
|
private void WriteMessageCore(HubMessage message, Stream stream)
|
|
{
|
|
using (var writer = new JsonTextWriter(new StreamWriter(stream)))
|
|
{
|
|
switch (message)
|
|
{
|
|
case InvocationMessage m:
|
|
WriteInvocationMessage(m, writer);
|
|
break;
|
|
case StreamInvocationMessage m:
|
|
WriteStreamInvocationMessage(m, writer);
|
|
break;
|
|
case StreamItemMessage m:
|
|
WriteStreamItemMessage(m, writer);
|
|
break;
|
|
case CompletionMessage m:
|
|
WriteCompletionMessage(m, writer);
|
|
break;
|
|
case CancelInvocationMessage m:
|
|
WriteCancelInvocationMessage(m, writer);
|
|
break;
|
|
default:
|
|
throw new InvalidOperationException($"Unsupported message type: {message.GetType().FullName}");
|
|
}
|
|
}
|
|
}
|
|
|
|
private void WriteCompletionMessage(CompletionMessage message, JsonTextWriter writer)
|
|
{
|
|
writer.WriteStartObject();
|
|
WriteHubMessageCommon(message, writer, CompletionMessageType);
|
|
if (!string.IsNullOrEmpty(message.Error))
|
|
{
|
|
writer.WritePropertyName(ErrorPropertyName);
|
|
writer.WriteValue(message.Error);
|
|
}
|
|
else if (message.HasResult)
|
|
{
|
|
writer.WritePropertyName(ResultPropertyName);
|
|
_payloadSerializer.Serialize(writer, message.Result);
|
|
}
|
|
writer.WriteEndObject();
|
|
}
|
|
|
|
private void WriteCancelInvocationMessage(CancelInvocationMessage message, JsonTextWriter writer)
|
|
{
|
|
writer.WriteStartObject();
|
|
WriteHubMessageCommon(message, writer, CancelInvocationMessageType);
|
|
writer.WriteEndObject();
|
|
}
|
|
|
|
private void WriteStreamItemMessage(StreamItemMessage message, JsonTextWriter writer)
|
|
{
|
|
writer.WriteStartObject();
|
|
WriteHubMessageCommon(message, writer, ResultMessageType);
|
|
writer.WritePropertyName(ItemPropertyName);
|
|
_payloadSerializer.Serialize(writer, message.Item);
|
|
writer.WriteEndObject();
|
|
}
|
|
|
|
private void WriteInvocationMessage(InvocationMessage message, JsonTextWriter writer)
|
|
{
|
|
writer.WriteStartObject();
|
|
WriteHubMessageCommon(message, writer, InvocationMessageType);
|
|
writer.WritePropertyName(TargetPropertyName);
|
|
writer.WriteValue(message.Target);
|
|
|
|
if (message.NonBlocking)
|
|
{
|
|
writer.WritePropertyName(NonBlockingPropertyName);
|
|
writer.WriteValue(message.NonBlocking);
|
|
}
|
|
|
|
WriteArguments(message.Arguments, writer);
|
|
|
|
writer.WriteEndObject();
|
|
}
|
|
|
|
private void WriteStreamInvocationMessage(StreamInvocationMessage message, JsonTextWriter writer)
|
|
{
|
|
writer.WriteStartObject();
|
|
WriteHubMessageCommon(message, writer, StreamInvocationMessageType);
|
|
writer.WritePropertyName(TargetPropertyName);
|
|
writer.WriteValue(message.Target);
|
|
|
|
WriteArguments(message.Arguments, writer);
|
|
|
|
writer.WriteEndObject();
|
|
}
|
|
|
|
private void WriteArguments(object[] arguments, JsonTextWriter writer)
|
|
{
|
|
writer.WritePropertyName(ArgumentsPropertyName);
|
|
writer.WriteStartArray();
|
|
foreach (var argument in arguments)
|
|
{
|
|
_payloadSerializer.Serialize(writer, argument);
|
|
}
|
|
writer.WriteEndArray();
|
|
}
|
|
|
|
private static void WriteHubMessageCommon(HubMessage message, JsonTextWriter writer, int type)
|
|
{
|
|
writer.WritePropertyName(InvocationIdPropertyName);
|
|
writer.WriteValue(message.InvocationId);
|
|
writer.WritePropertyName(TypePropertyName);
|
|
writer.WriteValue(type);
|
|
}
|
|
|
|
private InvocationMessage BindInvocationMessage(JObject json, IInvocationBinder binder)
|
|
{
|
|
var invocationId = JsonUtils.GetRequiredProperty<string>(json, InvocationIdPropertyName, JTokenType.String);
|
|
var target = JsonUtils.GetRequiredProperty<string>(json, TargetPropertyName, JTokenType.String);
|
|
var nonBlocking = JsonUtils.GetOptionalProperty<bool>(json, NonBlockingPropertyName, JTokenType.Boolean);
|
|
|
|
var args = JsonUtils.GetRequiredProperty<JArray>(json, ArgumentsPropertyName, JTokenType.Array);
|
|
|
|
var paramTypes = binder.GetParameterTypes(target);
|
|
|
|
try
|
|
{
|
|
var arguments = BindArguments(args, paramTypes);
|
|
return new InvocationMessage(invocationId, nonBlocking, target, argumentBindingException: null, arguments: arguments);
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
return new InvocationMessage(invocationId, nonBlocking, target, ExceptionDispatchInfo.Capture(ex));
|
|
}
|
|
}
|
|
|
|
private StreamInvocationMessage BindStreamInvocationMessage(JObject json, IInvocationBinder binder)
|
|
{
|
|
var invocationId = JsonUtils.GetRequiredProperty<string>(json, InvocationIdPropertyName, JTokenType.String);
|
|
var target = JsonUtils.GetRequiredProperty<string>(json, TargetPropertyName, JTokenType.String);
|
|
|
|
var args = JsonUtils.GetRequiredProperty<JArray>(json, ArgumentsPropertyName, JTokenType.Array);
|
|
|
|
var paramTypes = binder.GetParameterTypes(target);
|
|
|
|
try
|
|
{
|
|
var arguments = BindArguments(args, paramTypes);
|
|
return new StreamInvocationMessage(invocationId, target, argumentBindingException: null, arguments: arguments);
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
return new StreamInvocationMessage(invocationId, target, ExceptionDispatchInfo.Capture(ex));
|
|
}
|
|
}
|
|
|
|
private object[] BindArguments(JArray args, Type[] paramTypes)
|
|
{
|
|
var arguments = new object[args.Count];
|
|
if (paramTypes.Length != arguments.Length)
|
|
{
|
|
throw new FormatException($"Invocation provides {arguments.Length} argument(s) but target expects {paramTypes.Length}.");
|
|
}
|
|
|
|
try
|
|
{
|
|
for (var i = 0; i < paramTypes.Length; i++)
|
|
{
|
|
var paramType = paramTypes[i];
|
|
arguments[i] = args[i].ToObject(paramType, _payloadSerializer);
|
|
}
|
|
|
|
return arguments;
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
throw new FormatException("Error binding arguments. Make sure that the types of the provided values match the types of the hub method being invoked.", ex);
|
|
}
|
|
}
|
|
|
|
private StreamItemMessage BindResultMessage(JObject json, IInvocationBinder binder)
|
|
{
|
|
var invocationId = JsonUtils.GetRequiredProperty<string>(json, InvocationIdPropertyName, JTokenType.String);
|
|
var result = JsonUtils.GetRequiredProperty<JToken>(json, ItemPropertyName);
|
|
|
|
var returnType = binder.GetReturnType(invocationId);
|
|
return new StreamItemMessage(invocationId, result?.ToObject(returnType, _payloadSerializer));
|
|
}
|
|
|
|
private CompletionMessage BindCompletionMessage(JObject json, IInvocationBinder binder)
|
|
{
|
|
var invocationId = JsonUtils.GetRequiredProperty<string>(json, InvocationIdPropertyName, JTokenType.String);
|
|
var error = JsonUtils.GetOptionalProperty<string>(json, ErrorPropertyName, JTokenType.String);
|
|
var resultProp = json.Property(ResultPropertyName);
|
|
|
|
if (error != null && resultProp != null)
|
|
{
|
|
throw new FormatException("The 'error' and 'result' properties are mutually exclusive.");
|
|
}
|
|
|
|
if (resultProp == null)
|
|
{
|
|
return new CompletionMessage(invocationId, error, result: null, hasResult: false);
|
|
}
|
|
|
|
var returnType = binder.GetReturnType(invocationId);
|
|
var payload = resultProp.Value?.ToObject(returnType, _payloadSerializer);
|
|
return new CompletionMessage(invocationId, error, result: payload, hasResult: true);
|
|
}
|
|
|
|
private CancelInvocationMessage BindCancelInvocationMessage(JObject json)
|
|
{
|
|
var invocationId = JsonUtils.GetRequiredProperty<string>(json, InvocationIdPropertyName, JTokenType.String);
|
|
return new CancelInvocationMessage(invocationId);
|
|
}
|
|
|
|
public static JsonSerializerSettings CreateDefaultSerializerSettings()
|
|
{
|
|
return new JsonSerializerSettings { ContractResolver = new CamelCasePropertyNamesContractResolver() };
|
|
}
|
|
}
|
|
}
|