Reduce the per message read allocations (#1635)
- Introduced Utf8BufferTextReader that writes buffers directly into the char[] allocated by JSON.NET when reading via the JsonReader. - Use IArrayPool implementation over ArrayPool<char> when reading incomming messages. - Replaced JToken parsing with manual parsing using JsonTextReader. - Added tests for parsing incoming JSON messages with out of order properties. - Make access to message headers lazy - Changed IHubProtocol.TryParseMessage to be ReadOnlyMemory<byte> instead of ReadOnlySpan<byte>
This commit is contained in:
parent
881703e4c0
commit
b792fcb4ef
|
|
@ -52,7 +52,7 @@ namespace Microsoft.AspNetCore.SignalR.Microbenchmarks
|
|||
public string Name { 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;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -40,7 +40,7 @@ namespace Microsoft.AspNetCore.SignalR.Microbenchmarks
|
|||
[Benchmark]
|
||||
public void SingleBinaryMessage()
|
||||
{
|
||||
ReadOnlySpan<byte> buffer = _binaryInput;
|
||||
ReadOnlyMemory<byte> buffer = _binaryInput;
|
||||
if (!BinaryMessageParser.TryParseMessage(ref buffer, out _))
|
||||
{
|
||||
throw new InvalidOperationException("Failed to parse");
|
||||
|
|
@ -50,7 +50,7 @@ namespace Microsoft.AspNetCore.SignalR.Microbenchmarks
|
|||
[Benchmark]
|
||||
public void SingleTextMessage()
|
||||
{
|
||||
ReadOnlySpan<byte> buffer = _textInput;
|
||||
ReadOnlyMemory<byte> buffer = _textInput;
|
||||
if (!TextMessageParser.TryParseMessage(ref buffer, out _))
|
||||
{
|
||||
throw new InvalidOperationException("Failed to parse");
|
||||
|
|
|
|||
|
|
@ -7,13 +7,13 @@
|
|||
"@std/esm": {
|
||||
"version": "0.18.0",
|
||||
"resolved": "https://registry.npmjs.org/@std/esm/-/esm-0.18.0.tgz",
|
||||
"integrity": "sha512-oeHSSVp/WxC08ngpKgyYR4LcI0+EBwZiJcB58jvIqyJnOGxudSkxTgAQKsVfpNsMXfOoILgu9PWhuzIZ8GQEjw==",
|
||||
"integrity": "sha1-4hK1Zcdl+Tsp7FIqSToZLOeuK6Y=",
|
||||
"dev": true
|
||||
},
|
||||
"@types/debug": {
|
||||
"version": "0.0.30",
|
||||
"resolved": "https://registry.npmjs.org/@types/debug/-/debug-0.0.30.tgz",
|
||||
"integrity": "sha512-orGL5LXERPYsLov6CWs3Fh6203+dXzJkR7OnddIr2514Hsecwc8xRpzCapshBbKFImCsvS/mk6+FWiN5LyZJAQ==",
|
||||
"integrity": "sha1-3B5A9687nIFQE6eGDmJS9jUqhN8=",
|
||||
"dev": true
|
||||
},
|
||||
"@types/node": {
|
||||
|
|
@ -31,7 +31,7 @@
|
|||
"@types/strip-json-comments": {
|
||||
"version": "0.0.30",
|
||||
"resolved": "https://registry.npmjs.org/@types/strip-json-comments/-/strip-json-comments-0.0.30.tgz",
|
||||
"integrity": "sha512-7NQmHra/JILCd1QqpSzl8+mJRc8ZHz3uDm8YV1Ks9IhK0epEiTw8aIErbvH9PI+6XbqhyIQy3462nEsn7UVzjQ==",
|
||||
"integrity": "sha1-mqMMBNshKpoGSdaub9UKzMQHSKE=",
|
||||
"dev": true
|
||||
},
|
||||
"ansi-styles": {
|
||||
|
|
@ -69,7 +69,7 @@
|
|||
"color-convert": {
|
||||
"version": "1.9.1",
|
||||
"resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.1.tgz",
|
||||
"integrity": "sha512-mjGanIiwQJskCC18rPR6OmrZ6fm2Lc7PeGFYwCmy5J34wC6F1PzdGL6xeMfmgicfYcNLGuVFA3WzXtIDCQSZxQ==",
|
||||
"integrity": "sha1-wSYRB66y8pTr/+ye2eytUppgl+0=",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"color-name": "1.1.3"
|
||||
|
|
@ -90,7 +90,7 @@
|
|||
"debug": {
|
||||
"version": "3.1.0",
|
||||
"resolved": "https://registry.npmjs.org/debug/-/debug-3.1.0.tgz",
|
||||
"integrity": "sha512-OX8XqP7/1a9cqkxYw2yXss15f26NKWBpDXQd0/uK/KPqdQhxbPa994hnzjcE2VqQpDslf55723cKPUOGSmMY3g==",
|
||||
"integrity": "sha1-W7WgZyYotkFJVmuhaBnmFRjGcmE=",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"ms": "2.0.0"
|
||||
|
|
@ -300,19 +300,19 @@
|
|||
"safe-buffer": {
|
||||
"version": "5.1.1",
|
||||
"resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.1.tgz",
|
||||
"integrity": "sha512-kKvNJn6Mm93gAczWVJg7wH+wGYWNrDHdWvpUmHyEsgCtIwwo3bqPtV4tR5tuPaUhTOo/kvhVwd8XwwOllGYkbg==",
|
||||
"integrity": "sha1-iTMSr2myEj3vcfV4iQAWce6yyFM=",
|
||||
"dev": true
|
||||
},
|
||||
"source-map": {
|
||||
"version": "0.6.1",
|
||||
"resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz",
|
||||
"integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==",
|
||||
"integrity": "sha1-dHIq8y6WFOnCh6jQu95IteLxomM=",
|
||||
"dev": true
|
||||
},
|
||||
"source-map-support": {
|
||||
"version": "0.5.3",
|
||||
"resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.3.tgz",
|
||||
"integrity": "sha512-eKkTgWYeBOQqFGXRfKabMFdnWepo51vWqEdoeikaEPFiJC7MCU5j2h4+6Q8npkZTeLGbSyecZvRxiSoWl3rh+w==",
|
||||
"integrity": "sha1-Kz1f/ymM+k0a/X1DUtVp6aAVjnY=",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"source-map": "0.6.1"
|
||||
|
|
@ -321,7 +321,7 @@
|
|||
"split": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/split/-/split-1.0.1.tgz",
|
||||
"integrity": "sha512-mTyOoPbrivtXnwnIxZRFYRrPNtEFKlpB2fvjSnCQUiAA6qAZzqwna5envK4uk6OIeP17CsdF3rSBGYVBsU0Tkg==",
|
||||
"integrity": "sha1-YFvZvjA6pZ+zX5Ip++oN3snqB9k=",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"through": "2.3.8"
|
||||
|
|
@ -410,7 +410,7 @@
|
|||
"process-nextick-args": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.0.tgz",
|
||||
"integrity": "sha512-MtEC1TqN0EU5nephaJ4rAtThHtC86dNN9qCuEhtshvpVBkAW5ZO7BASN9REnF9eoXGcRub+pFuKEpOHE+HbEMw==",
|
||||
"integrity": "sha1-o31zL0JxtKsa0HDTVQjoKQeI/6o=",
|
||||
"dev": true
|
||||
},
|
||||
"readable-stream": {
|
||||
|
|
@ -431,7 +431,7 @@
|
|||
"string_decoder": {
|
||||
"version": "1.0.3",
|
||||
"resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.0.3.tgz",
|
||||
"integrity": "sha512-4AH6Z5fzNNBcH+6XDMfA/BTt87skxqJlO0lAh3Dker5zThcAxG6mKz+iGu308UKoPPQ8Dcqx/4JhujzltRa+hQ==",
|
||||
"integrity": "sha1-D8Z9fBQYJd6UKC3VNr7GubzoYKs=",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"safe-buffer": "5.1.1"
|
||||
|
|
@ -473,7 +473,7 @@
|
|||
"tap-teamcity": {
|
||||
"version": "3.0.2",
|
||||
"resolved": "https://registry.npmjs.org/tap-teamcity/-/tap-teamcity-3.0.2.tgz",
|
||||
"integrity": "sha512-FI26a4CGNx9LWx2vRh3fLNrel1GJm5smBVJl2tzabTwGrmX9d+KHWP2O9xdKgMtH5IOBpN6goy9Yh4P2NRaoQw==",
|
||||
"integrity": "sha1-727fs5PvMX09DcOZHugkurlIw54=",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"@std/esm": "0.18.0",
|
||||
|
|
@ -603,7 +603,7 @@
|
|||
"ts-node": {
|
||||
"version": "4.1.0",
|
||||
"resolved": "https://registry.npmjs.org/ts-node/-/ts-node-4.1.0.tgz",
|
||||
"integrity": "sha512-xcZH12oVg9PShKhy3UHyDmuDLV3y7iKwX25aMVPt1SIXSuAfWkFiGPEkg+th8R4YKW/QCxDoW7lJdb15lx6QWg==",
|
||||
"integrity": "sha1-NtlSnHuQu5kzBsQIzQf3dD3iBxI=",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"arrify": "1.0.1",
|
||||
|
|
@ -621,7 +621,7 @@
|
|||
"tsconfig": {
|
||||
"version": "7.0.0",
|
||||
"resolved": "https://registry.npmjs.org/tsconfig/-/tsconfig-7.0.0.tgz",
|
||||
"integrity": "sha512-vZXmzPrL+EmC4T/4rVlT2jNVMWCi/O4DIiSj3UHg1OE5kCKbk4mfrXc6dZksLgRM/TZlKnousKH9bbTazUWRRw==",
|
||||
"integrity": "sha1-hFOIdaTcIW5cSlQys6Tew9VOkbc=",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"@types/strip-bom": "3.0.0",
|
||||
|
|
|
|||
|
|
@ -9,7 +9,7 @@ namespace Microsoft.AspNetCore.SignalR.Internal.Formatters
|
|||
{
|
||||
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)
|
||||
{
|
||||
|
|
@ -33,10 +33,12 @@ namespace Microsoft.AspNetCore.SignalR.Internal.Formatters
|
|||
var numBytes = 0;
|
||||
|
||||
var lengthPrefixBuffer = buffer.Slice(0, Math.Min(MaxLengthPrefixSize, buffer.Length));
|
||||
var span = lengthPrefixBuffer.Span;
|
||||
|
||||
byte byteRead;
|
||||
do
|
||||
{
|
||||
byteRead = lengthPrefixBuffer[numBytes];
|
||||
byteRead = span[numBytes];
|
||||
length = length | (((uint)(byteRead & 0x7f)) << (numBytes * 7));
|
||||
numBytes++;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -7,9 +7,9 @@ namespace Microsoft.AspNetCore.SignalR.Internal.Formatters
|
|||
{
|
||||
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)
|
||||
{
|
||||
payload = default;
|
||||
|
|
|
|||
|
|
@ -7,15 +7,7 @@ namespace Microsoft.AspNetCore.SignalR.Internal.Protocol
|
|||
{
|
||||
public abstract class HubInvocationMessage : HubMessage
|
||||
{
|
||||
private Dictionary<string, string> _headers;
|
||||
|
||||
public IDictionary<string, string> Headers
|
||||
{
|
||||
get
|
||||
{
|
||||
return _headers ?? (_headers = new Dictionary<string, string>());
|
||||
}
|
||||
}
|
||||
public IDictionary<string, string> Headers { get; set; }
|
||||
|
||||
public string InvocationId { get; }
|
||||
|
||||
|
|
|
|||
|
|
@ -14,7 +14,7 @@ namespace Microsoft.AspNetCore.SignalR.Internal.Protocol
|
|||
|
||||
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);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,28 @@
|
|||
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 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))
|
||||
{
|
||||
// TODO: Need a span-native JSON parser!
|
||||
using (var memoryStream = new MemoryStream(payload.ToArray()))
|
||||
{
|
||||
messages.Add(ParseMessage(memoryStream, binder));
|
||||
}
|
||||
var textReader = new Utf8BufferTextReader(payload);
|
||||
messages.Add(ParseMessage(textReader, binder));
|
||||
}
|
||||
|
||||
return messages.Count > 0;
|
||||
|
|
@ -67,69 +64,264 @@ namespace Microsoft.AspNetCore.SignalR.Internal.Protocol
|
|||
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
|
||||
var token = JToken.ReadFrom(reader);
|
||||
// We parse using the JsonTextReader directly but this has a problem. Some of our properties are dependent on other properties
|
||||
// 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))
|
||||
{
|
||||
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 '{token?.Type}'. Expected a JSON Object.");
|
||||
throw new InvalidDataException($"Unexpected JSON Token Type '{JsonUtils.GetTokenString(reader.TokenType)}'. 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)
|
||||
do
|
||||
{
|
||||
case HubProtocolConstants.InvocationMessageType:
|
||||
return BindInvocationMessage(json, binder);
|
||||
case HubProtocolConstants.StreamInvocationMessageType:
|
||||
return BindStreamInvocationMessage(json, binder);
|
||||
case HubProtocolConstants.StreamItemMessageType:
|
||||
return BindStreamItemMessage(json, binder);
|
||||
case HubProtocolConstants.CompletionMessageType:
|
||||
return BindCompletionMessage(json, binder);
|
||||
case HubProtocolConstants.CancelInvocationMessageType:
|
||||
return BindCancelInvocationMessage(json);
|
||||
case HubProtocolConstants.PingMessageType:
|
||||
return PingMessage.Instance;
|
||||
default:
|
||||
throw new InvalidDataException($"Unknown message type: {type}");
|
||||
switch (reader.TokenType)
|
||||
{
|
||||
case JsonToken.PropertyName:
|
||||
string memberName = reader.Value.ToString();
|
||||
|
||||
switch (memberName)
|
||||
{
|
||||
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));
|
||||
}
|
||||
catch (JsonReaderException jrex)
|
||||
|
||||
HubMessage message = null;
|
||||
|
||||
switch (type)
|
||||
{
|
||||
throw new InvalidDataException("Error reading JSON.", jrex);
|
||||
case HubProtocolConstants.InvocationMessageType:
|
||||
{
|
||||
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:
|
||||
{
|
||||
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:
|
||||
if (itemToken != null)
|
||||
{
|
||||
var returnType = binder.GetReturnType(invocationId);
|
||||
item = itemToken.ToObject(returnType, PayloadSerializer);
|
||||
}
|
||||
|
||||
message = BindStreamItemMessage(invocationId, item, hasItem, binder);
|
||||
break;
|
||||
case HubProtocolConstants.CompletionMessageType:
|
||||
if (resultToken != null)
|
||||
{
|
||||
var returnType = binder.GetReturnType(invocationId);
|
||||
result = resultToken.ToObject(returnType, PayloadSerializer);
|
||||
}
|
||||
|
||||
message = BindCompletionMessage(invocationId, error, result, hasResult, binder);
|
||||
break;
|
||||
case HubProtocolConstants.CancelInvocationMessageType:
|
||||
message = BindCancelInvocationMessage(invocationId);
|
||||
break;
|
||||
case HubProtocolConstants.PingMessageType:
|
||||
return PingMessage.Instance;
|
||||
case null:
|
||||
throw new InvalidDataException($"Missing required property '{TypePropertyName}'.");
|
||||
default:
|
||||
throw new InvalidDataException($"Unknown message type: {type}");
|
||||
}
|
||||
|
||||
return ApplyHeaders(message, headers);
|
||||
}
|
||||
catch (JsonReaderException 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];
|
||||
if (headersProp != null)
|
||||
var headers = new Dictionary<string, string>();
|
||||
|
||||
if (reader.TokenType != JsonToken.StartObject)
|
||||
{
|
||||
if (headersProp.Type != JTokenType.Object)
|
||||
throw new InvalidDataException($"Expected '{HeadersPropertyName}' to be of type {JTokenType.Object}.");
|
||||
}
|
||||
|
||||
while (reader.Read())
|
||||
{
|
||||
switch (reader.TokenType)
|
||||
{
|
||||
throw new InvalidDataException($"Expected '{HeadersPropertyName}' to be of type {JTokenType.Object}.");
|
||||
}
|
||||
var headersObj = headersProp.Value<JObject>();
|
||||
foreach (var prop in headersObj)
|
||||
{
|
||||
if (prop.Value.Type != JTokenType.String)
|
||||
{
|
||||
throw new InvalidDataException($"Expected header '{prop.Key}' to be of type {JTokenType.String}.");
|
||||
}
|
||||
headers[prop.Key] = prop.Value.Value<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[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)
|
||||
|
|
@ -176,7 +368,7 @@ namespace Microsoft.AspNetCore.SignalR.Internal.Protocol
|
|||
|
||||
private void WriteHeaders(JsonTextWriter writer, HubInvocationMessage message)
|
||||
{
|
||||
if (message.Headers.Count > 0)
|
||||
if (message.Headers != null && message.Headers.Count > 0)
|
||||
{
|
||||
writer.WritePropertyName(HeadersPropertyName);
|
||||
writer.WriteStartObject();
|
||||
|
|
@ -260,95 +452,122 @@ namespace Microsoft.AspNetCore.SignalR.Internal.Protocol
|
|||
writer.WriteValue(type);
|
||||
}
|
||||
|
||||
private InvocationMessage BindInvocationMessage(JObject json, IInvocationBinder binder)
|
||||
private HubMessage BindCancelInvocationMessage(string invocationId)
|
||||
{
|
||||
var invocationId = JsonUtils.GetOptionalProperty<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);
|
||||
|
||||
InvocationMessage message;
|
||||
try
|
||||
if (string.IsNullOrEmpty(invocationId))
|
||||
{
|
||||
var arguments = BindArguments(args, paramTypes);
|
||||
message = new InvocationMessage(invocationId, target, argumentBindingException: null, arguments: arguments);
|
||||
throw new InvalidDataException($"Missing required property '{InvocationIdPropertyName}'.");
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
message = new InvocationMessage(invocationId, target, ExceptionDispatchInfo.Capture(ex));
|
||||
}
|
||||
ReadHeaders(json, message.Headers);
|
||||
return message;
|
||||
|
||||
return new CancelInvocationMessage(invocationId);
|
||||
}
|
||||
|
||||
private StreamInvocationMessage BindStreamInvocationMessage(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);
|
||||
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
|
||||
if (string.IsNullOrEmpty(invocationId))
|
||||
{
|
||||
var arguments = BindArguments(args, paramTypes);
|
||||
message = new StreamInvocationMessage(invocationId, target, argumentBindingException: null, arguments: arguments);
|
||||
throw new InvalidDataException($"Missing required property '{InvocationIdPropertyName}'.");
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
message = new StreamInvocationMessage(invocationId, target, ExceptionDispatchInfo.Capture(ex));
|
||||
}
|
||||
ReadHeaders(json, message.Headers);
|
||||
return message;
|
||||
}
|
||||
|
||||
private StreamItemMessage BindStreamItemMessage(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);
|
||||
var message = new StreamItemMessage(invocationId, result?.ToObject(returnType, PayloadSerializer));
|
||||
ReadHeaders(json, message.Headers);
|
||||
return message;
|
||||
}
|
||||
|
||||
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)
|
||||
if (error != null && hasResult)
|
||||
{
|
||||
throw new InvalidDataException("The 'error' and 'result' properties are mutually exclusive.");
|
||||
}
|
||||
|
||||
CompletionMessage message;
|
||||
if (resultProp == null)
|
||||
if (hasResult)
|
||||
{
|
||||
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;
|
||||
|
||||
return new CompletionMessage(invocationId, error, result: null, hasResult: false);
|
||||
}
|
||||
|
||||
private CancelInvocationMessage BindCancelInvocationMessage(JObject json)
|
||||
private HubMessage BindStreamItemMessage(string invocationId, object item, bool hasItem, IInvocationBinder binder)
|
||||
{
|
||||
var invocationId = JsonUtils.GetRequiredProperty<string>(json, InvocationIdPropertyName, JTokenType.String);
|
||||
var message = new CancelInvocationMessage(invocationId);
|
||||
ReadHeaders(json, message.Headers);
|
||||
return message;
|
||||
if (string.IsNullOrEmpty(invocationId))
|
||||
{
|
||||
throw new InvalidDataException($"Missing required property '{InvocationIdPropertyName}'.");
|
||||
}
|
||||
|
||||
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)
|
||||
|
|
@ -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()
|
||||
{
|
||||
return new JsonSerializerSettings { ContractResolver = new CamelCasePropertyNamesContractResolver() };
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@
|
|||
|
||||
using System;
|
||||
using System.IO;
|
||||
using Newtonsoft.Json;
|
||||
using Newtonsoft.Json.Linq;
|
||||
|
||||
namespace Microsoft.AspNetCore.SignalR.Internal.Protocol
|
||||
|
|
@ -41,5 +42,62 @@ namespace Microsoft.AspNetCore.SignalR.Internal.Protocol
|
|||
}
|
||||
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.Buffers;
|
||||
using System.Collections;
|
||||
using System.IO;
|
||||
using System.Text;
|
||||
using Microsoft.AspNetCore.SignalR.Internal.Formatters;
|
||||
|
|
@ -31,27 +30,27 @@ namespace Microsoft.AspNetCore.SignalR.Internal.Protocol
|
|||
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))
|
||||
{
|
||||
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)))
|
||||
{
|
||||
var token = JToken.ReadFrom(reader);
|
||||
if (token == null || token.Type != JTokenType.Object)
|
||||
{
|
||||
throw new InvalidDataException($"Unexpected JSON Token Type '{token?.Type}'. Expected a JSON Object.");
|
||||
}
|
||||
reader.ArrayPool = JsonArrayPool<char>.Shared;
|
||||
|
||||
var negotiationJObject = (JObject)token;
|
||||
var protocol = JsonUtils.GetRequiredProperty<string>(negotiationJObject, ProtocolPropertyName);
|
||||
negotiationMessage = new NegotiationMessage(protocol);
|
||||
var token = JToken.ReadFrom(reader);
|
||||
if (token == null || token.Type != JTokenType.Object)
|
||||
{
|
||||
throw new InvalidDataException($"Unexpected JSON Token Type '{token?.Type}'. Expected a JSON Object.");
|
||||
}
|
||||
|
||||
var negotiationJObject = (JObject)token;
|
||||
var protocol = JsonUtils.GetRequiredProperty<string>(negotiationJObject, ProtocolPropertyName);
|
||||
negotiationMessage = new NegotiationMessage(protocol);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
|
@ -75,7 +74,7 @@ namespace Microsoft.AspNetCore.SignalR.Internal.Protocol
|
|||
|
||||
var memory = buffer.IsSingleSegment ? buffer.First : buffer.ToArray();
|
||||
|
||||
return TryParseMessage(memory.Span, out negotiationMessage);
|
||||
return TryParseMessage(memory, out negotiationMessage);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,54 @@
|
|||
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;
|
||||
}
|
||||
|
||||
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))
|
||||
{
|
||||
|
|
@ -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[header.Key] = header.Value;
|
||||
}
|
||||
destination.Headers = source;
|
||||
}
|
||||
|
||||
return destination;
|
||||
|
|
|
|||
|
|
@ -255,7 +255,7 @@ namespace Microsoft.AspNetCore.SignalR.Client.Tests
|
|||
|
||||
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;
|
||||
if (_error != null)
|
||||
|
|
|
|||
|
|
@ -113,7 +113,7 @@ namespace Microsoft.AspNetCore.Sockets.Tests.Internal.Formatters
|
|||
{
|
||||
BinaryMessageFormatter.WriteLengthPrefix(payload.Length, ms);
|
||||
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.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")]
|
||||
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.Equal(0, span.Length);
|
||||
|
||||
|
|
@ -52,7 +52,7 @@ namespace Microsoft.AspNetCore.SignalR.Common.Tests.Internal.Formatters
|
|||
})]
|
||||
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.Equal(0, span.Length);
|
||||
Assert.Equal(payload, message.ToArray());
|
||||
|
|
@ -66,7 +66,7 @@ namespace Microsoft.AspNetCore.SignalR.Common.Tests.Internal.Formatters
|
|||
{
|
||||
var ex = Assert.Throws<FormatException>(() =>
|
||||
{
|
||||
var buffer = new ReadOnlySpan<byte>(payload);
|
||||
var buffer = new ReadOnlyMemory<byte>(payload);
|
||||
BinaryMessageParser.TryParseMessage(ref buffer, out var 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
|
||||
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));
|
||||
}
|
||||
|
||||
|
|
@ -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,
|
||||
};
|
||||
|
||||
ReadOnlySpan<byte> buffer = encoded;
|
||||
ReadOnlyMemory<byte> buffer = encoded;
|
||||
var messages = new List<byte[]>();
|
||||
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
|
||||
public void ReadIncompleteMessages(byte[] encoded)
|
||||
{
|
||||
ReadOnlySpan<byte> buffer = encoded;
|
||||
ReadOnlyMemory<byte> buffer = encoded;
|
||||
Assert.False(BinaryMessageParser.TryParseMessage(ref buffer, out var message));
|
||||
Assert.Equal(encoded.Length, buffer.Length);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -13,7 +13,7 @@ namespace Microsoft.AspNetCore.SignalR.Common.Tests.Internal.Formatters
|
|||
[Fact]
|
||||
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.Equal("ABC", Encoding.UTF8.GetString(payload.ToArray()));
|
||||
|
|
@ -23,14 +23,14 @@ namespace Microsoft.AspNetCore.SignalR.Common.Tests.Internal.Formatters
|
|||
[Fact]
|
||||
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));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
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.Equal("ABC", Encoding.UTF8.GetString(payload.ToArray()));
|
||||
Assert.True(TextMessageParser.TryParseMessage(ref message, out payload));
|
||||
|
|
@ -40,7 +40,7 @@ namespace Microsoft.AspNetCore.SignalR.Common.Tests.Internal.Formatters
|
|||
[Fact]
|
||||
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.Equal("ABC", Encoding.UTF8.GetString(payload.ToArray()));
|
||||
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)
|
||||
{
|
||||
if (hubMessage.Headers == null)
|
||||
{
|
||||
hubMessage.Headers = new Dictionary<string, string>();
|
||||
}
|
||||
|
||||
hubMessage.Headers[header.Key] = header.Value;
|
||||
}
|
||||
|
||||
return hubMessage;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -82,6 +82,14 @@ namespace Microsoft.AspNetCore.SignalR.Common.Tests.Internal.Protocol
|
|||
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]
|
||||
[MemberData(nameof(ProtocolTestData))]
|
||||
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':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)
|
||||
{
|
||||
input = Frame(input);
|
||||
|
|
@ -180,9 +189,25 @@ namespace Microsoft.AspNetCore.SignalR.Common.Tests.Internal.Protocol
|
|||
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]
|
||||
[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','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':[ '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)
|
||||
|
|
|
|||
|
|
@ -303,7 +303,7 @@ namespace Microsoft.AspNetCore.SignalR.Common.Tests.Internal.Protocol
|
|||
AssertMessages(testData.Encoded, bytes);
|
||||
|
||||
// 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));
|
||||
|
||||
// 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);
|
||||
}
|
||||
|
||||
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));
|
||||
var obj = Unpack(message.ToArray());
|
||||
|
|
|
|||
Loading…
Reference in New Issue