diff --git a/src/messaging/dotnet/Messaging.sln b/src/messaging/dotnet/Messaging.sln index 4ea5f1210..e56f5423f 100644 --- a/src/messaging/dotnet/Messaging.sln +++ b/src/messaging/dotnet/Messaging.sln @@ -11,6 +11,8 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "MorganStanley.ComposeUI.Mes EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "MorganStanley.ComposeUI.Messaging.Server.Tests", "test\Server.Tests\MorganStanley.ComposeUI.Messaging.Server.Tests.csproj", "{C95F8B4A-7E42-43A6-A57E-C3ACB6255EA3}" EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "MorganStanley.ComposeUI.Messaging.Host", "src\Host\MorganStanley.ComposeUI.Messaging.Host.csproj", "{C53E9D27-790A-450D-8E29-9B40E7D586CB}" +EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "MorganStanley.ComposeUI.Messaging.IntegrationTests", "test\IntegrationTests\MorganStanley.ComposeUI.Messaging.IntegrationTests.csproj", "{684367EE-406C-4727-993D-EF4B6FF5C37C}" EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "MorganStanley.ComposeUI.Messaging.Core.Tests", "test\Core.Tests\MorganStanley.ComposeUI.Messaging.Core.Tests.csproj", "{CDC30566-1801-40CD-9EBD-2913418FC2F9}" @@ -29,6 +31,8 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "lib", "lib", "{B7E63957-3C1 EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "MorganStanley.ComposeUI.Testing", "..\..\shared\dotnet\MorganStanley.ComposeUI.Testing\MorganStanley.ComposeUI.Testing.csproj", "{AE71CBC4-FD4E-4C66-B894-D7C31DE4D1BE}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MorganStanley.ComposeUI.Messaging.Host.Tests", "test\Host.Tests\MorganStanley.ComposeUI.Messaging.Host.Tests.csproj", "{CEF78D3F-C645-4471-BAD2-9C538A0CA763}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -51,6 +55,10 @@ Global {C95F8B4A-7E42-43A6-A57E-C3ACB6255EA3}.Debug|Any CPU.Build.0 = Debug|Any CPU {C95F8B4A-7E42-43A6-A57E-C3ACB6255EA3}.Release|Any CPU.ActiveCfg = Release|Any CPU {C95F8B4A-7E42-43A6-A57E-C3ACB6255EA3}.Release|Any CPU.Build.0 = Release|Any CPU + {C53E9D27-790A-450D-8E29-9B40E7D586CB}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {C53E9D27-790A-450D-8E29-9B40E7D586CB}.Debug|Any CPU.Build.0 = Debug|Any CPU + {C53E9D27-790A-450D-8E29-9B40E7D586CB}.Release|Any CPU.ActiveCfg = Release|Any CPU + {C53E9D27-790A-450D-8E29-9B40E7D586CB}.Release|Any CPU.Build.0 = Release|Any CPU {684367EE-406C-4727-993D-EF4B6FF5C37C}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {684367EE-406C-4727-993D-EF4B6FF5C37C}.Debug|Any CPU.Build.0 = Debug|Any CPU {684367EE-406C-4727-993D-EF4B6FF5C37C}.Release|Any CPU.ActiveCfg = Release|Any CPU @@ -71,6 +79,10 @@ Global {AE71CBC4-FD4E-4C66-B894-D7C31DE4D1BE}.Debug|Any CPU.Build.0 = Debug|Any CPU {AE71CBC4-FD4E-4C66-B894-D7C31DE4D1BE}.Release|Any CPU.ActiveCfg = Release|Any CPU {AE71CBC4-FD4E-4C66-B894-D7C31DE4D1BE}.Release|Any CPU.Build.0 = Release|Any CPU + {CEF78D3F-C645-4471-BAD2-9C538A0CA763}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {CEF78D3F-C645-4471-BAD2-9C538A0CA763}.Debug|Any CPU.Build.0 = Debug|Any CPU + {CEF78D3F-C645-4471-BAD2-9C538A0CA763}.Release|Any CPU.ActiveCfg = Release|Any CPU + {CEF78D3F-C645-4471-BAD2-9C538A0CA763}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -80,11 +92,13 @@ Global {A3156691-0B46-4C3D-B042-E71D9BAF02EC} = {37DE16A7-F731-4206-B650-8A1D9D066B60} {4AEFD2AA-CF42-401D-A058-E2B4C8E12283} = {37DE16A7-F731-4206-B650-8A1D9D066B60} {C95F8B4A-7E42-43A6-A57E-C3ACB6255EA3} = {78E94277-B0B4-425C-9C47-81E09933FE5B} + {C53E9D27-790A-450D-8E29-9B40E7D586CB} = {37DE16A7-F731-4206-B650-8A1D9D066B60} {684367EE-406C-4727-993D-EF4B6FF5C37C} = {78E94277-B0B4-425C-9C47-81E09933FE5B} {CDC30566-1801-40CD-9EBD-2913418FC2F9} = {78E94277-B0B4-425C-9C47-81E09933FE5B} {29122DF1-0AE3-4101-9D96-4711553C7696} = {B245E9BB-69CC-4457-9FDF-635E742993CB} {AA63BC7C-9DB8-4062-A689-7FA9689EF6B1} = {78E94277-B0B4-425C-9C47-81E09933FE5B} {AE71CBC4-FD4E-4C66-B894-D7C31DE4D1BE} = {B7E63957-3C1B-4A16-B530-3EAD0D0C5F07} + {CEF78D3F-C645-4471-BAD2-9C538A0CA763} = {78E94277-B0B4-425C-9C47-81E09933FE5B} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {38CFF5DF-459A-49E1-8189-2DB429A99B3A} diff --git a/src/messaging/dotnet/src/Client/Client/Abstractions/IConnection.cs b/src/messaging/dotnet/src/Client/Client/Abstractions/IConnection.cs index b972fb9a3..7b238e010 100644 --- a/src/messaging/dotnet/src/Client/Client/Abstractions/IConnection.cs +++ b/src/messaging/dotnet/src/Client/Client/Abstractions/IConnection.cs @@ -14,6 +14,8 @@ namespace MorganStanley.ComposeUI.Messaging.Client.Abstractions; +// TODO: Add IConnectionFactory abstraction to adhere to DI best practices + /// /// Represents a connection that can communicate with the server. /// diff --git a/src/messaging/dotnet/src/Client/Client/WebSocket/MessageRouterBuilderWebSocketExtensions.cs b/src/messaging/dotnet/src/Client/Client/WebSocket/MessageRouterBuilderWebSocketExtensions.cs index 56d769c9e..ecb29e435 100644 --- a/src/messaging/dotnet/src/Client/Client/WebSocket/MessageRouterBuilderWebSocketExtensions.cs +++ b/src/messaging/dotnet/src/Client/Client/WebSocket/MessageRouterBuilderWebSocketExtensions.cs @@ -10,11 +10,12 @@ // or implied. See the License for the specific language governing permissions // and limitations under the License. -using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Options; using MorganStanley.ComposeUI.Messaging.Client.Abstractions; +using MorganStanley.ComposeUI.Messaging.Client.WebSocket; -namespace MorganStanley.ComposeUI.Messaging.Client.WebSocket; +// ReSharper disable once CheckNamespace +namespace Microsoft.Extensions.DependencyInjection; /// /// Static utilities for configuring WebSocket connections. diff --git a/src/messaging/dotnet/src/Client/DependencyInjection/MessageRouterBuilder.cs b/src/messaging/dotnet/src/Client/DependencyInjection/MessageRouterBuilder.cs index bffc4f46b..7aef0dcb6 100644 --- a/src/messaging/dotnet/src/Client/DependencyInjection/MessageRouterBuilder.cs +++ b/src/messaging/dotnet/src/Client/DependencyInjection/MessageRouterBuilder.cs @@ -27,7 +27,7 @@ internal MessageRouterBuilder(IServiceCollection serviceCollection) ServiceCollection = serviceCollection; } - internal IServiceCollection ServiceCollection { get; } + public IServiceCollection ServiceCollection { get; } internal string? AccessToken { get; set; } } \ No newline at end of file diff --git a/src/messaging/dotnet/src/Client/MessageRouterClientExtensions.cs b/src/messaging/dotnet/src/Client/MessageRouterClientExtensions.cs index 301db8867..a59fa70e5 100644 --- a/src/messaging/dotnet/src/Client/MessageRouterClientExtensions.cs +++ b/src/messaging/dotnet/src/Client/MessageRouterClientExtensions.cs @@ -19,6 +19,7 @@ namespace MorganStanley.ComposeUI.Messaging; /// public static class MessageRouterClientExtensions { + // TODO: Move this to IMessageRouter - this should be available even if only the Core package is referenced public static IAsyncObservable Topic(this IMessageRouter messageRouter, string topic) { return messageRouter is MessageRouterClient messageRouterClient diff --git a/src/messaging/dotnet/src/Host/Client/Internal/InProcessConnection.cs b/src/messaging/dotnet/src/Host/Client/Internal/InProcessConnection.cs new file mode 100644 index 000000000..f65895e10 --- /dev/null +++ b/src/messaging/dotnet/src/Host/Client/Internal/InProcessConnection.cs @@ -0,0 +1,64 @@ +// Morgan Stanley makes this available to you under the Apache License, +// Version 2.0 (the "License"). You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0. +// +// See the NOTICE file distributed with this work for additional information +// regarding copyright ownership. Unless required by applicable law or agreed +// to in writing, software distributed under the License is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express +// or implied. See the License for the specific language governing permissions +// and limitations under the License. + +using System.Threading.Channels; +using MorganStanley.ComposeUI.Messaging.Client.Abstractions; +using MorganStanley.ComposeUI.Messaging.Protocol.Messages; +using MorganStanley.ComposeUI.Messaging.Server; +using MorganStanley.ComposeUI.Messaging.Server.Abstractions; + +namespace MorganStanley.ComposeUI.Messaging.Client.Internal; + +internal sealed class InProcessConnection : IConnection, IClientConnection +{ + public InProcessConnection(IMessageRouterServer server) + { + _server = server; + } + + ValueTask IClientConnection.SendAsync(Message message, CancellationToken cancellationToken = default) + { + return _serverToClient.Writer.WriteAsync(message, cancellationToken); + } + + ValueTask IClientConnection.ReceiveAsync(CancellationToken cancellationToken = default) + { + return _clientToServer.Reader.ReadAsync(cancellationToken); + } + + public ValueTask DisposeAsync() + { + _clientToServer.Writer.TryComplete(); + _serverToClient.Writer.TryComplete(); + + return ValueTask.CompletedTask; + } + + ValueTask IConnection.ConnectAsync(CancellationToken cancellationToken = default) + { + return _server.ClientConnected(this); + } + + ValueTask IConnection.SendAsync(Message message, CancellationToken cancellationToken = default) + { + return _clientToServer.Writer.WriteAsync(message, cancellationToken); + } + + ValueTask IConnection.ReceiveAsync(CancellationToken cancellationToken = default) + { + return _serverToClient.Reader.ReadAsync(cancellationToken); + } + + private readonly IMessageRouterServer _server; + private readonly Channel _clientToServer = Channel.CreateUnbounded(); + private readonly Channel _serverToClient = Channel.CreateUnbounded(); +} \ No newline at end of file diff --git a/src/messaging/dotnet/src/Host/Client/Internal/MessageRouterBuilderInProcessExtensions.cs b/src/messaging/dotnet/src/Host/Client/Internal/MessageRouterBuilderInProcessExtensions.cs new file mode 100644 index 000000000..0add6304d --- /dev/null +++ b/src/messaging/dotnet/src/Host/Client/Internal/MessageRouterBuilderInProcessExtensions.cs @@ -0,0 +1,35 @@ +// Morgan Stanley makes this available to you under the Apache License, +// Version 2.0 (the "License"). You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0. +// +// See the NOTICE file distributed with this work for additional information +// regarding copyright ownership. Unless required by applicable law or agreed +// to in writing, software distributed under the License is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express +// or implied. See the License for the specific language governing permissions +// and limitations under the License. + +using MorganStanley.ComposeUI.Messaging.Client.Abstractions; +using MorganStanley.ComposeUI.Messaging.Client.Internal; + +// ReSharper disable once CheckNamespace +namespace Microsoft.Extensions.DependencyInjection; + +/// +/// Adds extension methods for configuring an in-process Message Router client. +/// +public static class MessageRouterBuilderInProcessExtensions +{ + /// + /// Configures the Message Router to connect to the in-process server. + /// The server must be set up using . + /// + /// + public static MessageRouterBuilder UseServer(this MessageRouterBuilder builder) + { + builder.ServiceCollection.AddTransient(); + + return builder; + } +} \ No newline at end of file diff --git a/src/messaging/dotnet/src/Host/MorganStanley.ComposeUI.Messaging.Host.csproj b/src/messaging/dotnet/src/Host/MorganStanley.ComposeUI.Messaging.Host.csproj new file mode 100644 index 000000000..bf9d04edb --- /dev/null +++ b/src/messaging/dotnet/src/Host/MorganStanley.ComposeUI.Messaging.Host.csproj @@ -0,0 +1,17 @@ + + + + net6.0 + enable + enable + True + MorganStanley.ComposeUI.Messaging + + + + + + + + + \ No newline at end of file diff --git a/src/messaging/dotnet/src/Server/DependencyInjection/MessageRouterBuilder.cs b/src/messaging/dotnet/src/Server/DependencyInjection/MessageRouterServerBuilder.cs similarity index 72% rename from src/messaging/dotnet/src/Server/DependencyInjection/MessageRouterBuilder.cs rename to src/messaging/dotnet/src/Server/DependencyInjection/MessageRouterServerBuilder.cs index 7c8d4cda0..ca648f929 100644 --- a/src/messaging/dotnet/src/Server/DependencyInjection/MessageRouterBuilder.cs +++ b/src/messaging/dotnet/src/Server/DependencyInjection/MessageRouterServerBuilder.cs @@ -15,28 +15,28 @@ // ReSharper disable once CheckNamespace namespace Microsoft.Extensions.DependencyInjection; -public sealed class MessageRouterBuilder +public sealed class MessageRouterServerBuilder { - public MessageRouterBuilder(IServiceCollection serviceCollection) + public MessageRouterServerBuilder(IServiceCollection serviceCollection) { ServiceCollection = serviceCollection; } - public MessageRouterBuilder UseAccessTokenValidator(IAccessTokenValidator validator) + public MessageRouterServerBuilder UseAccessTokenValidator(IAccessTokenValidator validator) { ServiceCollection.AddSingleton(validator); return this; } - public MessageRouterBuilder UseAccessTokenValidator(Func factory) + public MessageRouterServerBuilder UseAccessTokenValidator(Func factory) { ServiceCollection.AddSingleton(factory); return this; } - public MessageRouterBuilder UseAccessTokenValidator(AccessTokenValidatorCallback validatorCallback) + public MessageRouterServerBuilder UseAccessTokenValidator(AccessTokenValidatorCallback validatorCallback) { ServiceCollection.AddSingleton( new AccessTokenValidator( @@ -50,14 +50,14 @@ public MessageRouterBuilder UseAccessTokenValidator(AccessTokenValidatorCallback return this; } - public MessageRouterBuilder UseAccessTokenValidator(AsyncAccessTokenValidatorCallback validatorCallback) + public MessageRouterServerBuilder UseAccessTokenValidator(AsyncAccessTokenValidatorCallback validatorCallback) { ServiceCollection.AddSingleton(new AccessTokenValidator(validatorCallback)); return this; } - public MessageRouterBuilder UseAccessTokenValidator(Func validatorCallback) + public MessageRouterServerBuilder UseAccessTokenValidator(Func validatorCallback) { ServiceCollection.AddSingleton( new AccessTokenValidator( diff --git a/src/messaging/dotnet/src/Server/DependencyInjection/ServiceCollectionMessageRouterExceptions.cs b/src/messaging/dotnet/src/Server/DependencyInjection/ServiceCollectionMessageRouterServerExtensions.cs similarity index 88% rename from src/messaging/dotnet/src/Server/DependencyInjection/ServiceCollectionMessageRouterExceptions.cs rename to src/messaging/dotnet/src/Server/DependencyInjection/ServiceCollectionMessageRouterServerExtensions.cs index 0010bf081..e1e54a083 100644 --- a/src/messaging/dotnet/src/Server/DependencyInjection/ServiceCollectionMessageRouterExceptions.cs +++ b/src/messaging/dotnet/src/Server/DependencyInjection/ServiceCollectionMessageRouterServerExtensions.cs @@ -18,7 +18,7 @@ namespace Microsoft.Extensions.DependencyInjection; /// /// Contains extension methods for adding the Message Router server to a service collection. /// -public static class ServiceCollectionMessageRouterExceptions +public static class ServiceCollectionMessageRouterServerExtensions { /// /// Adds and related types to the service collection. @@ -28,12 +28,12 @@ public static class ServiceCollectionMessageRouterExceptions /// public static IServiceCollection AddMessageRouterServer( this IServiceCollection serviceCollection, - Action builderAction) + Action builderAction) { serviceCollection.AddSingleton(); serviceCollection.AddSingleton(); - var builder = new MessageRouterBuilder(serviceCollection); + var builder = new MessageRouterServerBuilder(serviceCollection); builderAction(builder); return serviceCollection; diff --git a/src/messaging/dotnet/src/Server/Server/MessageRouterServer.cs b/src/messaging/dotnet/src/Server/Server/MessageRouterServer.cs index fbf2df99f..5c973f4f9 100644 --- a/src/messaging/dotnet/src/Server/Server/MessageRouterServer.cs +++ b/src/messaging/dotnet/src/Server/Server/MessageRouterServer.cs @@ -36,6 +36,7 @@ public async ValueTask DisposeAsync() { _stopTokenSource.Cancel(); + // TODO: Don't dispose objects that were created by someone else. Signal disconnection using a dedicated method. await Task.WhenAll(_clients.Values.Select(client => client.Connection.DisposeAsync().AsTask())); } diff --git a/src/messaging/dotnet/src/Server/Server/WebSocket/MessageRouterBuilderWebSocketExtensions.cs b/src/messaging/dotnet/src/Server/Server/WebSocket/MessageRouterServerBuilderWebSocketExtensions.cs similarity index 80% rename from src/messaging/dotnet/src/Server/Server/WebSocket/MessageRouterBuilderWebSocketExtensions.cs rename to src/messaging/dotnet/src/Server/Server/WebSocket/MessageRouterServerBuilderWebSocketExtensions.cs index 45091f768..460d354f9 100644 --- a/src/messaging/dotnet/src/Server/Server/WebSocket/MessageRouterBuilderWebSocketExtensions.cs +++ b/src/messaging/dotnet/src/Server/Server/WebSocket/MessageRouterServerBuilderWebSocketExtensions.cs @@ -10,15 +10,16 @@ // or implied. See the License for the specific language governing permissions // and limitations under the License. -using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Hosting; +using MorganStanley.ComposeUI.Messaging.Server.WebSocket; -namespace MorganStanley.ComposeUI.Messaging.Server.WebSocket; +// ReSharper disable once CheckNamespace +namespace Microsoft.Extensions.DependencyInjection; -public static class MessageRouterBuilderWebSocketExtensions +public static class MessageRouterServerBuilderWebSocketExtensions { - public static MessageRouterBuilder UseWebSockets( - this MessageRouterBuilder builder, + public static MessageRouterServerBuilder UseWebSockets( + this MessageRouterServerBuilder builder, Action? configureOptions = null) { if (configureOptions != null) diff --git a/src/messaging/dotnet/test/Host.Tests/Client/Internal/InProcessConnection.Tests.cs b/src/messaging/dotnet/test/Host.Tests/Client/Internal/InProcessConnection.Tests.cs new file mode 100644 index 000000000..72c97913a --- /dev/null +++ b/src/messaging/dotnet/test/Host.Tests/Client/Internal/InProcessConnection.Tests.cs @@ -0,0 +1,66 @@ +// Morgan Stanley makes this available to you under the Apache License, +// Version 2.0 (the "License"). You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0. +// +// See the NOTICE file distributed with this work for additional information +// regarding copyright ownership. Unless required by applicable law or agreed +// to in writing, software distributed under the License is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express +// or implied. See the License for the specific language governing permissions +// and limitations under the License. + +using MorganStanley.ComposeUI.Messaging.Client.Abstractions; +using MorganStanley.ComposeUI.Messaging.Protocol.Messages; +using MorganStanley.ComposeUI.Messaging.Server; +using MorganStanley.ComposeUI.Messaging.Server.Abstractions; + +namespace MorganStanley.ComposeUI.Messaging.Client.Internal; + +public class InProcessConnectionTests +{ + [Fact] + public async Task Connect_registers_itself_at_the_server() + { + var server = new Mock(); + var connection = (IConnection)new InProcessConnection(server.Object); + + await connection.ConnectAsync(); + + server.Verify(_ => _.ClientConnected(It.IsAny()),Times.Once()); + } + + [Fact] + public async Task The_client_can_send_messages_to_the_server() + { + var connection = new InProcessConnection(Mock.Of()); + var clientSideConnection = (IConnection) connection; + var serverSideConnection = (IClientConnection) connection; + + await clientSideConnection.ConnectAsync(); + + var message = new ConnectRequest(); + + await clientSideConnection.SendAsync(message); + var received = await serverSideConnection.ReceiveAsync(); + + received.Should().Be(message); + } + + [Fact] + public async Task The_server_can_send_messages_to_the_client() + { + var connection = new InProcessConnection(Mock.Of()); + var clientSideConnection = (IConnection) connection; + var serverSideConnection = (IClientConnection) connection; + + await clientSideConnection.ConnectAsync(); + + var message = new Protocol.Messages.TopicMessage(); + + await serverSideConnection.SendAsync(message); + var received = await clientSideConnection.ReceiveAsync(); + + received.Should().Be(message); + } +} \ No newline at end of file diff --git a/src/messaging/dotnet/test/Host.Tests/GlobalUsings.cs b/src/messaging/dotnet/test/Host.Tests/GlobalUsings.cs new file mode 100644 index 000000000..e18ed8249 --- /dev/null +++ b/src/messaging/dotnet/test/Host.Tests/GlobalUsings.cs @@ -0,0 +1,16 @@ +// Morgan Stanley makes this available to you under the Apache License, +// Version 2.0 (the "License"). You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0. +// +// See the NOTICE file distributed with this work for additional information +// regarding copyright ownership. Unless required by applicable law or agreed +// to in writing, software distributed under the License is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express +// or implied. See the License for the specific language governing permissions +// and limitations under the License. + +global using FluentAssertions; +global using Xunit; +global using Moq; + diff --git a/src/messaging/dotnet/test/Host.Tests/MorganStanley.ComposeUI.Messaging.Host.Tests.csproj b/src/messaging/dotnet/test/Host.Tests/MorganStanley.ComposeUI.Messaging.Host.Tests.csproj new file mode 100644 index 000000000..e68ad34d8 --- /dev/null +++ b/src/messaging/dotnet/test/Host.Tests/MorganStanley.ComposeUI.Messaging.Host.Tests.csproj @@ -0,0 +1,14 @@ + + + + net6.0 + false + MorganStanley.ComposeUI.Messaging + + + + + + + + \ No newline at end of file diff --git a/src/messaging/dotnet/test/IntegrationTests/EndToEndTestsBase.cs b/src/messaging/dotnet/test/IntegrationTests/EndToEndTestsBase.cs new file mode 100644 index 000000000..98ed46c7f --- /dev/null +++ b/src/messaging/dotnet/test/IntegrationTests/EndToEndTestsBase.cs @@ -0,0 +1,335 @@ +// Morgan Stanley makes this available to you under the Apache License, +// Version 2.0 (the "License"). You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0. +// +// See the NOTICE file distributed with this work for additional information +// regarding copyright ownership. Unless required by applicable law or agreed +// to in writing, software distributed under the License is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express +// or implied. See the License for the specific language governing permissions +// and limitations under the License. + +using System.Reactive.Linq; +using System.Text.Json; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Hosting; +using Nito.AsyncEx; +using TaskExtensions = MorganStanley.ComposeUI.Testing.TaskExtensions; + +namespace MorganStanley.ComposeUI.Messaging; + +public abstract class EndToEndTestsBase : IAsyncLifetime +{ + [Fact] + public async Task Client_can_connect() + { + await using var client = CreateClient(); + await client.ConnectAsync(); + } + + [Fact] + public async Task Client_can_subscribe_and_receive_messages() + { + await using var publisher = CreateClient(); + await using var subscriber = CreateClient(); + var observerMock = new Mock>(); + var receivedMessages = new List(); + observerMock.Setup(x => x.OnNext(Capture.In(receivedMessages))); + + await subscriber.SubscribeAsync(topic: "test-topic", observerMock.Object); + await TaskExtensions.WaitForBackgroundTasksAsync(DefaultTestTimeout); + + var publishedPayload = new TestPayload + { + IntProperty = 0x10203040, + StringProperty = "Compose UI 🔥" + }; + + await publisher.PublishAsync( + topic: "test-topic", + MessageBuffer.Create(JsonSerializer.SerializeToUtf8Bytes(publishedPayload))); + + await Task.Delay( + 10); // TODO: Investigate why WaitForBackgroundTasksAsync is unreliable in this particular scenario + + var receivedPayload = JsonSerializer.Deserialize(receivedMessages.Single().Payload!.GetSpan()); + + receivedPayload.Should().BeEquivalentTo(publishedPayload); + } + + [Fact] + public async Task Client_can_register_itself_as_a_service() + { + await using var client = CreateClient(); + await client.RegisterServiceAsync(endpoint: "test-service", (name, payload, context) => default); + await client.UnregisterServiceAsync("test-service"); + } + + [Fact] + public async Task Client_can_invoke_a_registered_service() + { + await using var service = CreateClient(); + + var handlerMock = new Mock(); + + handlerMock + .Setup(_ => _.Invoke("test-service", It.IsAny(), It.IsAny())) + .Returns(new ValueTask(MessageBuffer.Create("test-response"))); + + await service.RegisterServiceAsync(endpoint: "test-service", handlerMock.Object); + + await using var client = CreateClient(); + + var response = await client.InvokeAsync(endpoint: "test-service", payload: "test-request"); + + response.Should().BeEquivalentTo("test-response"); + + handlerMock.Verify( + _ => _.Invoke( + "test-service", + It.Is(buf => buf.GetString() == "test-request"), + It.IsAny())); + + await service.UnregisterServiceAsync("test-service"); + } + + [Fact] + public async Task Client_can_invoke_another_client_by_id_as_long_as_it_is_registered() + { + await using var callee = CreateClient(); + await using var caller = CreateClient(); + + var handlerMock = new Mock(); + + handlerMock.Setup(_ => _.Invoke(It.IsAny(), It.IsAny(), It.IsAny())) + .ReturnsAsync( + (string endpoint, MessageBuffer? payload, MessageContext context) => + MessageBuffer.Create("test-response")); + + await callee.RegisterEndpointAsync(endpoint: "test-endpoint", handlerMock.Object); + + var response = await caller.InvokeAsync( + endpoint: "test-endpoint", + payload: "test-request", + new InvokeOptions + { + Scope = MessageScope.FromClientId(callee.ClientId!) + }); + + response.Should().BeEquivalentTo("test-response"); + + handlerMock.Verify( + _ => _.Invoke( + "test-endpoint", + It.Is(buf => buf.GetString() == "test-request"), + It.IsAny())); + + await callee.UnregisterEndpointAsync("test-endpoint"); + + await Assert.ThrowsAsync( + async () => await caller.InvokeAsync( + endpoint: "test-endpoint", + payload: "test-request", + new InvokeOptions + { + Scope = MessageScope.FromClientId(callee.ClientId!) + })); + } + + [Fact] + public async Task Subscriber_can_invoke_a_service_without_deadlock() + { + await using var subscriber = CreateClient(); + await using var service = CreateClient(); + await using var publisher = CreateClient(); + await service.RegisterServiceAsync(endpoint: "test-service", new Mock().Object); + var tcs = new TaskCompletionSource(); + + await subscriber.SubscribeAsync( + topic: "test-topic", + AsyncObserver.Create( + new Func( + async msg => + { + await subscriber.InvokeAsync("test-service"); + tcs.SetResult(); + }))); + + await publisher.PublishAsync("test-topic"); + await tcs.Task; + } + + [Fact] + public async Task Subscriber_is_called_sequentially_without_concurrency() + { + await using var subscriber = CreateClient(); + await using var publisher = CreateClient(); + var semaphore = new AsyncSemaphore(1); + + var tcs = new TaskCompletionSource(); + + await subscriber.SubscribeAsync( + topic: "test-topic", + AsyncObserver.Create( + async msg => + { + using (await semaphore.LockAsync(new CancellationTokenSource(TimeSpan.Zero).Token)) + { + await TaskExtensions.WaitForBackgroundTasksAsync(DefaultTestTimeout); + } + + if (msg.Payload?.GetString() == "done") + { + tcs.SetResult(); + } + })); + + for (var i = 0; i < 10; i++) + { + await publisher.PublishAsync("test-topic"); + } + + await publisher.PublishAsync(topic: "test-topic", payload: "done"); + await tcs.Task; + } + + [Fact] + public async Task Endpoint_handler_can_invoke_a_service_without_deadlock() + { + await using var listener = CreateClient(); + await using var caller = CreateClient(); + await using var service = CreateClient(); + var tcs = new TaskCompletionSource(); + + await listener.RegisterEndpointAsync( + endpoint: "test-endpoint", + new MessageHandler( + async (endpoint, payload, context) => + { + await listener.InvokeAsync("test-service"); + tcs.SetResult(); + return null; + })); + + await service.RegisterServiceAsync(endpoint: "test-service", new Mock().Object); + await caller.InvokeAsync( + endpoint: "test-endpoint", + options: new InvokeOptions {Scope = MessageScope.FromClientId(listener.ClientId!)}); + await tcs.Task; + } + + [Fact] + public async Task Endpoint_handler_can_be_invoked_recursively_without_deadlock() + { + await using var serviceA = CreateClient(); + await using var serviceB = CreateClient(); + var tcs = new TaskCompletionSource(); + + await serviceA.RegisterServiceAsync( + endpoint: "test-service-a", + new MessageHandler( + async (endpoint, payload, context) => + { + if (payload?.GetString() == "done") + { + tcs.SetResult(); + } + else + { + await serviceA.InvokeAsync(endpoint: "test-service-b", payload: "hello"); + } + + return null; + })); + + await serviceB.RegisterServiceAsync( + endpoint: "test-service-b", + new MessageHandler( + async (endpoint, payload, context) => + { + await serviceB.InvokeAsync(endpoint: "test-service-a", payload: "done"); + + return null; + })); + + await serviceB.InvokeAsync("test-service-a"); + + await tcs.Task; + } + + public async Task InitializeAsync() + { + IHostBuilder builder = new HostBuilder(); + + builder.ConfigureServices( + services => + { + services.AddMessageRouterServer( + server => + { + server.UseAccessTokenValidator( + (clientId, token) => + { + if (token != AccessToken) + throw new InvalidOperationException("Invalid access token"); + }); + ConfigureServer(server); + }); + + ConfigureServices(services); + }); + + _host = builder.Build(); + await _host.StartAsync(); + } + + public async Task DisposeAsync() + { + foreach (var disposable in _cleanup) + { + if (disposable is IAsyncDisposable asyncDisposable) + { + await asyncDisposable.DisposeAsync(); + } + else + { + disposable.Dispose(); + } + } + + await _host.StopAsync(); + _host.Dispose(); + } + + public static readonly TimeSpan DefaultTestTimeout = TimeSpan.FromSeconds(1); + + protected IHost Host => _host ?? throw new InvalidOperationException("Host is not initialized yet."); + + protected void AddDisposable(IDisposable disposable) + { + _cleanup.Add(disposable); + } + + protected const string AccessToken = "token"; + + protected virtual void ConfigureServices(IServiceCollection services) + { + // Add any additional service registrations. + // Don't call AddMessageRouterServer. + } + + protected abstract void ConfigureServer(MessageRouterServerBuilder serverBuilder); + + protected abstract IMessageRouter CreateClient(); + + private IHost _host = null!; + + private readonly List _cleanup = new(); + + private class TestPayload + { + public int IntProperty { get; set; } + public string StringProperty { get; set; } + } +} \ No newline at end of file diff --git a/src/messaging/dotnet/test/IntegrationTests/InProcessEndToEndTests.cs b/src/messaging/dotnet/test/IntegrationTests/InProcessEndToEndTests.cs new file mode 100644 index 000000000..bed1c7eb5 --- /dev/null +++ b/src/messaging/dotnet/test/IntegrationTests/InProcessEndToEndTests.cs @@ -0,0 +1,33 @@ +// Morgan Stanley makes this available to you under the Apache License, +// Version 2.0 (the "License"). You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0. +// +// See the NOTICE file distributed with this work for additional information +// regarding copyright ownership. Unless required by applicable law or agreed +// to in writing, software distributed under the License is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express +// or implied. See the License for the specific language governing permissions +// and limitations under the License. + +using Microsoft.Extensions.DependencyInjection; + +namespace MorganStanley.ComposeUI.Messaging; + +public class InProcessEndToEndTests : EndToEndTestsBase +{ + protected override IMessageRouter CreateClient() + { + return Host.Services.GetRequiredService(); + } + + protected override void ConfigureServer(MessageRouterServerBuilder serverBuilder) + { + } + + protected override void ConfigureServices(IServiceCollection services) + { + base.ConfigureServices(services); + services.AddMessageRouter(mr => mr.UseServer().UseAccessToken(AccessToken)); + } +} \ No newline at end of file diff --git a/src/messaging/dotnet/test/IntegrationTests/MorganStanley.ComposeUI.Messaging.IntegrationTests.csproj b/src/messaging/dotnet/test/IntegrationTests/MorganStanley.ComposeUI.Messaging.IntegrationTests.csproj index dbc145af5..633b7721a 100644 --- a/src/messaging/dotnet/test/IntegrationTests/MorganStanley.ComposeUI.Messaging.IntegrationTests.csproj +++ b/src/messaging/dotnet/test/IntegrationTests/MorganStanley.ComposeUI.Messaging.IntegrationTests.csproj @@ -14,6 +14,7 @@ + diff --git a/src/messaging/dotnet/test/IntegrationTests/WebSocketEndToEndTests.cs b/src/messaging/dotnet/test/IntegrationTests/WebSocketEndToEndTests.cs index a22c26098..15b7099a7 100644 --- a/src/messaging/dotnet/test/IntegrationTests/WebSocketEndToEndTests.cs +++ b/src/messaging/dotnet/test/IntegrationTests/WebSocketEndToEndTests.cs @@ -10,305 +10,24 @@ // or implied. See the License for the specific language governing permissions // and limitations under the License. -using System.Reactive; -using System.Reactive.Linq; -using System.Text.Json; using Microsoft.Extensions.DependencyInjection; -using Microsoft.Extensions.Hosting; using MorganStanley.ComposeUI.Messaging.Client.WebSocket; -using MorganStanley.ComposeUI.Messaging.Server.WebSocket; -using Nito.AsyncEx; -using TaskExtensions = MorganStanley.ComposeUI.Testing.TaskExtensions; namespace MorganStanley.ComposeUI.Messaging; -public class WebSocketEndToEndTests : IAsyncLifetime +public class WebSocketEndToEndTests : EndToEndTestsBase { - [Fact] - public async Task Client_can_connect() + protected override void ConfigureServer(MessageRouterServerBuilder serverBuilder) { - await using var client = CreateClient(); - await client.ConnectAsync(); - } - - [Fact] - public async Task Client_can_subscribe_and_receive_messages() - { - await using var publisher = CreateClient(); - await using var subscriber = CreateClient(); - var observerMock = new Mock>(); - var receivedMessages = new List(); - observerMock.Setup(x => x.OnNext(Capture.In(receivedMessages))); - - await subscriber.SubscribeAsync("test-topic", observerMock.Object); - await TaskExtensions.WaitForBackgroundTasksAsync(DefaultTestTimeout); - - var publishedPayload = new TestPayload - { - IntProperty = 0x10203040, - StringProperty = "Compose UI 🔥" - }; - - await publisher.PublishAsync( - "test-topic", - MessageBuffer.Create(JsonSerializer.SerializeToUtf8Bytes(publishedPayload))); - - await Task.Delay(10); // TODO: Investigate why WaitForBackgroundTasksAsync is unreliable in this particular scenario - - var receivedPayload = JsonSerializer.Deserialize(receivedMessages.Single().Payload!.GetSpan()); - - receivedPayload.Should().BeEquivalentTo(publishedPayload); - } - - [Fact] - public async Task Client_can_register_itself_as_a_service() - { - await using var client = CreateClient(); - await client.RegisterServiceAsync("test-service", (name, payload, context) => default); - await client.UnregisterServiceAsync("test-service"); - } - - [Fact] - public async Task Client_can_invoke_a_registered_service() - { - await using var service = CreateClient(); - - var handlerMock = new Mock(); - - handlerMock - .Setup(_ => _.Invoke("test-service", It.IsAny(), It.IsAny())) - .Returns(new ValueTask(MessageBuffer.Create("test-response"))); - - await service.RegisterServiceAsync("test-service", handlerMock.Object); - - await using var client = CreateClient(); - - var response = await client.InvokeAsync("test-service", "test-request"); - - response.Should().BeEquivalentTo("test-response"); - - handlerMock.Verify( - _ => _.Invoke( - "test-service", - It.Is(buf => buf.GetString() == "test-request"), - It.IsAny())); - - await service.UnregisterServiceAsync("test-service"); - } - - [Fact] - public async Task Client_can_invoke_another_client_by_id_as_long_as_it_is_registered() - { - await using var callee = CreateClient(); - await using var caller = CreateClient(); - - var handlerMock = new Mock(); - - handlerMock.Setup(_ => _.Invoke(It.IsAny(), It.IsAny(), It.IsAny())) - .ReturnsAsync( - (string endpoint, MessageBuffer? payload, MessageContext context) => - MessageBuffer.Create("test-response")); - - await callee.RegisterEndpointAsync("test-endpoint", handlerMock.Object); - - var response = await caller.InvokeAsync( - "test-endpoint", - "test-request", - new InvokeOptions + serverBuilder.UseWebSockets( + opt => { - Scope = MessageScope.FromClientId(callee.ClientId!) + opt.RootPath = _webSocketUri.AbsolutePath; + opt.Port = _webSocketUri.Port; }); - - response.Should().BeEquivalentTo("test-response"); - - handlerMock.Verify( - _ => _.Invoke( - "test-endpoint", - It.Is(buf => buf.GetString() == "test-request"), - It.IsAny())); - - await callee.UnregisterEndpointAsync("test-endpoint"); - - await Assert.ThrowsAsync( - async () => await caller.InvokeAsync( - "test-endpoint", - "test-request", - new InvokeOptions - { - Scope = MessageScope.FromClientId(callee.ClientId!) - })); - } - - [Fact] - public async Task Subscriber_can_invoke_a_service_without_deadlock() - { - await using var subscriber = CreateClient(); - await using var service = CreateClient(); - await using var publisher = CreateClient(); - await service.RegisterServiceAsync("test-service", new Mock().Object); - var tcs = new TaskCompletionSource(); - - await subscriber.SubscribeAsync( - "test-topic", - AsyncObserver.Create( - new Func( - async msg => - { - await subscriber.InvokeAsync("test-service"); - tcs.SetResult(); - }))); - - await publisher.PublishAsync("test-topic"); - await tcs.Task; - } - - [Fact] - public async Task Subscriber_is_called_sequentially_without_concurrency() - { - await using var subscriber = CreateClient(); - await using var publisher = CreateClient(); - var semaphore = new AsyncSemaphore(1); - - var tcs = new TaskCompletionSource(); - - await subscriber.SubscribeAsync( - "test-topic", - AsyncObserver.Create( - async msg => - { - using (await semaphore.LockAsync(new CancellationTokenSource(TimeSpan.Zero).Token)) - { - await TaskExtensions.WaitForBackgroundTasksAsync(DefaultTestTimeout); - } - - if (msg.Payload?.GetString() == "done") - { - tcs.SetResult(); - } - })); - - for (var i = 0; i < 10; i++) - { - await publisher.PublishAsync("test-topic"); - } - - await publisher.PublishAsync("test-topic", "done"); - await tcs.Task; - } - - [Fact] - public async Task Endpoint_handler_can_invoke_a_service_without_deadlock() - { - await using var listener = CreateClient(); - await using var caller = CreateClient(); - await using var service = CreateClient(); - var tcs = new TaskCompletionSource(); - - await listener.RegisterEndpointAsync( - "test-endpoint", - new MessageHandler( - async (endpoint, payload, context) => - { - await listener.InvokeAsync("test-service"); - tcs.SetResult(); - return null; - })); - - await service.RegisterServiceAsync("test-service", new Mock().Object); - await caller.InvokeAsync( - "test-endpoint", - options: new InvokeOptions {Scope = MessageScope.FromClientId(listener.ClientId!)}); - await tcs.Task; } - [Fact] - public async Task Endpoint_handler_can_be_invoked_recursively_without_deadlock() - { - await using var serviceA = CreateClient(); - await using var serviceB = CreateClient(); - var tcs = new TaskCompletionSource(); - - await serviceA.RegisterServiceAsync( - "test-service-a", - new MessageHandler( - async (endpoint, payload, context) => - { - if (payload?.GetString() == "done") - { - tcs.SetResult(); - } - else - { - await serviceA.InvokeAsync("test-service-b", "hello"); - } - - return null; - })); - - await serviceB.RegisterServiceAsync( - "test-service-b", - new MessageHandler( - async (endpoint, payload, context) => - { - await serviceB.InvokeAsync("test-service-a", "done"); - - return null; - })); - - await serviceB.InvokeAsync("test-service-a"); - - await tcs.Task; - } - - public async Task InitializeAsync() - { - IHostBuilder builder = new HostBuilder(); - - builder.ConfigureServices( - services => services.AddMessageRouterServer( - mr => mr.UseWebSockets( - opt => - { - opt.RootPath = _webSocketUri.AbsolutePath; - opt.Port = _webSocketUri.Port; - }) - .UseAccessTokenValidator( - (clientId, token) => - { - if (token != AccessToken) - throw new InvalidOperationException("Invalid access token"); - }))); - - _host = builder.Build(); - await _host.StartAsync(); - } - - public async Task DisposeAsync() - { - foreach (var disposable in _cleanup) - { - if (disposable is IAsyncDisposable asyncDisposable) - { - await asyncDisposable.DisposeAsync(); - } - else - { - disposable.Dispose(); - } - } - - await _host.StopAsync(); - _host.Dispose(); - } - - public static readonly TimeSpan DefaultTestTimeout = TimeSpan.FromSeconds(1); - - private IHost _host = null!; - private readonly Uri _webSocketUri = new("ws://localhost:7098/ws"); - private const string AccessToken = "token"; - private readonly List _cleanup = new(); - - private IMessageRouter CreateClient() + protected override IMessageRouter CreateClient() { var services = new ServiceCollection() .AddMessageRouter( @@ -321,14 +40,11 @@ private IMessageRouter CreateClient() .UseAccessToken(AccessToken)) .BuildServiceProvider(); - _cleanup.Add(services); + AddDisposable(services); return services.GetRequiredService(); } - private class TestPayload - { - public int IntProperty { get; set; } - public string StringProperty { get; set; } - } + + private readonly Uri _webSocketUri = new("ws://localhost:7098/ws"); } \ No newline at end of file diff --git a/src/shell/dotnet/Shell.sln b/src/shell/dotnet/Shell.sln index a2c843ede..d21482d6c 100644 --- a/src/shell/dotnet/Shell.sln +++ b/src/shell/dotnet/Shell.sln @@ -15,6 +15,8 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "tests", "tests", "{BEA71FE5 EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Shell.Tests", "tests\Shell.Tests\Shell.Tests.csproj", "{70491F36-A27B-4E8C-AFD5-B49480EC1343}" EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "MorganStanley.ComposeUI.Messaging.Host", "..\..\messaging\dotnet\src\Host\MorganStanley.ComposeUI.Messaging.Host.csproj", "{DD91C297-06D4-4578-9C75-1BA5D8595AA0}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -37,6 +39,10 @@ Global {70491F36-A27B-4E8C-AFD5-B49480EC1343}.Debug|Any CPU.Build.0 = Debug|Any CPU {70491F36-A27B-4E8C-AFD5-B49480EC1343}.Release|Any CPU.ActiveCfg = Release|Any CPU {70491F36-A27B-4E8C-AFD5-B49480EC1343}.Release|Any CPU.Build.0 = Release|Any CPU + {DD91C297-06D4-4578-9C75-1BA5D8595AA0}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {DD91C297-06D4-4578-9C75-1BA5D8595AA0}.Debug|Any CPU.Build.0 = Debug|Any CPU + {DD91C297-06D4-4578-9C75-1BA5D8595AA0}.Release|Any CPU.ActiveCfg = Release|Any CPU + {DD91C297-06D4-4578-9C75-1BA5D8595AA0}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -45,6 +51,7 @@ Global {B2AA5676-2776-4BDA-95F8-BE09D7642393} = {E7A2C581-4BF4-47A5-8A11-59B2DEBADCA7} {F0DFAB55-14F5-4FF6-A12F-37B1745D4551} = {E7A2C581-4BF4-47A5-8A11-59B2DEBADCA7} {70491F36-A27B-4E8C-AFD5-B49480EC1343} = {BEA71FE5-7EC8-4BEA-819B-C0DB33125D91} + {DD91C297-06D4-4578-9C75-1BA5D8595AA0} = {E7A2C581-4BF4-47A5-8A11-59B2DEBADCA7} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {4C901E6C-4B9A-48C2-AB16-461040DC57B4} diff --git a/src/shell/dotnet/Shell/App.xaml.cs b/src/shell/dotnet/Shell/App.xaml.cs index eb4be75eb..77ee4e005 100644 --- a/src/shell/dotnet/Shell/App.xaml.cs +++ b/src/shell/dotnet/Shell/App.xaml.cs @@ -80,7 +80,7 @@ private async Task StartAsync(StartupEventArgs e) config => config.AddJsonFile("appsettings.json", optional: true)) .ConfigureLogging(l => l.AddDebug().SetMinimumLevel(LogLevel.Debug)) .ConfigureServices(ConfigureServices) - .Build(); + .Build(); await host.StartAsync(); _host = host; @@ -99,9 +99,16 @@ private void ConfigureServices(HostBuilderContext context, IServiceCollection se .UseAccessTokenValidator( (clientId, token) => { + // TODO: Assign a separate token for each client and only allow a single connection with each token if (_messageRouterAccessToken != token) throw new InvalidOperationException("The provided access token is invalid"); } )); + + services.AddMessageRouter( + mr => mr + .UseServer() + .UseAccessToken(_messageRouterAccessToken)); + services.Configure(context.Configuration.GetSection("Logging")); } diff --git a/src/shell/dotnet/Shell/Shell.csproj b/src/shell/dotnet/Shell/Shell.csproj index 988dc322d..e56a28cfa 100644 --- a/src/shell/dotnet/Shell/Shell.csproj +++ b/src/shell/dotnet/Shell/Shell.csproj @@ -42,8 +42,7 @@ - - + \ No newline at end of file