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"]
|
[submodule "googletest"]
|
||||||
path = src/submodules/googletest
|
path = src/submodules/googletest
|
||||||
url = https://github.com/google/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> {
|
async function initializeConnection(circuitHandlers: CircuitHandler[]): Promise<signalR.HubConnection> {
|
||||||
|
const hubProtocol = new MessagePackHubProtocol();
|
||||||
|
(hubProtocol as any).name = 'blazorpack';
|
||||||
const connection = new signalR.HubConnectionBuilder()
|
const connection = new signalR.HubConnectionBuilder()
|
||||||
.withUrl('_blazor')
|
.withUrl('_blazor')
|
||||||
.withHubProtocol(new MessagePackHubProtocol())
|
.withHubProtocol(hubProtocol)
|
||||||
.configureLogging(signalR.LogLevel.Information)
|
.configureLogging(signalR.LogLevel.Information)
|
||||||
.build();
|
.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;
|
||||||
using System.Collections.Concurrent;
|
using System.Collections.Concurrent;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
|
using System.IO;
|
||||||
using System.Text.Encodings.Web;
|
using System.Text.Encodings.Web;
|
||||||
using System.Threading;
|
using System.Threading;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
|
|
@ -114,7 +115,16 @@ namespace Microsoft.AspNetCore.Components.Browser.Rendering
|
||||||
// snapshot its contents now.
|
// snapshot its contents now.
|
||||||
// TODO: Consider using some kind of array pool instead of allocating a new
|
// TODO: Consider using some kind of array pool instead of allocating a new
|
||||||
// buffer on every render.
|
// 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)
|
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.Builder;
|
||||||
using Microsoft.AspNetCore.Components.Server;
|
using Microsoft.AspNetCore.Components.Server;
|
||||||
|
using Microsoft.AspNetCore.Components.Server.BlazorPack;
|
||||||
using Microsoft.AspNetCore.Components.Server.Circuits;
|
using Microsoft.AspNetCore.Components.Server.Circuits;
|
||||||
using Microsoft.AspNetCore.Components.Services;
|
using Microsoft.AspNetCore.Components.Services;
|
||||||
|
using Microsoft.AspNetCore.SignalR.Protocol;
|
||||||
using Microsoft.Extensions.DependencyInjection.Extensions;
|
using Microsoft.Extensions.DependencyInjection.Extensions;
|
||||||
using Microsoft.Extensions.Options;
|
using Microsoft.Extensions.Options;
|
||||||
using Microsoft.JSInterop;
|
using Microsoft.JSInterop;
|
||||||
|
|
@ -23,7 +25,15 @@ namespace Microsoft.Extensions.DependencyInjection
|
||||||
/// <returns>The <see cref="IServiceCollection"/>.</returns>
|
/// <returns>The <see cref="IServiceCollection"/>.</returns>
|
||||||
public static IServiceCollection AddRazorComponents(this IServiceCollection services)
|
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
|
// 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
|
// 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>
|
<PropertyGroup>
|
||||||
<TargetFramework>netcoreapp3.0</TargetFramework>
|
<TargetFramework>netcoreapp3.0</TargetFramework>
|
||||||
|
|
@ -6,6 +6,7 @@
|
||||||
<IsShippingPackage>true</IsShippingPackage>
|
<IsShippingPackage>true</IsShippingPackage>
|
||||||
<GenerateEmbeddedFilesManifest>true</GenerateEmbeddedFilesManifest>
|
<GenerateEmbeddedFilesManifest>true</GenerateEmbeddedFilesManifest>
|
||||||
<HasReferenceAssembly>false</HasReferenceAssembly>
|
<HasReferenceAssembly>false</HasReferenceAssembly>
|
||||||
|
<AllowUnsafeBlocks>true</AllowUnsafeBlocks>
|
||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
|
|
||||||
<!-- Producing this package requires building with NodeJS enabled. -->
|
<!-- Producing this package requires building with NodeJS enabled. -->
|
||||||
|
|
@ -21,7 +22,6 @@
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<Reference Include="Microsoft.AspNetCore.Components.Browser" />
|
<Reference Include="Microsoft.AspNetCore.Components.Browser" />
|
||||||
<Reference Include="Microsoft.AspNetCore.SignalR" />
|
<Reference Include="Microsoft.AspNetCore.SignalR" />
|
||||||
<Reference Include="Microsoft.AspNetCore.SignalR.Protocols.MessagePack" />
|
|
||||||
<Reference Include="Microsoft.AspNetCore.StaticFiles" />
|
<Reference Include="Microsoft.AspNetCore.StaticFiles" />
|
||||||
<Reference Include="Microsoft.Extensions.Caching.Memory" />
|
<Reference Include="Microsoft.Extensions.Caching.Memory" />
|
||||||
<Reference Include="Microsoft.Extensions.FileProviders.Composite" />
|
<Reference Include="Microsoft.Extensions.FileProviders.Composite" />
|
||||||
|
|
@ -33,6 +33,31 @@
|
||||||
<Reference Include="Microsoft.AspNetCore.Mvc.ViewFeatures" />
|
<Reference Include="Microsoft.AspNetCore.Mvc.ViewFeatures" />
|
||||||
</ItemGroup>
|
</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'">
|
<ItemGroup Condition="'$(BuildNodeJS)' != 'false'">
|
||||||
<!-- We need .Browser.JS to build first so we can embed its .js output -->
|
<!-- 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)" />
|
<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" />
|
<Reference Include="Microsoft.Extensions.Logging.Testing" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
|
<PropertyGroup>
|
||||||
|
<SignalRTestBase>$(RepositoryRoot)src\SignalR\common\SignalR.Common\test\Internal\Protocol\</SignalRTestBase>
|
||||||
|
</PropertyGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<Compile Include="..\..\Components\test\Rendering\HtmlRendererTestBase.cs" />
|
<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>
|
</ItemGroup>
|
||||||
|
|
||||||
</Project>
|
</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.IO;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using Microsoft.AspNetCore.Internal;
|
using Microsoft.AspNetCore.Internal;
|
||||||
|
using Microsoft.AspNetCore.SignalR.Protocol;
|
||||||
using Xunit;
|
using Xunit;
|
||||||
|
|
||||||
namespace Microsoft.AspNetCore.SignalR.Common.Tests.Internal.Protocol
|
namespace Microsoft.AspNetCore.SignalR.Common.Tests.Internal.Protocol
|
||||||
{
|
{
|
||||||
using Microsoft.AspNetCore.SignalR.Protocol;
|
|
||||||
using static HubMessageHelpers;
|
using static HubMessageHelpers;
|
||||||
|
|
||||||
public class MessagePackHubProtocolTests
|
public class MessagePackHubProtocolTests : MessagePackHubProtocolTestBase
|
||||||
{
|
{
|
||||||
private static readonly IDictionary<string, string> TestHeaders = new Dictionary<string, string>
|
protected override IHubProtocol HubProtocol => new MessagePackHubProtocol();
|
||||||
{
|
|
||||||
{ "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);
|
|
||||||
}
|
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public void ParseMessageWithExtraData()
|
public void SerializerCanSerializeTypesWithNoDefaultCtor()
|
||||||
{
|
{
|
||||||
var expectedMessage = new InvocationMessage("xyz", "method", Array.Empty<object>());
|
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);
|
||||||
// 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");
|
|
||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
|
|
@ -323,9 +34,9 @@ namespace Microsoft.AspNetCore.SignalR.Common.Tests.Internal.Protocol
|
||||||
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
_hubProtocol.WriteMessage(CompletionMessage.WithResult("xyz", dateTime), writer);
|
HubProtocol.WriteMessage(CompletionMessage.WithResult("xyz", dateTime), writer);
|
||||||
var bytes = new ReadOnlySequence<byte>(writer.ToArray());
|
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);
|
var completionMessage = Assert.IsType<CompletionMessage>(hubMessage);
|
||||||
|
|
||||||
|
|
@ -349,9 +60,9 @@ namespace Microsoft.AspNetCore.SignalR.Common.Tests.Internal.Protocol
|
||||||
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
_hubProtocol.WriteMessage(CompletionMessage.WithResult("xyz", dateTimeOffset), writer);
|
HubProtocol.WriteMessage(CompletionMessage.WithResult("xyz", dateTimeOffset), writer);
|
||||||
var bytes = new ReadOnlySequence<byte>(writer.ToArray());
|
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);
|
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
|
get
|
||||||
new InvalidMessageData("MessageTypeString", new byte[] { 0x91, 0xa3, (byte)'f', (byte)'o', (byte)'o' }, "Reading 'messageType' as Int32 failed."),
|
{
|
||||||
|
foreach (var k in TestData.Keys)
|
||||||
|
{
|
||||||
|
yield return new object[] { k };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Headers
|
// TestData that requires object serialization
|
||||||
new InvalidMessageData("HeadersNotAMap", new byte[] { 0x92, 1, 0xa3, (byte)'f', (byte)'o', (byte)'o' }, "Reading map length for 'headers' failed."),
|
public static IDictionary<string, MessagePackHubProtocolTestBase.ProtocolTestData> TestData => new[]
|
||||||
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."),
|
// Completion messages
|
||||||
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 ProtocolTestData(
|
||||||
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."),
|
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
|
// Invocation messages
|
||||||
new InvalidMessageData("InvocationMissingId", new byte[] { 0x92, 1, 0x80 }, "Reading 'invocationId' as String failed."),
|
new ProtocolTestData(
|
||||||
new InvalidMessageData("InvocationIdBoolean", new byte[] { 0x91, 1, 0x80, 0xc2 }, "Reading 'invocationId' as String failed."),
|
name: "InvocationWithNoHeadersNoIdAndSingleNullArg",
|
||||||
new InvalidMessageData("InvocationTargetMissing", new byte[] { 0x93, 1, 0x80, 0xa3, (byte)'a', (byte)'b', (byte)'c' }, "Reading 'target' as String failed."),
|
message: new InvocationMessage("method", new object[] { null }),
|
||||||
new InvalidMessageData("InvocationTargetInt", new byte[] { 0x94, 1, 0x80, 0xa3, (byte)'a', (byte)'b', (byte)'c', 42 }, "Reading 'target' as String failed."),
|
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
|
// StreamItem Messages
|
||||||
new InvalidMessageData("StreamInvocationMissingId", new byte[] { 0x92, 4, 0x80 }, "Reading 'invocationId' as String failed."),
|
new ProtocolTestData(
|
||||||
new InvalidMessageData("StreamInvocationIdBoolean", new byte[] { 0x93, 4, 0x80, 0xc2 }, "Reading 'invocationId' as String failed."),
|
name: "StreamItemWithNoHeadersAndNullItem",
|
||||||
new InvalidMessageData("StreamInvocationTargetMissing", new byte[] { 0x93, 4, 0x80, 0xa3, (byte)'a', (byte)'b', (byte)'c' }, "Reading 'target' as String failed."),
|
message: new StreamItemMessage("xyz", item: null),
|
||||||
new InvalidMessageData("StreamInvocationTargetInt", new byte[] { 0x94, 4, 0x80, 0xa3, (byte)'a', (byte)'b', (byte)'c', 42 }, "Reading 'target' as String failed."),
|
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
|
// StreamInvocation Messages
|
||||||
new InvalidMessageData("StreamItemMissingId", new byte[] { 0x92, 2, 0x80 }, "Reading 'invocationId' as String failed."),
|
new ProtocolTestData(
|
||||||
new InvalidMessageData("StreamItemInvocationIdBoolean", new byte[] { 0x93, 2, 0x80, 0xc2 }, "Reading 'invocationId' as String failed."),
|
name: "StreamInvocationWithNoHeadersAndEnumArg",
|
||||||
new InvalidMessageData("StreamItemMissing", new byte[] { 0x93, 2, 0x80, 0xa3, (byte)'x', (byte)'y', (byte)'z' }, "Deserializing object of the `String` type for 'item' failed."),
|
message: new StreamInvocationMessage("xyz", "method", new object[] { TestEnum.One }),
|
||||||
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."),
|
binary: "lgSAo3h5eqZtZXRob2SRo09uZZA="),
|
||||||
|
new ProtocolTestData(
|
||||||
// CompletionMessage
|
name: "StreamInvocationWithNoHeadersAndNullArg",
|
||||||
new InvalidMessageData("CompletionMissingId", new byte[] { 0x92, 3, 0x80 }, "Reading 'invocationId' as String failed."),
|
message: new StreamInvocationMessage("xyz", "method", new object[] { null }),
|
||||||
new InvalidMessageData("CompletionIdBoolean", new byte[] { 0x93, 3, 0x80, 0xc2 }, "Reading 'invocationId' as String failed."),
|
binary: "lgSAo3h5eqZtZXRob2SRwJA="),
|
||||||
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 ProtocolTestData(
|
||||||
new InvalidMessageData("CompletionResultKindOutOfRange", new byte[] { 0x94, 3, 0x80, 0xa3, (byte)'x', (byte)'y', (byte)'z', 42 }, "Invalid invocation result kind."),
|
name: "StreamInvocationWithNoHeadersAndIntStringAndCustomObjectArgs",
|
||||||
new InvalidMessageData("CompletionErrorMissing", new byte[] { 0x94, 3, 0x80, 0xa3, (byte)'x', (byte)'y', (byte)'z', 0x01 }, "Reading 'error' as String failed."),
|
message: new StreamInvocationMessage("xyz", "method", new object[] { 42, "string", new CustomObject() }),
|
||||||
new InvalidMessageData("CompletionErrorInt", new byte[] { 0x95, 3, 0x80, 0xa3, (byte)'x', (byte)'y', (byte)'z', 0x01, 42 }, "Reading 'error' as String failed."),
|
binary: "lgSAo3h5eqZtZXRob2STKqZzdHJpbmeGqlN0cmluZ1Byb3CoU2lnbmFsUiGqRG91YmxlUHJvcMtAGSH7VELPEqdJbnRQcm9wKqxEYXRlVGltZVByb3DW/1jsHICoTnVsbFByb3DAq0J5dGVBcnJQcm9wxAMBAgOQ"),
|
||||||
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 ProtocolTestData(
|
||||||
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."),
|
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);
|
}.ToDictionary(t => t.Name);
|
||||||
|
|
||||||
public static IEnumerable<object[]> InvalidPayloadNames => InvalidPayloads.Keys.Select(name => new object[] { name });
|
|
||||||
|
|
||||||
[Theory]
|
[Theory]
|
||||||
[MemberData(nameof(InvalidPayloadNames))]
|
[MemberData(nameof(TestDataNames))]
|
||||||
public void ParserThrowsForInvalidMessages(string invalidPayloadName)
|
public void ParseMessages(string testDataName)
|
||||||
{
|
{
|
||||||
var testData = InvalidPayloads[invalidPayloadName];
|
var testData = TestData[testDataName];
|
||||||
|
|
||||||
var buffer = Frame(testData.Encoded);
|
TestParseMessages(testData);
|
||||||
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]
|
[Theory]
|
||||||
[InlineData(new byte[] { 0x05, 0x01 })]
|
[MemberData(nameof(TestDataNames))]
|
||||||
public void ParserDoesNotConsumePartialData(byte[] payload)
|
public void WriteMessages(string testDataName)
|
||||||
{
|
{
|
||||||
var binder = new TestBinder(new[] { typeof(string) }, typeof(string));
|
var testData = TestData[testDataName];
|
||||||
var data = new ReadOnlySequence<byte>(payload);
|
|
||||||
var result = _hubProtocol.TryParseMessage(ref data, binder, out var message);
|
|
||||||
Assert.Null(message);
|
|
||||||
}
|
|
||||||
|
|
||||||
[Fact]
|
TestWriteMessages(testData);
|
||||||
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;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1 @@
|
||||||
|
Subproject commit f2dc12bf749e6f708563ac5175c772c025344ebc
|
||||||
Loading…
Reference in New Issue