Use MessagePackReader \ MessagePackWriter to implement IHubProtocol for server-side Blazor (#8687)
* Use MessagePackReader \ MessagePackWriter to implement IHubProtocol for server-side Blazor Fixes #7311
This commit is contained in:
parent
7f4dd27551
commit
d86c9b3f07
|
|
@ -1,3 +1,7 @@
|
|||
[submodule "googletest"]
|
||||
path = src/submodules/googletest
|
||||
url = https://github.com/google/googletest
|
||||
|
||||
[submodule "src/submodules/MessagePack-CSharp"]
|
||||
path = src/submodules/MessagePack-CSharp
|
||||
url = https://github.com/aspnet/MessagePack-CSharp.git
|
||||
|
|
|
|||
|
|
@ -42,9 +42,11 @@ async function boot() {
|
|||
}
|
||||
|
||||
async function initializeConnection(circuitHandlers: CircuitHandler[]): Promise<signalR.HubConnection> {
|
||||
const hubProtocol = new MessagePackHubProtocol();
|
||||
(hubProtocol as any).name = 'blazorpack';
|
||||
const connection = new signalR.HubConnectionBuilder()
|
||||
.withUrl('_blazor')
|
||||
.withHubProtocol(new MessagePackHubProtocol())
|
||||
.withHubProtocol(hubProtocol)
|
||||
.configureLogging(signalR.LogLevel.Information)
|
||||
.build();
|
||||
|
||||
|
|
|
|||
|
|
@ -0,0 +1,193 @@
|
|||
// 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.
|
||||
|
||||
// Copied from https://github.com/dotnet/corefx/blob/b0751dcd4a419ba6731dcaa7d240a8a1946c934c/src/System.Text.Json/src/System/Text/Json/Serialization/ArrayBufferWriter.cs
|
||||
|
||||
using System;
|
||||
using System.Buffers;
|
||||
using System.Diagnostics;
|
||||
|
||||
namespace Microsoft.AspNetCore.Components.Server.BlazorPack
|
||||
{
|
||||
// Note: this is currently an internal class that will be replaced with a shared version.
|
||||
internal sealed class ArrayBufferWriter<T> : IBufferWriter<T>, IDisposable
|
||||
{
|
||||
private T[] _rentedBuffer;
|
||||
private int _index;
|
||||
|
||||
private const int MinimumBufferSize = 256;
|
||||
|
||||
public ArrayBufferWriter()
|
||||
{
|
||||
_rentedBuffer = ArrayPool<T>.Shared.Rent(MinimumBufferSize);
|
||||
_index = 0;
|
||||
}
|
||||
|
||||
public ArrayBufferWriter(int initialCapacity)
|
||||
{
|
||||
if (initialCapacity <= 0)
|
||||
{
|
||||
throw new ArgumentException(nameof(initialCapacity));
|
||||
}
|
||||
|
||||
_rentedBuffer = ArrayPool<T>.Shared.Rent(initialCapacity);
|
||||
_index = 0;
|
||||
}
|
||||
|
||||
public ReadOnlyMemory<T> WrittenMemory
|
||||
{
|
||||
get
|
||||
{
|
||||
CheckIfDisposed();
|
||||
|
||||
return _rentedBuffer.AsMemory(0, _index);
|
||||
}
|
||||
}
|
||||
|
||||
public int WrittenCount
|
||||
{
|
||||
get
|
||||
{
|
||||
CheckIfDisposed();
|
||||
|
||||
return _index;
|
||||
}
|
||||
}
|
||||
|
||||
public int Capacity
|
||||
{
|
||||
get
|
||||
{
|
||||
CheckIfDisposed();
|
||||
|
||||
return _rentedBuffer.Length;
|
||||
}
|
||||
}
|
||||
|
||||
public int FreeCapacity
|
||||
{
|
||||
get
|
||||
{
|
||||
CheckIfDisposed();
|
||||
|
||||
return _rentedBuffer.Length - _index;
|
||||
}
|
||||
}
|
||||
|
||||
public void Clear()
|
||||
{
|
||||
CheckIfDisposed();
|
||||
|
||||
ClearHelper();
|
||||
}
|
||||
|
||||
private void ClearHelper()
|
||||
{
|
||||
Debug.Assert(_rentedBuffer != null);
|
||||
|
||||
_rentedBuffer.AsSpan(0, _index).Clear();
|
||||
_index = 0;
|
||||
}
|
||||
|
||||
// Returns the rented buffer back to the pool
|
||||
public void Dispose()
|
||||
{
|
||||
if (_rentedBuffer == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
ClearHelper();
|
||||
ArrayPool<T>.Shared.Return(_rentedBuffer);
|
||||
_rentedBuffer = null;
|
||||
}
|
||||
|
||||
private void CheckIfDisposed()
|
||||
{
|
||||
if (_rentedBuffer == null)
|
||||
{
|
||||
ThrowObjectDisposedException();
|
||||
}
|
||||
}
|
||||
|
||||
private static void ThrowObjectDisposedException()
|
||||
{
|
||||
throw new ObjectDisposedException(nameof(ArrayBufferWriter<T>));
|
||||
}
|
||||
|
||||
public void Advance(int count)
|
||||
{
|
||||
CheckIfDisposed();
|
||||
|
||||
if (count < 0)
|
||||
throw new ArgumentException(nameof(count));
|
||||
|
||||
if (_index > _rentedBuffer.Length - count)
|
||||
{
|
||||
ThrowInvalidOperationException(_rentedBuffer.Length);
|
||||
}
|
||||
|
||||
_index += count;
|
||||
}
|
||||
|
||||
public Memory<T> GetMemory(int sizeHint = 0)
|
||||
{
|
||||
CheckIfDisposed();
|
||||
|
||||
CheckAndResizeBuffer(sizeHint);
|
||||
return _rentedBuffer.AsMemory(_index);
|
||||
}
|
||||
|
||||
public Span<T> GetSpan(int sizeHint = 0)
|
||||
{
|
||||
CheckIfDisposed();
|
||||
|
||||
CheckAndResizeBuffer(sizeHint);
|
||||
return _rentedBuffer.AsSpan(_index);
|
||||
}
|
||||
|
||||
private void CheckAndResizeBuffer(int sizeHint)
|
||||
{
|
||||
Debug.Assert(_rentedBuffer != null);
|
||||
|
||||
if (sizeHint < 0)
|
||||
{
|
||||
throw new ArgumentException(nameof(sizeHint));
|
||||
}
|
||||
|
||||
if (sizeHint == 0)
|
||||
{
|
||||
sizeHint = MinimumBufferSize;
|
||||
}
|
||||
|
||||
var availableSpace = _rentedBuffer.Length - _index;
|
||||
|
||||
if (sizeHint > availableSpace)
|
||||
{
|
||||
var growBy = Math.Max(sizeHint, _rentedBuffer.Length);
|
||||
|
||||
var newSize = checked(_rentedBuffer.Length + growBy);
|
||||
|
||||
var oldBuffer = _rentedBuffer;
|
||||
|
||||
_rentedBuffer = ArrayPool<T>.Shared.Rent(newSize);
|
||||
|
||||
Debug.Assert(oldBuffer.Length >= _index);
|
||||
Debug.Assert(_rentedBuffer.Length >= _index);
|
||||
|
||||
var previousBuffer = oldBuffer.AsSpan(0, _index);
|
||||
previousBuffer.CopyTo(_rentedBuffer);
|
||||
previousBuffer.Clear();
|
||||
ArrayPool<T>.Shared.Return(oldBuffer);
|
||||
}
|
||||
|
||||
Debug.Assert(_rentedBuffer.Length - _index > 0);
|
||||
Debug.Assert(_rentedBuffer.Length - _index >= sizeHint);
|
||||
}
|
||||
|
||||
private static void ThrowInvalidOperationException(int capacity)
|
||||
{
|
||||
throw new InvalidOperationException($"Cannot advance past the end of the buffer, which has a size of {capacity}.");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,638 @@
|
|||
// 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 System.Collections.Generic;
|
||||
using System.Diagnostics;
|
||||
using System.IO;
|
||||
using System.Runtime.CompilerServices;
|
||||
using System.Runtime.ExceptionServices;
|
||||
using MessagePack;
|
||||
using Microsoft.AspNetCore.Connections;
|
||||
using Microsoft.AspNetCore.Internal;
|
||||
using Microsoft.AspNetCore.SignalR;
|
||||
using Microsoft.AspNetCore.SignalR.Protocol;
|
||||
|
||||
namespace Microsoft.AspNetCore.Components.Server.BlazorPack
|
||||
{
|
||||
/// <summary>
|
||||
/// Implements the SignalR Hub Protocol using MessagePack with limited type support.
|
||||
/// </summary>
|
||||
internal sealed class BlazorPackHubProtocol : IHubProtocol
|
||||
{
|
||||
internal const string ProtocolName = "blazorpack";
|
||||
private const int ErrorResult = 1;
|
||||
private const int VoidResult = 2;
|
||||
private const int NonVoidResult = 3;
|
||||
|
||||
private static readonly int ProtocolVersion = 1;
|
||||
private static readonly int ProtocolMinorVersion = 0;
|
||||
|
||||
/// <inheritdoc />
|
||||
public string Name => ProtocolName;
|
||||
|
||||
/// <inheritdoc />
|
||||
public int Version => ProtocolVersion;
|
||||
|
||||
/// <inheritdoc />
|
||||
public int MinorVersion => ProtocolMinorVersion;
|
||||
|
||||
/// <inheritdoc />
|
||||
public TransferFormat TransferFormat => TransferFormat.Binary;
|
||||
|
||||
/// <inheritdoc />
|
||||
public bool IsVersionSupported(int version)
|
||||
{
|
||||
return version == Version;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public bool TryParseMessage(ref ReadOnlySequence<byte> input, IInvocationBinder binder, out HubMessage message)
|
||||
{
|
||||
if (!BinaryMessageParser.TryParseMessage(ref input, out var payload))
|
||||
{
|
||||
message = null;
|
||||
return false;
|
||||
}
|
||||
|
||||
var reader = new MessagePackReader(payload);
|
||||
|
||||
var itemCount = reader.ReadArrayHeader();
|
||||
var messageType = ReadInt32(ref reader, "messageType");
|
||||
|
||||
switch (messageType)
|
||||
{
|
||||
case HubProtocolConstants.InvocationMessageType:
|
||||
message = CreateInvocationMessage(ref reader, binder, itemCount);
|
||||
return true;
|
||||
case HubProtocolConstants.StreamInvocationMessageType:
|
||||
message = CreateStreamInvocationMessage(ref reader, binder, itemCount);
|
||||
return true;
|
||||
case HubProtocolConstants.StreamItemMessageType:
|
||||
message = CreateStreamItemMessage(ref reader, binder);
|
||||
return true;
|
||||
case HubProtocolConstants.CompletionMessageType:
|
||||
message = CreateCompletionMessage(ref reader, binder);
|
||||
return true;
|
||||
case HubProtocolConstants.CancelInvocationMessageType:
|
||||
message = CreateCancelInvocationMessage(ref reader);
|
||||
return true;
|
||||
case HubProtocolConstants.PingMessageType:
|
||||
message = PingMessage.Instance;
|
||||
return true;
|
||||
case HubProtocolConstants.CloseMessageType:
|
||||
message = CreateCloseMessage(ref reader);
|
||||
return true;
|
||||
default:
|
||||
// Future protocol changes can add message types, old clients can ignore them
|
||||
message = null;
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
private static HubMessage CreateInvocationMessage(ref MessagePackReader reader, IInvocationBinder binder, int itemCount)
|
||||
{
|
||||
var headers = ReadHeaders(ref reader);
|
||||
var invocationId = ReadString(ref reader, "invocationId");
|
||||
|
||||
// For MsgPack, we represent an empty invocation ID as an empty string,
|
||||
// so we need to normalize that to "null", which is what indicates a non-blocking invocation.
|
||||
if (string.IsNullOrEmpty(invocationId))
|
||||
{
|
||||
invocationId = null;
|
||||
}
|
||||
|
||||
var target = ReadString(ref reader, "target");
|
||||
|
||||
object[] arguments;
|
||||
try
|
||||
{
|
||||
var parameterTypes = binder.GetParameterTypes(target);
|
||||
arguments = BindArguments(ref reader, parameterTypes);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
return new InvocationBindingFailureMessage(invocationId, target, ExceptionDispatchInfo.Capture(ex));
|
||||
}
|
||||
|
||||
string[] streams = null;
|
||||
// Previous clients will send 5 items, so we check if they sent a stream array or not
|
||||
if (itemCount > 5)
|
||||
{
|
||||
streams = ReadStreamIds(ref reader);
|
||||
}
|
||||
|
||||
return ApplyHeaders(headers, new InvocationMessage(invocationId, target, arguments, streams));
|
||||
}
|
||||
|
||||
private static HubMessage CreateStreamInvocationMessage(ref MessagePackReader reader, IInvocationBinder binder, int itemCount)
|
||||
{
|
||||
var headers = ReadHeaders(ref reader);
|
||||
var invocationId = ReadString(ref reader, "invocationId");
|
||||
var target = ReadString(ref reader, "target"); ;
|
||||
|
||||
object[] arguments;
|
||||
try
|
||||
{
|
||||
var parameterTypes = binder.GetParameterTypes(target);
|
||||
arguments = BindArguments(ref reader, parameterTypes);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
return new InvocationBindingFailureMessage(invocationId, target, ExceptionDispatchInfo.Capture(ex));
|
||||
}
|
||||
|
||||
string[] streams = null;
|
||||
// Previous clients will send 5 items, so we check if they sent a stream array or not
|
||||
if (itemCount > 5)
|
||||
{
|
||||
streams = ReadStreamIds(ref reader);
|
||||
}
|
||||
|
||||
return ApplyHeaders(headers, new StreamInvocationMessage(invocationId, target, arguments, streams));
|
||||
}
|
||||
|
||||
private static StreamItemMessage CreateStreamItemMessage(ref MessagePackReader reader, IInvocationBinder binder)
|
||||
{
|
||||
var headers = ReadHeaders(ref reader);
|
||||
var invocationId = ReadString(ref reader, "invocationId");
|
||||
|
||||
var itemType = binder.GetStreamItemType(invocationId);
|
||||
var value = DeserializeObject(ref reader, itemType, "item");
|
||||
return ApplyHeaders(headers, new StreamItemMessage(invocationId, value));
|
||||
}
|
||||
|
||||
private static CompletionMessage CreateCompletionMessage(ref MessagePackReader reader, IInvocationBinder binder)
|
||||
{
|
||||
var headers = ReadHeaders(ref reader);
|
||||
var invocationId = ReadString(ref reader, "invocationId");
|
||||
var resultKind = ReadInt32(ref reader, "resultKind");
|
||||
|
||||
string error = null;
|
||||
object result = null;
|
||||
var hasResult = false;
|
||||
|
||||
switch (resultKind)
|
||||
{
|
||||
case ErrorResult:
|
||||
error = ReadString(ref reader, "error");
|
||||
break;
|
||||
case NonVoidResult:
|
||||
var itemType = binder.GetReturnType(invocationId);
|
||||
result = DeserializeObject(ref reader, itemType, "argument");
|
||||
hasResult = true;
|
||||
break;
|
||||
case VoidResult:
|
||||
hasResult = false;
|
||||
break;
|
||||
default:
|
||||
throw new InvalidDataException("Invalid invocation result kind.");
|
||||
}
|
||||
|
||||
return ApplyHeaders(headers, new CompletionMessage(invocationId, error, result, hasResult));
|
||||
}
|
||||
|
||||
private static CancelInvocationMessage CreateCancelInvocationMessage(ref MessagePackReader reader)
|
||||
{
|
||||
var headers = ReadHeaders(ref reader);
|
||||
var invocationId = ReadString(ref reader, "invocationId");
|
||||
return ApplyHeaders(headers, new CancelInvocationMessage(invocationId));
|
||||
}
|
||||
|
||||
private static CloseMessage CreateCloseMessage(ref MessagePackReader reader)
|
||||
{
|
||||
var error = ReadString(ref reader, "error");
|
||||
return new CloseMessage(error);
|
||||
}
|
||||
|
||||
private static Dictionary<string, string> ReadHeaders(ref MessagePackReader reader)
|
||||
{
|
||||
var headerCount = ReadMapHeader(ref reader, "headers");
|
||||
if (headerCount == 0)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
var headers = new Dictionary<string, string>(StringComparer.Ordinal);
|
||||
for (var i = 0; i < headerCount; i++)
|
||||
{
|
||||
var key = ReadString(ref reader, $"headers[{i}].Key");
|
||||
var value = ReadString(ref reader, $"headers[{i}].Value");
|
||||
|
||||
headers[key] = value;
|
||||
}
|
||||
|
||||
return headers;
|
||||
}
|
||||
|
||||
private static string[] ReadStreamIds(ref MessagePackReader reader)
|
||||
{
|
||||
var streamIdCount = ReadArrayHeader(ref reader, "streamIds");
|
||||
|
||||
if (streamIdCount == 0)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
var streams = new List<string>();
|
||||
for (var i = 0; i < streamIdCount; i++)
|
||||
{
|
||||
streams.Add(reader.ReadString());
|
||||
}
|
||||
|
||||
return streams.ToArray();
|
||||
}
|
||||
|
||||
private static object[] BindArguments(ref MessagePackReader reader, IReadOnlyList<Type> parameterTypes)
|
||||
{
|
||||
var argumentCount = ReadArrayHeader(ref reader, "arguments");
|
||||
|
||||
if (parameterTypes.Count != argumentCount)
|
||||
{
|
||||
throw new InvalidDataException(
|
||||
$"Invocation provides {argumentCount} argument(s) but target expects {parameterTypes.Count}.");
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
var arguments = new object[argumentCount];
|
||||
for (var i = 0; i < argumentCount; i++)
|
||||
{
|
||||
arguments[i] = DeserializeObject(ref reader, parameterTypes[i], "argument");
|
||||
}
|
||||
|
||||
return arguments;
|
||||
}
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public void WriteMessage(HubMessage message, IBufferWriter<byte> output)
|
||||
{
|
||||
var writer = MemoryBufferWriter.Get();
|
||||
|
||||
try
|
||||
{
|
||||
// Write message to a buffer so we can get its length
|
||||
WriteMessageCore(message, writer);
|
||||
|
||||
// Write length then message to output
|
||||
BinaryMessageFormatter.WriteLengthPrefix(writer.Length, output);
|
||||
writer.CopyTo(output);
|
||||
}
|
||||
finally
|
||||
{
|
||||
MemoryBufferWriter.Return(writer);
|
||||
}
|
||||
}
|
||||
|
||||
///// <inheritdoc />
|
||||
public ReadOnlyMemory<byte> GetMessageBytes(HubMessage message)
|
||||
{
|
||||
using var writer = new ArrayBufferWriter<byte>();
|
||||
|
||||
// Write message to a buffer so we can get its length
|
||||
WriteMessageCore(message, writer);
|
||||
|
||||
var memory = writer.WrittenMemory;
|
||||
|
||||
var dataLength = memory.Length;
|
||||
var prefixLength = BinaryMessageFormatter.LengthPrefixLength(dataLength);
|
||||
|
||||
var array = new byte[dataLength + prefixLength];
|
||||
var span = array.AsSpan();
|
||||
|
||||
// Write length then message to output
|
||||
var written = BinaryMessageFormatter.WriteLengthPrefix(dataLength, span);
|
||||
Debug.Assert(written == prefixLength);
|
||||
|
||||
memory.Span.CopyTo(span.Slice(prefixLength));
|
||||
|
||||
return array;
|
||||
}
|
||||
|
||||
private void WriteMessageCore(HubMessage message, IBufferWriter<byte> bufferWriter)
|
||||
{
|
||||
var writer = new MessagePackWriter(bufferWriter);
|
||||
|
||||
switch (message)
|
||||
{
|
||||
case InvocationMessage invocationMessage:
|
||||
WriteInvocationMessage(invocationMessage, ref writer);
|
||||
break;
|
||||
case StreamInvocationMessage streamInvocationMessage:
|
||||
WriteStreamInvocationMessage(streamInvocationMessage, ref writer);
|
||||
break;
|
||||
case StreamItemMessage streamItemMessage:
|
||||
WriteStreamingItemMessage(streamItemMessage, ref writer);
|
||||
break;
|
||||
case CompletionMessage completionMessage:
|
||||
WriteCompletionMessage(completionMessage, ref writer);
|
||||
break;
|
||||
case CancelInvocationMessage cancelInvocationMessage:
|
||||
WriteCancelInvocationMessage(cancelInvocationMessage, ref writer);
|
||||
break;
|
||||
case PingMessage pingMessage:
|
||||
WritePingMessage(pingMessage, ref writer);
|
||||
break;
|
||||
case CloseMessage closeMessage:
|
||||
WriteCloseMessage(closeMessage, ref writer);
|
||||
break;
|
||||
default:
|
||||
throw new InvalidDataException($"Unexpected message type: {message.GetType().Name}");
|
||||
}
|
||||
|
||||
writer.Flush();
|
||||
}
|
||||
|
||||
private void WriteInvocationMessage(InvocationMessage message, ref MessagePackWriter writer)
|
||||
{
|
||||
writer.WriteArrayHeader(6);
|
||||
|
||||
writer.Write(HubProtocolConstants.InvocationMessageType);
|
||||
PackHeaders(ref writer, message.Headers);
|
||||
if (string.IsNullOrEmpty(message.InvocationId))
|
||||
{
|
||||
writer.WriteNil();
|
||||
}
|
||||
else
|
||||
{
|
||||
writer.Write(message.InvocationId);
|
||||
}
|
||||
writer.Write(message.Target);
|
||||
writer.WriteArrayHeader(message.Arguments.Length);
|
||||
foreach (var arg in message.Arguments)
|
||||
{
|
||||
SerializeArgument(ref writer, arg);
|
||||
}
|
||||
|
||||
WriteStreamIds(message.StreamIds, ref writer);
|
||||
}
|
||||
|
||||
private void WriteStreamInvocationMessage(StreamInvocationMessage message, ref MessagePackWriter writer)
|
||||
{
|
||||
writer.WriteArrayHeader(6);
|
||||
|
||||
writer.Write(HubProtocolConstants.StreamInvocationMessageType);
|
||||
PackHeaders(ref writer, message.Headers);
|
||||
writer.Write(message.InvocationId);
|
||||
writer.Write(message.Target);
|
||||
|
||||
writer.WriteArrayHeader(message.Arguments.Length);
|
||||
foreach (var arg in message.Arguments)
|
||||
{
|
||||
SerializeArgument(ref writer, arg);
|
||||
}
|
||||
|
||||
WriteStreamIds(message.StreamIds, ref writer);
|
||||
}
|
||||
|
||||
private void WriteStreamingItemMessage(StreamItemMessage message, ref MessagePackWriter writer)
|
||||
{
|
||||
writer.WriteArrayHeader(4);
|
||||
writer.Write(HubProtocolConstants.StreamItemMessageType);
|
||||
PackHeaders(ref writer, message.Headers);
|
||||
writer.Write(message.InvocationId);
|
||||
SerializeArgument(ref writer, message.Item);
|
||||
}
|
||||
|
||||
private void SerializeArgument(ref MessagePackWriter writer, object argument)
|
||||
{
|
||||
switch (argument)
|
||||
{
|
||||
case null:
|
||||
writer.WriteNil();
|
||||
break;
|
||||
|
||||
case bool boolValue:
|
||||
writer.Write(boolValue);
|
||||
break;
|
||||
|
||||
case string stringValue:
|
||||
writer.Write(stringValue);
|
||||
break;
|
||||
|
||||
case int intValue:
|
||||
writer.Write(intValue);
|
||||
break;
|
||||
|
||||
case long longValue:
|
||||
writer.Write(longValue);
|
||||
break;
|
||||
|
||||
case float floatValue:
|
||||
writer.Write(floatValue);
|
||||
break;
|
||||
|
||||
case byte[] byteArray:
|
||||
writer.Write(byteArray);
|
||||
break;
|
||||
|
||||
default:
|
||||
throw new FormatException($"Unsupported argument type {argument.GetType()}");
|
||||
}
|
||||
}
|
||||
|
||||
private static object DeserializeObject(ref MessagePackReader reader, Type type, string field)
|
||||
{
|
||||
try
|
||||
{
|
||||
if (type == typeof(string))
|
||||
{
|
||||
return ReadString(ref reader, "argument");
|
||||
}
|
||||
else if (type == typeof(bool))
|
||||
{
|
||||
return reader.ReadBoolean();
|
||||
}
|
||||
else if (type == typeof(int))
|
||||
{
|
||||
return reader.ReadInt32();
|
||||
}
|
||||
else if (type == typeof(long))
|
||||
{
|
||||
return reader.ReadInt64();
|
||||
}
|
||||
else if (type == typeof(float))
|
||||
{
|
||||
return reader.ReadSingle();
|
||||
}
|
||||
else if (type == typeof(byte[]))
|
||||
{
|
||||
var bytes = reader.ReadBytes();
|
||||
// MessagePack ensures there are at least as many bytes in the message as declared by the byte header.
|
||||
// Consequently it is safe to do ToArray on the returned SequenceReader instance.
|
||||
return bytes.ToArray();
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
throw new InvalidDataException($"Deserializing object of the `{type.Name}` type for '{field}' failed.", ex);
|
||||
}
|
||||
|
||||
throw new FormatException($"Type {type} is not supported");
|
||||
}
|
||||
|
||||
private void WriteStreamIds(string[] streamIds, ref MessagePackWriter writer)
|
||||
{
|
||||
if (streamIds != null)
|
||||
{
|
||||
writer.WriteArrayHeader(streamIds.Length);
|
||||
foreach (var streamId in streamIds)
|
||||
{
|
||||
writer.Write(streamId);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
writer.WriteArrayHeader(0);
|
||||
}
|
||||
}
|
||||
|
||||
private void WriteCompletionMessage(CompletionMessage message, ref MessagePackWriter writer)
|
||||
{
|
||||
var resultKind =
|
||||
message.Error != null ? ErrorResult :
|
||||
message.HasResult ? NonVoidResult :
|
||||
VoidResult;
|
||||
|
||||
writer.WriteArrayHeader(4 + (resultKind != VoidResult ? 1 : 0));
|
||||
writer.Write(HubProtocolConstants.CompletionMessageType);
|
||||
PackHeaders(ref writer, message.Headers);
|
||||
writer.Write(message.InvocationId);
|
||||
writer.Write(resultKind);
|
||||
switch (resultKind)
|
||||
{
|
||||
case ErrorResult:
|
||||
writer.Write(message.Error);
|
||||
break;
|
||||
case NonVoidResult:
|
||||
SerializeArgument(ref writer, message.Result);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
private void WriteCancelInvocationMessage(CancelInvocationMessage message, ref MessagePackWriter writer)
|
||||
{
|
||||
writer.WriteArrayHeader(3);
|
||||
writer.Write(HubProtocolConstants.CancelInvocationMessageType);
|
||||
PackHeaders(ref writer, message.Headers);
|
||||
writer.Write(message.InvocationId);
|
||||
}
|
||||
|
||||
private void WriteCloseMessage(CloseMessage message, ref MessagePackWriter writer)
|
||||
{
|
||||
writer.WriteArrayHeader(2);
|
||||
writer.Write(HubProtocolConstants.CloseMessageType);
|
||||
if (string.IsNullOrEmpty(message.Error))
|
||||
{
|
||||
writer.WriteNil();
|
||||
}
|
||||
else
|
||||
{
|
||||
writer.Write(message.Error);
|
||||
}
|
||||
}
|
||||
|
||||
private void WritePingMessage(PingMessage _, ref MessagePackWriter writer)
|
||||
{
|
||||
writer.WriteArrayHeader(1);
|
||||
writer.Write(HubProtocolConstants.PingMessageType);
|
||||
}
|
||||
|
||||
private void PackHeaders(ref MessagePackWriter writer, IDictionary<string, string> headers)
|
||||
{
|
||||
if (headers == null)
|
||||
{
|
||||
writer.WriteMapHeader(0);
|
||||
return;
|
||||
}
|
||||
|
||||
writer.WriteMapHeader(headers.Count);
|
||||
foreach (var header in headers)
|
||||
{
|
||||
writer.Write(header.Key);
|
||||
writer.Write(header.Value);
|
||||
}
|
||||
}
|
||||
|
||||
private static T ApplyHeaders<T>(IDictionary<string, string> source, T destination) where T : HubInvocationMessage
|
||||
{
|
||||
if (source != null && source.Count > 0)
|
||||
{
|
||||
destination.Headers = source;
|
||||
}
|
||||
|
||||
return destination;
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
private static int ReadInt32(ref MessagePackReader reader, string field)
|
||||
{
|
||||
if (reader.End || reader.NextMessagePackType != MessagePackType.Integer)
|
||||
{
|
||||
ThrowInvalidDataException(field, "Int32");
|
||||
}
|
||||
|
||||
return reader.ReadInt32();
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
private static string ReadString(ref MessagePackReader reader, string field)
|
||||
{
|
||||
if (reader.End)
|
||||
{
|
||||
ThrowInvalidDataException(field, "String");
|
||||
}
|
||||
|
||||
if (reader.IsNil)
|
||||
{
|
||||
reader.ReadNil();
|
||||
return null;
|
||||
}
|
||||
else if (reader.NextMessagePackType == MessagePackType.String)
|
||||
{
|
||||
return reader.ReadString();
|
||||
}
|
||||
|
||||
ThrowInvalidDataException(field, "String");
|
||||
return null; //This should never be reached.
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
private static int ReadArrayHeader(ref MessagePackReader reader, string field)
|
||||
{
|
||||
if (reader.End || reader.NextMessagePackType != MessagePackType.Array)
|
||||
{
|
||||
ThrowInvalidCollectionLengthException(field, "array");
|
||||
}
|
||||
|
||||
return reader.ReadArrayHeader();
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
private static int ReadMapHeader(ref MessagePackReader reader, string field)
|
||||
{
|
||||
if (reader.End || reader.NextMessagePackType != MessagePackType.Map)
|
||||
{
|
||||
ThrowInvalidCollectionLengthException(field, "map");
|
||||
}
|
||||
|
||||
return reader.ReadMapHeader();
|
||||
}
|
||||
|
||||
private static void ThrowInvalidDataException(string field, string targetType)
|
||||
{
|
||||
throw new InvalidDataException($"Reading '{field}' as {targetType} failed.");
|
||||
}
|
||||
|
||||
private static void ThrowInvalidCollectionLengthException(string field, string collection)
|
||||
{
|
||||
throw new InvalidDataException($"Reading {collection} length for '{field}' failed.");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,9 @@
|
|||
// 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.
|
||||
|
||||
namespace MessagePack.Formatters
|
||||
{
|
||||
internal class NativeDateTimeFormatter
|
||||
{
|
||||
}
|
||||
}
|
||||
|
|
@ -1,65 +0,0 @@
|
|||
// 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 MessagePack;
|
||||
using System;
|
||||
using System.IO;
|
||||
|
||||
namespace Microsoft.AspNetCore.Components.Server.Circuits
|
||||
{
|
||||
/// <summary>
|
||||
/// Provides Stream APIs for writing to a MessagePack-supplied expandable buffer.
|
||||
/// </summary>
|
||||
internal class MessagePackBufferStream : Stream
|
||||
{
|
||||
private byte[] _buffer;
|
||||
private int _headerStartOffset;
|
||||
private int _bodyLength;
|
||||
|
||||
public MessagePackBufferStream(byte[] buffer, int offset)
|
||||
{
|
||||
_buffer = buffer ?? throw new ArgumentNullException(nameof(buffer));
|
||||
_headerStartOffset = offset;
|
||||
_bodyLength = 0;
|
||||
}
|
||||
|
||||
public byte[] Buffer => _buffer;
|
||||
|
||||
public override bool CanRead => false;
|
||||
public override bool CanSeek => false;
|
||||
public override bool CanWrite => true;
|
||||
|
||||
// Length is the complete number of bytes being output
|
||||
public override long Length => _bodyLength;
|
||||
|
||||
// Position is the index into the writable body (i.e., so position zero
|
||||
// is the first byte you can actually write a value to)
|
||||
public override long Position
|
||||
{
|
||||
get => _bodyLength;
|
||||
set => throw new NotSupportedException();
|
||||
}
|
||||
|
||||
public override void Flush()
|
||||
{
|
||||
// Nothing to do, as we're not buffering separately anyway
|
||||
}
|
||||
|
||||
public override int Read(byte[] buffer, int offset, int count)
|
||||
=> throw new NotImplementedException();
|
||||
|
||||
public override long Seek(long offset, SeekOrigin origin)
|
||||
=> throw new NotImplementedException();
|
||||
|
||||
public override void SetLength(long value)
|
||||
=> throw new NotImplementedException();
|
||||
|
||||
public override void Write(byte[] src, int srcOffset, int count)
|
||||
{
|
||||
var outputOffset = _headerStartOffset + _bodyLength;
|
||||
MessagePackBinary.EnsureCapacity(ref _buffer, outputOffset, count);
|
||||
System.Buffer.BlockCopy(src, srcOffset, _buffer, outputOffset, count);
|
||||
_bodyLength += count;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -4,6 +4,7 @@
|
|||
using System;
|
||||
using System.Collections.Concurrent;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Text.Encodings.Web;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
|
|
@ -114,7 +115,16 @@ namespace Microsoft.AspNetCore.Components.Browser.Rendering
|
|||
// snapshot its contents now.
|
||||
// TODO: Consider using some kind of array pool instead of allocating a new
|
||||
// buffer on every render.
|
||||
var batchBytes = MessagePackSerializer.Serialize(batch, RenderBatchFormatterResolver.Instance);
|
||||
byte[] batchBytes;
|
||||
using (var memoryStream = new MemoryStream())
|
||||
{
|
||||
using (var renderBatchWriter = new RenderBatchWriter(memoryStream, false))
|
||||
{
|
||||
renderBatchWriter.Write(in batch);
|
||||
}
|
||||
|
||||
batchBytes = memoryStream.ToArray();
|
||||
}
|
||||
|
||||
if (!_client.Connected)
|
||||
{
|
||||
|
|
|
|||
|
|
@ -1,49 +0,0 @@
|
|||
// 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 MessagePack;
|
||||
using MessagePack.Formatters;
|
||||
using Microsoft.AspNetCore.Components.Rendering;
|
||||
using System;
|
||||
|
||||
namespace Microsoft.AspNetCore.Components.Server.Circuits
|
||||
{
|
||||
/// <summary>
|
||||
/// A MessagePack IFormatterResolver that provides an efficient binary serialization
|
||||
/// of <see cref="RenderBatch"/>. The client-side code knows how to walk through this
|
||||
/// binary representation directly, without it first being parsed as an object graph.
|
||||
/// </summary>
|
||||
internal class RenderBatchFormatterResolver : IFormatterResolver
|
||||
{
|
||||
public static readonly RenderBatchFormatterResolver Instance = new RenderBatchFormatterResolver();
|
||||
|
||||
public IMessagePackFormatter<T> GetFormatter<T>()
|
||||
=> typeof(T) == typeof(RenderBatch) ? (IMessagePackFormatter<T>)RenderBatchFormatter.Instance : null;
|
||||
|
||||
private class RenderBatchFormatter : IMessagePackFormatter<RenderBatch>
|
||||
{
|
||||
public static readonly RenderBatchFormatter Instance = new RenderBatchFormatter();
|
||||
|
||||
// No need to accept incoming RenderBatch
|
||||
public RenderBatch Deserialize(byte[] bytes, int offset, IFormatterResolver formatterResolver, out int readSize)
|
||||
=> throw new NotImplementedException();
|
||||
|
||||
public int Serialize(ref byte[] bytes, int offset, RenderBatch value, IFormatterResolver formatterResolver)
|
||||
{
|
||||
// Instead of using MessagePackBinary.WriteBytes, we write into a stream that
|
||||
// knows how to write the data using MessagePack writer APIs. The benefit
|
||||
// is that we don't have to allocate a second large buffer to capture the
|
||||
// RenderBatchWriter output - we can just write directly to the underlying
|
||||
// output buffer.
|
||||
using (var bufferStream = new MessagePackBufferStream(bytes, offset))
|
||||
using (var renderBatchWriter = new RenderBatchWriter(bufferStream, leaveOpen: false))
|
||||
{
|
||||
renderBatchWriter.Write(value);
|
||||
|
||||
bytes = bufferStream.Buffer; // In case the buffer was expanded
|
||||
return (int)bufferStream.Length;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -3,8 +3,10 @@
|
|||
|
||||
using Microsoft.AspNetCore.Builder;
|
||||
using Microsoft.AspNetCore.Components.Server;
|
||||
using Microsoft.AspNetCore.Components.Server.BlazorPack;
|
||||
using Microsoft.AspNetCore.Components.Server.Circuits;
|
||||
using Microsoft.AspNetCore.Components.Services;
|
||||
using Microsoft.AspNetCore.SignalR.Protocol;
|
||||
using Microsoft.Extensions.DependencyInjection.Extensions;
|
||||
using Microsoft.Extensions.Options;
|
||||
using Microsoft.JSInterop;
|
||||
|
|
@ -23,7 +25,15 @@ namespace Microsoft.Extensions.DependencyInjection
|
|||
/// <returns>The <see cref="IServiceCollection"/>.</returns>
|
||||
public static IServiceCollection AddRazorComponents(this IServiceCollection services)
|
||||
{
|
||||
services.AddSignalR().AddMessagePackProtocol();
|
||||
services.AddSignalR()
|
||||
.AddHubOptions<ComponentHub>(options =>
|
||||
{
|
||||
options.SupportedProtocols.Clear();
|
||||
options.SupportedProtocols.Add(BlazorPackHubProtocol.ProtocolName);
|
||||
});
|
||||
|
||||
// Register the Blazor specific hub protocol
|
||||
services.TryAddEnumerable(ServiceDescriptor.Singleton<IHubProtocol, BlazorPackHubProtocol>());
|
||||
|
||||
// Here we add a bunch of services that don't vary in any way based on the
|
||||
// user's configuration. So even if the user has multiple independent server-side
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>netcoreapp3.0</TargetFramework>
|
||||
|
|
@ -6,6 +6,7 @@
|
|||
<IsShippingPackage>true</IsShippingPackage>
|
||||
<GenerateEmbeddedFilesManifest>true</GenerateEmbeddedFilesManifest>
|
||||
<HasReferenceAssembly>false</HasReferenceAssembly>
|
||||
<AllowUnsafeBlocks>true</AllowUnsafeBlocks>
|
||||
</PropertyGroup>
|
||||
|
||||
<!-- Producing this package requires building with NodeJS enabled. -->
|
||||
|
|
@ -21,7 +22,6 @@
|
|||
<ItemGroup>
|
||||
<Reference Include="Microsoft.AspNetCore.Components.Browser" />
|
||||
<Reference Include="Microsoft.AspNetCore.SignalR" />
|
||||
<Reference Include="Microsoft.AspNetCore.SignalR.Protocols.MessagePack" />
|
||||
<Reference Include="Microsoft.AspNetCore.StaticFiles" />
|
||||
<Reference Include="Microsoft.Extensions.Caching.Memory" />
|
||||
<Reference Include="Microsoft.Extensions.FileProviders.Composite" />
|
||||
|
|
@ -33,6 +33,31 @@
|
|||
<Reference Include="Microsoft.AspNetCore.Mvc.ViewFeatures" />
|
||||
</ItemGroup>
|
||||
|
||||
<PropertyGroup>
|
||||
<MessagePackRoot>$(RepositoryRoot)src\submodules\MessagePack-CSharp\src\MessagePack\</MessagePackRoot>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<Compile Include="$(RepositoryRoot)src\SignalR\common\Shared\BinaryMessageFormatter.cs" LinkBase="BlazorPack" />
|
||||
<Compile Include="$(RepositoryRoot)src\SignalR\common\Shared\BinaryMessageParser.cs" LinkBase="BlazorPack" />
|
||||
<Compile Include="$(RepositoryRoot)src\SignalR\common\Shared\MemoryBufferWriter.cs" LinkBase="BlazorPack" />
|
||||
|
||||
<!-- MessagePack -->
|
||||
<Compile Include="$(MessagePackRoot)BufferWriter.cs" LinkBase="BlazorPack\MessagePack" />
|
||||
<Compile Include="$(MessagePackRoot)ExtensionHeader.cs" LinkBase="BlazorPack\MessagePack" />
|
||||
<Compile Include="$(MessagePackRoot)ExtensionResult.cs" LinkBase="BlazorPack\MessagePack" />
|
||||
<Compile Include="$(MessagePackRoot)MessagePackCode.cs" LinkBase="BlazorPack\MessagePack" />
|
||||
<Compile Include="$(MessagePackRoot)MessagePackReader.cs" LinkBase="BlazorPack\MessagePack" />
|
||||
<Compile Include="$(MessagePackRoot)MessagePackReader.Integers.cs" LinkBase="BlazorPack\MessagePack" />
|
||||
<Compile Include="$(MessagePackRoot)MessagePackWriter.cs" LinkBase="BlazorPack\MessagePack" />
|
||||
<Compile Include="$(MessagePackRoot)Nil.cs" LinkBase="BlazorPack\MessagePack" />
|
||||
<Compile Include="$(MessagePackRoot)FloatBits.cs" LinkBase="BlazorPack\MessagePack" />
|
||||
<Compile Include="$(MessagePackRoot)Internal\DateTimeConstants.cs" LinkBase="BlazorPack\MessagePack" />
|
||||
<Compile Include="$(MessagePackRoot)StringEncoding.cs" LinkBase="BlazorPack\MessagePack" />
|
||||
<Compile Include="$(MessagePackRoot)SequenceReader.cs" LinkBase="BlazorPack\MessagePack" />
|
||||
<Compile Include="$(MessagePackRoot)SequenceReaderExtensions.cs" LinkBase="BlazorPack\MessagePack" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup Condition="'$(BuildNodeJS)' != 'false'">
|
||||
<!-- We need .Browser.JS to build first so we can embed its .js output -->
|
||||
<EmbeddedResource Include="..\..\Browser.JS\src\dist\components.server.js" LogicalName="_framework\%(Filename)%(Extension)" />
|
||||
|
|
|
|||
|
|
@ -0,0 +1,13 @@
|
|||
// 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 Microsoft.AspNetCore.SignalR.Common.Tests.Internal.Protocol;
|
||||
using Microsoft.AspNetCore.SignalR.Protocol;
|
||||
|
||||
namespace Microsoft.AspNetCore.Components.Server.BlazorPack
|
||||
{
|
||||
public class BlazorPackHubProtocolTest : MessagePackHubProtocolTestBase
|
||||
{
|
||||
protected override IHubProtocol HubProtocol { get; } = new BlazorPackHubProtocol();
|
||||
}
|
||||
}
|
||||
|
|
@ -1,99 +0,0 @@
|
|||
// 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 Microsoft.AspNetCore.Components.Server.Circuits;
|
||||
using System;
|
||||
using Xunit;
|
||||
|
||||
namespace Microsoft.AspNetCore.Components.Server
|
||||
{
|
||||
public class MessagePackBufferStreamTest
|
||||
{
|
||||
[Fact]
|
||||
public void NullBuffer_Throws()
|
||||
{
|
||||
var ex = Assert.Throws<ArgumentNullException>(() =>
|
||||
{
|
||||
new MessagePackBufferStream(null, 0);
|
||||
});
|
||||
|
||||
Assert.Equal("buffer", ex.ParamName);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void WithWrites_WritesToUnderlyingBuffer()
|
||||
{
|
||||
// Arrange
|
||||
var buffer = new byte[100];
|
||||
var offset = 58; // Arbitrary
|
||||
|
||||
// Act/Assert
|
||||
using (var stream = new MessagePackBufferStream(buffer, offset))
|
||||
{
|
||||
stream.Write(new byte[] { 10, 20, 30, 40 }, 1, 2); // Write 2 bytes
|
||||
stream.Write(new byte[] { 101 }, 0, 1); // Write another 1 byte
|
||||
stream.Close();
|
||||
|
||||
Assert.Equal(20, buffer[offset]);
|
||||
Assert.Equal(30, buffer[offset + 1]);
|
||||
Assert.Equal(101, buffer[offset + 2]);
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void LengthAndPositionAreEquivalent()
|
||||
{
|
||||
// Arrange
|
||||
var buffer = new byte[20];
|
||||
var offset = 3;
|
||||
|
||||
// Act/Assert
|
||||
using (var stream = new MessagePackBufferStream(buffer, offset))
|
||||
{
|
||||
stream.Write(new byte[] { 0x01, 0x02 }, 0, 2);
|
||||
Assert.Equal(2, stream.Length);
|
||||
Assert.Equal(2, stream.Position);
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void WithWrites_ExpandsBufferWhenNeeded()
|
||||
{
|
||||
// Arrange
|
||||
var origBuffer = new byte[10];
|
||||
var offset = 6;
|
||||
origBuffer[0] = 123; // So we can check it was retained during expansion
|
||||
|
||||
// Act/Assert
|
||||
using (var stream = new MessagePackBufferStream(origBuffer, offset))
|
||||
{
|
||||
// We can fit the 6-byte offset plus 3 written bytes
|
||||
// into the original 10-byte buffer
|
||||
stream.Write(new byte[] { 10, 20, 30 }, 0, 3);
|
||||
Assert.Same(origBuffer, stream.Buffer);
|
||||
|
||||
// Trying to add two more exceeds the capacity, so the buffer expands
|
||||
stream.Write(new byte[] { 40, 50 }, 0, 2);
|
||||
Assert.NotSame(origBuffer, stream.Buffer);
|
||||
Assert.True(stream.Buffer.Length > origBuffer.Length);
|
||||
|
||||
// Check the expanded buffer has the expected contents
|
||||
stream.Close();
|
||||
Assert.Equal(123, stream.Buffer[0]); // Retains other values from original buffer
|
||||
Assert.Equal(10, stream.Buffer[offset]);
|
||||
Assert.Equal(20, stream.Buffer[offset + 1]);
|
||||
Assert.Equal(30, stream.Buffer[offset + 2]);
|
||||
Assert.Equal(40, stream.Buffer[offset + 3]);
|
||||
Assert.Equal(50, stream.Buffer[offset + 4]);
|
||||
}
|
||||
}
|
||||
|
||||
int ReadBigEndianInt32(byte[] buffer, int startOffset)
|
||||
{
|
||||
return (buffer[startOffset] << 24)
|
||||
+ (buffer[startOffset + 1] << 16)
|
||||
+ (buffer[startOffset + 2] << 8)
|
||||
+ (buffer[startOffset + 3]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -11,8 +11,16 @@
|
|||
<Reference Include="Microsoft.Extensions.Logging.Testing" />
|
||||
</ItemGroup>
|
||||
|
||||
<PropertyGroup>
|
||||
<SignalRTestBase>$(RepositoryRoot)src\SignalR\common\SignalR.Common\test\Internal\Protocol\</SignalRTestBase>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<Compile Include="..\..\Components\test\Rendering\HtmlRendererTestBase.cs" />
|
||||
<Compile Include="$(SignalRTestBase)HubMessageHelpers.cs" LinkBase="BlazorPack"/>
|
||||
<Compile Include="$(SignalRTestBase)MessagePackHubProtocolTestBase.cs" LinkBase="BlazorPack"/>
|
||||
<Compile Include="$(SignalRTestBase)TestBinder.cs" LinkBase="BlazorPack"/>
|
||||
<Compile Include="$(SignalRTestBase)TestHubMessageEqualityComparer.cs" LinkBase="BlazorPack"/>
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
|
|
|
|||
|
|
@ -0,0 +1,440 @@
|
|||
// 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 System.Collections.Generic;
|
||||
using System.Diagnostics;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using Microsoft.AspNetCore.Internal;
|
||||
using Microsoft.AspNetCore.SignalR.Protocol;
|
||||
using Xunit;
|
||||
|
||||
namespace Microsoft.AspNetCore.SignalR.Common.Tests.Internal.Protocol
|
||||
{
|
||||
using static HubMessageHelpers;
|
||||
|
||||
/// <summary>
|
||||
/// Common MessagePack-based hub protocol tests that is shared by MessagePackHubProtocol and Blazor's internal messagepack based-hub protocol.
|
||||
/// Since the latter only supports simple data types such as ints, strings, bools, and bytes for serialization, only tests that
|
||||
/// require no serialization (or deserialization), or tests that serialize simple data types should go here.
|
||||
/// Tests that verify deserialization of complex data types should go in MessagePackHubProtocolTests.
|
||||
/// </summary>
|
||||
public abstract class MessagePackHubProtocolTestBase
|
||||
{
|
||||
protected static readonly IDictionary<string, string> TestHeaders = new Dictionary<string, string>
|
||||
{
|
||||
{ "Foo", "Bar" },
|
||||
{ "KeyWith\nNew\r\nLines", "Still Works" },
|
||||
{ "ValueWithNewLines", "Also\nWorks\r\nFine" },
|
||||
};
|
||||
|
||||
protected abstract IHubProtocol HubProtocol { get; }
|
||||
|
||||
public enum TestEnum
|
||||
{
|
||||
Zero = 0,
|
||||
One
|
||||
}
|
||||
|
||||
// Test Data for Parse/WriteMessages:
|
||||
// * Name: A string name that is used when reporting the test (it's the ToString value for ProtocolTestData)
|
||||
// * Message: The HubMessage that is either expected (in Parse) or used as input (in Write)
|
||||
// * Binary: Base64-encoded binary "baseline" to sanity-check MessagePack-CSharp behavior
|
||||
//
|
||||
// When changing the tests/message pack parsing if you get test failures look at the base64 encoding and
|
||||
// use a tool like https://sugendran.github.io/msgpack-visualizer/ to verify that the MsgPack is correct and then just replace the Base64 value.
|
||||
|
||||
public static IEnumerable<object[]> BaseTestDataNames
|
||||
{
|
||||
get
|
||||
{
|
||||
foreach (var k in BaseTestData.Keys)
|
||||
{
|
||||
yield return new object[] { k };
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public static IDictionary<string, ProtocolTestData> BaseTestData => new[]
|
||||
{
|
||||
// Invocation messages
|
||||
new ProtocolTestData(
|
||||
name: "InvocationWithNoHeadersAndNoArgs",
|
||||
message: new InvocationMessage("xyz", "method", Array.Empty<object>()),
|
||||
binary: "lgGAo3h5eqZtZXRob2SQkA=="),
|
||||
new ProtocolTestData(
|
||||
name: "InvocationWithNoHeadersNoIdAndNoArgs",
|
||||
message: new InvocationMessage("method", Array.Empty<object>()),
|
||||
binary: "lgGAwKZtZXRob2SQkA=="),
|
||||
new ProtocolTestData(
|
||||
name: "InvocationWithNoHeadersNoIdAndSingleIntArg",
|
||||
message: new InvocationMessage("method", new object[] { 42 }),
|
||||
binary: "lgGAwKZtZXRob2SRKpA="),
|
||||
new ProtocolTestData(
|
||||
name: "InvocationWithNoHeadersNoIdIntAndStringArgs",
|
||||
message: new InvocationMessage("method", new object[] { 42, "string" }),
|
||||
binary: "lgGAwKZtZXRob2SSKqZzdHJpbmeQ"),
|
||||
new ProtocolTestData(
|
||||
name: "InvocationWithStreamArgument",
|
||||
message: new InvocationMessage(null, "Target", Array.Empty<object>(), new string[] { "__test_id__" }),
|
||||
binary: "lgGAwKZUYXJnZXSQkatfX3Rlc3RfaWRfXw=="),
|
||||
new ProtocolTestData(
|
||||
name: "InvocationWithStreamAndNormalArgument",
|
||||
message: new InvocationMessage(null, "Target", new object[] { 42 }, new string[] { "__test_id__" }),
|
||||
binary: "lgGAwKZUYXJnZXSRKpGrX190ZXN0X2lkX18="),
|
||||
new ProtocolTestData(
|
||||
name: "InvocationWithMulitpleStreams",
|
||||
message: new InvocationMessage(null, "Target", Array.Empty<object>(), new string[] { "__test_id__", "__test_id2__" }),
|
||||
binary: "lgGAwKZUYXJnZXSQkqtfX3Rlc3RfaWRfX6xfX3Rlc3RfaWQyX18="),
|
||||
|
||||
// StreamItem Messages
|
||||
new ProtocolTestData(
|
||||
name: "StreamItemWithNoHeadersAndIntItem",
|
||||
message: new StreamItemMessage("xyz", item: 42),
|
||||
binary: "lAKAo3h5eio="),
|
||||
new ProtocolTestData(
|
||||
name: "StreamItemWithNoHeadersAndFloatItem",
|
||||
message: new StreamItemMessage("xyz", item: 42.0f),
|
||||
binary: "lAKAo3h5espCKAAA"),
|
||||
new ProtocolTestData(
|
||||
name: "StreamItemWithNoHeadersAndStringItem",
|
||||
message: new StreamItemMessage("xyz", item: "string"),
|
||||
binary: "lAKAo3h5eqZzdHJpbmc="),
|
||||
new ProtocolTestData(
|
||||
name: "StreamItemWithNoHeadersAndBoolItem",
|
||||
message: new StreamItemMessage("xyz", item: true),
|
||||
binary: "lAKAo3h5esM="),
|
||||
|
||||
// Completion Messages
|
||||
new ProtocolTestData(
|
||||
name: "CompletionWithNoHeadersAndError",
|
||||
message: CompletionMessage.WithError("xyz", error: "Error not found!"),
|
||||
binary: "lQOAo3h5egGwRXJyb3Igbm90IGZvdW5kIQ=="),
|
||||
new ProtocolTestData(
|
||||
name: "CompletionWithHeadersAndError",
|
||||
message: AddHeaders(TestHeaders, CompletionMessage.WithError("xyz", error: "Error not found!")),
|
||||
binary: "lQODo0Zvb6NCYXKyS2V5V2l0aApOZXcNCkxpbmVzq1N0aWxsIFdvcmtzsVZhbHVlV2l0aE5ld0xpbmVzsEFsc28KV29ya3MNCkZpbmWjeHl6AbBFcnJvciBub3QgZm91bmQh"),
|
||||
new ProtocolTestData(
|
||||
name: "CompletionWithNoHeadersAndNoResult",
|
||||
message: CompletionMessage.Empty("xyz"),
|
||||
binary: "lAOAo3h5egI="),
|
||||
new ProtocolTestData(
|
||||
name: "CompletionWithHeadersAndNoResult",
|
||||
message: AddHeaders(TestHeaders, CompletionMessage.Empty("xyz")),
|
||||
binary: "lAODo0Zvb6NCYXKyS2V5V2l0aApOZXcNCkxpbmVzq1N0aWxsIFdvcmtzsVZhbHVlV2l0aE5ld0xpbmVzsEFsc28KV29ya3MNCkZpbmWjeHl6Ag=="),
|
||||
new ProtocolTestData(
|
||||
name: "CompletionWithNoHeadersAndIntResult",
|
||||
message: CompletionMessage.WithResult("xyz", payload: 42),
|
||||
binary: "lQOAo3h5egMq"),
|
||||
new ProtocolTestData(
|
||||
name: "CompletionWithNoHeadersAndFloatResult",
|
||||
message: CompletionMessage.WithResult("xyz", payload: 42.0f),
|
||||
binary: "lQOAo3h5egPKQigAAA=="),
|
||||
new ProtocolTestData(
|
||||
name: "CompletionWithNoHeadersAndStringResult",
|
||||
message: CompletionMessage.WithResult("xyz", payload: "string"),
|
||||
binary: "lQOAo3h5egOmc3RyaW5n"),
|
||||
new ProtocolTestData(
|
||||
name: "CompletionWithNoHeadersAndBooleanResult",
|
||||
message: CompletionMessage.WithResult("xyz", payload: true),
|
||||
binary: "lQOAo3h5egPD"),
|
||||
|
||||
// StreamInvocation Messages
|
||||
new ProtocolTestData(
|
||||
name: "StreamInvocationWithNoHeadersAndNoArgs",
|
||||
message: new StreamInvocationMessage("xyz", "method", Array.Empty<object>()),
|
||||
binary: "lgSAo3h5eqZtZXRob2SQkA=="),
|
||||
new ProtocolTestData(
|
||||
name: "StreamInvocationWithNoHeadersAndIntArg",
|
||||
message: new StreamInvocationMessage("xyz", "method", new object[] { 42 }),
|
||||
binary: "lgSAo3h5eqZtZXRob2SRKpA="),
|
||||
new ProtocolTestData(
|
||||
name: "StreamInvocationWithStreamArgument",
|
||||
message: new StreamInvocationMessage("xyz", "method", Array.Empty<object>(), new string[] { "__test_id__" }),
|
||||
binary: "lgSAo3h5eqZtZXRob2SQkatfX3Rlc3RfaWRfXw=="),
|
||||
new ProtocolTestData(
|
||||
name: "StreamInvocationWithStreamAndNormalArgument",
|
||||
message: new StreamInvocationMessage("xyz", "method", new object[] { 42 }, new string[] { "__test_id__" }),
|
||||
binary: "lgSAo3h5eqZtZXRob2SRKpGrX190ZXN0X2lkX18="),
|
||||
new ProtocolTestData(
|
||||
name: "StreamInvocationWithNoHeadersAndIntAndStringArgs",
|
||||
message: new StreamInvocationMessage("xyz", "method", new object[] { 42, "string" }),
|
||||
binary: "lgSAo3h5eqZtZXRob2SSKqZzdHJpbmeQ"),
|
||||
|
||||
// CancelInvocation Messages
|
||||
new ProtocolTestData(
|
||||
name: "CancelInvocationWithNoHeaders",
|
||||
message: new CancelInvocationMessage("xyz"),
|
||||
binary: "kwWAo3h5eg=="),
|
||||
new ProtocolTestData(
|
||||
name: "CancelInvocationWithHeaders",
|
||||
message: AddHeaders(TestHeaders, new CancelInvocationMessage("xyz")),
|
||||
binary: "kwWDo0Zvb6NCYXKyS2V5V2l0aApOZXcNCkxpbmVzq1N0aWxsIFdvcmtzsVZhbHVlV2l0aE5ld0xpbmVzsEFsc28KV29ya3MNCkZpbmWjeHl6"),
|
||||
|
||||
// Ping Messages
|
||||
new ProtocolTestData(
|
||||
name: "Ping",
|
||||
message: PingMessage.Instance,
|
||||
binary: "kQY="),
|
||||
}.ToDictionary(t => t.Name);
|
||||
|
||||
[Theory]
|
||||
[MemberData(nameof(BaseTestDataNames))]
|
||||
public void BaseParseMessages(string testDataName)
|
||||
{
|
||||
var testData = BaseTestData[testDataName];
|
||||
|
||||
TestParseMessages(testData);
|
||||
}
|
||||
|
||||
protected void TestParseMessages(ProtocolTestData testData)
|
||||
{
|
||||
// Verify that the input binary string decodes to the expected MsgPack primitives
|
||||
var bytes = Convert.FromBase64String(testData.Binary);
|
||||
|
||||
// Parse the input fully now.
|
||||
bytes = Frame(bytes);
|
||||
var data = new ReadOnlySequence<byte>(bytes);
|
||||
Assert.True(HubProtocol.TryParseMessage(ref data, new TestBinder(testData.Message), out var message));
|
||||
|
||||
Assert.NotNull(message);
|
||||
Assert.Equal(testData.Message, message, TestHubMessageEqualityComparer.Instance);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ParseMessageWithExtraData()
|
||||
{
|
||||
var expectedMessage = new InvocationMessage("xyz", "method", Array.Empty<object>());
|
||||
|
||||
// Verify that the input binary string decodes to the expected MsgPack primitives
|
||||
var bytes = new byte[] { ArrayBytes(8),
|
||||
1,
|
||||
0x80,
|
||||
StringBytes(3), (byte)'x', (byte)'y', (byte)'z',
|
||||
StringBytes(6), (byte)'m', (byte)'e', (byte)'t', (byte)'h', (byte)'o', (byte)'d',
|
||||
ArrayBytes(0), // Arguments
|
||||
ArrayBytes(0), // Streams
|
||||
0xc3,
|
||||
StringBytes(2), (byte)'e', (byte)'x' };
|
||||
|
||||
// Parse the input fully now.
|
||||
bytes = Frame(bytes);
|
||||
var data = new ReadOnlySequence<byte>(bytes);
|
||||
Assert.True(HubProtocol.TryParseMessage(ref data, new TestBinder(expectedMessage), out var message));
|
||||
|
||||
Assert.NotNull(message);
|
||||
Assert.Equal(expectedMessage, message, TestHubMessageEqualityComparer.Instance);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[MemberData(nameof(BaseTestDataNames))]
|
||||
public void BaseWriteMessages(string testDataName)
|
||||
{
|
||||
var testData = BaseTestData[testDataName];
|
||||
|
||||
TestWriteMessages(testData);
|
||||
}
|
||||
|
||||
protected void TestWriteMessages(ProtocolTestData testData)
|
||||
{
|
||||
var bytes = Write(testData.Message);
|
||||
|
||||
// Unframe the message to check the binary encoding
|
||||
var byteSpan = new ReadOnlySequence<byte>(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
|
||||
var actual = Convert.ToBase64String(unframed.ToArray());
|
||||
Assert.True(string.Equals(actual, testData.Binary, StringComparison.Ordinal), $"Binary encoding changed from{Environment.NewLine} [{testData.Binary}]{Environment.NewLine} to{Environment.NewLine} [{actual}]{Environment.NewLine}Please verify the MsgPack output and update the baseline");
|
||||
}
|
||||
|
||||
public static IDictionary<string, InvalidMessageData> BaseInvalidPayloads => new[]
|
||||
{
|
||||
// Message Type
|
||||
new InvalidMessageData("MessageTypeString", new byte[] { 0x91, 0xa3, (byte)'f', (byte)'o', (byte)'o' }, "Reading 'messageType' as Int32 failed."),
|
||||
|
||||
// Headers
|
||||
new InvalidMessageData("HeadersNotAMap", new byte[] { 0x92, 1, 0xa3, (byte)'f', (byte)'o', (byte)'o' }, "Reading map length for 'headers' failed."),
|
||||
new InvalidMessageData("HeaderKeyInt", new byte[] { 0x92, 1, 0x82, 0x2a, 0xa3, (byte)'f', (byte)'o', (byte)'o' }, "Reading 'headers[0].Key' as String failed."),
|
||||
new InvalidMessageData("HeaderValueInt", new byte[] { 0x92, 1, 0x82, 0xa3, (byte)'f', (byte)'o', (byte)'o', 42 }, "Reading 'headers[0].Value' as String failed."),
|
||||
new InvalidMessageData("HeaderKeyArray", new byte[] { 0x92, 1, 0x84, 0xa3, (byte)'f', (byte)'o', (byte)'o', 0xa3, (byte)'f', (byte)'o', (byte)'o', 0x90, 0xa3, (byte)'f', (byte)'o', (byte)'o' }, "Reading 'headers[1].Key' as String failed."),
|
||||
new InvalidMessageData("HeaderValueArray", new byte[] { 0x92, 1, 0x84, 0xa3, (byte)'f', (byte)'o', (byte)'o', 0xa3, (byte)'f', (byte)'o', (byte)'o', 0xa3, (byte)'f', (byte)'o', (byte)'o', 0x90 }, "Reading 'headers[1].Value' as String failed."),
|
||||
|
||||
// InvocationMessage
|
||||
new InvalidMessageData("InvocationMissingId", new byte[] { 0x92, 1, 0x80 }, "Reading 'invocationId' as String failed."),
|
||||
new InvalidMessageData("InvocationIdBoolean", new byte[] { 0x91, 1, 0x80, 0xc2 }, "Reading 'invocationId' as String failed."),
|
||||
new InvalidMessageData("InvocationTargetMissing", new byte[] { 0x93, 1, 0x80, 0xa3, (byte)'a', (byte)'b', (byte)'c' }, "Reading 'target' as String failed."),
|
||||
new InvalidMessageData("InvocationTargetInt", new byte[] { 0x94, 1, 0x80, 0xa3, (byte)'a', (byte)'b', (byte)'c', 42 }, "Reading 'target' as String failed."),
|
||||
|
||||
// StreamInvocationMessage
|
||||
new InvalidMessageData("StreamInvocationMissingId", new byte[] { 0x92, 4, 0x80 }, "Reading 'invocationId' as String failed."),
|
||||
new InvalidMessageData("StreamInvocationIdBoolean", new byte[] { 0x93, 4, 0x80, 0xc2 }, "Reading 'invocationId' as String failed."),
|
||||
new InvalidMessageData("StreamInvocationTargetMissing", new byte[] { 0x93, 4, 0x80, 0xa3, (byte)'a', (byte)'b', (byte)'c' }, "Reading 'target' as String failed."),
|
||||
new InvalidMessageData("StreamInvocationTargetInt", new byte[] { 0x94, 4, 0x80, 0xa3, (byte)'a', (byte)'b', (byte)'c', 42 }, "Reading 'target' as String failed."),
|
||||
|
||||
// StreamItemMessage
|
||||
new InvalidMessageData("StreamItemMissingId", new byte[] { 0x92, 2, 0x80 }, "Reading 'invocationId' as String failed."),
|
||||
new InvalidMessageData("StreamItemInvocationIdBoolean", new byte[] { 0x93, 2, 0x80, 0xc2 }, "Reading 'invocationId' as String failed."),
|
||||
new InvalidMessageData("StreamItemMissing", new byte[] { 0x93, 2, 0x80, 0xa3, (byte)'x', (byte)'y', (byte)'z' }, "Deserializing object of the `String` type for 'item' failed."),
|
||||
new InvalidMessageData("StreamItemTypeMismatch", new byte[] { 0x94, 2, 0x80, 0xa3, (byte)'x', (byte)'y', (byte)'z', 42 }, "Deserializing object of the `String` type for 'item' failed."),
|
||||
|
||||
// CompletionMessage
|
||||
new InvalidMessageData("CompletionMissingId", new byte[] { 0x92, 3, 0x80 }, "Reading 'invocationId' as String failed."),
|
||||
new InvalidMessageData("CompletionIdBoolean", new byte[] { 0x93, 3, 0x80, 0xc2 }, "Reading 'invocationId' as String failed."),
|
||||
new InvalidMessageData("CompletionResultKindString", new byte[] { 0x94, 3, 0x80, 0xa3, (byte)'x', (byte)'y', (byte)'z', 0xa3, (byte)'x', (byte)'y', (byte)'z' }, "Reading 'resultKind' as Int32 failed."),
|
||||
new InvalidMessageData("CompletionResultKindOutOfRange", new byte[] { 0x94, 3, 0x80, 0xa3, (byte)'x', (byte)'y', (byte)'z', 42 }, "Invalid invocation result kind."),
|
||||
new InvalidMessageData("CompletionErrorMissing", new byte[] { 0x94, 3, 0x80, 0xa3, (byte)'x', (byte)'y', (byte)'z', 0x01 }, "Reading 'error' as String failed."),
|
||||
new InvalidMessageData("CompletionErrorInt", new byte[] { 0x95, 3, 0x80, 0xa3, (byte)'x', (byte)'y', (byte)'z', 0x01, 42 }, "Reading 'error' as String failed."),
|
||||
new InvalidMessageData("CompletionResultMissing", new byte[] { 0x94, 3, 0x80, 0xa3, (byte)'x', (byte)'y', (byte)'z', 0x03 }, "Deserializing object of the `String` type for 'argument' failed."),
|
||||
new InvalidMessageData("CompletionResultTypeMismatch", new byte[] { 0x95, 3, 0x80, 0xa3, (byte)'x', (byte)'y', (byte)'z', 0x03, 42 }, "Deserializing object of the `String` type for 'argument' failed."),
|
||||
}.ToDictionary(t => t.Name);
|
||||
|
||||
public static IEnumerable<object[]> BaseInvalidPayloadNames => BaseInvalidPayloads.Keys.Select(name => new object[] { name });
|
||||
|
||||
[Theory]
|
||||
[MemberData(nameof(BaseInvalidPayloadNames))]
|
||||
public void ParserThrowsForInvalidMessages(string invalidPayloadName)
|
||||
{
|
||||
var testData = BaseInvalidPayloads[invalidPayloadName];
|
||||
|
||||
TestInvalidMessageDate(testData);
|
||||
}
|
||||
|
||||
protected void TestInvalidMessageDate(InvalidMessageData testData)
|
||||
{
|
||||
var buffer = Frame(testData.Encoded);
|
||||
var binder = new TestBinder(new[] { typeof(string) }, typeof(string));
|
||||
var data = new ReadOnlySequence<byte>(buffer);
|
||||
var exception = Assert.Throws<InvalidDataException>(() => HubProtocol.TryParseMessage(ref data, binder, out _));
|
||||
|
||||
Assert.Equal(testData.ErrorMessage, exception.Message);
|
||||
}
|
||||
|
||||
public static IDictionary<string, InvalidMessageData> ArgumentBindingErrors => new[]
|
||||
{
|
||||
// InvocationMessage
|
||||
new InvalidMessageData("InvocationArgumentArrayMissing", new byte[] { 0x94, 1, 0x80, 0xa3, (byte)'a', (byte)'b', (byte)'c', 0xa3, (byte)'x', (byte)'y', (byte)'z' }, "Reading array length for 'arguments' failed."),
|
||||
new InvalidMessageData("InvocationArgumentArrayNotAnArray", new byte[] { 0x95, 1, 0x80, 0xa3, (byte)'a', (byte)'b', (byte)'c', 0xa3, (byte)'x', (byte)'y', (byte)'z', 42 }, "Reading array length for 'arguments' failed."),
|
||||
new InvalidMessageData("InvocationArgumentArraySizeMismatchEmpty", new byte[] { 0x95, 1, 0x80, 0xa3, (byte)'a', (byte)'b', (byte)'c', 0xa3, (byte)'x', (byte)'y', (byte)'z', 0x90 }, "Invocation provides 0 argument(s) but target expects 1."),
|
||||
new InvalidMessageData("InvocationArgumentArraySizeMismatchTooLarge", new byte[] { 0x95, 1, 0x80, 0xa3, (byte)'a', (byte)'b', (byte)'c', 0xa3, (byte)'x', (byte)'y', (byte)'z', 0x92, 0xa1, (byte)'a', 0xa1, (byte)'b' }, "Invocation provides 2 argument(s) but target expects 1."),
|
||||
new InvalidMessageData("InvocationArgumentTypeMismatch", new byte[] { 0x95, 1, 0x80, 0xa3, (byte)'a', (byte)'b', (byte)'c', 0xa3, (byte)'x', (byte)'y', (byte)'z', 0x91, 42 }, "Error binding arguments. Make sure that the types of the provided values match the types of the hub method being invoked."),
|
||||
|
||||
// StreamInvocationMessage
|
||||
new InvalidMessageData("StreamInvocationArgumentArrayMissing", new byte[] { 0x94, 4, 0x80, 0xa3, (byte)'a', (byte)'b', (byte)'c', 0xa3, (byte)'x', (byte)'y', (byte)'z' }, "Reading array length for 'arguments' failed."), // array is missing
|
||||
new InvalidMessageData("StreamInvocationArgumentArrayNotAnArray", new byte[] { 0x95, 4, 0x80, 0xa3, (byte)'a', (byte)'b', (byte)'c', 0xa3, (byte)'x', (byte)'y', (byte)'z', 42 }, "Reading array length for 'arguments' failed."), // arguments isn't an array
|
||||
new InvalidMessageData("StreamInvocationArgumentArraySizeMismatchEmpty", new byte[] { 0x95, 4, 0x80, 0xa3, (byte)'a', (byte)'b', (byte)'c', 0xa3, (byte)'x', (byte)'y', (byte)'z', 0x90 }, "Invocation provides 0 argument(s) but target expects 1."), // array is missing elements
|
||||
new InvalidMessageData("StreamInvocationArgumentArraySizeMismatchTooLarge", new byte[] { 0x95, 4, 0x80, 0xa3, (byte)'a', (byte)'b', (byte)'c', 0xa3, (byte)'x', (byte)'y', (byte)'z', 0x92, 0xa1, (byte)'a', 0xa1, (byte)'b' }, "Invocation provides 2 argument(s) but target expects 1."), // argument count does not match binder argument count
|
||||
new InvalidMessageData("StreamInvocationArgumentTypeMismatch", new byte[] { 0x95, 4, 0x80, 0xa3, (byte)'a', (byte)'b', (byte)'c', 0xa3, (byte)'x', (byte)'y', (byte)'z', 0x91, 42 }, "Error binding arguments. Make sure that the types of the provided values match the types of the hub method being invoked."), // argument type mismatch
|
||||
}.ToDictionary(t => t.Name);
|
||||
|
||||
public static IEnumerable<object[]> ArgumentBindingErrorNames => ArgumentBindingErrors.Keys.Select(name => new object[] { name });
|
||||
|
||||
[Theory]
|
||||
[MemberData(nameof(ArgumentBindingErrorNames))]
|
||||
public void GettingArgumentsThrowsIfBindingFailed(string argumentBindingErrorName)
|
||||
{
|
||||
var testData = ArgumentBindingErrors[argumentBindingErrorName];
|
||||
|
||||
var buffer = Frame(testData.Encoded);
|
||||
var binder = new TestBinder(new[] { typeof(string) }, typeof(string));
|
||||
var data = new ReadOnlySequence<byte>(buffer);
|
||||
HubProtocol.TryParseMessage(ref data, binder, out var message);
|
||||
var bindingFailure = Assert.IsType<InvocationBindingFailureMessage>(message);
|
||||
Assert.Equal(testData.ErrorMessage, bindingFailure.BindingFailure.SourceException.Message);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData(new byte[] { 0x05, 0x01 })]
|
||||
public void ParserDoesNotConsumePartialData(byte[] payload)
|
||||
{
|
||||
var binder = new TestBinder(new[] { typeof(string) }, typeof(string));
|
||||
var data = new ReadOnlySequence<byte>(payload);
|
||||
var result = HubProtocol.TryParseMessage(ref data, binder, out var message);
|
||||
Assert.Null(message);
|
||||
}
|
||||
|
||||
protected byte ArrayBytes(int size)
|
||||
{
|
||||
Debug.Assert(size < 16, "Test code doesn't support array sizes greater than 15");
|
||||
|
||||
return (byte)(0x90 | size);
|
||||
}
|
||||
|
||||
protected byte StringBytes(int size)
|
||||
{
|
||||
Debug.Assert(size < 16, "Test code doesn't support string sizes greater than 15");
|
||||
|
||||
return (byte)(0xa0 | size);
|
||||
}
|
||||
|
||||
protected static void AssertMessages(byte[] expectedOutput, ReadOnlyMemory<byte> bytes)
|
||||
{
|
||||
var data = new ReadOnlySequence<byte>(bytes);
|
||||
Assert.True(BinaryMessageParser.TryParseMessage(ref data, out var message));
|
||||
Assert.Equal(expectedOutput, message.ToArray());
|
||||
}
|
||||
|
||||
protected static byte[] Frame(byte[] input)
|
||||
{
|
||||
var stream = MemoryBufferWriter.Get();
|
||||
try
|
||||
{
|
||||
BinaryMessageFormatter.WriteLengthPrefix(input.Length, stream);
|
||||
stream.Write(input);
|
||||
return stream.ToArray();
|
||||
}
|
||||
finally
|
||||
{
|
||||
MemoryBufferWriter.Return(stream);
|
||||
}
|
||||
}
|
||||
|
||||
protected byte[] Write(HubMessage message)
|
||||
{
|
||||
var writer = MemoryBufferWriter.Get();
|
||||
try
|
||||
{
|
||||
HubProtocol.WriteMessage(message, writer);
|
||||
return writer.ToArray();
|
||||
}
|
||||
finally
|
||||
{
|
||||
MemoryBufferWriter.Return(writer);
|
||||
}
|
||||
}
|
||||
|
||||
public class InvalidMessageData
|
||||
{
|
||||
public string Name { get; private set; }
|
||||
public byte[] Encoded { get; private set; }
|
||||
public string ErrorMessage { get; private set; }
|
||||
|
||||
public InvalidMessageData(string name, byte[] encoded, string errorMessage)
|
||||
{
|
||||
Name = name;
|
||||
Encoded = encoded;
|
||||
ErrorMessage = errorMessage;
|
||||
}
|
||||
|
||||
public override string ToString() => Name;
|
||||
}
|
||||
|
||||
public class ProtocolTestData
|
||||
{
|
||||
public string Name { get; }
|
||||
public string Binary { get; }
|
||||
public HubMessage Message { get; }
|
||||
|
||||
public ProtocolTestData(string name, HubMessage message, string binary)
|
||||
{
|
||||
Name = name;
|
||||
Message = message;
|
||||
Binary = binary;
|
||||
}
|
||||
|
||||
public override string ToString() => Name;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -8,311 +8,22 @@ using System.Diagnostics;
|
|||
using System.IO;
|
||||
using System.Linq;
|
||||
using Microsoft.AspNetCore.Internal;
|
||||
using Microsoft.AspNetCore.SignalR.Protocol;
|
||||
using Xunit;
|
||||
|
||||
namespace Microsoft.AspNetCore.SignalR.Common.Tests.Internal.Protocol
|
||||
{
|
||||
using Microsoft.AspNetCore.SignalR.Protocol;
|
||||
using static HubMessageHelpers;
|
||||
|
||||
public class MessagePackHubProtocolTests
|
||||
public class MessagePackHubProtocolTests : MessagePackHubProtocolTestBase
|
||||
{
|
||||
private static readonly IDictionary<string, string> TestHeaders = new Dictionary<string, string>
|
||||
{
|
||||
{ "Foo", "Bar" },
|
||||
{ "KeyWith\nNew\r\nLines", "Still Works" },
|
||||
{ "ValueWithNewLines", "Also\nWorks\r\nFine" },
|
||||
};
|
||||
|
||||
private static readonly MessagePackHubProtocol _hubProtocol
|
||||
= new MessagePackHubProtocol();
|
||||
|
||||
public enum TestEnum
|
||||
{
|
||||
Zero = 0,
|
||||
One
|
||||
}
|
||||
|
||||
// Test Data for Parse/WriteMessages:
|
||||
// * Name: A string name that is used when reporting the test (it's the ToString value for ProtocolTestData)
|
||||
// * Message: The HubMessage that is either expected (in Parse) or used as input (in Write)
|
||||
// * Binary: Base64-encoded binary "baseline" to sanity-check MessagePack-CSharp behavior
|
||||
//
|
||||
// When changing the tests/message pack parsing if you get test failures look at the base64 encoding and
|
||||
// use a tool like https://sugendran.github.io/msgpack-visualizer/ to verify that the MsgPack is correct and then just replace the Base64 value.
|
||||
|
||||
public static IEnumerable<object[]> TestDataNames
|
||||
{
|
||||
get
|
||||
{
|
||||
foreach (var k in TestData.Keys)
|
||||
{
|
||||
yield return new object[] { k };
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public static IDictionary<string, ProtocolTestData> TestData => new[]
|
||||
{
|
||||
// Invocation messages
|
||||
new ProtocolTestData(
|
||||
name: "InvocationWithNoHeadersAndNoArgs",
|
||||
message: new InvocationMessage("xyz", "method", Array.Empty<object>()),
|
||||
binary: "lgGAo3h5eqZtZXRob2SQkA=="),
|
||||
new ProtocolTestData(
|
||||
name: "InvocationWithNoHeadersNoIdAndNoArgs",
|
||||
message: new InvocationMessage("method", Array.Empty<object>()),
|
||||
binary: "lgGAwKZtZXRob2SQkA=="),
|
||||
new ProtocolTestData(
|
||||
name: "InvocationWithNoHeadersNoIdAndSingleNullArg",
|
||||
message: new InvocationMessage("method", new object[] { null }),
|
||||
binary: "lgGAwKZtZXRob2SRwJA="),
|
||||
new ProtocolTestData(
|
||||
name: "InvocationWithNoHeadersNoIdAndSingleIntArg",
|
||||
message: new InvocationMessage("method", new object[] { 42 }),
|
||||
binary: "lgGAwKZtZXRob2SRKpA="),
|
||||
new ProtocolTestData(
|
||||
name: "InvocationWithNoHeadersNoIdIntAndStringArgs",
|
||||
message: new InvocationMessage("method", new object[] { 42, "string" }),
|
||||
binary: "lgGAwKZtZXRob2SSKqZzdHJpbmeQ"),
|
||||
new ProtocolTestData(
|
||||
name: "InvocationWithNoHeadersNoIdIntAndEnumArgs",
|
||||
message: new InvocationMessage("method", new object[] { 42, TestEnum.One }),
|
||||
binary: "lgGAwKZtZXRob2SSKqNPbmWQ"),
|
||||
new ProtocolTestData(
|
||||
name: "InvocationWithNoHeadersNoIdAndCustomObjectArg",
|
||||
message: new InvocationMessage("method", new object[] { 42, "string", new CustomObject() }),
|
||||
binary: "lgGAwKZtZXRob2STKqZzdHJpbmeGqlN0cmluZ1Byb3CoU2lnbmFsUiGqRG91YmxlUHJvcMtAGSH7VELPEqdJbnRQcm9wKqxEYXRlVGltZVByb3DW/1jsHICoTnVsbFByb3DAq0J5dGVBcnJQcm9wxAMBAgOQ"),
|
||||
new ProtocolTestData(
|
||||
name: "InvocationWithNoHeadersNoIdAndArrayOfCustomObjectArgs",
|
||||
message: new InvocationMessage("method", new object[] { new CustomObject(), new CustomObject() }),
|
||||
binary: "lgGAwKZtZXRob2SShqpTdHJpbmdQcm9wqFNpZ25hbFIhqkRvdWJsZVByb3DLQBkh+1RCzxKnSW50UHJvcCqsRGF0ZVRpbWVQcm9w1v9Y7ByAqE51bGxQcm9wwKtCeXRlQXJyUHJvcMQDAQIDhqpTdHJpbmdQcm9wqFNpZ25hbFIhqkRvdWJsZVByb3DLQBkh+1RCzxKnSW50UHJvcCqsRGF0ZVRpbWVQcm9w1v9Y7ByAqE51bGxQcm9wwKtCeXRlQXJyUHJvcMQDAQIDkA=="),
|
||||
new ProtocolTestData(
|
||||
name: "InvocationWithHeadersNoIdAndArrayOfCustomObjectArgs",
|
||||
message: AddHeaders(TestHeaders, new InvocationMessage("method", new object[] { new CustomObject(), new CustomObject() })),
|
||||
binary: "lgGDo0Zvb6NCYXKyS2V5V2l0aApOZXcNCkxpbmVzq1N0aWxsIFdvcmtzsVZhbHVlV2l0aE5ld0xpbmVzsEFsc28KV29ya3MNCkZpbmXApm1ldGhvZJKGqlN0cmluZ1Byb3CoU2lnbmFsUiGqRG91YmxlUHJvcMtAGSH7VELPEqdJbnRQcm9wKqxEYXRlVGltZVByb3DW/1jsHICoTnVsbFByb3DAq0J5dGVBcnJQcm9wxAMBAgOGqlN0cmluZ1Byb3CoU2lnbmFsUiGqRG91YmxlUHJvcMtAGSH7VELPEqdJbnRQcm9wKqxEYXRlVGltZVByb3DW/1jsHICoTnVsbFByb3DAq0J5dGVBcnJQcm9wxAMBAgOQ"),
|
||||
new ProtocolTestData(
|
||||
name: "InvocationWithStreamArgument",
|
||||
message: new InvocationMessage(null, "Target", Array.Empty<object>(), new string[] { "__test_id__" }),
|
||||
binary: "lgGAwKZUYXJnZXSQkatfX3Rlc3RfaWRfXw=="),
|
||||
new ProtocolTestData(
|
||||
name: "InvocationWithStreamAndNormalArgument",
|
||||
message: new InvocationMessage(null, "Target", new object[] { 42 }, new string[] { "__test_id__" }),
|
||||
binary: "lgGAwKZUYXJnZXSRKpGrX190ZXN0X2lkX18="),
|
||||
new ProtocolTestData(
|
||||
name: "InvocationWithMulitpleStreams",
|
||||
message: new InvocationMessage(null, "Target", Array.Empty<object>(), new string[] { "__test_id__", "__test_id2__" }),
|
||||
binary: "lgGAwKZUYXJnZXSQkqtfX3Rlc3RfaWRfX6xfX3Rlc3RfaWQyX18="),
|
||||
|
||||
// StreamItem Messages
|
||||
new ProtocolTestData(
|
||||
name: "StreamItemWithNoHeadersAndNullItem",
|
||||
message: new StreamItemMessage("xyz", item: null),
|
||||
binary: "lAKAo3h5esA="),
|
||||
new ProtocolTestData(
|
||||
name: "StreamItemWithNoHeadersAndIntItem",
|
||||
message: new StreamItemMessage("xyz", item: 42),
|
||||
binary: "lAKAo3h5eio="),
|
||||
new ProtocolTestData(
|
||||
name: "StreamItemWithNoHeadersAndFloatItem",
|
||||
message: new StreamItemMessage("xyz", item: 42.0f),
|
||||
binary: "lAKAo3h5espCKAAA"),
|
||||
new ProtocolTestData(
|
||||
name: "StreamItemWithNoHeadersAndStringItem",
|
||||
message: new StreamItemMessage("xyz", item: "string"),
|
||||
binary: "lAKAo3h5eqZzdHJpbmc="),
|
||||
new ProtocolTestData(
|
||||
name: "StreamItemWithNoHeadersAndBoolItem",
|
||||
message: new StreamItemMessage("xyz", item: true),
|
||||
binary: "lAKAo3h5esM="),
|
||||
new ProtocolTestData(
|
||||
name: "StreamItemWithNoHeadersAndEnumItem",
|
||||
message: new StreamItemMessage("xyz", item: TestEnum.One),
|
||||
binary: "lAKAo3h5eqNPbmU="),
|
||||
new ProtocolTestData(
|
||||
name: "StreamItemWithNoHeadersAndCustomObjectItem",
|
||||
message: new StreamItemMessage("xyz", item: new CustomObject()),
|
||||
binary: "lAKAo3h5eoaqU3RyaW5nUHJvcKhTaWduYWxSIapEb3VibGVQcm9wy0AZIftUQs8Sp0ludFByb3AqrERhdGVUaW1lUHJvcNb/WOwcgKhOdWxsUHJvcMCrQnl0ZUFyclByb3DEAwECAw=="),
|
||||
new ProtocolTestData(
|
||||
name: "StreamItemWithNoHeadersAndCustomObjectArrayItem",
|
||||
message: new StreamItemMessage("xyz", item: new[] { new CustomObject(), new CustomObject() }),
|
||||
binary: "lAKAo3h5epKGqlN0cmluZ1Byb3CoU2lnbmFsUiGqRG91YmxlUHJvcMtAGSH7VELPEqdJbnRQcm9wKqxEYXRlVGltZVByb3DW/1jsHICoTnVsbFByb3DAq0J5dGVBcnJQcm9wxAMBAgOGqlN0cmluZ1Byb3CoU2lnbmFsUiGqRG91YmxlUHJvcMtAGSH7VELPEqdJbnRQcm9wKqxEYXRlVGltZVByb3DW/1jsHICoTnVsbFByb3DAq0J5dGVBcnJQcm9wxAMBAgM="),
|
||||
new ProtocolTestData(
|
||||
name: "StreamItemWithHeadersAndCustomObjectArrayItem",
|
||||
message: AddHeaders(TestHeaders, new StreamItemMessage("xyz", item: new[] { new CustomObject(), new CustomObject() })),
|
||||
binary: "lAKDo0Zvb6NCYXKyS2V5V2l0aApOZXcNCkxpbmVzq1N0aWxsIFdvcmtzsVZhbHVlV2l0aE5ld0xpbmVzsEFsc28KV29ya3MNCkZpbmWjeHl6koaqU3RyaW5nUHJvcKhTaWduYWxSIapEb3VibGVQcm9wy0AZIftUQs8Sp0ludFByb3AqrERhdGVUaW1lUHJvcNb/WOwcgKhOdWxsUHJvcMCrQnl0ZUFyclByb3DEAwECA4aqU3RyaW5nUHJvcKhTaWduYWxSIapEb3VibGVQcm9wy0AZIftUQs8Sp0ludFByb3AqrERhdGVUaW1lUHJvcNb/WOwcgKhOdWxsUHJvcMCrQnl0ZUFyclByb3DEAwECAw=="),
|
||||
|
||||
// Completion Messages
|
||||
new ProtocolTestData(
|
||||
name: "CompletionWithNoHeadersAndError",
|
||||
message: CompletionMessage.WithError("xyz", error: "Error not found!"),
|
||||
binary: "lQOAo3h5egGwRXJyb3Igbm90IGZvdW5kIQ=="),
|
||||
new ProtocolTestData(
|
||||
name: "CompletionWithHeadersAndError",
|
||||
message: AddHeaders(TestHeaders, CompletionMessage.WithError("xyz", error: "Error not found!")),
|
||||
binary: "lQODo0Zvb6NCYXKyS2V5V2l0aApOZXcNCkxpbmVzq1N0aWxsIFdvcmtzsVZhbHVlV2l0aE5ld0xpbmVzsEFsc28KV29ya3MNCkZpbmWjeHl6AbBFcnJvciBub3QgZm91bmQh"),
|
||||
new ProtocolTestData(
|
||||
name: "CompletionWithNoHeadersAndNoResult",
|
||||
message: CompletionMessage.Empty("xyz"),
|
||||
binary: "lAOAo3h5egI="),
|
||||
new ProtocolTestData(
|
||||
name: "CompletionWithHeadersAndNoResult",
|
||||
message: AddHeaders(TestHeaders, CompletionMessage.Empty("xyz")),
|
||||
binary: "lAODo0Zvb6NCYXKyS2V5V2l0aApOZXcNCkxpbmVzq1N0aWxsIFdvcmtzsVZhbHVlV2l0aE5ld0xpbmVzsEFsc28KV29ya3MNCkZpbmWjeHl6Ag=="),
|
||||
new ProtocolTestData(
|
||||
name: "CompletionWithNoHeadersAndNullResult",
|
||||
message: CompletionMessage.WithResult("xyz", payload: null),
|
||||
binary: "lQOAo3h5egPA"),
|
||||
new ProtocolTestData(
|
||||
name: "CompletionWithNoHeadersAndIntResult",
|
||||
message: CompletionMessage.WithResult("xyz", payload: 42),
|
||||
binary: "lQOAo3h5egMq"),
|
||||
new ProtocolTestData(
|
||||
name: "CompletionWithNoHeadersAndEnumResult",
|
||||
message: CompletionMessage.WithResult("xyz", payload: TestEnum.One),
|
||||
binary: "lQOAo3h5egOjT25l"),
|
||||
new ProtocolTestData(
|
||||
name: "CompletionWithNoHeadersAndFloatResult",
|
||||
message: CompletionMessage.WithResult("xyz", payload: 42.0f),
|
||||
binary: "lQOAo3h5egPKQigAAA=="),
|
||||
new ProtocolTestData(
|
||||
name: "CompletionWithNoHeadersAndStringResult",
|
||||
message: CompletionMessage.WithResult("xyz", payload: "string"),
|
||||
binary: "lQOAo3h5egOmc3RyaW5n"),
|
||||
new ProtocolTestData(
|
||||
name: "CompletionWithNoHeadersAndBooleanResult",
|
||||
message: CompletionMessage.WithResult("xyz", payload: true),
|
||||
binary: "lQOAo3h5egPD"),
|
||||
new ProtocolTestData(
|
||||
name: "CompletionWithNoHeadersAndCustomObjectResult",
|
||||
message: CompletionMessage.WithResult("xyz", payload: new CustomObject()),
|
||||
binary: "lQOAo3h5egOGqlN0cmluZ1Byb3CoU2lnbmFsUiGqRG91YmxlUHJvcMtAGSH7VELPEqdJbnRQcm9wKqxEYXRlVGltZVByb3DW/1jsHICoTnVsbFByb3DAq0J5dGVBcnJQcm9wxAMBAgM="),
|
||||
new ProtocolTestData(
|
||||
name: "CompletionWithNoHeadersAndCustomObjectArrayResult",
|
||||
message: CompletionMessage.WithResult("xyz", payload: new[] { new CustomObject(), new CustomObject() }),
|
||||
binary: "lQOAo3h5egOShqpTdHJpbmdQcm9wqFNpZ25hbFIhqkRvdWJsZVByb3DLQBkh+1RCzxKnSW50UHJvcCqsRGF0ZVRpbWVQcm9w1v9Y7ByAqE51bGxQcm9wwKtCeXRlQXJyUHJvcMQDAQIDhqpTdHJpbmdQcm9wqFNpZ25hbFIhqkRvdWJsZVByb3DLQBkh+1RCzxKnSW50UHJvcCqsRGF0ZVRpbWVQcm9w1v9Y7ByAqE51bGxQcm9wwKtCeXRlQXJyUHJvcMQDAQID"),
|
||||
new ProtocolTestData(
|
||||
name: "CompletionWithHeadersAndCustomObjectArrayResult",
|
||||
message: AddHeaders(TestHeaders, CompletionMessage.WithResult("xyz", payload: new[] { new CustomObject(), new CustomObject() })),
|
||||
binary: "lQODo0Zvb6NCYXKyS2V5V2l0aApOZXcNCkxpbmVzq1N0aWxsIFdvcmtzsVZhbHVlV2l0aE5ld0xpbmVzsEFsc28KV29ya3MNCkZpbmWjeHl6A5KGqlN0cmluZ1Byb3CoU2lnbmFsUiGqRG91YmxlUHJvcMtAGSH7VELPEqdJbnRQcm9wKqxEYXRlVGltZVByb3DW/1jsHICoTnVsbFByb3DAq0J5dGVBcnJQcm9wxAMBAgOGqlN0cmluZ1Byb3CoU2lnbmFsUiGqRG91YmxlUHJvcMtAGSH7VELPEqdJbnRQcm9wKqxEYXRlVGltZVByb3DW/1jsHICoTnVsbFByb3DAq0J5dGVBcnJQcm9wxAMBAgM="),
|
||||
|
||||
// StreamInvocation Messages
|
||||
new ProtocolTestData(
|
||||
name: "StreamInvocationWithNoHeadersAndNoArgs",
|
||||
message: new StreamInvocationMessage("xyz", "method", Array.Empty<object>()),
|
||||
binary: "lgSAo3h5eqZtZXRob2SQkA=="),
|
||||
new ProtocolTestData(
|
||||
name: "StreamInvocationWithNoHeadersAndNullArg",
|
||||
message: new StreamInvocationMessage("xyz", "method", new object[] { null }),
|
||||
binary: "lgSAo3h5eqZtZXRob2SRwJA="),
|
||||
new ProtocolTestData(
|
||||
name: "StreamInvocationWithNoHeadersAndIntArg",
|
||||
message: new StreamInvocationMessage("xyz", "method", new object[] { 42 }),
|
||||
binary: "lgSAo3h5eqZtZXRob2SRKpA="),
|
||||
new ProtocolTestData(
|
||||
name: "StreamInvocationWithNoHeadersAndEnumArg",
|
||||
message: new StreamInvocationMessage("xyz", "method", new object[] { TestEnum.One }),
|
||||
binary: "lgSAo3h5eqZtZXRob2SRo09uZZA="),
|
||||
new ProtocolTestData(
|
||||
name: "StreamInvocationWithStreamArgument",
|
||||
message: new StreamInvocationMessage("xyz", "method", Array.Empty<object>(), new string[] { "__test_id__" }),
|
||||
binary: "lgSAo3h5eqZtZXRob2SQkatfX3Rlc3RfaWRfXw=="),
|
||||
new ProtocolTestData(
|
||||
name: "StreamInvocationWithStreamAndNormalArgument",
|
||||
message: new StreamInvocationMessage("xyz", "method", new object[] { 42 }, new string[] { "__test_id__" }),
|
||||
binary: "lgSAo3h5eqZtZXRob2SRKpGrX190ZXN0X2lkX18="),
|
||||
new ProtocolTestData(
|
||||
name: "StreamInvocationWithNoHeadersAndIntAndStringArgs",
|
||||
message: new StreamInvocationMessage("xyz", "method", new object[] { 42, "string" }),
|
||||
binary: "lgSAo3h5eqZtZXRob2SSKqZzdHJpbmeQ"),
|
||||
new ProtocolTestData(
|
||||
name: "StreamInvocationWithNoHeadersAndIntStringAndCustomObjectArgs",
|
||||
message: new StreamInvocationMessage("xyz", "method", new object[] { 42, "string", new CustomObject() }),
|
||||
binary: "lgSAo3h5eqZtZXRob2STKqZzdHJpbmeGqlN0cmluZ1Byb3CoU2lnbmFsUiGqRG91YmxlUHJvcMtAGSH7VELPEqdJbnRQcm9wKqxEYXRlVGltZVByb3DW/1jsHICoTnVsbFByb3DAq0J5dGVBcnJQcm9wxAMBAgOQ"),
|
||||
new ProtocolTestData(
|
||||
name: "StreamInvocationWithNoHeadersAndCustomObjectArrayArg",
|
||||
message: new StreamInvocationMessage("xyz", "method", new object[] { new CustomObject(), new CustomObject() }),
|
||||
binary: "lgSAo3h5eqZtZXRob2SShqpTdHJpbmdQcm9wqFNpZ25hbFIhqkRvdWJsZVByb3DLQBkh+1RCzxKnSW50UHJvcCqsRGF0ZVRpbWVQcm9w1v9Y7ByAqE51bGxQcm9wwKtCeXRlQXJyUHJvcMQDAQIDhqpTdHJpbmdQcm9wqFNpZ25hbFIhqkRvdWJsZVByb3DLQBkh+1RCzxKnSW50UHJvcCqsRGF0ZVRpbWVQcm9w1v9Y7ByAqE51bGxQcm9wwKtCeXRlQXJyUHJvcMQDAQIDkA=="),
|
||||
new ProtocolTestData(
|
||||
name: "StreamInvocationWithHeadersAndCustomObjectArrayArg",
|
||||
message: AddHeaders(TestHeaders, new StreamInvocationMessage("xyz", "method", new object[] { new CustomObject(), new CustomObject() })),
|
||||
binary: "lgSDo0Zvb6NCYXKyS2V5V2l0aApOZXcNCkxpbmVzq1N0aWxsIFdvcmtzsVZhbHVlV2l0aE5ld0xpbmVzsEFsc28KV29ya3MNCkZpbmWjeHl6pm1ldGhvZJKGqlN0cmluZ1Byb3CoU2lnbmFsUiGqRG91YmxlUHJvcMtAGSH7VELPEqdJbnRQcm9wKqxEYXRlVGltZVByb3DW/1jsHICoTnVsbFByb3DAq0J5dGVBcnJQcm9wxAMBAgOGqlN0cmluZ1Byb3CoU2lnbmFsUiGqRG91YmxlUHJvcMtAGSH7VELPEqdJbnRQcm9wKqxEYXRlVGltZVByb3DW/1jsHICoTnVsbFByb3DAq0J5dGVBcnJQcm9wxAMBAgOQ"),
|
||||
|
||||
// CancelInvocation Messages
|
||||
new ProtocolTestData(
|
||||
name: "CancelInvocationWithNoHeaders",
|
||||
message: new CancelInvocationMessage("xyz"),
|
||||
binary: "kwWAo3h5eg=="),
|
||||
new ProtocolTestData(
|
||||
name: "CancelInvocationWithHeaders",
|
||||
message: AddHeaders(TestHeaders, new CancelInvocationMessage("xyz")),
|
||||
binary: "kwWDo0Zvb6NCYXKyS2V5V2l0aApOZXcNCkxpbmVzq1N0aWxsIFdvcmtzsVZhbHVlV2l0aE5ld0xpbmVzsEFsc28KV29ya3MNCkZpbmWjeHl6"),
|
||||
|
||||
// Ping Messages
|
||||
new ProtocolTestData(
|
||||
name: "Ping",
|
||||
message: PingMessage.Instance,
|
||||
binary: "kQY="),
|
||||
}.ToDictionary(t => t.Name);
|
||||
|
||||
[Theory]
|
||||
[MemberData(nameof(TestDataNames))]
|
||||
public void ParseMessages(string testDataName)
|
||||
{
|
||||
var testData = TestData[testDataName];
|
||||
|
||||
// Verify that the input binary string decodes to the expected MsgPack primitives
|
||||
var bytes = Convert.FromBase64String(testData.Binary);
|
||||
|
||||
// Parse the input fully now.
|
||||
bytes = Frame(bytes);
|
||||
var data = new ReadOnlySequence<byte>(bytes);
|
||||
Assert.True(_hubProtocol.TryParseMessage(ref data, new TestBinder(testData.Message), out var message));
|
||||
|
||||
Assert.NotNull(message);
|
||||
Assert.Equal(testData.Message, message, TestHubMessageEqualityComparer.Instance);
|
||||
}
|
||||
protected override IHubProtocol HubProtocol => new MessagePackHubProtocol();
|
||||
|
||||
[Fact]
|
||||
public void ParseMessageWithExtraData()
|
||||
public void SerializerCanSerializeTypesWithNoDefaultCtor()
|
||||
{
|
||||
var expectedMessage = new InvocationMessage("xyz", "method", Array.Empty<object>());
|
||||
|
||||
// Verify that the input binary string decodes to the expected MsgPack primitives
|
||||
var bytes = new byte[] { ArrayBytes(8),
|
||||
1,
|
||||
0x80,
|
||||
StringBytes(3), (byte)'x', (byte)'y', (byte)'z',
|
||||
StringBytes(6), (byte)'m', (byte)'e', (byte)'t', (byte)'h', (byte)'o', (byte)'d',
|
||||
ArrayBytes(0), // Arguments
|
||||
ArrayBytes(0), // Streams
|
||||
0xc3,
|
||||
StringBytes(2), (byte)'e', (byte)'x' };
|
||||
|
||||
// Parse the input fully now.
|
||||
bytes = Frame(bytes);
|
||||
var data = new ReadOnlySequence<byte>(bytes);
|
||||
Assert.True(_hubProtocol.TryParseMessage(ref data, new TestBinder(expectedMessage), out var message));
|
||||
|
||||
Assert.NotNull(message);
|
||||
Assert.Equal(expectedMessage, message, TestHubMessageEqualityComparer.Instance);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[MemberData(nameof(TestDataNames))]
|
||||
public void WriteMessages(string testDataName)
|
||||
{
|
||||
var testData = TestData[testDataName];
|
||||
|
||||
var bytes = Write(testData.Message);
|
||||
|
||||
// Unframe the message to check the binary encoding
|
||||
var byteSpan = new ReadOnlySequence<byte>(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
|
||||
var actual = Convert.ToBase64String(unframed.ToArray());
|
||||
Assert.True(string.Equals(actual, testData.Binary, StringComparison.Ordinal), $"Binary encoding changed from{Environment.NewLine} [{testData.Binary}]{Environment.NewLine} to{Environment.NewLine} [{actual}]{Environment.NewLine}Please verify the MsgPack output and update the baseline");
|
||||
var result = Write(CompletionMessage.WithResult("0", new List<int> { 42 }.AsReadOnly()));
|
||||
AssertMessages(new byte[] { ArrayBytes(5), 3, 0x80, StringBytes(1), (byte)'0', 0x03, ArrayBytes(1), 42 }, result);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
|
|
@ -323,9 +34,9 @@ namespace Microsoft.AspNetCore.SignalR.Common.Tests.Internal.Protocol
|
|||
|
||||
try
|
||||
{
|
||||
_hubProtocol.WriteMessage(CompletionMessage.WithResult("xyz", dateTime), writer);
|
||||
HubProtocol.WriteMessage(CompletionMessage.WithResult("xyz", dateTime), writer);
|
||||
var bytes = new ReadOnlySequence<byte>(writer.ToArray());
|
||||
_hubProtocol.TryParseMessage(ref bytes, new TestBinder(typeof(DateTime)), out var hubMessage);
|
||||
HubProtocol.TryParseMessage(ref bytes, new TestBinder(typeof(DateTime)), out var hubMessage);
|
||||
|
||||
var completionMessage = Assert.IsType<CompletionMessage>(hubMessage);
|
||||
|
||||
|
|
@ -349,9 +60,9 @@ namespace Microsoft.AspNetCore.SignalR.Common.Tests.Internal.Protocol
|
|||
|
||||
try
|
||||
{
|
||||
_hubProtocol.WriteMessage(CompletionMessage.WithResult("xyz", dateTimeOffset), writer);
|
||||
HubProtocol.WriteMessage(CompletionMessage.WithResult("xyz", dateTimeOffset), writer);
|
||||
var bytes = new ReadOnlySequence<byte>(writer.ToArray());
|
||||
_hubProtocol.TryParseMessage(ref bytes, new TestBinder(typeof(DateTimeOffset)), out var hubMessage);
|
||||
HubProtocol.TryParseMessage(ref bytes, new TestBinder(typeof(DateTimeOffset)), out var hubMessage);
|
||||
|
||||
var completionMessage = Assert.IsType<CompletionMessage>(hubMessage);
|
||||
|
||||
|
|
@ -364,193 +75,125 @@ namespace Microsoft.AspNetCore.SignalR.Common.Tests.Internal.Protocol
|
|||
}
|
||||
}
|
||||
|
||||
public static IDictionary<string, InvalidMessageData> InvalidPayloads => new[]
|
||||
public static IEnumerable<object[]> TestDataNames
|
||||
{
|
||||
// Message Type
|
||||
new InvalidMessageData("MessageTypeString", new byte[] { 0x91, 0xa3, (byte)'f', (byte)'o', (byte)'o' }, "Reading 'messageType' as Int32 failed."),
|
||||
get
|
||||
{
|
||||
foreach (var k in TestData.Keys)
|
||||
{
|
||||
yield return new object[] { k };
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Headers
|
||||
new InvalidMessageData("HeadersNotAMap", new byte[] { 0x92, 1, 0xa3, (byte)'f', (byte)'o', (byte)'o' }, "Reading map length for 'headers' failed."),
|
||||
new InvalidMessageData("HeaderKeyInt", new byte[] { 0x92, 1, 0x82, 0x2a, 0xa3, (byte)'f', (byte)'o', (byte)'o' }, "Reading 'headers[0].Key' as String failed."),
|
||||
new InvalidMessageData("HeaderValueInt", new byte[] { 0x92, 1, 0x82, 0xa3, (byte)'f', (byte)'o', (byte)'o', 42 }, "Reading 'headers[0].Value' as String failed."),
|
||||
new InvalidMessageData("HeaderKeyArray", new byte[] { 0x92, 1, 0x84, 0xa3, (byte)'f', (byte)'o', (byte)'o', 0xa3, (byte)'f', (byte)'o', (byte)'o', 0x90, 0xa3, (byte)'f', (byte)'o', (byte)'o' }, "Reading 'headers[1].Key' as String failed."),
|
||||
new InvalidMessageData("HeaderValueArray", new byte[] { 0x92, 1, 0x84, 0xa3, (byte)'f', (byte)'o', (byte)'o', 0xa3, (byte)'f', (byte)'o', (byte)'o', 0xa3, (byte)'f', (byte)'o', (byte)'o', 0x90 }, "Reading 'headers[1].Value' as String failed."),
|
||||
// TestData that requires object serialization
|
||||
public static IDictionary<string, MessagePackHubProtocolTestBase.ProtocolTestData> TestData => new[]
|
||||
{
|
||||
// Completion messages
|
||||
new ProtocolTestData(
|
||||
name: "CompletionWithNoHeadersAndNullResult",
|
||||
message: CompletionMessage.WithResult("xyz", payload: null),
|
||||
binary: "lQOAo3h5egPA"),
|
||||
new ProtocolTestData(
|
||||
name: "CompletionWithNoHeadersAndCustomObjectResult",
|
||||
message: CompletionMessage.WithResult("xyz", payload: new CustomObject()),
|
||||
binary: "lQOAo3h5egOGqlN0cmluZ1Byb3CoU2lnbmFsUiGqRG91YmxlUHJvcMtAGSH7VELPEqdJbnRQcm9wKqxEYXRlVGltZVByb3DW/1jsHICoTnVsbFByb3DAq0J5dGVBcnJQcm9wxAMBAgM="),
|
||||
new ProtocolTestData(
|
||||
name: "CompletionWithNoHeadersAndCustomObjectArrayResult",
|
||||
message: CompletionMessage.WithResult("xyz", payload: new[] { new CustomObject(), new CustomObject() }),
|
||||
binary: "lQOAo3h5egOShqpTdHJpbmdQcm9wqFNpZ25hbFIhqkRvdWJsZVByb3DLQBkh+1RCzxKnSW50UHJvcCqsRGF0ZVRpbWVQcm9w1v9Y7ByAqE51bGxQcm9wwKtCeXRlQXJyUHJvcMQDAQIDhqpTdHJpbmdQcm9wqFNpZ25hbFIhqkRvdWJsZVByb3DLQBkh+1RCzxKnSW50UHJvcCqsRGF0ZVRpbWVQcm9w1v9Y7ByAqE51bGxQcm9wwKtCeXRlQXJyUHJvcMQDAQID"),
|
||||
new ProtocolTestData(
|
||||
name: "CompletionWithHeadersAndCustomObjectArrayResult",
|
||||
message: AddHeaders(TestHeaders, CompletionMessage.WithResult("xyz", payload: new[] { new CustomObject(), new CustomObject() })),
|
||||
binary: "lQODo0Zvb6NCYXKyS2V5V2l0aApOZXcNCkxpbmVzq1N0aWxsIFdvcmtzsVZhbHVlV2l0aE5ld0xpbmVzsEFsc28KV29ya3MNCkZpbmWjeHl6A5KGqlN0cmluZ1Byb3CoU2lnbmFsUiGqRG91YmxlUHJvcMtAGSH7VELPEqdJbnRQcm9wKqxEYXRlVGltZVByb3DW/1jsHICoTnVsbFByb3DAq0J5dGVBcnJQcm9wxAMBAgOGqlN0cmluZ1Byb3CoU2lnbmFsUiGqRG91YmxlUHJvcMtAGSH7VELPEqdJbnRQcm9wKqxEYXRlVGltZVByb3DW/1jsHICoTnVsbFByb3DAq0J5dGVBcnJQcm9wxAMBAgM="),
|
||||
new ProtocolTestData(
|
||||
name: "CompletionWithNoHeadersAndEnumResult",
|
||||
message: CompletionMessage.WithResult("xyz", payload: TestEnum.One),
|
||||
binary: "lQOAo3h5egOjT25l"),
|
||||
|
||||
// InvocationMessage
|
||||
new InvalidMessageData("InvocationMissingId", new byte[] { 0x92, 1, 0x80 }, "Reading 'invocationId' as String failed."),
|
||||
new InvalidMessageData("InvocationIdBoolean", new byte[] { 0x91, 1, 0x80, 0xc2 }, "Reading 'invocationId' as String failed."),
|
||||
new InvalidMessageData("InvocationTargetMissing", new byte[] { 0x93, 1, 0x80, 0xa3, (byte)'a', (byte)'b', (byte)'c' }, "Reading 'target' as String failed."),
|
||||
new InvalidMessageData("InvocationTargetInt", new byte[] { 0x94, 1, 0x80, 0xa3, (byte)'a', (byte)'b', (byte)'c', 42 }, "Reading 'target' as String failed."),
|
||||
// Invocation messages
|
||||
new ProtocolTestData(
|
||||
name: "InvocationWithNoHeadersNoIdAndSingleNullArg",
|
||||
message: new InvocationMessage("method", new object[] { null }),
|
||||
binary: "lgGAwKZtZXRob2SRwJA="),
|
||||
new ProtocolTestData(
|
||||
name: "InvocationWithNoHeadersNoIdIntAndEnumArgs",
|
||||
message: new InvocationMessage("method", new object[] { 42, TestEnum.One }),
|
||||
binary: "lgGAwKZtZXRob2SSKqNPbmWQ"),
|
||||
new ProtocolTestData(
|
||||
name: "InvocationWithNoHeadersNoIdAndCustomObjectArg",
|
||||
message: new InvocationMessage("method", new object[] { 42, "string", new CustomObject() }),
|
||||
binary: "lgGAwKZtZXRob2STKqZzdHJpbmeGqlN0cmluZ1Byb3CoU2lnbmFsUiGqRG91YmxlUHJvcMtAGSH7VELPEqdJbnRQcm9wKqxEYXRlVGltZVByb3DW/1jsHICoTnVsbFByb3DAq0J5dGVBcnJQcm9wxAMBAgOQ"),
|
||||
new ProtocolTestData(
|
||||
name: "InvocationWithNoHeadersNoIdAndArrayOfCustomObjectArgs",
|
||||
message: new InvocationMessage("method", new object[] { new CustomObject(), new CustomObject() }),
|
||||
binary: "lgGAwKZtZXRob2SShqpTdHJpbmdQcm9wqFNpZ25hbFIhqkRvdWJsZVByb3DLQBkh+1RCzxKnSW50UHJvcCqsRGF0ZVRpbWVQcm9w1v9Y7ByAqE51bGxQcm9wwKtCeXRlQXJyUHJvcMQDAQIDhqpTdHJpbmdQcm9wqFNpZ25hbFIhqkRvdWJsZVByb3DLQBkh+1RCzxKnSW50UHJvcCqsRGF0ZVRpbWVQcm9w1v9Y7ByAqE51bGxQcm9wwKtCeXRlQXJyUHJvcMQDAQIDkA=="),
|
||||
new ProtocolTestData(
|
||||
name: "InvocationWithHeadersNoIdAndArrayOfCustomObjectArgs",
|
||||
message: AddHeaders(TestHeaders, new InvocationMessage("method", new object[] { new CustomObject(), new CustomObject() })),
|
||||
binary: "lgGDo0Zvb6NCYXKyS2V5V2l0aApOZXcNCkxpbmVzq1N0aWxsIFdvcmtzsVZhbHVlV2l0aE5ld0xpbmVzsEFsc28KV29ya3MNCkZpbmXApm1ldGhvZJKGqlN0cmluZ1Byb3CoU2lnbmFsUiGqRG91YmxlUHJvcMtAGSH7VELPEqdJbnRQcm9wKqxEYXRlVGltZVByb3DW/1jsHICoTnVsbFByb3DAq0J5dGVBcnJQcm9wxAMBAgOGqlN0cmluZ1Byb3CoU2lnbmFsUiGqRG91YmxlUHJvcMtAGSH7VELPEqdJbnRQcm9wKqxEYXRlVGltZVByb3DW/1jsHICoTnVsbFByb3DAq0J5dGVBcnJQcm9wxAMBAgOQ"),
|
||||
|
||||
// StreamInvocationMessage
|
||||
new InvalidMessageData("StreamInvocationMissingId", new byte[] { 0x92, 4, 0x80 }, "Reading 'invocationId' as String failed."),
|
||||
new InvalidMessageData("StreamInvocationIdBoolean", new byte[] { 0x93, 4, 0x80, 0xc2 }, "Reading 'invocationId' as String failed."),
|
||||
new InvalidMessageData("StreamInvocationTargetMissing", new byte[] { 0x93, 4, 0x80, 0xa3, (byte)'a', (byte)'b', (byte)'c' }, "Reading 'target' as String failed."),
|
||||
new InvalidMessageData("StreamInvocationTargetInt", new byte[] { 0x94, 4, 0x80, 0xa3, (byte)'a', (byte)'b', (byte)'c', 42 }, "Reading 'target' as String failed."),
|
||||
// StreamItem Messages
|
||||
new ProtocolTestData(
|
||||
name: "StreamItemWithNoHeadersAndNullItem",
|
||||
message: new StreamItemMessage("xyz", item: null),
|
||||
binary: "lAKAo3h5esA="),
|
||||
new ProtocolTestData(
|
||||
name: "StreamItemWithNoHeadersAndEnumItem",
|
||||
message: new StreamItemMessage("xyz", item: TestEnum.One),
|
||||
binary: "lAKAo3h5eqNPbmU="),
|
||||
new ProtocolTestData(
|
||||
name: "StreamItemWithNoHeadersAndCustomObjectItem",
|
||||
message: new StreamItemMessage("xyz", item: new CustomObject()),
|
||||
binary: "lAKAo3h5eoaqU3RyaW5nUHJvcKhTaWduYWxSIapEb3VibGVQcm9wy0AZIftUQs8Sp0ludFByb3AqrERhdGVUaW1lUHJvcNb/WOwcgKhOdWxsUHJvcMCrQnl0ZUFyclByb3DEAwECAw=="),
|
||||
new ProtocolTestData(
|
||||
name: "StreamItemWithNoHeadersAndCustomObjectArrayItem",
|
||||
message: new StreamItemMessage("xyz", item: new[] { new CustomObject(), new CustomObject() }),
|
||||
binary: "lAKAo3h5epKGqlN0cmluZ1Byb3CoU2lnbmFsUiGqRG91YmxlUHJvcMtAGSH7VELPEqdJbnRQcm9wKqxEYXRlVGltZVByb3DW/1jsHICoTnVsbFByb3DAq0J5dGVBcnJQcm9wxAMBAgOGqlN0cmluZ1Byb3CoU2lnbmFsUiGqRG91YmxlUHJvcMtAGSH7VELPEqdJbnRQcm9wKqxEYXRlVGltZVByb3DW/1jsHICoTnVsbFByb3DAq0J5dGVBcnJQcm9wxAMBAgM="),
|
||||
new ProtocolTestData(
|
||||
name: "StreamItemWithHeadersAndCustomObjectArrayItem",
|
||||
message: AddHeaders(TestHeaders, new StreamItemMessage("xyz", item: new[] { new CustomObject(), new CustomObject() })),
|
||||
binary: "lAKDo0Zvb6NCYXKyS2V5V2l0aApOZXcNCkxpbmVzq1N0aWxsIFdvcmtzsVZhbHVlV2l0aE5ld0xpbmVzsEFsc28KV29ya3MNCkZpbmWjeHl6koaqU3RyaW5nUHJvcKhTaWduYWxSIapEb3VibGVQcm9wy0AZIftUQs8Sp0ludFByb3AqrERhdGVUaW1lUHJvcNb/WOwcgKhOdWxsUHJvcMCrQnl0ZUFyclByb3DEAwECA4aqU3RyaW5nUHJvcKhTaWduYWxSIapEb3VibGVQcm9wy0AZIftUQs8Sp0ludFByb3AqrERhdGVUaW1lUHJvcNb/WOwcgKhOdWxsUHJvcMCrQnl0ZUFyclByb3DEAwECAw=="),
|
||||
|
||||
// StreamItemMessage
|
||||
new InvalidMessageData("StreamItemMissingId", new byte[] { 0x92, 2, 0x80 }, "Reading 'invocationId' as String failed."),
|
||||
new InvalidMessageData("StreamItemInvocationIdBoolean", new byte[] { 0x93, 2, 0x80, 0xc2 }, "Reading 'invocationId' as String failed."),
|
||||
new InvalidMessageData("StreamItemMissing", new byte[] { 0x93, 2, 0x80, 0xa3, (byte)'x', (byte)'y', (byte)'z' }, "Deserializing object of the `String` type for 'item' failed."),
|
||||
new InvalidMessageData("StreamItemTypeMismatch", new byte[] { 0x94, 2, 0x80, 0xa3, (byte)'x', (byte)'y', (byte)'z', 42 }, "Deserializing object of the `String` type for 'item' failed."),
|
||||
|
||||
// CompletionMessage
|
||||
new InvalidMessageData("CompletionMissingId", new byte[] { 0x92, 3, 0x80 }, "Reading 'invocationId' as String failed."),
|
||||
new InvalidMessageData("CompletionIdBoolean", new byte[] { 0x93, 3, 0x80, 0xc2 }, "Reading 'invocationId' as String failed."),
|
||||
new InvalidMessageData("CompletionResultKindString", new byte[] { 0x94, 3, 0x80, 0xa3, (byte)'x', (byte)'y', (byte)'z', 0xa3, (byte)'x', (byte)'y', (byte)'z' }, "Reading 'resultKind' as Int32 failed."),
|
||||
new InvalidMessageData("CompletionResultKindOutOfRange", new byte[] { 0x94, 3, 0x80, 0xa3, (byte)'x', (byte)'y', (byte)'z', 42 }, "Invalid invocation result kind."),
|
||||
new InvalidMessageData("CompletionErrorMissing", new byte[] { 0x94, 3, 0x80, 0xa3, (byte)'x', (byte)'y', (byte)'z', 0x01 }, "Reading 'error' as String failed."),
|
||||
new InvalidMessageData("CompletionErrorInt", new byte[] { 0x95, 3, 0x80, 0xa3, (byte)'x', (byte)'y', (byte)'z', 0x01, 42 }, "Reading 'error' as String failed."),
|
||||
new InvalidMessageData("CompletionResultMissing", new byte[] { 0x94, 3, 0x80, 0xa3, (byte)'x', (byte)'y', (byte)'z', 0x03 }, "Deserializing object of the `String` type for 'argument' failed."),
|
||||
new InvalidMessageData("CompletionResultTypeMismatch", new byte[] { 0x95, 3, 0x80, 0xa3, (byte)'x', (byte)'y', (byte)'z', 0x03, 42 }, "Deserializing object of the `String` type for 'argument' failed."),
|
||||
// StreamInvocation Messages
|
||||
new ProtocolTestData(
|
||||
name: "StreamInvocationWithNoHeadersAndEnumArg",
|
||||
message: new StreamInvocationMessage("xyz", "method", new object[] { TestEnum.One }),
|
||||
binary: "lgSAo3h5eqZtZXRob2SRo09uZZA="),
|
||||
new ProtocolTestData(
|
||||
name: "StreamInvocationWithNoHeadersAndNullArg",
|
||||
message: new StreamInvocationMessage("xyz", "method", new object[] { null }),
|
||||
binary: "lgSAo3h5eqZtZXRob2SRwJA="),
|
||||
new ProtocolTestData(
|
||||
name: "StreamInvocationWithNoHeadersAndIntStringAndCustomObjectArgs",
|
||||
message: new StreamInvocationMessage("xyz", "method", new object[] { 42, "string", new CustomObject() }),
|
||||
binary: "lgSAo3h5eqZtZXRob2STKqZzdHJpbmeGqlN0cmluZ1Byb3CoU2lnbmFsUiGqRG91YmxlUHJvcMtAGSH7VELPEqdJbnRQcm9wKqxEYXRlVGltZVByb3DW/1jsHICoTnVsbFByb3DAq0J5dGVBcnJQcm9wxAMBAgOQ"),
|
||||
new ProtocolTestData(
|
||||
name: "StreamInvocationWithNoHeadersAndCustomObjectArrayArg",
|
||||
message: new StreamInvocationMessage("xyz", "method", new object[] { new CustomObject(), new CustomObject() }),
|
||||
binary: "lgSAo3h5eqZtZXRob2SShqpTdHJpbmdQcm9wqFNpZ25hbFIhqkRvdWJsZVByb3DLQBkh+1RCzxKnSW50UHJvcCqsRGF0ZVRpbWVQcm9w1v9Y7ByAqE51bGxQcm9wwKtCeXRlQXJyUHJvcMQDAQIDhqpTdHJpbmdQcm9wqFNpZ25hbFIhqkRvdWJsZVByb3DLQBkh+1RCzxKnSW50UHJvcCqsRGF0ZVRpbWVQcm9w1v9Y7ByAqE51bGxQcm9wwKtCeXRlQXJyUHJvcMQDAQIDkA=="),
|
||||
new ProtocolTestData(
|
||||
name: "StreamInvocationWithHeadersAndCustomObjectArrayArg",
|
||||
message: AddHeaders(TestHeaders, new StreamInvocationMessage("xyz", "method", new object[] { new CustomObject(), new CustomObject() })),
|
||||
binary: "lgSDo0Zvb6NCYXKyS2V5V2l0aApOZXcNCkxpbmVzq1N0aWxsIFdvcmtzsVZhbHVlV2l0aE5ld0xpbmVzsEFsc28KV29ya3MNCkZpbmWjeHl6pm1ldGhvZJKGqlN0cmluZ1Byb3CoU2lnbmFsUiGqRG91YmxlUHJvcMtAGSH7VELPEqdJbnRQcm9wKqxEYXRlVGltZVByb3DW/1jsHICoTnVsbFByb3DAq0J5dGVBcnJQcm9wxAMBAgOGqlN0cmluZ1Byb3CoU2lnbmFsUiGqRG91YmxlUHJvcMtAGSH7VELPEqdJbnRQcm9wKqxEYXRlVGltZVByb3DW/1jsHICoTnVsbFByb3DAq0J5dGVBcnJQcm9wxAMBAgOQ"),
|
||||
}.ToDictionary(t => t.Name);
|
||||
|
||||
public static IEnumerable<object[]> InvalidPayloadNames => InvalidPayloads.Keys.Select(name => new object[] { name });
|
||||
|
||||
[Theory]
|
||||
[MemberData(nameof(InvalidPayloadNames))]
|
||||
public void ParserThrowsForInvalidMessages(string invalidPayloadName)
|
||||
[MemberData(nameof(TestDataNames))]
|
||||
public void ParseMessages(string testDataName)
|
||||
{
|
||||
var testData = InvalidPayloads[invalidPayloadName];
|
||||
var testData = TestData[testDataName];
|
||||
|
||||
var buffer = Frame(testData.Encoded);
|
||||
var binder = new TestBinder(new[] { typeof(string) }, typeof(string));
|
||||
var data = new ReadOnlySequence<byte>(buffer);
|
||||
var exception = Assert.Throws<InvalidDataException>(() => _hubProtocol.TryParseMessage(ref data, binder, out _));
|
||||
|
||||
Assert.Equal(testData.ErrorMessage, exception.Message);
|
||||
}
|
||||
|
||||
public static IDictionary<string, InvalidMessageData> ArgumentBindingErrors => new[]
|
||||
{
|
||||
// InvocationMessage
|
||||
new InvalidMessageData("InvocationArgumentArrayMissing", new byte[] { 0x94, 1, 0x80, 0xa3, (byte)'a', (byte)'b', (byte)'c', 0xa3, (byte)'x', (byte)'y', (byte)'z' }, "Reading array length for 'arguments' failed."),
|
||||
new InvalidMessageData("InvocationArgumentArrayNotAnArray", new byte[] { 0x95, 1, 0x80, 0xa3, (byte)'a', (byte)'b', (byte)'c', 0xa3, (byte)'x', (byte)'y', (byte)'z', 42 }, "Reading array length for 'arguments' failed."),
|
||||
new InvalidMessageData("InvocationArgumentArraySizeMismatchEmpty", new byte[] { 0x95, 1, 0x80, 0xa3, (byte)'a', (byte)'b', (byte)'c', 0xa3, (byte)'x', (byte)'y', (byte)'z', 0x90 }, "Invocation provides 0 argument(s) but target expects 1."),
|
||||
new InvalidMessageData("InvocationArgumentArraySizeMismatchTooLarge", new byte[] { 0x95, 1, 0x80, 0xa3, (byte)'a', (byte)'b', (byte)'c', 0xa3, (byte)'x', (byte)'y', (byte)'z', 0x92, 0xa1, (byte)'a', 0xa1, (byte)'b' }, "Invocation provides 2 argument(s) but target expects 1."),
|
||||
new InvalidMessageData("InvocationArgumentTypeMismatch", new byte[] { 0x95, 1, 0x80, 0xa3, (byte)'a', (byte)'b', (byte)'c', 0xa3, (byte)'x', (byte)'y', (byte)'z', 0x91, 42 }, "Error binding arguments. Make sure that the types of the provided values match the types of the hub method being invoked."),
|
||||
|
||||
// StreamInvocationMessage
|
||||
new InvalidMessageData("StreamInvocationArgumentArrayMissing", new byte[] { 0x94, 4, 0x80, 0xa3, (byte)'a', (byte)'b', (byte)'c', 0xa3, (byte)'x', (byte)'y', (byte)'z' }, "Reading array length for 'arguments' failed."), // array is missing
|
||||
new InvalidMessageData("StreamInvocationArgumentArrayNotAnArray", new byte[] { 0x95, 4, 0x80, 0xa3, (byte)'a', (byte)'b', (byte)'c', 0xa3, (byte)'x', (byte)'y', (byte)'z', 42 }, "Reading array length for 'arguments' failed."), // arguments isn't an array
|
||||
new InvalidMessageData("StreamInvocationArgumentArraySizeMismatchEmpty", new byte[] { 0x95, 4, 0x80, 0xa3, (byte)'a', (byte)'b', (byte)'c', 0xa3, (byte)'x', (byte)'y', (byte)'z', 0x90 }, "Invocation provides 0 argument(s) but target expects 1."), // array is missing elements
|
||||
new InvalidMessageData("StreamInvocationArgumentArraySizeMismatchTooLarge", new byte[] { 0x95, 4, 0x80, 0xa3, (byte)'a', (byte)'b', (byte)'c', 0xa3, (byte)'x', (byte)'y', (byte)'z', 0x92, 0xa1, (byte)'a', 0xa1, (byte)'b' }, "Invocation provides 2 argument(s) but target expects 1."), // argument count does not match binder argument count
|
||||
new InvalidMessageData("StreamInvocationArgumentTypeMismatch", new byte[] { 0x95, 4, 0x80, 0xa3, (byte)'a', (byte)'b', (byte)'c', 0xa3, (byte)'x', (byte)'y', (byte)'z', 0x91, 42 }, "Error binding arguments. Make sure that the types of the provided values match the types of the hub method being invoked."), // argument type mismatch
|
||||
}.ToDictionary(t => t.Name);
|
||||
|
||||
public static IEnumerable<object[]> ArgumentBindingErrorNames => ArgumentBindingErrors.Keys.Select(name => new object[] { name });
|
||||
|
||||
[Theory]
|
||||
[MemberData(nameof(ArgumentBindingErrorNames))]
|
||||
public void GettingArgumentsThrowsIfBindingFailed(string argumentBindingErrorName)
|
||||
{
|
||||
var testData = ArgumentBindingErrors[argumentBindingErrorName];
|
||||
|
||||
var buffer = Frame(testData.Encoded);
|
||||
var binder = new TestBinder(new[] { typeof(string) }, typeof(string));
|
||||
var data = new ReadOnlySequence<byte>(buffer);
|
||||
_hubProtocol.TryParseMessage(ref data, binder, out var message);
|
||||
var bindingFailure = Assert.IsType<InvocationBindingFailureMessage>(message);
|
||||
Assert.Equal(testData.ErrorMessage, bindingFailure.BindingFailure.SourceException.Message);
|
||||
TestParseMessages(testData);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData(new byte[] { 0x05, 0x01 })]
|
||||
public void ParserDoesNotConsumePartialData(byte[] payload)
|
||||
[MemberData(nameof(TestDataNames))]
|
||||
public void WriteMessages(string testDataName)
|
||||
{
|
||||
var binder = new TestBinder(new[] { typeof(string) }, typeof(string));
|
||||
var data = new ReadOnlySequence<byte>(payload);
|
||||
var result = _hubProtocol.TryParseMessage(ref data, binder, out var message);
|
||||
Assert.Null(message);
|
||||
}
|
||||
var testData = TestData[testDataName];
|
||||
|
||||
[Fact]
|
||||
public void SerializerCanSerializeTypesWithNoDefaultCtor()
|
||||
{
|
||||
var result = Write(CompletionMessage.WithResult("0", new List<int> { 42 }.AsReadOnly()));
|
||||
AssertMessages(new byte[] { ArrayBytes(5), 3, 0x80, StringBytes(1), (byte)'0', 0x03, ArrayBytes(1), 42 }, result);
|
||||
}
|
||||
|
||||
private byte ArrayBytes(int size)
|
||||
{
|
||||
Debug.Assert(size < 16, "Test code doesn't support array sizes greater than 15");
|
||||
|
||||
return (byte)(0x90 | size);
|
||||
}
|
||||
|
||||
private byte StringBytes(int size)
|
||||
{
|
||||
Debug.Assert(size < 16, "Test code doesn't support string sizes greater than 15");
|
||||
|
||||
return (byte)(0xa0 | size);
|
||||
}
|
||||
|
||||
private static void AssertMessages(byte[] expectedOutput, ReadOnlyMemory<byte> bytes)
|
||||
{
|
||||
var data = new ReadOnlySequence<byte>(bytes);
|
||||
Assert.True(BinaryMessageParser.TryParseMessage(ref data, out var message));
|
||||
Assert.Equal(expectedOutput, message.ToArray());
|
||||
}
|
||||
|
||||
private static byte[] Frame(byte[] input)
|
||||
{
|
||||
var stream = MemoryBufferWriter.Get();
|
||||
try
|
||||
{
|
||||
BinaryMessageFormatter.WriteLengthPrefix(input.Length, stream);
|
||||
stream.Write(input);
|
||||
return stream.ToArray();
|
||||
}
|
||||
finally
|
||||
{
|
||||
MemoryBufferWriter.Return(stream);
|
||||
}
|
||||
}
|
||||
|
||||
private static byte[] Write(HubMessage message)
|
||||
{
|
||||
var writer = MemoryBufferWriter.Get();
|
||||
try
|
||||
{
|
||||
_hubProtocol.WriteMessage(message, writer);
|
||||
return writer.ToArray();
|
||||
}
|
||||
finally
|
||||
{
|
||||
MemoryBufferWriter.Return(writer);
|
||||
}
|
||||
}
|
||||
|
||||
public class InvalidMessageData
|
||||
{
|
||||
public string Name { get; private set; }
|
||||
public byte[] Encoded { get; private set; }
|
||||
public string ErrorMessage { get; private set; }
|
||||
|
||||
public InvalidMessageData(string name, byte[] encoded, string errorMessage)
|
||||
{
|
||||
Name = name;
|
||||
Encoded = encoded;
|
||||
ErrorMessage = errorMessage;
|
||||
}
|
||||
|
||||
public override string ToString() => Name;
|
||||
}
|
||||
|
||||
public class ProtocolTestData
|
||||
{
|
||||
public string Name { get; }
|
||||
public string Binary { get; }
|
||||
public HubMessage Message { get; }
|
||||
|
||||
public ProtocolTestData(string name, HubMessage message, string binary)
|
||||
{
|
||||
Name = name;
|
||||
Message = message;
|
||||
Binary = binary;
|
||||
}
|
||||
|
||||
public override string ToString() => Name;
|
||||
TestWriteMessages(testData);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1 @@
|
|||
Subproject commit f2dc12bf749e6f708563ac5175c772c025344ebc
|
||||
Loading…
Reference in New Issue