Merge branch 'release/2.1' into dev
This commit is contained in:
commit
52befc6bc0
|
|
@ -52,7 +52,7 @@ namespace Microsoft.AspNetCore.SignalR.Microbenchmarks
|
||||||
public string Name { get; }
|
public string Name { get; }
|
||||||
public TransferFormat TransferFormat { get; }
|
public TransferFormat TransferFormat { get; }
|
||||||
|
|
||||||
public bool TryParseMessages(ReadOnlySpan<byte> input, IInvocationBinder binder, IList<HubMessage> messages)
|
public bool TryParseMessages(ReadOnlyMemory<byte> input, IInvocationBinder binder, IList<HubMessage> messages)
|
||||||
{
|
{
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -40,7 +40,7 @@ namespace Microsoft.AspNetCore.SignalR.Microbenchmarks
|
||||||
[Benchmark]
|
[Benchmark]
|
||||||
public void SingleBinaryMessage()
|
public void SingleBinaryMessage()
|
||||||
{
|
{
|
||||||
ReadOnlySpan<byte> buffer = _binaryInput;
|
ReadOnlyMemory<byte> buffer = _binaryInput;
|
||||||
if (!BinaryMessageParser.TryParseMessage(ref buffer, out _))
|
if (!BinaryMessageParser.TryParseMessage(ref buffer, out _))
|
||||||
{
|
{
|
||||||
throw new InvalidOperationException("Failed to parse");
|
throw new InvalidOperationException("Failed to parse");
|
||||||
|
|
@ -50,7 +50,7 @@ namespace Microsoft.AspNetCore.SignalR.Microbenchmarks
|
||||||
[Benchmark]
|
[Benchmark]
|
||||||
public void SingleTextMessage()
|
public void SingleTextMessage()
|
||||||
{
|
{
|
||||||
ReadOnlySpan<byte> buffer = _textInput;
|
ReadOnlyMemory<byte> buffer = _textInput;
|
||||||
if (!TextMessageParser.TryParseMessage(ref buffer, out _))
|
if (!TextMessageParser.TryParseMessage(ref buffer, out _))
|
||||||
{
|
{
|
||||||
throw new InvalidOperationException("Failed to parse");
|
throw new InvalidOperationException("Failed to parse");
|
||||||
|
|
|
||||||
|
|
@ -9,7 +9,7 @@ namespace Microsoft.AspNetCore.SignalR.Internal.Formatters
|
||||||
{
|
{
|
||||||
private const int MaxLengthPrefixSize = 5;
|
private const int MaxLengthPrefixSize = 5;
|
||||||
|
|
||||||
public static bool TryParseMessage(ref ReadOnlySpan<byte> buffer, out ReadOnlySpan<byte> payload)
|
public static bool TryParseMessage(ref ReadOnlyMemory<byte> buffer, out ReadOnlyMemory<byte> payload)
|
||||||
{
|
{
|
||||||
if (buffer.IsEmpty)
|
if (buffer.IsEmpty)
|
||||||
{
|
{
|
||||||
|
|
@ -33,10 +33,12 @@ namespace Microsoft.AspNetCore.SignalR.Internal.Formatters
|
||||||
var numBytes = 0;
|
var numBytes = 0;
|
||||||
|
|
||||||
var lengthPrefixBuffer = buffer.Slice(0, Math.Min(MaxLengthPrefixSize, buffer.Length));
|
var lengthPrefixBuffer = buffer.Slice(0, Math.Min(MaxLengthPrefixSize, buffer.Length));
|
||||||
|
var span = lengthPrefixBuffer.Span;
|
||||||
|
|
||||||
byte byteRead;
|
byte byteRead;
|
||||||
do
|
do
|
||||||
{
|
{
|
||||||
byteRead = lengthPrefixBuffer[numBytes];
|
byteRead = span[numBytes];
|
||||||
length = length | (((uint)(byteRead & 0x7f)) << (numBytes * 7));
|
length = length | (((uint)(byteRead & 0x7f)) << (numBytes * 7));
|
||||||
numBytes++;
|
numBytes++;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -7,9 +7,9 @@ namespace Microsoft.AspNetCore.SignalR.Internal.Formatters
|
||||||
{
|
{
|
||||||
public static class TextMessageParser
|
public static class TextMessageParser
|
||||||
{
|
{
|
||||||
public static bool TryParseMessage(ref ReadOnlySpan<byte> buffer, out ReadOnlySpan<byte> payload)
|
public static bool TryParseMessage(ref ReadOnlyMemory<byte> buffer, out ReadOnlyMemory<byte> payload)
|
||||||
{
|
{
|
||||||
var index = buffer.IndexOf(TextMessageFormatter.RecordSeparator);
|
var index = buffer.Span.IndexOf(TextMessageFormatter.RecordSeparator);
|
||||||
if (index == -1)
|
if (index == -1)
|
||||||
{
|
{
|
||||||
payload = default;
|
payload = default;
|
||||||
|
|
|
||||||
|
|
@ -7,15 +7,7 @@ namespace Microsoft.AspNetCore.SignalR.Internal.Protocol
|
||||||
{
|
{
|
||||||
public abstract class HubInvocationMessage : HubMessage
|
public abstract class HubInvocationMessage : HubMessage
|
||||||
{
|
{
|
||||||
private Dictionary<string, string> _headers;
|
public IDictionary<string, string> Headers { get; set; }
|
||||||
|
|
||||||
public IDictionary<string, string> Headers
|
|
||||||
{
|
|
||||||
get
|
|
||||||
{
|
|
||||||
return _headers ?? (_headers = new Dictionary<string, string>());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public string InvocationId { get; }
|
public string InvocationId { get; }
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -14,7 +14,7 @@ namespace Microsoft.AspNetCore.SignalR.Internal.Protocol
|
||||||
|
|
||||||
TransferFormat TransferFormat { get; }
|
TransferFormat TransferFormat { get; }
|
||||||
|
|
||||||
bool TryParseMessages(ReadOnlySpan<byte> input, IInvocationBinder binder, IList<HubMessage> messages);
|
bool TryParseMessages(ReadOnlyMemory<byte> input, IInvocationBinder binder, IList<HubMessage> messages);
|
||||||
|
|
||||||
void WriteMessage(HubMessage message, Stream output);
|
void WriteMessage(HubMessage message, Stream output);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,31 @@
|
||||||
|
// 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.Buffers;
|
||||||
|
using Newtonsoft.Json;
|
||||||
|
|
||||||
|
namespace Microsoft.AspNetCore.SignalR.Internal.Protocol
|
||||||
|
{
|
||||||
|
internal class JsonArrayPool<T> : IArrayPool<T>
|
||||||
|
{
|
||||||
|
private readonly ArrayPool<T> _inner;
|
||||||
|
|
||||||
|
internal static readonly JsonArrayPool<T> Shared = new JsonArrayPool<T>(ArrayPool<T>.Shared);
|
||||||
|
|
||||||
|
public JsonArrayPool(ArrayPool<T> inner)
|
||||||
|
{
|
||||||
|
_inner = inner;
|
||||||
|
}
|
||||||
|
|
||||||
|
public T[] Rent(int minimumLength)
|
||||||
|
{
|
||||||
|
return _inner.Rent(minimumLength);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Return(T[] array)
|
||||||
|
{
|
||||||
|
_inner.Return(array);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -47,15 +47,12 @@ namespace Microsoft.AspNetCore.SignalR.Internal.Protocol
|
||||||
|
|
||||||
public TransferFormat TransferFormat => TransferFormat.Text;
|
public TransferFormat TransferFormat => TransferFormat.Text;
|
||||||
|
|
||||||
public bool TryParseMessages(ReadOnlySpan<byte> input, IInvocationBinder binder, IList<HubMessage> messages)
|
public bool TryParseMessages(ReadOnlyMemory<byte> input, IInvocationBinder binder, IList<HubMessage> messages)
|
||||||
{
|
{
|
||||||
while (TextMessageParser.TryParseMessage(ref input, out var payload))
|
while (TextMessageParser.TryParseMessage(ref input, out var payload))
|
||||||
{
|
{
|
||||||
// TODO: Need a span-native JSON parser!
|
var textReader = new Utf8BufferTextReader(payload);
|
||||||
using (var memoryStream = new MemoryStream(payload.ToArray()))
|
messages.Add(ParseMessage(textReader, binder));
|
||||||
{
|
|
||||||
messages.Add(ParseMessage(memoryStream, binder));
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return messages.Count > 0;
|
return messages.Count > 0;
|
||||||
|
|
@ -67,69 +64,264 @@ namespace Microsoft.AspNetCore.SignalR.Internal.Protocol
|
||||||
TextMessageFormatter.WriteRecordSeparator(output);
|
TextMessageFormatter.WriteRecordSeparator(output);
|
||||||
}
|
}
|
||||||
|
|
||||||
private HubMessage ParseMessage(Stream input, IInvocationBinder binder)
|
private HubMessage ParseMessage(TextReader textReader, IInvocationBinder binder)
|
||||||
{
|
|
||||||
using (var reader = new JsonTextReader(new StreamReader(input)))
|
|
||||||
{
|
{
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
// PERF: Could probably use the JsonTextReader directly for better perf and fewer allocations
|
// We parse using the JsonTextReader directly but this has a problem. Some of our properties are dependent on other properties
|
||||||
var token = JToken.ReadFrom(reader);
|
// and since reading the json might be unordered, we need to store the parsed content as JToken to re-parse when true types are known.
|
||||||
|
// if we're lucky and the state we need to directly parse is available, then we'll use it.
|
||||||
|
|
||||||
if (token == null || token.Type != JTokenType.Object)
|
int? type = null;
|
||||||
|
string invocationId = null;
|
||||||
|
string target = null;
|
||||||
|
string error = null;
|
||||||
|
var hasItem = false;
|
||||||
|
object item = null;
|
||||||
|
JToken itemToken = null;
|
||||||
|
var hasResult = false;
|
||||||
|
object result = null;
|
||||||
|
JToken resultToken = null;
|
||||||
|
bool hasArguments = false;
|
||||||
|
object[] arguments = null;
|
||||||
|
JArray argumentsToken = null;
|
||||||
|
ExceptionDispatchInfo argumentBindingException = null;
|
||||||
|
Dictionary<string, string> headers = null;
|
||||||
|
var completed = false;
|
||||||
|
|
||||||
|
using (var reader = new JsonTextReader(textReader))
|
||||||
{
|
{
|
||||||
throw new InvalidDataException($"Unexpected JSON Token Type '{token?.Type}'. Expected a JSON Object.");
|
reader.ArrayPool = JsonArrayPool<char>.Shared;
|
||||||
|
|
||||||
|
JsonUtils.CheckRead(reader);
|
||||||
|
|
||||||
|
// We're always parsing a JSON object
|
||||||
|
if (reader.TokenType != JsonToken.StartObject)
|
||||||
|
{
|
||||||
|
throw new InvalidDataException($"Unexpected JSON Token Type '{JsonUtils.GetTokenString(reader.TokenType)}'. Expected a JSON Object.");
|
||||||
}
|
}
|
||||||
|
|
||||||
var json = (JObject)token;
|
do
|
||||||
|
{
|
||||||
|
switch (reader.TokenType)
|
||||||
|
{
|
||||||
|
case JsonToken.PropertyName:
|
||||||
|
string memberName = reader.Value.ToString();
|
||||||
|
|
||||||
// Determine the type of the message
|
switch (memberName)
|
||||||
var type = JsonUtils.GetRequiredProperty<int>(json, TypePropertyName, JTokenType.Integer);
|
{
|
||||||
|
case TypePropertyName:
|
||||||
|
var messageType = JsonUtils.ReadAsInt32(reader, TypePropertyName);
|
||||||
|
|
||||||
|
if (messageType == null)
|
||||||
|
{
|
||||||
|
throw new InvalidDataException($"Missing required property '{TypePropertyName}'.");
|
||||||
|
}
|
||||||
|
|
||||||
|
type = messageType.Value;
|
||||||
|
break;
|
||||||
|
case InvocationIdPropertyName:
|
||||||
|
invocationId = JsonUtils.ReadAsString(reader, InvocationIdPropertyName);
|
||||||
|
break;
|
||||||
|
case TargetPropertyName:
|
||||||
|
target = JsonUtils.ReadAsString(reader, TargetPropertyName);
|
||||||
|
break;
|
||||||
|
case ErrorPropertyName:
|
||||||
|
error = JsonUtils.ReadAsString(reader, ErrorPropertyName);
|
||||||
|
break;
|
||||||
|
case ResultPropertyName:
|
||||||
|
JsonUtils.CheckRead(reader);
|
||||||
|
|
||||||
|
hasResult = true;
|
||||||
|
|
||||||
|
if (string.IsNullOrEmpty(invocationId))
|
||||||
|
{
|
||||||
|
// If we don't have an invocation id then we need to store it as a JToken so we can parse it later
|
||||||
|
resultToken = JToken.Load(reader);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
// If we have an invocation id already we can parse the end result
|
||||||
|
var returnType = binder.GetReturnType(invocationId);
|
||||||
|
result = PayloadSerializer.Deserialize(reader, returnType);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case ItemPropertyName:
|
||||||
|
JsonUtils.CheckRead(reader);
|
||||||
|
|
||||||
|
hasItem = true;
|
||||||
|
|
||||||
|
if (string.IsNullOrEmpty(invocationId))
|
||||||
|
{
|
||||||
|
// If we don't have an invocation id then we need to store it as a JToken so we can parse it later
|
||||||
|
itemToken = JToken.Load(reader);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
var returnType = binder.GetReturnType(invocationId);
|
||||||
|
item = PayloadSerializer.Deserialize(reader, returnType);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case ArgumentsPropertyName:
|
||||||
|
JsonUtils.CheckRead(reader);
|
||||||
|
|
||||||
|
if (reader.TokenType != JsonToken.StartArray)
|
||||||
|
{
|
||||||
|
throw new InvalidDataException($"Expected '{ArgumentsPropertyName}' to be of type {JTokenType.Array}.");
|
||||||
|
}
|
||||||
|
|
||||||
|
hasArguments = true;
|
||||||
|
|
||||||
|
if (string.IsNullOrEmpty(target))
|
||||||
|
{
|
||||||
|
// We don't know the method name yet so just parse an array of generic JArray
|
||||||
|
argumentsToken = JArray.Load(reader);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var paramTypes = binder.GetParameterTypes(target);
|
||||||
|
arguments = BindArguments(reader, paramTypes);
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
argumentBindingException = ExceptionDispatchInfo.Capture(ex);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case HeadersPropertyName:
|
||||||
|
JsonUtils.CheckRead(reader);
|
||||||
|
headers = ReadHeaders(reader);
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
// Skip read the property name
|
||||||
|
JsonUtils.CheckRead(reader);
|
||||||
|
// Skip the value for this property
|
||||||
|
reader.Skip();
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case JsonToken.EndObject:
|
||||||
|
completed = true;
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
while (!completed && JsonUtils.CheckRead(reader));
|
||||||
|
}
|
||||||
|
|
||||||
|
HubMessage message = null;
|
||||||
|
|
||||||
switch (type)
|
switch (type)
|
||||||
{
|
{
|
||||||
case HubProtocolConstants.InvocationMessageType:
|
case HubProtocolConstants.InvocationMessageType:
|
||||||
return BindInvocationMessage(json, binder);
|
{
|
||||||
|
if (argumentsToken != null)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var paramTypes = binder.GetParameterTypes(target);
|
||||||
|
arguments = BindArguments(argumentsToken, paramTypes);
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
argumentBindingException = ExceptionDispatchInfo.Capture(ex);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
message = BindInvocationMessage(invocationId, target, argumentBindingException, arguments, hasArguments, binder);
|
||||||
|
}
|
||||||
|
break;
|
||||||
case HubProtocolConstants.StreamInvocationMessageType:
|
case HubProtocolConstants.StreamInvocationMessageType:
|
||||||
return BindStreamInvocationMessage(json, binder);
|
{
|
||||||
|
if (argumentsToken != null)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var paramTypes = binder.GetParameterTypes(target);
|
||||||
|
arguments = BindArguments(argumentsToken, paramTypes);
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
argumentBindingException = ExceptionDispatchInfo.Capture(ex);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
message = BindStreamInvocationMessage(invocationId, target, argumentBindingException, arguments, hasArguments, binder);
|
||||||
|
}
|
||||||
|
break;
|
||||||
case HubProtocolConstants.StreamItemMessageType:
|
case HubProtocolConstants.StreamItemMessageType:
|
||||||
return BindStreamItemMessage(json, binder);
|
if (itemToken != null)
|
||||||
|
{
|
||||||
|
var returnType = binder.GetReturnType(invocationId);
|
||||||
|
item = itemToken.ToObject(returnType, PayloadSerializer);
|
||||||
|
}
|
||||||
|
|
||||||
|
message = BindStreamItemMessage(invocationId, item, hasItem, binder);
|
||||||
|
break;
|
||||||
case HubProtocolConstants.CompletionMessageType:
|
case HubProtocolConstants.CompletionMessageType:
|
||||||
return BindCompletionMessage(json, binder);
|
if (resultToken != null)
|
||||||
|
{
|
||||||
|
var returnType = binder.GetReturnType(invocationId);
|
||||||
|
result = resultToken.ToObject(returnType, PayloadSerializer);
|
||||||
|
}
|
||||||
|
|
||||||
|
message = BindCompletionMessage(invocationId, error, result, hasResult, binder);
|
||||||
|
break;
|
||||||
case HubProtocolConstants.CancelInvocationMessageType:
|
case HubProtocolConstants.CancelInvocationMessageType:
|
||||||
return BindCancelInvocationMessage(json);
|
message = BindCancelInvocationMessage(invocationId);
|
||||||
|
break;
|
||||||
case HubProtocolConstants.PingMessageType:
|
case HubProtocolConstants.PingMessageType:
|
||||||
return PingMessage.Instance;
|
return PingMessage.Instance;
|
||||||
|
case null:
|
||||||
|
throw new InvalidDataException($"Missing required property '{TypePropertyName}'.");
|
||||||
default:
|
default:
|
||||||
throw new InvalidDataException($"Unknown message type: {type}");
|
throw new InvalidDataException($"Unknown message type: {type}");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return ApplyHeaders(message, headers);
|
||||||
}
|
}
|
||||||
catch (JsonReaderException jrex)
|
catch (JsonReaderException jrex)
|
||||||
{
|
{
|
||||||
throw new InvalidDataException("Error reading JSON.", jrex);
|
throw new InvalidDataException("Error reading JSON.", jrex);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
private void ReadHeaders(JObject json, IDictionary<string, string> headers)
|
private Dictionary<string, string> ReadHeaders(JsonTextReader reader)
|
||||||
{
|
{
|
||||||
var headersProp = json[HeadersPropertyName];
|
var headers = new Dictionary<string, string>();
|
||||||
if (headersProp != null)
|
|
||||||
{
|
if (reader.TokenType != JsonToken.StartObject)
|
||||||
if (headersProp.Type != JTokenType.Object)
|
|
||||||
{
|
{
|
||||||
throw new InvalidDataException($"Expected '{HeadersPropertyName}' to be of type {JTokenType.Object}.");
|
throw new InvalidDataException($"Expected '{HeadersPropertyName}' to be of type {JTokenType.Object}.");
|
||||||
}
|
}
|
||||||
var headersObj = headersProp.Value<JObject>();
|
|
||||||
foreach (var prop in headersObj)
|
while (reader.Read())
|
||||||
{
|
{
|
||||||
if (prop.Value.Type != JTokenType.String)
|
switch (reader.TokenType)
|
||||||
{
|
{
|
||||||
throw new InvalidDataException($"Expected header '{prop.Key}' to be of type {JTokenType.String}.");
|
case JsonToken.PropertyName:
|
||||||
|
string propertyName = reader.Value.ToString();
|
||||||
|
|
||||||
|
JsonUtils.CheckRead(reader);
|
||||||
|
|
||||||
|
if (reader.TokenType != JsonToken.String)
|
||||||
|
{
|
||||||
|
throw new InvalidDataException($"Expected header '{propertyName}' to be of type {JTokenType.String}.");
|
||||||
}
|
}
|
||||||
headers[prop.Key] = prop.Value.Value<string>();
|
|
||||||
|
headers[propertyName] = reader.Value?.ToString();
|
||||||
|
break;
|
||||||
|
case JsonToken.Comment:
|
||||||
|
break;
|
||||||
|
case JsonToken.EndObject:
|
||||||
|
return headers;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
throw new JsonReaderException("Unexpected end when reading message headers");
|
||||||
}
|
}
|
||||||
|
|
||||||
private void WriteMessageCore(HubMessage message, Stream stream)
|
private void WriteMessageCore(HubMessage message, Stream stream)
|
||||||
|
|
@ -176,7 +368,7 @@ namespace Microsoft.AspNetCore.SignalR.Internal.Protocol
|
||||||
|
|
||||||
private void WriteHeaders(JsonTextWriter writer, HubInvocationMessage message)
|
private void WriteHeaders(JsonTextWriter writer, HubInvocationMessage message)
|
||||||
{
|
{
|
||||||
if (message.Headers.Count > 0)
|
if (message.Headers != null && message.Headers.Count > 0)
|
||||||
{
|
{
|
||||||
writer.WritePropertyName(HeadersPropertyName);
|
writer.WritePropertyName(HeadersPropertyName);
|
||||||
writer.WriteStartObject();
|
writer.WriteStartObject();
|
||||||
|
|
@ -260,95 +452,122 @@ namespace Microsoft.AspNetCore.SignalR.Internal.Protocol
|
||||||
writer.WriteValue(type);
|
writer.WriteValue(type);
|
||||||
}
|
}
|
||||||
|
|
||||||
private InvocationMessage BindInvocationMessage(JObject json, IInvocationBinder binder)
|
private HubMessage BindCancelInvocationMessage(string invocationId)
|
||||||
{
|
{
|
||||||
var invocationId = JsonUtils.GetOptionalProperty<string>(json, InvocationIdPropertyName, JTokenType.String);
|
if (string.IsNullOrEmpty(invocationId))
|
||||||
var target = JsonUtils.GetRequiredProperty<string>(json, TargetPropertyName, JTokenType.String);
|
|
||||||
|
|
||||||
var args = JsonUtils.GetRequiredProperty<JArray>(json, ArgumentsPropertyName, JTokenType.Array);
|
|
||||||
|
|
||||||
var paramTypes = binder.GetParameterTypes(target);
|
|
||||||
|
|
||||||
InvocationMessage message;
|
|
||||||
try
|
|
||||||
{
|
{
|
||||||
var arguments = BindArguments(args, paramTypes);
|
throw new InvalidDataException($"Missing required property '{InvocationIdPropertyName}'.");
|
||||||
message = new InvocationMessage(invocationId, target, argumentBindingException: null, arguments: arguments);
|
|
||||||
}
|
|
||||||
catch (Exception ex)
|
|
||||||
{
|
|
||||||
message = new InvocationMessage(invocationId, target, ExceptionDispatchInfo.Capture(ex));
|
|
||||||
}
|
|
||||||
ReadHeaders(json, message.Headers);
|
|
||||||
return message;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private StreamInvocationMessage BindStreamInvocationMessage(JObject json, IInvocationBinder binder)
|
return new CancelInvocationMessage(invocationId);
|
||||||
{
|
|
||||||
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);
|
|
||||||
|
|
||||||
StreamInvocationMessage message;
|
|
||||||
try
|
|
||||||
{
|
|
||||||
var arguments = BindArguments(args, paramTypes);
|
|
||||||
message = new StreamInvocationMessage(invocationId, target, argumentBindingException: null, arguments: arguments);
|
|
||||||
}
|
|
||||||
catch (Exception ex)
|
|
||||||
{
|
|
||||||
message = new StreamInvocationMessage(invocationId, target, ExceptionDispatchInfo.Capture(ex));
|
|
||||||
}
|
|
||||||
ReadHeaders(json, message.Headers);
|
|
||||||
return message;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private StreamItemMessage BindStreamItemMessage(JObject json, IInvocationBinder binder)
|
private HubMessage BindCompletionMessage(string invocationId, string error, object result, bool hasResult, IInvocationBinder binder)
|
||||||
{
|
{
|
||||||
var invocationId = JsonUtils.GetRequiredProperty<string>(json, InvocationIdPropertyName, JTokenType.String);
|
if (string.IsNullOrEmpty(invocationId))
|
||||||
var result = JsonUtils.GetRequiredProperty<JToken>(json, ItemPropertyName);
|
{
|
||||||
|
throw new InvalidDataException($"Missing required property '{InvocationIdPropertyName}'.");
|
||||||
var returnType = binder.GetReturnType(invocationId);
|
|
||||||
var message = new StreamItemMessage(invocationId, result?.ToObject(returnType, PayloadSerializer));
|
|
||||||
ReadHeaders(json, message.Headers);
|
|
||||||
return message;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private CompletionMessage BindCompletionMessage(JObject json, IInvocationBinder binder)
|
if (error != null && hasResult)
|
||||||
{
|
|
||||||
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 InvalidDataException("The 'error' and 'result' properties are mutually exclusive.");
|
throw new InvalidDataException("The 'error' and 'result' properties are mutually exclusive.");
|
||||||
}
|
}
|
||||||
|
|
||||||
CompletionMessage message;
|
if (hasResult)
|
||||||
if (resultProp == null)
|
|
||||||
{
|
{
|
||||||
message = new CompletionMessage(invocationId, error, result: null, hasResult: false);
|
return new CompletionMessage(invocationId, error, result, hasResult: true);
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
var returnType = binder.GetReturnType(invocationId);
|
|
||||||
var payload = resultProp.Value?.ToObject(returnType, PayloadSerializer);
|
|
||||||
message = new CompletionMessage(invocationId, error, result: payload, hasResult: true);
|
|
||||||
}
|
|
||||||
ReadHeaders(json, message.Headers);
|
|
||||||
return message;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private CancelInvocationMessage BindCancelInvocationMessage(JObject json)
|
return new CompletionMessage(invocationId, error, result: null, hasResult: false);
|
||||||
|
}
|
||||||
|
|
||||||
|
private HubMessage BindStreamItemMessage(string invocationId, object item, bool hasItem, IInvocationBinder binder)
|
||||||
{
|
{
|
||||||
var invocationId = JsonUtils.GetRequiredProperty<string>(json, InvocationIdPropertyName, JTokenType.String);
|
if (string.IsNullOrEmpty(invocationId))
|
||||||
var message = new CancelInvocationMessage(invocationId);
|
{
|
||||||
ReadHeaders(json, message.Headers);
|
throw new InvalidDataException($"Missing required property '{InvocationIdPropertyName}'.");
|
||||||
return message;
|
}
|
||||||
|
|
||||||
|
if (!hasItem)
|
||||||
|
{
|
||||||
|
throw new InvalidDataException($"Missing required property '{ItemPropertyName}'.");
|
||||||
|
}
|
||||||
|
|
||||||
|
return new StreamItemMessage(invocationId, item);
|
||||||
|
}
|
||||||
|
|
||||||
|
private HubMessage BindStreamInvocationMessage(string invocationId, string target, ExceptionDispatchInfo argumentBindingException, object[] arguments, bool hasArguments, IInvocationBinder binder)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrEmpty(invocationId))
|
||||||
|
{
|
||||||
|
throw new InvalidDataException($"Missing required property '{InvocationIdPropertyName}'.");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!hasArguments)
|
||||||
|
{
|
||||||
|
throw new InvalidDataException($"Missing required property '{ArgumentsPropertyName}'.");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (string.IsNullOrEmpty(target))
|
||||||
|
{
|
||||||
|
throw new InvalidDataException($"Missing required property '{TargetPropertyName}'.");
|
||||||
|
}
|
||||||
|
|
||||||
|
return new StreamInvocationMessage(invocationId, target, argumentBindingException: argumentBindingException, arguments: arguments);
|
||||||
|
}
|
||||||
|
|
||||||
|
private HubMessage BindInvocationMessage(string invocationId, string target, ExceptionDispatchInfo argumentBindingException, object[] arguments, bool hasArguments, IInvocationBinder binder)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrEmpty(target))
|
||||||
|
{
|
||||||
|
throw new InvalidDataException($"Missing required property '{TargetPropertyName}'.");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!hasArguments)
|
||||||
|
{
|
||||||
|
throw new InvalidDataException($"Missing required property '{ArgumentsPropertyName}'.");
|
||||||
|
}
|
||||||
|
|
||||||
|
return new InvocationMessage(invocationId, target, argumentBindingException: argumentBindingException, arguments: arguments);
|
||||||
|
}
|
||||||
|
|
||||||
|
private object[] BindArguments(JsonTextReader reader, IReadOnlyList<Type> paramTypes)
|
||||||
|
{
|
||||||
|
var arguments = new object[paramTypes.Count];
|
||||||
|
var paramIndex = 0;
|
||||||
|
var argumentsCount = 0;
|
||||||
|
|
||||||
|
while (reader.Read())
|
||||||
|
{
|
||||||
|
if (reader.TokenType == JsonToken.EndArray)
|
||||||
|
{
|
||||||
|
if (argumentsCount != paramTypes.Count)
|
||||||
|
{
|
||||||
|
throw new InvalidDataException($"Invocation provides {argumentsCount} argument(s) but target expects {paramTypes.Count}.");
|
||||||
|
}
|
||||||
|
|
||||||
|
return arguments;
|
||||||
|
}
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
if (paramIndex < paramTypes.Count)
|
||||||
|
{
|
||||||
|
// Set all known arguments
|
||||||
|
arguments[paramIndex] = PayloadSerializer.Deserialize(reader, paramTypes[paramIndex]);
|
||||||
|
}
|
||||||
|
|
||||||
|
argumentsCount++;
|
||||||
|
paramIndex++;
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
throw new InvalidDataException("Error binding arguments. Make sure that the types of the provided values match the types of the hub method being invoked.", ex);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
throw new JsonReaderException("Unexpected end when reading JSON");
|
||||||
}
|
}
|
||||||
|
|
||||||
private object[] BindArguments(JArray args, IReadOnlyList<Type> paramTypes)
|
private object[] BindArguments(JArray args, IReadOnlyList<Type> paramTypes)
|
||||||
|
|
@ -375,6 +594,16 @@ namespace Microsoft.AspNetCore.SignalR.Internal.Protocol
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private HubMessage ApplyHeaders(HubMessage message, Dictionary<string, string> headers)
|
||||||
|
{
|
||||||
|
if (headers != null && message is HubInvocationMessage invocationMessage)
|
||||||
|
{
|
||||||
|
invocationMessage.Headers = headers;
|
||||||
|
}
|
||||||
|
|
||||||
|
return message;
|
||||||
|
}
|
||||||
|
|
||||||
internal static JsonSerializerSettings CreateDefaultSerializerSettings()
|
internal static JsonSerializerSettings CreateDefaultSerializerSettings()
|
||||||
{
|
{
|
||||||
return new JsonSerializerSettings { ContractResolver = new CamelCasePropertyNamesContractResolver() };
|
return new JsonSerializerSettings { ContractResolver = new CamelCasePropertyNamesContractResolver() };
|
||||||
|
|
|
||||||
|
|
@ -3,6 +3,7 @@
|
||||||
|
|
||||||
using System;
|
using System;
|
||||||
using System.IO;
|
using System.IO;
|
||||||
|
using Newtonsoft.Json;
|
||||||
using Newtonsoft.Json.Linq;
|
using Newtonsoft.Json.Linq;
|
||||||
|
|
||||||
namespace Microsoft.AspNetCore.SignalR.Internal.Protocol
|
namespace Microsoft.AspNetCore.SignalR.Internal.Protocol
|
||||||
|
|
@ -41,5 +42,62 @@ namespace Microsoft.AspNetCore.SignalR.Internal.Protocol
|
||||||
}
|
}
|
||||||
return prop.Value<T>();
|
return prop.Value<T>();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static string GetTokenString(JsonToken tokenType)
|
||||||
|
{
|
||||||
|
switch (tokenType)
|
||||||
|
{
|
||||||
|
case JsonToken.None:
|
||||||
|
break;
|
||||||
|
case JsonToken.StartObject:
|
||||||
|
return JTokenType.Object.ToString();
|
||||||
|
case JsonToken.StartArray:
|
||||||
|
return JTokenType.Array.ToString();
|
||||||
|
case JsonToken.PropertyName:
|
||||||
|
return JTokenType.Property.ToString();
|
||||||
|
default:
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
return tokenType.ToString();
|
||||||
|
}
|
||||||
|
|
||||||
|
public static int? ReadAsInt32(JsonTextReader reader, string propertyName)
|
||||||
|
{
|
||||||
|
reader.Read();
|
||||||
|
|
||||||
|
if (reader.TokenType != JsonToken.Integer)
|
||||||
|
{
|
||||||
|
throw new InvalidDataException($"Expected '{propertyName}' to be of type {JTokenType.Integer}.");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (reader.Value == null)
|
||||||
|
{
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return Convert.ToInt32(reader.Value);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static string ReadAsString(JsonTextReader reader, string propertyName)
|
||||||
|
{
|
||||||
|
reader.Read();
|
||||||
|
|
||||||
|
if (reader.TokenType != JsonToken.String)
|
||||||
|
{
|
||||||
|
throw new InvalidDataException($"Expected '{propertyName}' to be of type {JTokenType.String}.");
|
||||||
|
}
|
||||||
|
|
||||||
|
return reader.Value?.ToString();
|
||||||
|
}
|
||||||
|
|
||||||
|
public static bool CheckRead(JsonTextReader reader)
|
||||||
|
{
|
||||||
|
if (!reader.Read())
|
||||||
|
{
|
||||||
|
throw new JsonReaderException("Unexpected end when reading JSON");
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -3,7 +3,6 @@
|
||||||
|
|
||||||
using System;
|
using System;
|
||||||
using System.Buffers;
|
using System.Buffers;
|
||||||
using System.Collections;
|
|
||||||
using System.IO;
|
using System.IO;
|
||||||
using System.Text;
|
using System.Text;
|
||||||
using Microsoft.AspNetCore.SignalR.Internal.Formatters;
|
using Microsoft.AspNetCore.SignalR.Internal.Formatters;
|
||||||
|
|
@ -31,17 +30,18 @@ namespace Microsoft.AspNetCore.SignalR.Internal.Protocol
|
||||||
TextMessageFormatter.WriteRecordSeparator(output);
|
TextMessageFormatter.WriteRecordSeparator(output);
|
||||||
}
|
}
|
||||||
|
|
||||||
public static bool TryParseMessage(ReadOnlySpan<byte> input, out NegotiationMessage negotiationMessage)
|
public static bool TryParseMessage(ReadOnlyMemory<byte> input, out NegotiationMessage negotiationMessage)
|
||||||
{
|
{
|
||||||
if (!TextMessageParser.TryParseMessage(ref input, out var payload))
|
if (!TextMessageParser.TryParseMessage(ref input, out var payload))
|
||||||
{
|
{
|
||||||
throw new InvalidDataException("Unable to parse payload as a negotiation message.");
|
throw new InvalidDataException("Unable to parse payload as a negotiation message.");
|
||||||
}
|
}
|
||||||
|
|
||||||
using (var memoryStream = new MemoryStream(payload.ToArray()))
|
var textReader = new Utf8BufferTextReader(payload);
|
||||||
{
|
using (var reader = new JsonTextReader(textReader))
|
||||||
using (var reader = new JsonTextReader(new StreamReader(memoryStream)))
|
|
||||||
{
|
{
|
||||||
|
reader.ArrayPool = JsonArrayPool<char>.Shared;
|
||||||
|
|
||||||
var token = JToken.ReadFrom(reader);
|
var token = JToken.ReadFrom(reader);
|
||||||
if (token == null || token.Type != JTokenType.Object)
|
if (token == null || token.Type != JTokenType.Object)
|
||||||
{
|
{
|
||||||
|
|
@ -52,7 +52,6 @@ namespace Microsoft.AspNetCore.SignalR.Internal.Protocol
|
||||||
var protocol = JsonUtils.GetRequiredProperty<string>(negotiationJObject, ProtocolPropertyName);
|
var protocol = JsonUtils.GetRequiredProperty<string>(negotiationJObject, ProtocolPropertyName);
|
||||||
negotiationMessage = new NegotiationMessage(protocol);
|
negotiationMessage = new NegotiationMessage(protocol);
|
||||||
}
|
}
|
||||||
}
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -75,7 +74,7 @@ namespace Microsoft.AspNetCore.SignalR.Internal.Protocol
|
||||||
|
|
||||||
var memory = buffer.IsSingleSegment ? buffer.First : buffer.ToArray();
|
var memory = buffer.IsSingleSegment ? buffer.First : buffer.ToArray();
|
||||||
|
|
||||||
return TryParseMessage(memory.Span, out negotiationMessage);
|
return TryParseMessage(memory, out negotiationMessage);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,57 @@
|
||||||
|
// 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.IO;
|
||||||
|
using System.Runtime.InteropServices;
|
||||||
|
using System.Text;
|
||||||
|
|
||||||
|
namespace Microsoft.AspNetCore.SignalR.Internal.Protocol
|
||||||
|
{
|
||||||
|
internal class Utf8BufferTextReader : TextReader
|
||||||
|
{
|
||||||
|
private ReadOnlyMemory<byte> _utf8Buffer;
|
||||||
|
|
||||||
|
public Utf8BufferTextReader(ReadOnlyMemory<byte> utf8Buffer)
|
||||||
|
{
|
||||||
|
_utf8Buffer = utf8Buffer;
|
||||||
|
}
|
||||||
|
|
||||||
|
public override int Read(char[] buffer, int index, int count)
|
||||||
|
{
|
||||||
|
if (_utf8Buffer.IsEmpty)
|
||||||
|
{
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
var source = _utf8Buffer.Span;
|
||||||
|
var destination = new Span<char>(buffer, index, count);
|
||||||
|
var destinationBytesCount = Encoding.UTF8.GetByteCount(buffer, index, count);
|
||||||
|
|
||||||
|
// We have then the destination
|
||||||
|
if (source.Length > destinationBytesCount)
|
||||||
|
{
|
||||||
|
source = source.Slice(0, destinationBytesCount);
|
||||||
|
|
||||||
|
_utf8Buffer = _utf8Buffer.Slice(destinationBytesCount);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
_utf8Buffer = ReadOnlyMemory<byte>.Empty;
|
||||||
|
}
|
||||||
|
|
||||||
|
#if NETCOREAPP2_1
|
||||||
|
return Encoding.UTF8.GetChars(source, destination);
|
||||||
|
#else
|
||||||
|
unsafe
|
||||||
|
{
|
||||||
|
fixed (char* destinationChars = &MemoryMarshal.GetReference(destination))
|
||||||
|
fixed (byte* sourceBytes = &MemoryMarshal.GetReference(source))
|
||||||
|
{
|
||||||
|
return Encoding.UTF8.GetChars(sourceBytes, source.Length, destinationChars, destination.Length);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -37,7 +37,7 @@ namespace Microsoft.AspNetCore.SignalR.Internal.Protocol
|
||||||
SerializationContext = options.Value.SerializationContext;
|
SerializationContext = options.Value.SerializationContext;
|
||||||
}
|
}
|
||||||
|
|
||||||
public bool TryParseMessages(ReadOnlySpan<byte> input, IInvocationBinder binder, IList<HubMessage> messages)
|
public bool TryParseMessages(ReadOnlyMemory<byte> input, IInvocationBinder binder, IList<HubMessage> messages)
|
||||||
{
|
{
|
||||||
while (BinaryMessageParser.TryParseMessage(ref input, out var payload))
|
while (BinaryMessageParser.TryParseMessage(ref input, out var payload))
|
||||||
{
|
{
|
||||||
|
|
@ -213,14 +213,11 @@ namespace Microsoft.AspNetCore.SignalR.Internal.Protocol
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private static T ApplyHeaders<T>(IDictionary<string, string> source, T destination) where T: HubInvocationMessage
|
private static T ApplyHeaders<T>(IDictionary<string, string> source, T destination) where T : HubInvocationMessage
|
||||||
{
|
{
|
||||||
if(source != null && source.Count > 0)
|
if (source != null && source.Count > 0)
|
||||||
{
|
{
|
||||||
foreach(var header in source)
|
destination.Headers = source;
|
||||||
{
|
|
||||||
destination.Headers[header.Key] = header.Value;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return destination;
|
return destination;
|
||||||
|
|
|
||||||
|
|
@ -255,7 +255,7 @@ namespace Microsoft.AspNetCore.SignalR.Client.Tests
|
||||||
|
|
||||||
public TransferFormat TransferFormat => TransferFormat.Binary;
|
public TransferFormat TransferFormat => TransferFormat.Binary;
|
||||||
|
|
||||||
public bool TryParseMessages(ReadOnlySpan<byte> input, IInvocationBinder binder, IList<HubMessage> messages)
|
public bool TryParseMessages(ReadOnlyMemory<byte> input, IInvocationBinder binder, IList<HubMessage> messages)
|
||||||
{
|
{
|
||||||
ParseCalls += 1;
|
ParseCalls += 1;
|
||||||
if (_error != null)
|
if (_error != null)
|
||||||
|
|
|
||||||
|
|
@ -113,7 +113,7 @@ namespace Microsoft.AspNetCore.Sockets.Tests.Internal.Formatters
|
||||||
{
|
{
|
||||||
BinaryMessageFormatter.WriteLengthPrefix(payload.Length, ms);
|
BinaryMessageFormatter.WriteLengthPrefix(payload.Length, ms);
|
||||||
ms.Write(payload, 0, payload.Length);
|
ms.Write(payload, 0, payload.Length);
|
||||||
var buffer = new ReadOnlySpan<byte>(ms.ToArray());
|
var buffer = new ReadOnlyMemory<byte>(ms.ToArray());
|
||||||
Assert.True(BinaryMessageParser.TryParseMessage(ref buffer, out var roundtripped));
|
Assert.True(BinaryMessageParser.TryParseMessage(ref buffer, out var roundtripped));
|
||||||
Assert.Equal(payload, roundtripped.ToArray());
|
Assert.Equal(payload, roundtripped.ToArray());
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -17,7 +17,7 @@ namespace Microsoft.AspNetCore.SignalR.Common.Tests.Internal.Formatters
|
||||||
[InlineData(new byte[] { 0x0B, 0x41, 0x0A, 0x52, 0x0D, 0x43, 0x0D, 0x0A, 0x3B, 0x44, 0x45, 0x46 }, "A\nR\rC\r\n;DEF")]
|
[InlineData(new byte[] { 0x0B, 0x41, 0x0A, 0x52, 0x0D, 0x43, 0x0D, 0x0A, 0x3B, 0x44, 0x45, 0x46 }, "A\nR\rC\r\n;DEF")]
|
||||||
public void ReadMessage(byte[] encoded, string payload)
|
public void ReadMessage(byte[] encoded, string payload)
|
||||||
{
|
{
|
||||||
ReadOnlySpan<byte> span = encoded;
|
ReadOnlyMemory<byte> span = encoded;
|
||||||
Assert.True(BinaryMessageParser.TryParseMessage(ref span, out var message));
|
Assert.True(BinaryMessageParser.TryParseMessage(ref span, out var message));
|
||||||
Assert.Equal(0, span.Length);
|
Assert.Equal(0, span.Length);
|
||||||
|
|
||||||
|
|
@ -52,7 +52,7 @@ namespace Microsoft.AspNetCore.SignalR.Common.Tests.Internal.Formatters
|
||||||
})]
|
})]
|
||||||
public void ReadBinaryMessage(byte[] encoded, byte[] payload)
|
public void ReadBinaryMessage(byte[] encoded, byte[] payload)
|
||||||
{
|
{
|
||||||
ReadOnlySpan< byte> span = encoded;
|
ReadOnlyMemory< byte> span = encoded;
|
||||||
Assert.True(BinaryMessageParser.TryParseMessage(ref span, out var message));
|
Assert.True(BinaryMessageParser.TryParseMessage(ref span, out var message));
|
||||||
Assert.Equal(0, span.Length);
|
Assert.Equal(0, span.Length);
|
||||||
Assert.Equal(payload, message.ToArray());
|
Assert.Equal(payload, message.ToArray());
|
||||||
|
|
@ -66,7 +66,7 @@ namespace Microsoft.AspNetCore.SignalR.Common.Tests.Internal.Formatters
|
||||||
{
|
{
|
||||||
var ex = Assert.Throws<FormatException>(() =>
|
var ex = Assert.Throws<FormatException>(() =>
|
||||||
{
|
{
|
||||||
var buffer = new ReadOnlySpan<byte>(payload);
|
var buffer = new ReadOnlyMemory<byte>(payload);
|
||||||
BinaryMessageParser.TryParseMessage(ref buffer, out var message);
|
BinaryMessageParser.TryParseMessage(ref buffer, out var message);
|
||||||
});
|
});
|
||||||
Assert.Equal("Messages over 2GB in size are not supported.", ex.Message);
|
Assert.Equal("Messages over 2GB in size are not supported.", ex.Message);
|
||||||
|
|
@ -79,7 +79,7 @@ namespace Microsoft.AspNetCore.SignalR.Common.Tests.Internal.Formatters
|
||||||
[InlineData(new byte[] { 0x80 })] // size is cut
|
[InlineData(new byte[] { 0x80 })] // size is cut
|
||||||
public void BinaryMessageParserReturnsFalseForPartialPayloads(byte[] payload)
|
public void BinaryMessageParserReturnsFalseForPartialPayloads(byte[] payload)
|
||||||
{
|
{
|
||||||
var buffer = new ReadOnlySpan<byte>(payload);
|
var buffer = new ReadOnlyMemory<byte>(payload);
|
||||||
Assert.False(BinaryMessageParser.TryParseMessage(ref buffer, out var message));
|
Assert.False(BinaryMessageParser.TryParseMessage(ref buffer, out var message));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -94,7 +94,7 @@ namespace Microsoft.AspNetCore.SignalR.Common.Tests.Internal.Formatters
|
||||||
/* body: */ 0x48, 0x65, 0x6C, 0x6C, 0x6F, 0x2C, 0x0D, 0x0A, 0x57, 0x6F, 0x72, 0x6C, 0x64, 0x21,
|
/* body: */ 0x48, 0x65, 0x6C, 0x6C, 0x6F, 0x2C, 0x0D, 0x0A, 0x57, 0x6F, 0x72, 0x6C, 0x64, 0x21,
|
||||||
};
|
};
|
||||||
|
|
||||||
ReadOnlySpan<byte> buffer = encoded;
|
ReadOnlyMemory<byte> buffer = encoded;
|
||||||
var messages = new List<byte[]>();
|
var messages = new List<byte[]>();
|
||||||
while (BinaryMessageParser.TryParseMessage(ref buffer, out var message))
|
while (BinaryMessageParser.TryParseMessage(ref buffer, out var message))
|
||||||
{
|
{
|
||||||
|
|
@ -113,7 +113,7 @@ namespace Microsoft.AspNetCore.SignalR.Common.Tests.Internal.Formatters
|
||||||
[InlineData(new byte[] { 0x09, 0x00, 0x00 })] // Not enough data for payload
|
[InlineData(new byte[] { 0x09, 0x00, 0x00 })] // Not enough data for payload
|
||||||
public void ReadIncompleteMessages(byte[] encoded)
|
public void ReadIncompleteMessages(byte[] encoded)
|
||||||
{
|
{
|
||||||
ReadOnlySpan<byte> buffer = encoded;
|
ReadOnlyMemory<byte> buffer = encoded;
|
||||||
Assert.False(BinaryMessageParser.TryParseMessage(ref buffer, out var message));
|
Assert.False(BinaryMessageParser.TryParseMessage(ref buffer, out var message));
|
||||||
Assert.Equal(encoded.Length, buffer.Length);
|
Assert.Equal(encoded.Length, buffer.Length);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -13,7 +13,7 @@ namespace Microsoft.AspNetCore.SignalR.Common.Tests.Internal.Formatters
|
||||||
[Fact]
|
[Fact]
|
||||||
public void ReadMessage()
|
public void ReadMessage()
|
||||||
{
|
{
|
||||||
var message = new ReadOnlySpan<byte>(Encoding.UTF8.GetBytes("ABC\u001e"));
|
var message = new ReadOnlyMemory<byte>(Encoding.UTF8.GetBytes("ABC\u001e"));
|
||||||
|
|
||||||
Assert.True(TextMessageParser.TryParseMessage(ref message, out var payload));
|
Assert.True(TextMessageParser.TryParseMessage(ref message, out var payload));
|
||||||
Assert.Equal("ABC", Encoding.UTF8.GetString(payload.ToArray()));
|
Assert.Equal("ABC", Encoding.UTF8.GetString(payload.ToArray()));
|
||||||
|
|
@ -23,14 +23,14 @@ namespace Microsoft.AspNetCore.SignalR.Common.Tests.Internal.Formatters
|
||||||
[Fact]
|
[Fact]
|
||||||
public void TryReadingIncompleteMessage()
|
public void TryReadingIncompleteMessage()
|
||||||
{
|
{
|
||||||
var message = new ReadOnlySpan<byte>(Encoding.UTF8.GetBytes("ABC"));
|
var message = new ReadOnlyMemory<byte>(Encoding.UTF8.GetBytes("ABC"));
|
||||||
Assert.False(TextMessageParser.TryParseMessage(ref message, out var payload));
|
Assert.False(TextMessageParser.TryParseMessage(ref message, out var payload));
|
||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public void TryReadingMultipleMessages()
|
public void TryReadingMultipleMessages()
|
||||||
{
|
{
|
||||||
var message = new ReadOnlySpan<byte>(Encoding.UTF8.GetBytes("ABC\u001eXYZ\u001e"));
|
var message = new ReadOnlyMemory<byte>(Encoding.UTF8.GetBytes("ABC\u001eXYZ\u001e"));
|
||||||
Assert.True(TextMessageParser.TryParseMessage(ref message, out var payload));
|
Assert.True(TextMessageParser.TryParseMessage(ref message, out var payload));
|
||||||
Assert.Equal("ABC", Encoding.UTF8.GetString(payload.ToArray()));
|
Assert.Equal("ABC", Encoding.UTF8.GetString(payload.ToArray()));
|
||||||
Assert.True(TextMessageParser.TryParseMessage(ref message, out payload));
|
Assert.True(TextMessageParser.TryParseMessage(ref message, out payload));
|
||||||
|
|
@ -40,7 +40,7 @@ namespace Microsoft.AspNetCore.SignalR.Common.Tests.Internal.Formatters
|
||||||
[Fact]
|
[Fact]
|
||||||
public void IncompleteTrailingMessage()
|
public void IncompleteTrailingMessage()
|
||||||
{
|
{
|
||||||
var message = new ReadOnlySpan<byte>(Encoding.UTF8.GetBytes("ABC\u001eXYZ\u001e123"));
|
var message = new ReadOnlyMemory<byte>(Encoding.UTF8.GetBytes("ABC\u001eXYZ\u001e123"));
|
||||||
Assert.True(TextMessageParser.TryParseMessage(ref message, out var payload));
|
Assert.True(TextMessageParser.TryParseMessage(ref message, out var payload));
|
||||||
Assert.Equal("ABC", Encoding.UTF8.GetString(payload.ToArray()));
|
Assert.Equal("ABC", Encoding.UTF8.GetString(payload.ToArray()));
|
||||||
Assert.True(TextMessageParser.TryParseMessage(ref message, out payload));
|
Assert.True(TextMessageParser.TryParseMessage(ref message, out payload));
|
||||||
|
|
|
||||||
|
|
@ -13,8 +13,14 @@ namespace Microsoft.AspNetCore.SignalR.Common.Tests.Internal.Protocol
|
||||||
{
|
{
|
||||||
foreach (var header in headers)
|
foreach (var header in headers)
|
||||||
{
|
{
|
||||||
|
if (hubMessage.Headers == null)
|
||||||
|
{
|
||||||
|
hubMessage.Headers = new Dictionary<string, string>();
|
||||||
|
}
|
||||||
|
|
||||||
hubMessage.Headers[header.Key] = header.Value;
|
hubMessage.Headers[header.Key] = header.Value;
|
||||||
}
|
}
|
||||||
|
|
||||||
return hubMessage;
|
return hubMessage;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -82,6 +82,14 @@ namespace Microsoft.AspNetCore.SignalR.Common.Tests.Internal.Protocol
|
||||||
new object[] { PingMessage.Instance, true, NullValueHandling.Ignore, "{\"type\":6}" },
|
new object[] { PingMessage.Instance, true, NullValueHandling.Ignore, "{\"type\":6}" },
|
||||||
};
|
};
|
||||||
|
|
||||||
|
public static IEnumerable<object[]> OutOfOrderJsonTestData => new[]
|
||||||
|
{
|
||||||
|
new object[] { "{ \"arguments\": [1,2], \"type\":1, \"target\": \"Method\" }", new InvocationMessage("Method", argumentBindingException: null, 1, 2) },
|
||||||
|
new object[] { "{ \"type\":4, \"arguments\": [1,2], \"target\": \"Method\", \"invocationId\": \"3\" }", new StreamInvocationMessage("3", "Method", argumentBindingException: null, 1, 2) },
|
||||||
|
new object[] { "{ \"type\":3, \"result\": 10, \"invocationId\": \"15\" }", new CompletionMessage("15", null, 10, hasResult: true) },
|
||||||
|
new object[] { "{ \"item\": \"foo\", \"invocationId\": \"1a\", \"type\":2 }", new StreamItemMessage("1a", "foo") }
|
||||||
|
};
|
||||||
|
|
||||||
[Theory]
|
[Theory]
|
||||||
[MemberData(nameof(ProtocolTestData))]
|
[MemberData(nameof(ProtocolTestData))]
|
||||||
public void WriteMessage(HubMessage message, bool camelCase, NullValueHandling nullValueHandling, string expectedOutput)
|
public void WriteMessage(HubMessage message, bool camelCase, NullValueHandling nullValueHandling, string expectedOutput)
|
||||||
|
|
@ -169,6 +177,7 @@ namespace Microsoft.AspNetCore.SignalR.Common.Tests.Internal.Protocol
|
||||||
[InlineData("{'type':'foo'}", "Expected 'type' to be of type Integer.")]
|
[InlineData("{'type':'foo'}", "Expected 'type' to be of type Integer.")]
|
||||||
|
|
||||||
[InlineData("{'type':3,'invocationId':'42','error':'foo','result':true}", "The 'error' and 'result' properties are mutually exclusive.")]
|
[InlineData("{'type':3,'invocationId':'42','error':'foo','result':true}", "The 'error' and 'result' properties are mutually exclusive.")]
|
||||||
|
[InlineData("{'type':3,'invocationId':'42','result':true", "Error reading JSON.")]
|
||||||
public void InvalidMessages(string input, string expectedMessage)
|
public void InvalidMessages(string input, string expectedMessage)
|
||||||
{
|
{
|
||||||
input = Frame(input);
|
input = Frame(input);
|
||||||
|
|
@ -180,9 +189,25 @@ namespace Microsoft.AspNetCore.SignalR.Common.Tests.Internal.Protocol
|
||||||
Assert.Equal(expectedMessage, ex.Message);
|
Assert.Equal(expectedMessage, ex.Message);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Theory]
|
||||||
|
[MemberData(nameof(OutOfOrderJsonTestData))]
|
||||||
|
public void ParseOutOfOrderJson(string input, HubMessage expectedMessage)
|
||||||
|
{
|
||||||
|
input = Frame(input);
|
||||||
|
|
||||||
|
var binder = new TestBinder(expectedMessage);
|
||||||
|
var protocol = new JsonHubProtocol();
|
||||||
|
var messages = new List<HubMessage>();
|
||||||
|
protocol.TryParseMessages(Encoding.UTF8.GetBytes(input), binder, messages);
|
||||||
|
|
||||||
|
Assert.Equal(expectedMessage, messages[0], TestHubMessageEqualityComparer.Instance);
|
||||||
|
}
|
||||||
|
|
||||||
[Theory]
|
[Theory]
|
||||||
[InlineData("{'type':1,'invocationId':'42','target':'foo','arguments':[]}", "Invocation provides 0 argument(s) but target expects 2.")]
|
[InlineData("{'type':1,'invocationId':'42','target':'foo','arguments':[]}", "Invocation provides 0 argument(s) but target expects 2.")]
|
||||||
|
[InlineData("{'type':1,'arguments':[], 'invocationId':'42','target':'foo'}", "Invocation provides 0 argument(s) but target expects 2.")]
|
||||||
[InlineData("{'type':1,'invocationId':'42','target':'foo','arguments':[ 'abc', 'xyz']}", "Error binding arguments. Make sure that the types of the provided values match the types of the hub method being invoked.")]
|
[InlineData("{'type':1,'invocationId':'42','target':'foo','arguments':[ 'abc', 'xyz']}", "Error binding arguments. Make sure that the types of the provided values match the types of the hub method being invoked.")]
|
||||||
|
[InlineData("{'type':1,'invocationId':'42','arguments':[ 'abc', 'xyz'], 'target':'foo'}", "Error binding arguments. Make sure that the types of the provided values match the types of the hub method being invoked.")]
|
||||||
[InlineData("{'type':4,'invocationId':'42','target':'foo','arguments':[]}", "Invocation provides 0 argument(s) but target expects 2.")]
|
[InlineData("{'type':4,'invocationId':'42','target':'foo','arguments':[]}", "Invocation provides 0 argument(s) but target expects 2.")]
|
||||||
[InlineData("{'type':4,'invocationId':'42','target':'foo','arguments':[ 'abc', 'xyz']}", "Error binding arguments. Make sure that the types of the provided values match the types of the hub method being invoked.")]
|
[InlineData("{'type':4,'invocationId':'42','target':'foo','arguments':[ 'abc', 'xyz']}", "Error binding arguments. Make sure that the types of the provided values match the types of the hub method being invoked.")]
|
||||||
public void ArgumentBindingErrors(string input, string expectedMessage)
|
public void ArgumentBindingErrors(string input, string expectedMessage)
|
||||||
|
|
|
||||||
|
|
@ -303,7 +303,7 @@ namespace Microsoft.AspNetCore.SignalR.Common.Tests.Internal.Protocol
|
||||||
AssertMessages(testData.Encoded, bytes);
|
AssertMessages(testData.Encoded, bytes);
|
||||||
|
|
||||||
// Unframe the message to check the binary encoding
|
// Unframe the message to check the binary encoding
|
||||||
ReadOnlySpan<byte> byteSpan = bytes.AsSpan();
|
ReadOnlyMemory<byte> byteSpan = bytes;
|
||||||
Assert.True(BinaryMessageParser.TryParseMessage(ref byteSpan, out var unframed));
|
Assert.True(BinaryMessageParser.TryParseMessage(ref byteSpan, out var unframed));
|
||||||
|
|
||||||
// Check the baseline binary encoding, use Assert.True in order to configure the error message
|
// Check the baseline binary encoding, use Assert.True in order to configure the error message
|
||||||
|
|
@ -413,7 +413,7 @@ namespace Microsoft.AspNetCore.SignalR.Common.Tests.Internal.Protocol
|
||||||
AssertMessages(Array(HubProtocolConstants.CompletionMessageType, Map(), "0", 3, Array(42)), result);
|
AssertMessages(Array(HubProtocolConstants.CompletionMessageType, Map(), "0", 3, Array(42)), result);
|
||||||
}
|
}
|
||||||
|
|
||||||
private static void AssertMessages(MessagePackObject expectedOutput, ReadOnlySpan<byte> bytes)
|
private static void AssertMessages(MessagePackObject expectedOutput, ReadOnlyMemory<byte> bytes)
|
||||||
{
|
{
|
||||||
Assert.True(BinaryMessageParser.TryParseMessage(ref bytes, out var message));
|
Assert.True(BinaryMessageParser.TryParseMessage(ref bytes, out var message));
|
||||||
var obj = Unpack(message.ToArray());
|
var obj = Unpack(message.ToArray());
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue