diff --git a/benchmarks/Microsoft.AspNetCore.SignalR.Microbenchmarks/HubProtocolBenchmark.cs b/benchmarks/Microsoft.AspNetCore.SignalR.Microbenchmarks/HubProtocolBenchmark.cs index 63dd977f70..5ba9b47b91 100644 --- a/benchmarks/Microsoft.AspNetCore.SignalR.Microbenchmarks/HubProtocolBenchmark.cs +++ b/benchmarks/Microsoft.AspNetCore.SignalR.Microbenchmarks/HubProtocolBenchmark.cs @@ -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); } } -} \ No newline at end of file +} diff --git a/client-ts/Microsoft.AspNetCore.SignalR.Client.TS.Tests/HubConnection.spec.ts b/client-ts/Microsoft.AspNetCore.SignalR.Client.TS.Tests/HubConnection.spec.ts index a34e080af4..3498631c18 100644 --- a/client-ts/Microsoft.AspNetCore.SignalR.Client.TS.Tests/HubConnection.spec.ts +++ b/client-ts/Microsoft.AspNetCore.SignalR.Client.TS.Tests/HubConnection.spec.ts @@ -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 complete() { this.itemsSource.resolve(this.itemsReceived); } -}; \ No newline at end of file +}; diff --git a/client-ts/Microsoft.AspNetCore.SignalR.Client.TS.Tests/MessagePackHubProtocol.spec.ts b/client-ts/Microsoft.AspNetCore.SignalR.Client.TS.Tests/MessagePackHubProtocol.spec.ts index 717af6673e..164e638f49 100644 --- a/client-ts/Microsoft.AspNetCore.SignalR.Client.TS.Tests/MessagePackHubProtocol.spec.ts +++ b/client-ts/Microsoft.AspNetCore.SignalR.Client.TS.Tests/MessagePackHubProtocol.spec.ts @@ -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 = { + 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 = { type: MessageType.Invocation, invocationId: "123", target: "myMethod", - nonblocking: true, arguments: [42, true, "test", ["x1", "y2"], null] }; @@ -111,4 +122,4 @@ describe("MessageHubProtocol", () => { } ]) }) -}); \ No newline at end of file +}); diff --git a/client-ts/Microsoft.AspNetCore.SignalR.Client.TS/HubConnection.ts b/client-ts/Microsoft.AspNetCore.SignalR.Client.TS/HubConnection.ts index d62711bc10..a998c42813 100644 --- a/client-ts/Microsoft.AspNetCore.SignalR.Client.TS/HubConnection.ts +++ b/client-ts/Microsoft.AspNetCore.SignalR.Client.TS/HubConnection.ts @@ -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((message).invocationId); + let callback = this.callbacks.get((message).invocationId); if (callback != null) { if (message.type === MessageType.Completion) { - this.callbacks.delete((message).invocationId); + this.callbacks.delete((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 { + type: MessageType.Invocation, + target: methodName, + arguments: args, + }; + } + else { + let id = this.id; + this.id++; - return { - type: MessageType.Invocation, - invocationId: id.toString(), - target: methodName, - arguments: args, - nonblocking: nonblocking - }; + return { + type: MessageType.Invocation, + invocationId: id.toString(), + target: methodName, + arguments: args, + }; + } } private createStreamInvocation(methodName: string, args: any[]): StreamInvocationMessage { diff --git a/client-ts/Microsoft.AspNetCore.SignalR.Client.TS/IHubProtocol.ts b/client-ts/Microsoft.AspNetCore.SignalR.Client.TS/IHubProtocol.ts index f56d6ba197..01fc2831dd 100644 --- a/client-ts/Microsoft.AspNetCore.SignalR.Client.TS/IHubProtocol.ts +++ b/client-ts/Microsoft.AspNetCore.SignalR.Client.TS/IHubProtocol.ts @@ -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; - readonly nonblocking?: boolean; } -export interface StreamInvocationMessage extends HubInvocationMessage { +export interface StreamInvocationMessage extends HubMessage { + readonly invocationId: string; readonly target: string; readonly arguments: Array } -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; -} \ No newline at end of file +} diff --git a/client-ts/Microsoft.AspNetCore.SignalR.Client.TS/MessagePackHubProtocol.ts b/client-ts/Microsoft.AspNetCore.SignalR.Client.TS/MessagePackHubProtocol.ts index 634da82dc7..9b13f5cbfb 100644 --- a/client-ts/Microsoft.AspNetCore.SignalR.Client.TS/MessagePackHubProtocol.ts +++ b/client-ts/Microsoft.AspNetCore.SignalR.Client.TS/MessagePackHubProtocol.ts @@ -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()); } -} \ No newline at end of file +} diff --git a/client-ts/Microsoft.AspNetCore.SignalR.Test.Server/wwwroot/js/hubConnectionTests.js b/client-ts/Microsoft.AspNetCore.SignalR.Test.Server/wwwroot/js/hubConnectionTests.js index 6cf9d58a8e..4ac2280ef0 100644 --- a/client-ts/Microsoft.AspNetCore.SignalR.Test.Server/wwwroot/js/hubConnectionTests.js +++ b/client-ts/Microsoft.AspNetCore.SignalR.Test.Server/wwwroot/js/hubConnectionTests.js @@ -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, diff --git a/client-ts/package-lock.json b/client-ts/package-lock.json index 2ea73c4c1c..ef3bd4b10a 100644 --- a/client-ts/package-lock.json +++ b/client-ts/package-lock.json @@ -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", diff --git a/specs/HubProtocol.md b/specs/HubProtocol.md index 54d88ca3a7..26392c1e86 100644 --- a/specs/HubProtocol.md +++ b/specs/HubProtocol.md @@ -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` | `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` | `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. diff --git a/src/Microsoft.AspNetCore.SignalR.Client.Core/HubConnection.cs b/src/Microsoft.AspNetCore.SignalR.Client.Core/HubConnection.cs index 50de12a1a1..0fa46194fc 100644 --- a/src/Microsoft.AspNetCore.SignalR.Client.Core/HubConnection.cs +++ b/src/Microsoft.AspNetCore.SignalR.Client.Core/HubConnection.cs @@ -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); diff --git a/src/Microsoft.AspNetCore.SignalR.Common/Internal/Protocol/HubInvocationMessage.cs b/src/Microsoft.AspNetCore.SignalR.Common/Internal/Protocol/HubInvocationMessage.cs index a447dd1bf1..0bf0531643 100644 --- a/src/Microsoft.AspNetCore.SignalR.Common/Internal/Protocol/HubInvocationMessage.cs +++ b/src/Microsoft.AspNetCore.SignalR.Common/Internal/Protocol/HubInvocationMessage.cs @@ -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 diff --git a/src/Microsoft.AspNetCore.SignalR.Common/Internal/Protocol/HubMethodInvocationMessage.cs b/src/Microsoft.AspNetCore.SignalR.Common/Internal/Protocol/HubMethodInvocationMessage.cs index 02934044fe..087a1698d0 100644 --- a/src/Microsoft.AspNetCore.SignalR.Common/Internal/Protocol/HubMethodInvocationMessage.cs +++ b/src/Microsoft.AspNetCore.SignalR.Common/Internal/Protocol/HubMethodInvocationMessage.cs @@ -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() { diff --git a/src/Microsoft.AspNetCore.SignalR.Common/Internal/Protocol/JsonHubProtocol.cs b/src/Microsoft.AspNetCore.SignalR.Common/Internal/Protocol/JsonHubProtocol.cs index 7151aa2988..c1a7605ff2 100644 --- a/src/Microsoft.AspNetCore.SignalR.Common/Internal/Protocol/JsonHubProtocol.cs +++ b/src/Microsoft.AspNetCore.SignalR.Common/Internal/Protocol/JsonHubProtocol.cs @@ -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(json, InvocationIdPropertyName, JTokenType.String); + var invocationId = JsonUtils.GetOptionalProperty(json, InvocationIdPropertyName, JTokenType.String); var target = JsonUtils.GetRequiredProperty(json, TargetPropertyName, JTokenType.String); - var nonBlocking = JsonUtils.GetOptionalProperty(json, NonBlockingPropertyName, JTokenType.Boolean); var args = JsonUtils.GetRequiredProperty(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)); } } diff --git a/src/Microsoft.AspNetCore.SignalR.Common/Internal/Protocol/MessagePackHubProtocol.cs b/src/Microsoft.AspNetCore.SignalR.Common/Internal/Protocol/MessagePackHubProtocol.cs index 7d62bcfc9d..930c71a260 100644 --- a/src/Microsoft.AspNetCore.SignalR.Common/Internal/Protocol/MessagePackHubProtocol.cs +++ b/src/Microsoft.AspNetCore.SignalR.Common/Internal/Protocol/MessagePackHubProtocol.cs @@ -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) diff --git a/src/Microsoft.AspNetCore.SignalR.Core/DefaultHubLifetimeManager.cs b/src/Microsoft.AspNetCore.SignalR.Core/DefaultHubLifetimeManager.cs index 4a2612f78f..6f028038fb 100644 --- a/src/Microsoft.AspNetCore.SignalR.Core/DefaultHubLifetimeManager.cs +++ b/src/Microsoft.AspNetCore.SignalR.Core/DefaultHubLifetimeManager.cs @@ -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 : HubLifetimeManager { - 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 excludedIds) { return InvokeAllWhere(methodName, args, connection => diff --git a/src/Microsoft.AspNetCore.SignalR.Core/HubEndPoint.cs b/src/Microsoft.AspNetCore.SignalR.Core/HubEndPoint.cs index 4dbd7e1d9d..ab87e8b365 100644 --- a/src/Microsoft.AspNetCore.SignalR.Core/HubEndPoint.cs +++ b/src/Microsoft.AspNetCore.SignalR.Core/HubEndPoint.cs @@ -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, diff --git a/src/Microsoft.AspNetCore.SignalR.Redis/RedisHubLifetimeManager.cs b/src/Microsoft.AspNetCore.SignalR.Redis/RedisHubLifetimeManager.cs index 44426a04da..6a4d11e040 100644 --- a/src/Microsoft.AspNetCore.SignalR.Redis/RedisHubLifetimeManager.cs +++ b/src/Microsoft.AspNetCore.SignalR.Redis/RedisHubLifetimeManager.cs @@ -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> logger, IOptions 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 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(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(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(data); + var message = DeserializeMessage(data); var tasks = new List(_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(data); - var excludedIds = message.ExcludedIds; + var message = DeserializeMessage(data); + var excludedIds = message.ExcludedIds ?? Array.Empty(); var tasks = new List(_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(data); + var groupMessage = DeserializeMessage(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(data); + var groupMessage = DeserializeMessage(data); if (groupMessage.Action == GroupAction.Ack) { @@ -520,9 +512,9 @@ namespace Microsoft.AspNetCore.SignalR.Redis { try { - var message = DeserializeMessage(data); + var message = DeserializeMessage(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(data); + var message = DeserializeMessage(data); var tasks = new List(); + 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 ExcludedIds; - - public RedisExcludeClientsMessage(string invocationId, bool nonBlocking, string target, IReadOnlyList 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 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 excludedIds, object[] arguments) + { + Target = target; + ExcludedIds = excludedIds; + Arguments = arguments; + } + + public InvocationMessage CreateInvocation() + { + return new InvocationMessage(Target, argumentBindingException: null, Arguments); + } } } } diff --git a/test/Microsoft.AspNetCore.SignalR.Client.Tests/HubConnectionProtocolTests.cs b/test/Microsoft.AspNetCore.SignalR.Client.Tests/HubConnectionProtocolTests.cs index 2c029fcc41..a3bdc98478 100644 --- a/test/Microsoft.AspNetCore.SignalR.Client.Tests/HubConnectionProtocolTests.cs +++ b/test/Microsoft.AspNetCore.SignalR.Client.Tests/HubConnectionProtocolTests.cs @@ -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); diff --git a/test/Microsoft.AspNetCore.SignalR.Common.Tests/Internal/Protocol/JsonHubProtocolTests.cs b/test/Microsoft.AspNetCore.SignalR.Common.Tests/Internal/Protocol/JsonHubProtocolTests.cs index ff7f763726..209ae66fd4 100644 --- a/test/Microsoft.AspNetCore.SignalR.Common.Tests/Internal/Protocol/JsonHubProtocolTests.cs +++ b/test/Microsoft.AspNetCore.SignalR.Common.Tests/Internal/Protocol/JsonHubProtocolTests.cs @@ -17,14 +17,14 @@ namespace Microsoft.AspNetCore.SignalR.Common.Tests.Internal.Protocol { public static IEnumerable 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) { diff --git a/test/Microsoft.AspNetCore.SignalR.Common.Tests/Internal/Protocol/MessagePackHubProtocolTests.cs b/test/Microsoft.AspNetCore.SignalR.Common.Tests/Internal/Protocol/MessagePackHubProtocolTests.cs index 6edf5c7eb7..88208625c4 100644 --- a/test/Microsoft.AspNetCore.SignalR.Common.Tests/Internal/Protocol/MessagePackHubProtocolTests.cs +++ b/test/Microsoft.AspNetCore.SignalR.Common.Tests/Internal/Protocol/MessagePackHubProtocolTests.cs @@ -17,40 +17,40 @@ namespace Microsoft.AspNetCore.SignalR.Common.Tests.Internal.Protocol public static IEnumerable 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 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) diff --git a/test/Microsoft.AspNetCore.SignalR.Common.Tests/Internal/Protocol/TestHubMessageEqualityComparer.cs b/test/Microsoft.AspNetCore.SignalR.Common.Tests/Internal/Protocol/TestHubMessageEqualityComparer.cs index 6f3bf45fee..3f15617bc4 100644 --- a/test/Microsoft.AspNetCore.SignalR.Common.Tests/Internal/Protocol/TestHubMessageEqualityComparer.cs +++ b/test/Microsoft.AspNetCore.SignalR.Common.Tests/Internal/Protocol/TestHubMessageEqualityComparer.cs @@ -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) diff --git a/test/Microsoft.AspNetCore.SignalR.Redis.Tests/RedisEndToEnd.cs b/test/Microsoft.AspNetCore.SignalR.Redis.Tests/RedisEndToEnd.cs index 7664d7145c..10b2a9bb53 100644 --- a/test/Microsoft.AspNetCore.SignalR.Redis.Tests/RedisEndToEnd.cs +++ b/test/Microsoft.AspNetCore.SignalR.Redis.Tests/RedisEndToEnd.cs @@ -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) diff --git a/test/Microsoft.AspNetCore.SignalR.Redis.Tests/RedisHubLifetimeManagerTests.cs b/test/Microsoft.AspNetCore.SignalR.Redis.Tests/RedisHubLifetimeManagerTests.cs index bd10c29db0..bbe5b4c4ca 100644 --- a/test/Microsoft.AspNetCore.SignalR.Redis.Tests/RedisHubLifetimeManagerTests.cs +++ b/test/Microsoft.AspNetCore.SignalR.Redis.Tests/RedisHubLifetimeManagerTests.cs @@ -590,7 +590,7 @@ namespace Microsoft.AspNetCore.SignalR.Redis.Tests private async Task AssertMessageAsync(TestClient client) { - var message = Assert.IsType(await client.ReadAsync()); + var message = Assert.IsType(await client.ReadAsync().OrTimeout()); Assert.Equal("Hello", message.Target); Assert.Single(message.Arguments); Assert.Equal("World", (string)message.Arguments[0]); diff --git a/test/Microsoft.AspNetCore.SignalR.Tests.Utils/TestClient.cs b/test/Microsoft.AspNetCore.SignalR.Tests.Utils/TestClient.cs index 2afc8886e8..2c07cbfe3d 100644 --- a/test/Microsoft.AspNetCore.SignalR.Tests.Utils/TestClient.cs +++ b/test/Microsoft.AspNetCore.SignalR.Tests.Utils/TestClient.cs @@ -136,8 +136,8 @@ namespace Microsoft.AspNetCore.SignalR.Tests public Task 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)); } diff --git a/test/Microsoft.AspNetCore.SignalR.Tests/HubEndpointTests.cs b/test/Microsoft.AspNetCore.SignalR.Tests/HubEndpointTests.cs index 0d25189d41..6d74cc8a22 100644 --- a/test/Microsoft.AspNetCore.SignalR.Tests/HubEndpointTests.cs +++ b/test/Microsoft.AspNetCore.SignalR.Tests/HubEndpointTests.cs @@ -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(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;