Fix #1170 by removing invocationId from non-blocking calls (#1218)

This commit is contained in:
Andrew Stanton-Nurse 2017-12-19 10:40:58 -08:00 committed by GitHub
parent c8bd72be36
commit 00a6dc983a
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
25 changed files with 361 additions and 339 deletions

View File

@ -36,16 +36,16 @@ namespace Microsoft.AspNetCore.SignalR.Microbenchmarks
switch (Input)
{
case Message.NoArguments:
_hubMessage = new InvocationMessage("123", true, "Target", null);
_hubMessage = new InvocationMessage(target: "Target", argumentBindingException: null);
break;
case Message.FewArguments:
_hubMessage = new InvocationMessage("123", true, "Target", null, 1, "Foo", 2.0f);
_hubMessage = new InvocationMessage(target: "Target", argumentBindingException: null, 1, "Foo", 2.0f);
break;
case Message.ManyArguments:
_hubMessage = new InvocationMessage("123", true, "Target", null, 1, "string", 2.0f, true, (byte)9, new byte[] { 5, 4, 3, 2, 1 }, 'c', 123456789101112L);
_hubMessage = new InvocationMessage(target: "Target", argumentBindingException: null, 1, "string", 2.0f, true, (byte)9, new byte[] { 5, 4, 3, 2, 1 }, 'c', 123456789101112L);
break;
case Message.LargeArguments:
_hubMessage = new InvocationMessage("123", true, "Target", null, new string('F', 10240), new byte[10240]);
_hubMessage = new InvocationMessage(target: "Target", argumentBindingException: null, new string('F', 10240), new byte[10240]);
break;
}
@ -90,4 +90,4 @@ namespace Microsoft.AspNetCore.SignalR.Microbenchmarks
return _hubProtocolReaderWriter.WriteMessage(_hubMessage);
}
}
}
}

View File

