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:
Pranav K 2019-04-01 14:25:23 -07:00 committed by GitHub
parent 7f4dd27551
commit d86c9b3f07
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
16 changed files with 1472 additions and 689 deletions

4
.gitmodules vendored
View File

@ -1,3 +1,7 @@
[submodule "googletest"]
path = src/submodules/googletest
url = https://github.com/google/googletest
[submodule "src/submodules/MessagePack-CSharp"]
path = src/submodules/MessagePack-CSharp
url = https://github.com/aspnet/MessagePack-CSharp.git

View File

@ -42,9 +42,11 @@ async function boot() {
}
async function initializeConnection(circuitHandlers: CircuitHandler[]): Promise<signalR.HubConnection> {
const hubProtocol = new MessagePackHubProtocol();
(hubProtocol as any).name = 'blazorpack';
const connection = new signalR.HubConnectionBuilder()
.withUrl('_blazor')
.withHubProtocol(new MessagePackHubProtocol())
.withHubProtocol(hubProtocol)
.configureLogging(signalR.LogLevel.Information)
.build();

View File

@ -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}.");
}
}
}

View File

@ -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.");
}
}
}

View File

@ -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
{
}
}

View File

@ -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;
}
}
}

View File

@ -4,6 +4,7 @@
using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.IO;
using System.Text.Encodings.Web;
using System.Threading;
using System.Threading.Tasks;
@ -114,7 +115,16 @@ namespace Microsoft.AspNetCore.Components.Browser.Rendering
// snapshot its contents now.
// TODO: Consider using some kind of array pool instead of allocating a new
// buffer on every render.
var batchBytes = MessagePackSerializer.Serialize(batch, RenderBatchFormatterResolver.Instance);
byte[] batchBytes;
using (var memoryStream = new MemoryStream())
{
using (var renderBatchWriter = new RenderBatchWriter(memoryStream, false))
{
renderBatchWriter.Write(in batch);
}
batchBytes = memoryStream.ToArray();
}
if (!_client.Connected)
{

View File

@ -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;
}
}
}
}
}

View File

@ -3,8 +3,10 @@
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Components.Server;
using Microsoft.AspNetCore.Components.Server.BlazorPack;
using Microsoft.AspNetCore.Components.Server.Circuits;
using Microsoft.AspNetCore.Components.Services;
using Microsoft.AspNetCore.SignalR.Protocol;
using Microsoft.Extensions.DependencyInjection.Extensions;
using Microsoft.Extensions.Options;
using Microsoft.JSInterop;
@ -23,7 +25,15 @@ namespace Microsoft.Extensions.DependencyInjection
/// <returns>The <see cref="IServiceCollection"/>.</returns>
public static IServiceCollection AddRazorComponents(this IServiceCollection services)
{
services.AddSignalR().AddMessagePackProtocol();
services.AddSignalR()
.AddHubOptions<ComponentHub>(options =>
{
options.SupportedProtocols.Clear();
options.SupportedProtocols.Add(BlazorPackHubProtocol.ProtocolName);
});
// Register the Blazor specific hub protocol
services.TryAddEnumerable(ServiceDescriptor.Singleton<IHubProtocol, BlazorPackHubProtocol>());
// Here we add a bunch of services that don't vary in any way based on the
// user's configuration. So even if the user has multiple independent server-side

View File

@ -1,4 +1,4 @@
<Project Sdk="Microsoft.NET.Sdk">
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>netcoreapp3.0</TargetFramework>
@ -6,6 +6,7 @@
<IsShippingPackage>true</IsShippingPackage>
<GenerateEmbeddedFilesManifest>true</GenerateEmbeddedFilesManifest>
<HasReferenceAssembly>false</HasReferenceAssembly>
<AllowUnsafeBlocks>true</AllowUnsafeBlocks>
</PropertyGroup>
<!-- Producing this package requires building with NodeJS enabled. -->
@ -21,7 +22,6 @@
<ItemGroup>
<Reference Include="Microsoft.AspNetCore.Components.Browser" />
<Reference Include="Microsoft.AspNetCore.SignalR" />
<Reference Include="Microsoft.AspNetCore.SignalR.Protocols.MessagePack" />
<Reference Include="Microsoft.AspNetCore.StaticFiles" />
<Reference Include="Microsoft.Extensions.Caching.Memory" />
<Reference Include="Microsoft.Extensions.FileProviders.Composite" />
@ -33,6 +33,31 @@
<Reference Include="Microsoft.AspNetCore.Mvc.ViewFeatures" />
</ItemGroup>
<PropertyGroup>
<MessagePackRoot>$(RepositoryRoot)src\submodules\MessagePack-CSharp\src\MessagePack\</MessagePackRoot>
</PropertyGroup>
<ItemGroup>
<Compile Include="$(RepositoryRoot)src\SignalR\common\Shared\BinaryMessageFormatter.cs" LinkBase="BlazorPack" />
<Compile Include="$(RepositoryRoot)src\SignalR\common\Shared\BinaryMessageParser.cs" LinkBase="BlazorPack" />
<Compile Include="$(RepositoryRoot)src\SignalR\common\Shared\MemoryBufferWriter.cs" LinkBase="BlazorPack" />
<!-- MessagePack -->
<Compile Include="$(MessagePackRoot)BufferWriter.cs" LinkBase="BlazorPack\MessagePack" />
<Compile Include="$(MessagePackRoot)ExtensionHeader.cs" LinkBase="BlazorPack\MessagePack" />
<Compile Include="$(MessagePackRoot)ExtensionResult.cs" LinkBase="BlazorPack\MessagePack" />
<Compile Include="$(MessagePackRoot)MessagePackCode.cs" LinkBase="BlazorPack\MessagePack" />
<Compile Include="$(MessagePackRoot)MessagePackReader.cs" LinkBase="BlazorPack\MessagePack" />
<Compile Include="$(MessagePackRoot)MessagePackReader.Integers.cs" LinkBase="BlazorPack\MessagePack" />
<Compile Include="$(MessagePackRoot)MessagePackWriter.cs" LinkBase="BlazorPack\MessagePack" />
<Compile Include="$(MessagePackRoot)Nil.cs" LinkBase="BlazorPack\MessagePack" />
<Compile Include="$(MessagePackRoot)FloatBits.cs" LinkBase="BlazorPack\MessagePack" />
<Compile Include="$(MessagePackRoot)Internal\DateTimeConstants.cs" LinkBase="BlazorPack\MessagePack" />
<Compile Include="$(MessagePackRoot)StringEncoding.cs" LinkBase="BlazorPack\MessagePack" />
<Compile Include="$(MessagePackRoot)SequenceReader.cs" LinkBase="BlazorPack\MessagePack" />
<Compile Include="$(MessagePackRoot)SequenceReaderExtensions.cs" LinkBase="BlazorPack\MessagePack" />
</ItemGroup>
<ItemGroup Condition="'$(BuildNodeJS)' != 'false'">
<!-- We need .Browser.JS to build first so we can embed its .js output -->
<EmbeddedResource Include="..\..\Browser.JS\src\dist\components.server.js" LogicalName="_framework\%(Filename)%(Extension)" />

View File

@ -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();
}
}

View File

@ -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]);
}
}
}

View File

