add headers and revamp msgpack tests (#1382)

This commit is contained in:
Andrew Stanton-Nurse 2018-02-07 09:36:29 -08:00 committed by GitHub
parent d619d41df0
commit 0435b6dc6e
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
25 changed files with 1119 additions and 509 deletions

View File

@ -2,6 +2,7 @@
<PropertyGroup>
<TargetFramework>netcoreapp2.1</TargetFramework>
<TypeScriptCompileBlocked>True</TypeScriptCompileBlocked>
</PropertyGroup>
<ItemGroup>

View File

@ -2,11 +2,27 @@
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
import { MessagePackHubProtocol } from "../src/MessagePackHubProtocol"
import { MessageType, InvocationMessage, CompletionMessage, ResultMessage } from "@aspnet/signalr"
import { MessageType, InvocationMessage, CompletionMessage, StreamItemMessage } from "@aspnet/signalr"
describe("MessageHubProtocol", () => {
it("can write/read non-blocking Invocation message", () => {
let invocation = <InvocationMessage>{
headers: {},
type: MessageType.Invocation,
target: "myMethod",
arguments: [42, true, "test", ["x1", "y2"], null]
};
let protocol = new MessagePackHubProtocol();
var parsedMessages = protocol.parseMessages(protocol.writeMessage(invocation));
expect(parsedMessages).toEqual([invocation]);
});
it("can write/read Invocation message with headers", () => {
let invocation = <InvocationMessage>{
headers: {
"foo": "bar"
},
type: MessageType.Invocation,
target: "myMethod",
arguments: [42, true, "test", ["x1", "y2"], null]
@ -19,6 +35,7 @@ describe("MessageHubProtocol", () => {
it("can write/read Invocation message", () => {
let invocation = <InvocationMessage>{
headers: {},
type: MessageType.Invocation,
invocationId: "123",
target: "myMethod",
@ -31,22 +48,25 @@ describe("MessageHubProtocol", () => {
});
([
[[0x0b, 0x94, 0x03, 0xa3, 0x61, 0x62, 0x63, 0x01, 0xa3, 0x45, 0x72, 0x72],
[[0x0c, 0x95, 0x03, 0x80, 0xa3, 0x61, 0x62, 0x63, 0x01, 0xa3, 0x45, 0x72, 0x72],
{
headers: {},
type: MessageType.Completion,
invocationId: "abc",
error: "Err",
result: null
} as CompletionMessage],
[[0x0a, 0x94, 0x03, 0xa3, 0x61, 0x62, 0x63, 0x03, 0xa2, 0x4f, 0x4b],
[[0x0b, 0x95, 0x03, 0x80, 0xa3, 0x61, 0x62, 0x63, 0x03, 0xa2, 0x4f, 0x4b],
{
headers: {},
type: MessageType.Completion,
invocationId: "abc",
error: null,
result: "OK"
} as CompletionMessage],
[[0x07, 0x93, 0x03, 0xa3, 0x61, 0x62, 0x63, 0x02],
[[0x08, 0x94, 0x03, 0x80, 0xa3, 0x61, 0x62, 0x63, 0x02],
{
headers: {},
type: MessageType.Completion,
invocationId: "abc",
error: null,
@ -59,48 +79,68 @@ describe("MessageHubProtocol", () => {
}));
([
[[0x07, 0x93, 0x02, 0xa3, 0x61, 0x62, 0x63, 0x08],
[[0x08, 0x94, 0x02, 0x80, 0xa3, 0x61, 0x62, 0x63, 0x08],
{
headers: {},
type: MessageType.StreamItem,
invocationId: "abc",
item: 8
} as ResultMessage]
] as [[number[], CompletionMessage]]).forEach(([payload, expected_message]) =>
it("can read Result message", () => {
} as StreamItemMessage]
] as [[number[], StreamItemMessage]]).forEach(([payload, expected_message]) =>
it("can read StreamItem message", () => {
let messages = new MessagePackHubProtocol().parseMessages(new Uint8Array(payload).buffer);
expect(messages).toEqual([expected_message]);
}));
([
[[0x00], new Error("Invalid payload.")],
[[0x01, 0x90], new Error("Invalid payload.")],
[[0x01, 0xc2], new Error("Invalid payload.")],
[[0x02, 0x91, 0x05], new Error("Invalid message type.")],
[[0x03, 0x91, 0xa1, 0x78], new Error("Invalid message type.")],
[[0x02, 0x91, 0x01], new Error("Invalid payload for Invocation message.")],
[[0x02, 0x91, 0x02], new Error("Invalid payload for stream Result message.")],
[[0x03, 0x92, 0x03, 0xa0], new Error("Invalid payload for Completion message.")],
[[0x05, 0x94, 0x03, 0xa0, 0x02, 0x00], new Error("Invalid payload for Completion message.")],
[[0x04, 0x93, 0x03, 0xa0, 0x01], new Error("Invalid payload for Completion message.")],
[[0x04, 0x93, 0x03, 0xa0, 0x03], new Error("Invalid payload for Completion message.")]
] as [number[], Error][]).forEach(([payload, expected_error]) =>
it("throws for invalid messages", () => {
[[0x0c, 0x94, 0x02, 0x81, 0xa1, 0x74, 0xa1, 0x75, 0xa3, 0x61, 0x62, 0x63, 0x08],
{
headers: {
"t": "u"
},
type: MessageType.StreamItem,
invocationId: "abc",
item: 8
} as StreamItemMessage]
] as [[number[], StreamItemMessage]]).forEach(([payload, expected_message]) =>
it("can read message with headers", () => {
let messages = new MessagePackHubProtocol().parseMessages(new Uint8Array(payload).buffer);
expect(messages).toEqual([expected_message]);
}));
([
["message with no payload", [0x00], new Error("Invalid payload.")],
["message with empty array", [0x01, 0x90], new Error("Invalid payload.")],
["message without outer array", [0x01, 0xc2], new Error("Invalid payload.")],
["message with out-of-range message type", [0x03, 0x92, 0x05, 0x80], new Error("Invalid message type.")],
["message with non-integer message type", [0x04, 0x92, 0xa1, 0x78, 0x80], new Error("Invalid message type.")],
["message with invalid headers", [0x03, 0x92, 0x01, 0x05], new Error("Invalid headers.")],
["Invocation message with invalid invocation id", [0x03, 0x92, 0x01, 0x80], new Error("Invalid payload for Invocation message.")],
["StreamItem message with invalid invocation id", [0x03, 0x92, 0x02, 0x80], new Error("Invalid payload for stream Result message.")],
["Completion message with invalid invocation id", [0x04, 0x93, 0x03, 0x80, 0xa0], new Error("Invalid payload for Completion message.")],
["Completion message with unexpected result", [0x06, 0x95, 0x03, 0x80, 0xa0, 0x02, 0x00], new Error("Invalid payload for Completion message.")],
["Completion message with missing result", [0x05, 0x94, 0x03, 0x80, 0xa0, 0x01], new Error("Invalid payload for Completion message.")],
["Completion message with missing error", [0x05, 0x94, 0x03, 0x80, 0xa0, 0x03], new Error("Invalid payload for Completion message.")]
] as [string, number[], Error][]).forEach(([name, payload, expected_error]) =>
it("throws for " + name, () => {
expect(() => new MessagePackHubProtocol().parseMessages(new Uint8Array(payload).buffer))
.toThrow(expected_error);
}));
it("can read multiple messages", () => {
let payload = [
0x07, 0x93, 0x02, 0xa3, 0x61, 0x62, 0x63, 0x08,
0x0a, 0x94, 0x03, 0xa3, 0x61, 0x62, 0x63, 0x03, 0xa2, 0x4f, 0x4b];
0x08, 0x94, 0x02, 0x80, 0xa3, 0x61, 0x62, 0x63, 0x08,
0x0b, 0x95, 0x03, 0x80, 0xa3, 0x61, 0x62, 0x63, 0x03, 0xa2, 0x4f, 0x4b];
let messages = new MessagePackHubProtocol().parseMessages(new Uint8Array(payload).buffer);
expect(messages).toEqual([
{
headers: {},
type: MessageType.StreamItem,
invocationId: "abc",
item: 8
} as ResultMessage,
} as StreamItemMessage,
{
headers: {},
type: MessageType.Completion,
invocationId: "abc",
error: null,

View File

@ -1,7 +1,7 @@
// 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.
import { IHubProtocol, ProtocolType, MessageType, HubMessage, InvocationMessage, ResultMessage, CompletionMessage, StreamInvocationMessage } from "@aspnet/signalr";
import { IHubProtocol, ProtocolType, MessageType, HubMessage, InvocationMessage, StreamItemMessage, CompletionMessage, StreamInvocationMessage, MessageHeaders } from "@aspnet/signalr";
import { BinaryMessageFormat } from "./BinaryMessageFormat"
import { Buffer } from 'buffer';
import * as msgpack5 from "msgpack5";
@ -28,13 +28,14 @@ export class MessagePackHubProtocol implements IHubProtocol {
}
let messageType = properties[0] as MessageType;
switch (messageType) {
case MessageType.Invocation:
return this.createInvocationMessage(properties);
return this.createInvocationMessage(this.readHeaders(properties), properties);
case MessageType.StreamItem:
return this.createStreamItemMessage(properties);
return this.createStreamItemMessage(this.readHeaders(properties), properties);
case MessageType.Completion:
return this.createCompletionMessage(properties);
return this.createCompletionMessage(this.readHeaders(properties), properties);
case MessageType.Ping:
return this.createPingMessage(properties);
default:
@ -48,48 +49,52 @@ export class MessagePackHubProtocol implements IHubProtocol {
}
return {
type: properties[0]
// Ping messages have no headers.
type: MessageType.Ping
} as HubMessage;
}
private createInvocationMessage(properties: any[]): InvocationMessage {
if (properties.length != 4) {
private createInvocationMessage(headers: MessageHeaders, properties: any[]): InvocationMessage {
if (properties.length != 5) {
throw new Error("Invalid payload for Invocation message.");
}
let invocationId = properties[1];
let invocationId = properties[2] as string;
if (invocationId) {
return {
headers,
type: MessageType.Invocation,
invocationId: invocationId,
target: properties[2],
arguments: properties[3]
} as InvocationMessage;
target: properties[3] as string,
arguments: properties[4],
};
}
else {
return {
headers,
type: MessageType.Invocation,
target: properties[2],
arguments: properties[3]
} as InvocationMessage;
target: properties[3],
arguments: properties[4]
};
}
}
private createStreamItemMessage(properties: any[]): ResultMessage {
if (properties.length != 3) {
private createStreamItemMessage(headers: MessageHeaders, properties: any[]): StreamItemMessage {
if (properties.length != 4) {
throw new Error("Invalid payload for stream Result message.");
}
return {
headers,
type: MessageType.StreamItem,
invocationId: properties[1],
item: properties[2]
} as ResultMessage;
invocationId: properties[2],
item: properties[3]
} as StreamItemMessage;
}
private createCompletionMessage(properties: any[]): CompletionMessage {
if (properties.length < 3) {
private createCompletionMessage(headers: MessageHeaders, properties: any[]): CompletionMessage {
if (properties.length < 4) {
throw new Error("Invalid payload for Completion message.");
}
@ -97,26 +102,27 @@ export class MessagePackHubProtocol implements IHubProtocol {
const voidResult = 2;
const nonVoidResult = 3;
let resultKind = properties[2];
let resultKind = properties[3];
if ((resultKind === voidResult && properties.length != 3) ||
(resultKind !== voidResult && properties.length != 4)) {
if ((resultKind === voidResult && properties.length != 4) ||
(resultKind !== voidResult && properties.length != 5)) {
throw new Error("Invalid payload for Completion message.");
}
let completionMessage = {
headers,
type: MessageType.Completion,
invocationId: properties[1],
invocationId: properties[2],
error: null as string,
result: null as any
};
switch (resultKind) {
case errorResult:
completionMessage.error = properties[3];
completionMessage.error = properties[4];
break;
case nonVoidResult:
completionMessage.result = properties[3];
completionMessage.result = properties[4];
break;
}
@ -139,7 +145,7 @@ export class MessagePackHubProtocol implements IHubProtocol {
private writeInvocation(invocationMessage: InvocationMessage): ArrayBuffer {
let msgpack = msgpack5();
let payload = msgpack.encode([MessageType.Invocation, invocationMessage.invocationId || null,
let payload = msgpack.encode([MessageType.Invocation, invocationMessage.headers || {}, invocationMessage.invocationId || null,
invocationMessage.target, invocationMessage.arguments]);
return BinaryMessageFormat.write(payload.slice());
@ -147,9 +153,17 @@ export class MessagePackHubProtocol implements IHubProtocol {
private writeStreamInvocation(streamInvocationMessage: StreamInvocationMessage): ArrayBuffer {
let msgpack = msgpack5();
let payload = msgpack.encode([MessageType.StreamInvocation, streamInvocationMessage.invocationId,
let payload = msgpack.encode([MessageType.StreamInvocation, streamInvocationMessage.headers || {}, streamInvocationMessage.invocationId,
streamInvocationMessage.target, streamInvocationMessage.arguments]);
return BinaryMessageFormat.write(payload.slice());
}
private readHeaders(properties: any): MessageHeaders {
let headers: MessageHeaders = properties[1] as MessageHeaders;
if (typeof headers !== "object") {
throw new Error("Invalid headers.");
}
return headers;
}
}

View File

@ -6,7 +6,7 @@ import { IConnection } from "./IConnection"
import { HttpConnection, IHttpConnectionOptions } from "./HttpConnection"
import { TransportType, TransferMode } from "./Transports"
import { Subject, Observable } from "./Observable"
import { IHubProtocol, ProtocolType, MessageType, HubMessage, CompletionMessage, ResultMessage, InvocationMessage, StreamInvocationMessage, NegotiationMessage, CancelInvocation } from "./IHubProtocol";
import { IHubProtocol, ProtocolType, MessageType, HubMessage, CompletionMessage, StreamItemMessage, InvocationMessage, StreamInvocationMessage, NegotiationMessage, CancelInvocationMessage } from "./IHubProtocol";
import { JsonHubProtocol } from "./JsonHubProtocol";
import { TextMessageFormat } from "./TextMessageFormat"
import { Base64EncodedHubProtocol } from "./Base64EncodedHubProtocol"
@ -26,7 +26,7 @@ export class HubConnection {
private readonly connection: IConnection;
private readonly logger: ILogger;
private protocol: IHubProtocol;
private callbacks: Map<string, (invocationEvent: HubMessage, error?: Error) => void>;
private callbacks: Map<string, (invocationEvent: StreamItemMessage | CompletionMessage, error?: Error) => void>;
private methods: Map<string, ((...args: any[]) => void)[]>;
private id: number;
private closedCallbacks: ConnectionClosed[];
@ -72,14 +72,14 @@ export class HubConnection {
switch (message.type) {
case MessageType.Invocation:
this.invokeClientMethod(<InvocationMessage>message);
this.invokeClientMethod(message);
break;
case MessageType.StreamItem:
case MessageType.Completion:
let callback = this.callbacks.get((<any>message).invocationId);
let callback = this.callbacks.get(message.invocationId);
if (callback != null) {
if (message.type === MessageType.Completion) {
this.callbacks.delete((<any>message).invocationId);
this.callbacks.delete(message.invocationId);
}
callback(message);
}
@ -168,7 +168,7 @@ export class HubConnection {
let invocationDescriptor = this.createStreamInvocation(methodName, args);
let subject = new Subject<T>(() => {
let cancelInvocation: CancelInvocation = this.createCancelInvocation(invocationDescriptor.invocationId);
let cancelInvocation: CancelInvocationMessage = this.createCancelInvocation(invocationDescriptor.invocationId);
let message: any = this.protocol.writeMessage(cancelInvocation);
this.callbacks.delete(invocationDescriptor.invocationId);
@ -176,23 +176,22 @@ export class HubConnection {
return this.connection.send(message);
});
this.callbacks.set(invocationDescriptor.invocationId, (invocationEvent: HubMessage, error?: Error) => {
this.callbacks.set(invocationDescriptor.invocationId, (invocationEvent: CompletionMessage | StreamItemMessage, error?: Error) => {
if (error) {
subject.error(error);
return;
}
if (invocationEvent.type === MessageType.Completion) {
let completionMessage = <CompletionMessage>invocationEvent;
if (completionMessage.error) {
subject.error(new Error(completionMessage.error));
if (invocationEvent.error) {
subject.error(new Error(invocationEvent.error));
}
else {
subject.complete();
}
}
else {
subject.next(<T>(<ResultMessage>invocationEvent).item);
subject.next(<T>(invocationEvent.item));
}
});
@ -219,7 +218,7 @@ export class HubConnection {
let invocationDescriptor = this.createInvocation(methodName, args, false);
let p = new Promise<any>((resolve, reject) => {
this.callbacks.set(invocationDescriptor.invocationId, (invocationEvent: HubMessage, error?: Error) => {
this.callbacks.set(invocationDescriptor.invocationId, (invocationEvent: StreamItemMessage | CompletionMessage, error?: Error) => {
if (error) {
reject(error);
return;
@ -324,7 +323,7 @@ export class HubConnection {
};
}
private createCancelInvocation(id: string): CancelInvocation {
private createCancelInvocation(id: string): CancelInvocationMessage {
return {
type: MessageType.CancelInvocation,
invocationId: id,

View File

@ -10,29 +10,38 @@ export const enum MessageType {
Ping = 6,
}
export interface HubMessage {
export type MessageHeaders = { [key: string]: string };
export type HubMessage = InvocationMessage | StreamInvocationMessage | StreamItemMessage | CompletionMessage | CancelInvocationMessage | PingMessage;
export interface HubMessageBase {
readonly type: MessageType;
}
export interface InvocationMessage extends HubMessage {
export interface HubInvocationMessage extends HubMessageBase {
readonly headers?: MessageHeaders;
readonly invocationId?: string;
}
export interface InvocationMessage extends HubInvocationMessage {
readonly type: MessageType.Invocation;
readonly target: string;
readonly arguments: Array<any>;
}
export interface StreamInvocationMessage extends HubMessage {
readonly invocationId: string;
export interface StreamInvocationMessage extends HubInvocationMessage {
readonly type: MessageType.StreamInvocation;
readonly target: string;
readonly arguments: Array<any>
}
export interface ResultMessage extends HubMessage {
readonly invocationId: string;
export interface StreamItemMessage extends HubInvocationMessage {
readonly type: MessageType.StreamItem;
readonly item?: any;
}
export interface CompletionMessage extends HubMessage {
readonly invocationId: string;
export interface CompletionMessage extends HubInvocationMessage {
readonly type: MessageType.Completion;
readonly error?: string;
readonly result?: any;
}
@ -41,8 +50,12 @@ export interface NegotiationMessage {
readonly protocol: string;
}
export interface CancelInvocation extends HubMessage {
readonly invocationId: string;
export interface PingMessage extends HubInvocationMessage {
readonly type: MessageType.Ping;
}
export interface CancelInvocationMessage extends HubInvocationMessage {
readonly type: MessageType.CancelInvocation;
}
export const enum ProtocolType {

View File

@ -3,7 +3,7 @@
"windowsAuthentication": false,
"anonymousAuthentication": true,
"iisExpress": {
"applicationUrl": "http://localhost:54542/",
"applicationUrl": "http://localhost:57780/",
"sslPort": 0
}
},

View File

@ -3,7 +3,7 @@
"windowsAuthentication": false,
"anonymousAuthentication": true,
"iisExpress": {
"applicationUrl": "http://localhost:61694/",
"applicationUrl": "http://localhost:57707/",
"sslPort": 0
}
},

View File

@ -67,6 +67,10 @@ The `Target` of an `Invocation` message must refer to a specific method, overloa
**NOTE**: `Invocation ID`s are arbitrarily chosen by the Caller and the Callee is expected to use the same string in all response messages. Callees may establish reasonable limits on `Invocation ID` lengths and terminate the connection when an `Invocation ID` that is too long is received.
## Message Headers
All messages, except the `Ping` message, can carry additional headers. Headers are transmitted as a dictionary with string keys and string values. Clients and servers should disregard headers they do not understand. Since there are no headers defined in this spec, a client or server is never expected to interpret headers. However, clients and servers are expected to be able to process messages containing headers and disregard the headers.
## Non-Blocking Invocations
Invocations can be sent without an `Invocation ID` value. This indicates that the invocation is "non-blocking", and thus the caller does not expect a response. When a Callee receives an invocation without an `Invocation ID` value, it **must not** send any response to that invocation.
@ -410,6 +414,26 @@ Example
}
```
### JSON Header Encoding
Message headers are encoded into a JSON object, with string values, that are stored in the `headers` property. For example:
```json
{
"type": 1,
"headers": {
"Foo": "Bar"
},
"invocationId": "123",
"target": "Send",
"arguments": [
42,
"Test Message"
]
}
```
### JSON Payload Encoding
Items in the arguments array within the `Invocation` message type, as well as the `item` value of the `StreamItem` message and the `result` value of the `Completion` message, encode values which have meaning to each particular Binder. A general guideline for encoding/decoding these values is provided in the "Type Mapping" section at the end of this document, but Binders should provide configuration to applications to allow them to customize these mappings. These mappings need not be self-describing, because when decoding the value, the Binder is expected to know the destination type (by looking up the definition of the method indicated by the Target).
@ -427,10 +451,11 @@ MessagePack uses different formats to encode values. Refer to the [MsgPack forma
`Invocation` messages have the following structure:
```
[1, InvocationId, NonBlocking, Target, [Arguments]]
[1, Headers, InvocationId, NonBlocking, Target, [Arguments]]
```
* `1` - Message Type - `1` indicates this is an `Invocation` message.
* `Headers` - A MsgPack Map containing the headers, with string keys and string values (see MessagePack Headers Encoding below)
* InvocationId - One of:
* A `Nil`, indicating that there is no Invocation ID, OR
* A `String` encoding the Invocation ID for the message.
@ -442,13 +467,14 @@ MessagePack uses different formats to encode values. Refer to the [MsgPack forma
The following payload
```
0x94 0x01 0xa3 0x78 0x79 0x7a 0xa6 0x6d 0x65 0x74 0x68 0x6f 0x64 0x91 0x2a
0x94 0x01 0x80 0xa3 0x78 0x79 0x7a 0xa6 0x6d 0x65 0x74 0x68 0x6f 0x64 0x91 0x2a
```
is decoded as follows:
* `0x94` - 4-element array
* `0x95` - 5-element array
* `0x01` - `1` (Message Type - `Invocation` message)
* `0x80` - Map of length 0 (Headers)
* `0xa3` - string of length 3 (InvocationId)
* `0x78` - `x`
* `0x79` - `y`
@ -467,13 +493,14 @@ is decoded as follows:
The following payload
```
0x94 0x01 0xc0 0xa6 0x6d 0x65 0x74 0x68 0x6f 0x64 0x91 0x2a
0x95 0x01 0x80 0xc0 0xa6 0x6d 0x65 0x74 0x68 0x6f 0x64 0x91 0x2a
```
is decoded as follows:
* `0x94` - 4-element array
* `0x95` - 5-element array
* `0x01` - `1` (Message Type - `Invocation` message)
* `0x80` - Map of length 0 (Headers)
* `0xc0` - `nil` (Invocation ID)
* `0xa6` - string of length 6 (Target)
* `0x6d` - `m`
@ -490,10 +517,11 @@ is decoded as follows:
`StreamInvocation` messages have the following structure:
```
[4, InvocationId, Target, [Arguments]]
[4, Headers, InvocationId, Target, [Arguments]]
```
* `4` - Message Type - `4` indicates this is a `StreamInvocation` message.
* `Headers` - A MsgPack Map containing the headers, with string keys and string values (see MessagePack Headers Encoding below)
* InvocationId - A `String` encoding the Invocation ID for the message.
* Target - A `String` encoding the Target name, as expected by the Callee's Binder.
* Arguments - An Array containing arguments to apply to the method referred to in Target.
@ -503,13 +531,14 @@ Example:
The following payload
```
0x94 0x04 0xa3 0x78 0x79 0x7a 0xa6 0x6d 0x65 0x74 0x68 0x6f 0x64 0x91 0x2a
0x95 0x04 0x80 0xa3 0x78 0x79 0x7a 0xa6 0x6d 0x65 0x74 0x68 0x6f 0x64 0x91 0x2a
```
is decoded as follows:
* `0x94` - 4-element array
* `0x95` - 5-element array
* `0x04` - `4` (Message Type - `StreamInvocation` message)
* `0x80` - Map of length 0 (Headers)
* `0xa3` - string of length 3 (InvocationId)
* `0x78` - `x`
* `0x79` - `y`
@ -529,10 +558,11 @@ is decoded as follows:
`StreamItem` messages have the following structure:
```
[2, InvocationId, Item]
[2, Headers, InvocationId, Item]
```
* `2` - Message Type - `2` indicates this is a `StreamItem` message
* `Headers` - A MsgPack Map containing the headers, with string keys and string values (see MessagePack Headers Encoding below)
* InvocationId - A `String` encoding the Invocation ID for the message
* Item - the value of the stream item
@ -540,13 +570,14 @@ Example:
The following payload:
```
0x93 0x02 0xa3 0x78 0x79 0x7a 0x2a
0x94 0x02 0x80 0xa3 0x78 0x79 0x7a 0x2a
```
is decoded as follows:
* `0x93` - 3-element array
* `0x94` - 4-element array
* `0x02` - `2` (Message Type - `StreamItem` message)
* `0x80` - Map of length 0 (Headers)
* `0xa3` - string of length 3 (InvocationId)
* `0x78` - `x`
* `0x79` - `y`
@ -558,10 +589,11 @@ is decoded as follows:
`Completion` messages have the following structure
```
[3, InvocationId, ResultKind, Result?]
[3, Headers, InvocationId, ResultKind, Result?]
```
* `3` - Message Type - `3` indicates this is a `Completion` message
* `Headers` - A MsgPack Map containing the headers, with string keys and string values (see MessagePack Headers Encoding below)
* InvocationId - A `String` encoding the Invocation ID for the message
* ResultKind - A flag indicating the invocation result kind:
* `1` - Error result - Result contains a `String` with the error message
@ -575,13 +607,14 @@ Examples:
The following payload:
```
0x94 0x03 0xa3 0x78 0x79 0x7a 0x01 0xa5 0x45 0x72 0x72 0x6f 0x72
0x95 0x03 0x80 0xa3 0x78 0x79 0x7a 0x01 0xa5 0x45 0x72 0x72 0x6f 0x72
```
is decoded as follows:
* `0x94` - 4-element array
* `0x03` - `3` (Message Type - `Result` message)
* `0x80` - Map of length 0 (Headers)
* `0xa3` - string of length 3 (InvocationId)
* `0x78` - `x`
* `0x79` - `y`
@ -598,13 +631,14 @@ is decoded as follows:
The following payload:
```
0x93 0x03 0xa3 0x78 0x79 0x7a 0x02
0x94 0x03 0x80 0xa3 0x78 0x79 0x7a 0x02
```
is decoded as follows:
* `0x93` - 3-element array
* `0x94` - 4-element array
* `0x03` - `3` (Message Type - `Result` message)
* `0x80` - Map of length 0 (Headers)
* `0xa3` - string of length 3 (InvocationId)
* `0x78` - `x`
* `0x79` - `y`
@ -615,13 +649,14 @@ is decoded as follows:
The following payload:
```
0x94 0x03 0xa3 0x78 0x79 0x7a 0x03 0x2a
0x95 0x03 0x80 0xa3 0x78 0x79 0x7a 0x03 0x2a
```
is decoded as follows:
* `0x94` - 4-element array
* `0x95` - 5-element array
* `0x03` - `3` (Message Type - `Result` message)
* `0x80` - Map of length 0 (Headers)
* `0xa3` - string of length 3 (InvocationId)
* `0x78` - `x`
* `0x79` - `y`
@ -634,23 +669,25 @@ is decoded as follows:
`CancelInvocation` messages have the following structure
```
[5, InvocationId]
[5, Headers, InvocationId]
```
* `5` - Message Type - `5` indicates this is a `CancelInvocation` message
* `Headers` - A MsgPack Map containing the headers, with string keys and string values (see MessagePack Headers Encoding below)
* InvocationId - A `String` encoding the Invocation ID for the message
Example:
The following payload:
```
0x92 0x05 0xa3 0x78 0x79 0x7a
0x93 0x05 0x80 0xa3 0x78 0x79 0x7a
```
is decoded as follows:
* `0x92` - 2-element array
* `0x93` - 3-element array
* `0x05` - `5` (Message Type `CancelInvocation` message)
* `0x80` - Map of length 0 (Headers)
* `0xa3` - string of length 3 (InvocationId)
* `0x78` - `x`
* `0x79` - `y`
@ -677,9 +714,51 @@ The following payload:
is decoded as follows:
* `0x91` - 1-element array
* `0x92` - 2-element array
* `0x06` - `6` (Message Type - `Ping` message)
### MessagePack Headers Encoding
Headers are encoded in MessagePack messages as a Map that immediately follows the type value. The Map can be empty, in which case it is represented by the byte `0x80`. If there are items in the map,
both the keys and values must be String values.
Headers are not valid in a Ping message. The Ping message is **always exactly encoded** as `0x91 0x06`
Below shows an example encoding of a message containing headers:
```
0x95 0x01 0x82 0xa1 0x78 0xa1 0x79 0xa1 0x7a 0xa1 0x7a 0xa3 0x78 0x79 0x7a 0xa6 0x6d 0x65 0x74 0x68 0x6f 0x64 0x91 0x2a
```
and is decoded as follows:
* `0x95` - 5-element array
* `0x01` - `1` (Message Type - `Invocation` message)
* `0x82` - Map of length 2
* `0xa1` - string of length 1 (Key)
* `0x78` - `x`
* `0xa1` - string of length 1 (Value)
* `0x79` - `y`
* `0xa1` - string of length 1 (Key)
* `0x7a` - `z`
* `0xa1` - string of length 1 (Value)
* `0x7a` - `z`
* `0xa3` - string of length 3 (InvocationId)
* `0x78` - `x`
* `0x79` - `y`
* `0x7a` - `z`
* `0xa6` - string of length 6 (Target)
* `0x6d` - `m`
* `0x65` - `e`
* `0x74` - `t`
* `0x68` - `h`
* `0x6f` - `o`
* `0x64` - `d`
* `0x91` - 1-element array (Arguments)
* `0x2a` - `42` (Argument value)
and interpreted as an Invocation message with headers: `'x' = 'y'` and `'z' = 'z'`.
## Type Mappings
Below are some sample type mappings between JSON types and the .NET client. This is not an exhaustive or authoritative list, just informative guidance. Official clients will provide ways for users to override the default mapping behavior for a particular method, parameter, or parameter type

View File

@ -1,6 +1,8 @@
// 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.Collections.Generic;
namespace Microsoft.AspNetCore.SignalR.Internal.Protocol
{
public class CancelInvocationMessage : HubInvocationMessage

View File

@ -2,6 +2,7 @@
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System;
using System.Collections.Generic;
namespace Microsoft.AspNetCore.SignalR.Internal.Protocol
{

View File

@ -1,12 +1,22 @@
// 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.Collections.Generic;
namespace Microsoft.AspNetCore.SignalR.Internal.Protocol
{
public abstract class HubInvocationMessage : HubMessage
{
private Dictionary<string, string> _headers;
public IDictionary<string, string> Headers
{
get
{
return _headers ?? (_headers = new Dictionary<string, string>());
}
}
public string InvocationId { get; }
protected HubInvocationMessage(string invocationId)

View File

@ -3,8 +3,10 @@
namespace Microsoft.AspNetCore.SignalR.Internal.Protocol
{
// This is basically just a marker type now so that HubProtocol can return a common base class other than object.
public abstract class HubMessage
{
protected HubMessage()
{
}
}
}

View File

@ -2,9 +2,9 @@
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System;
using System.Collections.Generic;
using System.Linq;
using System.Runtime.ExceptionServices;
using Newtonsoft.Json;
namespace Microsoft.AspNetCore.SignalR.Internal.Protocol
{

View File

@ -23,6 +23,7 @@ namespace Microsoft.AspNetCore.SignalR.Internal.Protocol
private const string TargetPropertyName = "target";
private const string ArgumentsPropertyName = "arguments";
private const string PayloadPropertyName = "payload";
private const string HeadersPropertyName = "headers";
public static readonly string ProtocolName = "json";
@ -87,6 +88,7 @@ namespace Microsoft.AspNetCore.SignalR.Internal.Protocol
// Determine the type of the message
var type = JsonUtils.GetRequiredProperty<int>(json, TypePropertyName, JTokenType.Integer);
switch (type)
{
case HubProtocolConstants.InvocationMessageType:
@ -112,40 +114,87 @@ namespace Microsoft.AspNetCore.SignalR.Internal.Protocol
}
}
private void ReadHeaders(JObject json, IDictionary<string, string> headers)
{
var headersProp = json[HeadersPropertyName];
if (headersProp != null)
{
if (headersProp.Type != JTokenType.Object)
{
throw new InvalidDataException($"Expected '{HeadersPropertyName}' to be of type {JTokenType.Object}.");
}
var headersObj = headersProp.Value<JObject>();
foreach (var prop in headersObj)
{
if (prop.Value.Type != JTokenType.String)
{
throw new InvalidDataException($"Expected header '{prop.Key}' to be of type {JTokenType.String}.");
}
headers[prop.Key] = prop.Value.Value<string>();
}
}
}
private void WriteMessageCore(HubMessage message, Stream stream)
{
using (var writer = new JsonTextWriter(new StreamWriter(stream)))
{
writer.WriteStartObject();
switch (message)
{
case InvocationMessage m:
WriteMessageType(writer, HubProtocolConstants.InvocationMessageType);
WriteHeaders(writer, m);
WriteInvocationMessage(m, writer);
break;
case StreamInvocationMessage m:
WriteMessageType(writer, HubProtocolConstants.StreamInvocationMessageType);
WriteHeaders(writer, m);
WriteStreamInvocationMessage(m, writer);
break;
case StreamItemMessage m:
WriteMessageType(writer, HubProtocolConstants.StreamItemMessageType);
WriteHeaders(writer, m);
WriteStreamItemMessage(m, writer);
break;
case CompletionMessage m:
WriteMessageType(writer, HubProtocolConstants.CompletionMessageType);
WriteHeaders(writer, m);
WriteCompletionMessage(m, writer);
break;
case CancelInvocationMessage m:
WriteMessageType(writer, HubProtocolConstants.CancelInvocationMessageType);
WriteHeaders(writer, m);
WriteCancelInvocationMessage(m, writer);
break;
case PingMessage m:
WritePingMessage(m, writer);
case PingMessage _:
WriteMessageType(writer, HubProtocolConstants.PingMessageType);
break;
default:
throw new InvalidOperationException($"Unsupported message type: {message.GetType().FullName}");
}
writer.WriteEndObject();
}
}
private void WriteHeaders(JsonTextWriter writer, HubInvocationMessage message)
{
if (message.Headers.Count > 0)
{
writer.WritePropertyName(HeadersPropertyName);
writer.WriteStartObject();
foreach (var value in message.Headers)
{
writer.WritePropertyName(value.Key);
writer.WriteValue(value.Value);
}
writer.WriteEndObject();
}
}
private void WriteCompletionMessage(CompletionMessage message, JsonTextWriter writer)
{
writer.WriteStartObject();
WriteHubInvocationMessageCommon(message, writer, HubProtocolConstants.CompletionMessageType);
WriteInvocationId(message, writer);
if (!string.IsNullOrEmpty(message.Error))
{
writer.WritePropertyName(ErrorPropertyName);
@ -156,47 +205,36 @@ namespace Microsoft.AspNetCore.SignalR.Internal.Protocol
writer.WritePropertyName(ResultPropertyName);
PayloadSerializer.Serialize(writer, message.Result);
}
writer.WriteEndObject();
}
private void WriteCancelInvocationMessage(CancelInvocationMessage message, JsonTextWriter writer)
{
writer.WriteStartObject();
WriteHubInvocationMessageCommon(message, writer, HubProtocolConstants.CancelInvocationMessageType);
writer.WriteEndObject();
WriteInvocationId(message, writer);
}
private void WriteStreamItemMessage(StreamItemMessage message, JsonTextWriter writer)
{
writer.WriteStartObject();
WriteHubInvocationMessageCommon(message, writer, HubProtocolConstants.StreamItemMessageType);
WriteInvocationId(message, writer);
writer.WritePropertyName(ItemPropertyName);
PayloadSerializer.Serialize(writer, message.Item);
writer.WriteEndObject();
}
private void WriteInvocationMessage(InvocationMessage message, JsonTextWriter writer)
{
writer.WriteStartObject();
WriteHubInvocationMessageCommon(message, writer, HubProtocolConstants.InvocationMessageType);
WriteInvocationId(message, writer);
writer.WritePropertyName(TargetPropertyName);
writer.WriteValue(message.Target);
WriteArguments(message.Arguments, writer);
writer.WriteEndObject();
}
private void WriteStreamInvocationMessage(StreamInvocationMessage message, JsonTextWriter writer)
{
writer.WriteStartObject();
WriteHubInvocationMessageCommon(message, writer, HubProtocolConstants.StreamInvocationMessageType);
WriteInvocationId(message, writer);
writer.WritePropertyName(TargetPropertyName);
writer.WriteValue(message.Target);
WriteArguments(message.Arguments, writer);
writer.WriteEndObject();
}
private void WriteArguments(object[] arguments, JsonTextWriter writer)
@ -210,21 +248,13 @@ namespace Microsoft.AspNetCore.SignalR.Internal.Protocol
writer.WriteEndArray();
}
private void WritePingMessage(PingMessage m, JsonTextWriter writer)
{
writer.WriteStartObject();
WriteMessageType(writer, HubProtocolConstants.PingMessageType);
writer.WriteEndObject();
}
private static void WriteHubInvocationMessageCommon(HubInvocationMessage message, JsonTextWriter writer, int type)
private static void WriteInvocationId(HubInvocationMessage message, JsonTextWriter writer)
{
if (!string.IsNullOrEmpty(message.InvocationId))
{
writer.WritePropertyName(InvocationIdPropertyName);
writer.WriteValue(message.InvocationId);
}
WriteMessageType(writer, type);
}
private static void WriteMessageType(JsonTextWriter writer, int type)
@ -242,15 +272,18 @@ namespace Microsoft.AspNetCore.SignalR.Internal.Protocol
var paramTypes = binder.GetParameterTypes(target);
InvocationMessage message;
try
{
var arguments = BindArguments(args, paramTypes);
return new InvocationMessage(invocationId, target, argumentBindingException: null, arguments: arguments);
message = new InvocationMessage(invocationId, target, argumentBindingException: null, arguments: arguments);
}
catch (Exception ex)
{
return new InvocationMessage(invocationId, target, ExceptionDispatchInfo.Capture(ex));
message = new InvocationMessage(invocationId, target, ExceptionDispatchInfo.Capture(ex));
}
ReadHeaders(json, message.Headers);
return message;
}
private StreamInvocationMessage BindStreamInvocationMessage(JObject json, IInvocationBinder binder)
@ -262,15 +295,63 @@ namespace Microsoft.AspNetCore.SignalR.Internal.Protocol
var paramTypes = binder.GetParameterTypes(target);
StreamInvocationMessage message;
try
{
var arguments = BindArguments(args, paramTypes);
return new StreamInvocationMessage(invocationId, target, argumentBindingException: null, arguments: arguments);
message = new StreamInvocationMessage(invocationId, target, argumentBindingException: null, arguments: arguments);
}
catch (Exception ex)
{
return new StreamInvocationMessage(invocationId, target, ExceptionDispatchInfo.Capture(ex));
message = new StreamInvocationMessage(invocationId, target, ExceptionDispatchInfo.Capture(ex));
}
ReadHeaders(json, message.Headers);
return message;
}
private StreamItemMessage BindStreamItemMessage(JObject json, IInvocationBinder binder)
{
var invocationId = JsonUtils.GetRequiredProperty<string>(json, InvocationIdPropertyName, JTokenType.String);
var result = JsonUtils.GetRequiredProperty<JToken>(json, ItemPropertyName);
var returnType = binder.GetReturnType(invocationId);
var message = new StreamItemMessage(invocationId, result?.ToObject(returnType, PayloadSerializer));
ReadHeaders(json, message.Headers);
return message;
}
private CompletionMessage BindCompletionMessage(JObject json, IInvocationBinder binder)
{
var invocationId = JsonUtils.GetRequiredProperty<string>(json, InvocationIdPropertyName, JTokenType.String);
var error = JsonUtils.GetOptionalProperty<string>(json, ErrorPropertyName, JTokenType.String);
var resultProp = json.Property(ResultPropertyName);
if (error != null && resultProp != null)
{
throw new InvalidDataException("The 'error' and 'result' properties are mutually exclusive.");
}
CompletionMessage message;
if (resultProp == null)
{
message = new CompletionMessage(invocationId, error, result: null, hasResult: false);
}
else
{
var returnType = binder.GetReturnType(invocationId);
var payload = resultProp.Value?.ToObject(returnType, PayloadSerializer);
message = new CompletionMessage(invocationId, error, result: payload, hasResult: true);
}
ReadHeaders(json, message.Headers);
return message;
}
private CancelInvocationMessage BindCancelInvocationMessage(JObject json)
{
var invocationId = JsonUtils.GetRequiredProperty<string>(json, InvocationIdPropertyName, JTokenType.String);
var message = new CancelInvocationMessage(invocationId);
ReadHeaders(json, message.Headers);
return message;
}
private object[] BindArguments(JArray args, Type[] paramTypes)
@ -297,42 +378,6 @@ namespace Microsoft.AspNetCore.SignalR.Internal.Protocol
}
}
private StreamItemMessage BindStreamItemMessage(JObject json, IInvocationBinder binder)
{
var invocationId = JsonUtils.GetRequiredProperty<string>(json, InvocationIdPropertyName, JTokenType.String);
var result = JsonUtils.GetRequiredProperty<JToken>(json, ItemPropertyName);
var returnType = binder.GetReturnType(invocationId);
return new StreamItemMessage(invocationId, result?.ToObject(returnType, PayloadSerializer));
}
private CompletionMessage BindCompletionMessage(JObject json, IInvocationBinder binder)
{
var invocationId = JsonUtils.GetRequiredProperty<string>(json, InvocationIdPropertyName, JTokenType.String);
var error = JsonUtils.GetOptionalProperty<string>(json, ErrorPropertyName, JTokenType.String);
var resultProp = json.Property(ResultPropertyName);
if (error != null && resultProp != null)
{
throw new InvalidDataException("The 'error' and 'result' properties are mutually exclusive.");
}
if (resultProp == null)
{
return new CompletionMessage(invocationId, error, result: null, hasResult: false);
}
var returnType = binder.GetReturnType(invocationId);
var payload = resultProp.Value?.ToObject(returnType, PayloadSerializer);
return new CompletionMessage(invocationId, error, result: payload, hasResult: true);
}
private CancelInvocationMessage BindCancelInvocationMessage(JObject json)
{
var invocationId = JsonUtils.GetRequiredProperty<string>(json, InvocationIdPropertyName, JTokenType.String);
return new CancelInvocationMessage(invocationId);
}
internal static JsonSerializerSettings CreateDefaultSerializerSettings()
{
return new JsonSerializerSettings { ContractResolver = new CamelCasePropertyNamesContractResolver() };

View File

@ -1,6 +1,7 @@
// 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 Microsoft.AspNetCore.SignalR.Internal.Protocol
{
public class StreamItemMessage : HubInvocationMessage

View File

@ -55,6 +55,7 @@ namespace Microsoft.AspNetCore.SignalR.Internal.Protocol
using (var unpacker = Unpacker.Create(input))
{
_ = ReadArrayLength(unpacker, "elementCount");
var messageType = ReadInt32(unpacker, "messageType");
switch (messageType)
@ -79,6 +80,7 @@ namespace Microsoft.AspNetCore.SignalR.Internal.Protocol
private static InvocationMessage CreateInvocationMessage(Unpacker unpacker, IInvocationBinder binder)
{
var headers = ReadHeaders(unpacker);
var invocationId = ReadInvocationId(unpacker);
// For MsgPack, we represent an empty invocation ID as an empty string,
@ -94,27 +96,97 @@ namespace Microsoft.AspNetCore.SignalR.Internal.Protocol
try
{
var arguments = BindArguments(unpacker, parameterTypes);
return new InvocationMessage(invocationId, target, argumentBindingException: null, arguments: arguments);
return ApplyHeaders(headers, new InvocationMessage(invocationId, target, argumentBindingException: null, arguments: arguments));
}
catch (Exception ex)
{
return new InvocationMessage(invocationId, target, ExceptionDispatchInfo.Capture(ex));
return ApplyHeaders(headers, new InvocationMessage(invocationId, target, ExceptionDispatchInfo.Capture(ex)));
}
}
private static StreamInvocationMessage CreateStreamInvocationMessage(Unpacker unpacker, IInvocationBinder binder)
{
var headers = ReadHeaders(unpacker);
var invocationId = ReadInvocationId(unpacker);
var target = ReadString(unpacker, "target");
var parameterTypes = binder.GetParameterTypes(target);
try
{
var arguments = BindArguments(unpacker, parameterTypes);
return new StreamInvocationMessage(invocationId, target, argumentBindingException: null, arguments: arguments);
return ApplyHeaders(headers, new StreamInvocationMessage(invocationId, target, argumentBindingException: null, arguments: arguments));
}
catch (Exception ex)
{
return new StreamInvocationMessage(invocationId, target, ExceptionDispatchInfo.Capture(ex));
return ApplyHeaders(headers, new StreamInvocationMessage(invocationId, target, ExceptionDispatchInfo.Capture(ex)));
}
}
private static StreamItemMessage CreateStreamItemMessage(Unpacker unpacker, IInvocationBinder binder)
{
var headers = ReadHeaders(unpacker);
var invocationId = ReadInvocationId(unpacker);
var itemType = binder.GetReturnType(invocationId);
var value = DeserializeObject(unpacker, itemType, "item");
return ApplyHeaders(headers, new StreamItemMessage(invocationId, value));
}
private static CompletionMessage CreateCompletionMessage(Unpacker unpacker, IInvocationBinder binder)
{
var headers = ReadHeaders(unpacker);
var invocationId = ReadInvocationId(unpacker);
var resultKind = ReadInt32(unpacker, "resultKind");
string error = null;
object result = null;
var hasResult = false;
switch (resultKind)
{
case ErrorResult:
error = ReadString(unpacker, "error");
break;
case NonVoidResult:
var itemType = binder.GetReturnType(invocationId);
result = DeserializeObject(unpacker, itemType, "argument");
hasResult = true;
break;
case VoidResult:
hasResult = false;
break;
default:
throw new FormatException("Invalid invocation result kind.");
}
return ApplyHeaders(headers, new CompletionMessage(invocationId, error, result, hasResult));
}
private static CancelInvocationMessage CreateCancelInvocationMessage(Unpacker unpacker)
{
var headers = ReadHeaders(unpacker);
var invocationId = ReadInvocationId(unpacker);
return ApplyHeaders(headers, new CancelInvocationMessage(invocationId));
}
private static Dictionary<string, string> ReadHeaders(Unpacker unpacker)
{
var headerCount = ReadMapLength(unpacker, "headers");
if (headerCount > 0)
{
// If headerCount is larger than int.MaxValue, things are going to go horribly wrong anyway :)
var headers = new Dictionary<string, string>((int)headerCount);
for (var i = 0; i < headerCount; i++)
{
var key = ReadString(unpacker, $"headers[{i}].Key");
var value = ReadString(unpacker, $"headers[{i}].Value");
headers[key] = value;
}
return headers;
}
else
{
return null;
}
}
@ -144,47 +216,17 @@ namespace Microsoft.AspNetCore.SignalR.Internal.Protocol
}
}
private static StreamItemMessage CreateStreamItemMessage(Unpacker unpacker, IInvocationBinder binder)
private static T ApplyHeaders<T>(IDictionary<string, string> source, T destination) where T: HubInvocationMessage
{
var invocationId = ReadInvocationId(unpacker);
var itemType = binder.GetReturnType(invocationId);
var value = DeserializeObject(unpacker, itemType, "item");
return new StreamItemMessage(invocationId, value);
}
private static CompletionMessage CreateCompletionMessage(Unpacker unpacker, IInvocationBinder binder)
{
var invocationId = ReadInvocationId(unpacker);
var resultKind = ReadInt32(unpacker, "resultKind");
string error = null;
object result = null;
var hasResult = false;
switch (resultKind)
if(source != null && source.Count > 0)
{
case ErrorResult:
error = ReadString(unpacker, "error");
break;
case NonVoidResult:
var itemType = binder.GetReturnType(invocationId);
result = DeserializeObject(unpacker, itemType, "argument");
hasResult = true;
break;
case VoidResult:
hasResult = false;
break;
default:
throw new FormatException("Invalid invocation result kind.");
foreach(var header in source)
{
destination.Headers[header.Key] = header.Value;
}
}
return new CompletionMessage(invocationId, error, result, hasResult);
}
private static CancelInvocationMessage CreateCancelInvocationMessage(Unpacker unpacker)
{
var invocationId = ReadInvocationId(unpacker);
return new CancelInvocationMessage(invocationId);
return destination;
}
public void WriteMessage(HubMessage message, Stream output)
@ -226,67 +268,71 @@ namespace Microsoft.AspNetCore.SignalR.Internal.Protocol
}
}
private void WriteInvocationMessage(InvocationMessage invocationMessage, Packer packer)
private void WriteInvocationMessage(InvocationMessage message, Packer packer)
{
packer.PackArrayHeader(4);
packer.PackArrayHeader(5);
packer.Pack(HubProtocolConstants.InvocationMessageType);
if (string.IsNullOrEmpty(invocationMessage.InvocationId))
PackHeaders(packer, message.Headers);
if (string.IsNullOrEmpty(message.InvocationId))
{
packer.PackNull();
}
else
{
packer.PackString(invocationMessage.InvocationId);
packer.PackString(message.InvocationId);
}
packer.PackString(invocationMessage.Target);
packer.PackObject(invocationMessage.Arguments, SerializationContext);
packer.PackString(message.Target);
packer.PackObject(message.Arguments, SerializationContext);
}
private void WriteStreamInvocationMessage(StreamInvocationMessage streamInvocationMessage, Packer packer)
private void WriteStreamInvocationMessage(StreamInvocationMessage message, Packer packer)
{
packer.PackArrayHeader(5);
packer.Pack(HubProtocolConstants.StreamInvocationMessageType);
PackHeaders(packer, message.Headers);
packer.PackString(message.InvocationId);
packer.PackString(message.Target);
packer.PackObject(message.Arguments, SerializationContext);
}
private void WriteStreamingItemMessage(StreamItemMessage message, Packer packer)
{
packer.PackArrayHeader(4);
packer.Pack(HubProtocolConstants.StreamInvocationMessageType);
packer.PackString(streamInvocationMessage.InvocationId);
packer.PackString(streamInvocationMessage.Target);
packer.PackObject(streamInvocationMessage.Arguments, SerializationContext);
}
private void WriteStreamingItemMessage(StreamItemMessage streamItemMessage, Packer packer)
{
packer.PackArrayHeader(3);
packer.Pack(HubProtocolConstants.StreamItemMessageType);
packer.PackString(streamItemMessage.InvocationId);
packer.PackObject(streamItemMessage.Item, SerializationContext);
PackHeaders(packer, message.Headers);
packer.PackString(message.InvocationId);
packer.PackObject(message.Item, SerializationContext);
}
private void WriteCompletionMessage(CompletionMessage completionMessage, Packer packer)
private void WriteCompletionMessage(CompletionMessage message, Packer packer)
{
var resultKind =
completionMessage.Error != null ? ErrorResult :
completionMessage.HasResult ? NonVoidResult :
message.Error != null ? ErrorResult :
message.HasResult ? NonVoidResult :
VoidResult;
packer.PackArrayHeader(3 + (resultKind != VoidResult ? 1 : 0));
packer.PackArrayHeader(4 + (resultKind != VoidResult ? 1 : 0));
packer.Pack(HubProtocolConstants.CompletionMessageType);
packer.PackString(completionMessage.InvocationId);
PackHeaders(packer, message.Headers);
packer.PackString(message.InvocationId);
packer.Pack(resultKind);
switch (resultKind)
{
case ErrorResult:
packer.PackString(completionMessage.Error);
packer.PackString(message.Error);
break;
case NonVoidResult:
packer.PackObject(completionMessage.Result, SerializationContext);
packer.PackObject(message.Result, SerializationContext);
break;
}
}
private void WriteCancelInvocationMessage(CancelInvocationMessage cancelInvocationMessage, Packer packer)
private void WriteCancelInvocationMessage(CancelInvocationMessage message, Packer packer)
{
packer.PackArrayHeader(2);
packer.PackArrayHeader(3);
packer.Pack(HubProtocolConstants.CancelInvocationMessageType);
packer.PackString(cancelInvocationMessage.InvocationId);
PackHeaders(packer, message.Headers);
packer.PackString(message.InvocationId);
}
private void WritePingMessage(PingMessage pingMessage, Packer packer)
@ -295,6 +341,26 @@ namespace Microsoft.AspNetCore.SignalR.Internal.Protocol
packer.Pack(HubProtocolConstants.PingMessageType);
}
private void PackHeaders(Packer packer, IDictionary<string, string> headers)
{
if (headers != null)
{
packer.PackMapHeader(headers.Count);
if (headers.Count > 0)
{
foreach (var header in headers)
{
packer.PackString(header.Key);
packer.PackString(header.Value);
}
}
}
else
{
packer.PackMapHeader(0);
}
}
private static string ReadInvocationId(Unpacker unpacker)
{
return ReadString(unpacker, "invocationId");
@ -361,6 +427,24 @@ namespace Microsoft.AspNetCore.SignalR.Internal.Protocol
throw new FormatException($"Reading '{field}' as Boolean failed.", msgPackException);
}
private static long ReadMapLength(Unpacker unpacker, string field)
{
Exception msgPackException = null;
try
{
if (unpacker.ReadMapLength(out var value))
{
return value;
}
}
catch (Exception e)
{
msgPackException = e;
}
throw new FormatException($"Reading map length for '{field}' failed.", msgPackException);
}
private static long ReadArrayLength(Unpacker unpacker, string field)
{
Exception msgPackException = null;
@ -402,6 +486,7 @@ namespace Microsoft.AspNetCore.SignalR.Internal.Protocol
{
// serializes objects (here: arguments and results) as maps so that property names are preserved
var serializationContext = new SerializationContext { SerializationMethod = SerializationMethod.Map };
// allows for serializing objects that cannot be deserialized due to the lack of the default ctor etc.
serializationContext.CompatibilityOptions.AllowAsymmetricSerializer = true;
return serializationContext;

View File

@ -582,15 +582,15 @@ namespace Microsoft.AspNetCore.SignalR.Redis
}
var publishTasks = new List<Task>(connectionIds.Count);
var message = new RedisInvocationMessage(target: methodName, arguments: args);
foreach(string connectionId in connectionIds)
foreach (string connectionId in connectionIds)
{
var connection = _connections[connectionId];
// If the connection is local we can skip sending the message through the bus since we require sticky connections.
// This also saves serializing and deserializing the message!
if (connection != null)
{
publishTasks.Add(connection.WriteAsync(message.CreateInvocation()));
publishTasks.Add(connection.WriteAsync(message.CreateInvocation()));
}
else
{

View File

@ -78,7 +78,7 @@ namespace Microsoft.AspNetCore.SignalR.Client.Tests
await connection.ReadSentTextMessageAsync().OrTimeout();
var invokeMessage = await connection.ReadSentTextMessageAsync().OrTimeout();
Assert.Equal("{\"invocationId\":\"1\",\"type\":1,\"target\":\"Foo\",\"arguments\":[]}\u001e", invokeMessage);
Assert.Equal("{\"type\":1,\"invocationId\":\"1\",\"target\":\"Foo\",\"arguments\":[]}\u001e", invokeMessage);
}
finally
{
@ -102,7 +102,7 @@ namespace Microsoft.AspNetCore.SignalR.Client.Tests
await connection.ReadSentTextMessageAsync().OrTimeout();
var invokeMessage = await connection.ReadSentTextMessageAsync().OrTimeout();
Assert.Equal("{\"invocationId\":\"1\",\"type\":4,\"target\":\"Foo\",\"arguments\":[]}\u001e", invokeMessage);
Assert.Equal("{\"type\":4,\"invocationId\":\"1\",\"target\":\"Foo\",\"arguments\":[]}\u001e", invokeMessage);
// Complete the channel
await connection.ReceiveJsonMessage(new { invocationId = "1", type = 3 }).OrTimeout();

View File

@ -0,0 +1,21 @@
// 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.Collections.Generic;
using Microsoft.AspNetCore.SignalR.Internal.Protocol;
namespace Microsoft.AspNetCore.SignalR.Common.Tests.Internal.Protocol
{
internal static class HubMessageHelpers
{
// This lets you add headers to a hub message and return it, in a single expression.
public static HubMessage AddHeaders(IDictionary<string, string> headers, HubInvocationMessage hubMessage)
{
foreach (var header in headers)
{
hubMessage.Headers[header.Key] = header.Value;
}
return hubMessage;
}
}
}

View File

@ -14,11 +14,23 @@ using Xunit;
namespace Microsoft.AspNetCore.SignalR.Common.Tests.Internal.Protocol
{
using static HubMessageHelpers;
public class JsonHubProtocolTests
{
private static readonly IDictionary<string, string> TestHeaders = new Dictionary<string, string>
{
{ "Foo", "Bar" },
{ "KeyWith\nNew\r\nLines", "Still Works" },
{ "ValueWithNewLines", "Also\nWorks\r\nFine" },
};
// It's cleaner to do this as a prefix and use concatenation rather than string interpolation because JSON is already filled with '{'s.
private static readonly string SerializedHeaders = "\"headers\":{\"Foo\":\"Bar\",\"KeyWith\\nNew\\r\\nLines\":\"Still Works\",\"ValueWithNewLines\":\"Also\\nWorks\\r\\nFine\"}";
public static IEnumerable<object[]> ProtocolTestData => new[]
{
new object[] { new InvocationMessage("123", "Target", null, 1, "Foo", 2.0f), true, NullValueHandling.Ignore, "{\"invocationId\":\"123\",\"type\":1,\"target\":\"Target\",\"arguments\":[1,\"Foo\",2.0]}" },
new object[] { new InvocationMessage("123", "Target", null, 1, "Foo", 2.0f), true, NullValueHandling.Ignore, "{\"type\":1,\"invocationId\":\"123\",\"target\":\"Target\",\"arguments\":[1,\"Foo\",2.0]}" },
new object[] { new InvocationMessage(null, "Target", null, 1, "Foo", 2.0f), true, NullValueHandling.Ignore, "{\"type\":1,\"target\":\"Target\",\"arguments\":[1,\"Foo\",2.0]}" },
new object[] { new InvocationMessage(null, "Target", null, true), true, NullValueHandling.Ignore, "{\"type\":1,\"target\":\"Target\",\"arguments\":[true]}" },
new object[] { new InvocationMessage(null, "Target", null, new object[] { null }), true, NullValueHandling.Ignore, "{\"type\":1,\"target\":\"Target\",\"arguments\":[null]}" },
@ -26,38 +38,46 @@ namespace Microsoft.AspNetCore.SignalR.Common.Tests.Internal.Protocol
new object[] { new InvocationMessage(null, "Target", null, new CustomObject()), true, NullValueHandling.Ignore, "{\"type\":1,\"target\":\"Target\",\"arguments\":[{\"stringProp\":\"SignalR!\",\"doubleProp\":6.2831853071,\"intProp\":42,\"dateTimeProp\":\"2017-04-11T00:00:00\",\"byteArrProp\":\"AQID\"}]}" },
new object[] { new InvocationMessage(null, "Target", null, new CustomObject()), false, NullValueHandling.Include, "{\"type\":1,\"target\":\"Target\",\"arguments\":[{\"StringProp\":\"SignalR!\",\"DoubleProp\":6.2831853071,\"IntProp\":42,\"DateTimeProp\":\"2017-04-11T00:00:00\",\"NullProp\":null,\"ByteArrProp\":\"AQID\"}]}" },
new object[] { new InvocationMessage(null, "Target", null, new CustomObject()), true, NullValueHandling.Include, "{\"type\":1,\"target\":\"Target\",\"arguments\":[{\"stringProp\":\"SignalR!\",\"doubleProp\":6.2831853071,\"intProp\":42,\"dateTimeProp\":\"2017-04-11T00:00:00\",\"nullProp\":null,\"byteArrProp\":\"AQID\"}]}" },
new object[] { AddHeaders(TestHeaders, new InvocationMessage("123", "Target", null, 1, "Foo", 2.0f)), true, NullValueHandling.Ignore, "{\"type\":1," + SerializedHeaders + ",\"invocationId\":\"123\",\"target\":\"Target\",\"arguments\":[1,\"Foo\",2.0]}" },
new object[] { new StreamItemMessage("123", 1), true, NullValueHandling.Ignore, "{\"invocationId\":\"123\",\"type\":2,\"item\":1}" },
new object[] { new StreamItemMessage("123", "Foo"), true, NullValueHandling.Ignore, "{\"invocationId\":\"123\",\"type\":2,\"item\":\"Foo\"}" },
new object[] { new StreamItemMessage("123", 2.0f), true, NullValueHandling.Ignore, "{\"invocationId\":\"123\",\"type\":2,\"item\":2.0}" },
new object[] { new StreamItemMessage("123", true), true, NullValueHandling.Ignore, "{\"invocationId\":\"123\",\"type\":2,\"item\":true}" },
new object[] { new StreamItemMessage("123", null), true, NullValueHandling.Ignore, "{\"invocationId\":\"123\",\"type\":2,\"item\":null}" },
new object[] { new StreamItemMessage("123", new CustomObject()), false, NullValueHandling.Ignore, "{\"invocationId\":\"123\",\"type\":2,\"item\":{\"StringProp\":\"SignalR!\",\"DoubleProp\":6.2831853071,\"IntProp\":42,\"DateTimeProp\":\"2017-04-11T00:00:00\",\"ByteArrProp\":\"AQID\"}}" },
new object[] { new StreamItemMessage("123", new CustomObject()), true, NullValueHandling.Ignore, "{\"invocationId\":\"123\",\"type\":2,\"item\":{\"stringProp\":\"SignalR!\",\"doubleProp\":6.2831853071,\"intProp\":42,\"dateTimeProp\":\"2017-04-11T00:00:00\",\"byteArrProp\":\"AQID\"}}" },
new object[] { new StreamItemMessage("123", new CustomObject()), false, NullValueHandling.Include, "{\"invocationId\":\"123\",\"type\":2,\"item\":{\"StringProp\":\"SignalR!\",\"DoubleProp\":6.2831853071,\"IntProp\":42,\"DateTimeProp\":\"2017-04-11T00:00:00\",\"NullProp\":null,\"ByteArrProp\":\"AQID\"}}" },
new object[] { new StreamItemMessage("123", new CustomObject()), true, NullValueHandling.Include, "{\"invocationId\":\"123\",\"type\":2,\"item\":{\"stringProp\":\"SignalR!\",\"doubleProp\":6.2831853071,\"intProp\":42,\"dateTimeProp\":\"2017-04-11T00:00:00\",\"nullProp\":null,\"byteArrProp\":\"AQID\"}}" },
new object[] { new StreamItemMessage("123", 1), true, NullValueHandling.Ignore, "{\"type\":2,\"invocationId\":\"123\",\"item\":1}" },
new object[] { new StreamItemMessage("123", "Foo"), true, NullValueHandling.Ignore, "{\"type\":2,\"invocationId\":\"123\",\"item\":\"Foo\"}" },
new object[] { new StreamItemMessage("123", 2.0f), true, NullValueHandling.Ignore, "{\"type\":2,\"invocationId\":\"123\",\"item\":2.0}" },
new object[] { new StreamItemMessage("123", true), true, NullValueHandling.Ignore, "{\"type\":2,\"invocationId\":\"123\",\"item\":true}" },
new object[] { new StreamItemMessage("123", null), true, NullValueHandling.Ignore, "{\"type\":2,\"invocationId\":\"123\",\"item\":null}" },
new object[] { new StreamItemMessage("123", new CustomObject()), false, NullValueHandling.Ignore, "{\"type\":2,\"invocationId\":\"123\",\"item\":{\"StringProp\":\"SignalR!\",\"DoubleProp\":6.2831853071,\"IntProp\":42,\"DateTimeProp\":\"2017-04-11T00:00:00\",\"ByteArrProp\":\"AQID\"}}" },
new object[] { new StreamItemMessage("123", new CustomObject()), true, NullValueHandling.Ignore, "{\"type\":2,\"invocationId\":\"123\",\"item\":{\"stringProp\":\"SignalR!\",\"doubleProp\":6.2831853071,\"intProp\":42,\"dateTimeProp\":\"2017-04-11T00:00:00\",\"byteArrProp\":\"AQID\"}}" },
new object[] { new StreamItemMessage("123", new CustomObject()), false, NullValueHandling.Include, "{\"type\":2,\"invocationId\":\"123\",\"item\":{\"StringProp\":\"SignalR!\",\"DoubleProp\":6.2831853071,\"IntProp\":42,\"DateTimeProp\":\"2017-04-11T00:00:00\",\"NullProp\":null,\"ByteArrProp\":\"AQID\"}}" },
new object[] { new StreamItemMessage("123", new CustomObject()), true, NullValueHandling.Include, "{\"type\":2,\"invocationId\":\"123\",\"item\":{\"stringProp\":\"SignalR!\",\"doubleProp\":6.2831853071,\"intProp\":42,\"dateTimeProp\":\"2017-04-11T00:00:00\",\"nullProp\":null,\"byteArrProp\":\"AQID\"}}" },
new object[] { AddHeaders(TestHeaders, new StreamItemMessage("123", new CustomObject())), true, NullValueHandling.Include, "{\"type\":2," + SerializedHeaders + ",\"invocationId\":\"123\",\"item\":{\"stringProp\":\"SignalR!\",\"doubleProp\":6.2831853071,\"intProp\":42,\"dateTimeProp\":\"2017-04-11T00:00:00\",\"nullProp\":null,\"byteArrProp\":\"AQID\"}}" },
new object[] { CompletionMessage.WithResult("123", 1), true, NullValueHandling.Ignore, "{\"invocationId\":\"123\",\"type\":3,\"result\":1}" },
new object[] { CompletionMessage.WithResult("123", "Foo"), true, NullValueHandling.Ignore, "{\"invocationId\":\"123\",\"type\":3,\"result\":\"Foo\"}" },
new object[] { CompletionMessage.WithResult("123", 2.0f), true, NullValueHandling.Ignore, "{\"invocationId\":\"123\",\"type\":3,\"result\":2.0}" },
new object[] { CompletionMessage.WithResult("123", true), true, NullValueHandling.Ignore, "{\"invocationId\":\"123\",\"type\":3,\"result\":true}" },
new object[] { CompletionMessage.WithResult("123", null), true, NullValueHandling.Ignore, "{\"invocationId\":\"123\",\"type\":3,\"result\":null}" },
new object[] { CompletionMessage.WithError("123", "Whoops!"), false, NullValueHandling.Ignore, "{\"invocationId\":\"123\",\"type\":3,\"error\":\"Whoops!\"}" },
new object[] { CompletionMessage.WithResult("123", new CustomObject()), false, NullValueHandling.Ignore, "{\"invocationId\":\"123\",\"type\":3,\"result\":{\"StringProp\":\"SignalR!\",\"DoubleProp\":6.2831853071,\"IntProp\":42,\"DateTimeProp\":\"2017-04-11T00:00:00\",\"ByteArrProp\":\"AQID\"}}" },
new object[] { CompletionMessage.WithResult("123", new CustomObject()), true, NullValueHandling.Ignore, "{\"invocationId\":\"123\",\"type\":3,\"result\":{\"stringProp\":\"SignalR!\",\"doubleProp\":6.2831853071,\"intProp\":42,\"dateTimeProp\":\"2017-04-11T00:00:00\",\"byteArrProp\":\"AQID\"}}" },
new object[] { CompletionMessage.WithResult("123", new CustomObject()), false, NullValueHandling.Include, "{\"invocationId\":\"123\",\"type\":3,\"result\":{\"StringProp\":\"SignalR!\",\"DoubleProp\":6.2831853071,\"IntProp\":42,\"DateTimeProp\":\"2017-04-11T00:00:00\",\"NullProp\":null,\"ByteArrProp\":\"AQID\"}}" },
new object[] { CompletionMessage.WithResult("123", new CustomObject()), true, NullValueHandling.Include, "{\"invocationId\":\"123\",\"type\":3,\"result\":{\"stringProp\":\"SignalR!\",\"doubleProp\":6.2831853071,\"intProp\":42,\"dateTimeProp\":\"2017-04-11T00:00:00\",\"nullProp\":null,\"byteArrProp\":\"AQID\"}}" },
new object[] { CompletionMessage.WithResult("123", 1), true, NullValueHandling.Ignore, "{\"type\":3,\"invocationId\":\"123\",\"result\":1}" },
new object[] { CompletionMessage.WithResult("123", "Foo"), true, NullValueHandling.Ignore, "{\"type\":3,\"invocationId\":\"123\",\"result\":\"Foo\"}" },
new object[] { CompletionMessage.WithResult("123", 2.0f), true, NullValueHandling.Ignore, "{\"type\":3,\"invocationId\":\"123\",\"result\":2.0}" },
new object[] { CompletionMessage.WithResult("123", true), true, NullValueHandling.Ignore, "{\"type\":3,\"invocationId\":\"123\",\"result\":true}" },
new object[] { CompletionMessage.WithResult("123", null), true, NullValueHandling.Ignore, "{\"type\":3,\"invocationId\":\"123\",\"result\":null}" },
new object[] { CompletionMessage.WithResult("123", new CustomObject()), false, NullValueHandling.Ignore, "{\"type\":3,\"invocationId\":\"123\",\"result\":{\"StringProp\":\"SignalR!\",\"DoubleProp\":6.2831853071,\"IntProp\":42,\"DateTimeProp\":\"2017-04-11T00:00:00\",\"ByteArrProp\":\"AQID\"}}" },
new object[] { CompletionMessage.WithResult("123", new CustomObject()), true, NullValueHandling.Ignore, "{\"type\":3,\"invocationId\":\"123\",\"result\":{\"stringProp\":\"SignalR!\",\"doubleProp\":6.2831853071,\"intProp\":42,\"dateTimeProp\":\"2017-04-11T00:00:00\",\"byteArrProp\":\"AQID\"}}" },
new object[] { CompletionMessage.WithResult("123", new CustomObject()), false, NullValueHandling.Include, "{\"type\":3,\"invocationId\":\"123\",\"result\":{\"StringProp\":\"SignalR!\",\"DoubleProp\":6.2831853071,\"IntProp\":42,\"DateTimeProp\":\"2017-04-11T00:00:00\",\"NullProp\":null,\"ByteArrProp\":\"AQID\"}}" },
new object[] { CompletionMessage.WithResult("123", new CustomObject()), true, NullValueHandling.Include, "{\"type\":3,\"invocationId\":\"123\",\"result\":{\"stringProp\":\"SignalR!\",\"doubleProp\":6.2831853071,\"intProp\":42,\"dateTimeProp\":\"2017-04-11T00:00:00\",\"nullProp\":null,\"byteArrProp\":\"AQID\"}}" },
new object[] { AddHeaders(TestHeaders, CompletionMessage.WithResult("123", new CustomObject())), true, NullValueHandling.Include, "{\"type\":3," + SerializedHeaders + ",\"invocationId\":\"123\",\"result\":{\"stringProp\":\"SignalR!\",\"doubleProp\":6.2831853071,\"intProp\":42,\"dateTimeProp\":\"2017-04-11T00:00:00\",\"nullProp\":null,\"byteArrProp\":\"AQID\"}}" },
new object[] { CompletionMessage.WithError("123", "Whoops!"), false, NullValueHandling.Ignore, "{\"type\":3,\"invocationId\":\"123\",\"error\":\"Whoops!\"}" },
new object[] { AddHeaders(TestHeaders, CompletionMessage.WithError("123", "Whoops!")), false, NullValueHandling.Ignore, "{\"type\":3," + SerializedHeaders + ",\"invocationId\":\"123\",\"error\":\"Whoops!\"}" },
new object[] { CompletionMessage.Empty("123"), true, NullValueHandling.Ignore, "{\"type\":3,\"invocationId\":\"123\"}" },
new object[] { AddHeaders(TestHeaders, CompletionMessage.Empty("123")), true, NullValueHandling.Ignore, "{\"type\":3," + SerializedHeaders + ",\"invocationId\":\"123\"}" },
new object[] { new StreamInvocationMessage("123", "Target", null, 1, "Foo", 2.0f), true, NullValueHandling.Ignore, "{\"invocationId\":\"123\",\"type\":4,\"target\":\"Target\",\"arguments\":[1,\"Foo\",2.0]}" },
new object[] { new StreamInvocationMessage("123", "Target", null, 1, "Foo", 2.0f), true, NullValueHandling.Ignore, "{\"invocationId\":\"123\",\"type\":4,\"target\":\"Target\",\"arguments\":[1,\"Foo\",2.0]}" },
new object[] { new StreamInvocationMessage("123", "Target", null, true), true, NullValueHandling.Ignore, "{\"invocationId\":\"123\",\"type\":4,\"target\":\"Target\",\"arguments\":[true]}" },
new object[] { new StreamInvocationMessage("123", "Target", null, new object[] { null }), true, NullValueHandling.Ignore, "{\"invocationId\":\"123\",\"type\":4,\"target\":\"Target\",\"arguments\":[null]}" },
new object[] { new StreamInvocationMessage("123", "Target", null, new CustomObject()), false, NullValueHandling.Ignore, "{\"invocationId\":\"123\",\"type\":4,\"target\":\"Target\",\"arguments\":[{\"StringProp\":\"SignalR!\",\"DoubleProp\":6.2831853071,\"IntProp\":42,\"DateTimeProp\":\"2017-04-11T00:00:00\",\"ByteArrProp\":\"AQID\"}]}" },
new object[] { new StreamInvocationMessage("123", "Target", null, new CustomObject()), true, NullValueHandling.Ignore, "{\"invocationId\":\"123\",\"type\":4,\"target\":\"Target\",\"arguments\":[{\"stringProp\":\"SignalR!\",\"doubleProp\":6.2831853071,\"intProp\":42,\"dateTimeProp\":\"2017-04-11T00:00:00\",\"byteArrProp\":\"AQID\"}]}" },
new object[] { new StreamInvocationMessage("123", "Target", null, new CustomObject()), false, NullValueHandling.Include, "{\"invocationId\":\"123\",\"type\":4,\"target\":\"Target\",\"arguments\":[{\"StringProp\":\"SignalR!\",\"DoubleProp\":6.2831853071,\"IntProp\":42,\"DateTimeProp\":\"2017-04-11T00:00:00\",\"NullProp\":null,\"ByteArrProp\":\"AQID\"}]}" },
new object[] { new StreamInvocationMessage("123", "Target", null, new CustomObject()), true, NullValueHandling.Include, "{\"invocationId\":\"123\",\"type\":4,\"target\":\"Target\",\"arguments\":[{\"stringProp\":\"SignalR!\",\"doubleProp\":6.2831853071,\"intProp\":42,\"dateTimeProp\":\"2017-04-11T00:00:00\",\"nullProp\":null,\"byteArrProp\":\"AQID\"}]}" },
new object[] { new StreamInvocationMessage("123", "Target", null, 1, "Foo", 2.0f), true, NullValueHandling.Ignore, "{\"type\":4,\"invocationId\":\"123\",\"target\":\"Target\",\"arguments\":[1,\"Foo\",2.0]}" },
new object[] { new StreamInvocationMessage("123", "Target", null, 1, "Foo", 2.0f), true, NullValueHandling.Ignore, "{\"type\":4,\"invocationId\":\"123\",\"target\":\"Target\",\"arguments\":[1,\"Foo\",2.0]}" },
new object[] { new StreamInvocationMessage("123", "Target", null, true), true, NullValueHandling.Ignore, "{\"type\":4,\"invocationId\":\"123\",\"target\":\"Target\",\"arguments\":[true]}" },
new object[] { new StreamInvocationMessage("123", "Target", null, new object[] { null }), true, NullValueHandling.Ignore, "{\"type\":4,\"invocationId\":\"123\",\"target\":\"Target\",\"arguments\":[null]}" },
new object[] { new StreamInvocationMessage("123", "Target", null, new CustomObject()), false, NullValueHandling.Ignore, "{\"type\":4,\"invocationId\":\"123\",\"target\":\"Target\",\"arguments\":[{\"StringProp\":\"SignalR!\",\"DoubleProp\":6.2831853071,\"IntProp\":42,\"DateTimeProp\":\"2017-04-11T00:00:00\",\"ByteArrProp\":\"AQID\"}]}" },
new object[] { new StreamInvocationMessage("123", "Target", null, new CustomObject()), true, NullValueHandling.Ignore, "{\"type\":4,\"invocationId\":\"123\",\"target\":\"Target\",\"arguments\":[{\"stringProp\":\"SignalR!\",\"doubleProp\":6.2831853071,\"intProp\":42,\"dateTimeProp\":\"2017-04-11T00:00:00\",\"byteArrProp\":\"AQID\"}]}" },
new object[] { new StreamInvocationMessage("123", "Target", null, new CustomObject()), false, NullValueHandling.Include, "{\"type\":4,\"invocationId\":\"123\",\"target\":\"Target\",\"arguments\":[{\"StringProp\":\"SignalR!\",\"DoubleProp\":6.2831853071,\"IntProp\":42,\"DateTimeProp\":\"2017-04-11T00:00:00\",\"NullProp\":null,\"ByteArrProp\":\"AQID\"}]}" },
new object[] { new StreamInvocationMessage("123", "Target", null, new CustomObject()), true, NullValueHandling.Include, "{\"type\":4,\"invocationId\":\"123\",\"target\":\"Target\",\"arguments\":[{\"stringProp\":\"SignalR!\",\"doubleProp\":6.2831853071,\"intProp\":42,\"dateTimeProp\":\"2017-04-11T00:00:00\",\"nullProp\":null,\"byteArrProp\":\"AQID\"}]}" },
new object[] { AddHeaders(TestHeaders, new StreamInvocationMessage("123", "Target", null, new CustomObject())), true, NullValueHandling.Include, "{\"type\":4," + SerializedHeaders + ",\"invocationId\":\"123\",\"target\":\"Target\",\"arguments\":[{\"stringProp\":\"SignalR!\",\"doubleProp\":6.2831853071,\"intProp\":42,\"dateTimeProp\":\"2017-04-11T00:00:00\",\"nullProp\":null,\"byteArrProp\":\"AQID\"}]}" },
new object[] { new CancelInvocationMessage("123"), true, NullValueHandling.Ignore, "{\"invocationId\":\"123\",\"type\":5}" },
new object[] { new CancelInvocationMessage("123"), true, NullValueHandling.Ignore, "{\"type\":5,\"invocationId\":\"123\"}" },
new object[] { AddHeaders(TestHeaders, new CancelInvocationMessage("123")), true, NullValueHandling.Ignore, "{\"type\":5," + SerializedHeaders + ",\"invocationId\":\"123\"}" },
new object[] { PingMessage.Instance, true, NullValueHandling.Ignore, "{\"type\":6}" },
};
@ -118,6 +138,11 @@ namespace Microsoft.AspNetCore.SignalR.Common.Tests.Internal.Protocol
[InlineData("[42]", "Unexpected JSON Token Type 'Array'. Expected a JSON Object.")]
[InlineData("{}", "Missing required property 'type'.")]
[InlineData("{'type':1,'headers':{\"Foo\": 42},'target':'test',arguments:[]}", "Expected header 'Foo' to be of type String.")]
[InlineData("{'type':1,'headers':{\"Foo\": true},'target':'test',arguments:[]}", "Expected header 'Foo' to be of type String.")]
[InlineData("{'type':1,'headers':{\"Foo\": null},'target':'test',arguments:[]}", "Expected header 'Foo' to be of type String.")]
[InlineData("{'type':1,'headers':{\"Foo\": []},'target':'test',arguments:[]}", "Expected header 'Foo' to be of type String.")]
[InlineData("{'type':1}", "Missing required property 'target'.")]
[InlineData("{'type':1,'invocationId':42}", "Expected 'invocationId' to be of type String.")]
[InlineData("{'type':1,'invocationId':'42'}", "Missing required property 'target'.")]
@ -147,7 +172,7 @@ namespace Microsoft.AspNetCore.SignalR.Common.Tests.Internal.Protocol
{
input = Frame(input);
var binder = new TestBinder();
var binder = new TestBinder(Array.Empty<Type>(), typeof(object));
var protocol = new JsonHubProtocol();
var ex = Assert.Throws<InvalidDataException>(() => protocol.TryParseMessages(Encoding.UTF8.GetBytes(input), binder, out var messages));
Assert.Equal(expectedMessage, ex.Message);

View File

@ -0,0 +1,15 @@
using System;
using System.Linq;
using MsgPack;
namespace Microsoft.AspNetCore.SignalR.Common.Tests.Internal.Protocol
{
public static class MessagePackHelpers
{
public static MessagePackObject Array(params MessagePackObject[] items) =>
new MessagePackObject(items);
public static MessagePackObject Map(params (MessagePackObject Key, MessagePackObject Value)[] items) =>
new MessagePackObject(new MessagePackObjectDictionary(items.ToDictionary(i => i.Key, i => i.Value)));
}
}

View File

@ -3,192 +3,458 @@
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.IO;
using System.Linq;
using System.Text;
using Microsoft.AspNetCore.SignalR.Internal.Formatters;
using Microsoft.AspNetCore.SignalR.Internal.Protocol;
using MsgPack;
using Xunit;
using Xunit.Abstractions;
namespace Microsoft.AspNetCore.SignalR.Common.Tests.Internal.Protocol
{
using static MessagePackHelpers;
using static HubMessageHelpers;
public class MessagePackHubProtocolTests
{
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 MessagePackObject TestHeadersSerialized = Map(
("Foo", "Bar"),
("KeyWith\nNew\r\nLines", "Still Works"),
("ValueWithNewLines", "Also\nWorks\r\nFine"));
private static readonly MessagePackHubProtocol _hubProtocol
= new MessagePackHubProtocol();
public static IEnumerable<object[]> TestMessages => new[]
private static MessagePackObject CustomObjectSerialized = Map(
("ByteArrProp", new MessagePackObject(new byte[] { 1, 2, 3 }, isBinary: true)),
("DateTimeProp", new DateTime(2017, 4, 11).Ticks),
("DoubleProp", 6.2831853071),
("IntProp", 42),
("NullProp", MessagePackObject.Nil),
("StringProp", "SignalR!"));
// 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)
// * Encoded: Raw MessagePackObject values (using the MessagePackHelpers static "Arr" and "Map" helpers) describing the message
// * Binary: Base64-encoded binary "baseline" to sanity-check MsgPack-Cli behavior
//
// The Encoded value is used as input to "Parse" and as the expected output that is verified in "Write". So if our encoding changes,
// those values will change and the Assert will give you a useful error telling you how the MsgPack structure itself changed (rather than just
// a bunch of random bytes). However, we want to be sure MsgPack-Cli doesn't change behavior, so we also verify that the binary encoding
// matches our expectation by comparing against a base64-string.
//
// If you change MsgPack encoding, you should update the 'encoded' values for these items, and then re-run the test. You'll get a failure which will
// provide a new Base64 binary string to replace in the 'binary' value. 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[]> TestData => new[]
{
new object[] { new[] { new InvocationMessage(invocationId: "xyz", target: "method", argumentBindingException: null) } },
new object[] { new[] { new InvocationMessage(target: "method", argumentBindingException: null) } },
new object[] { new[] { new InvocationMessage(target: "method", argumentBindingException: null, new object[] { null }) } },
new object[] { new[] { new InvocationMessage(target: "method", argumentBindingException: null, 42) } },
new object[] { new[] { new InvocationMessage(target: "method", argumentBindingException: null, 42, "string") } },
new object[] { new[] { new InvocationMessage(target: "method", argumentBindingException: null, 42, "string", new CustomObject()) } },
new object[] { new[] { new InvocationMessage(target: "method", argumentBindingException: null, new[] { new CustomObject(), new CustomObject() }) } },
// Invocation messages
new object[] {
new ProtocolTestData(
name: "InvocationWithNoHeadersAndNoArgs",
message: new InvocationMessage(invocationId: "xyz", target: "method", argumentBindingException: null),
encoded: Array(HubProtocolConstants.InvocationMessageType, Map(), "xyz", "method", Array()),
binary: "lQGAo3h5eqZtZXRob2SQ"),
},
new object[] {
new ProtocolTestData(
name: "InvocationWithNoHeadersNoIdAndNoArgs",
message: new InvocationMessage(target: "method", argumentBindingException: null),
encoded: Array(HubProtocolConstants.InvocationMessageType, Map(), MessagePackObject.Nil, "method", Array()),
binary: "lQGAwKZtZXRob2SQ"),
},
new object[] {
new ProtocolTestData(
name: "InvocationWithNoHeadersNoIdAndSingleNullArg",
message: new InvocationMessage(target: "method", argumentBindingException: null, new object[] { null }),
encoded: Array(HubProtocolConstants.InvocationMessageType, Map(), MessagePackObject.Nil, "method", Array(MessagePackObject.Nil)),
binary: "lQGAwKZtZXRob2SRwA=="),
},
new object[] {
new ProtocolTestData(
name: "InvocationWithNoHeadersNoIdAndSingleIntArg",
message: new InvocationMessage(target: "method", argumentBindingException: null, 42),
encoded: Array(HubProtocolConstants.InvocationMessageType, Map(), MessagePackObject.Nil, "method", Array(42)),
binary: "lQGAwKZtZXRob2SRKg=="),
},
new object[] {
new ProtocolTestData(
name: "InvocationWithNoHeadersNoIdIntAndStringArgs",
message: new InvocationMessage(target: "method", argumentBindingException: null, 42, "string"),
encoded: Array(HubProtocolConstants.InvocationMessageType, Map(), MessagePackObject.Nil, "method", Array(42, "string")),
binary: "lQGAwKZtZXRob2SSKqZzdHJpbmc="),
},
new object[] {
new ProtocolTestData(
name: "InvocationWithNoHeadersNoIdAndCustomObjectArg",
message: new InvocationMessage(target: "method", argumentBindingException: null, 42, "string", new CustomObject()),
encoded: Array(HubProtocolConstants.InvocationMessageType, Map(), MessagePackObject.Nil, "method", Array(42, "string", CustomObjectSerialized)),
binary: "lQGAwKZtZXRob2STKqZzdHJpbmeGq0J5dGVBcnJQcm9wxAMBAgOsRGF0ZVRpbWVQcm9w0wjUgG2ydsAAqkRvdWJsZVByb3DLQBkh+1RCzxKnSW50UHJvcCqoTnVsbFByb3DAqlN0cmluZ1Byb3CoU2lnbmFsUiE="),
},
new object[] {
new ProtocolTestData(
name: "InvocationWithNoHeadersNoIdAndArrayOfCustomObjectArgs",
message: new InvocationMessage(target: "method", argumentBindingException: null, new[] { new CustomObject(), new CustomObject() }),
encoded: Array(HubProtocolConstants.InvocationMessageType, Map(), MessagePackObject.Nil, "method", Array(CustomObjectSerialized, CustomObjectSerialized)),
binary: "lQGAwKZtZXRob2SShqtCeXRlQXJyUHJvcMQDAQIDrERhdGVUaW1lUHJvcNMI1IBtsnbAAKpEb3VibGVQcm9wy0AZIftUQs8Sp0ludFByb3AqqE51bGxQcm9wwKpTdHJpbmdQcm9wqFNpZ25hbFIhhqtCeXRlQXJyUHJvcMQDAQIDrERhdGVUaW1lUHJvcNMI1IBtsnbAAKpEb3VibGVQcm9wy0AZIftUQs8Sp0ludFByb3AqqE51bGxQcm9wwKpTdHJpbmdQcm9wqFNpZ25hbFIh"),
},
new object[] {
new ProtocolTestData(
name: "InvocationWithHeadersNoIdAndArrayOfCustomObjectArgs",
message: AddHeaders(TestHeaders, new InvocationMessage(target: "method", argumentBindingException: null, new[] { new CustomObject(), new CustomObject() })),
encoded: Array(HubProtocolConstants.InvocationMessageType, TestHeadersSerialized, MessagePackObject.Nil, "method", Array(CustomObjectSerialized, CustomObjectSerialized)),
binary: "lQGDo0Zvb6NCYXKyS2V5V2l0aApOZXcNCkxpbmVzq1N0aWxsIFdvcmtzsVZhbHVlV2l0aE5ld0xpbmVzsEFsc28KV29ya3MNCkZpbmXApm1ldGhvZJKGq0J5dGVBcnJQcm9wxAMBAgOsRGF0ZVRpbWVQcm9w0wjUgG2ydsAAqkRvdWJsZVByb3DLQBkh+1RCzxKnSW50UHJvcCqoTnVsbFByb3DAqlN0cmluZ1Byb3CoU2lnbmFsUiGGq0J5dGVBcnJQcm9wxAMBAgOsRGF0ZVRpbWVQcm9w0wjUgG2ydsAAqkRvdWJsZVByb3DLQBkh+1RCzxKnSW50UHJvcCqoTnVsbFByb3DAqlN0cmluZ1Byb3CoU2lnbmFsUiE="),
},
new object[] { new[] { new CompletionMessage(invocationId: "xyz", error: "Error not found!", result: null, hasResult: false) } },
new object[] { new[] { new CompletionMessage(invocationId: "xyz", error: null, result: null, hasResult: false) } },
new object[] { new[] { new CompletionMessage(invocationId: "xyz", error: null, result: null, hasResult: true) } },
new object[] { new[] { new CompletionMessage(invocationId: "xyz", error: null, result: 42, hasResult: true) } },
new object[] { new[] { new CompletionMessage(invocationId: "xyz", error: null, result: 42.0f, hasResult: true) } },
new object[] { new[] { new CompletionMessage(invocationId: "xyz", error: null, result: "string", hasResult: true) } },
new object[] { new[] { new CompletionMessage(invocationId: "xyz", error: null, result: true, hasResult: true) } },
new object[] { new[] { new CompletionMessage(invocationId: "xyz", error: null, result: new CustomObject(), hasResult: true) } },
new object[] { new[] { new CompletionMessage(invocationId: "xyz", error: null, result: new[] { new CustomObject(), new CustomObject() }, hasResult: true) } },
// StreamItem Messages
new object[] {
new ProtocolTestData(
name: "StreamItemWithNoHeadersAndNullItem",
message: new StreamItemMessage(invocationId: "xyz", item: null),
encoded: Array(HubProtocolConstants.StreamItemMessageType, Map(), "xyz", MessagePackObject.Nil),
binary: "lAKAo3h5esA="),
},
new object[] {
new ProtocolTestData(
name: "StreamItemWithNoHeadersAndIntItem",
message: new StreamItemMessage(invocationId: "xyz", item: 42),
encoded: Array(HubProtocolConstants.StreamItemMessageType, Map(), "xyz", 42),
binary: "lAKAo3h5eio="),
},
new object[] {
new ProtocolTestData(
name: "StreamItemWithNoHeadersAndFloatItem",
message: new StreamItemMessage(invocationId: "xyz", item: 42.0f),
encoded: Array(HubProtocolConstants.StreamItemMessageType, Map(), "xyz", 42.0f),
binary: "lAKAo3h5espCKAAA"),
},
new object[] {
new ProtocolTestData(
name: "StreamItemWithNoHeadersAndStringItem",
message: new StreamItemMessage(invocationId: "xyz", item: "string"),
encoded: Array(HubProtocolConstants.StreamItemMessageType, Map(), "xyz", "string"),
binary: "lAKAo3h5eqZzdHJpbmc="),
},
new object[] {
new ProtocolTestData(
name: "StreamItemWithNoHeadersAndBoolItem",
message: new StreamItemMessage(invocationId: "xyz", item: true),
encoded: Array(HubProtocolConstants.StreamItemMessageType, Map(), "xyz", true),
binary: "lAKAo3h5esM="),
},
new object[] {
new ProtocolTestData(
name: "StreamItemWithNoHeadersAndCustomObjectItem",
message: new StreamItemMessage(invocationId: "xyz", item: new CustomObject()),
encoded: Array(HubProtocolConstants.StreamItemMessageType, Map(), "xyz", CustomObjectSerialized),
binary: "lAKAo3h5eoarQnl0ZUFyclByb3DEAwECA6xEYXRlVGltZVByb3DTCNSAbbJ2wACqRG91YmxlUHJvcMtAGSH7VELPEqdJbnRQcm9wKqhOdWxsUHJvcMCqU3RyaW5nUHJvcKhTaWduYWxSIQ=="),
},
new object[] {
new ProtocolTestData(
name: "StreamItemWithNoHeadersAndCustomObjectArrayItem",
message: new StreamItemMessage(invocationId: "xyz", item: new[] { new CustomObject(), new CustomObject() }),
encoded: Array(HubProtocolConstants.StreamItemMessageType, Map(), "xyz", Array(CustomObjectSerialized, CustomObjectSerialized)),
binary: "lAKAo3h5epKGq0J5dGVBcnJQcm9wxAMBAgOsRGF0ZVRpbWVQcm9w0wjUgG2ydsAAqkRvdWJsZVByb3DLQBkh+1RCzxKnSW50UHJvcCqoTnVsbFByb3DAqlN0cmluZ1Byb3CoU2lnbmFsUiGGq0J5dGVBcnJQcm9wxAMBAgOsRGF0ZVRpbWVQcm9w0wjUgG2ydsAAqkRvdWJsZVByb3DLQBkh+1RCzxKnSW50UHJvcCqoTnVsbFByb3DAqlN0cmluZ1Byb3CoU2lnbmFsUiE="),
},
new object[] {
new ProtocolTestData(
name: "StreamItemWithHeadersAndCustomObjectArrayItem",
message: AddHeaders(TestHeaders, new StreamItemMessage(invocationId: "xyz", item: new[] { new CustomObject(), new CustomObject() })),
encoded: Array(HubProtocolConstants.StreamItemMessageType, TestHeadersSerialized, "xyz", Array(CustomObjectSerialized, CustomObjectSerialized)),
binary: "lAKDo0Zvb6NCYXKyS2V5V2l0aApOZXcNCkxpbmVzq1N0aWxsIFdvcmtzsVZhbHVlV2l0aE5ld0xpbmVzsEFsc28KV29ya3MNCkZpbmWjeHl6koarQnl0ZUFyclByb3DEAwECA6xEYXRlVGltZVByb3DTCNSAbbJ2wACqRG91YmxlUHJvcMtAGSH7VELPEqdJbnRQcm9wKqhOdWxsUHJvcMCqU3RyaW5nUHJvcKhTaWduYWxSIYarQnl0ZUFyclByb3DEAwECA6xEYXRlVGltZVByb3DTCNSAbbJ2wACqRG91YmxlUHJvcMtAGSH7VELPEqdJbnRQcm9wKqhOdWxsUHJvcMCqU3RyaW5nUHJvcKhTaWduYWxSIQ=="),
},
new object[] { new[] { new StreamItemMessage(invocationId: "xyz", item: null) } },
new object[] { new[] { new StreamItemMessage(invocationId: "xyz", item: 42) } },
new object[] { new[] { new StreamItemMessage(invocationId: "xyz", item: 42.0f) } },
new object[] { new[] { new StreamItemMessage(invocationId: "xyz", item: "string") } },
new object[] { new[] { new StreamItemMessage(invocationId: "xyz", item: true) } },
new object[] { new[] { new StreamItemMessage(invocationId: "xyz", item: new CustomObject()) } },
new object[] { new[] { new StreamItemMessage(invocationId: "xyz", item: new[] { new CustomObject(), new CustomObject() }) } },
// Completion Messages
new object[] {
new ProtocolTestData(
name: "CompletionWithNoHeadersAndError",
message: CompletionMessage.WithError(invocationId: "xyz", error: "Error not found!"),
encoded: Array(HubProtocolConstants.CompletionMessageType, Map(), "xyz", 1, "Error not found!"),
binary: "lQOAo3h5egGwRXJyb3Igbm90IGZvdW5kIQ=="),
},
new object[] {
new ProtocolTestData(
name: "CompletionWithHeadersAndError",
message: AddHeaders(TestHeaders, CompletionMessage.WithError(invocationId: "xyz", error: "Error not found!")),
encoded: Array(HubProtocolConstants.CompletionMessageType, TestHeadersSerialized, "xyz", 1, "Error not found!"),
binary: "lQODo0Zvb6NCYXKyS2V5V2l0aApOZXcNCkxpbmVzq1N0aWxsIFdvcmtzsVZhbHVlV2l0aE5ld0xpbmVzsEFsc28KV29ya3MNCkZpbmWjeHl6AbBFcnJvciBub3QgZm91bmQh"),
},
new object[] {
new ProtocolTestData(
name: "CompletionWithNoHeadersAndNoResult",
message: CompletionMessage.Empty(invocationId: "xyz"),
encoded: Array(HubProtocolConstants.CompletionMessageType, Map(), "xyz", 2),
binary: "lAOAo3h5egI="),
},
new object[] {
new ProtocolTestData(
name: "CompletionWithHeadersAndNoResult",
message: AddHeaders(TestHeaders, CompletionMessage.Empty(invocationId: "xyz")),
encoded: Array(HubProtocolConstants.CompletionMessageType, TestHeadersSerialized, "xyz", 2),
binary: "lAODo0Zvb6NCYXKyS2V5V2l0aApOZXcNCkxpbmVzq1N0aWxsIFdvcmtzsVZhbHVlV2l0aE5ld0xpbmVzsEFsc28KV29ya3MNCkZpbmWjeHl6Ag=="),
},
new object[] {
new ProtocolTestData(
name: "CompletionWithNoHeadersAndNullResult",
message: CompletionMessage.WithResult(invocationId: "xyz", payload: null),
encoded: Array(HubProtocolConstants.CompletionMessageType, Map(), "xyz", 3, MessagePackObject.Nil),
binary: "lQOAo3h5egPA"),
},
new object[] {
new ProtocolTestData(
name: "CompletionWithNoHeadersAndIntResult",
message: CompletionMessage.WithResult(invocationId: "xyz", payload: 42),
encoded: Array(HubProtocolConstants.CompletionMessageType, Map(), "xyz", 3, 42),
binary: "lQOAo3h5egMq"),
},
new object[] {
new ProtocolTestData(
name: "CompletionWithNoHeadersAndFloatResult",
message: CompletionMessage.WithResult(invocationId: "xyz", payload: 42.0f),
encoded: Array(HubProtocolConstants.CompletionMessageType, Map(), "xyz", 3, 42.0f),
binary: "lQOAo3h5egPKQigAAA=="),
},
new object[] {
new ProtocolTestData(
name: "CompletionWithNoHeadersAndStringResult",
message: CompletionMessage.WithResult(invocationId: "xyz", payload: "string"),
encoded: Array(HubProtocolConstants.CompletionMessageType, Map(), "xyz", 3, "string"),
binary: "lQOAo3h5egOmc3RyaW5n"),
},
new object[] {
new ProtocolTestData(
name: "CompletionWithNoHeadersAndBooleanResult",
message: CompletionMessage.WithResult(invocationId: "xyz", payload: true),
encoded: Array(HubProtocolConstants.CompletionMessageType, Map(), "xyz", 3, true),
binary: "lQOAo3h5egPD"),
},
new object[] {
new ProtocolTestData(
name: "CompletionWithNoHeadersAndCustomObjectResult",
message: CompletionMessage.WithResult(invocationId: "xyz", payload: new CustomObject()),
encoded: Array(HubProtocolConstants.CompletionMessageType, Map(), "xyz", 3, CustomObjectSerialized),
binary: "lQOAo3h5egOGq0J5dGVBcnJQcm9wxAMBAgOsRGF0ZVRpbWVQcm9w0wjUgG2ydsAAqkRvdWJsZVByb3DLQBkh+1RCzxKnSW50UHJvcCqoTnVsbFByb3DAqlN0cmluZ1Byb3CoU2lnbmFsUiE="),
},
new object[] {
new ProtocolTestData(
name: "CompletionWithNoHeadersAndCustomObjectArrayResult",
message: CompletionMessage.WithResult(invocationId: "xyz", payload: new[] { new CustomObject(), new CustomObject() }),
encoded: Array(HubProtocolConstants.CompletionMessageType, Map(), "xyz", 3, Array(CustomObjectSerialized, CustomObjectSerialized)),
binary: "lQOAo3h5egOShqtCeXRlQXJyUHJvcMQDAQIDrERhdGVUaW1lUHJvcNMI1IBtsnbAAKpEb3VibGVQcm9wy0AZIftUQs8Sp0ludFByb3AqqE51bGxQcm9wwKpTdHJpbmdQcm9wqFNpZ25hbFIhhqtCeXRlQXJyUHJvcMQDAQIDrERhdGVUaW1lUHJvcNMI1IBtsnbAAKpEb3VibGVQcm9wy0AZIftUQs8Sp0ludFByb3AqqE51bGxQcm9wwKpTdHJpbmdQcm9wqFNpZ25hbFIh"),
},
new object[] {
new ProtocolTestData(
name: "CompletionWithHeadersAndCustomObjectArrayResult",
message: AddHeaders(TestHeaders, CompletionMessage.WithResult(invocationId: "xyz", payload: new[] { new CustomObject(), new CustomObject() })),
encoded: Array(HubProtocolConstants.CompletionMessageType, TestHeadersSerialized, "xyz", 3, Array(CustomObjectSerialized, CustomObjectSerialized)),
binary: "lQODo0Zvb6NCYXKyS2V5V2l0aApOZXcNCkxpbmVzq1N0aWxsIFdvcmtzsVZhbHVlV2l0aE5ld0xpbmVzsEFsc28KV29ya3MNCkZpbmWjeHl6A5KGq0J5dGVBcnJQcm9wxAMBAgOsRGF0ZVRpbWVQcm9w0wjUgG2ydsAAqkRvdWJsZVByb3DLQBkh+1RCzxKnSW50UHJvcCqoTnVsbFByb3DAqlN0cmluZ1Byb3CoU2lnbmFsUiGGq0J5dGVBcnJQcm9wxAMBAgOsRGF0ZVRpbWVQcm9w0wjUgG2ydsAAqkRvdWJsZVByb3DLQBkh+1RCzxKnSW50UHJvcCqoTnVsbFByb3DAqlN0cmluZ1Byb3CoU2lnbmFsUiE="),
},
new object[] { new[] { new StreamInvocationMessage(invocationId: "xyz", target: "method", argumentBindingException: null) } },
new object[] { new[] { new StreamInvocationMessage(invocationId: "xyz", target: "method", argumentBindingException: null, new object[] { null }) } },
new object[] { new[] { new StreamInvocationMessage(invocationId: "xyz", target: "method", argumentBindingException: null, 42) } },
new object[] { new[] { new StreamInvocationMessage(invocationId: "xyz", target: "method", argumentBindingException: null, 42, "string") } },
new object[] { new[] { new StreamInvocationMessage(invocationId: "xyz", target: "method", argumentBindingException: null, 42, "string", new CustomObject()) } },
new object[] { new[] { new StreamInvocationMessage(invocationId: "xyz", target: "method", argumentBindingException: null, new[] { new CustomObject(), new CustomObject() }) } },
// StreamInvocation Messages
new object[] {
new ProtocolTestData(
name: "StreamInvocationWithNoHeadersAndNoArgs",
message: new StreamInvocationMessage(invocationId: "xyz", target: "method", argumentBindingException: null),
encoded: Array(HubProtocolConstants.StreamInvocationMessageType, Map(), "xyz", "method", Array()),
binary: "lQSAo3h5eqZtZXRob2SQ"),
},
new object[] {
new ProtocolTestData(
name: "StreamInvocationWithNoHeadersAndNullArg",
message: new StreamInvocationMessage(invocationId: "xyz", target: "method", argumentBindingException: null, new object[] { null }),
encoded: Array(HubProtocolConstants.StreamInvocationMessageType, Map(), "xyz", "method", Array(MessagePackObject.Nil)),
binary: "lQSAo3h5eqZtZXRob2SRwA=="),
},
new object[] {
new ProtocolTestData(
name: "StreamInvocationWithNoHeadersAndIntArg",
message: new StreamInvocationMessage(invocationId: "xyz", target: "method", argumentBindingException: null, 42),
encoded: Array(HubProtocolConstants.StreamInvocationMessageType, Map(), "xyz", "method", Array(42)),
binary: "lQSAo3h5eqZtZXRob2SRKg=="),
},
new object[] {
new ProtocolTestData(
name: "StreamInvocationWithNoHeadersAndIntAndStringArgs",
message: new StreamInvocationMessage(invocationId: "xyz", target: "method", argumentBindingException: null, 42, "string"),
encoded: Array(HubProtocolConstants.StreamInvocationMessageType, Map(), "xyz", "method", Array(42, "string")),
binary: "lQSAo3h5eqZtZXRob2SSKqZzdHJpbmc="),
},
new object[] {
new ProtocolTestData(
name: "StreamInvocationWithNoHeadersAndIntStringAndCustomObjectArgs",
message: new StreamInvocationMessage(invocationId: "xyz", target: "method", argumentBindingException: null, 42, "string", new CustomObject()),
encoded: Array(HubProtocolConstants.StreamInvocationMessageType, Map(), "xyz", "method", Array(42, "string", CustomObjectSerialized)),
binary: "lQSAo3h5eqZtZXRob2STKqZzdHJpbmeGq0J5dGVBcnJQcm9wxAMBAgOsRGF0ZVRpbWVQcm9w0wjUgG2ydsAAqkRvdWJsZVByb3DLQBkh+1RCzxKnSW50UHJvcCqoTnVsbFByb3DAqlN0cmluZ1Byb3CoU2lnbmFsUiE="),
},
new object[] {
new ProtocolTestData(
name: "StreamInvocationWithNoHeadersAndCustomObjectArrayArg",
message: new StreamInvocationMessage(invocationId: "xyz", target: "method", argumentBindingException: null, new[] { new CustomObject(), new CustomObject() }),
encoded: Array(HubProtocolConstants.StreamInvocationMessageType, Map(), "xyz", "method", Array(CustomObjectSerialized, CustomObjectSerialized)),
binary: "lQSAo3h5eqZtZXRob2SShqtCeXRlQXJyUHJvcMQDAQIDrERhdGVUaW1lUHJvcNMI1IBtsnbAAKpEb3VibGVQcm9wy0AZIftUQs8Sp0ludFByb3AqqE51bGxQcm9wwKpTdHJpbmdQcm9wqFNpZ25hbFIhhqtCeXRlQXJyUHJvcMQDAQIDrERhdGVUaW1lUHJvcNMI1IBtsnbAAKpEb3VibGVQcm9wy0AZIftUQs8Sp0ludFByb3AqqE51bGxQcm9wwKpTdHJpbmdQcm9wqFNpZ25hbFIh"),
},
new object[] {
new ProtocolTestData(
name: "StreamInvocationWithHeadersAndCustomObjectArrayArg",
message: AddHeaders(TestHeaders, new StreamInvocationMessage(invocationId: "xyz", target: "method", argumentBindingException: null, new[] { new CustomObject(), new CustomObject() })),
encoded: Array(HubProtocolConstants.StreamInvocationMessageType, TestHeadersSerialized, "xyz", "method", Array(CustomObjectSerialized, CustomObjectSerialized)),
binary: "lQSDo0Zvb6NCYXKyS2V5V2l0aApOZXcNCkxpbmVzq1N0aWxsIFdvcmtzsVZhbHVlV2l0aE5ld0xpbmVzsEFsc28KV29ya3MNCkZpbmWjeHl6pm1ldGhvZJKGq0J5dGVBcnJQcm9wxAMBAgOsRGF0ZVRpbWVQcm9w0wjUgG2ydsAAqkRvdWJsZVByb3DLQBkh+1RCzxKnSW50UHJvcCqoTnVsbFByb3DAqlN0cmluZ1Byb3CoU2lnbmFsUiGGq0J5dGVBcnJQcm9wxAMBAgOsRGF0ZVRpbWVQcm9w0wjUgG2ydsAAqkRvdWJsZVByb3DLQBkh+1RCzxKnSW50UHJvcCqoTnVsbFByb3DAqlN0cmluZ1Byb3CoU2lnbmFsUiE="),
},
new object[] { new[] { new CancelInvocationMessage(invocationId: "xyz") } },
// CancelInvocation Messages
new object[] {
new ProtocolTestData(
name: "CancelInvocationWithNoHeaders",
message: new CancelInvocationMessage(invocationId: "xyz"),
encoded: Array(HubProtocolConstants.CancelInvocationMessageType, Map(), "xyz"),
binary: "kwWAo3h5eg=="),
},
new object[] {
new ProtocolTestData(
name: "CancelInvocationWithHeaders",
message: AddHeaders(TestHeaders, new CancelInvocationMessage(invocationId: "xyz")),
encoded: Array(HubProtocolConstants.CancelInvocationMessageType, TestHeadersSerialized, "xyz"),
binary: "kwWDo0Zvb6NCYXKyS2V5V2l0aApOZXcNCkxpbmVzq1N0aWxsIFdvcmtzsVZhbHVlV2l0aE5ld0xpbmVzsEFsc28KV29ya3MNCkZpbmWjeHl6"),
},
new object[] { new[] { PingMessage.Instance } },
new object[]
{
new HubMessage[]
{
new InvocationMessage(null, "method", null, 42, "string", new CustomObject()),
new CompletionMessage("xyz", error: null, result: 42, hasResult: true),
new StreamItemMessage("xyz", null),
PingMessage.Instance,
new StreamInvocationMessage("xyz", "method", null, 42, "string", new CustomObject()),
new CompletionMessage("xyz", error: null, result: new CustomObject(), hasResult: true),
}
}
// Ping Messages
new object[] {
new ProtocolTestData(
name: "Ping",
message: PingMessage.Instance,
encoded: Array(HubProtocolConstants.PingMessageType),
binary: "kQY="),
},
};
[Theory]
[MemberData(nameof(TestMessages))]
public void CanRoundTripInvocationMessage(HubMessage[] hubMessages)
[MemberData(nameof(TestData))]
public void ParseMessages(ProtocolTestData testData)
{
using (var memoryStream = new MemoryStream())
{
foreach (var hubMessage in hubMessages)
{
_hubProtocol.WriteMessage(hubMessage, memoryStream);
}
// Verify that the input binary string decodes to the expected MsgPack primitives
var bytes = Convert.FromBase64String(testData.Binary);
var obj = Unpack(bytes);
Assert.Equal(testData.Encoded, obj);
_hubProtocol.TryParseMessages(memoryStream.ToArray(), new CompositeTestBinder(hubMessages), out var messages);
// Parse the input fully now.
bytes = Frame(bytes);
var protocol = new MessagePackHubProtocol();
Assert.True(protocol.TryParseMessages(bytes, new TestBinder(testData.Message), out var messages));
Assert.Equal(hubMessages, messages, TestHubMessageEqualityComparer.Instance);
}
Assert.Equal(1, messages.Count);
Assert.Equal(testData.Message, messages[0], TestHubMessageEqualityComparer.Instance);
}
[Theory]
[MemberData(nameof(TestData))]
public void WriteMessages(ProtocolTestData testData)
{
var bytes = Write(testData.Message);
AssertMessages(testData.Encoded, bytes);
// Unframe the message to check the binary encoding
var byteSpan = bytes.AsReadOnlySpan();
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 IEnumerable<object[]> InvalidPayloads => new[]
{
new object[] { new byte[0], "Reading array length for 'elementCount' failed." },
new object[] { new byte[] { 0x91 }, "Reading 'messageType' as Int32 failed." },
new object[] { new byte[] { 0x91, 0xc2 } , "Reading 'messageType' as Int32 failed." }, // message type is not int
new object[] { new byte[] { 0x91, 0x0a } , "Invalid message type: 10." },
// Message Type
new object[] { new InvalidMessageData("MessageTypeString", Array("foo"), "Reading 'messageType' as Int32 failed.") },
new object[] { new InvalidMessageData("MessageTypeOutOfRange", Array(10), "Invalid message type: 10.") },
// Headers
new object[] { new InvalidMessageData("HeadersNotAMap", Array(HubProtocolConstants.InvocationMessageType, "foo"), "Reading map length for 'headers' failed.") },
new object[] { new InvalidMessageData("HeaderKeyInt", Array(HubProtocolConstants.InvocationMessageType, Map((42, "foo"))), "Reading 'headers[0].Key' as String failed.") },
new object[] { new InvalidMessageData("HeaderValueInt", Array(HubProtocolConstants.InvocationMessageType, Map(("foo", 42))), "Reading 'headers[0].Value' as String failed.") },
new object[] { new InvalidMessageData("HeaderKeyArray", Array(HubProtocolConstants.InvocationMessageType, Map(("biz", "boz"), (Array(), "foo"))), "Reading 'headers[1].Key' as String failed.") },
new object[] { new InvalidMessageData("HeaderValueArray", Array(HubProtocolConstants.InvocationMessageType, Map(("biz", "boz"), ("foo", Array()))), "Reading 'headers[1].Value' as String failed.") },
// InvocationMessage
new object[] { new byte[] { 0x94, 0x01 }, "Reading 'invocationId' as String failed." }, // invocationId missing
new object[] { new byte[] { 0x94, 0x01, 0xc2 }, "Reading 'invocationId' as String failed." }, // 0xc2 is Bool false
new object[] { new byte[] { 0x94, 0x01, 0xa3, 0x78, 0x79, 0x7a, 0xc2 }, "Reading 'target' as String failed." }, // target missing
new object[] { new byte[] { 0x94, 0x01, 0xa3, 0x78, 0x79, 0x7a, 0xc2, 0x00 }, "Reading 'target' as String failed." }, // 0x00 is Int
new object[] { new byte[] { 0x94, 0x01, 0xa3, 0x78, 0x79, 0x7a, 0xc2, 0xa1 }, "Reading 'target' as String failed." }, // string is cut
// StreamItemMessage
new object[] { new byte[] { 0x93, 0x02 }, "Reading 'invocationId' as String failed." }, // 0xc2 is Bool false
new object[] { new byte[] { 0x93, 0x02, 0xc2 }, "Reading 'invocationId' as String failed." }, // 0xc2 is Bool false
new object[] { new byte[] { 0x93, 0x02, 0xa3, 0x78, 0x79, 0x7a }, "Deserializing object of the `String` type for 'item' failed." }, // item is missing
new object[] { new byte[] { 0x93, 0x02, 0xa3, 0x78, 0x79, 0x7a, 0x00 }, "Deserializing object of the `String` type for 'item' failed." }, // item type mismatch
// CompletionMessage
new object[] { new byte[] { 0x93, 0x03 }, "Reading 'invocationId' as String failed." }, // 0xc2 is Bool false
new object[] { new byte[] { 0x93, 0x03, 0xc2 }, "Reading 'invocationId' as String failed." }, // 0xc2 is Bool false
new object[] { new byte[] { 0x93, 0x03, 0xa3, 0x78, 0x79, 0x7a, 0xc2 }, "Reading 'resultKind' as Int32 failed." }, // result kind is not int
new object[] { new byte[] { 0x93, 0x03, 0xa3, 0x78, 0x79, 0x7a, 0x0f }, "Invalid invocation result kind." }, // result kind is out of range
new object[] { new byte[] { 0x93, 0x03, 0xa3, 0x78, 0x79, 0x7a, 0x01 }, "Reading 'error' as String failed." }, // error result but no error
new object[] { new byte[] { 0x93, 0x03, 0xa3, 0x78, 0x79, 0x7a, 0x01, 0xa1 }, "Reading 'error' as String failed." }, // error is cut
new object[] { new byte[] { 0x93, 0x03, 0xa3, 0x78, 0x79, 0x7a, 0x03 }, "Deserializing object of the `String` type for 'argument' failed." }, // non void result but result missing
new object[] { new byte[] { 0x93, 0x03, 0xa3, 0x78, 0x79, 0x7a, 0x03, 0xa9 }, "Deserializing object of the `String` type for 'argument' failed." }, // result is cut
new object[] { new byte[] { 0x93, 0x03, 0xa3, 0x78, 0x79, 0x7a, 0x03, 0x00 }, "Deserializing object of the `String` type for 'argument' failed." }, // return type mismatch
new object[] { new InvalidMessageData("InvocationMissingId", Array(HubProtocolConstants.InvocationMessageType, Map()), "Reading 'invocationId' as String failed.") },
new object[] { new InvalidMessageData("InvocationIdBoolean", Array(HubProtocolConstants.InvocationMessageType, Map(), false), "Reading 'invocationId' as String failed.") },
new object[] { new InvalidMessageData("InvocationTargetMissing", Array(HubProtocolConstants.InvocationMessageType, Map(), "abc"), "Reading 'target' as String failed.") },
new object[] { new InvalidMessageData("InvocationTargetInt", Array(HubProtocolConstants.InvocationMessageType, Map(), "abc", 42), "Reading 'target' as String failed.") },
// StreamInvocationMessage
new object[] { new byte[] { 0x95, 0x04 }, "Reading 'invocationId' as String failed." }, // invocationId missing
new object[] { new byte[] { 0x95, 0x04, 0xc2 }, "Reading 'invocationId' as String failed." }, // 0xc2 is Bool false
new object[] { new byte[] { 0x95, 0x04, 0xa3, 0x78, 0x79, 0x7a }, "Reading 'target' as String failed." }, // target missing
new object[] { new byte[] { 0x95, 0x04, 0xa3, 0x78, 0x79, 0x7a, 0x00 }, "Reading 'target' as String failed." }, // 0x00 is Int
new object[] { new byte[] { 0x95, 0x04, 0xa3, 0x78, 0x79, 0x7a, 0xa1 }, "Reading 'target' as String failed." }, // string is cut
new object[] { new InvalidMessageData("StreamInvocationMissingId", Array(HubProtocolConstants.StreamInvocationMessageType, Map()), "Reading 'invocationId' as String failed.") },
new object[] { new InvalidMessageData("StreamInvocationIdBoolean", Array(HubProtocolConstants.StreamInvocationMessageType, Map(), false), "Reading 'invocationId' as String failed.") },
new object[] { new InvalidMessageData("StreamInvocationTargetMissing", Array(HubProtocolConstants.StreamInvocationMessageType, Map(), "abc"), "Reading 'target' as String failed.") },
new object[] { new InvalidMessageData("StreamInvocationTargetInt", Array(HubProtocolConstants.StreamInvocationMessageType, Map(), "abc", 42), "Reading 'target' as String failed.") },
// StreamItemMessage
new object[] { new InvalidMessageData("StreamItemMissingId", Array(HubProtocolConstants.StreamItemMessageType, Map()), "Reading 'invocationId' as String failed.") },
new object[] { new InvalidMessageData("StreamItemInvocationIdBoolean", Array(HubProtocolConstants.StreamItemMessageType, Map(), false), "Reading 'invocationId' as String failed.") },
new object[] { new InvalidMessageData("StreamItemMissing", Array(HubProtocolConstants.StreamItemMessageType, Map(), "xyz"), "Deserializing object of the `String` type for 'item' failed.") },
new object[] { new InvalidMessageData("StreamItemTypeMismatch", Array(HubProtocolConstants.StreamItemMessageType, Map(), "xyz", 42), "Deserializing object of the `String` type for 'item' failed.") },
// CompletionMessage
new object[] { new InvalidMessageData("CompletionMissingId", Array(HubProtocolConstants.CompletionMessageType, Map()), "Reading 'invocationId' as String failed.") },
new object[] { new InvalidMessageData("CompletionIdBoolean", Array(HubProtocolConstants.CompletionMessageType, Map(), false), "Reading 'invocationId' as String failed.") },
new object[] { new InvalidMessageData("CompletionResultKindString", Array(HubProtocolConstants.CompletionMessageType, Map(), "xyz", "abc"), "Reading 'resultKind' as Int32 failed.") },
new object[] { new InvalidMessageData("CompletionResultKindOutOfRange", Array(HubProtocolConstants.CompletionMessageType, Map(), "xyz", 42), "Invalid invocation result kind.") },
new object[] { new InvalidMessageData("CompletionErrorMissing", Array(HubProtocolConstants.CompletionMessageType, Map(), "xyz", 1), "Reading 'error' as String failed.") },
new object[] { new InvalidMessageData("CompletionErrorInt", Array(HubProtocolConstants.CompletionMessageType, Map(), "xyz", 1, 42), "Reading 'error' as String failed.") },
new object[] { new InvalidMessageData("CompletionResultMissing", Array(HubProtocolConstants.CompletionMessageType, Map(), "xyz", 3), "Deserializing object of the `String` type for 'argument' failed.") },
new object[] { new InvalidMessageData("CompletionResultTypeMismatch", Array(HubProtocolConstants.CompletionMessageType, Map(), "xyz", 3, 42), "Deserializing object of the `String` type for 'argument' failed.") },
};
[Theory]
[MemberData(nameof(InvalidPayloads))]
public void ParserThrowsForInvalidMessages(byte[] payload, string expectedExceptionMessage)
public void ParserThrowsForInvalidMessages(InvalidMessageData testData)
{
var payloadSize = payload.Length;
Debug.Assert(payloadSize <= 0x7f, "This test does not support payloads larger than 127 bytes");
// prefix payload with the size
var buffer = new byte[1 + payloadSize];
buffer[0] = (byte)(payloadSize & 0x7f);
Array.Copy(payload, 0, buffer, 1, payloadSize);
var buffer = Frame(Pack(testData.Encoded));
var binder = new TestBinder(new[] { typeof(string) }, typeof(string));
var exception = Assert.Throws<FormatException>(() => _hubProtocol.TryParseMessages(buffer, binder, out var messages));
Assert.Equal(expectedExceptionMessage, exception.Message);
Assert.Equal(testData.ErrorMessage, exception.Message);
}
public static IEnumerable<object[]> ArgumentBindingErrors => new[]
{
// InvocationMessage
new object[] { new byte[] { 0x94, 0x01, 0xc0, 0xa1, 0x78 }, "Reading array length for 'arguments' failed." }, // array is missing
new object[] { new byte[] { 0x94, 0x01, 0xc0, 0xa1, 0x78, 0x00 }, "Reading array length for 'arguments' failed." }, // 0x00 is not array marker
new object[] { new byte[] { 0x94, 0x01, 0xc0, 0xa1, 0x78, 0x91 }, "Error binding arguments. Make sure that the types of the provided values match the types of the hub method being invoked." }, // array is missing elements
new object[] { new byte[] { 0x94, 0x01, 0xc0, 0xa1, 0x78, 0x91, 0xa2, 0x78 }, "Error binding arguments. Make sure that the types of the provided values match the types of the hub method being invoked." }, // array element is cut
new object[] { new byte[] { 0x94, 0x01, 0xc0, 0xa1, 0x78, 0x92, 0xa0, 0x00 }, "Invocation provides 2 argument(s) but target expects 1." }, // argument count does not match binder argument count
new object[] { new byte[] { 0x94, 0x01, 0xc0, 0xa1, 0x78, 0x91, 0x00 }, "Error binding arguments. Make sure that the types of the provided values match the types of the hub method being invoked." }, // argument type mismatch
new object[] {new InvalidMessageData("InvocationArgumentArrayMissing", Array(HubProtocolConstants.InvocationMessageType, Map(), "abc", "xyz"), "Reading array length for 'arguments' failed.") },
new object[] {new InvalidMessageData("InvocationArgumentArrayNotAnArray", Array(HubProtocolConstants.InvocationMessageType, Map(), "abc", "xyz", 42), "Reading array length for 'arguments' failed.") },
new object[] {new InvalidMessageData("InvocationArgumentArraySizeMismatchEmpty", Array(HubProtocolConstants.InvocationMessageType, Map(), "abc", "xyz", Array()), "Invocation provides 0 argument(s) but target expects 1.") },
new object[] {new InvalidMessageData("InvocationArgumentArraySizeMismatchTooLarge", Array(HubProtocolConstants.InvocationMessageType, Map(), "abc", "xyz", Array("a", "b")), "Invocation provides 2 argument(s) but target expects 1.") },
new object[] {new InvalidMessageData("InvocationArgumentTypeMismatch", Array(HubProtocolConstants.InvocationMessageType, Map(), "abc", "xyz", Array(42)), "Error binding arguments. Make sure that the types of the provided values match the types of the hub method being invoked.") },
// StreamInvocationMessage
new object[] { new byte[] { 0x95, 0x04, 0xa3, 0x78, 0x79, 0x7a, 0xa1, 0x78 }, "Reading array length for 'arguments' failed." }, // array is missing
new object[] { new byte[] { 0x95, 0x04, 0xa3, 0x78, 0x79, 0x7a, 0xa1, 0x78, 0x00 }, "Reading array length for 'arguments' failed." }, // 0x00 is not array marker
new object[] { new byte[] { 0x95, 0x04, 0xa3, 0x78, 0x79, 0x7a, 0xa1, 0x78, 0x91 }, "Error binding arguments. Make sure that the types of the provided values match the types of the hub method being invoked." }, // array is missing elements
new object[] { new byte[] { 0x95, 0x04, 0xa3, 0x78, 0x79, 0x7a, 0xa1, 0x78, 0x91, 0xa2, 0x78 }, "Error binding arguments. Make sure that the types of the provided values match the types of the hub method being invoked." }, // array element is cut
new object[] { new byte[] { 0x95, 0x04, 0xa3, 0x78, 0x79, 0x7a, 0xa1, 0x78, 0x92, 0xa0, 0x00 }, "Invocation provides 2 argument(s) but target expects 1." }, // argument count does not match binder argument count
new object[] { new byte[] { 0x95, 0x04, 0xa3, 0x78, 0x79, 0x7a, 0xa1, 0x78, 0x91, 0x00 }, "Error binding arguments. Make sure that the types of the provided values match the types of the hub method being invoked." }, // argument type mismatch
new object[] {new InvalidMessageData("StreamInvocationArgumentArrayMissing", Array(HubProtocolConstants.StreamInvocationMessageType, Map(), "abc", "xyz"), "Reading array length for 'arguments' failed.") }, // array is missing
new object[] {new InvalidMessageData("StreamInvocationArgumentArrayNotAnArray", Array(HubProtocolConstants.StreamInvocationMessageType, Map(), "abc", "xyz", 42), "Reading array length for 'arguments' failed.") }, // arguments isn't an array
new object[] {new InvalidMessageData("StreamInvocationArgumentArraySizeMismatchEmpty", Array(HubProtocolConstants.StreamInvocationMessageType, Map(), "abc", "xyz", Array()), "Invocation provides 0 argument(s) but target expects 1.") }, // array is missing elements
new object[] {new InvalidMessageData("StreamInvocationArgumentArraySizeMismatchTooLarge", Array(HubProtocolConstants.StreamInvocationMessageType, Map(), "abc", "xyz", Array("a", "b")), "Invocation provides 2 argument(s) but target expects 1.") }, // argument count does not match binder argument count
new object[] {new InvalidMessageData("StreamInvocationArgumentTypeMismatch", Array(HubProtocolConstants.StreamInvocationMessageType, Map(), "abc", "xyz", Array(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
};
[Theory]
[MemberData(nameof(ArgumentBindingErrors))]
public void GettingArgumentsThrowsIfBindingFailed(byte[] payload, string expectedExceptionMessage)
public void GettingArgumentsThrowsIfBindingFailed(InvalidMessageData testData)
{
var payloadSize = payload.Length;
Debug.Assert(payloadSize <= 0x7f, "This test does not support payloads larger than 127 bytes");
// prefix payload with the size
var buffer = new byte[1 + payloadSize];
buffer[0] = (byte)(payloadSize & 0x7f);
Array.Copy(payload, 0, buffer, 1, payloadSize);
var buffer = Frame(Pack(testData.Encoded));
var binder = new TestBinder(new[] { typeof(string) }, typeof(string));
_hubProtocol.TryParseMessages(buffer, binder, out var messages);
var exception = Assert.Throws<FormatException>(() => ((HubMethodInvocationMessage)messages[0]).Arguments);
Assert.Equal(expectedExceptionMessage, exception.Message);
Assert.Equal(testData.ErrorMessage, exception.Message);
}
[Theory]
[InlineData(new object[] { new byte[] { 0x05, 0x01 }, 0 })]
[InlineData(new object[] {
new byte[]
{
0x05, 0x93, 0x03, 0xa1, 0x78, 0x02,
0x05, 0x93, 0x03, 0xa1, 0x78, 0x02,
0x05, 0x93, 0x03, 0xa1
}, 2 })]
public void ParserDoesNotConsumePartialData(byte[] payload, int expectedMessagesCount)
{
var binder = new TestBinder(new[] { typeof(string) }, typeof(string));
@ -197,117 +463,104 @@ namespace Microsoft.AspNetCore.SignalR.Common.Tests.Internal.Protocol
Assert.Equal(expectedMessagesCount, messages.Count);
}
public static IEnumerable<object[]> MessageAndPayload => new object[][]
[Fact]
public void SerializerCanSerializeTypesWithNoDefaultCtor()
{
new object[]
{
new InvocationMessage(null, "A", null, 1, new CustomObject()),
new byte[]
{
0x6a, 0x94, 0x01, 0xc0, 0xa1, 0x41,
0x92, // argument array
0x01, // 1 - first argument
// 0x86 - a map of 6 items (properties)
0x86, 0xab, 0x42, 0x79, 0x74, 0x65, 0x41, 0x72, 0x72, 0x50, 0x72, 0x6f, 0x70, 0xc4, 0x03, 0x01,
0x02, 0x03, 0xac, 0x44, 0x61, 0x74, 0x65, 0x54, 0x69, 0x6d, 0x65, 0x50, 0x72, 0x6f, 0x70, 0xd3,
0x08, 0xd4, 0x80, 0x6d, 0xb2, 0x76, 0xc0, 0x00, 0xaa, 0x44, 0x6f, 0x75, 0x62, 0x6c, 0x65, 0x50,
0x72, 0x6f, 0x70, 0xcb, 0x40, 0x19, 0x21, 0xfb, 0x54, 0x42, 0xcf, 0x12, 0xa7, 0x49, 0x6e, 0x74,
0x50, 0x72, 0x6f, 0x70, 0x2a, 0xa8, 0x4e, 0x75, 0x6c, 0x6c, 0x50, 0x72, 0x6f, 0x70, 0xc0, 0xaa,
0x53, 0x74, 0x72, 0x69, 0x6e, 0x67, 0x50, 0x72, 0x6f, 0x70, 0xa8, 0x53, 0x69, 0x67, 0x6e, 0x61,
0x6c, 0x52, 0x21
}
},
new object[]
{
CompletionMessage.WithResult("0", new CustomObject()),
new byte[]
{
0x68, 0x94, 0x03, 0xa1, 0x30, 0x03,
// 0x86 - a map of 6 items (properties)
0x86, 0xab, 0x42, 0x79, 0x74, 0x65, 0x41, 0x72, 0x72, 0x50, 0x72, 0x6f, 0x70, 0xc4, 0x03, 0x01,
0x02, 0x03, 0xac, 0x44, 0x61, 0x74, 0x65, 0x54, 0x69, 0x6d, 0x65, 0x50, 0x72, 0x6f, 0x70, 0xd3,
0x08, 0xd4, 0x80, 0x6d, 0xb2, 0x76, 0xc0, 0x00, 0xaa, 0x44, 0x6f, 0x75, 0x62, 0x6c, 0x65, 0x50,
0x72, 0x6f, 0x70, 0xcb, 0x40, 0x19, 0x21, 0xfb, 0x54, 0x42, 0xcf, 0x12, 0xa7, 0x49, 0x6e, 0x74,
0x50, 0x72, 0x6f, 0x70, 0x2a, 0xa8, 0x4e, 0x75, 0x6c, 0x6c, 0x50, 0x72, 0x6f, 0x70, 0xc0, 0xaa,
0x53, 0x74, 0x72, 0x69, 0x6e, 0x67, 0x50, 0x72, 0x6f, 0x70, 0xa8, 0x53, 0x69, 0x67, 0x6e, 0x61,
0x6c, 0x52, 0x21
}
},
new object[]
{
new StreamItemMessage("0", new CustomObject()),
new byte[]
{
0x67, 0x93, 0x02, 0xa1, 0x30,
// 0x86 - a map of 6 items (properties)
0x86, 0xab, 0x42, 0x79, 0x74, 0x65, 0x41, 0x72, 0x72, 0x50, 0x72, 0x6f, 0x70, 0xc4, 0x03, 0x01,
0x02, 0x03, 0xac, 0x44, 0x61, 0x74, 0x65, 0x54, 0x69, 0x6d, 0x65, 0x50, 0x72, 0x6f, 0x70, 0xd3,
0x08, 0xd4, 0x80, 0x6d, 0xb2, 0x76, 0xc0, 0x00, 0xaa, 0x44, 0x6f, 0x75, 0x62, 0x6c, 0x65, 0x50,
0x72, 0x6f, 0x70, 0xcb, 0x40, 0x19, 0x21, 0xfb, 0x54, 0x42, 0xcf, 0x12, 0xa7, 0x49, 0x6e, 0x74,
0x50, 0x72, 0x6f, 0x70, 0x2a, 0xa8, 0x4e, 0x75, 0x6c, 0x6c, 0x50, 0x72, 0x6f, 0x70, 0xc0, 0xaa,
0x53, 0x74, 0x72, 0x69, 0x6e, 0x67, 0x50, 0x72, 0x6f, 0x70, 0xa8, 0x53, 0x69, 0x67, 0x6e, 0x61,
0x6c, 0x52, 0x21
}
},
new object[]
{
new StreamInvocationMessage("0", "A", null, 1, new CustomObject()),
new byte[]
{
0x6b, 0x94, 0x04, 0xa1, 0x30, 0xa1, 0x41,
0x92, // argument array
0x01, // 1 - first argument
// 0x86 - a map of 6 items (properties)
0x86, 0xab, 0x42, 0x79, 0x74, 0x65, 0x41, 0x72, 0x72, 0x50, 0x72, 0x6f, 0x70, 0xc4, 0x03, 0x01,
0x02, 0x03, 0xac, 0x44, 0x61, 0x74, 0x65, 0x54, 0x69, 0x6d, 0x65, 0x50, 0x72, 0x6f, 0x70, 0xd3,
0x08, 0xd4, 0x80, 0x6d, 0xb2, 0x76, 0xc0, 0x00, 0xaa, 0x44, 0x6f, 0x75, 0x62, 0x6c, 0x65, 0x50,
0x72, 0x6f, 0x70, 0xcb, 0x40, 0x19, 0x21, 0xfb, 0x54, 0x42, 0xcf, 0x12, 0xa7, 0x49, 0x6e, 0x74,
0x50, 0x72, 0x6f, 0x70, 0x2a, 0xa8, 0x4e, 0x75, 0x6c, 0x6c, 0x50, 0x72, 0x6f, 0x70, 0xc0, 0xaa,
0x53, 0x74, 0x72, 0x69, 0x6e, 0x67, 0x50, 0x72, 0x6f, 0x70, 0xa8, 0x53, 0x69, 0x67, 0x6e, 0x61,
0x6c, 0x52, 0x21
}
},
new object[]
{
new CancelInvocationMessage("0"),
new byte[]
{
0x04, 0x92, 0x05, 0xa1, 0x30
}
},
new object[]
{
PingMessage.Instance,
new byte[]
{
0x02,
0x91, // message array length = 1 (fixarray)
0x06, // type = 6 = Ping (fixnum)
}
},
};
var result = Write(CompletionMessage.WithResult("0", new List<int> { 42 }.AsReadOnly()));
AssertMessages(Array(HubProtocolConstants.CompletionMessageType, Map(), "0", 3, Array(42)), result);
}
[Theory]
[MemberData(nameof(MessageAndPayload))]
public void SerializeMessageTest(HubMessage message, byte[] expectedPayload)
private static void AssertMessages(MessagePackObject expectedOutput, ReadOnlySpan<byte> bytes)
{
using (var memoryStream = new MemoryStream())
Assert.True(BinaryMessageParser.TryParseMessage(ref bytes, out var message));
var obj = Unpack(message.ToArray());
Assert.Equal(expectedOutput, obj);
}
private static byte[] Frame(byte[] input)
{
using (var stream = new MemoryStream())
{
_hubProtocol.WriteMessage(message, memoryStream);
Assert.Equal(expectedPayload, memoryStream.ToArray());
BinaryMessageFormatter.WriteMessage(input, stream);
stream.Flush();
return stream.ToArray();
}
}
[Fact]
public void CanWriteObjectsWithoutDefaultCtors()
private static MessagePackObject Unpack(byte[] input)
{
var expectedPayload = new byte[] { 0x07, 0x94, 0x03, 0xa1, 0x30, 0x03, 0x91, 0x2a };
using (var memoryStream = new MemoryStream())
using (var stream = new MemoryStream(input))
{
_hubProtocol.WriteMessage(CompletionMessage.WithResult("0", new List<int> { 42 }.AsReadOnly()), memoryStream);
Assert.Equal(expectedPayload, memoryStream.ToArray());
using (var unpacker = Unpacker.Create(stream))
{
Assert.True(unpacker.ReadObject(out var obj));
return obj;
}
}
}
private static byte[] Pack(MessagePackObject input)
{
var options = new PackingOptions()
{
StringEncoding = Encoding.UTF8
};
using (var stream = new MemoryStream())
{
using (var packer = Packer.Create(stream))
{
input.PackToMessage(packer, options);
packer.Flush();
}
stream.Flush();
return stream.ToArray();
}
}
private static byte[] Write(HubMessage message)
{
var protocol = new MessagePackHubProtocol();
using (var stream = new MemoryStream())
{
protocol.WriteMessage(message, stream);
stream.Flush();
return stream.ToArray();
}
}
public class InvalidMessageData
{
public string Name { get; private set; }
public MessagePackObject Encoded { get; private set; }
public string ErrorMessage { get; private set; }
public InvalidMessageData(string name, MessagePackObject 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 MessagePackObject Encoded { get; }
public HubMessage Message { get; }
public ProtocolTestData(string name, HubMessage message, MessagePackObject encoded, string binary)
{
Name = name;
Message = message;
Encoded = encoded;
Binary = binary;
}
public override string ToString() => Name;
}
}
}

View File

@ -44,7 +44,8 @@ namespace Microsoft.AspNetCore.SignalR.Common.Tests.Internal.Protocol
private bool CompletionMessagesEqual(CompletionMessage x, CompletionMessage y)
{
return string.Equals(x.InvocationId, y.InvocationId, StringComparison.Ordinal) &&
return SequenceEqual(x.Headers, y.Headers) &&
string.Equals(x.InvocationId, y.InvocationId, StringComparison.Ordinal) &&
string.Equals(x.Error, y.Error, StringComparison.Ordinal) &&
x.HasResult == y.HasResult &&
(Equals(x.Result, y.Result) || SequenceEqual(x.Result, y.Result));
@ -52,20 +53,23 @@ namespace Microsoft.AspNetCore.SignalR.Common.Tests.Internal.Protocol
private bool StreamItemMessagesEqual(StreamItemMessage x, StreamItemMessage y)
{
return string.Equals(x.InvocationId, y.InvocationId, StringComparison.Ordinal) &&
return SequenceEqual(x.Headers, y.Headers) &&
string.Equals(x.InvocationId, y.InvocationId, StringComparison.Ordinal) &&
(Equals(x.Item, y.Item) || SequenceEqual(x.Item, y.Item));
}
private bool InvocationMessagesEqual(InvocationMessage x, InvocationMessage y)
{
return string.Equals(x.InvocationId, y.InvocationId, StringComparison.Ordinal) &&
return SequenceEqual(x.Headers, y.Headers) &&
string.Equals(x.InvocationId, y.InvocationId, StringComparison.Ordinal) &&
string.Equals(x.Target, y.Target, StringComparison.Ordinal) &&
ArgumentListsEqual(x.Arguments, y.Arguments);
}
private bool StreamInvocationMessagesEqual(StreamInvocationMessage x, StreamInvocationMessage y)
{
return string.Equals(x.InvocationId, y.InvocationId, StringComparison.Ordinal) &&
return SequenceEqual(x.Headers, y.Headers) &&
string.Equals(x.InvocationId, y.InvocationId, StringComparison.Ordinal) &&
string.Equals(x.Target, y.Target, StringComparison.Ordinal) &&
ArgumentListsEqual(x.Arguments, y.Arguments);
}

View File

@ -41,7 +41,7 @@ namespace Microsoft.AspNetCore.SignalR.Tests
Connection = new DefaultConnectionContext(Guid.NewGuid().ToString(), _transport, Application);
var claimValue = Interlocked.Increment(ref _id).ToString();
var claims = new List<Claim>{ new Claim(ClaimTypes.Name, claimValue) };
var claims = new List<Claim> { new Claim(ClaimTypes.Name, claimValue) };
if (addClaimId)
{
claims.Add(new Claim(ClaimTypes.NameIdentifier, claimValue));

View File

@ -1766,7 +1766,7 @@ namespace Microsoft.AspNetCore.SignalR.Tests
while ((message = await client.ReadAsync().OrTimeout()) != null)
{
counter += 1;
Assert.Same(PingMessage.Instance, message);
Assert.IsType<PingMessage>(message);
}
Assert.InRange(counter, 1, Int32.MaxValue);
}