@ -40,9 +40,7 @@ describe("HubConnection", () => {
expect(connection.sentData.length).toBe(1);
expect(JSON.parse(connection.sentData[0])).toEqual({
type: MessageType.Invocation,
invocationId: connection.lastInvocationId,
target: "testMethod",
nonblocking: true,
arguments: [
"arg",
42
@ -68,7 +66,6 @@ describe("HubConnection", () => {
type: MessageType.Invocation,
invocationId: connection.lastInvocationId,
target: "testMethod",
nonblocking: false,
arguments: [
"arg",
42
@ -545,4 +542,4 @@ class TestObserver implements Observer<any>
complete() {
this.itemsSource.resolve(this.itemsReceived);
}
};
};

View File

@ -5,12 +5,23 @@ import { MessagePackHubProtocol } from "../Microsoft.AspNetCore.SignalR.Client.T
import { MessageType, InvocationMessage, CompletionMessage, ResultMessage } from "../Microsoft.AspNetCore.SignalR.Client.TS/IHubProtocol"
describe("MessageHubProtocol", () => {
it("can write/read non-blocking Invocation message", () => {
let invocation = <InvocationMessage>{
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", () => {
let invocation = <InvocationMessage>{
type: MessageType.Invocation,
invocationId: "123",
target: "myMethod",
nonblocking: true,
arguments: [42, true, "test", ["x1", "y2"], null]
};
@ -111,4 +122,4 @@ describe("MessageHubProtocol", () => {
}
])
})
});
});

View File

@ -6,7 +6,7 @@ import { IConnection } from "./IConnection"
import { HttpConnection } from "./HttpConnection"
import { TransportType, TransferMode } from "./Transports"
import { Subject, Observable } from "./Observable"
import { IHubProtocol, ProtocolType, MessageType, HubMessage, CompletionMessage, ResultMessage, InvocationMessage, StreamInvocationMessage, NegotiationMessage, HubInvocationMessage } from "./IHubProtocol";
import { IHubProtocol, ProtocolType, MessageType, HubMessage, CompletionMessage, ResultMessage, InvocationMessage, StreamInvocationMessage, NegotiationMessage } from "./IHubProtocol";
import { JsonHubProtocol } from "./JsonHubProtocol";
import { TextMessageFormat } from "./Formatters"
import { Base64EncodedHubProtocol } from "./Base64EncodedHubProtocol"
@ -75,10 +75,10 @@ export class HubConnection {
break;
case MessageType.StreamItem:
case MessageType.Completion:
let callback = this.callbacks.get((<HubInvocationMessage>message).invocationId);
let callback = this.callbacks.get((<any>message).invocationId);
if (callback != null) {
if (message.type === MessageType.Completion) {
this.callbacks.delete((<HubInvocationMessage>message).invocationId);
this.callbacks.delete((<any>message).invocationId);
}
callback(message);
}
@ -112,8 +112,11 @@ export class HubConnection {
let methods = this.methods.get(invocationMessage.target.toLowerCase());
if (methods) {
methods.forEach(m => m.apply(this, invocationMessage.arguments));
if (!invocationMessage.nonblocking) {
// TODO: send result back to the server?
if (invocationMessage.invocationId) {
// This is not supported in v1. So we return an error to avoid blocking the server waiting for the response.
let message = "Server requested a response, which is not supported in this version of the client."
this.logger.log(LogLevel.Error, message);
this.connection.stop(new Error(message))
}
}
else {
@ -275,16 +278,24 @@ export class HubConnection {
}
private createInvocation(methodName: string, args: any[], nonblocking: boolean): InvocationMessage {
let id = this.id;
this.id++;
if (nonblocking) {
return <InvocationMessage>{
type: MessageType.Invocation,
target: methodName,
arguments: args,
};
}
else {
let id = this.id;
this.id++;
return <InvocationMessage>{
type: MessageType.Invocation,
invocationId: id.toString(),
target: methodName,
arguments: args,
nonblocking: nonblocking
};
return <InvocationMessage>{
type: MessageType.Invocation,
invocationId: id.toString(),
target: methodName,
arguments: args,
};
}
}
private createStreamInvocation(methodName: string, args: any[]): StreamInvocationMessage {

View File

@ -14,26 +14,25 @@ export interface HubMessage {
readonly type: MessageType;
}
export interface HubInvocationMessage extends HubMessage {
readonly invocationId: string;
}
export interface InvocationMessage extends HubInvocationMessage {
export interface InvocationMessage extends HubMessage {
readonly invocationId?: string;
readonly target: string;
readonly arguments: Array<any>;
readonly nonblocking?: boolean;
}
export interface StreamInvocationMessage extends HubInvocationMessage {
export interface StreamInvocationMessage extends HubMessage {
readonly invocationId: string;
readonly target: string;
readonly arguments: Array<any>
}
export interface ResultMessage extends HubInvocationMessage {
export interface ResultMessage extends HubMessage {
readonly invocationId: string;
readonly item?: any;
}
export interface CompletionMessage extends HubInvocationMessage {
export interface CompletionMessage extends HubMessage {
readonly invocationId: string;
readonly error?: string;
readonly result?: any;
}
@ -52,4 +51,4 @@ export interface IHubProtocol {
readonly type: ProtocolType;
parseMessages(input: any): HubMessage[];
writeMessage(message: HubMessage): any;
}
}

View File

@ -52,17 +52,27 @@ export class MessagePackHubProtocol implements IHubProtocol {
}
private createInvocationMessage(properties: any[]): InvocationMessage {
if (properties.length != 5) {
if (properties.length != 4) {
throw new Error("Invalid payload for Invocation message.");
}
return {
type: MessageType.Invocation,
invocationId: properties[1],
nonblocking: properties[2],
target: properties[3],
arguments: properties[4]
} as InvocationMessage;
let invocationId = properties[1];
if (invocationId) {
return {
type: MessageType.Invocation,
invocationId: invocationId,
target: properties[2],
arguments: properties[3]
} as InvocationMessage;
}
else {
return {
type: MessageType.Invocation,
target: properties[2],
arguments: properties[3]
} as InvocationMessage;
}
}
private createStreamItemMessage(properties: any[]): ResultMessage {
@ -128,8 +138,8 @@ export class MessagePackHubProtocol implements IHubProtocol {
private writeInvocation(invocationMessage: InvocationMessage): ArrayBuffer {
let msgpack = msgpack5();
let payload = msgpack.encode([MessageType.Invocation, invocationMessage.invocationId,
invocationMessage.nonblocking, invocationMessage.target, invocationMessage.arguments]);
let payload = msgpack.encode([MessageType.Invocation, invocationMessage.invocationId || null,
invocationMessage.target, invocationMessage.arguments]);
return BinaryMessageFormat.write(payload.slice());
}
@ -141,4 +151,4 @@ export class MessagePackHubProtocol implements IHubProtocol {
return BinaryMessageFormat.write(payload.slice());
}
}
}

View File

@ -36,6 +36,32 @@ describe('hubConnection', function () {
});
});
it('can invoke server method non-blocking and not receive result', function (done) {
var message = '你好,世界!';
var options = {
transport: transportType,
protocol: protocol,
logging: signalR.LogLevel.Trace
};
var hubConnection = new signalR.HubConnection(TESTHUBENDPOINT_URL, options);
hubConnection.onclose(function (error) {
expect(error).toBe(undefined);
done();
});
hubConnection.start().then(function () {
hubConnection.send('Echo', message).catch(function (e) {
fail(e);
}).then(function () {
hubConnection.stop();
});
}).catch(function (e) {
fail(e);
done();
});
});
it('can invoke server method structural object and receive structural result', function (done) {
var options = {
transport: transportType,
@ -64,7 +90,6 @@ describe('hubConnection', function () {
});
it('can stream server method and receive result', function (done) {
var options = {
transport: transportType,
protocol: protocol,

View File

@ -28,16 +28,6 @@
"integrity": "sha512-zT+t9841g1HsjLtPMCYxmb1U4pcZ2TOegAKiomlmj6bIziuaEYHUavxLE9NRwdntY0vOCrgHho6OXjDX7fm/Kw==",
"dev": true
},
"JSONStream": {
"version": "1.3.1",
"resolved": "https://registry.npmjs.org/JSONStream/-/JSONStream-1.3.1.tgz",
"integrity": "sha1-cH92HgHa6eFvG8+TcDt4xwlmV5o=",
"dev": true,
"requires": {
"jsonparse": "1.3.1",
"through": "2.3.8"
}
},
"acorn": {
"version": "4.0.13",
"resolved": "https://registry.npmjs.org/acorn/-/acorn-4.0.13.tgz",
@ -1027,9 +1017,9 @@
"integrity": "sha1-+GzWzvT1MAyOY+B6TVEvZfv/RTE=",
"dev": true,
"requires": {
"JSONStream": "1.3.1",
"combine-source-map": "0.7.2",
"defined": "1.0.0",
"JSONStream": "1.3.1",
"through2": "2.0.3",
"umd": "3.0.1"
}
@ -1057,7 +1047,6 @@
"integrity": "sha1-tanJAgJD8McORnW+yCI7xifkFc4=",
"dev": true,
"requires": {
"JSONStream": "1.3.1",
"assert": "1.4.1",
"browser-pack": "6.0.2",
"browser-resolve": "1.11.2",
@ -1079,6 +1068,7 @@
"https-browserify": "0.0.1",
"inherits": "2.0.3",
"insert-module-globals": "7.0.1",
"JSONStream": "1.3.1",
"labeled-stream-splicer": "2.0.0",
"module-deps": "4.1.1",
"os-browserify": "0.1.2",
@ -2497,10 +2487,10 @@
"integrity": "sha1-wDv04BywhtW15azorQr+eInWOMM=",
"dev": true,
"requires": {
"JSONStream": "1.3.1",
"combine-source-map": "0.7.2",
"concat-stream": "1.5.2",
"is-buffer": "1.1.5",
"JSONStream": "1.3.1",
"lexical-scope": "1.2.0",
"process": "0.11.10",
"through2": "2.0.3",
@ -2773,6 +2763,16 @@
"integrity": "sha1-P02uSpH6wxX3EGL4UhzCOfE2YoA=",
"dev": true
},
"JSONStream": {
"version": "1.3.1",
"resolved": "https://registry.npmjs.org/JSONStream/-/JSONStream-1.3.1.tgz",
"integrity": "sha1-cH92HgHa6eFvG8+TcDt4xwlmV5o=",
"dev": true,
"requires": {
"jsonparse": "1.3.1",
"through": "2.3.8"
}
},
"kind-of": {
"version": "3.2.2",
"resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz",
@ -3126,7 +3126,6 @@
"integrity": "sha1-IyFYM/HaE/1gbMuAh7RIUty4If0=",
"dev": true,
"requires": {
"JSONStream": "1.3.1",
"browser-resolve": "1.11.2",
"cached-path-relative": "1.0.1",
"concat-stream": "1.5.2",
@ -3134,6 +3133,7 @@
"detective": "4.5.0",
"duplexer2": "0.1.4",
"inherits": "2.0.3",
"JSONStream": "1.3.1",
"parents": "1.0.1",
"readable-stream": "2.2.11",
"resolve": "1.3.3",

View File

@ -69,7 +69,7 @@ The `Target` of an `Invocation` message must refer to a specific method, overloa
## Non-Blocking Invocations
Invocations can be marked as "Non-Blocking" in the `Invocation` message, which indicates to the Callee that the Caller expects no results. When a Callee receives a "Non-Blocking" Invocation, it should dispatch the message, but send no results or errors back to the Caller. In a Caller application, the invocation will immediately return with no results. There is no tracking of completion for 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.
## Streaming
@ -244,7 +244,7 @@ S->C: Completion { Id = 42 } // This can be ignored
### Non-Blocking Call (`NonBlocking` example above)
```
C->S: Invocation { Id = 42, Target = "NonBlocking", Arguments = [ "foo" ], NonBlocking = true }
C->S: Invocation { Target = "NonBlocking", Arguments = [ "foo" ] }
```
### Ping
@ -262,8 +262,7 @@ In the JSON Encoding of the SignalR Protocol, each Message is represented as a s
An `Invocation` message is a JSON object with the following properties:
* `type` - A `Number` with the literal value 1, indicating that this message is an Invocation.
* `invocationId` - A `String` encoding the `Invocation ID` for a message.
* `nonblocking` - A `Boolean` indicating if the invocation is Non-Blocking (see "Non-Blocking Invocations" above). Optional and defaults to `false` if not present.
* `invocationId` - An optional `String` encoding the `Invocation ID` for a 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. This is a sequence of JSON `Token`s, encoded as indicated below in the "JSON Payload Encoding" section
@ -285,8 +284,6 @@ Example (Non-Blocking):
```json
{
"type": 1,
"invocationId": "123",
"nonblocking": true,
"target": "Send",
"arguments": [
42,
@ -434,28 +431,50 @@ MessagePack uses different formats to encode values. Refer to the [MsgPack forma
```
* `1` - Message Type - `1` indicates this is an `Invocation` message.
* InvocationId - A `String` encoding the Invocation ID for the message.
* NonBlocking - A `Boolean` indicating if the invocation is Non-Blocking (see "Non-Blocking Invocations" above).
* InvocationId - One of:
* A `Nil`, indicating that there is no Invocation ID, OR
* 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.
Example:
#### Example:
The following payload
```
0x95 0x01 0xa3 0x78 0x79 0x7a 0xc3 0xa6 0x6d 0x65 0x74 0x68 0x6f 0x64 0x91 0x2a
0x94 0x01 0xa3 0x78 0x79 0x7a 0xa6 0x6d 0x65 0x74 0x68 0x6f 0x64 0x91 0x2a
```
is decoded as follows:
* `0x95` - 5-element array
* `0x94` - 4-element array
* `0x01` - `1` (Message Type - `Invocation` message)
* `0xa3` - string of length 3 (InvocationId)
* `0x78` - `x`
* `0x79` - `y`
* `0x7a` - `z`
* `0xc3` - `true` (NonBlocking)
* `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)
#### Non-Blocking Example:
The following payload
```
0x94 0x01 0xc0 0xa6 0x6d 0x65 0x74 0x68 0x6f 0x64 0x91 0x2a
```
is decoded as follows:
* `0x94` - 4-element array
* `0x01` - `1` (Message Type - `Invocation` message)
* `0xc0` - `nil` (Invocation ID)
* `0xa6` - string of length 6 (Target)
* `0x6d` - `m`
* `0x65` - `e`
@ -661,101 +680,24 @@ is decoded as follows:
* `0x91` - 1-element array
* `0x06` - `6` (Message Type - `Ping` message)
## Protocol Buffers (ProtoBuf) Encoding
**Protobuf encoding is currently not implemented**
In order to support ProtoBuf, an application must provide a [ProtoBuf service definition](https://developers.google.com/protocol-buffers/docs/proto3) for the Hub. However, implementations may automatically generate these definitions from reflection information, if the underlying platform supports this. For example, the .NET implementation will attempt to generate service definitions for methods that use only simple primitive and enumerated types. The service definition provides a description of how to encode the arguments and return value for the call. For example, consider the following C# method:
```csharp
public bool SendMessageToUser(string userName, string message) {}
```
In order to invoke this method, the application must provide a ProtoBuf schema representing the input and output values and defining the message:
```protobuf
syntax = "proto3";
message SendMessageToUserRequest {
string userName = 1;
string message = 2;
}
message SendMessageToUserResponse {
bool result = 1;
}
service ChatService {
rpc SendMessageToUser (SendMessageToUserRequest) returns (SendMessageToUserResponse);
}
```
**NOTE**: the .NET implementation will provide a way to automatically generate these definitions at runtime, to avoid needing to generate them in advance, but applications still have the option of doing so. A general guideline for mapping .NET types to ProtoBuf types is listed in the "Type Mapping" section at the end of this document. In the current plan, custom .NET classes/structs not already listed in the table below will require a complete ProtoBuf mapping to be provided by the application.
## SignalR.proto
SignalR provides an outer ProtoBuf schema for encoding the RPC invocation process as a whole, which is defined by the .proto file below. A SignalR frame is encoded as a single message of type `SignalRFrame`, then transmitted using the underlying transport. Since the underlying transport provides the necessary framing, we can reliably decode a message without having to know the length or format of the arguments.
```protobuf
syntax = "proto3";
message Invocation {
string target = 1;
bool nonblocking = 2;
bytes arguments = 3;
}
message StreamItem {
bytes item = 1;
}
message Completion {
oneof payload {
bytes result = 1;
string error = 2;
}
}
message SignalRFrame {
string invocationId = 1;
oneof message {
Invocation invocation = 2;
StreamItem streamItem = 3;
Completion completion = 4;
}
}
```
## Invocation Message
When an invocation is issued by the Caller, we generate the necessary Request message according to the service definition, encode it into the ProtoBuf wire format, and then transmit an `Invocation` ProtoBuf message with that encoded argument data as the `arguments` field. The resulting `Invocation` message is wrapped in a `SignalRFrame` message and the `invocationId` is set. The final message is then encoded in the ProtoBuf format and transmitted to the Callee.
## StreamItem Message
When a result is emitted by the Callee, it is encoded using the ProtoBuf schema associated with the service and encoded into the `item` field of a `StreamItem` ProtoBuf message. If an error is emitted, the message is encoded into the error field of a StreamItem ProtoBuf message. The resulting `StreamItem` message is wrapped in a `SignalRFrame` message and the `invocationId` is set. The final message is then encoded in the ProtoBuf format and transmitted to the Callee.
## Completion Message
When a request completes, a `Completion` ProtoBuf message is constructed. If there is a final payload, it is encoded the same way as in the `StreamItem` message and stored in the `result` field of the message. If there is an error, it is encoded in the `error` field of the message. The resulting `Completion` message is wrapped in a `SignalRFrame` message and the `invocationId` is set. The final message is then encoded in the ProtoBuf format and transmitted to the Callee.
## 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
| .NET Type | JSON Type | MsgPack format family | ProtoBuf Type |
| ----------------------------------------------- | ---------------------------- |---------------------------|------------------------|
| `System.Byte`, `System.UInt16`, `System.UInt32` | `Number` | `positive fixint`, `uint` | `uint32` |
| `System.SByte`, `System.Int16`, `System.Int32` | `Number` | `fixit`, `int` | `int32` |
| `System.UInt64` | `Number` | `positive fixint`, `uint` | `uint64` |
| `System.Int64` | `Number` | `fixint`, `int` | `int64` |
| `System.Single` | `Number` | `float` | `float` |
| `System.Double` | `Number` | `float` | `double` |
| `System.Boolean` | `true` or `false` | `true`, `false` | `bool` |
| `System.String` | `String` | `fixstr`, `str` | `string` |
| `System.Byte`[] | `String` (Base64-encoded) | `bin` | `bytes` |
| `IEnumerable<T>` | `Array` | `bin` | `repeated` |
| custom `enum` | `Number` | `fixint`, `int` | `uint64` |
| custom `struct` or `class` | `Object` | `fixmap`, `map` | Requires an explicit .proto file definition |
| .NET Type | JSON Type | MsgPack format family |
| ----------------------------------------------- | ---------------------------- |---------------------------|
| `System.Byte`, `System.UInt16`, `System.UInt32` | `Number` | `positive fixint`, `uint` |
| `System.SByte`, `System.Int16`, `System.Int32` | `Number` | `fixit`, `int` |
| `System.UInt64` | `Number` | `positive fixint`, `uint` |
| `System.Int64` | `Number` | `fixint`, `int` |
| `System.Single` | `Number` | `float` |
| `System.Double` | `Number` | `float` |
| `System.Boolean` | `true` or `false` | `true`, `false` |
| `System.String` | `String` | `fixstr`, `str` |
| `System.Byte`[] | `String` (Base64-encoded) | `bin` |
| `IEnumerable<T>` | `Array` | `bin` |
| custom `enum` | `Number` | `fixint`, `int` |
| custom `struct` or `class` | `Object` | `fixmap`, `map` |
MessagePack payloads are wrapped in an outer message framing described below.

View File

@ -243,7 +243,7 @@ namespace Microsoft.AspNetCore.SignalR.Client
_logger.PreparingBlockingInvocation(irq.InvocationId, methodName, irq.ResultType.FullName, args.Length);
// Client invocations are always blocking
var invocationMessage = new InvocationMessage(irq.InvocationId, nonBlocking: false, target: methodName,
var invocationMessage = new InvocationMessage(irq.InvocationId, target: methodName,
argumentBindingException: null, arguments: args);
_logger.RegisterInvocation(invocationMessage.InvocationId);
@ -306,7 +306,7 @@ namespace Microsoft.AspNetCore.SignalR.Client
throw new InvalidOperationException($"The '{nameof(SendAsync)}' method cannot be called before the connection has been started.");
}
var invocationMessage = new InvocationMessage(GetNextId(), nonBlocking: true, target: methodName,
var invocationMessage = new InvocationMessage(null, target: methodName,
argumentBindingException: null, arguments: args);
ThrowIfConnectionTerminated(invocationMessage.InvocationId);

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;
namespace Microsoft.AspNetCore.SignalR.Internal.Protocol
{
public abstract class HubInvocationMessage : HubMessage

View File

@ -4,6 +4,7 @@
using System;
using System.Linq;
using System.Runtime.ExceptionServices;
using Newtonsoft.Json;
namespace Microsoft.AspNetCore.SignalR.Internal.Protocol
{
@ -35,16 +36,9 @@ namespace Microsoft.AspNetCore.SignalR.Internal.Protocol
}
}
public bool NonBlocking { get; protected set; }
public HubMethodInvocationMessage(string invocationId, string target, ExceptionDispatchInfo argumentBindingException, object[] arguments)
protected HubMethodInvocationMessage(string invocationId, string target, ExceptionDispatchInfo argumentBindingException, object[] arguments)
: base(invocationId)
{
if (string.IsNullOrEmpty(invocationId))
{
throw new ArgumentNullException(nameof(invocationId));
}
if (string.IsNullOrEmpty(target))
{
throw new ArgumentNullException(nameof(target));
@ -63,15 +57,19 @@ namespace Microsoft.AspNetCore.SignalR.Internal.Protocol
public class InvocationMessage : HubMethodInvocationMessage
{
public InvocationMessage(string invocationId, bool nonBlocking, string target, ExceptionDispatchInfo argumentBindingException, params object[] arguments)
public InvocationMessage(string target, ExceptionDispatchInfo argumentBindingException, params object[] arguments)
: this(invocationId: null, target, argumentBindingException, arguments)
{
}
public InvocationMessage(string invocationId, string target, ExceptionDispatchInfo argumentBindingException, params object[] arguments)
: base(invocationId, target, argumentBindingException, arguments)
{
NonBlocking = nonBlocking;
}
public override string ToString()
{
return $"InvocationMessage {{ {nameof(InvocationId)}: \"{InvocationId}\", {nameof(NonBlocking)}: {NonBlocking}, {nameof(Target)}: \"{Target}\", {nameof(Arguments)}: [ {string.Join(", ", Arguments?.Select(a => a?.ToString())) ?? string.Empty } ] }}";
return $"InvocationMessage {{ {nameof(InvocationId)}: \"{InvocationId}\", {nameof(Target)}: \"{Target}\", {nameof(Arguments)}: [ {string.Join(", ", Arguments?.Select(a => a?.ToString())) ?? string.Empty } ] }}";
}
}
@ -79,7 +77,12 @@ namespace Microsoft.AspNetCore.SignalR.Internal.Protocol
{
public StreamInvocationMessage(string invocationId, string target, ExceptionDispatchInfo argumentBindingException, params object[] arguments)
: base(invocationId, target, argumentBindingException, arguments)
{ }
{
if (string.IsNullOrEmpty(invocationId))
{
throw new ArgumentNullException(nameof(invocationId));
}
}
public override string ToString()
{

View File

@ -20,7 +20,6 @@ namespace Microsoft.AspNetCore.SignalR.Internal.Protocol
private const string TypePropertyName = "type";
private const string ErrorPropertyName = "error";
private const string TargetPropertyName = "target";
private const string NonBlockingPropertyName = "nonBlocking";
private const string ArgumentsPropertyName = "arguments";
private const string PayloadPropertyName = "payload";
@ -196,12 +195,6 @@ namespace Microsoft.AspNetCore.SignalR.Internal.Protocol
writer.WritePropertyName(TargetPropertyName);
writer.WriteValue(message.Target);
if (message.NonBlocking)
{
writer.WritePropertyName(NonBlockingPropertyName);
writer.WriteValue(message.NonBlocking);
}
WriteArguments(message.Arguments, writer);
writer.WriteEndObject();
@ -239,8 +232,11 @@ namespace Microsoft.AspNetCore.SignalR.Internal.Protocol
private static void WriteHubInvocationMessageCommon(HubInvocationMessage message, JsonTextWriter writer, int type)
{
writer.WritePropertyName(InvocationIdPropertyName);
writer.WriteValue(message.InvocationId);
if (!string.IsNullOrEmpty(message.InvocationId))
{
writer.WritePropertyName(InvocationIdPropertyName);
writer.WriteValue(message.InvocationId);
}
WriteMessageType(writer, type);
}
@ -252,9 +248,8 @@ namespace Microsoft.AspNetCore.SignalR.Internal.Protocol
private InvocationMessage BindInvocationMessage(JObject json, IInvocationBinder binder)
{
var invocationId = JsonUtils.GetRequiredProperty<string>(json, InvocationIdPropertyName, JTokenType.String);
var invocationId = JsonUtils.GetOptionalProperty<string>(json, InvocationIdPropertyName, JTokenType.String);
var target = JsonUtils.GetRequiredProperty<string>(json, TargetPropertyName, JTokenType.String);
var nonBlocking = JsonUtils.GetOptionalProperty<bool>(json, NonBlockingPropertyName, JTokenType.Boolean);
var args = JsonUtils.GetRequiredProperty<JArray>(json, ArgumentsPropertyName, JTokenType.Array);
@ -263,11 +258,11 @@ namespace Microsoft.AspNetCore.SignalR.Internal.Protocol
try
{
var arguments = BindArguments(args, paramTypes);
return new InvocationMessage(invocationId, nonBlocking, target, argumentBindingException: null, arguments: arguments);
return new InvocationMessage(invocationId, target, argumentBindingException: null, arguments: arguments);
}
catch (Exception ex)
{
return new InvocationMessage(invocationId, nonBlocking, target, ExceptionDispatchInfo.Capture(ex));
return new InvocationMessage(invocationId, target, ExceptionDispatchInfo.Capture(ex));
}
}

View File

@ -77,18 +77,25 @@ namespace Microsoft.AspNetCore.SignalR.Internal.Protocol
private static InvocationMessage CreateInvocationMessage(Unpacker unpacker, IInvocationBinder binder)
{
var invocationId = ReadInvocationId(unpacker);
var nonBlocking = ReadBoolean(unpacker, "nonBlocking");
// For MsgPack, we represent an empty invocation ID as an empty string,
// so we need to normalize that to "null", which is what indicates a non-blocking invocation.
if (string.IsNullOrEmpty(invocationId))
{
invocationId = null;
}
var target = ReadString(unpacker, "target");
var parameterTypes = binder.GetParameterTypes(target);
try
{
var arguments = BindArguments(unpacker, parameterTypes);
return new InvocationMessage(invocationId, nonBlocking, target, argumentBindingException: null, arguments: arguments);
return new InvocationMessage(invocationId, target, argumentBindingException: null, arguments: arguments);
}
catch (Exception ex)
{
return new InvocationMessage(invocationId, nonBlocking, target, ExceptionDispatchInfo.Capture(ex));
return new InvocationMessage(invocationId, target, ExceptionDispatchInfo.Capture(ex));
}
}
@ -218,10 +225,16 @@ namespace Microsoft.AspNetCore.SignalR.Internal.Protocol
private void WriteInvocationMessage(InvocationMessage invocationMessage, Packer packer)
{
packer.PackArrayHeader(5);
packer.PackArrayHeader(4);
packer.Pack(HubProtocolConstants.InvocationMessageType);
packer.PackString(invocationMessage.InvocationId);
packer.Pack(invocationMessage.NonBlocking);
if (string.IsNullOrEmpty(invocationMessage.InvocationId))
{
packer.PackNull();
}
else
{
packer.PackString(invocationMessage.InvocationId);
}
packer.PackString(invocationMessage.Target);
packer.PackObject(invocationMessage.Arguments, _serializationContext);
}
@ -307,9 +320,16 @@ namespace Microsoft.AspNetCore.SignalR.Internal.Protocol
Exception msgPackException = null;
try
{
if (unpacker.ReadString(out var value))
if (unpacker.Read())
{
return value;
if (unpacker.LastReadData.IsNil)
{
return null;
}
else
{
return unpacker.LastReadData.AsString();
}
}
}
catch (Exception e)

View File

@ -4,7 +4,6 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.AspNetCore.SignalR.Internal.Protocol;
@ -12,7 +11,6 @@ namespace Microsoft.AspNetCore.SignalR
{
public class DefaultHubLifetimeManager<THub> : HubLifetimeManager<THub>
{
private long _nextInvocationId = 0;
private readonly HubConnectionList _connections = new HubConnectionList();
private readonly HubGroupList _groups = new HubGroupList();
@ -150,7 +148,7 @@ namespace Microsoft.AspNetCore.SignalR
private InvocationMessage CreateInvocationMessage(string methodName, object[] args)
{
return new InvocationMessage(GetInvocationId(), nonBlocking: true, target: methodName, argumentBindingException: null, arguments: args);
return new InvocationMessage(target: methodName, argumentBindingException: null, arguments: args);
}
public override Task InvokeUserAsync(string userId, string methodName, object[] args)
@ -172,12 +170,6 @@ namespace Microsoft.AspNetCore.SignalR
return Task.CompletedTask;
}
private string GetInvocationId()
{
var invocationId = Interlocked.Increment(ref _nextInvocationId);
return invocationId.ToString();
}
public override Task InvokeAllExceptAsync(string methodName, object[] args, IReadOnlyList<string> excludedIds)
{
return InvokeAllWhere(methodName, args, connection =>

View File

@ -310,7 +310,8 @@ namespace Microsoft.AspNetCore.SignalR
_logger.StreamingResult(hubMethodInvocationMessage.InvocationId, methodExecutor.MethodReturnType.FullName);
await StreamResultsAsync(hubMethodInvocationMessage.InvocationId, connection, enumerator);
}
else if (!hubMethodInvocationMessage.NonBlocking)
// Non-empty/null InvocationId ==> Blocking invocation that needs a response
else if (!string.IsNullOrEmpty(hubMethodInvocationMessage.InvocationId))
{
_logger.SendingResult(hubMethodInvocationMessage.InvocationId, methodExecutor.MethodReturnType.FullName);
await SendMessageAsync(connection, CompletionMessage.WithResult(hubMethodInvocationMessage.InvocationId, result));
@ -358,7 +359,7 @@ namespace Microsoft.AspNetCore.SignalR
private async Task SendInvocationError(HubMethodInvocationMessage hubMethodInvocationMessage,
HubConnectionContext connection, string errorMessage)
{
if (hubMethodInvocationMessage.NonBlocking)
if (string.IsNullOrEmpty(hubMethodInvocationMessage.InvocationId))
{
return;
}
@ -426,7 +427,8 @@ namespace Microsoft.AspNetCore.SignalR
var isStreamedResult = IsStreamed(resultType);
if (isStreamedResult && !isStreamedInvocation)
{
if (!hubMethodInvocationMessage.NonBlocking)
// Non-null/empty InvocationId? Blocking
if (!string.IsNullOrEmpty(hubMethodInvocationMessage.InvocationId))
{
_logger.StreamingMethodCalledWithInvoke(hubMethodInvocationMessage);
await SendMessageAsync(connection, CompletionMessage.WithError(hubMethodInvocationMessage.InvocationId,

View File

@ -35,14 +35,10 @@ namespace Microsoft.AspNetCore.SignalR.Redis
// This serializer is ONLY use to transmit the data through redis, it has no connection to the serializer used on each connection.
private readonly JsonSerializer _serializer = new JsonSerializer
{
// We need to serialize objects "full-fidelity", even if it is noisy, so we preserve the original types
TypeNameAssemblyFormatHandling = TypeNameAssemblyFormatHandling.Simple,
TypeNameHandling = TypeNameHandling.All,
Formatting = Formatting.None
TypeNameHandling = TypeNameHandling.None,
Formatting = Formatting.None,
};
private long _nextInvocationId = 0;
public RedisHubLifetimeManager(ILogger<RedisHubLifetimeManager<THub>> logger,
IOptions<RedisOptions> options)
{
@ -152,14 +148,14 @@ namespace Microsoft.AspNetCore.SignalR.Redis
public override Task InvokeAllAsync(string methodName, object[] args)
{
var message = new InvocationMessage(GetInvocationId(), nonBlocking: true, target: methodName, argumentBindingException: null, arguments: args);
var message = new RedisInvocationMessage(target: methodName, arguments: args);
return PublishAsync(_channelNamePrefix, message);
}
public override Task InvokeAllExceptAsync(string methodName, object[] args, IReadOnlyList<string> excludedIds)
{
var message = new RedisExcludeClientsMessage(GetInvocationId(), nonBlocking: true, target: methodName, excludedIds: excludedIds, arguments: args);
var message = new RedisInvocationMessage(target: methodName, excludedIds: excludedIds, arguments: args);
return PublishAsync(_channelNamePrefix + ".AllExcept", message);
}
@ -170,14 +166,14 @@ namespace Microsoft.AspNetCore.SignalR.Redis
throw new ArgumentNullException(nameof(connectionId));
}
var message = new InvocationMessage(GetInvocationId(), nonBlocking: true, target: methodName, argumentBindingException: null, arguments: args);
var message = new RedisInvocationMessage(target: methodName, arguments: args);
// 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!
var connection = _connections[connectionId];
if (connection != null)
{
return connection.WriteAsync(message);
return connection.WriteAsync(message.CreateInvocation());
}
return PublishAsync(_channelNamePrefix + "." + connectionId, message);
@ -190,7 +186,7 @@ namespace Microsoft.AspNetCore.SignalR.Redis
throw new ArgumentNullException(nameof(groupName));
}
var message = new RedisExcludeClientsMessage(GetInvocationId(), nonBlocking: true, target: methodName, excludedIds: null, arguments: args);
var message = new RedisInvocationMessage(target: methodName, excludedIds: null, arguments: args);
return PublishAsync(_channelNamePrefix + ".group." + groupName, message);
}
@ -202,25 +198,25 @@ namespace Microsoft.AspNetCore.SignalR.Redis
throw new ArgumentNullException(nameof(groupName));
}
var message = new RedisExcludeClientsMessage(GetInvocationId(), nonBlocking: true, target: methodName, excludedIds: excludedIds, arguments: args);
var message = new RedisInvocationMessage(methodName, excludedIds, args);
return PublishAsync(_channelNamePrefix + ".group." + groupName, message);
}
public override Task InvokeUserAsync(string userId, string methodName, object[] args)
{
var message = new InvocationMessage(GetInvocationId(), nonBlocking: true, target: methodName, argumentBindingException: null, arguments: args);
var message = new RedisInvocationMessage(methodName, args);
return PublishAsync(_channelNamePrefix + ".user." + userId, message);
}
private async Task PublishAsync<TMessage>(string channel, TMessage hubMessage)
private async Task PublishAsync(string channel, IRedisMessage message)
{
byte[] payload;
using (var stream = new MemoryStream())
using (var writer = new JsonTextWriter(new StreamWriter(stream)))
{
_serializer.Serialize(writer, hubMessage);
_serializer.Serialize(writer, message);
await writer.FlushAsync();
payload = stream.ToArray();
}
@ -363,7 +359,7 @@ namespace Microsoft.AspNetCore.SignalR.Redis
var id = Interlocked.Increment(ref _internalId);
var ack = _ackHandler.CreateAck(id);
// Send Add/Remove Group to other servers and wait for an ack or timeout
await PublishAsync(_channelNamePrefix + ".internal.group", new GroupMessage
await PublishAsync(_channelNamePrefix + ".internal.group", new RedisGroupMessage
{
Action = action,
ConnectionId = connectionId,
@ -382,12 +378,6 @@ namespace Microsoft.AspNetCore.SignalR.Redis
_ackHandler.Dispose();
}
private string GetInvocationId()
{
var invocationId = Interlocked.Increment(ref _nextInvocationId);
return invocationId.ToString();
}
private T DeserializeMessage<T>(RedisValue data)
{
using (var reader = new JsonTextReader(new StreamReader(new MemoryStream(data))))
@ -405,13 +395,14 @@ namespace Microsoft.AspNetCore.SignalR.Redis
{
_logger.ReceivedFromChannel(_channelNamePrefix);
var message = DeserializeMessage<HubInvocationMessage>(data);
var message = DeserializeMessage<RedisInvocationMessage>(data);
var tasks = new List<Task>(_connections.Count);
var invocation = message.CreateInvocation();
foreach (var connection in _connections)
{
tasks.Add(connection.WriteAsync(message));
tasks.Add(connection.WriteAsync(invocation));
}
await Task.WhenAll(tasks);
@ -433,16 +424,17 @@ namespace Microsoft.AspNetCore.SignalR.Redis
{
_logger.ReceivedFromChannel(channelName);
var message = DeserializeMessage<RedisExcludeClientsMessage>(data);
var excludedIds = message.ExcludedIds;
var message = DeserializeMessage<RedisInvocationMessage>(data);
var excludedIds = message.ExcludedIds ?? Array.Empty<string>();
var tasks = new List<Task>(_connections.Count);
var invocation = message.CreateInvocation();
foreach (var connection in _connections)
{
if (!excludedIds.Contains(connection.ConnectionId))
{
tasks.Add(connection.WriteAsync(message));
tasks.Add(connection.WriteAsync(invocation));
}
}
@ -462,7 +454,7 @@ namespace Microsoft.AspNetCore.SignalR.Redis
{
try
{
var groupMessage = DeserializeMessage<GroupMessage>(data);
var groupMessage = DeserializeMessage<RedisGroupMessage>(data);
var connection = _connections[groupMessage.ConnectionId];
if (connection == null)
@ -482,7 +474,7 @@ namespace Microsoft.AspNetCore.SignalR.Redis
}
// Sending ack to server that sent the original add/remove
await PublishAsync($"{_channelNamePrefix}.internal.{groupMessage.Server}", new GroupMessage
await PublishAsync($"{_channelNamePrefix}.internal.{groupMessage.Server}", new RedisGroupMessage
{
Action = GroupAction.Ack,
Id = groupMessage.Id
@ -501,7 +493,7 @@ namespace Microsoft.AspNetCore.SignalR.Redis
var serverChannel = $"{_channelNamePrefix}.internal.{_serverName}";
_bus.Subscribe(serverChannel, (c, data) =>
{
var groupMessage = DeserializeMessage<GroupMessage>(data);
var groupMessage = DeserializeMessage<RedisGroupMessage>(data);
if (groupMessage.Action == GroupAction.Ack)
{
@ -520,9 +512,9 @@ namespace Microsoft.AspNetCore.SignalR.Redis
{
try
{
var message = DeserializeMessage<HubInvocationMessage>(data);
var message = DeserializeMessage<RedisInvocationMessage>(data);
await connection.WriteAsync(message);
await connection.WriteAsync(message.CreateInvocation());
}
catch (Exception ex)
{
@ -559,9 +551,10 @@ namespace Microsoft.AspNetCore.SignalR.Redis
{
try
{
var message = DeserializeMessage<RedisExcludeClientsMessage>(data);
var message = DeserializeMessage<RedisInvocationMessage>(data);
var tasks = new List<Task>();
var invocation = message.CreateInvocation();
foreach (var groupConnection in group.Connections)
{
if (message.ExcludedIds?.Contains(groupConnection.ConnectionId) == true)
@ -569,7 +562,7 @@ namespace Microsoft.AspNetCore.SignalR.Redis
continue;
}
tasks.Add(groupConnection.WriteAsync(message));
tasks.Add(groupConnection.WriteAsync(invocation));
}
await Task.WhenAll(tasks);
@ -603,17 +596,6 @@ namespace Microsoft.AspNetCore.SignalR.Redis
}
}
private class RedisExcludeClientsMessage : InvocationMessage
{
public IReadOnlyList<string> ExcludedIds;
public RedisExcludeClientsMessage(string invocationId, bool nonBlocking, string target, IReadOnlyList<string> excludedIds, params object[] arguments)
: base(invocationId, nonBlocking, target, argumentBindingException: null, arguments: arguments)
{
ExcludedIds = excludedIds;
}
}
private class GroupData
{
public SemaphoreSlim Lock = new SemaphoreSlim(1, 1);
@ -639,13 +621,45 @@ namespace Microsoft.AspNetCore.SignalR.Redis
Ack
}
private class GroupMessage
// Marker interface to represent the messages that can be sent over Redis.
private interface IRedisMessage { }
private class RedisGroupMessage : IRedisMessage
{
public string ConnectionId;
public string Group;
public int Id;
public GroupAction Action;
public string Server;
public string ConnectionId { get; set; }
public string Group { get; set; }
public int Id { get; set; }
public GroupAction Action { get; set; }
public string Server { get; set; }
}
// Represents a message published to the Redis bus
private class RedisInvocationMessage : IRedisMessage
{
public string Target { get; set; }
public IReadOnlyList<string> ExcludedIds { get; set; }
public object[] Arguments { get; set; }
public RedisInvocationMessage()
{
}
public RedisInvocationMessage(string target, object[] arguments)
: this(target, excludedIds: null, arguments)
{
}
public RedisInvocationMessage(string target, IReadOnlyList<string> excludedIds, object[] arguments)
{
Target = target;
ExcludedIds = excludedIds;
Arguments = arguments;
}
public InvocationMessage CreateInvocation()
{
return new InvocationMessage(Target, argumentBindingException: null, Arguments);
}
}
}
}

View File

@ -35,7 +35,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\",\"nonBlocking\":true,\"arguments\":[]}\u001e", invokeMessage);
Assert.Equal("{\"type\":1,\"target\":\"Foo\",\"arguments\":[]}\u001e", invokeMessage);
}
finally
{
@ -372,7 +372,7 @@ namespace Microsoft.AspNetCore.SignalR.Client.Tests
using (var ms = new MemoryStream())
{
new MessagePackHubProtocol()
.WriteMessage(new InvocationMessage("1", true, "MyMethod", null, 42), ms);
.WriteMessage(new InvocationMessage(null, "MyMethod", null, 42), ms);
var invokeMessage = Convert.ToBase64String(ms.ToArray());
var payloadSize = invokeMessage.Length.ToString(CultureInfo.InvariantCulture);

View File

@ -17,14 +17,14 @@ namespace Microsoft.AspNetCore.SignalR.Common.Tests.Internal.Protocol
{
public static IEnumerable<object[]> ProtocolTestData => new[]
{
new object[] { new InvocationMessage("123", true, "Target", null, 1, "Foo", 2.0f), true, NullValueHandling.Ignore, "{\"invocationId\":\"123\",\"type\":1,\"target\":\"Target\",\"nonBlocking\":true,\"arguments\":[1,\"Foo\",2.0]}" },
new object[] { new InvocationMessage("123", false, "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", false, "Target", null, true), true, NullValueHandling.Ignore, "{\"invocationId\":\"123\",\"type\":1,\"target\":\"Target\",\"arguments\":[true]}" },
new object[] { new InvocationMessage("123", false, "Target", null, new object[] { null }), true, NullValueHandling.Ignore, "{\"invocationId\":\"123\",\"type\":1,\"target\":\"Target\",\"arguments\":[null]}" },
new object[] { new InvocationMessage("123", false, "Target", null, new CustomObject()), false, NullValueHandling.Ignore, "{\"invocationId\":\"123\",\"type\":1,\"target\":\"Target\",\"arguments\":[{\"StringProp\":\"SignalR!\",\"DoubleProp\":6.2831853071,\"IntProp\":42,\"DateTimeProp\":\"2017-04-11T00:00:00\",\"ByteArrProp\":\"AQID\"}]}" },
new object[] { new InvocationMessage("123", false, "Target", null, new CustomObject()), true, NullValueHandling.Ignore, "{\"invocationId\":\"123\",\"type\":1,\"target\":\"Target\",\"arguments\":[{\"stringProp\":\"SignalR!\",\"doubleProp\":6.2831853071,\"intProp\":42,\"dateTimeProp\":\"2017-04-11T00:00:00\",\"byteArrProp\":\"AQID\"}]}" },
new object[] { new InvocationMessage("123", false, "Target", null, new CustomObject()), false, NullValueHandling.Include, "{\"invocationId\":\"123\",\"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("123", false, "Target", null, new CustomObject()), true, NullValueHandling.Include, "{\"invocationId\":\"123\",\"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("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(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]}" },
new object[] { new InvocationMessage(null, "Target", null, new CustomObject()), false, 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()), 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[] { 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\"}" },
@ -111,8 +111,9 @@ 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}", "Missing required property 'invocationId'.")]
[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'.")]
[InlineData("{'type':1,'invocationId':'42','target':42}", "Expected 'target' to be of type String.")]
[InlineData("{'type':1,'invocationId':'42','target':'foo'}", "Missing required property 'arguments'.")]
[InlineData("{'type':1,'invocationId':'42','target':'foo','arguments':{}}", "Expected 'arguments' to be of type Array.")]
@ -134,7 +135,6 @@ namespace Microsoft.AspNetCore.SignalR.Common.Tests.Internal.Protocol
[InlineData("{'type':9}", "Unknown message type: 9")]
[InlineData("{'type':'foo'}", "Expected 'type' to be of type Integer.")]
[InlineData("{'type':1,'invocationId':'42','target':'foo','arguments':[42, 'foo'],'nonBlocking':42}", "Expected 'nonBlocking' to be of type Boolean.")]
[InlineData("{'type':3,'invocationId':'42','error':'foo','result':true}", "The 'error' and 'result' properties are mutually exclusive.")]
public void InvalidMessages(string input, string expectedMessage)
{

View File

@ -17,40 +17,40 @@ namespace Microsoft.AspNetCore.SignalR.Common.Tests.Internal.Protocol
public static IEnumerable<object[]> TestMessages => new[]
{
new object[] { new[] { new InvocationMessage("xyz", /*nonBlocking*/ false, "method", null) } },
new object[] { new[] { new InvocationMessage("xyz", /*nonBlocking*/ true, "method", null) } },
new object[] { new[] { new InvocationMessage("xyz", /*nonBlocking*/ true, "method", null, new object[] { null }) } },
new object[] { new[] { new InvocationMessage("xyz", /*nonBlocking*/ true, "method", null, 42) } },
new object[] { new[] { new InvocationMessage("xyz", /*nonBlocking*/ true, "method", null, 42, "string") } },
new object[] { new[] { new InvocationMessage("xyz", /*nonBlocking*/ true, "method", null, 42, "string", new CustomObject()) } },
new object[] { new[] { new InvocationMessage("xyz", /*nonBlocking*/ true, "method", null, new[] { new CustomObject(), new CustomObject() }) } },
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() }) } },
new object[] { new[] { new CompletionMessage("xyz", error: "Error not found!", result: null, hasResult: false) } },
new object[] { new[] { new CompletionMessage("xyz", error: null, result: null, hasResult: false) } },
new object[] { new[] { new CompletionMessage("xyz", error: null, result: null, hasResult: true) } },
new object[] { new[] { new CompletionMessage("xyz", error: null, result: 42, hasResult: true) } },
new object[] { new[] { new CompletionMessage("xyz", error: null, result: 42.0f, hasResult: true) } },
new object[] { new[] { new CompletionMessage("xyz", error: null, result: "string", hasResult: true) } },
new object[] { new[] { new CompletionMessage("xyz", error: null, result: true, hasResult: true) } },
new object[] { new[] { new CompletionMessage("xyz", error: null, result: new CustomObject(), hasResult: true) } },
new object[] { new[] { new CompletionMessage("xyz", error: null, result: new[] { new CustomObject(), new CustomObject() }, hasResult: true) } },
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) } },
new object[] { new[] { new StreamItemMessage("xyz", null) } },
new object[] { new[] { new StreamItemMessage("xyz", 42) } },
new object[] { new[] { new StreamItemMessage("xyz", 42.0f) } },
new object[] { new[] { new StreamItemMessage("xyz", "string") } },
new object[] { new[] { new StreamItemMessage("xyz", true) } },
new object[] { new[] { new StreamItemMessage("xyz", new CustomObject()) } },
new object[] { new[] { new StreamItemMessage("xyz", new[] { new CustomObject(), new CustomObject() }) } },
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() }) } },
new object[] { new[] { new StreamInvocationMessage("xyz", "method", null) } },
new object[] { new[] { new StreamInvocationMessage("xyz", "method", null, new object[] { null }) } },
new object[] { new[] { new StreamInvocationMessage("xyz", "method", null, 42) } },
new object[] { new[] { new StreamInvocationMessage("xyz", "method", null, 42, "string") } },
new object[] { new[] { new StreamInvocationMessage("xyz", "method", null, 42, "string", new CustomObject()) } },
new object[] { new[] { new StreamInvocationMessage("xyz", "method", null, new[] { new CustomObject(), new CustomObject() }) } },
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() }) } },
new object[] { new[] { new CancelInvocationMessage("xyz") } },
new object[] { new[] { new CancelInvocationMessage(invocationId: "xyz") } },
new object[] { new[] { PingMessage.Instance } },
@ -58,7 +58,7 @@ namespace Microsoft.AspNetCore.SignalR.Common.Tests.Internal.Protocol
{
new HubMessage[]
{
new InvocationMessage("xyz", /*nonBlocking*/ true, "method", null, 42, "string", new CustomObject()),
new InvocationMessage(null, "method", null, 42, "string", new CustomObject()),
new CompletionMessage("xyz", error: null, result: 42, hasResult: true),
new StreamItemMessage("xyz", null),
PingMessage.Instance,
@ -93,13 +93,11 @@ namespace Microsoft.AspNetCore.SignalR.Common.Tests.Internal.Protocol
new object[] { new byte[] { 0x91, 0x0a } , "Invalid message type: 10." },
// InvocationMessage
new object[] { new byte[] { 0x95, 0x01 }, "Reading 'invocationId' as String failed." }, // invocationId missing
new object[] { new byte[] { 0x95, 0x01, 0xc2 }, "Reading 'invocationId' as String failed." }, // 0xc2 is Bool false
new object[] { new byte[] { 0x95, 0x01, 0xa3, 0x78, 0x79, 0x7a }, "Reading 'nonBlocking' as Boolean failed." }, // nonBlocking missing
new object[] { new byte[] { 0x95, 0x01, 0xa3, 0x78, 0x79, 0x7a, 0x00 }, "Reading 'nonBlocking' as Boolean failed." }, // nonBlocking is not bool
new object[] { new byte[] { 0x95, 0x01, 0xa3, 0x78, 0x79, 0x7a, 0xc2 }, "Reading 'target' as String failed." }, // target missing
new object[] { new byte[] { 0x95, 0x01, 0xa3, 0x78, 0x79, 0x7a, 0xc2, 0x00 }, "Reading 'target' as String failed." }, // 0x00 is Int
new object[] { new byte[] { 0x95, 0x01, 0xa3, 0x78, 0x79, 0x7a, 0xc2, 0xa1 }, "Reading 'target' as String failed." }, // string is cut
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
@ -147,12 +145,12 @@ namespace Microsoft.AspNetCore.SignalR.Common.Tests.Internal.Protocol
public static IEnumerable<object[]> ArgumentBindingErrors => new[]
{
// InvocationMessage
new object[] { new byte[] { 0x95, 0x01, 0xa3, 0x78, 0x79, 0x7a, 0xc2, 0xa1, 0x78 }, "Reading array length for 'arguments' failed." }, // array is missing
new object[] { new byte[] { 0x95, 0x01, 0xa3, 0x78, 0x79, 0x7a, 0xc2, 0xa1, 0x78, 0x00 }, "Reading array length for 'arguments' failed." }, // 0x00 is not array marker
new object[] { new byte[] { 0x95, 0x01, 0xa3, 0x78, 0x79, 0x7a, 0xc2, 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, 0x01, 0xa3, 0x78, 0x79, 0x7a, 0xc2, 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, 0x01, 0xa3, 0x78, 0x79, 0x7a, 0xc2, 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, 0x01, 0xa3, 0x78, 0x79, 0x7a, 0xc2, 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 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
// StreamInvocationMessage
new object[] { new byte[] { 0x95, 0x04, 0xa3, 0x78, 0x79, 0x7a, 0xa1, 0x78 }, "Reading array length for 'arguments' failed." }, // array is missing
@ -203,10 +201,10 @@ namespace Microsoft.AspNetCore.SignalR.Common.Tests.Internal.Protocol
{
new object[]
{
new InvocationMessage("0", false, "A", null, 1, new CustomObject()),
new InvocationMessage(null, "A", null, 1, new CustomObject()),
new byte[]
{
0x6c, 0x95, 0x01, 0xa1, 0x30, 0xc2, 0xa1, 0x41,
0x6a, 0x94, 0x01, 0xc0, 0xa1, 0x41,
0x92, // argument array
0x01, // 1 - first argument
// 0x86 - a map of 6 items (properties)

View File

@ -60,16 +60,14 @@ namespace Microsoft.AspNetCore.SignalR.Common.Tests.Internal.Protocol
{
return string.Equals(x.InvocationId, y.InvocationId, StringComparison.Ordinal) &&
string.Equals(x.Target, y.Target, StringComparison.Ordinal) &&
ArgumentListsEqual(x.Arguments, y.Arguments) &&
x.NonBlocking == y.NonBlocking;
ArgumentListsEqual(x.Arguments, y.Arguments);
}
private bool StreamInvocationMessagesEqual(StreamInvocationMessage x, StreamInvocationMessage y)
{
return string.Equals(x.InvocationId, y.InvocationId, StringComparison.Ordinal) &&
string.Equals(x.Target, y.Target, StringComparison.Ordinal) &&
ArgumentListsEqual(x.Arguments, y.Arguments) &&
x.NonBlocking == y.NonBlocking;
ArgumentListsEqual(x.Arguments, y.Arguments);
}
private bool ArgumentListsEqual(object[] left, object[] right)

View File

@ -37,7 +37,7 @@ namespace Microsoft.AspNetCore.SignalR.Redis.Tests
_serverFixture = serverFixture;
}
[ConditionalTheory(Skip = "Docker tests are flaky")]
[ConditionalTheory()]
[SkipIfDockerNotPresent]
[MemberData(nameof(TransportTypesAndProtocolTypes))]
public async Task HubConnectionCanSendAndReceiveMessages(TransportType transportType, IHubProtocol protocol)
@ -56,7 +56,7 @@ namespace Microsoft.AspNetCore.SignalR.Redis.Tests
}
}
[ConditionalTheory(Skip = "Docker tests are flaky")]
[ConditionalTheory()]
[SkipIfDockerNotPresent]
[MemberData(nameof(TransportTypesAndProtocolTypes))]
public async Task HubConnectionCanSendAndReceiveGroupMessages(TransportType transportType, IHubProtocol protocol)

View File

@ -590,7 +590,7 @@ namespace Microsoft.AspNetCore.SignalR.Redis.Tests
private async Task AssertMessageAsync(TestClient client)
{
var message = Assert.IsType<InvocationMessage>(await client.ReadAsync());
var message = Assert.IsType<InvocationMessage>(await client.ReadAsync().OrTimeout());
Assert.Equal("Hello", message.Target);
Assert.Single(message.Arguments);
Assert.Equal("World", (string)message.Arguments[0]);

View File

@ -136,8 +136,8 @@ namespace Microsoft.AspNetCore.SignalR.Tests
public Task<string> SendInvocationAsync(string methodName, bool nonBlocking, params object[] args)
{
var invocationId = GetInvocationId();
return SendHubMessageAsync(new InvocationMessage(invocationId, nonBlocking, methodName,
var invocationId = nonBlocking ? null : GetInvocationId();
return SendHubMessageAsync(new InvocationMessage(invocationId, methodName,
argumentBindingException: null, arguments: args));
}

View File

@ -11,7 +11,6 @@ using System.Threading.Channels;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.SignalR.Core;
using Microsoft.AspNetCore.SignalR.Internal;
using Microsoft.AspNetCore.SignalR.Internal.Protocol;
using Microsoft.AspNetCore.Sockets;
@ -460,7 +459,7 @@ namespace Microsoft.AspNetCore.SignalR.Tests
}
[Fact]
public async Task HubMethodCanReturnValue()
public async Task HubMethodDoesNotSendResultWhenInvocationIsNonBlocking()
{
var serviceProvider = CreateServiceProvider();
@ -470,14 +469,14 @@ namespace Microsoft.AspNetCore.SignalR.Tests
{
var endPointTask = endPoint.OnConnectedAsync(client.Connection);
var result = (await client.InvokeAsync(nameof(MethodHub.ValueMethod)).OrTimeout()).Result;
// json serializer makes this a long
Assert.Equal(43L, result);
await client.SendInvocationAsync(nameof(MethodHub.ValueMethod), nonBlocking: true).OrTimeout();
// kill the connection
client.Dispose();
// Ensure the client channel is empty
Assert.Null(client.TryRead());
await endPointTask.OrTimeout();
}
}
@ -553,6 +552,7 @@ namespace Microsoft.AspNetCore.SignalR.Tests
[Theory]
[InlineData(nameof(MethodHub.VoidMethod))]
[InlineData(nameof(MethodHub.MethodThatThrows))]
[InlineData(nameof(MethodHub.ValueMethod))]
public async Task NonBlockingInvocationDoesNotSendCompletion(string methodName)
{
var serviceProvider = CreateServiceProvider();
@ -566,12 +566,12 @@ namespace Microsoft.AspNetCore.SignalR.Tests
// This invocation should be completely synchronous
await client.SendInvocationAsync(methodName, nonBlocking: true).OrTimeout();
// Nothing should have been written
Assert.False(client.Application.Reader.TryRead(out var buffer));
// kill the connection
client.Dispose();
// Nothing should have been written
Assert.False(client.Application.Reader.TryRead(out var buffer));
await endPointTask.OrTimeout();
}
}
@ -1655,7 +1655,10 @@ namespace Microsoft.AspNetCore.SignalR.Tests
break;
case InvocationMessage expectedInvocation:
var actualInvocation = Assert.IsType<InvocationMessage>(actual);
Assert.Equal(expectedInvocation.NonBlocking, actualInvocation.NonBlocking);
// Either both must have non-null invocationIds or both must have null invocation IDs. Checking the exact value is NOT desired here though as it could be randomly generated
Assert.True((expectedInvocation.InvocationId == null && actualInvocation.InvocationId == null) ||
(expectedInvocation.InvocationId != null && actualInvocation.InvocationId != null));
Assert.Equal(expectedInvocation.Target, actualInvocation.Target);
Assert.Equal(expectedInvocation.Arguments, actualInvocation.Arguments);
break;