@ -11,8 +11,16 @@
<Reference Include="Microsoft.Extensions.Logging.Testing" />
</ItemGroup>
<PropertyGroup>
<SignalRTestBase>$(RepositoryRoot)src\SignalR\common\SignalR.Common\test\Internal\Protocol\</SignalRTestBase>
</PropertyGroup>
<ItemGroup>
<Compile Include="..\..\Components\test\Rendering\HtmlRendererTestBase.cs" />
<Compile Include="$(SignalRTestBase)HubMessageHelpers.cs" LinkBase="BlazorPack"/>
<Compile Include="$(SignalRTestBase)MessagePackHubProtocolTestBase.cs" LinkBase="BlazorPack"/>
<Compile Include="$(SignalRTestBase)TestBinder.cs" LinkBase="BlazorPack"/>
<Compile Include="$(SignalRTestBase)TestHubMessageEqualityComparer.cs" LinkBase="BlazorPack"/>
</ItemGroup>
</Project>

View File

@ -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;
}
}
}

View File

@ -8,311 +8,22 @@ using System.Diagnostics;
using System.IO;
using System.Linq;
using Microsoft.AspNetCore.Internal;
using Microsoft.AspNetCore.SignalR.Protocol;
using Xunit;
namespace Microsoft.AspNetCore.SignalR.Common.Tests.Internal.Protocol
{
using Microsoft.AspNetCore.SignalR.Protocol;
using static HubMessageHelpers;
public class MessagePackHubProtocolTests
public class MessagePackHubProtocolTests : MessagePackHubProtocolTestBase
{
private static readonly IDictionary<string, string> TestHeaders = new Dictionary<string, string>
{
{ "Foo", "Bar" },
{ "KeyWith\nNew\r\nLines", "Still Works" },
{ "ValueWithNewLines", "Also\nWorks\r\nFine" },
};
private static readonly MessagePackHubProtocol _hubProtocol
= new MessagePackHubProtocol();
public enum TestEnum
{
Zero = 0,
One
}
// Test Data for Parse/WriteMessages:
// * Name: A string name that is used when reporting the test (it's the ToString value for ProtocolTestData)
// * Message: The HubMessage that is either expected (in Parse) or used as input (in Write)
// * Binary: Base64-encoded binary "baseline" to sanity-check MessagePack-CSharp behavior
//
// When changing the tests/message pack parsing if you get test failures look at the base64 encoding and
// use a tool like https://sugendran.github.io/msgpack-visualizer/ to verify that the MsgPack is correct and then just replace the Base64 value.
public static IEnumerable<object[]> TestDataNames
{
get
{
foreach (var k in TestData.Keys)
{
yield return new object[] { k };
}
}
}
public static IDictionary<string, ProtocolTestData> TestData => new[]
{
// Invocation messages
new ProtocolTestData(
name: "InvocationWithNoHeadersAndNoArgs",
message: new InvocationMessage("xyz", "method", Array.Empty<object>()),
binary: "lgGAo3h5eqZtZXRob2SQkA=="),
new ProtocolTestData(
name: "InvocationWithNoHeadersNoIdAndNoArgs",
message: new InvocationMessage("method", Array.Empty<object>()),
binary: "lgGAwKZtZXRob2SQkA=="),
new ProtocolTestData(
name: "InvocationWithNoHeadersNoIdAndSingleNullArg",
message: new InvocationMessage("method", new object[] { null }),
binary: "lgGAwKZtZXRob2SRwJA="),
new ProtocolTestData(
name: "InvocationWithNoHeadersNoIdAndSingleIntArg",
message: new InvocationMessage("method", new object[] { 42 }),
binary: "lgGAwKZtZXRob2SRKpA="),
new ProtocolTestData(
name: "InvocationWithNoHeadersNoIdIntAndStringArgs",
message: new InvocationMessage("method", new object[] { 42, "string" }),
binary: "lgGAwKZtZXRob2SSKqZzdHJpbmeQ"),
new ProtocolTestData(
name: "InvocationWithNoHeadersNoIdIntAndEnumArgs",
message: new InvocationMessage("method", new object[] { 42, TestEnum.One }),
binary: "lgGAwKZtZXRob2SSKqNPbmWQ"),
new ProtocolTestData(
name: "InvocationWithNoHeadersNoIdAndCustomObjectArg",
message: new InvocationMessage("method", new object[] { 42, "string", new CustomObject() }),
binary: "lgGAwKZtZXRob2STKqZzdHJpbmeGqlN0cmluZ1Byb3CoU2lnbmFsUiGqRG91YmxlUHJvcMtAGSH7VELPEqdJbnRQcm9wKqxEYXRlVGltZVByb3DW/1jsHICoTnVsbFByb3DAq0J5dGVBcnJQcm9wxAMBAgOQ"),
new ProtocolTestData(
name: "InvocationWithNoHeadersNoIdAndArrayOfCustomObjectArgs",
message: new InvocationMessage("method", new object[] { new CustomObject(), new CustomObject() }),
binary: "lgGAwKZtZXRob2SShqpTdHJpbmdQcm9wqFNpZ25hbFIhqkRvdWJsZVByb3DLQBkh+1RCzxKnSW50UHJvcCqsRGF0ZVRpbWVQcm9w1v9Y7ByAqE51bGxQcm9wwKtCeXRlQXJyUHJvcMQDAQIDhqpTdHJpbmdQcm9wqFNpZ25hbFIhqkRvdWJsZVByb3DLQBkh+1RCzxKnSW50UHJvcCqsRGF0ZVRpbWVQcm9w1v9Y7ByAqE51bGxQcm9wwKtCeXRlQXJyUHJvcMQDAQIDkA=="),
new ProtocolTestData(
name: "InvocationWithHeadersNoIdAndArrayOfCustomObjectArgs",
message: AddHeaders(TestHeaders, new InvocationMessage("method", new object[] { new CustomObject(), new CustomObject() })),
binary: "lgGDo0Zvb6NCYXKyS2V5V2l0aApOZXcNCkxpbmVzq1N0aWxsIFdvcmtzsVZhbHVlV2l0aE5ld0xpbmVzsEFsc28KV29ya3MNCkZpbmXApm1ldGhvZJKGqlN0cmluZ1Byb3CoU2lnbmFsUiGqRG91YmxlUHJvcMtAGSH7VELPEqdJbnRQcm9wKqxEYXRlVGltZVByb3DW/1jsHICoTnVsbFByb3DAq0J5dGVBcnJQcm9wxAMBAgOGqlN0cmluZ1Byb3CoU2lnbmFsUiGqRG91YmxlUHJvcMtAGSH7VELPEqdJbnRQcm9wKqxEYXRlVGltZVByb3DW/1jsHICoTnVsbFByb3DAq0J5dGVBcnJQcm9wxAMBAgOQ"),
new ProtocolTestData(
name: "InvocationWithStreamArgument",
message: new InvocationMessage(null, "Target", Array.Empty<object>(), new string[] { "__test_id__" }),
binary: "lgGAwKZUYXJnZXSQkatfX3Rlc3RfaWRfXw=="),
new ProtocolTestData(
name: "InvocationWithStreamAndNormalArgument",
message: new InvocationMessage(null, "Target", new object[] { 42 }, new string[] { "__test_id__" }),
binary: "lgGAwKZUYXJnZXSRKpGrX190ZXN0X2lkX18="),
new ProtocolTestData(
name: "InvocationWithMulitpleStreams",
message: new InvocationMessage(null, "Target", Array.Empty<object>(), new string[] { "__test_id__", "__test_id2__" }),
binary: "lgGAwKZUYXJnZXSQkqtfX3Rlc3RfaWRfX6xfX3Rlc3RfaWQyX18="),
// StreamItem Messages
new ProtocolTestData(
name: "StreamItemWithNoHeadersAndNullItem",
message: new StreamItemMessage("xyz", item: null),
binary: "lAKAo3h5esA="),
new ProtocolTestData(
name: "StreamItemWithNoHeadersAndIntItem",
message: new StreamItemMessage("xyz", item: 42),
binary: "lAKAo3h5eio="),
new ProtocolTestData(
name: "StreamItemWithNoHeadersAndFloatItem",
message: new StreamItemMessage("xyz", item: 42.0f),
binary: "lAKAo3h5espCKAAA"),
new ProtocolTestData(
name: "StreamItemWithNoHeadersAndStringItem",
message: new StreamItemMessage("xyz", item: "string"),
binary: "lAKAo3h5eqZzdHJpbmc="),
new ProtocolTestData(
name: "StreamItemWithNoHeadersAndBoolItem",
message: new StreamItemMessage("xyz", item: true),
binary: "lAKAo3h5esM="),
new ProtocolTestData(
name: "StreamItemWithNoHeadersAndEnumItem",
message: new StreamItemMessage("xyz", item: TestEnum.One),
binary: "lAKAo3h5eqNPbmU="),
new ProtocolTestData(
name: "StreamItemWithNoHeadersAndCustomObjectItem",
message: new StreamItemMessage("xyz", item: new CustomObject()),
binary: "lAKAo3h5eoaqU3RyaW5nUHJvcKhTaWduYWxSIapEb3VibGVQcm9wy0AZIftUQs8Sp0ludFByb3AqrERhdGVUaW1lUHJvcNb/WOwcgKhOdWxsUHJvcMCrQnl0ZUFyclByb3DEAwECAw=="),
new ProtocolTestData(
name: "StreamItemWithNoHeadersAndCustomObjectArrayItem",
message: new StreamItemMessage("xyz", item: new[] { new CustomObject(), new CustomObject() }),
binary: "lAKAo3h5epKGqlN0cmluZ1Byb3CoU2lnbmFsUiGqRG91YmxlUHJvcMtAGSH7VELPEqdJbnRQcm9wKqxEYXRlVGltZVByb3DW/1jsHICoTnVsbFByb3DAq0J5dGVBcnJQcm9wxAMBAgOGqlN0cmluZ1Byb3CoU2lnbmFsUiGqRG91YmxlUHJvcMtAGSH7VELPEqdJbnRQcm9wKqxEYXRlVGltZVByb3DW/1jsHICoTnVsbFByb3DAq0J5dGVBcnJQcm9wxAMBAgM="),
new ProtocolTestData(
name: "StreamItemWithHeadersAndCustomObjectArrayItem",
message: AddHeaders(TestHeaders, new StreamItemMessage("xyz", item: new[] { new CustomObject(), new CustomObject() })),
binary: "lAKDo0Zvb6NCYXKyS2V5V2l0aApOZXcNCkxpbmVzq1N0aWxsIFdvcmtzsVZhbHVlV2l0aE5ld0xpbmVzsEFsc28KV29ya3MNCkZpbmWjeHl6koaqU3RyaW5nUHJvcKhTaWduYWxSIapEb3VibGVQcm9wy0AZIftUQs8Sp0ludFByb3AqrERhdGVUaW1lUHJvcNb/WOwcgKhOdWxsUHJvcMCrQnl0ZUFyclByb3DEAwECA4aqU3RyaW5nUHJvcKhTaWduYWxSIapEb3VibGVQcm9wy0AZIftUQs8Sp0ludFByb3AqrERhdGVUaW1lUHJvcNb/WOwcgKhOdWxsUHJvcMCrQnl0ZUFyclByb3DEAwECAw=="),
// Completion Messages
new ProtocolTestData(
name: "CompletionWithNoHeadersAndError",
message: CompletionMessage.WithError("xyz", error: "Error not found!"),
binary: "lQOAo3h5egGwRXJyb3Igbm90IGZvdW5kIQ=="),
new ProtocolTestData(
name: "CompletionWithHeadersAndError",
message: AddHeaders(TestHeaders, CompletionMessage.WithError("xyz", error: "Error not found!")),
binary: "lQODo0Zvb6NCYXKyS2V5V2l0aApOZXcNCkxpbmVzq1N0aWxsIFdvcmtzsVZhbHVlV2l0aE5ld0xpbmVzsEFsc28KV29ya3MNCkZpbmWjeHl6AbBFcnJvciBub3QgZm91bmQh"),
new ProtocolTestData(
name: "CompletionWithNoHeadersAndNoResult",
message: CompletionMessage.Empty("xyz"),
binary: "lAOAo3h5egI="),
new ProtocolTestData(
name: "CompletionWithHeadersAndNoResult",
message: AddHeaders(TestHeaders, CompletionMessage.Empty("xyz")),
binary: "lAODo0Zvb6NCYXKyS2V5V2l0aApOZXcNCkxpbmVzq1N0aWxsIFdvcmtzsVZhbHVlV2l0aE5ld0xpbmVzsEFsc28KV29ya3MNCkZpbmWjeHl6Ag=="),
new ProtocolTestData(
name: "CompletionWithNoHeadersAndNullResult",
message: CompletionMessage.WithResult("xyz", payload: null),
binary: "lQOAo3h5egPA"),
new ProtocolTestData(
name: "CompletionWithNoHeadersAndIntResult",
message: CompletionMessage.WithResult("xyz", payload: 42),
binary: "lQOAo3h5egMq"),
new ProtocolTestData(
name: "CompletionWithNoHeadersAndEnumResult",
message: CompletionMessage.WithResult("xyz", payload: TestEnum.One),
binary: "lQOAo3h5egOjT25l"),
new ProtocolTestData(
name: "CompletionWithNoHeadersAndFloatResult",
message: CompletionMessage.WithResult("xyz", payload: 42.0f),
binary: "lQOAo3h5egPKQigAAA=="),
new ProtocolTestData(
name: "CompletionWithNoHeadersAndStringResult",
message: CompletionMessage.WithResult("xyz", payload: "string"),
binary: "lQOAo3h5egOmc3RyaW5n"),
new ProtocolTestData(
name: "CompletionWithNoHeadersAndBooleanResult",
message: CompletionMessage.WithResult("xyz", payload: true),
binary: "lQOAo3h5egPD"),
new ProtocolTestData(
name: "CompletionWithNoHeadersAndCustomObjectResult",
message: CompletionMessage.WithResult("xyz", payload: new CustomObject()),
binary: "lQOAo3h5egOGqlN0cmluZ1Byb3CoU2lnbmFsUiGqRG91YmxlUHJvcMtAGSH7VELPEqdJbnRQcm9wKqxEYXRlVGltZVByb3DW/1jsHICoTnVsbFByb3DAq0J5dGVBcnJQcm9wxAMBAgM="),
new ProtocolTestData(
name: "CompletionWithNoHeadersAndCustomObjectArrayResult",
message: CompletionMessage.WithResult("xyz", payload: new[] { new CustomObject(), new CustomObject() }),
binary: "lQOAo3h5egOShqpTdHJpbmdQcm9wqFNpZ25hbFIhqkRvdWJsZVByb3DLQBkh+1RCzxKnSW50UHJvcCqsRGF0ZVRpbWVQcm9w1v9Y7ByAqE51bGxQcm9wwKtCeXRlQXJyUHJvcMQDAQIDhqpTdHJpbmdQcm9wqFNpZ25hbFIhqkRvdWJsZVByb3DLQBkh+1RCzxKnSW50UHJvcCqsRGF0ZVRpbWVQcm9w1v9Y7ByAqE51bGxQcm9wwKtCeXRlQXJyUHJvcMQDAQID"),
new ProtocolTestData(
name: "CompletionWithHeadersAndCustomObjectArrayResult",
message: AddHeaders(TestHeaders, CompletionMessage.WithResult("xyz", payload: new[] { new CustomObject(), new CustomObject() })),
binary: "lQODo0Zvb6NCYXKyS2V5V2l0aApOZXcNCkxpbmVzq1N0aWxsIFdvcmtzsVZhbHVlV2l0aE5ld0xpbmVzsEFsc28KV29ya3MNCkZpbmWjeHl6A5KGqlN0cmluZ1Byb3CoU2lnbmFsUiGqRG91YmxlUHJvcMtAGSH7VELPEqdJbnRQcm9wKqxEYXRlVGltZVByb3DW/1jsHICoTnVsbFByb3DAq0J5dGVBcnJQcm9wxAMBAgOGqlN0cmluZ1Byb3CoU2lnbmFsUiGqRG91YmxlUHJvcMtAGSH7VELPEqdJbnRQcm9wKqxEYXRlVGltZVByb3DW/1jsHICoTnVsbFByb3DAq0J5dGVBcnJQcm9wxAMBAgM="),
// StreamInvocation Messages
new ProtocolTestData(
name: "StreamInvocationWithNoHeadersAndNoArgs",
message: new StreamInvocationMessage("xyz", "method", Array.Empty<object>()),
binary: "lgSAo3h5eqZtZXRob2SQkA=="),
new ProtocolTestData(
name: "StreamInvocationWithNoHeadersAndNullArg",
message: new StreamInvocationMessage("xyz", "method", new object[] { null }),
binary: "lgSAo3h5eqZtZXRob2SRwJA="),
new ProtocolTestData(
name: "StreamInvocationWithNoHeadersAndIntArg",
message: new StreamInvocationMessage("xyz", "method", new object[] { 42 }),
binary: "lgSAo3h5eqZtZXRob2SRKpA="),
new ProtocolTestData(
name: "StreamInvocationWithNoHeadersAndEnumArg",
message: new StreamInvocationMessage("xyz", "method", new object[] { TestEnum.One }),
binary: "lgSAo3h5eqZtZXRob2SRo09uZZA="),
new ProtocolTestData(
name: "StreamInvocationWithStreamArgument",
message: new StreamInvocationMessage("xyz", "method", Array.Empty<object>(), new string[] { "__test_id__" }),
binary: "lgSAo3h5eqZtZXRob2SQkatfX3Rlc3RfaWRfXw=="),
new ProtocolTestData(
name: "StreamInvocationWithStreamAndNormalArgument",
message: new StreamInvocationMessage("xyz", "method", new object[] { 42 }, new string[] { "__test_id__" }),
binary: "lgSAo3h5eqZtZXRob2SRKpGrX190ZXN0X2lkX18="),
new ProtocolTestData(
name: "StreamInvocationWithNoHeadersAndIntAndStringArgs",
message: new StreamInvocationMessage("xyz", "method", new object[] { 42, "string" }),
binary: "lgSAo3h5eqZtZXRob2SSKqZzdHJpbmeQ"),
new ProtocolTestData(
name: "StreamInvocationWithNoHeadersAndIntStringAndCustomObjectArgs",
message: new StreamInvocationMessage("xyz", "method", new object[] { 42, "string", new CustomObject() }),
binary: "lgSAo3h5eqZtZXRob2STKqZzdHJpbmeGqlN0cmluZ1Byb3CoU2lnbmFsUiGqRG91YmxlUHJvcMtAGSH7VELPEqdJbnRQcm9wKqxEYXRlVGltZVByb3DW/1jsHICoTnVsbFByb3DAq0J5dGVBcnJQcm9wxAMBAgOQ"),
new ProtocolTestData(
name: "StreamInvocationWithNoHeadersAndCustomObjectArrayArg",
message: new StreamInvocationMessage("xyz", "method", new object[] { new CustomObject(), new CustomObject() }),
binary: "lgSAo3h5eqZtZXRob2SShqpTdHJpbmdQcm9wqFNpZ25hbFIhqkRvdWJsZVByb3DLQBkh+1RCzxKnSW50UHJvcCqsRGF0ZVRpbWVQcm9w1v9Y7ByAqE51bGxQcm9wwKtCeXRlQXJyUHJvcMQDAQIDhqpTdHJpbmdQcm9wqFNpZ25hbFIhqkRvdWJsZVByb3DLQBkh+1RCzxKnSW50UHJvcCqsRGF0ZVRpbWVQcm9w1v9Y7ByAqE51bGxQcm9wwKtCeXRlQXJyUHJvcMQDAQIDkA=="),
new ProtocolTestData(
name: "StreamInvocationWithHeadersAndCustomObjectArrayArg",
message: AddHeaders(TestHeaders, new StreamInvocationMessage("xyz", "method", new object[] { new CustomObject(), new CustomObject() })),
binary: "lgSDo0Zvb6NCYXKyS2V5V2l0aApOZXcNCkxpbmVzq1N0aWxsIFdvcmtzsVZhbHVlV2l0aE5ld0xpbmVzsEFsc28KV29ya3MNCkZpbmWjeHl6pm1ldGhvZJKGqlN0cmluZ1Byb3CoU2lnbmFsUiGqRG91YmxlUHJvcMtAGSH7VELPEqdJbnRQcm9wKqxEYXRlVGltZVByb3DW/1jsHICoTnVsbFByb3DAq0J5dGVBcnJQcm9wxAMBAgOGqlN0cmluZ1Byb3CoU2lnbmFsUiGqRG91YmxlUHJvcMtAGSH7VELPEqdJbnRQcm9wKqxEYXRlVGltZVByb3DW/1jsHICoTnVsbFByb3DAq0J5dGVBcnJQcm9wxAMBAgOQ"),
// CancelInvocation Messages
new ProtocolTestData(
name: "CancelInvocationWithNoHeaders",
message: new CancelInvocationMessage("xyz"),
binary: "kwWAo3h5eg=="),
new ProtocolTestData(
name: "CancelInvocationWithHeaders",
message: AddHeaders(TestHeaders, new CancelInvocationMessage("xyz")),
binary: "kwWDo0Zvb6NCYXKyS2V5V2l0aApOZXcNCkxpbmVzq1N0aWxsIFdvcmtzsVZhbHVlV2l0aE5ld0xpbmVzsEFsc28KV29ya3MNCkZpbmWjeHl6"),
// Ping Messages
new ProtocolTestData(
name: "Ping",
message: PingMessage.Instance,
binary: "kQY="),
}.ToDictionary(t => t.Name);
[Theory]
[MemberData(nameof(TestDataNames))]
public void ParseMessages(string testDataName)
{
var testData = TestData[testDataName];
// Verify that the input binary string decodes to the expected MsgPack primitives
var bytes = Convert.FromBase64String(testData.Binary);
// Parse the input fully now.
bytes = Frame(bytes);
var data = new ReadOnlySequence<byte>(bytes);
Assert.True(_hubProtocol.TryParseMessage(ref data, new TestBinder(testData.Message), out var message));
Assert.NotNull(message);
Assert.Equal(testData.Message, message, TestHubMessageEqualityComparer.Instance);
}
protected override IHubProtocol HubProtocol => new MessagePackHubProtocol();
[Fact]
public void ParseMessageWithExtraData()
public void SerializerCanSerializeTypesWithNoDefaultCtor()
{
var expectedMessage = new InvocationMessage("xyz", "method", Array.Empty<object>());
// Verify that the input binary string decodes to the expected MsgPack primitives
var bytes = new byte[] { ArrayBytes(8),
1,
0x80,
StringBytes(3), (byte)'x', (byte)'y', (byte)'z',
StringBytes(6), (byte)'m', (byte)'e', (byte)'t', (byte)'h', (byte)'o', (byte)'d',
ArrayBytes(0), // Arguments
ArrayBytes(0), // Streams
0xc3,
StringBytes(2), (byte)'e', (byte)'x' };
// Parse the input fully now.
bytes = Frame(bytes);
var data = new ReadOnlySequence<byte>(bytes);
Assert.True(_hubProtocol.TryParseMessage(ref data, new TestBinder(expectedMessage), out var message));
Assert.NotNull(message);
Assert.Equal(expectedMessage, message, TestHubMessageEqualityComparer.Instance);
}
[Theory]
[MemberData(nameof(TestDataNames))]
public void WriteMessages(string testDataName)
{
var testData = TestData[testDataName];
var bytes = Write(testData.Message);
// Unframe the message to check the binary encoding
var byteSpan = new ReadOnlySequence<byte>(bytes);
Assert.True(BinaryMessageParser.TryParseMessage(ref byteSpan, out var unframed));
// Check the baseline binary encoding, use Assert.True in order to configure the error message
var actual = Convert.ToBase64String(unframed.ToArray());
Assert.True(string.Equals(actual, testData.Binary, StringComparison.Ordinal), $"Binary encoding changed from{Environment.NewLine} [{testData.Binary}]{Environment.NewLine} to{Environment.NewLine} [{actual}]{Environment.NewLine}Please verify the MsgPack output and update the baseline");
var result = Write(CompletionMessage.WithResult("0", new List<int> { 42 }.AsReadOnly()));
AssertMessages(new byte[] { ArrayBytes(5), 3, 0x80, StringBytes(1), (byte)'0', 0x03, ArrayBytes(1), 42 }, result);
}
[Fact]
@ -323,9 +34,9 @@ namespace Microsoft.AspNetCore.SignalR.Common.Tests.Internal.Protocol
try
{
_hubProtocol.WriteMessage(CompletionMessage.WithResult("xyz", dateTime), writer);
HubProtocol.WriteMessage(CompletionMessage.WithResult("xyz", dateTime), writer);
var bytes = new ReadOnlySequence<byte>(writer.ToArray());
_hubProtocol.TryParseMessage(ref bytes, new TestBinder(typeof(DateTime)), out var hubMessage);
HubProtocol.TryParseMessage(ref bytes, new TestBinder(typeof(DateTime)), out var hubMessage);
var completionMessage = Assert.IsType<CompletionMessage>(hubMessage);
@ -349,9 +60,9 @@ namespace Microsoft.AspNetCore.SignalR.Common.Tests.Internal.Protocol
try
{
_hubProtocol.WriteMessage(CompletionMessage.WithResult("xyz", dateTimeOffset), writer);
HubProtocol.WriteMessage(CompletionMessage.WithResult("xyz", dateTimeOffset), writer);
var bytes = new ReadOnlySequence<byte>(writer.ToArray());
_hubProtocol.TryParseMessage(ref bytes, new TestBinder(typeof(DateTimeOffset)), out var hubMessage);
HubProtocol.TryParseMessage(ref bytes, new TestBinder(typeof(DateTimeOffset)), out var hubMessage);
var completionMessage = Assert.IsType<CompletionMessage>(hubMessage);
@ -364,193 +75,125 @@ namespace Microsoft.AspNetCore.SignalR.Common.Tests.Internal.Protocol
}
}
public static IDictionary<string, InvalidMessageData> InvalidPayloads => new[]
public static IEnumerable<object[]> TestDataNames
{
// Message Type
new InvalidMessageData("MessageTypeString", new byte[] { 0x91, 0xa3, (byte)'f', (byte)'o', (byte)'o' }, "Reading 'messageType' as Int32 failed."),
get
{
foreach (var k in TestData.Keys)
{
yield return new object[] { k };
}
}
}
// Headers
new InvalidMessageData("HeadersNotAMap", new byte[] { 0x92, 1, 0xa3, (byte)'f', (byte)'o', (byte)'o' }, "Reading map length for 'headers' failed."),
new InvalidMessageData("HeaderKeyInt", new byte[] { 0x92, 1, 0x82, 0x2a, 0xa3, (byte)'f', (byte)'o', (byte)'o' }, "Reading 'headers[0].Key' as String failed."),
new InvalidMessageData("HeaderValueInt", new byte[] { 0x92, 1, 0x82, 0xa3, (byte)'f', (byte)'o', (byte)'o', 42 }, "Reading 'headers[0].Value' as String failed."),
new InvalidMessageData("HeaderKeyArray", new byte[] { 0x92, 1, 0x84, 0xa3, (byte)'f', (byte)'o', (byte)'o', 0xa3, (byte)'f', (byte)'o', (byte)'o', 0x90, 0xa3, (byte)'f', (byte)'o', (byte)'o' }, "Reading 'headers[1].Key' as String failed."),
new InvalidMessageData("HeaderValueArray", new byte[] { 0x92, 1, 0x84, 0xa3, (byte)'f', (byte)'o', (byte)'o', 0xa3, (byte)'f', (byte)'o', (byte)'o', 0xa3, (byte)'f', (byte)'o', (byte)'o', 0x90 }, "Reading 'headers[1].Value' as String failed."),
// TestData that requires object serialization
public static IDictionary<string, MessagePackHubProtocolTestBase.ProtocolTestData> TestData => new[]
{
// Completion messages
new ProtocolTestData(
name: "CompletionWithNoHeadersAndNullResult",
message: CompletionMessage.WithResult("xyz", payload: null),
binary: "lQOAo3h5egPA"),
new ProtocolTestData(
name: "CompletionWithNoHeadersAndCustomObjectResult",
message: CompletionMessage.WithResult("xyz", payload: new CustomObject()),
binary: "lQOAo3h5egOGqlN0cmluZ1Byb3CoU2lnbmFsUiGqRG91YmxlUHJvcMtAGSH7VELPEqdJbnRQcm9wKqxEYXRlVGltZVByb3DW/1jsHICoTnVsbFByb3DAq0J5dGVBcnJQcm9wxAMBAgM="),
new ProtocolTestData(
name: "CompletionWithNoHeadersAndCustomObjectArrayResult",
message: CompletionMessage.WithResult("xyz", payload: new[] { new CustomObject(), new CustomObject() }),
binary: "lQOAo3h5egOShqpTdHJpbmdQcm9wqFNpZ25hbFIhqkRvdWJsZVByb3DLQBkh+1RCzxKnSW50UHJvcCqsRGF0ZVRpbWVQcm9w1v9Y7ByAqE51bGxQcm9wwKtCeXRlQXJyUHJvcMQDAQIDhqpTdHJpbmdQcm9wqFNpZ25hbFIhqkRvdWJsZVByb3DLQBkh+1RCzxKnSW50UHJvcCqsRGF0ZVRpbWVQcm9w1v9Y7ByAqE51bGxQcm9wwKtCeXRlQXJyUHJvcMQDAQID"),
new ProtocolTestData(
name: "CompletionWithHeadersAndCustomObjectArrayResult",
message: AddHeaders(TestHeaders, CompletionMessage.WithResult("xyz", payload: new[] { new CustomObject(), new CustomObject() })),
binary: "lQODo0Zvb6NCYXKyS2V5V2l0aApOZXcNCkxpbmVzq1N0aWxsIFdvcmtzsVZhbHVlV2l0aE5ld0xpbmVzsEFsc28KV29ya3MNCkZpbmWjeHl6A5KGqlN0cmluZ1Byb3CoU2lnbmFsUiGqRG91YmxlUHJvcMtAGSH7VELPEqdJbnRQcm9wKqxEYXRlVGltZVByb3DW/1jsHICoTnVsbFByb3DAq0J5dGVBcnJQcm9wxAMBAgOGqlN0cmluZ1Byb3CoU2lnbmFsUiGqRG91YmxlUHJvcMtAGSH7VELPEqdJbnRQcm9wKqxEYXRlVGltZVByb3DW/1jsHICoTnVsbFByb3DAq0J5dGVBcnJQcm9wxAMBAgM="),
new ProtocolTestData(
name: "CompletionWithNoHeadersAndEnumResult",
message: CompletionMessage.WithResult("xyz", payload: TestEnum.One),
binary: "lQOAo3h5egOjT25l"),
// InvocationMessage
new InvalidMessageData("InvocationMissingId", new byte[] { 0x92, 1, 0x80 }, "Reading 'invocationId' as String failed."),
new InvalidMessageData("InvocationIdBoolean", new byte[] { 0x91, 1, 0x80, 0xc2 }, "Reading 'invocationId' as String failed."),
new InvalidMessageData("InvocationTargetMissing", new byte[] { 0x93, 1, 0x80, 0xa3, (byte)'a', (byte)'b', (byte)'c' }, "Reading 'target' as String failed."),
new InvalidMessageData("InvocationTargetInt", new byte[] { 0x94, 1, 0x80, 0xa3, (byte)'a', (byte)'b', (byte)'c', 42 }, "Reading 'target' as String failed."),
// Invocation messages
new ProtocolTestData(
name: "InvocationWithNoHeadersNoIdAndSingleNullArg",
message: new InvocationMessage("method", new object[] { null }),
binary: "lgGAwKZtZXRob2SRwJA="),
new ProtocolTestData(
name: "InvocationWithNoHeadersNoIdIntAndEnumArgs",
message: new InvocationMessage("method", new object[] { 42, TestEnum.One }),
binary: "lgGAwKZtZXRob2SSKqNPbmWQ"),
new ProtocolTestData(
name: "InvocationWithNoHeadersNoIdAndCustomObjectArg",
message: new InvocationMessage("method", new object[] { 42, "string", new CustomObject() }),
binary: "lgGAwKZtZXRob2STKqZzdHJpbmeGqlN0cmluZ1Byb3CoU2lnbmFsUiGqRG91YmxlUHJvcMtAGSH7VELPEqdJbnRQcm9wKqxEYXRlVGltZVByb3DW/1jsHICoTnVsbFByb3DAq0J5dGVBcnJQcm9wxAMBAgOQ"),
new ProtocolTestData(
name: "InvocationWithNoHeadersNoIdAndArrayOfCustomObjectArgs",
message: new InvocationMessage("method", new object[] { new CustomObject(), new CustomObject() }),
binary: "lgGAwKZtZXRob2SShqpTdHJpbmdQcm9wqFNpZ25hbFIhqkRvdWJsZVByb3DLQBkh+1RCzxKnSW50UHJvcCqsRGF0ZVRpbWVQcm9w1v9Y7ByAqE51bGxQcm9wwKtCeXRlQXJyUHJvcMQDAQIDhqpTdHJpbmdQcm9wqFNpZ25hbFIhqkRvdWJsZVByb3DLQBkh+1RCzxKnSW50UHJvcCqsRGF0ZVRpbWVQcm9w1v9Y7ByAqE51bGxQcm9wwKtCeXRlQXJyUHJvcMQDAQIDkA=="),
new ProtocolTestData(
name: "InvocationWithHeadersNoIdAndArrayOfCustomObjectArgs",
message: AddHeaders(TestHeaders, new InvocationMessage("method", new object[] { new CustomObject(), new CustomObject() })),
binary: "lgGDo0Zvb6NCYXKyS2V5V2l0aApOZXcNCkxpbmVzq1N0aWxsIFdvcmtzsVZhbHVlV2l0aE5ld0xpbmVzsEFsc28KV29ya3MNCkZpbmXApm1ldGhvZJKGqlN0cmluZ1Byb3CoU2lnbmFsUiGqRG91YmxlUHJvcMtAGSH7VELPEqdJbnRQcm9wKqxEYXRlVGltZVByb3DW/1jsHICoTnVsbFByb3DAq0J5dGVBcnJQcm9wxAMBAgOGqlN0cmluZ1Byb3CoU2lnbmFsUiGqRG91YmxlUHJvcMtAGSH7VELPEqdJbnRQcm9wKqxEYXRlVGltZVByb3DW/1jsHICoTnVsbFByb3DAq0J5dGVBcnJQcm9wxAMBAgOQ"),
// StreamInvocationMessage
new InvalidMessageData("StreamInvocationMissingId", new byte[] { 0x92, 4, 0x80 }, "Reading 'invocationId' as String failed."),
new InvalidMessageData("StreamInvocationIdBoolean", new byte[] { 0x93, 4, 0x80, 0xc2 }, "Reading 'invocationId' as String failed."),
new InvalidMessageData("StreamInvocationTargetMissing", new byte[] { 0x93, 4, 0x80, 0xa3, (byte)'a', (byte)'b', (byte)'c' }, "Reading 'target' as String failed."),
new InvalidMessageData("StreamInvocationTargetInt", new byte[] { 0x94, 4, 0x80, 0xa3, (byte)'a', (byte)'b', (byte)'c', 42 }, "Reading 'target' as String failed."),
// StreamItem Messages
new ProtocolTestData(
name: "StreamItemWithNoHeadersAndNullItem",
message: new StreamItemMessage("xyz", item: null),
binary: "lAKAo3h5esA="),
new ProtocolTestData(
name: "StreamItemWithNoHeadersAndEnumItem",
message: new StreamItemMessage("xyz", item: TestEnum.One),
binary: "lAKAo3h5eqNPbmU="),
new ProtocolTestData(
name: "StreamItemWithNoHeadersAndCustomObjectItem",
message: new StreamItemMessage("xyz", item: new CustomObject()),
binary: "lAKAo3h5eoaqU3RyaW5nUHJvcKhTaWduYWxSIapEb3VibGVQcm9wy0AZIftUQs8Sp0ludFByb3AqrERhdGVUaW1lUHJvcNb/WOwcgKhOdWxsUHJvcMCrQnl0ZUFyclByb3DEAwECAw=="),
new ProtocolTestData(
name: "StreamItemWithNoHeadersAndCustomObjectArrayItem",
message: new StreamItemMessage("xyz", item: new[] { new CustomObject(), new CustomObject() }),
binary: "lAKAo3h5epKGqlN0cmluZ1Byb3CoU2lnbmFsUiGqRG91YmxlUHJvcMtAGSH7VELPEqdJbnRQcm9wKqxEYXRlVGltZVByb3DW/1jsHICoTnVsbFByb3DAq0J5dGVBcnJQcm9wxAMBAgOGqlN0cmluZ1Byb3CoU2lnbmFsUiGqRG91YmxlUHJvcMtAGSH7VELPEqdJbnRQcm9wKqxEYXRlVGltZVByb3DW/1jsHICoTnVsbFByb3DAq0J5dGVBcnJQcm9wxAMBAgM="),
new ProtocolTestData(
name: "StreamItemWithHeadersAndCustomObjectArrayItem",
message: AddHeaders(TestHeaders, new StreamItemMessage("xyz", item: new[] { new CustomObject(), new CustomObject() })),
binary: "lAKDo0Zvb6NCYXKyS2V5V2l0aApOZXcNCkxpbmVzq1N0aWxsIFdvcmtzsVZhbHVlV2l0aE5ld0xpbmVzsEFsc28KV29ya3MNCkZpbmWjeHl6koaqU3RyaW5nUHJvcKhTaWduYWxSIapEb3VibGVQcm9wy0AZIftUQs8Sp0ludFByb3AqrERhdGVUaW1lUHJvcNb/WOwcgKhOdWxsUHJvcMCrQnl0ZUFyclByb3DEAwECA4aqU3RyaW5nUHJvcKhTaWduYWxSIapEb3VibGVQcm9wy0AZIftUQs8Sp0ludFByb3AqrERhdGVUaW1lUHJvcNb/WOwcgKhOdWxsUHJvcMCrQnl0ZUFyclByb3DEAwECAw=="),
// StreamItemMessage
new InvalidMessageData("StreamItemMissingId", new byte[] { 0x92, 2, 0x80 }, "Reading 'invocationId' as String failed."),
new InvalidMessageData("StreamItemInvocationIdBoolean", new byte[] { 0x93, 2, 0x80, 0xc2 }, "Reading 'invocationId' as String failed."),
new InvalidMessageData("StreamItemMissing", new byte[] { 0x93, 2, 0x80, 0xa3, (byte)'x', (byte)'y', (byte)'z' }, "Deserializing object of the `String` type for 'item' failed."),
new InvalidMessageData("StreamItemTypeMismatch", new byte[] { 0x94, 2, 0x80, 0xa3, (byte)'x', (byte)'y', (byte)'z', 42 }, "Deserializing object of the `String` type for 'item' failed."),
// CompletionMessage
new InvalidMessageData("CompletionMissingId", new byte[] { 0x92, 3, 0x80 }, "Reading 'invocationId' as String failed."),
new InvalidMessageData("CompletionIdBoolean", new byte[] { 0x93, 3, 0x80, 0xc2 }, "Reading 'invocationId' as String failed."),
new InvalidMessageData("CompletionResultKindString", new byte[] { 0x94, 3, 0x80, 0xa3, (byte)'x', (byte)'y', (byte)'z', 0xa3, (byte)'x', (byte)'y', (byte)'z' }, "Reading 'resultKind' as Int32 failed."),
new InvalidMessageData("CompletionResultKindOutOfRange", new byte[] { 0x94, 3, 0x80, 0xa3, (byte)'x', (byte)'y', (byte)'z', 42 }, "Invalid invocation result kind."),
new InvalidMessageData("CompletionErrorMissing", new byte[] { 0x94, 3, 0x80, 0xa3, (byte)'x', (byte)'y', (byte)'z', 0x01 }, "Reading 'error' as String failed."),
new InvalidMessageData("CompletionErrorInt", new byte[] { 0x95, 3, 0x80, 0xa3, (byte)'x', (byte)'y', (byte)'z', 0x01, 42 }, "Reading 'error' as String failed."),
new InvalidMessageData("CompletionResultMissing", new byte[] { 0x94, 3, 0x80, 0xa3, (byte)'x', (byte)'y', (byte)'z', 0x03 }, "Deserializing object of the `String` type for 'argument' failed."),
new InvalidMessageData("CompletionResultTypeMismatch", new byte[] { 0x95, 3, 0x80, 0xa3, (byte)'x', (byte)'y', (byte)'z', 0x03, 42 }, "Deserializing object of the `String` type for 'argument' failed."),
// StreamInvocation Messages
new ProtocolTestData(
name: "StreamInvocationWithNoHeadersAndEnumArg",
message: new StreamInvocationMessage("xyz", "method", new object[] { TestEnum.One }),
binary: "lgSAo3h5eqZtZXRob2SRo09uZZA="),
new ProtocolTestData(
name: "StreamInvocationWithNoHeadersAndNullArg",
message: new StreamInvocationMessage("xyz", "method", new object[] { null }),
binary: "lgSAo3h5eqZtZXRob2SRwJA="),
new ProtocolTestData(
name: "StreamInvocationWithNoHeadersAndIntStringAndCustomObjectArgs",
message: new StreamInvocationMessage("xyz", "method", new object[] { 42, "string", new CustomObject() }),
binary: "lgSAo3h5eqZtZXRob2STKqZzdHJpbmeGqlN0cmluZ1Byb3CoU2lnbmFsUiGqRG91YmxlUHJvcMtAGSH7VELPEqdJbnRQcm9wKqxEYXRlVGltZVByb3DW/1jsHICoTnVsbFByb3DAq0J5dGVBcnJQcm9wxAMBAgOQ"),
new ProtocolTestData(
name: "StreamInvocationWithNoHeadersAndCustomObjectArrayArg",
message: new StreamInvocationMessage("xyz", "method", new object[] { new CustomObject(), new CustomObject() }),
binary: "lgSAo3h5eqZtZXRob2SShqpTdHJpbmdQcm9wqFNpZ25hbFIhqkRvdWJsZVByb3DLQBkh+1RCzxKnSW50UHJvcCqsRGF0ZVRpbWVQcm9w1v9Y7ByAqE51bGxQcm9wwKtCeXRlQXJyUHJvcMQDAQIDhqpTdHJpbmdQcm9wqFNpZ25hbFIhqkRvdWJsZVByb3DLQBkh+1RCzxKnSW50UHJvcCqsRGF0ZVRpbWVQcm9w1v9Y7ByAqE51bGxQcm9wwKtCeXRlQXJyUHJvcMQDAQIDkA=="),
new ProtocolTestData(
name: "StreamInvocationWithHeadersAndCustomObjectArrayArg",
message: AddHeaders(TestHeaders, new StreamInvocationMessage("xyz", "method", new object[] { new CustomObject(), new CustomObject() })),
binary: "lgSDo0Zvb6NCYXKyS2V5V2l0aApOZXcNCkxpbmVzq1N0aWxsIFdvcmtzsVZhbHVlV2l0aE5ld0xpbmVzsEFsc28KV29ya3MNCkZpbmWjeHl6pm1ldGhvZJKGqlN0cmluZ1Byb3CoU2lnbmFsUiGqRG91YmxlUHJvcMtAGSH7VELPEqdJbnRQcm9wKqxEYXRlVGltZVByb3DW/1jsHICoTnVsbFByb3DAq0J5dGVBcnJQcm9wxAMBAgOGqlN0cmluZ1Byb3CoU2lnbmFsUiGqRG91YmxlUHJvcMtAGSH7VELPEqdJbnRQcm9wKqxEYXRlVGltZVByb3DW/1jsHICoTnVsbFByb3DAq0J5dGVBcnJQcm9wxAMBAgOQ"),
}.ToDictionary(t => t.Name);
public static IEnumerable<object[]> InvalidPayloadNames => InvalidPayloads.Keys.Select(name => new object[] { name });
[Theory]
[MemberData(nameof(InvalidPayloadNames))]
public void ParserThrowsForInvalidMessages(string invalidPayloadName)
[MemberData(nameof(TestDataNames))]
public void ParseMessages(string testDataName)
{
var testData = InvalidPayloads[invalidPayloadName];
var testData = TestData[testDataName];
var buffer = Frame(testData.Encoded);
var binder = new TestBinder(new[] { typeof(string) }, typeof(string));
var data = new ReadOnlySequence<byte>(buffer);
var exception = Assert.Throws<InvalidDataException>(() => _hubProtocol.TryParseMessage(ref data, binder, out _));
Assert.Equal(testData.ErrorMessage, exception.Message);
}
public static IDictionary<string, InvalidMessageData> ArgumentBindingErrors => new[]
{
// InvocationMessage
new InvalidMessageData("InvocationArgumentArrayMissing", new byte[] { 0x94, 1, 0x80, 0xa3, (byte)'a', (byte)'b', (byte)'c', 0xa3, (byte)'x', (byte)'y', (byte)'z' }, "Reading array length for 'arguments' failed."),
new InvalidMessageData("InvocationArgumentArrayNotAnArray", new byte[] { 0x95, 1, 0x80, 0xa3, (byte)'a', (byte)'b', (byte)'c', 0xa3, (byte)'x', (byte)'y', (byte)'z', 42 }, "Reading array length for 'arguments' failed."),
new InvalidMessageData("InvocationArgumentArraySizeMismatchEmpty", new byte[] { 0x95, 1, 0x80, 0xa3, (byte)'a', (byte)'b', (byte)'c', 0xa3, (byte)'x', (byte)'y', (byte)'z', 0x90 }, "Invocation provides 0 argument(s) but target expects 1."),
new InvalidMessageData("InvocationArgumentArraySizeMismatchTooLarge", new byte[] { 0x95, 1, 0x80, 0xa3, (byte)'a', (byte)'b', (byte)'c', 0xa3, (byte)'x', (byte)'y', (byte)'z', 0x92, 0xa1, (byte)'a', 0xa1, (byte)'b' }, "Invocation provides 2 argument(s) but target expects 1."),
new InvalidMessageData("InvocationArgumentTypeMismatch", new byte[] { 0x95, 1, 0x80, 0xa3, (byte)'a', (byte)'b', (byte)'c', 0xa3, (byte)'x', (byte)'y', (byte)'z', 0x91, 42 }, "Error binding arguments. Make sure that the types of the provided values match the types of the hub method being invoked."),
// StreamInvocationMessage
new InvalidMessageData("StreamInvocationArgumentArrayMissing", new byte[] { 0x94, 4, 0x80, 0xa3, (byte)'a', (byte)'b', (byte)'c', 0xa3, (byte)'x', (byte)'y', (byte)'z' }, "Reading array length for 'arguments' failed."), // array is missing
new InvalidMessageData("StreamInvocationArgumentArrayNotAnArray", new byte[] { 0x95, 4, 0x80, 0xa3, (byte)'a', (byte)'b', (byte)'c', 0xa3, (byte)'x', (byte)'y', (byte)'z', 42 }, "Reading array length for 'arguments' failed."), // arguments isn't an array
new InvalidMessageData("StreamInvocationArgumentArraySizeMismatchEmpty", new byte[] { 0x95, 4, 0x80, 0xa3, (byte)'a', (byte)'b', (byte)'c', 0xa3, (byte)'x', (byte)'y', (byte)'z', 0x90 }, "Invocation provides 0 argument(s) but target expects 1."), // array is missing elements
new InvalidMessageData("StreamInvocationArgumentArraySizeMismatchTooLarge", new byte[] { 0x95, 4, 0x80, 0xa3, (byte)'a', (byte)'b', (byte)'c', 0xa3, (byte)'x', (byte)'y', (byte)'z', 0x92, 0xa1, (byte)'a', 0xa1, (byte)'b' }, "Invocation provides 2 argument(s) but target expects 1."), // argument count does not match binder argument count
new InvalidMessageData("StreamInvocationArgumentTypeMismatch", new byte[] { 0x95, 4, 0x80, 0xa3, (byte)'a', (byte)'b', (byte)'c', 0xa3, (byte)'x', (byte)'y', (byte)'z', 0x91, 42 }, "Error binding arguments. Make sure that the types of the provided values match the types of the hub method being invoked."), // argument type mismatch
}.ToDictionary(t => t.Name);
public static IEnumerable<object[]> ArgumentBindingErrorNames => ArgumentBindingErrors.Keys.Select(name => new object[] { name });
[Theory]
[MemberData(nameof(ArgumentBindingErrorNames))]
public void GettingArgumentsThrowsIfBindingFailed(string argumentBindingErrorName)
{
var testData = ArgumentBindingErrors[argumentBindingErrorName];
var buffer = Frame(testData.Encoded);
var binder = new TestBinder(new[] { typeof(string) }, typeof(string));
var data = new ReadOnlySequence<byte>(buffer);
_hubProtocol.TryParseMessage(ref data, binder, out var message);
var bindingFailure = Assert.IsType<InvocationBindingFailureMessage>(message);
Assert.Equal(testData.ErrorMessage, bindingFailure.BindingFailure.SourceException.Message);
TestParseMessages(testData);
}
[Theory]
[InlineData(new byte[] { 0x05, 0x01 })]
public void ParserDoesNotConsumePartialData(byte[] payload)
[MemberData(nameof(TestDataNames))]
public void WriteMessages(string testDataName)
{
var binder = new TestBinder(new[] { typeof(string) }, typeof(string));
var data = new ReadOnlySequence<byte>(payload);
var result = _hubProtocol.TryParseMessage(ref data, binder, out var message);
Assert.Null(message);
}
var testData = TestData[testDataName];
[Fact]
public void SerializerCanSerializeTypesWithNoDefaultCtor()
{
var result = Write(CompletionMessage.WithResult("0", new List<int> { 42 }.AsReadOnly()));
AssertMessages(new byte[] { ArrayBytes(5), 3, 0x80, StringBytes(1), (byte)'0', 0x03, ArrayBytes(1), 42 }, result);
}
private byte ArrayBytes(int size)
{
Debug.Assert(size < 16, "Test code doesn't support array sizes greater than 15");
return (byte)(0x90 | size);
}
private byte StringBytes(int size)
{
Debug.Assert(size < 16, "Test code doesn't support string sizes greater than 15");
return (byte)(0xa0 | size);
}
private static void AssertMessages(byte[] expectedOutput, ReadOnlyMemory<byte> bytes)
{
var data = new ReadOnlySequence<byte>(bytes);
Assert.True(BinaryMessageParser.TryParseMessage(ref data, out var message));
Assert.Equal(expectedOutput, message.ToArray());
}
private static byte[] Frame(byte[] input)
{
var stream = MemoryBufferWriter.Get();
try
{
BinaryMessageFormatter.WriteLengthPrefix(input.Length, stream);
stream.Write(input);
return stream.ToArray();
}
finally
{
MemoryBufferWriter.Return(stream);
}
}
private static byte[] Write(HubMessage message)
{
var writer = MemoryBufferWriter.Get();
try
{
_hubProtocol.WriteMessage(message, writer);
return writer.ToArray();
}
finally
{
MemoryBufferWriter.Return(writer);
}
}
public class InvalidMessageData
{
public string Name { get; private set; }
public byte[] Encoded { get; private set; }
public string ErrorMessage { get; private set; }
public InvalidMessageData(string name, byte[] encoded, string errorMessage)
{
Name = name;
Encoded = encoded;
ErrorMessage = errorMessage;
}
public override string ToString() => Name;
}
public class ProtocolTestData
{
public string Name { get; }
public string Binary { get; }
public HubMessage Message { get; }
public ProtocolTestData(string name, HubMessage message, string binary)
{
Name = name;
Message = message;
Binary = binary;
}
public override string ToString() => Name;
TestWriteMessages(testData);
}
}
}

@ -0,0 +1 @@
Subproject commit f2dc12bf749e6f708563ac5175c772c025344ebc