From e43e7f8e36235910436322479d2b6a7de43c2c9a Mon Sep 17 00:00:00 2001 From: Christian <6939810+chkr1011@users.noreply.github.com> Date: Sat, 30 Aug 2025 10:52:59 +0200 Subject: [PATCH] Use latest language features (#2176) * Make use of language features line AsyncDispose, init etc. * Fix build errors * Improve Unit Tests * Improve Unit Tests * Improve Unit Tests * Update samples * Remove obsolete benchmarks * Code cleanup * Improve unit tests * Improve Unit Tests * Fix Unit Tests * Improve Unit Tests * Improve Unit Tests * Restore unit tests * Exclude load tests from unit tests --- .github/workflows/ci.yml | 2 +- MQTTnet.sln.DotSettings | 4 + Samples/Client/Client_Connection_Samples.cs | 2 +- Samples/Client/Client_Publish_Samples.cs | 72 +- Samples/Client/Client_Subscribe_Samples.cs | 122 +- Samples/Diagnostics/Logger_Samples.cs | 30 +- .../Diagnostics/PackageInspection_Samples.cs | 22 +- Samples/RpcClient/RpcClient_Samples.cs | 28 +- Samples/Server/Server_Diagnostics_Samples.cs | 32 +- Samples/Server/Server_Intercepting_Samples.cs | 26 +- .../Server_Retained_Messages_Samples.cs | 100 +- Samples/Server/Server_Simple_Samples.cs | 110 +- Samples/Server/Server_TLS_Samples.cs | 42 +- .../MQTTnet.AspTestApp/Pages/Index.cshtml.cs | 23 +- .../ApplicationBuilderExtensions.cs | 6 +- .../ConnectionBuilderExtensions.cs | 13 +- .../EndpointRouterExtensions.cs | 22 +- .../MqttClientConnectionContextFactory.cs | 33 +- .../MqttConnectionHandler.cs | 10 +- .../MqttWebSocketServerAdapter.cs | 6 +- Source/MQTTnet.AspnetCore/SocketAwaitable.cs | 20 +- Source/MQTTnet.AspnetCore/SocketConnection.cs | 14 +- .../MQTTnet.Benchmarks/AsyncLockBenchmark.cs | 87 +- Source/MQTTnet.Benchmarks/BaseBenchmark.cs | 7 +- .../ChannelAdapterBenchmark.cs | 109 +- Source/MQTTnet.Benchmarks/LoggerBenchmark.cs | 150 +- .../MQTTnet.Benchmarks/MemoryCopyBenchmark.cs | 62 +- .../MessageDeliveryBenchmark.cs | 67 +- ...rocessingMqttConnectionContextBenchmark.cs | 111 +- .../MqttBufferReaderBenchmark.cs | 51 +- .../MqttPacketReaderWriterBenchmark.cs | 129 +- .../MqttTcpChannelBenchmark.cs | 1 - Source/MQTTnet.Benchmarks/Program.cs | 207 +- .../ReaderExtensionsBenchmark.cs | 292 +-- .../RoundtripProcessingBenchmark.cs | 41 +- .../SendPacketAsyncBenchmark.cs | 100 +- .../MQTTnet.Benchmarks/SerializerBenchmark.cs | 183 +- .../ServerProcessingBenchmark.cs | 53 +- .../MQTTnet.Benchmarks/SubscribeBenchmark.cs | 2 +- .../MQTTnet.Benchmarks/TcpPipesBenchmark.cs | 121 +- .../TopicFilterComparerBenchmark.cs | 277 +-- Source/MQTTnet.Benchmarks/TopicGenerator.cs | 125 +- .../UnsubscribeBenchmark.cs | 4 +- ...ultMqttRpcClientTopicGenerationStrategy.cs | 37 +- .../MQTTnet.Extensions.Rpc/IMqttRpcClient.cs | 13 +- .../IMqttRpcClientTopicGenerationStrategy.cs | 11 +- .../MqttFactoryExtensions.cs | 33 +- .../MQTTnet.Extensions.Rpc/MqttRpcClient.cs | 216 +- .../MqttRpcClientExtensions.cs | 15 +- .../MqttRpcClientOptions.cs | 11 +- .../MqttRpcClientOptionsBuilder.cs | 29 +- .../MqttRpcTopicPair.cs | 15 +- .../TopicGenerationContext.cs | 59 +- .../MQTTnet.Extensions.TopicTemplate.csproj | 2 +- .../MqttTopicTemplate.cs | 4 +- .../MqttServerClientDisconnectOptions.cs | 39 +- ...qttServerClientDisconnectOptionsBuilder.cs | 77 +- .../ApplicationMessageEnqueuedEventArgs.cs | 33 +- .../ApplicationMessageNotConsumedEventArgs.cs | 33 +- ...lientAcknowledgedPublishPacketEventArgs.cs | 80 +- .../Events/ClientConnectedEventArgs.cs | 111 +- .../Events/ClientDisconnectedEventArgs.cs | 151 +- .../Events/ClientSubscribedTopicEventArgs.cs | 62 +- .../ClientUnsubscribedTopicEventArgs.cs | 62 +- ...lientApplicationMessageEnqueueEventArgs.cs | 49 +- .../Events/InterceptingPacketEventArgs.cs | 101 +- .../Events/InterceptingPublishEventArgs.cs | 91 +- .../InterceptingSubscriptionEventArgs.cs | 156 +- .../InterceptingUnsubscriptionEventArgs.cs | 130 +- .../LoadingRetainedMessagesEventArgs.cs | 10 +- .../Events/PreparingSessionEventArgs.cs | 51 +- .../QueueMessageOverwrittenEventArgs.cs | 22 +- .../Events/RetainedMessageChangedEventArgs.cs | 28 +- .../Events/SessionDeletedEventArgs.cs | 46 +- .../Internal/Adapter/MqttTcpServerAdapter.cs | 165 +- .../Internal/Adapter/MqttTcpServerListener.cs | 441 ++-- .../Internal/CheckSubscriptionsResult.cs | 23 +- .../DispatchApplicationMessageResult.cs | 33 +- .../Internal/EnqueueDataPacketResult.cs | 11 +- .../Formatter/MqttUnsubAckPacketFactory.cs | 27 +- .../ISubscriptionChangedNotification.cs | 13 +- .../Internal/MqttClientSessionsManager.cs | 18 +- .../Internal/MqttClientStatistics.cs | 165 +- .../MqttClientSubscriptionsManager.cs | 853 ++++---- .../Internal/MqttRetainedMessagesManager.cs | 188 +- .../Internal/MqttServerEventContainer.cs | 53 +- .../Internal/MqttServerKeepAliveMonitor.cs | 219 +- .../Internal/MqttSessionsStorage.cs | 121 +- .../Internal/MqttSubscription.cs | 77 +- .../MQTTnet.Server/Internal/MqttTopicHash.cs | 487 +++-- .../Internal/SubscribeResult.cs | 31 +- .../Internal/TopicHashMaskSubscriptions.cs | 41 +- .../Internal/UnsubscribeResult.cs | 13 +- Source/MQTTnet.Server/MqttServerExtensions.cs | 4 +- Source/MQTTnet.Server/MqttServerFactory.cs | 5 +- .../IMqttServerCertificateCredentials.cs | 11 +- .../MqttPendingMessagesOverflowStrategy.cs | 15 +- .../MqttServerCertificateCredentials.cs | 11 +- .../Options/MqttServerKeepAliveOptions.cs | 23 +- .../Options/MqttServerOptionsBuilder.cs | 384 ++-- .../MqttServerTcpEndpointBaseOptions.cs | 83 +- .../Options/MqttServerTcpEndpointOptions.cs | 13 +- .../MqttServerTlsTcpEndpointOptions.cs | 45 +- .../Stopping/MqttServerStopOptions.cs | 27 +- .../Stopping/MqttServerStopOptionsBuilder.cs | 41 +- Source/MQTTnet.Server/SubscribeResponse.cs | 2 +- Source/MQTTnet.Server/UnsubscribeResponse.cs | 2 +- Source/MQTTnet.TestApp/AsyncLockTest.cs | 55 +- Source/MQTTnet.TestApp/ClientFlowTest.cs | 63 +- Source/MQTTnet.TestApp/ClientTest.cs | 137 +- .../MQTTnet.TestApp/MessageThroughputTest.cs | 617 +++--- .../MQTTnet.TestApp/MqttNetConsoleLogger.cs | 95 +- Source/MQTTnet.TestApp/PerformanceTest.cs | 669 +++--- Source/MQTTnet.TestApp/PublicBrokerTest.cs | 295 ++- Source/MQTTnet.TestApp/ServerAndClientTest.cs | 31 +- Source/MQTTnet.TestApp/ServerTest.cs | 363 ++-- Source/MQTTnet.TestApp/TopicGenerator.cs | 151 +- .../ASP/Mockups/DuplexPipeMockup.cs | 35 +- .../ASP/Mockups/LimitedMemoryPool.cs | 23 +- .../MQTTnet.Tests/ASP/Mockups/MemoryOwner.cs | 41 +- .../ASP/MqttConnectionContextTest.cs | 210 +- .../MQTTnet.Tests/ASP/ReaderExtensionsTest.cs | 15 +- Source/MQTTnet.Tests/BaseTestClass.cs | 25 +- .../LowLevelMqttClient_Tests.cs | 343 ++- .../MqttClient/MqttClient_Connection_Tests.cs | 510 ++--- .../Clients/MqttClient/MqttClient_Tests.cs | 1685 +++++++-------- .../Clients/MqttClientOptionsBuilder_Tests.cs | 31 +- .../MQTTnet.Tests/Diagnostics/Logger_Tests.cs | 104 +- .../Diagnostics/PacketInspection_Tests.cs | 58 +- .../Diagnostics/SourceLogger_Tests.cs | 33 +- .../Extensions/MqttTopicTemplate_Tests.cs | 478 ++-- Source/MQTTnet.Tests/Extensions/Rpc_Tests.cs | 462 ++-- .../Factory/MqttFactory_Tests.cs | 133 +- .../Formatter/MqttBufferReader_Tests.cs | 516 ++--- ...MqttPacketSerialization_V3_Binary_Tests.cs | 934 ++++---- .../MqttPacketSerialization_V3_Tests.cs | 879 ++++---- .../MqttPacketSerialization_V5_Tests.cs | 1 + .../Formatter/MqttPacketWriter_Tests.cs | 179 +- .../Helpers/MqttClientExtensions.cs | 17 +- .../Helpers/MqttPacketWriterExtensions.cs | 19 +- .../Helpers/ReflectionExtensions.cs | 23 +- .../Internal/AsyncEvent_Tests.cs | 234 +- .../MQTTnet.Tests/Internal/AsyncLock_Tests.cs | 478 ++-- .../Internal/AsyncQueue_Tests.cs | 262 +-- .../Internal/AsyncSignal_Tests.cs | 190 +- .../Internal/BlockingQueue_Tests.cs | 174 +- .../Internal/CrossPlatformSocket_Tests.cs | 122 +- .../Internal/MqttPacketBusItem_Tests.cs | 48 +- .../Internal/MqttPacketBus_Tests.cs | 235 +- Source/MQTTnet.Tests/MQTTnet.Tests.csproj | 8 +- Source/MQTTnet.Tests/MQTTv5/Client_Tests.cs | 697 +++--- Source/MQTTnet.Tests/MQTTv5/Server_Tests.cs | 409 ++-- .../Mockups/MemoryMqttChannel.cs | 88 +- .../TestApplicationMessageReceivedHandler.cs | 115 +- .../MQTTnet.Tests/Mockups/TestEnvironment.cs | 768 ++++--- Source/MQTTnet.Tests/Mockups/TestLogger.cs | 29 +- .../MqttApplicationMessageBuilder_Tests.cs | 104 +- .../MqttApplicationMessageValidator_Tests.cs | 58 +- .../MqttClientOptionsValidator_Tests.cs | 72 +- .../MqttPacketIdentifierProvider_Tests.cs | 52 +- .../MQTTnet.Tests/MqttPacketWriter_Tests.cs | 48 +- Source/MQTTnet.Tests/MqttTcpChannel_Tests.cs | 1 + .../MqttTopicValidatorSubscribe_Tests.cs | 94 +- .../MQTTnet.Tests/MqttTopicValidator_Tests.cs | 52 +- Source/MQTTnet.Tests/Protocol_Tests.cs | 62 +- Source/MQTTnet.Tests/RoundtripTime_Tests.cs | 80 +- .../Server/Assigned_Client_ID_Tests.cs | 142 +- .../MQTTnet.Tests/Server/Connection_Tests.cs | 109 +- .../Server/Cross_Version_Tests.cs | 84 +- Source/MQTTnet.Tests/Server/Events_Tests.cs | 268 ++- Source/MQTTnet.Tests/Server/General.cs | 1919 ++++++++--------- .../Server/HotSwapCerts_Tests.cs | 691 +++--- .../MQTTnet.Tests/Server/Injection_Tests.cs | 790 +++---- .../MQTTnet.Tests/Server/Keep_Alive_Tests.cs | 92 +- Source/MQTTnet.Tests/Server/Load_Tests.cs | 264 ++- .../MqttRetainedMessageManager_Tests.cs | 34 +- .../Server/MqttSubscriptionsManager_Tests.cs | 370 ++-- Source/MQTTnet.Tests/Server/No_Local_Tests.cs | 62 +- .../MQTTnet.Tests/Server/Publishing_Tests.cs | 236 +- Source/MQTTnet.Tests/Server/QoS_Tests.cs | 185 +- .../Server/Retain_As_Published_Tests.cs | 58 +- .../Server/Retain_Handling_Tests.cs | 86 +- .../Server/Retained_Messages_Tests.cs | 470 ++-- Source/MQTTnet.Tests/Server/Security_Tests.cs | 278 ++- .../Server/Server_Reference_Tests.cs | 33 +- Source/MQTTnet.Tests/Server/Session_Tests.cs | 606 +++--- .../Server/Shared_Subscriptions_Tests.cs | 56 +- Source/MQTTnet.Tests/Server/Status_Tests.cs | 203 +- .../MQTTnet.Tests/Server/Subscribe_Tests.cs | 794 ++++--- .../Server/Subscription_Identifier_Tests.cs | 116 +- .../Server/Subscription_TopicHash_Tests.cs | 1103 +++++----- Source/MQTTnet.Tests/Server/Tls_Tests.cs | 284 ++- .../MQTTnet.Tests/Server/Topic_Alias_Tests.cs | 122 +- .../MQTTnet.Tests/Server/Unsubscribe_Tests.cs | 73 +- .../Server/User_Properties_Tests.cs | 80 +- .../Wildcard_Subscription_Available_Tests.cs | 54 +- Source/MQTTnet.Tests/Server/Will_Tests.cs | 104 +- .../TopicFilterComparer_Tests.cs | 301 ++- Source/MQTTnet.Tests/TopicGenerator.cs | 149 +- Source/MQTTnet/Adapter/MqttChannelAdapter.cs | 2 +- Source/MQTTnet/Adapter/MqttPacketInspector.cs | 6 +- Source/MQTTnet/Adapter/ReceivedMqttPacket.cs | 2 +- .../Certificates/BlobCertificateProvider.cs | 32 +- .../Certificates/ICertificateProvider.cs | 11 +- .../Certificates/X509CertificateProvider.cs | 22 +- .../Connecting/MqttClientConnectResult.cs | 38 +- .../MqttClientConnectResultFactory.cs | 47 +- .../Diagnostics/Logger/IMqttNetLogger.cs | 15 +- .../Diagnostics/Logger/MqttNetEventLogger.cs | 86 +- .../Diagnostics/Logger/MqttNetLogLevel.cs | 17 +- .../Diagnostics/Logger/MqttNetLogMessage.cs | 49 +- .../MqttNetLogMessagePublishedEventArgs.cs | 16 +- .../Diagnostics/Logger/MqttNetNullLogger.cs | 28 +- .../Diagnostics/Logger/MqttNetSourceLogger.cs | 26 +- .../Logger/MqttNetSourceLoggerExtensions.cs | 296 ++- .../InspectMqttPacketEventArgs.cs | 21 +- .../MqttPacketFlowDirection.cs | 13 +- .../MqttClientDisconnectOptionsBuilder.cs | 6 +- .../MqttClientDisconnectOptionsValidator.cs | 3 +- .../MqttClientNotConnectedException.cs | 15 +- ...ntUnexpectedDisconnectReceivedException.cs | 25 +- .../Exceptions/MqttCommunicationException.cs | 25 +- .../MqttCommunicationTimedOutException.cs | 21 +- .../Exceptions/MqttConfigurationException.cs | 31 +- .../MqttProtocolViolationException.cs | 13 +- .../MQTTnet/Formatter/IMqttPacketFormatter.cs | 13 +- .../MqttApplicationMessageFactory.cs | 47 +- Source/MQTTnet/Formatter/MqttBufferReader.cs | 255 ++- Source/MQTTnet/Formatter/MqttBufferWriter.cs | 471 ++-- .../Formatter/MqttConnectPacketFactory.cs | 28 +- .../MqttConnectReasonCodeConverter.cs | 81 +- Source/MQTTnet/Formatter/MqttFixedHeader.cs | 20 +- Source/MQTTnet/Formatter/MqttPacketBuffer.cs | 80 +- .../Formatter/MqttPacketFormatterAdapter.cs | 201 +- .../MQTTnet/Formatter/MqttProtocolVersion.cs | 17 +- .../Formatter/MqttPublishPacketFactory.cs | 50 +- .../Formatter/V3/MqttV3PacketFormatter.cs | 1517 +++++++------ .../Formatter/V5/MqttV5PacketDecoder.cs | 1264 ++++++----- .../Formatter/V5/MqttV5PacketEncoder.cs | 83 +- .../Formatter/V5/MqttV5PacketFormatter.cs | 35 +- .../Formatter/V5/MqttV5PropertiesReader.cs | 383 ++-- .../Formatter/V5/MqttV5PropertiesWriter.cs | 592 +++-- .../MqttClientAdapterFactory.cs | 61 +- .../MQTTnet/Implementations/MqttTcpChannel.cs | 503 +++-- .../Implementations/MqttWebSocketChannel.cs | 397 ++-- Source/MQTTnet/Internal/AsyncEvent.cs | 155 +- .../MQTTnet/Internal/AsyncEventInvocator.cs | 64 +- Source/MQTTnet/Internal/AsyncLock.cs | 255 ++- Source/MQTTnet/Internal/AsyncQueue.cs | 137 +- .../Internal/AsyncQueueDequeueResult.cs | 24 +- Source/MQTTnet/Internal/AsyncSignal.cs | 270 ++- .../Internal/AsyncTaskCompletionSource.cs | 40 +- Source/MQTTnet/Internal/BlockingQueue.cs | 223 +- .../CancellationTokenSourceExtensions.cs | 39 +- Source/MQTTnet/Internal/CompletedTask.cs | 9 +- Source/MQTTnet/Internal/Disposable.cs | 60 +- Source/MQTTnet/Internal/EmptyBuffer.cs | 13 +- .../Internal/MqttClientResultFactory.cs | 13 - Source/MQTTnet/Internal/MqttMemoryHelper.cs | 117 +- Source/MQTTnet/Internal/MqttPacketBus.cs | 287 ++- Source/MQTTnet/Internal/MqttPacketBusItem.cs | 53 +- .../Internal/MqttPacketBusPartition.cs | 17 +- Source/MQTTnet/Internal/TaskExtensions.cs | 71 +- .../LowLevelClient/ILowLevelMqttClient.cs | 19 +- .../LowLevelClient/LowLevelMqttClient.cs | 207 +- Source/MQTTnet/MQTTnet.csproj | 2 +- Source/MQTTnet/MqttApplicationMessage.cs | 261 ++- .../MQTTnet/MqttApplicationMessageBuilder.cs | 481 ++--- .../MqttApplicationMessageValidator.cs | 94 +- Source/MQTTnet/MqttClient.cs | 126 +- Source/MQTTnet/MqttClientFactory.cs | 3 + Source/MQTTnet/MqttTopicFilterBuilder.cs | 173 +- .../MQTTnet/MqttTopicFilterCompareResult.cs | 21 +- Source/MQTTnet/MqttTopicFilterComparer.cs | 267 ++- .../DefaultMqttCertificatesProvider.cs | 2 +- ...ientDefaultCertificateValidationHandler.cs | 2 +- .../Options/MqttClientOptionsBuilder.cs | 10 +- .../Options/MqttClientOptionsValidator.cs | 17 +- .../Options/MqttClientWebSocketOptions.cs | 3 +- .../MqttClientWebSocketOptionsBuilder.cs | 2 +- .../MqttEnhancedAuthenticationEventArgs.cs | 5 +- .../MqttEnhancedAuthenticationExchangeData.cs | 2 +- .../PacketDispatcher/IMqttPacketAwaitable.cs | 17 +- .../PacketDispatcher/MqttPacketAwaitable.cs | 87 +- .../MqttPacketAwaitableFilter.cs | 13 +- .../PacketDispatcher/MqttPacketDispatcher.cs | 226 +- Source/MQTTnet/Packets/MqttConnectPacket.cs | 133 +- .../MQTTnet/Packets/MqttDisconnectPacket.cs | 61 +- Source/MQTTnet/Packets/MqttPacket.cs | 4 +- .../MQTTnet/Packets/MqttPacketExtensions.cs | 99 +- .../Packets/MqttPacketWithIdentifier.cs | 11 +- Source/MQTTnet/Packets/MqttPingReqPacket.cs | 21 +- Source/MQTTnet/Packets/MqttPubRecPacket.cs | 41 +- Source/MQTTnet/Packets/MqttPubRelPacket.cs | 41 +- Source/MQTTnet/Packets/MqttPublishPacket.cs | 2 +- Source/MQTTnet/Packets/MqttSubAckPacket.cs | 43 +- Source/MQTTnet/Packets/MqttSubscribePacket.cs | 2 +- Source/MQTTnet/Packets/MqttTopicFilter.cs | 89 +- Source/MQTTnet/Packets/MqttUnsubAckPacket.cs | 49 +- .../MQTTnet/Packets/MqttUnsubscribePacket.cs | 2 +- Source/MQTTnet/Packets/MqttUserProperty.cs | 4 +- .../Protocol/MqttAuthenticateReasonCode.cs | 15 +- .../MQTTnet/Protocol/MqttConnectReasonCode.cs | 53 +- .../MQTTnet/Protocol/MqttConnectReturnCode.cs | 21 +- .../MQTTnet/Protocol/MqttControlPacketType.cs | 39 +- .../Protocol/MqttDisconnectReasonCode.cs | 67 +- .../Protocol/MqttPayloadFormatIndicator.cs | 13 +- Source/MQTTnet/Protocol/MqttPorts.cs | 13 +- Source/MQTTnet/Protocol/MqttPropertyId.cs | 67 +- .../MQTTnet/Protocol/MqttPubAckReasonCode.cs | 37 +- .../MQTTnet/Protocol/MqttPubCompReasonCode.cs | 13 +- .../MQTTnet/Protocol/MqttPubRecReasonCode.cs | 27 +- .../MQTTnet/Protocol/MqttPubRelReasonCode.cs | 13 +- .../Protocol/MqttQualityOfServiceLevel.cs | 15 +- Source/MQTTnet/Protocol/MqttRetainHandling.cs | 19 +- .../Protocol/MqttSubscribeReasonCode.cs | 39 +- .../Protocol/MqttSubscribeReturnCode.cs | 17 +- Source/MQTTnet/Protocol/MqttTopicValidator.cs | 71 +- .../Protocol/MqttUnsubscribeReasonCode.cs | 23 +- .../Publishing/MqttClientPublishResult.cs | 2 +- .../MqttClientPublishResultFactory.cs | 6 +- ...MqttApplicationMessageReceivedEventArgs.cs | 2 +- .../Subscribing/MqttClientSubscribeOptions.cs | 2 +- .../MqttClientSubscribeOptionsBuilder.cs | 13 +- .../MqttClientSubscribeOptionsValidator.cs | 2 +- .../MqttClientSubscribeResultFactory.cs | 7 +- .../MqttClientUnsubscribeOptions.cs | 2 +- .../MqttClientUnsubscribeOptionsBuilder.cs | 12 +- .../MqttClientUnsubscribeOptionsValidator.cs | 3 +- .../MqttClientUnsubscribeResultFactory.cs | 4 +- 330 files changed, 21417 insertions(+), 22919 deletions(-) delete mode 100644 Source/MQTTnet/Internal/MqttClientResultFactory.cs diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 46ee666a..0116131c 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -46,7 +46,7 @@ jobs: uses: actions/checkout@v4 - name: Execute tests - run: dotnet test --framework net8.0 Source/MQTTnet.Tests/MQTTnet.Tests.csproj + run: dotnet test --framework net8.0 --configuration Release Source/MQTTnet.Tests/MQTTnet.Tests.csproj sign: needs: build diff --git a/MQTTnet.sln.DotSettings b/MQTTnet.sln.DotSettings index 962baa61..64de5263 100644 --- a/MQTTnet.sln.DotSettings +++ b/MQTTnet.sln.DotSettings @@ -1,4 +1,8 @@  + DO_NOT_SHOW + SUGGESTION + DO_NOT_SHOW + DO_NOT_SHOW Required Required Required diff --git a/Samples/Client/Client_Connection_Samples.cs b/Samples/Client/Client_Connection_Samples.cs index 8d716f50..32a8784c 100644 --- a/Samples/Client/Client_Connection_Samples.cs +++ b/Samples/Client/Client_Connection_Samples.cs @@ -413,7 +413,7 @@ m/XriWr/Cq4h/JfB7NTsezVslgkBaoU= if (e.ClientWasConnected) { // Use the current options as the new options. - await mqttClient.ConnectAsync(mqttClient.Options); + await mqttClient.ConnectAsync(mqttClientOptions); } }; diff --git a/Samples/Client/Client_Publish_Samples.cs b/Samples/Client/Client_Publish_Samples.cs index 16cc67c7..957baf95 100644 --- a/Samples/Client/Client_Publish_Samples.cs +++ b/Samples/Client/Client_Publish_Samples.cs @@ -23,25 +23,23 @@ public static class Client_Publish_Samples var mqttFactory = new MqttClientFactory(); - using (var mqttClient = mqttFactory.CreateMqttClient()) - { - var mqttClientOptions = new MqttClientOptionsBuilder() - .WithTcpServer("broker.hivemq.com") - .Build(); + using var mqttClient = mqttFactory.CreateMqttClient(); + var mqttClientOptions = new MqttClientOptionsBuilder() + .WithTcpServer("broker.hivemq.com") + .Build(); - await mqttClient.ConnectAsync(mqttClientOptions, CancellationToken.None); + await mqttClient.ConnectAsync(mqttClientOptions, CancellationToken.None); - var applicationMessage = new MqttApplicationMessageBuilder() - .WithTopic("samples/temperature/living_room") - .WithPayload("19.5") - .Build(); + var applicationMessage = new MqttApplicationMessageBuilder() + .WithTopic("samples/temperature/living_room") + .WithPayload("19.5") + .Build(); - await mqttClient.PublishAsync(applicationMessage, CancellationToken.None); + await mqttClient.PublishAsync(applicationMessage, CancellationToken.None); - await mqttClient.DisconnectAsync(); + await mqttClient.DisconnectAsync(); - Console.WriteLine("MQTT application message is published."); - } + Console.WriteLine("MQTT application message is published."); } public static async Task Publish_Multiple_Application_Messages() @@ -54,38 +52,36 @@ public static class Client_Publish_Samples var mqttFactory = new MqttClientFactory(); - using (var mqttClient = mqttFactory.CreateMqttClient()) - { - var mqttClientOptions = new MqttClientOptionsBuilder() - .WithTcpServer("broker.hivemq.com") - .Build(); + using var mqttClient = mqttFactory.CreateMqttClient(); + var mqttClientOptions = new MqttClientOptionsBuilder() + .WithTcpServer("broker.hivemq.com") + .Build(); - await mqttClient.ConnectAsync(mqttClientOptions, CancellationToken.None); + await mqttClient.ConnectAsync(mqttClientOptions, CancellationToken.None); - var applicationMessage = new MqttApplicationMessageBuilder() - .WithTopic("samples/temperature/living_room") - .WithPayload("19.5") - .Build(); + var applicationMessage = new MqttApplicationMessageBuilder() + .WithTopic("samples/temperature/living_room") + .WithPayload("19.5") + .Build(); - await mqttClient.PublishAsync(applicationMessage, CancellationToken.None); + await mqttClient.PublishAsync(applicationMessage, CancellationToken.None); - applicationMessage = new MqttApplicationMessageBuilder() - .WithTopic("samples/temperature/living_room") - .WithPayload("20.0") - .Build(); + applicationMessage = new MqttApplicationMessageBuilder() + .WithTopic("samples/temperature/living_room") + .WithPayload("20.0") + .Build(); - await mqttClient.PublishAsync(applicationMessage, CancellationToken.None); + await mqttClient.PublishAsync(applicationMessage, CancellationToken.None); - applicationMessage = new MqttApplicationMessageBuilder() - .WithTopic("samples/temperature/living_room") - .WithPayload("21.0") - .Build(); + applicationMessage = new MqttApplicationMessageBuilder() + .WithTopic("samples/temperature/living_room") + .WithPayload("21.0") + .Build(); - await mqttClient.PublishAsync(applicationMessage, CancellationToken.None); + await mqttClient.PublishAsync(applicationMessage, CancellationToken.None); - await mqttClient.DisconnectAsync(); + await mqttClient.DisconnectAsync(); - Console.WriteLine("MQTT application message is published."); - } + Console.WriteLine("MQTT application message is published."); } } \ No newline at end of file diff --git a/Samples/Client/Client_Subscribe_Samples.cs b/Samples/Client/Client_Subscribe_Samples.cs index 96a2a6f2..058d3ff9 100644 --- a/Samples/Client/Client_Subscribe_Samples.cs +++ b/Samples/Client/Client_Subscribe_Samples.cs @@ -26,32 +26,30 @@ public static class Client_Subscribe_Samples var mqttFactory = new MqttClientFactory(); - using (var mqttClient = mqttFactory.CreateMqttClient()) + using var mqttClient = mqttFactory.CreateMqttClient(); + var mqttClientOptions = new MqttClientOptionsBuilder().WithTcpServer("broker.hivemq.com").Build(); + + // Setup message handling before connecting so that queued messages + // are also handled properly. When there is no event handler attached all + // received messages get lost. + mqttClient.ApplicationMessageReceivedAsync += e => { - var mqttClientOptions = new MqttClientOptionsBuilder().WithTcpServer("broker.hivemq.com").Build(); + Console.WriteLine("Received application message."); + e.DumpToConsole(); - // Setup message handling before connecting so that queued messages - // are also handled properly. When there is no event handler attached all - // received messages get lost. - mqttClient.ApplicationMessageReceivedAsync += e => - { - Console.WriteLine("Received application message."); - e.DumpToConsole(); + return Task.CompletedTask; + }; - return Task.CompletedTask; - }; + await mqttClient.ConnectAsync(mqttClientOptions, CancellationToken.None); - await mqttClient.ConnectAsync(mqttClientOptions, CancellationToken.None); + var mqttSubscribeOptions = mqttFactory.CreateSubscribeOptionsBuilder().WithTopicTemplate(sampleTemplate.WithParameter("id", "2")).Build(); - var mqttSubscribeOptions = mqttFactory.CreateSubscribeOptionsBuilder().WithTopicTemplate(sampleTemplate.WithParameter("id", "2")).Build(); + await mqttClient.SubscribeAsync(mqttSubscribeOptions, CancellationToken.None); - await mqttClient.SubscribeAsync(mqttSubscribeOptions, CancellationToken.None); + Console.WriteLine("MQTT client subscribed to topic."); - Console.WriteLine("MQTT client subscribed to topic."); - - Console.WriteLine("Press enter to exit."); - Console.ReadLine(); - } + Console.WriteLine("Press enter to exit."); + Console.ReadLine(); } public static async Task Send_Responses() @@ -62,36 +60,34 @@ public static class Client_Subscribe_Samples var mqttFactory = new MqttClientFactory(); - using (var mqttClient = mqttFactory.CreateMqttClient()) + using var mqttClient = mqttFactory.CreateMqttClient(); + mqttClient.ApplicationMessageReceivedAsync += delegate(MqttApplicationMessageReceivedEventArgs args) { - mqttClient.ApplicationMessageReceivedAsync += delegate(MqttApplicationMessageReceivedEventArgs args) - { - // Do some work with the message... + // Do some work with the message... - // Now respond to the broker with a reason code other than success. - args.ReasonCode = MqttApplicationMessageReceivedReasonCode.ImplementationSpecificError; - args.ResponseReasonString = "That did not work!"; + // Now respond to the broker with a reason code other than success. + args.ReasonCode = MqttApplicationMessageReceivedReasonCode.ImplementationSpecificError; + args.ResponseReasonString = "That did not work!"; - // User properties require MQTT v5! - args.ResponseUserProperties.Add(new MqttUserProperty("My", "Data")); + // User properties require MQTT v5! + args.ResponseUserProperties.Add(new MqttUserProperty("My", "Data")); - // Now the broker will resend the message again. - return Task.CompletedTask; - }; + // Now the broker will resend the message again. + return Task.CompletedTask; + }; - var mqttClientOptions = new MqttClientOptionsBuilder().WithTcpServer("broker.hivemq.com").Build(); + var mqttClientOptions = new MqttClientOptionsBuilder().WithTcpServer("broker.hivemq.com").Build(); - await mqttClient.ConnectAsync(mqttClientOptions, CancellationToken.None); + await mqttClient.ConnectAsync(mqttClientOptions, CancellationToken.None); - var mqttSubscribeOptions = mqttFactory.CreateSubscribeOptionsBuilder().WithTopicTemplate(sampleTemplate.WithParameter("id", "1")).Build(); + var mqttSubscribeOptions = mqttFactory.CreateSubscribeOptionsBuilder().WithTopicTemplate(sampleTemplate.WithParameter("id", "1")).Build(); - var response = await mqttClient.SubscribeAsync(mqttSubscribeOptions, CancellationToken.None); + var response = await mqttClient.SubscribeAsync(mqttSubscribeOptions, CancellationToken.None); - Console.WriteLine("MQTT client subscribed to topic."); + Console.WriteLine("MQTT client subscribed to topic."); - // The response contains additional data sent by the server after subscribing. - response.DumpToConsole(); - } + // The response contains additional data sent by the server after subscribing. + response.DumpToConsole(); } public static async Task Subscribe_Multiple_Topics() @@ -102,27 +98,25 @@ public static class Client_Subscribe_Samples var mqttFactory = new MqttClientFactory(); - using (var mqttClient = mqttFactory.CreateMqttClient()) - { - var mqttClientOptions = new MqttClientOptionsBuilder().WithTcpServer("broker.hivemq.com").Build(); + using var mqttClient = mqttFactory.CreateMqttClient(); + var mqttClientOptions = new MqttClientOptionsBuilder().WithTcpServer("broker.hivemq.com").Build(); - await mqttClient.ConnectAsync(mqttClientOptions, CancellationToken.None); + await mqttClient.ConnectAsync(mqttClientOptions, CancellationToken.None); - // Create the subscribe options including several topics with different options. - // It is also possible to all of these topics using a dedicated call of _SubscribeAsync_ per topic. - var mqttSubscribeOptions = mqttFactory.CreateSubscribeOptionsBuilder() - .WithTopicTemplate(sampleTemplate.WithParameter("id", "1")) - .WithTopicTemplate(sampleTemplate.WithParameter("id", "2"), noLocal: true) - .WithTopicTemplate(sampleTemplate.WithParameter("id", "3"), retainHandling: MqttRetainHandling.SendAtSubscribe) - .Build(); + // Create the subscribe options including several topics with different options. + // It is also possible to all of these topics using a dedicated call of _SubscribeAsync_ per topic. + var mqttSubscribeOptions = mqttFactory.CreateSubscribeOptionsBuilder() + .WithTopicTemplate(sampleTemplate.WithParameter("id", "1")) + .WithTopicTemplate(sampleTemplate.WithParameter("id", "2"), noLocal: true) + .WithTopicTemplate(sampleTemplate.WithParameter("id", "3"), retainHandling: MqttRetainHandling.SendAtSubscribe) + .Build(); - var response = await mqttClient.SubscribeAsync(mqttSubscribeOptions, CancellationToken.None); + var response = await mqttClient.SubscribeAsync(mqttSubscribeOptions, CancellationToken.None); - Console.WriteLine("MQTT client subscribed to topics."); + Console.WriteLine("MQTT client subscribed to topics."); - // The response contains additional data sent by the server after subscribing. - response.DumpToConsole(); - } + // The response contains additional data sent by the server after subscribing. + response.DumpToConsole(); } public static async Task Subscribe_Topic() @@ -133,21 +127,19 @@ public static class Client_Subscribe_Samples var mqttFactory = new MqttClientFactory(); - using (var mqttClient = mqttFactory.CreateMqttClient()) - { - var mqttClientOptions = new MqttClientOptionsBuilder().WithTcpServer("broker.hivemq.com").Build(); + using var mqttClient = mqttFactory.CreateMqttClient(); + var mqttClientOptions = new MqttClientOptionsBuilder().WithTcpServer("broker.hivemq.com").Build(); - await mqttClient.ConnectAsync(mqttClientOptions, CancellationToken.None); + await mqttClient.ConnectAsync(mqttClientOptions, CancellationToken.None); - var mqttSubscribeOptions = mqttFactory.CreateSubscribeOptionsBuilder().WithTopicTemplate(sampleTemplate.WithParameter("id", "1")).Build(); + var mqttSubscribeOptions = mqttFactory.CreateSubscribeOptionsBuilder().WithTopicTemplate(sampleTemplate.WithParameter("id", "1")).Build(); - var response = await mqttClient.SubscribeAsync(mqttSubscribeOptions, CancellationToken.None); + var response = await mqttClient.SubscribeAsync(mqttSubscribeOptions, CancellationToken.None); - Console.WriteLine("MQTT client subscribed to topic."); + Console.WriteLine("MQTT client subscribed to topic."); - // The response contains additional data sent by the server after subscribing. - response.DumpToConsole(); - } + // The response contains additional data sent by the server after subscribing. + response.DumpToConsole(); } static void ConcurrentProcessingDisableAutoAcknowledge(CancellationToken shutdownToken, IMqttClient mqttClient) diff --git a/Samples/Diagnostics/Logger_Samples.cs b/Samples/Diagnostics/Logger_Samples.cs index 505ad748..1e6d9b2d 100644 --- a/Samples/Diagnostics/Logger_Samples.cs +++ b/Samples/Diagnostics/Logger_Samples.cs @@ -26,17 +26,15 @@ public static class Logger_Samples .WithTcpServer("broker.hivemq.com") .Build(); - using (var mqttClient = mqttFactory.CreateMqttClient()) - { - await mqttClient.ConnectAsync(mqttClientOptions, CancellationToken.None); + using var mqttClient = mqttFactory.CreateMqttClient(); + await mqttClient.ConnectAsync(mqttClientOptions, CancellationToken.None); - Console.WriteLine("MQTT client is connected."); + Console.WriteLine("MQTT client is connected."); - var mqttClientDisconnectOptions = mqttFactory.CreateClientDisconnectOptionsBuilder() - .Build(); + var mqttClientDisconnectOptions = mqttFactory.CreateClientDisconnectOptionsBuilder() + .Build(); - await mqttClient.DisconnectAsync(mqttClientDisconnectOptions, CancellationToken.None); - } + await mqttClient.DisconnectAsync(mqttClientDisconnectOptions, CancellationToken.None); } public static async Task Use_Event_Logger() @@ -50,7 +48,7 @@ public static class Logger_Samples // The logger ID is optional but can be set do distinguish different logger instances. var mqttEventLogger = new MqttNetEventLogger("MyCustomLogger"); - mqttEventLogger.LogMessagePublished += (sender, args) => + mqttEventLogger.LogMessagePublished += (_, args) => { var output = new StringBuilder(); output.AppendLine($">> [{args.LogMessage.Timestamp:O}] [{args.LogMessage.ThreadId}] [{args.LogMessage.Source}] [{args.LogMessage.Level}]: {args.LogMessage.Message}"); @@ -68,17 +66,15 @@ public static class Logger_Samples .WithTcpServer("broker.hivemq.com") .Build(); - using (var mqttClient = mqttFactory.CreateMqttClient()) - { - await mqttClient.ConnectAsync(mqttClientOptions, CancellationToken.None); + using var mqttClient = mqttFactory.CreateMqttClient(); + await mqttClient.ConnectAsync(mqttClientOptions, CancellationToken.None); - Console.WriteLine("MQTT client is connected."); + Console.WriteLine("MQTT client is connected."); - var mqttClientDisconnectOptions = mqttFactory.CreateClientDisconnectOptionsBuilder() - .Build(); + var mqttClientDisconnectOptions = mqttFactory.CreateClientDisconnectOptionsBuilder() + .Build(); - await mqttClient.DisconnectAsync(mqttClientDisconnectOptions, CancellationToken.None); - } + await mqttClient.DisconnectAsync(mqttClientDisconnectOptions, CancellationToken.None); } sealed class MyLogger : IMqttNetLogger diff --git a/Samples/Diagnostics/PackageInspection_Samples.cs b/Samples/Diagnostics/PackageInspection_Samples.cs index 1ab996a2..7a4d94b3 100644 --- a/Samples/Diagnostics/PackageInspection_Samples.cs +++ b/Samples/Diagnostics/PackageInspection_Samples.cs @@ -20,23 +20,21 @@ public static class PackageInspection_Samples var mqttFactory = new MqttClientFactory(); - using (var mqttClient = mqttFactory.CreateMqttClient()) - { - var mqttClientOptions = mqttFactory.CreateClientOptionsBuilder() - .WithTcpServer("broker.hivemq.com") - .Build(); + using var mqttClient = mqttFactory.CreateMqttClient(); + var mqttClientOptions = mqttFactory.CreateClientOptionsBuilder() + .WithTcpServer("broker.hivemq.com") + .Build(); - mqttClient.InspectPacketAsync += OnInspectPacket; + mqttClient.InspectPacketAsync += OnInspectPacket; - await mqttClient.ConnectAsync(mqttClientOptions, CancellationToken.None); + await mqttClient.ConnectAsync(mqttClientOptions, CancellationToken.None); - Console.WriteLine("MQTT client is connected."); + Console.WriteLine("MQTT client is connected."); - var mqttClientDisconnectOptions = mqttFactory.CreateClientDisconnectOptionsBuilder() - .Build(); + var mqttClientDisconnectOptions = mqttFactory.CreateClientDisconnectOptionsBuilder() + .Build(); - await mqttClient.DisconnectAsync(mqttClientDisconnectOptions, CancellationToken.None); - } + await mqttClient.DisconnectAsync(mqttClientDisconnectOptions, CancellationToken.None); } static Task OnInspectPacket(InspectMqttPacketEventArgs eventArgs) diff --git a/Samples/RpcClient/RpcClient_Samples.cs b/Samples/RpcClient/RpcClient_Samples.cs index b50c9121..5d3c0b23 100644 --- a/Samples/RpcClient/RpcClient_Samples.cs +++ b/Samples/RpcClient/RpcClient_Samples.cs @@ -26,23 +26,21 @@ public static class RpcClient_Samples // The RPC client is an addon for the existing client. So we need a regular client // which is wrapped later. - using (var mqttClient = mqttFactory.CreateMqttClient()) + using var mqttClient = mqttFactory.CreateMqttClient(); + var mqttClientOptions = new MqttClientOptionsBuilder() + .WithTcpServer("broker.hivemq.com") + .Build(); + + await mqttClient.ConnectAsync(mqttClientOptions); + + using (var mqttRpcClient = mqttFactory.CreateMqttRpcClient(mqttClient)) { - var mqttClientOptions = new MqttClientOptionsBuilder() - .WithTcpServer("broker.hivemq.com") - .Build(); - - await mqttClient.ConnectAsync(mqttClientOptions); - - using (var mqttRpcClient = mqttFactory.CreateMqttRpcClient(mqttClient)) - { - // Access to a fully featured application message is not supported for RPC calls! - // The method will throw an exception when the response was not received in time. - await mqttRpcClient.ExecuteAsync(TimeSpan.FromSeconds(2), "ping", "", MqttQualityOfServiceLevel.AtMostOnce); - } - - Console.WriteLine("The RPC call was successful."); + // Access to a fully featured application message is not supported for RPC calls! + // The method will throw an exception when the response was not received in time. + await mqttRpcClient.ExecuteAsync(TimeSpan.FromSeconds(2), "ping", "", MqttQualityOfServiceLevel.AtMostOnce); } + + Console.WriteLine("The RPC call was successful."); } /* diff --git a/Samples/Server/Server_Diagnostics_Samples.cs b/Samples/Server/Server_Diagnostics_Samples.cs index 07bc8e0b..3994e9c7 100644 --- a/Samples/Server/Server_Diagnostics_Samples.cs +++ b/Samples/Server/Server_Diagnostics_Samples.cs @@ -25,28 +25,26 @@ public static class Server_Diagnostics_Samples var mqttServerFactory = new MqttServerFactory(); var mqttServerOptions = new MqttServerOptionsBuilder().WithDefaultEndpoint().Build(); - using (var mqttServer = mqttServerFactory.CreateMqttServer(mqttServerOptions)) + using var mqttServer = mqttServerFactory.CreateMqttServer(mqttServerOptions); + // Attach the event handler. + mqttServer.ClientAcknowledgedPublishPacketAsync += e => { - // Attach the event handler. - mqttServer.ClientAcknowledgedPublishPacketAsync += e => - { - Console.WriteLine($"Client '{e.ClientId}' acknowledged packet {e.PublishPacket.PacketIdentifier} with topic '{e.PublishPacket.Topic}'"); + Console.WriteLine($"Client '{e.ClientId}' acknowledged packet {e.PublishPacket.PacketIdentifier} with topic '{e.PublishPacket.Topic}'"); - // It is also possible to read additional data from the client response. This requires casting the response packet. - var qos1AcknowledgePacket = e.AcknowledgePacket as MqttPubAckPacket; - Console.WriteLine($"QoS 1 reason code: {qos1AcknowledgePacket?.ReasonCode}"); + // It is also possible to read additional data from the client response. This requires casting the response packet. + var qos1AcknowledgePacket = e.AcknowledgePacket as MqttPubAckPacket; + Console.WriteLine($"QoS 1 reason code: {qos1AcknowledgePacket?.ReasonCode}"); - var qos2AcknowledgePacket = e.AcknowledgePacket as MqttPubCompPacket; - Console.WriteLine($"QoS 2 reason code: {qos1AcknowledgePacket?.ReasonCode}"); - return CompletedTask.Instance; - }; + var qos2AcknowledgePacket = e.AcknowledgePacket as MqttPubCompPacket; + Console.WriteLine($"QoS 2 reason code: {qos2AcknowledgePacket?.ReasonCode}"); + return CompletedTask.Instance; + }; - await mqttServer.StartAsync(); + await mqttServer.StartAsync(); - Console.WriteLine("Press Enter to exit."); - Console.ReadLine(); + Console.WriteLine("Press Enter to exit."); + Console.ReadLine(); - await mqttServer.StopAsync(); - } + await mqttServer.StopAsync(); } } \ No newline at end of file diff --git a/Samples/Server/Server_Intercepting_Samples.cs b/Samples/Server/Server_Intercepting_Samples.cs index 31bfa39b..68c5c8ba 100644 --- a/Samples/Server/Server_Intercepting_Samples.cs +++ b/Samples/Server/Server_Intercepting_Samples.cs @@ -25,23 +25,21 @@ public static class Server_Intercepting_Samples var mqttServerFactory = new MqttServerFactory(); var mqttServerOptions = new MqttServerOptionsBuilder().WithDefaultEndpoint().Build(); - using (var mqttServer = mqttServerFactory.CreateMqttServer(mqttServerOptions)) + using var mqttServer = mqttServerFactory.CreateMqttServer(mqttServerOptions); + mqttServer.InterceptingPublishAsync += args => { - mqttServer.InterceptingPublishAsync += args => - { - // Here we only change the topic of the received application message. - // but also changing the payload etc. is required. Changing the QoS after - // transmitting is not supported and makes no sense at all. - args.ApplicationMessage.Topic += "/manipulated"; + // Here we only change the topic of the received application message. + // but also changing the payload etc. is required. Changing the QoS after + // transmitting is not supported and makes no sense at all. + args.ApplicationMessage.Topic += "/manipulated"; - return CompletedTask.Instance; - }; + return CompletedTask.Instance; + }; - await mqttServer.StartAsync(); + await mqttServer.StartAsync(); - Console.WriteLine("Press Enter to exit."); - Console.ReadLine(); - await mqttServer.StopAsync(); - } + Console.WriteLine("Press Enter to exit."); + Console.ReadLine(); + await mqttServer.StopAsync(); } } \ No newline at end of file diff --git a/Samples/Server/Server_Retained_Messages_Samples.cs b/Samples/Server/Server_Retained_Messages_Samples.cs index 4564acae..38159ce3 100644 --- a/Samples/Server/Server_Retained_Messages_Samples.cs +++ b/Samples/Server/Server_Retained_Messages_Samples.cs @@ -29,64 +29,62 @@ public static class Server_Retained_Messages_Samples // Due to security reasons the "default" endpoint (which is unencrypted) is not enabled by default! var mqttServerOptions = mqttServerFactory.CreateServerOptionsBuilder().WithDefaultEndpoint().Build(); - using (var server = mqttServerFactory.CreateMqttServer(mqttServerOptions)) + using var server = mqttServerFactory.CreateMqttServer(mqttServerOptions); + // Make sure that the server will load the retained messages. + server.LoadingRetainedMessageAsync += async eventArgs => { - // Make sure that the server will load the retained messages. - server.LoadingRetainedMessageAsync += async eventArgs => + try { - try - { - var models = await JsonSerializer.DeserializeAsync>(File.OpenRead(storePath)) ?? new List(); - var retainedMessages = models.Select(m => m.ToApplicationMessage()).ToList(); + var models = await JsonSerializer.DeserializeAsync>(File.OpenRead(storePath)) ?? new List(); + var retainedMessages = models.Select(m => m.ToApplicationMessage()).ToList(); - eventArgs.LoadedRetainedMessages = retainedMessages; - Console.WriteLine("Retained messages loaded."); - } - catch (FileNotFoundException) - { - // Ignore because nothing is stored yet. - Console.WriteLine("No retained messages stored yet."); - } - catch (Exception exception) - { - Console.WriteLine(exception); - } - }; - - // Make sure to persist the changed retained messages. - server.RetainedMessageChangedAsync += async eventArgs => + eventArgs.LoadedRetainedMessages = retainedMessages; + Console.WriteLine("Retained messages loaded."); + } + catch (FileNotFoundException) { - try - { - // This sample uses the property _StoredRetainedMessages_ which will contain all(!) retained messages. - // The event args also contain the affected retained message (property ChangedRetainedMessage). This can be - // used to write all retained messages to dedicated files etc. Then all files must be loaded and a full list - // of retained messages must be provided in the loaded event. - - var models = eventArgs.StoredRetainedMessages.Select(MqttRetainedMessageModel.Create); - - var buffer = JsonSerializer.SerializeToUtf8Bytes(models); - await File.WriteAllBytesAsync(storePath, buffer); - Console.WriteLine("Retained messages saved."); - } - catch (Exception exception) - { - Console.WriteLine(exception); - } - }; - - // Make sure to clear the retained messages when they are all deleted via API. - server.RetainedMessagesClearedAsync += _ => + // Ignore because nothing is stored yet. + Console.WriteLine("No retained messages stored yet."); + } + catch (Exception exception) { - File.Delete(storePath); - return Task.CompletedTask; - }; + Console.WriteLine(exception); + } + }; - await server.StartAsync(); + // Make sure to persist the changed retained messages. + server.RetainedMessageChangedAsync += async eventArgs => + { + try + { + // This sample uses the property _StoredRetainedMessages_ which will contain all(!) retained messages. + // The event args also contain the affected retained message (property ChangedRetainedMessage). This can be + // used to write all retained messages to dedicated files etc. Then all files must be loaded and a full list + // of retained messages must be provided in the loaded event. - Console.WriteLine("Press Enter to exit."); - Console.ReadLine(); - } + var models = eventArgs.StoredRetainedMessages.Select(MqttRetainedMessageModel.Create); + + var buffer = JsonSerializer.SerializeToUtf8Bytes(models); + await File.WriteAllBytesAsync(storePath, buffer); + Console.WriteLine("Retained messages saved."); + } + catch (Exception exception) + { + Console.WriteLine(exception); + } + }; + + // Make sure to clear the retained messages when they are all deleted via API. + server.RetainedMessagesClearedAsync += _ => + { + File.Delete(storePath); + return Task.CompletedTask; + }; + + await server.StartAsync(); + + Console.WriteLine("Press Enter to exit."); + Console.ReadLine(); } sealed class MqttRetainedMessageModel diff --git a/Samples/Server/Server_Simple_Samples.cs b/Samples/Server/Server_Simple_Samples.cs index e7ed4bfe..06eb9e86 100644 --- a/Samples/Server/Server_Simple_Samples.cs +++ b/Samples/Server/Server_Simple_Samples.cs @@ -22,17 +22,15 @@ public static class Server_Simple_Samples * See _Run_Minimal_Server_ for more information. */ - using (var mqttServer = await StartMqttServer()) - { - // Let the client connect. - await Task.Delay(TimeSpan.FromSeconds(5)); + using var mqttServer = await StartMqttServer(); + // Let the client connect. + await Task.Delay(TimeSpan.FromSeconds(5)); - // Now disconnect the client (if connected). - var affectedClient = (await mqttServer.GetClientsAsync()).FirstOrDefault(c => c.Id == "MyClient"); - if (affectedClient != null) - { - await affectedClient.DisconnectAsync(); - } + // Now disconnect the client (if connected). + var affectedClient = (await mqttServer.GetClientsAsync()).FirstOrDefault(c => c.Id == "MyClient"); + if (affectedClient != null) + { + await affectedClient.DisconnectAsync(); } } @@ -44,18 +42,16 @@ public static class Server_Simple_Samples * See _Run_Minimal_Server_ for more information. */ - using (var mqttServer = await StartMqttServer()) - { - // Create a new message using the builder as usual. - var message = new MqttApplicationMessageBuilder().WithTopic("HelloWorld").WithPayload("Test").Build(); + using var mqttServer = await StartMqttServer(); + // Create a new message using the builder as usual. + var message = new MqttApplicationMessageBuilder().WithTopic("HelloWorld").WithPayload("Test").Build(); - // Now inject the new message at the broker. - await mqttServer.InjectApplicationMessage( - new InjectedMqttApplicationMessage(message) - { - SenderClientId = "SenderClientId" - }); - } + // Now inject the new message at the broker. + await mqttServer.InjectApplicationMessage( + new InjectedMqttApplicationMessage(message) + { + SenderClientId = "SenderClientId" + }); } public static async Task Run_Minimal_Server() @@ -77,16 +73,14 @@ public static class Server_Simple_Samples // .WithDefaultEndpointPort(1234) // .Build(); - using (var mqttServer = mqttServerFactory.CreateMqttServer(mqttServerOptions)) - { - await mqttServer.StartAsync(); + using var mqttServer = mqttServerFactory.CreateMqttServer(mqttServerOptions); + await mqttServer.StartAsync(); - Console.WriteLine("Press Enter to exit."); - Console.ReadLine(); + Console.WriteLine("Press Enter to exit."); + Console.ReadLine(); - // Stop and dispose the MQTT server if it is no longer needed! - await mqttServer.StopAsync(); - } + // Stop and dispose the MQTT server if it is no longer needed! + await mqttServer.StopAsync(); } public static async Task Run_Server_With_Logging() @@ -103,16 +97,14 @@ public static class Server_Simple_Samples var mqttServerOptions = new MqttServerOptionsBuilder().WithDefaultEndpoint().Build(); - using (var mqttServer = mqttServerFactory.CreateMqttServer(mqttServerOptions)) - { - await mqttServer.StartAsync(); + using var mqttServer = mqttServerFactory.CreateMqttServer(mqttServerOptions); + await mqttServer.StartAsync(); - Console.WriteLine("Press Enter to exit."); - Console.ReadLine(); + Console.WriteLine("Press Enter to exit."); + Console.ReadLine(); - // Stop and dispose the MQTT server if it is no longer needed! - await mqttServer.StopAsync(); - } + // Stop and dispose the MQTT server if it is no longer needed! + await mqttServer.StopAsync(); } public static async Task Validating_Connections() @@ -127,37 +119,35 @@ public static class Server_Simple_Samples var mqttServerOptions = new MqttServerOptionsBuilder().WithDefaultEndpoint().Build(); - using (var mqttServer = mqttServerFactory.CreateMqttServer(mqttServerOptions)) + using var mqttServer = mqttServerFactory.CreateMqttServer(mqttServerOptions); + // Setup connection validation before starting the server so that there is + // no change to connect without valid credentials. + mqttServer.ValidatingConnectionAsync += e => { - // Setup connection validation before starting the server so that there is - // no change to connect without valid credentials. - mqttServer.ValidatingConnectionAsync += e => + if (e.ClientId != "ValidClientId") { - if (e.ClientId != "ValidClientId") - { - e.ReasonCode = MqttConnectReasonCode.ClientIdentifierNotValid; - } + e.ReasonCode = MqttConnectReasonCode.ClientIdentifierNotValid; + } - if (e.UserName != "ValidUser") - { - e.ReasonCode = MqttConnectReasonCode.BadUserNameOrPassword; - } + if (e.UserName != "ValidUser") + { + e.ReasonCode = MqttConnectReasonCode.BadUserNameOrPassword; + } - if (e.Password != "SecretPassword") - { - e.ReasonCode = MqttConnectReasonCode.BadUserNameOrPassword; - } + if (e.Password != "SecretPassword") + { + e.ReasonCode = MqttConnectReasonCode.BadUserNameOrPassword; + } - return Task.CompletedTask; - }; + return Task.CompletedTask; + }; - await mqttServer.StartAsync(); + await mqttServer.StartAsync(); - Console.WriteLine("Press Enter to exit."); - Console.ReadLine(); + Console.WriteLine("Press Enter to exit."); + Console.ReadLine(); - await mqttServer.StopAsync(); - } + await mqttServer.StopAsync(); } static async Task StartMqttServer() diff --git a/Samples/Server/Server_TLS_Samples.cs b/Samples/Server/Server_TLS_Samples.cs index 83d0d601..ff859ea3 100644 --- a/Samples/Server/Server_TLS_Samples.cs +++ b/Samples/Server/Server_TLS_Samples.cs @@ -31,16 +31,14 @@ public static class Server_TLS_Samples var mqttServerOptions = new MqttServerOptionsBuilder().WithEncryptionCertificate(certificate).WithEncryptedEndpoint().Build(); - using (var mqttServer = mqttServerFactory.CreateMqttServer(mqttServerOptions)) - { - await mqttServer.StartAsync(); + using var mqttServer = mqttServerFactory.CreateMqttServer(mqttServerOptions); + await mqttServer.StartAsync(); - Console.WriteLine("Press Enter to exit."); - Console.ReadLine(); + Console.WriteLine("Press Enter to exit."); + Console.ReadLine(); - // Stop and dispose the MQTT server if it is no longer needed! - await mqttServer.StopAsync(); - } + // Stop and dispose the MQTT server if it is no longer needed! + await mqttServer.StopAsync(); } static X509Certificate2 CreateSelfSignedCertificate(string oid) @@ -50,26 +48,22 @@ public static class Server_TLS_Samples sanBuilder.AddIpAddress(IPAddress.IPv6Loopback); sanBuilder.AddDnsName("localhost"); - using (var rsa = RSA.Create()) - { - var certRequest = new CertificateRequest("CN=localhost", rsa, HashAlgorithmName.SHA512, RSASignaturePadding.Pkcs1); + using var rsa = RSA.Create(); + var certRequest = new CertificateRequest("CN=localhost", rsa, HashAlgorithmName.SHA512, RSASignaturePadding.Pkcs1); - certRequest.CertificateExtensions.Add( - new X509KeyUsageExtension(X509KeyUsageFlags.DataEncipherment | X509KeyUsageFlags.KeyEncipherment | X509KeyUsageFlags.DigitalSignature, false)); + certRequest.CertificateExtensions.Add( + new X509KeyUsageExtension(X509KeyUsageFlags.DataEncipherment | X509KeyUsageFlags.KeyEncipherment | X509KeyUsageFlags.DigitalSignature, false)); - certRequest.CertificateExtensions.Add(new X509EnhancedKeyUsageExtension(new OidCollection { new(oid) }, false)); + certRequest.CertificateExtensions.Add(new X509EnhancedKeyUsageExtension(new OidCollection { new(oid) }, false)); - certRequest.CertificateExtensions.Add(sanBuilder.Build()); + certRequest.CertificateExtensions.Add(sanBuilder.Build()); - using (var certificate = certRequest.CreateSelfSigned(DateTimeOffset.Now.AddMinutes(-10), DateTimeOffset.Now.AddMinutes(10))) - { - var pfxCertificate = new X509Certificate2( - certificate.Export(X509ContentType.Pfx), - (string)null!, - X509KeyStorageFlags.MachineKeySet | X509KeyStorageFlags.Exportable); + using var certificate = certRequest.CreateSelfSigned(DateTimeOffset.Now.AddMinutes(-10), DateTimeOffset.Now.AddMinutes(10)); + var pfxCertificate = new X509Certificate2( + certificate.Export(X509ContentType.Pfx), + (string)null!, + X509KeyStorageFlags.MachineKeySet | X509KeyStorageFlags.Exportable); - return pfxCertificate; - } - } + return pfxCertificate; } } \ No newline at end of file diff --git a/Source/MQTTnet.AspTestApp/Pages/Index.cshtml.cs b/Source/MQTTnet.AspTestApp/Pages/Index.cshtml.cs index 558db0f5..e7293992 100644 --- a/Source/MQTTnet.AspTestApp/Pages/Index.cshtml.cs +++ b/Source/MQTTnet.AspTestApp/Pages/Index.cshtml.cs @@ -4,20 +4,19 @@ using Microsoft.AspNetCore.Mvc.RazorPages; -namespace MQTTnet.AspTestApp.Pages +namespace MQTTnet.AspTestApp.Pages; + +public class IndexModel : PageModel { - public class IndexModel : PageModel + readonly ILogger _logger; + + public IndexModel(ILogger logger) { - private readonly ILogger _logger; + _logger = logger; + } - public IndexModel(ILogger logger) - { - _logger = logger; - } - - public void OnGet() - { - - } + public void OnGet() + { + _logger.LogDebug("OnGet"); } } \ No newline at end of file diff --git a/Source/MQTTnet.AspnetCore/ApplicationBuilderExtensions.cs b/Source/MQTTnet.AspnetCore/ApplicationBuilderExtensions.cs index f98983da..62699181 100644 --- a/Source/MQTTnet.AspnetCore/ApplicationBuilderExtensions.cs +++ b/Source/MQTTnet.AspnetCore/ApplicationBuilderExtensions.cs @@ -33,10 +33,8 @@ public static class ApplicationBuilderExtensions } var adapter = app.ApplicationServices.GetRequiredService(); - using (var webSocket = await context.WebSockets.AcceptWebSocketAsync(subProtocol).ConfigureAwait(false)) - { - await adapter.RunWebSocketConnectionAsync(webSocket, context); - } + using var webSocket = await context.WebSockets.AcceptWebSocketAsync(subProtocol).ConfigureAwait(false); + await adapter.RunWebSocketConnectionAsync(webSocket, context); }); return app; diff --git a/Source/MQTTnet.AspnetCore/ConnectionBuilderExtensions.cs b/Source/MQTTnet.AspnetCore/ConnectionBuilderExtensions.cs index 9ea8922e..8eee0bae 100644 --- a/Source/MQTTnet.AspnetCore/ConnectionBuilderExtensions.cs +++ b/Source/MQTTnet.AspnetCore/ConnectionBuilderExtensions.cs @@ -4,13 +4,12 @@ using Microsoft.AspNetCore.Connections; -namespace MQTTnet.AspNetCore +namespace MQTTnet.AspNetCore; + +public static class ConnectionBuilderExtensions { - public static class ConnectionBuilderExtensions + public static IConnectionBuilder UseMqtt(this IConnectionBuilder builder) { - public static IConnectionBuilder UseMqtt(this IConnectionBuilder builder) - { - return builder.UseConnectionHandler(); - } + return builder.UseConnectionHandler(); } -} +} \ No newline at end of file diff --git a/Source/MQTTnet.AspnetCore/EndpointRouterExtensions.cs b/Source/MQTTnet.AspnetCore/EndpointRouterExtensions.cs index 8e96a6c2..af8bbcd2 100644 --- a/Source/MQTTnet.AspnetCore/EndpointRouterExtensions.cs +++ b/Source/MQTTnet.AspnetCore/EndpointRouterExtensions.cs @@ -6,19 +6,17 @@ using System; using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Routing; -namespace MQTTnet.AspNetCore +namespace MQTTnet.AspNetCore; + +public static class EndpointRouterExtensions { - public static class EndpointRouterExtensions + public static void MapMqtt(this IEndpointRouteBuilder endpoints, string pattern) { - public static void MapMqtt(this IEndpointRouteBuilder endpoints, string pattern) + ArgumentNullException.ThrowIfNull(endpoints); + + endpoints.MapConnectionHandler(pattern, options => { - ArgumentNullException.ThrowIfNull(endpoints); - - endpoints.MapConnectionHandler(pattern, options => - { - options.WebSockets.SubProtocolSelector = MqttSubProtocolSelector.SelectSubProtocol; - }); - } + options.WebSockets.SubProtocolSelector = MqttSubProtocolSelector.SelectSubProtocol; + }); } -} - +} \ No newline at end of file diff --git a/Source/MQTTnet.AspnetCore/MqttClientConnectionContextFactory.cs b/Source/MQTTnet.AspnetCore/MqttClientConnectionContextFactory.cs index 0ddbbd8f..fadf9065 100644 --- a/Source/MQTTnet.AspnetCore/MqttClientConnectionContextFactory.cs +++ b/Source/MQTTnet.AspnetCore/MqttClientConnectionContextFactory.cs @@ -7,28 +7,27 @@ using MQTTnet.Adapter; using MQTTnet.Diagnostics.Logger; using MQTTnet.Formatter; -namespace MQTTnet.AspNetCore +namespace MQTTnet.AspNetCore; + +public sealed class MqttClientConnectionContextFactory : IMqttClientAdapterFactory { - public sealed class MqttClientConnectionContextFactory : IMqttClientAdapterFactory + public IMqttChannelAdapter CreateClientAdapter(MqttClientOptions options, MqttPacketInspector packetInspector, IMqttNetLogger logger) { - public IMqttChannelAdapter CreateClientAdapter(MqttClientOptions options, MqttPacketInspector packetInspector, IMqttNetLogger logger) + if (options == null) throw new ArgumentNullException(nameof(options)); + + switch (options.ChannelOptions) { - if (options == null) throw new ArgumentNullException(nameof(options)); - - switch (options.ChannelOptions) + case MqttClientTcpOptions tcpOptions: { - case MqttClientTcpOptions tcpOptions: - { - var tcpConnection = new SocketConnection(tcpOptions.RemoteEndpoint); + var tcpConnection = new SocketConnection(tcpOptions.RemoteEndpoint); - var formatter = new MqttPacketFormatterAdapter(options.ProtocolVersion, new MqttBufferWriter(4096, 65535)); - return new MqttConnectionContext(formatter, tcpConnection); - } - default: - { - throw new NotSupportedException(); - } + var formatter = new MqttPacketFormatterAdapter(options.ProtocolVersion, new MqttBufferWriter(4096, 65535)); + return new MqttConnectionContext(formatter, tcpConnection); + } + default: + { + throw new NotSupportedException(); } } } -} +} \ No newline at end of file diff --git a/Source/MQTTnet.AspnetCore/MqttConnectionHandler.cs b/Source/MQTTnet.AspnetCore/MqttConnectionHandler.cs index b4cbc42a..dfeb2f3f 100644 --- a/Source/MQTTnet.AspnetCore/MqttConnectionHandler.cs +++ b/Source/MQTTnet.AspnetCore/MqttConnectionHandler.cs @@ -33,13 +33,11 @@ public sealed class MqttConnectionHandler : ConnectionHandler, IMqttServerAdapte } var formatter = new MqttPacketFormatterAdapter(new MqttBufferWriter(_serverOptions.WriterBufferSize, _serverOptions.WriterBufferSizeMax)); - using (var adapter = new MqttConnectionContext(formatter, connection)) + using var adapter = new MqttConnectionContext(formatter, connection); + var clientHandler = ClientHandler; + if (clientHandler != null) { - var clientHandler = ClientHandler; - if (clientHandler != null) - { - await clientHandler(adapter).ConfigureAwait(false); - } + await clientHandler(adapter).ConfigureAwait(false); } } diff --git a/Source/MQTTnet.AspnetCore/MqttWebSocketServerAdapter.cs b/Source/MQTTnet.AspnetCore/MqttWebSocketServerAdapter.cs index 18a382a1..6f28c1e2 100644 --- a/Source/MQTTnet.AspnetCore/MqttWebSocketServerAdapter.cs +++ b/Source/MQTTnet.AspnetCore/MqttWebSocketServerAdapter.cs @@ -43,10 +43,8 @@ public sealed class MqttWebSocketServerAdapter : IMqttServerAdapter var formatter = new MqttPacketFormatterAdapter(new MqttBufferWriter(4096, 65535)); var channel = new MqttWebSocketChannel(webSocket, remoteEndPoint, isSecureConnection, clientCertificate); - using (var channelAdapter = new MqttChannelAdapter(channel, formatter, _logger)) - { - await clientHandler(channelAdapter).ConfigureAwait(false); - } + using var channelAdapter = new MqttChannelAdapter(channel, formatter, _logger); + await clientHandler(channelAdapter).ConfigureAwait(false); } } finally diff --git a/Source/MQTTnet.AspnetCore/SocketAwaitable.cs b/Source/MQTTnet.AspnetCore/SocketAwaitable.cs index 2c960727..17e03db8 100644 --- a/Source/MQTTnet.AspnetCore/SocketAwaitable.cs +++ b/Source/MQTTnet.AspnetCore/SocketAwaitable.cs @@ -12,34 +12,28 @@ using System.Threading.Tasks; namespace MQTTnet.AspNetCore; -public class SocketAwaitable : ICriticalNotifyCompletion +public class SocketAwaitable(PipeScheduler ioScheduler) : ICriticalNotifyCompletion { - static readonly Action _callbackCompleted = () => + static readonly Action CallbackCompleted = () => { }; - readonly PipeScheduler _ioScheduler; int _bytesTransferred; Action _callback; SocketError _error; - public SocketAwaitable(PipeScheduler ioScheduler) - { - _ioScheduler = ioScheduler; - } - - public bool IsCompleted => ReferenceEquals(_callback, _callbackCompleted); + public bool IsCompleted => ReferenceEquals(_callback, CallbackCompleted); public void Complete(int bytesTransferred, SocketError socketError) { _error = socketError; _bytesTransferred = bytesTransferred; - var continuation = Interlocked.Exchange(ref _callback, _callbackCompleted); + var continuation = Interlocked.Exchange(ref _callback, CallbackCompleted); if (continuation != null) { - _ioScheduler.Schedule(state => ((Action)state)(), continuation); + ioScheduler.Schedule(state => ((Action)state)(), continuation); } } @@ -50,7 +44,7 @@ public class SocketAwaitable : ICriticalNotifyCompletion public int GetResult() { - Debug.Assert(ReferenceEquals(_callback, _callbackCompleted)); + Debug.Assert(ReferenceEquals(_callback, CallbackCompleted)); _callback = null; @@ -64,7 +58,7 @@ public class SocketAwaitable : ICriticalNotifyCompletion public void OnCompleted(Action continuation) { - if (ReferenceEquals(_callback, _callbackCompleted) || ReferenceEquals(Interlocked.CompareExchange(ref _callback, continuation, null), _callbackCompleted)) + if (ReferenceEquals(_callback, CallbackCompleted) || ReferenceEquals(Interlocked.CompareExchange(ref _callback, continuation, null), CallbackCompleted)) { Task.Run(continuation); } diff --git a/Source/MQTTnet.AspnetCore/SocketConnection.cs b/Source/MQTTnet.AspnetCore/SocketConnection.cs index 2021ecca..ee5f1cb3 100644 --- a/Source/MQTTnet.AspnetCore/SocketConnection.cs +++ b/Source/MQTTnet.AspnetCore/SocketConnection.cs @@ -18,11 +18,11 @@ namespace MQTTnet.AspNetCore; public sealed class SocketConnection : ConnectionContext { readonly EndPoint _endPoint; + volatile bool _aborted; IDuplexPipe _application; SocketReceiver _receiver; SocketSender _sender; - Socket _socket; public SocketConnection(EndPoint endPoint) @@ -40,10 +40,13 @@ public sealed class SocketConnection : ConnectionContext } public override string ConnectionId { get; set; } + public override IFeatureCollection Features { get; } public bool IsConnected { get; private set; } + public override IDictionary Items { get; set; } + public override IDuplexPipe Transport { get; set; } public override ValueTask DisposeAsync() @@ -78,7 +81,7 @@ public sealed class SocketConnection : ConnectionContext IsConnected = true; } - Exception ConnectionAborted() + static Exception ConnectionAborted() { return new MqttCommunicationException("Connection Aborted"); } @@ -95,8 +98,7 @@ public sealed class SocketConnection : ConnectionContext { error = new MqttCommunicationException(ex); } - catch (SocketException ex) when (ex.SocketErrorCode == SocketError.OperationAborted || ex.SocketErrorCode == SocketError.ConnectionAborted || - ex.SocketErrorCode == SocketError.Interrupted || ex.SocketErrorCode == SocketError.InvalidArgument) + catch (SocketException ex) when (ex.SocketErrorCode is SocketError.OperationAborted or SocketError.ConnectionAborted or SocketError.Interrupted or SocketError.InvalidArgument) { if (!_aborted) { @@ -123,10 +125,10 @@ public sealed class SocketConnection : ConnectionContext { if (_aborted) { - error = error ?? ConnectionAborted(); + error ??= ConnectionAborted(); } - _application.Output.Complete(error); + await _application.Output.CompleteAsync(error); } } diff --git a/Source/MQTTnet.Benchmarks/AsyncLockBenchmark.cs b/Source/MQTTnet.Benchmarks/AsyncLockBenchmark.cs index 27a348d4..4912fcf4 100644 --- a/Source/MQTTnet.Benchmarks/AsyncLockBenchmark.cs +++ b/Source/MQTTnet.Benchmarks/AsyncLockBenchmark.cs @@ -9,57 +9,54 @@ using BenchmarkDotNet.Attributes; using BenchmarkDotNet.Jobs; using MQTTnet.Internal; -namespace MQTTnet.Benchmarks +namespace MQTTnet.Benchmarks; + +[SimpleJob(RuntimeMoniker.Net60)] +[MemoryDiagnoser] +public class AsyncLockBenchmark : BaseBenchmark { - [SimpleJob(RuntimeMoniker.Net60)] - [MemoryDiagnoser] - public class AsyncLockBenchmark : BaseBenchmark + [Benchmark] + public async Task Synchronize_100_Tasks() { - [Benchmark] - public async Task Synchronize_100_Tasks() + const int tasksCount = 100; + + var tasks = new Task[tasksCount]; + var asyncLock = new AsyncLock(); + var globalI = 0; + + for (var i = 0; i < tasksCount; i++) { - const int tasksCount = 100; - - var tasks = new Task[tasksCount]; - var asyncLock = new AsyncLock(); - var globalI = 0; - - for (var i = 0; i < tasksCount; i++) - { - tasks[i] = Task.Run( - async () => - { - using (await asyncLock.EnterAsync().ConfigureAwait(false)) - { - var localI = globalI; - await Task.Delay(5).ConfigureAwait(false); // Increase the chance for wrong data. - localI++; - globalI = localI; - } - }); - } - - await Task.WhenAll(tasks).ConfigureAwait(false); - - if (globalI != tasksCount) - { - throw new Exception($"Code is broken ({globalI})!"); - } - } - - [Benchmark] - public async Task Wait_100_000_Times() - { - var asyncLock = new AsyncLock(); - - using (var cancellationToken = new CancellationTokenSource()) - { - for (var i = 0; i < 100000; i++) + tasks[i] = Task.Run( + async () => { - using (await asyncLock.EnterAsync(cancellationToken.Token).ConfigureAwait(false)) + using (await asyncLock.EnterAsync().ConfigureAwait(false)) { + var localI = globalI; + await Task.Delay(5).ConfigureAwait(false); // Increase the chance for wrong data. + localI++; + globalI = localI; } - } + }); + } + + await Task.WhenAll(tasks).ConfigureAwait(false); + + if (globalI != tasksCount) + { + throw new Exception($"Code is broken ({globalI})!"); + } + } + + [Benchmark] + public async Task Wait_100_000_Times() + { + var asyncLock = new AsyncLock(); + + using var cancellationToken = new CancellationTokenSource(); + for (var i = 0; i < 100000; i++) + { + using (await asyncLock.EnterAsync(cancellationToken.Token).ConfigureAwait(false)) + { } } } diff --git a/Source/MQTTnet.Benchmarks/BaseBenchmark.cs b/Source/MQTTnet.Benchmarks/BaseBenchmark.cs index a3a50ce9..ece739b2 100644 --- a/Source/MQTTnet.Benchmarks/BaseBenchmark.cs +++ b/Source/MQTTnet.Benchmarks/BaseBenchmark.cs @@ -2,9 +2,8 @@ // The .NET Foundation licenses this file to you under the MIT license. // See the LICENSE file in the project root for more information. -namespace MQTTnet.Benchmarks +namespace MQTTnet.Benchmarks; + +public abstract class BaseBenchmark { - public abstract class BaseBenchmark - { - } } \ No newline at end of file diff --git a/Source/MQTTnet.Benchmarks/ChannelAdapterBenchmark.cs b/Source/MQTTnet.Benchmarks/ChannelAdapterBenchmark.cs index cbc3f799..7942cee5 100644 --- a/Source/MQTTnet.Benchmarks/ChannelAdapterBenchmark.cs +++ b/Source/MQTTnet.Benchmarks/ChannelAdapterBenchmark.cs @@ -12,79 +12,78 @@ using MQTTnet.Formatter; using MQTTnet.Packets; using MQTTnet.Tests.Mockups; -namespace MQTTnet.Benchmarks +namespace MQTTnet.Benchmarks; + +[MemoryDiagnoser] +public sealed class ChannelAdapterBenchmark : BaseBenchmark { - [MemoryDiagnoser] - public sealed class ChannelAdapterBenchmark : BaseBenchmark + MqttChannelAdapter _channelAdapter; + int _iterations; + MqttPublishPacket _packet; + MemoryStream _stream; + + [Benchmark] + public void Receive_10000_Messages() { - MqttChannelAdapter _channelAdapter; - int _iterations; - MqttPublishPacket _packet; - MemoryStream _stream; + _stream.Position = 0; - [Benchmark] - public void Receive_10000_Messages() + for (var i = 0; i < 10000; i++) { - _stream.Position = 0; - - for (var i = 0; i < 10000; i++) - { - _channelAdapter.ReceivePacketAsync(CancellationToken.None).GetAwaiter().GetResult(); - } - - _stream.Position = 0; + _channelAdapter.ReceivePacketAsync(CancellationToken.None).GetAwaiter().GetResult(); } - [Benchmark] - public void Send_10000_Messages() + _stream.Position = 0; + } + + [Benchmark] + public void Send_10000_Messages() + { + _stream.Position = 0; + + for (var i = 0; i < 10000; i++) { - _stream.Position = 0; - - for (var i = 0; i < 10000; i++) - { - _channelAdapter.SendPacketAsync(_packet, CancellationToken.None).GetAwaiter().GetResult(); - } - - _stream.Position = 0; + _channelAdapter.SendPacketAsync(_packet, CancellationToken.None).GetAwaiter().GetResult(); } - [GlobalSetup] - public void Setup() + _stream.Position = 0; + } + + [GlobalSetup] + public void Setup() + { + _packet = new MqttPublishPacket { - _packet = new MqttPublishPacket - { - Topic = "A" - }; + Topic = "A" + }; - var serializer = new MqttPacketFormatterAdapter(MqttProtocolVersion.V311, new MqttBufferWriter(4096, 65535)); + var serializer = new MqttPacketFormatterAdapter(MqttProtocolVersion.V311, new MqttBufferWriter(4096, 65535)); - var serializedPacket = Join(serializer.Encode(_packet).Join()); + var serializedPacket = Join(serializer.Encode(_packet).Join()); - _iterations = 10000; + _iterations = 10000; - _stream = new MemoryStream(_iterations * serializedPacket.Length); + _stream = new MemoryStream(_iterations * serializedPacket.Length); - for (var i = 0; i < _iterations; i++) - { - _stream.Write(serializedPacket, 0, serializedPacket.Length); - } - - _stream.Position = 0; - - var channel = new MemoryMqttChannel(_stream); - - _channelAdapter = new MqttChannelAdapter(channel, serializer, new MqttNetEventLogger()); + for (var i = 0; i < _iterations; i++) + { + _stream.Write(serializedPacket, 0, serializedPacket.Length); } - static byte[] Join(params ArraySegment[] chunks) - { - var buffer = new MemoryStream(); - foreach (var chunk in chunks) - { - buffer.Write(chunk.Array, chunk.Offset, chunk.Count); - } + _stream.Position = 0; - return buffer.ToArray(); + var channel = new MemoryMqttChannel(_stream); + + _channelAdapter = new MqttChannelAdapter(channel, serializer, new MqttNetEventLogger()); + } + + static byte[] Join(params ArraySegment[] chunks) + { + var buffer = new MemoryStream(); + foreach (var chunk in chunks) + { + buffer.Write(chunk.Array, chunk.Offset, chunk.Count); } + + return buffer.ToArray(); } } \ No newline at end of file diff --git a/Source/MQTTnet.Benchmarks/LoggerBenchmark.cs b/Source/MQTTnet.Benchmarks/LoggerBenchmark.cs index 6b8b5e41..53703820 100644 --- a/Source/MQTTnet.Benchmarks/LoggerBenchmark.cs +++ b/Source/MQTTnet.Benchmarks/LoggerBenchmark.cs @@ -2,90 +2,90 @@ // The .NET Foundation licenses this file to you under the MIT license. // See the LICENSE file in the project root for more information. +using System.Diagnostics; using BenchmarkDotNet.Attributes; using BenchmarkDotNet.Jobs; using MQTTnet.Diagnostics.Logger; -namespace MQTTnet.Benchmarks +namespace MQTTnet.Benchmarks; + +[SimpleJob(RuntimeMoniker.Net60)] +[RPlotExporter] +[MemoryDiagnoser] +public class LoggerBenchmark : BaseBenchmark { - [SimpleJob(RuntimeMoniker.Net60)] - [RPlotExporter] - [MemoryDiagnoser] - public class LoggerBenchmark : BaseBenchmark + MqttNetNullLogger _nullLogger; + MqttNetSourceLogger _sourceNullLogger; + + MqttNetEventLogger _eventLogger; + MqttNetSourceLogger _sourceEventLogger; + + MqttNetEventLogger _eventLoggerNoListener; + MqttNetSourceLogger _sourceEventLoggerNoListener; + + bool _useHandler; + + [GlobalSetup] + public void Setup() { - MqttNetNullLogger _nullLogger; - MqttNetSourceLogger _sourceNullLogger; + _nullLogger = new MqttNetNullLogger(); + _sourceNullLogger = _nullLogger.WithSource("Source"); - MqttNetEventLogger _eventLogger; - MqttNetSourceLogger _sourceEventLogger; + _eventLogger = new MqttNetEventLogger(); + _eventLogger.LogMessagePublished += OnLogMessagePublished; + _sourceEventLogger = _eventLogger.WithSource("Source"); - MqttNetEventLogger _eventLoggerNoListener; - MqttNetSourceLogger _sourceEventLoggerNoListener; + _eventLoggerNoListener = new MqttNetEventLogger(); + _sourceEventLoggerNoListener = _eventLoggerNoListener.WithSource("Source"); + } - bool _useHandler; - - [GlobalSetup] - public void Setup() + void OnLogMessagePublished(object sender, MqttNetLogMessagePublishedEventArgs eventArgs) + { + if (_useHandler) { - _nullLogger = new MqttNetNullLogger(); - _sourceNullLogger = _nullLogger.WithSource("Source"); - - _eventLogger = new MqttNetEventLogger(); - _eventLogger.LogMessagePublished += OnLogMessagePublished; - _sourceEventLogger = _eventLogger.WithSource("Source"); - - _eventLoggerNoListener = new MqttNetEventLogger(); - _sourceEventLoggerNoListener = _eventLoggerNoListener.WithSource("Source"); - } - - void OnLogMessagePublished(object sender, MqttNetLogMessagePublishedEventArgs eventArgs) - { - if (_useHandler) - { - eventArgs.LogMessage.ToString(); - } - } - - [Benchmark] - public void Log_10000_Messages_No_Listener() - { - _useHandler = false; - - for (var i = 0; i < 10000; i++) - { - _sourceEventLoggerNoListener.Verbose("test log message {0}", "parameter"); - } - } - - [Benchmark] - public void Log_10000_Messages_With_To_String() - { - _useHandler = true; - - for (var i = 0; i < 10000; i++) - { - _sourceEventLogger.Verbose("test log message {0}", "parameter"); - } - } - - [Benchmark] - public void Log_10000_Messages_Without_To_String() - { - _useHandler = false; - - for (var i = 0; i < 10000; i++) - { - _sourceEventLogger.Verbose("test log message {0}", "parameter"); - } - } - - [Benchmark] - public void Log_10000_Messages_With_NullLogger() - { - for (var i = 0; i < 10000; i++) - { - _sourceNullLogger.Verbose("test log message {0}", "parameter"); - } + Debug.WriteLine(eventArgs.LogMessage.ToString()); } } -} + + [Benchmark] + public void Log_10000_Messages_No_Listener() + { + _useHandler = false; + + for (var i = 0; i < 10000; i++) + { + _sourceEventLoggerNoListener.Verbose("test log message {0}", "parameter"); + } + } + + [Benchmark] + public void Log_10000_Messages_With_To_String() + { + _useHandler = true; + + for (var i = 0; i < 10000; i++) + { + _sourceEventLogger.Verbose("test log message {0}", "parameter"); + } + } + + [Benchmark] + public void Log_10000_Messages_Without_To_String() + { + _useHandler = false; + + for (var i = 0; i < 10000; i++) + { + _sourceEventLogger.Verbose("test log message {0}", "parameter"); + } + } + + [Benchmark] + public void Log_10000_Messages_With_NullLogger() + { + for (var i = 0; i < 10000; i++) + { + _sourceNullLogger.Verbose("test log message {0}", "parameter"); + } + } +} \ No newline at end of file diff --git a/Source/MQTTnet.Benchmarks/MemoryCopyBenchmark.cs b/Source/MQTTnet.Benchmarks/MemoryCopyBenchmark.cs index 0733e2bb..900519c5 100644 --- a/Source/MQTTnet.Benchmarks/MemoryCopyBenchmark.cs +++ b/Source/MQTTnet.Benchmarks/MemoryCopyBenchmark.cs @@ -3,38 +3,38 @@ using BenchmarkDotNet.Jobs; using System; -namespace MQTTnet.Benchmarks +namespace MQTTnet.Benchmarks; + +[SimpleJob(RuntimeMoniker.Net60)] +[RPlotExporter, RankColumn] +[MemoryDiagnoser] +public class MemoryCopyBenchmark { - [SimpleJob(RuntimeMoniker.Net60)] - [RPlotExporter, RankColumn] - [MemoryDiagnoser] - public class MemoryCopyBenchmark + const int MaxLength = 1024 * 8; + + byte[] _source; + byte[] _target; + + [Params(64 - 1, 128 - 1, 256 - 1, 512 - 1, 1024 - 1, 2048 - 1, 5096 - 1)] + public int Length { get; set; } + + [GlobalSetup] + public void Setup() { - const int max_length = 1024 * 8; - private byte[] source; - private byte[] target; - - [Params(64 - 1, 128 - 1, 256 - 1, 512 - 1, 1024 - 1, 2048 - 1, 5096 - 1)] - public int Length { get; set; } - - [GlobalSetup] - public void Setup() - { - source = new byte[max_length]; - target = new byte[max_length]; - } - - [Benchmark(Baseline = true)] - public void Array_Copy() - { - Array.Copy(source, 0, target, 0, Length); - } - - [Benchmark] - public void Memory_Copy() - { - MQTTnet.Internal.MqttMemoryHelper.Copy(source, 0, target, 0, Length); - } - + _source = new byte[MaxLength]; + _target = new byte[MaxLength]; } + + [Benchmark(Baseline = true)] + public void Array_Copy() + { + Array.Copy(_source, 0, _target, 0, Length); + } + + [Benchmark] + public void Memory_Copy() + { + Internal.MqttMemoryHelper.Copy(_source, 0, _target, 0, Length); + } + } \ No newline at end of file diff --git a/Source/MQTTnet.Benchmarks/MessageDeliveryBenchmark.cs b/Source/MQTTnet.Benchmarks/MessageDeliveryBenchmark.cs index e14eb7b1..bca65529 100644 --- a/Source/MQTTnet.Benchmarks/MessageDeliveryBenchmark.cs +++ b/Source/MQTTnet.Benchmarks/MessageDeliveryBenchmark.cs @@ -4,7 +4,6 @@ using System; using System.Collections.Generic; -using System.Linq; using System.Threading; using System.Threading.Tasks; using BenchmarkDotNet.Attributes; @@ -23,22 +22,19 @@ public class MessageDeliveryBenchmark : BaseBenchmark int _messagesExpectedCount; int _messagesReceivedCount; Dictionary _mqttPublisherClientsByPublisherName; - MqttServer _mqttServer; List _mqttSubscriberClients; + + [Params(1000, 10000)] public int _numPublishers; + + [Params(5, 10, 20, 50)] public int _numSubscribedTopicsPerSubscriber; + + [Params(10)] public int _numSubscribers; + + [Params(1, 5)] public int _numTopicsPerPublisher; Dictionary _publisherByTopic; - List _topicPublishMessages; - Dictionary> _topicsByPublisher; - [Params(1000, 10000)] public int NumPublishers; - - [Params(5, 10, 20, 50)] public int NumSubscribedTopicsPerSubscriber; - - [Params(10)] public int NumSubscribers; - - [Params(1, 5)] public int NumTopicsPerPublisher; - [GlobalCleanup] public void Cleanup() { @@ -64,14 +60,11 @@ public class MessageDeliveryBenchmark : BaseBenchmark _mqttServer = null; } - /// - /// Publish messages and wait for messages sent to subscribers - /// [Benchmark] public void DeliverMessages() { // There should be one message received per publish for each subscribed topic - _messagesExpectedCount = NumSubscribedTopicsPerSubscriber * NumSubscribers; + _messagesExpectedCount = _numSubscribedTopicsPerSubscriber * _numSubscribers; // Loop for a while and exchange messages @@ -82,8 +75,6 @@ public class MessageDeliveryBenchmark : BaseBenchmark // same payload for all messages var payload = new byte[] { 1, 2, 3, 4 }; - var tasks = new List(); - // publish a message for each subscribed topic foreach (var topic in _allSubscribedTopics) { @@ -101,35 +92,23 @@ public class MessageDeliveryBenchmark : BaseBenchmark } catch { + // Ignore all errors. } _cancellationTokenSource.Dispose(); if (_messagesReceivedCount < _messagesExpectedCount) { - throw new Exception(string.Format("Messages Received Count mismatch, expected {0}, received {1}", _messagesExpectedCount, _messagesReceivedCount)); + throw new Exception($"Messages Received Count mismatch, expected {_messagesExpectedCount}, received {_messagesReceivedCount}"); } } - [GlobalSetup] public void Setup() { _lockMsgCount = new object(); - Dictionary> singleWildcardTopicsByPublisher; - Dictionary> multiWildcardTopicsByPublisher; - - TopicGenerator.Generate(NumPublishers, NumTopicsPerPublisher, out _topicsByPublisher, out singleWildcardTopicsByPublisher, out multiWildcardTopicsByPublisher); - - var topics = _topicsByPublisher.First().Value; - _topicPublishMessages = new List(); - // Prepare messages, same for each publisher - foreach (var topic in topics) - { - var message = new MqttApplicationMessageBuilder().WithTopic(topic).WithPayload(new byte[] { 1, 2, 3, 4, 5, 6, 7, 8 }).Build(); - _topicPublishMessages.Add(message); - } + TopicGenerator.Generate(_numPublishers, _numTopicsPerPublisher, out _topicsByPublisher, out _, out _); // Create server var serverFactory = new MqttServerFactory(); @@ -139,7 +118,7 @@ public class MessageDeliveryBenchmark : BaseBenchmark _mqttServer.StartAsync().GetAwaiter().GetResult(); // Create publisher clients - _mqttPublisherClientsByPublisherName = new Dictionary(); + _mqttPublisherClientsByPublisherName = []; foreach (var pt in _topicsByPublisher) { var publisherName = pt.Key; @@ -151,13 +130,13 @@ public class MessageDeliveryBenchmark : BaseBenchmark // Create subscriber clients _mqttSubscriberClients = new List(); - for (var i = 0; i < NumSubscribers; i++) + for (var i = 0; i < _numSubscribers; i++) { var mqttSubscriberClient = clientFactory.CreateMqttClient(); _mqttSubscriberClients.Add(mqttSubscriberClient); var subscriberOptions = new MqttClientOptionsBuilder().WithTcpServer("localhost").WithClientId("subscriber" + i).Build(); - mqttSubscriberClient.ApplicationMessageReceivedAsync += r => + mqttSubscriberClient.ApplicationMessageReceivedAsync += _ => { // count messages and signal cancellation when expected message count is reached lock (_lockMsgCount) @@ -171,12 +150,13 @@ public class MessageDeliveryBenchmark : BaseBenchmark return Task.CompletedTask; }; + mqttSubscriberClient.ConnectAsync(subscriberOptions).GetAwaiter().GetResult(); } var allTopics = new List(); - _publisherByTopic = new Dictionary(); + _publisherByTopic = []; foreach (var t in _topicsByPublisher) { foreach (var topic in t.Value) @@ -189,21 +169,18 @@ public class MessageDeliveryBenchmark : BaseBenchmark // Subscribe to NumSubscribedTopics topics spread across all topics _allSubscribedTopics = new List(); - var totalNumTopics = NumPublishers * NumTopicsPerPublisher; - var topicIndexStep = totalNumTopics / (NumSubscribedTopicsPerSubscriber * NumSubscribers); - if (topicIndexStep * NumSubscribedTopicsPerSubscriber * NumSubscribers != totalNumTopics) + var totalNumTopics = _numPublishers * _numTopicsPerPublisher; + var topicIndexStep = totalNumTopics / (_numSubscribedTopicsPerSubscriber * _numSubscribers); + if (topicIndexStep * _numSubscribedTopicsPerSubscriber * _numSubscribers != totalNumTopics) { throw new Exception( - string.Format( - "The total number of topics must be divisible by the number of subscribed topics across all subscribers. Total number of topics: {0}, topic step: {1}", - totalNumTopics, - topicIndexStep)); + $"The total number of topics must be divisible by the number of subscribed topics across all subscribers. Total number of topics: {totalNumTopics}, topic step: {topicIndexStep}"); } var topicIndex = 0; foreach (var mqttSubscriber in _mqttSubscriberClients) { - for (var i = 0; i < NumSubscribedTopicsPerSubscriber; ++i, topicIndex += topicIndexStep) + for (var i = 0; i < _numSubscribedTopicsPerSubscriber; ++i, topicIndex += topicIndexStep) { var topic = allTopics[topicIndex]; _allSubscribedTopics.Add(topic); diff --git a/Source/MQTTnet.Benchmarks/MessageProcessingMqttConnectionContextBenchmark.cs b/Source/MQTTnet.Benchmarks/MessageProcessingMqttConnectionContextBenchmark.cs index b22d365e..75ef27af 100644 --- a/Source/MQTTnet.Benchmarks/MessageProcessingMqttConnectionContextBenchmark.cs +++ b/Source/MQTTnet.Benchmarks/MessageProcessingMqttConnectionContextBenchmark.cs @@ -9,65 +9,64 @@ using Microsoft.AspNetCore.Hosting; using MQTTnet.AspNetCore; using MQTTnet.Diagnostics.Logger; -namespace MQTTnet.Benchmarks +namespace MQTTnet.Benchmarks; + +[SimpleJob(RuntimeMoniker.Net60)] +[MemoryDiagnoser] +public class MessageProcessingMqttConnectionContextBenchmark : BaseBenchmark { - [SimpleJob(RuntimeMoniker.Net60)] - [MemoryDiagnoser] - public class MessageProcessingMqttConnectionContextBenchmark : BaseBenchmark + IWebHost _host; + IMqttClient _mqttClient; + MqttApplicationMessage _message; + + [GlobalSetup] + public void Setup() { - IWebHost _host; - IMqttClient _mqttClient; - MqttApplicationMessage _message; + _host = WebHost.CreateDefaultBuilder() + .UseKestrel(o => o.ListenAnyIP(1883, l => l.UseMqtt())) + .ConfigureServices(services => { + services + .AddHostedMqttServer(mqttServerOptions => mqttServerOptions.WithoutDefaultEndpoint()) + .AddMqttConnectionHandler(); + }) + .Configure(app => { + app.UseMqttServer(_ => { - [GlobalSetup] - public void Setup() + }); + }) + .Build(); + + var factory = new MqttClientFactory(); + _mqttClient = factory.CreateMqttClient(new MqttNetEventLogger(), new MqttClientConnectionContextFactory()); + + _host.StartAsync().GetAwaiter().GetResult(); + + var clientOptions = new MqttClientOptionsBuilder() + .WithTcpServer("localhost").Build(); + + _mqttClient.ConnectAsync(clientOptions).GetAwaiter().GetResult(); + + _message = new MqttApplicationMessageBuilder() + .WithTopic("A") + .Build(); + } + + [GlobalCleanup] + public void Cleanup() + { + _mqttClient.DisconnectAsync().GetAwaiter().GetResult(); + _mqttClient.Dispose(); + + _host.StopAsync().GetAwaiter().GetResult(); + _host.Dispose(); + } + + [Benchmark] + public void Send_10000_Messages() + { + for (var i = 0; i < 10000; i++) { - _host = WebHost.CreateDefaultBuilder() - .UseKestrel(o => o.ListenAnyIP(1883, l => l.UseMqtt())) - .ConfigureServices(services => { - services - .AddHostedMqttServer(mqttServerOptions => mqttServerOptions.WithoutDefaultEndpoint()) - .AddMqttConnectionHandler(); - }) - .Configure(app => { - app.UseMqttServer(s => { - - }); - }) - .Build(); - - var factory = new MqttClientFactory(); - _mqttClient = factory.CreateMqttClient(new MqttNetEventLogger(), new MqttClientConnectionContextFactory()); - - _host.StartAsync().GetAwaiter().GetResult(); - - var clientOptions = new MqttClientOptionsBuilder() - .WithTcpServer("localhost").Build(); - - _mqttClient.ConnectAsync(clientOptions).GetAwaiter().GetResult(); - - _message = new MqttApplicationMessageBuilder() - .WithTopic("A") - .Build(); - } - - [GlobalCleanup] - public void Cleanup() - { - _mqttClient.DisconnectAsync().GetAwaiter().GetResult(); - _mqttClient.Dispose(); - - _host.StopAsync().GetAwaiter().GetResult(); - _host.Dispose(); - } - - [Benchmark] - public void Send_10000_Messages() - { - for (var i = 0; i < 10000; i++) - { - _mqttClient.PublishAsync(_message).GetAwaiter().GetResult(); - } + _mqttClient.PublishAsync(_message).GetAwaiter().GetResult(); } } -} +} \ No newline at end of file diff --git a/Source/MQTTnet.Benchmarks/MqttBufferReaderBenchmark.cs b/Source/MQTTnet.Benchmarks/MqttBufferReaderBenchmark.cs index bfa3d209..8a63e808 100644 --- a/Source/MQTTnet.Benchmarks/MqttBufferReaderBenchmark.cs +++ b/Source/MQTTnet.Benchmarks/MqttBufferReaderBenchmark.cs @@ -8,36 +8,35 @@ using BenchmarkDotNet.Attributes; using BenchmarkDotNet.Jobs; using MQTTnet.Formatter; -namespace MQTTnet.Benchmarks +namespace MQTTnet.Benchmarks; + +[SimpleJob(RuntimeMoniker.Net60)] +[MemoryDiagnoser] +public class MqttBufferReaderBenchmark { - [SimpleJob(RuntimeMoniker.Net60)] - [MemoryDiagnoser] - public class MqttBufferReaderBenchmark + byte[] _buffer; + int _bufferLength; + + [GlobalSetup] + public void GlobalSetup() { - byte[] _buffer; - int _bufferLength; + var writer = new MqttBufferWriter(1024, 1024); + writer.WriteString("hgfjkdfkjlghfdjghdfljkdfhgdlkjfshgsldkfjghsdflkjghdsflkjhrstiuoghlkfjbhnfbutghjoiöjhklötnbhtroliöuhbjntluiobkjzbhtdrlskbhtruhjkfthgbkftgjhgfiklhotriuöhbjtrsioöbtrsötrhträhtrühjtriüoätrhjtsrölbktrbnhtrulöbionhströloubinströoliubhnsöotrunbtöroisntröointröioujhgötiohjgötorshjnbgtöorihbnjtröoihbjntröobntröoibntrjhötrohjbtröoihntröoibnrtoiöbtrjnboöitrhjtnriohötrhjtöroihjtroöihjtroösibntsroönbotöirsbntöoihjntröoihntroöbtrboöitrnhoöitrhjntröoishbnjtröosbhtröbntriohjtröoijtöoitbjöotibjnhöotirhbjntroöibhnjrtoöibnhtroöibnhtörsbnhtöoirbnhtöroibntoörhjnbträöbtrbträbtrbtirbätrsibohjntrsöiobthnjiohjsrtoib"); - [GlobalSetup] - public void GlobalSetup() - { - var writer = new MqttBufferWriter(1024, 1024); - writer.WriteString("hgfjkdfkjlghfdjghdfljkdfhgdlkjfshgsldkfjghsdflkjghdsflkjhrstiuoghlkfjbhnfbutghjoiöjhklötnbhtroliöuhbjntluiobkjzbhtdrlskbhtruhjkfthgbkftgjhgfiklhotriuöhbjtrsioöbtrsötrhträhtrühjtriüoätrhjtsrölbktrbnhtrulöbionhströloubinströoliubhnsöotrunbtöroisntröointröioujhgötiohjgötorshjnbgtöorihbnjtröoihbjntröobntröoibntrjhötrohjbtröoihntröoibnrtoiöbtrjnboöitrhjtnriohötrhjtöroihjtroöihjtroösibntsroönbotöirsbntöoihjntröoihntroöbtrboöitrnhoöitrhjntröoishbnjtröosbhtröbntriohjtröoijtöoitbjöotibjnhöotirhbjntroöibhnjrtoöibnhtroöibnhtörsbnhtöoirbnhtöroibntoörhjnbträöbtrbträbtrbtirbätrsibohjntrsöiobthnjiohjsrtoib"); + _buffer = writer.GetBuffer(); + _bufferLength = writer.Length; + } - _buffer = writer.GetBuffer(); - _bufferLength = writer.Length; - } + [Benchmark] + public void Use_Span() + { + var span = _buffer.AsSpan(0, _bufferLength); + Encoding.UTF8.GetString(span); + } - [Benchmark] - public void Use_Span() - { - var span = _buffer.AsSpan(0, _bufferLength); - Encoding.UTF8.GetString(span); - } - - [Benchmark] - public void Use_Encoding() - { - Encoding.UTF8.GetString(_buffer, 0, _bufferLength); - } + [Benchmark] + public void Use_Encoding() + { + Encoding.UTF8.GetString(_buffer, 0, _bufferLength); } } \ No newline at end of file diff --git a/Source/MQTTnet.Benchmarks/MqttPacketReaderWriterBenchmark.cs b/Source/MQTTnet.Benchmarks/MqttPacketReaderWriterBenchmark.cs index 0efc7ffa..cd18097a 100644 --- a/Source/MQTTnet.Benchmarks/MqttPacketReaderWriterBenchmark.cs +++ b/Source/MQTTnet.Benchmarks/MqttPacketReaderWriterBenchmark.cs @@ -8,89 +8,88 @@ using BenchmarkDotNet.Jobs; using MQTTnet.Formatter; using MQTTnet.Tests.Mockups; -namespace MQTTnet.Benchmarks +namespace MQTTnet.Benchmarks; + +[SimpleJob(RuntimeMoniker.Net60)] +[MemoryDiagnoser] +public class MqttPacketReaderWriterBenchmark : BaseBenchmark { - [SimpleJob(RuntimeMoniker.Net60)] - [MemoryDiagnoser] - public class MqttPacketReaderWriterBenchmark : BaseBenchmark + readonly byte[] _demoPayload = new byte[1024]; + + byte[] _readPayload; + + [GlobalCleanup] + public void GlobalCleanup() { - readonly byte[] _demoPayload = new byte[1024]; - - byte[] _readPayload; + } - [GlobalCleanup] - public void GlobalCleanup() + [GlobalSetup] + public void GlobalSetup() + { + TestEnvironment.EnableLogger = false; + + var writer = new MqttBufferWriter(4096, 65535); + writer.WriteString("A relative short string."); + writer.WriteBinary(_demoPayload); + writer.WriteByte(0x01); + writer.WriteByte(0x02); + writer.WriteVariableByteInteger(5647382); + writer.WriteString("A relative short string."); + writer.WriteVariableByteInteger(8574489); + writer.WriteBinary(_demoPayload); + writer.WriteByte(2); + writer.WriteByte(0x02); + writer.WriteString("fjgffiogfhgfhoihgoireghreghreguhreguireoghreouighreouighreughreguiorehreuiohruiorehreuioghreug"); + writer.WriteBinary(_demoPayload); + + _readPayload = new ArraySegment(writer.GetBuffer(), 0, writer.Length).ToArray(); + } + + [Benchmark] + public void Read_100_000_Messages() + { + var reader = new MqttBufferReader(); + reader.SetBuffer(_readPayload, 0, _readPayload.Length); + + for (var i = 0; i < 100000; i++) { + reader.Seek(0); + + reader.ReadString(); + reader.ReadBinaryData(); + reader.ReadByte(); + reader.ReadByte(); + reader.ReadVariableByteInteger(); + reader.ReadString(); + reader.ReadVariableByteInteger(); + reader.ReadBinaryData(); + reader.ReadByte(); + reader.ReadByte(); + reader.ReadString(); + reader.ReadBinaryData(); } + } - [GlobalSetup] - public void GlobalSetup() + [Benchmark] + public void Write_100_000_Messages() + { + var writer = new MqttBufferWriter(4096, 65535); + + for (var i = 0; i < 100000; i++) { - TestEnvironment.EnableLogger = false; - - var writer = new MqttBufferWriter(4096, 65535); writer.WriteString("A relative short string."); - writer.WriteBinary(_demoPayload); writer.WriteByte(0x01); writer.WriteByte(0x02); writer.WriteVariableByteInteger(5647382); writer.WriteString("A relative short string."); - writer.WriteVariableByteInteger(8574489); + writer.WriteVariableByteInteger(8574589); writer.WriteBinary(_demoPayload); writer.WriteByte(2); writer.WriteByte(0x02); writer.WriteString("fjgffiogfhgfhoihgoireghreghreguhreguireoghreouighreouighreughreguiorehreuiohruiorehreuioghreug"); writer.WriteBinary(_demoPayload); - _readPayload = new ArraySegment(writer.GetBuffer(), 0, writer.Length).ToArray(); - } - - [Benchmark] - public void Read_100_000_Messages() - { - var reader = new MqttBufferReader(); - reader.SetBuffer(_readPayload, 0, _readPayload.Length); - - for (var i = 0; i < 100000; i++) - { - reader.Seek(0); - - reader.ReadString(); - reader.ReadBinaryData(); - reader.ReadByte(); - reader.ReadByte(); - reader.ReadVariableByteInteger(); - reader.ReadString(); - reader.ReadVariableByteInteger(); - reader.ReadBinaryData(); - reader.ReadByte(); - reader.ReadByte(); - reader.ReadString(); - reader.ReadBinaryData(); - } - } - - [Benchmark] - public void Write_100_000_Messages() - { - var writer = new MqttBufferWriter(4096, 65535); - - for (var i = 0; i < 100000; i++) - { - writer.WriteString("A relative short string."); - writer.WriteByte(0x01); - writer.WriteByte(0x02); - writer.WriteVariableByteInteger(5647382); - writer.WriteString("A relative short string."); - writer.WriteVariableByteInteger(8574589); - writer.WriteBinary(_demoPayload); - writer.WriteByte(2); - writer.WriteByte(0x02); - writer.WriteString("fjgffiogfhgfhoihgoireghreghreguhreguireoghreouighreouighreughreguiorehreuiohruiorehreuioghreug"); - writer.WriteBinary(_demoPayload); - - writer.Reset(0); - } + writer.Reset(0); } } } \ No newline at end of file diff --git a/Source/MQTTnet.Benchmarks/MqttTcpChannelBenchmark.cs b/Source/MQTTnet.Benchmarks/MqttTcpChannelBenchmark.cs index 61364747..fce8cf1e 100644 --- a/Source/MQTTnet.Benchmarks/MqttTcpChannelBenchmark.cs +++ b/Source/MQTTnet.Benchmarks/MqttTcpChannelBenchmark.cs @@ -2,7 +2,6 @@ // The .NET Foundation licenses this file to you under the MIT license. // See the LICENSE file in the project root for more information. -using System; using System.Buffers; using System.Reflection; using System.Threading; diff --git a/Source/MQTTnet.Benchmarks/Program.cs b/Source/MQTTnet.Benchmarks/Program.cs index d395269f..481f87fa 100644 --- a/Source/MQTTnet.Benchmarks/Program.cs +++ b/Source/MQTTnet.Benchmarks/Program.cs @@ -5,138 +5,135 @@ using System; using System.Collections.Generic; using System.Linq; -using BenchmarkDotNet.Loggers; using BenchmarkDotNet.Running; -using MQTTnet.Diagnostics; -namespace MQTTnet.Benchmarks +namespace MQTTnet.Benchmarks; + +public static class Program { - public static class Program + static int _selectedBenchmarkIndex; + static List _benchmarks; + + public static void Main(string[] arguments) { - static int _selectedBenchmarkIndex; - static List _benchmarks; + _benchmarks = CollectBenchmarks(); + HandleArguments(arguments); - public static void Main(string[] arguments) + while (true) { - _benchmarks = CollectBenchmarks(); - HandleArguments(arguments); + RenderMenu(); - while (true) + var key = Console.ReadKey(true); + + if (key.Key == ConsoleKey.DownArrow) { - RenderMenu(); - - var key = Console.ReadKey(true); - - if (key.Key == ConsoleKey.DownArrow) + _selectedBenchmarkIndex++; + if (_selectedBenchmarkIndex > _benchmarks.Count - 1) { - _selectedBenchmarkIndex++; - if (_selectedBenchmarkIndex > _benchmarks.Count - 1) - { - _selectedBenchmarkIndex = 0; - } - } - else if (key.Key == ConsoleKey.UpArrow) - { - _selectedBenchmarkIndex--; - if (_selectedBenchmarkIndex < 0) - { - _selectedBenchmarkIndex = _benchmarks.Count - 1; - } - } - else if (key.Key == ConsoleKey.Escape) - { - Environment.Exit(0); - } - else if (key.Key == ConsoleKey.Enter) - { - break; + _selectedBenchmarkIndex = 0; } } - - BenchmarkRunner.Run(_benchmarks[_selectedBenchmarkIndex]); - - Console.ForegroundColor = ConsoleColor.Yellow; - Console.WriteLine("Press any key to exit"); - Console.ReadLine(); - } - - static List CollectBenchmarks() - { - var benchmarks = new List(); - - var types = typeof(Program).Assembly.GetExportedTypes(); - foreach (var type in types) + else if (key.Key == ConsoleKey.UpArrow) { - if (typeof(BaseBenchmark).IsAssignableFrom(type)) + _selectedBenchmarkIndex--; + if (_selectedBenchmarkIndex < 0) { - if (type != typeof(BaseBenchmark)) - { - benchmarks.Add(type); - } + _selectedBenchmarkIndex = _benchmarks.Count - 1; } } - - return benchmarks.OrderBy(b => b.Name).ToList(); - } - - static void HandleArguments(string[] arguments) - { - if (arguments.Length == 0) + else if (key.Key == ConsoleKey.Escape) { - return; + Environment.Exit(0); } - - // Allow for preselection to avoid developer frustration. - - if (int.TryParse(arguments[0], out var benchmarkIndex)) + else if (key.Key == ConsoleKey.Enter) { - _selectedBenchmarkIndex = benchmarkIndex; - return; - } - - _selectedBenchmarkIndex = _benchmarks.FindIndex(b => b.Name.Equals(arguments[0])); - - if (_selectedBenchmarkIndex < 0) - { - _selectedBenchmarkIndex = 0; - } - - if (_selectedBenchmarkIndex > _benchmarks.Count - 1) - { - _selectedBenchmarkIndex = _benchmarks.Count - 1; + break; } } - static void RenderMenu() + BenchmarkRunner.Run(_benchmarks[_selectedBenchmarkIndex]); + + Console.ForegroundColor = ConsoleColor.Yellow; + Console.WriteLine("Press any key to exit"); + Console.ReadLine(); + } + + static List CollectBenchmarks() + { + var benchmarks = new List(); + + var types = typeof(Program).Assembly.GetExportedTypes(); + foreach (var type in types) { - Console.Clear(); - - Console.WriteLine($"MQTTnet - Benchmarks"); - Console.WriteLine("-----------------------------------------------"); - Console.ForegroundColor = ConsoleColor.Yellow; - Console.WriteLine("Press arrow keys for benchmark selection"); - Console.WriteLine("Press Enter for benchmark execution"); - Console.WriteLine("Press Esc for exit"); - Console.ForegroundColor = ConsoleColor.White; - Console.WriteLine(); - - for (var index = 0; index < _benchmarks.Count; index++) + if (typeof(BaseBenchmark).IsAssignableFrom(type)) { - var benchmark = _benchmarks[index]; - - if (_selectedBenchmarkIndex == index) + if (type != typeof(BaseBenchmark)) { - Console.Write("-> "); + benchmarks.Add(type); } - else - { - Console.Write(" "); - } - - Console.WriteLine(benchmark.Name); } + } - Console.WriteLine(); + return benchmarks.OrderBy(b => b.Name).ToList(); + } + + static void HandleArguments(string[] arguments) + { + if (arguments.Length == 0) + { + return; + } + + // Allow for preselection to avoid developer frustration. + + if (int.TryParse(arguments[0], out var benchmarkIndex)) + { + _selectedBenchmarkIndex = benchmarkIndex; + return; + } + + _selectedBenchmarkIndex = _benchmarks.FindIndex(b => b.Name.Equals(arguments[0])); + + if (_selectedBenchmarkIndex < 0) + { + _selectedBenchmarkIndex = 0; + } + + if (_selectedBenchmarkIndex > _benchmarks.Count - 1) + { + _selectedBenchmarkIndex = _benchmarks.Count - 1; } } + + static void RenderMenu() + { + Console.Clear(); + + Console.WriteLine($"MQTTnet - Benchmarks"); + Console.WriteLine("-----------------------------------------------"); + Console.ForegroundColor = ConsoleColor.Yellow; + Console.WriteLine("Press arrow keys for benchmark selection"); + Console.WriteLine("Press Enter for benchmark execution"); + Console.WriteLine("Press Esc for exit"); + Console.ForegroundColor = ConsoleColor.White; + Console.WriteLine(); + + for (var index = 0; index < _benchmarks.Count; index++) + { + var benchmark = _benchmarks[index]; + + if (_selectedBenchmarkIndex == index) + { + Console.Write("-> "); + } + else + { + Console.Write(" "); + } + + Console.WriteLine(benchmark.Name); + } + + Console.WriteLine(); + } } \ No newline at end of file diff --git a/Source/MQTTnet.Benchmarks/ReaderExtensionsBenchmark.cs b/Source/MQTTnet.Benchmarks/ReaderExtensionsBenchmark.cs index 5f224246..1779a57d 100644 --- a/Source/MQTTnet.Benchmarks/ReaderExtensionsBenchmark.cs +++ b/Source/MQTTnet.Benchmarks/ReaderExtensionsBenchmark.cs @@ -1,240 +1,86 @@ using BenchmarkDotNet.Attributes; using BenchmarkDotNet.Jobs; -using MQTTnet.Adapter; using MQTTnet.AspNetCore; using MQTTnet.Exceptions; using MQTTnet.Formatter; -using MQTTnet.Packets; -using System; using System.Buffers; using System.IO; using System.IO.Pipelines; -using System.Linq; using System.Threading.Tasks; -namespace MQTTnet.Benchmarks -{ - [SimpleJob(RuntimeMoniker.Net60)] - [RPlotExporter, RankColumn] - [MemoryDiagnoser] - public class ReaderExtensionsBenchmark - { - MqttPacketFormatterAdapter mqttPacketFormatter; - MemoryStream stream; +namespace MQTTnet.Benchmarks; - [GlobalSetup] - public void GlobalSetup() - { - mqttPacketFormatter = new MqttPacketFormatterAdapter(MqttProtocolVersion.V311, new MqttBufferWriter(4096, 65535)); - var mqttMessage = new MqttApplicationMessageBuilder() +[SimpleJob(RuntimeMoniker.Net60)] +[RPlotExporter, RankColumn] +[MemoryDiagnoser] +public class ReaderExtensionsBenchmark +{ + MqttPacketFormatterAdapter _mqttPacketFormatter; + MemoryStream _stream; + + [GlobalSetup] + public void GlobalSetup() + { + _mqttPacketFormatter = new MqttPacketFormatterAdapter(MqttProtocolVersion.V311, new MqttBufferWriter(4096, 65535)); + var mqttMessage = new MqttApplicationMessageBuilder() .WithTopic("topic") .WithPayload(new byte[10 * 1024]) .Build(); - var packet = MqttPublishPacketFactory.Create(mqttMessage); - - var buffer = mqttPacketFormatter.Encode(packet); - stream = new MemoryStream(); - stream.Write(buffer.Packet); - stream.Write(buffer.Payload.ToArray()); - mqttPacketFormatter.Cleanup(); - } - - [Benchmark(Baseline = true)] - public async Task Before() - { - stream.Position = 0; - var input = PipeReader.Create(stream); - - while (true) - { - ReadResult readResult; - var readTask = input.ReadAsync(); - if (readTask.IsCompleted) - { - readResult = readTask.Result; - } - else - { - readResult = await readTask.ConfigureAwait(false); - } - - var buffer = readResult.Buffer; - - var consumed = buffer.Start; - var observed = buffer.Start; - - try - { - if (!buffer.IsEmpty) - { - if (ReaderExtensions_Before.TryDecode(mqttPacketFormatter, buffer, out var packet, out consumed, out observed, out var received)) - { - break; - } - } - else if (readResult.IsCompleted) - { - throw new MqttCommunicationException("Connection Aborted"); - } - } - finally - { - // The buffer was sliced up to where it was consumed, so we can just advance to the start. - // We mark examined as buffer.End so that if we didn't receive a full frame, we'll wait for more data - // before yielding the read again. - input.AdvanceTo(consumed, observed); - } - } - } - - [Benchmark] - public async Task After() - { - stream.Position = 0; - var input = PipeReader.Create(stream); - - while (true) - { - ReadResult readResult; - var readTask = input.ReadAsync(); - if (readTask.IsCompleted) - { - readResult = readTask.Result; - } - else - { - readResult = await readTask.ConfigureAwait(false); - } - - var buffer = readResult.Buffer; - - var consumed = buffer.Start; - var observed = buffer.Start; - - try - { - if (!buffer.IsEmpty) - { - if (ReaderExtensions.TryDecode(mqttPacketFormatter, buffer, out var packet, out consumed, out observed, out var received)) - { - break; - } - } - else if (readResult.IsCompleted) - { - throw new MqttCommunicationException("Connection Aborted"); - } - } - finally - { - // The buffer was sliced up to where it was consumed, so we can just advance to the start. - // We mark examined as buffer.End so that if we didn't receive a full frame, we'll wait for more data - // before yielding the read again. - input.AdvanceTo(consumed, observed); - } - } - } - - public static class ReaderExtensions_Before - { - public static bool TryDecode(MqttPacketFormatterAdapter formatter, - in ReadOnlySequence input, - out MqttPacket packet, - out SequencePosition consumed, - out SequencePosition observed, - out int bytesRead) - { - ArgumentNullException.ThrowIfNull(formatter); - - packet = null; - consumed = input.Start; - observed = input.End; - bytesRead = 0; - var copy = input; - - if (copy.Length < 2) - { - return false; - } - - var fixedHeader = copy.First.Span[0]; - if (!TryReadBodyLength(ref copy, out int headerLength, out var bodyLength)) - { - return false; - } - - if (copy.Length < bodyLength) - { - return false; - } - - var bodySlice = copy.Slice(0, bodyLength); - var buffer = GetMemory(bodySlice).ToArray(); - - var receivedMqttPacket = new ReceivedMqttPacket(fixedHeader, new ArraySegment(buffer, 0, buffer.Length), buffer.Length + 2); - - if (formatter.ProtocolVersion == MqttProtocolVersion.Unknown) - { - formatter.DetectProtocolVersion(receivedMqttPacket); - } - - packet = formatter.Decode(receivedMqttPacket); - consumed = bodySlice.End; - observed = bodySlice.End; - bytesRead = headerLength + bodyLength; - return true; - } - - static ReadOnlyMemory GetMemory(in ReadOnlySequence input) - { - if (input.IsSingleSegment) - { - return input.First; - } - - // Should be rare - return input.ToArray(); - } - - static bool TryReadBodyLength(ref ReadOnlySequence input, out int headerLength, out int bodyLength) - { - // Alorithm taken from https://docs.oasis-open.org/mqtt/mqtt/v3.1.1/errata01/os/mqtt-v3.1.1-errata01-os-complete.html. - var multiplier = 1; - var value = 0; - byte encodedByte; - var index = 1; - headerLength = 0; - bodyLength = 0; - - var temp = GetMemory(input.Slice(0, Math.Min(5, input.Length))).Span; - - do - { - if (index == temp.Length) - { - return false; - } - - encodedByte = temp[index]; - index++; - - value += (byte)(encodedByte & 127) * multiplier; - if (multiplier > 128 * 128 * 128) - { - throw new MqttProtocolViolationException($"Remaining length is invalid (Data={string.Join(",", temp.Slice(1, index).ToArray())})."); - } - - multiplier *= 128; - } while ((encodedByte & 128) != 0); - - input = input.Slice(index); - - headerLength = index; - bodyLength = value; - return true; - } - } + var packet = MqttPublishPacketFactory.Create(mqttMessage); + var buffer = _mqttPacketFormatter.Encode(packet); + _stream = new MemoryStream(); + _stream.Write(buffer.Packet); + _stream.Write(buffer.Payload.ToArray()); + _mqttPacketFormatter.Cleanup(); } -} + + [Benchmark] + public async Task After() + { + _stream.Position = 0; + var input = PipeReader.Create(_stream); + + while (true) + { + ReadResult readResult; + var readTask = input.ReadAsync(); + if (readTask.IsCompleted) + { + readResult = readTask.Result; + } + else + { + readResult = await readTask.ConfigureAwait(false); + } + + var buffer = readResult.Buffer; + + var consumed = buffer.Start; + var observed = buffer.Start; + + try + { + if (!buffer.IsEmpty) + { + if (_mqttPacketFormatter.TryDecode(buffer, out _, out consumed, out observed, out _)) + { + break; + } + } + else if (readResult.IsCompleted) + { + throw new MqttCommunicationException("Connection Aborted"); + } + } + finally + { + // The buffer was sliced up to where it was consumed, so we can just advance to the start. + // We mark examined as buffer.End so that if we didn't receive a full frame, we'll wait for more data + // before yielding the read again. + input.AdvanceTo(consumed, observed); + } + } + } +} \ No newline at end of file diff --git a/Source/MQTTnet.Benchmarks/RoundtripProcessingBenchmark.cs b/Source/MQTTnet.Benchmarks/RoundtripProcessingBenchmark.cs index e3358fb9..8f638e2e 100644 --- a/Source/MQTTnet.Benchmarks/RoundtripProcessingBenchmark.cs +++ b/Source/MQTTnet.Benchmarks/RoundtripProcessingBenchmark.cs @@ -3,28 +3,27 @@ using BenchmarkDotNet.Jobs; using MQTTnet.Tests.Mockups; using MQTTnet.Tests.Server; -namespace MQTTnet.Benchmarks +namespace MQTTnet.Benchmarks; + +[SimpleJob(RuntimeMoniker.Net60)] +[RPlotExporter, RankColumn] +[MemoryDiagnoser] +public class RoundtripProcessingBenchmark : BaseBenchmark { - [SimpleJob(RuntimeMoniker.Net60)] - [RPlotExporter, RankColumn] - [MemoryDiagnoser] - public class RoundtripProcessingBenchmark : BaseBenchmark + [GlobalSetup] + public void GlobalSetup() { - [GlobalSetup] - public void GlobalSetup() - { - TestEnvironment.EnableLogger = false; - } - - [GlobalCleanup] - public void GlobalCleanup() - { - } - - [Benchmark] - public void Handle_100_000_Messages_In_Receiving_Client() - { - new Load_Tests().Handle_100_000_Messages_In_Receiving_Client().GetAwaiter().GetResult(); - } + TestEnvironment.EnableLogger = false; + } + + [GlobalCleanup] + public void GlobalCleanup() + { + } + + [Benchmark] + public void Handle_100_000_Messages_In_Receiving_Client() + { + new Load_Tests().Handle_100_000_Messages_In_Receiving_Client().GetAwaiter().GetResult(); } } \ No newline at end of file diff --git a/Source/MQTTnet.Benchmarks/SendPacketAsyncBenchmark.cs b/Source/MQTTnet.Benchmarks/SendPacketAsyncBenchmark.cs index b31782e6..82f14553 100644 --- a/Source/MQTTnet.Benchmarks/SendPacketAsyncBenchmark.cs +++ b/Source/MQTTnet.Benchmarks/SendPacketAsyncBenchmark.cs @@ -7,63 +7,61 @@ using System.IO; using System.IO.Pipelines; using System.Threading.Tasks; -namespace MQTTnet.Benchmarks +namespace MQTTnet.Benchmarks; + +[SimpleJob(RuntimeMoniker.Net60)] +[RPlotExporter, RankColumn] +[MemoryDiagnoser] +public class SendPacketAsyncBenchmark : BaseBenchmark { - [SimpleJob(RuntimeMoniker.Net60)] - [RPlotExporter, RankColumn] - [MemoryDiagnoser] - public class SendPacketAsyncBenchmark : BaseBenchmark + MemoryStream _stream; + MqttPacketBuffer _buffer; + + [GlobalSetup] + public void GlobalSetup() { - MemoryStream stream; - MqttPacketBuffer buffer; + _stream = new MemoryStream(1024); + var packet = new ArraySegment(new byte[10]); + _buffer = new MqttPacketBuffer(packet); + } - [GlobalSetup] - public void GlobalSetup() + [Benchmark(Baseline = true)] + public async ValueTask Before() + { + _stream.Position = 0; + var output = PipeWriter.Create(_stream); + + WritePacketBuffer(output, _buffer); + await output.FlushAsync(); + } + + [Benchmark] + public async ValueTask After() + { + _stream.Position = 0; + var output = PipeWriter.Create(_stream); + + if (_buffer.Payload.Length == 0) { - stream = new MemoryStream(1024); - var packet = new ArraySegment(new byte[10]); - buffer = new MqttPacketBuffer(packet); + await output.WriteAsync(_buffer.Packet).ConfigureAwait(false); } - - [Benchmark(Baseline = true)] - public async ValueTask Before() + else { - stream.Position = 0; - var output = PipeWriter.Create(stream); - - WritePacketBuffer(output, buffer); - await output.FlushAsync(); - } - - [Benchmark] - public async ValueTask After() - { - stream.Position = 0; - var output = PipeWriter.Create(stream); - - if (buffer.Payload.Length == 0) - { - await output.WriteAsync(buffer.Packet).ConfigureAwait(false); - } - else - { - WritePacketBuffer(output, buffer); - await output.FlushAsync().ConfigureAwait(false); - } - } - - - static void WritePacketBuffer(PipeWriter output, MqttPacketBuffer buffer) - { - // copy MqttPacketBuffer's Packet and Payload to the same buffer block of PipeWriter - // MqttPacket will be transmitted within the bounds of a WebSocket frame after PipeWriter.FlushAsync - - var span = output.GetSpan(buffer.Length); - - buffer.Packet.AsSpan().CopyTo(span); - buffer.Payload.CopyTo(span.Slice(buffer.Packet.Count)); - - output.Advance(buffer.Length); + WritePacketBuffer(output, _buffer); + await output.FlushAsync().ConfigureAwait(false); } } + + static void WritePacketBuffer(PipeWriter output, MqttPacketBuffer buffer) + { + // copy MqttPacketBuffer's Packet and Payload to the same buffer block of PipeWriter + // MqttPacket will be transmitted within the bounds of a WebSocket frame after PipeWriter.FlushAsync + + var span = output.GetSpan(buffer.Length); + + buffer.Packet.AsSpan().CopyTo(span); + buffer.Payload.CopyTo(span[buffer.Packet.Count..]); + + output.Advance(buffer.Length); + } } \ No newline at end of file diff --git a/Source/MQTTnet.Benchmarks/SerializerBenchmark.cs b/Source/MQTTnet.Benchmarks/SerializerBenchmark.cs index 48117232..213cf19e 100644 --- a/Source/MQTTnet.Benchmarks/SerializerBenchmark.cs +++ b/Source/MQTTnet.Benchmarks/SerializerBenchmark.cs @@ -17,103 +17,102 @@ using MQTTnet.Diagnostics.Logger; using System.Buffers; using System.Net; -namespace MQTTnet.Benchmarks +namespace MQTTnet.Benchmarks; + +[SimpleJob(RuntimeMoniker.Net60)] +[RPlotExporter] +[MemoryDiagnoser] +public class SerializerBenchmark : BaseBenchmark { - [SimpleJob(RuntimeMoniker.Net60)] - [RPlotExporter] - [MemoryDiagnoser] - public class SerializerBenchmark : BaseBenchmark + MqttPacket _packet; + ArraySegment _serializedPacket; + IMqttPacketFormatter _serializer; + MqttBufferWriter _bufferWriter; + + [GlobalSetup] + public void GlobalSetup() { - MqttPacket _packet; - ArraySegment _serializedPacket; - IMqttPacketFormatter _serializer; - MqttBufferWriter _bufferWriter; - - [GlobalSetup] - public void GlobalSetup() + _packet = new MqttPublishPacket { - _packet = new MqttPublishPacket - { - Topic = "A" - }; + Topic = "A" + }; - _bufferWriter = new MqttBufferWriter(4096, 65535); - _serializer = new MqttV3PacketFormatter(_bufferWriter, MqttProtocolVersion.V311); - _serializedPacket = _serializer.Encode(_packet).Join(); - } + _bufferWriter = new MqttBufferWriter(4096, 65535); + _serializer = new MqttV3PacketFormatter(_bufferWriter, MqttProtocolVersion.V311); + _serializedPacket = _serializer.Encode(_packet).Join(); + } - [Benchmark] - public void Serialize_10000_Messages() + [Benchmark] + public void Serialize_10000_Messages() + { + for (var i = 0; i < 10000; i++) { - for (var i = 0; i < 10000; i++) - { - _serializer.Encode(_packet); - _bufferWriter.Cleanup(); - } - } - - [Benchmark] - public void Deserialize_10000_Messages() - { - var channel = new BenchmarkMqttChannel(_serializedPacket); - var reader = new MqttChannelAdapter(channel, new MqttPacketFormatterAdapter(new MqttBufferWriter(4096, 65535)), new MqttNetEventLogger()); - - for (var i = 0; i < 10000; i++) - { - channel.Reset(); - - var header = reader.ReceivePacketAsync(CancellationToken.None).GetAwaiter().GetResult(); - } - } - - class BenchmarkMqttChannel : IMqttChannel - { - readonly ArraySegment _buffer; - int _position; - - public BenchmarkMqttChannel(ArraySegment buffer) - { - _buffer = buffer; - _position = _buffer.Offset; - } - - public EndPoint RemoteEndPoint { get; set; } - - public bool IsSecureConnection { get; } = false; - - public X509Certificate2 ClientCertificate { get; } - - public void Reset() - { - _position = _buffer.Offset; - } - - public Task ConnectAsync(CancellationToken cancellationToken) - { - throw new NotSupportedException(); - } - - public Task DisconnectAsync(CancellationToken cancellationToken) - { - throw new NotSupportedException(); - } - - public Task ReadAsync(byte[] buffer, int offset, int count, CancellationToken cancellationToken) - { - Array.Copy(_buffer.Array, _position, buffer, offset, count); - _position += count; - - return Task.FromResult(count); - } - - public Task WriteAsync(ReadOnlySequence buffer, bool isEndOfPacket, CancellationToken cancellationToken) - { - throw new NotSupportedException(); - } - - public void Dispose() - { - } + _serializer.Encode(_packet); + _bufferWriter.Cleanup(); } } -} + + [Benchmark] + public void Deserialize_10000_Messages() + { + var channel = new BenchmarkMqttChannel(_serializedPacket); + var reader = new MqttChannelAdapter(channel, new MqttPacketFormatterAdapter(new MqttBufferWriter(4096, 65535)), new MqttNetEventLogger()); + + for (var i = 0; i < 10000; i++) + { + channel.Reset(); + + reader.ReceivePacketAsync(CancellationToken.None).GetAwaiter().GetResult(); + } + } + + class BenchmarkMqttChannel : IMqttChannel + { + readonly ArraySegment _buffer; + int _position; + + public BenchmarkMqttChannel(ArraySegment buffer) + { + _buffer = buffer; + _position = _buffer.Offset; + } + + public EndPoint RemoteEndPoint { get; set; } + + public bool IsSecureConnection { get; } = false; + + public X509Certificate2 ClientCertificate { get; set; } + + public void Reset() + { + _position = _buffer.Offset; + } + + public Task ConnectAsync(CancellationToken cancellationToken) + { + throw new NotSupportedException(); + } + + public Task DisconnectAsync(CancellationToken cancellationToken) + { + throw new NotSupportedException(); + } + + public Task ReadAsync(byte[] buffer, int offset, int count, CancellationToken cancellationToken) + { + Array.Copy(_buffer!.Array!, _position, buffer, offset, count); + _position += count; + + return Task.FromResult(count); + } + + public Task WriteAsync(ReadOnlySequence buffer, bool isEndOfPacket, CancellationToken cancellationToken) + { + throw new NotSupportedException(); + } + + public void Dispose() + { + } + } +} \ No newline at end of file diff --git a/Source/MQTTnet.Benchmarks/ServerProcessingBenchmark.cs b/Source/MQTTnet.Benchmarks/ServerProcessingBenchmark.cs index fbac6dc0..9278a44a 100644 --- a/Source/MQTTnet.Benchmarks/ServerProcessingBenchmark.cs +++ b/Source/MQTTnet.Benchmarks/ServerProcessingBenchmark.cs @@ -7,34 +7,33 @@ using BenchmarkDotNet.Jobs; using MQTTnet.Tests.Mockups; using MQTTnet.Tests.Server; -namespace MQTTnet.Benchmarks +namespace MQTTnet.Benchmarks; + +[SimpleJob(RuntimeMoniker.Net60)] +[RPlotExporter, RankColumn] +[MemoryDiagnoser] +public class ServerProcessingBenchmark : BaseBenchmark { - [SimpleJob(RuntimeMoniker.Net60)] - [RPlotExporter, RankColumn] - [MemoryDiagnoser] - public class ServerProcessingBenchmark : BaseBenchmark + [GlobalSetup] + public void GlobalSetup() { - [GlobalSetup] - public void GlobalSetup() - { - TestEnvironment.EnableLogger = false; - } - - [GlobalCleanup] - public void GlobalCleanup() - { - } - - [Benchmark] - public void Handle_100_000_Messages_In_Server_MqttClient() - { - new Load_Tests().Handle_100_000_Messages_In_Server().GetAwaiter().GetResult(); - } - - //[Benchmark] - public void Handle_100_000_Messages_In_Server_LowLevelMqttClient() - { - new Load_Tests().Handle_100_000_Messages_In_Low_Level_Client().GetAwaiter().GetResult(); - } + TestEnvironment.EnableLogger = false; + } + + [GlobalCleanup] + public void GlobalCleanup() + { + } + + [Benchmark] + public void Handle_100_000_Messages_In_Server_MqttClient() + { + new Load_Tests().Handle_100_000_Messages_In_Server().GetAwaiter().GetResult(); + } + + //[Benchmark] + public void Handle_100_000_Messages_In_Server_LowLevelMqttClient() + { + new Load_Tests().Handle_100_000_Messages_In_Low_Level_Client().GetAwaiter().GetResult(); } } \ No newline at end of file diff --git a/Source/MQTTnet.Benchmarks/SubscribeBenchmark.cs b/Source/MQTTnet.Benchmarks/SubscribeBenchmark.cs index 8c56ee62..7e0d3775 100644 --- a/Source/MQTTnet.Benchmarks/SubscribeBenchmark.cs +++ b/Source/MQTTnet.Benchmarks/SubscribeBenchmark.cs @@ -30,7 +30,7 @@ public class SubscribeBenchmark : BaseBenchmark [GlobalSetup] public void Setup() { - TopicGenerator.Generate(NumPublishers, NumTopicsPerPublisher, out var topicsByPublisher, out var singleWildcardTopicsByPublisher, out var multiWildcardTopicsByPublisher); + TopicGenerator.Generate(NumPublishers, NumTopicsPerPublisher, out var topicsByPublisher, out _, out _); _topics = topicsByPublisher.Values.First(); var serverOptions = new MqttServerOptionsBuilder().WithDefaultEndpoint().Build(); diff --git a/Source/MQTTnet.Benchmarks/TcpPipesBenchmark.cs b/Source/MQTTnet.Benchmarks/TcpPipesBenchmark.cs index 7692f78b..19a080cc 100644 --- a/Source/MQTTnet.Benchmarks/TcpPipesBenchmark.cs +++ b/Source/MQTTnet.Benchmarks/TcpPipesBenchmark.cs @@ -11,70 +11,69 @@ using BenchmarkDotNet.Attributes; using BenchmarkDotNet.Jobs; using MQTTnet.AspNetCore; -namespace MQTTnet.Benchmarks +namespace MQTTnet.Benchmarks; + +[SimpleJob(RuntimeMoniker.Net60)] +[MemoryDiagnoser] +public class TcpPipesBenchmark : BaseBenchmark { - [SimpleJob(RuntimeMoniker.Net60)] - [MemoryDiagnoser] - public class TcpPipesBenchmark : BaseBenchmark + IDuplexPipe _client; + IDuplexPipe _server; + + [GlobalSetup] + public void Setup() { - IDuplexPipe _client; - IDuplexPipe _server; + var server = new TcpListener(IPAddress.Any, 1883); - [GlobalSetup] - public void Setup() + server.Start(1); + + var task = Task.Run(() => server.AcceptSocket()); + + var clientConnection = new SocketConnection(new IPEndPoint(IPAddress.Loopback, 1883)); + + clientConnection.StartAsync().GetAwaiter().GetResult(); + _client = clientConnection.Transport; + + var serverConnection = new SocketConnection(task.GetAwaiter().GetResult()); + serverConnection.StartAsync().GetAwaiter().GetResult(); + _server = serverConnection.Transport; + } + + + [Benchmark] + public async Task Send_10000_Chunks_Pipe() + { + var size = 5; + var iterations = 10000; + + await Task.WhenAll(WriteAsync(iterations, size), ReadAsync(iterations, size)); + } + + async Task ReadAsync(int iterations, int size) + { + await Task.Yield(); + + var expected = iterations * size; + long read = 0; + var input = _client.Input; + + while (read < expected) { - var server = new TcpListener(IPAddress.Any, 1883); - - server.Start(1); - - var task = Task.Run(() => server.AcceptSocket()); - - var clientConnection = new SocketConnection(new IPEndPoint(IPAddress.Loopback, 1883)); - - clientConnection.StartAsync().GetAwaiter().GetResult(); - _client = clientConnection.Transport; - - var serverConnection = new SocketConnection(task.GetAwaiter().GetResult()); - serverConnection.StartAsync().GetAwaiter().GetResult(); - _server = serverConnection.Transport; - } - - - [Benchmark] - public async Task Send_10000_Chunks_Pipe() - { - var size = 5; - var iterations = 10000; - - await Task.WhenAll(WriteAsync(iterations, size), ReadAsync(iterations, size)); - } - - async Task ReadAsync(int iterations, int size) - { - await Task.Yield(); - - var expected = iterations * size; - long read = 0; - var input = _client.Input; - - while (read < expected) - { - var readresult = await input.ReadAsync(CancellationToken.None).ConfigureAwait(false); - input.AdvanceTo(readresult.Buffer.End); - read += readresult.Buffer.Length; - } - } - - async Task WriteAsync(int iterations, int size) - { - await Task.Yield(); - - var output = _server.Output; - - for (var i = 0; i < iterations; i++) - { - await output.WriteAsync(new byte[size], CancellationToken.None).ConfigureAwait(false); - } + var readresult = await input.ReadAsync(CancellationToken.None).ConfigureAwait(false); + input.AdvanceTo(readresult.Buffer.End); + read += readresult.Buffer.Length; } } -} + + async Task WriteAsync(int iterations, int size) + { + await Task.Yield(); + + var output = _server.Output; + + for (var i = 0; i < iterations; i++) + { + await output.WriteAsync(new byte[size], CancellationToken.None).ConfigureAwait(false); + } + } +} \ No newline at end of file diff --git a/Source/MQTTnet.Benchmarks/TopicFilterComparerBenchmark.cs b/Source/MQTTnet.Benchmarks/TopicFilterComparerBenchmark.cs index f78cb81c..d5e18ba9 100644 --- a/Source/MQTTnet.Benchmarks/TopicFilterComparerBenchmark.cs +++ b/Source/MQTTnet.Benchmarks/TopicFilterComparerBenchmark.cs @@ -2,269 +2,32 @@ // The .NET Foundation licenses this file to you under the MIT license. // See the LICENSE file in the project root for more information. -using System; using BenchmarkDotNet.Attributes; using BenchmarkDotNet.Jobs; -namespace MQTTnet.Benchmarks +namespace MQTTnet.Benchmarks; + +[SimpleJob(RuntimeMoniker.Net60)] +[RPlotExporter] +[MemoryDiagnoser] +public class TopicFilterComparerBenchmark : BaseBenchmark { - [SimpleJob(RuntimeMoniker.Net60)] - [RPlotExporter] - [MemoryDiagnoser] - public class TopicFilterComparerBenchmark : BaseBenchmark + readonly string _longTopic = + "AAAAAAAAAAAAAssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssshhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkAAAAAAAAAAAAAAABBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCC"; + + [Benchmark] + public void MqttTopicFilterComparer_10000_LoopMethod() { - static readonly char[] TopicLevelSeparator = { '/' }; - - readonly string _longTopic = - "AAAAAAAAAAAAAssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssshhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkAAAAAAAAAAAAAAABBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCC"; - - [Benchmark] - public void MqttTopicFilterComparer_10000_LoopMethod() + for (var i = 0; i < 100000; i++) { - for (var i = 0; i < 100000; i++) - { - MqttTopicFilterComparer.Compare("sport/tennis/player1", "sport/#"); - MqttTopicFilterComparer.Compare("sport/tennis/player1/ranking", "sport/#/ranking"); - MqttTopicFilterComparer.Compare("sport/tennis/player1/score/wimbledon", "sport/+/player1/#"); - MqttTopicFilterComparer.Compare("sport/tennis/player1", "sport/tennis/+"); - MqttTopicFilterComparer.Compare("/finance", "+/+"); - MqttTopicFilterComparer.Compare("/finance", "/+"); - MqttTopicFilterComparer.Compare("/finance", "+"); - MqttTopicFilterComparer.Compare(_longTopic, _longTopic); - } - } - - [Benchmark] - public void MqttTopicFilterComparer_10000_LoopMethod_Without_Pointer() - { - for (var i = 0; i < 100000; i++) - { - MqttTopicFilterComparerWithoutPointer.Compare("sport/tennis/player1", "sport/#"); - MqttTopicFilterComparerWithoutPointer.Compare("sport/tennis/player1/ranking", "sport/#/ranking"); - MqttTopicFilterComparerWithoutPointer.Compare("sport/tennis/player1/score/wimbledon", "sport/+/player1/#"); - MqttTopicFilterComparerWithoutPointer.Compare("sport/tennis/player1", "sport/tennis/+"); - MqttTopicFilterComparerWithoutPointer.Compare("/finance", "+/+"); - MqttTopicFilterComparerWithoutPointer.Compare("/finance", "/+"); - MqttTopicFilterComparerWithoutPointer.Compare("/finance", "+"); - MqttTopicFilterComparerWithoutPointer.Compare(_longTopic, _longTopic); - } - } - - [Benchmark] - public void MqttTopicFilterComparer_10000_StringSplitMethod() - { - for (var i = 0; i < 100000; i++) - { - LegacyMethodByStringSplit("sport/tennis/player1", "sport/#"); - LegacyMethodByStringSplit("sport/tennis/player1/ranking", "sport/#/ranking"); - LegacyMethodByStringSplit("sport/tennis/player1/score/wimbledon", "sport/+/player1/#"); - LegacyMethodByStringSplit("sport/tennis/player1", "sport/tennis/+"); - LegacyMethodByStringSplit("/finance", "+/+"); - LegacyMethodByStringSplit("/finance", "/+"); - LegacyMethodByStringSplit("/finance", "+"); - MqttTopicFilterComparer.Compare(_longTopic, _longTopic); - } - } - - [GlobalSetup] - public void Setup() - { - } - - static bool LegacyMethodByStringSplit(string topic, string filter) - { - ArgumentNullException.ThrowIfNull(topic); - ArgumentNullException.ThrowIfNull(filter); - - if (string.Equals(topic, filter, StringComparison.Ordinal)) - { - return true; - } - - var fragmentsTopic = topic.Split(TopicLevelSeparator, StringSplitOptions.None); - var fragmentsFilter = filter.Split(TopicLevelSeparator, StringSplitOptions.None); - - // # > In either case it MUST be the last character specified in the Topic Filter [MQTT-4.7.1-2]. - for (var i = 0; i < fragmentsFilter.Length; i++) - { - if (fragmentsFilter[i] == "+") - { - continue; - } - - if (fragmentsFilter[i] == "#") - { - return true; - } - - if (i >= fragmentsTopic.Length) - { - return false; - } - - if (!string.Equals(fragmentsFilter[i], fragmentsTopic[i], StringComparison.Ordinal)) - { - return false; - } - } - - return fragmentsTopic.Length == fragmentsFilter.Length; - } - - public static class MqttTopicFilterComparerWithoutPointer - { - public const char LevelSeparator = '/'; - public const char MultiLevelWildcard = '#'; - public const char SingleLevelWildcard = '+'; - public const char ReservedTopicPrefix = '$'; - - public static MqttTopicFilterCompareResult Compare(string topic, string filter) - { - if (string.IsNullOrEmpty(topic)) - { - return MqttTopicFilterCompareResult.TopicInvalid; - } - - if (string.IsNullOrEmpty(filter)) - { - return MqttTopicFilterCompareResult.FilterInvalid; - } - - var filterOffset = 0; - var filterLength = filter.Length; - - var topicOffset = 0; - var topicLength = topic.Length; - - var topicPointer = topic; - var filterPointer = filter; - - var isMultiLevelFilter = filterPointer[filterLength - 1] == MultiLevelWildcard; - var isReservedTopic = topicPointer[0] == ReservedTopicPrefix; - - if (isReservedTopic && filterLength == 1 && isMultiLevelFilter) - { - // It is not allowed to receive i.e. '$foo/bar' with filter '#'. - return MqttTopicFilterCompareResult.NoMatch; - } - - if (isReservedTopic && filterPointer[0] == SingleLevelWildcard) - { - // It is not allowed to receive i.e. '$SYS/monitor/Clients' with filter '+/monitor/Clients'. - return MqttTopicFilterCompareResult.NoMatch; - } - - if (filterLength == 1 && isMultiLevelFilter) - { - // Filter '#' matches basically everything. - return MqttTopicFilterCompareResult.IsMatch; - } - - // Go through the filter char by char. - while (filterOffset < filterLength && topicOffset < topicLength) - { - // Check if the current char is a multi level wildcard. The char is only allowed - // at the very las position. - if (filterPointer[filterOffset] == MultiLevelWildcard && filterOffset != filterLength - 1) - { - return MqttTopicFilterCompareResult.FilterInvalid; - } - - if (filterPointer[filterOffset] == topicPointer[topicOffset]) - { - if (topicOffset == topicLength - 1) - { - // Check for e.g. "foo" matching "foo/#" - if (filterOffset == filterLength - 3 && filterPointer[filterOffset + 1] == LevelSeparator && isMultiLevelFilter) - { - return MqttTopicFilterCompareResult.IsMatch; - } - - // Check for e.g. "foo/" matching "foo/#" - if (filterOffset == filterLength - 2 && filterPointer[filterOffset] == LevelSeparator && isMultiLevelFilter) - { - return MqttTopicFilterCompareResult.IsMatch; - } - } - - filterOffset++; - topicOffset++; - - // Check if the end was reached and i.e. "foo/bar" matches "foo/bar" - if (filterOffset == filterLength && topicOffset == topicLength) - { - return MqttTopicFilterCompareResult.IsMatch; - } - - var endOfTopic = topicOffset == topicLength; - - if (endOfTopic && filterOffset == filterLength - 1 && filterPointer[filterOffset] == SingleLevelWildcard) - { - if (filterOffset > 0 && filterPointer[filterOffset - 1] != LevelSeparator) - { - return MqttTopicFilterCompareResult.FilterInvalid; - } - - return MqttTopicFilterCompareResult.IsMatch; - } - } - else - { - if (filterPointer[filterOffset] == SingleLevelWildcard) - { - // Check for invalid "+foo" or "a/+foo" subscription - if (filterOffset > 0 && filterPointer[filterOffset - 1] != LevelSeparator) - { - return MqttTopicFilterCompareResult.FilterInvalid; - } - - // Check for bad "foo+" or "foo+/a" subscription - if (filterOffset < filterLength - 1 && filterPointer[filterOffset + 1] != LevelSeparator) - { - return MqttTopicFilterCompareResult.FilterInvalid; - } - - filterOffset++; - while (topicOffset < topicLength && topicPointer[topicOffset] != LevelSeparator) - { - topicOffset++; - } - - if (topicOffset == topicLength && filterOffset == filterLength) - { - return MqttTopicFilterCompareResult.IsMatch; - } - } - else if (filterPointer[filterOffset] == MultiLevelWildcard) - { - if (filterOffset > 0 && filterPointer[filterOffset - 1] != LevelSeparator) - { - return MqttTopicFilterCompareResult.FilterInvalid; - } - - if (filterOffset + 1 != filterLength) - { - return MqttTopicFilterCompareResult.FilterInvalid; - } - - return MqttTopicFilterCompareResult.IsMatch; - } - else - { - // Check for e.g. "foo/bar" matching "foo/+/#". - if (filterOffset > 0 && filterOffset + 2 == filterLength && topicOffset == topicLength && filterPointer[filterOffset - 1] == SingleLevelWildcard && - filterPointer[filterOffset] == LevelSeparator && isMultiLevelFilter) - { - return MqttTopicFilterCompareResult.IsMatch; - } - - return MqttTopicFilterCompareResult.NoMatch; - } - } - } - - return MqttTopicFilterCompareResult.NoMatch; - } + MqttTopicFilterComparer.Compare("sport/tennis/player1", "sport/#"); + MqttTopicFilterComparer.Compare("sport/tennis/player1/ranking", "sport/#/ranking"); + MqttTopicFilterComparer.Compare("sport/tennis/player1/score/wimbledon", "sport/+/player1/#"); + MqttTopicFilterComparer.Compare("sport/tennis/player1", "sport/tennis/+"); + MqttTopicFilterComparer.Compare("/finance", "+/+"); + MqttTopicFilterComparer.Compare("/finance", "/+"); + MqttTopicFilterComparer.Compare("/finance", "+"); + MqttTopicFilterComparer.Compare(_longTopic, _longTopic); } } } \ No newline at end of file diff --git a/Source/MQTTnet.Benchmarks/TopicGenerator.cs b/Source/MQTTnet.Benchmarks/TopicGenerator.cs index d4f9d8e7..1b2327ee 100644 --- a/Source/MQTTnet.Benchmarks/TopicGenerator.cs +++ b/Source/MQTTnet.Benchmarks/TopicGenerator.cs @@ -1,84 +1,83 @@ using System; using System.Collections.Generic; -namespace MQTTnet.Benchmarks +namespace MQTTnet.Benchmarks; + +public class TopicGenerator { - public class TopicGenerator + public static void Generate( + int numPublishers, + int numTopicsPerPublisher, + out Dictionary> topicsByPublisher, + out Dictionary> singleWildcardTopicsByPublisher, + out Dictionary> multiWildcardTopicsByPublisher) { - public static void Generate( - int numPublishers, - int numTopicsPerPublisher, - out Dictionary> topicsByPublisher, - out Dictionary> singleWildcardTopicsByPublisher, - out Dictionary> multiWildcardTopicsByPublisher) + topicsByPublisher = new Dictionary>(); + singleWildcardTopicsByPublisher = new Dictionary>(); + multiWildcardTopicsByPublisher = new Dictionary>(); + + // Find some reasonable distribution across three topic levels + var topicsPerLevel = (int)Math.Pow(numTopicsPerPublisher, 1.0 / 3.0); + if (topicsPerLevel <= 0) { - topicsByPublisher = new Dictionary>(); - singleWildcardTopicsByPublisher = new Dictionary>(); - multiWildcardTopicsByPublisher = new Dictionary>(); + topicsPerLevel = 1; + } - // Find some reasonable distribution across three topic levels - var topicsPerLevel = (int)Math.Pow(numTopicsPerPublisher, 1.0 / 3.0); - if (topicsPerLevel <= 0) + var numLevel1Topics = topicsPerLevel; + var numLevel2Topics = topicsPerLevel; + + var maxNumLevel3Topics = 1 + (int)((double)numTopicsPerPublisher / numLevel1Topics / numLevel2Topics); + if (maxNumLevel3Topics <= 0) + { + maxNumLevel3Topics = 1; + } + + for (var p = 0; p < numPublishers; ++p) + { + var publisherTopicCount = 0; + var publisherName = "pub" + p; + for (var l1 = 0; l1 < numLevel1Topics; ++l1) { - topicsPerLevel = 1; - } - - var numLevel1Topics = topicsPerLevel; - var numLevel2Topics = topicsPerLevel; - - var maxNumLevel3Topics = 1 + (int)((double)numTopicsPerPublisher / numLevel1Topics / numLevel2Topics); - if (maxNumLevel3Topics <= 0) - { - maxNumLevel3Topics = 1; - } - - for (var p = 0; p < numPublishers; ++p) - { - var publisherTopicCount = 0; - var publisherName = "pub" + p; - for (var l1 = 0; l1 < numLevel1Topics; ++l1) + for (var l2 = 0; l2 < numLevel2Topics; ++l2) { - for (var l2 = 0; l2 < numLevel2Topics; ++l2) + for (var l3 = 0; l3 < maxNumLevel3Topics; ++l3) { - for (var l3 = 0; l3 < maxNumLevel3Topics; ++l3) + if (publisherTopicCount >= numTopicsPerPublisher) { - if (publisherTopicCount >= numTopicsPerPublisher) - { - break; - } - - var topic = $"{publisherName}/building{l1 + 1}/level{l2 + 1}/sensor{l3 + 1}"; - AddPublisherTopic(publisherName, topic, topicsByPublisher); - - if (l2 == 0) - { - var singleWildcardTopic = $"{publisherName}/building{l1 + 1}/+/sensor{l3 + 1}"; - AddPublisherTopic(publisherName, singleWildcardTopic, singleWildcardTopicsByPublisher); - } - - if (l1 == 0 && l3 == 0) - { - var multiWildcardTopic = $"{publisherName}/+/level{l2 + 1}/+"; - AddPublisherTopic(publisherName, multiWildcardTopic, multiWildcardTopicsByPublisher); - } - - ++publisherTopicCount; + break; } + + var topic = $"{publisherName}/building{l1 + 1}/level{l2 + 1}/sensor{l3 + 1}"; + AddPublisherTopic(publisherName, topic, topicsByPublisher); + + if (l2 == 0) + { + var singleWildcardTopic = $"{publisherName}/building{l1 + 1}/+/sensor{l3 + 1}"; + AddPublisherTopic(publisherName, singleWildcardTopic, singleWildcardTopicsByPublisher); + } + + if (l1 == 0 && l3 == 0) + { + var multiWildcardTopic = $"{publisherName}/+/level{l2 + 1}/+"; + AddPublisherTopic(publisherName, multiWildcardTopic, multiWildcardTopicsByPublisher); + } + + ++publisherTopicCount; } } } } + } - static void AddPublisherTopic(string publisherName, string topic, Dictionary> topicsByPublisher) + static void AddPublisherTopic(string publisherName, string topic, Dictionary> topicsByPublisher) + { + List topicList; + if (!topicsByPublisher.TryGetValue(publisherName, out topicList)) { - List topicList; - if (!topicsByPublisher.TryGetValue(publisherName, out topicList)) - { - topicList = new List(); - topicsByPublisher.Add(publisherName, topicList); - } - - topicList.Add(topic); + topicList = new List(); + topicsByPublisher.Add(publisherName, topicList); } + + topicList.Add(topic); } } \ No newline at end of file diff --git a/Source/MQTTnet.Benchmarks/UnsubscribeBenchmark.cs b/Source/MQTTnet.Benchmarks/UnsubscribeBenchmark.cs index 41f3adcd..8e1ac675 100644 --- a/Source/MQTTnet.Benchmarks/UnsubscribeBenchmark.cs +++ b/Source/MQTTnet.Benchmarks/UnsubscribeBenchmark.cs @@ -14,15 +14,15 @@ public class UnsubscribeBenchmark : BaseBenchmark { const int NumPublishers = 1; const int NumTopicsPerPublisher = 10000; + IMqttClient _mqttClient; MqttServer _mqttServer; - List _topics; [GlobalSetup] public void Setup() { - TopicGenerator.Generate(NumPublishers, NumTopicsPerPublisher, out var topicsByPublisher, out var singleWildcardTopicsByPublisher, out var multiWildcardTopicsByPublisher); + TopicGenerator.Generate(NumPublishers, NumTopicsPerPublisher, out var topicsByPublisher, out _, out _); _topics = topicsByPublisher.Values.First(); var serverOptions = new MqttServerOptionsBuilder().WithDefaultEndpoint().Build(); diff --git a/Source/MQTTnet.Extensions.Rpc/DefaultMqttRpcClientTopicGenerationStrategy.cs b/Source/MQTTnet.Extensions.Rpc/DefaultMqttRpcClientTopicGenerationStrategy.cs index b21affed..13d78958 100644 --- a/Source/MQTTnet.Extensions.Rpc/DefaultMqttRpcClientTopicGenerationStrategy.cs +++ b/Source/MQTTnet.Extensions.Rpc/DefaultMqttRpcClientTopicGenerationStrategy.cs @@ -4,27 +4,26 @@ using System; -namespace MQTTnet.Extensions.Rpc +namespace MQTTnet.Extensions.Rpc; + +public sealed class DefaultMqttRpcClientTopicGenerationStrategy : IMqttRpcClientTopicGenerationStrategy { - public sealed class DefaultMqttRpcClientTopicGenerationStrategy : IMqttRpcClientTopicGenerationStrategy + public MqttRpcTopicPair CreateRpcTopics(TopicGenerationContext context) { - public MqttRpcTopicPair CreateRpcTopics(TopicGenerationContext context) + ArgumentNullException.ThrowIfNull(context); + + if (context.MethodName.Contains("/") || context.MethodName.Contains("+") || context.MethodName.Contains("#")) { - ArgumentNullException.ThrowIfNull(context); - - if (context.MethodName.Contains("/") || context.MethodName.Contains("+") || context.MethodName.Contains("#")) - { - throw new ArgumentException("The method name cannot contain /, + or #."); - } - - var requestTopic = $"MQTTnet.RPC/{Guid.NewGuid():N}/{context.MethodName}"; - var responseTopic = requestTopic + "/response"; - - return new MqttRpcTopicPair - { - RequestTopic = requestTopic, - ResponseTopic = responseTopic - }; + throw new ArgumentException("The method name cannot contain /, + or #."); } + + var requestTopic = $"MQTTnet.RPC/{Guid.NewGuid():N}/{context.MethodName}"; + var responseTopic = requestTopic + "/response"; + + return new MqttRpcTopicPair + { + RequestTopic = requestTopic, + ResponseTopic = responseTopic + }; } -} +} \ No newline at end of file diff --git a/Source/MQTTnet.Extensions.Rpc/IMqttRpcClient.cs b/Source/MQTTnet.Extensions.Rpc/IMqttRpcClient.cs index ccece444..84846071 100644 --- a/Source/MQTTnet.Extensions.Rpc/IMqttRpcClient.cs +++ b/Source/MQTTnet.Extensions.Rpc/IMqttRpcClient.cs @@ -8,12 +8,11 @@ using System.Threading; using System.Threading.Tasks; using MQTTnet.Protocol; -namespace MQTTnet.Extensions.Rpc +namespace MQTTnet.Extensions.Rpc; + +public interface IMqttRpcClient : IDisposable { - public interface IMqttRpcClient : IDisposable - { - Task ExecuteAsync(TimeSpan timeout, string methodName, byte[] payload, MqttQualityOfServiceLevel qualityOfServiceLevel, IDictionary parameters = null); - - Task ExecuteAsync(string methodName, byte[] payload, MqttQualityOfServiceLevel qualityOfServiceLevel, IDictionary parameters = null, CancellationToken cancellationToken = default); - } + Task ExecuteAsync(TimeSpan timeout, string methodName, byte[] payload, MqttQualityOfServiceLevel qualityOfServiceLevel, IDictionary parameters = null); + + Task ExecuteAsync(string methodName, byte[] payload, MqttQualityOfServiceLevel qualityOfServiceLevel, IDictionary parameters = null, CancellationToken cancellationToken = default); } \ No newline at end of file diff --git a/Source/MQTTnet.Extensions.Rpc/IMqttRpcClientTopicGenerationStrategy.cs b/Source/MQTTnet.Extensions.Rpc/IMqttRpcClientTopicGenerationStrategy.cs index 487daa13..2a2746ad 100644 --- a/Source/MQTTnet.Extensions.Rpc/IMqttRpcClientTopicGenerationStrategy.cs +++ b/Source/MQTTnet.Extensions.Rpc/IMqttRpcClientTopicGenerationStrategy.cs @@ -2,10 +2,9 @@ // The .NET Foundation licenses this file to you under the MIT license. // See the LICENSE file in the project root for more information. -namespace MQTTnet.Extensions.Rpc +namespace MQTTnet.Extensions.Rpc; + +public interface IMqttRpcClientTopicGenerationStrategy { - public interface IMqttRpcClientTopicGenerationStrategy - { - MqttRpcTopicPair CreateRpcTopics(TopicGenerationContext context); - } -} + MqttRpcTopicPair CreateRpcTopics(TopicGenerationContext context); +} \ No newline at end of file diff --git a/Source/MQTTnet.Extensions.Rpc/MqttFactoryExtensions.cs b/Source/MQTTnet.Extensions.Rpc/MqttFactoryExtensions.cs index 3de66954..cedc3a2f 100644 --- a/Source/MQTTnet.Extensions.Rpc/MqttFactoryExtensions.cs +++ b/Source/MQTTnet.Extensions.Rpc/MqttFactoryExtensions.cs @@ -4,26 +4,25 @@ using System; -namespace MQTTnet.Extensions.Rpc +namespace MQTTnet.Extensions.Rpc; + +public static class MqttFactoryExtensions { - public static class MqttFactoryExtensions + public static IMqttRpcClient CreateMqttRpcClient(this MqttClientFactory clientFactory, IMqttClient mqttClient) { - public static IMqttRpcClient CreateMqttRpcClient(this MqttClientFactory clientFactory, IMqttClient mqttClient) - { - return clientFactory.CreateMqttRpcClient( - mqttClient, - new MqttRpcClientOptions - { - TopicGenerationStrategy = new DefaultMqttRpcClientTopicGenerationStrategy() - }); - } + return clientFactory.CreateMqttRpcClient( + mqttClient, + new MqttRpcClientOptions + { + TopicGenerationStrategy = new DefaultMqttRpcClientTopicGenerationStrategy() + }); + } - public static IMqttRpcClient CreateMqttRpcClient(this MqttClientFactory _, IMqttClient mqttClient, MqttRpcClientOptions rpcClientOptions) - { - ArgumentNullException.ThrowIfNull(mqttClient); - ArgumentNullException.ThrowIfNull(rpcClientOptions); + public static IMqttRpcClient CreateMqttRpcClient(this MqttClientFactory _, IMqttClient mqttClient, MqttRpcClientOptions rpcClientOptions) + { + ArgumentNullException.ThrowIfNull(mqttClient); + ArgumentNullException.ThrowIfNull(rpcClientOptions); - return new MqttRpcClient(mqttClient, rpcClientOptions); - } + return new MqttRpcClient(mqttClient, rpcClientOptions); } } \ No newline at end of file diff --git a/Source/MQTTnet.Extensions.Rpc/MqttRpcClient.cs b/Source/MQTTnet.Extensions.Rpc/MqttRpcClient.cs index c64b93c2..fc83bff1 100644 --- a/Source/MQTTnet.Extensions.Rpc/MqttRpcClient.cs +++ b/Source/MQTTnet.Extensions.Rpc/MqttRpcClient.cs @@ -6,7 +6,6 @@ using System; using System.Buffers; using System.Collections.Concurrent; using System.Collections.Generic; -using System.Linq; using System.Threading; using System.Threading.Tasks; using MQTTnet.Exceptions; @@ -14,128 +13,125 @@ using MQTTnet.Formatter; using MQTTnet.Internal; using MQTTnet.Protocol; -namespace MQTTnet.Extensions.Rpc +namespace MQTTnet.Extensions.Rpc; + +public sealed class MqttRpcClient : IMqttRpcClient { - public sealed class MqttRpcClient : IMqttRpcClient + readonly IMqttClient _mqttClient; + readonly MqttRpcClientOptions _options; + + readonly ConcurrentDictionary> _waitingCalls = new ConcurrentDictionary>(); + + public MqttRpcClient(IMqttClient mqttClient, MqttRpcClientOptions options) { - readonly IMqttClient _mqttClient; - readonly MqttRpcClientOptions _options; + _mqttClient = mqttClient ?? throw new ArgumentNullException(nameof(mqttClient)); + _options = options ?? throw new ArgumentNullException(nameof(options)); - readonly ConcurrentDictionary> _waitingCalls = new ConcurrentDictionary>(); + _mqttClient.ApplicationMessageReceivedAsync += HandleApplicationMessageReceivedAsync; + } - public MqttRpcClient(IMqttClient mqttClient, MqttRpcClientOptions options) + public void Dispose() + { + _mqttClient.ApplicationMessageReceivedAsync -= HandleApplicationMessageReceivedAsync; + + foreach (var tcs in _waitingCalls) { - _mqttClient = mqttClient ?? throw new ArgumentNullException(nameof(mqttClient)); - _options = options ?? throw new ArgumentNullException(nameof(options)); - - _mqttClient.ApplicationMessageReceivedAsync += HandleApplicationMessageReceivedAsync; + tcs.Value.TrySetCanceled(); } - public void Dispose() + _waitingCalls.Clear(); + } + + public async Task ExecuteAsync(TimeSpan timeout, string methodName, byte[] payload, MqttQualityOfServiceLevel qualityOfServiceLevel, IDictionary parameters = null) + { + using var timeoutToken = new CancellationTokenSource(timeout); + try { - _mqttClient.ApplicationMessageReceivedAsync -= HandleApplicationMessageReceivedAsync; - - foreach (var tcs in _waitingCalls) - { - tcs.Value.TrySetCanceled(); - } - - _waitingCalls.Clear(); + return await ExecuteAsync(methodName, payload, qualityOfServiceLevel, parameters, timeoutToken.Token).ConfigureAwait(false); } - - public async Task ExecuteAsync(TimeSpan timeout, string methodName, byte[] payload, MqttQualityOfServiceLevel qualityOfServiceLevel, IDictionary parameters = null) + catch (OperationCanceledException exception) { - using (var timeoutToken = new CancellationTokenSource(timeout)) + if (timeoutToken.IsCancellationRequested) { - try - { - return await ExecuteAsync(methodName, payload, qualityOfServiceLevel, parameters, timeoutToken.Token).ConfigureAwait(false); - } - catch (OperationCanceledException exception) - { - if (timeoutToken.IsCancellationRequested) - { - throw new MqttCommunicationTimedOutException(exception); - } - - throw; - } - } - } - - public async Task ExecuteAsync(string methodName, byte[] payload, MqttQualityOfServiceLevel qualityOfServiceLevel, IDictionary parameters = null, CancellationToken cancellationToken = default) - { - ArgumentNullException.ThrowIfNull(methodName); - - var context = new TopicGenerationContext(_mqttClient, _options, methodName, parameters, qualityOfServiceLevel); - var topicNames = _options.TopicGenerationStrategy.CreateRpcTopics(context); - - var requestTopic = topicNames.RequestTopic; - var responseTopic = topicNames.ResponseTopic; - - if (string.IsNullOrWhiteSpace(requestTopic)) - { - throw new MqttProtocolViolationException("RPC request topic is empty."); + throw new MqttCommunicationTimedOutException(exception); } - if (string.IsNullOrWhiteSpace(responseTopic)) - { - throw new MqttProtocolViolationException("RPC response topic is empty."); - } - - var requestMessageBuilder = new MqttApplicationMessageBuilder().WithTopic(requestTopic).WithPayload(payload).WithQualityOfServiceLevel(qualityOfServiceLevel); - - if (_mqttClient.Options.ProtocolVersion == MqttProtocolVersion.V500) - { - requestMessageBuilder.WithResponseTopic(responseTopic); - } - - var requestMessage = requestMessageBuilder.Build(); - - try - { - var awaitable = new AsyncTaskCompletionSource(); - - if (!_waitingCalls.TryAdd(responseTopic, awaitable)) - { - throw new InvalidOperationException(); - } - - var subscribeOptions = new MqttClientSubscribeOptionsBuilder().WithTopicFilter(responseTopic, qualityOfServiceLevel).Build(); - - await _mqttClient.SubscribeAsync(subscribeOptions, cancellationToken).ConfigureAwait(false); - await _mqttClient.PublishAsync(requestMessage, cancellationToken).ConfigureAwait(false); - - using (cancellationToken.Register( - () => - { - awaitable.TrySetCanceled(); - })) - { - return await awaitable.Task.ConfigureAwait(false); - } - } - finally - { - _waitingCalls.TryRemove(responseTopic, out _); - await _mqttClient.UnsubscribeAsync(responseTopic, CancellationToken.None).ConfigureAwait(false); - } - } - - Task HandleApplicationMessageReceivedAsync(MqttApplicationMessageReceivedEventArgs eventArgs) - { - if (!_waitingCalls.TryRemove(eventArgs.ApplicationMessage.Topic, out var awaitable)) - { - return CompletedTask.Instance; - } - - var payloadBuffer = eventArgs.ApplicationMessage.Payload.ToArray(); - awaitable.TrySetResult(payloadBuffer); - - // Set this message to handled to that other code can avoid execution etc. - eventArgs.IsHandled = true; - - return CompletedTask.Instance; + throw; } } + + public async Task ExecuteAsync(string methodName, byte[] payload, MqttQualityOfServiceLevel qualityOfServiceLevel, IDictionary parameters = null, CancellationToken cancellationToken = default) + { + ArgumentNullException.ThrowIfNull(methodName); + + var context = new TopicGenerationContext(_mqttClient, _options, methodName, parameters, qualityOfServiceLevel); + var topicNames = _options.TopicGenerationStrategy.CreateRpcTopics(context); + + var requestTopic = topicNames.RequestTopic; + var responseTopic = topicNames.ResponseTopic; + + if (string.IsNullOrWhiteSpace(requestTopic)) + { + throw new MqttProtocolViolationException("RPC request topic is empty."); + } + + if (string.IsNullOrWhiteSpace(responseTopic)) + { + throw new MqttProtocolViolationException("RPC response topic is empty."); + } + + var requestMessageBuilder = new MqttApplicationMessageBuilder().WithTopic(requestTopic).WithPayload(payload).WithQualityOfServiceLevel(qualityOfServiceLevel); + + if (_mqttClient.Options.ProtocolVersion == MqttProtocolVersion.V500) + { + requestMessageBuilder.WithResponseTopic(responseTopic); + } + + var requestMessage = requestMessageBuilder.Build(); + + try + { + var awaitable = new AsyncTaskCompletionSource(); + + if (!_waitingCalls.TryAdd(responseTopic, awaitable)) + { + throw new InvalidOperationException(); + } + + var subscribeOptions = new MqttClientSubscribeOptionsBuilder().WithTopicFilter(responseTopic, qualityOfServiceLevel).Build(); + + await _mqttClient.SubscribeAsync(subscribeOptions, cancellationToken).ConfigureAwait(false); + await _mqttClient.PublishAsync(requestMessage, cancellationToken).ConfigureAwait(false); + + using (cancellationToken.Register( + () => + { + awaitable.TrySetCanceled(); + })) + { + return await awaitable.Task.ConfigureAwait(false); + } + } + finally + { + _waitingCalls.TryRemove(responseTopic, out _); + await _mqttClient.UnsubscribeAsync(responseTopic, CancellationToken.None).ConfigureAwait(false); + } + } + + Task HandleApplicationMessageReceivedAsync(MqttApplicationMessageReceivedEventArgs eventArgs) + { + if (!_waitingCalls.TryRemove(eventArgs.ApplicationMessage.Topic, out var awaitable)) + { + return CompletedTask.Instance; + } + + var payloadBuffer = eventArgs.ApplicationMessage.Payload.ToArray(); + awaitable.TrySetResult(payloadBuffer); + + // Set this message to handled to that other code can avoid execution etc. + eventArgs.IsHandled = true; + + return CompletedTask.Instance; + } } \ No newline at end of file diff --git a/Source/MQTTnet.Extensions.Rpc/MqttRpcClientExtensions.cs b/Source/MQTTnet.Extensions.Rpc/MqttRpcClientExtensions.cs index a0b0ebfe..bde8aefc 100644 --- a/Source/MQTTnet.Extensions.Rpc/MqttRpcClientExtensions.cs +++ b/Source/MQTTnet.Extensions.Rpc/MqttRpcClientExtensions.cs @@ -8,17 +8,16 @@ using System.Text; using System.Threading.Tasks; using MQTTnet.Protocol; -namespace MQTTnet.Extensions.Rpc +namespace MQTTnet.Extensions.Rpc; + +public static class MqttRpcClientExtensions { - public static class MqttRpcClientExtensions + public static Task ExecuteAsync(this IMqttRpcClient client, TimeSpan timeout, string methodName, string payload, MqttQualityOfServiceLevel qualityOfServiceLevel, IDictionary parameters = null) { - public static Task ExecuteAsync(this IMqttRpcClient client, TimeSpan timeout, string methodName, string payload, MqttQualityOfServiceLevel qualityOfServiceLevel, IDictionary parameters = null) - { - if (client == null) throw new ArgumentNullException(nameof(client)); + if (client == null) throw new ArgumentNullException(nameof(client)); - var buffer = Encoding.UTF8.GetBytes(payload ?? string.Empty); + var buffer = Encoding.UTF8.GetBytes(payload ?? string.Empty); - return client.ExecuteAsync(timeout, methodName, buffer, qualityOfServiceLevel, parameters); - } + return client.ExecuteAsync(timeout, methodName, buffer, qualityOfServiceLevel, parameters); } } \ No newline at end of file diff --git a/Source/MQTTnet.Extensions.Rpc/MqttRpcClientOptions.cs b/Source/MQTTnet.Extensions.Rpc/MqttRpcClientOptions.cs index bb891705..251282ee 100644 --- a/Source/MQTTnet.Extensions.Rpc/MqttRpcClientOptions.cs +++ b/Source/MQTTnet.Extensions.Rpc/MqttRpcClientOptions.cs @@ -2,10 +2,9 @@ // The .NET Foundation licenses this file to you under the MIT license. // See the LICENSE file in the project root for more information. -namespace MQTTnet.Extensions.Rpc +namespace MQTTnet.Extensions.Rpc; + +public sealed class MqttRpcClientOptions { - public sealed class MqttRpcClientOptions - { - public IMqttRpcClientTopicGenerationStrategy TopicGenerationStrategy { get; set; } = new DefaultMqttRpcClientTopicGenerationStrategy(); - } -} + public IMqttRpcClientTopicGenerationStrategy TopicGenerationStrategy { get; set; } = new DefaultMqttRpcClientTopicGenerationStrategy(); +} \ No newline at end of file diff --git a/Source/MQTTnet.Extensions.Rpc/MqttRpcClientOptionsBuilder.cs b/Source/MQTTnet.Extensions.Rpc/MqttRpcClientOptionsBuilder.cs index 5fb0ea2e..e0566035 100644 --- a/Source/MQTTnet.Extensions.Rpc/MqttRpcClientOptionsBuilder.cs +++ b/Source/MQTTnet.Extensions.Rpc/MqttRpcClientOptionsBuilder.cs @@ -4,25 +4,24 @@ using System; -namespace MQTTnet.Extensions.Rpc +namespace MQTTnet.Extensions.Rpc; + +public sealed class MqttRpcClientOptionsBuilder { - public sealed class MqttRpcClientOptionsBuilder + IMqttRpcClientTopicGenerationStrategy _topicGenerationStrategy = new DefaultMqttRpcClientTopicGenerationStrategy(); + + public MqttRpcClientOptions Build() { - IMqttRpcClientTopicGenerationStrategy _topicGenerationStrategy = new DefaultMqttRpcClientTopicGenerationStrategy(); - - public MqttRpcClientOptions Build() + return new MqttRpcClientOptions { - return new MqttRpcClientOptions - { - TopicGenerationStrategy = _topicGenerationStrategy - }; - } + TopicGenerationStrategy = _topicGenerationStrategy + }; + } - public MqttRpcClientOptionsBuilder WithTopicGenerationStrategy(IMqttRpcClientTopicGenerationStrategy value) - { - _topicGenerationStrategy = value ?? throw new ArgumentNullException(nameof(value)); + public MqttRpcClientOptionsBuilder WithTopicGenerationStrategy(IMqttRpcClientTopicGenerationStrategy value) + { + _topicGenerationStrategy = value ?? throw new ArgumentNullException(nameof(value)); - return this; - } + return this; } } \ No newline at end of file diff --git a/Source/MQTTnet.Extensions.Rpc/MqttRpcTopicPair.cs b/Source/MQTTnet.Extensions.Rpc/MqttRpcTopicPair.cs index 4cbb1660..2cdf7ffc 100644 --- a/Source/MQTTnet.Extensions.Rpc/MqttRpcTopicPair.cs +++ b/Source/MQTTnet.Extensions.Rpc/MqttRpcTopicPair.cs @@ -2,12 +2,11 @@ // The .NET Foundation licenses this file to you under the MIT license. // See the LICENSE file in the project root for more information. -namespace MQTTnet.Extensions.Rpc -{ - public sealed class MqttRpcTopicPair - { - public string RequestTopic { get; set; } +namespace MQTTnet.Extensions.Rpc; - public string ResponseTopic { get; set; } - } -} +public sealed class MqttRpcTopicPair +{ + public string RequestTopic { get; set; } + + public string ResponseTopic { get; set; } +} \ No newline at end of file diff --git a/Source/MQTTnet.Extensions.Rpc/TopicGenerationContext.cs b/Source/MQTTnet.Extensions.Rpc/TopicGenerationContext.cs index c40544d2..9559f638 100644 --- a/Source/MQTTnet.Extensions.Rpc/TopicGenerationContext.cs +++ b/Source/MQTTnet.Extensions.Rpc/TopicGenerationContext.cs @@ -6,37 +6,36 @@ using System; using System.Collections.Generic; using MQTTnet.Protocol; -namespace MQTTnet.Extensions.Rpc +namespace MQTTnet.Extensions.Rpc; + +public sealed class TopicGenerationContext { - public sealed class TopicGenerationContext + public TopicGenerationContext(IMqttClient mqttClient, MqttRpcClientOptions options, string methodName, + IDictionary parameters, MqttQualityOfServiceLevel qualityOfServiceLevel) { - public TopicGenerationContext(IMqttClient mqttClient, MqttRpcClientOptions options, string methodName, - IDictionary parameters, MqttQualityOfServiceLevel qualityOfServiceLevel) - { - MethodName = methodName ?? throw new ArgumentNullException(nameof(methodName)); - Parameters = parameters; - QualityOfServiceLevel = qualityOfServiceLevel; - MqttClient = mqttClient ?? throw new ArgumentNullException(nameof(mqttClient)); - Options = options ?? throw new ArgumentNullException(nameof(options)); - } - - public string MethodName { get; } - - public IDictionary Parameters { get; } - - public IMqttClient MqttClient { get; } - - public MqttRpcClientOptions Options { get; } - - /// - /// Gets or sets the quality of service level. - /// The Quality of Service (QoS) level is an agreement between the sender of a message and the receiver of a message - /// that defines the guarantee of delivery for a specific message. - /// There are 3 QoS levels in MQTT: - /// - At most once (0): Message gets delivered no time, once or multiple times. - /// - At least once (1): Message gets delivered at least once (one time or more often). - /// - Exactly once (2): Message gets delivered exactly once (It's ensured that the message only comes once). - /// - public MqttQualityOfServiceLevel QualityOfServiceLevel { get; } + MethodName = methodName ?? throw new ArgumentNullException(nameof(methodName)); + Parameters = parameters; + QualityOfServiceLevel = qualityOfServiceLevel; + MqttClient = mqttClient ?? throw new ArgumentNullException(nameof(mqttClient)); + Options = options ?? throw new ArgumentNullException(nameof(options)); } + + public string MethodName { get; } + + public IDictionary Parameters { get; } + + public IMqttClient MqttClient { get; } + + public MqttRpcClientOptions Options { get; } + + /// + /// Gets or sets the quality of service level. + /// The Quality of Service (QoS) level is an agreement between the sender of a message and the receiver of a message + /// that defines the guarantee of delivery for a specific message. + /// There are 3 QoS levels in MQTT: + /// - At most once (0): Message gets delivered no time, once or multiple times. + /// - At least once (1): Message gets delivered at least once (one time or more often). + /// - Exactly once (2): Message gets delivered exactly once (It's ensured that the message only comes once). + /// + public MqttQualityOfServiceLevel QualityOfServiceLevel { get; } } \ No newline at end of file diff --git a/Source/MQTTnet.Extensions.TopicTemplate/MQTTnet.Extensions.TopicTemplate.csproj b/Source/MQTTnet.Extensions.TopicTemplate/MQTTnet.Extensions.TopicTemplate.csproj index 5bfcfeb5..01568c41 100644 --- a/Source/MQTTnet.Extensions.TopicTemplate/MQTTnet.Extensions.TopicTemplate.csproj +++ b/Source/MQTTnet.Extensions.TopicTemplate/MQTTnet.Extensions.TopicTemplate.csproj @@ -47,7 +47,7 @@ - + diff --git a/Source/MQTTnet.Extensions.TopicTemplate/MqttTopicTemplate.cs b/Source/MQTTnet.Extensions.TopicTemplate/MqttTopicTemplate.cs index 66b7369e..30dbf6e5 100644 --- a/Source/MQTTnet.Extensions.TopicTemplate/MqttTopicTemplate.cs +++ b/Source/MQTTnet.Extensions.TopicTemplate/MqttTopicTemplate.cs @@ -230,7 +230,7 @@ public sealed class MqttTopicTemplate : IEquatable throw new ArgumentException("the topic has to match this template", nameof(topic)); } - return parseParameterValuesInternal(topic); + return ParseParameterValuesInternal(topic); } /// @@ -330,7 +330,7 @@ public sealed class MqttTopicTemplate : IEquatable return parameters.Aggregate(this, (t, p) => t.TrySetParameter(p.parameter, p.value)); } - IEnumerable<(string parameter, int index, string value)> parseParameterValuesInternal(string topic) + IEnumerable<(string parameter, int index, string value)> ParseParameterValuesInternal(string topic) { // because we have a match, we know the segment array is at least the template's length var segments = topic.Split(MqttTopicFilterComparer.LevelSeparator); diff --git a/Source/MQTTnet.Server/Disconnecting/MqttServerClientDisconnectOptions.cs b/Source/MQTTnet.Server/Disconnecting/MqttServerClientDisconnectOptions.cs index 763217fb..fe4c6be4 100644 --- a/Source/MQTTnet.Server/Disconnecting/MqttServerClientDisconnectOptions.cs +++ b/Source/MQTTnet.Server/Disconnecting/MqttServerClientDisconnectOptions.cs @@ -5,28 +5,27 @@ using MQTTnet.Packets; using MQTTnet.Protocol; -namespace MQTTnet.Server +namespace MQTTnet.Server; + +public sealed class MqttServerClientDisconnectOptions { - public sealed class MqttServerClientDisconnectOptions - { - public MqttDisconnectReasonCode ReasonCode { get; set; } = MqttDisconnectReasonCode.NormalDisconnection; + public MqttDisconnectReasonCode ReasonCode { get; set; } = MqttDisconnectReasonCode.NormalDisconnection; - /// - /// The reason string is sent to every client via a DISCONNECT packet. - /// MQTT 5.0.0+ feature. - /// - public string ReasonString { get; set; } + /// + /// The reason string is sent to every client via a DISCONNECT packet. + /// MQTT 5.0.0+ feature. + /// + public string ReasonString { get; set; } - /// - /// The server reference is sent to every client via a DISCONNECT packet. - /// MQTT 5.0.0+ feature. - /// - public string ServerReference { get; set; } + /// + /// The server reference is sent to every client via a DISCONNECT packet. + /// MQTT 5.0.0+ feature. + /// + public string ServerReference { get; set; } - /// - /// These user properties are sent to every client via a DISCONNECT packet. - /// MQTT 5.0.0+ feature. - /// - public List UserProperties { get; set; } - } + /// + /// These user properties are sent to every client via a DISCONNECT packet. + /// MQTT 5.0.0+ feature. + /// + public List UserProperties { get; set; } } \ No newline at end of file diff --git a/Source/MQTTnet.Server/Disconnecting/MqttServerClientDisconnectOptionsBuilder.cs b/Source/MQTTnet.Server/Disconnecting/MqttServerClientDisconnectOptionsBuilder.cs index 7173ac64..8cac4369 100644 --- a/Source/MQTTnet.Server/Disconnecting/MqttServerClientDisconnectOptionsBuilder.cs +++ b/Source/MQTTnet.Server/Disconnecting/MqttServerClientDisconnectOptionsBuilder.cs @@ -5,50 +5,49 @@ using MQTTnet.Packets; using MQTTnet.Protocol; -namespace MQTTnet.Server +namespace MQTTnet.Server; + +public sealed class MqttServerClientDisconnectOptionsBuilder { - public sealed class MqttServerClientDisconnectOptionsBuilder + readonly MqttServerClientDisconnectOptions _options = new MqttServerClientDisconnectOptions(); + + public MqttServerClientDisconnectOptions Build() { - readonly MqttServerClientDisconnectOptions _options = new MqttServerClientDisconnectOptions(); + return _options; + } - public MqttServerClientDisconnectOptions Build() + public MqttServerClientDisconnectOptionsBuilder WithReasonCode(MqttDisconnectReasonCode value) + { + _options.ReasonCode = value; + return this; + } + + public MqttServerClientDisconnectOptionsBuilder WithReasonString(string value) + { + _options.ReasonString = value; + return this; + } + + public MqttServerClientDisconnectOptionsBuilder WithServerReference(string value) + { + _options.ServerReference = value; + return this; + } + + public MqttServerClientDisconnectOptionsBuilder WithUserProperties(List value) + { + _options.UserProperties = value; + return this; + } + + public MqttServerClientDisconnectOptionsBuilder WithUserProperty(string name, string value) + { + if (_options.UserProperties == null) { - return _options; + _options.UserProperties = new List(); } - public MqttServerClientDisconnectOptionsBuilder WithReasonCode(MqttDisconnectReasonCode value) - { - _options.ReasonCode = value; - return this; - } - - public MqttServerClientDisconnectOptionsBuilder WithReasonString(string value) - { - _options.ReasonString = value; - return this; - } - - public MqttServerClientDisconnectOptionsBuilder WithServerReference(string value) - { - _options.ServerReference = value; - return this; - } - - public MqttServerClientDisconnectOptionsBuilder WithUserProperties(List value) - { - _options.UserProperties = value; - return this; - } - - public MqttServerClientDisconnectOptionsBuilder WithUserProperty(string name, string value) - { - if (_options.UserProperties == null) - { - _options.UserProperties = new List(); - } - - _options.UserProperties.Add(new MqttUserProperty(name, value)); - return this; - } + _options.UserProperties.Add(new MqttUserProperty(name, value)); + return this; } } \ No newline at end of file diff --git a/Source/MQTTnet.Server/Events/ApplicationMessageEnqueuedEventArgs.cs b/Source/MQTTnet.Server/Events/ApplicationMessageEnqueuedEventArgs.cs index 518c0a58..a85b1321 100644 --- a/Source/MQTTnet.Server/Events/ApplicationMessageEnqueuedEventArgs.cs +++ b/Source/MQTTnet.Server/Events/ApplicationMessageEnqueuedEventArgs.cs @@ -2,26 +2,23 @@ // The .NET Foundation licenses this file to you under the MIT license. // See the LICENSE file in the project root for more information. -using System; +namespace MQTTnet.Server; -namespace MQTTnet.Server +public sealed class ApplicationMessageEnqueuedEventArgs : EventArgs { - public sealed class ApplicationMessageEnqueuedEventArgs : EventArgs + public ApplicationMessageEnqueuedEventArgs(string senderClientId, string receiverClientId, MqttApplicationMessage applicationMessage, bool isDropped) { - public ApplicationMessageEnqueuedEventArgs(string senderClientId, string receiverClientId, MqttApplicationMessage applicationMessage, bool isDropped) - { - SenderClientId = senderClientId ?? throw new ArgumentNullException( nameof(senderClientId)); - ReceiverClientId = receiverClientId ?? throw new ArgumentNullException(nameof(receiverClientId)); - ApplicationMessage = applicationMessage ?? throw new ArgumentNullException(nameof(applicationMessage)); - IsDropped = isDropped; - } - - public string SenderClientId { get; } - - public string ReceiverClientId { get; } - - public bool IsDropped { get; } - - public MqttApplicationMessage ApplicationMessage { get; } + SenderClientId = senderClientId ?? throw new ArgumentNullException( nameof(senderClientId)); + ReceiverClientId = receiverClientId ?? throw new ArgumentNullException(nameof(receiverClientId)); + ApplicationMessage = applicationMessage ?? throw new ArgumentNullException(nameof(applicationMessage)); + IsDropped = isDropped; } + + public string SenderClientId { get; } + + public string ReceiverClientId { get; } + + public bool IsDropped { get; } + + public MqttApplicationMessage ApplicationMessage { get; } } \ No newline at end of file diff --git a/Source/MQTTnet.Server/Events/ApplicationMessageNotConsumedEventArgs.cs b/Source/MQTTnet.Server/Events/ApplicationMessageNotConsumedEventArgs.cs index d81e6bb7..212572f3 100644 --- a/Source/MQTTnet.Server/Events/ApplicationMessageNotConsumedEventArgs.cs +++ b/Source/MQTTnet.Server/Events/ApplicationMessageNotConsumedEventArgs.cs @@ -2,26 +2,23 @@ // The .NET Foundation licenses this file to you under the MIT license. // See the LICENSE file in the project root for more information. -using System; +namespace MQTTnet.Server; -namespace MQTTnet.Server +public sealed class ApplicationMessageNotConsumedEventArgs : EventArgs { - public sealed class ApplicationMessageNotConsumedEventArgs : EventArgs + public ApplicationMessageNotConsumedEventArgs(MqttApplicationMessage applicationMessage, string senderId) { - public ApplicationMessageNotConsumedEventArgs(MqttApplicationMessage applicationMessage, string senderId) - { - ApplicationMessage = applicationMessage ?? throw new ArgumentNullException(nameof(applicationMessage)); - SenderId = senderId; - } - - /// - /// Gets the application message which was not consumed by any client. - /// - public MqttApplicationMessage ApplicationMessage { get; } - - /// - /// Gets the ID of the client which has sent the affected application message. - /// - public string SenderId { get; } + ApplicationMessage = applicationMessage ?? throw new ArgumentNullException(nameof(applicationMessage)); + SenderId = senderId; } + + /// + /// Gets the application message which was not consumed by any client. + /// + public MqttApplicationMessage ApplicationMessage { get; } + + /// + /// Gets the ID of the client which has sent the affected application message. + /// + public string SenderId { get; } } \ No newline at end of file diff --git a/Source/MQTTnet.Server/Events/ClientAcknowledgedPublishPacketEventArgs.cs b/Source/MQTTnet.Server/Events/ClientAcknowledgedPublishPacketEventArgs.cs index 8a206cc2..316c8641 100644 --- a/Source/MQTTnet.Server/Events/ClientAcknowledgedPublishPacketEventArgs.cs +++ b/Source/MQTTnet.Server/Events/ClientAcknowledgedPublishPacketEventArgs.cs @@ -2,51 +2,49 @@ // The .NET Foundation licenses this file to you under the MIT license. // See the LICENSE file in the project root for more information. -using System; using System.Collections; using MQTTnet.Packets; -namespace MQTTnet.Server +namespace MQTTnet.Server; + +public sealed class ClientAcknowledgedPublishPacketEventArgs : EventArgs { - public sealed class ClientAcknowledgedPublishPacketEventArgs : EventArgs + public ClientAcknowledgedPublishPacketEventArgs(string clientId, string userName, IDictionary sessionItems, MqttPublishPacket publishPacket, MqttPacketWithIdentifier acknowledgePacket) { - public ClientAcknowledgedPublishPacketEventArgs(string clientId, string userName, IDictionary sessionItems, MqttPublishPacket publishPacket, MqttPacketWithIdentifier acknowledgePacket) - { - ClientId = clientId ?? throw new ArgumentNullException(nameof(clientId)); - UserName = userName; - SessionItems = sessionItems ?? throw new ArgumentNullException(nameof(sessionItems)); - PublishPacket = publishPacket ?? throw new ArgumentNullException(nameof(publishPacket)); - AcknowledgePacket = acknowledgePacket ?? throw new ArgumentNullException(nameof(acknowledgePacket)); - } - - /// - /// Gets the packet which was used for acknowledge. This can be a PubAck or PubComp packet. - /// - public MqttPacketWithIdentifier AcknowledgePacket { get; } - - /// - /// Gets the ID of the client which acknowledged a PUBLISH packet. - /// - public string ClientId { get; } - - /// - /// Gets the user name of the client. - /// - public string UserName { get; } - - /// - /// Gets whether the PUBLISH packet is fully acknowledged. This is the case for PUBACK (QoS 1) and PUBCOMP (QoS 2. - /// - public bool IsCompleted => AcknowledgePacket is MqttPubAckPacket || AcknowledgePacket is MqttPubCompPacket; - - /// - /// Gets the PUBLISH packet which was acknowledged. - /// - public MqttPublishPacket PublishPacket { get; } - - /// - /// Gets the session items which contain custom user data per session. - /// - public IDictionary SessionItems { get; } + ClientId = clientId ?? throw new ArgumentNullException(nameof(clientId)); + UserName = userName; + SessionItems = sessionItems ?? throw new ArgumentNullException(nameof(sessionItems)); + PublishPacket = publishPacket ?? throw new ArgumentNullException(nameof(publishPacket)); + AcknowledgePacket = acknowledgePacket ?? throw new ArgumentNullException(nameof(acknowledgePacket)); } + + /// + /// Gets the packet which was used for acknowledge. This can be a PubAck or PubComp packet. + /// + public MqttPacketWithIdentifier AcknowledgePacket { get; } + + /// + /// Gets the ID of the client which acknowledged a PUBLISH packet. + /// + public string ClientId { get; } + + /// + /// Gets the user name of the client. + /// + public string UserName { get; } + + /// + /// Gets whether the PUBLISH packet is fully acknowledged. This is the case for PUBACK (QoS 1) and PUBCOMP (QoS 2. + /// + public bool IsCompleted => AcknowledgePacket is MqttPubAckPacket || AcknowledgePacket is MqttPubCompPacket; + + /// + /// Gets the PUBLISH packet which was acknowledged. + /// + public MqttPublishPacket PublishPacket { get; } + + /// + /// Gets the session items which contain custom user data per session. + /// + public IDictionary SessionItems { get; } } \ No newline at end of file diff --git a/Source/MQTTnet.Server/Events/ClientConnectedEventArgs.cs b/Source/MQTTnet.Server/Events/ClientConnectedEventArgs.cs index 4249eea6..718ecb65 100644 --- a/Source/MQTTnet.Server/Events/ClientConnectedEventArgs.cs +++ b/Source/MQTTnet.Server/Events/ClientConnectedEventArgs.cs @@ -2,70 +2,67 @@ // The .NET Foundation licenses this file to you under the MIT license. // See the LICENSE file in the project root for more information. -using System; using System.Collections; -using System.Collections.Generic; using System.Net; using System.Text; using MQTTnet.Formatter; using MQTTnet.Packets; -namespace MQTTnet.Server +namespace MQTTnet.Server; + +public sealed class ClientConnectedEventArgs : EventArgs { - public sealed class ClientConnectedEventArgs : EventArgs + readonly MqttConnectPacket _connectPacket; + + public ClientConnectedEventArgs(MqttConnectPacket connectPacket, MqttProtocolVersion protocolVersion, EndPoint remoteEndPoint, IDictionary sessionItems) { - readonly MqttConnectPacket _connectPacket; - - public ClientConnectedEventArgs(MqttConnectPacket connectPacket, MqttProtocolVersion protocolVersion, EndPoint remoteEndPoint, IDictionary sessionItems) - { - _connectPacket = connectPacket ?? throw new ArgumentNullException(nameof(connectPacket)); - ProtocolVersion = protocolVersion; - RemoteEndPoint = remoteEndPoint; - SessionItems = sessionItems ?? throw new ArgumentNullException(nameof(sessionItems)); - } - - public byte[] AuthenticationData => _connectPacket.AuthenticationData; - - public string AuthenticationMethod => _connectPacket.AuthenticationMethod; - - /// - /// Gets the client identifier of the connected client. - /// Hint: This identifier needs to be unique over all used clients / devices on the broker to avoid connection issues. - /// - public string ClientId => _connectPacket.ClientId; - - /// - /// Gets the endpoint of the connected client. - /// - public EndPoint RemoteEndPoint { get; } - - [Obsolete("Use RemoteEndPoint instead.")] - public string Endpoint => RemoteEndPoint?.ToString(); - - /// - /// Gets the protocol version which is used by the connected client. - /// - public MqttProtocolVersion ProtocolVersion { get; } - - /// - /// Gets or sets a key/value collection that can be used to share data within the scope of this session. - /// - public IDictionary SessionItems { get; } - - /// - /// Gets the user name of the connected client. - /// - public string UserName => _connectPacket.Username; - - /// - /// Gets the password of the connected client. - /// - public string Password => Encoding.UTF8.GetString(_connectPacket.Password.AsSpan()); - - /// - /// Gets the user properties sent by the client. - /// MQTT 5.0.0+ feature. - /// - public List UserProperties => _connectPacket?.UserProperties; + _connectPacket = connectPacket ?? throw new ArgumentNullException(nameof(connectPacket)); + ProtocolVersion = protocolVersion; + RemoteEndPoint = remoteEndPoint; + SessionItems = sessionItems ?? throw new ArgumentNullException(nameof(sessionItems)); } + + public byte[] AuthenticationData => _connectPacket.AuthenticationData; + + public string AuthenticationMethod => _connectPacket.AuthenticationMethod; + + /// + /// Gets the client identifier of the connected client. + /// Hint: This identifier needs to be unique over all used clients / devices on the broker to avoid connection issues. + /// + public string ClientId => _connectPacket.ClientId; + + /// + /// Gets the endpoint of the connected client. + /// + public EndPoint RemoteEndPoint { get; } + + [Obsolete("Use RemoteEndPoint instead.")] + public string Endpoint => RemoteEndPoint?.ToString(); + + /// + /// Gets the protocol version which is used by the connected client. + /// + public MqttProtocolVersion ProtocolVersion { get; } + + /// + /// Gets or sets a key/value collection that can be used to share data within the scope of this session. + /// + public IDictionary SessionItems { get; } + + /// + /// Gets the user name of the connected client. + /// + public string UserName => _connectPacket.Username; + + /// + /// Gets the password of the connected client. + /// + public string Password => Encoding.UTF8.GetString(_connectPacket.Password.AsSpan()); + + /// + /// Gets the user properties sent by the client. + /// MQTT 5.0.0+ feature. + /// + public List UserProperties => _connectPacket?.UserProperties; } \ No newline at end of file diff --git a/Source/MQTTnet.Server/Events/ClientDisconnectedEventArgs.cs b/Source/MQTTnet.Server/Events/ClientDisconnectedEventArgs.cs index f64a2f3b..9cb85202 100644 --- a/Source/MQTTnet.Server/Events/ClientDisconnectedEventArgs.cs +++ b/Source/MQTTnet.Server/Events/ClientDisconnectedEventArgs.cs @@ -2,91 +2,88 @@ // The .NET Foundation licenses this file to you under the MIT license. // See the LICENSE file in the project root for more information. -using System; using System.Collections; -using System.Collections.Generic; using System.Net; using System.Text; using MQTTnet.Packets; using MQTTnet.Protocol; -namespace MQTTnet.Server +namespace MQTTnet.Server; + +public sealed class ClientDisconnectedEventArgs : EventArgs { - public sealed class ClientDisconnectedEventArgs : EventArgs + readonly MqttConnectPacket _connectPacket; + readonly MqttDisconnectPacket _disconnectPacket; + + public ClientDisconnectedEventArgs( + MqttConnectPacket connectPacket, + MqttDisconnectPacket disconnectPacket, + MqttClientDisconnectType disconnectType, + EndPoint remoteEndPoint, + IDictionary sessionItems) { - readonly MqttConnectPacket _connectPacket; - readonly MqttDisconnectPacket _disconnectPacket; + DisconnectType = disconnectType; + RemoteEndPoint = remoteEndPoint; + SessionItems = sessionItems ?? throw new ArgumentNullException(nameof(sessionItems)); - public ClientDisconnectedEventArgs( - MqttConnectPacket connectPacket, - MqttDisconnectPacket disconnectPacket, - MqttClientDisconnectType disconnectType, - EndPoint remoteEndPoint, - IDictionary sessionItems) - { - DisconnectType = disconnectType; - RemoteEndPoint = remoteEndPoint; - SessionItems = sessionItems ?? throw new ArgumentNullException(nameof(sessionItems)); - - _connectPacket = connectPacket; - // The DISCONNECT packet can be null in case of a non clean disconnect or session takeover. - _disconnectPacket = disconnectPacket; - } - - /// - /// Gets the client identifier. - /// Hint: This identifier needs to be unique over all used clients / devices on the broker to avoid connection issues. - /// - public string ClientId => _connectPacket.ClientId; - - /// - /// Gets the user name of the client. - /// - public string UserName => _connectPacket.Username; - - /// - /// Gets the password of the client. - /// - public string Password => Encoding.UTF8.GetString(_connectPacket.Password.AsSpan()); - - public MqttClientDisconnectType DisconnectType { get; } - - public EndPoint RemoteEndPoint { get; } - - [Obsolete("Use RemoteEndPoint instead.")] - public string Endpoint => RemoteEndPoint?.ToString(); - - /// - /// Gets the reason code sent by the client. - /// Only available for clean disconnects. - /// MQTT 5.0.0+ feature. - /// - public MqttDisconnectReasonCode? ReasonCode => _disconnectPacket?.ReasonCode; - - /// - /// Gets the reason string sent by the client. - /// Only available for clean disconnects. - /// MQTT 5.0.0+ feature. - /// - public string ReasonString => _disconnectPacket?.ReasonString; - - /// - /// Gets the session expiry interval sent by the client. - /// Only available for clean disconnects. - /// MQTT 5.0.0+ feature. - /// - public uint SessionExpiryInterval => _disconnectPacket?.SessionExpiryInterval ?? 0; - - /// - /// Gets or sets a key/value collection that can be used to share data within the scope of this session. - /// - public IDictionary SessionItems { get; } - - /// - /// Gets the user properties sent by the client. - /// Only available for clean disconnects. - /// MQTT 5.0.0+ feature. - /// - public List UserProperties => _disconnectPacket?.UserProperties; + _connectPacket = connectPacket; + // The DISCONNECT packet can be null in case of a non clean disconnect or session takeover. + _disconnectPacket = disconnectPacket; } + + /// + /// Gets the client identifier. + /// Hint: This identifier needs to be unique over all used clients / devices on the broker to avoid connection issues. + /// + public string ClientId => _connectPacket.ClientId; + + /// + /// Gets the user name of the client. + /// + public string UserName => _connectPacket.Username; + + /// + /// Gets the password of the client. + /// + public string Password => Encoding.UTF8.GetString(_connectPacket.Password.AsSpan()); + + public MqttClientDisconnectType DisconnectType { get; } + + public EndPoint RemoteEndPoint { get; } + + [Obsolete("Use RemoteEndPoint instead.")] + public string Endpoint => RemoteEndPoint?.ToString(); + + /// + /// Gets the reason code sent by the client. + /// Only available for clean disconnects. + /// MQTT 5.0.0+ feature. + /// + public MqttDisconnectReasonCode? ReasonCode => _disconnectPacket?.ReasonCode; + + /// + /// Gets the reason string sent by the client. + /// Only available for clean disconnects. + /// MQTT 5.0.0+ feature. + /// + public string ReasonString => _disconnectPacket?.ReasonString; + + /// + /// Gets the session expiry interval sent by the client. + /// Only available for clean disconnects. + /// MQTT 5.0.0+ feature. + /// + public uint SessionExpiryInterval => _disconnectPacket?.SessionExpiryInterval ?? 0; + + /// + /// Gets or sets a key/value collection that can be used to share data within the scope of this session. + /// + public IDictionary SessionItems { get; } + + /// + /// Gets the user properties sent by the client. + /// Only available for clean disconnects. + /// MQTT 5.0.0+ feature. + /// + public List UserProperties => _disconnectPacket?.UserProperties; } \ No newline at end of file diff --git a/Source/MQTTnet.Server/Events/ClientSubscribedTopicEventArgs.cs b/Source/MQTTnet.Server/Events/ClientSubscribedTopicEventArgs.cs index 2f524683..e11f05ca 100644 --- a/Source/MQTTnet.Server/Events/ClientSubscribedTopicEventArgs.cs +++ b/Source/MQTTnet.Server/Events/ClientSubscribedTopicEventArgs.cs @@ -2,42 +2,40 @@ // The .NET Foundation licenses this file to you under the MIT license. // See the LICENSE file in the project root for more information. -using System; using System.Collections; using MQTTnet.Packets; -namespace MQTTnet.Server +namespace MQTTnet.Server; + +public sealed class ClientSubscribedTopicEventArgs : EventArgs { - public sealed class ClientSubscribedTopicEventArgs : EventArgs + public ClientSubscribedTopicEventArgs(string clientId, string userName, MqttTopicFilter topicFilter, IDictionary sessionItems) { - public ClientSubscribedTopicEventArgs(string clientId, string userName, MqttTopicFilter topicFilter, IDictionary sessionItems) - { - ClientId = clientId ?? throw new ArgumentNullException(nameof(clientId)); - UserName = userName; - TopicFilter = topicFilter ?? throw new ArgumentNullException(nameof(topicFilter)); - SessionItems = sessionItems ?? throw new ArgumentNullException(nameof(sessionItems)); - } - - /// - /// Gets the client identifier. - /// Hint: This identifier needs to be unique over all used clients / devices on the broker to avoid connection issues. - /// - public string ClientId { get; } - - /// - /// Gets the user name of the client. - /// - public string UserName { get; } - - /// - /// Gets or sets a key/value collection that can be used to share data within the scope of this session. - /// - public IDictionary SessionItems { get; } - - /// - /// Gets the topic filter. - /// The topic filter can contain topics and wildcards. - /// - public MqttTopicFilter TopicFilter { get; } + ClientId = clientId ?? throw new ArgumentNullException(nameof(clientId)); + UserName = userName; + TopicFilter = topicFilter ?? throw new ArgumentNullException(nameof(topicFilter)); + SessionItems = sessionItems ?? throw new ArgumentNullException(nameof(sessionItems)); } + + /// + /// Gets the client identifier. + /// Hint: This identifier needs to be unique over all used clients / devices on the broker to avoid connection issues. + /// + public string ClientId { get; } + + /// + /// Gets the user name of the client. + /// + public string UserName { get; } + + /// + /// Gets or sets a key/value collection that can be used to share data within the scope of this session. + /// + public IDictionary SessionItems { get; } + + /// + /// Gets the topic filter. + /// The topic filter can contain topics and wildcards. + /// + public MqttTopicFilter TopicFilter { get; } } \ No newline at end of file diff --git a/Source/MQTTnet.Server/Events/ClientUnsubscribedTopicEventArgs.cs b/Source/MQTTnet.Server/Events/ClientUnsubscribedTopicEventArgs.cs index 677236b3..484c1b73 100644 --- a/Source/MQTTnet.Server/Events/ClientUnsubscribedTopicEventArgs.cs +++ b/Source/MQTTnet.Server/Events/ClientUnsubscribedTopicEventArgs.cs @@ -2,41 +2,39 @@ // The .NET Foundation licenses this file to you under the MIT license. // See the LICENSE file in the project root for more information. -using System; using System.Collections; -namespace MQTTnet.Server +namespace MQTTnet.Server; + +public sealed class ClientUnsubscribedTopicEventArgs : EventArgs { - public sealed class ClientUnsubscribedTopicEventArgs : EventArgs + public ClientUnsubscribedTopicEventArgs(string clientId, string userName, string topicFilter, IDictionary sessionItems) { - public ClientUnsubscribedTopicEventArgs(string clientId, string userName, string topicFilter, IDictionary sessionItems) - { - ClientId = clientId ?? throw new ArgumentNullException(nameof(clientId)); - UserName = userName; - TopicFilter = topicFilter ?? throw new ArgumentNullException(nameof(topicFilter)); - SessionItems = sessionItems ?? throw new ArgumentNullException(nameof(sessionItems)); - } - - /// - /// Gets the client identifier. - /// Hint: This identifier needs to be unique over all used clients / devices on the broker to avoid connection issues. - /// - public string ClientId { get; } - - /// - /// Gets the user name of the client. - /// - public string UserName { get; } - - /// - /// Gets or sets a key/value collection that can be used to share data within the scope of this session. - /// - public IDictionary SessionItems { get; } - - /// - /// Gets or sets the topic filter. - /// The topic filter can contain topics and wildcards. - /// - public string TopicFilter { get; } + ClientId = clientId ?? throw new ArgumentNullException(nameof(clientId)); + UserName = userName; + TopicFilter = topicFilter ?? throw new ArgumentNullException(nameof(topicFilter)); + SessionItems = sessionItems ?? throw new ArgumentNullException(nameof(sessionItems)); } + + /// + /// Gets the client identifier. + /// Hint: This identifier needs to be unique over all used clients / devices on the broker to avoid connection issues. + /// + public string ClientId { get; } + + /// + /// Gets the user name of the client. + /// + public string UserName { get; } + + /// + /// Gets or sets a key/value collection that can be used to share data within the scope of this session. + /// + public IDictionary SessionItems { get; } + + /// + /// Gets or sets the topic filter. + /// The topic filter can contain topics and wildcards. + /// + public string TopicFilter { get; } } \ No newline at end of file diff --git a/Source/MQTTnet.Server/Events/InterceptingClientApplicationMessageEnqueueEventArgs.cs b/Source/MQTTnet.Server/Events/InterceptingClientApplicationMessageEnqueueEventArgs.cs index eb8e45fe..71447eca 100644 --- a/Source/MQTTnet.Server/Events/InterceptingClientApplicationMessageEnqueueEventArgs.cs +++ b/Source/MQTTnet.Server/Events/InterceptingClientApplicationMessageEnqueueEventArgs.cs @@ -2,34 +2,31 @@ // The .NET Foundation licenses this file to you under the MIT license. // See the LICENSE file in the project root for more information. -using System; +namespace MQTTnet.Server; -namespace MQTTnet.Server +public sealed class InterceptingClientApplicationMessageEnqueueEventArgs : EventArgs { - public sealed class InterceptingClientApplicationMessageEnqueueEventArgs : EventArgs + public InterceptingClientApplicationMessageEnqueueEventArgs(string senderClientId, string receiverClientId, MqttApplicationMessage applicationMessage) { - public InterceptingClientApplicationMessageEnqueueEventArgs(string senderClientId, string receiverClientId, MqttApplicationMessage applicationMessage) - { - SenderClientId = senderClientId ?? throw new ArgumentNullException(nameof(senderClientId)); - ReceiverClientId = receiverClientId ?? throw new ArgumentNullException(nameof(receiverClientId)); - ApplicationMessage = applicationMessage ?? throw new ArgumentNullException(nameof(applicationMessage)); - } - - /// - /// Gets or sets whether the enqueue of the application message should be performed or not. - /// If set to _False_ the client will not receive the application message. - /// - public bool AcceptEnqueue { get; set; } = true; - - public MqttApplicationMessage ApplicationMessage { get; } - - /// - /// Indicates if the connection with the sender should be closed. - /// - public bool CloseSenderConnection { get; set; } - - public string ReceiverClientId { get; } - - public string SenderClientId { get; } + SenderClientId = senderClientId ?? throw new ArgumentNullException(nameof(senderClientId)); + ReceiverClientId = receiverClientId ?? throw new ArgumentNullException(nameof(receiverClientId)); + ApplicationMessage = applicationMessage ?? throw new ArgumentNullException(nameof(applicationMessage)); } + + /// + /// Gets or sets whether the enqueue of the application message should be performed or not. + /// If set to _False_ the client will not receive the application message. + /// + public bool AcceptEnqueue { get; set; } = true; + + public MqttApplicationMessage ApplicationMessage { get; } + + /// + /// Indicates if the connection with the sender should be closed. + /// + public bool CloseSenderConnection { get; set; } + + public string ReceiverClientId { get; } + + public string SenderClientId { get; } } \ No newline at end of file diff --git a/Source/MQTTnet.Server/Events/InterceptingPacketEventArgs.cs b/Source/MQTTnet.Server/Events/InterceptingPacketEventArgs.cs index 6067a17e..d266d1a2 100644 --- a/Source/MQTTnet.Server/Events/InterceptingPacketEventArgs.cs +++ b/Source/MQTTnet.Server/Events/InterceptingPacketEventArgs.cs @@ -2,63 +2,60 @@ // The .NET Foundation licenses this file to you under the MIT license. // See the LICENSE file in the project root for more information. -using System; using System.Collections; using System.Net; -using System.Threading; using MQTTnet.Packets; -namespace MQTTnet.Server +namespace MQTTnet.Server; + +public sealed class InterceptingPacketEventArgs : EventArgs { - public sealed class InterceptingPacketEventArgs : EventArgs + public InterceptingPacketEventArgs(CancellationToken cancellationToken, string clientId, string userName, EndPoint remoteEndPoint, MqttPacket packet, IDictionary sessionItems) { - public InterceptingPacketEventArgs(CancellationToken cancellationToken, string clientId, string userName, EndPoint remoteEndPoint, MqttPacket packet, IDictionary sessionItems) - { - CancellationToken = cancellationToken; - ClientId = clientId ?? throw new ArgumentNullException(nameof(clientId)); - UserName = userName; - RemoteEndPoint = remoteEndPoint; - Packet = packet ?? throw new ArgumentNullException(nameof(packet)); - SessionItems = sessionItems; - } - - /// - /// Gets the cancellation token from the connection managing thread. - /// Use this in further event processing. - /// - public CancellationToken CancellationToken { get; } - - /// - /// Gets the client ID which has sent the packet or will receive the packet. - /// - public string ClientId { get; } - - /// - /// Gets the user name of the client. - /// - public string UserName { get; } - - /// - /// Gets the endpoint of the sending or receiving client. - /// - public EndPoint RemoteEndPoint { get; } - - [Obsolete("Use RemoteEndPoint instead.")] - public string Endpoint => RemoteEndPoint?.ToString(); - - /// - /// Gets or sets the MQTT packet which was received or will be sent. - /// - public MqttPacket Packet { get; set; } - - /// - /// Gets or sets whether the packet should be processed or not. - /// - public bool ProcessPacket { get; set; } = true; - - /// - /// Gets or sets a key/value collection that can be used to share data within the scope of this session. - /// - public IDictionary SessionItems { get; } + CancellationToken = cancellationToken; + ClientId = clientId ?? throw new ArgumentNullException(nameof(clientId)); + UserName = userName; + RemoteEndPoint = remoteEndPoint; + Packet = packet ?? throw new ArgumentNullException(nameof(packet)); + SessionItems = sessionItems; } + + /// + /// Gets the cancellation token from the connection managing thread. + /// Use this in further event processing. + /// + public CancellationToken CancellationToken { get; } + + /// + /// Gets the client ID which has sent the packet or will receive the packet. + /// + public string ClientId { get; } + + /// + /// Gets the user name of the client. + /// + public string UserName { get; } + + /// + /// Gets the endpoint of the sending or receiving client. + /// + public EndPoint RemoteEndPoint { get; } + + [Obsolete("Use RemoteEndPoint instead.")] + public string Endpoint => RemoteEndPoint?.ToString(); + + /// + /// Gets or sets the MQTT packet which was received or will be sent. + /// + public MqttPacket Packet { get; set; } + + /// + /// Gets or sets whether the packet should be processed or not. + /// + public bool ProcessPacket { get; set; } = true; + + /// + /// Gets or sets a key/value collection that can be used to share data within the scope of this session. + /// + public IDictionary SessionItems { get; } } \ No newline at end of file diff --git a/Source/MQTTnet.Server/Events/InterceptingPublishEventArgs.cs b/Source/MQTTnet.Server/Events/InterceptingPublishEventArgs.cs index b2096104..9b93112a 100644 --- a/Source/MQTTnet.Server/Events/InterceptingPublishEventArgs.cs +++ b/Source/MQTTnet.Server/Events/InterceptingPublishEventArgs.cs @@ -2,56 +2,53 @@ // The .NET Foundation licenses this file to you under the MIT license. // See the LICENSE file in the project root for more information. -using System; using System.Collections; -using System.Threading; -namespace MQTTnet.Server +namespace MQTTnet.Server; + +public sealed class InterceptingPublishEventArgs : EventArgs { - public sealed class InterceptingPublishEventArgs : EventArgs + public InterceptingPublishEventArgs(MqttApplicationMessage applicationMessage, CancellationToken cancellationToken, string clientId, string userName, IDictionary sessionItems) { - public InterceptingPublishEventArgs(MqttApplicationMessage applicationMessage, CancellationToken cancellationToken, string clientId, string userName, IDictionary sessionItems) - { - ApplicationMessage = applicationMessage ?? throw new ArgumentNullException(nameof(applicationMessage)); - CancellationToken = cancellationToken; - ClientId = clientId ?? throw new ArgumentNullException(nameof(clientId)); - UserName = userName; - SessionItems = sessionItems ?? throw new ArgumentNullException(nameof(sessionItems)); - } - - public MqttApplicationMessage ApplicationMessage { get; set; } - - /// - /// Gets the cancellation token which can indicate that the client connection gets down. - /// - public CancellationToken CancellationToken { get; } - - /// - /// Gets the client identifier. - /// Hint: This identifier needs to be unique over all used clients / devices on the broker to avoid connection issues. - /// - public string ClientId { get; } - - /// - /// Gets the user name of the client. - /// - public string UserName { get; } - - public bool CloseConnection { get; set; } - - /// - /// Gets or sets whether the publish should be processed internally. - /// - public bool ProcessPublish { get; set; } = true; - - /// - /// Gets the response which will be sent to the client via the PUBACK etc. packets. - /// - public PublishResponse Response { get; } = new PublishResponse(); - - /// - /// Gets or sets a key/value collection that can be used to share data within the scope of this session. - /// - public IDictionary SessionItems { get; } + ApplicationMessage = applicationMessage ?? throw new ArgumentNullException(nameof(applicationMessage)); + CancellationToken = cancellationToken; + ClientId = clientId ?? throw new ArgumentNullException(nameof(clientId)); + UserName = userName; + SessionItems = sessionItems ?? throw new ArgumentNullException(nameof(sessionItems)); } + + public MqttApplicationMessage ApplicationMessage { get; set; } + + /// + /// Gets the cancellation token which can indicate that the client connection gets down. + /// + public CancellationToken CancellationToken { get; } + + /// + /// Gets the client identifier. + /// Hint: This identifier needs to be unique over all used clients / devices on the broker to avoid connection issues. + /// + public string ClientId { get; } + + /// + /// Gets the user name of the client. + /// + public string UserName { get; } + + public bool CloseConnection { get; set; } + + /// + /// Gets or sets whether the publish should be processed internally. + /// + public bool ProcessPublish { get; set; } = true; + + /// + /// Gets the response which will be sent to the client via the PUBACK etc. packets. + /// + public PublishResponse Response { get; } = new PublishResponse(); + + /// + /// Gets or sets a key/value collection that can be used to share data within the scope of this session. + /// + public IDictionary SessionItems { get; } } \ No newline at end of file diff --git a/Source/MQTTnet.Server/Events/InterceptingSubscriptionEventArgs.cs b/Source/MQTTnet.Server/Events/InterceptingSubscriptionEventArgs.cs index 7f62d68e..69c532c9 100644 --- a/Source/MQTTnet.Server/Events/InterceptingSubscriptionEventArgs.cs +++ b/Source/MQTTnet.Server/Events/InterceptingSubscriptionEventArgs.cs @@ -2,90 +2,86 @@ // The .NET Foundation licenses this file to you under the MIT license. // See the LICENSE file in the project root for more information. -using System; using System.Collections; -using System.Collections.Generic; -using System.Threading; using MQTTnet.Packets; -namespace MQTTnet.Server +namespace MQTTnet.Server; + +public sealed class InterceptingSubscriptionEventArgs : EventArgs { - public sealed class InterceptingSubscriptionEventArgs : EventArgs + public InterceptingSubscriptionEventArgs( + CancellationToken cancellationToken, + string clientId, + string userName, + MqttSessionStatus session, + MqttTopicFilter topicFilter, + List userProperties) { - public InterceptingSubscriptionEventArgs( - CancellationToken cancellationToken, - string clientId, - string userName, - MqttSessionStatus session, - MqttTopicFilter topicFilter, - List userProperties) - { - CancellationToken = cancellationToken; - ClientId = clientId; - UserName = userName; - Session = session; - TopicFilter = topicFilter; - UserProperties = userProperties; - } - - /// - /// Gets the cancellation token which can indicate that the client connection gets down. - /// - public CancellationToken CancellationToken { get; } - - /// - /// Gets the client identifier. - /// Hint: This identifier needs to be unique over all used clients / devices on the broker to avoid connection issues. - /// - public string ClientId { get; } - - /// - /// Gets the user name of the client. - /// - public string UserName { get; } - - /// - /// Gets or sets whether the broker should close the client connection. - /// - public bool CloseConnection { get; set; } - - /// - /// Gets or sets whether the broker should create an internal subscription for the client. - /// The broker can also avoid this and return "success" to the client. - /// This feature allows using the MQTT Broker as the Frontend and another system as the backend. - /// - public bool ProcessSubscription { get; set; } = true; - - /// - /// Gets or sets the reason string which will be sent to the client in the SUBACK packet. - /// - public string ReasonString { get; set; } - - /// - /// Gets the response which will be sent to the client via the SUBACK packet. - /// - public SubscribeResponse Response { get; } = new SubscribeResponse(); - - /// - /// Gets the current client session. - /// - public MqttSessionStatus Session { get; } - - /// - /// Gets or sets a key/value collection that can be used to share data within the scope of this session. - /// - public IDictionary SessionItems => Session.Items; - - /// - /// Gets or sets the topic filter. - /// The topic filter can contain topics and wildcards. - /// - public MqttTopicFilter TopicFilter { get; set; } - - /// - /// Gets or sets the user properties. - /// MQTT 5.0.0+ feature. - /// - public List UserProperties { get; set; } + CancellationToken = cancellationToken; + ClientId = clientId; + UserName = userName; + Session = session; + TopicFilter = topicFilter; + UserProperties = userProperties; } + + /// + /// Gets the cancellation token which can indicate that the client connection gets down. + /// + public CancellationToken CancellationToken { get; } + + /// + /// Gets the client identifier. + /// Hint: This identifier needs to be unique over all used clients / devices on the broker to avoid connection issues. + /// + public string ClientId { get; } + + /// + /// Gets the user name of the client. + /// + public string UserName { get; } + + /// + /// Gets or sets whether the broker should close the client connection. + /// + public bool CloseConnection { get; set; } + + /// + /// Gets or sets whether the broker should create an internal subscription for the client. + /// The broker can also avoid this and return "success" to the client. + /// This feature allows using the MQTT Broker as the Frontend and another system as the backend. + /// + public bool ProcessSubscription { get; set; } = true; + + /// + /// Gets or sets the reason string which will be sent to the client in the SUBACK packet. + /// + public string ReasonString { get; set; } + + /// + /// Gets the response which will be sent to the client via the SUBACK packet. + /// + public SubscribeResponse Response { get; } = new SubscribeResponse(); + + /// + /// Gets the current client session. + /// + public MqttSessionStatus Session { get; } + + /// + /// Gets or sets a key/value collection that can be used to share data within the scope of this session. + /// + public IDictionary SessionItems => Session.Items; + + /// + /// Gets or sets the topic filter. + /// The topic filter can contain topics and wildcards. + /// + public MqttTopicFilter TopicFilter { get; set; } + + /// + /// Gets or sets the user properties. + /// MQTT 5.0.0+ feature. + /// + public List UserProperties { get; set; } } \ No newline at end of file diff --git a/Source/MQTTnet.Server/Events/InterceptingUnsubscriptionEventArgs.cs b/Source/MQTTnet.Server/Events/InterceptingUnsubscriptionEventArgs.cs index 5838aadc..053fdbb4 100644 --- a/Source/MQTTnet.Server/Events/InterceptingUnsubscriptionEventArgs.cs +++ b/Source/MQTTnet.Server/Events/InterceptingUnsubscriptionEventArgs.cs @@ -2,77 +2,73 @@ // The .NET Foundation licenses this file to you under the MIT license. // See the LICENSE file in the project root for more information. -using System; using System.Collections; -using System.Collections.Generic; -using System.Threading; using MQTTnet.Packets; -namespace MQTTnet.Server +namespace MQTTnet.Server; + +public sealed class InterceptingUnsubscriptionEventArgs : EventArgs { - public sealed class InterceptingUnsubscriptionEventArgs : EventArgs + public InterceptingUnsubscriptionEventArgs(CancellationToken cancellationToken, string clientId, string userName, IDictionary sessionItems, string topic, List userProperties) { - public InterceptingUnsubscriptionEventArgs(CancellationToken cancellationToken, string clientId, string userName, IDictionary sessionItems, string topic, List userProperties) - { - CancellationToken = cancellationToken; - ClientId = clientId; - UserName = userName; - SessionItems = sessionItems; - Topic = topic; - UserProperties = userProperties; - } - - /// - /// Gets the cancellation token which can indicate that the client connection gets down. - /// - public CancellationToken CancellationToken { get; } - - /// - /// Gets the client identifier. - /// Hint: This identifier needs to be unique over all used clients / devices on the broker to avoid connection issues. - /// - public string ClientId { get; } - - /// - /// Gets the user name of the client. - /// - public string UserName { get; } - - /// - /// Gets or sets whether the broker should close the client connection. - /// - public bool CloseConnection { get; set; } - - /// - /// Gets or sets whether the broker should remove an internal subscription for the client. - /// The broker can also avoid this and return "success" to the client. - /// This feature allows using the MQTT Broker as the Frontend and another system as the backend. - /// - public bool ProcessUnsubscription { get; set; } = true; - - /// - /// Gets the response which will be sent to the client via the UNSUBACK pocket. - /// - public UnsubscribeResponse Response { get; } = new UnsubscribeResponse(); - - /// - /// Gets or sets a key/value collection that can be used to share data within the scope of this session. - /// - public IDictionary SessionItems { get; } - - /// - /// Gets or sets the MQTT topic. - /// In MQTT, the word topic refers to an UTF-8 string that the broker uses to filter messages for each connected - /// client. - /// The topic consists of one or more topic levels. Each topic level is separated by a forward slash (topic level - /// separator). - /// - public string Topic { get; } - - /// - /// Gets or sets the user properties. - /// MQTT 5.0.0+ feature. - /// - public List UserProperties { get; set; } + CancellationToken = cancellationToken; + ClientId = clientId; + UserName = userName; + SessionItems = sessionItems; + Topic = topic; + UserProperties = userProperties; } + + /// + /// Gets the cancellation token which can indicate that the client connection gets down. + /// + public CancellationToken CancellationToken { get; } + + /// + /// Gets the client identifier. + /// Hint: This identifier needs to be unique over all used clients / devices on the broker to avoid connection issues. + /// + public string ClientId { get; } + + /// + /// Gets the user name of the client. + /// + public string UserName { get; } + + /// + /// Gets or sets whether the broker should close the client connection. + /// + public bool CloseConnection { get; set; } + + /// + /// Gets or sets whether the broker should remove an internal subscription for the client. + /// The broker can also avoid this and return "success" to the client. + /// This feature allows using the MQTT Broker as the Frontend and another system as the backend. + /// + public bool ProcessUnsubscription { get; set; } = true; + + /// + /// Gets the response which will be sent to the client via the UNSUBACK pocket. + /// + public UnsubscribeResponse Response { get; } = new UnsubscribeResponse(); + + /// + /// Gets or sets a key/value collection that can be used to share data within the scope of this session. + /// + public IDictionary SessionItems { get; } + + /// + /// Gets or sets the MQTT topic. + /// In MQTT, the word topic refers to an UTF-8 string that the broker uses to filter messages for each connected + /// client. + /// The topic consists of one or more topic levels. Each topic level is separated by a forward slash (topic level + /// separator). + /// + public string Topic { get; } + + /// + /// Gets or sets the user properties. + /// MQTT 5.0.0+ feature. + /// + public List UserProperties { get; set; } } \ No newline at end of file diff --git a/Source/MQTTnet.Server/Events/LoadingRetainedMessagesEventArgs.cs b/Source/MQTTnet.Server/Events/LoadingRetainedMessagesEventArgs.cs index 12aa0469..99b1ea51 100644 --- a/Source/MQTTnet.Server/Events/LoadingRetainedMessagesEventArgs.cs +++ b/Source/MQTTnet.Server/Events/LoadingRetainedMessagesEventArgs.cs @@ -2,13 +2,9 @@ // The .NET Foundation licenses this file to you under the MIT license. // See the LICENSE file in the project root for more information. -using System; -using System.Collections.Generic; +namespace MQTTnet.Server; -namespace MQTTnet.Server +public sealed class LoadingRetainedMessagesEventArgs : EventArgs { - public sealed class LoadingRetainedMessagesEventArgs : EventArgs - { - public List LoadedRetainedMessages { get; set; } = new List(); - } + public List LoadedRetainedMessages { get; set; } = new List(); } \ No newline at end of file diff --git a/Source/MQTTnet.Server/Events/PreparingSessionEventArgs.cs b/Source/MQTTnet.Server/Events/PreparingSessionEventArgs.cs index d75ed503..e2ee068d 100644 --- a/Source/MQTTnet.Server/Events/PreparingSessionEventArgs.cs +++ b/Source/MQTTnet.Server/Events/PreparingSessionEventArgs.cs @@ -2,50 +2,47 @@ // The .NET Foundation licenses this file to you under the MIT license. // See the LICENSE file in the project root for more information. -using System; -using System.Collections.Generic; using MQTTnet.Packets; using MQTTnet.Server.Internal; -namespace MQTTnet.Server +namespace MQTTnet.Server; + +public sealed class PreparingSessionEventArgs : EventArgs { - public sealed class PreparingSessionEventArgs : EventArgs - { - public string Id { get; set; } + public string Id { get; set; } - // TODO: Allow adding of packets to the queue etc. + // TODO: Allow adding of packets to the queue etc. - /* - * The Session State in the Server consists of: + /* + * The Session State in the Server consists of: · The existence of a Session, even if the rest of the Session State is empty. · The Clients subscriptions, including any Subscription Identifiers. · QoS 1 and QoS 2 messages which have been sent to the Client, but have not been completely acknowledged. · QoS 1 and QoS 2 messages pending transmission to the Client and OPTIONALLY QoS 0 messages pending transmission to the Client. · QoS 2 messages which have been received from the Client, but have not been completely acknowledged.The Will Message and the Will Delay Interval · If the Session is currently not connected, the time at which the Session will end and Session State will be discarded. - */ + */ - public bool IsExistingSession { get; set; } + public bool IsExistingSession { get; set; } - public IDictionary Items { get; set; } + public IDictionary Items { get; set; } - public List PublishPackets { get; } = new List(); + public List PublishPackets { get; } = new List(); - DateTime? SessionExpiryTimestamp { get; set; } + DateTime? SessionExpiryTimestamp { get; set; } - public List Subscriptions { get; } = new List(); + public List Subscriptions { get; } = new List(); - /// - /// Gets the will delay interval. - /// This is the time between the client disconnect and the time the will message will be sent. - /// - public uint? WillDelayInterval { get; set; } + /// + /// Gets the will delay interval. + /// This is the time between the client disconnect and the time the will message will be sent. + /// + public uint? WillDelayInterval { get; set; } - // - // Gets the last will message. - // In MQTT, you use the last will message feature to notify other clients about an ungracefully disconnected client. - // - // TODO: Use single properties. No entire will message. - //MqttApplicationMessage WillMessage { get; set; } - } + // + // Gets the last will message. + // In MQTT, you use the last will message feature to notify other clients about an ungracefully disconnected client. + // + // TODO: Use single properties. No entire will message. + //MqttApplicationMessage WillMessage { get; set; } } \ No newline at end of file diff --git a/Source/MQTTnet.Server/Events/QueueMessageOverwrittenEventArgs.cs b/Source/MQTTnet.Server/Events/QueueMessageOverwrittenEventArgs.cs index 161e5054..7f28cddd 100644 --- a/Source/MQTTnet.Server/Events/QueueMessageOverwrittenEventArgs.cs +++ b/Source/MQTTnet.Server/Events/QueueMessageOverwrittenEventArgs.cs @@ -2,21 +2,19 @@ // The .NET Foundation licenses this file to you under the MIT license. // See the LICENSE file in the project root for more information. -using System; using MQTTnet.Packets; -namespace MQTTnet.Server +namespace MQTTnet.Server; + +public sealed class QueueMessageOverwrittenEventArgs : EventArgs { - public sealed class QueueMessageOverwrittenEventArgs : EventArgs + public QueueMessageOverwrittenEventArgs(string receiverClientId, MqttPacket packet) { - public QueueMessageOverwrittenEventArgs(string receiverClientId, MqttPacket packet) - { - ReceiverClientId = receiverClientId ?? throw new ArgumentNullException(nameof(receiverClientId)); - Packet = packet ?? throw new ArgumentNullException(nameof(packet)); - } - - public MqttPacket Packet { get; } - - public string ReceiverClientId { get; } + ReceiverClientId = receiverClientId ?? throw new ArgumentNullException(nameof(receiverClientId)); + Packet = packet ?? throw new ArgumentNullException(nameof(packet)); } + + public MqttPacket Packet { get; } + + public string ReceiverClientId { get; } } \ No newline at end of file diff --git a/Source/MQTTnet.Server/Events/RetainedMessageChangedEventArgs.cs b/Source/MQTTnet.Server/Events/RetainedMessageChangedEventArgs.cs index c7beb807..61c21cd4 100644 --- a/Source/MQTTnet.Server/Events/RetainedMessageChangedEventArgs.cs +++ b/Source/MQTTnet.Server/Events/RetainedMessageChangedEventArgs.cs @@ -2,24 +2,20 @@ // The .NET Foundation licenses this file to you under the MIT license. // See the LICENSE file in the project root for more information. -using System; -using System.Collections.Generic; +namespace MQTTnet.Server; -namespace MQTTnet.Server +public sealed class RetainedMessageChangedEventArgs : EventArgs { - public sealed class RetainedMessageChangedEventArgs : EventArgs + public RetainedMessageChangedEventArgs(string clientId, MqttApplicationMessage changedRetainedMessage, List storedRetainedMessages) { - public RetainedMessageChangedEventArgs(string clientId, MqttApplicationMessage changedRetainedMessage, List storedRetainedMessages) - { - ClientId = clientId ?? throw new ArgumentNullException(nameof(clientId)); - ChangedRetainedMessage = changedRetainedMessage ?? throw new ArgumentNullException(nameof(changedRetainedMessage)); - StoredRetainedMessages = storedRetainedMessages ?? throw new ArgumentNullException(nameof(storedRetainedMessages)); - } - - public MqttApplicationMessage ChangedRetainedMessage { get; } - - public string ClientId { get; } - - public List StoredRetainedMessages { get; } + ClientId = clientId ?? throw new ArgumentNullException(nameof(clientId)); + ChangedRetainedMessage = changedRetainedMessage ?? throw new ArgumentNullException(nameof(changedRetainedMessage)); + StoredRetainedMessages = storedRetainedMessages ?? throw new ArgumentNullException(nameof(storedRetainedMessages)); } + + public MqttApplicationMessage ChangedRetainedMessage { get; } + + public string ClientId { get; } + + public List StoredRetainedMessages { get; } } \ No newline at end of file diff --git a/Source/MQTTnet.Server/Events/SessionDeletedEventArgs.cs b/Source/MQTTnet.Server/Events/SessionDeletedEventArgs.cs index dd903723..4819de91 100644 --- a/Source/MQTTnet.Server/Events/SessionDeletedEventArgs.cs +++ b/Source/MQTTnet.Server/Events/SessionDeletedEventArgs.cs @@ -2,33 +2,31 @@ // The .NET Foundation licenses this file to you under the MIT license. // See the LICENSE file in the project root for more information. -using System; using System.Collections; -namespace MQTTnet.Server +namespace MQTTnet.Server; + +public sealed class SessionDeletedEventArgs : EventArgs { - public sealed class SessionDeletedEventArgs : EventArgs + public SessionDeletedEventArgs(string id, string userName, IDictionary sessionItems) { - public SessionDeletedEventArgs(string id, string userName, IDictionary sessionItems) - { - Id = id ?? throw new ArgumentNullException(nameof(id)); - UserName = userName; - SessionItems = sessionItems ?? throw new ArgumentNullException(nameof(sessionItems)); - } - - /// - /// Gets the ID of the session. - /// - public string Id { get; } - - /// - /// Gets the user name of the session. - /// - public string UserName { get; } - - /// - /// Gets or sets a key/value collection that can be used to share data within the scope of this session. - /// - public IDictionary SessionItems { get; } + Id = id ?? throw new ArgumentNullException(nameof(id)); + UserName = userName; + SessionItems = sessionItems ?? throw new ArgumentNullException(nameof(sessionItems)); } + + /// + /// Gets the ID of the session. + /// + public string Id { get; } + + /// + /// Gets the user name of the session. + /// + public string UserName { get; } + + /// + /// Gets or sets a key/value collection that can be used to share data within the scope of this session. + /// + public IDictionary SessionItems { get; } } \ No newline at end of file diff --git a/Source/MQTTnet.Server/Internal/Adapter/MqttTcpServerAdapter.cs b/Source/MQTTnet.Server/Internal/Adapter/MqttTcpServerAdapter.cs index 7d2e716f..9c955a03 100644 --- a/Source/MQTTnet.Server/Internal/Adapter/MqttTcpServerAdapter.cs +++ b/Source/MQTTnet.Server/Internal/Adapter/MqttTcpServerAdapter.cs @@ -8,118 +8,117 @@ using MQTTnet.Adapter; using MQTTnet.Diagnostics.Logger; using MQTTnet.Internal; -namespace MQTTnet.Server.Internal.Adapter +namespace MQTTnet.Server.Internal.Adapter; + +public sealed class MqttTcpServerAdapter : IMqttServerAdapter { - public sealed class MqttTcpServerAdapter : IMqttServerAdapter + readonly List _listeners = new List(); + + CancellationTokenSource _cancellationTokenSource; + + MqttServerOptions _serverOptions; + + public Func ClientHandler { get; set; } + + public bool TreatSocketOpeningErrorAsWarning { get; set; } + + public void Dispose() { - readonly List _listeners = new List(); + Cleanup(); + } - CancellationTokenSource _cancellationTokenSource; - - MqttServerOptions _serverOptions; - - public Func ClientHandler { get; set; } - - public bool TreatSocketOpeningErrorAsWarning { get; set; } - - public void Dispose() + public Task StartAsync(MqttServerOptions options, IMqttNetLogger logger) + { + if (_cancellationTokenSource != null) { - Cleanup(); + throw new InvalidOperationException("Server is already started."); } - public Task StartAsync(MqttServerOptions options, IMqttNetLogger logger) + _serverOptions = options; + + _cancellationTokenSource = new CancellationTokenSource(); + + if (options.DefaultEndpointOptions.IsEnabled) { - if (_cancellationTokenSource != null) + RegisterListeners(options.DefaultEndpointOptions, logger, _cancellationTokenSource.Token); + } + + if (options.TlsEndpointOptions?.IsEnabled == true) + { + if (options.TlsEndpointOptions.CertificateProvider == null) { - throw new InvalidOperationException("Server is already started."); + throw new ArgumentException("TLS certificate is not set."); } - _serverOptions = options; + RegisterListeners(options.TlsEndpointOptions, logger, _cancellationTokenSource.Token); + } - _cancellationTokenSource = new CancellationTokenSource(); + return CompletedTask.Instance; + } - if (options.DefaultEndpointOptions.IsEnabled) + public Task StopAsync() + { + Cleanup(); + return CompletedTask.Instance; + } + + void Cleanup() + { + try + { + _cancellationTokenSource?.Cancel(false); + } + finally + { + _cancellationTokenSource?.Dispose(); + _cancellationTokenSource = null; + + foreach (var listener in _listeners) { - RegisterListeners(options.DefaultEndpointOptions, logger, _cancellationTokenSource.Token); + listener.Dispose(); } - if (options.TlsEndpointOptions?.IsEnabled == true) - { - if (options.TlsEndpointOptions.CertificateProvider == null) - { - throw new ArgumentException("TLS certificate is not set."); - } - - RegisterListeners(options.TlsEndpointOptions, logger, _cancellationTokenSource.Token); - } + _listeners.Clear(); + } + } + Task OnClientAcceptedAsync(IMqttChannelAdapter channelAdapter) + { + var clientHandler = ClientHandler; + if (clientHandler == null) + { return CompletedTask.Instance; } - public Task StopAsync() + return clientHandler(channelAdapter); + } + + void RegisterListeners(MqttServerTcpEndpointBaseOptions tcpEndpointOptions, IMqttNetLogger logger, CancellationToken cancellationToken) + { + if (!tcpEndpointOptions.BoundInterNetworkAddress.Equals(IPAddress.None)) { - Cleanup(); - return CompletedTask.Instance; - } - - void Cleanup() - { - try + var listenerV4 = new MqttTcpServerListener(AddressFamily.InterNetwork, _serverOptions, tcpEndpointOptions, logger) { - _cancellationTokenSource?.Cancel(false); - } - finally + ClientHandler = OnClientAcceptedAsync + }; + + if (listenerV4.Start(TreatSocketOpeningErrorAsWarning, cancellationToken)) { - _cancellationTokenSource?.Dispose(); - _cancellationTokenSource = null; - - foreach (var listener in _listeners) - { - listener.Dispose(); - } - - _listeners.Clear(); + _listeners.Add(listenerV4); } } - Task OnClientAcceptedAsync(IMqttChannelAdapter channelAdapter) + if (!tcpEndpointOptions.BoundInterNetworkV6Address.Equals(IPAddress.None)) { - var clientHandler = ClientHandler; - if (clientHandler == null) + var listenerV6 = new MqttTcpServerListener(AddressFamily.InterNetworkV6, _serverOptions, tcpEndpointOptions, logger) { - return CompletedTask.Instance; - } + ClientHandler = OnClientAcceptedAsync + }; - return clientHandler(channelAdapter); - } - - void RegisterListeners(MqttServerTcpEndpointBaseOptions tcpEndpointOptions, IMqttNetLogger logger, CancellationToken cancellationToken) - { - if (!tcpEndpointOptions.BoundInterNetworkAddress.Equals(IPAddress.None)) + if (listenerV6.Start(TreatSocketOpeningErrorAsWarning, cancellationToken)) { - var listenerV4 = new MqttTcpServerListener(AddressFamily.InterNetwork, _serverOptions, tcpEndpointOptions, logger) - { - ClientHandler = OnClientAcceptedAsync - }; - - if (listenerV4.Start(TreatSocketOpeningErrorAsWarning, cancellationToken)) - { - _listeners.Add(listenerV4); - } - } - - if (!tcpEndpointOptions.BoundInterNetworkV6Address.Equals(IPAddress.None)) - { - var listenerV6 = new MqttTcpServerListener(AddressFamily.InterNetworkV6, _serverOptions, tcpEndpointOptions, logger) - { - ClientHandler = OnClientAcceptedAsync - }; - - if (listenerV6.Start(TreatSocketOpeningErrorAsWarning, cancellationToken)) - { - _listeners.Add(listenerV6); - } + _listeners.Add(listenerV6); } } } -} +} \ No newline at end of file diff --git a/Source/MQTTnet.Server/Internal/Adapter/MqttTcpServerListener.cs b/Source/MQTTnet.Server/Internal/Adapter/MqttTcpServerListener.cs index 7a0453ce..e539dc60 100644 --- a/Source/MQTTnet.Server/Internal/Adapter/MqttTcpServerListener.cs +++ b/Source/MQTTnet.Server/Internal/Adapter/MqttTcpServerListener.cs @@ -12,248 +12,247 @@ using MQTTnet.Formatter; using MQTTnet.Implementations; using MQTTnet.Internal; -namespace MQTTnet.Server.Internal.Adapter +namespace MQTTnet.Server.Internal.Adapter; + +public sealed class MqttTcpServerListener : IDisposable { - public sealed class MqttTcpServerListener : IDisposable + readonly MqttNetSourceLogger _logger; + readonly IMqttNetLogger _rootLogger; + readonly AddressFamily _addressFamily; + readonly MqttServerOptions _serverOptions; + readonly MqttServerTcpEndpointBaseOptions _options; + readonly MqttServerTlsTcpEndpointOptions _tlsOptions; + + CrossPlatformSocket _socket; + IPEndPoint _localEndPoint; + + public MqttTcpServerListener( + AddressFamily addressFamily, + MqttServerOptions serverOptions, + MqttServerTcpEndpointBaseOptions tcpEndpointOptions, + IMqttNetLogger logger) { - readonly MqttNetSourceLogger _logger; - readonly IMqttNetLogger _rootLogger; - readonly AddressFamily _addressFamily; - readonly MqttServerOptions _serverOptions; - readonly MqttServerTcpEndpointBaseOptions _options; - readonly MqttServerTlsTcpEndpointOptions _tlsOptions; + _addressFamily = addressFamily; + _serverOptions = serverOptions ?? throw new ArgumentNullException(nameof(serverOptions)); + _options = tcpEndpointOptions ?? throw new ArgumentNullException(nameof(tcpEndpointOptions)); + _rootLogger = logger; + _logger = logger.WithSource(nameof(MqttTcpServerListener)); - CrossPlatformSocket _socket; - IPEndPoint _localEndPoint; - - public MqttTcpServerListener( - AddressFamily addressFamily, - MqttServerOptions serverOptions, - MqttServerTcpEndpointBaseOptions tcpEndpointOptions, - IMqttNetLogger logger) + if (_options is MqttServerTlsTcpEndpointOptions tlsOptions) { - _addressFamily = addressFamily; - _serverOptions = serverOptions ?? throw new ArgumentNullException(nameof(serverOptions)); - _options = tcpEndpointOptions ?? throw new ArgumentNullException(nameof(tcpEndpointOptions)); - _rootLogger = logger; - _logger = logger.WithSource(nameof(MqttTcpServerListener)); - - if (_options is MqttServerTlsTcpEndpointOptions tlsOptions) - { - _tlsOptions = tlsOptions; - } + _tlsOptions = tlsOptions; } + } - public Func ClientHandler { get; set; } + public Func ClientHandler { get; set; } - public bool Start(bool treatErrorsAsWarning, CancellationToken cancellationToken) + public bool Start(bool treatErrorsAsWarning, CancellationToken cancellationToken) + { + try + { + var boundIp = _options.BoundInterNetworkAddress; + if (_addressFamily == AddressFamily.InterNetworkV6) + { + boundIp = _options.BoundInterNetworkV6Address; + } + + _localEndPoint = new IPEndPoint(boundIp, _options.Port); + + _logger.Info("Starting TCP listener (Endpoint={0}, TLS={1})", _localEndPoint, _tlsOptions?.CertificateProvider != null); + + _socket = new CrossPlatformSocket(_addressFamily, ProtocolType.Tcp); + + // Usage of socket options is described here: https://docs.microsoft.com/en-us/dotnet/api/system.net.sockets.socket.setsocketoption?view=netcore-2.2 + if (_options.ReuseAddress) + { + _socket.ReuseAddress = true; + } + + if (_options.NoDelay) + { + _socket.NoDelay = true; + } + + if (_options.LingerState != null) + { + _socket.LingerState = _options.LingerState; + } + + if (_options.KeepAlive.HasValue) + { + _socket.KeepAlive = _options.KeepAlive.Value; + } + + if (_options.TcpKeepAliveInterval.HasValue) + { + _socket.TcpKeepAliveInterval = _options.TcpKeepAliveInterval.Value; + } + + if (_options.TcpKeepAliveRetryCount.HasValue) + { + _socket.TcpKeepAliveInterval = _options.TcpKeepAliveRetryCount.Value; + } + + if (_options.TcpKeepAliveTime.HasValue) + { + _socket.TcpKeepAliveTime = _options.TcpKeepAliveTime.Value; + } + + _socket.Bind(_localEndPoint); + + // Get the local endpoint back from the socket. The port may have changed. + // This can happen when port 0 is used. Then the OS will choose the next free port. + _localEndPoint = (IPEndPoint)_socket.LocalEndPoint; + _options.Port = _localEndPoint.Port; + + _socket.Listen(_options.ConnectionBacklog); + + _logger.Verbose("TCP listener started (Endpoint={0})", _localEndPoint); + + Task.Run(() => AcceptClientConnectionsAsync(cancellationToken), cancellationToken).RunInBackground(_logger); + + return true; + } + catch (Exception exception) + { + if (!treatErrorsAsWarning) + { + throw; + } + + _logger.Warning(exception, "Error while starting TCP listener (Endpoint={0})", _localEndPoint); + return false; + } + } + + public void Dispose() + { + _socket?.Dispose(); + } + + async Task AcceptClientConnectionsAsync(CancellationToken cancellationToken) + { + while (!cancellationToken.IsCancellationRequested) { try { - var boundIp = _options.BoundInterNetworkAddress; - if (_addressFamily == AddressFamily.InterNetworkV6) + var clientSocket = await _socket.AcceptAsync(cancellationToken).ConfigureAwait(false); + if (clientSocket == null) { - boundIp = _options.BoundInterNetworkV6Address; + continue; } - _localEndPoint = new IPEndPoint(boundIp, _options.Port); - - _logger.Info("Starting TCP listener (Endpoint={0}, TLS={1})", _localEndPoint, _tlsOptions?.CertificateProvider != null); - - _socket = new CrossPlatformSocket(_addressFamily, ProtocolType.Tcp); - - // Usage of socket options is described here: https://docs.microsoft.com/en-us/dotnet/api/system.net.sockets.socket.setsocketoption?view=netcore-2.2 - if (_options.ReuseAddress) - { - _socket.ReuseAddress = true; - } - - if (_options.NoDelay) - { - _socket.NoDelay = true; - } - - if (_options.LingerState != null) - { - _socket.LingerState = _options.LingerState; - } - - if (_options.KeepAlive.HasValue) - { - _socket.KeepAlive = _options.KeepAlive.Value; - } - - if (_options.TcpKeepAliveInterval.HasValue) - { - _socket.TcpKeepAliveInterval = _options.TcpKeepAliveInterval.Value; - } - - if (_options.TcpKeepAliveRetryCount.HasValue) - { - _socket.TcpKeepAliveInterval = _options.TcpKeepAliveRetryCount.Value; - } - - if (_options.TcpKeepAliveTime.HasValue) - { - _socket.TcpKeepAliveTime = _options.TcpKeepAliveTime.Value; - } - - _socket.Bind(_localEndPoint); - - // Get the local endpoint back from the socket. The port may have changed. - // This can happen when port 0 is used. Then the OS will choose the next free port. - _localEndPoint = (IPEndPoint)_socket.LocalEndPoint; - _options.Port = _localEndPoint.Port; - - _socket.Listen(_options.ConnectionBacklog); - - _logger.Verbose("TCP listener started (Endpoint={0})", _localEndPoint); - - Task.Run(() => AcceptClientConnectionsAsync(cancellationToken), cancellationToken).RunInBackground(_logger); - - return true; + _ = Task.Factory.StartNew(() => TryHandleClientConnectionAsync(clientSocket), cancellationToken, TaskCreationOptions.PreferFairness, TaskScheduler.Default).ConfigureAwait(false); + } + catch (OperationCanceledException) + { } catch (Exception exception) { - if (!treatErrorsAsWarning) + if (exception is SocketException socketException) { - throw; - } - - _logger.Warning(exception, "Error while starting TCP listener (Endpoint={0})", _localEndPoint); - return false; - } - } - - public void Dispose() - { - _socket?.Dispose(); - } - - async Task AcceptClientConnectionsAsync(CancellationToken cancellationToken) - { - while (!cancellationToken.IsCancellationRequested) - { - try - { - var clientSocket = await _socket.AcceptAsync(cancellationToken).ConfigureAwait(false); - if (clientSocket == null) + if (socketException.SocketErrorCode is SocketError.ConnectionAborted or SocketError.OperationAborted) { continue; } + } - _ = Task.Factory.StartNew(() => TryHandleClientConnectionAsync(clientSocket), cancellationToken, TaskCreationOptions.PreferFairness, TaskScheduler.Default).ConfigureAwait(false); - } - catch (OperationCanceledException) - { - } - catch (Exception exception) - { - if (exception is SocketException socketException) - { - if (socketException.SocketErrorCode == SocketError.ConnectionAborted || - socketException.SocketErrorCode == SocketError.OperationAborted) - { - continue; - } - } - - _logger.Error(exception, "Error while accepting TCP connection (Endpoint={0})", _localEndPoint); - await Task.Delay(TimeSpan.FromSeconds(1), cancellationToken).ConfigureAwait(false); - } + _logger.Error(exception, "Error while accepting TCP connection (Endpoint={0})", _localEndPoint); + await Task.Delay(TimeSpan.FromSeconds(1), cancellationToken).ConfigureAwait(false); } } - - async Task TryHandleClientConnectionAsync(CrossPlatformSocket clientSocket) - { - Stream stream = null; - EndPoint remoteEndPoint = null; - - try - { - remoteEndPoint = clientSocket.RemoteEndPoint; - - _logger.Verbose("TCP client '{0}' accepted (Local endpoint={1})", remoteEndPoint, _localEndPoint); - - clientSocket.NoDelay = _options.NoDelay; - stream = clientSocket.GetStream(); - var clientCertificate = _tlsOptions?.CertificateProvider?.GetCertificate(); - - if (clientCertificate != null) - { - if (!clientCertificate.HasPrivateKey) - { - throw new InvalidOperationException("The certificate for TLS encryption must contain the private key."); - } - - var sslStream = new SslStream(stream, false, _tlsOptions.RemoteCertificateValidationCallback); - - await sslStream.AuthenticateAsServerAsync( - new SslServerAuthenticationOptions - { - ServerCertificate = clientCertificate, - ClientCertificateRequired = _tlsOptions.ClientCertificateRequired, - EnabledSslProtocols = _tlsOptions.SslProtocol, - CertificateRevocationCheckMode = _tlsOptions.CheckCertificateRevocation ? X509RevocationMode.Online : X509RevocationMode.NoCheck, - EncryptionPolicy = EncryptionPolicy.RequireEncryption, - CipherSuitesPolicy = _tlsOptions.CipherSuitesPolicy - }).ConfigureAwait(false); - - stream = sslStream; - - clientCertificate = sslStream.RemoteCertificate as X509Certificate2; - - if (clientCertificate == null && sslStream.RemoteCertificate != null) - { - clientCertificate = new X509Certificate2(sslStream.RemoteCertificate.Export(X509ContentType.Cert)); - } - } - - var clientHandler = ClientHandler; - if (clientHandler != null) - { - var tcpChannel = new MqttTcpChannel(stream, remoteEndPoint, clientCertificate); - var bufferWriter = new MqttBufferWriter(_serverOptions.WriterBufferSize, _serverOptions.WriterBufferSizeMax); - var packetFormatterAdapter = new MqttPacketFormatterAdapter(bufferWriter); - - using (var clientAdapter = new MqttChannelAdapter(tcpChannel, packetFormatterAdapter, _rootLogger)) - { - clientAdapter.AllowPacketFragmentation = _options.AllowPacketFragmentation; - await clientHandler(clientAdapter).ConfigureAwait(false); - } - } - } - catch (Exception exception) - { - if (exception is ObjectDisposedException) - { - // It can happen that the listener socket is accessed after the cancellation token is already set and the listener socket is disposed. - return; - } - - if (exception is SocketException socketException && - socketException.SocketErrorCode == SocketError.OperationAborted) - { - return; - } - - _logger.Error(exception, "Error while handling TCP client connection"); - } - finally - { - try - { - // ReSharper disable once MethodHasAsyncOverload - stream?.Dispose(); - clientSocket?.Dispose(); - } - catch (Exception disposeException) - { - _logger.Error(disposeException, "Error while cleaning up client connection"); - } - } - - _logger.Verbose("TCP client '{0}' disconnected (Local endpoint={1})", remoteEndPoint, _localEndPoint); - } } -} + + async Task TryHandleClientConnectionAsync(CrossPlatformSocket clientSocket) + { + Stream stream = null; + EndPoint remoteEndPoint = null; + + try + { + remoteEndPoint = clientSocket.RemoteEndPoint; + + _logger.Verbose("TCP client '{0}' accepted (Local endpoint={1})", remoteEndPoint, _localEndPoint); + + clientSocket.NoDelay = _options.NoDelay; + stream = clientSocket.GetStream(); + var clientCertificate = _tlsOptions?.CertificateProvider?.GetCertificate(); + + if (clientCertificate != null) + { + if (!clientCertificate.HasPrivateKey) + { + throw new InvalidOperationException("The certificate for TLS encryption must contain the private key."); + } + + var sslStream = new SslStream(stream, false, _tlsOptions.RemoteCertificateValidationCallback); + + await sslStream.AuthenticateAsServerAsync( + new SslServerAuthenticationOptions + { + ServerCertificate = clientCertificate, + ClientCertificateRequired = _tlsOptions.ClientCertificateRequired, + EnabledSslProtocols = _tlsOptions.SslProtocol, + CertificateRevocationCheckMode = _tlsOptions.CheckCertificateRevocation ? X509RevocationMode.Online : X509RevocationMode.NoCheck, + EncryptionPolicy = EncryptionPolicy.RequireEncryption, + CipherSuitesPolicy = _tlsOptions.CipherSuitesPolicy + }).ConfigureAwait(false); + + stream = sslStream; + + clientCertificate = sslStream.RemoteCertificate as X509Certificate2; + + if (clientCertificate == null && sslStream.RemoteCertificate != null) + { + clientCertificate = new X509Certificate2(sslStream.RemoteCertificate.Export(X509ContentType.Cert)); + } + } + + var clientHandler = ClientHandler; + if (clientHandler != null) + { + var tcpChannel = new MqttTcpChannel(stream, remoteEndPoint, clientCertificate); + var bufferWriter = new MqttBufferWriter(_serverOptions.WriterBufferSize, _serverOptions.WriterBufferSizeMax); + var packetFormatterAdapter = new MqttPacketFormatterAdapter(bufferWriter); + + using var clientAdapter = new MqttChannelAdapter(tcpChannel, packetFormatterAdapter, _rootLogger); + clientAdapter.AllowPacketFragmentation = _options.AllowPacketFragmentation; + await clientHandler(clientAdapter).ConfigureAwait(false); + } + } + catch (Exception exception) + { + if (exception is ObjectDisposedException) + { + // It can happen that the listener socket is accessed after the cancellation token is already set and the listener socket is disposed. + return; + } + + if (exception is SocketException socketException && + socketException.SocketErrorCode == SocketError.OperationAborted) + { + return; + } + + _logger.Error(exception, "Error while handling TCP client connection"); + } + finally + { + try + { + if (stream != null) + { + await stream.DisposeAsync().ConfigureAwait(false); + } + + clientSocket?.Dispose(); + } + catch (Exception disposeException) + { + _logger.Error(disposeException, "Error while cleaning up client connection"); + } + } + + _logger.Verbose("TCP client '{0}' disconnected (Local endpoint={1})", remoteEndPoint, _localEndPoint); + } +} \ No newline at end of file diff --git a/Source/MQTTnet.Server/Internal/CheckSubscriptionsResult.cs b/Source/MQTTnet.Server/Internal/CheckSubscriptionsResult.cs index 298a6b21..1e1eefd5 100644 --- a/Source/MQTTnet.Server/Internal/CheckSubscriptionsResult.cs +++ b/Source/MQTTnet.Server/Internal/CheckSubscriptionsResult.cs @@ -4,18 +4,17 @@ using MQTTnet.Protocol; -namespace MQTTnet.Server.Internal +namespace MQTTnet.Server.Internal; + +public sealed class CheckSubscriptionsResult { - public sealed class CheckSubscriptionsResult - { - public static CheckSubscriptionsResult NotSubscribed { get; } = new CheckSubscriptionsResult(); + public static CheckSubscriptionsResult NotSubscribed { get; } = new CheckSubscriptionsResult(); - public bool IsSubscribed { get; set; } + public bool IsSubscribed { get; set; } - public bool RetainAsPublished { get; set; } - - public List SubscriptionIdentifiers { get; set; } - - public MqttQualityOfServiceLevel QualityOfServiceLevel { get; set; } - } -} + public bool RetainAsPublished { get; set; } + + public List SubscriptionIdentifiers { get; set; } + + public MqttQualityOfServiceLevel QualityOfServiceLevel { get; set; } +} \ No newline at end of file diff --git a/Source/MQTTnet.Server/Internal/DispatchApplicationMessageResult.cs b/Source/MQTTnet.Server/Internal/DispatchApplicationMessageResult.cs index e7daeecc..190b48bc 100644 --- a/Source/MQTTnet.Server/Internal/DispatchApplicationMessageResult.cs +++ b/Source/MQTTnet.Server/Internal/DispatchApplicationMessageResult.cs @@ -4,24 +4,23 @@ using MQTTnet.Packets; -namespace MQTTnet.Server.Internal +namespace MQTTnet.Server.Internal; + +public sealed class DispatchApplicationMessageResult { - public sealed class DispatchApplicationMessageResult + public DispatchApplicationMessageResult(int reasonCode, bool closeConnection, string reasonString, List userProperties) { - public DispatchApplicationMessageResult(int reasonCode, bool closeConnection, string reasonString, List userProperties) - { - ReasonCode = reasonCode; - CloseConnection = closeConnection; - ReasonString = reasonString; - UserProperties = userProperties; - } - - public bool CloseConnection { get; } - - public int ReasonCode { get; } - - public string ReasonString { get; } - - public List UserProperties { get; } + ReasonCode = reasonCode; + CloseConnection = closeConnection; + ReasonString = reasonString; + UserProperties = userProperties; } + + public bool CloseConnection { get; } + + public int ReasonCode { get; } + + public string ReasonString { get; } + + public List UserProperties { get; } } \ No newline at end of file diff --git a/Source/MQTTnet.Server/Internal/EnqueueDataPacketResult.cs b/Source/MQTTnet.Server/Internal/EnqueueDataPacketResult.cs index 0952efbb..68dcaffe 100644 --- a/Source/MQTTnet.Server/Internal/EnqueueDataPacketResult.cs +++ b/Source/MQTTnet.Server/Internal/EnqueueDataPacketResult.cs @@ -2,11 +2,10 @@ // The .NET Foundation licenses this file to you under the MIT license. // See the LICENSE file in the project root for more information. -namespace MQTTnet.Server.Internal +namespace MQTTnet.Server.Internal; + +public enum EnqueueDataPacketResult { - public enum EnqueueDataPacketResult - { - Enqueued, - Dropped - } + Enqueued, + Dropped } \ No newline at end of file diff --git a/Source/MQTTnet.Server/Internal/Formatter/MqttUnsubAckPacketFactory.cs b/Source/MQTTnet.Server/Internal/Formatter/MqttUnsubAckPacketFactory.cs index cd9e3676..57e19053 100644 --- a/Source/MQTTnet.Server/Internal/Formatter/MqttUnsubAckPacketFactory.cs +++ b/Source/MQTTnet.Server/Internal/Formatter/MqttUnsubAckPacketFactory.cs @@ -4,24 +4,23 @@ using MQTTnet.Packets; -namespace MQTTnet.Server.Internal.Formatter +namespace MQTTnet.Server.Internal.Formatter; + +public static class MqttUnsubAckPacketFactory { - public static class MqttUnsubAckPacketFactory + public static MqttUnsubAckPacket Create(MqttUnsubscribePacket unsubscribePacket, UnsubscribeResult unsubscribeResult) { - public static MqttUnsubAckPacket Create(MqttUnsubscribePacket unsubscribePacket, UnsubscribeResult unsubscribeResult) + ArgumentNullException.ThrowIfNull(unsubscribePacket); + ArgumentNullException.ThrowIfNull(unsubscribeResult); + + var unsubAckPacket = new MqttUnsubAckPacket { - ArgumentNullException.ThrowIfNull(unsubscribePacket); - ArgumentNullException.ThrowIfNull(unsubscribeResult); + PacketIdentifier = unsubscribePacket.PacketIdentifier + }; - var unsubAckPacket = new MqttUnsubAckPacket - { - PacketIdentifier = unsubscribePacket.PacketIdentifier - }; + // MQTTv5.0.0 only. + unsubAckPacket.ReasonCodes = unsubscribeResult.ReasonCodes; - // MQTTv5.0.0 only. - unsubAckPacket.ReasonCodes = unsubscribeResult.ReasonCodes; - - return unsubAckPacket; - } + return unsubAckPacket; } } \ No newline at end of file diff --git a/Source/MQTTnet.Server/Internal/ISubscriptionChangedNotification.cs b/Source/MQTTnet.Server/Internal/ISubscriptionChangedNotification.cs index af9538df..6447d51b 100644 --- a/Source/MQTTnet.Server/Internal/ISubscriptionChangedNotification.cs +++ b/Source/MQTTnet.Server/Internal/ISubscriptionChangedNotification.cs @@ -1,9 +1,8 @@ -namespace MQTTnet.Server.Internal -{ - public interface ISubscriptionChangedNotification - { - void OnSubscriptionsAdded(MqttSession clientSession, List subscriptionsTopics); +namespace MQTTnet.Server.Internal; - void OnSubscriptionsRemoved(MqttSession clientSession, List subscriptionTopics); - } +public interface ISubscriptionChangedNotification +{ + void OnSubscriptionsAdded(MqttSession clientSession, List subscriptionsTopics); + + void OnSubscriptionsRemoved(MqttSession clientSession, List subscriptionTopics); } \ No newline at end of file diff --git a/Source/MQTTnet.Server/Internal/MqttClientSessionsManager.cs b/Source/MQTTnet.Server/Internal/MqttClientSessionsManager.cs index 2356b554..4b874546 100644 --- a/Source/MQTTnet.Server/Internal/MqttClientSessionsManager.cs +++ b/Source/MQTTnet.Server/Internal/MqttClientSessionsManager.cs @@ -439,10 +439,8 @@ public sealed class MqttClientSessionsManager : ISubscriptionChangedNotification } } - using (var timeout = new CancellationTokenSource(_options.DefaultCommunicationTimeout)) - { - await channelAdapter.DisconnectAsync(timeout.Token).ConfigureAwait(false); - } + using var timeout = new CancellationTokenSource(_options.DefaultCommunicationTimeout); + await channelAdapter.DisconnectAsync(timeout.Token).ConfigureAwait(false); } } @@ -673,14 +671,12 @@ public sealed class MqttClientSessionsManager : ISubscriptionChangedNotification { try { - using (var timeoutToken = new CancellationTokenSource(_options.DefaultCommunicationTimeout)) - using (var effectiveCancellationToken = CancellationTokenSource.CreateLinkedTokenSource(timeoutToken.Token, cancellationToken)) + using var timeoutToken = new CancellationTokenSource(_options.DefaultCommunicationTimeout); + using var effectiveCancellationToken = CancellationTokenSource.CreateLinkedTokenSource(timeoutToken.Token, cancellationToken); + var firstPacket = await channelAdapter.ReceivePacketAsync(effectiveCancellationToken.Token).ConfigureAwait(false); + if (firstPacket is MqttConnectPacket connectPacket) { - var firstPacket = await channelAdapter.ReceivePacketAsync(effectiveCancellationToken.Token).ConfigureAwait(false); - if (firstPacket is MqttConnectPacket connectPacket) - { - return connectPacket; - } + return connectPacket; } } catch (OperationCanceledException) diff --git a/Source/MQTTnet.Server/Internal/MqttClientStatistics.cs b/Source/MQTTnet.Server/Internal/MqttClientStatistics.cs index 481f2945..63524bac 100644 --- a/Source/MQTTnet.Server/Internal/MqttClientStatistics.cs +++ b/Source/MQTTnet.Server/Internal/MqttClientStatistics.cs @@ -4,101 +4,100 @@ using MQTTnet.Packets; -namespace MQTTnet.Server.Internal +namespace MQTTnet.Server.Internal; + +public sealed class MqttClientStatistics { - public sealed class MqttClientStatistics + Statistics _statistics = new Statistics // mutable struct, don't make readonly! { - Statistics _statistics = new Statistics // mutable struct, don't make readonly! + // Start with 1 because the CONNACK packet is not counted here. + _receivedPacketsCount = 1, + + // Start with 1 because the CONNECT packet is not counted here. + _sentPacketsCount = 1 + }; + + public MqttClientStatistics() + { + ConnectedTimestamp = DateTime.UtcNow; + + LastPacketReceivedTimestamp = ConnectedTimestamp; + LastPacketSentTimestamp = ConnectedTimestamp; + + LastNonKeepAlivePacketReceivedTimestamp = ConnectedTimestamp; + } + + public DateTime ConnectedTimestamp { get; } + public DateTime LastNonKeepAlivePacketReceivedTimestamp { get; private set; } + + /// + /// Timestamp of the last package that has been sent to the client ("received" from the client's perspective) + /// + public DateTime LastPacketReceivedTimestamp { get; private set; } + + /// + /// Timestamp of the last package that has been received from the client ("sent" from the client's perspective) + /// + public DateTime LastPacketSentTimestamp { get; private set; } + + public long SentApplicationMessagesCount => Volatile.Read(ref _statistics._sentApplicationMessagesCount); + + public long ReceivedApplicationMessagesCount => Volatile.Read(ref _statistics._receivedApplicationMessagesCount); + + public long SentPacketsCount => Volatile.Read(ref _statistics._sentPacketsCount); + + public long ReceivedPacketsCount => Volatile.Read(ref _statistics._receivedPacketsCount); + + public void HandleReceivedPacket(MqttPacket packet) + { + ArgumentNullException.ThrowIfNull(packet); + + // This class is tracking all values from Clients perspective! + LastPacketSentTimestamp = DateTime.UtcNow; + + Interlocked.Increment(ref _statistics._sentPacketsCount); + + if (packet is MqttPublishPacket) { - // Start with 1 because the CONNACK packet is not counted here. - _receivedPacketsCount = 1, - - // Start with 1 because the CONNECT packet is not counted here. - _sentPacketsCount = 1 - }; - - public MqttClientStatistics() - { - ConnectedTimestamp = DateTime.UtcNow; - - LastPacketReceivedTimestamp = ConnectedTimestamp; - LastPacketSentTimestamp = ConnectedTimestamp; - - LastNonKeepAlivePacketReceivedTimestamp = ConnectedTimestamp; + Interlocked.Increment(ref _statistics._sentApplicationMessagesCount); } - public DateTime ConnectedTimestamp { get; } - public DateTime LastNonKeepAlivePacketReceivedTimestamp { get; private set; } - - /// - /// Timestamp of the last package that has been sent to the client ("received" from the client's perspective) - /// - public DateTime LastPacketReceivedTimestamp { get; private set; } - - /// - /// Timestamp of the last package that has been received from the client ("sent" from the client's perspective) - /// - public DateTime LastPacketSentTimestamp { get; private set; } - - public long SentApplicationMessagesCount => Volatile.Read(ref _statistics._sentApplicationMessagesCount); - - public long ReceivedApplicationMessagesCount => Volatile.Read(ref _statistics._receivedApplicationMessagesCount); - - public long SentPacketsCount => Volatile.Read(ref _statistics._sentPacketsCount); - - public long ReceivedPacketsCount => Volatile.Read(ref _statistics._receivedPacketsCount); - - public void HandleReceivedPacket(MqttPacket packet) + if (!(packet is MqttPingReqPacket || packet is MqttPingRespPacket)) { - ArgumentNullException.ThrowIfNull(packet); - - // This class is tracking all values from Clients perspective! - LastPacketSentTimestamp = DateTime.UtcNow; - - Interlocked.Increment(ref _statistics._sentPacketsCount); - - if (packet is MqttPublishPacket) - { - Interlocked.Increment(ref _statistics._sentApplicationMessagesCount); - } - - if (!(packet is MqttPingReqPacket || packet is MqttPingRespPacket)) - { - LastNonKeepAlivePacketReceivedTimestamp = LastPacketReceivedTimestamp; - } + LastNonKeepAlivePacketReceivedTimestamp = LastPacketReceivedTimestamp; } - public void ResetStatistics() => _statistics.Reset(); + } + public void ResetStatistics() => _statistics.Reset(); - public void HandleSentPacket(MqttPacket packet) + public void HandleSentPacket(MqttPacket packet) + { + ArgumentNullException.ThrowIfNull(packet); + + // This class is tracking all values from Clients perspective! + LastPacketReceivedTimestamp = DateTime.UtcNow; + + Interlocked.Increment(ref _statistics._receivedPacketsCount); + + if (packet is MqttPublishPacket) { - ArgumentNullException.ThrowIfNull(packet); - - // This class is tracking all values from Clients perspective! - LastPacketReceivedTimestamp = DateTime.UtcNow; - - Interlocked.Increment(ref _statistics._receivedPacketsCount); - - if (packet is MqttPublishPacket) - { - Interlocked.Increment(ref _statistics._receivedApplicationMessagesCount); - } + Interlocked.Increment(ref _statistics._receivedApplicationMessagesCount); } + } - struct Statistics + struct Statistics + { + public long _receivedPacketsCount; + public long _sentPacketsCount; + + public long _receivedApplicationMessagesCount; + public long _sentApplicationMessagesCount; + + public void Reset() { - public long _receivedPacketsCount; - public long _sentPacketsCount; - - public long _receivedApplicationMessagesCount; - public long _sentApplicationMessagesCount; - - public void Reset() - { - Volatile.Write(ref _receivedPacketsCount, 0); - Volatile.Write(ref _sentPacketsCount, 0); - Volatile.Write(ref _receivedApplicationMessagesCount, 0); - Volatile.Write(ref _sentApplicationMessagesCount, 0); - } + Volatile.Write(ref _receivedPacketsCount, 0); + Volatile.Write(ref _sentPacketsCount, 0); + Volatile.Write(ref _receivedApplicationMessagesCount, 0); + Volatile.Write(ref _sentApplicationMessagesCount, 0); } } } \ No newline at end of file diff --git a/Source/MQTTnet.Server/Internal/MqttClientSubscriptionsManager.cs b/Source/MQTTnet.Server/Internal/MqttClientSubscriptionsManager.cs index 7c1023e5..381a155e 100644 --- a/Source/MQTTnet.Server/Internal/MqttClientSubscriptionsManager.cs +++ b/Source/MQTTnet.Server/Internal/MqttClientSubscriptionsManager.cs @@ -5,180 +5,237 @@ using MQTTnet.Packets; using MQTTnet.Protocol; -namespace MQTTnet.Server.Internal +namespace MQTTnet.Server.Internal; + +public sealed class MqttClientSubscriptionsManager : IDisposable { - public sealed class MqttClientSubscriptionsManager : IDisposable + static readonly List EmptySubscriptionIdentifiers = new List(); + + readonly MqttServerEventContainer _eventContainer; + readonly Dictionary> _noWildcardSubscriptionsByTopicHash = new Dictionary>(); + readonly MqttRetainedMessagesManager _retainedMessagesManager; + + readonly MqttSession _session; + + // Callback to maintain list of subscriber clients + readonly ISubscriptionChangedNotification _subscriptionChangedNotification; + + // Subscriptions are stored in various dictionaries and use a "topic hash"; see the MqttSubscription object for a detailed explanation. + // The additional lock is important to coordinate complex update logic with multiple steps, checks and interceptors. + readonly Dictionary _subscriptions = new Dictionary(); + + // Use subscription lock to maintain consistency across subscriptions and topic hash dictionaries + readonly ReaderWriterLockSlim _subscriptionsLock = new ReaderWriterLockSlim(); + readonly Dictionary _wildcardSubscriptionsByTopicHash = new Dictionary(); + + public MqttClientSubscriptionsManager( + MqttSession session, + MqttServerEventContainer eventContainer, + MqttRetainedMessagesManager retainedMessagesManager, + ISubscriptionChangedNotification subscriptionChangedNotification) { - static readonly List EmptySubscriptionIdentifiers = new List(); + _session = session ?? throw new ArgumentNullException(nameof(session)); + _eventContainer = eventContainer ?? throw new ArgumentNullException(nameof(eventContainer)); + _retainedMessagesManager = retainedMessagesManager ?? throw new ArgumentNullException(nameof(retainedMessagesManager)); + _subscriptionChangedNotification = subscriptionChangedNotification; + } - readonly MqttServerEventContainer _eventContainer; - readonly Dictionary> _noWildcardSubscriptionsByTopicHash = new Dictionary>(); - readonly MqttRetainedMessagesManager _retainedMessagesManager; + public CheckSubscriptionsResult CheckSubscriptions(string topic, ulong topicHash, MqttQualityOfServiceLevel qualityOfServiceLevel, string senderId) + { + var possibleSubscriptions = new List(); - readonly MqttSession _session; - - // Callback to maintain list of subscriber clients - readonly ISubscriptionChangedNotification _subscriptionChangedNotification; - - // Subscriptions are stored in various dictionaries and use a "topic hash"; see the MqttSubscription object for a detailed explanation. - // The additional lock is important to coordinate complex update logic with multiple steps, checks and interceptors. - readonly Dictionary _subscriptions = new Dictionary(); - - // Use subscription lock to maintain consistency across subscriptions and topic hash dictionaries - readonly ReaderWriterLockSlim _subscriptionsLock = new ReaderWriterLockSlim(); - readonly Dictionary _wildcardSubscriptionsByTopicHash = new Dictionary(); - - public MqttClientSubscriptionsManager( - MqttSession session, - MqttServerEventContainer eventContainer, - MqttRetainedMessagesManager retainedMessagesManager, - ISubscriptionChangedNotification subscriptionChangedNotification) + // Check for possible subscriptions. They might have collisions but this is fine. + _subscriptionsLock.EnterReadLock(); + try { - _session = session ?? throw new ArgumentNullException(nameof(session)); - _eventContainer = eventContainer ?? throw new ArgumentNullException(nameof(eventContainer)); - _retainedMessagesManager = retainedMessagesManager ?? throw new ArgumentNullException(nameof(retainedMessagesManager)); - _subscriptionChangedNotification = subscriptionChangedNotification; - } - - public CheckSubscriptionsResult CheckSubscriptions(string topic, ulong topicHash, MqttQualityOfServiceLevel qualityOfServiceLevel, string senderId) - { - var possibleSubscriptions = new List(); - - // Check for possible subscriptions. They might have collisions but this is fine. - _subscriptionsLock.EnterReadLock(); - try + if (_noWildcardSubscriptionsByTopicHash.TryGetValue(topicHash, out var noWildcardSubscriptions)) { - if (_noWildcardSubscriptionsByTopicHash.TryGetValue(topicHash, out var noWildcardSubscriptions)) - { - possibleSubscriptions.AddRange(noWildcardSubscriptions); - } + possibleSubscriptions.AddRange(noWildcardSubscriptions); + } - foreach (var wcs in _wildcardSubscriptionsByTopicHash) + foreach (var wcs in _wildcardSubscriptionsByTopicHash) + { + var subscriptionHash = wcs.Key; + var subscriptionsByHashMask = wcs.Value.SubscriptionsByHashMask; + foreach (var shm in subscriptionsByHashMask) { - var subscriptionHash = wcs.Key; - var subscriptionsByHashMask = wcs.Value.SubscriptionsByHashMask; - foreach (var shm in subscriptionsByHashMask) + var subscriptionHashMask = shm.Key; + if ((topicHash & subscriptionHashMask) == subscriptionHash) { - var subscriptionHashMask = shm.Key; - if ((topicHash & subscriptionHashMask) == subscriptionHash) - { - var subscriptions = shm.Value; - possibleSubscriptions.AddRange(subscriptions); - } + var subscriptions = shm.Value; + possibleSubscriptions.AddRange(subscriptions); } } } - finally - { - _subscriptionsLock.ExitReadLock(); - } - - // The pre check has evaluated that nothing is subscribed. - // If there were some possible candidates they get checked below - // again to avoid collisions. - if (possibleSubscriptions.Count == 0) - { - return CheckSubscriptionsResult.NotSubscribed; - } - - var senderIsReceiver = string.Equals(senderId, _session.Id); - var maxQoSLevel = -1; // Not subscribed. - - HashSet subscriptionIdentifiers = null; - var retainAsPublished = false; - - foreach (var subscription in possibleSubscriptions) - { - if (subscription.NoLocal && senderIsReceiver) - { - // This is a MQTTv5 feature! - continue; - } - - if (MqttTopicFilterComparer.Compare(topic, subscription.Topic) != MqttTopicFilterCompareResult.IsMatch) - { - continue; - } - - if (subscription.RetainAsPublished) - { - // This is a MQTTv5 feature! - retainAsPublished = true; - } - - if ((int)subscription.GrantedQualityOfServiceLevel > maxQoSLevel) - { - maxQoSLevel = (int)subscription.GrantedQualityOfServiceLevel; - } - - if (subscription.Identifier > 0) - { - if (subscriptionIdentifiers == null) - { - subscriptionIdentifiers = new HashSet(); - } - - subscriptionIdentifiers.Add(subscription.Identifier); - } - } - - if (maxQoSLevel == -1) - { - return CheckSubscriptionsResult.NotSubscribed; - } - - var result = new CheckSubscriptionsResult - { - IsSubscribed = true, - RetainAsPublished = retainAsPublished, - SubscriptionIdentifiers = subscriptionIdentifiers?.ToList() ?? EmptySubscriptionIdentifiers, - - // Start with the same QoS as the publisher. - QualityOfServiceLevel = qualityOfServiceLevel - }; - - // Now downgrade if required. - // - // If a subscribing Client has been granted maximum QoS 1 for a particular Topic Filter, then a QoS 0 Application Message matching the filter is delivered - // to the Client at QoS 0. This means that at most one copy of the message is received by the Client. On the other hand, a QoS 2 Message published to - // the same topic is downgraded by the Server to QoS 1 for delivery to the Client, so that Client might receive duplicate copies of the Message. - - // Subscribing to a Topic Filter at QoS 2 is equivalent to saying "I would like to receive Messages matching this filter at the QoS with which they were published". - // This means a publisher is responsible for determining the maximum QoS a Message can be delivered at, but a subscriber is able to require that the Server - // downgrades the QoS to one more suitable for its usage. - if (maxQoSLevel < (int)qualityOfServiceLevel) - { - result.QualityOfServiceLevel = (MqttQualityOfServiceLevel)maxQoSLevel; - } - - return result; + } + finally + { + _subscriptionsLock.ExitReadLock(); } - public void Dispose() + // The pre check has evaluated that nothing is subscribed. + // If there were some possible candidates they get checked below + // again to avoid collisions. + if (possibleSubscriptions.Count == 0) { - _subscriptionsLock?.Dispose(); + return CheckSubscriptionsResult.NotSubscribed; } - public async Task Subscribe(MqttSubscribePacket subscribePacket, CancellationToken cancellationToken) + var senderIsReceiver = string.Equals(senderId, _session.Id); + var maxQoSLevel = -1; // Not subscribed. + + HashSet subscriptionIdentifiers = null; + var retainAsPublished = false; + + foreach (var subscription in possibleSubscriptions) { - ArgumentNullException.ThrowIfNull(subscribePacket); - - var retainedApplicationMessages = await _retainedMessagesManager.GetMessages().ConfigureAwait(false); - var result = new SubscribeResult(subscribePacket.TopicFilters.Count); - - var addedSubscriptions = new List(); - var finalTopicFilters = new List(); - - // The topic filters are order by its QoS so that the higher QoS will win over a - // lower one. - foreach (var topicFilterItem in subscribePacket.TopicFilters.OrderByDescending(f => f.QualityOfServiceLevel)) + if (subscription.NoLocal && senderIsReceiver) { - var interceptorEventArgs = await InterceptSubscribe(topicFilterItem, subscribePacket.UserProperties, cancellationToken).ConfigureAwait(false); - var topicFilter = interceptorEventArgs.TopicFilter; - var processSubscription = interceptorEventArgs.ProcessSubscription && interceptorEventArgs.Response.ReasonCode <= MqttSubscribeReasonCode.GrantedQoS2; + // This is a MQTTv5 feature! + continue; + } + + if (MqttTopicFilterComparer.Compare(topic, subscription.Topic) != MqttTopicFilterCompareResult.IsMatch) + { + continue; + } + + if (subscription.RetainAsPublished) + { + // This is a MQTTv5 feature! + retainAsPublished = true; + } + + if ((int)subscription.GrantedQualityOfServiceLevel > maxQoSLevel) + { + maxQoSLevel = (int)subscription.GrantedQualityOfServiceLevel; + } + + if (subscription.Identifier > 0) + { + if (subscriptionIdentifiers == null) + { + subscriptionIdentifiers = new HashSet(); + } + + subscriptionIdentifiers.Add(subscription.Identifier); + } + } + + if (maxQoSLevel == -1) + { + return CheckSubscriptionsResult.NotSubscribed; + } + + var result = new CheckSubscriptionsResult + { + IsSubscribed = true, + RetainAsPublished = retainAsPublished, + SubscriptionIdentifiers = subscriptionIdentifiers?.ToList() ?? EmptySubscriptionIdentifiers, + + // Start with the same QoS as the publisher. + QualityOfServiceLevel = qualityOfServiceLevel + }; + + // Now downgrade if required. + // + // If a subscribing Client has been granted maximum QoS 1 for a particular Topic Filter, then a QoS 0 Application Message matching the filter is delivered + // to the Client at QoS 0. This means that at most one copy of the message is received by the Client. On the other hand, a QoS 2 Message published to + // the same topic is downgraded by the Server to QoS 1 for delivery to the Client, so that Client might receive duplicate copies of the Message. + + // Subscribing to a Topic Filter at QoS 2 is equivalent to saying "I would like to receive Messages matching this filter at the QoS with which they were published". + // This means a publisher is responsible for determining the maximum QoS a Message can be delivered at, but a subscriber is able to require that the Server + // downgrades the QoS to one more suitable for its usage. + if (maxQoSLevel < (int)qualityOfServiceLevel) + { + result.QualityOfServiceLevel = (MqttQualityOfServiceLevel)maxQoSLevel; + } + + return result; + } + + public void Dispose() + { + _subscriptionsLock?.Dispose(); + } + + public async Task Subscribe(MqttSubscribePacket subscribePacket, CancellationToken cancellationToken) + { + ArgumentNullException.ThrowIfNull(subscribePacket); + + var retainedApplicationMessages = await _retainedMessagesManager.GetMessages().ConfigureAwait(false); + var result = new SubscribeResult(subscribePacket.TopicFilters.Count); + + var addedSubscriptions = new List(); + var finalTopicFilters = new List(); + + // The topic filters are order by its QoS so that the higher QoS will win over a + // lower one. + foreach (var topicFilterItem in subscribePacket.TopicFilters.OrderByDescending(f => f.QualityOfServiceLevel)) + { + var interceptorEventArgs = await InterceptSubscribe(topicFilterItem, subscribePacket.UserProperties, cancellationToken).ConfigureAwait(false); + var topicFilter = interceptorEventArgs.TopicFilter; + var processSubscription = interceptorEventArgs.ProcessSubscription && interceptorEventArgs.Response.ReasonCode <= MqttSubscribeReasonCode.GrantedQoS2; + + result.UserProperties = interceptorEventArgs.UserProperties; + result.ReasonString = interceptorEventArgs.ReasonString; + result.ReasonCodes.Add(interceptorEventArgs.Response.ReasonCode); + + if (interceptorEventArgs.CloseConnection) + { + // When any of the interceptor calls leads to a connection close the connection + // must be closed. So do not revert to false! + result.CloseConnection = true; + } + + if (!processSubscription || string.IsNullOrEmpty(topicFilter.Topic)) + { + continue; + } + + var createSubscriptionResult = CreateSubscription(topicFilter, subscribePacket.SubscriptionIdentifier, interceptorEventArgs.Response.ReasonCode); + + addedSubscriptions.Add(topicFilter.Topic); + finalTopicFilters.Add(topicFilter); + + FilterRetainedApplicationMessages(retainedApplicationMessages, createSubscriptionResult, result); + } + + // This call will add the new subscription to the internal storage. + // So the event _ClientSubscribedTopicEvent_ must be called afterwards. + _subscriptionChangedNotification?.OnSubscriptionsAdded(_session, addedSubscriptions); + + if (_eventContainer.ClientSubscribedTopicEvent.HasHandlers) + { + foreach (var finalTopicFilter in finalTopicFilters) + { + var eventArgs = new ClientSubscribedTopicEventArgs(_session.Id, _session.UserName, finalTopicFilter, _session.Items); + await _eventContainer.ClientSubscribedTopicEvent.InvokeAsync(eventArgs).ConfigureAwait(false); + } + } + + return result; + } + + public async Task Unsubscribe(MqttUnsubscribePacket unsubscribePacket, CancellationToken cancellationToken) + { + ArgumentNullException.ThrowIfNull(unsubscribePacket); + + var result = new UnsubscribeResult(); + + var removedSubscriptions = new List(); + + _subscriptionsLock.EnterWriteLock(); + try + { + foreach (var topicFilter in unsubscribePacket.TopicFilters) + { + _subscriptions.TryGetValue(topicFilter, out var existingSubscription); + + var interceptorEventArgs = await InterceptUnsubscribe(topicFilter, existingSubscription, unsubscribePacket.UserProperties, cancellationToken).ConfigureAwait(false); + var acceptUnsubscription = interceptorEventArgs.Response.ReasonCode == MqttUnsubscribeReasonCode.Success; result.UserProperties = interceptorEventArgs.UserProperties; - result.ReasonString = interceptorEventArgs.ReasonString; result.ReasonCodes.Add(interceptorEventArgs.Response.ReasonCode); if (interceptorEventArgs.CloseConnection) @@ -188,329 +245,271 @@ namespace MQTTnet.Server.Internal result.CloseConnection = true; } - if (!processSubscription || string.IsNullOrEmpty(topicFilter.Topic)) + if (!acceptUnsubscription) { continue; } - var createSubscriptionResult = CreateSubscription(topicFilter, subscribePacket.SubscriptionIdentifier, interceptorEventArgs.Response.ReasonCode); - - addedSubscriptions.Add(topicFilter.Topic); - finalTopicFilters.Add(topicFilter); - - FilterRetainedApplicationMessages(retainedApplicationMessages, createSubscriptionResult, result); - } - - // This call will add the new subscription to the internal storage. - // So the event _ClientSubscribedTopicEvent_ must be called afterwards. - _subscriptionChangedNotification?.OnSubscriptionsAdded(_session, addedSubscriptions); - - if (_eventContainer.ClientSubscribedTopicEvent.HasHandlers) - { - foreach (var finalTopicFilter in finalTopicFilters) + if (interceptorEventArgs.ProcessUnsubscription) { - var eventArgs = new ClientSubscribedTopicEventArgs(_session.Id, _session.UserName, finalTopicFilter, _session.Items); - await _eventContainer.ClientSubscribedTopicEvent.InvokeAsync(eventArgs).ConfigureAwait(false); - } - } + _subscriptions.Remove(topicFilter); - return result; - } - - public async Task Unsubscribe(MqttUnsubscribePacket unsubscribePacket, CancellationToken cancellationToken) - { - ArgumentNullException.ThrowIfNull(unsubscribePacket); - - var result = new UnsubscribeResult(); - - var removedSubscriptions = new List(); - - _subscriptionsLock.EnterWriteLock(); - try - { - foreach (var topicFilter in unsubscribePacket.TopicFilters) - { - _subscriptions.TryGetValue(topicFilter, out var existingSubscription); - - var interceptorEventArgs = await InterceptUnsubscribe(topicFilter, existingSubscription, unsubscribePacket.UserProperties, cancellationToken).ConfigureAwait(false); - var acceptUnsubscription = interceptorEventArgs.Response.ReasonCode == MqttUnsubscribeReasonCode.Success; - - result.UserProperties = interceptorEventArgs.UserProperties; - result.ReasonCodes.Add(interceptorEventArgs.Response.ReasonCode); - - if (interceptorEventArgs.CloseConnection) + // must remove subscription object from topic hash dictionary also + if (existingSubscription != null) { - // When any of the interceptor calls leads to a connection close the connection - // must be closed. So do not revert to false! - result.CloseConnection = true; - } + var topicHash = existingSubscription.TopicHash; - if (!acceptUnsubscription) - { - continue; - } - - if (interceptorEventArgs.ProcessUnsubscription) - { - _subscriptions.Remove(topicFilter); - - // must remove subscription object from topic hash dictionary also - if (existingSubscription != null) + if (existingSubscription.TopicHasWildcard) { - var topicHash = existingSubscription.TopicHash; - - if (existingSubscription.TopicHasWildcard) + if (_wildcardSubscriptionsByTopicHash.TryGetValue(topicHash, out var subscriptions)) { - if (_wildcardSubscriptionsByTopicHash.TryGetValue(topicHash, out var subscriptions)) + subscriptions.RemoveSubscription(existingSubscription); + if (subscriptions.SubscriptionsByHashMask.Count == 0) { - subscriptions.RemoveSubscription(existingSubscription); - if (subscriptions.SubscriptionsByHashMask.Count == 0) - { - _wildcardSubscriptionsByTopicHash.Remove(topicHash); - } - } - } - else - { - if (_noWildcardSubscriptionsByTopicHash.TryGetValue(topicHash, out var subscriptions)) - { - subscriptions.Remove(existingSubscription); - if (subscriptions.Count == 0) - { - _noWildcardSubscriptionsByTopicHash.Remove(topicHash); - } + _wildcardSubscriptionsByTopicHash.Remove(topicHash); + } + } + } + else + { + if (_noWildcardSubscriptionsByTopicHash.TryGetValue(topicHash, out var subscriptions)) + { + subscriptions.Remove(existingSubscription); + if (subscriptions.Count == 0) + { + _noWildcardSubscriptionsByTopicHash.Remove(topicHash); } } } - - removedSubscriptions.Add(topicFilter); } + + removedSubscriptions.Add(topicFilter); } } - finally - { - _subscriptionsLock.ExitWriteLock(); - _subscriptionChangedNotification?.OnSubscriptionsRemoved(_session, removedSubscriptions); - } - - if (_eventContainer.ClientUnsubscribedTopicEvent.HasHandlers) - { - foreach (var topicFilter in unsubscribePacket.TopicFilters) - { - var eventArgs = new ClientUnsubscribedTopicEventArgs(_session.Id, _session.UserName, topicFilter, _session.Items); - await _eventContainer.ClientUnsubscribedTopicEvent.InvokeAsync(eventArgs).ConfigureAwait(false); - } - } - - return result; + } + finally + { + _subscriptionsLock.ExitWriteLock(); + _subscriptionChangedNotification?.OnSubscriptionsRemoved(_session, removedSubscriptions); } - CreateSubscriptionResult CreateSubscription(MqttTopicFilter topicFilter, uint subscriptionIdentifier, MqttSubscribeReasonCode reasonCode) + if (_eventContainer.ClientUnsubscribedTopicEvent.HasHandlers) { - MqttQualityOfServiceLevel grantedQualityOfServiceLevel; - - if (reasonCode == MqttSubscribeReasonCode.GrantedQoS0) + foreach (var topicFilter in unsubscribePacket.TopicFilters) { - grantedQualityOfServiceLevel = MqttQualityOfServiceLevel.AtMostOnce; + var eventArgs = new ClientUnsubscribedTopicEventArgs(_session.Id, _session.UserName, topicFilter, _session.Items); + await _eventContainer.ClientUnsubscribedTopicEvent.InvokeAsync(eventArgs).ConfigureAwait(false); } - else if (reasonCode == MqttSubscribeReasonCode.GrantedQoS1) + } + + return result; + } + + CreateSubscriptionResult CreateSubscription(MqttTopicFilter topicFilter, uint subscriptionIdentifier, MqttSubscribeReasonCode reasonCode) + { + MqttQualityOfServiceLevel grantedQualityOfServiceLevel; + + if (reasonCode == MqttSubscribeReasonCode.GrantedQoS0) + { + grantedQualityOfServiceLevel = MqttQualityOfServiceLevel.AtMostOnce; + } + else if (reasonCode == MqttSubscribeReasonCode.GrantedQoS1) + { + grantedQualityOfServiceLevel = MqttQualityOfServiceLevel.AtLeastOnce; + } + else if (reasonCode == MqttSubscribeReasonCode.GrantedQoS2) + { + grantedQualityOfServiceLevel = MqttQualityOfServiceLevel.ExactlyOnce; + } + else + { + throw new InvalidOperationException(); + } + + var subscription = new MqttSubscription( + topicFilter.Topic, + topicFilter.NoLocal, + topicFilter.RetainHandling, + topicFilter.RetainAsPublished, + grantedQualityOfServiceLevel, + subscriptionIdentifier); + + bool isNewSubscription; + + // Add to subscriptions and maintain topic hash dictionaries + + _subscriptionsLock.EnterWriteLock(); + try + { + MqttTopicHash.Calculate(topicFilter.Topic, out var topicHash, out _, out var hasWildcard); + + if (_subscriptions.TryGetValue(topicFilter.Topic, out var existingSubscription)) { - grantedQualityOfServiceLevel = MqttQualityOfServiceLevel.AtLeastOnce; - } - else if (reasonCode == MqttSubscribeReasonCode.GrantedQoS2) - { - grantedQualityOfServiceLevel = MqttQualityOfServiceLevel.ExactlyOnce; - } - else - { - throw new InvalidOperationException(); - } - - var subscription = new MqttSubscription( - topicFilter.Topic, - topicFilter.NoLocal, - topicFilter.RetainHandling, - topicFilter.RetainAsPublished, - grantedQualityOfServiceLevel, - subscriptionIdentifier); - - bool isNewSubscription; - - // Add to subscriptions and maintain topic hash dictionaries - - _subscriptionsLock.EnterWriteLock(); - try - { - MqttTopicHash.Calculate(topicFilter.Topic, out var topicHash, out _, out var hasWildcard); - - if (_subscriptions.TryGetValue(topicFilter.Topic, out var existingSubscription)) - { - // must remove object from topic hash dictionary first - if (hasWildcard) - { - if (_wildcardSubscriptionsByTopicHash.TryGetValue(topicHash, out var subscriptions)) - { - subscriptions.RemoveSubscription(existingSubscription); - // no need to remove empty entry because we'll be adding subscription again below - } - } - else - { - if (_noWildcardSubscriptionsByTopicHash.TryGetValue(topicHash, out var subscriptions)) - { - subscriptions.Remove(existingSubscription); - // no need to remove empty entry because we'll be adding subscription again below - } - } - } - - isNewSubscription = existingSubscription == null; - _subscriptions[topicFilter.Topic] = subscription; - - // Add or re-add to topic hash dictionary + // must remove object from topic hash dictionary first if (hasWildcard) { - if (!_wildcardSubscriptionsByTopicHash.TryGetValue(topicHash, out var subscriptions)) + if (_wildcardSubscriptionsByTopicHash.TryGetValue(topicHash, out var subscriptions)) { - subscriptions = new TopicHashMaskSubscriptions(); - _wildcardSubscriptionsByTopicHash.Add(topicHash, subscriptions); + subscriptions.RemoveSubscription(existingSubscription); + // no need to remove empty entry because we'll be adding subscription again below } - - subscriptions.AddSubscription(subscription); } else { - if (!_noWildcardSubscriptionsByTopicHash.TryGetValue(topicHash, out var subscriptions)) + if (_noWildcardSubscriptionsByTopicHash.TryGetValue(topicHash, out var subscriptions)) { - subscriptions = new HashSet(); - _noWildcardSubscriptionsByTopicHash.Add(topicHash, subscriptions); + subscriptions.Remove(existingSubscription); + // no need to remove empty entry because we'll be adding subscription again below } - - subscriptions.Add(subscription); } } - finally - { - _subscriptionsLock.ExitWriteLock(); - } - return new CreateSubscriptionResult - { - IsNewSubscription = isNewSubscription, - Subscription = subscription - }; - } + isNewSubscription = existingSubscription == null; + _subscriptions[topicFilter.Topic] = subscription; - static void FilterRetainedApplicationMessages( - IList retainedMessages, - CreateSubscriptionResult createSubscriptionResult, - SubscribeResult subscribeResult) - { - if (createSubscriptionResult.Subscription.RetainHandling == MqttRetainHandling.DoNotSendOnSubscribe) + // Add or re-add to topic hash dictionary + if (hasWildcard) { - // This is a MQTT V5+ feature. - return; - } - - if (createSubscriptionResult.Subscription.RetainHandling == MqttRetainHandling.SendAtSubscribeIfNewSubscriptionOnly && !createSubscriptionResult.IsNewSubscription) - { - // This is a MQTT V5+ feature. - return; - } - - for (var index = retainedMessages.Count - 1; index >= 0; index--) - { - var retainedMessage = retainedMessages[index]; - if (retainedMessage == null) + if (!_wildcardSubscriptionsByTopicHash.TryGetValue(topicHash, out var subscriptions)) { - continue; + subscriptions = new TopicHashMaskSubscriptions(); + _wildcardSubscriptionsByTopicHash.Add(topicHash, subscriptions); } - if (MqttTopicFilterComparer.Compare(retainedMessage.Topic, createSubscriptionResult.Subscription.Topic) != MqttTopicFilterCompareResult.IsMatch) - { - continue; - } - - var retainedMessageMatch = new MqttRetainedMessageMatch(retainedMessage, createSubscriptionResult.Subscription.GrantedQualityOfServiceLevel); - if (retainedMessageMatch.SubscriptionQualityOfServiceLevel > retainedMessageMatch.ApplicationMessage.QualityOfServiceLevel) - { - // UPGRADING the QoS is not allowed! - // From MQTT spec: Subscribing to a Topic Filter at QoS 2 is equivalent to saying - // "I would like to receive Messages matching this filter at the QoS with which they were published". - // This means a publisher is responsible for determining the maximum QoS a Message can be delivered at, - // but a subscriber is able to require that the Server downgrades the QoS to one more suitable for its usage. - retainedMessageMatch.SubscriptionQualityOfServiceLevel = retainedMessageMatch.ApplicationMessage.QualityOfServiceLevel; - } - - if (subscribeResult.RetainedMessages == null) - { - subscribeResult.RetainedMessages = new List(); - } - - subscribeResult.RetainedMessages.Add(retainedMessageMatch); - - // Clear the retained message from the list because the client should receive every message only - // one time even if multiple subscriptions affect them. - retainedMessages[index] = null; - } - } - - async Task InterceptSubscribe( - MqttTopicFilter topicFilter, - List userProperties, - CancellationToken cancellationToken) - { - var eventArgs = new InterceptingSubscriptionEventArgs(cancellationToken, _session.Id, _session.UserName, new MqttSessionStatus(_session), topicFilter, userProperties); - - if (topicFilter.QualityOfServiceLevel == MqttQualityOfServiceLevel.AtMostOnce) - { - eventArgs.Response.ReasonCode = MqttSubscribeReasonCode.GrantedQoS0; - } - else if (topicFilter.QualityOfServiceLevel == MqttQualityOfServiceLevel.AtLeastOnce) - { - eventArgs.Response.ReasonCode = MqttSubscribeReasonCode.GrantedQoS1; - } - else if (topicFilter.QualityOfServiceLevel == MqttQualityOfServiceLevel.ExactlyOnce) - { - eventArgs.Response.ReasonCode = MqttSubscribeReasonCode.GrantedQoS2; - } - - if (topicFilter.Topic.StartsWith("$share/")) - { - eventArgs.Response.ReasonCode = MqttSubscribeReasonCode.SharedSubscriptionsNotSupported; + subscriptions.AddSubscription(subscription); } else { - await _eventContainer.InterceptingSubscriptionEvent.InvokeAsync(eventArgs).ConfigureAwait(false); + if (!_noWildcardSubscriptionsByTopicHash.TryGetValue(topicHash, out var subscriptions)) + { + subscriptions = new HashSet(); + _noWildcardSubscriptionsByTopicHash.Add(topicHash, subscriptions); + } + + subscriptions.Add(subscription); + } + } + finally + { + _subscriptionsLock.ExitWriteLock(); + } + + return new CreateSubscriptionResult + { + IsNewSubscription = isNewSubscription, + Subscription = subscription + }; + } + + static void FilterRetainedApplicationMessages( + IList retainedMessages, + CreateSubscriptionResult createSubscriptionResult, + SubscribeResult subscribeResult) + { + if (createSubscriptionResult.Subscription.RetainHandling == MqttRetainHandling.DoNotSendOnSubscribe) + { + // This is a MQTT V5+ feature. + return; + } + + if (createSubscriptionResult.Subscription.RetainHandling == MqttRetainHandling.SendAtSubscribeIfNewSubscriptionOnly && !createSubscriptionResult.IsNewSubscription) + { + // This is a MQTT V5+ feature. + return; + } + + for (var index = retainedMessages.Count - 1; index >= 0; index--) + { + var retainedMessage = retainedMessages[index]; + if (retainedMessage == null) + { + continue; } - return eventArgs; - } - - async Task InterceptUnsubscribe( - string topicFilter, - MqttSubscription mqttSubscription, - List userProperties, - CancellationToken cancellationToken) - { - var clientUnsubscribingTopicEventArgs = new InterceptingUnsubscriptionEventArgs(cancellationToken, _session.Id, _session.UserName, _session.Items, topicFilter, userProperties) + if (MqttTopicFilterComparer.Compare(retainedMessage.Topic, createSubscriptionResult.Subscription.Topic) != MqttTopicFilterCompareResult.IsMatch) { - Response = - { - ReasonCode = mqttSubscription == null ? MqttUnsubscribeReasonCode.NoSubscriptionExisted : MqttUnsubscribeReasonCode.Success - } - }; + continue; + } - await _eventContainer.InterceptingUnsubscriptionEvent.InvokeAsync(clientUnsubscribingTopicEventArgs).ConfigureAwait(false); + var retainedMessageMatch = new MqttRetainedMessageMatch(retainedMessage, createSubscriptionResult.Subscription.GrantedQualityOfServiceLevel); + if (retainedMessageMatch.SubscriptionQualityOfServiceLevel > retainedMessageMatch.ApplicationMessage.QualityOfServiceLevel) + { + // UPGRADING the QoS is not allowed! + // From MQTT spec: Subscribing to a Topic Filter at QoS 2 is equivalent to saying + // "I would like to receive Messages matching this filter at the QoS with which they were published". + // This means a publisher is responsible for determining the maximum QoS a Message can be delivered at, + // but a subscriber is able to require that the Server downgrades the QoS to one more suitable for its usage. + retainedMessageMatch.SubscriptionQualityOfServiceLevel = retainedMessageMatch.ApplicationMessage.QualityOfServiceLevel; + } - return clientUnsubscribingTopicEventArgs; - } + if (subscribeResult.RetainedMessages == null) + { + subscribeResult.RetainedMessages = new List(); + } - sealed class CreateSubscriptionResult - { - public bool IsNewSubscription { get; set; } + subscribeResult.RetainedMessages.Add(retainedMessageMatch); - public MqttSubscription Subscription { get; set; } + // Clear the retained message from the list because the client should receive every message only + // one time even if multiple subscriptions affect them. + retainedMessages[index] = null; } } -} + + async Task InterceptSubscribe( + MqttTopicFilter topicFilter, + List userProperties, + CancellationToken cancellationToken) + { + var eventArgs = new InterceptingSubscriptionEventArgs(cancellationToken, _session.Id, _session.UserName, new MqttSessionStatus(_session), topicFilter, userProperties); + + if (topicFilter.QualityOfServiceLevel == MqttQualityOfServiceLevel.AtMostOnce) + { + eventArgs.Response.ReasonCode = MqttSubscribeReasonCode.GrantedQoS0; + } + else if (topicFilter.QualityOfServiceLevel == MqttQualityOfServiceLevel.AtLeastOnce) + { + eventArgs.Response.ReasonCode = MqttSubscribeReasonCode.GrantedQoS1; + } + else if (topicFilter.QualityOfServiceLevel == MqttQualityOfServiceLevel.ExactlyOnce) + { + eventArgs.Response.ReasonCode = MqttSubscribeReasonCode.GrantedQoS2; + } + + if (topicFilter.Topic.StartsWith("$share/")) + { + eventArgs.Response.ReasonCode = MqttSubscribeReasonCode.SharedSubscriptionsNotSupported; + } + else + { + await _eventContainer.InterceptingSubscriptionEvent.InvokeAsync(eventArgs).ConfigureAwait(false); + } + + return eventArgs; + } + + async Task InterceptUnsubscribe( + string topicFilter, + MqttSubscription mqttSubscription, + List userProperties, + CancellationToken cancellationToken) + { + var clientUnsubscribingTopicEventArgs = new InterceptingUnsubscriptionEventArgs(cancellationToken, _session.Id, _session.UserName, _session.Items, topicFilter, userProperties) + { + Response = + { + ReasonCode = mqttSubscription == null ? MqttUnsubscribeReasonCode.NoSubscriptionExisted : MqttUnsubscribeReasonCode.Success + } + }; + + await _eventContainer.InterceptingUnsubscriptionEvent.InvokeAsync(clientUnsubscribingTopicEventArgs).ConfigureAwait(false); + + return clientUnsubscribingTopicEventArgs; + } + + sealed class CreateSubscriptionResult + { + public bool IsNewSubscription { get; set; } + + public MqttSubscription Subscription { get; set; } + } +} \ No newline at end of file diff --git a/Source/MQTTnet.Server/Internal/MqttRetainedMessagesManager.cs b/Source/MQTTnet.Server/Internal/MqttRetainedMessagesManager.cs index f4bd1965..41dacb9b 100644 --- a/Source/MQTTnet.Server/Internal/MqttRetainedMessagesManager.cs +++ b/Source/MQTTnet.Server/Internal/MqttRetainedMessagesManager.cs @@ -2,148 +2,146 @@ // The .NET Foundation licenses this file to you under the MIT license. // See the LICENSE file in the project root for more information. -using System.Collections.ObjectModel; using MQTTnet.Diagnostics.Logger; using MQTTnet.Internal; -namespace MQTTnet.Server.Internal +namespace MQTTnet.Server.Internal; + +public sealed class MqttRetainedMessagesManager { - public sealed class MqttRetainedMessagesManager + readonly Dictionary _messages = new Dictionary(4096); + readonly AsyncLock _storageAccessLock = new AsyncLock(); + + readonly MqttServerEventContainer _eventContainer; + readonly MqttNetSourceLogger _logger; + + public MqttRetainedMessagesManager(MqttServerEventContainer eventContainer, IMqttNetLogger logger) { - readonly Dictionary _messages = new Dictionary(4096); - readonly AsyncLock _storageAccessLock = new AsyncLock(); + _eventContainer = eventContainer ?? throw new ArgumentNullException(nameof(eventContainer)); - readonly MqttServerEventContainer _eventContainer; - readonly MqttNetSourceLogger _logger; + ArgumentNullException.ThrowIfNull(logger); - public MqttRetainedMessagesManager(MqttServerEventContainer eventContainer, IMqttNetLogger logger) + _logger = logger.WithSource(nameof(MqttRetainedMessagesManager)); + } + + public async Task Start() + { + try { - _eventContainer = eventContainer ?? throw new ArgumentNullException(nameof(eventContainer)); + var eventArgs = new LoadingRetainedMessagesEventArgs(); + await _eventContainer.LoadingRetainedMessagesEvent.InvokeAsync(eventArgs).ConfigureAwait(false); - ArgumentNullException.ThrowIfNull(logger); - - _logger = logger.WithSource(nameof(MqttRetainedMessagesManager)); - } - - public async Task Start() - { - try + lock (_messages) { - var eventArgs = new LoadingRetainedMessagesEventArgs(); - await _eventContainer.LoadingRetainedMessagesEvent.InvokeAsync(eventArgs).ConfigureAwait(false); + _messages.Clear(); - lock (_messages) + if (eventArgs.LoadedRetainedMessages != null) { - _messages.Clear(); - - if (eventArgs.LoadedRetainedMessages != null) + foreach (var retainedMessage in eventArgs.LoadedRetainedMessages) { - foreach (var retainedMessage in eventArgs.LoadedRetainedMessages) - { - _messages[retainedMessage.Topic] = retainedMessage; - } + _messages[retainedMessage.Topic] = retainedMessage; } } } - catch (Exception exception) - { - _logger.Error(exception, "Unhandled exception while loading retained messages."); - } } - - public async Task UpdateMessage(string clientId, MqttApplicationMessage applicationMessage) + catch (Exception exception) { - ArgumentNullException.ThrowIfNull(applicationMessage); + _logger.Error(exception, "Unhandled exception while loading retained messages."); + } + } - try + public async Task UpdateMessage(string clientId, MqttApplicationMessage applicationMessage) + { + ArgumentNullException.ThrowIfNull(applicationMessage); + + try + { + List messagesForSave = null; + var saveIsRequired = false; + + lock (_messages) { - List messagesForSave = null; - var saveIsRequired = false; + var payload = applicationMessage.Payload; + var hasPayload = payload.Length > 0; - lock (_messages) + if (!hasPayload) { - var payload = applicationMessage.Payload; - var hasPayload = payload.Length > 0; - - if (!hasPayload) + saveIsRequired = _messages.Remove(applicationMessage.Topic); + _logger.Verbose("Client '{0}' cleared retained message for topic '{1}'.", clientId, applicationMessage.Topic); + } + else + { + if (!_messages.TryGetValue(applicationMessage.Topic, out var existingMessage)) { - saveIsRequired = _messages.Remove(applicationMessage.Topic); - _logger.Verbose("Client '{0}' cleared retained message for topic '{1}'.", clientId, applicationMessage.Topic); + _messages[applicationMessage.Topic] = applicationMessage; + saveIsRequired = true; } else { - if (!_messages.TryGetValue(applicationMessage.Topic, out var existingMessage)) + if (existingMessage.QualityOfServiceLevel != applicationMessage.QualityOfServiceLevel || + !MqttMemoryHelper.SequenceEqual(existingMessage.Payload, payload)) { _messages[applicationMessage.Topic] = applicationMessage; saveIsRequired = true; } - else - { - if (existingMessage.QualityOfServiceLevel != applicationMessage.QualityOfServiceLevel || - !MqttMemoryHelper.SequenceEqual(existingMessage.Payload, payload)) - { - _messages[applicationMessage.Topic] = applicationMessage; - saveIsRequired = true; - } - } - - _logger.Verbose("Client '{0}' set retained message for topic '{1}'.", clientId, applicationMessage.Topic); } - if (saveIsRequired) - { - messagesForSave = new List(_messages.Values); - } + _logger.Verbose("Client '{0}' set retained message for topic '{1}'.", clientId, applicationMessage.Topic); } if (saveIsRequired) { - using (await _storageAccessLock.EnterAsync().ConfigureAwait(false)) - { - var eventArgs = new RetainedMessageChangedEventArgs(clientId, applicationMessage, messagesForSave); - await _eventContainer.RetainedMessageChangedEvent.InvokeAsync(eventArgs).ConfigureAwait(false); - } + messagesForSave = new List(_messages.Values); } } - catch (Exception exception) - { - _logger.Error(exception, "Unhandled exception while handling retained messages."); - } - } - public Task> GetMessages() - { - lock (_messages) + if (saveIsRequired) { - var result = new List(_messages.Values); - return Task.FromResult((IList)result); - } - } - - public Task GetMessage(string topic) - { - lock (_messages) - { - if (_messages.TryGetValue(topic, out var message)) + using (await _storageAccessLock.EnterAsync().ConfigureAwait(false)) { - return Task.FromResult(message); + var eventArgs = new RetainedMessageChangedEventArgs(clientId, applicationMessage, messagesForSave); + await _eventContainer.RetainedMessageChangedEvent.InvokeAsync(eventArgs).ConfigureAwait(false); } } + } + catch (Exception exception) + { + _logger.Error(exception, "Unhandled exception while handling retained messages."); + } + } - return Task.FromResult(null); + public Task> GetMessages() + { + lock (_messages) + { + var result = new List(_messages.Values); + return Task.FromResult((IList)result); + } + } + + public Task GetMessage(string topic) + { + lock (_messages) + { + if (_messages.TryGetValue(topic, out var message)) + { + return Task.FromResult(message); + } } - public async Task ClearMessages() - { - lock (_messages) - { - _messages.Clear(); - } + return Task.FromResult(null); + } - using (await _storageAccessLock.EnterAsync().ConfigureAwait(false)) - { - await _eventContainer.RetainedMessagesClearedEvent.InvokeAsync(EventArgs.Empty).ConfigureAwait(false); - } + public async Task ClearMessages() + { + lock (_messages) + { + _messages.Clear(); + } + + using (await _storageAccessLock.EnterAsync().ConfigureAwait(false)) + { + await _eventContainer.RetainedMessagesClearedEvent.InvokeAsync(EventArgs.Empty).ConfigureAwait(false); } } } \ No newline at end of file diff --git a/Source/MQTTnet.Server/Internal/MqttServerEventContainer.cs b/Source/MQTTnet.Server/Internal/MqttServerEventContainer.cs index 9392be6d..12aacad1 100644 --- a/Source/MQTTnet.Server/Internal/MqttServerEventContainer.cs +++ b/Source/MQTTnet.Server/Internal/MqttServerEventContainer.cs @@ -4,52 +4,51 @@ using MQTTnet.Internal; -namespace MQTTnet.Server.Internal +namespace MQTTnet.Server.Internal; + +public sealed class MqttServerEventContainer { - public sealed class MqttServerEventContainer - { - public AsyncEvent ApplicationMessageNotConsumedEvent { get; } = new AsyncEvent(); + public AsyncEvent ApplicationMessageNotConsumedEvent { get; } = new AsyncEvent(); - public AsyncEvent ClientAcknowledgedPublishPacketEvent { get; } = new AsyncEvent(); + public AsyncEvent ClientAcknowledgedPublishPacketEvent { get; } = new AsyncEvent(); - public AsyncEvent ClientConnectedEvent { get; } = new AsyncEvent(); + public AsyncEvent ClientConnectedEvent { get; } = new AsyncEvent(); - public AsyncEvent ClientDisconnectedEvent { get; } = new AsyncEvent(); + public AsyncEvent ClientDisconnectedEvent { get; } = new AsyncEvent(); - public AsyncEvent ClientSubscribedTopicEvent { get; } = new AsyncEvent(); + public AsyncEvent ClientSubscribedTopicEvent { get; } = new AsyncEvent(); - public AsyncEvent ClientUnsubscribedTopicEvent { get; } = new AsyncEvent(); + public AsyncEvent ClientUnsubscribedTopicEvent { get; } = new AsyncEvent(); - public AsyncEvent InterceptingClientEnqueueEvent { get; } = new AsyncEvent(); - - public AsyncEvent ApplicationMessageEnqueuedOrDroppedEvent { get; } = new AsyncEvent(); + public AsyncEvent InterceptingClientEnqueueEvent { get; } = new AsyncEvent(); - public AsyncEvent QueuedApplicationMessageOverwrittenEvent { get; } = new AsyncEvent(); + public AsyncEvent ApplicationMessageEnqueuedOrDroppedEvent { get; } = new AsyncEvent(); - public AsyncEvent InterceptingInboundPacketEvent { get; } = new AsyncEvent(); + public AsyncEvent QueuedApplicationMessageOverwrittenEvent { get; } = new AsyncEvent(); - public AsyncEvent InterceptingOutboundPacketEvent { get; } = new AsyncEvent(); + public AsyncEvent InterceptingInboundPacketEvent { get; } = new AsyncEvent(); - public AsyncEvent InterceptingPublishEvent { get; } = new AsyncEvent(); + public AsyncEvent InterceptingOutboundPacketEvent { get; } = new AsyncEvent(); - public AsyncEvent InterceptingSubscriptionEvent { get; } = new AsyncEvent(); + public AsyncEvent InterceptingPublishEvent { get; } = new AsyncEvent(); - public AsyncEvent InterceptingUnsubscriptionEvent { get; } = new AsyncEvent(); + public AsyncEvent InterceptingSubscriptionEvent { get; } = new AsyncEvent(); - public AsyncEvent LoadingRetainedMessagesEvent { get; } = new AsyncEvent(); + public AsyncEvent InterceptingUnsubscriptionEvent { get; } = new AsyncEvent(); - public AsyncEvent PreparingSessionEvent { get; } = new AsyncEvent(); + public AsyncEvent LoadingRetainedMessagesEvent { get; } = new AsyncEvent(); - public AsyncEvent RetainedMessageChangedEvent { get; } = new AsyncEvent(); + public AsyncEvent PreparingSessionEvent { get; } = new AsyncEvent(); - public AsyncEvent RetainedMessagesClearedEvent { get; } = new AsyncEvent(); + public AsyncEvent RetainedMessageChangedEvent { get; } = new AsyncEvent(); - public AsyncEvent SessionDeletedEvent { get; } = new AsyncEvent(); + public AsyncEvent RetainedMessagesClearedEvent { get; } = new AsyncEvent(); - public AsyncEvent StartedEvent { get; } = new AsyncEvent(); + public AsyncEvent SessionDeletedEvent { get; } = new AsyncEvent(); - public AsyncEvent StoppedEvent { get; } = new AsyncEvent(); + public AsyncEvent StartedEvent { get; } = new AsyncEvent(); - public AsyncEvent ValidatingConnectionEvent { get; } = new AsyncEvent(); - } + public AsyncEvent StoppedEvent { get; } = new AsyncEvent(); + + public AsyncEvent ValidatingConnectionEvent { get; } = new AsyncEvent(); } \ No newline at end of file diff --git a/Source/MQTTnet.Server/Internal/MqttServerKeepAliveMonitor.cs b/Source/MQTTnet.Server/Internal/MqttServerKeepAliveMonitor.cs index 69802e4e..83ed9cff 100644 --- a/Source/MQTTnet.Server/Internal/MqttServerKeepAliveMonitor.cs +++ b/Source/MQTTnet.Server/Internal/MqttServerKeepAliveMonitor.cs @@ -6,130 +6,129 @@ using MQTTnet.Diagnostics.Logger; using MQTTnet.Internal; using MQTTnet.Protocol; -namespace MQTTnet.Server.Internal +namespace MQTTnet.Server.Internal; + +public sealed class MqttServerKeepAliveMonitor { - public sealed class MqttServerKeepAliveMonitor + static readonly MqttServerClientDisconnectOptions DefaultDisconnectOptions = new MqttServerClientDisconnectOptions { - static readonly MqttServerClientDisconnectOptions DefaultDisconnectOptions = new MqttServerClientDisconnectOptions + ReasonCode = MqttDisconnectReasonCode.KeepAliveTimeout, + UserProperties = null, + ReasonString = null, + ServerReference = null + }; + + readonly MqttNetSourceLogger _logger; + readonly MqttServerOptions _options; + readonly MqttClientSessionsManager _sessionsManager; + + public MqttServerKeepAliveMonitor(MqttServerOptions options, MqttClientSessionsManager sessionsManager, IMqttNetLogger logger) + { + _options = options ?? throw new ArgumentNullException(nameof(options)); + _sessionsManager = sessionsManager ?? throw new ArgumentNullException(nameof(sessionsManager)); + + ArgumentNullException.ThrowIfNull(logger); + + _logger = logger.WithSource(nameof(MqttServerKeepAliveMonitor)); + } + + public void Start(CancellationToken cancellationToken) + { + // The keep alive monitor spawns a real new thread (LongRunning) because it does not + // support async/await. Async etc. is avoided here because the thread will usually check + // the connections every few milliseconds and thus the context changes (due to async) are + // only consuming resources. Also there is just 1 thread for the entire server which is fine at all! + Task.Factory.StartNew(_ => DoWork(cancellationToken), cancellationToken, TaskCreationOptions.LongRunning).RunInBackground(_logger); + } + + void DoWork(CancellationToken cancellationToken) + { + try { - ReasonCode = MqttDisconnectReasonCode.KeepAliveTimeout, - UserProperties = null, - ReasonString = null, - ServerReference = null - }; + _logger.Info("Starting keep alive monitor"); - readonly MqttNetSourceLogger _logger; - readonly MqttServerOptions _options; - readonly MqttClientSessionsManager _sessionsManager; - - public MqttServerKeepAliveMonitor(MqttServerOptions options, MqttClientSessionsManager sessionsManager, IMqttNetLogger logger) - { - _options = options ?? throw new ArgumentNullException(nameof(options)); - _sessionsManager = sessionsManager ?? throw new ArgumentNullException(nameof(sessionsManager)); - - ArgumentNullException.ThrowIfNull(logger); - - _logger = logger.WithSource(nameof(MqttServerKeepAliveMonitor)); - } - - public void Start(CancellationToken cancellationToken) - { - // The keep alive monitor spawns a real new thread (LongRunning) because it does not - // support async/await. Async etc. is avoided here because the thread will usually check - // the connections every few milliseconds and thus the context changes (due to async) are - // only consuming resources. Also there is just 1 thread for the entire server which is fine at all! - Task.Factory.StartNew(_ => DoWork(cancellationToken), cancellationToken, TaskCreationOptions.LongRunning).RunInBackground(_logger); - } - - void DoWork(CancellationToken cancellationToken) - { - try + while (!cancellationToken.IsCancellationRequested) { - _logger.Info("Starting keep alive monitor"); - - while (!cancellationToken.IsCancellationRequested) - { - TryProcessClients(); - Sleep(_options.KeepAliveOptions.MonitorInterval); - } - } - catch (OperationCanceledException) - { - } - catch (Exception exception) - { - _logger.Error(exception, "Unhandled exception while checking keep alive timeouts"); - } - finally - { - _logger.Verbose("Stopped checking keep alive timeout"); + TryProcessClients(); + Sleep(_options.KeepAliveOptions.MonitorInterval); } } - - static void Sleep(TimeSpan timeout) + catch (OperationCanceledException) { - try - { - Thread.Sleep(timeout); - } - catch (ThreadAbortException) - { - // The ThreadAbortException is not actively catched in this project. - // So we use a one which is similar and will be catched properly. - throw new OperationCanceledException(); - } } - - void TryProcessClient(MqttConnectedClient connection, DateTime now) + catch (Exception exception) { - try - { - if (!connection.IsRunning) - { - // The connection is already dead or just created so there is no need to check it. - return; - } - - if (connection.ConnectPacket.KeepAlivePeriod == 0) - { - // The keep alive feature is not used by the current connection. - return; - } - - // Values described here: [MQTT-3.1.2-24]. - // If the client sends 5 sec. the server will allow up to 7.5 seconds. - // If the client sends 1 sec. the server will allow up to 1.5 seconds. - var maxSecondsWithoutPacket = connection.ConnectPacket.KeepAlivePeriod * 1.5D; - - var secondsWithoutPackage = (now - connection.Statistics.LastPacketSentTimestamp).TotalSeconds; - if (secondsWithoutPackage < maxSecondsWithoutPacket) - { - // A packet was received before the timeout is affected. - return; - } - - _logger.Warning("Client '{0}': Did not receive any packet or keep alive signal", connection.Id); - - // Execute the disconnection in background so that the keep alive monitor can continue - // with checking other connections. - // We do not need to wait for the task so no await is needed. - // Also the internal state of the connection must be swapped to "Finalizing" because the - // next iteration of the keep alive timer happens. - _ = connection.StopAsync(DefaultDisconnectOptions); - } - catch (Exception exception) - { - _logger.Error(exception, "Client {0}: Unhandled exception while checking keep alive timeouts", connection.Id); - } + _logger.Error(exception, "Unhandled exception while checking keep alive timeouts"); } - - void TryProcessClients() + finally { - var now = DateTime.UtcNow; - foreach (var client in _sessionsManager.GetClients()) + _logger.Verbose("Stopped checking keep alive timeout"); + } + } + + static void Sleep(TimeSpan timeout) + { + try + { + Thread.Sleep(timeout); + } + catch (ThreadAbortException) + { + // The ThreadAbortException is not actively catched in this project. + // So we use a one which is similar and will be catched properly. + throw new OperationCanceledException(); + } + } + + void TryProcessClient(MqttConnectedClient connection, DateTime now) + { + try + { + if (!connection.IsRunning) { - TryProcessClient(client, now); + // The connection is already dead or just created so there is no need to check it. + return; } + + if (connection.ConnectPacket.KeepAlivePeriod == 0) + { + // The keep alive feature is not used by the current connection. + return; + } + + // Values described here: [MQTT-3.1.2-24]. + // If the client sends 5 sec. the server will allow up to 7.5 seconds. + // If the client sends 1 sec. the server will allow up to 1.5 seconds. + var maxSecondsWithoutPacket = connection.ConnectPacket.KeepAlivePeriod * 1.5D; + + var secondsWithoutPackage = (now - connection.Statistics.LastPacketSentTimestamp).TotalSeconds; + if (secondsWithoutPackage < maxSecondsWithoutPacket) + { + // A packet was received before the timeout is affected. + return; + } + + _logger.Warning("Client '{0}': Did not receive any packet or keep alive signal", connection.Id); + + // Execute the disconnection in background so that the keep alive monitor can continue + // with checking other connections. + // We do not need to wait for the task so no await is needed. + // Also the internal state of the connection must be swapped to "Finalizing" because the + // next iteration of the keep alive timer happens. + _ = connection.StopAsync(DefaultDisconnectOptions); + } + catch (Exception exception) + { + _logger.Error(exception, "Client {0}: Unhandled exception while checking keep alive timeouts", connection.Id); + } + } + + void TryProcessClients() + { + var now = DateTime.UtcNow; + foreach (var client in _sessionsManager.GetClients()) + { + TryProcessClient(client, now); } } } \ No newline at end of file diff --git a/Source/MQTTnet.Server/Internal/MqttSessionsStorage.cs b/Source/MQTTnet.Server/Internal/MqttSessionsStorage.cs index 2aab0b41..37d47296 100644 --- a/Source/MQTTnet.Server/Internal/MqttSessionsStorage.cs +++ b/Source/MQTTnet.Server/Internal/MqttSessionsStorage.cs @@ -4,78 +4,77 @@ using System.Collections.ObjectModel; -namespace MQTTnet.Server.Internal +namespace MQTTnet.Server.Internal; + +public sealed class MqttSessionsStorage { - public sealed class MqttSessionsStorage + readonly Dictionary _sessions = new Dictionary(4096); + + public void Clear() { - readonly Dictionary _sessions = new Dictionary(4096); + // Make sure that the sessions are also getting disposed! + // This object can be reused after disposing. + Dispose(); + } - public void Clear() + public void Dispose() + { + foreach (var sessionItem in _sessions) { - // Make sure that the sessions are also getting disposed! - // This object can be reused after disposing. - Dispose(); + sessionItem.Value.Dispose(); } - public void Dispose() + _sessions.Clear(); + } + + public IReadOnlyCollection ReadAllSessions() + { + return new ReadOnlyCollection(_sessions.Values.ToList()); + } + + public bool TryGetSession(string id, out MqttSession session) + { + ArgumentNullException.ThrowIfNull(id); + + if (!_sessions.TryGetValue(id, out session)) { - foreach (var sessionItem in _sessions) - { - sessionItem.Value.Dispose(); - } - - _sessions.Clear(); - } - - public IReadOnlyCollection ReadAllSessions() - { - return new ReadOnlyCollection(_sessions.Values.ToList()); - } - - public bool TryGetSession(string id, out MqttSession session) - { - ArgumentNullException.ThrowIfNull(id); - - if (!_sessions.TryGetValue(id, out session)) - { - return false; - } - - // If the Session Expiry Interval is 0xFFFFFFFF (UINT_MAX), the Session does not expire. - if (session.ExpiryInterval > 0 && session.ExpiryInterval != uint.MaxValue) - { - if (session.DisconnectedTimestamp.HasValue) - { - if (DateTime.UtcNow > session.DisconnectedTimestamp.Value.AddSeconds(session.ExpiryInterval)) - { - TryRemoveSession(id, out var oldSession); - oldSession?.Dispose(); - return false; - } - } - } - - return true; - } - - public bool TryRemoveSession(string id, out MqttSession session) - { - ArgumentNullException.ThrowIfNull(id); - - if (_sessions.TryGetValue(id, out session)) - { - _sessions.Remove(id); - return true; - } - return false; } - public void UpdateSession(string id, MqttSession session) + // If the Session Expiry Interval is 0xFFFFFFFF (UINT_MAX), the Session does not expire. + if (session.ExpiryInterval > 0 && session.ExpiryInterval != uint.MaxValue) { - ArgumentNullException.ThrowIfNull(id); - - _sessions[id] = session; + if (session.DisconnectedTimestamp.HasValue) + { + if (DateTime.UtcNow > session.DisconnectedTimestamp.Value.AddSeconds(session.ExpiryInterval)) + { + TryRemoveSession(id, out var oldSession); + oldSession?.Dispose(); + return false; + } + } } + + return true; + } + + public bool TryRemoveSession(string id, out MqttSession session) + { + ArgumentNullException.ThrowIfNull(id); + + if (_sessions.TryGetValue(id, out session)) + { + _sessions.Remove(id); + return true; + } + + return false; + } + + public void UpdateSession(string id, MqttSession session) + { + ArgumentNullException.ThrowIfNull(id); + + _sessions[id] = session; } } \ No newline at end of file diff --git a/Source/MQTTnet.Server/Internal/MqttSubscription.cs b/Source/MQTTnet.Server/Internal/MqttSubscription.cs index 95b18cab..d329359d 100644 --- a/Source/MQTTnet.Server/Internal/MqttSubscription.cs +++ b/Source/MQTTnet.Server/Internal/MqttSubscription.cs @@ -4,47 +4,46 @@ using MQTTnet.Protocol; -namespace MQTTnet.Server.Internal +namespace MQTTnet.Server.Internal; + +public sealed class MqttSubscription { - public sealed class MqttSubscription + public MqttSubscription( + string topic, + bool noLocal, + MqttRetainHandling retainHandling, + bool retainAsPublished, + MqttQualityOfServiceLevel qualityOfServiceLevel, + uint identifier) { - public MqttSubscription( - string topic, - bool noLocal, - MqttRetainHandling retainHandling, - bool retainAsPublished, - MqttQualityOfServiceLevel qualityOfServiceLevel, - uint identifier) - { - Topic = topic; - NoLocal = noLocal; - RetainHandling = retainHandling; - RetainAsPublished = retainAsPublished; - GrantedQualityOfServiceLevel = qualityOfServiceLevel; - Identifier = identifier; + Topic = topic; + NoLocal = noLocal; + RetainHandling = retainHandling; + RetainAsPublished = retainAsPublished; + GrantedQualityOfServiceLevel = qualityOfServiceLevel; + Identifier = identifier; - MqttTopicHash.Calculate(Topic, out var hash, out var hashMask, out var hasWildcard); - TopicHash = hash; - TopicHashMask = hashMask; - TopicHasWildcard = hasWildcard; - } - - public MqttQualityOfServiceLevel GrantedQualityOfServiceLevel { get; } - - public uint Identifier { get; } - - public bool NoLocal { get; } - - public bool RetainAsPublished { get; } - - public MqttRetainHandling RetainHandling { get; } - - public string Topic { get; } - - public ulong TopicHash { get; } - - public ulong TopicHashMask { get; } - - public bool TopicHasWildcard { get; } + MqttTopicHash.Calculate(Topic, out var hash, out var hashMask, out var hasWildcard); + TopicHash = hash; + TopicHashMask = hashMask; + TopicHasWildcard = hasWildcard; } + + public MqttQualityOfServiceLevel GrantedQualityOfServiceLevel { get; } + + public uint Identifier { get; } + + public bool NoLocal { get; } + + public bool RetainAsPublished { get; } + + public MqttRetainHandling RetainHandling { get; } + + public string Topic { get; } + + public ulong TopicHash { get; } + + public ulong TopicHashMask { get; } + + public bool TopicHasWildcard { get; } } \ No newline at end of file diff --git a/Source/MQTTnet.Server/Internal/MqttTopicHash.cs b/Source/MQTTnet.Server/Internal/MqttTopicHash.cs index 0e23a9d8..90bff596 100644 --- a/Source/MQTTnet.Server/Internal/MqttTopicHash.cs +++ b/Source/MQTTnet.Server/Internal/MqttTopicHash.cs @@ -2,267 +2,266 @@ // The .NET Foundation licenses this file to you under the MIT license. // See the LICENSE file in the project root for more information. -namespace MQTTnet.Server.Internal +namespace MQTTnet.Server.Internal; + +/* + * The MqttSubscription object stores subscription parameters and calculates + * topic hashes. + * + * Use of Topic Hashes to improve message processing performance + * ============================================================= + * + * Motivation + * ----------- + * In a typical use case for MQTT there may be many publishers (sensors or + * other devices in the field) and few subscribers (monitoring all or many topics). + * Each publisher may have one or more topic(s) to publish and therefore both, the + * number of publishers and the number of topics may be large. + * + * Maintaining subscribers in a separate container + * ----------------------------------------------- + * Instead of placing all sessions into a single _sessions container, subscribers + * are added into another _subscriberSessions container (if a client is a + * subscriber and a publisher then the client is present in both containers). The + * cost is some additional bookkeeping work upon subscription where each client + * session needs to maintain a list of subscribed topics. + * + * When the client subscribes to the first topic, then the session manager adds + * the client to the _subscriberSessions container, and when the client + * unsubscribes from the last topic then the session manager removes the client + * from the container. Now, when an application message arrives, only the list of + * subscribers need processing instead of looping through potentially thousands of + * publishers. + * + * Improving subscriber topic lookup + * --------------------------------- + * For each subscriber, it needs to be determined whether an application message + * matches any topic the subscriber has subscribed to. There may only be few + * subscribers but there may be many subscribed topics, including wildcard topics. + * + * The implemented approach uses a topic hash and a hash mask calculated on the + * subscribed topic and the published topic (the application message topic) to + * find candidates for a match, with the existing match logic evaluating a reduced + * number of candidates. + * + * For each subscription, the topic hash and a hash mask is stored with the + * subscription, and for each application message received, the hash is calculated + * for the published topic before attempting to find matching subscriptions. The + * hash calculation itself is simple and does not have a large performance impact. + * + * We'll first explain how topic hashes and hash masks are constructed and then how + * they are used. + * + * Topic hash + * ---------- + * Topic hashes are stored as 64-bit numbers. Each byte within the 64-bit number + * relates to one MQTT topic level. A checksum is calculated for each topic level + * by iterating over the characters within the topic level (cast to byte) and the + * result is stored into the corresponding byte of the 64-bit number. If a topic + * level contains a wildcard character, then 0x00 is stored instead of the + * checksum. + * + * If there are less than 8 levels then the rest of the 64-bit number is filled + * with 0xff. If there are more than 8 levels then topics where the first 8 MQTT + * topic levels are identical will have the same hash value. + * + * This is the topic hash for the MQTT topic below: 0x655D4AF1FFFFFF + * + * client1/building1/level1/sensor1 (empty) (empty) (empty) (empty) + * \_____/ \_______/ \____/ \_____/ \_____/ \_____/ \_____/ \_____/ + * | | | | | | | | + * 0x65 0x5D 0x4A 0xF1 0xFF 0xFF 0xFF 0xFF + * + * This is the topic hash for an MQTT topic containing a wildcard: 0x655D00F1FFFFFF + * + * client1/building1/ + /sensor1 (empty) (empty) (empty) (empty) + * \_____/ \_______/ \_/ \_____/ \_____/ \_____/ \_____/ \_____/ + * | | | | | | | | + * 0x65 0x5D 0 0xF1 0xFF 0xFF 0xFF 0xFF + * + * For topics that contain the multi level wildcard # at the end, the topic hash + * is filled with 0x00: 0x65004A00000000 + * + * client1/ + /level1/ # (empty) (empty) (empty) (empty) + * \_____/ \_/ \____/ \_/ \_____/ \_____/ \_____/ \_____/ + * | | | | | | | | + * 0x65 0 0x4A 0 0 0 0 0 + * + * + * Topic hash mask + * --------------- + * The hash mask simply contains 0xFF for non-wildcard topic levels and 0x00 for + * wildcard topic levels. Here are the topic hash masks for the examples above. + * + * client1/building1/level1/sensor1 (empty) (empty) (empty) (empty) + * \_____/ \_______/ \____/ \_____/ \_____/ \_____/ \_____/ \_____/ + * | | | | | | | | + * 0xFF 0xFF 0xFF 0xFF 0xFF 0xFF 0xFF 0xFF + * + * client1/building1/ + /sensor1 (empty) (empty) (empty) (empty) + * \_____/ \_______/ \_/ \_____/ \_____/ \_____/ \_____/ \_____/ + * | | | | | | | | + * 0xFF 0xFF 0 0xFF 0xFF 0xFF 0xFF 0xFF + * + * client1/ + /level1/ # (empty) (empty) (empty) (empty) + * \_____/ \_/ \____/ \_/ \_____/ \_____/ \_____/ \_____/ + * | | | | | | | | + * 0xFF 0 0xFF 0 0 0 0 0 + * + * + * Topic hash and hash mask properties + * ----------------------------------- + * The following properties of topic hashes and hash masks can be exploited to + * find potentially matching subscribed topics for a given published topic. + * + * (1) If a subscribed topic does not contain wildcards then the topic hash of the + * subscribed topic must be equal to the topic hash of the published topic, + * otherwise the subscribed topic cannot be a candidate for a match. + * + * (2) If a subscribed topic contains wildcards then the hash of the published + * topic masked with the subscribed topic's hash mask must be equal to the hash of + * the subscribed topic. I.e. a subscribed topic is a candidate for a match if: + * (publishedTopicHash & subscribedTopicHashMask) == subscribedTopicHash + * + * (3) If a subscribed topic contains wildcards then any potentially matching + * published topic must have a hash value that is greater than or equal to the + * hash value of the subscribed topic (because the subscribed topic contains + * zeroes in wildcard positions). + * + * Match finding + * ------------- + * The subscription manager maintains two separate dictionaries to assist finding + * matches using topic hashes: a _noWildcardSubscriptionsByTopicHash dictionary + * containing all subscriptions that do not have wildcards, and a + * _wildcardSubscriptionsByTopicHash dictionary containing subscriptions with + * wildcards. + * + * For subscriptions without wildcards, all potential candidates for a match are + * obtained by a single look-up (exploiting point 1 above). + * + * For subscriptions with wildcards, the subscription manager loops through the + * wildcard subscriptions and selects candidates that satisfy condition + * (publishedTopicHash & subscribedTopicMask) == subscribedTopicHash (point 2). + * The loop could exit early if wildcard subscriptions were stored into a sorted + * dictionary (utilizing point 3), but, after testing, there does not seem to be + * any real benefit doing so. + * + * Other considerations + * -------------------- + * Characters in the topic string are cast to byte and any additional bytes in a + * multi-byte character are disregarded. Best guess is that this does not impact + * performance in practice. + * + * Instead of one-byte checksums per topic level, one-word checksums per topic + * level could be used. If most topics contained four levels or less then hash + * buckets would be shallower. + * + * For very large numbers of topics, performing a parallel search may help further. + * + * To also handle a larger number of subscribers, it may be beneficial to maintain + * a subscribers-by-subscription-topic dictionary. + */ +public static class MqttTopicHash { - /* - * The MqttSubscription object stores subscription parameters and calculates - * topic hashes. - * - * Use of Topic Hashes to improve message processing performance - * ============================================================= - * - * Motivation - * ----------- - * In a typical use case for MQTT there may be many publishers (sensors or - * other devices in the field) and few subscribers (monitoring all or many topics). - * Each publisher may have one or more topic(s) to publish and therefore both, the - * number of publishers and the number of topics may be large. - * - * Maintaining subscribers in a separate container - * ----------------------------------------------- - * Instead of placing all sessions into a single _sessions container, subscribers - * are added into another _subscriberSessions container (if a client is a - * subscriber and a publisher then the client is present in both containers). The - * cost is some additional bookkeeping work upon subscription where each client - * session needs to maintain a list of subscribed topics. - * - * When the client subscribes to the first topic, then the session manager adds - * the client to the _subscriberSessions container, and when the client - * unsubscribes from the last topic then the session manager removes the client - * from the container. Now, when an application message arrives, only the list of - * subscribers need processing instead of looping through potentially thousands of - * publishers. - * - * Improving subscriber topic lookup - * --------------------------------- - * For each subscriber, it needs to be determined whether an application message - * matches any topic the subscriber has subscribed to. There may only be few - * subscribers but there may be many subscribed topics, including wildcard topics. - * - * The implemented approach uses a topic hash and a hash mask calculated on the - * subscribed topic and the published topic (the application message topic) to - * find candidates for a match, with the existing match logic evaluating a reduced - * number of candidates. - * - * For each subscription, the topic hash and a hash mask is stored with the - * subscription, and for each application message received, the hash is calculated - * for the published topic before attempting to find matching subscriptions. The - * hash calculation itself is simple and does not have a large performance impact. - * - * We'll first explain how topic hashes and hash masks are constructed and then how - * they are used. - * - * Topic hash - * ---------- - * Topic hashes are stored as 64-bit numbers. Each byte within the 64-bit number - * relates to one MQTT topic level. A checksum is calculated for each topic level - * by iterating over the characters within the topic level (cast to byte) and the - * result is stored into the corresponding byte of the 64-bit number. If a topic - * level contains a wildcard character, then 0x00 is stored instead of the - * checksum. - * - * If there are less than 8 levels then the rest of the 64-bit number is filled - * with 0xff. If there are more than 8 levels then topics where the first 8 MQTT - * topic levels are identical will have the same hash value. - * - * This is the topic hash for the MQTT topic below: 0x655D4AF1FFFFFF - * - * client1/building1/level1/sensor1 (empty) (empty) (empty) (empty) - * \_____/ \_______/ \____/ \_____/ \_____/ \_____/ \_____/ \_____/ - * | | | | | | | | - * 0x65 0x5D 0x4A 0xF1 0xFF 0xFF 0xFF 0xFF - * - * This is the topic hash for an MQTT topic containing a wildcard: 0x655D00F1FFFFFF - * - * client1/building1/ + /sensor1 (empty) (empty) (empty) (empty) - * \_____/ \_______/ \_/ \_____/ \_____/ \_____/ \_____/ \_____/ - * | | | | | | | | - * 0x65 0x5D 0 0xF1 0xFF 0xFF 0xFF 0xFF - * - * For topics that contain the multi level wildcard # at the end, the topic hash - * is filled with 0x00: 0x65004A00000000 - * - * client1/ + /level1/ # (empty) (empty) (empty) (empty) - * \_____/ \_/ \____/ \_/ \_____/ \_____/ \_____/ \_____/ - * | | | | | | | | - * 0x65 0 0x4A 0 0 0 0 0 - * - * - * Topic hash mask - * --------------- - * The hash mask simply contains 0xFF for non-wildcard topic levels and 0x00 for - * wildcard topic levels. Here are the topic hash masks for the examples above. - * - * client1/building1/level1/sensor1 (empty) (empty) (empty) (empty) - * \_____/ \_______/ \____/ \_____/ \_____/ \_____/ \_____/ \_____/ - * | | | | | | | | - * 0xFF 0xFF 0xFF 0xFF 0xFF 0xFF 0xFF 0xFF - * - * client1/building1/ + /sensor1 (empty) (empty) (empty) (empty) - * \_____/ \_______/ \_/ \_____/ \_____/ \_____/ \_____/ \_____/ - * | | | | | | | | - * 0xFF 0xFF 0 0xFF 0xFF 0xFF 0xFF 0xFF - * - * client1/ + /level1/ # (empty) (empty) (empty) (empty) - * \_____/ \_/ \____/ \_/ \_____/ \_____/ \_____/ \_____/ - * | | | | | | | | - * 0xFF 0 0xFF 0 0 0 0 0 - * - * - * Topic hash and hash mask properties - * ----------------------------------- - * The following properties of topic hashes and hash masks can be exploited to - * find potentially matching subscribed topics for a given published topic. - * - * (1) If a subscribed topic does not contain wildcards then the topic hash of the - * subscribed topic must be equal to the topic hash of the published topic, - * otherwise the subscribed topic cannot be a candidate for a match. - * - * (2) If a subscribed topic contains wildcards then the hash of the published - * topic masked with the subscribed topic's hash mask must be equal to the hash of - * the subscribed topic. I.e. a subscribed topic is a candidate for a match if: - * (publishedTopicHash & subscribedTopicHashMask) == subscribedTopicHash - * - * (3) If a subscribed topic contains wildcards then any potentially matching - * published topic must have a hash value that is greater than or equal to the - * hash value of the subscribed topic (because the subscribed topic contains - * zeroes in wildcard positions). - * - * Match finding - * ------------- - * The subscription manager maintains two separate dictionaries to assist finding - * matches using topic hashes: a _noWildcardSubscriptionsByTopicHash dictionary - * containing all subscriptions that do not have wildcards, and a - * _wildcardSubscriptionsByTopicHash dictionary containing subscriptions with - * wildcards. - * - * For subscriptions without wildcards, all potential candidates for a match are - * obtained by a single look-up (exploiting point 1 above). - * - * For subscriptions with wildcards, the subscription manager loops through the - * wildcard subscriptions and selects candidates that satisfy condition - * (publishedTopicHash & subscribedTopicMask) == subscribedTopicHash (point 2). - * The loop could exit early if wildcard subscriptions were stored into a sorted - * dictionary (utilizing point 3), but, after testing, there does not seem to be - * any real benefit doing so. - * - * Other considerations - * -------------------- - * Characters in the topic string are cast to byte and any additional bytes in a - * multi-byte character are disregarded. Best guess is that this does not impact - * performance in practice. - * - * Instead of one-byte checksums per topic level, one-word checksums per topic - * level could be used. If most topics contained four levels or less then hash - * buckets would be shallower. - * - * For very large numbers of topics, performing a parallel search may help further. - * - * To also handle a larger number of subscribers, it may be beneficial to maintain - * a subscribers-by-subscription-topic dictionary. - */ - public static class MqttTopicHash + public static void Calculate(string topic, out ulong resultHash, out ulong resultHashMask, out bool resultHasWildcard) { - public static void Calculate(string topic, out ulong resultHash, out ulong resultHashMask, out bool resultHasWildcard) + // calculate topic hash + ulong hash = 0; + ulong hashMaskInverted = 0; + ulong levelBitMask = 0; + ulong fillLevelBitMask = 0; + var hasWildcard = false; + byte checkSum = 0; + var level = 0; + + var i = 0; + while (i < topic.Length) { - // calculate topic hash - ulong hash = 0; - ulong hashMaskInverted = 0; - ulong levelBitMask = 0; - ulong fillLevelBitMask = 0; - var hasWildcard = false; - byte checkSum = 0; - var level = 0; - - var i = 0; - while (i < topic.Length) - { - var c = topic[i]; - if (c == MqttTopicFilterComparer.LevelSeparator) - { - // done with this level - hash <<= 8; - hash |= checkSum; - hashMaskInverted <<= 8; - hashMaskInverted |= levelBitMask; - checkSum = 0; - levelBitMask = 0; - ++level; - if (level >= 8) - { - break; - } - } - else if (c == MqttTopicFilterComparer.SingleLevelWildcard) - { - levelBitMask = 0xff; - hasWildcard = true; - } - else if (c == MqttTopicFilterComparer.MultiLevelWildcard) - { - // checksum is zero for a valid topic - levelBitMask = 0xff; - // fill rest with this fillLevelBitMask - fillLevelBitMask = 0xff; - hasWildcard = true; - break; - } - else - { - // The checksum should be designed to reduce the hash bucket depth for the expected - // fairly regularly named MQTT topics that don't differ much, - // i.e. "room1/sensor1" - // "room1/sensor2" - // "room1/sensor3" - // etc. - if ((c & 1) == 0) - { - checkSum += (byte)c; - } - else - { - checkSum ^= (byte)(c >> 1); - } - } - - ++i; - } - - // Shift hash left and leave zeroes to fill ulong - if (level < 8) + var c = topic[i]; + if (c == MqttTopicFilterComparer.LevelSeparator) { + // done with this level hash <<= 8; hash |= checkSum; hashMaskInverted <<= 8; hashMaskInverted |= levelBitMask; + checkSum = 0; + levelBitMask = 0; ++level; - while (level < 8) + if (level >= 8) { - hash <<= 8; - hashMaskInverted <<= 8; - hashMaskInverted |= fillLevelBitMask; - ++level; + break; } } - - if (!hasWildcard) + else if (c == MqttTopicFilterComparer.SingleLevelWildcard) { - while (i < topic.Length) + levelBitMask = 0xff; + hasWildcard = true; + } + else if (c == MqttTopicFilterComparer.MultiLevelWildcard) + { + // checksum is zero for a valid topic + levelBitMask = 0xff; + // fill rest with this fillLevelBitMask + fillLevelBitMask = 0xff; + hasWildcard = true; + break; + } + else + { + // The checksum should be designed to reduce the hash bucket depth for the expected + // fairly regularly named MQTT topics that don't differ much, + // i.e. "room1/sensor1" + // "room1/sensor2" + // "room1/sensor3" + // etc. + if ((c & 1) == 0) { - var c = topic[i]; - if (c == MqttTopicFilterComparer.SingleLevelWildcard || c == MqttTopicFilterComparer.MultiLevelWildcard) - { - hasWildcard = true; - break; - } - - ++i; + checkSum += (byte)c; + } + else + { + checkSum ^= (byte)(c >> 1); } } - resultHash = hash; - resultHashMask = ~hashMaskInverted; - resultHasWildcard = hasWildcard; + ++i; } + + // Shift hash left and leave zeroes to fill ulong + if (level < 8) + { + hash <<= 8; + hash |= checkSum; + hashMaskInverted <<= 8; + hashMaskInverted |= levelBitMask; + ++level; + while (level < 8) + { + hash <<= 8; + hashMaskInverted <<= 8; + hashMaskInverted |= fillLevelBitMask; + ++level; + } + } + + if (!hasWildcard) + { + while (i < topic.Length) + { + var c = topic[i]; + if (c == MqttTopicFilterComparer.SingleLevelWildcard || c == MqttTopicFilterComparer.MultiLevelWildcard) + { + hasWildcard = true; + break; + } + + ++i; + } + } + + resultHash = hash; + resultHashMask = ~hashMaskInverted; + resultHasWildcard = hasWildcard; } } \ No newline at end of file diff --git a/Source/MQTTnet.Server/Internal/SubscribeResult.cs b/Source/MQTTnet.Server/Internal/SubscribeResult.cs index c4d20aa1..b2bd4830 100644 --- a/Source/MQTTnet.Server/Internal/SubscribeResult.cs +++ b/Source/MQTTnet.Server/Internal/SubscribeResult.cs @@ -5,23 +5,22 @@ using MQTTnet.Packets; using MQTTnet.Protocol; -namespace MQTTnet.Server.Internal +namespace MQTTnet.Server.Internal; + +public sealed class SubscribeResult { - public sealed class SubscribeResult + public SubscribeResult(int topicsCount) { - public SubscribeResult(int topicsCount) - { - ReasonCodes = new List(topicsCount); - } - - public bool CloseConnection { get; set; } - - public List ReasonCodes { get; set; } - - public string ReasonString { get; set; } - - public List RetainedMessages { get; set; } - - public List UserProperties { get; set; } + ReasonCodes = new List(topicsCount); } + + public bool CloseConnection { get; set; } + + public List ReasonCodes { get; set; } + + public string ReasonString { get; set; } + + public List RetainedMessages { get; set; } + + public List UserProperties { get; set; } } \ No newline at end of file diff --git a/Source/MQTTnet.Server/Internal/TopicHashMaskSubscriptions.cs b/Source/MQTTnet.Server/Internal/TopicHashMaskSubscriptions.cs index 38f75e50..0d6371f7 100644 --- a/Source/MQTTnet.Server/Internal/TopicHashMaskSubscriptions.cs +++ b/Source/MQTTnet.Server/Internal/TopicHashMaskSubscriptions.cs @@ -1,31 +1,30 @@ -namespace MQTTnet.Server.Internal +namespace MQTTnet.Server.Internal; + +/// +/// Helper class that stores subscriptions by their topic hash mask. +/// +public sealed class TopicHashMaskSubscriptions { - /// - /// Helper class that stores subscriptions by their topic hash mask. - /// - public sealed class TopicHashMaskSubscriptions + public Dictionary> SubscriptionsByHashMask { get; } = new Dictionary>(); + + public void AddSubscription(MqttSubscription subscription) { - public Dictionary> SubscriptionsByHashMask { get; } = new Dictionary>(); - - public void AddSubscription(MqttSubscription subscription) + if (!SubscriptionsByHashMask.TryGetValue(subscription.TopicHashMask, out var subscriptions)) { - if (!SubscriptionsByHashMask.TryGetValue(subscription.TopicHashMask, out var subscriptions)) - { - subscriptions = new HashSet(); - SubscriptionsByHashMask.Add(subscription.TopicHashMask, subscriptions); - } - subscriptions.Add(subscription); + subscriptions = new HashSet(); + SubscriptionsByHashMask.Add(subscription.TopicHashMask, subscriptions); } + subscriptions.Add(subscription); + } - public void RemoveSubscription(MqttSubscription subscription) + public void RemoveSubscription(MqttSubscription subscription) + { + if (SubscriptionsByHashMask.TryGetValue(subscription.TopicHashMask, out var subscriptions)) { - if (SubscriptionsByHashMask.TryGetValue(subscription.TopicHashMask, out var subscriptions)) + subscriptions.Remove(subscription); + if (subscriptions.Count == 0) { - subscriptions.Remove(subscription); - if (subscriptions.Count == 0) - { - SubscriptionsByHashMask.Remove(subscription.TopicHashMask); - } + SubscriptionsByHashMask.Remove(subscription.TopicHashMask); } } } diff --git a/Source/MQTTnet.Server/Internal/UnsubscribeResult.cs b/Source/MQTTnet.Server/Internal/UnsubscribeResult.cs index 1c1b34eb..f9156880 100644 --- a/Source/MQTTnet.Server/Internal/UnsubscribeResult.cs +++ b/Source/MQTTnet.Server/Internal/UnsubscribeResult.cs @@ -5,14 +5,13 @@ using MQTTnet.Packets; using MQTTnet.Protocol; -namespace MQTTnet.Server.Internal +namespace MQTTnet.Server.Internal; + +public sealed class UnsubscribeResult { - public sealed class UnsubscribeResult - { - public List ReasonCodes { get; } = new List(128); + public List ReasonCodes { get; } = new List(128); - public bool CloseConnection { get; set; } + public bool CloseConnection { get; set; } - public List UserProperties { get; set; } - } + public List UserProperties { get; set; } } \ No newline at end of file diff --git a/Source/MQTTnet.Server/MqttServerExtensions.cs b/Source/MQTTnet.Server/MqttServerExtensions.cs index c25c8945..747a9dfb 100644 --- a/Source/MQTTnet.Server/MqttServerExtensions.cs +++ b/Source/MQTTnet.Server/MqttServerExtensions.cs @@ -29,9 +29,9 @@ public static class MqttServerExtensions ArgumentNullException.ThrowIfNull(topic); var payloadBuffer = EmptyBuffer.Array; - if (payload is string stringPayload) + if (payload != null) { - payloadBuffer = Encoding.UTF8.GetBytes(stringPayload); + payloadBuffer = Encoding.UTF8.GetBytes(payload); } return server.InjectApplicationMessage( diff --git a/Source/MQTTnet.Server/MqttServerFactory.cs b/Source/MQTTnet.Server/MqttServerFactory.cs index 36e64059..9971c5a4 100644 --- a/Source/MQTTnet.Server/MqttServerFactory.cs +++ b/Source/MQTTnet.Server/MqttServerFactory.cs @@ -21,10 +21,7 @@ public sealed class MqttServerFactory public IMqttNetLogger DefaultLogger { get; } - public IList> DefaultServerAdapters { get; } = new List> - { - factory => new MqttTcpServerAdapter() - }; + public IList> DefaultServerAdapters { get; } = [_ => new MqttTcpServerAdapter()]; public IDictionary Properties { get; } = new Dictionary(); diff --git a/Source/MQTTnet.Server/Options/IMqttServerCertificateCredentials.cs b/Source/MQTTnet.Server/Options/IMqttServerCertificateCredentials.cs index d0b75631..0910742d 100644 --- a/Source/MQTTnet.Server/Options/IMqttServerCertificateCredentials.cs +++ b/Source/MQTTnet.Server/Options/IMqttServerCertificateCredentials.cs @@ -2,10 +2,9 @@ // The .NET Foundation licenses this file to you under the MIT license. // See the LICENSE file in the project root for more information. -namespace MQTTnet.Server +namespace MQTTnet.Server; + +public interface IMqttServerCertificateCredentials { - public interface IMqttServerCertificateCredentials - { - string Password { get; } - } -} + string Password { get; } +} \ No newline at end of file diff --git a/Source/MQTTnet.Server/Options/MqttPendingMessagesOverflowStrategy.cs b/Source/MQTTnet.Server/Options/MqttPendingMessagesOverflowStrategy.cs index ebd4f987..0ffdb046 100644 --- a/Source/MQTTnet.Server/Options/MqttPendingMessagesOverflowStrategy.cs +++ b/Source/MQTTnet.Server/Options/MqttPendingMessagesOverflowStrategy.cs @@ -2,12 +2,11 @@ // The .NET Foundation licenses this file to you under the MIT license. // See the LICENSE file in the project root for more information. -namespace MQTTnet.Server -{ - public enum MqttPendingMessagesOverflowStrategy - { - DropOldestQueuedMessage, +namespace MQTTnet.Server; - DropNewMessage - } -} +public enum MqttPendingMessagesOverflowStrategy +{ + DropOldestQueuedMessage, + + DropNewMessage +} \ No newline at end of file diff --git a/Source/MQTTnet.Server/Options/MqttServerCertificateCredentials.cs b/Source/MQTTnet.Server/Options/MqttServerCertificateCredentials.cs index f0ce0013..f5770cc9 100644 --- a/Source/MQTTnet.Server/Options/MqttServerCertificateCredentials.cs +++ b/Source/MQTTnet.Server/Options/MqttServerCertificateCredentials.cs @@ -2,10 +2,9 @@ // The .NET Foundation licenses this file to you under the MIT license. // See the LICENSE file in the project root for more information. -namespace MQTTnet.Server +namespace MQTTnet.Server; + +public class MqttServerCertificateCredentials : IMqttServerCertificateCredentials { - public class MqttServerCertificateCredentials : IMqttServerCertificateCredentials - { - public string Password { get; set; } - } -} + public string Password { get; set; } +} \ No newline at end of file diff --git a/Source/MQTTnet.Server/Options/MqttServerKeepAliveOptions.cs b/Source/MQTTnet.Server/Options/MqttServerKeepAliveOptions.cs index 58f8a673..e05e1a24 100644 --- a/Source/MQTTnet.Server/Options/MqttServerKeepAliveOptions.cs +++ b/Source/MQTTnet.Server/Options/MqttServerKeepAliveOptions.cs @@ -2,20 +2,17 @@ // The .NET Foundation licenses this file to you under the MIT license. // See the LICENSE file in the project root for more information. -using System; +namespace MQTTnet.Server; -namespace MQTTnet.Server +public sealed class MqttServerKeepAliveOptions { - public sealed class MqttServerKeepAliveOptions - { - /// - /// When this mode is enabled the MQTT server will not close a connection when the - /// client is currently sending a (large) payload. This may lead to "dead" connections - /// When this mode is disabled the MQTT server will disconnect a client when the keep - /// alive timeout is reached even if the client is currently sending a (large) payload. - /// - public bool DisconnectClientWhenReadingPayload { get; set; } + /// + /// When this mode is enabled the MQTT server will not close a connection when the + /// client is currently sending a (large) payload. This may lead to "dead" connections + /// When this mode is disabled the MQTT server will disconnect a client when the keep + /// alive timeout is reached even if the client is currently sending a (large) payload. + /// + public bool DisconnectClientWhenReadingPayload { get; set; } - public TimeSpan MonitorInterval { get; set; } = TimeSpan.FromMilliseconds(500); - } + public TimeSpan MonitorInterval { get; set; } = TimeSpan.FromMilliseconds(500); } \ No newline at end of file diff --git a/Source/MQTTnet.Server/Options/MqttServerOptionsBuilder.cs b/Source/MQTTnet.Server/Options/MqttServerOptionsBuilder.cs index c238ea01..52035890 100644 --- a/Source/MQTTnet.Server/Options/MqttServerOptionsBuilder.cs +++ b/Source/MQTTnet.Server/Options/MqttServerOptionsBuilder.cs @@ -2,7 +2,6 @@ // The .NET Foundation licenses this file to you under the MIT license. // See the LICENSE file in the project root for more information. -using System; using System.Net; using System.Net.Security; using System.Security.Authentication; @@ -10,207 +9,206 @@ using MQTTnet.Certificates; using System.Security.Cryptography.X509Certificates; // ReSharper disable UnusedMember.Global -namespace MQTTnet.Server +namespace MQTTnet.Server; + +public class MqttServerOptionsBuilder { - public class MqttServerOptionsBuilder + readonly MqttServerOptions _options = new(); + + public MqttServerOptions Build() { - readonly MqttServerOptions _options = new MqttServerOptions(); + return _options; + } - public MqttServerOptions Build() + public MqttServerOptionsBuilder WithClientCertificate(RemoteCertificateValidationCallback validationCallback = null, bool checkCertificateRevocation = false) + { + _options.TlsEndpointOptions.ClientCertificateRequired = true; + _options.TlsEndpointOptions.CheckCertificateRevocation = checkCertificateRevocation; + _options.TlsEndpointOptions.RemoteCertificateValidationCallback = validationCallback; + return this; + } + + public MqttServerOptionsBuilder WithConnectionBacklog(int value) + { + _options.DefaultEndpointOptions.ConnectionBacklog = value; + _options.TlsEndpointOptions.ConnectionBacklog = value; + return this; + } + + public MqttServerOptionsBuilder WithDefaultCommunicationTimeout(TimeSpan value) + { + _options.DefaultCommunicationTimeout = value; + return this; + } + + public MqttServerOptionsBuilder WithDefaultEndpoint() + { + _options.DefaultEndpointOptions.IsEnabled = true; + return this; + } + + public MqttServerOptionsBuilder WithDefaultEndpointBoundIPAddress(IPAddress value) + { + _options.DefaultEndpointOptions.BoundInterNetworkAddress = value ?? IPAddress.Any; + return this; + } + + public MqttServerOptionsBuilder WithDefaultEndpointBoundIPV6Address(IPAddress value) + { + _options.DefaultEndpointOptions.BoundInterNetworkV6Address = value ?? IPAddress.Any; + return this; + } + + public MqttServerOptionsBuilder WithDefaultEndpointPort(int value) + { + _options.DefaultEndpointOptions.Port = value; + return this; + } + + public MqttServerOptionsBuilder WithDefaultEndpointReuseAddress() + { + _options.DefaultEndpointOptions.ReuseAddress = true; + return this; + } + + public MqttServerOptionsBuilder WithEncryptedEndpoint() + { + _options.TlsEndpointOptions.IsEnabled = true; + return this; + } + + public MqttServerOptionsBuilder WithEncryptedEndpointBoundIPAddress(IPAddress value) + { + _options.TlsEndpointOptions.BoundInterNetworkAddress = value; + return this; + } + + public MqttServerOptionsBuilder WithEncryptedEndpointBoundIPV6Address(IPAddress value) + { + _options.TlsEndpointOptions.BoundInterNetworkV6Address = value; + return this; + } + + public MqttServerOptionsBuilder WithEncryptedEndpointPort(int value) + { + _options.TlsEndpointOptions.Port = value; + return this; + } + + public MqttServerOptionsBuilder WithEncryptionSslProtocol(SslProtocols value) + { + _options.TlsEndpointOptions.SslProtocol = value; + return this; + } + + public MqttServerOptionsBuilder WithKeepAlive() + { + _options.DefaultEndpointOptions.KeepAlive = true; + _options.TlsEndpointOptions.KeepAlive = true; + return this; + } + + public MqttServerOptionsBuilder WithMaxPendingMessagesPerClient(int value) + { + _options.MaxPendingMessagesPerClient = value; + return this; + } + + public MqttServerOptionsBuilder WithPendingMessagesOverflowStrategy(MqttPendingMessagesOverflowStrategy value) + { + _options.PendingMessagesOverflowStrategy = value; + return this; + } + + public MqttServerOptionsBuilder WithoutDefaultEndpoint() + { + _options.DefaultEndpointOptions.IsEnabled = false; + return this; + } + + public MqttServerOptionsBuilder WithoutEncryptedEndpoint() + { + _options.TlsEndpointOptions.IsEnabled = false; + return this; + } + + /// + /// Usually the MQTT packets can be send partially to improve memory allocations. + /// This is done by using multiple TCP packets or WebSocket frames etc. + /// Unfortunately not all clients do support this and will close the connection when receiving partial packets. + /// + public MqttServerOptionsBuilder WithoutPacketFragmentation() + { + _options.DefaultEndpointOptions.AllowPacketFragmentation = false; + _options.TlsEndpointOptions.AllowPacketFragmentation = false; + return this; + } + + public MqttServerOptionsBuilder WithPersistentSessions(bool value = true) + { + _options.EnablePersistentSessions = value; + return this; + } + + public MqttServerOptionsBuilder WithRemoteCertificateValidationCallback(RemoteCertificateValidationCallback value) + { + _options.TlsEndpointOptions.RemoteCertificateValidationCallback = value; + return this; + } + + public MqttServerOptionsBuilder WithTcpKeepAliveInterval(int value) + { + _options.DefaultEndpointOptions.TcpKeepAliveInterval = value; + _options.TlsEndpointOptions.TcpKeepAliveInterval = value; + return this; + } + + public MqttServerOptionsBuilder WithTcpKeepAliveRetryCount(int value) + { + _options.DefaultEndpointOptions.TcpKeepAliveRetryCount = value; + _options.TlsEndpointOptions.TcpKeepAliveRetryCount = value; + return this; + } + + public MqttServerOptionsBuilder WithTcpKeepAliveTime(int value) + { + _options.DefaultEndpointOptions.TcpKeepAliveTime = value; + _options.TlsEndpointOptions.TcpKeepAliveTime = value; + return this; + } + + public MqttServerOptionsBuilder WithTlsEndpointReuseAddress() + { + _options.TlsEndpointOptions.ReuseAddress = true; + return this; + } + + public MqttServerOptionsBuilder WithEncryptionCertificate(byte[] value, IMqttServerCertificateCredentials credentials = null) + { + ArgumentNullException.ThrowIfNull(value); + + _options.TlsEndpointOptions.CertificateProvider = new BlobCertificateProvider(value) { - return _options; - } + Password = credentials?.Password + }; - public MqttServerOptionsBuilder WithClientCertificate(RemoteCertificateValidationCallback validationCallback = null, bool checkCertificateRevocation = false) - { - _options.TlsEndpointOptions.ClientCertificateRequired = true; - _options.TlsEndpointOptions.CheckCertificateRevocation = checkCertificateRevocation; - _options.TlsEndpointOptions.RemoteCertificateValidationCallback = validationCallback; - return this; - } + return this; + } - public MqttServerOptionsBuilder WithConnectionBacklog(int value) - { - _options.DefaultEndpointOptions.ConnectionBacklog = value; - _options.TlsEndpointOptions.ConnectionBacklog = value; - return this; - } + public MqttServerOptionsBuilder WithEncryptionCertificate(X509Certificate2 certificate) + { + ArgumentNullException.ThrowIfNull(certificate); - public MqttServerOptionsBuilder WithDefaultCommunicationTimeout(TimeSpan value) - { - _options.DefaultCommunicationTimeout = value; - return this; - } + _options.TlsEndpointOptions.CertificateProvider = new X509CertificateProvider(certificate); + return this; + } - public MqttServerOptionsBuilder WithDefaultEndpoint() - { - _options.DefaultEndpointOptions.IsEnabled = true; - return this; - } + public MqttServerOptionsBuilder WithEncryptionCertificate(ICertificateProvider certificateProvider) + { + ArgumentNullException.ThrowIfNull(certificateProvider); - public MqttServerOptionsBuilder WithDefaultEndpointBoundIPAddress(IPAddress value) - { - _options.DefaultEndpointOptions.BoundInterNetworkAddress = value ?? IPAddress.Any; - return this; - } + _options.TlsEndpointOptions.CertificateProvider = certificateProvider; - public MqttServerOptionsBuilder WithDefaultEndpointBoundIPV6Address(IPAddress value) - { - _options.DefaultEndpointOptions.BoundInterNetworkV6Address = value ?? IPAddress.Any; - return this; - } - - public MqttServerOptionsBuilder WithDefaultEndpointPort(int value) - { - _options.DefaultEndpointOptions.Port = value; - return this; - } - - public MqttServerOptionsBuilder WithDefaultEndpointReuseAddress() - { - _options.DefaultEndpointOptions.ReuseAddress = true; - return this; - } - - public MqttServerOptionsBuilder WithEncryptedEndpoint() - { - _options.TlsEndpointOptions.IsEnabled = true; - return this; - } - - public MqttServerOptionsBuilder WithEncryptedEndpointBoundIPAddress(IPAddress value) - { - _options.TlsEndpointOptions.BoundInterNetworkAddress = value; - return this; - } - - public MqttServerOptionsBuilder WithEncryptedEndpointBoundIPV6Address(IPAddress value) - { - _options.TlsEndpointOptions.BoundInterNetworkV6Address = value; - return this; - } - - public MqttServerOptionsBuilder WithEncryptedEndpointPort(int value) - { - _options.TlsEndpointOptions.Port = value; - return this; - } - - public MqttServerOptionsBuilder WithEncryptionSslProtocol(SslProtocols value) - { - _options.TlsEndpointOptions.SslProtocol = value; - return this; - } - - public MqttServerOptionsBuilder WithKeepAlive() - { - _options.DefaultEndpointOptions.KeepAlive = true; - _options.TlsEndpointOptions.KeepAlive = true; - return this; - } - - public MqttServerOptionsBuilder WithMaxPendingMessagesPerClient(int value) - { - _options.MaxPendingMessagesPerClient = value; - return this; - } - - public MqttServerOptionsBuilder WithPendingMessagesOverflowStrategy(MqttPendingMessagesOverflowStrategy value) - { - _options.PendingMessagesOverflowStrategy = value; - return this; - } - - public MqttServerOptionsBuilder WithoutDefaultEndpoint() - { - _options.DefaultEndpointOptions.IsEnabled = false; - return this; - } - - public MqttServerOptionsBuilder WithoutEncryptedEndpoint() - { - _options.TlsEndpointOptions.IsEnabled = false; - return this; - } - - /// - /// Usually the MQTT packets can be send partially to improve memory allocations. - /// This is done by using multiple TCP packets or WebSocket frames etc. - /// Unfortunately not all clients do support this and will close the connection when receiving partial packets. - /// - public MqttServerOptionsBuilder WithoutPacketFragmentation() - { - _options.DefaultEndpointOptions.AllowPacketFragmentation = false; - _options.TlsEndpointOptions.AllowPacketFragmentation = false; - return this; - } - - public MqttServerOptionsBuilder WithPersistentSessions(bool value = true) - { - _options.EnablePersistentSessions = value; - return this; - } - - public MqttServerOptionsBuilder WithRemoteCertificateValidationCallback(RemoteCertificateValidationCallback value) - { - _options.TlsEndpointOptions.RemoteCertificateValidationCallback = value; - return this; - } - - public MqttServerOptionsBuilder WithTcpKeepAliveInterval(int value) - { - _options.DefaultEndpointOptions.TcpKeepAliveInterval = value; - _options.TlsEndpointOptions.TcpKeepAliveInterval = value; - return this; - } - - public MqttServerOptionsBuilder WithTcpKeepAliveRetryCount(int value) - { - _options.DefaultEndpointOptions.TcpKeepAliveRetryCount = value; - _options.TlsEndpointOptions.TcpKeepAliveRetryCount = value; - return this; - } - - public MqttServerOptionsBuilder WithTcpKeepAliveTime(int value) - { - _options.DefaultEndpointOptions.TcpKeepAliveTime = value; - _options.TlsEndpointOptions.TcpKeepAliveTime = value; - return this; - } - - public MqttServerOptionsBuilder WithTlsEndpointReuseAddress() - { - _options.TlsEndpointOptions.ReuseAddress = true; - return this; - } - - public MqttServerOptionsBuilder WithEncryptionCertificate(byte[] value, IMqttServerCertificateCredentials credentials = null) - { - ArgumentNullException.ThrowIfNull(value); - - _options.TlsEndpointOptions.CertificateProvider = new BlobCertificateProvider(value) - { - Password = credentials?.Password - }; - - return this; - } - - public MqttServerOptionsBuilder WithEncryptionCertificate(X509Certificate2 certificate) - { - ArgumentNullException.ThrowIfNull(certificate); - - _options.TlsEndpointOptions.CertificateProvider = new X509CertificateProvider(certificate); - return this; - } - - public MqttServerOptionsBuilder WithEncryptionCertificate(ICertificateProvider certificateProvider) - { - ArgumentNullException.ThrowIfNull(certificateProvider); - - _options.TlsEndpointOptions.CertificateProvider = certificateProvider; - - return this; - } + return this; } } \ No newline at end of file diff --git a/Source/MQTTnet.Server/Options/MqttServerTcpEndpointBaseOptions.cs b/Source/MQTTnet.Server/Options/MqttServerTcpEndpointBaseOptions.cs index 3567050b..bdd95315 100644 --- a/Source/MQTTnet.Server/Options/MqttServerTcpEndpointBaseOptions.cs +++ b/Source/MQTTnet.Server/Options/MqttServerTcpEndpointBaseOptions.cs @@ -5,59 +5,58 @@ using System.Net; using System.Net.Sockets; -namespace MQTTnet.Server +namespace MQTTnet.Server; + +public abstract class MqttServerTcpEndpointBaseOptions { - public abstract class MqttServerTcpEndpointBaseOptions - { - public bool IsEnabled { get; set; } + public bool IsEnabled { get; set; } - public int Port { get; set; } + public int Port { get; set; } - public int ConnectionBacklog { get; set; } = 100; + public int ConnectionBacklog { get; set; } = 100; - public bool NoDelay { get; set; } = true; + public bool NoDelay { get; set; } = true; - /// - /// Gets or sets whether the sockets keep alive feature should be used. - /// The value _null_ indicates that the OS and framework defaults should be used. - /// - public bool? KeepAlive { get; set; } + /// + /// Gets or sets whether the sockets keep alive feature should be used. + /// The value _null_ indicates that the OS and framework defaults should be used. + /// + public bool? KeepAlive { get; set; } - /// - /// Usually the MQTT packets can be send partially. This is done by using multiple TCP packets - /// or WebSocket frames etc. Unfortunately not all clients do support this feature and - /// will close the connection when receiving such packets. If such clients are connecting to this - /// server the flag must be set to _false_. - /// - public bool AllowPacketFragmentation { get; set; } = true; + /// + /// Usually the MQTT packets can be send partially. This is done by using multiple TCP packets + /// or WebSocket frames etc. Unfortunately not all clients do support this feature and + /// will close the connection when receiving such packets. If such clients are connecting to this + /// server the flag must be set to _false_. + /// + public bool AllowPacketFragmentation { get; set; } = true; - /// - /// Gets or sets the TCP keep alive interval. - /// The value _null_ indicates that the OS and framework defaults should be used. - /// - public int? TcpKeepAliveInterval { get; set; } + /// + /// Gets or sets the TCP keep alive interval. + /// The value _null_ indicates that the OS and framework defaults should be used. + /// + public int? TcpKeepAliveInterval { get; set; } - /// - /// Gets or sets the TCP keep alive retry count. - /// The value _null_ indicates that the OS and framework defaults should be used. - /// - public int? TcpKeepAliveRetryCount { get; set; } + /// + /// Gets or sets the TCP keep alive retry count. + /// The value _null_ indicates that the OS and framework defaults should be used. + /// + public int? TcpKeepAliveRetryCount { get; set; } - /// - /// Gets or sets the TCP keep alive time. - /// The value _null_ indicates that the OS and framework defaults should be used. - /// - public int? TcpKeepAliveTime { get; set; } + /// + /// Gets or sets the TCP keep alive time. + /// The value _null_ indicates that the OS and framework defaults should be used. + /// + public int? TcpKeepAliveTime { get; set; } - public LingerOption LingerState { get; set; } = new LingerOption(true, 0); + public LingerOption LingerState { get; set; } = new LingerOption(true, 0); - public IPAddress BoundInterNetworkAddress { get; set; } = IPAddress.Any; + public IPAddress BoundInterNetworkAddress { get; set; } = IPAddress.Any; - public IPAddress BoundInterNetworkV6Address { get; set; } = IPAddress.IPv6Any; + public IPAddress BoundInterNetworkV6Address { get; set; } = IPAddress.IPv6Any; - /// - /// This requires admin permissions on Linux. - /// - public bool ReuseAddress { get; set; } - } + /// + /// This requires admin permissions on Linux. + /// + public bool ReuseAddress { get; set; } } \ No newline at end of file diff --git a/Source/MQTTnet.Server/Options/MqttServerTcpEndpointOptions.cs b/Source/MQTTnet.Server/Options/MqttServerTcpEndpointOptions.cs index 0437da06..bc3c6744 100644 --- a/Source/MQTTnet.Server/Options/MqttServerTcpEndpointOptions.cs +++ b/Source/MQTTnet.Server/Options/MqttServerTcpEndpointOptions.cs @@ -2,13 +2,12 @@ // The .NET Foundation licenses this file to you under the MIT license. // See the LICENSE file in the project root for more information. -namespace MQTTnet.Server +namespace MQTTnet.Server; + +public class MqttServerTcpEndpointOptions : MqttServerTcpEndpointBaseOptions { - public class MqttServerTcpEndpointOptions : MqttServerTcpEndpointBaseOptions + public MqttServerTcpEndpointOptions() { - public MqttServerTcpEndpointOptions() - { - Port = 1883; - } + Port = 1883; } -} +} \ No newline at end of file diff --git a/Source/MQTTnet.Server/Options/MqttServerTlsTcpEndpointOptions.cs b/Source/MQTTnet.Server/Options/MqttServerTlsTcpEndpointOptions.cs index fb8a1b02..eb78f6f4 100644 --- a/Source/MQTTnet.Server/Options/MqttServerTlsTcpEndpointOptions.cs +++ b/Source/MQTTnet.Server/Options/MqttServerTlsTcpEndpointOptions.cs @@ -5,29 +5,28 @@ using System.Security.Authentication; using MQTTnet.Certificates; -namespace MQTTnet.Server +namespace MQTTnet.Server; + +public sealed class MqttServerTlsTcpEndpointOptions : MqttServerTcpEndpointBaseOptions { - public sealed class MqttServerTlsTcpEndpointOptions : MqttServerTcpEndpointBaseOptions + public MqttServerTlsTcpEndpointOptions() { - public MqttServerTlsTcpEndpointOptions() - { - Port = 8883; - } - - public System.Net.Security.RemoteCertificateValidationCallback RemoteCertificateValidationCallback { get; set; } - - public ICertificateProvider CertificateProvider { get; set; } - - public bool ClientCertificateRequired { get; set; } - - public bool CheckCertificateRevocation { get; set; } - - /// - /// The default value is SslProtocols.None, which allows the operating system to choose the best protocol to use, and to block protocols that are not secure. - /// - /// SslProtocols - public SslProtocols SslProtocol { get; set; } = SslProtocols.None; - - public System.Net.Security.CipherSuitesPolicy CipherSuitesPolicy { get; set; } + Port = 8883; } -} + + public System.Net.Security.RemoteCertificateValidationCallback RemoteCertificateValidationCallback { get; set; } + + public ICertificateProvider CertificateProvider { get; set; } + + public bool ClientCertificateRequired { get; set; } + + public bool CheckCertificateRevocation { get; set; } + + /// + /// The default value is SslProtocols.None, which allows the operating system to choose the best protocol to use, and to block protocols that are not secure. + /// + /// SslProtocols + public SslProtocols SslProtocol { get; set; } = SslProtocols.None; + + public System.Net.Security.CipherSuitesPolicy CipherSuitesPolicy { get; set; } +} \ No newline at end of file diff --git a/Source/MQTTnet.Server/Stopping/MqttServerStopOptions.cs b/Source/MQTTnet.Server/Stopping/MqttServerStopOptions.cs index 5bd97b93..1ca28a53 100644 --- a/Source/MQTTnet.Server/Stopping/MqttServerStopOptions.cs +++ b/Source/MQTTnet.Server/Stopping/MqttServerStopOptions.cs @@ -4,20 +4,19 @@ using MQTTnet.Protocol; -namespace MQTTnet.Server +namespace MQTTnet.Server; + +public sealed class MqttServerStopOptions { - public sealed class MqttServerStopOptions + /// + /// These disconnect options are sent to every connected client via a DISCONNECT packet. + /// MQTT 5.0.0+ feature. + /// + public MqttServerClientDisconnectOptions DefaultClientDisconnectOptions { get; set; } = new MqttServerClientDisconnectOptions { - /// - /// These disconnect options are sent to every connected client via a DISCONNECT packet. - /// MQTT 5.0.0+ feature. - /// - public MqttServerClientDisconnectOptions DefaultClientDisconnectOptions { get; set; } = new MqttServerClientDisconnectOptions - { - ReasonCode = MqttDisconnectReasonCode.ServerShuttingDown, - UserProperties = null, - ReasonString = null, - ServerReference = null - }; - } + ReasonCode = MqttDisconnectReasonCode.ServerShuttingDown, + UserProperties = null, + ReasonString = null, + ServerReference = null + }; } \ No newline at end of file diff --git a/Source/MQTTnet.Server/Stopping/MqttServerStopOptionsBuilder.cs b/Source/MQTTnet.Server/Stopping/MqttServerStopOptionsBuilder.cs index b851f25d..9d3e92e8 100644 --- a/Source/MQTTnet.Server/Stopping/MqttServerStopOptionsBuilder.cs +++ b/Source/MQTTnet.Server/Stopping/MqttServerStopOptionsBuilder.cs @@ -2,34 +2,31 @@ // The .NET Foundation licenses this file to you under the MIT license. // See the LICENSE file in the project root for more information. -using System; +namespace MQTTnet.Server; -namespace MQTTnet.Server +public sealed class MqttServerStopOptionsBuilder { - public sealed class MqttServerStopOptionsBuilder + readonly MqttServerStopOptions _options = new(); + + public MqttServerStopOptionsBuilder WithDefaultClientDisconnectOptions(MqttServerClientDisconnectOptions value) { - readonly MqttServerStopOptions _options = new MqttServerStopOptions(); + _options.DefaultClientDisconnectOptions = value; + return this; + } - public MqttServerStopOptionsBuilder WithDefaultClientDisconnectOptions(MqttServerClientDisconnectOptions value) - { - _options.DefaultClientDisconnectOptions = value; - return this; - } + public MqttServerStopOptionsBuilder WithDefaultClientDisconnectOptions(Action builder) + { + ArgumentNullException.ThrowIfNull(builder); - public MqttServerStopOptionsBuilder WithDefaultClientDisconnectOptions(Action builder) - { - ArgumentNullException.ThrowIfNull(builder); + var optionsBuilder = new MqttServerClientDisconnectOptionsBuilder(); + builder(optionsBuilder); - var optionsBuilder = new MqttServerClientDisconnectOptionsBuilder(); - builder.Invoke(optionsBuilder); + _options.DefaultClientDisconnectOptions = optionsBuilder.Build(); + return this; + } - _options.DefaultClientDisconnectOptions = optionsBuilder.Build(); - return this; - } - - public MqttServerStopOptions Build() - { - return _options; - } + public MqttServerStopOptions Build() + { + return _options; } } \ No newline at end of file diff --git a/Source/MQTTnet.Server/SubscribeResponse.cs b/Source/MQTTnet.Server/SubscribeResponse.cs index bbee1672..00ebf402 100644 --- a/Source/MQTTnet.Server/SubscribeResponse.cs +++ b/Source/MQTTnet.Server/SubscribeResponse.cs @@ -18,5 +18,5 @@ public sealed class SubscribeResponse public string ReasonString { get; set; } - public List UserProperties { get; } = new(); + public List UserProperties { get; } = []; } \ No newline at end of file diff --git a/Source/MQTTnet.Server/UnsubscribeResponse.cs b/Source/MQTTnet.Server/UnsubscribeResponse.cs index 463f65ca..741d323d 100644 --- a/Source/MQTTnet.Server/UnsubscribeResponse.cs +++ b/Source/MQTTnet.Server/UnsubscribeResponse.cs @@ -17,5 +17,5 @@ public sealed class UnsubscribeResponse public string ReasonString { get; set; } - public List UserProperties { get; } = new(); + public List UserProperties { get; } = []; } \ No newline at end of file diff --git a/Source/MQTTnet.TestApp/AsyncLockTest.cs b/Source/MQTTnet.TestApp/AsyncLockTest.cs index 035f3d46..65c12826 100644 --- a/Source/MQTTnet.TestApp/AsyncLockTest.cs +++ b/Source/MQTTnet.TestApp/AsyncLockTest.cs @@ -2,27 +2,58 @@ // The .NET Foundation licenses this file to you under the MIT license. // See the LICENSE file in the project root for more information. +using System; using System.Threading; using System.Threading.Tasks; -using MQTTnet.Internal; +using MQTTnet.Exceptions; -namespace MQTTnet.TestApp +namespace MQTTnet.TestApp; + +public sealed class AsyncLockTest { - public sealed class AsyncLockTest + public async Task Run() { - public async Task Run() + try { - var asyncLock = new AsyncLock(); + var semaphore = new SemaphoreSlim(1, 1); - using (var cancellationToken = new CancellationTokenSource()) + await semaphore.WaitAsync(); + try { - for (var i = 0; i < 100000; i++) - { - using (await asyncLock.EnterAsync(cancellationToken.Token).ConfigureAwait(false)) - { - } - } + // Wait for data from socket etc... + // Then get an exception. + semaphore.Dispose(); + throw new MqttCommunicationException("Connection closed"); + } + finally + { + semaphore.Release(); } } + catch (Exception exception) + { + Console.WriteLine(exception.ToString()); + } + + + + + + + + + + + + + // var asyncLock = new AsyncLock(); + // + // using var cancellationToken = new CancellationTokenSource(); + // for (var i = 0; i < 100000; i++) + // { + // using (await asyncLock.EnterAsync(cancellationToken.Token).ConfigureAwait(false)) + // { + // } + // } } } \ No newline at end of file diff --git a/Source/MQTTnet.TestApp/ClientFlowTest.cs b/Source/MQTTnet.TestApp/ClientFlowTest.cs index 9044a781..995d6d63 100644 --- a/Source/MQTTnet.TestApp/ClientFlowTest.cs +++ b/Source/MQTTnet.TestApp/ClientFlowTest.cs @@ -6,49 +6,48 @@ using System; using System.Threading.Tasks; using MQTTnet.Diagnostics.Logger; -namespace MQTTnet.TestApp +namespace MQTTnet.TestApp; + +public static class ClientFlowTest { - public static class ClientFlowTest + public static async Task RunAsync() { - public static async Task RunAsync() + try { - try - { - var logger = new MqttNetEventLogger(); - MqttNetConsoleLogger.ForwardToConsole(logger); + var logger = new MqttNetEventLogger(); + MqttNetConsoleLogger.ForwardToConsole(logger); - var factory = new MqttClientFactory(logger); + var factory = new MqttClientFactory(logger); - var client = factory.CreateMqttClient(); + var client = factory.CreateMqttClient(); - var options = new MqttClientOptionsBuilder() - .WithTcpServer("localhost") - .Build(); + var options = new MqttClientOptionsBuilder() + .WithTcpServer("localhost") + .Build(); - Console.WriteLine("BEFORE CONNECT"); - await client.ConnectAsync(options); - Console.WriteLine("AFTER CONNECT"); + Console.WriteLine("BEFORE CONNECT"); + await client.ConnectAsync(options); + Console.WriteLine("AFTER CONNECT"); - Console.WriteLine("BEFORE SUBSCRIBE"); - await client.SubscribeAsync("test/topic"); - Console.WriteLine("AFTER SUBSCRIBE"); + Console.WriteLine("BEFORE SUBSCRIBE"); + await client.SubscribeAsync("test/topic"); + Console.WriteLine("AFTER SUBSCRIBE"); - Console.WriteLine("BEFORE PUBLISH"); - await client.PublishStringAsync("test/topic", "payload"); - Console.WriteLine("AFTER PUBLISH"); + Console.WriteLine("BEFORE PUBLISH"); + await client.PublishStringAsync("test/topic", "payload"); + Console.WriteLine("AFTER PUBLISH"); - await Task.Delay(1000); + await Task.Delay(1000); - Console.WriteLine("BEFORE DISCONNECT"); - await client.DisconnectAsync(); - Console.WriteLine("AFTER DISCONNECT"); + Console.WriteLine("BEFORE DISCONNECT"); + await client.DisconnectAsync(); + Console.WriteLine("AFTER DISCONNECT"); - Console.WriteLine("FINISHED"); - } - catch (Exception ex) - { - Console.WriteLine(ex); - } + Console.WriteLine("FINISHED"); + } + catch (Exception ex) + { + Console.WriteLine(ex); } } -} +} \ No newline at end of file diff --git a/Source/MQTTnet.TestApp/ClientTest.cs b/Source/MQTTnet.TestApp/ClientTest.cs index 4c68f48e..a874da7d 100644 --- a/Source/MQTTnet.TestApp/ClientTest.cs +++ b/Source/MQTTnet.TestApp/ClientTest.cs @@ -10,99 +10,98 @@ using MQTTnet.Diagnostics.Logger; using MQTTnet.Internal; using MQTTnet.Protocol; -namespace MQTTnet.TestApp +namespace MQTTnet.TestApp; + +public static class ClientTest { - public static class ClientTest + public static async Task RunAsync() { - public static async Task RunAsync() + try { - try + var logger = new MqttNetEventLogger(); + MqttNetConsoleLogger.ForwardToConsole(logger); + + var factory = new MqttClientFactory(logger); + var client = factory.CreateMqttClient(); + var clientOptions = new MqttClientOptions { - var logger = new MqttNetEventLogger(); - MqttNetConsoleLogger.ForwardToConsole(logger); - - var factory = new MqttClientFactory(logger); - var client = factory.CreateMqttClient(); - var clientOptions = new MqttClientOptions + ChannelOptions = new MqttClientTcpOptions { - ChannelOptions = new MqttClientTcpOptions - { - RemoteEndpoint = new DnsEndPoint("127.0.0.1", 0) - } - }; + RemoteEndpoint = new DnsEndPoint("127.0.0.1", 0) + } + }; - client.ApplicationMessageReceivedAsync += e => + client.ApplicationMessageReceivedAsync += e => + { + var payloadText = string.Empty; + if (e.ApplicationMessage.Payload.Length > 0) { - var payloadText = string.Empty; - if (e.ApplicationMessage.Payload.Length > 0) - { - payloadText = Encoding.UTF8.GetString(e.ApplicationMessage.Payload); - } + payloadText = Encoding.UTF8.GetString(e.ApplicationMessage.Payload); + } - Console.WriteLine("### RECEIVED APPLICATION MESSAGE ###"); - Console.WriteLine($"+ Topic = {e.ApplicationMessage.Topic}"); - Console.WriteLine($"+ Payload = {payloadText}"); - Console.WriteLine($"+ QoS = {e.ApplicationMessage.QualityOfServiceLevel}"); - Console.WriteLine($"+ Retain = {e.ApplicationMessage.Retain}"); - Console.WriteLine(); + Console.WriteLine("### RECEIVED APPLICATION MESSAGE ###"); + Console.WriteLine($"+ Topic = {e.ApplicationMessage.Topic}"); + Console.WriteLine($"+ Payload = {payloadText}"); + Console.WriteLine($"+ QoS = {e.ApplicationMessage.QualityOfServiceLevel}"); + Console.WriteLine($"+ Retain = {e.ApplicationMessage.Retain}"); + Console.WriteLine(); - return CompletedTask.Instance; - }; + return CompletedTask.Instance; + }; - client.ConnectedAsync += async e => - { - Console.WriteLine("### CONNECTED WITH SERVER ###"); + client.ConnectedAsync += async _ => + { + Console.WriteLine("### CONNECTED WITH SERVER ###"); - await client.SubscribeAsync("#"); + await client.SubscribeAsync("#"); - Console.WriteLine("### SUBSCRIBED ###"); - }; + Console.WriteLine("### SUBSCRIBED ###"); + }; - client.DisconnectedAsync += async e => - { - Console.WriteLine("### DISCONNECTED FROM SERVER ###"); - await Task.Delay(TimeSpan.FromSeconds(5)); - - try - { - await client.ConnectAsync(clientOptions); - } - catch - { - Console.WriteLine("### RECONNECTING FAILED ###"); - } - }; + client.DisconnectedAsync += async _ => + { + Console.WriteLine("### DISCONNECTED FROM SERVER ###"); + await Task.Delay(TimeSpan.FromSeconds(5)); try { await client.ConnectAsync(clientOptions); } - catch (Exception exception) + catch { - Console.WriteLine("### CONNECTING FAILED ###" + Environment.NewLine + exception); + Console.WriteLine("### RECONNECTING FAILED ###"); } + }; - Console.WriteLine("### WAITING FOR APPLICATION MESSAGES ###"); - - while (true) - { - Console.ReadLine(); - - await client.SubscribeAsync("test"); - - var applicationMessage = new MqttApplicationMessageBuilder() - .WithTopic("A/B/C") - .WithPayload("Hello World") - .WithQualityOfServiceLevel(MqttQualityOfServiceLevel.AtLeastOnce) - .Build(); - - await client.PublishAsync(applicationMessage); - } + try + { + await client.ConnectAsync(clientOptions); } catch (Exception exception) { - Console.WriteLine(exception); + Console.WriteLine("### CONNECTING FAILED ###" + Environment.NewLine + exception); + } + + Console.WriteLine("### WAITING FOR APPLICATION MESSAGES ###"); + + while (true) + { + Console.ReadLine(); + + await client.SubscribeAsync("test"); + + var applicationMessage = new MqttApplicationMessageBuilder() + .WithTopic("A/B/C") + .WithPayload("Hello World") + .WithQualityOfServiceLevel(MqttQualityOfServiceLevel.AtLeastOnce) + .Build(); + + await client.PublishAsync(applicationMessage); } } + catch (Exception exception) + { + Console.WriteLine(exception); + } } -} +} \ No newline at end of file diff --git a/Source/MQTTnet.TestApp/MessageThroughputTest.cs b/Source/MQTTnet.TestApp/MessageThroughputTest.cs index 3d4046cb..4a813c9e 100644 --- a/Source/MQTTnet.TestApp/MessageThroughputTest.cs +++ b/Source/MQTTnet.TestApp/MessageThroughputTest.cs @@ -1,342 +1,345 @@ using MQTTnet.Server; using System.Collections.Generic; using System; +using System.Diagnostics; using System.Threading; using System.Threading.Tasks; using System.Linq; using MQTTnet.Internal; -namespace MQTTnet.TestApp +namespace MQTTnet.TestApp; + +/// +/// Connect a number of publisher clients and one subscriber client, then publish messages and measure +/// the number of messages per second that can be exchanged between publishers and subscriber. +/// Measurements are performed for subscriptions containing no wildcard, a single wildcard or multiple wildcards. +/// +public class MessageThroughputTest { - /// - /// Connect a number of publisher clients and one subscriber client, then publish messages and measure - /// the number of messages per second that can be exchanged between publishers and subscriber. - /// Measurements are performed for subscriptions containing no wildcard, a single wildcard or multiple wildcards. - /// - public class MessageThroughputTest + // Change these constants to suit + const int NumPublishers = 5000; + const int NumTopicsPerPublisher = 10; + + // Note: Other code changes may be required when changing this constant: + const int NumSubscribers = 1; // Fixed + + // Number of publish calls before a response for all published messages is awaited. + // This must be limited to a reasonable value that the server or TCP pipeline can handle. + const int NumPublishCallsPerBatch = 250; + + // Message counters are set/reset in the PublishAllAsync loop + int _messagesReceivedCount; + int _messagesExpectedCount; + + CancellationTokenSource _cancellationTokenSource; + + MqttServer _mqttServer; + Dictionary _mqttPublisherClientsByPublisherName; + List _mqttSubscriberClients; + + Dictionary> _topicsByPublisher; + Dictionary> _singleWildcardTopicsByPublisher; + Dictionary> _multiWildcardTopicsByPublisher; + + public async Task Run() { - // Change these constants to suit - const int NumPublishers = 5000; - const int NumTopicsPerPublisher = 10; - - // Note: Other code changes may be required when changing this constant: - const int NumSubscribers = 1; // Fixed - - // Number of publish calls before a response for all published messages is awaited. - // This must be limited to a reasonable value that the server or TCP pipeline can handle. - const int NumPublishCallsPerBatch = 250; - - // Message counters are set/reset in the PublishAllAsync loop - int _messagesReceivedCount; - int _messagesExpectedCount; - - CancellationTokenSource _cancellationTokenSource; - - MqttServer _mqttServer; - Dictionary _mqttPublisherClientsByPublisherName; - List _mqttSubscriberClients; - - Dictionary> _topicsByPublisher; - Dictionary> _singleWildcardTopicsByPublisher; - Dictionary> _multiWildcardTopicsByPublisher; - - public async Task Run() + try { - try - { - Console.WriteLine(); - Console.WriteLine("Begin message throughput test"); - Console.WriteLine(); + Console.WriteLine(); + Console.WriteLine("Begin message throughput test"); + Console.WriteLine(); - Console.WriteLine("Number of publishers: " + NumPublishers); - Console.WriteLine("Number of published topics (total): " + NumPublishers * NumTopicsPerPublisher); - Console.WriteLine("Number of subscribers: " + NumSubscribers); + Console.WriteLine("Number of publishers: " + NumPublishers); + Console.WriteLine("Number of published topics (total): " + NumPublishers * NumTopicsPerPublisher); + Console.WriteLine("Number of subscribers: " + NumSubscribers); - await Setup(); + await Setup(); - await Subscribe_to_No_Wildcard_Topics(); - await Subscribe_to_Single_Wildcard_Topics(); - await Subscribe_to_Multi_Wildcard_Topics(); - - Console.WriteLine(); - Console.WriteLine("End message throughput test"); - } - catch (Exception ex) - { - ConsoleWriteLineError(ex.Message); - } - finally - { - await Cleanup(); - } - } - - public async Task Setup() - { - new TopicGenerator().Generate(NumPublishers, NumTopicsPerPublisher, out _topicsByPublisher, out _singleWildcardTopicsByPublisher, out _multiWildcardTopicsByPublisher); - - var serverOptions = new MqttServerOptionsBuilder().WithDefaultEndpoint().Build(); - var mqttClientFactory = new MqttClientFactory(); - var mqttServerFactory = new MqttServerFactory(); - _mqttServer = mqttServerFactory.CreateMqttServer(serverOptions); - await _mqttServer.StartAsync(); + await Subscribe_to_No_Wildcard_Topics(); + await Subscribe_to_Single_Wildcard_Topics(); + await Subscribe_to_Multi_Wildcard_Topics(); Console.WriteLine(); - Console.WriteLine("Begin connect " + NumPublishers + " publisher(s)..."); + Console.WriteLine("End message throughput test"); + } + catch (Exception ex) + { + ConsoleWriteLineError(ex.Message); + } + finally + { + await Cleanup(); + } + } - var stopWatch = new System.Diagnostics.Stopwatch(); - stopWatch.Start(); - _mqttPublisherClientsByPublisherName = new Dictionary(); - foreach (var pt in _topicsByPublisher) - { - var publisherName = pt.Key; - var mqttClient = mqttClientFactory.CreateMqttClient(); - var publisherOptions = new MqttClientOptionsBuilder() - .WithTcpServer("localhost") - .WithClientId(publisherName) - .Build(); - await mqttClient.ConnectAsync(publisherOptions); - _mqttPublisherClientsByPublisherName.Add(publisherName, mqttClient); - } - stopWatch.Stop(); + public async Task Setup() + { + new TopicGenerator().Generate(NumPublishers, NumTopicsPerPublisher, out _topicsByPublisher, out _singleWildcardTopicsByPublisher, out _multiWildcardTopicsByPublisher); - Console.Write($"{NumPublishers} publisher(s) connected in {stopWatch.ElapsedMilliseconds / 1000.0:0.000} seconds, "); - Console.WriteLine($"connections per second: {NumPublishers / (stopWatch.ElapsedMilliseconds / 1000.0):0.000}"); + var serverOptions = new MqttServerOptionsBuilder().WithDefaultEndpoint().Build(); + var mqttClientFactory = new MqttClientFactory(); + var mqttServerFactory = new MqttServerFactory(); + _mqttServer = mqttServerFactory.CreateMqttServer(serverOptions); + await _mqttServer.StartAsync(); - _mqttSubscriberClients = new List(); - for (var i = 0; i < NumSubscribers; ++i) - { - var mqttClient = mqttClientFactory.CreateMqttClient(); - var subsriberOptions = new MqttClientOptionsBuilder() - .WithTcpServer("localhost") - .WithClientId("sub" + i) - .Build(); - await mqttClient.ConnectAsync(subsriberOptions); - mqttClient.ApplicationMessageReceivedAsync += HandleApplicationMessageReceivedAsync; - _mqttSubscriberClients.Add(mqttClient); - } + Console.WriteLine(); + Console.WriteLine("Begin connect " + NumPublishers + " publisher(s)..."); + + var stopWatch = new Stopwatch(); + stopWatch.Start(); + _mqttPublisherClientsByPublisherName = []; + foreach (var pt in _topicsByPublisher) + { + var publisherName = pt.Key; + var mqttClient = mqttClientFactory.CreateMqttClient(); + var publisherOptions = new MqttClientOptionsBuilder() + .WithTcpServer("localhost") + .WithClientId(publisherName) + .Build(); + await mqttClient.ConnectAsync(publisherOptions); + _mqttPublisherClientsByPublisherName.Add(publisherName, mqttClient); + } + stopWatch.Stop(); + + Console.Write($"{NumPublishers} publisher(s) connected in {stopWatch.ElapsedMilliseconds / 1000.0:0.000} seconds, "); + Console.WriteLine($"connections per second: {NumPublishers / (stopWatch.ElapsedMilliseconds / 1000.0):0.000}"); + + _mqttSubscriberClients = new List(); + for (var i = 0; i < NumSubscribers; ++i) + { + var mqttClient = mqttClientFactory.CreateMqttClient(); + var subsriberOptions = new MqttClientOptionsBuilder() + .WithTcpServer("localhost") + .WithClientId("sub" + i) + .Build(); + await mqttClient.ConnectAsync(subsriberOptions); + mqttClient.ApplicationMessageReceivedAsync += HandleApplicationMessageReceivedAsync; + _mqttSubscriberClients.Add(mqttClient); + } + } + + Task HandleApplicationMessageReceivedAsync(MqttApplicationMessageReceivedEventArgs eventArgs) + { + // count messages and signal cancellation when expected message count is reached + ++_messagesReceivedCount; + if (_messagesReceivedCount == _messagesExpectedCount) + { + _cancellationTokenSource.Cancel(); + } + return CompletedTask.Instance; + } + + public async Task Cleanup() + { + foreach (var mqttClient in _mqttSubscriberClients) + { + await mqttClient.DisconnectAsync(); + mqttClient.ApplicationMessageReceivedAsync -= HandleApplicationMessageReceivedAsync; + mqttClient.Dispose(); } - Task HandleApplicationMessageReceivedAsync(MqttApplicationMessageReceivedEventArgs eventArgs) + foreach (var pub in _mqttPublisherClientsByPublisherName) { - // count messages and signal cancellation when expected message count is reached - ++_messagesReceivedCount; - if (_messagesReceivedCount == _messagesExpectedCount) - { - _cancellationTokenSource.Cancel(); - } - return CompletedTask.Instance; + var mqttClient = pub.Value; + await mqttClient.DisconnectAsync(); + mqttClient.Dispose(); } - public async Task Cleanup() + await _mqttServer.StopAsync(); + _mqttServer.Dispose(); + } + + /// + /// Measure no-wildcard topic subscription message exchange performance + /// + public Task Subscribe_to_No_Wildcard_Topics() + { + return ProcessMessages(_topicsByPublisher, "no wildcards"); + } + + /// + /// Measure single-wildcard topic subscription message exchange performance + /// + public Task Subscribe_to_Single_Wildcard_Topics() + { + return ProcessMessages(_singleWildcardTopicsByPublisher, "single wildcard"); + } + + /// + /// Measure multi-wildcard topic subscription message exchange performance + /// + public Task Subscribe_to_Multi_Wildcard_Topics() + { + return ProcessMessages(_multiWildcardTopicsByPublisher, "multi wildcard"); + } + + + /// + /// Subscribe to all topics, then run message exchange + /// + public async Task ProcessMessages(Dictionary> topicsByPublisher, string topicTypeDescription) + { + var numTopics = CountTopics(topicsByPublisher); + + Console.WriteLine(); + Console.Write($"Subscribing to {numTopics} topics "); + ConsoleWriteInfo($"({topicTypeDescription})"); + Console.WriteLine($" for {NumSubscribers} subscriber(s)..."); + + var stopWatch = new Stopwatch(); + stopWatch.Start(); + + // each subscriber subscribes to all topics, one call per topic + foreach (var subscriber in _mqttSubscriberClients) { - foreach (var mqttClient in _mqttSubscriberClients) - { - await mqttClient.DisconnectAsync(); - mqttClient.ApplicationMessageReceivedAsync -= HandleApplicationMessageReceivedAsync; - mqttClient.Dispose(); - } - - foreach (var pub in _mqttPublisherClientsByPublisherName) - { - var mqttClient = pub.Value; - await mqttClient.DisconnectAsync(); - mqttClient.Dispose(); - } - - await _mqttServer.StopAsync(); - _mqttServer.Dispose(); - } - - /// - /// Measure no-wildcard topic subscription message exchange performance - /// - public Task Subscribe_to_No_Wildcard_Topics() - { - return ProcessMessages(_topicsByPublisher, "no wildcards"); - } - - /// - /// Measure single-wildcard topic subscription message exchange performance - /// - public Task Subscribe_to_Single_Wildcard_Topics() - { - return ProcessMessages(_singleWildcardTopicsByPublisher, "single wildcard"); - } - - /// - /// Measure multi-wildcard topic subscription message exchange performance - /// - public Task Subscribe_to_Multi_Wildcard_Topics() - { - return ProcessMessages(_multiWildcardTopicsByPublisher, "multi wildcard"); - } - - - /// - /// Subscribe to all topics, then run message exchange - /// - public async Task ProcessMessages(Dictionary> topicsByPublisher, string topicTypeDescription) - { - var numTopics = CountTopics(topicsByPublisher); - - Console.WriteLine(); - Console.Write($"Subscribing to {numTopics} topics "); - ConsoleWriteInfo($"({topicTypeDescription})"); - Console.WriteLine($" for {NumSubscribers} subscriber(s)..."); - - var stopWatch = new System.Diagnostics.Stopwatch(); - stopWatch.Start(); - - // each subscriber subscribes to all topics, one call per topic - foreach (var subscriber in _mqttSubscriberClients) - { - foreach (var tp in topicsByPublisher) - { - var topics = tp.Value; - foreach (var topic in topics) - { - var topicFilter = new Packets.MqttTopicFilter() { Topic = topic }; - await subscriber.SubscribeAsync(topicFilter).ConfigureAwait(false); - } - } - } - - stopWatch.Stop(); - - Console.Write($"{NumSubscribers} subscriber(s) subscribed in {stopWatch.ElapsedMilliseconds / 1000.0:0.000} seconds, "); - Console.WriteLine($"subscribe calls per second: {numTopics / (stopWatch.ElapsedMilliseconds / 1000.0):0.000}"); - - await PublishAllAsync(); - - Console.WriteLine($"Unsubscribing {numTopics} topics ({topicTypeDescription}) for {NumSubscribers} subscriber(s)..."); - - stopWatch.Restart(); - - // unsubscribe to all topics, one call per topic - foreach (var subscriber in _mqttSubscriberClients) - { - foreach (var tp in topicsByPublisher) - { - var topics = tp.Value; - foreach (var topic in topics) - { - var topicFilter = new Packets.MqttTopicFilter() { Topic = topic }; - - MqttClientUnsubscribeOptions options = - new MqttClientUnsubscribeOptionsBuilder() - .WithTopicFilter(topicFilter) - .Build(); - await subscriber.UnsubscribeAsync(options).ConfigureAwait(false); - } - } - } - - stopWatch.Stop(); - - Console.Write($"{NumSubscribers} subscriber(s) unsubscribed in {stopWatch.ElapsedMilliseconds / 1000.0:0.000} seconds, "); - Console.WriteLine($"unsubscribe calls per second: {numTopics / (stopWatch.ElapsedMilliseconds / 1000.0):0.000}"); - } - - /// - /// Publish messages in batches of NumPublishCallsPerBatch, wait for messages sent to subscriber - /// - async Task PublishAllAsync() - { - Console.WriteLine("Begin message exchange..."); - - int publisherIndexCounter = 0; // index to loop around all publishers to publish - int topicIndexCounter = 0; // index to loop around all topics to publish - int totalNumMessagesReceived = 0; - - var publisherNames = _topicsByPublisher.Keys.ToList(); - - // There should be one message received per publish - _messagesExpectedCount = NumPublishCallsPerBatch; - - var stopWatch = new System.Diagnostics.Stopwatch(); - stopWatch.Start(); - - // Loop for a while and exchange messages - - while (stopWatch.ElapsedMilliseconds < 10000) - { - _messagesReceivedCount = 0; - - _cancellationTokenSource = new CancellationTokenSource(); - - for (var publishCallCount = 0; publishCallCount < NumPublishCallsPerBatch; ++publishCallCount) - { - // pick a publisher - var publisherName = publisherNames[publisherIndexCounter % publisherNames.Count]; - var publisherTopics = _topicsByPublisher[publisherName]; - // pick a publisher topic - var topic = publisherTopics[topicIndexCounter % publisherTopics.Count]; - var message = new MqttApplicationMessageBuilder() - .WithTopic(topic) - .Build(); - await _mqttPublisherClientsByPublisherName[publisherName].PublishAsync(message).ConfigureAwait(false); - ++topicIndexCounter; - ++publisherIndexCounter; - } - - // Wait for at least one message per publish to be received by subscriber (in the subscriber's application message handler), - // then loop around to send another batch - try - { - await Task.Delay(30000, _cancellationTokenSource.Token).ConfigureAwait(false); - } - catch - { - } - - _cancellationTokenSource.Dispose(); - - if (_messagesReceivedCount < _messagesExpectedCount) - { - ConsoleWriteLineError(string.Format("Messages Received Count mismatch, expected {0}, received {1}", _messagesExpectedCount, _messagesReceivedCount)); - return; - } - - totalNumMessagesReceived += _messagesReceivedCount; - } - - stopWatch.Stop(); - - Console.Write($"{totalNumMessagesReceived} messages published and received in {stopWatch.ElapsedMilliseconds / 1000.0:0.000} seconds, "); - ConsoleWriteLineSuccess($"messages per second: {(int)(totalNumMessagesReceived / (stopWatch.ElapsedMilliseconds / 1000.0))}"); - } - - int CountTopics(Dictionary> topicsByPublisher) - { - var count = 0; foreach (var tp in topicsByPublisher) { - count += tp.Value.Count; + var topics = tp.Value; + foreach (var topic in topics) + { + var topicFilter = new Packets.MqttTopicFilter() { Topic = topic }; + await subscriber.SubscribeAsync(topicFilter).ConfigureAwait(false); + } } - return count; } - void ConsoleWriteLineError(string message) + stopWatch.Stop(); + + Console.Write($"{NumSubscribers} subscriber(s) subscribed in {stopWatch.ElapsedMilliseconds / 1000.0:0.000} seconds, "); + Console.WriteLine($"subscribe calls per second: {numTopics / (stopWatch.ElapsedMilliseconds / 1000.0):0.000}"); + + await PublishAllAsync(); + + Console.WriteLine($"Unsubscribing {numTopics} topics ({topicTypeDescription}) for {NumSubscribers} subscriber(s)..."); + + stopWatch.Restart(); + + // unsubscribe to all topics, one call per topic + foreach (var subscriber in _mqttSubscriberClients) { - Console.ForegroundColor = ConsoleColor.Red; - Console.WriteLine(message); - Console.ResetColor(); + foreach (var tp in topicsByPublisher) + { + var topics = tp.Value; + foreach (var topic in topics) + { + var topicFilter = new Packets.MqttTopicFilter { Topic = topic }; + + var options = + new MqttClientUnsubscribeOptionsBuilder() + .WithTopicFilter(topicFilter) + .Build(); + await subscriber.UnsubscribeAsync(options).ConfigureAwait(false); + } + } } - void ConsoleWriteLineSuccess(string message) - { - Console.ForegroundColor = ConsoleColor.Green; - Console.WriteLine(message); - Console.ResetColor(); - } - - void ConsoleWriteInfo(string message) - { - Console.ForegroundColor = ConsoleColor.White; - Console.Write(message); - Console.ResetColor(); - } + stopWatch.Stop(); + Console.Write($"{NumSubscribers} subscriber(s) unsubscribed in {stopWatch.ElapsedMilliseconds / 1000.0:0.000} seconds, "); + Console.WriteLine($"unsubscribe calls per second: {numTopics / (stopWatch.ElapsedMilliseconds / 1000.0):0.000}"); } -} + + /// + /// Publish messages in batches of NumPublishCallsPerBatch, wait for messages sent to subscriber + /// + async Task PublishAllAsync() + { + Console.WriteLine("Begin message exchange..."); + + var publisherIndexCounter = 0; // index to loop around all publishers to publish + var topicIndexCounter = 0; // index to loop around all topics to publish + var totalNumMessagesReceived = 0; + + var publisherNames = _topicsByPublisher.Keys.ToList(); + + // There should be one message received per publish + _messagesExpectedCount = NumPublishCallsPerBatch; + + var stopWatch = new Stopwatch(); + stopWatch.Start(); + + // Loop for a while and exchange messages + + while (stopWatch.ElapsedMilliseconds < 10000) + { + _messagesReceivedCount = 0; + + _cancellationTokenSource = new CancellationTokenSource(); + + for (var publishCallCount = 0; publishCallCount < NumPublishCallsPerBatch; ++publishCallCount) + { + // pick a publisher + var publisherName = publisherNames[publisherIndexCounter % publisherNames.Count]; + var publisherTopics = _topicsByPublisher[publisherName]; + // pick a publisher topic + var topic = publisherTopics[topicIndexCounter % publisherTopics.Count]; + var message = new MqttApplicationMessageBuilder() + .WithTopic(topic) + .Build(); + + await _mqttPublisherClientsByPublisherName[publisherName].PublishAsync(message).ConfigureAwait(false); + ++topicIndexCounter; + ++publisherIndexCounter; + } + + // Wait for at least one message per publish to be received by subscriber (in the subscriber's application message handler), + // then loop around to send another batch + try + { + await Task.Delay(30000, _cancellationTokenSource.Token).ConfigureAwait(false); + } + catch (Exception exception) + { + Debug.Write(exception.ToString()); + } + + _cancellationTokenSource.Dispose(); + + if (_messagesReceivedCount < _messagesExpectedCount) + { + ConsoleWriteLineError($"Messages Received Count mismatch, expected {_messagesExpectedCount}, received {_messagesReceivedCount}"); + return; + } + + totalNumMessagesReceived += _messagesReceivedCount; + } + + stopWatch.Stop(); + + Console.Write($"{totalNumMessagesReceived} messages published and received in {stopWatch.ElapsedMilliseconds / 1000.0:0.000} seconds, "); + ConsoleWriteLineSuccess($"messages per second: {(int)(totalNumMessagesReceived / (stopWatch.ElapsedMilliseconds / 1000.0))}"); + } + + static int CountTopics(Dictionary> topicsByPublisher) + { + var count = 0; + foreach (var tp in topicsByPublisher) + { + count += tp.Value.Count; + } + + return count; + } + + static void ConsoleWriteLineError(string message) + { + Console.ForegroundColor = ConsoleColor.Red; + Console.WriteLine(message); + Console.ResetColor(); + } + + static void ConsoleWriteLineSuccess(string message) + { + Console.ForegroundColor = ConsoleColor.Green; + Console.WriteLine(message); + Console.ResetColor(); + } + + static void ConsoleWriteInfo(string message) + { + Console.ForegroundColor = ConsoleColor.White; + Console.Write(message); + Console.ResetColor(); + } + +} \ No newline at end of file diff --git a/Source/MQTTnet.TestApp/MqttNetConsoleLogger.cs b/Source/MQTTnet.TestApp/MqttNetConsoleLogger.cs index 40ea09b6..30a153e5 100644 --- a/Source/MQTTnet.TestApp/MqttNetConsoleLogger.cs +++ b/Source/MQTTnet.TestApp/MqttNetConsoleLogger.cs @@ -6,58 +6,57 @@ using System; using System.Text; using MQTTnet.Diagnostics.Logger; -namespace MQTTnet.TestApp +namespace MQTTnet.TestApp; + +public static class MqttNetConsoleLogger { - public static class MqttNetConsoleLogger + static readonly object Lock = new(); + + public static void ForwardToConsole(MqttNetEventLogger logger) { - static readonly object _lock = new object(); + ArgumentNullException.ThrowIfNull(logger); - public static void ForwardToConsole(MqttNetEventLogger logger) + logger.LogMessagePublished -= PrintToConsole; + logger.LogMessagePublished += PrintToConsole; + } + + public static void PrintToConsole(string message, ConsoleColor color) + { + lock (Lock) { - ArgumentNullException.ThrowIfNull(logger); - - logger.LogMessagePublished -= PrintToConsole; - logger.LogMessagePublished += PrintToConsole; - } - - public static void PrintToConsole(string message, ConsoleColor color) - { - lock (_lock) - { - var backupColor = Console.ForegroundColor; - Console.ForegroundColor = color; - Console.Write(message); - Console.ForegroundColor = backupColor; - } - } - - static void PrintToConsole(object sender, MqttNetLogMessagePublishedEventArgs e) - { - var output = new StringBuilder(); - output.AppendLine($">> [{e.LogMessage.Timestamp:O}] [{e.LogMessage.ThreadId}] [{e.LogMessage.Source}] [{e.LogMessage.Level}]: {e.LogMessage.Message}"); - if (e.LogMessage.Exception != null) - { - output.AppendLine(e.LogMessage.Exception.ToString()); - } - - var color = ConsoleColor.Red; - switch (e.LogMessage.Level) - { - case MqttNetLogLevel.Error: - color = ConsoleColor.Red; - break; - case MqttNetLogLevel.Warning: - color = ConsoleColor.Yellow; - break; - case MqttNetLogLevel.Info: - color = ConsoleColor.Green; - break; - case MqttNetLogLevel.Verbose: - color = ConsoleColor.Gray; - break; - } - - PrintToConsole(output.ToString(), color); + var backupColor = Console.ForegroundColor; + Console.ForegroundColor = color; + Console.Write(message); + Console.ForegroundColor = backupColor; } } + + static void PrintToConsole(object sender, MqttNetLogMessagePublishedEventArgs e) + { + var output = new StringBuilder(); + output.AppendLine($">> [{e.LogMessage.Timestamp:O}] [{e.LogMessage.ThreadId}] [{e.LogMessage.Source}] [{e.LogMessage.Level}]: {e.LogMessage.Message}"); + if (e.LogMessage.Exception != null) + { + output.AppendLine(e.LogMessage.Exception.ToString()); + } + + var color = ConsoleColor.Red; + switch (e.LogMessage.Level) + { + case MqttNetLogLevel.Error: + color = ConsoleColor.Red; + break; + case MqttNetLogLevel.Warning: + color = ConsoleColor.Yellow; + break; + case MqttNetLogLevel.Info: + color = ConsoleColor.Green; + break; + case MqttNetLogLevel.Verbose: + color = ConsoleColor.Gray; + break; + } + + PrintToConsole(output.ToString(), color); + } } \ No newline at end of file diff --git a/Source/MQTTnet.TestApp/PerformanceTest.cs b/Source/MQTTnet.TestApp/PerformanceTest.cs index 64480324..472d2c40 100644 --- a/Source/MQTTnet.TestApp/PerformanceTest.cs +++ b/Source/MQTTnet.TestApp/PerformanceTest.cs @@ -12,355 +12,354 @@ using System.Threading.Tasks; using MQTTnet.Protocol; using MQTTnet.Server; -namespace MQTTnet.TestApp +namespace MQTTnet.TestApp; + +public static class PerformanceTest { - public static class PerformanceTest + public static void RunClientOnly() { - public static void RunClientOnly() + try { - try + var options = new MqttClientOptions { - var options = new MqttClientOptions + ChannelOptions = new MqttClientTcpOptions { - ChannelOptions = new MqttClientTcpOptions - { - RemoteEndpoint = new DnsEndPoint("127.0.0.1", 0) - }, - CleanSession = true - }; - - var client = new MqttClientFactory().CreateMqttClient(); - client.ConnectAsync(options).GetAwaiter().GetResult(); - - var message = CreateMessage(); - var stopwatch = new Stopwatch(); - - for (var i = 0; i < 10; i++) - { - var sentMessagesCount = 0; - - stopwatch.Restart(); - while (stopwatch.ElapsedMilliseconds < 1000) - { - client.PublishAsync(message).GetAwaiter().GetResult(); - sentMessagesCount++; - } - - Console.WriteLine($"Sending {sentMessagesCount} messages per second. #" + (i + 1)); - - GC.Collect(); - } - } - catch (Exception exception) - { - Console.WriteLine(exception); - } - } - - public static async Task RunClientAndServer() - { - try - { - var mqttClientFactory = new MqttClientFactory(); - var mqttServerFactory = new MqttServerFactory(); - var mqttServerOptions = new MqttServerOptionsBuilder().WithDefaultEndpoint().Build(); - var mqttServer = mqttServerFactory.CreateMqttServer(mqttServerOptions); - await mqttServer.StartAsync().ConfigureAwait(false); - - var options = new MqttClientOptions - { - ChannelOptions = new MqttClientTcpOptions - { - RemoteEndpoint = new DnsEndPoint("127.0.0.1", 0) - } - }; - - var client = mqttClientFactory.CreateMqttClient(); - await client.ConnectAsync(options).ConfigureAwait(false); - - var message = new MqttApplicationMessageBuilder().WithTopic("t") - .Build(); - - var stopwatch = new Stopwatch(); - - for (var i = 0; i < 10; i++) - { - stopwatch.Restart(); - - var sentMessagesCount = 0; - while (stopwatch.ElapsedMilliseconds < 1000) - { - await client.PublishAsync(message, CancellationToken.None).ConfigureAwait(false); - sentMessagesCount++; - } - - Console.WriteLine($"Sending {sentMessagesCount} messages per second. #" + (i + 1)); - } - } - catch (Exception exception) - { - Console.WriteLine(exception); - } - } - - static Task RunClientsAsync(int msgChunkSize, TimeSpan interval, bool concurrent) - { - return Task.WhenAll(Enumerable.Range(0, 3).Select(i => Task.Run(() => RunClientAsync(msgChunkSize, interval, concurrent)))); - } - - static async Task RunClientAsync(int msgChunkSize, TimeSpan interval, bool concurrent) - { - try - { - var options = new MqttClientOptions - { - ChannelOptions = new MqttClientTcpOptions - { - RemoteEndpoint = new DnsEndPoint("localhost", 0) - }, - ClientId = "Client1", - CleanSession = true - }; - - var client = new MqttClientFactory().CreateMqttClient(); - - try - { - await client.ConnectAsync(options).ConfigureAwait(false); - } - catch (Exception exception) - { - Console.WriteLine("### CONNECTING FAILED ###" + Environment.NewLine + exception); - } - - var message = CreateMessage(); - var stopwatch = Stopwatch.StartNew(); - - var testMessageCount = 10000; - for (var i = 0; i < testMessageCount; i++) - { - await client.PublishAsync(message); - } - - stopwatch.Stop(); - Console.WriteLine($"Sent 10.000 messages within {stopwatch.ElapsedMilliseconds} ms ({stopwatch.ElapsedMilliseconds / (float)testMessageCount} ms / message)."); - - var last = DateTime.Now; - var msgCount = 0; - - while (true) - { - var msgs = Enumerable.Range(0, msgChunkSize) - .Select(i => CreateMessage()) - .ToList(); - - if (concurrent) - { - //send concurrent (test for raceconditions) - var sendTasks = msgs - .Select(msg => PublishSingleMessage(client, msg, ref msgCount)) - .ToList(); - - await Task.WhenAll(sendTasks); - } - else - { - foreach (var msg in msgs) - { - await client.PublishAsync(msg); - msgCount += msgs.Count; - //send multiple - } - } - - var now = DateTime.Now; - if (last < now - TimeSpan.FromSeconds(1)) - { - Console.WriteLine($"sending {msgCount} intended {msgChunkSize / interval.TotalSeconds}"); - msgCount = 0; - last = now; - } - - await Task.Delay(interval).ConfigureAwait(false); - } - } - catch (Exception exception) - { - Console.WriteLine(exception); - } - } - - static MqttApplicationMessage CreateMessage() - { - //const string Payload = "###############################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################" - //const string Payload = "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"; - const string Payload = "Hello World"; - - return new MqttApplicationMessage - { - Topic = "A/B/C", - PayloadSegment = new ArraySegment(Encoding.UTF8.GetBytes(Payload)), - QualityOfServiceLevel = MqttQualityOfServiceLevel.AtLeastOnce + RemoteEndpoint = new DnsEndPoint("127.0.0.1", 0) + }, + CleanSession = true }; - } - static Task PublishSingleMessage(IMqttClient client, MqttApplicationMessage applicationMessage, ref int count) - { - Interlocked.Increment(ref count); - return Task.Run(() => client.PublishAsync(applicationMessage)); - } + var client = new MqttClientFactory().CreateMqttClient(); + client.ConnectAsync(options).GetAwaiter().GetResult(); - public static async Task RunQoS2Test() - { - try + var message = CreateMessage(); + var stopwatch = new Stopwatch(); + + for (var i = 0; i < 10; i++) { - var mqttServer = new MqttServerFactory().CreateMqttServer(new MqttServerOptions()); - await mqttServer.StartAsync(); + var sentMessagesCount = 0; - var options = new MqttClientOptions + stopwatch.Restart(); + while (stopwatch.ElapsedMilliseconds < 1000) { - ChannelOptions = new MqttClientTcpOptions - { - RemoteEndpoint = new DnsEndPoint("127.0.0.1", 0) - }, - CleanSession = true - }; - - var client = new MqttClientFactory().CreateMqttClient(); - await client.ConnectAsync(options); - - var message = new MqttApplicationMessage - { - Topic = "A/B/C", - PayloadSegment = new ArraySegment(Encoding.UTF8.GetBytes("Hello World")), - QualityOfServiceLevel = MqttQualityOfServiceLevel.ExactlyOnce - }; - - var stopwatch = new Stopwatch(); - - var iteration = 1; - while (true) - { - var sentMessagesCount = 0; - - stopwatch.Restart(); - while (stopwatch.ElapsedMilliseconds < 1000) - { - await client.PublishAsync(message).ConfigureAwait(false); - sentMessagesCount++; - } - - Console.WriteLine($"Sent {sentMessagesCount} messages in iteration #" + iteration); - - iteration++; + client.PublishAsync(message).GetAwaiter().GetResult(); + sentMessagesCount++; } - } - catch (Exception exception) - { - Console.WriteLine(exception); + + Console.WriteLine($"Sending {sentMessagesCount} messages per second. #" + (i + 1)); + + GC.Collect(); } } - - public static async Task RunQoS1Test() + catch (Exception exception) { - try - { - var mqttServer = new MqttServerFactory().CreateMqttServer(new MqttServerOptions()); - await mqttServer.StartAsync(); - - var options = new MqttClientOptions - { - ChannelOptions = new MqttClientTcpOptions - { - RemoteEndpoint = new DnsEndPoint("127.0.0.1", 0) - }, - CleanSession = true - }; - - var client = new MqttClientFactory().CreateMqttClient(); - await client.ConnectAsync(options); - - var message = new MqttApplicationMessage - { - Topic = "A/B/C", - PayloadSegment = new ArraySegment(Encoding.UTF8.GetBytes("Hello World")), - QualityOfServiceLevel = MqttQualityOfServiceLevel.AtLeastOnce - }; - - var stopwatch = new Stopwatch(); - - var iteration = 1; - while (true) - { - var sentMessagesCount = 0; - - stopwatch.Restart(); - while (stopwatch.ElapsedMilliseconds < 1000) - { - await client.PublishAsync(message).ConfigureAwait(false); - sentMessagesCount++; - } - - Console.WriteLine($"Sent {sentMessagesCount} messages in iteration #" + iteration); - - iteration++; - } - } - catch (Exception exception) - { - Console.WriteLine(exception); - } - } - - public static async Task RunQoS0Test() - { - try - { - //var mqttServer = new MqttFactory().CreateMqttServer(); - //await mqttServer.StartAsync(new MqttServerOptions()); - - var options = new MqttClientOptions - { - ChannelOptions = new MqttClientTcpOptions - { - RemoteEndpoint = new DnsEndPoint("127.0.0.1", 0) - }, - CleanSession = true - }; - - var client = new MqttClientFactory().CreateMqttClient(); - await client.ConnectAsync(options); - - var message = new MqttApplicationMessage - { - Topic = "A/B/C", - PayloadSegment = new ArraySegment(Encoding.UTF8.GetBytes("Hello World")), - QualityOfServiceLevel = MqttQualityOfServiceLevel.AtMostOnce - }; - - var stopwatch = new Stopwatch(); - - var iteration = 1; - while (true) - { - var sentMessagesCount = 0; - - stopwatch.Restart(); - while (stopwatch.ElapsedMilliseconds < 1000) - { - await client.PublishAsync(message).ConfigureAwait(false); - sentMessagesCount++; - } - - Console.WriteLine($"Sent {sentMessagesCount} messages in iteration #" + iteration); - - iteration++; - } - } - catch (Exception exception) - { - Console.WriteLine(exception); - } + Console.WriteLine(exception); } } -} + + public static async Task RunClientAndServer() + { + try + { + var mqttClientFactory = new MqttClientFactory(); + var mqttServerFactory = new MqttServerFactory(); + var mqttServerOptions = new MqttServerOptionsBuilder().WithDefaultEndpoint().Build(); + var mqttServer = mqttServerFactory.CreateMqttServer(mqttServerOptions); + await mqttServer.StartAsync().ConfigureAwait(false); + + var options = new MqttClientOptions + { + ChannelOptions = new MqttClientTcpOptions + { + RemoteEndpoint = new DnsEndPoint("127.0.0.1", 0) + } + }; + + var client = mqttClientFactory.CreateMqttClient(); + await client.ConnectAsync(options).ConfigureAwait(false); + + var message = new MqttApplicationMessageBuilder().WithTopic("t") + .Build(); + + var stopwatch = new Stopwatch(); + + for (var i = 0; i < 10; i++) + { + stopwatch.Restart(); + + var sentMessagesCount = 0; + while (stopwatch.ElapsedMilliseconds < 1000) + { + await client.PublishAsync(message, CancellationToken.None).ConfigureAwait(false); + sentMessagesCount++; + } + + Console.WriteLine($"Sending {sentMessagesCount} messages per second. #" + (i + 1)); + } + } + catch (Exception exception) + { + Console.WriteLine(exception); + } + } + + static Task RunClientsAsync(int msgChunkSize, TimeSpan interval, bool concurrent) + { + return Task.WhenAll(Enumerable.Range(0, 3).Select(_ => Task.Run(() => RunClientAsync(msgChunkSize, interval, concurrent)))); + } + + static async Task RunClientAsync(int msgChunkSize, TimeSpan interval, bool concurrent) + { + try + { + var options = new MqttClientOptions + { + ChannelOptions = new MqttClientTcpOptions + { + RemoteEndpoint = new DnsEndPoint("localhost", 0) + }, + ClientId = "Client1", + CleanSession = true + }; + + var client = new MqttClientFactory().CreateMqttClient(); + + try + { + await client.ConnectAsync(options).ConfigureAwait(false); + } + catch (Exception exception) + { + Console.WriteLine("### CONNECTING FAILED ###" + Environment.NewLine + exception); + } + + var message = CreateMessage(); + var stopwatch = Stopwatch.StartNew(); + + var testMessageCount = 10000; + for (var i = 0; i < testMessageCount; i++) + { + await client.PublishAsync(message); + } + + stopwatch.Stop(); + Console.WriteLine($"Sent 10.000 messages within {stopwatch.ElapsedMilliseconds} ms ({stopwatch.ElapsedMilliseconds / (float)testMessageCount} ms / message)."); + + var last = DateTime.Now; + var msgCount = 0; + + while (true) + { + var msgs = Enumerable.Range(0, msgChunkSize) + .Select(_ => CreateMessage()) + .ToList(); + + if (concurrent) + { + //send concurrent (test for raceconditions) + var sendTasks = msgs + .Select(msg => PublishSingleMessage(client, msg, ref msgCount)) + .ToList(); + + await Task.WhenAll(sendTasks); + } + else + { + foreach (var msg in msgs) + { + await client.PublishAsync(msg); + msgCount += msgs.Count; + //send multiple + } + } + + var now = DateTime.Now; + if (last < now - TimeSpan.FromSeconds(1)) + { + Console.WriteLine($"sending {msgCount} intended {msgChunkSize / interval.TotalSeconds}"); + msgCount = 0; + last = now; + } + + await Task.Delay(interval).ConfigureAwait(false); + } + } + catch (Exception exception) + { + Console.WriteLine(exception); + } + } + + static MqttApplicationMessage CreateMessage() + { + //const string Payload = "###############################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################" + //const string Payload = "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"; + const string payload = "Hello World"; + + return new MqttApplicationMessage + { + Topic = "A/B/C", + PayloadSegment = new ArraySegment(Encoding.UTF8.GetBytes(payload)), + QualityOfServiceLevel = MqttQualityOfServiceLevel.AtLeastOnce + }; + } + + static Task PublishSingleMessage(IMqttClient client, MqttApplicationMessage applicationMessage, ref int count) + { + Interlocked.Increment(ref count); + return Task.Run(() => client.PublishAsync(applicationMessage)); + } + + public static async Task RunQoS2Test() + { + try + { + var mqttServer = new MqttServerFactory().CreateMqttServer(new MqttServerOptions()); + await mqttServer.StartAsync(); + + var options = new MqttClientOptions + { + ChannelOptions = new MqttClientTcpOptions + { + RemoteEndpoint = new DnsEndPoint("127.0.0.1", 0) + }, + CleanSession = true + }; + + var client = new MqttClientFactory().CreateMqttClient(); + await client.ConnectAsync(options); + + var message = new MqttApplicationMessage + { + Topic = "A/B/C", + PayloadSegment = new ArraySegment(Encoding.UTF8.GetBytes("Hello World")), + QualityOfServiceLevel = MqttQualityOfServiceLevel.ExactlyOnce + }; + + var stopwatch = new Stopwatch(); + + var iteration = 1; + while (true) + { + var sentMessagesCount = 0; + + stopwatch.Restart(); + while (stopwatch.ElapsedMilliseconds < 1000) + { + await client.PublishAsync(message).ConfigureAwait(false); + sentMessagesCount++; + } + + Console.WriteLine($"Sent {sentMessagesCount} messages in iteration #" + iteration); + + iteration++; + } + } + catch (Exception exception) + { + Console.WriteLine(exception); + } + } + + public static async Task RunQoS1Test() + { + try + { + var mqttServer = new MqttServerFactory().CreateMqttServer(new MqttServerOptions()); + await mqttServer.StartAsync(); + + var options = new MqttClientOptions + { + ChannelOptions = new MqttClientTcpOptions + { + RemoteEndpoint = new DnsEndPoint("127.0.0.1", 0) + }, + CleanSession = true + }; + + var client = new MqttClientFactory().CreateMqttClient(); + await client.ConnectAsync(options); + + var message = new MqttApplicationMessage + { + Topic = "A/B/C", + PayloadSegment = new ArraySegment(Encoding.UTF8.GetBytes("Hello World")), + QualityOfServiceLevel = MqttQualityOfServiceLevel.AtLeastOnce + }; + + var stopwatch = new Stopwatch(); + + var iteration = 1; + while (true) + { + var sentMessagesCount = 0; + + stopwatch.Restart(); + while (stopwatch.ElapsedMilliseconds < 1000) + { + await client.PublishAsync(message).ConfigureAwait(false); + sentMessagesCount++; + } + + Console.WriteLine($"Sent {sentMessagesCount} messages in iteration #" + iteration); + + iteration++; + } + } + catch (Exception exception) + { + Console.WriteLine(exception); + } + } + + public static async Task RunQoS0Test() + { + try + { + //var mqttServer = new MqttFactory().CreateMqttServer(); + //await mqttServer.StartAsync(new MqttServerOptions()); + + var options = new MqttClientOptions + { + ChannelOptions = new MqttClientTcpOptions + { + RemoteEndpoint = new DnsEndPoint("127.0.0.1", 0) + }, + CleanSession = true + }; + + var client = new MqttClientFactory().CreateMqttClient(); + await client.ConnectAsync(options); + + var message = new MqttApplicationMessage + { + Topic = "A/B/C", + PayloadSegment = new ArraySegment(Encoding.UTF8.GetBytes("Hello World")), + QualityOfServiceLevel = MqttQualityOfServiceLevel.AtMostOnce + }; + + var stopwatch = new Stopwatch(); + + var iteration = 1; + while (true) + { + var sentMessagesCount = 0; + + stopwatch.Restart(); + while (stopwatch.ElapsedMilliseconds < 1000) + { + await client.PublishAsync(message).ConfigureAwait(false); + sentMessagesCount++; + } + + Console.WriteLine($"Sent {sentMessagesCount} messages in iteration #" + iteration); + + iteration++; + } + } + catch (Exception exception) + { + Console.WriteLine(exception); + } + } +} \ No newline at end of file diff --git a/Source/MQTTnet.TestApp/PublicBrokerTest.cs b/Source/MQTTnet.TestApp/PublicBrokerTest.cs index 9e2d8bae..21c55fdc 100644 --- a/Source/MQTTnet.TestApp/PublicBrokerTest.cs +++ b/Source/MQTTnet.TestApp/PublicBrokerTest.cs @@ -10,173 +10,172 @@ using MQTTnet.Formatter; using MQTTnet.Internal; using MQTTnet.Protocol; -namespace MQTTnet.TestApp +namespace MQTTnet.TestApp; + +public static class PublicBrokerTest { - public static class PublicBrokerTest + public static async Task RunAsync() { - public static async Task RunAsync() + // TLS13 is only available in Net5.0 + var unsafeTls13 = new MqttClientTlsOptions { - // TLS13 is only available in Net5.0 - var unsafeTls13 = new MqttClientTlsOptions - { - UseTls = true, - SslProtocol = SslProtocols.Tls13, - // Don't use this in production code. This handler simply allows any invalid certificate to work. - AllowUntrustedCertificates = true, - IgnoreCertificateChainErrors = true, - CertificateValidationHandler = _ => true - }; + UseTls = true, + SslProtocol = SslProtocols.Tls13, + // Don't use this in production code. This handler simply allows any invalid certificate to work. + AllowUntrustedCertificates = true, + IgnoreCertificateChainErrors = true, + CertificateValidationHandler = _ => true + }; - // Also defining TLS12 for servers that don't seem no to support TLS13. - var unsafeTls12 = new MqttClientTlsOptions - { - UseTls = true, - SslProtocol = SslProtocols.Tls12, - // Don't use this in production code. This handler simply allows any invalid certificate to work. - AllowUntrustedCertificates = true, - IgnoreCertificateChainErrors = true, - CertificateValidationHandler = _ => true - }; - - // mqtt.eclipseprojects.io - await ExecuteTestsAsync( - "mqtt.eclipseprojects.io TCP", - new MqttClientOptionsBuilder().WithTcpServer("mqtt.eclipseprojects.io", 1883).WithProtocolVersion(MqttProtocolVersion.V311).Build()); - - await ExecuteTestsAsync( - "mqtt.eclipseprojects.io WS", - new MqttClientOptionsBuilder().WithWebSocketServer(o => o.WithUri("mqtt.eclipseprojects.io:80/mqtt")).WithProtocolVersion(MqttProtocolVersion.V311).Build()); - - await ExecuteTestsAsync("mqtt.eclipseprojects.io WS TLS13", - new MqttClientOptionsBuilder().WithWebSocketServer(o => o.WithUri("mqtt.eclipseprojects.io:443/mqtt")) - .WithProtocolVersion(MqttProtocolVersion.V311).WithTlsOptions(unsafeTls13).Build()); - - // test.mosquitto.org - await ExecuteTestsAsync( - "test.mosquitto.org TCP", - new MqttClientOptionsBuilder().WithTcpServer("test.mosquitto.org", 1883).WithProtocolVersion(MqttProtocolVersion.V311).Build()); - - await ExecuteTestsAsync( - "test.mosquitto.org TCP - Authenticated", - new MqttClientOptionsBuilder().WithTcpServer("test.mosquitto.org", 1884).WithCredentials("rw", "readwrite").WithProtocolVersion(MqttProtocolVersion.V311).Build()); - - await ExecuteTestsAsync( - "test.mosquitto.org TCP TLS12", - new MqttClientOptionsBuilder().WithTcpServer("test.mosquitto.org", 8883).WithProtocolVersion(MqttProtocolVersion.V311).WithTlsOptions(unsafeTls12).Build()); - - await ExecuteTestsAsync("test.mosquitto.org TCP TLS13", - new MqttClientOptionsBuilder().WithTcpServer("test.mosquitto.org", 8883) - .WithProtocolVersion(MqttProtocolVersion.V311).WithTlsOptions(unsafeTls13).Build()); - - await ExecuteTestsAsync( - "test.mosquitto.org TCP TLS12 - Authenticated", - new MqttClientOptionsBuilder().WithTcpServer("test.mosquitto.org", 8885) - .WithCredentials("rw", "readwrite") - .WithProtocolVersion(MqttProtocolVersion.V311) - .WithTlsOptions(unsafeTls12) - .Build()); - - await ExecuteTestsAsync( - "test.mosquitto.org WS", - new MqttClientOptionsBuilder().WithWebSocketServer(o => o.WithUri("test.mosquitto.org:8080/mqtt")).WithProtocolVersion(MqttProtocolVersion.V311).Build()); - - await ExecuteTestsAsync( - "test.mosquitto.org WS TLS12", - new MqttClientOptionsBuilder().WithWebSocketServer(o => o.WithUri("test.mosquitto.org:8081/mqtt")).WithProtocolVersion(MqttProtocolVersion.V311).WithTlsOptions(unsafeTls12).Build()); - - // broker.emqx.io - await ExecuteTestsAsync( - "broker.emqx.io TCP", - new MqttClientOptionsBuilder().WithTcpServer("broker.emqx.io", 1883).WithProtocolVersion(MqttProtocolVersion.V311).Build()); - - await ExecuteTestsAsync( - "broker.emqx.io TCP TLS12", - new MqttClientOptionsBuilder().WithTcpServer("broker.emqx.io", 8883).WithProtocolVersion(MqttProtocolVersion.V311).WithTlsOptions(unsafeTls12).Build()); - - await ExecuteTestsAsync("broker.emqx.io TCP TLS13", - new MqttClientOptionsBuilder().WithTcpServer("broker.emqx.io", 8883) - .WithProtocolVersion(MqttProtocolVersion.V311).WithTlsOptions(unsafeTls13).Build()); - - await ExecuteTestsAsync( - "broker.emqx.io WS", - new MqttClientOptionsBuilder().WithWebSocketServer(o => o.WithUri("broker.emqx.io:8083/mqtt")).WithProtocolVersion(MqttProtocolVersion.V311).Build()); - - await ExecuteTestsAsync( - "broker.emqx.io WS TLS12", - new MqttClientOptionsBuilder().WithWebSocketServer(o => o.WithUri("broker.emqx.io:8084/mqtt")).WithProtocolVersion(MqttProtocolVersion.V311).WithTlsOptions(unsafeTls12).Build()); - - // broker.hivemq.com - await ExecuteTestsAsync( - "broker.hivemq.com TCP", - new MqttClientOptionsBuilder().WithTcpServer("broker.hivemq.com", 1883).WithProtocolVersion(MqttProtocolVersion.V311).Build()); - - await ExecuteTestsAsync( - "broker.hivemq.com WS", - new MqttClientOptionsBuilder().WithWebSocketServer(o => o.WithUri("broker.hivemq.com:8000/mqtt")).WithProtocolVersion(MqttProtocolVersion.V311).Build()); - - // mqtt.swifitch.cz: Does not seem to operate any more - // cloudmqtt.com: Cannot test because it does not offer a free plan any more. - - Write("Finished.", ConsoleColor.White); - Console.ReadLine(); - } - - static async Task ExecuteTestsAsync(string name, MqttClientOptions options) + // Also defining TLS12 for servers that don't seem no to support TLS13. + var unsafeTls12 = new MqttClientTlsOptions { - options.ProtocolVersion = MqttProtocolVersion.V311; - await ExecuteTestAsync(name + " V3.1.1", options); + UseTls = true, + SslProtocol = SslProtocols.Tls12, + // Don't use this in production code. This handler simply allows any invalid certificate to work. + AllowUntrustedCertificates = true, + IgnoreCertificateChainErrors = true, + CertificateValidationHandler = _ => true + }; - options.ProtocolVersion = MqttProtocolVersion.V500; - await ExecuteTestAsync(name + " V5.0.0", options); - } + // mqtt.eclipseprojects.io + await ExecuteTestsAsync( + "mqtt.eclipseprojects.io TCP", + new MqttClientOptionsBuilder().WithTcpServer("mqtt.eclipseprojects.io", 1883).WithProtocolVersion(MqttProtocolVersion.V311).Build()); - static async Task ExecuteTestAsync(string name, MqttClientOptions options) + await ExecuteTestsAsync( + "mqtt.eclipseprojects.io WS", + new MqttClientOptionsBuilder().WithWebSocketServer(o => o.WithUri("mqtt.eclipseprojects.io:80/mqtt")).WithProtocolVersion(MqttProtocolVersion.V311).Build()); + + await ExecuteTestsAsync("mqtt.eclipseprojects.io WS TLS13", + new MqttClientOptionsBuilder().WithWebSocketServer(o => o.WithUri("mqtt.eclipseprojects.io:443/mqtt")) + .WithProtocolVersion(MqttProtocolVersion.V311).WithTlsOptions(unsafeTls13).Build()); + + // test.mosquitto.org + await ExecuteTestsAsync( + "test.mosquitto.org TCP", + new MqttClientOptionsBuilder().WithTcpServer("test.mosquitto.org", 1883).WithProtocolVersion(MqttProtocolVersion.V311).Build()); + + await ExecuteTestsAsync( + "test.mosquitto.org TCP - Authenticated", + new MqttClientOptionsBuilder().WithTcpServer("test.mosquitto.org", 1884).WithCredentials("rw", "readwrite").WithProtocolVersion(MqttProtocolVersion.V311).Build()); + + await ExecuteTestsAsync( + "test.mosquitto.org TCP TLS12", + new MqttClientOptionsBuilder().WithTcpServer("test.mosquitto.org", 8883).WithProtocolVersion(MqttProtocolVersion.V311).WithTlsOptions(unsafeTls12).Build()); + + await ExecuteTestsAsync("test.mosquitto.org TCP TLS13", + new MqttClientOptionsBuilder().WithTcpServer("test.mosquitto.org", 8883) + .WithProtocolVersion(MqttProtocolVersion.V311).WithTlsOptions(unsafeTls13).Build()); + + await ExecuteTestsAsync( + "test.mosquitto.org TCP TLS12 - Authenticated", + new MqttClientOptionsBuilder().WithTcpServer("test.mosquitto.org", 8885) + .WithCredentials("rw", "readwrite") + .WithProtocolVersion(MqttProtocolVersion.V311) + .WithTlsOptions(unsafeTls12) + .Build()); + + await ExecuteTestsAsync( + "test.mosquitto.org WS", + new MqttClientOptionsBuilder().WithWebSocketServer(o => o.WithUri("test.mosquitto.org:8080/mqtt")).WithProtocolVersion(MqttProtocolVersion.V311).Build()); + + await ExecuteTestsAsync( + "test.mosquitto.org WS TLS12", + new MqttClientOptionsBuilder().WithWebSocketServer(o => o.WithUri("test.mosquitto.org:8081/mqtt")).WithProtocolVersion(MqttProtocolVersion.V311).WithTlsOptions(unsafeTls12).Build()); + + // broker.emqx.io + await ExecuteTestsAsync( + "broker.emqx.io TCP", + new MqttClientOptionsBuilder().WithTcpServer("broker.emqx.io", 1883).WithProtocolVersion(MqttProtocolVersion.V311).Build()); + + await ExecuteTestsAsync( + "broker.emqx.io TCP TLS12", + new MqttClientOptionsBuilder().WithTcpServer("broker.emqx.io", 8883).WithProtocolVersion(MqttProtocolVersion.V311).WithTlsOptions(unsafeTls12).Build()); + + await ExecuteTestsAsync("broker.emqx.io TCP TLS13", + new MqttClientOptionsBuilder().WithTcpServer("broker.emqx.io", 8883) + .WithProtocolVersion(MqttProtocolVersion.V311).WithTlsOptions(unsafeTls13).Build()); + + await ExecuteTestsAsync( + "broker.emqx.io WS", + new MqttClientOptionsBuilder().WithWebSocketServer(o => o.WithUri("broker.emqx.io:8083/mqtt")).WithProtocolVersion(MqttProtocolVersion.V311).Build()); + + await ExecuteTestsAsync( + "broker.emqx.io WS TLS12", + new MqttClientOptionsBuilder().WithWebSocketServer(o => o.WithUri("broker.emqx.io:8084/mqtt")).WithProtocolVersion(MqttProtocolVersion.V311).WithTlsOptions(unsafeTls12).Build()); + + // broker.hivemq.com + await ExecuteTestsAsync( + "broker.hivemq.com TCP", + new MqttClientOptionsBuilder().WithTcpServer("broker.hivemq.com", 1883).WithProtocolVersion(MqttProtocolVersion.V311).Build()); + + await ExecuteTestsAsync( + "broker.hivemq.com WS", + new MqttClientOptionsBuilder().WithWebSocketServer(o => o.WithUri("broker.hivemq.com:8000/mqtt")).WithProtocolVersion(MqttProtocolVersion.V311).Build()); + + // mqtt.swifitch.cz: Does not seem to operate any more + // cloudmqtt.com: Cannot test because it does not offer a free plan any more. + + Write("Finished.", ConsoleColor.White); + Console.ReadLine(); + } + + static async Task ExecuteTestsAsync(string name, MqttClientOptions options) + { + options.ProtocolVersion = MqttProtocolVersion.V311; + await ExecuteTestAsync(name + " V3.1.1", options); + + options.ProtocolVersion = MqttProtocolVersion.V500; + await ExecuteTestAsync(name + " V5.0.0", options); + } + + static async Task ExecuteTestAsync(string name, MqttClientOptions options) + { + try { - try + Write("Testing '" + name + "'... ", ConsoleColor.Gray); + + var factory = new MqttClientFactory(); + + using (var client = factory.CreateMqttClient()) { - Write("Testing '" + name + "'... ", ConsoleColor.Gray); - - var factory = new MqttClientFactory(); - - using (var client = factory.CreateMqttClient()) + MqttApplicationMessage receivedMessage = null; + client.ApplicationMessageReceivedAsync += e => { - MqttApplicationMessage receivedMessage = null; - client.ApplicationMessageReceivedAsync += e => - { - receivedMessage = e.ApplicationMessage; - return CompletedTask.Instance; - }; + receivedMessage = e.ApplicationMessage; + return CompletedTask.Instance; + }; - await client.ConnectAsync(options).ConfigureAwait(false); + await client.ConnectAsync(options).ConfigureAwait(false); - var topic = Guid.NewGuid().ToString(); - await client.SubscribeAsync(topic, MqttQualityOfServiceLevel.AtLeastOnce).ConfigureAwait(false); - await client.PublishStringAsync(topic, "Hello_World", MqttQualityOfServiceLevel.AtLeastOnce).ConfigureAwait(false); + var topic = Guid.NewGuid().ToString(); + await client.SubscribeAsync(topic, MqttQualityOfServiceLevel.AtLeastOnce).ConfigureAwait(false); + await client.PublishStringAsync(topic, "Hello_World", MqttQualityOfServiceLevel.AtLeastOnce).ConfigureAwait(false); - SpinWait.SpinUntil(() => receivedMessage != null, 5000); + SpinWait.SpinUntil(() => receivedMessage != null, 5000); - if (receivedMessage?.Topic != topic || receivedMessage?.ConvertPayloadToString() != "Hello_World") - { - throw new Exception("Message invalid."); - } - - await client.UnsubscribeAsync(topic).ConfigureAwait(false); - await client.DisconnectAsync().ConfigureAwait(false); + if (receivedMessage?.Topic != topic || receivedMessage?.ConvertPayloadToString() != "Hello_World") + { + throw new Exception("Message invalid."); } - Write("[OK]\n", ConsoleColor.Green); + await client.UnsubscribeAsync(topic).ConfigureAwait(false); + await client.DisconnectAsync().ConfigureAwait(false); } - catch (Exception exception) - { - Write("[FAILED]" + Environment.NewLine, ConsoleColor.Red); - Write(exception + Environment.NewLine, ConsoleColor.Red); - } - } - static void Write(string message, ConsoleColor color) + Write("[OK]\n", ConsoleColor.Green); + } + catch (Exception exception) { - Console.ForegroundColor = color; - Console.Write(message); + Write("[FAILED]" + Environment.NewLine, ConsoleColor.Red); + Write(exception + Environment.NewLine, ConsoleColor.Red); } } + + static void Write(string message, ConsoleColor color) + { + Console.ForegroundColor = color; + Console.Write(message); + } } \ No newline at end of file diff --git a/Source/MQTTnet.TestApp/ServerAndClientTest.cs b/Source/MQTTnet.TestApp/ServerAndClientTest.cs index cd0b0be1..d43c8f9c 100644 --- a/Source/MQTTnet.TestApp/ServerAndClientTest.cs +++ b/Source/MQTTnet.TestApp/ServerAndClientTest.cs @@ -7,26 +7,25 @@ using System.Threading.Tasks; using MQTTnet.Diagnostics.Logger; using MQTTnet.Server; -namespace MQTTnet.TestApp +namespace MQTTnet.TestApp; + +public static class ServerAndClientTest { - public static class ServerAndClientTest + public static async Task RunAsync() { - public static async Task RunAsync() - { - var logger = new MqttNetEventLogger(); - MqttNetConsoleLogger.ForwardToConsole(logger); + var logger = new MqttNetEventLogger(); + MqttNetConsoleLogger.ForwardToConsole(logger); - var mqttServerFactory = new MqttServerFactory(); - var mqttClientFactory = new MqttClientFactory(logger); - var server = mqttServerFactory.CreateMqttServer( new MqttServerOptionsBuilder().Build()); - var client = mqttClientFactory.CreateMqttClient(); + var mqttServerFactory = new MqttServerFactory(); + var mqttClientFactory = new MqttClientFactory(logger); + var server = mqttServerFactory.CreateMqttServer( new MqttServerOptionsBuilder().Build()); + var client = mqttClientFactory.CreateMqttClient(); - await server.StartAsync(); + await server.StartAsync(); - var clientOptions = new MqttClientOptionsBuilder().WithTcpServer("localhost").Build(); - await client.ConnectAsync(clientOptions); + var clientOptions = new MqttClientOptionsBuilder().WithTcpServer("localhost").Build(); + await client.ConnectAsync(clientOptions); - await Task.Delay(Timeout.Infinite); - } + await Task.Delay(Timeout.Infinite); } -} +} \ No newline at end of file diff --git a/Source/MQTTnet.TestApp/ServerTest.cs b/Source/MQTTnet.TestApp/ServerTest.cs index 241abcca..ae0e0975 100644 --- a/Source/MQTTnet.TestApp/ServerTest.cs +++ b/Source/MQTTnet.TestApp/ServerTest.cs @@ -13,189 +13,188 @@ using MQTTnet.Protocol; using MQTTnet.Server; using Newtonsoft.Json; -namespace MQTTnet.TestApp +namespace MQTTnet.TestApp; + +public static class ServerTest { - public static class ServerTest + public static void RunEmptyServer() { - public static void RunEmptyServer() - { - var mqttServer = new MqttServerFactory().CreateMqttServer(new MqttServerOptions()); - mqttServer.StartAsync().GetAwaiter().GetResult(); + var mqttServer = new MqttServerFactory().CreateMqttServer(new MqttServerOptions()); + mqttServer.StartAsync().GetAwaiter().GetResult(); - Console.WriteLine("Press any key to exit."); - Console.ReadLine(); - } - - public static void RunEmptyServerWithLogging() - { - var logger = new MqttNetEventLogger(); - MqttNetConsoleLogger.ForwardToConsole(logger); - - var mqttServerFactory = new MqttServerFactory(logger); - var mqttServer = mqttServerFactory.CreateMqttServer(new MqttServerOptions()); - mqttServer.StartAsync().GetAwaiter().GetResult(); - - Console.WriteLine("Press any key to exit."); - Console.ReadLine(); - } - - public static async Task RunAsync() - { - try - { - var options = new MqttServerOptionsBuilder() - .WithDefaultEndpoint() - .Build(); - - // Extend the timestamp for all messages from clients. - // Protect several topics from being subscribed from every client. - - //var certificate = new X509Certificate(@"C:\certs\test\test.cer", ""); - //options.TlsEndpointOptions.Certificate = certificate.Export(X509ContentType.Cert); - //options.ConnectionBacklog = 5; - //options.DefaultEndpointOptions.IsEnabled = true; - //options.TlsEndpointOptions.IsEnabled = false; - - var mqttServer = new MqttServerFactory().CreateMqttServer(options); - - const string Filename = "C:\\MQTT\\RetainedMessages.json"; - - mqttServer.RetainedMessageChangedAsync += e => - { - var directory = Path.GetDirectoryName(Filename); - if (!Directory.Exists(directory)) - { - Directory.CreateDirectory(directory); - } - - File.WriteAllText(Filename, JsonConvert.SerializeObject(e.StoredRetainedMessages)); - return CompletedTask.Instance; - }; - - mqttServer.RetainedMessagesClearedAsync += e => - { - File.Delete(Filename); - return CompletedTask.Instance; - }; - - mqttServer.LoadingRetainedMessageAsync += e => - { - List retainedMessages; - if (File.Exists(Filename)) - { - var json = File.ReadAllText(Filename); - retainedMessages = JsonConvert.DeserializeObject>(json); - } - else - { - retainedMessages = new List(); - } - - e.LoadedRetainedMessages = retainedMessages; - - return CompletedTask.Instance; - }; - - mqttServer.InterceptingPublishAsync += e => - { - if (MqttTopicFilterComparer.Compare(e.ApplicationMessage.Topic, "/myTopic/WithTimestamp/#") == MqttTopicFilterCompareResult.IsMatch) - { - // Replace the payload with the timestamp. But also extending a JSON - // based payload with the timestamp is a suitable use case. - e.ApplicationMessage.PayloadSegment = new ArraySegment(Encoding.UTF8.GetBytes(DateTime.Now.ToString("O"))); - } - - if (e.ApplicationMessage.Topic == "not_allowed_topic") - { - e.ProcessPublish = false; - e.CloseConnection = true; - } - - return CompletedTask.Instance; - }; - - mqttServer.ValidatingConnectionAsync += e => - { - if (e.ClientId == "SpecialClient") - { - if (e.UserName != "USER" || e.Password != "PASS") - { - e.ReasonCode = MqttConnectReasonCode.BadUserNameOrPassword; - } - } - - return CompletedTask.Instance; - }; - - mqttServer.InterceptingSubscriptionAsync += e => - { - if (e.TopicFilter.Topic.StartsWith("admin/foo/bar") && e.ClientId != "theAdmin") - { - e.Response.ReasonCode = MqttSubscribeReasonCode.ImplementationSpecificError; - } - - if (e.TopicFilter.Topic.StartsWith("the/secret/stuff") && e.ClientId != "Imperator") - { - e.Response.ReasonCode = MqttSubscribeReasonCode.ImplementationSpecificError; - e.CloseConnection = true; - } - - return CompletedTask.Instance; - }; - - mqttServer.InterceptingPublishAsync += e => - { - var payloadText = string.Empty; - if (e.ApplicationMessage.Payload.Length > 0) - { - payloadText = Encoding.UTF8.GetString(e.ApplicationMessage.Payload); - } - - MqttNetConsoleLogger.PrintToConsole($"'{e.ClientId}' reported '{e.ApplicationMessage.Topic}' > '{payloadText}'", ConsoleColor.Magenta); - return CompletedTask.Instance; - }; - - //options.ApplicationMessageInterceptor = c => - //{ - // if (c.ApplicationMessage.Payload == null || c.ApplicationMessage.Payload.Length == 0) - // { - // return; - // } - - // try - // { - // var content = JObject.Parse(Encoding.UTF8.GetString(c.ApplicationMessage.Payload)); - // var timestampProperty = content.Property("timestamp"); - // if (timestampProperty != null && timestampProperty.Value.Type == JTokenType.Null) - // { - // timestampProperty.Value = DateTime.Now.ToString("O"); - // c.ApplicationMessage.Payload = Encoding.UTF8.GetBytes(content.ToString()); - // } - // } - // catch (Exception) - // { - // } - //}; - - mqttServer.ClientConnectedAsync += e => - { - Console.Write("Client disconnected event fired."); - return CompletedTask.Instance; - }; - - await mqttServer.StartAsync(); - - Console.WriteLine("Press any key to exit."); - Console.ReadLine(); - - await mqttServer.StopAsync(); - } - catch (Exception e) - { - Console.WriteLine(e); - } - - Console.ReadLine(); - } + Console.WriteLine("Press any key to exit."); + Console.ReadLine(); } -} + + public static void RunEmptyServerWithLogging() + { + var logger = new MqttNetEventLogger(); + MqttNetConsoleLogger.ForwardToConsole(logger); + + var mqttServerFactory = new MqttServerFactory(logger); + var mqttServer = mqttServerFactory.CreateMqttServer(new MqttServerOptions()); + mqttServer.StartAsync().GetAwaiter().GetResult(); + + Console.WriteLine("Press any key to exit."); + Console.ReadLine(); + } + + public static async Task RunAsync() + { + try + { + var options = new MqttServerOptionsBuilder() + .WithDefaultEndpoint() + .Build(); + + // Extend the timestamp for all messages from clients. + // Protect several topics from being subscribed from every client. + + //var certificate = new X509Certificate(@"C:\certs\test\test.cer", ""); + //options.TlsEndpointOptions.Certificate = certificate.Export(X509ContentType.Cert); + //options.ConnectionBacklog = 5; + //options.DefaultEndpointOptions.IsEnabled = true; + //options.TlsEndpointOptions.IsEnabled = false; + + var mqttServer = new MqttServerFactory().CreateMqttServer(options); + + const string filename = @"C:\MQTT\RetainedMessages.json"; + + mqttServer.RetainedMessageChangedAsync += e => + { + var directory = Path.GetDirectoryName(filename); + if (!Directory.Exists(directory)) + { + Directory.CreateDirectory(directory!); + } + + File.WriteAllText(filename, JsonConvert.SerializeObject(e.StoredRetainedMessages)); + return CompletedTask.Instance; + }; + + mqttServer.RetainedMessagesClearedAsync += _ => + { + File.Delete(filename); + return CompletedTask.Instance; + }; + + mqttServer.LoadingRetainedMessageAsync += e => + { + List retainedMessages; + if (File.Exists(filename)) + { + var json = File.ReadAllText(filename); + retainedMessages = JsonConvert.DeserializeObject>(json); + } + else + { + retainedMessages = new List(); + } + + e.LoadedRetainedMessages = retainedMessages; + + return CompletedTask.Instance; + }; + + mqttServer.InterceptingPublishAsync += e => + { + if (MqttTopicFilterComparer.Compare(e.ApplicationMessage.Topic, "/myTopic/WithTimestamp/#") == MqttTopicFilterCompareResult.IsMatch) + { + // Replace the payload with the timestamp. But also extending a JSON + // based payload with the timestamp is a suitable use case. + e.ApplicationMessage.PayloadSegment = new ArraySegment(Encoding.UTF8.GetBytes(DateTime.Now.ToString("O"))); + } + + if (e.ApplicationMessage.Topic == "not_allowed_topic") + { + e.ProcessPublish = false; + e.CloseConnection = true; + } + + return CompletedTask.Instance; + }; + + mqttServer.ValidatingConnectionAsync += e => + { + if (e.ClientId == "SpecialClient") + { + if (e.UserName != "USER" || e.Password != "PASS") + { + e.ReasonCode = MqttConnectReasonCode.BadUserNameOrPassword; + } + } + + return CompletedTask.Instance; + }; + + mqttServer.InterceptingSubscriptionAsync += e => + { + if (e.TopicFilter.Topic.StartsWith("admin/foo/bar") && e.ClientId != "theAdmin") + { + e.Response.ReasonCode = MqttSubscribeReasonCode.ImplementationSpecificError; + } + + if (e.TopicFilter.Topic.StartsWith("the/secret/stuff") && e.ClientId != "Imperator") + { + e.Response.ReasonCode = MqttSubscribeReasonCode.ImplementationSpecificError; + e.CloseConnection = true; + } + + return CompletedTask.Instance; + }; + + mqttServer.InterceptingPublishAsync += e => + { + var payloadText = string.Empty; + if (e.ApplicationMessage.Payload.Length > 0) + { + payloadText = Encoding.UTF8.GetString(e.ApplicationMessage.Payload); + } + + MqttNetConsoleLogger.PrintToConsole($"'{e.ClientId}' reported '{e.ApplicationMessage.Topic}' > '{payloadText}'", ConsoleColor.Magenta); + return CompletedTask.Instance; + }; + + //options.ApplicationMessageInterceptor = c => + //{ + // if (c.ApplicationMessage.Payload == null || c.ApplicationMessage.Payload.Length == 0) + // { + // return; + // } + + // try + // { + // var content = JObject.Parse(Encoding.UTF8.GetString(c.ApplicationMessage.Payload)); + // var timestampProperty = content.Property("timestamp"); + // if (timestampProperty != null && timestampProperty.Value.Type == JTokenType.Null) + // { + // timestampProperty.Value = DateTime.Now.ToString("O"); + // c.ApplicationMessage.Payload = Encoding.UTF8.GetBytes(content.ToString()); + // } + // } + // catch (Exception) + // { + // } + //}; + + mqttServer.ClientConnectedAsync += _ => + { + Console.Write("Client disconnected event fired."); + return CompletedTask.Instance; + }; + + await mqttServer.StartAsync(); + + Console.WriteLine("Press any key to exit."); + Console.ReadLine(); + + await mqttServer.StopAsync(); + } + catch (Exception e) + { + Console.WriteLine(e); + } + + Console.ReadLine(); + } +} \ No newline at end of file diff --git a/Source/MQTTnet.TestApp/TopicGenerator.cs b/Source/MQTTnet.TestApp/TopicGenerator.cs index ad7014a0..76ba1a61 100644 --- a/Source/MQTTnet.TestApp/TopicGenerator.cs +++ b/Source/MQTTnet.TestApp/TopicGenerator.cs @@ -2,97 +2,96 @@ using System; using System.Collections.Generic; using MQTTnet.Extensions.TopicTemplate; -namespace MQTTnet.TestApp +namespace MQTTnet.TestApp; + +public class TopicGenerator { - public class TopicGenerator + public void Generate( + int numPublishers, int numTopicsPerPublisher, + out Dictionary> topicsByPublisher, + out Dictionary> singleWildcardTopicsByPublisher, + out Dictionary> multiWildcardTopicsByPublisher + ) { - public void Generate( - int numPublishers, int numTopicsPerPublisher, - out Dictionary> topicsByPublisher, - out Dictionary> singleWildcardTopicsByPublisher, - out Dictionary> multiWildcardTopicsByPublisher - ) + topicsByPublisher = new Dictionary>(); + singleWildcardTopicsByPublisher = new Dictionary>(); + multiWildcardTopicsByPublisher = new Dictionary>(); + + MqttTopicTemplate baseTemplate = new MqttTopicTemplate("{publisher}/{building}/{level}/{sensor}"); + + // Find some reasonable distribution across three topic levels + var topicsPerLevel = (int)Math.Pow(numTopicsPerPublisher, (1.0 / 3.0)); + + int numLevel1Topics = topicsPerLevel; + if (numLevel1Topics <= 0) { - topicsByPublisher = new Dictionary>(); - singleWildcardTopicsByPublisher = new Dictionary>(); - multiWildcardTopicsByPublisher = new Dictionary>(); + numLevel1Topics = 1; + } + int numLevel2Topics = topicsPerLevel; + if (numLevel2Topics <= 0) + { + numLevel2Topics = 1; + } + var maxNumLevel3Topics = 1 + (int)((double)numTopicsPerPublisher / numLevel1Topics / numLevel2Topics); + if (maxNumLevel3Topics <= 0) + { + maxNumLevel3Topics = 1; + } + for (var p = 0; p < numPublishers; ++p) + { + int publisherTopicCount = 0; - MqttTopicTemplate baseTemplate = new MqttTopicTemplate("{publisher}/{building}/{level}/{sensor}"); - - // Find some reasonable distribution across three topic levels - var topicsPerLevel = (int)Math.Pow(numTopicsPerPublisher, (1.0 / 3.0)); - - int numLevel1Topics = topicsPerLevel; - if (numLevel1Topics <= 0) + var publisherName = "pub" + p; + var publisherTemplate = baseTemplate + .WithParameter("publisher", publisherName); + for (var l1 = 0; l1 < numLevel1Topics; ++l1) { - numLevel1Topics = 1; - } - int numLevel2Topics = topicsPerLevel; - if (numLevel2Topics <= 0) - { - numLevel2Topics = 1; - } - var maxNumLevel3Topics = 1 + (int)((double)numTopicsPerPublisher / numLevel1Topics / numLevel2Topics); - if (maxNumLevel3Topics <= 0) - { - maxNumLevel3Topics = 1; - } - for (var p = 0; p < numPublishers; ++p) - { - int publisherTopicCount = 0; - - var publisherName = "pub" + p; - var publisherTemplate = baseTemplate - .WithParameter("publisher", publisherName); - for (var l1 = 0; l1 < numLevel1Topics; ++l1) + var l1Template = publisherTemplate + .WithParameter("building", "building" + (l1 + 1)); + for (var l2 = 0; l2 < numLevel2Topics; ++l2) { - var l1Template = publisherTemplate - .WithParameter("building", "building" + (l1 + 1)); - for (var l2 = 0; l2 < numLevel2Topics; ++l2) + var l2Template = l1Template + .WithParameter("level", "level" + (l2 + 1)); + for (var l3 = 0; l3 < maxNumLevel3Topics; ++l3) { - var l2Template = l1Template - .WithParameter("level", "level" + (l2 + 1)); - for (var l3 = 0; l3 < maxNumLevel3Topics; ++l3) - { - if (publisherTopicCount >= numTopicsPerPublisher) - break; + if (publisherTopicCount >= numTopicsPerPublisher) + break; - var topic = l2Template + var topic = l2Template + .WithParameter("sensor", "sensor" + (l3 + 1)) + .TopicFilter; + AddPublisherTopic(publisherName, topic, topicsByPublisher); + + if (l2 == 0) + { + var singleWildcardTopic = l1Template .WithParameter("sensor", "sensor" + (l3 + 1)) .TopicFilter; - AddPublisherTopic(publisherName, topic, topicsByPublisher); - - if (l2 == 0) + AddPublisherTopic(publisherName, singleWildcardTopic, singleWildcardTopicsByPublisher); + if (l1 == 0) { - var singleWildcardTopic = l1Template - .WithParameter("sensor", "sensor" + (l3 + 1)) - .TopicFilter; - AddPublisherTopic(publisherName, singleWildcardTopic, singleWildcardTopicsByPublisher); - if (l1 == 0) - { - var multiWildcardTopic = publisherTemplate - .WithParameter("sensor", "sensor" + (l3 + 1)) - .TopicFilter; - AddPublisherTopic(publisherName, multiWildcardTopic, multiWildcardTopicsByPublisher); - } + var multiWildcardTopic = publisherTemplate + .WithParameter("sensor", "sensor" + (l3 + 1)) + .TopicFilter; + AddPublisherTopic(publisherName, multiWildcardTopic, multiWildcardTopicsByPublisher); } - - ++publisherTopicCount; } + + ++publisherTopicCount; } } } } - - void AddPublisherTopic(string publisherName, string topic, Dictionary> topicsByPublisher) - { - List topicList; - if (!topicsByPublisher.TryGetValue(publisherName, out topicList)) - { - topicList = new List(); - topicsByPublisher.Add(publisherName, topicList); - } - topicList.Add(topic); - } } -} + + void AddPublisherTopic(string publisherName, string topic, Dictionary> topicsByPublisher) + { + List topicList; + if (!topicsByPublisher.TryGetValue(publisherName, out topicList)) + { + topicList = new List(); + topicsByPublisher.Add(publisherName, topicList); + } + topicList.Add(topic); + } +} \ No newline at end of file diff --git a/Source/MQTTnet.Tests/ASP/Mockups/DuplexPipeMockup.cs b/Source/MQTTnet.Tests/ASP/Mockups/DuplexPipeMockup.cs index 773000df..a6a9195f 100644 --- a/Source/MQTTnet.Tests/ASP/Mockups/DuplexPipeMockup.cs +++ b/Source/MQTTnet.Tests/ASP/Mockups/DuplexPipeMockup.cs @@ -4,24 +4,23 @@ using System.IO.Pipelines; -namespace MQTTnet.Tests.ASP.Mockups +namespace MQTTnet.Tests.ASP.Mockups; + +public sealed class DuplexPipeMockup : IDuplexPipe { - public sealed class DuplexPipeMockup : IDuplexPipe + public DuplexPipeMockup() { - public DuplexPipeMockup() - { - var pool = new LimitedMemoryPool(); - var pipeOptions = new PipeOptions(pool); - Receive = new Pipe(pipeOptions); - Send = new Pipe(pipeOptions); - } - - PipeReader IDuplexPipe.Input => Receive.Reader; - - PipeWriter IDuplexPipe.Output => Send.Writer; - - public Pipe Receive { get; } - - public Pipe Send { get; } + var pool = new LimitedMemoryPool(); + var pipeOptions = new PipeOptions(pool); + Receive = new Pipe(pipeOptions); + Send = new Pipe(pipeOptions); } -} + + PipeReader IDuplexPipe.Input => Receive.Reader; + + PipeWriter IDuplexPipe.Output => Send.Writer; + + public Pipe Receive { get; } + + public Pipe Send { get; } +} \ No newline at end of file diff --git a/Source/MQTTnet.Tests/ASP/Mockups/LimitedMemoryPool.cs b/Source/MQTTnet.Tests/ASP/Mockups/LimitedMemoryPool.cs index acb1fd19..b9755954 100644 --- a/Source/MQTTnet.Tests/ASP/Mockups/LimitedMemoryPool.cs +++ b/Source/MQTTnet.Tests/ASP/Mockups/LimitedMemoryPool.cs @@ -4,19 +4,18 @@ using System.Buffers; -namespace MQTTnet.Tests.ASP.Mockups +namespace MQTTnet.Tests.ASP.Mockups; + +public sealed class LimitedMemoryPool : MemoryPool { - public sealed class LimitedMemoryPool : MemoryPool + protected override void Dispose(bool disposing) { - protected override void Dispose(bool disposing) - { - } - - public override IMemoryOwner Rent(int minBufferSize = -1) - { - return new MemoryOwner(minBufferSize); - } - - public override int MaxBufferSize { get; } } + + public override IMemoryOwner Rent(int minBufferSize = -1) + { + return new MemoryOwner(minBufferSize); + } + + public override int MaxBufferSize { get; } = 1; } \ No newline at end of file diff --git a/Source/MQTTnet.Tests/ASP/Mockups/MemoryOwner.cs b/Source/MQTTnet.Tests/ASP/Mockups/MemoryOwner.cs index 6fc8a83d..c1607302 100644 --- a/Source/MQTTnet.Tests/ASP/Mockups/MemoryOwner.cs +++ b/Source/MQTTnet.Tests/ASP/Mockups/MemoryOwner.cs @@ -5,33 +5,32 @@ using System; using System.Buffers; -namespace MQTTnet.Tests.ASP.Mockups +namespace MQTTnet.Tests.ASP.Mockups; + +public sealed class MemoryOwner : IMemoryOwner { - public sealed class MemoryOwner : IMemoryOwner + readonly byte[] _raw; + + public MemoryOwner(int size) { - readonly byte[] _raw; - - public MemoryOwner(int size) + if (size <= 0) { - if (size <= 0) - { - size = 1024; - } - - if (size > 4096) - { - size = 4096; - } - - _raw = ArrayPool.Shared.Rent(size); - Memory = _raw; + size = 1024; } - public Memory Memory { get; } - - public void Dispose() + if (size > 4096) { - ArrayPool.Shared.Return(_raw); + size = 4096; } + + _raw = ArrayPool.Shared.Rent(size); + Memory = _raw; + } + + public Memory Memory { get; } + + public void Dispose() + { + ArrayPool.Shared.Return(_raw); } } \ No newline at end of file diff --git a/Source/MQTTnet.Tests/ASP/MqttConnectionContextTest.cs b/Source/MQTTnet.Tests/ASP/MqttConnectionContextTest.cs index bfd0f843..2f33f965 100644 --- a/Source/MQTTnet.Tests/ASP/MqttConnectionContextTest.cs +++ b/Source/MQTTnet.Tests/ASP/MqttConnectionContextTest.cs @@ -3,7 +3,6 @@ // See the LICENSE file in the project root for more information. using System; -using System.Buffers; using System.Threading; using System.Threading.Tasks; using Microsoft.AspNetCore.Builder; @@ -17,114 +16,113 @@ using MQTTnet.Protocol; using MQTTnet.Tests.ASP.Mockups; using MQTTnet.Tests.Helpers; -namespace MQTTnet.Tests.ASP +namespace MQTTnet.Tests.ASP; + +[TestClass] +public class MqttConnectionContextTest { - [TestClass] - public class MqttConnectionContextTest + [TestMethod] + public async Task TestCorruptedConnectPacket() { - [TestMethod] - public async Task TestCorruptedConnectPacket() + var writer = new MqttBufferWriter(4096, 65535); + var serializer = new MqttPacketFormatterAdapter(writer); + var pipe = new DuplexPipeMockup(); + var connection = new DefaultConnectionContext(); + connection.Transport = pipe; + var ctx = new MqttConnectionContext(serializer, connection); + + await pipe.Receive.Writer.WriteAsync(writer.AddMqttHeader(MqttControlPacketType.Connect, [])); + + await Assert.ThrowsExceptionAsync(() => ctx.ReceivePacketAsync(CancellationToken.None)); + + // the first exception should complete the pipes so if someone tries to use the connection after that it should throw immidiatly + await Assert.ThrowsExceptionAsync(() => ctx.ReceivePacketAsync(CancellationToken.None)).ConfigureAwait(false); + } + + // TODO: Fix test + // [TestMethod] + // public async Task TestEndpoint() + // { + // var mockup = new ConnectionHandlerMockup(); + // + // using (var host = new WebHostBuilder().UseKestrel(kestrel => kestrel.ListenLocalhost(1883, listener => listener.Use((ctx, next) => mockup.OnConnectedAsync(ctx)))) + // .UseStartup() + // .ConfigureServices( + // (hostContext, services) => + // { + // services.AddHostedMqttServer(o => o.WithoutDefaultEndpoint()); + // services.AddSingleton(mockup); + // }) + // .Build()) + // + // using (var client = new MqttFactory().CreateMqttClient()) + // { + // host.Start(); + // await client.ConnectAsync(new MqttClientOptionsBuilder().WithTcpServer("localhost").Build(), CancellationToken.None); + // + // var ctx = await mockup.Context.Task; + // + // var ep = IPEndPoint.Parse(ctx.Endpoint); + // Assert.IsNotNull(ep); + // + // Assert.IsNotNull(ctx); + // } + // } + + // COMMENTED OUT DUE TO DEAD LOCK? OR VERY VERY SLOW PERFORMANCE ON LOCAL DEV MACHINE. TEST WAS STILL RUNNING AFTER SEVERAL MINUTES! + //[TestMethod] + //public async Task TestParallelWrites() + //{ + // var serializer = new MqttPacketFormatterAdapter(MqttProtocolVersion.V311); + // var pipe = new DuplexPipeMockup(); + // var connection = new DefaultConnectionContext(); + // connection.Transport = pipe; + // var ctx = new MqttConnectionContext(serializer, connection); + + // var tasks = Enumerable.Range(1, 100).Select(_ => Task.Run(async () => + // { + // for (int i = 0; i < 100; i++) + // { + // await ctx.SendPacketAsync(new MqttPublishPacket(), TimeSpan.Zero, CancellationToken.None).ConfigureAwait(false); + // } + // })); + + // await Task.WhenAll(tasks).ConfigureAwait(false); + //} + + [TestMethod] + public async Task TestLargePacket() + { + var serializer = new MqttPacketFormatterAdapter(MqttProtocolVersion.V311, new MqttBufferWriter(4096, 65535)); + var pipe = new DuplexPipeMockup(); + var connection = new DefaultConnectionContext(); + connection.Transport = pipe; + var ctx = new MqttConnectionContext(serializer, connection); + + await ctx.SendPacketAsync(new MqttPublishPacket { PayloadSegment = new byte[20_000] }, CancellationToken.None).ConfigureAwait(false); + + var readResult = await pipe.Send.Reader.ReadAsync(); + Assert.IsTrue(readResult.Buffer.Length > 20000); + } + + [TestMethod] + public async Task TestReceivePacketAsyncThrowsWhenReaderCompleted() + { + var serializer = new MqttPacketFormatterAdapter(MqttProtocolVersion.V311, new MqttBufferWriter(4096, 65535)); + var pipe = new DuplexPipeMockup(); + var connection = new DefaultConnectionContext(); + connection.Transport = pipe; + var ctx = new MqttConnectionContext(serializer, connection); + + await pipe.Receive.Writer.CompleteAsync(); + + await Assert.ThrowsExceptionAsync(() => ctx.ReceivePacketAsync(CancellationToken.None)).ConfigureAwait(false); + } + + class Startup + { + public void Configure(IApplicationBuilder app) { - var writer = new MqttBufferWriter(4096, 65535); - var serializer = new MqttPacketFormatterAdapter(writer); - var pipe = new DuplexPipeMockup(); - var connection = new DefaultConnectionContext(); - connection.Transport = pipe; - var ctx = new MqttConnectionContext(serializer, connection); - - await pipe.Receive.Writer.WriteAsync(writer.AddMqttHeader(MqttControlPacketType.Connect, Array.Empty())); - - await Assert.ThrowsExceptionAsync(() => ctx.ReceivePacketAsync(CancellationToken.None)); - - // the first exception should complete the pipes so if someone tries to use the connection after that it should throw immidiatly - await Assert.ThrowsExceptionAsync(() => ctx.ReceivePacketAsync(CancellationToken.None)); - } - - // TODO: Fix test - // [TestMethod] - // public async Task TestEndpoint() - // { - // var mockup = new ConnectionHandlerMockup(); - // - // using (var host = new WebHostBuilder().UseKestrel(kestrel => kestrel.ListenLocalhost(1883, listener => listener.Use((ctx, next) => mockup.OnConnectedAsync(ctx)))) - // .UseStartup() - // .ConfigureServices( - // (hostContext, services) => - // { - // services.AddHostedMqttServer(o => o.WithoutDefaultEndpoint()); - // services.AddSingleton(mockup); - // }) - // .Build()) - // - // using (var client = new MqttFactory().CreateMqttClient()) - // { - // host.Start(); - // await client.ConnectAsync(new MqttClientOptionsBuilder().WithTcpServer("localhost").Build(), CancellationToken.None); - // - // var ctx = await mockup.Context.Task; - // - // var ep = IPEndPoint.Parse(ctx.Endpoint); - // Assert.IsNotNull(ep); - // - // Assert.IsNotNull(ctx); - // } - // } - - // COMMENTED OUT DUE TO DEAD LOCK? OR VERY VERY SLOW PERFORMANCE ON LOCAL DEV MACHINE. TEST WAS STILL RUNNING AFTER SEVERAL MINUTES! - //[TestMethod] - //public async Task TestParallelWrites() - //{ - // var serializer = new MqttPacketFormatterAdapter(MqttProtocolVersion.V311); - // var pipe = new DuplexPipeMockup(); - // var connection = new DefaultConnectionContext(); - // connection.Transport = pipe; - // var ctx = new MqttConnectionContext(serializer, connection); - - // var tasks = Enumerable.Range(1, 100).Select(_ => Task.Run(async () => - // { - // for (int i = 0; i < 100; i++) - // { - // await ctx.SendPacketAsync(new MqttPublishPacket(), TimeSpan.Zero, CancellationToken.None).ConfigureAwait(false); - // } - // })); - - // await Task.WhenAll(tasks).ConfigureAwait(false); - //} - - [TestMethod] - public async Task TestLargePacket() - { - var serializer = new MqttPacketFormatterAdapter(MqttProtocolVersion.V311, new MqttBufferWriter(4096, 65535)); - var pipe = new DuplexPipeMockup(); - var connection = new DefaultConnectionContext(); - connection.Transport = pipe; - var ctx = new MqttConnectionContext(serializer, connection); - - await ctx.SendPacketAsync(new MqttPublishPacket { PayloadSegment = new byte[20_000] }, CancellationToken.None).ConfigureAwait(false); - - var readResult = await pipe.Send.Reader.ReadAsync(); - Assert.IsTrue(readResult.Buffer.Length > 20000); - } - - [TestMethod] - public async Task TestReceivePacketAsyncThrowsWhenReaderCompleted() - { - var serializer = new MqttPacketFormatterAdapter(MqttProtocolVersion.V311, new MqttBufferWriter(4096, 65535)); - var pipe = new DuplexPipeMockup(); - var connection = new DefaultConnectionContext(); - connection.Transport = pipe; - var ctx = new MqttConnectionContext(serializer, connection); - - pipe.Receive.Writer.Complete(); - - await Assert.ThrowsExceptionAsync(() => ctx.ReceivePacketAsync(CancellationToken.None)); - } - - class Startup - { - public void Configure(IApplicationBuilder app) - { - } } } } \ No newline at end of file diff --git a/Source/MQTTnet.Tests/ASP/ReaderExtensionsTest.cs b/Source/MQTTnet.Tests/ASP/ReaderExtensionsTest.cs index 6c9cac8f..ce0a82d6 100644 --- a/Source/MQTTnet.Tests/ASP/ReaderExtensionsTest.cs +++ b/Source/MQTTnet.Tests/ASP/ReaderExtensionsTest.cs @@ -22,25 +22,20 @@ public sealed class ReaderExtensionsTest var sequence = new ReadOnlySequence(buffer.Array, buffer.Offset, buffer.Count); - var part = sequence; - var consumed = part.Start; - var observed = part.Start; - var read = 0; - - part = sequence.Slice(sequence.Start, 0); // empty message should fail - var result = serializer.TryDecode(part, out _, out consumed, out observed, out read); + var part = sequence.Slice(sequence.Start, 0); // empty message should fail + var result = serializer.TryDecode(part, out _, out _, out _, out _); Assert.IsFalse(result); part = sequence.Slice(sequence.Start, 1); // partial fixed header should fail - result = serializer.TryDecode(part, out _, out consumed, out observed, out read); + result = serializer.TryDecode(part, out _, out _, out _, out _); Assert.IsFalse(result); part = sequence.Slice(sequence.Start, 4); // partial body should fail - result = serializer.TryDecode(part, out _, out consumed, out observed, out read); + result = serializer.TryDecode(part, out _, out _, out _, out _); Assert.IsFalse(result); part = sequence; // complete msg should work - result = serializer.TryDecode(part, out _, out consumed, out observed, out read); + result = serializer.TryDecode(part, out _, out _, out _, out _); Assert.IsTrue(result); } } \ No newline at end of file diff --git a/Source/MQTTnet.Tests/BaseTestClass.cs b/Source/MQTTnet.Tests/BaseTestClass.cs index 00291b02..aabef5ae 100644 --- a/Source/MQTTnet.Tests/BaseTestClass.cs +++ b/Source/MQTTnet.Tests/BaseTestClass.cs @@ -8,21 +8,20 @@ using Microsoft.VisualStudio.TestTools.UnitTesting; using MQTTnet.Formatter; using MQTTnet.Tests.Mockups; -namespace MQTTnet.Tests +namespace MQTTnet.Tests; + +public abstract class BaseTestClass { - public abstract class BaseTestClass + public TestContext TestContext { get; set; } + + protected TestEnvironment CreateTestEnvironment( + MqttProtocolVersion protocolVersion = MqttProtocolVersion.V311, bool trackUnobservedTaskException = true) { - public TestContext TestContext { get; set; } + return new TestEnvironment(TestContext, protocolVersion, trackUnobservedTaskException); + } - protected TestEnvironment CreateTestEnvironment( - MqttProtocolVersion protocolVersion = MqttProtocolVersion.V311, bool trackUnobservedTaskException = true) - { - return new TestEnvironment(TestContext, protocolVersion, trackUnobservedTaskException); - } - - protected Task LongTestDelay() - { - return Task.Delay(TimeSpan.FromSeconds(1)); - } + protected Task LongTestDelay() + { + return Task.Delay(TimeSpan.FromSeconds(1)); } } \ No newline at end of file diff --git a/Source/MQTTnet.Tests/Clients/LowLevelMqttClient/LowLevelMqttClient_Tests.cs b/Source/MQTTnet.Tests/Clients/LowLevelMqttClient/LowLevelMqttClient_Tests.cs index 4c8a44ad..2e308a5b 100644 --- a/Source/MQTTnet.Tests/Clients/LowLevelMqttClient/LowLevelMqttClient_Tests.cs +++ b/Source/MQTTnet.Tests/Clients/LowLevelMqttClient/LowLevelMqttClient_Tests.cs @@ -14,207 +14,194 @@ using MQTTnet.Packets; using MQTTnet.Protocol; using MQTTnet.Server; -namespace MQTTnet.Tests.Clients.LowLevelMqttClient +namespace MQTTnet.Tests.Clients.LowLevelMqttClient; + +// ReSharper disable InconsistentNaming +[TestClass] +public sealed class LowLevelMqttClient_Tests : BaseTestClass { - [TestClass] - public sealed class LowLevelMqttClient_Tests : BaseTestClass + [TestMethod] + public async Task Authenticate() { - [TestMethod] - public async Task Authenticate() + using var testEnvironment = CreateTestEnvironment(); + await testEnvironment.StartServer(); + + var factory = new MqttClientFactory(); + var lowLevelClient = factory.CreateLowLevelMqttClient(); + + await lowLevelClient.ConnectAsync(new MqttClientOptionsBuilder().WithTcpServer("127.0.0.1", testEnvironment.ServerPort).Build(), CancellationToken.None); + + var receivedPacket = await Authenticate(lowLevelClient).ConfigureAwait(false); + + await lowLevelClient.DisconnectAsync(CancellationToken.None).ConfigureAwait(false); + + Assert.IsNotNull(receivedPacket); + Assert.AreEqual(MqttConnectReturnCode.ConnectionAccepted, receivedPacket.ReturnCode); + } + + [TestMethod] + public async Task Connect_And_Disconnect() + { + using var testEnvironment = CreateTestEnvironment(); + await testEnvironment.StartServer(); + + var lowLevelClient = testEnvironment.ClientFactory.CreateLowLevelMqttClient(); + + await lowLevelClient.ConnectAsync(new MqttClientOptionsBuilder().WithTcpServer("127.0.0.1", testEnvironment.ServerPort).Build(), CancellationToken.None); + + await lowLevelClient.DisconnectAsync(CancellationToken.None); + } + + [TestMethod] + [ExpectedException(typeof(MqttCommunicationException))] + public async Task Connect_To_Not_Existing_Broker() + { + var client = new MqttClientFactory().CreateLowLevelMqttClient(); + var options = new MqttClientOptionsBuilder().WithTcpServer("localhost").Build(); + + await client.ConnectAsync(options, CancellationToken.None).ConfigureAwait(false); + } + + [TestMethod] + [ExpectedException(typeof(MqttCommunicationException))] + public async Task Connect_To_Wrong_Host() + { + var client = new MqttClientFactory().CreateLowLevelMqttClient(); + var options = new MqttClientOptionsBuilder().WithTcpServer("123.456.789.10").Build(); + + await client.ConnectAsync(options, CancellationToken.None).ConfigureAwait(false); + } + + [TestMethod] + public async Task Loose_Connection() + { + using var testEnvironment = CreateTestEnvironment(); + testEnvironment.IgnoreServerLogErrors = true; + + testEnvironment.ServerPort = 8364; + var server = await testEnvironment.StartServer(); + + var client = await testEnvironment.ConnectLowLevelClient(o => o.WithTimeout(TimeSpan.Zero)); + + await Authenticate(client).ConfigureAwait(false); + + await server.StopAsync(); + + await Task.Delay(2000); + + try { - using (var testEnvironment = CreateTestEnvironment()) - { - await testEnvironment.StartServer(); - - var factory = new MqttClientFactory(); - var lowLevelClient = factory.CreateLowLevelMqttClient(); - - await lowLevelClient.ConnectAsync(new MqttClientOptionsBuilder().WithTcpServer("127.0.0.1", testEnvironment.ServerPort).Build(), CancellationToken.None); - - var receivedPacket = await Authenticate(lowLevelClient).ConfigureAwait(false); - - await lowLevelClient.DisconnectAsync(CancellationToken.None).ConfigureAwait(false); - - Assert.IsNotNull(receivedPacket); - Assert.AreEqual(MqttConnectReturnCode.ConnectionAccepted, receivedPacket.ReturnCode); - } + await client.SendAsync(MqttPingReqPacket.Instance, CancellationToken.None).ConfigureAwait(false); + await Task.Delay(2000); + await client.SendAsync(MqttPingReqPacket.Instance, CancellationToken.None).ConfigureAwait(false); + } + catch (MqttCommunicationException exception) + { + Assert.IsTrue(exception.InnerException is SocketException); + return; + } + catch + { + Assert.Fail("Wrong exception type thrown."); } - [TestMethod] - public async Task Connect_And_Disconnect() + Assert.Fail("This MUST fail"); + } + + [TestMethod] + public async Task Maintain_IsConnected_Property() + { + using var testEnvironment = CreateTestEnvironment(); + testEnvironment.IgnoreServerLogErrors = true; + + var server = await testEnvironment.StartServer(); + + using var lowLevelClient = testEnvironment.CreateLowLevelClient(); + Assert.IsFalse(lowLevelClient.IsConnected); + + var clientOptions = new MqttClientOptionsBuilder().WithTcpServer("127.0.0.1", testEnvironment.ServerPort).WithTimeout(TimeSpan.FromSeconds(1)).Build(); + + await lowLevelClient.ConnectAsync(clientOptions, CancellationToken.None); + + Assert.IsTrue(lowLevelClient.IsConnected); + + await server.StopAsync(); + server.Dispose(); + + await LongTestDelay(); + + Assert.IsTrue(lowLevelClient.IsConnected); + + try { - using (var testEnvironment = CreateTestEnvironment()) - { - await testEnvironment.StartServer(); + using var timeout = new CancellationTokenSource(TimeSpan.FromSeconds(5)); + await lowLevelClient.SendAsync(MqttPingReqPacket.Instance, CancellationToken.None); + await LongTestDelay(); - var lowLevelClient = testEnvironment.ClientFactory.CreateLowLevelMqttClient(); - - await lowLevelClient.ConnectAsync(new MqttClientOptionsBuilder().WithTcpServer("127.0.0.1", testEnvironment.ServerPort).Build(), CancellationToken.None); - - await lowLevelClient.DisconnectAsync(CancellationToken.None); - } + await lowLevelClient.ReceiveAsync(timeout.Token); + } + catch + { + // The connection is now closed! } - [TestMethod] - [ExpectedException(typeof(MqttCommunicationException))] - public async Task Connect_To_Not_Existing_Broker() - { - var client = new MqttClientFactory().CreateLowLevelMqttClient(); - var options = new MqttClientOptionsBuilder().WithTcpServer("localhost").Build(); + Assert.IsFalse(lowLevelClient.IsConnected); + } - await client.ConnectAsync(options, CancellationToken.None).ConfigureAwait(false); - } + [TestMethod] + public async Task Subscribe() + { + using var testEnvironment = CreateTestEnvironment(); + await testEnvironment.StartServer(); - [TestMethod] - [ExpectedException(typeof(MqttCommunicationException))] - public async Task Connect_To_Wrong_Host() - { - var client = new MqttClientFactory().CreateLowLevelMqttClient(); - var options = new MqttClientOptionsBuilder().WithTcpServer("123.456.789.10").Build(); + var factory = new MqttClientFactory(); + var lowLevelClient = factory.CreateLowLevelMqttClient(); - await client.ConnectAsync(options, CancellationToken.None).ConfigureAwait(false); - } + await lowLevelClient.ConnectAsync(new MqttClientOptionsBuilder().WithTcpServer("127.0.0.1", testEnvironment.ServerPort).Build(), CancellationToken.None); - [TestMethod] - public async Task Loose_Connection() - { - using (var testEnvironment = CreateTestEnvironment()) - { - testEnvironment.IgnoreServerLogErrors = true; + await Authenticate(lowLevelClient).ConfigureAwait(false); - testEnvironment.ServerPort = 8364; - var server = await testEnvironment.StartServer(); + var receivedPacket = await Subscribe(lowLevelClient, "a").ConfigureAwait(false); - var client = await testEnvironment.ConnectLowLevelClient(o => o.WithTimeout(TimeSpan.Zero)); + await lowLevelClient.DisconnectAsync(CancellationToken.None).ConfigureAwait(false); - await Authenticate(client).ConfigureAwait(false); + Assert.IsNotNull(receivedPacket); + Assert.AreEqual(MqttSubscribeReasonCode.GrantedQoS0, receivedPacket.ReasonCodes[0]); + } - await server.StopAsync(); - - await Task.Delay(2000); - - try + async Task Authenticate(ILowLevelMqttClient client) + { + await client.SendAsync( + new MqttConnectPacket { - await client.SendAsync(MqttPingReqPacket.Instance, CancellationToken.None).ConfigureAwait(false); - await Task.Delay(2000); - await client.SendAsync(MqttPingReqPacket.Instance, CancellationToken.None).ConfigureAwait(false); - } - catch (MqttCommunicationException exception) + CleanSession = true, + ClientId = TestContext.TestName, + Username = "user", + Password = Encoding.UTF8.GetBytes("pass") + }, + CancellationToken.None) + .ConfigureAwait(false); + + return await client.ReceiveAsync(CancellationToken.None).ConfigureAwait(false) as MqttConnAckPacket; + } + + async Task Subscribe(ILowLevelMqttClient client, string topic) + { + await client.SendAsync( + new MqttSubscribePacket { - Assert.IsTrue(exception.InnerException is SocketException); - return; - } - catch - { - Assert.Fail("Wrong exception type thrown."); - } - - Assert.Fail("This MUST fail"); - } - } - - [TestMethod] - public async Task Maintain_IsConnected_Property() - { - using (var testEnvironment = CreateTestEnvironment()) - { - testEnvironment.IgnoreServerLogErrors = true; - - var server = await testEnvironment.StartServer(); - - using (var lowLevelClient = testEnvironment.CreateLowLevelClient()) - { - Assert.IsFalse(lowLevelClient.IsConnected); - - var clientOptions = new MqttClientOptionsBuilder().WithTcpServer("127.0.0.1", testEnvironment.ServerPort).WithTimeout(TimeSpan.FromSeconds(1)).Build(); - - await lowLevelClient.ConnectAsync(clientOptions, CancellationToken.None); - - Assert.IsTrue(lowLevelClient.IsConnected); - - await server.StopAsync(); - server.Dispose(); - - await LongTestDelay(); - - Assert.IsTrue(lowLevelClient.IsConnected); - - try + PacketIdentifier = 1, + TopicFilters = { - using (var timeout = new CancellationTokenSource(TimeSpan.FromSeconds(5))) + new MqttTopicFilter { - await lowLevelClient.SendAsync(MqttPingReqPacket.Instance, CancellationToken.None); - await LongTestDelay(); - - await lowLevelClient.ReceiveAsync(timeout.Token); + Topic = topic } } - catch - { - } + }, + CancellationToken.None) + .ConfigureAwait(false); - Assert.IsFalse(lowLevelClient.IsConnected); - } - } - } - - [TestMethod] - public async Task Subscribe() - { - using (var testEnvironment = CreateTestEnvironment()) - { - await testEnvironment.StartServer(); - - var factory = new MqttClientFactory(); - var lowLevelClient = factory.CreateLowLevelMqttClient(); - - await lowLevelClient.ConnectAsync(new MqttClientOptionsBuilder().WithTcpServer("127.0.0.1", testEnvironment.ServerPort).Build(), CancellationToken.None); - - await Authenticate(lowLevelClient).ConfigureAwait(false); - - var receivedPacket = await Subscribe(lowLevelClient, "a").ConfigureAwait(false); - - await lowLevelClient.DisconnectAsync(CancellationToken.None).ConfigureAwait(false); - - Assert.IsNotNull(receivedPacket); - Assert.AreEqual(MqttSubscribeReasonCode.GrantedQoS0, receivedPacket.ReasonCodes[0]); - } - } - - async Task Authenticate(ILowLevelMqttClient client) - { - await client.SendAsync( - new MqttConnectPacket - { - CleanSession = true, - ClientId = TestContext.TestName, - Username = "user", - Password = Encoding.UTF8.GetBytes("pass") - }, - CancellationToken.None) - .ConfigureAwait(false); - - return await client.ReceiveAsync(CancellationToken.None).ConfigureAwait(false) as MqttConnAckPacket; - } - - async Task Subscribe(ILowLevelMqttClient client, string topic) - { - await client.SendAsync( - new MqttSubscribePacket - { - PacketIdentifier = 1, - TopicFilters = - { - new MqttTopicFilter - { - Topic = topic - } - } - }, - CancellationToken.None) - .ConfigureAwait(false); - - return await client.ReceiveAsync(CancellationToken.None).ConfigureAwait(false) as MqttSubAckPacket; - } + return await client.ReceiveAsync(CancellationToken.None).ConfigureAwait(false) as MqttSubAckPacket; } } \ No newline at end of file diff --git a/Source/MQTTnet.Tests/Clients/MqttClient/MqttClient_Connection_Tests.cs b/Source/MQTTnet.Tests/Clients/MqttClient/MqttClient_Connection_Tests.cs index ef8d7569..91c37552 100644 --- a/Source/MQTTnet.Tests/Clients/MqttClient/MqttClient_Connection_Tests.cs +++ b/Source/MQTTnet.Tests/Clients/MqttClient/MqttClient_Connection_Tests.cs @@ -14,284 +14,284 @@ using MQTTnet.Protocol; using MQTTnet.Server; using MQTTnet.Server.EnhancedAuthentication; -namespace MQTTnet.Tests.Clients.MqttClient +namespace MQTTnet.Tests.Clients.MqttClient; + +// ReSharper disable InconsistentNaming +[TestClass] +public sealed class MqttClient_Connection_Tests : BaseTestClass { - [TestClass] - public sealed class MqttClient_Connection_Tests : BaseTestClass + [TestMethod] + [ExpectedException(typeof(MqttCommunicationException))] + public async Task Connect_To_Invalid_Server_Port_Not_Opened() { - [TestMethod] - [ExpectedException(typeof(MqttCommunicationException))] - public async Task Connect_To_Invalid_Server_Port_Not_Opened() - { - var client = new MqttClientFactory().CreateMqttClient(); - using var timeout = new CancellationTokenSource(TimeSpan.FromSeconds(5)); - await client.ConnectAsync(new MqttClientOptionsBuilder().WithTcpServer("127.0.0.1", 12345).Build(), timeout.Token); - } + var client = new MqttClientFactory().CreateMqttClient(); + using var timeout = new CancellationTokenSource(TimeSpan.FromSeconds(5)); + await client.ConnectAsync(new MqttClientOptionsBuilder().WithTcpServer("127.0.0.1", 12345).Build(), timeout.Token); + } - [TestMethod] - [ExpectedException(typeof(OperationCanceledException))] - public async Task Connect_To_Invalid_Server_Wrong_IP() - { - var client = new MqttClientFactory().CreateMqttClient(); - using var timeout = new CancellationTokenSource(TimeSpan.FromSeconds(2)); - await client.ConnectAsync(new MqttClientOptionsBuilder().WithTcpServer("1.2.3.4").Build(), timeout.Token); - } + [TestMethod] + [ExpectedException(typeof(OperationCanceledException))] + public async Task Connect_To_Invalid_Server_Wrong_IP() + { + var client = new MqttClientFactory().CreateMqttClient(); + using var timeout = new CancellationTokenSource(TimeSpan.FromSeconds(2)); + await client.ConnectAsync(new MqttClientOptionsBuilder().WithTcpServer("1.2.3.4").Build(), timeout.Token); + } - [TestMethod] - [ExpectedException(typeof(MqttCommunicationException))] - public async Task Connect_To_Invalid_Server_Wrong_Protocol() - { - var client = new MqttClientFactory().CreateMqttClient(); - await client.ConnectAsync(new MqttClientOptionsBuilder().WithTcpServer("http://127.0.0.1", 12345).WithTimeout(TimeSpan.FromSeconds(2)).Build()); - } + [TestMethod] + [ExpectedException(typeof(MqttCommunicationException))] + public async Task Connect_To_Invalid_Server_Wrong_Protocol() + { + var client = new MqttClientFactory().CreateMqttClient(); + await client.ConnectAsync(new MqttClientOptionsBuilder().WithTcpServer("http://127.0.0.1", 12345).WithTimeout(TimeSpan.FromSeconds(2)).Build()).ConfigureAwait(false); + } - [TestMethod] - public async Task ConnectTimeout_Throws_Exception() + [TestMethod] + public async Task ConnectTimeout_Throws_Exception() + { + var factory = new MqttClientFactory(); + using var client = factory.CreateMqttClient(); + var disconnectHandlerCalled = false; + try { - var factory = new MqttClientFactory(); - using var client = factory.CreateMqttClient(); - var disconnectHandlerCalled = false; - try + client.DisconnectedAsync += _ => { - client.DisconnectedAsync += _ => - { - disconnectHandlerCalled = true; - return CompletedTask.Instance; - }; - - await client.ConnectAsync(new MqttClientOptionsBuilder().WithTcpServer("127.0.0.1").Build()); - - Assert.Fail("Must fail!"); - } - catch (Exception exception) - { - Assert.IsNotNull(exception); - Assert.IsInstanceOfType(exception, typeof(MqttCommunicationException)); - } - - await LongTestDelay(); // disconnected handler is called async - Assert.IsTrue(disconnectHandlerCalled); - } - - [TestMethod] - public async Task Disconnect_Clean() - { - using var testEnvironment = CreateTestEnvironment(MqttProtocolVersion.V500); - var server = await testEnvironment.StartServer(); - - ClientDisconnectedEventArgs eventArgs = null; - server.ClientDisconnectedAsync += args => - { - eventArgs = args; + disconnectHandlerCalled = true; return CompletedTask.Instance; }; - var client = await testEnvironment.ConnectClient(); + await client.ConnectAsync(new MqttClientOptionsBuilder().WithTcpServer("127.0.0.1").Build()); - var disconnectOptions = testEnvironment.ClientFactory.CreateClientDisconnectOptionsBuilder().WithReason(MqttClientDisconnectOptionsReason.MessageRateTooHigh).Build(); - - // Perform a clean disconnect. - await client.DisconnectAsync(disconnectOptions); - - await LongTestDelay(); - - Assert.IsNotNull(eventArgs); - Assert.AreEqual(MqttClientDisconnectType.Clean, eventArgs.DisconnectType); + Assert.Fail("Must fail!"); + } + catch (Exception exception) + { + Assert.IsNotNull(exception); + Assert.IsInstanceOfType(exception, typeof(MqttCommunicationException)); } - [TestMethod] - public async Task Disconnect_Clean_With_Custom_Reason() - { - using var testEnvironment = CreateTestEnvironment(MqttProtocolVersion.V500); - var server = await testEnvironment.StartServer(); + await LongTestDelay(); // disconnected handler is called async + Assert.IsTrue(disconnectHandlerCalled); + } - ClientDisconnectedEventArgs eventArgs = null; - server.ClientDisconnectedAsync += args => + [TestMethod] + public async Task Disconnect_Clean() + { + using var testEnvironment = CreateTestEnvironment(MqttProtocolVersion.V500); + var server = await testEnvironment.StartServer(); + + ClientDisconnectedEventArgs eventArgs = null; + server.ClientDisconnectedAsync += args => + { + eventArgs = args; + return CompletedTask.Instance; + }; + + var client = await testEnvironment.ConnectClient(); + + var disconnectOptions = testEnvironment.ClientFactory.CreateClientDisconnectOptionsBuilder().WithReason(MqttClientDisconnectOptionsReason.MessageRateTooHigh).Build(); + + // Perform a clean disconnect. + await client.DisconnectAsync(disconnectOptions); + + await LongTestDelay(); + + Assert.IsNotNull(eventArgs); + Assert.AreEqual(MqttClientDisconnectType.Clean, eventArgs.DisconnectType); + } + + [TestMethod] + public async Task Disconnect_Clean_With_Custom_Reason() + { + using var testEnvironment = CreateTestEnvironment(MqttProtocolVersion.V500); + var server = await testEnvironment.StartServer(); + + ClientDisconnectedEventArgs eventArgs = null; + server.ClientDisconnectedAsync += args => + { + eventArgs = args; + return CompletedTask.Instance; + }; + + var client = await testEnvironment.ConnectClient(); + + var disconnectOptions = testEnvironment.ClientFactory.CreateClientDisconnectOptionsBuilder().WithReason(MqttClientDisconnectOptionsReason.MessageRateTooHigh).Build(); + + // Perform a clean disconnect. + await client.DisconnectAsync(disconnectOptions); + + await LongTestDelay(); + + Assert.IsNotNull(eventArgs); + Assert.AreEqual(MqttDisconnectReasonCode.MessageRateTooHigh, eventArgs.ReasonCode); + } + + [TestMethod] + public async Task Disconnect_Clean_With_User_Properties() + { + using var testEnvironment = CreateTestEnvironment(MqttProtocolVersion.V500); + var server = await testEnvironment.StartServer(); + + ClientDisconnectedEventArgs eventArgs = null; + server.ClientDisconnectedAsync += args => + { + eventArgs = args; + return CompletedTask.Instance; + }; + + var client = await testEnvironment.ConnectClient(); + + var disconnectOptions = testEnvironment.ClientFactory.CreateClientDisconnectOptionsBuilder().WithUserProperty("test_name", "test_value").Build(); + + // Perform a clean disconnect. + await client.DisconnectAsync(disconnectOptions); + + await LongTestDelay(); + + Assert.IsNotNull(eventArgs); + Assert.IsNotNull(eventArgs.UserProperties); + Assert.AreEqual(1, eventArgs.UserProperties.Count); + Assert.AreEqual("test_name", eventArgs.UserProperties[0].Name); + Assert.AreEqual("test_value", eventArgs.UserProperties[0].Value); + } + + class TestClientKerberosAuthenticationHandler : IMqttEnhancedAuthenticationHandler + { + public async Task HandleEnhancedAuthenticationAsync(MqttEnhancedAuthenticationEventArgs eventArgs) + { + if (eventArgs.AuthenticationMethod != "GS2-KRB5") { - eventArgs = args; - return CompletedTask.Instance; + throw new InvalidOperationException("Wrong authentication method"); + } + + var sendOptions = new SendMqttEnhancedAuthenticationDataOptions + { + Data = "initial context token"u8.ToArray() }; - var client = await testEnvironment.ConnectClient(); + await eventArgs.SendAsync(sendOptions, eventArgs.CancellationToken); - var disconnectOptions = testEnvironment.ClientFactory.CreateClientDisconnectOptionsBuilder().WithReason(MqttClientDisconnectOptionsReason.MessageRateTooHigh).Build(); + var response = await eventArgs.ReceiveAsync(eventArgs.CancellationToken); - // Perform a clean disconnect. - await client.DisconnectAsync(disconnectOptions); + Assert.AreEqual(Encoding.UTF8.GetString(response.AuthenticationData), "reply context token"); - await LongTestDelay(); - - Assert.IsNotNull(eventArgs); - Assert.AreEqual(MqttDisconnectReasonCode.MessageRateTooHigh, eventArgs.ReasonCode); - } - - [TestMethod] - public async Task Disconnect_Clean_With_User_Properties() - { - using var testEnvironment = CreateTestEnvironment(MqttProtocolVersion.V500); - var server = await testEnvironment.StartServer(); - - ClientDisconnectedEventArgs eventArgs = null; - server.ClientDisconnectedAsync += args => + // No further data is required, but we have to fulfil the exchange. + sendOptions = new SendMqttEnhancedAuthenticationDataOptions { - eventArgs = args; - return CompletedTask.Instance; + Data = [] }; - var client = await testEnvironment.ConnectClient(); - - var disconnectOptions = testEnvironment.ClientFactory.CreateClientDisconnectOptionsBuilder().WithUserProperty("test_name", "test_value").Build(); - - // Perform a clean disconnect. - await client.DisconnectAsync(disconnectOptions); - - await LongTestDelay(); - - Assert.IsNotNull(eventArgs); - Assert.IsNotNull(eventArgs.UserProperties); - Assert.AreEqual(1, eventArgs.UserProperties.Count); - Assert.AreEqual("test_name", eventArgs.UserProperties[0].Name); - Assert.AreEqual("test_value", eventArgs.UserProperties[0].Value); - } - - class TestClientKerberosAuthenticationHandler : IMqttEnhancedAuthenticationHandler - { - public async Task HandleEnhancedAuthenticationAsync(MqttEnhancedAuthenticationEventArgs eventArgs) - { - if (eventArgs.AuthenticationMethod != "GS2-KRB5") - { - throw new InvalidOperationException("Wrong authentication method"); - } - - var sendOptions = new SendMqttEnhancedAuthenticationDataOptions - { - Data = "initial context token"u8.ToArray() - }; - - await eventArgs.SendAsync(sendOptions, eventArgs.CancellationToken); - - var response = await eventArgs.ReceiveAsync(eventArgs.CancellationToken); - - Assert.AreEqual(Encoding.UTF8.GetString(response.AuthenticationData), "reply context token"); - - // No further data is required, but we have to fulfil the exchange. - sendOptions = new SendMqttEnhancedAuthenticationDataOptions - { - Data = [] - }; - - await eventArgs.SendAsync(sendOptions, eventArgs.CancellationToken); - } - } - - [TestMethod] - public async Task Use_Enhanced_Authentication() - { - using var testEnvironment = CreateTestEnvironment(MqttProtocolVersion.V500); - var server = await testEnvironment.StartServer(); - - server.ValidatingConnectionAsync += async args => - { - if (args.AuthenticationMethod == "GS2-KRB5") - { - var result = await args.ExchangeEnhancedAuthenticationAsync(new ExchangeEnhancedAuthenticationOptions(), args.CancellationToken); - - Assert.AreEqual(Encoding.UTF8.GetString(result.AuthenticationData), "initial context token"); - - var authOptions = testEnvironment.ServerFactory.CreateExchangeExtendedAuthenticationOptionsBuilder().WithAuthenticationData("reply context token").Build(); - - result = await args.ExchangeEnhancedAuthenticationAsync(authOptions, args.CancellationToken); - - Assert.AreEqual(Encoding.UTF8.GetString(result.AuthenticationData), ""); - - args.ResponseAuthenticationData = "outcome of authentication"u8.ToArray(); - } - else - { - args.ReasonCode = MqttConnectReasonCode.BadAuthenticationMethod; - } - }; - - // Use Kerberos sample from the MQTT RFC. - var kerberosAuthenticationHandler = new TestClientKerberosAuthenticationHandler(); - - var clientOptions = testEnvironment.CreateDefaultClientOptionsBuilder().WithEnhancedAuthentication("GS2-KRB5").WithEnhancedAuthenticationHandler(kerberosAuthenticationHandler); - var client = await testEnvironment.ConnectClient(clientOptions); - - Assert.IsTrue(client.IsConnected); - } - - [TestMethod] - public async Task No_Unobserved_Exception() - { - using var testEnvironment = CreateTestEnvironment(); - testEnvironment.IgnoreClientLogErrors = true; - - var client = testEnvironment.CreateClient(); - var options = new MqttClientOptionsBuilder().WithTcpServer("1.2.3.4").WithTimeout(TimeSpan.FromSeconds(2)).Build(); - - try - { - using var timeout = new CancellationTokenSource(TimeSpan.FromSeconds(0.5)); - await client.ConnectAsync(options, timeout.Token); - } - catch (OperationCanceledException) - { - } - - client.Dispose(); - - // These delays and GC calls are required in order to make calling the finalizer reproducible. - GC.Collect(); - GC.WaitForPendingFinalizers(); - await LongTestDelay(); - await LongTestDelay(); - await LongTestDelay(); - } - - [TestMethod] - public async Task Return_Non_Success() - { - using var testEnvironment = CreateTestEnvironment(MqttProtocolVersion.V500); - var server = await testEnvironment.StartServer(); - - server.ValidatingConnectionAsync += args => - { - args.ResponseUserProperties = [new("Property", "Value")]; - - args.ReasonCode = MqttConnectReasonCode.QuotaExceeded; - - return CompletedTask.Instance; - }; - - var client = testEnvironment.CreateClient(); - - var response = await client.ConnectAsync(testEnvironment.CreateDefaultClientOptionsBuilder().Build()); - - Assert.IsNotNull(response); - Assert.AreEqual(MqttClientConnectResultCode.QuotaExceeded, response.ResultCode); - Assert.AreEqual(response.UserProperties[0].Name, "Property"); - Assert.AreEqual(response.UserProperties[0].Value, "Value"); - } - - [TestMethod] - public async Task Throw_Proper_Exception_When_Not_Connected() - { - try - { - var mqttFactory = new MqttClientFactory(); - using var mqttClient = mqttFactory.CreateMqttClient(); - await mqttClient.SubscribeAsync("test", MqttQualityOfServiceLevel.AtLeastOnce); - } - catch (MqttClientNotConnectedException exception) - { - if (exception.Message == "The MQTT client is not connected.") - { - return; - } - } - - Assert.Fail(); + await eventArgs.SendAsync(sendOptions, eventArgs.CancellationToken).ConfigureAwait(false); } } + + [TestMethod] + public async Task Use_Enhanced_Authentication() + { + using var testEnvironment = CreateTestEnvironment(MqttProtocolVersion.V500); + var server = await testEnvironment.StartServer(); + + server.ValidatingConnectionAsync += async args => + { + if (args.AuthenticationMethod == "GS2-KRB5") + { + var result = await args.ExchangeEnhancedAuthenticationAsync(new ExchangeEnhancedAuthenticationOptions(), args.CancellationToken); + + Assert.AreEqual(Encoding.UTF8.GetString(result.AuthenticationData), "initial context token"); + + var authOptions = testEnvironment.ServerFactory.CreateExchangeExtendedAuthenticationOptionsBuilder().WithAuthenticationData("reply context token").Build(); + + result = await args.ExchangeEnhancedAuthenticationAsync(authOptions, args.CancellationToken); + + Assert.AreEqual(Encoding.UTF8.GetString(result.AuthenticationData), ""); + + args.ResponseAuthenticationData = "outcome of authentication"u8.ToArray(); + } + else + { + args.ReasonCode = MqttConnectReasonCode.BadAuthenticationMethod; + } + }; + + // Use Kerberos sample from the MQTT RFC. + var kerberosAuthenticationHandler = new TestClientKerberosAuthenticationHandler(); + + var clientOptions = testEnvironment.CreateDefaultClientOptionsBuilder().WithEnhancedAuthentication("GS2-KRB5").WithEnhancedAuthenticationHandler(kerberosAuthenticationHandler); + var client = await testEnvironment.ConnectClient(clientOptions); + + Assert.IsTrue(client.IsConnected); + } + + [TestMethod] + public async Task No_Unobserved_Exception() + { + using var testEnvironment = CreateTestEnvironment(); + testEnvironment.IgnoreClientLogErrors = true; + + var client = testEnvironment.CreateClient(); + var options = new MqttClientOptionsBuilder().WithTcpServer("1.2.3.4").WithTimeout(TimeSpan.FromSeconds(2)).Build(); + + try + { + using var timeout = new CancellationTokenSource(TimeSpan.FromSeconds(0.5)); + await client.ConnectAsync(options, timeout.Token); + } + catch (OperationCanceledException) + { + } + + client.Dispose(); + + // These delays and GC calls are required in order to make calling the finalizer reproducible. + GC.Collect(); + GC.WaitForPendingFinalizers(); + await LongTestDelay(); + await LongTestDelay(); + await LongTestDelay(); + } + + [TestMethod] + public async Task Return_Non_Success() + { + using var testEnvironment = CreateTestEnvironment(MqttProtocolVersion.V500); + var server = await testEnvironment.StartServer(); + + server.ValidatingConnectionAsync += args => + { + args.ResponseUserProperties = [new("Property", "Value")]; + + args.ReasonCode = MqttConnectReasonCode.QuotaExceeded; + + return CompletedTask.Instance; + }; + + var client = testEnvironment.CreateClient(); + + var response = await client.ConnectAsync(testEnvironment.CreateDefaultClientOptionsBuilder().Build()); + + Assert.IsNotNull(response); + Assert.AreEqual(MqttClientConnectResultCode.QuotaExceeded, response.ResultCode); + Assert.AreEqual(response.UserProperties[0].Name, "Property"); + Assert.AreEqual(response.UserProperties[0].Value, "Value"); + } + + [TestMethod] + public async Task Throw_Proper_Exception_When_Not_Connected() + { + try + { + var mqttFactory = new MqttClientFactory(); + using var mqttClient = mqttFactory.CreateMqttClient(); + await mqttClient.SubscribeAsync("test", MqttQualityOfServiceLevel.AtLeastOnce); + } + catch (MqttClientNotConnectedException exception) + { + if (exception.Message == "The MQTT client is not connected.") + { + return; + } + } + + Assert.Fail(); + } } \ No newline at end of file diff --git a/Source/MQTTnet.Tests/Clients/MqttClient/MqttClient_Tests.cs b/Source/MQTTnet.Tests/Clients/MqttClient/MqttClient_Tests.cs index b8b1ba9e..e43d1f27 100644 --- a/Source/MQTTnet.Tests/Clients/MqttClient/MqttClient_Tests.cs +++ b/Source/MQTTnet.Tests/Clients/MqttClient/MqttClient_Tests.cs @@ -20,910 +20,857 @@ using MQTTnet.Protocol; using MQTTnet.Server; using MQTTnet.Tests.Mockups; +namespace MQTTnet.Tests.Clients.MqttClient; + // ReSharper disable InconsistentNaming - -namespace MQTTnet.Tests.Clients.MqttClient +[TestClass] +public sealed class MqttClient_Tests : BaseTestClass { - [TestClass] - public sealed class MqttClient_Tests : BaseTestClass + [TestMethod] + [DataRow(MqttQualityOfServiceLevel.ExactlyOnce)] + [DataRow(MqttQualityOfServiceLevel.AtMostOnce)] + [DataRow(MqttQualityOfServiceLevel.AtLeastOnce)] + public async Task Concurrent_Processing(MqttQualityOfServiceLevel qos) { - [TestMethod] - [DataRow(MqttQualityOfServiceLevel.ExactlyOnce)] - [DataRow(MqttQualityOfServiceLevel.AtMostOnce)] - [DataRow(MqttQualityOfServiceLevel.AtLeastOnce)] - public async Task Concurrent_Processing(MqttQualityOfServiceLevel qos) + long concurrency = 0; + var success = false; + + using var testEnvironment = new TestEnvironment(TestContext); + await testEnvironment.StartServer(); + var publisher = await testEnvironment.ConnectClient(); + var subscriber = await testEnvironment.ConnectClient(new MqttClientOptionsBuilder().WithClientId(qos.ToString())); + await subscriber.SubscribeAsync("#", qos); + + subscriber.ApplicationMessageReceivedAsync += e => { - long concurrency = 0; - var success = false; + e.AutoAcknowledge = false; - using (var testEnvironment = new TestEnvironment(TestContext)) + async Task InvokeInternal() { - await testEnvironment.StartServer(); - var publisher = await testEnvironment.ConnectClient(); - var subscriber = await testEnvironment.ConnectClient(new MqttClientOptionsBuilder().WithClientId(qos.ToString())); - await subscriber.SubscribeAsync("#", qos); - - subscriber.ApplicationMessageReceivedAsync += e => + if (Interlocked.Increment(ref concurrency) > 1) { - e.AutoAcknowledge = false; - - async Task InvokeInternal() - { - if (Interlocked.Increment(ref concurrency) > 1) - { - success = true; - } - - await Task.Delay(100); - Interlocked.Decrement(ref concurrency); - } - - _ = InvokeInternal(); - return CompletedTask.Instance; - }; - - var publishes = Task.WhenAll(publisher.PublishStringAsync("a", null, qos), publisher.PublishStringAsync("b", null, qos)); - - await Task.Delay(200); - - await publishes; - Assert.IsTrue(success); - } - } - - [TestMethod] - public async Task Connect_Disconnect_Connect() - { - using (var testEnvironment = CreateTestEnvironment()) - { - await testEnvironment.StartServer(); - - var clientOptions = testEnvironment.CreateDefaultClientOptions(); - var client = testEnvironment.CreateClient(); - - await client.ConnectAsync(clientOptions); - await client.DisconnectAsync(); - await client.ConnectAsync(clientOptions); - } - } - - [TestMethod] - [ExpectedException(typeof(InvalidOperationException))] - public async Task Connect_Multiple_Times_Should_Fail() - { - using (var testEnvironment = CreateTestEnvironment()) - { - await testEnvironment.StartServer(); - - var clientOptions = testEnvironment.CreateDefaultClientOptions(); - var client = testEnvironment.CreateClient(); - - await client.ConnectAsync(clientOptions); - await client.ConnectAsync(clientOptions); - } - } - - [TestMethod] - public async Task Disconnect_Event_Contains_Exception() - { - var factory = new MqttClientFactory(); - using (var client = factory.CreateMqttClient()) - { - Exception ex = null; - client.DisconnectedAsync += e => - { - ex = e.Exception; - return CompletedTask.Instance; - }; - - try - { - await client.ConnectAsync(new MqttClientOptionsBuilder().WithTcpServer("wrong-server").Build()); - } - catch - { - // Ignore errors. + success = true; } - await Task.Delay(500); - - Assert.IsNotNull(ex); - Assert.IsInstanceOfType(ex, typeof(MqttCommunicationException)); - Assert.IsInstanceOfType(ex.InnerException, typeof(SocketException)); + await Task.Delay(100); + Interlocked.Decrement(ref concurrency); } + + _ = InvokeInternal(); + return CompletedTask.Instance; + }; + + var publishes = Task.WhenAll(publisher.PublishStringAsync("a", null, qos), publisher.PublishStringAsync("b", null, qos)); + + await Task.Delay(200); + + await publishes; + Assert.IsTrue(success); + } + + [TestMethod] + public async Task Connect_Disconnect_Connect() + { + using var testEnvironment = CreateTestEnvironment(); + await testEnvironment.StartServer(); + + var clientOptions = testEnvironment.CreateDefaultClientOptions(); + var client = testEnvironment.CreateClient(); + + await client.ConnectAsync(clientOptions); + await client.DisconnectAsync(); + await client.ConnectAsync(clientOptions); + } + + [TestMethod] + [ExpectedException(typeof(InvalidOperationException))] + public async Task Connect_Multiple_Times_Should_Fail() + { + using var testEnvironment = CreateTestEnvironment(); + await testEnvironment.StartServer(); + + var clientOptions = testEnvironment.CreateDefaultClientOptions(); + var client = testEnvironment.CreateClient(); + + await client.ConnectAsync(clientOptions); + await client.ConnectAsync(clientOptions); + } + + [TestMethod] + public async Task Disconnect_Event_Contains_Exception() + { + var factory = new MqttClientFactory(); + using var client = factory.CreateMqttClient(); + Exception ex = null; + client.DisconnectedAsync += e => + { + ex = e.Exception; + return CompletedTask.Instance; + }; + + try + { + await client.ConnectAsync(new MqttClientOptionsBuilder().WithTcpServer("wrong-server").Build()); + } + catch + { + // Ignore errors. } - [TestMethod] - public async Task Ensure_Queue_Drain() + await Task.Delay(500); + + Assert.IsNotNull(ex); + Assert.IsInstanceOfType(ex, typeof(MqttCommunicationException)); + Assert.IsInstanceOfType(ex.InnerException, typeof(SocketException)); + } + + [TestMethod] + public async Task Ensure_Queue_Drain() + { + using var testEnvironment = new TestEnvironment(TestContext); + var server = await testEnvironment.StartServer(); + var client = await testEnvironment.ConnectLowLevelClient(); + + var i = 0; + server.InterceptingPublishAsync += _ => { - using (var testEnvironment = new TestEnvironment(TestContext)) + i++; + return CompletedTask.Instance; + }; + + await client.SendAsync( + new MqttConnectPacket { - var server = await testEnvironment.StartServer(); - var client = await testEnvironment.ConnectLowLevelClient(); + ClientId = "Ensure_Queue_Drain_Test" + }, + CancellationToken.None); - var i = 0; - server.InterceptingPublishAsync += c => - { - i++; - return CompletedTask.Instance; - }; + await client.SendAsync( + new MqttPublishPacket + { + Topic = "Test" + }, + CancellationToken.None); - await client.SendAsync( - new MqttConnectPacket - { - ClientId = "Ensure_Queue_Drain_Test" - }, - CancellationToken.None); + await Task.Delay(500); - await client.SendAsync( - new MqttPublishPacket - { - Topic = "Test" - }, - CancellationToken.None); + // This will simulate a device which closes the connection directly + // after sending the data so do delay is added between send and dispose! + client.Dispose(); - await Task.Delay(500); + await Task.Delay(1000); - // This will simulate a device which closes the connection directly - // after sending the data so do delay is added between send and dispose! - client.Dispose(); + Assert.AreEqual(1, i); + } - await Task.Delay(1000); + [TestMethod] + public async Task Fire_Disconnected_Event_On_Server_Shutdown() + { + using var testEnvironment = new TestEnvironment(TestContext); + var server = await testEnvironment.StartServer(); + var client = await testEnvironment.ConnectClient(); - Assert.AreEqual(1, i); - } + var handlerFired = false; + client.DisconnectedAsync += _ => + { + handlerFired = true; + return CompletedTask.Instance; + }; + + await server.StopAsync(); + + await Task.Delay(4000); + + Assert.IsTrue(handlerFired); + } + + [TestMethod] + public async Task Frequent_Connects() + { + using var testEnvironment = new TestEnvironment(TestContext); + await testEnvironment.StartServer(); + + var clients = new List(); + for (var i = 0; i < 100; i++) + { + clients.Add(await testEnvironment.ConnectClient(new MqttClientOptionsBuilder().WithClientId("a"))); } - [TestMethod] - public async Task Fire_Disconnected_Event_On_Server_Shutdown() + await Task.Delay(500); + + var clientStatus = await testEnvironment.Server.GetClientsAsync(); + var sessionStatus = await testEnvironment.Server.GetSessionsAsync(); + + for (var i = 0; i < 98; i++) { - using (var testEnvironment = new TestEnvironment(TestContext)) - { - var server = await testEnvironment.StartServer(); - var client = await testEnvironment.ConnectClient(); - - var handlerFired = false; - client.DisconnectedAsync += e => - { - handlerFired = true; - return CompletedTask.Instance; - }; - - await server.StopAsync(); - - await Task.Delay(4000); - - Assert.IsTrue(handlerFired); - } + Assert.IsFalse(clients[i].TryPingAsync().GetAwaiter().GetResult(), $"clients[{i}] is not connected"); } - [TestMethod] - public async Task Frequent_Connects() + Assert.IsTrue(clients[99].TryPingAsync().GetAwaiter().GetResult()); + + Assert.AreEqual(1, clientStatus.Count); + Assert.AreEqual(1, sessionStatus.Count); + + var receiveClient = clients[99]; + object receivedPayload = null; + receiveClient.ApplicationMessageReceivedAsync += e => { - using (var testEnvironment = new TestEnvironment(TestContext)) - { - await testEnvironment.StartServer(); + receivedPayload = e.ApplicationMessage.ConvertPayloadToString(); + return CompletedTask.Instance; + }; - var clients = new List(); - for (var i = 0; i < 100; i++) - { - clients.Add(await testEnvironment.ConnectClient(new MqttClientOptionsBuilder().WithClientId("a"))); - } + await receiveClient.SubscribeAsync("x"); - await Task.Delay(500); + var sendClient = await testEnvironment.ConnectClient(); + await sendClient.PublishStringAsync("x", "1"); - var clientStatus = await testEnvironment.Server.GetClientsAsync(); - var sessionStatus = await testEnvironment.Server.GetSessionsAsync(); + await Task.Delay(250); - for (var i = 0; i < 98; i++) - { - Assert.IsFalse(clients[i].TryPingAsync().GetAwaiter().GetResult(), $"clients[{i}] is not connected"); - } + Assert.AreEqual("1", receivedPayload); + } - Assert.IsTrue(clients[99].TryPingAsync().GetAwaiter().GetResult()); + [TestMethod] + public async Task Invalid_Connect_Throws_Exception() + { + var factory = new MqttClientFactory(); + using var client = factory.CreateMqttClient(); + try + { + await client.ConnectAsync(new MqttClientOptionsBuilder().WithTcpServer("wrong-server").Build()); - Assert.AreEqual(1, clientStatus.Count); - Assert.AreEqual(1, sessionStatus.Count); - - var receiveClient = clients[99]; - object receivedPayload = null; - receiveClient.ApplicationMessageReceivedAsync += e => - { - receivedPayload = e.ApplicationMessage.ConvertPayloadToString(); - return CompletedTask.Instance; - }; - - await receiveClient.SubscribeAsync("x"); - - var sendClient = await testEnvironment.ConnectClient(); - await sendClient.PublishStringAsync("x", "1"); - - await Task.Delay(250); - - Assert.AreEqual("1", receivedPayload); - } + Assert.Fail("Must fail!"); } - - [TestMethod] - public async Task Invalid_Connect_Throws_Exception() + catch (Exception exception) { - var factory = new MqttClientFactory(); - using (var client = factory.CreateMqttClient()) - { - try - { - await client.ConnectAsync(new MqttClientOptionsBuilder().WithTcpServer("wrong-server").Build()); - - Assert.Fail("Must fail!"); - } - catch (Exception exception) - { - Assert.IsNotNull(exception); - Assert.IsInstanceOfType(exception, typeof(MqttCommunicationException)); - Assert.IsInstanceOfType(exception.InnerException, typeof(SocketException)); - } - } - } - - [TestMethod] - public async Task No_Payload() - { - using (var testEnvironment = new TestEnvironment(TestContext)) - { - await testEnvironment.StartServer(); - - var sender = await testEnvironment.ConnectClient(); - var receiver = await testEnvironment.ConnectClient(); - - var message = new MqttApplicationMessageBuilder().WithTopic("A"); - - await receiver.SubscribeAsync( - new MqttClientSubscribeOptions - { - TopicFilters = new List { new MqttTopicFilter { Topic = "#" } } - }, - CancellationToken.None); - - MqttApplicationMessage receivedMessage = null; - receiver.ApplicationMessageReceivedAsync += e => - { - receivedMessage = e.ApplicationMessage; - return CompletedTask.Instance; - }; - - await sender.PublishAsync(message.Build(), CancellationToken.None); - - await Task.Delay(1000); - - Assert.IsNotNull(receivedMessage); - Assert.AreEqual("A", receivedMessage.Topic); - Assert.AreEqual(0, receivedMessage.Payload.Length); - } - } - - [TestMethod] - public async Task NoConnectedHandler_Connect_DoesNotThrowException() - { - using (var testEnvironment = new TestEnvironment(TestContext)) - { - await testEnvironment.StartServer(); - - var client = await testEnvironment.ConnectClient(); - - Assert.IsTrue(client.TryPingAsync().GetAwaiter().GetResult()); - } - } - - [TestMethod] - public async Task NoDisconnectedHandler_Disconnect_DoesNotThrowException() - { - using (var testEnvironment = new TestEnvironment(TestContext)) - { - await testEnvironment.StartServer(); - var client = await testEnvironment.ConnectClient(); - Assert.IsTrue(client.TryPingAsync().GetAwaiter().GetResult()); - - await client.DisconnectAsync(); - - Assert.IsFalse(client.TryPingAsync().GetAwaiter().GetResult()); - } - } - - [TestMethod] - public async Task PacketIdentifier_In_Publish_Result() - { - using (var testEnvironment = new TestEnvironment(TestContext)) - { - await testEnvironment.StartServer(); - var client = await testEnvironment.ConnectClient(); - - var result = await client.PublishStringAsync("a", "a"); - Assert.AreEqual(null, result.PacketIdentifier); - - result = await client.PublishStringAsync("b", "b"); - Assert.AreEqual(null, result.PacketIdentifier); - - result = await client.PublishStringAsync("a", "a", MqttQualityOfServiceLevel.AtLeastOnce); - Assert.AreEqual((ushort)1, result.PacketIdentifier); - - result = await client.PublishStringAsync("b", "b", MqttQualityOfServiceLevel.AtLeastOnce); - Assert.AreEqual((ushort)2, result.PacketIdentifier); - - result = await client.PublishStringAsync("a", "a", MqttQualityOfServiceLevel.ExactlyOnce); - Assert.AreEqual((ushort)3, result.PacketIdentifier); - - result = await client.PublishStringAsync("b", "b", MqttQualityOfServiceLevel.ExactlyOnce); - Assert.AreEqual((ushort)4, result.PacketIdentifier); - } - } - - [TestMethod] - public async Task Preserve_Message_Order() - { - // The messages are sent in reverse or to ensure that the delay in the handler - // needs longer for the first messages and later messages may be processed earlier (if there - // is an issue). - const int MessagesCount = 50; - - using (var testEnvironment = new TestEnvironment(TestContext)) - { - await testEnvironment.StartServer(); - - var client1 = await testEnvironment.ConnectClient(); - await client1.SubscribeAsync("x"); - - var receivedValues = new List(); - - async Task Handler1(MqttApplicationMessageReceivedEventArgs eventArgs) - { - var value = int.Parse(eventArgs.ApplicationMessage.ConvertPayloadToString()); - await Task.Delay(value); - - lock (receivedValues) - { - receivedValues.Add(value); - } - } - - client1.ApplicationMessageReceivedAsync += Handler1; - - var client2 = await testEnvironment.ConnectClient(); - for (var i = MessagesCount; i > 0; i--) - { - await client2.PublishStringAsync("x", i.ToString()); - } - - await Task.Delay(5000); - - for (var i = MessagesCount; i > 0; i--) - { - Assert.AreEqual(i, receivedValues[MessagesCount - i]); - } - } - } - - [TestMethod] - public async Task Preserve_Message_Order_With_Delayed_Acknowledgement() - { - // The messages are sent in reverse or to ensure that the delay in the handler - // needs longer for the first messages and later messages may be processed earlier (if there - // is an issue). - const int MessagesCount = 50; - - using (var testEnvironment = new TestEnvironment(TestContext)) - { - await testEnvironment.StartServer(); - - var client1 = await testEnvironment.ConnectClient(); - await client1.SubscribeAsync("x", MqttQualityOfServiceLevel.ExactlyOnce); - - var receivedValues = new List(); - - Task Handler1(MqttApplicationMessageReceivedEventArgs eventArgs) - { - var value = int.Parse(eventArgs.ApplicationMessage.ConvertPayloadToString()); - eventArgs.AutoAcknowledge = false; - Task.Delay(value).ContinueWith(x => eventArgs.AcknowledgeAsync(CancellationToken.None)); - - Debug.WriteLine($"received {value}"); - lock (receivedValues) - { - receivedValues.Add(value); - } - - return CompletedTask.Instance; - } - - client1.ApplicationMessageReceivedAsync += Handler1; - - var client2 = await testEnvironment.ConnectClient(); - for (var i = MessagesCount; i > 0; i--) - { - await client2.PublishStringAsync("x", i.ToString(), MqttQualityOfServiceLevel.ExactlyOnce); - } - - await Task.Delay(5000); - - for (var i = MessagesCount; i > 0; i--) - { - Assert.AreEqual(i, receivedValues[MessagesCount - i]); - } - } - } - - [TestMethod] - public async Task Publish_QoS_0_Over_Period_Exceeding_KeepAlive() - { - using (var testEnvironment = new TestEnvironment(TestContext)) - { - const int KeepAlivePeriodSecs = 3; - - await testEnvironment.StartServer(); - - var options = new MqttClientOptionsBuilder().WithKeepAlivePeriod(TimeSpan.FromSeconds(KeepAlivePeriodSecs)); - var client = await testEnvironment.ConnectClient(options); - var message = new MqttApplicationMessageBuilder().WithTopic("a").Build(); - - try - { - // Publish messages over a time period exceeding the keep alive period. - // This should not cause an exception because of, i.e. "Client disconnected". - - for (var count = 0; count < KeepAlivePeriodSecs * 3; ++count) - { - // Send Publish requests well before the keep alive period expires - await client.PublishAsync(message); - await Task.Delay(1000); - } - } - catch (Exception ex) - { - Assert.Fail(ex.Message); - } - } - } - - [TestMethod] - public async Task Publish_QoS_1_In_ApplicationMessageReceiveHandler() - { - using (var testEnvironment = new TestEnvironment(TestContext)) - { - await testEnvironment.StartServer(); - - const string client1Topic = "client1/topic"; - const string client2Topic = "client2/topic"; - const string expectedClient2Message = "hello client2"; - - var client1 = await testEnvironment.ConnectClient(); - client1.ApplicationMessageReceivedAsync += async e => - { - await client1.PublishStringAsync(client2Topic, expectedClient2Message, MqttQualityOfServiceLevel.AtLeastOnce); - }; - - await client1.SubscribeAsync(client1Topic, MqttQualityOfServiceLevel.AtLeastOnce); - - var client2 = await testEnvironment.ConnectClient(); - - var client2TopicResults = new List(); - - client2.ApplicationMessageReceivedAsync += e => - { - client2TopicResults.Add(Encoding.UTF8.GetString(e.ApplicationMessage.Payload.ToArray())); - return CompletedTask.Instance; - }; - - await client2.SubscribeAsync(client2Topic); - - var client3 = await testEnvironment.ConnectClient(); - var message = new MqttApplicationMessageBuilder().WithTopic(client1Topic).Build(); - await client3.PublishAsync(message); - await client3.PublishAsync(message); - - await Task.Delay(500); - - Assert.AreEqual(2, client2TopicResults.Count); - Assert.AreEqual(expectedClient2Message, client2TopicResults[0]); - Assert.AreEqual(expectedClient2Message, client2TopicResults[1]); - } - } - - [TestMethod] - public async Task Publish_With_Correct_Retain_Flag() - { - using (var testEnvironment = new TestEnvironment(TestContext)) - { - await testEnvironment.StartServer(); - - var receivedMessages = new List(); - - var client1 = await testEnvironment.ConnectClient(); - client1.ApplicationMessageReceivedAsync += e => - { - lock (receivedMessages) - { - receivedMessages.Add(e.ApplicationMessage); - } - - return CompletedTask.Instance; - }; - - await client1.SubscribeAsync("a"); - - var client2 = await testEnvironment.ConnectClient(); - var message = new MqttApplicationMessageBuilder().WithTopic("a").WithRetainFlag().Build(); - await client2.PublishAsync(message); - - await Task.Delay(500); - - Assert.AreEqual(1, receivedMessages.Count); - Assert.IsFalse(receivedMessages.First().Retain); // Must be false even if set above! - } - } - - [TestMethod] - public async Task Reconnect() - { - using (var testEnvironment = new TestEnvironment(TestContext)) - { - var server = await testEnvironment.StartServer(); - var client = await testEnvironment.ConnectClient(); - - await Task.Delay(500); - Assert.IsTrue(await client.TryPingAsync()); - - await server.StopAsync(); - await Task.Delay(500); - Assert.IsFalse(await client.TryPingAsync()); - - await server.StartAsync(); - await Task.Delay(500); - - await client.ConnectAsync(new MqttClientOptionsBuilder().WithTcpServer("127.0.0.1", testEnvironment.ServerPort).Build()); - Assert.IsTrue(await client.TryPingAsync()); - } - } - - [TestMethod] - public async Task Reconnect_From_Disconnected_Event() - { - using (var testEnvironment = new TestEnvironment(TestContext)) - { - testEnvironment.IgnoreClientLogErrors = true; - - var client = testEnvironment.CreateClient(); - - var tries = 0; - var maxTries = 3; - - client.DisconnectedAsync += async e => - { - if (tries >= maxTries) - { - return; - } - - Interlocked.Increment(ref tries); - - await Task.Delay(100); - await client.ConnectAsync(new MqttClientOptionsBuilder().WithTcpServer("127.0.0.1", testEnvironment.ServerPort).Build()); - }; - - try - { - await client.ConnectAsync(new MqttClientOptionsBuilder().WithTcpServer("127.0.0.1", testEnvironment.ServerPort).Build()); - Assert.Fail("Must fail!"); - } - catch - { - // Ignore errors. - } - - SpinWait.SpinUntil(() => tries >= maxTries, 10000); - - Assert.AreEqual(maxTries, tries); - } - } - - [TestMethod] - public async Task Reconnect_While_Server_Offline() - { - using (var testEnvironment = new TestEnvironment(TestContext)) - { - testEnvironment.IgnoreClientLogErrors = true; - - var server = await testEnvironment.StartServer(); - var client = await testEnvironment.ConnectClient(); - - await Task.Delay(500); - Assert.IsTrue(client.TryPingAsync().GetAwaiter().GetResult()); - - await server.StopAsync(); - await Task.Delay(500); - Assert.IsFalse(client.TryPingAsync().GetAwaiter().GetResult()); - - for (var i = 0; i < 5; i++) - { - try - { - await client.ConnectAsync(new MqttClientOptionsBuilder().WithTcpServer("127.0.0.1", testEnvironment.ServerPort).Build()); - Assert.Fail("Must fail!"); - } - catch - { - // Ignore errors. - } - } - - await server.StartAsync(); - await Task.Delay(500); - - await client.ConnectAsync(new MqttClientOptionsBuilder().WithTcpServer("127.0.0.1", testEnvironment.ServerPort).Build()); - await client.PingAsync(); - } - } - - [TestMethod] - public async Task Send_Manual_Ping() - { - using (var testEnvironment = new TestEnvironment(TestContext)) - { - await testEnvironment.StartServer(); - var client = await testEnvironment.ConnectClient(); - - await client.PingAsync(CancellationToken.None); - } - } - - [TestMethod] - public async Task Send_Reply_For_Any_Received_Message() - { - using (var testEnvironment = new TestEnvironment(TestContext)) - { - await testEnvironment.StartServer(); - - var client1 = await testEnvironment.ConnectClient(); - await client1.SubscribeAsync("request/+"); - - async Task Handler1(MqttApplicationMessageReceivedEventArgs eventArgs) - { - await client1.PublishStringAsync($"reply/{eventArgs.ApplicationMessage.Topic}"); - } - - client1.ApplicationMessageReceivedAsync += Handler1; - - var client2 = await testEnvironment.ConnectClient(); - await client2.SubscribeAsync("reply/#"); - - var replies = new List(); - - Task Handler2(MqttApplicationMessageReceivedEventArgs eventArgs) - { - lock (replies) - { - replies.Add(eventArgs.ApplicationMessage.Topic); - } - - return CompletedTask.Instance; - } - - client2.ApplicationMessageReceivedAsync += Handler2; - - await Task.Delay(500); - - await client2.PublishStringAsync("request/a"); - await client2.PublishStringAsync("request/b"); - await client2.PublishStringAsync("request/c"); - - await Task.Delay(500); - - Assert.AreEqual("reply/request/a,reply/request/b,reply/request/c", string.Join(",", replies)); - } - } - - [TestMethod] - public async Task Send_Reply_In_Message_Handler() - { - using (var testEnvironment = new TestEnvironment()) - { - await testEnvironment.StartServer(); - var client1 = await testEnvironment.ConnectClient(); - var client2 = await testEnvironment.ConnectClient(); - - await client1.SubscribeAsync("#"); - await client2.SubscribeAsync("#"); - - var replyReceived = false; - - client1.ApplicationMessageReceivedAsync += e => - { - if (e.ApplicationMessage.Topic == "reply") - { - replyReceived = true; - } - - return CompletedTask.Instance; - }; - - client2.ApplicationMessageReceivedAsync += async e => - { - if (e.ApplicationMessage.Topic == "request") - { - // Use AtMostOnce here because with QoS 1 or even QoS 2 the process waits for - // the ACK etc. The problem is that the SpinUntil below only waits until the - // flag is set. It does not wait until the client has sent the ACK - await client2.PublishStringAsync("reply"); - } - }; - - await client1.PublishStringAsync("request", null, MqttQualityOfServiceLevel.AtLeastOnce); - - await Task.Delay(500); - - SpinWait.SpinUntil(() => replyReceived, TimeSpan.FromSeconds(10)); - - await Task.Delay(500); - - Assert.IsTrue(replyReceived); - } - } - - [TestMethod] - public async Task Send_Reply_In_Message_Handler_For_Same_Client() - { - using (var testEnvironment = new TestEnvironment(TestContext)) - { - await testEnvironment.StartServer(); - var client = await testEnvironment.ConnectClient(); - - await client.SubscribeAsync("#"); - - var replyReceived = false; - - client.ApplicationMessageReceivedAsync += e => - { - if (e.ApplicationMessage.Topic == "request") - { -#pragma warning disable 4014 - Task.Run(() => client.PublishStringAsync("reply", null, MqttQualityOfServiceLevel.AtLeastOnce)); -#pragma warning restore 4014 - } - else - { - replyReceived = true; - } - - return CompletedTask.Instance; - }; - - await client.PublishStringAsync("request", null, MqttQualityOfServiceLevel.AtLeastOnce); - - SpinWait.SpinUntil(() => replyReceived, TimeSpan.FromSeconds(10)); - - Assert.IsTrue(replyReceived); - } - } - - [TestMethod] - public async Task Set_ClientWasConnected_On_ClientDisconnect() - { - using (var testEnvironment = new TestEnvironment(TestContext)) - { - await testEnvironment.StartServer(); - var client = await testEnvironment.ConnectClient(); - - Assert.IsTrue(client.TryPingAsync().GetAwaiter().GetResult()); - client.DisconnectedAsync += e => - { - Assert.IsTrue(e.ClientWasConnected); - return CompletedTask.Instance; - }; - - await client.DisconnectAsync(); - await Task.Delay(200); - } - } - - [TestMethod] - public async Task Set_ClientWasConnected_On_ServerDisconnect() - { - using (var testEnvironment = new TestEnvironment(TestContext)) - { - var server = await testEnvironment.StartServer(); - var client = await testEnvironment.ConnectClient(); - - Assert.IsTrue(client.TryPingAsync().GetAwaiter().GetResult()); - client.DisconnectedAsync += e => - { - Assert.IsTrue(e.ClientWasConnected); - return CompletedTask.Instance; - }; - - await server.StopAsync(); - await Task.Delay(4000); - } - } - - - [TestMethod] - public async Task Subscribe_In_Callback_Events() - { - using (var testEnvironment = new TestEnvironment(TestContext)) - { - await testEnvironment.StartServer(); - - var receivedMessages = new List(); - - var client = testEnvironment.CreateClient(); - - client.ConnectedAsync += async e => - { - await client.SubscribeAsync("RCU/P1/H0001/R0003"); - - var msg = new MqttApplicationMessageBuilder().WithPayload("DA|18RS00SC00XI0000RV00R100R200R300R400L100L200L300L400Y100Y200AC0102031800BELK0000BM0000|") - .WithTopic("RCU/P1/H0001/R0003"); - - await client.PublishAsync(msg.Build()); - }; - - client.ApplicationMessageReceivedAsync += e => - { - lock (receivedMessages) - { - receivedMessages.Add(e.ApplicationMessage); - } - - return CompletedTask.Instance; - }; - - await client.ConnectAsync(new MqttClientOptionsBuilder().WithTcpServer("localhost", testEnvironment.ServerPort).Build()); - - await Task.Delay(500); - - Assert.AreEqual(1, receivedMessages.Count); - Assert.AreEqual("DA|18RS00SC00XI0000RV00R100R200R300R400L100L200L300L400Y100Y200AC0102031800BELK0000BM0000|", receivedMessages.First().ConvertPayloadToString()); - } - } - - [TestMethod] - public async Task Subscribe_With_QoS2() - { - using (var testEnvironment = new TestEnvironment()) - { - await testEnvironment.StartServer(); - var client1 = await testEnvironment.ConnectClient(o => o.WithProtocolVersion(MqttProtocolVersion.V500)); - var client2 = await testEnvironment.ConnectClient(o => o.WithProtocolVersion(MqttProtocolVersion.V500)); - - var disconnectedFired = false; - client1.DisconnectedAsync += c => - { - disconnectedFired = true; - return CompletedTask.Instance; - }; - - var messageReceived = false; - client1.ApplicationMessageReceivedAsync += e => - { - messageReceived = true; - return CompletedTask.Instance; - }; - - await client1.SubscribeAsync(new MqttTopicFilterBuilder().WithTopic("topic1").WithExactlyOnceQoS().Build()); - - await Task.Delay(500); - - var message = new MqttApplicationMessageBuilder().WithTopic("topic1") - .WithPayload("Hello World") - .WithQualityOfServiceLevel(MqttQualityOfServiceLevel.ExactlyOnce) - .WithRetainFlag() - .Build(); - - await client2.PublishAsync(message); - await Task.Delay(500); - - Assert.IsTrue(messageReceived); - Assert.IsTrue(client1.TryPingAsync().GetAwaiter().GetResult()); - Assert.IsFalse(disconnectedFired); - } + Assert.IsNotNull(exception); + Assert.IsInstanceOfType(exception, typeof(MqttCommunicationException)); + Assert.IsInstanceOfType(exception.InnerException, typeof(SocketException)); } } + + [TestMethod] + public async Task No_Payload() + { + using var testEnvironment = new TestEnvironment(TestContext); + await testEnvironment.StartServer(); + + var sender = await testEnvironment.ConnectClient(); + var receiver = await testEnvironment.ConnectClient(); + + var message = new MqttApplicationMessageBuilder().WithTopic("A"); + + await receiver.SubscribeAsync( + new MqttClientSubscribeOptions + { + TopicFilters = + [ + new() { Topic = "#" } + ] + }, + CancellationToken.None); + + MqttApplicationMessage receivedMessage = null; + receiver.ApplicationMessageReceivedAsync += e => + { + receivedMessage = e.ApplicationMessage; + return CompletedTask.Instance; + }; + + await sender.PublishAsync(message.Build(), CancellationToken.None); + + await Task.Delay(1000); + + Assert.IsNotNull(receivedMessage); + Assert.AreEqual("A", receivedMessage.Topic); + Assert.AreEqual(0, receivedMessage.Payload.Length); + } + + [TestMethod] + public async Task NoConnectedHandler_Connect_DoesNotThrowException() + { + using var testEnvironment = new TestEnvironment(TestContext); + await testEnvironment.StartServer(); + + var client = await testEnvironment.ConnectClient(); + + Assert.IsTrue(client.TryPingAsync().GetAwaiter().GetResult()); + } + + [TestMethod] + public async Task NoDisconnectedHandler_Disconnect_DoesNotThrowException() + { + using var testEnvironment = new TestEnvironment(TestContext); + await testEnvironment.StartServer(); + var client = await testEnvironment.ConnectClient(); + Assert.IsTrue(client.TryPingAsync().GetAwaiter().GetResult()); + + await client.DisconnectAsync(); + + Assert.IsFalse(client.TryPingAsync().GetAwaiter().GetResult()); + } + + [TestMethod] + public async Task PacketIdentifier_In_Publish_Result() + { + using var testEnvironment = new TestEnvironment(TestContext); + await testEnvironment.StartServer(); + var client = await testEnvironment.ConnectClient(); + + var result = await client.PublishStringAsync("a", "a"); + Assert.AreEqual(null, result.PacketIdentifier); + + result = await client.PublishStringAsync("b", "b"); + Assert.AreEqual(null, result.PacketIdentifier); + + result = await client.PublishStringAsync("a", "a", MqttQualityOfServiceLevel.AtLeastOnce); + Assert.AreEqual((ushort)1, result.PacketIdentifier); + + result = await client.PublishStringAsync("b", "b", MqttQualityOfServiceLevel.AtLeastOnce); + Assert.AreEqual((ushort)2, result.PacketIdentifier); + + result = await client.PublishStringAsync("a", "a", MqttQualityOfServiceLevel.ExactlyOnce); + Assert.AreEqual((ushort)3, result.PacketIdentifier); + + result = await client.PublishStringAsync("b", "b", MqttQualityOfServiceLevel.ExactlyOnce); + Assert.AreEqual((ushort)4, result.PacketIdentifier); + } + + [TestMethod] + public async Task Preserve_Message_Order() + { + // The messages are sent in reverse or to ensure that the delay in the handler + // needs longer for the first messages and later messages may be processed earlier (if there + // is an issue). + const int MessagesCount = 50; + + using var testEnvironment = new TestEnvironment(TestContext); + await testEnvironment.StartServer(); + + var client1 = await testEnvironment.ConnectClient(); + await client1.SubscribeAsync("x"); + + var receivedValues = new List(); + + async Task Handler1(MqttApplicationMessageReceivedEventArgs eventArgs) + { + var value = int.Parse(eventArgs.ApplicationMessage.ConvertPayloadToString()); + await Task.Delay(value); + + lock (receivedValues) + { + receivedValues.Add(value); + } + } + + client1.ApplicationMessageReceivedAsync += Handler1; + + var client2 = await testEnvironment.ConnectClient(); + for (var i = MessagesCount; i > 0; i--) + { + await client2.PublishStringAsync("x", i.ToString()); + } + + await Task.Delay(5000); + + for (var i = MessagesCount; i > 0; i--) + { + Assert.AreEqual(i, receivedValues[MessagesCount - i]); + } + } + + [TestMethod] + public async Task Preserve_Message_Order_With_Delayed_Acknowledgement() + { + // The messages are sent in reverse or to ensure that the delay in the handler + // needs longer for the first messages and later messages may be processed earlier (if there + // is an issue). + const int MessagesCount = 50; + + using var testEnvironment = new TestEnvironment(TestContext); + await testEnvironment.StartServer(); + + var client1 = await testEnvironment.ConnectClient(); + await client1.SubscribeAsync("x", MqttQualityOfServiceLevel.ExactlyOnce); + + var receivedValues = new List(); + + client1.ApplicationMessageReceivedAsync += Handler1; + + var client2 = await testEnvironment.ConnectClient(); + for (var i = MessagesCount; i > 0; i--) + { + await client2.PublishStringAsync("x", i.ToString(), MqttQualityOfServiceLevel.ExactlyOnce); + } + + await Task.Delay(5000); + + for (var i = MessagesCount; i > 0; i--) + { + Assert.AreEqual(i, receivedValues[MessagesCount - i]); + } + + return; + + Task Handler1(MqttApplicationMessageReceivedEventArgs eventArgs) + { + var value = int.Parse(eventArgs.ApplicationMessage.ConvertPayloadToString()); + eventArgs.AutoAcknowledge = false; + Task.Delay(value).ContinueWith(_ => eventArgs.AcknowledgeAsync(CancellationToken.None)); + + Debug.WriteLine($"received {value}"); + lock (receivedValues) + { + receivedValues.Add(value); + } + + return CompletedTask.Instance; + } + } + + [TestMethod] + public async Task Publish_QoS_0_Over_Period_Exceeding_KeepAlive() + { + using var testEnvironment = new TestEnvironment(TestContext); + const int KeepAlivePeriodSecs = 3; + + await testEnvironment.StartServer(); + + var options = new MqttClientOptionsBuilder().WithKeepAlivePeriod(TimeSpan.FromSeconds(KeepAlivePeriodSecs)); + var client = await testEnvironment.ConnectClient(options); + var message = new MqttApplicationMessageBuilder().WithTopic("a").Build(); + + try + { + // Publish messages over a time period exceeding the keep alive period. + // This should not cause an exception because of, i.e. "Client disconnected". + + for (var count = 0; count < KeepAlivePeriodSecs * 3; ++count) + { + // Send Publish requests well before the keep alive period expires + await client.PublishAsync(message); + await Task.Delay(1000); + } + } + catch (Exception ex) + { + Assert.Fail(ex.Message); + } + } + + [TestMethod] + public async Task Publish_QoS_1_In_ApplicationMessageReceiveHandler() + { + using var testEnvironment = new TestEnvironment(TestContext); + await testEnvironment.StartServer(); + + const string client1Topic = "client1/topic"; + const string client2Topic = "client2/topic"; + const string expectedClient2Message = "hello client2"; + + var client1 = await testEnvironment.ConnectClient(); + client1.ApplicationMessageReceivedAsync += async _ => + { + await client1.PublishStringAsync(client2Topic, expectedClient2Message, MqttQualityOfServiceLevel.AtLeastOnce).ConfigureAwait(false); + }; + + await client1.SubscribeAsync(client1Topic, MqttQualityOfServiceLevel.AtLeastOnce); + + var client2 = await testEnvironment.ConnectClient(); + + var client2TopicResults = new List(); + + client2.ApplicationMessageReceivedAsync += e => + { + client2TopicResults.Add(Encoding.UTF8.GetString(e.ApplicationMessage.Payload.ToArray())); + return CompletedTask.Instance; + }; + + await client2.SubscribeAsync(client2Topic); + + var client3 = await testEnvironment.ConnectClient(); + var message = new MqttApplicationMessageBuilder().WithTopic(client1Topic).Build(); + await client3.PublishAsync(message); + await client3.PublishAsync(message); + + await Task.Delay(500); + + Assert.AreEqual(2, client2TopicResults.Count); + Assert.AreEqual(expectedClient2Message, client2TopicResults[0]); + Assert.AreEqual(expectedClient2Message, client2TopicResults[1]); + } + + [TestMethod] + public async Task Publish_With_Correct_Retain_Flag() + { + using var testEnvironment = new TestEnvironment(TestContext); + await testEnvironment.StartServer(); + + var receivedMessages = new List(); + + var client1 = await testEnvironment.ConnectClient(); + client1.ApplicationMessageReceivedAsync += e => + { + lock (receivedMessages) + { + receivedMessages.Add(e.ApplicationMessage); + } + + return CompletedTask.Instance; + }; + + await client1.SubscribeAsync("a"); + + var client2 = await testEnvironment.ConnectClient(); + var message = new MqttApplicationMessageBuilder().WithTopic("a").WithRetainFlag().Build(); + await client2.PublishAsync(message); + + await Task.Delay(500); + + Assert.AreEqual(1, receivedMessages.Count); + Assert.IsFalse(receivedMessages[0].Retain); // Must be false even if set above! + } + + [TestMethod] + public async Task Reconnect() + { + using var testEnvironment = new TestEnvironment(TestContext); + var server = await testEnvironment.StartServer(); + var client = await testEnvironment.ConnectClient(); + + await Task.Delay(500); + Assert.IsTrue(await client.TryPingAsync()); + + await server.StopAsync(); + await Task.Delay(500); + Assert.IsFalse(await client.TryPingAsync()); + + await server.StartAsync(); + await Task.Delay(500); + + await client.ConnectAsync(new MqttClientOptionsBuilder().WithTcpServer("127.0.0.1", testEnvironment.ServerPort).Build()); + Assert.IsTrue(await client.TryPingAsync()); + } + + [TestMethod] + public async Task Reconnect_From_Disconnected_Event() + { + using var testEnvironment = new TestEnvironment(TestContext); + testEnvironment.IgnoreClientLogErrors = true; + + var client = testEnvironment.CreateClient(); + + var tries = 0; + var maxTries = 3; + + client.DisconnectedAsync += async _ => + { + if (tries >= maxTries) + { + return; + } + + Interlocked.Increment(ref tries); + + await Task.Delay(100); + await client.ConnectAsync(new MqttClientOptionsBuilder().WithTcpServer("127.0.0.1", testEnvironment.ServerPort).Build()).ConfigureAwait(false); + }; + + try + { + await client.ConnectAsync(new MqttClientOptionsBuilder().WithTcpServer("127.0.0.1", testEnvironment.ServerPort).Build()); + Assert.Fail("Must fail!"); + } + catch + { + // Ignore errors. + } + + SpinWait.SpinUntil(() => tries >= maxTries, 10000); + + Assert.AreEqual(maxTries, tries); + } + + [TestMethod] + public async Task Reconnect_While_Server_Offline() + { + using var testEnvironment = new TestEnvironment(TestContext); + testEnvironment.IgnoreClientLogErrors = true; + + var server = await testEnvironment.StartServer(); + var client = await testEnvironment.ConnectClient(); + + await Task.Delay(500); + Assert.IsTrue(client.TryPingAsync().GetAwaiter().GetResult()); + + await server.StopAsync(); + await Task.Delay(500); + Assert.IsFalse(client.TryPingAsync().GetAwaiter().GetResult()); + + for (var i = 0; i < 5; i++) + { + try + { + await client.ConnectAsync(new MqttClientOptionsBuilder().WithTcpServer("127.0.0.1", testEnvironment.ServerPort).Build()); + Assert.Fail("Must fail!"); + } + catch + { + // Ignore errors. + } + } + + await server.StartAsync(); + await Task.Delay(500); + + await client.ConnectAsync(new MqttClientOptionsBuilder().WithTcpServer("127.0.0.1", testEnvironment.ServerPort).Build()); + await client.PingAsync(); + } + + [TestMethod] + public async Task Send_Manual_Ping() + { + using var testEnvironment = new TestEnvironment(TestContext); + await testEnvironment.StartServer(); + var client = await testEnvironment.ConnectClient(); + + await client.PingAsync(CancellationToken.None); + } + + [TestMethod] + public async Task Send_Reply_For_Any_Received_Message() + { + using var testEnvironment = new TestEnvironment(TestContext); + await testEnvironment.StartServer(); + + var client1 = await testEnvironment.ConnectClient(); + await client1.SubscribeAsync("request/+"); + + async Task Handler1(MqttApplicationMessageReceivedEventArgs eventArgs) + { + await client1.PublishStringAsync($"reply/{eventArgs.ApplicationMessage.Topic}"); + } + + client1.ApplicationMessageReceivedAsync += Handler1; + + var client2 = await testEnvironment.ConnectClient(); + await client2.SubscribeAsync("reply/#"); + + var replies = new List(); + + Task Handler2(MqttApplicationMessageReceivedEventArgs eventArgs) + { + lock (replies) + { + replies.Add(eventArgs.ApplicationMessage.Topic); + } + + return CompletedTask.Instance; + } + + client2.ApplicationMessageReceivedAsync += Handler2; + + await Task.Delay(500); + + await client2.PublishStringAsync("request/a"); + await client2.PublishStringAsync("request/b"); + await client2.PublishStringAsync("request/c"); + + await Task.Delay(500); + + Assert.AreEqual("reply/request/a,reply/request/b,reply/request/c", string.Join(",", replies)); + } + + [TestMethod] + public async Task Send_Reply_In_Message_Handler() + { + using var testEnvironment = new TestEnvironment(); + await testEnvironment.StartServer(); + var client1 = await testEnvironment.ConnectClient(); + var client2 = await testEnvironment.ConnectClient(); + + await client1.SubscribeAsync("#"); + await client2.SubscribeAsync("#"); + + var replyReceived = false; + + client1.ApplicationMessageReceivedAsync += e => + { + if (e.ApplicationMessage.Topic == "reply") + { + replyReceived = true; + } + + return CompletedTask.Instance; + }; + + client2.ApplicationMessageReceivedAsync += async e => + { + if (e.ApplicationMessage.Topic == "request") + { + // Use AtMostOnce here because with QoS 1 or even QoS 2 the process waits for + // the ACK etc. The problem is that the SpinUntil below only waits until the + // flag is set. It does not wait until the client has sent the ACK + await client2.PublishStringAsync("reply"); + } + }; + + await client1.PublishStringAsync("request", null, MqttQualityOfServiceLevel.AtLeastOnce); + + await Task.Delay(500); + + SpinWait.SpinUntil(() => replyReceived, TimeSpan.FromSeconds(10)); + + await Task.Delay(500); + + Assert.IsTrue(replyReceived); + } + + [TestMethod] + public async Task Send_Reply_In_Message_Handler_For_Same_Client() + { + using var testEnvironment = new TestEnvironment(TestContext); + await testEnvironment.StartServer(); + var client = await testEnvironment.ConnectClient(); + + await client.SubscribeAsync("#"); + + var replyReceived = false; + + client.ApplicationMessageReceivedAsync += e => + { + if (e.ApplicationMessage.Topic == "request") + { +#pragma warning disable 4014 + Task.Run(() => client.PublishStringAsync("reply", null, MqttQualityOfServiceLevel.AtLeastOnce)); +#pragma warning restore 4014 + } + else + { + replyReceived = true; + } + + return CompletedTask.Instance; + }; + + await client.PublishStringAsync("request", null, MqttQualityOfServiceLevel.AtLeastOnce); + + SpinWait.SpinUntil(() => replyReceived, TimeSpan.FromSeconds(10)); + + Assert.IsTrue(replyReceived); + } + + [TestMethod] + public async Task Set_ClientWasConnected_On_ClientDisconnect() + { + using var testEnvironment = new TestEnvironment(TestContext); + await testEnvironment.StartServer(); + var client = await testEnvironment.ConnectClient(); + + Assert.IsTrue(client.TryPingAsync().GetAwaiter().GetResult()); + client.DisconnectedAsync += e => + { + Assert.IsTrue(e.ClientWasConnected); + return CompletedTask.Instance; + }; + + await client.DisconnectAsync(); + await Task.Delay(200); + } + + [TestMethod] + public async Task Set_ClientWasConnected_On_ServerDisconnect() + { + using var testEnvironment = new TestEnvironment(TestContext); + var server = await testEnvironment.StartServer(); + var client = await testEnvironment.ConnectClient(); + + Assert.IsTrue(client.TryPingAsync().GetAwaiter().GetResult()); + client.DisconnectedAsync += e => + { + Assert.IsTrue(e.ClientWasConnected); + return CompletedTask.Instance; + }; + + await server.StopAsync(); + await Task.Delay(4000); + } + + + [TestMethod] + public async Task Subscribe_In_Callback_Events() + { + using var testEnvironment = new TestEnvironment(TestContext); + await testEnvironment.StartServer(); + + var receivedMessages = new List(); + + var client = testEnvironment.CreateClient(); + + client.ConnectedAsync += async e => + { + await client.SubscribeAsync("RCU/P1/H0001/R0003"); + + var msg = new MqttApplicationMessageBuilder().WithPayload("DA|18RS00SC00XI0000RV00R100R200R300R400L100L200L300L400Y100Y200AC0102031800BELK0000BM0000|") + .WithTopic("RCU/P1/H0001/R0003"); + + await client.PublishAsync(msg.Build()); + }; + + client.ApplicationMessageReceivedAsync += e => + { + lock (receivedMessages) + { + receivedMessages.Add(e.ApplicationMessage); + } + + return CompletedTask.Instance; + }; + + await client.ConnectAsync(new MqttClientOptionsBuilder().WithTcpServer("localhost", testEnvironment.ServerPort).Build()); + + await Task.Delay(500); + + Assert.AreEqual(1, receivedMessages.Count); + Assert.AreEqual("DA|18RS00SC00XI0000RV00R100R200R300R400L100L200L300L400Y100Y200AC0102031800BELK0000BM0000|", receivedMessages.First().ConvertPayloadToString()); + } + + [TestMethod] + public async Task Subscribe_With_QoS2() + { + using var testEnvironment = new TestEnvironment(); + await testEnvironment.StartServer(); + var client1 = await testEnvironment.ConnectClient(o => o.WithProtocolVersion(MqttProtocolVersion.V500)); + var client2 = await testEnvironment.ConnectClient(o => o.WithProtocolVersion(MqttProtocolVersion.V500)); + + var disconnectedFired = false; + client1.DisconnectedAsync += c => + { + disconnectedFired = true; + return CompletedTask.Instance; + }; + + var messageReceived = false; + client1.ApplicationMessageReceivedAsync += _ => + { + messageReceived = true; + return CompletedTask.Instance; + }; + + await client1.SubscribeAsync(new MqttTopicFilterBuilder().WithTopic("topic1").WithExactlyOnceQoS().Build()); + + await Task.Delay(500); + + var message = new MqttApplicationMessageBuilder().WithTopic("topic1") + .WithPayload("Hello World") + .WithQualityOfServiceLevel(MqttQualityOfServiceLevel.ExactlyOnce) + .WithRetainFlag() + .Build(); + + await client2.PublishAsync(message); + await Task.Delay(500); + + Assert.IsTrue(messageReceived); + Assert.IsTrue(client1.TryPingAsync().GetAwaiter().GetResult()); + Assert.IsFalse(disconnectedFired); + } } \ No newline at end of file diff --git a/Source/MQTTnet.Tests/Clients/MqttClientOptionsBuilder_Tests.cs b/Source/MQTTnet.Tests/Clients/MqttClientOptionsBuilder_Tests.cs index 65e03f7d..c6fab628 100644 --- a/Source/MQTTnet.Tests/Clients/MqttClientOptionsBuilder_Tests.cs +++ b/Source/MQTTnet.Tests/Clients/MqttClientOptionsBuilder_Tests.cs @@ -3,23 +3,22 @@ // See the LICENSE file in the project root for more information. using System.Linq; -using System.Text; using Microsoft.VisualStudio.TestTools.UnitTesting; -namespace MQTTnet.Tests.Clients -{ - [TestClass] - public class MqttClientOptionsBuilder_Tests - { - [TestMethod] - public void WithConnectionUri_Credential_Test() - { - var options = new MqttClientOptionsBuilder() - .WithConnectionUri("mqtt://user:password@127.0.0.1") - .Build(); +namespace MQTTnet.Tests.Clients; - Assert.AreEqual("user", options.Credentials.GetUserName(null)); - Assert.IsTrue(Encoding.UTF8.GetBytes("password").SequenceEqual(options.Credentials.GetPassword(null))); - } +// ReSharper disable InconsistentNaming +[TestClass] +public class MqttClientOptionsBuilder_Tests +{ + [TestMethod] + public void WithConnectionUri_Credential_Test() + { + var options = new MqttClientOptionsBuilder() + .WithConnectionUri("mqtt://user:password@127.0.0.1") + .Build(); + + Assert.AreEqual("user", options.Credentials.GetUserName(null)); + Assert.IsTrue("password"u8.ToArray().SequenceEqual(options.Credentials.GetPassword(null))); } -} +} \ No newline at end of file diff --git a/Source/MQTTnet.Tests/Diagnostics/Logger_Tests.cs b/Source/MQTTnet.Tests/Diagnostics/Logger_Tests.cs index 8b2c5d1e..636a8a06 100644 --- a/Source/MQTTnet.Tests/Diagnostics/Logger_Tests.cs +++ b/Source/MQTTnet.Tests/Diagnostics/Logger_Tests.cs @@ -6,61 +6,61 @@ using System; using Microsoft.VisualStudio.TestTools.UnitTesting; using MQTTnet.Diagnostics.Logger; -namespace MQTTnet.Tests.Diagnostics +namespace MQTTnet.Tests.Diagnostics; + +// ReSharper disable InconsistentNaming +[TestClass] +public sealed class Logger_Tests : BaseTestClass { - [TestClass] - public sealed class Logger_Tests : BaseTestClass + [TestMethod] + public void Log_Without_Source() { - [TestMethod] - public void Log_Without_Source() + var logger = new MqttNetEventLogger(); + + MqttNetLogMessage logMessage = null; + logger.LogMessagePublished += (_, e) => { logMessage = e.LogMessage; }; + + logger.Publish(MqttNetLogLevel.Info, "SOURCE", "MESSAGE", ["ABC"], new InvalidOperationException()); + + Assert.AreEqual(MqttNetLogLevel.Info, logMessage.Level); + Assert.AreEqual("SOURCE", logMessage.Source); + Assert.AreEqual("MESSAGE", logMessage.Message); + Assert.AreEqual("InvalidOperationException", logMessage.Exception.GetType().Name); + } + + [TestMethod] + public void Root_Log_Messages() + { + var logger = new MqttNetEventLogger(); + var childLogger = logger.WithSource("Source1"); + + var logMessagesCount = 0; + + logger.LogMessagePublished += (_, _) => { logMessagesCount++; }; + + childLogger.Verbose("Verbose"); + childLogger.Info("Info"); + childLogger.Warning((Exception)null, "Warning"); + childLogger.Error(null, "Error"); + + Assert.AreEqual(4, logMessagesCount); + } + + [TestMethod] + public void Use_Custom_Log_Id() + { + var logger = new MqttNetEventLogger("logId"); + var childLogger = logger.WithSource("Source1"); + + logger.LogMessagePublished += (_, e) => { - var logger = new MqttNetEventLogger(); + Assert.AreEqual("logId", e.LogMessage.LogId); + Assert.AreEqual("Source1", e.LogMessage.Source); + }; - MqttNetLogMessage logMessage = null; - logger.LogMessagePublished += (s, e) => { logMessage = e.LogMessage; }; - - logger.Publish(MqttNetLogLevel.Info, "SOURCE", "MESSAGE", new object[] { "ABC" }, new InvalidOperationException()); - - Assert.AreEqual(MqttNetLogLevel.Info, logMessage.Level); - Assert.AreEqual("SOURCE", logMessage.Source); - Assert.AreEqual("MESSAGE", logMessage.Message); - Assert.AreEqual("InvalidOperationException", logMessage.Exception.GetType().Name); - } - - [TestMethod] - public void Root_Log_Messages() - { - var logger = new MqttNetEventLogger(); - var childLogger = logger.WithSource("Source1"); - - var logMessagesCount = 0; - - logger.LogMessagePublished += (s, e) => { logMessagesCount++; }; - - childLogger.Verbose("Verbose"); - childLogger.Info("Info"); - childLogger.Warning((Exception)null, "Warning"); - childLogger.Error(null, "Error"); - - Assert.AreEqual(4, logMessagesCount); - } - - [TestMethod] - public void Use_Custom_Log_Id() - { - var logger = new MqttNetEventLogger("logId"); - var childLogger = logger.WithSource("Source1"); - - logger.LogMessagePublished += (s, e) => - { - Assert.AreEqual("logId", e.LogMessage.LogId); - Assert.AreEqual("Source1", e.LogMessage.Source); - }; - - childLogger.Verbose("Verbose"); - childLogger.Info("Info"); - childLogger.Warning((Exception)null, "Warning"); - childLogger.Error(null, "Error"); - } + childLogger.Verbose("Verbose"); + childLogger.Info("Info"); + childLogger.Warning((Exception)null, "Warning"); + childLogger.Error(null, "Error"); } } \ No newline at end of file diff --git a/Source/MQTTnet.Tests/Diagnostics/PacketInspection_Tests.cs b/Source/MQTTnet.Tests/Diagnostics/PacketInspection_Tests.cs index 7db513f5..0bc6ea6d 100644 --- a/Source/MQTTnet.Tests/Diagnostics/PacketInspection_Tests.cs +++ b/Source/MQTTnet.Tests/Diagnostics/PacketInspection_Tests.cs @@ -10,41 +10,37 @@ using Microsoft.VisualStudio.TestTools.UnitTesting; using MQTTnet.Formatter; using MQTTnet.Internal; -namespace MQTTnet.Tests.Diagnostics +namespace MQTTnet.Tests.Diagnostics; + +// ReSharper disable InconsistentNaming +[TestClass] +public sealed class PacketInspection_Tests : BaseTestClass { - [TestClass] - public sealed class PacketInspection_Tests : BaseTestClass + [TestMethod] + public async Task Inspect_Client_Packets() { - [TestMethod] - public async Task Inspect_Client_Packets() + using var testEnvironment = CreateTestEnvironment(); + await testEnvironment.StartServer(); + + using var mqttClient = testEnvironment.CreateClient(); + var mqttClientOptions = testEnvironment.ClientFactory.CreateClientOptionsBuilder() + .WithClientId("CLIENT_ID") // Must be fixed. + .WithProtocolVersion(MqttProtocolVersion.V311) + .WithTcpServer("127.0.0.1", testEnvironment.ServerPort) + .Build(); + + var packets = new List(); + + mqttClient.InspectPacketAsync += eventArgs => { - using (var testEnvironment = CreateTestEnvironment()) - { - await testEnvironment.StartServer(); + packets.Add(eventArgs.Direction + ":" + Convert.ToBase64String(eventArgs.Buffer)); + return CompletedTask.Instance; + }; - using (var mqttClient = testEnvironment.CreateClient()) - { - var mqttClientOptions = testEnvironment.ClientFactory.CreateClientOptionsBuilder() - .WithClientId("CLIENT_ID") // Must be fixed. - .WithProtocolVersion(MqttProtocolVersion.V311) - .WithTcpServer("127.0.0.1", testEnvironment.ServerPort) - .Build(); + await mqttClient.ConnectAsync(mqttClientOptions, CancellationToken.None); - var packets = new List(); - - mqttClient.InspectPacketAsync += eventArgs => - { - packets.Add(eventArgs.Direction + ":" + Convert.ToBase64String(eventArgs.Buffer)); - return CompletedTask.Instance; - }; - - await mqttClient.ConnectAsync(mqttClientOptions, CancellationToken.None); - - Assert.AreEqual(2, packets.Count); - Assert.AreEqual("Outbound:ECwABE1RVFQEAgAPACBJbnNwZWN0X0NsaWVudF9QYWNrZXRzX0NMSUVOVF9JRA==", packets[0]); // CONNECT - Assert.AreEqual("Inbound:IAIAAA==", packets[1]); // CONNACK - } - } - } + Assert.AreEqual(2, packets.Count); + Assert.AreEqual("Outbound:ECwABE1RVFQEAgAPACBJbnNwZWN0X0NsaWVudF9QYWNrZXRzX0NMSUVOVF9JRA==", packets[0]); // CONNECT + Assert.AreEqual("Inbound:IAIAAA==", packets[1]); // CONNACK } } \ No newline at end of file diff --git a/Source/MQTTnet.Tests/Diagnostics/SourceLogger_Tests.cs b/Source/MQTTnet.Tests/Diagnostics/SourceLogger_Tests.cs index 3e3c411c..8adf3765 100644 --- a/Source/MQTTnet.Tests/Diagnostics/SourceLogger_Tests.cs +++ b/Source/MQTTnet.Tests/Diagnostics/SourceLogger_Tests.cs @@ -3,29 +3,28 @@ // See the LICENSE file in the project root for more information. using Microsoft.VisualStudio.TestTools.UnitTesting; - using MQTTnet.Diagnostics.Logger; -namespace MQTTnet.Tests.Diagnostics +namespace MQTTnet.Tests.Diagnostics; + +// ReSharper disable InconsistentNaming +[TestClass] +public sealed class SourceLogger_Tests : BaseTestClass { - [TestClass] - public sealed class SourceLogger_Tests : BaseTestClass + [TestMethod] + public void Log_With_Source() { - [TestMethod] - public void Log_With_Source() + MqttNetLogMessage logMessage = null; + + var logger = new MqttNetEventLogger(); + logger.LogMessagePublished += (_, e) => { - MqttNetLogMessage logMessage = null; + logMessage = e.LogMessage; + }; - var logger = new MqttNetEventLogger(); - logger.LogMessagePublished += (s, e) => - { - logMessage = e.LogMessage; - }; + var sourceLogger = logger.WithSource("The_Source"); + sourceLogger.Info("MESSAGE", (object)null, (object)null); - var sourceLogger = logger.WithSource("The_Source"); - sourceLogger.Info("MESSAGE", (object)null, (object)null); - - Assert.AreEqual("The_Source", logMessage.Source); - } + Assert.AreEqual("The_Source", logMessage.Source); } } \ No newline at end of file diff --git a/Source/MQTTnet.Tests/Extensions/MqttTopicTemplate_Tests.cs b/Source/MQTTnet.Tests/Extensions/MqttTopicTemplate_Tests.cs index e3c7ad20..e261f763 100644 --- a/Source/MQTTnet.Tests/Extensions/MqttTopicTemplate_Tests.cs +++ b/Source/MQTTnet.Tests/Extensions/MqttTopicTemplate_Tests.cs @@ -8,266 +8,254 @@ using Microsoft.VisualStudio.TestTools.UnitTesting; using MQTTnet.Exceptions; using MQTTnet.Extensions.TopicTemplate; -namespace MQTTnet.Tests.Extensions +namespace MQTTnet.Tests.Extensions; + +// ReSharper disable InconsistentNaming +[TestClass] +public sealed class MqttTopicTemplate_Tests { - [TestClass] - public sealed class MqttTopicTemplate_Tests + [TestMethod] + public void AcceptsEmptyValue() { - [TestMethod] - public void BasicOps() - { - var template = new MqttTopicTemplate("A/B/{param}/D"); - Assert.AreEqual("A/B/C/D", template.WithParameter("param", "C").Template); - Assert.AreEqual("A/B/+/D", template.WithoutParameter("param").Template); - Assert.AreEqual("A/B/+/D", template.TopicFilter); - Assert.AreEqual("A/B/+/D/#", template.TopicTreeRootFilter); + var template = new MqttTopicTemplate("A/B/{foo}/D"); + template.WithParameter("foo", ""); + } - // corner cases - Assert.AreEqual("#", new MqttTopicTemplate("#").TopicFilter); - Assert.AreEqual("+", new MqttTopicTemplate("+").TopicFilter); - Assert.AreEqual("/", new MqttTopicTemplate("/").TopicFilter); - } + [TestMethod] + public void AcceptsValidTopics() + { + _ = new MqttTopicTemplate("A"); + _ = new MqttTopicTemplate("/"); + _ = new MqttTopicTemplate("/A"); + _ = new MqttTopicTemplate("A/#"); + _ = new MqttTopicTemplate("$SYS/#"); + _ = new MqttTopicTemplate("////"); + _ = new MqttTopicTemplate("A/B/{foo}/D/"); + } - [TestMethod] - public void BasicOpsWithHash() - { - var template = new MqttTopicTemplate("A/B/{param}/D/#"); - Assert.AreEqual("A/B/C/D/#", template.WithParameter("param", "C").Template); - Assert.AreEqual("A/B/+/D/#", template.WithoutParameter("param").Template); - Assert.AreEqual("A/B/+/D/#", template.TopicFilter); - Assert.AreEqual("A/B/+/D/#", template.TopicTreeRootFilter); - } + [TestMethod] + public void BasicOps() + { + var template = new MqttTopicTemplate("A/B/{param}/D"); + Assert.AreEqual("A/B/C/D", template.WithParameter("param", "C").Template); + Assert.AreEqual("A/B/+/D", template.WithoutParameter("param").Template); + Assert.AreEqual("A/B/+/D", template.TopicFilter); + Assert.AreEqual("A/B/+/D/#", template.TopicTreeRootFilter); - [TestMethod] - [ExpectedException(typeof(ArgumentException))] - public void RejectsReservedChars1() - { - var template = new MqttTopicTemplate("A/B/{foo}/D"); - template.WithParameter("foo", "a#"); - } + // corner cases + Assert.AreEqual("#", new MqttTopicTemplate("#").TopicFilter); + Assert.AreEqual("+", new MqttTopicTemplate("+").TopicFilter); + Assert.AreEqual("/", new MqttTopicTemplate("/").TopicFilter); + } - [TestMethod] - [ExpectedException(typeof(ArgumentException))] - public void RejectsReservedChars2() - { - var template = new MqttTopicTemplate("A/B/{foo}/D"); - template.WithParameter("foo", "a+b"); - } + [TestMethod] + public void BasicOpsWithHash() + { + var template = new MqttTopicTemplate("A/B/{param}/D/#"); + Assert.AreEqual("A/B/C/D/#", template.WithParameter("param", "C").Template); + Assert.AreEqual("A/B/+/D/#", template.WithoutParameter("param").Template); + Assert.AreEqual("A/B/+/D/#", template.TopicFilter); + Assert.AreEqual("A/B/+/D/#", template.TopicTreeRootFilter); + } - [TestMethod] - [ExpectedException(typeof(ArgumentException))] - public void RejectsReservedChars3() - { - var template = new MqttTopicTemplate("A/B/{foo}/D"); - template.WithParameter("foo", "a/b"); - } + [TestMethod] + public void CanonicalPrefixFilter() + { + var template1 = new MqttTopicTemplate("A/v1/{param}/F"); + var template2 = new MqttTopicTemplate("A/v1/E/F"); + var template3 = new MqttTopicTemplate("A/v1/E/F/G/H"); + var canonicalFilter = MqttTopicTemplate.FindCanonicalPrefix([template1, template2, template3]); + // possible improvement: Assert.AreEqual("A/v1/+/F", canonicalFilter.TopicFilter); + Assert.AreEqual("A/v1/+", canonicalFilter.TopicFilter); + Assert.AreEqual("A/v1/+/#", canonicalFilter.TopicTreeRootFilter); - [TestMethod] - public void AcceptsEmptyValue() - { - var template = new MqttTopicTemplate("A/B/{foo}/D"); - template.WithParameter("foo", ""); - } - - [TestMethod] - [ExpectedException(typeof(MqttProtocolViolationException))] - public void RejectsEmptyTemplate() - { - var _ = new MqttTopicTemplate(""); - } - - [TestMethod] - [ExpectedException(typeof(ArgumentNullException))] - public void RejectsNullTemplate() - { - var _ = new MqttTopicTemplate(null); - } - - [TestMethod] - public void IgnoresEmptyParameters() - { - var template = new MqttTopicTemplate("A/B/{}/D"); - Assert.IsFalse(template.Parameters.Any()); - } - - [TestMethod] - public void AcceptsValidTopics() - { - _ = new MqttTopicTemplate("A"); - _ = new MqttTopicTemplate("/"); - _ = new MqttTopicTemplate("/A"); - _ = new MqttTopicTemplate("A/#"); - _ = new MqttTopicTemplate("$SYS/#"); - _ = new MqttTopicTemplate("////"); - _ = new MqttTopicTemplate("A/B/{foo}/D/"); - } - - [TestMethod] - public void DynamicRoutingSupport() - { - var v1template = new MqttTopicTemplate("A/v1/{id}/F"); - var v2template = new MqttTopicTemplate("A/v2/foo/{id}/{bar}"); - var incoming_v1_topic = "A/v1/32/F"; - - var compat_v2_topic = v2template - .WithParameterValuesFrom( - v1template.ParseParameterValues(incoming_v1_topic)) - .WithParameter("bar", "unknown"); - - // id: 32 was relocated and bar: unknown added - Assert.AreEqual("A/v2/foo/32/unknown", compat_v2_topic.Template); - } - - [TestMethod] - public void SubscriptionSupport() - { - var template = new MqttTopicTemplate("A/v1/{param}/F"); - var filter = template.BuildFilter() - .WithAtLeastOnceQoS() - .WithNoLocal() - .Build(); - - Assert.AreEqual("A/v1/+/F", filter.Topic); - } - - [TestMethod] - public void SubscriptionSupport2() - { - var template = new MqttTopicTemplate("A/v1/{param}/F"); - - var subscribeOptions = new MqttClientFactory().CreateSubscribeOptionsBuilder() - .WithTopicTemplate(template) - .WithSubscriptionIdentifier(5) - .Build(); - - Assert.AreEqual("A/v1/+/F", subscribeOptions.TopicFilters[0].Topic); - } - - [TestMethod] - public void SendAndSubscribeSupport() - { - var template = new MqttTopicTemplate("App/v1/{sender}/message"); - - var filter = new MqttTopicFilterBuilder() - .WithTopicTemplate(template) - .WithAtLeastOnceQoS() - .WithNoLocal() - .Build(); - - Assert.AreEqual("App/v1/+/message", filter.Topic); - - var myTopic = template.WithParameter("sender", "me, myself & i"); - - var message = new MqttApplicationMessageBuilder() - .WithTopicTemplate( - myTopic) - .WithPayload("Hello!") - .Build(); - - Assert.IsTrue(message.MatchesTopicTemplate(template)); - } - - [TestMethod] - public void SendAndSubscribeSupport2() - { - var template = new MqttTopicTemplate("App/v1/{sender}/message"); - Assert.ThrowsException(() => - template.BuildMessage()); - } - - [TestMethod] - public void CanonicalPrefixFilter() - { - var template1 = new MqttTopicTemplate("A/v1/{param}/F"); - var template2 = new MqttTopicTemplate("A/v1/E/F"); - var template3 = new MqttTopicTemplate("A/v1/E/F/G/H"); - var canonicalFilter = MqttTopicTemplate.FindCanonicalPrefix(new[] { template1, template2, template3 }); - // possible improvement: Assert.AreEqual("A/v1/+/F", canonicalFilter.TopicFilter); - Assert.AreEqual("A/v1/+", canonicalFilter.TopicFilter); - Assert.AreEqual("A/v1/+/#", canonicalFilter.TopicTreeRootFilter); - - var template2b = new MqttTopicTemplate("A/v1/E/X"); - canonicalFilter = MqttTopicTemplate.FindCanonicalPrefix(new[] { template1, template2, template3, template2b }); - Assert.AreEqual("A/v1/+", canonicalFilter.Template); - Assert.AreEqual("A/v1/+", canonicalFilter.TopicFilter); - Assert.AreEqual("A/v1/+/#", canonicalFilter.TopicTreeRootFilter); + var template2B = new MqttTopicTemplate("A/v1/E/X"); + canonicalFilter = MqttTopicTemplate.FindCanonicalPrefix([template1, template2, template3, template2B]); + Assert.AreEqual("A/v1/+", canonicalFilter.Template); + Assert.AreEqual("A/v1/+", canonicalFilter.TopicFilter); + Assert.AreEqual("A/v1/+/#", canonicalFilter.TopicTreeRootFilter); - var template4 = new MqttTopicTemplate("A/v2/{prefix}"); - var canonicalFilter2 = MqttTopicTemplate.FindCanonicalPrefix(new[] { template1, template2, template3, template4 }); - Assert.AreEqual("A/+", canonicalFilter2.TopicFilter); - Assert.AreEqual("A/+/#", canonicalFilter2.TopicTreeRootFilter); - } + var template4 = new MqttTopicTemplate("A/v2/{prefix}"); + var canonicalFilter2 = MqttTopicTemplate.FindCanonicalPrefix([template1, template2, template3, template4]); + Assert.AreEqual("A/+", canonicalFilter2.TopicFilter); + Assert.AreEqual("A/+/#", canonicalFilter2.TopicTreeRootFilter); + } - [TestMethod] - public void CanonicalPrefixFilterSimple1() - { - var template = MqttTopicTemplate.FindCanonicalPrefix(new[] - { - new MqttTopicTemplate("A"), - new MqttTopicTemplate("B") - }); - Assert.AreEqual("#", template.TopicFilter); - Assert.AreEqual("#", template.Template); - } + [TestMethod] + public void CanonicalPrefixFilterSimple1() + { + var template = MqttTopicTemplate.FindCanonicalPrefix( + [ + new MqttTopicTemplate("A"), + new MqttTopicTemplate("B") + ]); - [TestMethod] - public void CanonicalPrefixFilterSimple2() - { - var template = MqttTopicTemplate.FindCanonicalPrefix(new[] - { - new MqttTopicTemplate("A/v1/{param}/F/"), - new MqttTopicTemplate("A/v1/E/X") - }); - Assert.AreEqual("A/v1/+", template.TopicFilter); - } + Assert.AreEqual("#", template.TopicFilter); + Assert.AreEqual("#", template.Template); + } - [TestMethod] - public void CanonicalPrefixFilterSimple3() - { - var template = MqttTopicTemplate.FindCanonicalPrefix(new[] - { - new MqttTopicTemplate("A/v1/{param}/F"), - new MqttTopicTemplate("A/v1/{param}/F/X") - }); - Assert.AreEqual("A/v1/+/+", template.TopicFilter); - Assert.AreEqual("A/v1/{param}/+", template.Template); - } + [TestMethod] + public void CanonicalPrefixFilterSimple2() + { + var template = MqttTopicTemplate.FindCanonicalPrefix( + [ + new MqttTopicTemplate("A/v1/{param}/F/"), + new MqttTopicTemplate("A/v1/E/X") + ]); - [TestMethod] - public void CanonicalPrefixFilterSimple4() - { - var template = MqttTopicTemplate.FindCanonicalPrefix(new[] - { - new MqttTopicTemplate("A/v1/{param}/FFA"), - new MqttTopicTemplate("A/v1/{param}/FX") - }); - Assert.AreEqual("A/v1/+/+", template.TopicFilter); - Assert.AreEqual("A/v1/{param}/+", template.Template); - } + Assert.AreEqual("A/v1/+", template.TopicFilter); + } - [TestMethod] - public void CanonicalPrefixFilterSimple5() - { - var template = MqttTopicTemplate.FindCanonicalPrefix(new[] - { - new MqttTopicTemplate("A/v1/E/F/G/H"), - new MqttTopicTemplate("A/v2/{param}") - }); - Assert.AreEqual("A/+", template.TopicFilter); - Assert.AreEqual("A/+/#", template.TopicTreeRootFilter); - } + [TestMethod] + public void CanonicalPrefixFilterSimple3() + { + var template = MqttTopicTemplate.FindCanonicalPrefix( + [ + new MqttTopicTemplate("A/v1/{param}/F"), + new MqttTopicTemplate("A/v1/{param}/F/X") + ]); - [TestMethod] - public void CanonicalPrefixFilterSimple6() - { - var template = MqttTopicTemplate.FindCanonicalPrefix(new[] - { - new MqttTopicTemplate("A/{version}/E/F/G/H"), - new MqttTopicTemplate("A/{version}/{param}") - }); - Assert.AreEqual("A/+/+", template.TopicFilter); - Assert.AreEqual("A/+/+/#", template.TopicTreeRootFilter); - } + Assert.AreEqual("A/v1/+/+", template.TopicFilter); + Assert.AreEqual("A/v1/{param}/+", template.Template); + } + + [TestMethod] + public void CanonicalPrefixFilterSimple4() + { + var template = MqttTopicTemplate.FindCanonicalPrefix( + [ + new MqttTopicTemplate("A/v1/{param}/FFA"), + new MqttTopicTemplate("A/v1/{param}/FX") + ]); + + Assert.AreEqual("A/v1/+/+", template.TopicFilter); + Assert.AreEqual("A/v1/{param}/+", template.Template); + } + + [TestMethod] + public void CanonicalPrefixFilterSimple5() + { + var template = MqttTopicTemplate.FindCanonicalPrefix( + [ + new MqttTopicTemplate("A/v1/E/F/G/H"), + new MqttTopicTemplate("A/v2/{param}") + ]); + + Assert.AreEqual("A/+", template.TopicFilter); + Assert.AreEqual("A/+/#", template.TopicTreeRootFilter); + } + + [TestMethod] + public void CanonicalPrefixFilterSimple6() + { + var template = MqttTopicTemplate.FindCanonicalPrefix( + [ + new MqttTopicTemplate("A/{version}/E/F/G/H"), + new MqttTopicTemplate("A/{version}/{param}") + ]); + + Assert.AreEqual("A/+/+", template.TopicFilter); + Assert.AreEqual("A/+/+/#", template.TopicTreeRootFilter); + } + + [TestMethod] + public void DynamicRoutingSupport() + { + var v1Template = new MqttTopicTemplate("A/v1/{id}/F"); + var v2Template = new MqttTopicTemplate("A/v2/foo/{id}/{bar}"); + var incomingV1Topic = "A/v1/32/F"; + + var compatV2Topic = v2Template.WithParameterValuesFrom(v1Template.ParseParameterValues(incomingV1Topic)).WithParameter("bar", "unknown"); + + // id: 32 was relocated and bar: unknown added + Assert.AreEqual("A/v2/foo/32/unknown", compatV2Topic.Template); + } + + [TestMethod] + public void IgnoresEmptyParameters() + { + var template = new MqttTopicTemplate("A/B/{}/D"); + Assert.IsFalse(template.Parameters.Any()); + } + + [TestMethod] + [ExpectedException(typeof(MqttProtocolViolationException))] + public void RejectsEmptyTemplate() + { + _ = new MqttTopicTemplate(""); + } + + [TestMethod] + [ExpectedException(typeof(ArgumentNullException))] + public void RejectsNullTemplate() + { + _ = new MqttTopicTemplate(null); + } + + [TestMethod] + [ExpectedException(typeof(ArgumentException))] + public void RejectsReservedChars1() + { + var template = new MqttTopicTemplate("A/B/{foo}/D"); + template.WithParameter("foo", "a#"); + } + + [TestMethod] + [ExpectedException(typeof(ArgumentException))] + public void RejectsReservedChars2() + { + var template = new MqttTopicTemplate("A/B/{foo}/D"); + template.WithParameter("foo", "a+b"); + } + + [TestMethod] + [ExpectedException(typeof(ArgumentException))] + public void RejectsReservedChars3() + { + var template = new MqttTopicTemplate("A/B/{foo}/D"); + template.WithParameter("foo", "a/b"); + } + + [TestMethod] + public void SendAndSubscribeSupport() + { + var template = new MqttTopicTemplate("App/v1/{sender}/message"); + + var filter = new MqttTopicFilterBuilder().WithTopicTemplate(template).WithAtLeastOnceQoS().WithNoLocal().Build(); + + Assert.AreEqual("App/v1/+/message", filter.Topic); + + var myTopic = template.WithParameter("sender", "me, myself & i"); + + var message = new MqttApplicationMessageBuilder().WithTopicTemplate(myTopic).WithPayload("Hello!").Build(); + + Assert.IsTrue(message.MatchesTopicTemplate(template)); + } + + [TestMethod] + public void SendAndSubscribeSupport2() + { + var template = new MqttTopicTemplate("App/v1/{sender}/message"); + Assert.ThrowsException(() => template.BuildMessage()); + } + + [TestMethod] + public void SubscriptionSupport() + { + var template = new MqttTopicTemplate("A/v1/{param}/F"); + var filter = template.BuildFilter().WithAtLeastOnceQoS().WithNoLocal().Build(); + + Assert.AreEqual("A/v1/+/F", filter.Topic); + } + + [TestMethod] + public void SubscriptionSupport2() + { + var template = new MqttTopicTemplate("A/v1/{param}/F"); + + var subscribeOptions = new MqttClientFactory().CreateSubscribeOptionsBuilder().WithTopicTemplate(template).WithSubscriptionIdentifier(5).Build(); + + Assert.AreEqual("A/v1/+/F", subscribeOptions.TopicFilters[0].Topic); } } \ No newline at end of file diff --git a/Source/MQTTnet.Tests/Extensions/Rpc_Tests.cs b/Source/MQTTnet.Tests/Extensions/Rpc_Tests.cs index 02dddbfa..622f2fb7 100644 --- a/Source/MQTTnet.Tests/Extensions/Rpc_Tests.cs +++ b/Source/MQTTnet.Tests/Extensions/Rpc_Tests.cs @@ -14,268 +14,242 @@ using MQTTnet.Internal; using MQTTnet.Protocol; using MQTTnet.Tests.Mockups; -namespace MQTTnet.Tests.Extensions +namespace MQTTnet.Tests.Extensions; + +// ReSharper disable InconsistentNaming +[TestClass] +public sealed class Rpc_Tests : BaseTestClass { - [TestClass] - public sealed class Rpc_Tests : BaseTestClass + [TestMethod] + public async Task Execute_Success_MQTT_V5_Mixed_Clients() { - [TestMethod] - public async Task Execute_Success_MQTT_V5_Mixed_Clients() + using var testEnvironment = CreateTestEnvironment(); + await testEnvironment.StartServer(); + var responseSender = await testEnvironment.ConnectClient(); + await responseSender.SubscribeAsync("MQTTnet.RPC/+/ping"); + + responseSender.ApplicationMessageReceivedAsync += async e => { - using (var testEnvironment = CreateTestEnvironment()) - { - await testEnvironment.StartServer(); - var responseSender = await testEnvironment.ConnectClient(); - await responseSender.SubscribeAsync("MQTTnet.RPC/+/ping"); + Assert.IsNull(e.ApplicationMessage.ResponseTopic); + await responseSender.PublishStringAsync(e.ApplicationMessage.Topic + "/response", "pong").ConfigureAwait(false); + }; - responseSender.ApplicationMessageReceivedAsync += async e => - { - Assert.IsNull(e.ApplicationMessage.ResponseTopic); - await responseSender.PublishStringAsync(e.ApplicationMessage.Topic + "/response", "pong"); - }; + var requestSender = await testEnvironment.ConnectClient(new MqttClientOptionsBuilder().WithProtocolVersion(MqttProtocolVersion.V500)); - var requestSender = await testEnvironment.ConnectClient(new MqttClientOptionsBuilder().WithProtocolVersion(MqttProtocolVersion.V500)); + using var rpcClient = new MqttRpcClient(requestSender, new MqttRpcClientOptionsBuilder().Build()); + var response = await rpcClient.ExecuteAsync(TimeSpan.FromSeconds(5), "ping", "", MqttQualityOfServiceLevel.AtMostOnce); - using (var rpcClient = new MqttRpcClient(requestSender, new MqttRpcClientOptionsBuilder().Build())) - { - var response = await rpcClient.ExecuteAsync(TimeSpan.FromSeconds(5), "ping", "", MqttQualityOfServiceLevel.AtMostOnce); + Assert.AreEqual("pong", Encoding.UTF8.GetString(response)); + } - Assert.AreEqual("pong", Encoding.UTF8.GetString(response)); - } - } - } - - [TestMethod] - public async Task Execute_Success_Parameters_Propagated_Correctly() + [TestMethod] + public async Task Execute_Success_Parameters_Propagated_Correctly() + { + var paramValue = "123"; + var parameters = new Dictionary { - var paramValue = "123"; - var parameters = new Dictionary + { TestParametersTopicGenerationStrategy.ExpectedParamName, "123" } + }; + + using var testEnvironment = CreateTestEnvironment(); + await testEnvironment.StartServer(); + + var responseSender = await testEnvironment.ConnectClient(new MqttClientOptionsBuilder()); + await responseSender.SubscribeAsync($"MQTTnet.RPC/+/ping/{paramValue}"); + + responseSender.ApplicationMessageReceivedAsync += e => responseSender.PublishStringAsync(e.ApplicationMessage.Topic + "/response", "pong"); + + using var rpcClient = await testEnvironment.ConnectRpcClient(new MqttRpcClientOptionsBuilder() + .WithTopicGenerationStrategy(new TestParametersTopicGenerationStrategy()).Build()); + var response = await rpcClient.ExecuteAsync(TimeSpan.FromSeconds(5), "ping", "", MqttQualityOfServiceLevel.AtMostOnce, parameters); + + Assert.AreEqual("pong", Encoding.UTF8.GetString(response)); + } + + [TestMethod] + public Task Execute_Success_With_QoS_0() + { + return Execute_Success(MqttQualityOfServiceLevel.AtMostOnce, MqttProtocolVersion.V311); + } + + [TestMethod] + public Task Execute_Success_With_QoS_0_MQTT_V5() + { + return Execute_Success(MqttQualityOfServiceLevel.AtMostOnce, MqttProtocolVersion.V500); + } + + [TestMethod] + public Task Execute_Success_With_QoS_0_MQTT_V5_Use_ResponseTopic() + { + return Execute_Success_MQTT_V5(MqttQualityOfServiceLevel.AtMostOnce); + } + + [TestMethod] + public Task Execute_Success_With_QoS_1() + { + return Execute_Success(MqttQualityOfServiceLevel.AtLeastOnce, MqttProtocolVersion.V311); + } + + [TestMethod] + public Task Execute_Success_With_QoS_1_MQTT_V5() + { + return Execute_Success(MqttQualityOfServiceLevel.AtLeastOnce, MqttProtocolVersion.V500); + } + + [TestMethod] + public Task Execute_Success_With_QoS_1_MQTT_V5_Use_ResponseTopic() + { + return Execute_Success_MQTT_V5(MqttQualityOfServiceLevel.AtLeastOnce); + } + + [TestMethod] + public Task Execute_Success_With_QoS_2() + { + return Execute_Success(MqttQualityOfServiceLevel.ExactlyOnce, MqttProtocolVersion.V311); + } + + [TestMethod] + public Task Execute_Success_With_QoS_2_MQTT_V5() + { + return Execute_Success(MqttQualityOfServiceLevel.ExactlyOnce, MqttProtocolVersion.V500); + } + + [TestMethod] + public Task Execute_Success_With_QoS_2_MQTT_V5_Use_ResponseTopic() + { + return Execute_Success_MQTT_V5(MqttQualityOfServiceLevel.ExactlyOnce); + } + + + [TestMethod] + [ExpectedException(typeof(MqttCommunicationTimedOutException))] + public async Task Execute_Timeout() + { + using var testEnvironment = CreateTestEnvironment(); + await testEnvironment.StartServer(); + + var requestSender = await testEnvironment.ConnectClient(); + + var rpcClient = new MqttRpcClient(requestSender, new MqttRpcClientOptionsBuilder().Build()); + await rpcClient.ExecuteAsync(TimeSpan.FromSeconds(2), "ping", "", MqttQualityOfServiceLevel.AtMostOnce); + } + + [TestMethod] + [ExpectedException(typeof(MqttCommunicationTimedOutException))] + public async Task Execute_Timeout_MQTT_V5_Mixed_Clients() + { + using var testEnvironment = new TestEnvironment(TestContext); + await testEnvironment.StartServer(); + var responseSender = await testEnvironment.ConnectClient(); + await responseSender.SubscribeAsync("MQTTnet.RPC/+/ping"); + + responseSender.ApplicationMessageReceivedAsync += e => + { + Assert.IsNull(e.ApplicationMessage.ResponseTopic); + return CompletedTask.Instance; + }; + + var requestSender = await testEnvironment.ConnectClient(new MqttClientOptionsBuilder().WithProtocolVersion(MqttProtocolVersion.V500)); + + using var rpcClient = new MqttRpcClient(requestSender, new MqttRpcClientOptionsBuilder().Build()); + await rpcClient.ExecuteAsync(TimeSpan.FromSeconds(2), "ping", "", MqttQualityOfServiceLevel.AtMostOnce); + } + + [TestMethod] + [ExpectedException(typeof(MqttCommunicationTimedOutException))] + public async Task Execute_With_Custom_Topic_Names() + { + using var testEnvironment = CreateTestEnvironment(); + await testEnvironment.StartServer(); + + var rpcClient = await testEnvironment.ConnectRpcClient(new MqttRpcClientOptionsBuilder().WithTopicGenerationStrategy(new TestTopicStrategy()).Build()); + + await rpcClient.ExecuteAsync(TimeSpan.FromSeconds(2), "ping", "", MqttQualityOfServiceLevel.AtMostOnce); + } + + [TestMethod] + public void Use_Factory() + { + var factory = new MqttClientFactory(); + using var client = factory.CreateMqttClient(); + var rpcClient = factory.CreateMqttRpcClient(client); + + Assert.IsNotNull(rpcClient); + } + + async Task Execute_Success(MqttQualityOfServiceLevel qosLevel, MqttProtocolVersion protocolVersion) + { + using var testEnvironment = CreateTestEnvironment(); + await testEnvironment.StartServer(); + var responseSender = await testEnvironment.ConnectClient(new MqttClientOptionsBuilder().WithProtocolVersion(protocolVersion)); + await responseSender.SubscribeAsync("MQTTnet.RPC/+/ping", qosLevel); + + responseSender.ApplicationMessageReceivedAsync += e => responseSender.PublishStringAsync(e.ApplicationMessage.Topic + "/response", "pong"); + + var requestSender = await testEnvironment.ConnectClient(); + + using var rpcClient = new MqttRpcClient(requestSender, new MqttRpcClientOptionsBuilder().Build()); + var response = await rpcClient.ExecuteAsync(TimeSpan.FromSeconds(5), "ping", "", qosLevel); + + Assert.AreEqual("pong", Encoding.UTF8.GetString(response)); + } + + async Task Execute_Success_MQTT_V5(MqttQualityOfServiceLevel qosLevel) + { + using var testEnvironment = CreateTestEnvironment(); + await testEnvironment.StartServer(); + var responseSender = await testEnvironment.ConnectClient(new MqttClientOptionsBuilder().WithProtocolVersion(MqttProtocolVersion.V500)); + await responseSender.SubscribeAsync("MQTTnet.RPC/+/ping", qosLevel); + + responseSender.ApplicationMessageReceivedAsync += async e => + { + await responseSender.PublishStringAsync(e.ApplicationMessage.ResponseTopic, "pong").ConfigureAwait(false); + }; + + var requestSender = await testEnvironment.ConnectClient(new MqttClientOptionsBuilder().WithProtocolVersion(MqttProtocolVersion.V500)); + + using var rpcClient = new MqttRpcClient(requestSender, new MqttRpcClientOptionsBuilder().Build()); + var response = await rpcClient.ExecuteAsync(TimeSpan.FromSeconds(5), "ping", "", qosLevel); + + Assert.AreEqual("pong", Encoding.UTF8.GetString(response)); + } + + class TestTopicStrategy : IMqttRpcClientTopicGenerationStrategy + { + public MqttRpcTopicPair CreateRpcTopics(TopicGenerationContext context) + { + return new MqttRpcTopicPair { - { TestParametersTopicGenerationStrategy.ExpectedParamName, "123" } + RequestTopic = "a", + ResponseTopic = "b" }; + } + } - using (var testEnvironment = CreateTestEnvironment()) + class TestParametersTopicGenerationStrategy : IMqttRpcClientTopicGenerationStrategy + { + internal const string ExpectedParamName = "test_param_name"; + + public MqttRpcTopicPair CreateRpcTopics(TopicGenerationContext context) + { + if (context.Parameters == null) { - await testEnvironment.StartServer(); - - var responseSender = await testEnvironment.ConnectClient(new MqttClientOptionsBuilder()); - await responseSender.SubscribeAsync($"MQTTnet.RPC/+/ping/{paramValue}"); - - responseSender.ApplicationMessageReceivedAsync += e => responseSender.PublishStringAsync(e.ApplicationMessage.Topic + "/response", "pong"); - - using (var rpcClient = await testEnvironment.ConnectRpcClient(new MqttRpcClientOptionsBuilder() - .WithTopicGenerationStrategy(new TestParametersTopicGenerationStrategy()).Build())) - { - var response = await rpcClient.ExecuteAsync(TimeSpan.FromSeconds(5), "ping", "", MqttQualityOfServiceLevel.AtMostOnce, parameters); - - Assert.AreEqual("pong", Encoding.UTF8.GetString(response)); - } + throw new InvalidOperationException("Parameters dictionary expected to be not null"); } - } - [TestMethod] - public Task Execute_Success_With_QoS_0() - { - return Execute_Success(MqttQualityOfServiceLevel.AtMostOnce, MqttProtocolVersion.V311); - } - - [TestMethod] - public Task Execute_Success_With_QoS_0_MQTT_V5() - { - return Execute_Success(MqttQualityOfServiceLevel.AtMostOnce, MqttProtocolVersion.V500); - } - - [TestMethod] - public Task Execute_Success_With_QoS_0_MQTT_V5_Use_ResponseTopic() - { - return Execute_Success_MQTT_V5(MqttQualityOfServiceLevel.AtMostOnce); - } - - [TestMethod] - public Task Execute_Success_With_QoS_1() - { - return Execute_Success(MqttQualityOfServiceLevel.AtLeastOnce, MqttProtocolVersion.V311); - } - - [TestMethod] - public Task Execute_Success_With_QoS_1_MQTT_V5() - { - return Execute_Success(MqttQualityOfServiceLevel.AtLeastOnce, MqttProtocolVersion.V500); - } - - [TestMethod] - public Task Execute_Success_With_QoS_1_MQTT_V5_Use_ResponseTopic() - { - return Execute_Success_MQTT_V5(MqttQualityOfServiceLevel.AtLeastOnce); - } - - [TestMethod] - public Task Execute_Success_With_QoS_2() - { - return Execute_Success(MqttQualityOfServiceLevel.ExactlyOnce, MqttProtocolVersion.V311); - } - - [TestMethod] - public Task Execute_Success_With_QoS_2_MQTT_V5() - { - return Execute_Success(MqttQualityOfServiceLevel.ExactlyOnce, MqttProtocolVersion.V500); - } - - [TestMethod] - public Task Execute_Success_With_QoS_2_MQTT_V5_Use_ResponseTopic() - { - return Execute_Success_MQTT_V5(MqttQualityOfServiceLevel.ExactlyOnce); - } - - - [TestMethod] - [ExpectedException(typeof(MqttCommunicationTimedOutException))] - public async Task Execute_Timeout() - { - using (var testEnvironment = CreateTestEnvironment()) + if (!context.Parameters.TryGetValue(ExpectedParamName, out var paramValue)) { - await testEnvironment.StartServer(); - - var requestSender = await testEnvironment.ConnectClient(); - - var rpcClient = new MqttRpcClient(requestSender, new MqttRpcClientOptionsBuilder().Build()); - await rpcClient.ExecuteAsync(TimeSpan.FromSeconds(2), "ping", "", MqttQualityOfServiceLevel.AtMostOnce); + throw new InvalidOperationException($"Parameter with name {ExpectedParamName} not present"); } - } - [TestMethod] - [ExpectedException(typeof(MqttCommunicationTimedOutException))] - public async Task Execute_Timeout_MQTT_V5_Mixed_Clients() - { - using (var testEnvironment = new TestEnvironment(TestContext)) + var requestTopic = $"MQTTnet.RPC/{Guid.NewGuid():N}/{context.MethodName}/{paramValue}"; + var responseTopic = requestTopic + "/response"; + + return new MqttRpcTopicPair { - await testEnvironment.StartServer(); - var responseSender = await testEnvironment.ConnectClient(); - await responseSender.SubscribeAsync("MQTTnet.RPC/+/ping"); - - responseSender.ApplicationMessageReceivedAsync += async e => - { - Assert.IsNull(e.ApplicationMessage.ResponseTopic); - await CompletedTask.Instance; - }; - - var requestSender = await testEnvironment.ConnectClient(new MqttClientOptionsBuilder().WithProtocolVersion(MqttProtocolVersion.V500)); - - using (var rpcClient = new MqttRpcClient(requestSender, new MqttRpcClientOptionsBuilder().Build())) - { - await rpcClient.ExecuteAsync(TimeSpan.FromSeconds(2), "ping", "", MqttQualityOfServiceLevel.AtMostOnce); - } - } - } - - [TestMethod] - [ExpectedException(typeof(MqttCommunicationTimedOutException))] - public async Task Execute_With_Custom_Topic_Names() - { - using (var testEnvironment = CreateTestEnvironment()) - { - await testEnvironment.StartServer(); - - var rpcClient = await testEnvironment.ConnectRpcClient(new MqttRpcClientOptionsBuilder().WithTopicGenerationStrategy(new TestTopicStrategy()).Build()); - - await rpcClient.ExecuteAsync(TimeSpan.FromSeconds(2), "ping", "", MqttQualityOfServiceLevel.AtMostOnce); - } - } - - [TestMethod] - public void Use_Factory() - { - var factory = new MqttClientFactory(); - using (var client = factory.CreateMqttClient()) - { - var rpcClient = factory.CreateMqttRpcClient(client); - - Assert.IsNotNull(rpcClient); - } - } - - async Task Execute_Success(MqttQualityOfServiceLevel qosLevel, MqttProtocolVersion protocolVersion) - { - using (var testEnvironment = CreateTestEnvironment()) - { - await testEnvironment.StartServer(); - var responseSender = await testEnvironment.ConnectClient(new MqttClientOptionsBuilder().WithProtocolVersion(protocolVersion)); - await responseSender.SubscribeAsync("MQTTnet.RPC/+/ping", qosLevel); - - responseSender.ApplicationMessageReceivedAsync += e => responseSender.PublishStringAsync(e.ApplicationMessage.Topic + "/response", "pong"); - - var requestSender = await testEnvironment.ConnectClient(); - - using (var rpcClient = new MqttRpcClient(requestSender, new MqttRpcClientOptionsBuilder().Build())) - { - var response = await rpcClient.ExecuteAsync(TimeSpan.FromSeconds(5), "ping", "", qosLevel); - - Assert.AreEqual("pong", Encoding.UTF8.GetString(response)); - } - } - } - - async Task Execute_Success_MQTT_V5(MqttQualityOfServiceLevel qosLevel) - { - using (var testEnvironment = CreateTestEnvironment()) - { - await testEnvironment.StartServer(); - var responseSender = await testEnvironment.ConnectClient(new MqttClientOptionsBuilder().WithProtocolVersion(MqttProtocolVersion.V500)); - await responseSender.SubscribeAsync("MQTTnet.RPC/+/ping", qosLevel); - - responseSender.ApplicationMessageReceivedAsync += async e => - { - await responseSender.PublishStringAsync(e.ApplicationMessage.ResponseTopic, "pong"); - }; - - var requestSender = await testEnvironment.ConnectClient(new MqttClientOptionsBuilder().WithProtocolVersion(MqttProtocolVersion.V500)); - - using (var rpcClient = new MqttRpcClient(requestSender, new MqttRpcClientOptionsBuilder().Build())) - { - var response = await rpcClient.ExecuteAsync(TimeSpan.FromSeconds(5), "ping", "", qosLevel); - - Assert.AreEqual("pong", Encoding.UTF8.GetString(response)); - } - } - } - - class TestTopicStrategy : IMqttRpcClientTopicGenerationStrategy - { - public MqttRpcTopicPair CreateRpcTopics(TopicGenerationContext context) - { - return new MqttRpcTopicPair - { - RequestTopic = "a", - ResponseTopic = "b" - }; - } - } - - class TestParametersTopicGenerationStrategy : IMqttRpcClientTopicGenerationStrategy - { - internal const string ExpectedParamName = "test_param_name"; - - public MqttRpcTopicPair CreateRpcTopics(TopicGenerationContext context) - { - if (context.Parameters == null) - { - throw new InvalidOperationException("Parameters dictionary expected to be not null"); - } - - if (!context.Parameters.TryGetValue(ExpectedParamName, out var paramValue)) - { - throw new InvalidOperationException($"Parameter with name {ExpectedParamName} not present"); - } - - var requestTopic = $"MQTTnet.RPC/{Guid.NewGuid():N}/{context.MethodName}/{paramValue}"; - var responseTopic = requestTopic + "/response"; - - return new MqttRpcTopicPair - { - RequestTopic = requestTopic, - ResponseTopic = responseTopic - }; - } + RequestTopic = requestTopic, + ResponseTopic = responseTopic + }; } } } \ No newline at end of file diff --git a/Source/MQTTnet.Tests/Factory/MqttFactory_Tests.cs b/Source/MQTTnet.Tests/Factory/MqttFactory_Tests.cs index 5220cee3..e262a121 100644 --- a/Source/MQTTnet.Tests/Factory/MqttFactory_Tests.cs +++ b/Source/MQTTnet.Tests/Factory/MqttFactory_Tests.cs @@ -5,91 +5,90 @@ using Microsoft.VisualStudio.TestTools.UnitTesting; using MQTTnet.Server; -namespace MQTTnet.Tests.Factory +namespace MQTTnet.Tests.Factory; + +// ReSharper disable InconsistentNaming +[TestClass] +public sealed class MqttFactory_Tests : BaseTestClass { - [TestClass] - public sealed class MqttFactory_Tests : BaseTestClass + [TestMethod] + public void Create_ApplicationMessageBuilder() { + var factory = new MqttClientFactory(); + var builder = factory.CreateApplicationMessageBuilder(); - [TestMethod] - public void Create_ApplicationMessageBuilder() - { - var factory = new MqttClientFactory(); - var builder = factory.CreateApplicationMessageBuilder(); + Assert.IsNotNull(builder); + } - Assert.IsNotNull(builder); - } + [TestMethod] + public void Create_ClientOptionsBuilder() + { + var factory = new MqttClientFactory(); + var builder = factory.CreateClientOptionsBuilder(); - [TestMethod] - public void Create_ClientOptionsBuilder() - { - var factory = new MqttClientFactory(); - var builder = factory.CreateClientOptionsBuilder(); + Assert.IsNotNull(builder); + } - Assert.IsNotNull(builder); - } + [TestMethod] + public void Create_ServerOptionsBuilder() + { + var factory = new MqttServerFactory(); + var builder = factory.CreateServerOptionsBuilder(); - [TestMethod] - public void Create_ServerOptionsBuilder() - { - var factory = new MqttServerFactory(); - var builder = factory.CreateServerOptionsBuilder(); + Assert.IsNotNull(builder); + } - Assert.IsNotNull(builder); - } + [TestMethod] + public void Create_SubscribeOptionsBuilder() + { + var factory = new MqttClientFactory(); + var builder = factory.CreateSubscribeOptionsBuilder(); - [TestMethod] - public void Create_SubscribeOptionsBuilder() - { - var factory = new MqttClientFactory(); - var builder = factory.CreateSubscribeOptionsBuilder(); + Assert.IsNotNull(builder); + } - Assert.IsNotNull(builder); - } + [TestMethod] + public void Create_UnsubscribeOptionsBuilder() + { + var factory = new MqttClientFactory(); + var builder = factory.CreateUnsubscribeOptionsBuilder(); - [TestMethod] - public void Create_UnsubscribeOptionsBuilder() - { - var factory = new MqttClientFactory(); - var builder = factory.CreateUnsubscribeOptionsBuilder(); + Assert.IsNotNull(builder); + } - Assert.IsNotNull(builder); - } + [TestMethod] + public void Create_TopicFilterBuilder() + { + var factory = new MqttClientFactory(); + var builder = factory.CreateTopicFilterBuilder(); - [TestMethod] - public void Create_TopicFilterBuilder() - { - var factory = new MqttClientFactory(); - var builder = factory.CreateTopicFilterBuilder(); + Assert.IsNotNull(builder); + } - Assert.IsNotNull(builder); - } + [TestMethod] + public void Create_MqttServer() + { + var factory = new MqttServerFactory(); + var server = factory.CreateMqttServer(new MqttServerOptionsBuilder().Build()); - [TestMethod] - public void Create_MqttServer() - { - var factory = new MqttServerFactory(); - var server = factory.CreateMqttServer(new MqttServerOptionsBuilder().Build()); + Assert.IsNotNull(server); + } - Assert.IsNotNull(server); - } + [TestMethod] + public void Create_MqttClient() + { + var factory = new MqttClientFactory(); + var client = factory.CreateMqttClient(); - [TestMethod] - public void Create_MqttClient() - { - var factory = new MqttClientFactory(); - var client = factory.CreateMqttClient(); + Assert.IsNotNull(client); + } - Assert.IsNotNull(client); - } + [TestMethod] + public void Create_LowLevelMqttClient() + { + var factory = new MqttClientFactory(); + var client = factory.CreateLowLevelMqttClient(); - [TestMethod] - public void Create_LowLevelMqttClient() - { - var factory = new MqttClientFactory(); - var client = factory.CreateLowLevelMqttClient(); - - Assert.IsNotNull(client); - } + Assert.IsNotNull(client); } } \ No newline at end of file diff --git a/Source/MQTTnet.Tests/Formatter/MqttBufferReader_Tests.cs b/Source/MQTTnet.Tests/Formatter/MqttBufferReader_Tests.cs index 84fb48e0..0779b091 100644 --- a/Source/MQTTnet.Tests/Formatter/MqttBufferReader_Tests.cs +++ b/Source/MQTTnet.Tests/Formatter/MqttBufferReader_Tests.cs @@ -9,303 +9,303 @@ using Microsoft.VisualStudio.TestTools.UnitTesting; using MQTTnet.Exceptions; using MQTTnet.Formatter; -namespace MQTTnet.Tests.Formatter +namespace MQTTnet.Tests.Formatter; + +// ReSharper disable InconsistentNaming +[TestClass] +public sealed class MqttBufferReader_Tests { - [TestClass] - public sealed class MqttBufferReader_Tests + [TestMethod] + [ExpectedException(typeof(MqttProtocolViolationException), "Expected at least 4 bytes but there are only 3 bytes")] + public void Fire_Exception_If_Not_Enough_Data() { - [TestMethod] - [ExpectedException(typeof(MqttProtocolViolationException), "Expected at least 4 bytes but there are only 3 bytes")] - public void Fire_Exception_If_Not_Enough_Data() + var buffer = new byte[] { 0, 1, 2 }; + + var reader = new MqttBufferReader(); + + reader.SetBuffer(buffer, 0, 3); + + // 1 byte is missing. + reader.ReadFourByteInteger(); + } + + [TestMethod] + [ExpectedException(typeof(MqttProtocolViolationException), "Expected at least 4 bytes but there are only 3 bytes")] + public void Fire_Exception_If_Not_Enough_Data_With_Longer_Buffer() + { + var buffer = new byte[] { 0, 1, 2, 3, 4, 5, 6 }; + + var reader = new MqttBufferReader(); + + reader.SetBuffer(buffer, 0, 3); + + // 1 byte is missing. + reader.ReadFourByteInteger(); + } + + [TestMethod] + public void Is_EndOfStream_Without_Buffer() + { + var reader = new MqttBufferReader(); + + Assert.IsTrue(reader.EndOfStream); + Assert.AreEqual(0, reader.BytesLeft); + } + + [TestMethod] + public void Read_Remaining_Data_From_Larger_Buffer() + { + var buffer = new byte[] { 0, 1, 2, 3, 4, 5, 6, 7, 8, 9 }; + + var reader = new MqttBufferReader(); + + // The used buffer contains more data than used! + reader.SetBuffer(buffer, 0, 5); + + // This should only read 5 bytes even if more data is in the buffer + // due to custom bounds. + var remainingData = reader.ReadRemainingData(); + + Assert.IsTrue(reader.EndOfStream); + Assert.AreEqual(0, reader.BytesLeft); + Assert.AreEqual(5, remainingData.Length); + } + + + [TestMethod] + public void Read_Various_Positions_and_Offsets() + { + const int numBufferElements = 1000; + const int numTestSequences = 10000; + + const string testString = "The quick brown fox jumps over the lazy dog."; + + var rndSeed = Environment.TickCount; + // rndSeed = ...; // assign random seed here if there is an error so that it can be repeated + var rnd = new Random(rndSeed); + // rndSeed is printed when an assert fails + + var bufferElements = new List(); + + byte[] elementBuffer; + + // Fill a buffer with strings or numbers in big endian format + using (var ms = new MemoryStream()) { - var buffer = new byte[] { 0, 1, 2 }; - - var reader = new MqttBufferReader(); - - reader.SetBuffer(buffer, 0, 3); - - // 1 byte is missing. - reader.ReadFourByteInteger(); - } - - [TestMethod] - [ExpectedException(typeof(MqttProtocolViolationException), "Expected at least 4 bytes but there are only 3 bytes")] - public void Fire_Exception_If_Not_Enough_Data_With_Longer_Buffer() - { - var buffer = new byte[] { 0, 1, 2, 3, 4, 5, 6 }; - - var reader = new MqttBufferReader(); - - reader.SetBuffer(buffer, 0, 3); - - // 1 byte is missing. - reader.ReadFourByteInteger(); - } - - [TestMethod] - public void Is_EndOfStream_Without_Buffer() - { - var reader = new MqttBufferReader(); - - Assert.IsTrue(reader.EndOfStream); - Assert.AreEqual(0, reader.BytesLeft); - } - - [TestMethod] - public void Read_Remaining_Data_From_Larger_Buffer() - { - var buffer = new byte[] { 0, 1, 2, 3, 4, 5, 6, 7, 8, 9 }; - - var reader = new MqttBufferReader(); - - // The used buffer contains more data than used! - reader.SetBuffer(buffer, 0, 5); - - // This should only read 5 bytes even if more data is in the buffer - // due to custom bounds. - var remainingData = reader.ReadRemainingData(); - - Assert.IsTrue(reader.EndOfStream); - Assert.AreEqual(0, reader.BytesLeft); - Assert.AreEqual(5, remainingData.Length); - } - - - [TestMethod] - public void Read_Various_Positions_and_Offsets() - { - const int NumBufferElements = 1000; - const int NumTestSequences = 10000; - - const string TestString = "The quick brown fox jumps over the lazy dog."; - - var rndSeed = Environment.TickCount; - // rndSeed = ...; // assign random seed here if there is an error so that it can be repeated - var rnd = new Random(rndSeed); - // rndSeed is printed when an assert fails - - var bufferElements = new List(); - - byte[] elementBuffer; - - // Fill a buffer with strings or numbers in big endian format - using (var ms = new MemoryStream()) + using (var bw = new BinaryWriter(ms)) { - using (var bw = new BinaryWriter(ms)) + var bufferOffset = 0; + + for (var i = 0; i < numBufferElements; i++) { - var bufferOffset = 0; + var nextElementType = (ElementReference.BufferElementType)rnd.Next(ElementReference.NumBufferElementTypes); - for (var i = 0; i < NumBufferElements; i++) - { - var nextElementType = (ElementReference.BufferElementType)rnd.Next(ElementReference.NumBufferElementTypes); - - byte[] elementBytes = null; - uint elementNumberValue = 0; // all element number values fit into uint - string elementStringValue = null; - var elementSize = 0; - - var alreadyBigEndian = false; - switch (nextElementType) - { - case ElementReference.BufferElementType.Byte: - var byteValue = (byte)i; - elementNumberValue = byteValue; - elementSize = sizeof(byte); - elementBytes = new byte[1] { byteValue }; - alreadyBigEndian = true; // nothing to swap - break; - case ElementReference.BufferElementType.TwoByteInt: - var ushortValue = (ushort)i; - elementNumberValue = ushortValue; - elementSize = sizeof(ushort); - elementBytes = BitConverter.GetBytes(ushortValue); - break; - case ElementReference.BufferElementType.FourByteInt: - var uintValue = (uint)i; - elementNumberValue = uintValue; - elementSize = sizeof(uint); - elementBytes = BitConverter.GetBytes(uintValue); - break; - case ElementReference.BufferElementType.VariableSizeInt: - { - elementNumberValue = (uint)i; - var writer = new MqttBufferWriter(4, 4); - writer.WriteVariableByteInteger(elementNumberValue); - elementSize = writer.Length; - elementBytes = new byte[elementSize]; - var buffer = writer.GetBuffer(); - Array.Copy(buffer, elementBytes, elementSize); - alreadyBigEndian = true; // nothing to swap - } - break; - case ElementReference.BufferElementType.String: - { - var stringLen = rnd.Next(TestString.Length); - elementStringValue = TestString.Substring(0, stringLen); // could be empty - var writer = new MqttBufferWriter(stringLen + 1, stringLen + 1); - writer.WriteString(elementStringValue); - elementSize = writer.Length; - elementBytes = new byte[elementSize]; - var buffer = writer.GetBuffer(); - Array.Copy(buffer, elementBytes, elementSize); - alreadyBigEndian = true; // nothing to swap - } - break; - } - - if (BitConverter.IsLittleEndian && !alreadyBigEndian) - { - Array.Reverse(elementBytes); - } - - bw.Write(elementBytes); - bufferElements.Add(new ElementReference(nextElementType, elementSize, elementNumberValue, elementStringValue, bufferOffset)); - bufferOffset += elementSize; - } - - elementBuffer = ms.ToArray(); - } - } - - for (var i = 0; i < NumTestSequences; i++) - { - var firstElementIndex = rnd.Next(NumBufferElements - 1); - // segmentElementIndex will be < NumBufferElements - 1, which means there is at least one byte left for segment element count - var elementCount = rnd.Next(NumBufferElements - firstElementIndex) + 1; - var lastElementIndex = firstElementIndex + elementCount - 1; - // Use element index to get buffer offsets - var lastElement = bufferElements[lastElementIndex]; - var segmentStartPosition = bufferElements[firstElementIndex].BufferOffset; - var segmentEndPosition = lastElement.BufferOffset + lastElement.Size; - var segmentLength = segmentEndPosition - segmentStartPosition; - - var reader = new MqttBufferReader(); - reader.SetBuffer(elementBuffer, segmentStartPosition, segmentLength); - - // read all elements in the buffer segment; values should be as expected - for (var n = 0; n < elementCount; n++) - { - var element = bufferElements[firstElementIndex + n]; - uint elementNumberValue = 0; + byte[] elementBytes = null; + uint elementNumberValue = 0; // all element number values fit into uint string elementStringValue = null; + var elementSize = 0; - var expectedPosition = element.BufferOffset - segmentStartPosition; - Assert.AreEqual(expectedPosition, reader.Position); - - switch (element.Type) + var alreadyBigEndian = false; + switch (nextElementType) { case ElementReference.BufferElementType.Byte: - { - elementNumberValue = reader.ReadByte(); - } + var byteValue = (byte)i; + elementNumberValue = byteValue; + elementSize = sizeof(byte); + elementBytes = [byteValue]; + alreadyBigEndian = true; // nothing to swap break; case ElementReference.BufferElementType.TwoByteInt: - { - elementNumberValue = reader.ReadTwoByteInteger(); - } + var ushortValue = (ushort)i; + elementNumberValue = ushortValue; + elementSize = sizeof(ushort); + elementBytes = BitConverter.GetBytes(ushortValue); break; case ElementReference.BufferElementType.FourByteInt: - { - elementNumberValue = reader.ReadFourByteInteger(); - } + var uintValue = (uint)i; + elementNumberValue = uintValue; + elementSize = sizeof(uint); + elementBytes = BitConverter.GetBytes(uintValue); break; case ElementReference.BufferElementType.VariableSizeInt: { - elementNumberValue = reader.ReadVariableByteInteger(); + elementNumberValue = (uint)i; + var writer = new MqttBufferWriter(4, 4); + writer.WriteVariableByteInteger(elementNumberValue); + elementSize = writer.Length; + elementBytes = new byte[elementSize]; + var buffer = writer.GetBuffer(); + Array.Copy(buffer, elementBytes, elementSize); + alreadyBigEndian = true; // nothing to swap } break; case ElementReference.BufferElementType.String: { - elementStringValue = reader.ReadString(); + var stringLen = rnd.Next(testString.Length); + elementStringValue = testString[..stringLen]; // could be empty + var writer = new MqttBufferWriter(stringLen + 1, stringLen + 1); + writer.WriteString(elementStringValue); + elementSize = writer.Length; + elementBytes = new byte[elementSize]; + var buffer = writer.GetBuffer(); + Array.Copy(buffer, elementBytes, elementSize); + alreadyBigEndian = true; // nothing to swap } break; } - if (elementStringValue != null) + if (BitConverter.IsLittleEndian && !alreadyBigEndian) { - Assert.AreEqual(elementStringValue, element.StringValue, $"'{elementStringValue}' not equal '{element.StringValue}' with random seed {rndSeed}."); - } - else - { - Assert.AreEqual(elementNumberValue, element.NumberValue, $"{elementNumberValue} not equal {element.NumberValue} with random seed {rndSeed}."); + Array.Reverse(elementBytes); } + + bw.Write(elementBytes); + bufferElements.Add(new ElementReference(nextElementType, elementSize, elementNumberValue, elementStringValue, bufferOffset)); + bufferOffset += elementSize; } - // confirm end of stream - Assert.AreEqual(0, reader.BytesLeft, "BytesLeft not zero as expected with random seed " + rndSeed); - Assert.IsTrue(reader.EndOfStream, "Not end of stream as expected with random seed " + rndSeed); + elementBuffer = ms.ToArray(); } } - [TestMethod] - public void Report_Correct_Length_For_Full_Buffer() + for (var i = 0; i < numTestSequences; i++) { - var buffer = new byte[] { 5, 6, 7, 8, 9 }; + var firstElementIndex = rnd.Next(numBufferElements - 1); + // segmentElementIndex will be < NumBufferElements - 1, which means there is at least one byte left for segment element count + var elementCount = rnd.Next(numBufferElements - firstElementIndex) + 1; + var lastElementIndex = firstElementIndex + elementCount - 1; + // Use element index to get buffer offsets + var lastElement = bufferElements[lastElementIndex]; + var segmentStartPosition = bufferElements[firstElementIndex].BufferOffset; + var segmentEndPosition = lastElement.BufferOffset + lastElement.Size; + var segmentLength = segmentEndPosition - segmentStartPosition; var reader = new MqttBufferReader(); - reader.SetBuffer(buffer, 0, 5); + reader.SetBuffer(elementBuffer, segmentStartPosition, segmentLength); - Assert.IsFalse(reader.EndOfStream); - Assert.AreEqual(5, reader.BytesLeft); - } - - [TestMethod] - public void Report_Correct_Length_For_Partial_End_Buffer() - { - var buffer = new byte[] { 0, 1, 2, 3, 4, 5, 6, 7, 8, 9 }; - - var reader = new MqttBufferReader(); - - // The used buffer contains more data than used! - reader.SetBuffer(buffer, 5, 5); - - Assert.IsFalse(reader.EndOfStream); - Assert.AreEqual(5, reader.BytesLeft); - } - - [TestMethod] - public void Report_Correct_Length_For_Partial_Start_Buffer() - { - var buffer = new byte[] { 0, 1, 2, 3, 4, 5, 6, 7, 8, 9 }; - - var reader = new MqttBufferReader(); - - // The used buffer contains more data than used! - reader.SetBuffer(buffer, 0, 5); - - Assert.IsFalse(reader.EndOfStream); - Assert.AreEqual(5, reader.BytesLeft); - } - - - // Helper class to build up a reference to elements of various types in a buffer - class ElementReference - { - public enum BufferElementType + // read all elements in the buffer segment; values should be as expected + for (var n = 0; n < elementCount; n++) { - Byte, - TwoByteInt, - FourByteInt, - VariableSizeInt, - String + var element = bufferElements[firstElementIndex + n]; + uint elementNumberValue = 0; + string elementStringValue = null; + + var expectedPosition = element.BufferOffset - segmentStartPosition; + Assert.AreEqual(expectedPosition, reader.Position); + + switch (element.Type) + { + case ElementReference.BufferElementType.Byte: + { + elementNumberValue = reader.ReadByte(); + } + break; + case ElementReference.BufferElementType.TwoByteInt: + { + elementNumberValue = reader.ReadTwoByteInteger(); + } + break; + case ElementReference.BufferElementType.FourByteInt: + { + elementNumberValue = reader.ReadFourByteInteger(); + } + break; + case ElementReference.BufferElementType.VariableSizeInt: + { + elementNumberValue = reader.ReadVariableByteInteger(); + } + break; + case ElementReference.BufferElementType.String: + { + elementStringValue = reader.ReadString(); + } + break; + } + + if (elementStringValue != null) + { + Assert.AreEqual(elementStringValue, element.StringValue, $"'{elementStringValue}' not equal '{element.StringValue}' with random seed {rndSeed}."); + } + else + { + Assert.AreEqual(elementNumberValue, element.NumberValue, $"{elementNumberValue} not equal {element.NumberValue} with random seed {rndSeed}."); + } } - public const int NumBufferElementTypes = 5; - - public ElementReference(BufferElementType type, int size, uint numberValue, string stringValue, int bufferOffset) - { - Type = type; - Size = size; - NumberValue = numberValue; - StringValue = stringValue; - BufferOffset = bufferOffset; - } - - public int BufferOffset { get; } - public uint NumberValue { get; } - public int Size { get; } - public string StringValue { get; } - public BufferElementType Type { get; } + // confirm end of stream + Assert.AreEqual(0, reader.BytesLeft, "BytesLeft not zero as expected with random seed " + rndSeed); + Assert.IsTrue(reader.EndOfStream, "Not end of stream as expected with random seed " + rndSeed); } } + + [TestMethod] + public void Report_Correct_Length_For_Full_Buffer() + { + var buffer = new byte[] { 5, 6, 7, 8, 9 }; + + var reader = new MqttBufferReader(); + reader.SetBuffer(buffer, 0, 5); + + Assert.IsFalse(reader.EndOfStream); + Assert.AreEqual(5, reader.BytesLeft); + } + + [TestMethod] + public void Report_Correct_Length_For_Partial_End_Buffer() + { + var buffer = new byte[] { 0, 1, 2, 3, 4, 5, 6, 7, 8, 9 }; + + var reader = new MqttBufferReader(); + + // The used buffer contains more data than used! + reader.SetBuffer(buffer, 5, 5); + + Assert.IsFalse(reader.EndOfStream); + Assert.AreEqual(5, reader.BytesLeft); + } + + [TestMethod] + public void Report_Correct_Length_For_Partial_Start_Buffer() + { + var buffer = new byte[] { 0, 1, 2, 3, 4, 5, 6, 7, 8, 9 }; + + var reader = new MqttBufferReader(); + + // The used buffer contains more data than used! + reader.SetBuffer(buffer, 0, 5); + + Assert.IsFalse(reader.EndOfStream); + Assert.AreEqual(5, reader.BytesLeft); + } + + + // Helper class to build up a reference to elements of various types in a buffer + class ElementReference + { + public enum BufferElementType + { + Byte, + TwoByteInt, + FourByteInt, + VariableSizeInt, + String + } + + public const int NumBufferElementTypes = 5; + + public ElementReference(BufferElementType type, int size, uint numberValue, string stringValue, int bufferOffset) + { + Type = type; + Size = size; + NumberValue = numberValue; + StringValue = stringValue; + BufferOffset = bufferOffset; + } + + public int BufferOffset { get; } + public uint NumberValue { get; } + public int Size { get; } + public string StringValue { get; } + public BufferElementType Type { get; } + } } \ No newline at end of file diff --git a/Source/MQTTnet.Tests/Formatter/MqttPacketSerialization_V3_Binary_Tests.cs b/Source/MQTTnet.Tests/Formatter/MqttPacketSerialization_V3_Binary_Tests.cs index 8343e2f5..5a8d4fd3 100644 --- a/Source/MQTTnet.Tests/Formatter/MqttPacketSerialization_V3_Binary_Tests.cs +++ b/Source/MQTTnet.Tests/Formatter/MqttPacketSerialization_V3_Binary_Tests.cs @@ -4,9 +4,7 @@ using System; using System.Buffers; -using System.Collections.Generic; using System.IO; -using System.Text; using System.Threading; using Microsoft.VisualStudio.TestTools.UnitTesting; using MQTTnet.Adapter; @@ -18,601 +16,593 @@ using MQTTnet.Protocol; using MQTTnet.Tests.Helpers; using MQTTnet.Tests.Mockups; -namespace MQTTnet.Tests.Formatter +namespace MQTTnet.Tests.Formatter; + +// ReSharper disable InconsistentNaming +[TestClass] +public sealed class MqttPacketSerialization_V3_Binary_Tests { - [TestClass] - public sealed class MqttPacketSerialization_V3_Binary_Tests + [TestMethod] + public void DeserializeV310_MqttConnAckPacket() { - [TestMethod] - public void DeserializeV310_MqttConnAckPacket() + var p = new MqttConnAckPacket { - var p = new MqttConnAckPacket - { - ReturnCode = MqttConnectReturnCode.ConnectionRefusedNotAuthorized - }; + ReturnCode = MqttConnectReturnCode.ConnectionRefusedNotAuthorized + }; - DeserializeAndCompare(p, "IAIABQ==", MqttProtocolVersion.V310); - } + DeserializeAndCompare(p, "IAIABQ==", MqttProtocolVersion.V310); + } - [TestMethod] - public void DeserializeV311_MqttConnAckPacket() + [TestMethod] + public void DeserializeV311_MqttConnAckPacket() + { + var p = new MqttConnAckPacket { - var p = new MqttConnAckPacket - { - IsSessionPresent = true, - ReturnCode = MqttConnectReturnCode.ConnectionRefusedNotAuthorized - }; + IsSessionPresent = true, + ReturnCode = MqttConnectReturnCode.ConnectionRefusedNotAuthorized + }; - DeserializeAndCompare(p, "IAIBBQ=="); - } + DeserializeAndCompare(p, "IAIBBQ=="); + } - [TestMethod] - public void DeserializeV311_MqttConnectPacket() + [TestMethod] + public void DeserializeV311_MqttConnectPacket() + { + var p = new MqttConnectPacket { - var p = new MqttConnectPacket - { - ClientId = "XYZ", - Password = Encoding.UTF8.GetBytes("PASS"), - Username = "USER", - KeepAlivePeriod = 123, - CleanSession = true - }; + ClientId = "XYZ", + Password = "PASS"u8.ToArray(), + Username = "USER", + KeepAlivePeriod = 123, + CleanSession = true + }; - DeserializeAndCompare(p, "EBsABE1RVFQEwgB7AANYWVoABFVTRVIABFBBU1M="); - } + DeserializeAndCompare(p, "EBsABE1RVFQEwgB7AANYWVoABFVTRVIABFBBU1M="); + } - [TestMethod] - public void DeserializeV311_MqttConnectPacketWithWillMessage() + [TestMethod] + public void DeserializeV311_MqttConnectPacketWithWillMessage() + { + var p = new MqttConnectPacket { - var p = new MqttConnectPacket - { - ClientId = "XYZ", - Password = Encoding.UTF8.GetBytes("PASS"), - Username = "USER", - KeepAlivePeriod = 123, - CleanSession = true, - WillFlag = true, - WillTopic = "My/last/will", - WillMessage = Encoding.UTF8.GetBytes("Good byte."), - WillQoS = MqttQualityOfServiceLevel.AtLeastOnce, - WillRetain = true - }; + ClientId = "XYZ", + Password = "PASS"u8.ToArray(), + Username = "USER", + KeepAlivePeriod = 123, + CleanSession = true, + WillFlag = true, + WillTopic = "My/last/will", + WillMessage = "Good byte."u8.ToArray(), + WillQoS = MqttQualityOfServiceLevel.AtLeastOnce, + WillRetain = true + }; - DeserializeAndCompare(p, "EDUABE1RVFQE7gB7AANYWVoADE15L2xhc3Qvd2lsbAAKR29vZCBieXRlLgAEVVNFUgAEUEFTUw=="); - } + DeserializeAndCompare(p, "EDUABE1RVFQE7gB7AANYWVoADE15L2xhc3Qvd2lsbAAKR29vZCBieXRlLgAEVVNFUgAEUEFTUw=="); + } - [TestMethod] - public void DeserializeV311_MqttPubAckPacket() + [TestMethod] + public void DeserializeV311_MqttPubAckPacket() + { + var p = new MqttPubAckPacket { - var p = new MqttPubAckPacket - { - PacketIdentifier = 123 - }; + PacketIdentifier = 123 + }; - DeserializeAndCompare(p, "QAIAew=="); - } + DeserializeAndCompare(p, "QAIAew=="); + } - [TestMethod] - public void DeserializeV311_MqttPubCompPacket() + [TestMethod] + public void DeserializeV311_MqttPubCompPacket() + { + var p = new MqttPubCompPacket { - var p = new MqttPubCompPacket - { - PacketIdentifier = 123 - }; + PacketIdentifier = 123 + }; - DeserializeAndCompare(p, "cAIAew=="); - } + DeserializeAndCompare(p, "cAIAew=="); + } - [TestMethod] - public void DeserializeV311_MqttPublishPacket() + [TestMethod] + public void DeserializeV311_MqttPublishPacket() + { + var p = new MqttPublishPacket { - var p = new MqttPublishPacket - { - PacketIdentifier = 123, - Dup = true, - Retain = true, - PayloadSegment = new ArraySegment(Encoding.ASCII.GetBytes("HELLO")), - QualityOfServiceLevel = MqttQualityOfServiceLevel.AtLeastOnce, - Topic = "A/B/C" - }; + PacketIdentifier = 123, + Dup = true, + Retain = true, + PayloadSegment = new ArraySegment("HELLO"u8.ToArray()), + QualityOfServiceLevel = MqttQualityOfServiceLevel.AtLeastOnce, + Topic = "A/B/C" + }; - DeserializeAndCompare(p, "Ow4ABUEvQi9DAHtIRUxMTw=="); - } + DeserializeAndCompare(p, "Ow4ABUEvQi9DAHtIRUxMTw=="); + } - - [TestMethod] - public void DeserializeV311_MqttPublishPacket_DupFalse() + [TestMethod] + public void DeserializeV311_MqttPublishPacket_DupFalse() + { + var p = new MqttPublishPacket { - var p = new MqttPublishPacket - { - Dup = false - }; + Dup = false + }; - var p2 = Roundtrip(p); + var p2 = Roundtrip(p); - Assert.AreEqual(p.Dup, p2.Dup); - } + Assert.AreEqual(p.Dup, p2.Dup); + } - [TestMethod] - public void DeserializeV311_MqttPublishPacket_Qos1() + [TestMethod] + public void DeserializeV311_MqttPublishPacket_Qos1() + { + var p = new MqttPublishPacket { - var p = new MqttPublishPacket - { - QualityOfServiceLevel = MqttQualityOfServiceLevel.AtMostOnce - }; + QualityOfServiceLevel = MqttQualityOfServiceLevel.AtMostOnce + }; - var p2 = Roundtrip(p); + var p2 = Roundtrip(p); - Assert.AreEqual(p.QualityOfServiceLevel, p2.QualityOfServiceLevel); - Assert.AreEqual(p.Dup, p2.Dup); - } + Assert.AreEqual(p.QualityOfServiceLevel, p2.QualityOfServiceLevel); + Assert.AreEqual(p.Dup, p2.Dup); + } - [TestMethod] - public void DeserializeV311_MqttPublishPacket_Qos2() + [TestMethod] + public void DeserializeV311_MqttPublishPacket_Qos2() + { + var p = new MqttPublishPacket { - var p = new MqttPublishPacket - { - QualityOfServiceLevel = MqttQualityOfServiceLevel.AtLeastOnce, - PacketIdentifier = 1 - }; + QualityOfServiceLevel = MqttQualityOfServiceLevel.AtLeastOnce, + PacketIdentifier = 1 + }; - var p2 = Roundtrip(p); + var p2 = Roundtrip(p); - Assert.AreEqual(p.QualityOfServiceLevel, p2.QualityOfServiceLevel); - Assert.AreEqual(p.Dup, p2.Dup); - } + Assert.AreEqual(p.QualityOfServiceLevel, p2.QualityOfServiceLevel); + Assert.AreEqual(p.Dup, p2.Dup); + } - [TestMethod] - public void DeserializeV311_MqttPublishPacket_Qos3() + [TestMethod] + public void DeserializeV311_MqttPublishPacket_Qos3() + { + var p = new MqttPublishPacket { - var p = new MqttPublishPacket - { - QualityOfServiceLevel = MqttQualityOfServiceLevel.ExactlyOnce, - PacketIdentifier = 1 - }; + QualityOfServiceLevel = MqttQualityOfServiceLevel.ExactlyOnce, + PacketIdentifier = 1 + }; - var p2 = Roundtrip(p); + var p2 = Roundtrip(p); - Assert.AreEqual(p.QualityOfServiceLevel, p2.QualityOfServiceLevel); - Assert.AreEqual(p.Dup, p2.Dup); - } + Assert.AreEqual(p.QualityOfServiceLevel, p2.QualityOfServiceLevel); + Assert.AreEqual(p.Dup, p2.Dup); + } - [TestMethod] - public void DeserializeV311_MqttPubRecPacket() + [TestMethod] + public void DeserializeV311_MqttPubRecPacket() + { + var p = new MqttPubRecPacket { - var p = new MqttPubRecPacket - { - PacketIdentifier = 123 - }; + PacketIdentifier = 123 + }; - DeserializeAndCompare(p, "UAIAew=="); - } + DeserializeAndCompare(p, "UAIAew=="); + } - [TestMethod] - public void DeserializeV311_MqttPubRelPacket() + [TestMethod] + public void DeserializeV311_MqttPubRelPacket() + { + var p = new MqttPubRelPacket { - var p = new MqttPubRelPacket - { - PacketIdentifier = 123 - }; + PacketIdentifier = 123 + }; - DeserializeAndCompare(p, "YgIAew=="); - } + DeserializeAndCompare(p, "YgIAew=="); + } - [TestMethod] - public void DeserializeV311_MqttSubAckPacket() + [TestMethod] + public void DeserializeV311_MqttSubAckPacket() + { + var p = new MqttSubAckPacket { - var p = new MqttSubAckPacket - { - PacketIdentifier = 123, - ReasonCodes = new List - { - MqttSubscribeReasonCode.GrantedQoS0, - MqttSubscribeReasonCode.GrantedQoS1, - MqttSubscribeReasonCode.GrantedQoS2, - MqttSubscribeReasonCode.UnspecifiedError - } - }; + PacketIdentifier = 123, + ReasonCodes = + [ + MqttSubscribeReasonCode.GrantedQoS0, + MqttSubscribeReasonCode.GrantedQoS1, + MqttSubscribeReasonCode.GrantedQoS2, + MqttSubscribeReasonCode.UnspecifiedError + ] + }; - DeserializeAndCompare(p, "kAYAewABAoA="); - } + DeserializeAndCompare(p, "kAYAewABAoA="); + } - [TestMethod] - public void DeserializeV311_MqttSubscribePacket() + [TestMethod] + public void DeserializeV311_MqttSubscribePacket() + { + var p = new MqttSubscribePacket { - var p = new MqttSubscribePacket - { - PacketIdentifier = 123, - TopicFilters = new List - { - new MqttTopicFilter { Topic = "A/B/C", QualityOfServiceLevel = MqttQualityOfServiceLevel.ExactlyOnce }, - new MqttTopicFilter { Topic = "1/2/3", QualityOfServiceLevel = MqttQualityOfServiceLevel.AtLeastOnce }, - new MqttTopicFilter { Topic = "x/y/z", QualityOfServiceLevel = MqttQualityOfServiceLevel.AtMostOnce } - } - }; + PacketIdentifier = 123, + TopicFilters = + [ + new MqttTopicFilter { Topic = "A/B/C", QualityOfServiceLevel = MqttQualityOfServiceLevel.ExactlyOnce }, + new MqttTopicFilter { Topic = "1/2/3", QualityOfServiceLevel = MqttQualityOfServiceLevel.AtLeastOnce }, + new MqttTopicFilter { Topic = "x/y/z", QualityOfServiceLevel = MqttQualityOfServiceLevel.AtMostOnce } + ] + }; - DeserializeAndCompare(p, "ghoAewAFQS9CL0MCAAUxLzIvMwEABXgveS96AA=="); - } + DeserializeAndCompare(p, "ghoAewAFQS9CL0MCAAUxLzIvMwEABXgveS96AA=="); + } - [TestMethod] - public void DeserializeV311_MqttUnsubAckPacket() + [TestMethod] + public void DeserializeV311_MqttUnsubAckPacket() + { + var p = new MqttUnsubAckPacket { - var p = new MqttUnsubAckPacket - { - PacketIdentifier = 123 - }; + PacketIdentifier = 123 + }; - DeserializeAndCompare(p, "sAIAew=="); - } + DeserializeAndCompare(p, "sAIAew=="); + } - [TestMethod] - public void DeserializeV311_MqttUnsubscribePacket() + [TestMethod] + public void DeserializeV311_MqttUnsubscribePacket() + { + var p = new MqttUnsubscribePacket { - var p = new MqttUnsubscribePacket - { - PacketIdentifier = 123 - }; + PacketIdentifier = 123 + }; - p.TopicFilters.Add("A/B/C"); - p.TopicFilters.Add("1/2/3"); - p.TopicFilters.Add("x/y/z"); + p.TopicFilters.Add("A/B/C"); + p.TopicFilters.Add("1/2/3"); + p.TopicFilters.Add("x/y/z"); - DeserializeAndCompare(p, "ohcAewAFQS9CL0MABTEvMi8zAAV4L3kveg=="); - } + DeserializeAndCompare(p, "ohcAewAFQS9CL0MABTEvMi8zAAV4L3kveg=="); + } - [TestMethod] - public void DetectVersionFromMqttConnectPacket() + [TestMethod] + public void DetectVersionFromMqttConnectPacket() + { + var packet = new MqttConnectPacket { - var packet = new MqttConnectPacket - { - ClientId = "XYZ", - Password = Encoding.UTF8.GetBytes("PASS"), - Username = "USER", - KeepAlivePeriod = 123, - CleanSession = true - }; + ClientId = "XYZ", + Password = "PASS"u8.ToArray(), + Username = "USER", + KeepAlivePeriod = 123, + CleanSession = true + }; - Assert.AreEqual( - MqttProtocolVersion.V310, - DeserializeAndDetectVersion(new MqttPacketFormatterAdapter(new MqttBufferWriter(4096, 65535)), Serialize(packet, MqttProtocolVersion.V310))); + Assert.AreEqual( + MqttProtocolVersion.V310, + DeserializeAndDetectVersion(new MqttPacketFormatterAdapter(new MqttBufferWriter(4096, 65535)), Serialize(packet, MqttProtocolVersion.V310))); - Assert.AreEqual( - MqttProtocolVersion.V311, - DeserializeAndDetectVersion(new MqttPacketFormatterAdapter(new MqttBufferWriter(4096, 65535)), Serialize(packet, MqttProtocolVersion.V311))); + Assert.AreEqual( + MqttProtocolVersion.V311, + DeserializeAndDetectVersion(new MqttPacketFormatterAdapter(new MqttBufferWriter(4096, 65535)), Serialize(packet, MqttProtocolVersion.V311))); - Assert.AreEqual( - MqttProtocolVersion.V500, - DeserializeAndDetectVersion(new MqttPacketFormatterAdapter(new MqttBufferWriter(4096, 65535)), Serialize(packet, MqttProtocolVersion.V500))); + Assert.AreEqual( + MqttProtocolVersion.V500, + DeserializeAndDetectVersion(new MqttPacketFormatterAdapter(new MqttBufferWriter(4096, 65535)), Serialize(packet, MqttProtocolVersion.V500))); - var adapter = new MqttPacketFormatterAdapter(new MqttBufferWriter(4096, 65535)); + var adapter = new MqttPacketFormatterAdapter(new MqttBufferWriter(4096, 65535)); - var ex = Assert.ThrowsException( - () => DeserializeAndDetectVersion(adapter, WriterFactory().AddMqttHeader(MqttControlPacketType.Connect, new byte[0]))); - Assert.AreEqual("CONNECT packet must have at least 7 bytes.", ex.Message); - ex = Assert.ThrowsException( - () => DeserializeAndDetectVersion(adapter, WriterFactory().AddMqttHeader(MqttControlPacketType.Connect, new byte[7]))); - Assert.AreEqual("Protocol '' not supported.", ex.Message); - ex = Assert.ThrowsException( - () => DeserializeAndDetectVersion(adapter, WriterFactory().AddMqttHeader(MqttControlPacketType.Connect, new byte[] { 255, 255, 0, 0, 0, 0, 0 }))); - Assert.AreEqual("Expected at least 65537 bytes but there are only 7 bytes", ex.Message); - } + var ex = Assert.ThrowsException( + () => DeserializeAndDetectVersion(adapter, WriterFactory().AddMqttHeader(MqttControlPacketType.Connect, []))); + Assert.AreEqual("CONNECT packet must have at least 7 bytes.", ex.Message); + ex = Assert.ThrowsException( + () => DeserializeAndDetectVersion(adapter, WriterFactory().AddMqttHeader(MqttControlPacketType.Connect, new byte[7]))); + Assert.AreEqual("Protocol '' not supported.", ex.Message); + ex = Assert.ThrowsException( + () => DeserializeAndDetectVersion(adapter, WriterFactory().AddMqttHeader(MqttControlPacketType.Connect, [255, 255, 0, 0, 0, 0, 0]))); + Assert.AreEqual("Expected at least 65537 bytes but there are only 7 bytes", ex.Message); + } - [TestMethod] - public void Serialize_LargePacket() + [TestMethod] + public void Serialize_LargePacket() + { + const int payloadLength = 80000; + + var payload = new byte[payloadLength]; + + var value = 0; + for (var i = 0; i < payloadLength; i++) { - const int payloadLength = 80000; - - var payload = new byte[payloadLength]; - - var value = 0; - for (var i = 0; i < payloadLength; i++) + if (value > 255) { - if (value > 255) - { - value = 0; - } - - payload[i] = (byte)value; + value = 0; } - var publishPacket = new MqttPublishPacket - { - Topic = "abcdefghijklmnopqrstuvwxyz0123456789", - PayloadSegment = new ArraySegment(payload) - }; - - var serializationHelper = new MqttPacketSerializationHelper(); - - var buffer = serializationHelper.Encode(publishPacket); - var publishPacketCopy = serializationHelper.Decode(buffer) as MqttPublishPacket; - - Assert.IsNotNull(publishPacketCopy); - Assert.AreEqual(publishPacket.Topic, publishPacketCopy.Topic); - CollectionAssert.AreEqual(publishPacket.Payload.ToArray(), publishPacketCopy.Payload.ToArray()); - - // Now modify the payload and test again. - publishPacket.PayloadSegment = new ArraySegment(Encoding.UTF8.GetBytes("MQTT")); - - buffer = serializationHelper.Encode(publishPacket); - var publishPacketCopy2 = serializationHelper.Decode(buffer) as MqttPublishPacket; - - Assert.IsNotNull(publishPacketCopy2); - Assert.AreEqual(publishPacket.Topic, publishPacketCopy2.Topic); - CollectionAssert.AreEqual(publishPacket.Payload.ToArray(), publishPacketCopy2.Payload.ToArray()); + payload[i] = (byte)value; + value++; } - [TestMethod] - public void SerializeV310_MqttConnAckPacket() + var publishPacket = new MqttPublishPacket { - var p = new MqttConnAckPacket - { - ReturnCode = MqttConnectReturnCode.ConnectionRefusedNotAuthorized - }; + Topic = "abcdefghijklmnopqrstuvwxyz0123456789", + PayloadSegment = new ArraySegment(payload) + }; - SerializeAndCompare(p, "IAIABQ==", MqttProtocolVersion.V310); - } + var serializationHelper = new MqttPacketSerializationHelper(); - [TestMethod] - public void SerializeV310_MqttConnectPacket() + var buffer = serializationHelper.Encode(publishPacket); + var publishPacketCopy = serializationHelper.Decode(buffer) as MqttPublishPacket; + + Assert.IsNotNull(publishPacketCopy); + Assert.AreEqual(publishPacket.Topic, publishPacketCopy.Topic); + CollectionAssert.AreEqual(publishPacket.Payload.ToArray(), publishPacketCopy.Payload.ToArray()); + + // Now modify the payload and test again. + publishPacket.PayloadSegment = new ArraySegment("MQTT"u8.ToArray()); + + buffer = serializationHelper.Encode(publishPacket); + var publishPacketCopy2 = serializationHelper.Decode(buffer) as MqttPublishPacket; + + Assert.IsNotNull(publishPacketCopy2); + Assert.AreEqual(publishPacket.Topic, publishPacketCopy2.Topic); + CollectionAssert.AreEqual(publishPacket.Payload.ToArray(), publishPacketCopy2.Payload.ToArray()); + } + + [TestMethod] + public void SerializeV310_MqttConnAckPacket() + { + var p = new MqttConnAckPacket { - var p = new MqttConnectPacket - { - ClientId = "XYZ", - Password = Encoding.UTF8.GetBytes("PASS"), - Username = "USER", - KeepAlivePeriod = 123, - CleanSession = true - }; + ReturnCode = MqttConnectReturnCode.ConnectionRefusedNotAuthorized + }; - SerializeAndCompare(p, "EB0ABk1RSXNkcAPCAHsAA1hZWgAEVVNFUgAEUEFTUw==", MqttProtocolVersion.V310); - } + SerializeAndCompare(p, "IAIABQ==", MqttProtocolVersion.V310); + } - [TestMethod] - public void SerializeV311_MqttConnAckPacket() + [TestMethod] + public void SerializeV310_MqttConnectPacket() + { + var p = new MqttConnectPacket { - var p = new MqttConnAckPacket - { - IsSessionPresent = true, - ReturnCode = MqttConnectReturnCode.ConnectionRefusedNotAuthorized - }; + ClientId = "XYZ", + Password = "PASS"u8.ToArray(), + Username = "USER", + KeepAlivePeriod = 123, + CleanSession = true + }; - SerializeAndCompare(p, "IAIBBQ=="); - } + SerializeAndCompare(p, "EB0ABk1RSXNkcAPCAHsAA1hZWgAEVVNFUgAEUEFTUw==", MqttProtocolVersion.V310); + } - [TestMethod] - public void SerializeV311_MqttConnectPacket() + [TestMethod] + public void SerializeV311_MqttConnAckPacket() + { + var p = new MqttConnAckPacket { - var p = new MqttConnectPacket - { - ClientId = "XYZ", - Password = Encoding.UTF8.GetBytes("PASS"), - Username = "USER", - KeepAlivePeriod = 123, - CleanSession = true - }; + IsSessionPresent = true, + ReturnCode = MqttConnectReturnCode.ConnectionRefusedNotAuthorized + }; - SerializeAndCompare(p, "EBsABE1RVFQEwgB7AANYWVoABFVTRVIABFBBU1M="); - } + SerializeAndCompare(p, "IAIBBQ=="); + } - [TestMethod] - public void SerializeV311_MqttConnectPacketWithWillMessage() + [TestMethod] + public void SerializeV311_MqttConnectPacket() + { + var p = new MqttConnectPacket { - var p = new MqttConnectPacket - { - ClientId = "XYZ", - Password = Encoding.UTF8.GetBytes("PASS"), - Username = "USER", - KeepAlivePeriod = 123, - CleanSession = true, - WillFlag = true, - WillTopic = "My/last/will", - WillMessage = Encoding.UTF8.GetBytes("Good byte."), - WillQoS = MqttQualityOfServiceLevel.AtLeastOnce, - WillRetain = true - }; + ClientId = "XYZ", + Password = "PASS"u8.ToArray(), + Username = "USER", + KeepAlivePeriod = 123, + CleanSession = true + }; - SerializeAndCompare(p, "EDUABE1RVFQE7gB7AANYWVoADE15L2xhc3Qvd2lsbAAKR29vZCBieXRlLgAEVVNFUgAEUEFTUw=="); - } + SerializeAndCompare(p, "EBsABE1RVFQEwgB7AANYWVoABFVTRVIABFBBU1M="); + } - [TestMethod] - public void SerializeV311_MqttDisconnectPacket() + [TestMethod] + public void SerializeV311_MqttConnectPacketWithWillMessage() + { + var p = new MqttConnectPacket { - SerializeAndCompare(new MqttDisconnectPacket(), "4AA="); - } + ClientId = "XYZ", + Password = "PASS"u8.ToArray(), + Username = "USER", + KeepAlivePeriod = 123, + CleanSession = true, + WillFlag = true, + WillTopic = "My/last/will", + WillMessage = "Good byte."u8.ToArray(), + WillQoS = MqttQualityOfServiceLevel.AtLeastOnce, + WillRetain = true + }; - [TestMethod] - public void SerializeV311_MqttPingReqPacket() + SerializeAndCompare(p, "EDUABE1RVFQE7gB7AANYWVoADE15L2xhc3Qvd2lsbAAKR29vZCBieXRlLgAEVVNFUgAEUEFTUw=="); + } + + [TestMethod] + public void SerializeV311_MqttDisconnectPacket() + { + SerializeAndCompare(new MqttDisconnectPacket(), "4AA="); + } + + [TestMethod] + public void SerializeV311_MqttPingReqPacket() + { + SerializeAndCompare(new MqttPingReqPacket(), "wAA="); + } + + [TestMethod] + public void SerializeV311_MqttPingRespPacket() + { + SerializeAndCompare(new MqttPingRespPacket(), "0AA="); + } + + [TestMethod] + public void SerializeV311_MqttPubAckPacket() + { + var p = new MqttPubAckPacket { - SerializeAndCompare(new MqttPingReqPacket(), "wAA="); - } + PacketIdentifier = 123 + }; - [TestMethod] - public void SerializeV311_MqttPingRespPacket() + SerializeAndCompare(p, "QAIAew=="); + } + + [TestMethod] + public void SerializeV311_MqttPubCompPacket() + { + var p = new MqttPubCompPacket { - SerializeAndCompare(new MqttPingRespPacket(), "0AA="); - } + PacketIdentifier = 123 + }; - [TestMethod] - public void SerializeV311_MqttPubAckPacket() + SerializeAndCompare(p, "cAIAew=="); + } + + [TestMethod] + public void SerializeV311_MqttPublishPacket() + { + var p = new MqttPublishPacket { - var p = new MqttPubAckPacket - { - PacketIdentifier = 123 - }; + PacketIdentifier = 123, + Dup = true, + Retain = true, + PayloadSegment = new ArraySegment("HELLO"u8.ToArray()), + QualityOfServiceLevel = MqttQualityOfServiceLevel.AtLeastOnce, + Topic = "A/B/C" + }; - SerializeAndCompare(p, "QAIAew=="); - } + SerializeAndCompare(p, "Ow4ABUEvQi9DAHtIRUxMTw=="); + } - [TestMethod] - public void SerializeV311_MqttPubCompPacket() + [TestMethod] + public void SerializeV311_MqttPubRecPacket() + { + var p = new MqttPubRecPacket { - var p = new MqttPubCompPacket - { - PacketIdentifier = 123 - }; + PacketIdentifier = 123 + }; - SerializeAndCompare(p, "cAIAew=="); - } + SerializeAndCompare(p, "UAIAew=="); + } - [TestMethod] - public void SerializeV311_MqttPublishPacket() + [TestMethod] + public void SerializeV311_MqttPubRelPacket() + { + var p = new MqttPubRelPacket { - var p = new MqttPublishPacket - { - PacketIdentifier = 123, - Dup = true, - Retain = true, - PayloadSegment = new ArraySegment(Encoding.ASCII.GetBytes("HELLO")), - QualityOfServiceLevel = MqttQualityOfServiceLevel.AtLeastOnce, - Topic = "A/B/C" - }; + PacketIdentifier = 123 + }; - SerializeAndCompare(p, "Ow4ABUEvQi9DAHtIRUxMTw=="); - } + SerializeAndCompare(p, "YgIAew=="); + } - [TestMethod] - public void SerializeV311_MqttPubRecPacket() + [TestMethod] + public void SerializeV311_MqttSubAckPacket() + { + var p = new MqttSubAckPacket { - var p = new MqttPubRecPacket - { - PacketIdentifier = 123 - }; + PacketIdentifier = 123, + ReasonCodes = + [ + MqttSubscribeReasonCode.GrantedQoS0, + MqttSubscribeReasonCode.GrantedQoS1, + MqttSubscribeReasonCode.GrantedQoS2, + MqttSubscribeReasonCode.UnspecifiedError + ] + }; - SerializeAndCompare(p, "UAIAew=="); - } + SerializeAndCompare(p, "kAYAewABAoA="); + } - [TestMethod] - public void SerializeV311_MqttPubRelPacket() + [TestMethod] + public void SerializeV311_MqttSubscribePacket() + { + var p = new MqttSubscribePacket { - var p = new MqttPubRelPacket - { - PacketIdentifier = 123 - }; + PacketIdentifier = 123 + }; - SerializeAndCompare(p, "YgIAew=="); - } + p.TopicFilters.Add(new MqttTopicFilter { Topic = "A/B/C", QualityOfServiceLevel = MqttQualityOfServiceLevel.ExactlyOnce }); + p.TopicFilters.Add(new MqttTopicFilter { Topic = "1/2/3", QualityOfServiceLevel = MqttQualityOfServiceLevel.AtLeastOnce }); + p.TopicFilters.Add(new MqttTopicFilter { Topic = "x/y/z", QualityOfServiceLevel = MqttQualityOfServiceLevel.AtMostOnce }); - [TestMethod] - public void SerializeV311_MqttSubAckPacket() + SerializeAndCompare(p, "ghoAewAFQS9CL0MCAAUxLzIvMwEABXgveS96AA=="); + } + + [TestMethod] + public void SerializeV311_MqttUnsubAckPacket() + { + var p = new MqttUnsubAckPacket { - var p = new MqttSubAckPacket - { - PacketIdentifier = 123, - ReasonCodes = new List - { - MqttSubscribeReasonCode.GrantedQoS0, - MqttSubscribeReasonCode.GrantedQoS1, - MqttSubscribeReasonCode.GrantedQoS2, - MqttSubscribeReasonCode.UnspecifiedError - } - }; + PacketIdentifier = 123 + }; - SerializeAndCompare(p, "kAYAewABAoA="); - } + SerializeAndCompare(p, "sAIAew=="); + } - [TestMethod] - public void SerializeV311_MqttSubscribePacket() + [TestMethod] + public void SerializeV311_MqttUnsubscribePacket() + { + var p = new MqttUnsubscribePacket { - var p = new MqttSubscribePacket - { - PacketIdentifier = 123 - }; + PacketIdentifier = 123 + }; - p.TopicFilters.Add(new MqttTopicFilter { Topic = "A/B/C", QualityOfServiceLevel = MqttQualityOfServiceLevel.ExactlyOnce }); - p.TopicFilters.Add(new MqttTopicFilter { Topic = "1/2/3", QualityOfServiceLevel = MqttQualityOfServiceLevel.AtLeastOnce }); - p.TopicFilters.Add(new MqttTopicFilter { Topic = "x/y/z", QualityOfServiceLevel = MqttQualityOfServiceLevel.AtMostOnce }); + p.TopicFilters.Add("A/B/C"); + p.TopicFilters.Add("1/2/3"); + p.TopicFilters.Add("x/y/z"); - SerializeAndCompare(p, "ghoAewAFQS9CL0MCAAUxLzIvMwEABXgveS96AA=="); - } - - [TestMethod] - public void SerializeV311_MqttUnsubAckPacket() - { - var p = new MqttUnsubAckPacket - { - PacketIdentifier = 123 - }; - - SerializeAndCompare(p, "sAIAew=="); - } - - [TestMethod] - public void SerializeV311_MqttUnsubscribePacket() - { - var p = new MqttUnsubscribePacket - { - PacketIdentifier = 123 - }; - - p.TopicFilters.Add("A/B/C"); - p.TopicFilters.Add("1/2/3"); - p.TopicFilters.Add("x/y/z"); - - SerializeAndCompare(p, "ohcAewAFQS9CL0MABTEvMi8zAAV4L3kveg=="); - } + SerializeAndCompare(p, "ohcAewAFQS9CL0MABTEvMi8zAAV4L3kveg=="); + } - void DeserializeAndCompare(MqttPacket packet, string expectedBase64Value, MqttProtocolVersion protocolVersion = MqttProtocolVersion.V311) - { - var writer = WriterFactory(); + void DeserializeAndCompare(MqttPacket packet, string expectedBase64Value, MqttProtocolVersion protocolVersion = MqttProtocolVersion.V311) + { + var writer = WriterFactory(); - var serializer = MqttPacketFormatterAdapter.GetMqttPacketFormatter(protocolVersion, writer); - var buffer1 = serializer.Encode(packet); + var serializer = MqttPacketFormatterAdapter.GetMqttPacketFormatter(protocolVersion, writer); + var buffer1 = serializer.Encode(packet); - using (var headerStream = new MemoryStream(buffer1.Join().ToArray())) - { - using (var channel = new MemoryMqttChannel(headerStream)) - { - using (var adapter = new MqttChannelAdapter( - channel, - new MqttPacketFormatterAdapter(protocolVersion, new MqttBufferWriter(4096, 65535)), - new MqttNetEventLogger())) - { - var receivedPacket = adapter.ReceivePacketAsync(CancellationToken.None).GetAwaiter().GetResult(); + using var headerStream = new MemoryStream(buffer1.Join().ToArray()); + using var channel = new MemoryMqttChannel(headerStream); + using var adapter = new MqttChannelAdapter( + channel, + new MqttPacketFormatterAdapter(protocolVersion, new MqttBufferWriter(4096, 65535)), + new MqttNetEventLogger()); + var receivedPacket = adapter.ReceivePacketAsync(CancellationToken.None).GetAwaiter().GetResult(); - var buffer2 = serializer.Encode(receivedPacket); + var buffer2 = serializer.Encode(receivedPacket); - Assert.AreEqual(expectedBase64Value, Convert.ToBase64String(buffer2.Join().ToArray())); - } - } - } - } + Assert.AreEqual(expectedBase64Value, Convert.ToBase64String(buffer2.Join().ToArray())); + } - MqttProtocolVersion DeserializeAndDetectVersion(MqttPacketFormatterAdapter packetFormatterAdapter, byte[] buffer) - { - var channel = new MemoryMqttChannel(buffer); - var adapter = new MqttChannelAdapter(channel, packetFormatterAdapter, new MqttNetEventLogger()); + static MqttProtocolVersion DeserializeAndDetectVersion(MqttPacketFormatterAdapter packetFormatterAdapter, byte[] buffer) + { + var channel = new MemoryMqttChannel(buffer); + var adapter = new MqttChannelAdapter(channel, packetFormatterAdapter, new MqttNetEventLogger()); - adapter.ReceivePacketAsync(CancellationToken.None).GetAwaiter().GetResult(); - return packetFormatterAdapter.ProtocolVersion; - } + adapter.ReceivePacketAsync(CancellationToken.None).GetAwaiter().GetResult(); + return packetFormatterAdapter.ProtocolVersion; + } - TPacket Roundtrip(TPacket packet, MqttProtocolVersion protocolVersion = MqttProtocolVersion.V311, MqttBufferReader bufferReader = null, MqttBufferWriter bufferWriter = null) where TPacket : MqttPacket - { - var writer = bufferWriter ?? WriterFactory(); - var serializer = MqttPacketFormatterAdapter.GetMqttPacketFormatter(protocolVersion, writer); - var buffer = serializer.Encode(packet); + static TPacket Roundtrip(TPacket packet, MqttProtocolVersion protocolVersion = MqttProtocolVersion.V311, MqttBufferWriter bufferWriter = null) where TPacket : MqttPacket + { + var writer = bufferWriter ?? WriterFactory(); + var serializer = MqttPacketFormatterAdapter.GetMqttPacketFormatter(protocolVersion, writer); + var buffer = serializer.Encode(packet); - using (var channel = new MemoryMqttChannel(buffer.Join().ToArray())) - { - var adapter = new MqttChannelAdapter(channel, new MqttPacketFormatterAdapter(protocolVersion, new MqttBufferWriter(4096, 65535)), new MqttNetEventLogger()); - return (TPacket)adapter.ReceivePacketAsync(CancellationToken.None).GetAwaiter().GetResult(); - } - } + using var channel = new MemoryMqttChannel(buffer.Join().ToArray()); + var adapter = new MqttChannelAdapter(channel, new MqttPacketFormatterAdapter(protocolVersion, new MqttBufferWriter(4096, 65535)), new MqttNetEventLogger()); + return (TPacket)adapter.ReceivePacketAsync(CancellationToken.None).GetAwaiter().GetResult(); + } - byte[] Serialize(MqttPacket packet, MqttProtocolVersion protocolVersion) - { - return MqttPacketFormatterAdapter.GetMqttPacketFormatter(protocolVersion, WriterFactory()).Encode(packet).Join().ToArray(); - } + static byte[] Serialize(MqttPacket packet, MqttProtocolVersion protocolVersion) + { + return MqttPacketFormatterAdapter.GetMqttPacketFormatter(protocolVersion, WriterFactory()).Encode(packet).Join().ToArray(); + } - void SerializeAndCompare(MqttPacket packet, string expectedBase64Value, MqttProtocolVersion protocolVersion = MqttProtocolVersion.V311) - { - Assert.AreEqual(expectedBase64Value, Convert.ToBase64String(Serialize(packet, protocolVersion))); - } + static void SerializeAndCompare(MqttPacket packet, string expectedBase64Value, MqttProtocolVersion protocolVersion = MqttProtocolVersion.V311) + { + Assert.AreEqual(expectedBase64Value, Convert.ToBase64String(Serialize(packet, protocolVersion))); + } - MqttBufferWriter WriterFactory() - { - return new MqttBufferWriter(4096, 65535); - } + static MqttBufferWriter WriterFactory() + { + return new MqttBufferWriter(4096, 65535); } } \ No newline at end of file diff --git a/Source/MQTTnet.Tests/Formatter/MqttPacketSerialization_V3_Tests.cs b/Source/MQTTnet.Tests/Formatter/MqttPacketSerialization_V3_Tests.cs index 92ee5aa6..914445e9 100644 --- a/Source/MQTTnet.Tests/Formatter/MqttPacketSerialization_V3_Tests.cs +++ b/Source/MQTTnet.Tests/Formatter/MqttPacketSerialization_V3_Tests.cs @@ -4,494 +4,437 @@ using System; using System.Buffers; -using System.Collections.Generic; -using System.Linq; -using System.Text; using Microsoft.VisualStudio.TestTools.UnitTesting; using MQTTnet.Exceptions; using MQTTnet.Formatter; using MQTTnet.Packets; using MQTTnet.Protocol; -namespace MQTTnet.Tests.Formatter +namespace MQTTnet.Tests.Formatter; + +// ReSharper disable InconsistentNaming +[TestClass] +public sealed class MqttPacketSerialization_V3_Tests { - [TestClass] - public sealed class MqttPacketSerialization_V3_Tests + [TestMethod] + public void Serialize_Full_MqttAuthPacket_V311() { - [TestMethod] - public void Serialize_Full_MqttAuthPacket_V311() - { - var authPacket = new MqttAuthPacket(); - Assert.ThrowsException(() => MqttPacketSerializationHelper.EncodeAndDecodePacket(authPacket, MqttProtocolVersion.V311)); - } + var authPacket = new MqttAuthPacket(); + Assert.ThrowsException(() => MqttPacketSerializationHelper.EncodeAndDecodePacket(authPacket, MqttProtocolVersion.V311)); + } - [TestMethod] - public void Serialize_Full_MqttConnAckPacket_V311() + [TestMethod] + public void Serialize_Full_MqttConnAckPacket_V311() + { + var connAckPacket = new MqttConnAckPacket { - var connAckPacket = new MqttConnAckPacket - { - AuthenticationData = Encoding.UTF8.GetBytes("AuthenticationData"), - AuthenticationMethod = "AuthenticationMethod", - ReasonCode = MqttConnectReasonCode.ServerUnavailable, - ReasonString = "ReasonString", - ReceiveMaximum = 123, - ResponseInformation = "ResponseInformation", - RetainAvailable = true, - ReturnCode = MqttConnectReturnCode.ConnectionRefusedNotAuthorized, - ServerReference = "ServerReference", - AssignedClientIdentifier = "AssignedClientIdentifier", - IsSessionPresent = true, - MaximumPacketSize = 456, - MaximumQoS = MqttQualityOfServiceLevel.ExactlyOnce, - ServerKeepAlive = 789, - SessionExpiryInterval = 852, - SharedSubscriptionAvailable = true, - SubscriptionIdentifiersAvailable = true, - TopicAliasMaximum = 963, - WildcardSubscriptionAvailable = true, - UserProperties = new List + AuthenticationData = "AuthenticationData"u8.ToArray(), + AuthenticationMethod = "AuthenticationMethod", + ReasonCode = MqttConnectReasonCode.ServerUnavailable, + ReasonString = "ReasonString", + ReceiveMaximum = 123, + ResponseInformation = "ResponseInformation", + RetainAvailable = true, + ReturnCode = MqttConnectReturnCode.ConnectionRefusedNotAuthorized, + ServerReference = "ServerReference", + AssignedClientIdentifier = "AssignedClientIdentifier", + IsSessionPresent = true, + MaximumPacketSize = 456, + MaximumQoS = MqttQualityOfServiceLevel.ExactlyOnce, + ServerKeepAlive = 789, + SessionExpiryInterval = 852, + SharedSubscriptionAvailable = true, + SubscriptionIdentifiersAvailable = true, + TopicAliasMaximum = 963, + WildcardSubscriptionAvailable = true, + UserProperties = [new("Foo", "Bar")] + }; + + var deserialized = MqttPacketSerializationHelper.EncodeAndDecodePacket(connAckPacket, MqttProtocolVersion.V311); + + CollectionAssert.AreEqual(null, deserialized.AuthenticationData); // Not supported in v3.1.1 + Assert.AreEqual(null, deserialized.AuthenticationMethod); // Not supported in v3.1.1 + //Assert.AreEqual(connAckPacket.ReasonCode, deserialized.ReasonCode); + Assert.AreEqual(null, deserialized.ReasonString); // Not supported in v3.1.1 + Assert.AreEqual(0U, deserialized.ReceiveMaximum); // Not supported in v3.1.1 + Assert.AreEqual(null, deserialized.ResponseInformation); // Not supported in v3.1.1 + Assert.AreEqual(false, deserialized.RetainAvailable); // Not supported in v3.1.1 + Assert.AreEqual(MqttConnectReturnCode.ConnectionRefusedNotAuthorized, deserialized.ReturnCode); + Assert.AreEqual(null, deserialized.ServerReference); // Not supported in v3.1.1 + Assert.AreEqual(null, deserialized.AssignedClientIdentifier); // Not supported in v3.1.1 + Assert.AreEqual(connAckPacket.IsSessionPresent, deserialized.IsSessionPresent); + Assert.AreEqual(0U, deserialized.MaximumPacketSize); // Not supported in v3.1.1 + Assert.AreEqual(MqttQualityOfServiceLevel.AtMostOnce, deserialized.MaximumQoS); // Not supported in v3.1.1 + Assert.AreEqual(0U, deserialized.ServerKeepAlive); // Not supported in v3.1.1 + Assert.AreEqual(0U, deserialized.SessionExpiryInterval); // Not supported in v3.1.1 + Assert.AreEqual(false, deserialized.SharedSubscriptionAvailable); // Not supported in v3.1.1 + Assert.AreEqual(false, deserialized.SubscriptionIdentifiersAvailable); // Not supported in v3.1.1 + Assert.AreEqual(0U, deserialized.TopicAliasMaximum); // Not supported in v3.1.1 + Assert.AreEqual(false, deserialized.WildcardSubscriptionAvailable); + Assert.IsNull(deserialized.UserProperties); // Not supported in v3.1.1 + } + + [TestMethod] + public void Serialize_Full_MqttConnAckPacket_V310() + { + var connAckPacket = new MqttConnAckPacket + { + AuthenticationData = "AuthenticationData"u8.ToArray(), + AuthenticationMethod = "AuthenticationMethod", + ReasonCode = MqttConnectReasonCode.ServerUnavailable, + ReasonString = "ReasonString", + ReceiveMaximum = 123, + ResponseInformation = "ResponseInformation", + RetainAvailable = true, + ReturnCode = MqttConnectReturnCode.ConnectionRefusedNotAuthorized, + ServerReference = "ServerReference", + AssignedClientIdentifier = "AssignedClientIdentifier", + IsSessionPresent = true, + MaximumPacketSize = 456, + MaximumQoS = MqttQualityOfServiceLevel.ExactlyOnce, + ServerKeepAlive = 789, + SessionExpiryInterval = 852, + SharedSubscriptionAvailable = true, + SubscriptionIdentifiersAvailable = true, + TopicAliasMaximum = 963, + WildcardSubscriptionAvailable = true, + UserProperties = [new MqttUserProperty("Foo", "Bar")] + }; + + var deserialized = MqttPacketSerializationHelper.EncodeAndDecodePacket(connAckPacket, MqttProtocolVersion.V310); + + CollectionAssert.AreEqual(null, deserialized.AuthenticationData); // Not supported in v3.1.1 + Assert.AreEqual(null, deserialized.AuthenticationMethod); // Not supported in v3.1.1 + //Assert.AreEqual(connAckPacket.ReasonCode, deserialized.ReasonCode); + Assert.AreEqual(null, deserialized.ReasonString); // Not supported in v3.1.1 + Assert.AreEqual(0U, deserialized.ReceiveMaximum); // Not supported in v3.1.1 + Assert.AreEqual(null, deserialized.ResponseInformation); // Not supported in v3.1.1 + Assert.AreEqual(false, deserialized.RetainAvailable); // Not supported in v3.1.1 + Assert.AreEqual(MqttConnectReturnCode.ConnectionRefusedNotAuthorized, deserialized.ReturnCode); + Assert.AreEqual(null, deserialized.ServerReference); // Not supported in v3.1.1 + Assert.AreEqual(null, deserialized.AssignedClientIdentifier); // Not supported in v3.1.1 + Assert.AreEqual(false, deserialized.IsSessionPresent); // Not supported in v3.1.0 <- ! + Assert.AreEqual(0U, deserialized.MaximumPacketSize); // Not supported in v3.1.1 + Assert.AreEqual(MqttQualityOfServiceLevel.AtMostOnce, deserialized.MaximumQoS); // Not supported in v3.1.1 + Assert.AreEqual(0U, deserialized.ServerKeepAlive); // Not supported in v3.1.1 + Assert.AreEqual(0U, deserialized.SessionExpiryInterval); // Not supported in v3.1.1 + Assert.AreEqual(false, deserialized.SharedSubscriptionAvailable); // Not supported in v3.1.1 + Assert.AreEqual(false, deserialized.SubscriptionIdentifiersAvailable); // Not supported in v3.1.1 + Assert.AreEqual(0U, deserialized.TopicAliasMaximum); // Not supported in v3.1.1 + Assert.AreEqual(false, deserialized.WildcardSubscriptionAvailable); + Assert.IsNull(deserialized.UserProperties); // Not supported in v3.1.1 + } + + [TestMethod] + public void Serialize_Full_MqttConnectPacket_V311() + { + var connectPacket = new MqttConnectPacket + { + Username = "Username", + Password = "Password"u8.ToArray(), + ClientId = "ClientId", + AuthenticationData = "AuthenticationData"u8.ToArray(), + AuthenticationMethod = "AuthenticationMethod", + CleanSession = true, + ReceiveMaximum = 123, + WillFlag = true, + WillTopic = "WillTopic", + WillMessage = "WillMessage"u8.ToArray(), + WillRetain = true, + KeepAlivePeriod = 456, + MaximumPacketSize = 789, + RequestProblemInformation = true, + RequestResponseInformation = true, + SessionExpiryInterval = 27, + TopicAliasMaximum = 67, + WillContentType = "WillContentType", + WillCorrelationData = "WillCorrelationData"u8.ToArray(), + WillDelayInterval = 782, + WillQoS = MqttQualityOfServiceLevel.ExactlyOnce, + WillResponseTopic = "WillResponseTopic", + WillMessageExpiryInterval = 542, + WillPayloadFormatIndicator = MqttPayloadFormatIndicator.CharacterData, + UserProperties = [new("Foo", "Bar")], + WillUserProperties = [new("WillFoo", "WillBar")] + }; + + var deserialized = MqttPacketSerializationHelper.EncodeAndDecodePacket(connectPacket, MqttProtocolVersion.V311); + + Assert.AreEqual(connectPacket.Username, deserialized.Username); + CollectionAssert.AreEqual(connectPacket.Password, deserialized.Password); + Assert.AreEqual(connectPacket.ClientId, deserialized.ClientId); + CollectionAssert.AreEqual(null, deserialized.AuthenticationData); // Not supported in v3.1.1 + Assert.AreEqual(null, deserialized.AuthenticationMethod); // Not supported in v3.1.1 + Assert.AreEqual(connectPacket.CleanSession, deserialized.CleanSession); + Assert.AreEqual(0L, deserialized.ReceiveMaximum); // Not supported in v3.1.1 + Assert.AreEqual(connectPacket.WillFlag, deserialized.WillFlag); + Assert.AreEqual(connectPacket.WillTopic, deserialized.WillTopic); + CollectionAssert.AreEqual(connectPacket.WillMessage, deserialized.WillMessage); + Assert.AreEqual(connectPacket.WillRetain, deserialized.WillRetain); + Assert.AreEqual(connectPacket.KeepAlivePeriod, deserialized.KeepAlivePeriod); + // MaximumPacketSize not available in MQTTv3. + // RequestProblemInformation not available in MQTTv3. + // RequestResponseInformation not available in MQTTv3. + // SessionExpiryInterval not available in MQTTv3. + // TopicAliasMaximum not available in MQTTv3. + // WillContentType not available in MQTTv3. + // WillCorrelationData not available in MQTTv3. + // WillDelayInterval not available in MQTTv3. + Assert.AreEqual(connectPacket.WillQoS, deserialized.WillQoS); + // WillResponseTopic not available in MQTTv3. + // WillMessageExpiryInterval not available in MQTTv3. + // WillPayloadFormatIndicator not available in MQTTv3. + Assert.IsNull(deserialized.UserProperties); // Not supported in v3.1.1 + Assert.IsNull(deserialized.WillUserProperties); // Not supported in v3.1.1 + } + + [TestMethod] + public void Serialize_Full_MqttDisconnectPacket_V311() + { + var disconnectPacket = new MqttDisconnectPacket + { + ReasonCode = MqttDisconnectReasonCode.NormalDisconnection, // MQTTv3 has no other values than this. + ReasonString = "ReasonString", + ServerReference = "ServerReference", + SessionExpiryInterval = 234, + UserProperties = [new("Foo", "Bar")] + }; + + var deserialized = MqttPacketSerializationHelper.EncodeAndDecodePacket(disconnectPacket, MqttProtocolVersion.V311); + + Assert.AreEqual(disconnectPacket.ReasonCode, deserialized.ReasonCode); + Assert.AreEqual(null, deserialized.ReasonString); // Not supported in v3.1.1 + Assert.AreEqual(null, deserialized.ServerReference); // Not supported in v3.1.1 + Assert.AreEqual(0U, deserialized.SessionExpiryInterval); // Not supported in v3.1.1 + CollectionAssert.AreEqual(null, deserialized.UserProperties); + } + + [TestMethod] + public void Serialize_Full_MqttPingReqPacket_V311() + { + var pingReqPacket = new MqttPingReqPacket(); + + var deserialized = MqttPacketSerializationHelper.EncodeAndDecodePacket(pingReqPacket, MqttProtocolVersion.V311); + + Assert.IsNotNull(deserialized); + } + + [TestMethod] + public void Serialize_Full_MqttPingRespPacket_V311() + { + var pingRespPacket = new MqttPingRespPacket(); + + var deserialized = MqttPacketSerializationHelper.EncodeAndDecodePacket(pingRespPacket, MqttProtocolVersion.V311); + + Assert.IsNotNull(deserialized); + } + + [TestMethod] + public void Serialize_Full_MqttPubAckPacket_V311() + { + var pubAckPacket = new MqttPubAckPacket + { + PacketIdentifier = 123, + ReasonCode = MqttPubAckReasonCode.NoMatchingSubscribers, + ReasonString = "ReasonString", + UserProperties = [new("Foo", "Bar")] + }; + + var deserialized = MqttPacketSerializationHelper.EncodeAndDecodePacket(pubAckPacket, MqttProtocolVersion.V311); + + Assert.AreEqual(pubAckPacket.PacketIdentifier, deserialized.PacketIdentifier); + Assert.AreEqual(MqttPubAckReasonCode.Success, deserialized.ReasonCode); // Not supported in v3.1.1 + Assert.AreEqual(null, deserialized.ReasonString); // Not supported in v3.1.1 + CollectionAssert.AreEqual(null, deserialized.UserProperties); + } + + [TestMethod] + public void Serialize_Full_MqttPubCompPacket_V311() + { + var pubCompPacket = new MqttPubCompPacket + { + PacketIdentifier = 123, + ReasonCode = MqttPubCompReasonCode.PacketIdentifierNotFound, + ReasonString = "ReasonString", + UserProperties = [new("Foo", "Bar")] + }; + + var deserialized = MqttPacketSerializationHelper.EncodeAndDecodePacket(pubCompPacket, MqttProtocolVersion.V311); + + Assert.AreEqual(pubCompPacket.PacketIdentifier, deserialized.PacketIdentifier); + // ReasonCode not available in MQTTv3. + // ReasonString not available in MQTTv3. + // UserProperties not available in MQTTv3. + Assert.IsNull(deserialized.UserProperties); + } + + [TestMethod] + public void Serialize_Full_MqttPublishPacket_V311() + { + var publishPacket = new MqttPublishPacket + { + PacketIdentifier = 123, + Dup = true, + Retain = true, + PayloadSegment = new ArraySegment("Payload"u8.ToArray()), + QualityOfServiceLevel = MqttQualityOfServiceLevel.AtLeastOnce, + Topic = "Topic", + ResponseTopic = "/Response", + ContentType = "Content-Type", + CorrelationData = "CorrelationData"u8.ToArray(), + TopicAlias = 27, + SubscriptionIdentifiers = [123], + MessageExpiryInterval = 38, + PayloadFormatIndicator = MqttPayloadFormatIndicator.CharacterData, + UserProperties = [new("Foo", "Bar")] + }; + + var deserialized = MqttPacketSerializationHelper.EncodeAndDecodePacket(publishPacket, MqttProtocolVersion.V311); + + Assert.AreEqual(publishPacket.PacketIdentifier, deserialized.PacketIdentifier); + Assert.AreEqual(publishPacket.Dup, deserialized.Dup); + Assert.AreEqual(publishPacket.Retain, deserialized.Retain); + CollectionAssert.AreEqual(publishPacket.Payload.ToArray(), deserialized.Payload.ToArray()); + Assert.AreEqual(publishPacket.QualityOfServiceLevel, deserialized.QualityOfServiceLevel); + Assert.AreEqual(publishPacket.Topic, deserialized.Topic); + Assert.AreEqual(null, deserialized.ResponseTopic); // Not supported in v3.1.1. + Assert.AreEqual(null, deserialized.ContentType); // Not supported in v3.1.1. + CollectionAssert.AreEqual(null, deserialized.CorrelationData); // Not supported in v3.1.1. + Assert.AreEqual(0U, deserialized.TopicAlias); // Not supported in v3.1.1. + CollectionAssert.AreEqual(null, deserialized.SubscriptionIdentifiers); // Not supported in v3.1.1 + Assert.AreEqual(0U, deserialized.MessageExpiryInterval); // Not supported in v3.1.1 + Assert.AreEqual(MqttPayloadFormatIndicator.Unspecified, deserialized.PayloadFormatIndicator); // Not supported in v3.1.1 + Assert.IsNull(deserialized.UserProperties); // Not supported in v3.1.1 + } + + [TestMethod] + public void Serialize_Full_MqttPubRecPacket_V311() + { + var pubRecPacket = new MqttPubRecPacket + { + PacketIdentifier = 123, + ReasonCode = MqttPubRecReasonCode.UnspecifiedError, + ReasonString = "ReasonString", + UserProperties = [new("Foo", "Bar")] + }; + + var deserialized = MqttPacketSerializationHelper.EncodeAndDecodePacket(pubRecPacket, MqttProtocolVersion.V311); + + Assert.AreEqual(pubRecPacket.PacketIdentifier, deserialized.PacketIdentifier); + // ReasonCode not available in MQTTv3. + // ReasonString not available in MQTTv3. + // UserProperties not available in MQTTv3. + Assert.IsNull(deserialized.UserProperties); + } + + [TestMethod] + public void Serialize_Full_MqttPubRelPacket_V311() + { + var pubRelPacket = new MqttPubRelPacket + { + PacketIdentifier = 123, + ReasonCode = MqttPubRelReasonCode.PacketIdentifierNotFound, + ReasonString = "ReasonString", + UserProperties = [new("Foo", "Bar")] + }; + + var deserialized = MqttPacketSerializationHelper.EncodeAndDecodePacket(pubRelPacket, MqttProtocolVersion.V311); + + Assert.AreEqual(pubRelPacket.PacketIdentifier, deserialized.PacketIdentifier); + // ReasonCode not available in MQTTv3. + // ReasonString not available in MQTTv3. + // UserProperties not available in MQTTv3. + Assert.IsNull(deserialized.UserProperties); + } + + [TestMethod] + public void Serialize_Full_MqttSubAckPacket_V311() + { + var subAckPacket = new MqttSubAckPacket + { + PacketIdentifier = 123, + ReasonString = "ReasonString", + ReasonCodes = [MqttSubscribeReasonCode.GrantedQoS1], + UserProperties = [new("Foo", "Bar")] + }; + + var deserialized = MqttPacketSerializationHelper.EncodeAndDecodePacket(subAckPacket, MqttProtocolVersion.V311); + + Assert.AreEqual(subAckPacket.PacketIdentifier, deserialized.PacketIdentifier); + Assert.AreEqual(null, deserialized.ReasonString); // Not supported in v3.1.1 + Assert.AreEqual(subAckPacket.ReasonCodes.Count, deserialized.ReasonCodes.Count); + Assert.AreEqual(subAckPacket.ReasonCodes[0], deserialized.ReasonCodes[0]); + CollectionAssert.AreEqual(null, deserialized.UserProperties); // Not supported in v3.1.1 + } + + [TestMethod] + public void Serialize_Full_MqttSubscribePacket_V311() + { + var subscribePacket = new MqttSubscribePacket + { + PacketIdentifier = 123, + SubscriptionIdentifier = 456, + TopicFilters = + [ + new() { - new MqttUserProperty("Foo", "Bar") + Topic = "Topic", + NoLocal = true, + RetainHandling = MqttRetainHandling.SendAtSubscribeIfNewSubscriptionOnly, + RetainAsPublished = true, + QualityOfServiceLevel = MqttQualityOfServiceLevel.AtMostOnce } - }; + ], + UserProperties = [new("Foo", "Bar")] + }; - var deserialized = MqttPacketSerializationHelper.EncodeAndDecodePacket(connAckPacket, MqttProtocolVersion.V311); + var deserialized = MqttPacketSerializationHelper.EncodeAndDecodePacket(subscribePacket, MqttProtocolVersion.V311); - CollectionAssert.AreEqual(null, deserialized.AuthenticationData); // Not supported in v3.1.1 - Assert.AreEqual(null, deserialized.AuthenticationMethod); // Not supported in v3.1.1 - //Assert.AreEqual(connAckPacket.ReasonCode, deserialized.ReasonCode); - Assert.AreEqual(null, deserialized.ReasonString); // Not supported in v3.1.1 - Assert.AreEqual(0U, deserialized.ReceiveMaximum); // Not supported in v3.1.1 - Assert.AreEqual(null, deserialized.ResponseInformation); // Not supported in v3.1.1 - Assert.AreEqual(false, deserialized.RetainAvailable); // Not supported in v3.1.1 - Assert.AreEqual(MqttConnectReturnCode.ConnectionRefusedNotAuthorized, deserialized.ReturnCode); - Assert.AreEqual(null, deserialized.ServerReference); // Not supported in v3.1.1 - Assert.AreEqual(null, deserialized.AssignedClientIdentifier); // Not supported in v3.1.1 - Assert.AreEqual(connAckPacket.IsSessionPresent, deserialized.IsSessionPresent); - Assert.AreEqual(0U, deserialized.MaximumPacketSize); // Not supported in v3.1.1 - Assert.AreEqual(MqttQualityOfServiceLevel.AtMostOnce, deserialized.MaximumQoS); // Not supported in v3.1.1 - Assert.AreEqual(0U, deserialized.ServerKeepAlive); // Not supported in v3.1.1 - Assert.AreEqual(0U, deserialized.SessionExpiryInterval); // Not supported in v3.1.1 - Assert.AreEqual(false, deserialized.SharedSubscriptionAvailable); // Not supported in v3.1.1 - Assert.AreEqual(false, deserialized.SubscriptionIdentifiersAvailable); // Not supported in v3.1.1 - Assert.AreEqual(0U, deserialized.TopicAliasMaximum); // Not supported in v3.1.1 - Assert.AreEqual(false, deserialized.WildcardSubscriptionAvailable); - Assert.IsNull(deserialized.UserProperties); // Not supported in v3.1.1 - } - - [TestMethod] - public void Serialize_Full_MqttConnAckPacket_V310() + Assert.AreEqual(subscribePacket.PacketIdentifier, deserialized.PacketIdentifier); + Assert.AreEqual(0U, deserialized.SubscriptionIdentifier); // Not supported in v3.1.1 + Assert.AreEqual(1, deserialized.TopicFilters.Count); + Assert.AreEqual(subscribePacket.TopicFilters[0].Topic, deserialized.TopicFilters[0].Topic); + Assert.AreEqual(false, deserialized.TopicFilters[0].NoLocal); // Not supported in v3.1.1 + Assert.AreEqual(MqttRetainHandling.SendAtSubscribe, deserialized.TopicFilters[0].RetainHandling); // Not supported in v3.1.1 + Assert.AreEqual(false, deserialized.TopicFilters[0].RetainAsPublished); // Not supported in v3.1.1 + Assert.AreEqual(subscribePacket.TopicFilters[0].QualityOfServiceLevel, deserialized.TopicFilters[0].QualityOfServiceLevel); + CollectionAssert.AreEqual(null, deserialized.UserProperties); // Not supported in v3.1.1 + } + + [TestMethod] + public void Serialize_Full_MqttUnsubAckPacket_V311() + { + var unsubAckPacket = new MqttUnsubAckPacket { - var connAckPacket = new MqttConnAckPacket - { - AuthenticationData = Encoding.UTF8.GetBytes("AuthenticationData"), - AuthenticationMethod = "AuthenticationMethod", - ReasonCode = MqttConnectReasonCode.ServerUnavailable, - ReasonString = "ReasonString", - ReceiveMaximum = 123, - ResponseInformation = "ResponseInformation", - RetainAvailable = true, - ReturnCode = MqttConnectReturnCode.ConnectionRefusedNotAuthorized, - ServerReference = "ServerReference", - AssignedClientIdentifier = "AssignedClientIdentifier", - IsSessionPresent = true, - MaximumPacketSize = 456, - MaximumQoS = MqttQualityOfServiceLevel.ExactlyOnce, - ServerKeepAlive = 789, - SessionExpiryInterval = 852, - SharedSubscriptionAvailable = true, - SubscriptionIdentifiersAvailable = true, - TopicAliasMaximum = 963, - WildcardSubscriptionAvailable = true, - UserProperties = new List - { - new MqttUserProperty("Foo", "Bar") - } - }; + PacketIdentifier = 123, + ReasonCodes = [MqttUnsubscribeReasonCode.ImplementationSpecificError], + ReasonString = "ReasonString", + UserProperties = [new("Foo", "Bar")] + }; - var deserialized = MqttPacketSerializationHelper.EncodeAndDecodePacket(connAckPacket, MqttProtocolVersion.V310); + var deserialized = MqttPacketSerializationHelper.EncodeAndDecodePacket(unsubAckPacket, MqttProtocolVersion.V311); - CollectionAssert.AreEqual(null, deserialized.AuthenticationData); // Not supported in v3.1.1 - Assert.AreEqual(null, deserialized.AuthenticationMethod); // Not supported in v3.1.1 - //Assert.AreEqual(connAckPacket.ReasonCode, deserialized.ReasonCode); - Assert.AreEqual(null, deserialized.ReasonString); // Not supported in v3.1.1 - Assert.AreEqual(0U, deserialized.ReceiveMaximum); // Not supported in v3.1.1 - Assert.AreEqual(null, deserialized.ResponseInformation); // Not supported in v3.1.1 - Assert.AreEqual(false, deserialized.RetainAvailable); // Not supported in v3.1.1 - Assert.AreEqual(MqttConnectReturnCode.ConnectionRefusedNotAuthorized, deserialized.ReturnCode); - Assert.AreEqual(null, deserialized.ServerReference); // Not supported in v3.1.1 - Assert.AreEqual(null, deserialized.AssignedClientIdentifier); // Not supported in v3.1.1 - Assert.AreEqual(false, deserialized.IsSessionPresent); // Not supported in v3.1.0 <- ! - Assert.AreEqual(0U, deserialized.MaximumPacketSize); // Not supported in v3.1.1 - Assert.AreEqual(MqttQualityOfServiceLevel.AtMostOnce, deserialized.MaximumQoS); // Not supported in v3.1.1 - Assert.AreEqual(0U, deserialized.ServerKeepAlive); // Not supported in v3.1.1 - Assert.AreEqual(0U, deserialized.SessionExpiryInterval); // Not supported in v3.1.1 - Assert.AreEqual(false, deserialized.SharedSubscriptionAvailable); // Not supported in v3.1.1 - Assert.AreEqual(false, deserialized.SubscriptionIdentifiersAvailable); // Not supported in v3.1.1 - Assert.AreEqual(0U, deserialized.TopicAliasMaximum); // Not supported in v3.1.1 - Assert.AreEqual(false, deserialized.WildcardSubscriptionAvailable); - Assert.IsNull(deserialized.UserProperties); // Not supported in v3.1.1 - } + Assert.AreEqual(unsubAckPacket.PacketIdentifier, deserialized.PacketIdentifier); + Assert.AreEqual(null, deserialized.ReasonString); // Not supported in v3.1.1 + CollectionAssert.AreEqual(null, deserialized.ReasonCodes); // Not supported in v3.1.1 + CollectionAssert.AreEqual(null, deserialized.UserProperties); // Not supported in v3.1.1 + } - [TestMethod] - public void Serialize_Full_MqttConnectPacket_V311() + [TestMethod] + public void Serialize_Full_MqttUnsubscribePacket_V311() + { + var unsubscribePacket = new MqttUnsubscribePacket { - var connectPacket = new MqttConnectPacket - { - Username = "Username", - Password = Encoding.UTF8.GetBytes("Password"), - ClientId = "ClientId", - AuthenticationData = Encoding.UTF8.GetBytes("AuthenticationData"), - AuthenticationMethod = "AuthenticationMethod", - CleanSession = true, - ReceiveMaximum = 123, - WillFlag = true, - WillTopic = "WillTopic", - WillMessage = Encoding.UTF8.GetBytes("WillMessage"), - WillRetain = true, - KeepAlivePeriod = 456, - MaximumPacketSize = 789, - RequestProblemInformation = true, - RequestResponseInformation = true, - SessionExpiryInterval = 27, - TopicAliasMaximum = 67, - WillContentType = "WillContentType", - WillCorrelationData = Encoding.UTF8.GetBytes("WillCorrelationData"), - WillDelayInterval = 782, - WillQoS = MqttQualityOfServiceLevel.ExactlyOnce, - WillResponseTopic = "WillResponseTopic", - WillMessageExpiryInterval = 542, - WillPayloadFormatIndicator = MqttPayloadFormatIndicator.CharacterData, - UserProperties = new List - { - new MqttUserProperty("Foo", "Bar") - }, - WillUserProperties = new List - { - new MqttUserProperty("WillFoo", "WillBar") - } - }; + PacketIdentifier = 123, + TopicFilters = ["TopicFilter1"], + UserProperties = [new("Foo", "Bar")] + }; - var deserialized = MqttPacketSerializationHelper.EncodeAndDecodePacket(connectPacket, MqttProtocolVersion.V311); + var deserialized = MqttPacketSerializationHelper.EncodeAndDecodePacket(unsubscribePacket, MqttProtocolVersion.V311); - Assert.AreEqual(connectPacket.Username, deserialized.Username); - CollectionAssert.AreEqual(connectPacket.Password, deserialized.Password); - Assert.AreEqual(connectPacket.ClientId, deserialized.ClientId); - CollectionAssert.AreEqual(null, deserialized.AuthenticationData); // Not supported in v3.1.1 - Assert.AreEqual(null, deserialized.AuthenticationMethod); // Not supported in v3.1.1 - Assert.AreEqual(connectPacket.CleanSession, deserialized.CleanSession); - Assert.AreEqual(0L, deserialized.ReceiveMaximum); // Not supported in v3.1.1 - Assert.AreEqual(connectPacket.WillFlag, deserialized.WillFlag); - Assert.AreEqual(connectPacket.WillTopic, deserialized.WillTopic); - CollectionAssert.AreEqual(connectPacket.WillMessage, deserialized.WillMessage); - Assert.AreEqual(connectPacket.WillRetain, deserialized.WillRetain); - Assert.AreEqual(connectPacket.KeepAlivePeriod, deserialized.KeepAlivePeriod); - // MaximumPacketSize not available in MQTTv3. - // RequestProblemInformation not available in MQTTv3. - // RequestResponseInformation not available in MQTTv3. - // SessionExpiryInterval not available in MQTTv3. - // TopicAliasMaximum not available in MQTTv3. - // WillContentType not available in MQTTv3. - // WillCorrelationData not available in MQTTv3. - // WillDelayInterval not available in MQTTv3. - Assert.AreEqual(connectPacket.WillQoS, deserialized.WillQoS); - // WillResponseTopic not available in MQTTv3. - // WillMessageExpiryInterval not available in MQTTv3. - // WillPayloadFormatIndicator not available in MQTTv3. - Assert.IsNull(deserialized.UserProperties); // Not supported in v3.1.1 - Assert.IsNull(deserialized.WillUserProperties); // Not supported in v3.1.1 - } - - [TestMethod] - public void Serialize_Full_MqttDisconnectPacket_V311() - { - var disconnectPacket = new MqttDisconnectPacket - { - ReasonCode = MqttDisconnectReasonCode.NormalDisconnection, // MQTTv3 has no other values than this. - ReasonString = "ReasonString", - ServerReference = "ServerReference", - SessionExpiryInterval = 234, - UserProperties = new List - { - new MqttUserProperty("Foo", "Bar") - } - }; - - var deserialized = MqttPacketSerializationHelper.EncodeAndDecodePacket(disconnectPacket, MqttProtocolVersion.V311); - - Assert.AreEqual(disconnectPacket.ReasonCode, deserialized.ReasonCode); - Assert.AreEqual(null, deserialized.ReasonString); // Not supported in v3.1.1 - Assert.AreEqual(null, deserialized.ServerReference); // Not supported in v3.1.1 - Assert.AreEqual(0U, deserialized.SessionExpiryInterval); // Not supported in v3.1.1 - CollectionAssert.AreEqual(null, deserialized.UserProperties); - } - - [TestMethod] - public void Serialize_Full_MqttPingReqPacket_V311() - { - var pingReqPacket = new MqttPingReqPacket(); - - var deserialized = MqttPacketSerializationHelper.EncodeAndDecodePacket(pingReqPacket, MqttProtocolVersion.V311); - - Assert.IsNotNull(deserialized); - } - - [TestMethod] - public void Serialize_Full_MqttPingRespPacket_V311() - { - var pingRespPacket = new MqttPingRespPacket(); - - var deserialized = MqttPacketSerializationHelper.EncodeAndDecodePacket(pingRespPacket, MqttProtocolVersion.V311); - - Assert.IsNotNull(deserialized); - } - - [TestMethod] - public void Serialize_Full_MqttPubAckPacket_V311() - { - var pubAckPacket = new MqttPubAckPacket - { - PacketIdentifier = 123, - ReasonCode = MqttPubAckReasonCode.NoMatchingSubscribers, - ReasonString = "ReasonString", - UserProperties = new List - { - new MqttUserProperty("Foo", "Bar") - } - }; - - var deserialized = MqttPacketSerializationHelper.EncodeAndDecodePacket(pubAckPacket, MqttProtocolVersion.V311); - - Assert.AreEqual(pubAckPacket.PacketIdentifier, deserialized.PacketIdentifier); - Assert.AreEqual(MqttPubAckReasonCode.Success, deserialized.ReasonCode); // Not supported in v3.1.1 - Assert.AreEqual(null, deserialized.ReasonString); // Not supported in v3.1.1 - CollectionAssert.AreEqual(null, deserialized.UserProperties); - } - - [TestMethod] - public void Serialize_Full_MqttPubCompPacket_V311() - { - var pubCompPacket = new MqttPubCompPacket - { - PacketIdentifier = 123, - ReasonCode = MqttPubCompReasonCode.PacketIdentifierNotFound, - ReasonString = "ReasonString", - UserProperties = new List - { - new MqttUserProperty("Foo", "Bar") - } - }; - - var deserialized = MqttPacketSerializationHelper.EncodeAndDecodePacket(pubCompPacket, MqttProtocolVersion.V311); - - Assert.AreEqual(pubCompPacket.PacketIdentifier, deserialized.PacketIdentifier); - // ReasonCode not available in MQTTv3. - // ReasonString not available in MQTTv3. - // UserProperties not available in MQTTv3. - Assert.IsNull(deserialized.UserProperties); - } - - [TestMethod] - public void Serialize_Full_MqttPublishPacket_V311() - { - var publishPacket = new MqttPublishPacket - { - PacketIdentifier = 123, - Dup = true, - Retain = true, - PayloadSegment = new ArraySegment(Encoding.ASCII.GetBytes("Payload")), - QualityOfServiceLevel = MqttQualityOfServiceLevel.AtLeastOnce, - Topic = "Topic", - ResponseTopic = "/Response", - ContentType = "Content-Type", - CorrelationData = Encoding.UTF8.GetBytes("CorrelationData"), - TopicAlias = 27, - SubscriptionIdentifiers = new List - { - 123 - }, - MessageExpiryInterval = 38, - PayloadFormatIndicator = MqttPayloadFormatIndicator.CharacterData, - UserProperties = new List - { - new MqttUserProperty("Foo", "Bar") - } - }; - - var deserialized = MqttPacketSerializationHelper.EncodeAndDecodePacket(publishPacket, MqttProtocolVersion.V311); - - Assert.AreEqual(publishPacket.PacketIdentifier, deserialized.PacketIdentifier); - Assert.AreEqual(publishPacket.Dup, deserialized.Dup); - Assert.AreEqual(publishPacket.Retain, deserialized.Retain); - CollectionAssert.AreEqual(publishPacket.Payload.ToArray(), deserialized.Payload.ToArray()); - Assert.AreEqual(publishPacket.QualityOfServiceLevel, deserialized.QualityOfServiceLevel); - Assert.AreEqual(publishPacket.Topic, deserialized.Topic); - Assert.AreEqual(null, deserialized.ResponseTopic); // Not supported in v3.1.1. - Assert.AreEqual(null, deserialized.ContentType); // Not supported in v3.1.1. - CollectionAssert.AreEqual(null, deserialized.CorrelationData); // Not supported in v3.1.1. - Assert.AreEqual(0U, deserialized.TopicAlias); // Not supported in v3.1.1. - CollectionAssert.AreEqual(null, deserialized.SubscriptionIdentifiers); // Not supported in v3.1.1 - Assert.AreEqual(0U, deserialized.MessageExpiryInterval); // Not supported in v3.1.1 - Assert.AreEqual(MqttPayloadFormatIndicator.Unspecified, deserialized.PayloadFormatIndicator); // Not supported in v3.1.1 - Assert.IsNull(deserialized.UserProperties); // Not supported in v3.1.1 - } - - [TestMethod] - public void Serialize_Full_MqttPubRecPacket_V311() - { - var pubRecPacket = new MqttPubRecPacket - { - PacketIdentifier = 123, - ReasonCode = MqttPubRecReasonCode.UnspecifiedError, - ReasonString = "ReasonString", - UserProperties = new List - { - new MqttUserProperty("Foo", "Bar") - } - }; - - var deserialized = MqttPacketSerializationHelper.EncodeAndDecodePacket(pubRecPacket, MqttProtocolVersion.V311); - - Assert.AreEqual(pubRecPacket.PacketIdentifier, deserialized.PacketIdentifier); - // ReasonCode not available in MQTTv3. - // ReasonString not available in MQTTv3. - // UserProperties not available in MQTTv3. - Assert.IsNull(deserialized.UserProperties); - } - - [TestMethod] - public void Serialize_Full_MqttPubRelPacket_V311() - { - var pubRelPacket = new MqttPubRelPacket - { - PacketIdentifier = 123, - ReasonCode = MqttPubRelReasonCode.PacketIdentifierNotFound, - ReasonString = "ReasonString", - UserProperties = new List - { - new MqttUserProperty("Foo", "Bar") - } - }; - - var deserialized = MqttPacketSerializationHelper.EncodeAndDecodePacket(pubRelPacket, MqttProtocolVersion.V311); - - Assert.AreEqual(pubRelPacket.PacketIdentifier, deserialized.PacketIdentifier); - // ReasonCode not available in MQTTv3. - // ReasonString not available in MQTTv3. - // UserProperties not available in MQTTv3. - Assert.IsNull(deserialized.UserProperties); - } - - [TestMethod] - public void Serialize_Full_MqttSubAckPacket_V311() - { - var subAckPacket = new MqttSubAckPacket - { - PacketIdentifier = 123, - ReasonString = "ReasonString", - ReasonCodes = new List - { - MqttSubscribeReasonCode.GrantedQoS1 - }, - UserProperties = new List - { - new MqttUserProperty("Foo", "Bar") - } - }; - - var deserialized = MqttPacketSerializationHelper.EncodeAndDecodePacket(subAckPacket, MqttProtocolVersion.V311); - - Assert.AreEqual(subAckPacket.PacketIdentifier, deserialized.PacketIdentifier); - Assert.AreEqual(null, deserialized.ReasonString); // Not supported in v3.1.1 - Assert.AreEqual(subAckPacket.ReasonCodes.Count, deserialized.ReasonCodes.Count); - Assert.AreEqual(subAckPacket.ReasonCodes[0], deserialized.ReasonCodes[0]); - CollectionAssert.AreEqual(null, deserialized.UserProperties); // Not supported in v3.1.1 - } - - [TestMethod] - public void Serialize_Full_MqttSubscribePacket_V311() - { - var subscribePacket = new MqttSubscribePacket - { - PacketIdentifier = 123, - SubscriptionIdentifier = 456, - TopicFilters = new List - { - new MqttTopicFilter - { - Topic = "Topic", - NoLocal = true, - RetainHandling = MqttRetainHandling.SendAtSubscribeIfNewSubscriptionOnly, - RetainAsPublished = true, - QualityOfServiceLevel = MqttQualityOfServiceLevel.AtMostOnce - } - }, - UserProperties = new List - { - new MqttUserProperty("Foo", "Bar") - } - }; - - var deserialized = MqttPacketSerializationHelper.EncodeAndDecodePacket(subscribePacket, MqttProtocolVersion.V311); - - Assert.AreEqual(subscribePacket.PacketIdentifier, deserialized.PacketIdentifier); - Assert.AreEqual(0U, deserialized.SubscriptionIdentifier); // Not supported in v3.1.1 - Assert.AreEqual(1, deserialized.TopicFilters.Count); - Assert.AreEqual(subscribePacket.TopicFilters[0].Topic, deserialized.TopicFilters[0].Topic); - Assert.AreEqual(false, deserialized.TopicFilters[0].NoLocal); // Not supported in v3.1.1 - Assert.AreEqual(MqttRetainHandling.SendAtSubscribe, deserialized.TopicFilters[0].RetainHandling); // Not supported in v3.1.1 - Assert.AreEqual(false, deserialized.TopicFilters[0].RetainAsPublished); // Not supported in v3.1.1 - Assert.AreEqual(subscribePacket.TopicFilters[0].QualityOfServiceLevel, deserialized.TopicFilters[0].QualityOfServiceLevel); - CollectionAssert.AreEqual(null, deserialized.UserProperties); // Not supported in v3.1.1 - } - - [TestMethod] - public void Serialize_Full_MqttUnsubAckPacket_V311() - { - var unsubAckPacket = new MqttUnsubAckPacket - { - PacketIdentifier = 123, - ReasonCodes = new List - { - MqttUnsubscribeReasonCode.ImplementationSpecificError - }, - ReasonString = "ReasonString", - UserProperties = new List - { - new MqttUserProperty("Foo", "Bar") - } - }; - - var deserialized = MqttPacketSerializationHelper.EncodeAndDecodePacket(unsubAckPacket, MqttProtocolVersion.V311); - - Assert.AreEqual(unsubAckPacket.PacketIdentifier, deserialized.PacketIdentifier); - Assert.AreEqual(null, deserialized.ReasonString); // Not supported in v3.1.1 - CollectionAssert.AreEqual(null, deserialized.ReasonCodes); // Not supported in v3.1.1 - CollectionAssert.AreEqual(null, deserialized.UserProperties); // Not supported in v3.1.1 - } - - [TestMethod] - public void Serialize_Full_MqttUnsubscribePacket_V311() - { - var unsubscribePacket = new MqttUnsubscribePacket - { - PacketIdentifier = 123, - TopicFilters = new List - { - "TopicFilter1" - }, - UserProperties = new List - { - new MqttUserProperty("Foo", "Bar") - } - }; - - var deserialized = MqttPacketSerializationHelper.EncodeAndDecodePacket(unsubscribePacket, MqttProtocolVersion.V311); - - Assert.AreEqual(unsubscribePacket.PacketIdentifier, deserialized.PacketIdentifier); - Assert.AreEqual(unsubscribePacket.TopicFilters.Count, deserialized.TopicFilters.Count); - Assert.AreEqual(unsubscribePacket.TopicFilters[0], deserialized.TopicFilters[0]); - CollectionAssert.AreEqual(null, deserialized.UserProperties); - } + Assert.AreEqual(unsubscribePacket.PacketIdentifier, deserialized.PacketIdentifier); + Assert.AreEqual(unsubscribePacket.TopicFilters.Count, deserialized.TopicFilters.Count); + Assert.AreEqual(unsubscribePacket.TopicFilters[0], deserialized.TopicFilters[0]); + CollectionAssert.AreEqual(null, deserialized.UserProperties); } } \ No newline at end of file diff --git a/Source/MQTTnet.Tests/Formatter/MqttPacketSerialization_V5_Tests.cs b/Source/MQTTnet.Tests/Formatter/MqttPacketSerialization_V5_Tests.cs index f74ba975..2f327765 100644 --- a/Source/MQTTnet.Tests/Formatter/MqttPacketSerialization_V5_Tests.cs +++ b/Source/MQTTnet.Tests/Formatter/MqttPacketSerialization_V5_Tests.cs @@ -11,6 +11,7 @@ using MQTTnet.Protocol; namespace MQTTnet.Tests.Formatter; +// ReSharper disable InconsistentNaming [TestClass] public sealed class MqttPacketSerialization_V5_Tests { diff --git a/Source/MQTTnet.Tests/Formatter/MqttPacketWriter_Tests.cs b/Source/MQTTnet.Tests/Formatter/MqttPacketWriter_Tests.cs index 7a16ff30..35412e41 100644 --- a/Source/MQTTnet.Tests/Formatter/MqttPacketWriter_Tests.cs +++ b/Source/MQTTnet.Tests/Formatter/MqttPacketWriter_Tests.cs @@ -1,102 +1,101 @@ using System; -using System.Linq; using Microsoft.VisualStudio.TestTools.UnitTesting; using MQTTnet.Exceptions; using MQTTnet.Formatter; -namespace MQTTnet.Tests.Formatter +namespace MQTTnet.Tests.Formatter; + +// ReSharper disable InconsistentNaming +[TestClass] +public sealed class MqttPacketWriter_Tests { - [TestClass] - public sealed class MqttPacketWriter_Tests + [TestMethod] + public void Reset_After_Usage() { - [TestMethod] - public void Reset_After_Usage() + var writer = new MqttBufferWriter(4096, 65535); + writer.WriteString("AString"); + writer.WriteByte(0x1); + writer.WriteByte(0x0); + writer.WriteByte(0x1); + writer.WriteVariableByteInteger(1234U); + writer.WriteVariableByteInteger(9876U); + + writer.Reset(0); + + Assert.AreEqual(0, writer.Length); + } + + [TestMethod] + public void Use_All_Data_Types() + { + var writer = new MqttBufferWriter(4096, 65535); + writer.WriteString("AString"); + writer.WriteByte(0x1); + writer.WriteByte(0x0); + writer.WriteByte(0x1); + writer.WriteVariableByteInteger(1234U); + writer.WriteVariableByteInteger(9876U); + + var buffer = writer.GetBuffer(); + + var reader = new MqttBufferReader(); + reader.SetBuffer(buffer, 0, writer.Length); + + Assert.AreEqual("AString", reader.ReadString()); + Assert.IsTrue(reader.ReadByte() == 1); + Assert.IsTrue(reader.ReadByte() == 0); + Assert.IsTrue(reader.ReadByte() == 1); + Assert.AreEqual(1234U, reader.ReadVariableByteInteger()); + Assert.AreEqual(9876U, reader.ReadVariableByteInteger()); + } + + [TestMethod] + [ExpectedException(typeof(MqttProtocolViolationException))] + public void Throw_If_String_Too_Long() + { + var writer = new MqttBufferWriter(4096, 65535); + + writer.WriteString(string.Empty.PadLeft(65536)); + } + + [TestMethod] + public void Write_And_Read_Multiple_Times() + { + var writer = new MqttBufferWriter(4096, 65535); + writer.WriteString("A relative short string."); + writer.WriteBinary(new byte[1234]); + writer.WriteByte(0x01); + writer.WriteByte(0x02); + writer.WriteVariableByteInteger(5647382); + writer.WriteString("A relative short string."); + writer.WriteVariableByteInteger(8574489); + writer.WriteBinary(new byte[48]); + writer.WriteByte(2); + writer.WriteByte(0x02); + writer.WriteString("fjgffiogfhgfhoihgoireghreghreguhreguireoghreouighreouighreughreguiorehreuiohruiorehreuioghreug"); + writer.WriteBinary(new byte[3]); + + var readPayload = new ArraySegment(writer.GetBuffer(), 0, writer.Length).ToArray(); + + var reader = new MqttBufferReader(); + reader.SetBuffer(readPayload, 0, readPayload.Length); + + for (var i = 0; i < 100000; i++) { - var writer = new MqttBufferWriter(4096, 65535); - writer.WriteString("AString"); - writer.WriteByte(0x1); - writer.WriteByte(0x0); - writer.WriteByte(0x1); - writer.WriteVariableByteInteger(1234U); - writer.WriteVariableByteInteger(9876U); + reader.Seek(0); - writer.Reset(0); - - Assert.AreEqual(0, writer.Length); - } - - [TestMethod] - public void Use_All_Data_Types() - { - var writer = new MqttBufferWriter(4096, 65535); - writer.WriteString("AString"); - writer.WriteByte(0x1); - writer.WriteByte(0x0); - writer.WriteByte(0x1); - writer.WriteVariableByteInteger(1234U); - writer.WriteVariableByteInteger(9876U); - - var buffer = writer.GetBuffer(); - - var reader = new MqttBufferReader(); - reader.SetBuffer(buffer, 0, writer.Length); - - Assert.AreEqual("AString", reader.ReadString()); - Assert.IsTrue(reader.ReadByte() == 1); - Assert.IsTrue(reader.ReadByte() == 0); - Assert.IsTrue(reader.ReadByte() == 1); - Assert.AreEqual(1234U, reader.ReadVariableByteInteger()); - Assert.AreEqual(9876U, reader.ReadVariableByteInteger()); - } - - [TestMethod] - [ExpectedException(typeof(MqttProtocolViolationException))] - public void Throw_If_String_Too_Long() - { - var writer = new MqttBufferWriter(4096, 65535); - - writer.WriteString(string.Empty.PadLeft(65536)); - } - - [TestMethod] - public void Write_And_Read_Multiple_Times() - { - var writer = new MqttBufferWriter(4096, 65535); - writer.WriteString("A relative short string."); - writer.WriteBinary(new byte[1234]); - writer.WriteByte(0x01); - writer.WriteByte(0x02); - writer.WriteVariableByteInteger(5647382); - writer.WriteString("A relative short string."); - writer.WriteVariableByteInteger(8574489); - writer.WriteBinary(new byte[48]); - writer.WriteByte(2); - writer.WriteByte(0x02); - writer.WriteString("fjgffiogfhgfhoihgoireghreghreguhreguireoghreouighreouighreughreguiorehreuiohruiorehreuioghreug"); - writer.WriteBinary(new byte[3]); - - var readPayload = new ArraySegment(writer.GetBuffer(), 0, writer.Length).ToArray(); - - var reader = new MqttBufferReader(); - reader.SetBuffer(readPayload, 0, readPayload.Length); - - for (var i = 0; i < 100000; i++) - { - reader.Seek(0); - - reader.ReadString(); - reader.ReadBinaryData(); - reader.ReadByte(); - reader.ReadByte(); - reader.ReadVariableByteInteger(); - reader.ReadString(); - reader.ReadVariableByteInteger(); - reader.ReadBinaryData(); - reader.ReadByte(); - reader.ReadByte(); - reader.ReadString(); - reader.ReadBinaryData(); - } + reader.ReadString(); + reader.ReadBinaryData(); + reader.ReadByte(); + reader.ReadByte(); + reader.ReadVariableByteInteger(); + reader.ReadString(); + reader.ReadVariableByteInteger(); + reader.ReadBinaryData(); + reader.ReadByte(); + reader.ReadByte(); + reader.ReadString(); + reader.ReadBinaryData(); } } } \ No newline at end of file diff --git a/Source/MQTTnet.Tests/Helpers/MqttClientExtensions.cs b/Source/MQTTnet.Tests/Helpers/MqttClientExtensions.cs index 83650729..20198de8 100644 --- a/Source/MQTTnet.Tests/Helpers/MqttClientExtensions.cs +++ b/Source/MQTTnet.Tests/Helpers/MqttClientExtensions.cs @@ -5,15 +5,14 @@ using System; using MQTTnet.Tests.Mockups; -namespace MQTTnet.Tests.Helpers -{ - public static class MqttClientExtensions - { - public static TestApplicationMessageReceivedHandler TrackReceivedMessages(this IMqttClient client) - { - ArgumentNullException.ThrowIfNull(client); +namespace MQTTnet.Tests.Helpers; - return new TestApplicationMessageReceivedHandler(client); - } +public static class MqttClientExtensions +{ + public static TestApplicationMessageReceivedHandler TrackReceivedMessages(this IMqttClient client) + { + ArgumentNullException.ThrowIfNull(client); + + return new TestApplicationMessageReceivedHandler(client); } } \ No newline at end of file diff --git a/Source/MQTTnet.Tests/Helpers/MqttPacketWriterExtensions.cs b/Source/MQTTnet.Tests/Helpers/MqttPacketWriterExtensions.cs index 1c6e1bf1..def919ef 100644 --- a/Source/MQTTnet.Tests/Helpers/MqttPacketWriterExtensions.cs +++ b/Source/MQTTnet.Tests/Helpers/MqttPacketWriterExtensions.cs @@ -5,16 +5,15 @@ using MQTTnet.Formatter; using MQTTnet.Protocol; -namespace MQTTnet.Tests.Helpers +namespace MQTTnet.Tests.Helpers; + +public static class MqttPacketWriterExtensions { - public static class MqttPacketWriterExtensions + public static byte[] AddMqttHeader(this MqttBufferWriter writer, MqttControlPacketType header, byte[] body) { - public static byte[] AddMqttHeader(this MqttBufferWriter writer, MqttControlPacketType header, byte[] body) - { - writer.WriteByte(MqttBufferWriter.BuildFixedHeader(header)); - writer.WriteVariableByteInteger((uint)body.Length); - writer.WriteBinary(body, 0, body.Length); - return writer.GetBuffer(); - } + writer.WriteByte(MqttBufferWriter.BuildFixedHeader(header)); + writer.WriteVariableByteInteger((uint)body.Length); + writer.WriteBinary(body, 0, body.Length); + return writer.GetBuffer(); } -} +} \ No newline at end of file diff --git a/Source/MQTTnet.Tests/Helpers/ReflectionExtensions.cs b/Source/MQTTnet.Tests/Helpers/ReflectionExtensions.cs index 6e857aeb..00c3d153 100644 --- a/Source/MQTTnet.Tests/Helpers/ReflectionExtensions.cs +++ b/Source/MQTTnet.Tests/Helpers/ReflectionExtensions.cs @@ -5,21 +5,20 @@ using System; using System.Reflection; -namespace MQTTnet.Tests.Helpers +namespace MQTTnet.Tests.Helpers; + +public static class ReflectionExtensions { - public static class ReflectionExtensions + public static object GetFieldValue(this object source, string fieldName) { - public static object GetFieldValue(this object source, string fieldName) + ArgumentNullException.ThrowIfNull(source); + + var field = source.GetType().GetField(fieldName, BindingFlags.Instance | BindingFlags.NonPublic); + if (field == null) { - ArgumentNullException.ThrowIfNull(source); - - var field = source.GetType().GetField(fieldName, BindingFlags.Instance | BindingFlags.NonPublic); - if (field == null) - { - throw new ArgumentException($"Field {fieldName} not found."); - } - - return field.GetValue(source); + throw new ArgumentException($"Field {fieldName} not found."); } + + return field.GetValue(source); } } \ No newline at end of file diff --git a/Source/MQTTnet.Tests/Internal/AsyncEvent_Tests.cs b/Source/MQTTnet.Tests/Internal/AsyncEvent_Tests.cs index 94a960e5..f741675f 100644 --- a/Source/MQTTnet.Tests/Internal/AsyncEvent_Tests.cs +++ b/Source/MQTTnet.Tests/Internal/AsyncEvent_Tests.cs @@ -8,133 +8,133 @@ using System.Threading.Tasks; using Microsoft.VisualStudio.TestTools.UnitTesting; using MQTTnet.Internal; -namespace MQTTnet.Tests.Internal +namespace MQTTnet.Tests.Internal; + +// ReSharper disable InconsistentNaming +[TestClass] +public sealed class AsyncEvent_Tests { - [TestClass] - public sealed class AsyncEvent_Tests + int _testEventAsyncCount; + + [TestMethod] + public async Task Attach_and_Detach_Sync_And_Async_EventHandler() { - int _testEventAsyncCount; + var testClass = new TestClass(); - [TestMethod] - public async Task Attach_and_Detach_Sync_And_Async_EventHandler() + testClass.TestEventAsync += OnTestEventAsync; + testClass.TestEvent += OnTestEvent; + + await testClass.FireEventAsync(EventArgs.Empty); + Assert.AreEqual(2, _testEventAsyncCount); + + testClass.TestEvent -= OnTestEvent; + + await testClass.FireEventAsync(EventArgs.Empty); + Assert.AreEqual(3, _testEventAsyncCount); + + testClass.TestEventAsync -= OnTestEventAsync; + + await testClass.FireEventAsync(EventArgs.Empty); + Assert.AreEqual(3, _testEventAsyncCount); + } + + [TestMethod] + public async Task Attach_EventHandler() + { + var testClass = new TestClass(); + + testClass.TestEventAsync += OnTestEventAsync; + + await testClass.FireEventAsync(EventArgs.Empty); + + Assert.AreEqual(1, _testEventAsyncCount); + } + + [TestMethod] + public async Task Attach_Sync_And_Async_EventHandler() + { + var testClass = new TestClass(); + + testClass.TestEventAsync += OnTestEventAsync; + testClass.TestEvent += OnTestEvent; + + await testClass.FireEventAsync(EventArgs.Empty); + + Assert.AreEqual(2, _testEventAsyncCount); + } + + [TestMethod] + public async Task Detach_EventHandler() + { + var testClass = new TestClass(); + + testClass.TestEventAsync += OnTestEventAsync; + testClass.TestEventAsync -= OnTestEventAsync; + + await testClass.FireEventAsync(EventArgs.Empty); + + Assert.AreEqual(0, _testEventAsyncCount); + } + + [TestMethod] + public void Has_Handlers() + { + var testClass = new TestClass(); + testClass.TestEventAsync += OnTestEventAsync; + + Assert.AreEqual(true, testClass.HasTestHandlers); + } + + [TestMethod] + public void No_Handlers() + { + var testClass = new TestClass(); + + Assert.AreEqual(false, testClass.HasTestHandlers); + } + + [TestMethod] + public void Remove_Handlers() + { + var testClass = new TestClass(); + testClass.TestEventAsync += OnTestEventAsync; + testClass.TestEventAsync -= OnTestEventAsync; + + Assert.AreEqual(false, testClass.HasTestHandlers); + } + + void OnTestEvent(EventArgs arg) + { + Interlocked.Increment(ref _testEventAsyncCount); + } + + Task OnTestEventAsync(EventArgs arg) + { + Interlocked.Increment(ref _testEventAsyncCount); + return CompletedTask.Instance; + } + + sealed class TestClass + { + readonly AsyncEvent _testEvent = new(); + + public event Action TestEvent { - var testClass = new TestClass(); - - testClass.TestEventAsync += OnTestEventAsync; - testClass.TestEvent += OnTestEvent; - - await testClass.FireEventAsync(EventArgs.Empty); - Assert.AreEqual(2, _testEventAsyncCount); - - testClass.TestEvent -= OnTestEvent; - - await testClass.FireEventAsync(EventArgs.Empty); - Assert.AreEqual(3, _testEventAsyncCount); - - testClass.TestEventAsync -= OnTestEventAsync; - - await testClass.FireEventAsync(EventArgs.Empty); - Assert.AreEqual(3, _testEventAsyncCount); + add => _testEvent.AddHandler(value); + remove => _testEvent.RemoveHandler(value); } - [TestMethod] - public async Task Attach_EventHandler() + public event Func TestEventAsync { - var testClass = new TestClass(); - - testClass.TestEventAsync += OnTestEventAsync; - - await testClass.FireEventAsync(EventArgs.Empty); - - Assert.AreEqual(1, _testEventAsyncCount); + add => _testEvent.AddHandler(value); + remove => _testEvent.RemoveHandler(value); } - [TestMethod] - public async Task Attach_Sync_And_Async_EventHandler() + public bool HasTestHandlers => _testEvent.HasHandlers; + + public Task FireEventAsync(EventArgs eventArgs) { - var testClass = new TestClass(); - - testClass.TestEventAsync += OnTestEventAsync; - testClass.TestEvent += OnTestEvent; - - await testClass.FireEventAsync(EventArgs.Empty); - - Assert.AreEqual(2, _testEventAsyncCount); - } - - [TestMethod] - public async Task Detach_EventHandler() - { - var testClass = new TestClass(); - - testClass.TestEventAsync += OnTestEventAsync; - testClass.TestEventAsync -= OnTestEventAsync; - - await testClass.FireEventAsync(EventArgs.Empty); - - Assert.AreEqual(0, _testEventAsyncCount); - } - - [TestMethod] - public void Has_Handlers() - { - var testClass = new TestClass(); - testClass.TestEventAsync += OnTestEventAsync; - - Assert.AreEqual(true, testClass.HasTestHandlers); - } - - [TestMethod] - public void No_Handlers() - { - var testClass = new TestClass(); - - Assert.AreEqual(false, testClass.HasTestHandlers); - } - - [TestMethod] - public void Remove_Handlers() - { - var testClass = new TestClass(); - testClass.TestEventAsync += OnTestEventAsync; - testClass.TestEventAsync -= OnTestEventAsync; - - Assert.AreEqual(false, testClass.HasTestHandlers); - } - - void OnTestEvent(EventArgs arg) - { - Interlocked.Increment(ref _testEventAsyncCount); - } - - Task OnTestEventAsync(EventArgs arg) - { - Interlocked.Increment(ref _testEventAsyncCount); - return CompletedTask.Instance; - } - - sealed class TestClass - { - readonly AsyncEvent _testEvent = new AsyncEvent(); - - public event Action TestEvent - { - add => _testEvent.AddHandler(value); - remove => _testEvent.RemoveHandler(value); - } - - public event Func TestEventAsync - { - add => _testEvent.AddHandler(value); - remove => _testEvent.RemoveHandler(value); - } - - public bool HasTestHandlers => _testEvent.HasHandlers; - - public Task FireEventAsync(EventArgs eventArgs) - { - return _testEvent.InvokeAsync(eventArgs); - } + return _testEvent.InvokeAsync(eventArgs); } } } \ No newline at end of file diff --git a/Source/MQTTnet.Tests/Internal/AsyncLock_Tests.cs b/Source/MQTTnet.Tests/Internal/AsyncLock_Tests.cs index e30fd63d..30d4eb2f 100644 --- a/Source/MQTTnet.Tests/Internal/AsyncLock_Tests.cs +++ b/Source/MQTTnet.Tests/Internal/AsyncLock_Tests.cs @@ -9,283 +9,267 @@ using System.Threading.Tasks; using Microsoft.VisualStudio.TestTools.UnitTesting; using MQTTnet.Internal; -namespace MQTTnet.Tests.Internal +namespace MQTTnet.Tests.Internal; + +// ReSharper disable InconsistentNaming +[TestClass] +public sealed class AsyncLock_Tests { - [TestClass] - public sealed class AsyncLock_Tests + [TestMethod] + public async Task Cancellation_Of_Awaiter() { - [TestMethod] - public async Task Cancellation_Of_Awaiter() + var @lock = new AsyncLock(); + + // This call will not yet "release" the lock due to missing _using_. + var releaser = await @lock.EnterAsync().ConfigureAwait(false); + + var counter = 0; + + Debug.WriteLine("Prepared locked lock."); + + _ = Task.Run( + async () => + { + // SHOULD GET TIMEOUT! + using var timeout = new CancellationTokenSource(TimeSpan.FromSeconds(1)); + using (await @lock.EnterAsync(timeout.Token)) + { + Debug.WriteLine("Task 1 incremented"); + counter++; + } + }); + + _ = Task.Run( + async () => + { + // SHOULD GET TIMEOUT! + using var timeout = new CancellationTokenSource(TimeSpan.FromSeconds(2)); + using (await @lock.EnterAsync(timeout.Token)) + { + Debug.WriteLine("Task 2 incremented"); + counter++; + } + }); + + _ = Task.Run( + async () => + { + // SHOULD GET TIMEOUT! + using var timeout = new CancellationTokenSource(TimeSpan.FromSeconds(3)); + using (await @lock.EnterAsync(timeout.Token)) + { + Debug.WriteLine("Task 3 incremented"); + counter++; + } + }); + + _ = Task.Run( + async () => + { + using var timeout = new CancellationTokenSource(TimeSpan.FromSeconds(4)); + using (await @lock.EnterAsync(timeout.Token)) + { + Debug.WriteLine("Task 4 incremented"); + counter++; + } + }); + + _ = Task.Run( + async () => + { + using var timeout = new CancellationTokenSource(TimeSpan.FromSeconds(5)); + using (await @lock.EnterAsync(timeout.Token)) + { + Debug.WriteLine("Task 5 incremented"); + counter++; + } + }); + + _ = Task.Run( + async () => + { + using var timeout = new CancellationTokenSource(TimeSpan.FromSeconds(6)); + using (await @lock.EnterAsync(timeout.Token)) + { + Debug.WriteLine("Task 6 incremented"); + counter++; + } + }); + + Debug.WriteLine("Delay before release..."); + await Task.Delay(TimeSpan.FromSeconds(3.1)); + releaser.Dispose(); + + Debug.WriteLine("Wait for all tasks..."); + await Task.Delay(TimeSpan.FromSeconds(6.1)); + + Assert.AreEqual(3, counter); + } + + [TestMethod] + public void Lock_Parallel_Tasks() + { + const int taskCount = 50; + + var @lock = new AsyncLock(); + + var tasks = new Task[taskCount]; + var globalI = 0; + for (var i = 0; i < taskCount; i++) { - var @lock = new AsyncLock(); - - // This call will not yet "release" the lock due to missing _using_. - var releaser = await @lock.EnterAsync().ConfigureAwait(false); - - var counter = 0; - - Debug.WriteLine("Prepared locked lock."); - - _ = Task.Run( +#pragma warning disable CS4014 // Because this call is not awaited, execution of the current method continues before the call is completed + tasks[i] = Task.Run( async () => { - // SHOULD GET TIMEOUT! - using (var timeout = new CancellationTokenSource(TimeSpan.FromSeconds(1))) + using (await @lock.EnterAsync()) { - using (await @lock.EnterAsync(timeout.Token)) - { - Debug.WriteLine("Task 1 incremented"); - counter++; - } + var localI = globalI; + await Task.Delay(5); // Increase the chance for wrong data. + localI++; + globalI = localI; } }); - - _ = Task.Run( - async () => - { - // SHOULD GET TIMEOUT! - using (var timeout = new CancellationTokenSource(TimeSpan.FromSeconds(2))) - { - using (await @lock.EnterAsync(timeout.Token)) - { - Debug.WriteLine("Task 2 incremented"); - counter++; - } - } - }); - - _ = Task.Run( - async () => - { - // SHOULD GET TIMEOUT! - using (var timeout = new CancellationTokenSource(TimeSpan.FromSeconds(3))) - { - using (await @lock.EnterAsync(timeout.Token)) - { - Debug.WriteLine("Task 3 incremented"); - counter++; - } - } - }); - - _ = Task.Run( - async () => - { - using (var timeout = new CancellationTokenSource(TimeSpan.FromSeconds(4))) - { - using (await @lock.EnterAsync(timeout.Token)) - { - Debug.WriteLine("Task 4 incremented"); - counter++; - } - } - }); - - _ = Task.Run( - async () => - { - using (var timeout = new CancellationTokenSource(TimeSpan.FromSeconds(5))) - { - using (await @lock.EnterAsync(timeout.Token)) - { - Debug.WriteLine("Task 5 incremented"); - counter++; - } - } - }); - - _ = Task.Run( - async () => - { - using (var timeout = new CancellationTokenSource(TimeSpan.FromSeconds(6))) - { - using (await @lock.EnterAsync(timeout.Token)) - { - Debug.WriteLine("Task 6 incremented"); - counter++; - } - } - }); - - Debug.WriteLine("Delay before release..."); - await Task.Delay(TimeSpan.FromSeconds(3.1)); - releaser.Dispose(); - - Debug.WriteLine("Wait for all tasks..."); - await Task.Delay(TimeSpan.FromSeconds(6.1)); - - Assert.AreEqual(3, counter); +#pragma warning restore CS4014 // Because this call is not awaited, execution of the current method continues before the call is completed } - [TestMethod] - public void Lock_Parallel_Tasks() + Task.WaitAll(tasks); + Assert.AreEqual(taskCount, globalI); + } + + [TestMethod] + public void Lock_10_Parallel_Tasks_With_Dispose_Doesnt_Lockup() + { + const int ThreadsCount = 10; + + var threads = new Task[ThreadsCount]; + var @lock = new AsyncLock(); + var globalI = 0; + for (var i = 0; i < ThreadsCount; i++) { - const int taskCount = 50; - - var @lock = new AsyncLock(); - - var tasks = new Task[taskCount]; - var globalI = 0; - for (var i = 0; i < taskCount; i++) - { #pragma warning disable CS4014 // Because this call is not awaited, execution of the current method continues before the call is completed - tasks[i] = Task.Run( + threads[i] = Task.Run( async () => { using (await @lock.EnterAsync()) { var localI = globalI; - await Task.Delay(5); // Increase the chance for wrong data. + await Task.Delay(10); // Increase the chance for wrong data. localI++; globalI = localI; } + }) + .ContinueWith( + x => + { + if (globalI == 5) + { + @lock.Dispose(); + @lock = new AsyncLock(); + } + + if (x.Exception != null) + { + Debug.WriteLine(x.Exception.GetBaseException().GetType().Name); + } }); #pragma warning restore CS4014 // Because this call is not awaited, execution of the current method continues before the call is completed - } - - Task.WaitAll(tasks); - Assert.AreEqual(taskCount, globalI); } - [TestMethod] - public void Lock_10_Parallel_Tasks_With_Dispose_Doesnt_Lockup() + Task.WaitAll(threads); + + // Expect only 6 because the others are failing due to disposal (if (globalI == 5)). + Assert.AreEqual(6, globalI); + } + + [TestMethod] + public async Task Lock_Serial_Calls() + { + var sum = 0; + + var @lock = new AsyncLock(); + for (var i = 0; i < 100; i++) { - const int ThreadsCount = 10; - - var threads = new Task[ThreadsCount]; - var @lock = new AsyncLock(); - var globalI = 0; - for (var i = 0; i < ThreadsCount; i++) - { -#pragma warning disable CS4014 // Because this call is not awaited, execution of the current method continues before the call is completed - threads[i] = Task.Run( - async () => - { - using (await @lock.EnterAsync()) - { - var localI = globalI; - await Task.Delay(10); // Increase the chance for wrong data. - localI++; - globalI = localI; - } - }) - .ContinueWith( - x => - { - if (globalI == 5) - { - @lock.Dispose(); - @lock = new AsyncLock(); - } - - if (x.Exception != null) - { - Debug.WriteLine(x.Exception.GetBaseException().GetType().Name); - } - }); -#pragma warning restore CS4014 // Because this call is not awaited, execution of the current method continues before the call is completed - } - - Task.WaitAll(threads); - - // Expect only 6 because the others are failing due to disposal (if (globalI == 5)). - Assert.AreEqual(6, globalI); - } - - [TestMethod] - public async Task Lock_Serial_Calls() - { - var sum = 0; - - var @lock = new AsyncLock(); - for (var i = 0; i < 100; i++) - { - using (await @lock.EnterAsync().ConfigureAwait(false)) - { - sum++; - } - } - - Assert.AreEqual(100, sum); - } - - [TestMethod] - [ExpectedException(typeof(TaskCanceledException))] - public async Task Test_Cancellation() - { - var @lock = new AsyncLock(); - - // This call will never "release" the lock due to missing _using_. - await @lock.EnterAsync().ConfigureAwait(false); - - using (var cts = new CancellationTokenSource(TimeSpan.FromSeconds(3))) - { - await @lock.EnterAsync(cts.Token).ConfigureAwait(false); - } - } - - [TestMethod] - public async Task Test_Cancellation_With_Later_Access() - { - var asyncLock = new AsyncLock(); - - var releaser = await asyncLock.EnterAsync().ConfigureAwait(false); - - try - { - using (var timeout = new CancellationTokenSource(TimeSpan.FromSeconds(3))) - { - await asyncLock.EnterAsync(timeout.Token).ConfigureAwait(false); - } - - Assert.Fail("Exception should be thrown!"); - } - catch (OperationCanceledException) - { - } - - releaser.Dispose(); - - using (await asyncLock.EnterAsync(CancellationToken.None).ConfigureAwait(false)) - { - // When the method finished, the thread got access. - } - } - - [TestMethod] - public async Task Use_After_Cancellation() - { - var @lock = new AsyncLock(); - - // This call will not yet "release" the lock due to missing _using_. - var releaser = await @lock.EnterAsync().ConfigureAwait(false); - - try - { - using (var cts = new CancellationTokenSource(TimeSpan.FromSeconds(3))) - { - await @lock.EnterAsync(cts.Token).ConfigureAwait(false); - } - } - catch (OperationCanceledException) - { - // Expected exception! - } - - releaser.Dispose(); - - // Regular usage after cancellation. using (await @lock.EnterAsync().ConfigureAwait(false)) { + sum++; + } + } + + Assert.AreEqual(100, sum); + } + + [TestMethod] + [ExpectedException(typeof(TaskCanceledException))] + public async Task Test_Cancellation() + { + var @lock = new AsyncLock(); + + // This call will never "release" the lock due to missing _using_. + await @lock.EnterAsync().ConfigureAwait(false); + + using var cts = new CancellationTokenSource(TimeSpan.FromSeconds(3)); + await @lock.EnterAsync(cts.Token).ConfigureAwait(false); + } + + [TestMethod] + public async Task Test_Cancellation_With_Later_Access() + { + var asyncLock = new AsyncLock(); + + var releaser = await asyncLock.EnterAsync().ConfigureAwait(false); + + try + { + using (var timeout = new CancellationTokenSource(TimeSpan.FromSeconds(3))) + { + await asyncLock.EnterAsync(timeout.Token).ConfigureAwait(false); } - using (await @lock.EnterAsync().ConfigureAwait(false)) - { - } + Assert.Fail("Exception should be thrown!"); + } + catch (OperationCanceledException) + { + } - using (await @lock.EnterAsync().ConfigureAwait(false)) - { - } + releaser.Dispose(); + + using (await asyncLock.EnterAsync(CancellationToken.None).ConfigureAwait(false)) + { + // When the method finished, the thread got access. + } + } + + [TestMethod] + public async Task Use_After_Cancellation() + { + var @lock = new AsyncLock(); + + // This call will not yet "release" the lock due to missing _using_. + var releaser = await @lock.EnterAsync().ConfigureAwait(false); + + try + { + using var cts = new CancellationTokenSource(TimeSpan.FromSeconds(3)); + await @lock.EnterAsync(cts.Token).ConfigureAwait(false); + } + catch (OperationCanceledException) + { + // Expected exception! + } + + releaser.Dispose(); + + // Regular usage after cancellation. + using (await @lock.EnterAsync().ConfigureAwait(false)) + { + } + + using (await @lock.EnterAsync().ConfigureAwait(false)) + { + } + + using (await @lock.EnterAsync().ConfigureAwait(false)) + { } } } \ No newline at end of file diff --git a/Source/MQTTnet.Tests/Internal/AsyncQueue_Tests.cs b/Source/MQTTnet.Tests/Internal/AsyncQueue_Tests.cs index 91887eb6..33d75619 100644 --- a/Source/MQTTnet.Tests/Internal/AsyncQueue_Tests.cs +++ b/Source/MQTTnet.Tests/Internal/AsyncQueue_Tests.cs @@ -8,138 +8,138 @@ using System.Threading.Tasks; using Microsoft.VisualStudio.TestTools.UnitTesting; using MQTTnet.Internal; -namespace MQTTnet.Tests.Internal +namespace MQTTnet.Tests.Internal; + +// ReSharper disable InconsistentNaming +[TestClass] +public class AsyncQueue_Tests { - [TestClass] - public class AsyncQueue_Tests + [TestMethod] + public async Task Preserve_Order() { - [TestMethod] - public async Task Preserve_Order() - { - var queue = new AsyncQueue(); - queue.Enqueue("1"); - queue.Enqueue("2"); - queue.Enqueue("3"); + var queue = new AsyncQueue(); + queue.Enqueue("1"); + queue.Enqueue("2"); + queue.Enqueue("3"); - Assert.AreEqual("1", (await queue.TryDequeueAsync(CancellationToken.None)).Item); - Assert.AreEqual("2", (await queue.TryDequeueAsync(CancellationToken.None)).Item); - Assert.AreEqual("3", (await queue.TryDequeueAsync(CancellationToken.None)).Item); - } - - [TestMethod] - public void Count() - { - var queue = new AsyncQueue(); - - queue.Enqueue("1"); - Assert.AreEqual(1, queue.Count); - - queue.Enqueue("2"); - Assert.AreEqual(2, queue.Count); - - queue.Enqueue("3"); - Assert.AreEqual(3, queue.Count); - } - - [TestMethod] - public async Task Cancellation() - { - var queue = new AsyncQueue(); - - bool success; - using (var cancellationTokenSource = new CancellationTokenSource(TimeSpan.FromSeconds(3))) - { - success = (await queue.TryDequeueAsync(cancellationTokenSource.Token)).IsSuccess; - } - - Assert.AreEqual(false, success); - } - - [TestMethod] - public async Task Process_Async() - { - var queue = new AsyncQueue(); - - var sum = 0; - var worker = Task.Run(async () => - { - while (sum < 6) - { - sum += (await queue.TryDequeueAsync(CancellationToken.None)).Item; - } - }); - - queue.Enqueue(1); - await Task.Delay(500); - - queue.Enqueue(2); - await Task.Delay(500); - - queue.Enqueue(3); - await Task.Delay(500); - - Assert.AreEqual(6, sum); - Assert.AreEqual(TaskStatus.RanToCompletion, worker.Status); - } - - [TestMethod] - public async Task Process_Async_With_Initial_Delay() - { - var queue = new AsyncQueue(); - - var sum = 0; - var worker = Task.Run(async () => - { - while (sum < 6) - { - sum += (await queue.TryDequeueAsync(CancellationToken.None)).Item; - } - }); - - // This line is the diff to test _Process_Async_ - await Task.Delay(500); - - queue.Enqueue(1); - await Task.Delay(500); - - queue.Enqueue(2); - await Task.Delay(500); - - queue.Enqueue(3); - await Task.Delay(500); - - Assert.AreEqual(6, sum); - Assert.AreEqual(TaskStatus.RanToCompletion, worker.Status); - } - - [TestMethod] - public void Dequeue_Sync() - { - var queue = new AsyncQueue(); - queue.Enqueue("1"); - queue.Enqueue("2"); - queue.Enqueue("3"); - - Assert.AreEqual("1", queue.TryDequeue().Item); - Assert.AreEqual("2", queue.TryDequeue().Item); - Assert.AreEqual("3", queue.TryDequeue().Item); - } - - [TestMethod] - public void Clear() - { - var queue = new AsyncQueue(); - queue.Enqueue("1"); - queue.Enqueue("2"); - queue.Enqueue("3"); - - queue.Clear(); - Assert.AreEqual(0, queue.Count); - - queue.Enqueue("4"); - - Assert.AreEqual(1, queue.Count); - Assert.AreEqual("4", queue.TryDequeue().Item); - } + Assert.AreEqual("1", (await queue.TryDequeueAsync(CancellationToken.None)).Item); + Assert.AreEqual("2", (await queue.TryDequeueAsync(CancellationToken.None)).Item); + Assert.AreEqual("3", (await queue.TryDequeueAsync(CancellationToken.None)).Item); } -} + + [TestMethod] + public void Count() + { + var queue = new AsyncQueue(); + + queue.Enqueue("1"); + Assert.AreEqual(1, queue.Count); + + queue.Enqueue("2"); + Assert.AreEqual(2, queue.Count); + + queue.Enqueue("3"); + Assert.AreEqual(3, queue.Count); + } + + [TestMethod] + public async Task Cancellation() + { + var queue = new AsyncQueue(); + + bool success; + using (var cancellationTokenSource = new CancellationTokenSource(TimeSpan.FromSeconds(3))) + { + success = (await queue.TryDequeueAsync(cancellationTokenSource.Token)).IsSuccess; + } + + Assert.AreEqual(false, success); + } + + [TestMethod] + public async Task Process_Async() + { + var queue = new AsyncQueue(); + + var sum = 0; + var worker = Task.Run(async () => + { + while (sum < 6) + { + sum += (await queue.TryDequeueAsync(CancellationToken.None)).Item; + } + }); + + queue.Enqueue(1); + await Task.Delay(500); + + queue.Enqueue(2); + await Task.Delay(500); + + queue.Enqueue(3); + await Task.Delay(500); + + Assert.AreEqual(6, sum); + Assert.AreEqual(TaskStatus.RanToCompletion, worker.Status); + } + + [TestMethod] + public async Task Process_Async_With_Initial_Delay() + { + var queue = new AsyncQueue(); + + var sum = 0; + var worker = Task.Run(async () => + { + while (sum < 6) + { + sum += (await queue.TryDequeueAsync(CancellationToken.None)).Item; + } + }); + + // This line is the diff to test _Process_Async_ + await Task.Delay(500); + + queue.Enqueue(1); + await Task.Delay(500); + + queue.Enqueue(2); + await Task.Delay(500); + + queue.Enqueue(3); + await Task.Delay(500); + + Assert.AreEqual(6, sum); + Assert.AreEqual(TaskStatus.RanToCompletion, worker.Status); + } + + [TestMethod] + public void Dequeue_Sync() + { + var queue = new AsyncQueue(); + queue.Enqueue("1"); + queue.Enqueue("2"); + queue.Enqueue("3"); + + Assert.AreEqual("1", queue.TryDequeue().Item); + Assert.AreEqual("2", queue.TryDequeue().Item); + Assert.AreEqual("3", queue.TryDequeue().Item); + } + + [TestMethod] + public void Clear() + { + var queue = new AsyncQueue(); + queue.Enqueue("1"); + queue.Enqueue("2"); + queue.Enqueue("3"); + + queue.Clear(); + Assert.AreEqual(0, queue.Count); + + queue.Enqueue("4"); + + Assert.AreEqual(1, queue.Count); + Assert.AreEqual("4", queue.TryDequeue().Item); + } +} \ No newline at end of file diff --git a/Source/MQTTnet.Tests/Internal/AsyncSignal_Tests.cs b/Source/MQTTnet.Tests/Internal/AsyncSignal_Tests.cs index 9adac5dc..4b79934b 100644 --- a/Source/MQTTnet.Tests/Internal/AsyncSignal_Tests.cs +++ b/Source/MQTTnet.Tests/Internal/AsyncSignal_Tests.cs @@ -9,120 +9,114 @@ using System.Threading.Tasks; using Microsoft.VisualStudio.TestTools.UnitTesting; using MQTTnet.Internal; -namespace MQTTnet.Tests.Internal +namespace MQTTnet.Tests.Internal; + +// ReSharper disable InconsistentNaming +[TestClass] +public sealed class AsyncSignal_Tests { - [TestClass] - public sealed class AsyncSignal_Tests + [TestMethod] + [ExpectedException(typeof(TaskCanceledException))] + public async Task Cancel_If_No_Signal() { - [TestMethod] - [ExpectedException(typeof(TaskCanceledException))] - public async Task Cancel_If_No_Signal() - { - var asyncSignal = new AsyncSignal(); + var asyncSignal = new AsyncSignal(); - using (var timeout = new CancellationTokenSource(TimeSpan.FromSeconds(2))) + using var timeout = new CancellationTokenSource(TimeSpan.FromSeconds(2)); + await asyncSignal.WaitAsync(timeout.Token); + + Assert.Fail("There is no signal. So we must fail here!"); + } + + [TestMethod] + [ExpectedException(typeof(ObjectDisposedException))] + public async Task Dispose_Properly() + { + var asyncSignal = new AsyncSignal(); + + // The timeout will not be reached but another task will kill the async signal. + using var timeout = new CancellationTokenSource(TimeSpan.FromSeconds(999)); + _ = Task.Run( + async () => { - await asyncSignal.WaitAsync(timeout.Token); + await Task.Delay(2000, CancellationToken.None); + asyncSignal.Dispose(); + }, + CancellationToken.None); - Assert.Fail("There is no signal. So we must fail here!"); - } - } + await asyncSignal.WaitAsync(timeout.Token); - [TestMethod] - [ExpectedException(typeof(ObjectDisposedException))] - public async Task Dispose_Properly() + Assert.Fail("There is no signal. So we must fail here!"); + } + + [TestMethod] + public async Task Reset_Signal() + { + var asyncSignal = new AsyncSignal(); + + // WaitAsync should fail because no signal is available. + for (var i = 0; i < 10; i++) { - var asyncSignal = new AsyncSignal(); - - // The timeout will not be reached but another task will kill the async signal. - using (var timeout = new CancellationTokenSource(TimeSpan.FromSeconds(999))) + try { - _ = Task.Run( - async () => - { - await Task.Delay(2000, CancellationToken.None); - asyncSignal.Dispose(); - }, - CancellationToken.None); - - await asyncSignal.WaitAsync(timeout.Token); - - Assert.Fail("There is no signal. So we must fail here!"); - } - } - - [TestMethod] - public async Task Reset_Signal() - { - var asyncSignal = new AsyncSignal(); - - // WaitAsync should fail because no signal is available. - for (var i = 0; i < 10; i++) - { - try + using (var timeout = new CancellationTokenSource(TimeSpan.FromMilliseconds(100))) { - using (var timeout = new CancellationTokenSource(TimeSpan.FromMilliseconds(100))) - { - await asyncSignal.WaitAsync(timeout.Token); - } - - Assert.Fail("This must fail because the signal is not yet set."); + await asyncSignal.WaitAsync(timeout.Token); } - catch (OperationCanceledException) - { - } - - asyncSignal.Set(); - - // WaitAsync should return directly because the signal is available. - await asyncSignal.WaitAsync(); + + Assert.Fail("This must fail because the signal is not yet set."); } - } - - [TestMethod] - public async Task Signal() - { - var asyncSignal = new AsyncSignal(); - - // The timeout will not be reached but another task will kill the async signal. - using (var timeout = new CancellationTokenSource(TimeSpan.FromSeconds(3))) + catch (OperationCanceledException) { - var stopwatch = Stopwatch.StartNew(); - - _ = Task.Run( - async () => - { - await Task.Delay(1000, CancellationToken.None); - asyncSignal.Set(); - }, - CancellationToken.None); - - await asyncSignal.WaitAsync(timeout.Token); - - stopwatch.Stop(); - - Assert.IsTrue(stopwatch.ElapsedMilliseconds > 900); } - } - [TestMethod] - [ExpectedException(typeof(InvalidOperationException))] - public async Task Fail_For_Two_Waiters() - { - var asyncSignal = new AsyncSignal(); + asyncSignal.Set(); - // This thread will wait properly because it is the first waiter. - _ = Task.Run( - async () => - { - await asyncSignal.WaitAsync(); - }, - CancellationToken.None); - - await Task.Delay(1000); - - // Now the current thread must fail because there is already a waiter. + // WaitAsync should return directly because the signal is available. await asyncSignal.WaitAsync(); } } + + [TestMethod] + public async Task Signal() + { + var asyncSignal = new AsyncSignal(); + + // The timeout will not be reached but another task will kill the async signal. + using var timeout = new CancellationTokenSource(TimeSpan.FromSeconds(3)); + var stopwatch = Stopwatch.StartNew(); + + _ = Task.Run( + async () => + { + await Task.Delay(1000, CancellationToken.None); + asyncSignal.Set(); + }, + CancellationToken.None); + + await asyncSignal.WaitAsync(timeout.Token); + + stopwatch.Stop(); + + Assert.IsTrue(stopwatch.ElapsedMilliseconds > 900); + } + + [TestMethod] + [ExpectedException(typeof(InvalidOperationException))] + public async Task Fail_For_Two_Waiters() + { + var asyncSignal = new AsyncSignal(); + + // This thread will wait properly because it is the first waiter. + _ = Task.Run( + async () => + { + await asyncSignal.WaitAsync().ConfigureAwait(false); + }, + CancellationToken.None); + + await Task.Delay(1000); + + // Now the current thread must fail because there is already a waiter. + await asyncSignal.WaitAsync().ConfigureAwait(false); + } } \ No newline at end of file diff --git a/Source/MQTTnet.Tests/Internal/BlockingQueue_Tests.cs b/Source/MQTTnet.Tests/Internal/BlockingQueue_Tests.cs index 49837065..3175c98e 100644 --- a/Source/MQTTnet.Tests/Internal/BlockingQueue_Tests.cs +++ b/Source/MQTTnet.Tests/Internal/BlockingQueue_Tests.cs @@ -8,122 +8,122 @@ using System.Threading.Tasks; using Microsoft.VisualStudio.TestTools.UnitTesting; using MQTTnet.Internal; -namespace MQTTnet.Tests.Internal +namespace MQTTnet.Tests.Internal; + +// ReSharper disable InconsistentNaming +[TestClass] +public class BlockingQueue_Tests { - [TestClass] - public class BlockingQueue_Tests + [TestMethod] + public void Preserve_Order() { - [TestMethod] - public void Preserve_Order() - { - var queue = new BlockingQueue(); - queue.Enqueue("a"); - queue.Enqueue("b"); - queue.Enqueue("c"); + var queue = new BlockingQueue(); + queue.Enqueue("a"); + queue.Enqueue("b"); + queue.Enqueue("c"); - Assert.AreEqual(3, queue.Count); + Assert.AreEqual(3, queue.Count); - Assert.AreEqual("a", queue.Dequeue()); - Assert.AreEqual("b", queue.Dequeue()); - Assert.AreEqual("c", queue.Dequeue()); - } + Assert.AreEqual("a", queue.Dequeue()); + Assert.AreEqual("b", queue.Dequeue()); + Assert.AreEqual("c", queue.Dequeue()); + } - [TestMethod] - public void Remove_First_Items() - { - var queue = new BlockingQueue(); - queue.Enqueue("a"); - queue.Enqueue("b"); - queue.Enqueue("c"); + [TestMethod] + public void Remove_First_Items() + { + var queue = new BlockingQueue(); + queue.Enqueue("a"); + queue.Enqueue("b"); + queue.Enqueue("c"); - Assert.AreEqual("a", queue.RemoveFirst()); - Assert.AreEqual("b", queue.RemoveFirst()); + Assert.AreEqual("a", queue.RemoveFirst()); + Assert.AreEqual("b", queue.RemoveFirst()); - Assert.AreEqual(1, queue.Count); + Assert.AreEqual(1, queue.Count); - Assert.AreEqual("c", queue.Dequeue()); - } + Assert.AreEqual("c", queue.Dequeue()); + } - [TestMethod] - public void Clear_Items() - { - var queue = new BlockingQueue(); - queue.Enqueue("a"); - queue.Enqueue("b"); - queue.Enqueue("c"); + [TestMethod] + public void Clear_Items() + { + var queue = new BlockingQueue(); + queue.Enqueue("a"); + queue.Enqueue("b"); + queue.Enqueue("c"); - Assert.AreEqual(3, queue.Count); + Assert.AreEqual(3, queue.Count); - queue.Clear(); + queue.Clear(); - Assert.AreEqual(0, queue.Count); - } + Assert.AreEqual(0, queue.Count); + } - [TestMethod] - public async Task Wait_For_Item() - { - var queue = new BlockingQueue(); + [TestMethod] + public async Task Wait_For_Item() + { + var queue = new BlockingQueue(); - string item = null; + string item = null; #pragma warning disable CS4014 // Because this call is not awaited, execution of the current method continues before the call is completed - Task.Run(() => - { - item = queue.Dequeue(); - }); + Task.Run(() => + { + item = queue.Dequeue(); + }); #pragma warning restore CS4014 // Because this call is not awaited, execution of the current method continues before the call is completed - await Task.Delay(100); + await Task.Delay(100); - Assert.AreEqual(0, queue.Count); - Assert.AreEqual(null, item); + Assert.AreEqual(0, queue.Count); + Assert.AreEqual(null, item); - queue.Enqueue("x"); + queue.Enqueue("x"); - await Task.Delay(100); + await Task.Delay(100); - Assert.AreEqual("x", item); - Assert.AreEqual(0, queue.Count); - } + Assert.AreEqual("x", item); + Assert.AreEqual(0, queue.Count); + } - [TestMethod] - public void Wait_For_Items() - { - var number = 0; + [TestMethod] + public void Wait_For_Items() + { + var number = 0; - var queue = new BlockingQueue(); + var queue = new BlockingQueue(); #pragma warning disable CS4014 // Because this call is not awaited, execution of the current method continues before the call is completed - Task.Run(async () => + Task.Run(async () => + { + while (true) { - while (true) - { - queue.Enqueue(1); - await Task.Delay(100); - } - }); -#pragma warning restore CS4014 // Because this call is not awaited, execution of the current method continues before the call is completed - - while (number < 50) - { - queue.Dequeue(); - Interlocked.Increment(ref number); + queue.Enqueue(1); + await Task.Delay(100); } - } - - [TestMethod] - [ExpectedException(typeof(OperationCanceledException))] - public void Use_Disposed_Queue() - { - var queue = new BlockingQueue(); -#pragma warning disable CS4014 // Because this call is not awaited, execution of the current method continues before the call is completed - Task.Run(() => - { - Thread.Sleep(1000); - queue.Dispose(); - }); + }); #pragma warning restore CS4014 // Because this call is not awaited, execution of the current method continues before the call is completed - queue.Dequeue(new CancellationTokenSource(TimeSpan.FromSeconds(2)).Token); + while (number < 50) + { + queue.Dequeue(); + Interlocked.Increment(ref number); } } + + [TestMethod] + [ExpectedException(typeof(OperationCanceledException))] + public void Use_Disposed_Queue() + { + var queue = new BlockingQueue(); +#pragma warning disable CS4014 // Because this call is not awaited, execution of the current method continues before the call is completed + Task.Run(() => + { + Thread.Sleep(1000); + queue.Dispose(); + }); +#pragma warning restore CS4014 // Because this call is not awaited, execution of the current method continues before the call is completed + + queue.Dequeue(new CancellationTokenSource(TimeSpan.FromSeconds(2)).Token); + } } \ No newline at end of file diff --git a/Source/MQTTnet.Tests/Internal/CrossPlatformSocket_Tests.cs b/Source/MQTTnet.Tests/Internal/CrossPlatformSocket_Tests.cs index 7476ffea..56e5fb49 100644 --- a/Source/MQTTnet.Tests/Internal/CrossPlatformSocket_Tests.cs +++ b/Source/MQTTnet.Tests/Internal/CrossPlatformSocket_Tests.cs @@ -11,71 +11,71 @@ using System.Threading.Tasks; using Microsoft.VisualStudio.TestTools.UnitTesting; using MQTTnet.Implementations; -namespace MQTTnet.Tests.Internal +namespace MQTTnet.Tests.Internal; + +// ReSharper disable InconsistentNaming +[TestClass] +public class CrossPlatformSocket_Tests { - [TestClass] - public class CrossPlatformSocket_Tests + [TestMethod] + public async Task Connect_Send_Receive() { - [TestMethod] - public async Task Connect_Send_Receive() - { - var crossPlatformSocket = new CrossPlatformSocket(ProtocolType.Tcp); - await crossPlatformSocket.ConnectAsync(new DnsEndPoint("www.google.de", 80), CancellationToken.None); + var crossPlatformSocket = new CrossPlatformSocket(ProtocolType.Tcp); + await crossPlatformSocket.ConnectAsync(new DnsEndPoint("www.github.com", 80), CancellationToken.None); - var requestBuffer = Encoding.UTF8.GetBytes("GET / HTTP/1.1\r\nHost: www.google.de\r\n\r\n"); - await crossPlatformSocket.SendAsync(new ArraySegment(requestBuffer), System.Net.Sockets.SocketFlags.None); + var requestBuffer = "GET / HTTP/1.1\r\nHost: www.github.com\r\n\r\n"u8.ToArray(); + await crossPlatformSocket.SendAsync(new ArraySegment(requestBuffer), SocketFlags.None); - var buffer = new byte[1024]; - var length = await crossPlatformSocket.ReceiveAsync(new ArraySegment(buffer), System.Net.Sockets.SocketFlags.None); - crossPlatformSocket.Dispose(); + var buffer = new byte[1024]; + var length = await crossPlatformSocket.ReceiveAsync(new ArraySegment(buffer), SocketFlags.None); + crossPlatformSocket.Dispose(); - var responseText = Encoding.UTF8.GetString(buffer, 0, length); + var responseText = Encoding.UTF8.GetString(buffer, 0, length); - Assert.IsTrue(responseText.Contains("HTTP/1.1 200 OK")); - } - - [TestMethod] - [ExpectedException(typeof(OperationCanceledException))] - public async Task Try_Connect_Invalid_Host() - { - var crossPlatformSocket = new CrossPlatformSocket(ProtocolType.Tcp); - - var cancellationToken = new CancellationTokenSource(TimeSpan.FromSeconds(5)); - cancellationToken.Token.Register(() => crossPlatformSocket.Dispose()); - - await crossPlatformSocket.ConnectAsync(new DnsEndPoint("www.google.de", 54321), cancellationToken.Token); - } - - //[TestMethod] - //public async Task Use_Disconnected_Socket() - //{ - // var crossPlatformSocket = new CrossPlatformSocket(); - - // await crossPlatformSocket.ConnectAsync("www.google.de", 80); - - // var requestBuffer = Encoding.UTF8.GetBytes("GET /wrong_uri HTTP/1.1\r\nConnection: close\r\n\r\n"); - // await crossPlatformSocket.SendAsync(new ArraySegment(requestBuffer), System.Net.Sockets.SocketFlags.None); - - // var buffer = new byte[64000]; - // var length = await crossPlatformSocket.ReceiveAsync(new ArraySegment(buffer), System.Net.Sockets.SocketFlags.None); - - // await Task.Delay(500); - - // await crossPlatformSocket.SendAsync(new ArraySegment(requestBuffer), System.Net.Sockets.SocketFlags.None); - //} - - [TestMethod] - public void Set_Options() - { - var crossPlatformSocket = new CrossPlatformSocket(ProtocolType.Tcp); - - Assert.IsFalse(crossPlatformSocket.ReuseAddress); - crossPlatformSocket.ReuseAddress = true; - Assert.IsTrue(crossPlatformSocket.ReuseAddress); - - Assert.IsFalse(crossPlatformSocket.NoDelay); - crossPlatformSocket.NoDelay = true; - Assert.IsTrue(crossPlatformSocket.NoDelay); - } + Assert.IsTrue(responseText.Contains("HTTP/1.1")); } -} + + [TestMethod] + [ExpectedException(typeof(OperationCanceledException))] + public async Task Try_Connect_Invalid_Host() + { + var crossPlatformSocket = new CrossPlatformSocket(ProtocolType.Tcp); + + var cancellationToken = new CancellationTokenSource(TimeSpan.FromSeconds(5)); + cancellationToken.Token.Register(() => crossPlatformSocket.Dispose()); + + await crossPlatformSocket.ConnectAsync(new DnsEndPoint("www.github.com", 54321), cancellationToken.Token).ConfigureAwait(false); + } + + // [TestMethod] + // public async Task Use_Disconnected_Socket() + // { + // var crossPlatformSocket = new CrossPlatformSocket(ProtocolType.Tcp); + // + // await crossPlatformSocket.ConnectAsync("www.github.com", 80); + // + // var requestBuffer = Encoding.UTF8.GetBytes("GET /wrong_uri HTTP/1.1\r\nConnection: close\r\n\r\n"); + // await crossPlatformSocket.SendAsync(new ArraySegment(requestBuffer), System.Net.Sockets.SocketFlags.None); + // + // var buffer = new byte[64000]; + // var length = await crossPlatformSocket.ReceiveAsync(new ArraySegment(buffer), System.Net.Sockets.SocketFlags.None); + // + // await Task.Delay(500); + // + // await crossPlatformSocket.SendAsync(new ArraySegment(requestBuffer), System.Net.Sockets.SocketFlags.None); + // } + + [TestMethod] + public void Set_Options() + { + var crossPlatformSocket = new CrossPlatformSocket(ProtocolType.Tcp); + + Assert.IsFalse(crossPlatformSocket.ReuseAddress); + crossPlatformSocket.ReuseAddress = true; + Assert.IsTrue(crossPlatformSocket.ReuseAddress); + + Assert.IsFalse(crossPlatformSocket.NoDelay); + crossPlatformSocket.NoDelay = true; + Assert.IsTrue(crossPlatformSocket.NoDelay); + } +} \ No newline at end of file diff --git a/Source/MQTTnet.Tests/Internal/MqttPacketBusItem_Tests.cs b/Source/MQTTnet.Tests/Internal/MqttPacketBusItem_Tests.cs index 912d38a0..11bf4594 100644 --- a/Source/MQTTnet.Tests/Internal/MqttPacketBusItem_Tests.cs +++ b/Source/MQTTnet.Tests/Internal/MqttPacketBusItem_Tests.cs @@ -7,37 +7,37 @@ using Microsoft.VisualStudio.TestTools.UnitTesting; using MQTTnet.Internal; using MQTTnet.Packets; -namespace MQTTnet.Tests.Internal +namespace MQTTnet.Tests.Internal; + +// ReSharper disable InconsistentNaming +[TestClass] +public sealed class MqttPacketBusItem_Tests { - [TestClass] - public sealed class MqttPacketBusItem_Tests + [TestMethod] + public void Fire_Completed_Event() { - [TestMethod] - public void Fire_Completed_Event() + var eventFired = false; + + var item = new MqttPacketBusItem(new MqttPublishPacket()); + item.Completed += (_, _) => { - var eventFired = false; + eventFired = true; + }; - var item = new MqttPacketBusItem(new MqttPublishPacket()); - item.Completed += (_, __) => - { - eventFired = true; - }; + item.Complete(); - item.Complete(); + Assert.IsTrue(eventFired); + } - Assert.IsTrue(eventFired); - } + [TestMethod] + [ExpectedException(typeof(TaskCanceledException))] + public async Task Wait_Packet_Bus_Item_After_Already_Canceled() + { + var item = new MqttPacketBusItem(new MqttPublishPacket()); - [TestMethod] - [ExpectedException(typeof(TaskCanceledException))] - public async Task Wait_Packet_Bus_Item_After_Already_Canceled() - { - var item = new MqttPacketBusItem(new MqttPublishPacket()); + // Finish the item before the actual + item.Cancel(); - // Finish the item before the actual - item.Cancel(); - - await item.WaitAsync(); - } + await item.WaitAsync().ConfigureAwait(false); } } \ No newline at end of file diff --git a/Source/MQTTnet.Tests/Internal/MqttPacketBus_Tests.cs b/Source/MQTTnet.Tests/Internal/MqttPacketBus_Tests.cs index 62f547ef..020b9d14 100644 --- a/Source/MQTTnet.Tests/Internal/MqttPacketBus_Tests.cs +++ b/Source/MQTTnet.Tests/Internal/MqttPacketBus_Tests.cs @@ -3,149 +3,146 @@ // See the LICENSE file in the project root for more information. using System; +using System.Diagnostics.CodeAnalysis; using System.Threading; using System.Threading.Tasks; using Microsoft.VisualStudio.TestTools.UnitTesting; using MQTTnet.Internal; using MQTTnet.Packets; -namespace MQTTnet.Tests.Internal +namespace MQTTnet.Tests.Internal; + +[SuppressMessage("ReSharper", "InconsistentNaming")] +[TestClass] +public sealed class MqttPacketBus_Tests { - [TestClass] - public sealed class MqttPacketBus_Tests - { - [TestMethod] - public void Alternate_Priorities() + [TestMethod] + public void Alternate_Priorities() + { + var bus = new MqttPacketBus(); + + bus.EnqueueItem(new MqttPacketBusItem(new MqttPublishPacket()), MqttPacketBusPartition.Data); + bus.EnqueueItem(new MqttPacketBusItem(new MqttPublishPacket()), MqttPacketBusPartition.Data); + bus.EnqueueItem(new MqttPacketBusItem(new MqttPublishPacket()), MqttPacketBusPartition.Data); + + bus.EnqueueItem(new MqttPacketBusItem(new MqttSubAckPacket()), MqttPacketBusPartition.Control); + bus.EnqueueItem(new MqttPacketBusItem(new MqttSubAckPacket()), MqttPacketBusPartition.Control); + bus.EnqueueItem(new MqttPacketBusItem(new MqttSubAckPacket()), MqttPacketBusPartition.Control); + + bus.EnqueueItem(new MqttPacketBusItem(new MqttPingRespPacket()), MqttPacketBusPartition.Health); + bus.EnqueueItem(new MqttPacketBusItem(new MqttPingRespPacket()), MqttPacketBusPartition.Health); + bus.EnqueueItem(new MqttPacketBusItem(new MqttPingRespPacket()), MqttPacketBusPartition.Health); + + Assert.AreEqual(9, bus.TotalItemsCount); + + Assert.IsInstanceOfType(bus.DequeueItemAsync(CancellationToken.None).Result.Packet, typeof(MqttPublishPacket)); + Assert.IsInstanceOfType(bus.DequeueItemAsync(CancellationToken.None).Result.Packet, typeof(MqttSubAckPacket)); + Assert.IsInstanceOfType(bus.DequeueItemAsync(CancellationToken.None).Result.Packet, typeof(MqttPingRespPacket)); + Assert.IsInstanceOfType(bus.DequeueItemAsync(CancellationToken.None).Result.Packet, typeof(MqttPublishPacket)); + Assert.IsInstanceOfType(bus.DequeueItemAsync(CancellationToken.None).Result.Packet, typeof(MqttSubAckPacket)); + Assert.IsInstanceOfType(bus.DequeueItemAsync(CancellationToken.None).Result.Packet, typeof(MqttPingRespPacket)); + Assert.IsInstanceOfType(bus.DequeueItemAsync(CancellationToken.None).Result.Packet, typeof(MqttPublishPacket)); + Assert.IsInstanceOfType(bus.DequeueItemAsync(CancellationToken.None).Result.Packet, typeof(MqttSubAckPacket)); + Assert.IsInstanceOfType(bus.DequeueItemAsync(CancellationToken.None).Result.Packet, typeof(MqttPingRespPacket)); + + Assert.AreEqual(0, bus.TotalItemsCount); + } + + [TestMethod] + public void Await_Single_Packet() + { + var bus = new MqttPacketBus(); + + var delivered = false; + + var item1 = new MqttPacketBusItem(new MqttPublishPacket()); + var item2 = new MqttPacketBusItem(new MqttPublishPacket()); + + var item3 = new MqttPacketBusItem(new MqttPublishPacket()); + item3.Completed += (_, _) => { - var bus = new MqttPacketBus(); + delivered = true; + }; - bus.EnqueueItem(new MqttPacketBusItem(new MqttPublishPacket()), MqttPacketBusPartition.Data); - bus.EnqueueItem(new MqttPacketBusItem(new MqttPublishPacket()), MqttPacketBusPartition.Data); - bus.EnqueueItem(new MqttPacketBusItem(new MqttPublishPacket()), MqttPacketBusPartition.Data); + bus.EnqueueItem(item1, MqttPacketBusPartition.Data); + bus.EnqueueItem(item2, MqttPacketBusPartition.Data); + bus.EnqueueItem(item3, MqttPacketBusPartition.Data); - bus.EnqueueItem(new MqttPacketBusItem(new MqttSubAckPacket()), MqttPacketBusPartition.Control); - bus.EnqueueItem(new MqttPacketBusItem(new MqttSubAckPacket()), MqttPacketBusPartition.Control); - bus.EnqueueItem(new MqttPacketBusItem(new MqttSubAckPacket()), MqttPacketBusPartition.Control); + Assert.IsFalse(delivered); - bus.EnqueueItem(new MqttPacketBusItem(new MqttPingRespPacket()), MqttPacketBusPartition.Health); - bus.EnqueueItem(new MqttPacketBusItem(new MqttPingRespPacket()), MqttPacketBusPartition.Health); - bus.EnqueueItem(new MqttPacketBusItem(new MqttPingRespPacket()), MqttPacketBusPartition.Health); + bus.DequeueItemAsync(CancellationToken.None).Result.Complete(); - Assert.AreEqual(9, bus.TotalItemsCount); + Assert.IsFalse(delivered); - Assert.IsInstanceOfType(bus.DequeueItemAsync(CancellationToken.None).Result.Packet, typeof(MqttPublishPacket)); - Assert.IsInstanceOfType(bus.DequeueItemAsync(CancellationToken.None).Result.Packet, typeof(MqttSubAckPacket)); - Assert.IsInstanceOfType(bus.DequeueItemAsync(CancellationToken.None).Result.Packet, typeof(MqttPingRespPacket)); - Assert.IsInstanceOfType(bus.DequeueItemAsync(CancellationToken.None).Result.Packet, typeof(MqttPublishPacket)); - Assert.IsInstanceOfType(bus.DequeueItemAsync(CancellationToken.None).Result.Packet, typeof(MqttSubAckPacket)); - Assert.IsInstanceOfType(bus.DequeueItemAsync(CancellationToken.None).Result.Packet, typeof(MqttPingRespPacket)); - Assert.IsInstanceOfType(bus.DequeueItemAsync(CancellationToken.None).Result.Packet, typeof(MqttPublishPacket)); - Assert.IsInstanceOfType(bus.DequeueItemAsync(CancellationToken.None).Result.Packet, typeof(MqttSubAckPacket)); - Assert.IsInstanceOfType(bus.DequeueItemAsync(CancellationToken.None).Result.Packet, typeof(MqttPingRespPacket)); + bus.DequeueItemAsync(CancellationToken.None).Result.Complete(); - Assert.AreEqual(0, bus.TotalItemsCount); - } + Assert.IsFalse(delivered); - [TestMethod] - public void Await_Single_Packet() - { - var bus = new MqttPacketBus(); + bus.DequeueItemAsync(CancellationToken.None).Result.Complete(); - var delivered = false; + // The third packet has the event attached. + Assert.IsTrue(delivered); + } - var item1 = new MqttPacketBusItem(new MqttPublishPacket()); - var item2 = new MqttPacketBusItem(new MqttPublishPacket()); + [TestMethod] + public void Export_Packets_Without_Dequeue() + { + var bus = new MqttPacketBus(); - var item3 = new MqttPacketBusItem(new MqttPublishPacket()); - item3.Completed += (_, __) => + bus.EnqueueItem(new MqttPacketBusItem(new MqttPublishPacket()), MqttPacketBusPartition.Data); + bus.EnqueueItem(new MqttPacketBusItem(new MqttPublishPacket()), MqttPacketBusPartition.Data); + bus.EnqueueItem(new MqttPacketBusItem(new MqttPublishPacket()), MqttPacketBusPartition.Data); + + Assert.AreEqual(3, bus.TotalItemsCount); + + var exportedPackets = bus.ExportPackets(MqttPacketBusPartition.Control); + Assert.AreEqual(0, exportedPackets.Count); + + exportedPackets = bus.ExportPackets(MqttPacketBusPartition.Health); + Assert.AreEqual(0, exportedPackets.Count); + + exportedPackets = bus.ExportPackets(MqttPacketBusPartition.Data); + Assert.AreEqual(3, exportedPackets.Count); + + Assert.AreEqual(3, bus.TotalItemsCount); + } + + [TestMethod] + public async Task Fill_From_Different_Task() + { + const int messageCount = 500; + + var delayRandom = new Random(); + + var bus = new MqttPacketBus(); + + _ = Task.Run( + () => { - delivered = true; - }; - - bus.EnqueueItem(item1, MqttPacketBusPartition.Data); - bus.EnqueueItem(item2, MqttPacketBusPartition.Data); - bus.EnqueueItem(item3, MqttPacketBusPartition.Data); - - Assert.IsFalse(delivered); - - bus.DequeueItemAsync(CancellationToken.None).Result.Complete(); - - Assert.IsFalse(delivered); - - bus.DequeueItemAsync(CancellationToken.None).Result.Complete(); - - Assert.IsFalse(delivered); - - bus.DequeueItemAsync(CancellationToken.None).Result.Complete(); - - // The third packet has the event attached. - Assert.IsTrue(delivered); - } - - [TestMethod] - public void Export_Packets_Without_Dequeue() - { - var bus = new MqttPacketBus(); - - bus.EnqueueItem(new MqttPacketBusItem(new MqttPublishPacket()), MqttPacketBusPartition.Data); - bus.EnqueueItem(new MqttPacketBusItem(new MqttPublishPacket()), MqttPacketBusPartition.Data); - bus.EnqueueItem(new MqttPacketBusItem(new MqttPublishPacket()), MqttPacketBusPartition.Data); - - Assert.AreEqual(3, bus.TotalItemsCount); - - var exportedPackets = bus.ExportPackets(MqttPacketBusPartition.Control); - Assert.AreEqual(0, exportedPackets.Count); - - exportedPackets = bus.ExportPackets(MqttPacketBusPartition.Health); - Assert.AreEqual(0, exportedPackets.Count); - - exportedPackets = bus.ExportPackets(MqttPacketBusPartition.Data); - Assert.AreEqual(3, exportedPackets.Count); - - Assert.AreEqual(3, bus.TotalItemsCount); - } - - [TestMethod] - public async Task Fill_From_Different_Task() - { - const int MessageCount = 500; - - var delayRandom = new Random(); - - var bus = new MqttPacketBus(); - - _ = Task.Run( - () => + for (var i = 0; i < messageCount; i++) { - for (var i = 0; i < MessageCount; i++) - { - bus.EnqueueItem(new MqttPacketBusItem(MqttPingReqPacket.Instance), MqttPacketBusPartition.Health); + bus.EnqueueItem(new MqttPacketBusItem(MqttPingReqPacket.Instance), MqttPacketBusPartition.Health); - Thread.Sleep(delayRandom.Next(0, 10)); - } - }); - - for (var i = 0; i < MessageCount; i++) - { - using (var timeout = new CancellationTokenSource(TimeSpan.FromSeconds(10))) - { - await bus.DequeueItemAsync(timeout.Token); + Thread.Sleep(delayRandom.Next(0, 10)); } - } + }); - Assert.AreEqual(0, bus.TotalItemsCount); - } - - [TestMethod] - [ExpectedException(typeof(TaskCanceledException))] - public async Task Wait_With_Empty_Bus() + for (var i = 0; i < messageCount; i++) { - var bus = new MqttPacketBus(); - - using (var timeout = new CancellationTokenSource(TimeSpan.FromSeconds(1))) - { - await bus.DequeueItemAsync(timeout.Token); - } + using var timeout = new CancellationTokenSource(TimeSpan.FromSeconds(10)); + await bus.DequeueItemAsync(timeout.Token); } + + Assert.AreEqual(0, bus.TotalItemsCount); + } + + [TestMethod] + [ExpectedException(typeof(TaskCanceledException))] + public async Task Wait_With_Empty_Bus() + { + var bus = new MqttPacketBus(); + + using var timeout = new CancellationTokenSource(TimeSpan.FromSeconds(1)); + await bus.DequeueItemAsync(timeout.Token); } } \ No newline at end of file diff --git a/Source/MQTTnet.Tests/MQTTnet.Tests.csproj b/Source/MQTTnet.Tests/MQTTnet.Tests.csproj index 2641e015..2b4e6acd 100644 --- a/Source/MQTTnet.Tests/MQTTnet.Tests.csproj +++ b/Source/MQTTnet.Tests/MQTTnet.Tests.csproj @@ -1,7 +1,7 @@ - + Exe net8.0 false @@ -18,9 +18,9 @@ - - - + + + diff --git a/Source/MQTTnet.Tests/MQTTv5/Client_Tests.cs b/Source/MQTTnet.Tests/MQTTv5/Client_Tests.cs index 7b7570a9..e57e0a86 100644 --- a/Source/MQTTnet.Tests/MQTTv5/Client_Tests.cs +++ b/Source/MQTTnet.Tests/MQTTv5/Client_Tests.cs @@ -3,7 +3,7 @@ // See the LICENSE file in the project root for more information. using System.Buffers; -using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; using System.Linq; using System.Threading.Tasks; using Microsoft.IO; @@ -13,370 +13,337 @@ using MQTTnet.Internal; using MQTTnet.Packets; using MQTTnet.Protocol; -namespace MQTTnet.Tests.MQTTv5 +namespace MQTTnet.Tests.MQTTv5; + +[SuppressMessage("ReSharper", "InconsistentNaming")] +[TestClass] +public sealed class Client_Tests : BaseTestClass { - [TestClass] - public sealed class Client_Tests : BaseTestClass + [TestMethod] + public async Task Connect() { - [TestMethod] - public async Task Connect_With_New_Mqtt_Features() - { - using (var testEnvironment = CreateTestEnvironment()) - { - await testEnvironment.StartServer(); - - // This test can be also executed against "broker.hivemq.com" to validate package format. - var client = await testEnvironment.ConnectClient( - new MqttClientOptionsBuilder() - //.WithTcpServer("broker.hivemq.com") - .WithTcpServer("127.0.0.1", testEnvironment.ServerPort) - .WithProtocolVersion(MqttProtocolVersion.V500) - .WithTopicAliasMaximum(20) - .WithReceiveMaximum(20) - .WithWillTopic("abc") - .WithWillDelayInterval(20) - .Build()); - - MqttApplicationMessage receivedMessage = null; - - await client.SubscribeAsync("a"); - client.ApplicationMessageReceivedAsync += e => - { - receivedMessage = e.ApplicationMessage; - return CompletedTask.Instance; - }; - - await client.PublishAsync(new MqttApplicationMessageBuilder() - .WithTopic("a") - .WithPayload("x") - .WithUserProperty("a", "1") - .WithUserProperty("b", "2") - .WithPayloadFormatIndicator(MqttPayloadFormatIndicator.CharacterData) - .WithQualityOfServiceLevel(MqttQualityOfServiceLevel.AtLeastOnce) - .Build()); - - await Task.Delay(500); - - Assert.IsNotNull(receivedMessage); - - Assert.AreEqual(2, receivedMessage.UserProperties.Count); - } - } - - [TestMethod] - public async Task Connect() - { - using (var testEnvironment = CreateTestEnvironment()) - { - await testEnvironment.StartServer(); - await testEnvironment.ConnectClient(o => o.WithProtocolVersion(MqttProtocolVersion.V500).Build()); - } - } - - [TestMethod] - public async Task Connect_And_Disconnect() - { - using (var testEnvironment = CreateTestEnvironment()) - { - await testEnvironment.StartServer(); - - var client = await testEnvironment.ConnectClient(o => o.WithProtocolVersion(MqttProtocolVersion.V500)); - await client.DisconnectAsync(); - } - } - - [TestMethod] - public async Task Subscribe() - { - using (var testEnvironment = CreateTestEnvironment()) - { - await testEnvironment.StartServer(); - - var client = - await testEnvironment.ConnectClient(o => o.WithProtocolVersion(MqttProtocolVersion.V500)); - - var result = await client.SubscribeAsync(new MqttClientSubscribeOptions() - { - SubscriptionIdentifier = 1, - TopicFilters = new List - { - new MqttTopicFilter {Topic = "a", QualityOfServiceLevel = MqttQualityOfServiceLevel.AtLeastOnce} - } - }); - - await client.DisconnectAsync(); - - Assert.AreEqual(1, result.Items.Count); - Assert.AreEqual(MqttClientSubscribeResultCode.GrantedQoS1, result.Items.First().ResultCode); - } - } - - [TestMethod] - public async Task Unsubscribe() - { - using (var testEnvironment = CreateTestEnvironment()) - { - await testEnvironment.StartServer(); - - var client = await testEnvironment.ConnectClient(o => o.WithProtocolVersion(MqttProtocolVersion.V500)); - await client.SubscribeAsync("a"); - - var result = await client.UnsubscribeAsync("a"); - await client.DisconnectAsync(); - - Assert.AreEqual(1, result.Items.Count); - Assert.AreEqual(MqttClientUnsubscribeResultCode.Success, result.Items.First().ResultCode); - } - } - - [TestMethod] - public async Task Publish_QoS_0_LargeBuffer() - { - using var recyclableMemoryStream = GetLargePayload(); - using (var testEnvironment = CreateTestEnvironment()) - { - await testEnvironment.StartServer(); - - var client = await testEnvironment.ConnectClient(o => o.WithProtocolVersion(MqttProtocolVersion.V500)); - var result = await client.PublishSequenceAsync("a", recyclableMemoryStream.GetReadOnlySequence()); - await client.DisconnectAsync(); - - Assert.AreEqual(MqttClientPublishReasonCode.Success, result.ReasonCode); - } - } - - [TestMethod] - public async Task Publish_QoS_1_LargeBuffer() - { - using var recyclableMemoryStream = GetLargePayload(); - using (var testEnvironment = CreateTestEnvironment()) - { - await testEnvironment.StartServer(); - - var client = await testEnvironment.ConnectClient(o => o.WithProtocolVersion(MqttProtocolVersion.V500)); - var result = await client.PublishSequenceAsync("a", recyclableMemoryStream.GetReadOnlySequence(), MqttQualityOfServiceLevel.AtLeastOnce); - await client.DisconnectAsync(); - - Assert.AreEqual(MqttClientPublishReasonCode.NoMatchingSubscribers, result.ReasonCode); - } - } - - [TestMethod] - public async Task Publish_QoS_2_LargeBuffer() - { - using var recyclableMemoryStream = GetLargePayload(); - using (var testEnvironment = CreateTestEnvironment()) - { - await testEnvironment.StartServer(); - - var client = await testEnvironment.ConnectClient(o => o.WithProtocolVersion(MqttProtocolVersion.V500)); - var result = await client.PublishSequenceAsync("a", recyclableMemoryStream.GetReadOnlySequence(), MqttQualityOfServiceLevel.ExactlyOnce); - await client.DisconnectAsync(); - - Assert.AreEqual(MqttClientPublishReasonCode.NoMatchingSubscribers, result.ReasonCode); - } - } - - [TestMethod] - public async Task Publish_QoS_0() - { - using (var testEnvironment = CreateTestEnvironment()) - { - await testEnvironment.StartServer(); - - var client = await testEnvironment.ConnectClient(o => o.WithProtocolVersion(MqttProtocolVersion.V500)); - var result = await client.PublishStringAsync("a", "b"); - await client.DisconnectAsync(); - - Assert.AreEqual(MqttClientPublishReasonCode.Success, result.ReasonCode); - } - } - - [TestMethod] - public async Task Publish_QoS_1() - { - using (var testEnvironment = CreateTestEnvironment()) - { - await testEnvironment.StartServer(); - - var client = await testEnvironment.ConnectClient(o => o.WithProtocolVersion(MqttProtocolVersion.V500)); - var result = await client.PublishStringAsync("a", "b", MqttQualityOfServiceLevel.AtLeastOnce); - await client.DisconnectAsync(); - - Assert.AreEqual(MqttClientPublishReasonCode.NoMatchingSubscribers, result.ReasonCode); - } - } - - [TestMethod] - public async Task Publish_QoS_2() - { - using (var testEnvironment = CreateTestEnvironment()) - { - await testEnvironment.StartServer(); - - var client = await testEnvironment.ConnectClient(o => o.WithProtocolVersion(MqttProtocolVersion.V500)); - var result = await client.PublishStringAsync("a", "b", MqttQualityOfServiceLevel.ExactlyOnce); - await client.DisconnectAsync(); - - Assert.AreEqual(MqttClientPublishReasonCode.NoMatchingSubscribers, result.ReasonCode); - } - } - - [TestMethod] - public async Task Publish_With_RecyclableMemoryStream() - { - var memoryManager = new RecyclableMemoryStreamManager(options: new RecyclableMemoryStreamManager.Options { BlockSize = 4096 }); - using (var testEnvironment = CreateTestEnvironment()) - { - await testEnvironment.StartServer(); - - var client = await testEnvironment.ConnectClient(o => o.WithProtocolVersion(MqttProtocolVersion.V500)); - - const int payloadSize = 100000; - using var memoryStream = memoryManager.GetStream(); - - byte testValue = 0; - while (memoryStream.Length < payloadSize) - { - memoryStream.WriteByte(testValue++); - } - - var applicationMessage = new MqttApplicationMessageBuilder() - .WithTopic("Hello") - .WithPayload(memoryStream.GetReadOnlySequence()) - .WithQualityOfServiceLevel(MqttQualityOfServiceLevel.AtMostOnce) - .WithUserProperty("x", "1") - .WithUserProperty("y", "2") - .WithResponseTopic("response") - .WithContentType("text") - .WithMessageExpiryInterval(50) - .WithCorrelationData(new byte[12]) - .WithTopicAlias(2) - .Build(); - - var result = await client.PublishAsync(applicationMessage); - await client.DisconnectAsync(); - - Assert.AreEqual(MqttClientPublishReasonCode.Success, result.ReasonCode); - } - } - - [TestMethod] - public async Task Publish_With_Properties() - { - using (var testEnvironment = CreateTestEnvironment()) - { - await testEnvironment.StartServer(); - - var client = await testEnvironment.ConnectClient(o => o.WithProtocolVersion(MqttProtocolVersion.V500)); - - var applicationMessage = new MqttApplicationMessageBuilder() - .WithTopic("Hello") - .WithPayload("World") - .WithQualityOfServiceLevel(MqttQualityOfServiceLevel.AtMostOnce) - .WithUserProperty("x", "1") - .WithUserProperty("y", "2") - .WithResponseTopic("response") - .WithContentType("text") - .WithMessageExpiryInterval(50) - .WithCorrelationData(new byte[12]) - .WithTopicAlias(2) - .Build(); - - var result = await client.PublishAsync(applicationMessage); - await client.DisconnectAsync(); - - Assert.AreEqual(MqttClientPublishReasonCode.Success, result.ReasonCode); - } - } - - [TestMethod] - public async Task Subscribe_And_Publish() - { - using (var testEnvironment = CreateTestEnvironment()) - { - await testEnvironment.StartServer(); - - var client1 = await testEnvironment.ConnectClient(o => o.WithProtocolVersion(MqttProtocolVersion.V500).WithClientId("client1")); - - var testMessageHandler = testEnvironment.CreateApplicationMessageHandler(client1); - - await client1.SubscribeAsync("a"); - - var client2 = await testEnvironment.ConnectClient(o => o.WithProtocolVersion(MqttProtocolVersion.V500).WithClientId("client2")); - await client2.PublishStringAsync("a", "b"); - - await Task.Delay(500); - - await client2.DisconnectAsync(); - await client1.DisconnectAsync(); - - Assert.AreEqual(1, testMessageHandler.ReceivedEventArgs.Count); - Assert.AreEqual("Subscribe_And_Publish_client1", testMessageHandler.ReceivedEventArgs[0].ClientId); - Assert.AreEqual("a", testMessageHandler.ReceivedEventArgs[0].ApplicationMessage.Topic); - Assert.AreEqual("b", testMessageHandler.ReceivedEventArgs[0].ApplicationMessage.ConvertPayloadToString()); - } - } - - [TestMethod] - public async Task Publish_And_Receive_New_Properties() - { - using (var testEnvironment = CreateTestEnvironment()) - { - await testEnvironment.StartServer(); - - var receiver = await testEnvironment.ConnectClient(new MqttClientOptionsBuilder().WithProtocolVersion(MqttProtocolVersion.V500)); - await receiver.SubscribeAsync("#"); - - MqttApplicationMessage receivedMessage = null; - receiver.ApplicationMessageReceivedAsync += e => - { - receivedMessage = e.ApplicationMessage; - return CompletedTask.Instance; - }; - - var sender = await testEnvironment.ConnectClient(new MqttClientOptionsBuilder().WithProtocolVersion(MqttProtocolVersion.V500)); - - var applicationMessage = new MqttApplicationMessageBuilder() - .WithTopic("Hello") - .WithPayload("World") - .WithQualityOfServiceLevel(MqttQualityOfServiceLevel.AtMostOnce) - .WithUserProperty("x", "1") - .WithUserProperty("y", "2") - .WithResponseTopic("response") - .WithContentType("text") - .WithMessageExpiryInterval(50) - .WithCorrelationData(new byte[12]) - .WithTopicAlias(2) - .Build(); - - await sender.PublishAsync(applicationMessage); - - await Task.Delay(500); - - Assert.IsNotNull(receivedMessage); - Assert.AreEqual(applicationMessage.Topic, receivedMessage.Topic); - Assert.AreEqual(applicationMessage.TopicAlias, receivedMessage.TopicAlias); - Assert.AreEqual(applicationMessage.ContentType, receivedMessage.ContentType); - Assert.AreEqual(applicationMessage.ResponseTopic, receivedMessage.ResponseTopic); - Assert.AreEqual(applicationMessage.MessageExpiryInterval, receivedMessage.MessageExpiryInterval); - CollectionAssert.AreEqual(applicationMessage.CorrelationData, receivedMessage.CorrelationData); - CollectionAssert.AreEqual(applicationMessage.Payload.ToArray(), receivedMessage.Payload.ToArray()); - CollectionAssert.AreEqual(applicationMessage.UserProperties, receivedMessage.UserProperties); - } - } - - private RecyclableMemoryStream GetLargePayload() - { - var memoryManager = new RecyclableMemoryStreamManager(); - var memoryStream = memoryManager.GetStream(); - for (var i = 0; i < 100000; i++) - { - memoryStream.WriteByte((byte)i); - } - - memoryStream.Position = 0; - return memoryStream; - } + using var testEnvironment = CreateTestEnvironment(); + await testEnvironment.StartServer(); + await testEnvironment.ConnectClient(o => o.WithProtocolVersion(MqttProtocolVersion.V500).Build()); } -} + + [TestMethod] + public async Task Connect_And_Disconnect() + { + using var testEnvironment = CreateTestEnvironment(); + await testEnvironment.StartServer(); + + var client = await testEnvironment.ConnectClient(o => o.WithProtocolVersion(MqttProtocolVersion.V500)); + await client.DisconnectAsync(); + } + + [TestMethod] + public async Task Connect_With_New_Mqtt_Features() + { + using var testEnvironment = CreateTestEnvironment(); + await testEnvironment.StartServer(); + + // This test can be also executed against "broker.hivemq.com" to validate package format. + var client = await testEnvironment.ConnectClient( + new MqttClientOptionsBuilder() + //.WithTcpServer("broker.hivemq.com") + .WithTcpServer("127.0.0.1", testEnvironment.ServerPort) + .WithProtocolVersion(MqttProtocolVersion.V500) + .WithTopicAliasMaximum(20) + .WithReceiveMaximum(20) + .WithWillTopic("abc") + .WithWillDelayInterval(20) + .Build()); + + MqttApplicationMessage receivedMessage = null; + + await client.SubscribeAsync("a"); + client.ApplicationMessageReceivedAsync += e => + { + receivedMessage = e.ApplicationMessage; + return CompletedTask.Instance; + }; + + await client.PublishAsync( + new MqttApplicationMessageBuilder().WithTopic("a") + .WithPayload("x") + .WithUserProperty("a", "1") + .WithUserProperty("b", "2") + .WithPayloadFormatIndicator(MqttPayloadFormatIndicator.CharacterData) + .WithQualityOfServiceLevel(MqttQualityOfServiceLevel.AtLeastOnce) + .Build()); + + await Task.Delay(500); + + Assert.IsNotNull(receivedMessage); + + Assert.AreEqual(2, receivedMessage.UserProperties.Count); + } + + [TestMethod] + public async Task Publish_And_Receive_New_Properties() + { + using var testEnvironment = CreateTestEnvironment(); + await testEnvironment.StartServer(); + + var receiver = await testEnvironment.ConnectClient(new MqttClientOptionsBuilder().WithProtocolVersion(MqttProtocolVersion.V500)); + await receiver.SubscribeAsync("#"); + + MqttApplicationMessage receivedMessage = null; + receiver.ApplicationMessageReceivedAsync += e => + { + receivedMessage = e.ApplicationMessage; + return CompletedTask.Instance; + }; + + var sender = await testEnvironment.ConnectClient(new MqttClientOptionsBuilder().WithProtocolVersion(MqttProtocolVersion.V500)); + + var applicationMessage = new MqttApplicationMessageBuilder().WithTopic("Hello") + .WithPayload("World") + .WithQualityOfServiceLevel(MqttQualityOfServiceLevel.AtMostOnce) + .WithUserProperty("x", "1") + .WithUserProperty("y", "2") + .WithResponseTopic("response") + .WithContentType("text") + .WithMessageExpiryInterval(50) + .WithCorrelationData(new byte[12]) + .WithTopicAlias(2) + .Build(); + + await sender.PublishAsync(applicationMessage); + + await Task.Delay(500); + + Assert.IsNotNull(receivedMessage); + Assert.AreEqual(applicationMessage.Topic, receivedMessage.Topic); + Assert.AreEqual(applicationMessage.TopicAlias, receivedMessage.TopicAlias); + Assert.AreEqual(applicationMessage.ContentType, receivedMessage.ContentType); + Assert.AreEqual(applicationMessage.ResponseTopic, receivedMessage.ResponseTopic); + Assert.AreEqual(applicationMessage.MessageExpiryInterval, receivedMessage.MessageExpiryInterval); + CollectionAssert.AreEqual(applicationMessage.CorrelationData, receivedMessage.CorrelationData); + CollectionAssert.AreEqual(applicationMessage.Payload.ToArray(), receivedMessage.Payload.ToArray()); + CollectionAssert.AreEqual(applicationMessage.UserProperties, receivedMessage.UserProperties); + } + + [TestMethod] + public async Task Publish_QoS_0() + { + using var testEnvironment = CreateTestEnvironment(); + await testEnvironment.StartServer(); + + var client = await testEnvironment.ConnectClient(o => o.WithProtocolVersion(MqttProtocolVersion.V500)); + var result = await client.PublishStringAsync("a", "b"); + await client.DisconnectAsync(); + + Assert.AreEqual(MqttClientPublishReasonCode.Success, result.ReasonCode); + } + + [TestMethod] + public async Task Publish_QoS_0_LargeBuffer() + { + await using var recyclableMemoryStream = GetLargePayload(); + using var testEnvironment = CreateTestEnvironment(); + await testEnvironment.StartServer(); + + var client = await testEnvironment.ConnectClient(o => o.WithProtocolVersion(MqttProtocolVersion.V500)); + var result = await client.PublishSequenceAsync("a", recyclableMemoryStream.GetReadOnlySequence()); + await client.DisconnectAsync(); + + Assert.AreEqual(MqttClientPublishReasonCode.Success, result.ReasonCode); + } + + [TestMethod] + public async Task Publish_QoS_1() + { + using var testEnvironment = CreateTestEnvironment(); + await testEnvironment.StartServer(); + + var client = await testEnvironment.ConnectClient(o => o.WithProtocolVersion(MqttProtocolVersion.V500)); + var result = await client.PublishStringAsync("a", "b", MqttQualityOfServiceLevel.AtLeastOnce); + await client.DisconnectAsync(); + + Assert.AreEqual(MqttClientPublishReasonCode.NoMatchingSubscribers, result.ReasonCode); + } + + [TestMethod] + public async Task Publish_QoS_1_LargeBuffer() + { + await using var recyclableMemoryStream = GetLargePayload(); + using var testEnvironment = CreateTestEnvironment(); + await testEnvironment.StartServer(); + + var client = await testEnvironment.ConnectClient(o => o.WithProtocolVersion(MqttProtocolVersion.V500)); + var result = await client.PublishSequenceAsync("a", recyclableMemoryStream.GetReadOnlySequence(), MqttQualityOfServiceLevel.AtLeastOnce); + await client.DisconnectAsync(); + + Assert.AreEqual(MqttClientPublishReasonCode.NoMatchingSubscribers, result.ReasonCode); + } + + [TestMethod] + public async Task Publish_QoS_2() + { + using var testEnvironment = CreateTestEnvironment(); + await testEnvironment.StartServer(); + + var client = await testEnvironment.ConnectClient(o => o.WithProtocolVersion(MqttProtocolVersion.V500)); + var result = await client.PublishStringAsync("a", "b", MqttQualityOfServiceLevel.ExactlyOnce); + await client.DisconnectAsync(); + + Assert.AreEqual(MqttClientPublishReasonCode.NoMatchingSubscribers, result.ReasonCode); + } + + [TestMethod] + public async Task Publish_QoS_2_LargeBuffer() + { + await using var recyclableMemoryStream = GetLargePayload(); + using var testEnvironment = CreateTestEnvironment(); + await testEnvironment.StartServer(); + + var client = await testEnvironment.ConnectClient(o => o.WithProtocolVersion(MqttProtocolVersion.V500)); + var result = await client.PublishSequenceAsync("a", recyclableMemoryStream.GetReadOnlySequence(), MqttQualityOfServiceLevel.ExactlyOnce); + await client.DisconnectAsync(); + + Assert.AreEqual(MqttClientPublishReasonCode.NoMatchingSubscribers, result.ReasonCode); + } + + [TestMethod] + public async Task Publish_With_Properties() + { + using var testEnvironment = CreateTestEnvironment(); + await testEnvironment.StartServer(); + + var client = await testEnvironment.ConnectClient(o => o.WithProtocolVersion(MqttProtocolVersion.V500)); + + var applicationMessage = new MqttApplicationMessageBuilder().WithTopic("Hello") + .WithPayload("World") + .WithQualityOfServiceLevel(MqttQualityOfServiceLevel.AtMostOnce) + .WithUserProperty("x", "1") + .WithUserProperty("y", "2") + .WithResponseTopic("response") + .WithContentType("text") + .WithMessageExpiryInterval(50) + .WithCorrelationData(new byte[12]) + .WithTopicAlias(2) + .Build(); + + var result = await client.PublishAsync(applicationMessage); + await client.DisconnectAsync(); + + Assert.AreEqual(MqttClientPublishReasonCode.Success, result.ReasonCode); + } + + [TestMethod] + public async Task Publish_With_RecyclableMemoryStream() + { + var memoryManager = new RecyclableMemoryStreamManager(new RecyclableMemoryStreamManager.Options { BlockSize = 4096 }); + using var testEnvironment = CreateTestEnvironment(); + await testEnvironment.StartServer(); + + var client = await testEnvironment.ConnectClient(o => o.WithProtocolVersion(MqttProtocolVersion.V500)); + + const int payloadSize = 100000; + await using var memoryStream = memoryManager.GetStream(); + + byte testValue = 0; + while (memoryStream.Length < payloadSize) + { + memoryStream.WriteByte(testValue++); + } + + var applicationMessage = new MqttApplicationMessageBuilder().WithTopic("Hello") + .WithPayload(memoryStream.GetReadOnlySequence()) + .WithQualityOfServiceLevel(MqttQualityOfServiceLevel.AtMostOnce) + .WithUserProperty("x", "1") + .WithUserProperty("y", "2") + .WithResponseTopic("response") + .WithContentType("text") + .WithMessageExpiryInterval(50) + .WithCorrelationData(new byte[12]) + .WithTopicAlias(2) + .Build(); + + var result = await client.PublishAsync(applicationMessage); + await client.DisconnectAsync(); + + Assert.AreEqual(MqttClientPublishReasonCode.Success, result.ReasonCode); + } + + [TestMethod] + public async Task Subscribe() + { + using var testEnvironment = CreateTestEnvironment(); + await testEnvironment.StartServer(); + + var client = await testEnvironment.ConnectClient(o => o.WithProtocolVersion(MqttProtocolVersion.V500)); + + var result = await client.SubscribeAsync( + new MqttClientSubscribeOptions + { + SubscriptionIdentifier = 1, + TopicFilters = + [ + new MqttTopicFilter() { Topic = "a", QualityOfServiceLevel = MqttQualityOfServiceLevel.AtLeastOnce } + ] + }); + + await client.DisconnectAsync(); + + Assert.AreEqual(1, result.Items.Count); + Assert.AreEqual(MqttClientSubscribeResultCode.GrantedQoS1, result.Items.First().ResultCode); + } + + [TestMethod] + public async Task Subscribe_And_Publish() + { + using var testEnvironment = CreateTestEnvironment(); + await testEnvironment.StartServer(); + + var client1 = await testEnvironment.ConnectClient(o => o.WithProtocolVersion(MqttProtocolVersion.V500).WithClientId("client1")); + + var testMessageHandler = testEnvironment.CreateApplicationMessageHandler(client1); + + await client1.SubscribeAsync("a"); + + var client2 = await testEnvironment.ConnectClient(o => o.WithProtocolVersion(MqttProtocolVersion.V500).WithClientId("client2")); + await client2.PublishStringAsync("a", "b"); + + await Task.Delay(500); + + await client2.DisconnectAsync(); + await client1.DisconnectAsync(); + + Assert.AreEqual(1, testMessageHandler.ReceivedEventArgs.Count); + Assert.AreEqual("Subscribe_And_Publish_client1", testMessageHandler.ReceivedEventArgs[0].ClientId); + Assert.AreEqual("a", testMessageHandler.ReceivedEventArgs[0].ApplicationMessage.Topic); + Assert.AreEqual("b", testMessageHandler.ReceivedEventArgs[0].ApplicationMessage.ConvertPayloadToString()); + } + + [TestMethod] + public async Task Unsubscribe() + { + using var testEnvironment = CreateTestEnvironment(); + await testEnvironment.StartServer(); + + var client = await testEnvironment.ConnectClient(o => o.WithProtocolVersion(MqttProtocolVersion.V500)); + await client.SubscribeAsync("a"); + + var result = await client.UnsubscribeAsync("a"); + await client.DisconnectAsync(); + + Assert.AreEqual(1, result.Items.Count); + Assert.AreEqual(MqttClientUnsubscribeResultCode.Success, result.Items.First().ResultCode); + } + + static RecyclableMemoryStream GetLargePayload() + { + var memoryManager = new RecyclableMemoryStreamManager(); + var memoryStream = memoryManager.GetStream(); + for (var i = 0; i < 100000; i++) + { + memoryStream.WriteByte((byte)i); + } + + memoryStream.Position = 0; + return memoryStream; + } +} \ No newline at end of file diff --git a/Source/MQTTnet.Tests/MQTTv5/Server_Tests.cs b/Source/MQTTnet.Tests/MQTTv5/Server_Tests.cs index 067fed79..df3060e1 100644 --- a/Source/MQTTnet.Tests/MQTTv5/Server_Tests.cs +++ b/Source/MQTTnet.Tests/MQTTv5/Server_Tests.cs @@ -11,221 +11,210 @@ using MQTTnet.Internal; using MQTTnet.Protocol; using MQTTnet.Server; -namespace MQTTnet.Tests.MQTTv5 +namespace MQTTnet.Tests.MQTTv5; + +// ReSharper disable InconsistentNaming +[TestClass] +public sealed class Server_Tests : BaseTestClass { - [TestClass] - public sealed class Server_Tests : BaseTestClass + [TestMethod] + public async Task Will_Message_Send() { - [TestMethod] - public async Task Will_Message_Send() + using var testEnvironment = CreateTestEnvironment(); + await testEnvironment.StartServer(); + + var clientOptions = new MqttClientOptionsBuilder().WithWillTopic("My/last/will").WithWillQualityOfServiceLevel(MqttQualityOfServiceLevel.AtMostOnce).WithProtocolVersion(MqttProtocolVersion.V500); + + var c1 = await testEnvironment.ConnectClient(new MqttClientOptionsBuilder().WithProtocolVersion(MqttProtocolVersion.V500)); + + var receivedMessagesCount = 0; + c1.ApplicationMessageReceivedAsync += _ => { - using (var testEnvironment = CreateTestEnvironment()) - { - await testEnvironment.StartServer(); + Interlocked.Increment(ref receivedMessagesCount); + return CompletedTask.Instance; + }; - var clientOptions = new MqttClientOptionsBuilder().WithWillTopic("My/last/will").WithWillQualityOfServiceLevel(MqttQualityOfServiceLevel.AtMostOnce).WithProtocolVersion(MqttProtocolVersion.V500); + await c1.SubscribeAsync(new MqttTopicFilterBuilder().WithTopic("#").Build()); - var c1 = await testEnvironment.ConnectClient(new MqttClientOptionsBuilder().WithProtocolVersion(MqttProtocolVersion.V500)); + var c2 = await testEnvironment.ConnectClient(clientOptions); + c2.Dispose(); // Dispose will not send a DISCONNECT packet first so the will message must be sent. - var receivedMessagesCount = 0; - c1.ApplicationMessageReceivedAsync += e => - { - Interlocked.Increment(ref receivedMessagesCount); - return CompletedTask.Instance; - }; + await LongTestDelay(); - await c1.SubscribeAsync(new MqttTopicFilterBuilder().WithTopic("#").Build()); - - var c2 = await testEnvironment.ConnectClient(clientOptions); - c2.Dispose(); // Dispose will not send a DISCONNECT packet first so the will message must be sent. - - await LongTestDelay(); - - Assert.AreEqual(1, receivedMessagesCount); - } - } - - [TestMethod] - public async Task Validate_IsSessionPresent() - { - using (var testEnvironment = CreateTestEnvironment()) - { - // Create server with persistent sessions enabled - - await testEnvironment.StartServer(o => o.WithPersistentSessions()); - - const string ClientId = "Client1"; - - // Create client without clean session and long session expiry interval - var options1 = CreateClientOptions(testEnvironment, ClientId, false, 9999); - var client1 = await testEnvironment.ConnectClient(options1); - - // Disconnect; empty session should remain on server - - await client1.DisconnectAsync(); - - // Simulate some time delay between connections - - await Task.Delay(1000); - - // Reconnect the same client ID to existing session - - var client2 = testEnvironment.CreateClient(); - var options2 = CreateClientOptions(testEnvironment, ClientId, false, 9999); - var result = await client2.ConnectAsync(options2).ConfigureAwait(false); - - await client2.DisconnectAsync(); - - // Session should be present - - Assert.IsTrue(result.IsSessionPresent, "Session not present"); - } - } - - [TestMethod] - public async Task Connect_with_Undefined_SessionExpiryInterval() - { - using (var testEnvironment = CreateTestEnvironment()) - { - // Create server with persistent sessions enabled - - await testEnvironment.StartServer(o => o.WithPersistentSessions()); - - const string ClientId = "Client1"; - - // Create client without clean session and NO session expiry interval, - // that means, the session should not persist - - var options1 = CreateClientOptions(testEnvironment, ClientId, false, 0); - var client1 = await testEnvironment.ConnectClient(options1); - - // Disconnect; no session should remain on server because the session expiry interval was undefined - - await client1.DisconnectAsync(); - - // Simulate some time delay between connections - - await Task.Delay(1000); - - // Reconnect the same client ID to existing session - - var client2 = testEnvironment.CreateClient(); - var options2 = CreateClientOptions(testEnvironment, ClientId, false, 9999); - var result = await client2.ConnectAsync(options2).ConfigureAwait(false); - - await client2.DisconnectAsync(); - - // Session should not be present - - Assert.IsTrue(!result.IsSessionPresent, "Session is present when it should not"); - } - } - - - [TestMethod] - public async Task Reconnect_with_different_SessionExpiryInterval() - { - using (var testEnvironment = CreateTestEnvironment()) - { - // Create server with persistent sessions enabled - - await testEnvironment.StartServer(o => o.WithPersistentSessions()); - - const string ClientId = "Client1"; - - // Create client with clean session and session expiry interval > 0 - - var options = CreateClientOptions(testEnvironment, ClientId, true, 9999); - var client1 = await testEnvironment.ConnectClient(options); - - // Disconnect; session should remain on server - - await client1.DisconnectAsync(); - - await Task.Delay(1000); - - // Reconnect the same client ID to the existing session but leave session expiry interval undefined this time. - // Session should be present because the client1 connection had SessionExpiryInterval > 0 - - var client2 = testEnvironment.CreateClient(); - var options2 = CreateClientOptions(testEnvironment, ClientId, false, 0); - var result2 = await client2.ConnectAsync(options2).ConfigureAwait(false); - - await client2.DisconnectAsync(); - - Assert.IsTrue(result2.IsSessionPresent, "Session is not present when it should"); - - // Simulate some time delay between connections - - await Task.Delay(1000); - - // Reconnect the same client ID. - // No session should be present because the previous session expiry interval was undefined for the client2 connection - - var client3 = testEnvironment.CreateClient(); - var options3 = CreateClientOptions(testEnvironment, ClientId, false, 0); - var result3 = await client2.ConnectAsync(options3).ConfigureAwait(false); - - await client3.DisconnectAsync(); - - // Session should be present - - Assert.IsTrue(!result3.IsSessionPresent, "Session is present when it should not"); - } - } - - [TestMethod] - public async Task Disconnect_with_Reason() - { - using (var testEnvironment = CreateTestEnvironment()) - { - var disconnectReason = MqttClientDisconnectReason.UnspecifiedError; - - string testClientId = null; - - await testEnvironment.StartServer(); - - testEnvironment.Server.ClientConnectedAsync += e => - { - testClientId = e.ClientId; - return CompletedTask.Instance; - }; - - var client = await testEnvironment.ConnectClient(new MqttClientOptionsBuilder().WithProtocolVersion(MqttProtocolVersion.V500)); - - client.DisconnectedAsync += e => - { - disconnectReason = e.Reason; - return CompletedTask.Instance; - }; - - await LongTestDelay(); - - // Test client should be connected now - - Assert.IsTrue(testClientId != null); - - // Have the server disconnect the client with AdministrativeAction reason - - await testEnvironment.Server.DisconnectClientAsync(testClientId, MqttDisconnectReasonCode.AdministrativeAction); - - await LongTestDelay(); - - // The reason should be returned to the client in the DISCONNECT packet - - Assert.AreEqual(MqttClientDisconnectReason.AdministrativeAction, disconnectReason); - } - } - - static MqttClientOptions CreateClientOptions(TestEnvironment testEnvironment, string clientId, bool cleanSession, uint sessionExpiryInterval) - { - return testEnvironment.ClientFactory.CreateClientOptionsBuilder() - .WithProtocolVersion(MqttProtocolVersion.V500) - .WithTcpServer("127.0.0.1", testEnvironment.ServerPort) - .WithSessionExpiryInterval(sessionExpiryInterval) - .WithCleanSession(cleanSession) - .WithClientId(clientId) - .Build(); - } + Assert.AreEqual(1, receivedMessagesCount); } -} + + [TestMethod] + public async Task Validate_IsSessionPresent() + { + using var testEnvironment = CreateTestEnvironment(); + // Create server with persistent sessions enabled + + await testEnvironment.StartServer(o => o.WithPersistentSessions()); + + const string clientId = "Client1"; + + // Create client without clean session and long session expiry interval + var options1 = CreateClientOptions(testEnvironment, clientId, false, 9999); + var client1 = await testEnvironment.ConnectClient(options1); + + // Disconnect; empty session should remain on server + + await client1.DisconnectAsync(); + + // Simulate some time delay between connections + + await Task.Delay(1000); + + // Reconnect the same client ID to existing session + + var client2 = testEnvironment.CreateClient(); + var options2 = CreateClientOptions(testEnvironment, clientId, false, 9999); + var result = await client2.ConnectAsync(options2).ConfigureAwait(false); + + await client2.DisconnectAsync(); + + // Session should be present + + Assert.IsTrue(result.IsSessionPresent, "Session not present"); + } + + [TestMethod] + public async Task Connect_with_Undefined_SessionExpiryInterval() + { + using var testEnvironment = CreateTestEnvironment(); + // Create server with persistent sessions enabled + + await testEnvironment.StartServer(o => o.WithPersistentSessions()); + + const string clientId = "Client1"; + + // Create client without clean session and NO session expiry interval, + // that means, the session should not persist + + var options1 = CreateClientOptions(testEnvironment, clientId, false, 0); + var client1 = await testEnvironment.ConnectClient(options1); + + // Disconnect; no session should remain on server because the session expiry interval was undefined + + await client1.DisconnectAsync(); + + // Simulate some time delay between connections + + await Task.Delay(1000); + + // Reconnect the same client ID to existing session + + var client2 = testEnvironment.CreateClient(); + var options2 = CreateClientOptions(testEnvironment, clientId, false, 9999); + var result = await client2.ConnectAsync(options2).ConfigureAwait(false); + + await client2.DisconnectAsync(); + + // Session should not be present + + Assert.IsTrue(!result.IsSessionPresent, "Session is present when it should not"); + } + + [TestMethod] + public async Task Reconnect_with_different_SessionExpiryInterval() + { + using var testEnvironment = CreateTestEnvironment(); + // Create server with persistent sessions enabled + + await testEnvironment.StartServer(o => o.WithPersistentSessions()); + + const string clientId = "Client1"; + + // Create client with clean session and session expiry interval > 0 + + var options = CreateClientOptions(testEnvironment, clientId, true, 9999); + var client1 = await testEnvironment.ConnectClient(options); + + // Disconnect; session should remain on server + + await client1.DisconnectAsync(); + + await Task.Delay(1000); + + // Reconnect the same client ID to the existing session but leave session expiry interval undefined this time. + // Session should be present because the client1 connection had SessionExpiryInterval > 0 + + var client2 = testEnvironment.CreateClient(); + var options2 = CreateClientOptions(testEnvironment, clientId, false, 0); + var result2 = await client2.ConnectAsync(options2).ConfigureAwait(false); + + await client2.DisconnectAsync(); + + Assert.IsTrue(result2.IsSessionPresent, "Session is not present when it should"); + + // Simulate some time delay between connections + + await Task.Delay(1000); + + // Reconnect the same client ID. + // No session should be present because the previous session expiry interval was undefined for the client2 connection + + var client3 = testEnvironment.CreateClient(); + var options3 = CreateClientOptions(testEnvironment, clientId, false, 0); + var result3 = await client2.ConnectAsync(options3).ConfigureAwait(false); + + await client3.DisconnectAsync(); + + // Session should be present + + Assert.IsTrue(!result3.IsSessionPresent, "Session is present when it should not"); + } + + [TestMethod] + public async Task Disconnect_with_Reason() + { + using var testEnvironment = CreateTestEnvironment(); + var disconnectReason = MqttClientDisconnectReason.UnspecifiedError; + + string testClientId = null; + + await testEnvironment.StartServer(); + + testEnvironment.Server.ClientConnectedAsync += e => + { + testClientId = e.ClientId; + return CompletedTask.Instance; + }; + + var client = await testEnvironment.ConnectClient(new MqttClientOptionsBuilder().WithProtocolVersion(MqttProtocolVersion.V500)); + + client.DisconnectedAsync += e => + { + disconnectReason = e.Reason; + return CompletedTask.Instance; + }; + + await LongTestDelay(); + + // Test client should be connected now + + Assert.IsTrue(testClientId != null); + + // Have the server disconnect the client with AdministrativeAction reason + + await testEnvironment.Server.DisconnectClientAsync(testClientId, MqttDisconnectReasonCode.AdministrativeAction); + + await LongTestDelay(); + + // The reason should be returned to the client in the DISCONNECT packet + + Assert.AreEqual(MqttClientDisconnectReason.AdministrativeAction, disconnectReason); + } + + static MqttClientOptions CreateClientOptions(TestEnvironment testEnvironment, string clientId, bool cleanSession, uint sessionExpiryInterval) + { + return testEnvironment.ClientFactory.CreateClientOptionsBuilder() + .WithProtocolVersion(MqttProtocolVersion.V500) + .WithTcpServer("127.0.0.1", testEnvironment.ServerPort) + .WithSessionExpiryInterval(sessionExpiryInterval) + .WithCleanSession(cleanSession) + .WithClientId(clientId) + .Build(); + } +} \ No newline at end of file diff --git a/Source/MQTTnet.Tests/Mockups/MemoryMqttChannel.cs b/Source/MQTTnet.Tests/Mockups/MemoryMqttChannel.cs index 20c76602..6d305686 100644 --- a/Source/MQTTnet.Tests/Mockups/MemoryMqttChannel.cs +++ b/Source/MQTTnet.Tests/Mockups/MemoryMqttChannel.cs @@ -2,7 +2,6 @@ // The .NET Foundation licenses this file to you under the MIT license. // See the LICENSE file in the project root for more information. -using Microsoft.AspNetCore.Http; using MQTTnet.Channel; using MQTTnet.Internal; using System.Buffers; @@ -12,53 +11,52 @@ using System.Security.Cryptography.X509Certificates; using System.Threading; using System.Threading.Tasks; -namespace MQTTnet.Tests.Mockups +namespace MQTTnet.Tests.Mockups; + +public sealed class MemoryMqttChannel : IMqttChannel { - public sealed class MemoryMqttChannel : IMqttChannel + readonly MemoryStream _stream; + + public MemoryMqttChannel(MemoryStream stream) { - readonly MemoryStream _stream; + _stream = stream; + } - public MemoryMqttChannel(MemoryStream stream) - { - _stream = stream; - } - - public MemoryMqttChannel(byte[] buffer) - { - _stream = new MemoryStream(buffer); - } - - public EndPoint RemoteEndPoint { get; set; } - - public bool IsSecureConnection { get; } = false; - - public X509Certificate2 ClientCertificate { get; } - - public Task ConnectAsync(CancellationToken cancellationToken) - { - return CompletedTask.Instance; - } - - public Task DisconnectAsync(CancellationToken cancellationToken) - { - return CompletedTask.Instance; - } - - public Task ReadAsync(byte[] buffer, int offset, int count, CancellationToken cancellationToken) - { - return _stream.ReadAsync(buffer, offset, count, cancellationToken); - } - - public async Task WriteAsync(ReadOnlySequence buffer, bool isEndOfPacket, CancellationToken cancellationToken) - { - foreach (var segment in buffer) - { - await _stream.WriteAsync(segment, cancellationToken).ConfigureAwait(false); - } - } - - public void Dispose() + public MemoryMqttChannel(byte[] buffer) + { + _stream = new MemoryStream(buffer); + } + + public EndPoint RemoteEndPoint { get; set; } + + public bool IsSecureConnection { get; } = false; + + public X509Certificate2 ClientCertificate { get; set; } + + public Task ConnectAsync(CancellationToken cancellationToken) + { + return CompletedTask.Instance; + } + + public Task DisconnectAsync(CancellationToken cancellationToken) + { + return CompletedTask.Instance; + } + + public Task ReadAsync(byte[] buffer, int offset, int count, CancellationToken cancellationToken) + { + return _stream.ReadAsync(buffer, offset, count, cancellationToken); + } + + public async Task WriteAsync(ReadOnlySequence buffer, bool isEndOfPacket, CancellationToken cancellationToken) + { + foreach (var segment in buffer) { + await _stream.WriteAsync(segment, cancellationToken).ConfigureAwait(false); } } -} + + public void Dispose() + { + } +} \ No newline at end of file diff --git a/Source/MQTTnet.Tests/Mockups/TestApplicationMessageReceivedHandler.cs b/Source/MQTTnet.Tests/Mockups/TestApplicationMessageReceivedHandler.cs index 66b44bd1..d806322d 100644 --- a/Source/MQTTnet.Tests/Mockups/TestApplicationMessageReceivedHandler.cs +++ b/Source/MQTTnet.Tests/Mockups/TestApplicationMessageReceivedHandler.cs @@ -10,72 +10,71 @@ using System.Threading.Tasks; using Microsoft.VisualStudio.TestTools.UnitTesting; using MQTTnet.Internal; -namespace MQTTnet.Tests.Mockups +namespace MQTTnet.Tests.Mockups; + +public sealed class TestApplicationMessageReceivedHandler { - public sealed class TestApplicationMessageReceivedHandler + readonly List _receivedEventArgs = new List(); + + public TestApplicationMessageReceivedHandler(IMqttClient mqttClient) { - readonly List _receivedEventArgs = new List(); + ArgumentNullException.ThrowIfNull(mqttClient); - public TestApplicationMessageReceivedHandler(IMqttClient mqttClient) - { - ArgumentNullException.ThrowIfNull(mqttClient); + mqttClient.ApplicationMessageReceivedAsync += OnApplicationMessageReceivedAsync; + } - mqttClient.ApplicationMessageReceivedAsync += OnApplicationMessageReceivedAsync; - } - - public int Count - { - get - { - lock (_receivedEventArgs) - { - return _receivedEventArgs.Count; - } - } - } - - public List ReceivedEventArgs - { - get - { - lock (_receivedEventArgs) - { - return _receivedEventArgs.ToList(); - } - } - } - - public void AssertReceivedCountEquals(int expectedCount) - { - Assert.AreEqual(expectedCount, Count); - } - - public string GeneratePayloadSequence() - { - var sequence = new StringBuilder(); - - lock (_receivedEventArgs) - { - foreach (var receivedEventArg in _receivedEventArgs) - { - var payload = receivedEventArg.ApplicationMessage.ConvertPayloadToString(); - - // An empty payload is not part of the sequence! - sequence.Append(payload); - } - } - - return sequence.ToString(); - } - - Task OnApplicationMessageReceivedAsync(MqttApplicationMessageReceivedEventArgs eventArgs) + public int Count + { + get { lock (_receivedEventArgs) { - _receivedEventArgs.Add(eventArgs); + return _receivedEventArgs.Count; } - - return CompletedTask.Instance; } } + + public List ReceivedEventArgs + { + get + { + lock (_receivedEventArgs) + { + return _receivedEventArgs.ToList(); + } + } + } + + public void AssertReceivedCountEquals(int expectedCount) + { + Assert.AreEqual(expectedCount, Count); + } + + public string GeneratePayloadSequence() + { + var sequence = new StringBuilder(); + + lock (_receivedEventArgs) + { + foreach (var receivedEventArg in _receivedEventArgs) + { + var payload = receivedEventArg.ApplicationMessage.ConvertPayloadToString(); + + // An empty payload is not part of the sequence! + sequence.Append(payload); + } + } + + return sequence.ToString(); + } + + Task OnApplicationMessageReceivedAsync(MqttApplicationMessageReceivedEventArgs eventArgs) + { + lock (_receivedEventArgs) + { + _receivedEventArgs.Add(eventArgs); + } + + return CompletedTask.Instance; + } } \ No newline at end of file diff --git a/Source/MQTTnet.Tests/Mockups/TestEnvironment.cs b/Source/MQTTnet.Tests/Mockups/TestEnvironment.cs index 6f2d8ae7..c0083f93 100644 --- a/Source/MQTTnet.Tests/Mockups/TestEnvironment.cs +++ b/Source/MQTTnet.Tests/Mockups/TestEnvironment.cs @@ -17,422 +17,410 @@ using MQTTnet.LowLevelClient; using MQTTnet.Protocol; using MQTTnet.Server; -namespace MQTTnet.Tests.Mockups +namespace MQTTnet.Tests.Mockups; + +public sealed class TestEnvironment : IDisposable { - public sealed class TestEnvironment : IDisposable + readonly List _clientErrors = new(); + readonly List _clients = new(); + readonly List _exceptions = new(); + readonly List _lowLevelClients = new(); + readonly MqttProtocolVersion _protocolVersion; + readonly List _serverErrors = new(); + + public TestEnvironment() : this(null) { - readonly List _clientErrors = new(); - readonly List _clients = new(); - readonly List _exceptions = new(); - readonly List _lowLevelClients = new(); - readonly MqttProtocolVersion _protocolVersion; - readonly List _serverErrors = new(); + } - public TestEnvironment() : this(null) + public TestEnvironment( + TestContext testContext, MqttProtocolVersion protocolVersion = MqttProtocolVersion.V311, bool trackUnobservedTaskException = true) + { + _protocolVersion = protocolVersion; + TestContext = testContext; + + if (trackUnobservedTaskException) { + TaskScheduler.UnobservedTaskException += TrackUnobservedTaskException; } - public TestEnvironment( - TestContext testContext, MqttProtocolVersion protocolVersion = MqttProtocolVersion.V311, bool trackUnobservedTaskException = true) + ServerLogger.LogMessagePublished += (_, e) => { - _protocolVersion = protocolVersion; - TestContext = testContext; + Debug.WriteLine(e.LogMessage.ToString()); - if (trackUnobservedTaskException) - { - TaskScheduler.UnobservedTaskException += TrackUnobservedTaskException; - } - - ServerLogger.LogMessagePublished += (s, e) => - { - if (Debugger.IsAttached) - { - Debug.WriteLine(e.LogMessage.ToString()); - } - - if (e.LogMessage.Level == MqttNetLogLevel.Error) - { - lock (_serverErrors) - { - _serverErrors.Add(e.LogMessage.ToString()); - } - } - }; - - ClientLogger.LogMessagePublished += (s, e) => - { - if (Debugger.IsAttached) - { - Debug.WriteLine(e.LogMessage.ToString()); - } - - if (e.LogMessage.Level == MqttNetLogLevel.Error) - { - if (!IgnoreClientLogErrors) - { - lock (_clientErrors) - { - _clientErrors.Add(e.LogMessage.ToString()); - } - } - } - }; - } - - public MqttNetEventLogger ClientLogger { get; } = new("client"); - - public static bool EnableLogger { get; set; } = true; - - public MqttClientFactory ClientFactory { get; } = new(); - - public MqttServerFactory ServerFactory { get; } = new(); - - public bool IgnoreClientLogErrors { get; set; } - - public bool IgnoreServerLogErrors { get; set; } - - public MqttServer Server { get; private set; } - - public MqttNetEventLogger ServerLogger { get; } = new("server"); - - public int ServerPort { get; set; } - - public TestContext TestContext { get; } - - public Task ConnectClient() - { - return ConnectClient(ClientFactory.CreateClientOptionsBuilder().WithProtocolVersion(_protocolVersion)); - } - - public async Task ConnectClient(Action configureOptions, TimeSpan timeout = default) - { - ArgumentNullException.ThrowIfNull(configureOptions); - - // Start with initial default values. - var optionsBuilder = ClientFactory.CreateClientOptionsBuilder().WithProtocolVersion(_protocolVersion).WithTcpServer("127.0.0.1", ServerPort); - - // Let the caller override settings. Do not touch the options after this. - configureOptions.Invoke(optionsBuilder); - - var options = optionsBuilder.Build(); - - var client = CreateClient(); - - if (timeout == TimeSpan.Zero) - { - await client.ConnectAsync(options).ConfigureAwait(false); - } - else - { - using (var timeoutToken = new CancellationTokenSource(timeout)) - { - await client.ConnectAsync(options, timeoutToken.Token).ConfigureAwait(false); - } - } - - return client; - } - - public async Task ConnectClient(MqttClientOptionsBuilder options, TimeSpan timeout = default) - { - ArgumentNullException.ThrowIfNull(options); - - options = options.WithTcpServer("127.0.0.1", ServerPort); - - var client = CreateClient(); - - if (timeout == TimeSpan.Zero) - { - await client.ConnectAsync(options.Build()).ConfigureAwait(false); - } - else - { - using (var timeoutToken = new CancellationTokenSource(timeout)) - { - await client.ConnectAsync(options.Build(), timeoutToken.Token).ConfigureAwait(false); - } - } - - return client; - } - - public async Task ConnectClient(MqttClientOptions options, TimeSpan timeout = default) - { - ArgumentNullException.ThrowIfNull(options); - - var client = CreateClient(); - - if (timeout == TimeSpan.Zero) - { - await client.ConnectAsync(options).ConfigureAwait(false); - } - else - { - using (var timeoutToken = new CancellationTokenSource(timeout)) - { - await client.ConnectAsync(options, timeoutToken.Token).ConfigureAwait(false); - } - } - - return client; - } - - public async Task ConnectLowLevelClient(Action optionsBuilder = null) - { - var options = new MqttClientOptionsBuilder(); - options = options.WithTcpServer("127.0.0.1", ServerPort); - optionsBuilder?.Invoke(options); - - var client = CreateLowLevelClient(); - await client.ConnectAsync(options.Build(), CancellationToken.None).ConfigureAwait(false); - - return client; - } - - public async Task ConnectRpcClient(MqttRpcClientOptions options) - { - return new MqttRpcClient(await ConnectClient(), options); - } - - public TestApplicationMessageReceivedHandler CreateApplicationMessageHandler(IMqttClient mqttClient) - { - return new TestApplicationMessageReceivedHandler(mqttClient); - } - - public IMqttClient CreateClient() - { - var logger = EnableLogger ? (IMqttNetLogger)ClientLogger : MqttNetNullLogger.Instance; - - var client = ClientFactory.CreateMqttClient(logger); - - client.ConnectingAsync += e => - { - if (TestContext != null) - { - var clientOptions = e.ClientOptions; - var existingClientId = clientOptions.ClientId; - if (existingClientId != null && !existingClientId.StartsWith(TestContext.TestName)) - { - clientOptions.ClientId = TestContext.TestName + "_" + existingClientId; - } - } - - return CompletedTask.Instance; - }; - - lock (_clients) - { - _clients.Add(client); - } - - return client; - } - - public MqttClientOptions CreateDefaultClientOptions() - { - return CreateDefaultClientOptionsBuilder().Build(); - } - - public MqttClientOptionsBuilder CreateDefaultClientOptionsBuilder() - { - return ClientFactory.CreateClientOptionsBuilder() - .WithProtocolVersion(_protocolVersion) - .WithTcpServer("127.0.0.1", ServerPort) - .WithClientId(TestContext.TestName + "_" + Guid.NewGuid()); - } - - public ILowLevelMqttClient CreateLowLevelClient() - { - var client = ClientFactory.CreateLowLevelMqttClient(ClientLogger); - - lock (_lowLevelClients) - { - _lowLevelClients.Add(client); - } - - return client; - } - - public MqttServer CreateServer(MqttServerOptions options) - { - if (Server != null) - { - throw new InvalidOperationException("Server already started."); - } - - var logger = EnableLogger ? (IMqttNetLogger)ServerLogger : new MqttNetNullLogger(); - - Server = ServerFactory.CreateMqttServer(options, logger); - - Server.ValidatingConnectionAsync += e => - { - if (TestContext != null) - { - // Null is used when the client id is assigned from the server! - if (!string.IsNullOrEmpty(e.ClientId) && !e.ClientId.StartsWith(TestContext.TestName)) - { - TrackException(new InvalidOperationException($"Invalid client ID used ({e.ClientId}). It must start with UnitTest name.")); - e.ReasonCode = MqttConnectReasonCode.ClientIdentifierNotValid; - } - } - - return CompletedTask.Instance; - }; - - return Server; - } - - public void Dispose() - { - try - { - lock (_clients) - { - foreach (var mqttClient in _clients) - { - try - { - //mqttClient.DisconnectAsync().GetAwaiter().GetResult(); - } - catch - { - // This can happen when the test already disconnected the client. - } - finally - { - mqttClient?.Dispose(); - } - } - - _clients.Clear(); - } - - lock (_lowLevelClients) - { - foreach (var lowLevelMqttClient in _lowLevelClients) - { - lowLevelMqttClient.Dispose(); - } - - _lowLevelClients.Clear(); - } - - try - { - Server?.StopAsync().GetAwaiter().GetResult(); - } - catch - { - // This can happen when the test already stopped the server. - } - finally - { - Server?.Dispose(); - } - - Server = null; - - ThrowIfLogErrors(); - - GC.Collect(); - GC.WaitForFullGCComplete(); - GC.WaitForPendingFinalizers(); - - if (_exceptions.Any()) - { - throw new Exception($"{_exceptions.Count} exceptions tracked.\r\n" + string.Join(Environment.NewLine, _exceptions)); - } - } - finally - { - TaskScheduler.UnobservedTaskException -= TrackUnobservedTaskException; - } - } - - public Task StartServer() - { - return StartServer(ServerFactory.CreateServerOptionsBuilder()); - } - - public async Task StartServer(MqttServerOptionsBuilder optionsBuilder) - { - optionsBuilder.WithDefaultEndpoint(); - optionsBuilder.WithDefaultEndpointPort(ServerPort); - optionsBuilder.WithMaxPendingMessagesPerClient(int.MaxValue); - - var options = optionsBuilder.Build(); - var server = CreateServer(options); - await server.StartAsync().ConfigureAwait(false); - - // The OS has chosen the port to we have to properly expose it to the tests. - ServerPort = options.DefaultEndpointOptions.Port; - return server; - } - - public async Task StartServer(Action configure) - { - var optionsBuilder = ServerFactory.CreateServerOptionsBuilder(); - - optionsBuilder.WithDefaultEndpoint(); - optionsBuilder.WithDefaultEndpointPort(ServerPort); - optionsBuilder.WithMaxPendingMessagesPerClient(int.MaxValue); - - configure?.Invoke(optionsBuilder); - - var options = optionsBuilder.Build(); - var server = CreateServer(options); - await server.StartAsync().ConfigureAwait(false); - - // The OS has chosen the port to we have to properly expose it to the tests. - ServerPort = options.DefaultEndpointOptions.Port; - return server; - } - - public void ThrowIfLogErrors() - { - if (!IgnoreServerLogErrors) + if (e.LogMessage.Level == MqttNetLogLevel.Error) { lock (_serverErrors) { - if (_serverErrors.Count > 0) - { - var message = $"Server had {_serverErrors.Count} errors (${string.Join(Environment.NewLine, _serverErrors)})."; - Console.WriteLine(message); - throw new Exception(message); - } + _serverErrors.Add(e.LogMessage.ToString()); } } + }; - if (!IgnoreClientLogErrors) + ClientLogger.LogMessagePublished += (_, e) => + { + Debug.WriteLine(e.LogMessage.ToString()); + + if (e.LogMessage.Level == MqttNetLogLevel.Error) { - lock (_clientErrors) + if (!IgnoreClientLogErrors) { - if (_clientErrors.Count > 0) + lock (_clientErrors) { - var message = $"Client(s) had {_clientErrors.Count} errors (${string.Join(Environment.NewLine, _clientErrors)})"; - Console.WriteLine(message); - throw new Exception(message); + _clientErrors.Add(e.LogMessage.ToString()); } } } + }; + } + + public MqttNetEventLogger ClientLogger { get; } = new("client"); + + public static bool EnableLogger { get; set; } = true; + + public MqttClientFactory ClientFactory { get; } = new(); + + public MqttServerFactory ServerFactory { get; } = new(); + + public bool IgnoreClientLogErrors { get; set; } + + public bool IgnoreServerLogErrors { get; set; } + + public MqttServer Server { get; private set; } + + public MqttNetEventLogger ServerLogger { get; } = new("server"); + + public int ServerPort { get; set; } + + public TestContext TestContext { get; } + + public Task ConnectClient() + { + return ConnectClient(ClientFactory.CreateClientOptionsBuilder().WithProtocolVersion(_protocolVersion)); + } + + public async Task ConnectClient(Action configureOptions, TimeSpan timeout = default) + { + ArgumentNullException.ThrowIfNull(configureOptions); + + // Start with initial default values. + var optionsBuilder = ClientFactory.CreateClientOptionsBuilder().WithProtocolVersion(_protocolVersion).WithTcpServer("127.0.0.1", ServerPort); + + // Let the caller override settings. Do not touch the options after this. + configureOptions.Invoke(optionsBuilder); + + var options = optionsBuilder.Build(); + + var client = CreateClient(); + + if (timeout == TimeSpan.Zero) + { + await client.ConnectAsync(options).ConfigureAwait(false); + } + else + { + using var timeoutToken = new CancellationTokenSource(timeout); + await client.ConnectAsync(options, timeoutToken.Token).ConfigureAwait(false); } - public void TrackException(Exception exception) - { - if (exception == null) - { - return; - } + return client; + } - lock (_exceptions) - { - _exceptions.Add(exception); - } + public async Task ConnectClient(MqttClientOptionsBuilder options, TimeSpan timeout = default) + { + ArgumentNullException.ThrowIfNull(options); + + options = options.WithTcpServer("127.0.0.1", ServerPort); + + var client = CreateClient(); + + if (timeout == TimeSpan.Zero) + { + await client.ConnectAsync(options.Build()).ConfigureAwait(false); + } + else + { + using var timeoutToken = new CancellationTokenSource(timeout); + await client.ConnectAsync(options.Build(), timeoutToken.Token).ConfigureAwait(false); } - void TrackUnobservedTaskException(object sender, UnobservedTaskExceptionEventArgs e) + return client; + } + + public async Task ConnectClient(MqttClientOptions options, TimeSpan timeout = default) + { + ArgumentNullException.ThrowIfNull(options); + + var client = CreateClient(); + + if (timeout == TimeSpan.Zero) { - TrackException(e.Exception); + await client.ConnectAsync(options).ConfigureAwait(false); + } + else + { + using var timeoutToken = new CancellationTokenSource(timeout); + await client.ConnectAsync(options, timeoutToken.Token).ConfigureAwait(false); + } + + return client; + } + + public async Task ConnectLowLevelClient(Action optionsBuilder = null) + { + var options = new MqttClientOptionsBuilder(); + options = options.WithTcpServer("127.0.0.1", ServerPort); + optionsBuilder?.Invoke(options); + + var client = CreateLowLevelClient(); + await client.ConnectAsync(options.Build(), CancellationToken.None).ConfigureAwait(false); + + return client; + } + + public async Task ConnectRpcClient(MqttRpcClientOptions options) + { + return new MqttRpcClient(await ConnectClient(), options); + } + + public TestApplicationMessageReceivedHandler CreateApplicationMessageHandler(IMqttClient mqttClient) + { + return new TestApplicationMessageReceivedHandler(mqttClient); + } + + public IMqttClient CreateClient() + { + var logger = EnableLogger ? (IMqttNetLogger)ClientLogger : MqttNetNullLogger.Instance; + + var client = ClientFactory.CreateMqttClient(logger); + + client.ConnectingAsync += e => + { + if (TestContext != null) + { + var clientOptions = e.ClientOptions; + var existingClientId = clientOptions.ClientId; + if (existingClientId != null && !existingClientId.StartsWith(TestContext.TestName)) + { + clientOptions.ClientId = TestContext.TestName + "_" + existingClientId; + } + } + + return CompletedTask.Instance; + }; + + lock (_clients) + { + _clients.Add(client); + } + + return client; + } + + public MqttClientOptions CreateDefaultClientOptions() + { + return CreateDefaultClientOptionsBuilder().Build(); + } + + public MqttClientOptionsBuilder CreateDefaultClientOptionsBuilder() + { + return ClientFactory.CreateClientOptionsBuilder() + .WithProtocolVersion(_protocolVersion) + .WithTcpServer("127.0.0.1", ServerPort) + .WithClientId(TestContext.TestName + "_" + Guid.NewGuid()); + } + + public ILowLevelMqttClient CreateLowLevelClient() + { + var client = ClientFactory.CreateLowLevelMqttClient(ClientLogger); + + lock (_lowLevelClients) + { + _lowLevelClients.Add(client); + } + + return client; + } + + public MqttServer CreateServer(MqttServerOptions options) + { + if (Server != null) + { + throw new InvalidOperationException("Server already started."); + } + + var logger = EnableLogger ? (IMqttNetLogger)ServerLogger : new MqttNetNullLogger(); + + Server = ServerFactory.CreateMqttServer(options, logger); + + Server.ValidatingConnectionAsync += e => + { + if (TestContext != null) + { + // Null is used when the client id is assigned from the server! + if (!string.IsNullOrEmpty(e.ClientId) && !e.ClientId.StartsWith(TestContext.TestName)) + { + TrackException(new InvalidOperationException($"Invalid client ID used ({e.ClientId}). It must start with UnitTest name.")); + e.ReasonCode = MqttConnectReasonCode.ClientIdentifierNotValid; + } + } + + return CompletedTask.Instance; + }; + + return Server; + } + + public void Dispose() + { + try + { + lock (_clients) + { + foreach (var mqttClient in _clients) + { + try + { + //mqttClient.DisconnectAsync().GetAwaiter().GetResult(); + } + catch + { + // This can happen when the test already disconnected the client. + } + finally + { + mqttClient?.Dispose(); + } + } + + _clients.Clear(); + } + + lock (_lowLevelClients) + { + foreach (var lowLevelMqttClient in _lowLevelClients) + { + lowLevelMqttClient.Dispose(); + } + + _lowLevelClients.Clear(); + } + + try + { + Server?.StopAsync().GetAwaiter().GetResult(); + } + catch + { + // This can happen when the test already stopped the server. + } + finally + { + Server?.Dispose(); + } + + Server = null; + + ThrowIfLogErrors(); + + GC.Collect(); + GC.WaitForFullGCComplete(); + GC.WaitForPendingFinalizers(); + + if (_exceptions.Any()) + { + // ReSharper disable once ThrowExceptionInUnexpectedLocation + throw new Exception($"{_exceptions.Count} exceptions tracked.\r\n" + string.Join(Environment.NewLine, _exceptions)); + } + } + finally + { + TaskScheduler.UnobservedTaskException -= TrackUnobservedTaskException; } } + + public Task StartServer() + { + return StartServer(ServerFactory.CreateServerOptionsBuilder()); + } + + public async Task StartServer(MqttServerOptionsBuilder optionsBuilder) + { + optionsBuilder.WithDefaultEndpoint(); + optionsBuilder.WithDefaultEndpointPort(ServerPort); + optionsBuilder.WithMaxPendingMessagesPerClient(int.MaxValue); + + var options = optionsBuilder.Build(); + var server = CreateServer(options); + await server.StartAsync().ConfigureAwait(false); + + // The OS has chosen the port to we have to properly expose it to the tests. + ServerPort = options.DefaultEndpointOptions.Port; + return server; + } + + public async Task StartServer(Action configure) + { + var optionsBuilder = ServerFactory.CreateServerOptionsBuilder(); + + optionsBuilder.WithDefaultEndpoint(); + optionsBuilder.WithDefaultEndpointPort(ServerPort); + optionsBuilder.WithMaxPendingMessagesPerClient(int.MaxValue); + + configure?.Invoke(optionsBuilder); + + var options = optionsBuilder.Build(); + var server = CreateServer(options); + await server.StartAsync().ConfigureAwait(false); + + // The OS has chosen the port to we have to properly expose it to the tests. + ServerPort = options.DefaultEndpointOptions.Port; + return server; + } + + public void ThrowIfLogErrors() + { + if (!IgnoreServerLogErrors) + { + lock (_serverErrors) + { + if (_serverErrors.Count > 0) + { + var message = $"Server had {_serverErrors.Count} errors (${string.Join(Environment.NewLine, _serverErrors)})."; + Console.WriteLine(message); + throw new Exception(message); + } + } + } + + if (!IgnoreClientLogErrors) + { + lock (_clientErrors) + { + if (_clientErrors.Count > 0) + { + var message = $"Client(s) had {_clientErrors.Count} errors (${string.Join(Environment.NewLine, _clientErrors)})"; + Console.WriteLine(message); + throw new Exception(message); + } + } + } + } + + public void TrackException(Exception exception) + { + if (exception == null) + { + return; + } + + lock (_exceptions) + { + _exceptions.Add(exception); + } + } + + void TrackUnobservedTaskException(object sender, UnobservedTaskExceptionEventArgs e) + { + TrackException(e.Exception); + } } \ No newline at end of file diff --git a/Source/MQTTnet.Tests/Mockups/TestLogger.cs b/Source/MQTTnet.Tests/Mockups/TestLogger.cs index 4cec9d3c..ab2d535c 100644 --- a/Source/MQTTnet.Tests/Mockups/TestLogger.cs +++ b/Source/MQTTnet.Tests/Mockups/TestLogger.cs @@ -5,22 +5,21 @@ using System; using MQTTnet.Diagnostics.Logger; -namespace MQTTnet.Tests.Mockups +namespace MQTTnet.Tests.Mockups; + +public sealed class TestLogger : IMqttNetLogger { - public sealed class TestLogger : IMqttNetLogger + public event EventHandler LogMessagePublished; + + public bool IsEnabled { get; } = true; + + public void Publish(MqttNetLogLevel logLevel, string source, string message, object[] parameters, Exception exception) { - public event EventHandler LogMessagePublished; - - public bool IsEnabled { get; } = true; - - public void Publish(MqttNetLogLevel logLevel, string source, string message, object[] parameters, Exception exception) + LogMessagePublished?.Invoke(this, new MqttNetLogMessagePublishedEventArgs(new MqttNetLogMessage { - LogMessagePublished?.Invoke(this, new MqttNetLogMessagePublishedEventArgs(new MqttNetLogMessage - { - Level = logLevel, - Message = string.Format(message, parameters), - Exception = exception - })); - } + Level = logLevel, + Message = string.Format(message, parameters), + Exception = exception + })); } -} +} \ No newline at end of file diff --git a/Source/MQTTnet.Tests/MqttApplicationMessageBuilder_Tests.cs b/Source/MQTTnet.Tests/MqttApplicationMessageBuilder_Tests.cs index 139a3085..817d90df 100644 --- a/Source/MQTTnet.Tests/MqttApplicationMessageBuilder_Tests.cs +++ b/Source/MQTTnet.Tests/MqttApplicationMessageBuilder_Tests.cs @@ -8,58 +8,58 @@ using System.Text; using Microsoft.VisualStudio.TestTools.UnitTesting; using MQTTnet.Protocol; -namespace MQTTnet.Tests +namespace MQTTnet.Tests; + +// ReSharper disable InconsistentNaming +[TestClass] +public sealed class MqttApplicationMessageBuilder_Tests { - [TestClass] - public sealed class MqttApplicationMessageBuilder_Tests + [TestMethod] + public void CreateApplicationMessage_TopicOnly() { - [TestMethod] - public void CreateApplicationMessage_TopicOnly() - { - var message = new MqttApplicationMessageBuilder().WithTopic("Abc").Build(); - Assert.AreEqual("Abc", message.Topic); - Assert.IsFalse(message.Retain); - Assert.AreEqual(MqttQualityOfServiceLevel.AtMostOnce, message.QualityOfServiceLevel); - } - - [TestMethod] - public void CreateApplicationMessage_TimeStampPayload() - { - var message = new MqttApplicationMessageBuilder().WithTopic("xyz").WithPayload(TimeSpan.FromSeconds(360).ToString()).Build(); - Assert.AreEqual("xyz", message.Topic); - Assert.IsFalse(message.Retain); - Assert.AreEqual(MqttQualityOfServiceLevel.AtMostOnce, message.QualityOfServiceLevel); - Assert.AreEqual(Encoding.UTF8.GetString(message.Payload), "00:06:00"); - } - - [TestMethod] - public void CreateApplicationMessage_StreamPayload() - { - var stream = new MemoryStream(Encoding.UTF8.GetBytes("xHello")) { Position = 1 }; - - var message = new MqttApplicationMessageBuilder().WithTopic("123").WithPayload(stream).Build(); - Assert.AreEqual("123", message.Topic); - Assert.IsFalse(message.Retain); - Assert.AreEqual(MqttQualityOfServiceLevel.AtMostOnce, message.QualityOfServiceLevel); - Assert.AreEqual(Encoding.UTF8.GetString(message.Payload), "Hello"); - } - - [TestMethod] - public void CreateApplicationMessage_Retained() - { - var message = new MqttApplicationMessageBuilder().WithTopic("lol").WithRetainFlag().Build(); - Assert.AreEqual("lol", message.Topic); - Assert.IsTrue(message.Retain); - Assert.AreEqual(MqttQualityOfServiceLevel.AtMostOnce, message.QualityOfServiceLevel); - } - - [TestMethod] - public void CreateApplicationMessage_QosLevel2() - { - var message = new MqttApplicationMessageBuilder().WithTopic("rofl").WithRetainFlag().WithQualityOfServiceLevel(MqttQualityOfServiceLevel.ExactlyOnce).Build(); - Assert.AreEqual("rofl", message.Topic); - Assert.IsTrue(message.Retain); - Assert.AreEqual(MqttQualityOfServiceLevel.ExactlyOnce, message.QualityOfServiceLevel); - } + var message = new MqttApplicationMessageBuilder().WithTopic("Abc").Build(); + Assert.AreEqual("Abc", message.Topic); + Assert.IsFalse(message.Retain); + Assert.AreEqual(MqttQualityOfServiceLevel.AtMostOnce, message.QualityOfServiceLevel); } -} + + [TestMethod] + public void CreateApplicationMessage_TimeStampPayload() + { + var message = new MqttApplicationMessageBuilder().WithTopic("xyz").WithPayload(TimeSpan.FromSeconds(360).ToString()).Build(); + Assert.AreEqual("xyz", message.Topic); + Assert.IsFalse(message.Retain); + Assert.AreEqual(MqttQualityOfServiceLevel.AtMostOnce, message.QualityOfServiceLevel); + Assert.AreEqual(Encoding.UTF8.GetString(message.Payload), "00:06:00"); + } + + [TestMethod] + public void CreateApplicationMessage_StreamPayload() + { + var stream = new MemoryStream("xHello"u8.ToArray()) { Position = 1 }; + + var message = new MqttApplicationMessageBuilder().WithTopic("123").WithPayload(stream).Build(); + Assert.AreEqual("123", message.Topic); + Assert.IsFalse(message.Retain); + Assert.AreEqual(MqttQualityOfServiceLevel.AtMostOnce, message.QualityOfServiceLevel); + Assert.AreEqual(Encoding.UTF8.GetString(message.Payload), "Hello"); + } + + [TestMethod] + public void CreateApplicationMessage_Retained() + { + var message = new MqttApplicationMessageBuilder().WithTopic("lol").WithRetainFlag().Build(); + Assert.AreEqual("lol", message.Topic); + Assert.IsTrue(message.Retain); + Assert.AreEqual(MqttQualityOfServiceLevel.AtMostOnce, message.QualityOfServiceLevel); + } + + [TestMethod] + public void CreateApplicationMessage_QosLevel2() + { + var message = new MqttApplicationMessageBuilder().WithTopic("rofl").WithRetainFlag().WithQualityOfServiceLevel(MqttQualityOfServiceLevel.ExactlyOnce).Build(); + Assert.AreEqual("rofl", message.Topic); + Assert.IsTrue(message.Retain); + Assert.AreEqual(MqttQualityOfServiceLevel.ExactlyOnce, message.QualityOfServiceLevel); + } +} \ No newline at end of file diff --git a/Source/MQTTnet.Tests/MqttApplicationMessageValidator_Tests.cs b/Source/MQTTnet.Tests/MqttApplicationMessageValidator_Tests.cs index cb47d499..1674a8ff 100644 --- a/Source/MQTTnet.Tests/MqttApplicationMessageValidator_Tests.cs +++ b/Source/MQTTnet.Tests/MqttApplicationMessageValidator_Tests.cs @@ -6,39 +6,39 @@ using System; using Microsoft.VisualStudio.TestTools.UnitTesting; using MQTTnet.Formatter; -namespace MQTTnet.Tests +namespace MQTTnet.Tests; + +// ReSharper disable InconsistentNaming +[TestClass] +public sealed class MqttApplicationMessageValidator_Tests { - [TestClass] - public sealed class MqttApplicationMessageValidator_Tests + [TestMethod] + [ExpectedException(typeof(NotSupportedException))] + public void Succeed_When_Using_TopicAlias_And_MQTT_311() { - [TestMethod] - [ExpectedException(typeof(NotSupportedException))] - public void Succeed_When_Using_TopicAlias_And_MQTT_311() - { - MqttApplicationMessageValidator.ThrowIfNotSupported(new MqttApplicationMessageBuilder().WithTopicAlias(1).Build(), MqttProtocolVersion.V311); - } + MqttApplicationMessageValidator.ThrowIfNotSupported(new MqttApplicationMessageBuilder().WithTopicAlias(1).Build(), MqttProtocolVersion.V311); + } - [TestMethod] - public void Succeed_When_Using_TopicAlias_And_MQTT_500() - { - MqttApplicationMessageValidator.ThrowIfNotSupported(new MqttApplicationMessageBuilder().WithTopicAlias(1).Build(), MqttProtocolVersion.V500); - } + [TestMethod] + public void Succeed_When_Using_TopicAlias_And_MQTT_500() + { + MqttApplicationMessageValidator.ThrowIfNotSupported(new MqttApplicationMessageBuilder().WithTopicAlias(1).Build(), MqttProtocolVersion.V500); + } - [TestMethod] - public void Succeed_When_Using_UserProperties_And_MQTT_500() - { - MqttApplicationMessageValidator.ThrowIfNotSupported( - new MqttApplicationMessageBuilder().WithTopic("A").WithUserProperty("User", "Property").Build(), - MqttProtocolVersion.V500); - } + [TestMethod] + public void Succeed_When_Using_UserProperties_And_MQTT_500() + { + MqttApplicationMessageValidator.ThrowIfNotSupported( + new MqttApplicationMessageBuilder().WithTopic("A").WithUserProperty("User", "Property").Build(), + MqttProtocolVersion.V500); + } - [TestMethod] - [ExpectedException(typeof(NotSupportedException))] - public void Succeed_When_Using_WillUserProperties_And_MQTT_311() - { - MqttApplicationMessageValidator.ThrowIfNotSupported( - new MqttApplicationMessageBuilder().WithTopic("B").WithUserProperty("User", "Property").Build(), - MqttProtocolVersion.V311); - } + [TestMethod] + [ExpectedException(typeof(NotSupportedException))] + public void Succeed_When_Using_WillUserProperties_And_MQTT_311() + { + MqttApplicationMessageValidator.ThrowIfNotSupported( + new MqttApplicationMessageBuilder().WithTopic("B").WithUserProperty("User", "Property").Build(), + MqttProtocolVersion.V311); } } \ No newline at end of file diff --git a/Source/MQTTnet.Tests/MqttClientOptionsValidator_Tests.cs b/Source/MQTTnet.Tests/MqttClientOptionsValidator_Tests.cs index c7b6ab23..e15145a4 100644 --- a/Source/MQTTnet.Tests/MqttClientOptionsValidator_Tests.cs +++ b/Source/MQTTnet.Tests/MqttClientOptionsValidator_Tests.cs @@ -6,48 +6,48 @@ using System; using Microsoft.VisualStudio.TestTools.UnitTesting; using MQTTnet.Formatter; -namespace MQTTnet.Tests +namespace MQTTnet.Tests; + +// ReSharper disable InconsistentNaming +[TestClass] +public sealed class MqttClientOptionsValidator_Tests { - [TestClass] - public sealed class MqttClientOptionsValidator_Tests + [TestMethod] + public void Succeed_When_Using_UserProperties_And_MQTT_500() { - [TestMethod] - public void Succeed_When_Using_UserProperties_And_MQTT_500() - { - new MqttClientOptionsBuilder().WithProtocolVersion(MqttProtocolVersion.V500).WithUserProperty("User", "Property").WithTcpServer("FAKE").Build(); - } + new MqttClientOptionsBuilder().WithProtocolVersion(MqttProtocolVersion.V500).WithUserProperty("User", "Property").WithTcpServer("FAKE").Build(); + } - [TestMethod] - [ExpectedException(typeof(NotSupportedException))] - public void Succeed_When_Using_WillUserProperties_And_MQTT_311() - { - new MqttClientOptionsBuilder().WithProtocolVersion(MqttProtocolVersion.V311).WithWillUserProperty("User", "Property").WithTcpServer("FAKE").Build(); - } + [TestMethod] + [ExpectedException(typeof(NotSupportedException))] + public void Succeed_When_Using_WillUserProperties_And_MQTT_311() + { + new MqttClientOptionsBuilder().WithProtocolVersion(MqttProtocolVersion.V311).WithWillUserProperty("User", "Property").WithTcpServer("FAKE").Build(); + } - [TestMethod] - public void Succeed_When_Using_WillUserProperties_And_MQTT_500() - { - new MqttClientOptionsBuilder().WithProtocolVersion(MqttProtocolVersion.V500).WithWillUserProperty("User", "Property").WithTcpServer("FAKE").Build(); - } + [TestMethod] + public void Succeed_When_Using_WillUserProperties_And_MQTT_500() + { + new MqttClientOptionsBuilder().WithProtocolVersion(MqttProtocolVersion.V500).WithWillUserProperty("User", "Property").WithTcpServer("FAKE").Build(); + } - [TestMethod] - [ExpectedException(typeof(NotSupportedException))] - public void Throw_When_Using_UserProperties_And_MQTT_311() - { - new MqttClientOptionsBuilder().WithProtocolVersion(MqttProtocolVersion.V311).WithUserProperty("User", "Property").WithTcpServer("FAKE").Build(); - } + [TestMethod] + [ExpectedException(typeof(NotSupportedException))] + public void Throw_When_Using_UserProperties_And_MQTT_311() + { + new MqttClientOptionsBuilder().WithProtocolVersion(MqttProtocolVersion.V311).WithUserProperty("User", "Property").WithTcpServer("FAKE").Build(); + } - [TestMethod] - [ExpectedException(typeof(NotSupportedException))] - public void Throw_When_Using_WithRequestResponseInformation_And_MQTT_311() - { - new MqttClientOptionsBuilder().WithProtocolVersion(MqttProtocolVersion.V311).WithRequestResponseInformation().WithTcpServer("FAKE").Build(); - } + [TestMethod] + [ExpectedException(typeof(NotSupportedException))] + public void Throw_When_Using_WithRequestResponseInformation_And_MQTT_311() + { + new MqttClientOptionsBuilder().WithProtocolVersion(MqttProtocolVersion.V311).WithRequestResponseInformation().WithTcpServer("FAKE").Build(); + } - [TestMethod] - public void Throw_When_Using_WithRequestResponseInformation_And_MQTT_500() - { - new MqttClientOptionsBuilder().WithProtocolVersion(MqttProtocolVersion.V500).WithRequestResponseInformation().WithTcpServer("FAKE").Build(); - } + [TestMethod] + public void Throw_When_Using_WithRequestResponseInformation_And_MQTT_500() + { + new MqttClientOptionsBuilder().WithProtocolVersion(MqttProtocolVersion.V500).WithRequestResponseInformation().WithTcpServer("FAKE").Build(); } } \ No newline at end of file diff --git a/Source/MQTTnet.Tests/MqttPacketIdentifierProvider_Tests.cs b/Source/MQTTnet.Tests/MqttPacketIdentifierProvider_Tests.cs index 0c69624c..32c5775d 100644 --- a/Source/MQTTnet.Tests/MqttPacketIdentifierProvider_Tests.cs +++ b/Source/MQTTnet.Tests/MqttPacketIdentifierProvider_Tests.cs @@ -4,32 +4,32 @@ using Microsoft.VisualStudio.TestTools.UnitTesting; -namespace MQTTnet.Tests +namespace MQTTnet.Tests; + +// ReSharper disable InconsistentNaming +[TestClass] +public class MqttPacketIdentifierProvider_Tests { - [TestClass] - public class MqttPacketIdentifierProvider_Tests + [TestMethod] + public void Reset() { - [TestMethod] - public void Reset() - { - var p = new MqttPacketIdentifierProvider(); - Assert.AreEqual(1, p.GetNextPacketIdentifier()); - Assert.AreEqual(2, p.GetNextPacketIdentifier()); - p.Reset(); - Assert.AreEqual(1, p.GetNextPacketIdentifier()); - } - - [TestMethod] - public void ReachBoundaries() - { - var p = new MqttPacketIdentifierProvider(); - - for (ushort i = 0; i < ushort.MaxValue; i++) - { - Assert.AreEqual(i + 1, p.GetNextPacketIdentifier()); - } - - Assert.AreEqual(1, p.GetNextPacketIdentifier()); - } + var p = new MqttPacketIdentifierProvider(); + Assert.AreEqual(1, p.GetNextPacketIdentifier()); + Assert.AreEqual(2, p.GetNextPacketIdentifier()); + p.Reset(); + Assert.AreEqual(1, p.GetNextPacketIdentifier()); } -} + + [TestMethod] + public void ReachBoundaries() + { + var p = new MqttPacketIdentifierProvider(); + + for (ushort i = 0; i < ushort.MaxValue; i++) + { + Assert.AreEqual(i + 1, p.GetNextPacketIdentifier()); + } + + Assert.AreEqual(1, p.GetNextPacketIdentifier()); + } +} \ No newline at end of file diff --git a/Source/MQTTnet.Tests/MqttPacketWriter_Tests.cs b/Source/MQTTnet.Tests/MqttPacketWriter_Tests.cs index 0a3d3f5e..fc4d8453 100644 --- a/Source/MQTTnet.Tests/MqttPacketWriter_Tests.cs +++ b/Source/MQTTnet.Tests/MqttPacketWriter_Tests.cs @@ -5,30 +5,30 @@ using Microsoft.VisualStudio.TestTools.UnitTesting; using MQTTnet.Formatter; -namespace MQTTnet.Tests +namespace MQTTnet.Tests; + +// ReSharper disable InconsistentNaming +[TestClass] +public class MqttPacketWriter_Tests { - [TestClass] - public class MqttPacketWriter_Tests + protected virtual MqttBufferWriter WriterFactory() { - protected virtual MqttBufferWriter WriterFactory() - { - return new MqttBufferWriter(4096, 65535); - } - - [TestMethod] - public void WritePacket() - { - var writer = WriterFactory(); - Assert.AreEqual(0, writer.Length); - - writer.WriteString("1234567890"); - Assert.AreEqual(10 + 2, writer.Length); - - writer.WriteBinary(new byte[300]); - Assert.AreEqual(300 + 2 + 12, writer.Length); - - writer.WriteBinary(new byte[5000]); - Assert.AreEqual(5000 + 2 + 300 + 2 + 12, writer.Length); - } + return new MqttBufferWriter(4096, 65535); } -} + + [TestMethod] + public void WritePacket() + { + var writer = WriterFactory(); + Assert.AreEqual(0, writer.Length); + + writer.WriteString("1234567890"); + Assert.AreEqual(10 + 2, writer.Length); + + writer.WriteBinary(new byte[300]); + Assert.AreEqual(300 + 2 + 12, writer.Length); + + writer.WriteBinary(new byte[5000]); + Assert.AreEqual(5000 + 2 + 300 + 2 + 12, writer.Length); + } +} \ No newline at end of file diff --git a/Source/MQTTnet.Tests/MqttTcpChannel_Tests.cs b/Source/MQTTnet.Tests/MqttTcpChannel_Tests.cs index caad82f6..8af3050c 100644 --- a/Source/MQTTnet.Tests/MqttTcpChannel_Tests.cs +++ b/Source/MQTTnet.Tests/MqttTcpChannel_Tests.cs @@ -12,6 +12,7 @@ using MQTTnet.Implementations; namespace MQTTnet.Tests; +// ReSharper disable InconsistentNaming [TestClass] public class MqttTcpChannel_Tests { diff --git a/Source/MQTTnet.Tests/MqttTopicValidatorSubscribe_Tests.cs b/Source/MQTTnet.Tests/MqttTopicValidatorSubscribe_Tests.cs index 13394a71..8f07d068 100644 --- a/Source/MQTTnet.Tests/MqttTopicValidatorSubscribe_Tests.cs +++ b/Source/MQTTnet.Tests/MqttTopicValidatorSubscribe_Tests.cs @@ -6,53 +6,53 @@ using Microsoft.VisualStudio.TestTools.UnitTesting; using MQTTnet.Exceptions; using MQTTnet.Protocol; -namespace MQTTnet.Tests +namespace MQTTnet.Tests; + +// ReSharper disable InconsistentNaming +[TestClass] +public class MqttTopicValidatorSubscribe_Tests { - [TestClass] - public class MqttTopicValidatorSubscribe_Tests + [TestMethod] + public void Valid_Topic() { - [TestMethod] - public void Valid_Topic() - { - MqttTopicValidator.ThrowIfInvalidSubscribe("/a/b/c"); - } - - [TestMethod] - public void Valid_Topic_Plus_In_Between() - { - MqttTopicValidator.ThrowIfInvalidSubscribe("/a/+/c"); - } - - [TestMethod] - public void Valid_Topic_Plus_Last_Char() - { - MqttTopicValidator.ThrowIfInvalidSubscribe("/a/+"); - } - - [TestMethod] - public void Valid_Topic_Hash_Last_Char() - { - MqttTopicValidator.ThrowIfInvalidSubscribe("/a/#"); - } - - [TestMethod] - public void Valid_Topic_Only_Hash() - { - MqttTopicValidator.ThrowIfInvalidSubscribe("#"); - } - - [TestMethod] - [ExpectedException(typeof(MqttProtocolViolationException))] - public void Invalid_Topic_Hash_In_Between() - { - MqttTopicValidator.ThrowIfInvalidSubscribe("/a/#/c"); - } - - [TestMethod] - [ExpectedException(typeof(MqttProtocolViolationException))] - public void Invalid_Topic_Empty() - { - MqttTopicValidator.ThrowIfInvalidSubscribe(string.Empty); - } + MqttTopicValidator.ThrowIfInvalidSubscribe("/a/b/c"); } -} + + [TestMethod] + public void Valid_Topic_Plus_In_Between() + { + MqttTopicValidator.ThrowIfInvalidSubscribe("/a/+/c"); + } + + [TestMethod] + public void Valid_Topic_Plus_Last_Char() + { + MqttTopicValidator.ThrowIfInvalidSubscribe("/a/+"); + } + + [TestMethod] + public void Valid_Topic_Hash_Last_Char() + { + MqttTopicValidator.ThrowIfInvalidSubscribe("/a/#"); + } + + [TestMethod] + public void Valid_Topic_Only_Hash() + { + MqttTopicValidator.ThrowIfInvalidSubscribe("#"); + } + + [TestMethod] + [ExpectedException(typeof(MqttProtocolViolationException))] + public void Invalid_Topic_Hash_In_Between() + { + MqttTopicValidator.ThrowIfInvalidSubscribe("/a/#/c"); + } + + [TestMethod] + [ExpectedException(typeof(MqttProtocolViolationException))] + public void Invalid_Topic_Empty() + { + MqttTopicValidator.ThrowIfInvalidSubscribe(string.Empty); + } +} \ No newline at end of file diff --git a/Source/MQTTnet.Tests/MqttTopicValidator_Tests.cs b/Source/MQTTnet.Tests/MqttTopicValidator_Tests.cs index bd22742d..26fa2323 100644 --- a/Source/MQTTnet.Tests/MqttTopicValidator_Tests.cs +++ b/Source/MQTTnet.Tests/MqttTopicValidator_Tests.cs @@ -6,36 +6,36 @@ using Microsoft.VisualStudio.TestTools.UnitTesting; using MQTTnet.Exceptions; using MQTTnet.Protocol; -namespace MQTTnet.Tests +namespace MQTTnet.Tests; + +// ReSharper disable InconsistentNaming +[TestClass] +public sealed class MqttTopicValidator_Tests { - [TestClass] - public sealed class MqttTopicValidator_Tests + [TestMethod] + [ExpectedException(typeof(MqttProtocolViolationException))] + public void Invalid_Topic_Empty() { - [TestMethod] - [ExpectedException(typeof(MqttProtocolViolationException))] - public void Invalid_Topic_Empty() - { - MqttTopicValidator.ThrowIfInvalid(string.Empty); - } + MqttTopicValidator.ThrowIfInvalid(string.Empty); + } - [TestMethod] - [ExpectedException(typeof(MqttProtocolViolationException))] - public void Invalid_Topic_Hash() - { - MqttTopicValidator.ThrowIfInvalid("/a/#/c"); - } + [TestMethod] + [ExpectedException(typeof(MqttProtocolViolationException))] + public void Invalid_Topic_Hash() + { + MqttTopicValidator.ThrowIfInvalid("/a/#/c"); + } - [TestMethod] - [ExpectedException(typeof(MqttProtocolViolationException))] - public void Invalid_Topic_Plus() - { - MqttTopicValidator.ThrowIfInvalid("/a/+/c"); - } + [TestMethod] + [ExpectedException(typeof(MqttProtocolViolationException))] + public void Invalid_Topic_Plus() + { + MqttTopicValidator.ThrowIfInvalid("/a/+/c"); + } - [TestMethod] - public void Valid_Topic() - { - MqttTopicValidator.ThrowIfInvalid("/a/b/c"); - } + [TestMethod] + public void Valid_Topic() + { + MqttTopicValidator.ThrowIfInvalid("/a/b/c"); } } \ No newline at end of file diff --git a/Source/MQTTnet.Tests/Protocol_Tests.cs b/Source/MQTTnet.Tests/Protocol_Tests.cs index 6f5146c8..30a57986 100644 --- a/Source/MQTTnet.Tests/Protocol_Tests.cs +++ b/Source/MQTTnet.Tests/Protocol_Tests.cs @@ -5,51 +5,51 @@ using Microsoft.VisualStudio.TestTools.UnitTesting; using MQTTnet.Formatter; -namespace MQTTnet.Tests +namespace MQTTnet.Tests; + +// ReSharper disable InconsistentNaming +[TestClass] +public sealed class Protocol_Tests { - [TestClass] - public sealed class Protocol_Tests + [TestMethod] + public void Encode_Four_Byte_Integer() { - [TestMethod] - public void Encode_Four_Byte_Integer() + var writer = new MqttBufferWriter(4, 4); + + for (uint value = 0; value < 268435455; value++) { - var writer = new MqttBufferWriter(4, 4); + writer.WriteVariableByteInteger(value); - for (uint value = 0; value < 268435455; value++) - { - writer.WriteVariableByteInteger(value); + var buffer = writer.GetBuffer(); - var buffer = writer.GetBuffer(); + var reader = new MqttBufferReader(); + reader.SetBuffer(buffer, 0, writer.Length); + var checkValue = reader.ReadVariableByteInteger(); - var reader = new MqttBufferReader(); - reader.SetBuffer(buffer, 0, writer.Length); - var checkValue = reader.ReadVariableByteInteger(); + Assert.AreEqual(value, checkValue); - Assert.AreEqual(value, checkValue); - - writer.Reset(0); - } + writer.Reset(0); } + } - [TestMethod] - public void Encode_Two_Byte_Integer() + [TestMethod] + public void Encode_Two_Byte_Integer() + { + var writer = new MqttBufferWriter(2, 2); + + for (ushort value = 0; value < ushort.MaxValue; value++) { - var writer = new MqttBufferWriter(2, 2); + writer.WriteTwoByteInteger(value); - for (ushort value = 0; value < ushort.MaxValue; value++) - { - writer.WriteTwoByteInteger(value); + var buffer = writer.GetBuffer(); - var buffer = writer.GetBuffer(); + var reader = new MqttBufferReader(); + reader.SetBuffer(buffer, 0, writer.Length); + var checkValue = reader.ReadTwoByteInteger(); - var reader = new MqttBufferReader(); - reader.SetBuffer(buffer, 0, writer.Length); - var checkValue = reader.ReadTwoByteInteger(); + Assert.AreEqual(value, checkValue); - Assert.AreEqual(value, checkValue); - - writer.Reset(0); - } + writer.Reset(0); } } } \ No newline at end of file diff --git a/Source/MQTTnet.Tests/RoundtripTime_Tests.cs b/Source/MQTTnet.Tests/RoundtripTime_Tests.cs index b1359bc3..d3d99f0e 100644 --- a/Source/MQTTnet.Tests/RoundtripTime_Tests.cs +++ b/Source/MQTTnet.Tests/RoundtripTime_Tests.cs @@ -10,52 +10,50 @@ using Microsoft.VisualStudio.TestTools.UnitTesting; using MQTTnet.Internal; using MQTTnet.Tests.Mockups; -namespace MQTTnet.Tests +namespace MQTTnet.Tests; + +// ReSharper disable InconsistentNaming +[TestClass] +public class RoundtripTime_Tests { - [TestClass] - public class RoundtripTime_Tests + public TestContext TestContext { get; set; } + + [TestMethod] + public async Task Round_Trip_Time() { - public TestContext TestContext { get; set; } + using var testEnvironment = new TestEnvironment(TestContext); + await testEnvironment.StartServer(); - [TestMethod] - public async Task Round_Trip_Time() + var receiverClient = await testEnvironment.ConnectClient(); + var senderClient = await testEnvironment.ConnectClient(); + + TaskCompletionSource response = null; + + receiverClient.ApplicationMessageReceivedAsync += e => { - using (var testEnvironment = new TestEnvironment(TestContext)) + response?.TrySetResult(e.ApplicationMessage.ConvertPayloadToString()); + return CompletedTask.Instance; + }; + + await receiverClient.SubscribeAsync("#"); + + var times = new List(); + var stopwatch = Stopwatch.StartNew(); + + await Task.Delay(1000); + + for (var i = 0; i < 100; i++) + { + response = new TaskCompletionSource(); + await senderClient.PublishStringAsync("test", DateTime.UtcNow.Ticks.ToString()); + if (!response.Task.Wait(TimeSpan.FromSeconds(5))) { - await testEnvironment.StartServer(); - - var receiverClient = await testEnvironment.ConnectClient(); - var senderClient = await testEnvironment.ConnectClient(); - - TaskCompletionSource response = null; - - receiverClient.ApplicationMessageReceivedAsync += e => - { - response?.TrySetResult(e.ApplicationMessage.ConvertPayloadToString()); - return CompletedTask.Instance; - }; - - await receiverClient.SubscribeAsync("#"); - - var times = new List(); - var stopwatch = Stopwatch.StartNew(); - - await Task.Delay(1000); - - for (var i = 0; i < 100; i++) - { - response = new TaskCompletionSource(); - await senderClient.PublishStringAsync("test", DateTime.UtcNow.Ticks.ToString()); - if (!response.Task.Wait(TimeSpan.FromSeconds(5))) - { - throw new TimeoutException(); - } - - stopwatch.Stop(); - times.Add(stopwatch.Elapsed); - stopwatch.Restart(); - } + throw new TimeoutException(); } + + stopwatch.Stop(); + times.Add(stopwatch.Elapsed); + stopwatch.Restart(); } } -} +} \ No newline at end of file diff --git a/Source/MQTTnet.Tests/Server/Assigned_Client_ID_Tests.cs b/Source/MQTTnet.Tests/Server/Assigned_Client_ID_Tests.cs index 69ed3887..798a16fa 100644 --- a/Source/MQTTnet.Tests/Server/Assigned_Client_ID_Tests.cs +++ b/Source/MQTTnet.Tests/Server/Assigned_Client_ID_Tests.cs @@ -9,84 +9,82 @@ using MQTTnet.Formatter; using MQTTnet.Internal; using MQTTnet.Protocol; -namespace MQTTnet.Tests.Server +namespace MQTTnet.Tests.Server; + +// ReSharper disable InconsistentNaming +[TestClass] +public sealed class Assigned_Client_ID_Tests : BaseTestClass { - [TestClass] - public sealed class Assigned_Client_ID_Tests : BaseTestClass + [TestMethod] + public Task Connect_With_No_Client_Id() { - [TestMethod] - public Task Connect_With_No_Client_Id() - { - return Connect_With_Client_Id("test_123", "test_123", null, "test_123"); - } + return Connect_With_Client_Id("test_123", "test_123", null, "test_123"); + } - [TestMethod] - public Task Connect_With_Client_Id() - { - return Connect_With_Client_Id("Connect_With_Client_Id_test_456", null, "test_456", null); - } + [TestMethod] + public Task Connect_With_Client_Id() + { + return Connect_With_Client_Id("Connect_With_Client_Id_test_456", null, "test_456", null); + } - async Task Connect_With_Client_Id(string expectedClientId, string expectedReturnedClientId, string usedClientId, string assignedClientId) + async Task Connect_With_Client_Id(string expectedClientId, string expectedReturnedClientId, string usedClientId, string assignedClientId) + { + using var testEnvironment = CreateTestEnvironment(); + string serverConnectedClientId = null; + string serverDisconnectedClientId = null; + string clientAssignedClientId = null; + + // Arrange server + var disconnectedMre = new ManualResetEventSlim(); + + var server = await testEnvironment.StartServer(); + server.ValidatingConnectionAsync += e => { - using (var testEnvironment = CreateTestEnvironment()) + if (string.IsNullOrEmpty(e.ClientId)) { - string serverConnectedClientId = null; - string serverDisconnectedClientId = null; - string clientAssignedClientId = null; - - // Arrange server - var disconnectedMre = new ManualResetEventSlim(); - - var server = await testEnvironment.StartServer(); - server.ValidatingConnectionAsync += e => - { - if (string.IsNullOrEmpty(e.ClientId)) - { - e.AssignedClientIdentifier = assignedClientId; - e.ReasonCode = MqttConnectReasonCode.Success; - } - - return CompletedTask.Instance; - }; - - testEnvironment.Server.ClientConnectedAsync += args => - { - serverConnectedClientId = args.ClientId; - return CompletedTask.Instance; - }; - - testEnvironment.Server.ClientDisconnectedAsync += args => - { - serverDisconnectedClientId = args.ClientId; - disconnectedMre.Set(); - return CompletedTask.Instance; - }; - - // Arrange client - var client = testEnvironment.CreateClient(); - client.ConnectedAsync += args => - { - clientAssignedClientId = args.ConnectResult.AssignedClientIdentifier; - return CompletedTask.Instance; - }; - - // Act - await client.ConnectAsync(new MqttClientOptionsBuilder() - .WithTcpServer("127.0.0.1", testEnvironment.ServerPort) - .WithProtocolVersion(MqttProtocolVersion.V500) - .WithClientId(usedClientId) - .Build()); - - await client.DisconnectAsync(); - - // Wait for ClientDisconnectedHandler to trigger - disconnectedMre.Wait(1000); - - // Assert - Assert.AreEqual(expectedClientId, serverConnectedClientId); - Assert.AreEqual(expectedClientId, serverDisconnectedClientId); - Assert.AreEqual(expectedReturnedClientId, clientAssignedClientId); + e.AssignedClientIdentifier = assignedClientId; + e.ReasonCode = MqttConnectReasonCode.Success; } - } + + return CompletedTask.Instance; + }; + + testEnvironment.Server.ClientConnectedAsync += args => + { + serverConnectedClientId = args.ClientId; + return CompletedTask.Instance; + }; + + testEnvironment.Server.ClientDisconnectedAsync += args => + { + serverDisconnectedClientId = args.ClientId; + disconnectedMre.Set(); + return CompletedTask.Instance; + }; + + // Arrange client + var client = testEnvironment.CreateClient(); + client.ConnectedAsync += args => + { + clientAssignedClientId = args.ConnectResult.AssignedClientIdentifier; + return CompletedTask.Instance; + }; + + // Act + await client.ConnectAsync(new MqttClientOptionsBuilder() + .WithTcpServer("127.0.0.1", testEnvironment.ServerPort) + .WithProtocolVersion(MqttProtocolVersion.V500) + .WithClientId(usedClientId) + .Build()); + + await client.DisconnectAsync(); + + // Wait for ClientDisconnectedHandler to trigger + disconnectedMre.Wait(1000); + + // Assert + Assert.AreEqual(expectedClientId, serverConnectedClientId); + Assert.AreEqual(expectedClientId, serverDisconnectedClientId); + Assert.AreEqual(expectedReturnedClientId, clientAssignedClientId); } } \ No newline at end of file diff --git a/Source/MQTTnet.Tests/Server/Connection_Tests.cs b/Source/MQTTnet.Tests/Server/Connection_Tests.cs index ef2a482c..3b44df66 100644 --- a/Source/MQTTnet.Tests/Server/Connection_Tests.cs +++ b/Source/MQTTnet.Tests/Server/Connection_Tests.cs @@ -5,78 +5,73 @@ using System; using System.Net; using System.Net.Sockets; -using System.Text; using System.Threading; using System.Threading.Tasks; using Microsoft.VisualStudio.TestTools.UnitTesting; using MQTTnet.Implementations; using MQTTnet.Server; -namespace MQTTnet.Tests.Server +namespace MQTTnet.Tests.Server; + +// ReSharper disable InconsistentNaming +[TestClass] +public sealed class Connection_Tests : BaseTestClass { - [TestClass] - public sealed class Connection_Tests : BaseTestClass + [TestMethod] + public async Task Close_Idle_Connection_On_Connect() { - [TestMethod] - public async Task Close_Idle_Connection_On_Connect() + using var testEnvironment = CreateTestEnvironment(); + await testEnvironment.StartServer(new MqttServerOptionsBuilder().WithDefaultCommunicationTimeout(TimeSpan.FromSeconds(1))); + + var client = new CrossPlatformSocket(AddressFamily.InterNetwork, ProtocolType.Tcp); + await client.ConnectAsync(new DnsEndPoint("localhost", testEnvironment.ServerPort), CancellationToken.None); + + // Don't send anything. The server should close the connection. + await Task.Delay(TimeSpan.FromSeconds(3)); + + try { - using (var testEnvironment = CreateTestEnvironment()) + var receivedBytes = await client.ReceiveAsync(new ArraySegment(new byte[10]), SocketFlags.Partial); + if (receivedBytes == 0) { - await testEnvironment.StartServer(new MqttServerOptionsBuilder().WithDefaultCommunicationTimeout(TimeSpan.FromSeconds(1))); - - var client = new CrossPlatformSocket(AddressFamily.InterNetwork, ProtocolType.Tcp); - await client.ConnectAsync(new DnsEndPoint("localhost", testEnvironment.ServerPort), CancellationToken.None); - - // Don't send anything. The server should close the connection. - await Task.Delay(TimeSpan.FromSeconds(3)); - - try - { - var receivedBytes = await client.ReceiveAsync(new ArraySegment(new byte[10]), SocketFlags.Partial); - if (receivedBytes == 0) - { - return; - } - - Assert.Fail("Receive should throw an exception."); - } - catch (SocketException) - { - } + return; } + + Assert.Fail("Receive should throw an exception."); } - - [TestMethod] - public async Task Send_Garbage() + catch (SocketException) { - using (var testEnvironment = CreateTestEnvironment()) + } + } + + [TestMethod] + public async Task Send_Garbage() + { + using var testEnvironment = CreateTestEnvironment(); + await testEnvironment.StartServer(new MqttServerOptionsBuilder().WithDefaultCommunicationTimeout(TimeSpan.FromSeconds(1))); + + // Send an invalid packet and ensure that the server will close the connection and stay in a waiting state + // forever. This is security related. + var client = new CrossPlatformSocket(AddressFamily.InterNetwork, ProtocolType.Tcp); + await client.ConnectAsync(new DnsEndPoint("localhost", testEnvironment.ServerPort), CancellationToken.None); + + var buffer = "Garbage"u8.ToArray(); + await client.SendAsync(new ArraySegment(buffer), SocketFlags.None); + + await Task.Delay(TimeSpan.FromSeconds(3)); + + try + { + var receivedBytes = await client.ReceiveAsync(new ArraySegment(new byte[10]), SocketFlags.Partial); + if (receivedBytes == 0) { - await testEnvironment.StartServer(new MqttServerOptionsBuilder().WithDefaultCommunicationTimeout(TimeSpan.FromSeconds(1))); - - // Send an invalid packet and ensure that the server will close the connection and stay in a waiting state - // forever. This is security related. - var client = new CrossPlatformSocket(AddressFamily.InterNetwork, ProtocolType.Tcp); - await client.ConnectAsync(new DnsEndPoint("localhost", testEnvironment.ServerPort), CancellationToken.None); - - var buffer = Encoding.UTF8.GetBytes("Garbage"); - await client.SendAsync(new ArraySegment(buffer), SocketFlags.None); - - await Task.Delay(TimeSpan.FromSeconds(3)); - - try - { - var receivedBytes = await client.ReceiveAsync(new ArraySegment(new byte[10]), SocketFlags.Partial); - if (receivedBytes == 0) - { - return; - } - - Assert.Fail("Receive should throw an exception."); - } - catch (SocketException) - { - } + return; } + + Assert.Fail("Receive should throw an exception."); + } + catch (SocketException) + { } } } \ No newline at end of file diff --git a/Source/MQTTnet.Tests/Server/Cross_Version_Tests.cs b/Source/MQTTnet.Tests/Server/Cross_Version_Tests.cs index 83a30ecf..585730d7 100644 --- a/Source/MQTTnet.Tests/Server/Cross_Version_Tests.cs +++ b/Source/MQTTnet.Tests/Server/Cross_Version_Tests.cs @@ -4,63 +4,59 @@ using System.Threading.Tasks; using Microsoft.VisualStudio.TestTools.UnitTesting; using MQTTnet.Formatter; -namespace MQTTnet.Tests.Server +namespace MQTTnet.Tests.Server; + +// ReSharper disable InconsistentNaming +[TestClass] +public sealed class Cross_Version_Tests : BaseTestClass { - [TestClass] - public sealed class Cross_Version_Tests : BaseTestClass + [TestMethod] + public async Task Send_V311_Receive_V500() { - [TestMethod] - public async Task Send_V311_Receive_V500() - { - using (var testEnvironment = CreateTestEnvironment()) - { - await testEnvironment.StartServer(); + using var testEnvironment = CreateTestEnvironment(); + await testEnvironment.StartServer(); - var receiver = await testEnvironment.ConnectClient(o => o.WithProtocolVersion(MqttProtocolVersion.V500)); - var receivedApplicationMessages = testEnvironment.CreateApplicationMessageHandler(receiver); - await receiver.SubscribeAsync("#"); + var receiver = await testEnvironment.ConnectClient(o => o.WithProtocolVersion(MqttProtocolVersion.V500)); + var receivedApplicationMessages = testEnvironment.CreateApplicationMessageHandler(receiver); + await receiver.SubscribeAsync("#"); - var sender = await testEnvironment.ConnectClient(); + var sender = await testEnvironment.ConnectClient(); - var applicationMessage = new MqttApplicationMessageBuilder().WithTopic("My/Message").WithPayload("My_Payload").Build(); - await sender.PublishAsync(applicationMessage); + var applicationMessage = new MqttApplicationMessageBuilder().WithTopic("My/Message").WithPayload("My_Payload").Build(); + await sender.PublishAsync(applicationMessage); - await LongTestDelay(); + await LongTestDelay(); - Assert.AreEqual(1, receivedApplicationMessages.ReceivedEventArgs.Count); - Assert.AreEqual("My/Message", receivedApplicationMessages.ReceivedEventArgs.First().ApplicationMessage.Topic); - Assert.AreEqual("My_Payload", receivedApplicationMessages.ReceivedEventArgs.First().ApplicationMessage.ConvertPayloadToString()); - } - } + Assert.AreEqual(1, receivedApplicationMessages.ReceivedEventArgs.Count); + Assert.AreEqual("My/Message", receivedApplicationMessages.ReceivedEventArgs.First().ApplicationMessage.Topic); + Assert.AreEqual("My_Payload", receivedApplicationMessages.ReceivedEventArgs.First().ApplicationMessage.ConvertPayloadToString()); + } - [TestMethod] - public async Task Send_V500_Receive_V311() - { - using (var testEnvironment = CreateTestEnvironment(MqttProtocolVersion.V500)) - { - await testEnvironment.StartServer(); + [TestMethod] + public async Task Send_V500_Receive_V311() + { + using var testEnvironment = CreateTestEnvironment(MqttProtocolVersion.V500); + await testEnvironment.StartServer(); - var receiver = await testEnvironment.ConnectClient(o => o.WithProtocolVersion(MqttProtocolVersion.V311)); - var receivedApplicationMessages = testEnvironment.CreateApplicationMessageHandler(receiver); - await receiver.SubscribeAsync("#"); + var receiver = await testEnvironment.ConnectClient(o => o.WithProtocolVersion(MqttProtocolVersion.V311)); + var receivedApplicationMessages = testEnvironment.CreateApplicationMessageHandler(receiver); + await receiver.SubscribeAsync("#"); - var sender = await testEnvironment.ConnectClient(); + var sender = await testEnvironment.ConnectClient(); - var applicationMessage = new MqttApplicationMessageBuilder().WithTopic("My/Message") - .WithPayload("My_Payload") - .WithUserProperty("A", "B") - .WithResponseTopic("Response") - .WithCorrelationData(Encoding.UTF8.GetBytes("Correlation")) - .Build(); + var applicationMessage = new MqttApplicationMessageBuilder().WithTopic("My/Message") + .WithPayload("My_Payload") + .WithUserProperty("A", "B") + .WithResponseTopic("Response") + .WithCorrelationData(Encoding.UTF8.GetBytes("Correlation")) + .Build(); - await sender.PublishAsync(applicationMessage); + await sender.PublishAsync(applicationMessage); - await LongTestDelay(); + await LongTestDelay(); - Assert.AreEqual(1, receivedApplicationMessages.ReceivedEventArgs.Count); - Assert.AreEqual("My/Message", receivedApplicationMessages.ReceivedEventArgs.First().ApplicationMessage.Topic); - Assert.AreEqual("My_Payload", receivedApplicationMessages.ReceivedEventArgs.First().ApplicationMessage.ConvertPayloadToString()); - } - } + Assert.AreEqual(1, receivedApplicationMessages.ReceivedEventArgs.Count); + Assert.AreEqual("My/Message", receivedApplicationMessages.ReceivedEventArgs.First().ApplicationMessage.Topic); + Assert.AreEqual("My_Payload", receivedApplicationMessages.ReceivedEventArgs.First().ApplicationMessage.ConvertPayloadToString()); } } \ No newline at end of file diff --git a/Source/MQTTnet.Tests/Server/Events_Tests.cs b/Source/MQTTnet.Tests/Server/Events_Tests.cs index bce9197e..183acec4 100644 --- a/Source/MQTTnet.Tests/Server/Events_Tests.cs +++ b/Source/MQTTnet.Tests/Server/Events_Tests.cs @@ -10,194 +10,180 @@ using MQTTnet.Internal; using MQTTnet.Protocol; using MQTTnet.Server; -namespace MQTTnet.Tests.Server +namespace MQTTnet.Tests.Server; + +// ReSharper disable InconsistentNaming +[TestClass] +public sealed class Events_Tests : BaseTestClass { - [TestClass] - public sealed class Events_Tests : BaseTestClass + [TestMethod] + public async Task Fire_Client_Connected_Event() { - [TestMethod] - public async Task Fire_Client_Connected_Event() + using var testEnvironment = CreateTestEnvironment(); + var server = await testEnvironment.StartServer(); + + ClientConnectedEventArgs eventArgs = null; + server.ClientConnectedAsync += e => { - using (var testEnvironment = CreateTestEnvironment()) - { - var server = await testEnvironment.StartServer(); + eventArgs = e; + return CompletedTask.Instance; + }; - ClientConnectedEventArgs eventArgs = null; - server.ClientConnectedAsync += e => - { - eventArgs = e; - return CompletedTask.Instance; - }; + await testEnvironment.ConnectClient(o => o.WithCredentials("TheUser", "ThePassword")); - await testEnvironment.ConnectClient(o => o.WithCredentials("TheUser", "ThePassword")); + await LongTestDelay(); - await LongTestDelay(); + Assert.IsNotNull(eventArgs); - Assert.IsNotNull(eventArgs); + Assert.IsTrue(eventArgs.ClientId.StartsWith(nameof(Fire_Client_Connected_Event))); + Assert.IsTrue(eventArgs.RemoteEndPoint.ToString().Contains("127.0.0.1")); + Assert.AreEqual(MqttProtocolVersion.V311, eventArgs.ProtocolVersion); + Assert.AreEqual("TheUser", eventArgs.UserName); + Assert.AreEqual("ThePassword", eventArgs.Password); + } - Assert.IsTrue(eventArgs.ClientId.StartsWith(nameof(Fire_Client_Connected_Event))); - Assert.IsTrue(eventArgs.RemoteEndPoint.ToString().Contains("127.0.0.1")); - Assert.AreEqual(MqttProtocolVersion.V311, eventArgs.ProtocolVersion); - Assert.AreEqual("TheUser", eventArgs.UserName); - Assert.AreEqual("ThePassword", eventArgs.Password); - } - } + [TestMethod] + public async Task Fire_Client_Disconnected_Event() + { + using var testEnvironment = CreateTestEnvironment(); + var server = await testEnvironment.StartServer(); - [TestMethod] - public async Task Fire_Client_Disconnected_Event() + ClientDisconnectedEventArgs eventArgs = null; + server.ClientDisconnectedAsync += e => { - using (var testEnvironment = CreateTestEnvironment()) - { - var server = await testEnvironment.StartServer(); + eventArgs = e; + return CompletedTask.Instance; + }; - ClientDisconnectedEventArgs eventArgs = null; - server.ClientDisconnectedAsync += e => - { - eventArgs = e; - return CompletedTask.Instance; - }; + var client = await testEnvironment.ConnectClient(o => o.WithCredentials("TheUser", "ThePassword")); + await client.DisconnectAsync(); - var client = await testEnvironment.ConnectClient(o => o.WithCredentials("TheUser", "ThePassword")); - await client.DisconnectAsync(); + await LongTestDelay(); - await LongTestDelay(); + Assert.IsNotNull(eventArgs); - Assert.IsNotNull(eventArgs); + Assert.IsTrue(eventArgs.ClientId.StartsWith(nameof(Fire_Client_Disconnected_Event))); + Assert.IsTrue(eventArgs.RemoteEndPoint.ToString().Contains("127.0.0.1")); + Assert.AreEqual(MqttClientDisconnectType.Clean, eventArgs.DisconnectType); - Assert.IsTrue(eventArgs.ClientId.StartsWith(nameof(Fire_Client_Disconnected_Event))); - Assert.IsTrue(eventArgs.RemoteEndPoint.ToString().Contains("127.0.0.1")); - Assert.AreEqual(MqttClientDisconnectType.Clean, eventArgs.DisconnectType); + Assert.AreEqual("TheUser", eventArgs.UserName); + Assert.AreEqual("ThePassword", eventArgs.Password); + } - Assert.AreEqual("TheUser", eventArgs.UserName); - Assert.AreEqual("ThePassword", eventArgs.Password); - } - } + [TestMethod] + public async Task Fire_Client_Subscribed_Event() + { + using var testEnvironment = CreateTestEnvironment(); + var server = await testEnvironment.StartServer(); - [TestMethod] - public async Task Fire_Client_Subscribed_Event() + ClientSubscribedTopicEventArgs eventArgs = null; + server.ClientSubscribedTopicAsync += e => { - using (var testEnvironment = CreateTestEnvironment()) - { - var server = await testEnvironment.StartServer(); + eventArgs = e; + return CompletedTask.Instance; + }; - ClientSubscribedTopicEventArgs eventArgs = null; - server.ClientSubscribedTopicAsync += e => - { - eventArgs = e; - return CompletedTask.Instance; - }; + var client = await testEnvironment.ConnectClient(o => o.WithCredentials("TheUser")); + await client.SubscribeAsync("The/Topic", MqttQualityOfServiceLevel.AtLeastOnce); - var client = await testEnvironment.ConnectClient(o => o.WithCredentials("TheUser")); - await client.SubscribeAsync("The/Topic", MqttQualityOfServiceLevel.AtLeastOnce); + await LongTestDelay(); - await LongTestDelay(); + Assert.IsNotNull(eventArgs); - Assert.IsNotNull(eventArgs); + Assert.IsTrue(eventArgs.ClientId.StartsWith(nameof(Fire_Client_Subscribed_Event))); + Assert.AreEqual("The/Topic", eventArgs.TopicFilter.Topic); + Assert.AreEqual(MqttQualityOfServiceLevel.AtLeastOnce, eventArgs.TopicFilter.QualityOfServiceLevel); + Assert.AreEqual("TheUser", eventArgs.UserName); + } - Assert.IsTrue(eventArgs.ClientId.StartsWith(nameof(Fire_Client_Subscribed_Event))); - Assert.AreEqual("The/Topic", eventArgs.TopicFilter.Topic); - Assert.AreEqual(MqttQualityOfServiceLevel.AtLeastOnce, eventArgs.TopicFilter.QualityOfServiceLevel); - Assert.AreEqual("TheUser", eventArgs.UserName); - } - } + [TestMethod] + public async Task Fire_Client_Unsubscribed_Event() + { + using var testEnvironment = CreateTestEnvironment(); + var server = await testEnvironment.StartServer(); - [TestMethod] - public async Task Fire_Client_Unsubscribed_Event() + ClientUnsubscribedTopicEventArgs eventArgs = null; + server.ClientUnsubscribedTopicAsync += e => { - using (var testEnvironment = CreateTestEnvironment()) - { - var server = await testEnvironment.StartServer(); + eventArgs = e; + return CompletedTask.Instance; + }; - ClientUnsubscribedTopicEventArgs eventArgs = null; - server.ClientUnsubscribedTopicAsync += e => - { - eventArgs = e; - return CompletedTask.Instance; - }; + var client = await testEnvironment.ConnectClient(o => o.WithCredentials("TheUser")); + await client.UnsubscribeAsync("The/Topic"); - var client = await testEnvironment.ConnectClient(o => o.WithCredentials("TheUser")); - await client.UnsubscribeAsync("The/Topic"); + await LongTestDelay(); - await LongTestDelay(); + Assert.IsNotNull(eventArgs); - Assert.IsNotNull(eventArgs); + Assert.IsTrue(eventArgs.ClientId.StartsWith(nameof(Fire_Client_Unsubscribed_Event))); + Assert.AreEqual("The/Topic", eventArgs.TopicFilter); + Assert.AreEqual("TheUser", eventArgs.UserName); + } - Assert.IsTrue(eventArgs.ClientId.StartsWith(nameof(Fire_Client_Unsubscribed_Event))); - Assert.AreEqual("The/Topic", eventArgs.TopicFilter); - Assert.AreEqual("TheUser", eventArgs.UserName); - } - } + [TestMethod] + public async Task Fire_Application_Message_Received_Event() + { + using var testEnvironment = CreateTestEnvironment(); + var server = await testEnvironment.StartServer(); - [TestMethod] - public async Task Fire_Application_Message_Received_Event() + InterceptingPublishEventArgs eventArgs = null; + server.InterceptingPublishAsync += e => { - using (var testEnvironment = CreateTestEnvironment()) - { - var server = await testEnvironment.StartServer(); + eventArgs = e; + return CompletedTask.Instance; + }; - InterceptingPublishEventArgs eventArgs = null; - server.InterceptingPublishAsync += e => - { - eventArgs = e; - return CompletedTask.Instance; - }; + var client = await testEnvironment.ConnectClient(o => o.WithCredentials("TheUser")); + await client.PublishStringAsync("The_Topic", "The_Payload"); - var client = await testEnvironment.ConnectClient(o => o.WithCredentials("TheUser")); - await client.PublishStringAsync("The_Topic", "The_Payload"); + await LongTestDelay(); - await LongTestDelay(); + Assert.IsNotNull(eventArgs); - Assert.IsNotNull(eventArgs); + Assert.IsTrue(eventArgs.ClientId.StartsWith(nameof(Fire_Application_Message_Received_Event))); + Assert.AreEqual("The_Topic", eventArgs.ApplicationMessage.Topic); + Assert.AreEqual("The_Payload", eventArgs.ApplicationMessage.ConvertPayloadToString()); + Assert.AreEqual("TheUser", eventArgs.UserName); + } - Assert.IsTrue(eventArgs.ClientId.StartsWith(nameof(Fire_Application_Message_Received_Event))); - Assert.AreEqual("The_Topic", eventArgs.ApplicationMessage.Topic); - Assert.AreEqual("The_Payload", eventArgs.ApplicationMessage.ConvertPayloadToString()); - Assert.AreEqual("TheUser", eventArgs.UserName); - } - } + [TestMethod] + public async Task Fire_Started_Event() + { + using var testEnvironment = CreateTestEnvironment(); + var server = testEnvironment.CreateServer(new MqttServerOptions()); - [TestMethod] - public async Task Fire_Started_Event() + EventArgs eventArgs = null; + server.StartedAsync += e => { - using (var testEnvironment = CreateTestEnvironment()) - { - var server = testEnvironment.CreateServer(new MqttServerOptions()); + eventArgs = e; + return CompletedTask.Instance; + }; - EventArgs eventArgs = null; - server.StartedAsync += e => - { - eventArgs = e; - return CompletedTask.Instance; - }; + await server.StartAsync(); - await server.StartAsync(); + await LongTestDelay(); - await LongTestDelay(); + Assert.IsNotNull(eventArgs); + } - Assert.IsNotNull(eventArgs); - } - } + [TestMethod] + public async Task Fire_Stopped_Event() + { + using var testEnvironment = CreateTestEnvironment(); + var server = await testEnvironment.StartServer(); - [TestMethod] - public async Task Fire_Stopped_Event() + EventArgs eventArgs = null; + server.StoppedAsync += e => { - using (var testEnvironment = CreateTestEnvironment()) - { - var server = await testEnvironment.StartServer(); + eventArgs = e; + return CompletedTask.Instance; + }; - EventArgs eventArgs = null; - server.StoppedAsync += e => - { - eventArgs = e; - return CompletedTask.Instance; - }; + await server.StopAsync(); - await server.StopAsync(); + await LongTestDelay(); - await LongTestDelay(); - - Assert.IsNotNull(eventArgs); - } - } + Assert.IsNotNull(eventArgs); } } \ No newline at end of file diff --git a/Source/MQTTnet.Tests/Server/General.cs b/Source/MQTTnet.Tests/Server/General.cs index 45cff198..30b8c7b6 100644 --- a/Source/MQTTnet.Tests/Server/General.cs +++ b/Source/MQTTnet.Tests/Server/General.cs @@ -15,1018 +15,965 @@ using System.Text; using System.Threading; using System.Threading.Tasks; -namespace MQTTnet.Tests.Server +namespace MQTTnet.Tests.Server; + +// ReSharper disable InconsistentNaming +[TestClass] +public sealed class General_Tests : BaseTestClass { - [TestClass] - public sealed class General_Tests : BaseTestClass + Dictionary _connected; + + [TestMethod] + public async Task Client_Disconnect_Without_Errors() { - Dictionary _connected; + using var testEnvironment = CreateTestEnvironment(); + bool clientWasConnected; - [TestMethod] - public async Task Client_Disconnect_Without_Errors() + var server = await testEnvironment.StartServer(new MqttServerOptionsBuilder()); + try { - using (var testEnvironment = CreateTestEnvironment()) - { - bool clientWasConnected; + var client = await testEnvironment.ConnectClient(new MqttClientOptionsBuilder()); - var server = await testEnvironment.StartServer(new MqttServerOptionsBuilder()); - try - { - var client = await testEnvironment.ConnectClient(new MqttClientOptionsBuilder()); + clientWasConnected = true; - clientWasConnected = true; + await client.DisconnectAsync(); - await client.DisconnectAsync(); - - await Task.Delay(500); - } - finally - { - await server.StopAsync(); - } - - Assert.IsTrue(clientWasConnected); - - testEnvironment.ThrowIfLogErrors(); - } + await Task.Delay(500); + } + finally + { + await server.StopAsync(); } - [TestMethod] - public async Task Collect_Messages_In_Disconnected_Session() + Assert.IsTrue(clientWasConnected); + + testEnvironment.ThrowIfLogErrors(); + } + + [TestMethod] + public async Task Collect_Messages_In_Disconnected_Session() + { + using var testEnvironment = CreateTestEnvironment(); + var server = await testEnvironment.StartServer(new MqttServerOptionsBuilder().WithPersistentSessions()); + + // Create the session including the subscription. + var client1 = await testEnvironment.ConnectClient(new MqttClientOptionsBuilder().WithClientId("a").WithCleanSession(false).WithSessionExpiryInterval(60)); + await client1.SubscribeAsync("x"); + await client1.DisconnectAsync(); + await Task.Delay(500); + + var clientStatus = await server.GetClientsAsync(); + Assert.AreEqual(0, clientStatus.Count); + + var client2 = await testEnvironment.ConnectClient(new MqttClientOptionsBuilder().WithClientId("b").WithCleanSession(false).WithSessionExpiryInterval(60)); + await client2.PublishStringAsync("x", "1"); + await client2.PublishStringAsync("x", "2"); + await client2.PublishStringAsync("x", "3"); + await client2.DisconnectAsync(); + + await Task.Delay(500); + + clientStatus = await server.GetClientsAsync(); + var sessionStatus = await server.GetSessionsAsync(); + + Assert.AreEqual(0, clientStatus.Count); + Assert.AreEqual(2, sessionStatus.Count); + + Assert.AreEqual(3, sessionStatus.First(s => s.Id == client1.Options.ClientId).PendingApplicationMessagesCount); + } + + [TestMethod] + public async Task Deny_Connection() + { + using var testEnvironment = CreateTestEnvironment(); + testEnvironment.IgnoreClientLogErrors = true; + + var server = await testEnvironment.StartServer(); + + server.ValidatingConnectionAsync += e => { - using (var testEnvironment = CreateTestEnvironment()) + e.ReasonCode = MqttConnectReasonCode.NotAuthorized; + return CompletedTask.Instance; + }; + + var client = testEnvironment.CreateClient(); + var response = await client.ConnectAsync(testEnvironment.CreateDefaultClientOptions()); + + Assert.AreEqual(MqttClientConnectResultCode.NotAuthorized, response.ResultCode); + } + + [TestMethod] + public async Task Do_Not_Send_Retained_Messages_For_Denied_Subscription() + { + using var testEnvironment = CreateTestEnvironment(); + var server = await testEnvironment.StartServer(); + + server.InterceptingSubscriptionAsync += e => + { + // This should lead to no subscriptions for "n" at all. So also no sending of retained messages. + if (e.TopicFilter.Topic == "n") { - var server = await testEnvironment.StartServer(new MqttServerOptionsBuilder().WithPersistentSessions()); - - // Create the session including the subscription. - var client1 = await testEnvironment.ConnectClient(new MqttClientOptionsBuilder().WithClientId("a").WithCleanSession(false).WithSessionExpiryInterval(60)); - await client1.SubscribeAsync("x"); - await client1.DisconnectAsync(); - await Task.Delay(500); - - var clientStatus = await server.GetClientsAsync(); - Assert.AreEqual(0, clientStatus.Count); - - var client2 = await testEnvironment.ConnectClient(new MqttClientOptionsBuilder().WithClientId("b").WithCleanSession(false).WithSessionExpiryInterval(60)); - await client2.PublishStringAsync("x", "1"); - await client2.PublishStringAsync("x", "2"); - await client2.PublishStringAsync("x", "3"); - await client2.DisconnectAsync(); - - await Task.Delay(500); - - clientStatus = await server.GetClientsAsync(); - var sessionStatus = await server.GetSessionsAsync(); - - Assert.AreEqual(0, clientStatus.Count); - Assert.AreEqual(2, sessionStatus.Count); - - Assert.AreEqual(3, sessionStatus.First(s => s.Id == client1.Options.ClientId).PendingApplicationMessagesCount); + e.Response.ReasonCode = MqttSubscribeReasonCode.UnspecifiedError; } - } - [TestMethod] - public async Task Deny_Connection() + return CompletedTask.Instance; + }; + + // Prepare some retained messages. + var client1 = await testEnvironment.ConnectClient(); + await client1.PublishAsync(new MqttApplicationMessageBuilder().WithTopic("y").WithPayload("x").WithRetainFlag().Build()); + await client1.PublishAsync(new MqttApplicationMessageBuilder().WithTopic("n").WithPayload("x").WithRetainFlag().Build()); + await client1.DisconnectAsync(); + + await Task.Delay(500); + + // Subscribe to all retained message types. + // It is important to do this in a range of filters to ensure that a subscription is not "hidden". + var client2 = await testEnvironment.ConnectClient(); + + var buffer = new StringBuilder(); + + client2.ApplicationMessageReceivedAsync += e => { - using (var testEnvironment = CreateTestEnvironment()) + lock (buffer) { - testEnvironment.IgnoreClientLogErrors = true; - - var server = await testEnvironment.StartServer(); - - server.ValidatingConnectionAsync += e => - { - e.ReasonCode = MqttConnectReasonCode.NotAuthorized; - return CompletedTask.Instance; - }; - - var client = testEnvironment.CreateClient(); - var response = await client.ConnectAsync(testEnvironment.CreateDefaultClientOptions()); - - Assert.AreEqual(MqttClientConnectResultCode.NotAuthorized, response.ResultCode); + buffer.Append(e.ApplicationMessage.Topic); } - } - [TestMethod] - public async Task Do_Not_Send_Retained_Messages_For_Denied_Subscription() + return CompletedTask.Instance; + }; + + await client2.SubscribeAsync("y"); + await client2.SubscribeAsync("n"); + + await Task.Delay(500); + + // No 'n' in buffer! + Assert.AreEqual("y", buffer.ToString()); + } + + [TestMethod] + public async Task Handle_Clean_Disconnect() + { + using var testEnvironment = CreateTestEnvironment(); + var server = await testEnvironment.StartServer(new MqttServerOptionsBuilder()); + + var clientConnectedCalled = 0; + var clientDisconnectedCalled = 0; + + server.ClientConnectedAsync += _ => { - using (var testEnvironment = CreateTestEnvironment()) - { - var server = await testEnvironment.StartServer(); + Interlocked.Increment(ref clientConnectedCalled); + return CompletedTask.Instance; + }; - server.InterceptingSubscriptionAsync += e => - { - // This should lead to no subscriptions for "n" at all. So also no sending of retained messages. - if (e.TopicFilter.Topic == "n") + server.ClientDisconnectedAsync += _ => + { + Interlocked.Increment(ref clientDisconnectedCalled); + return CompletedTask.Instance; + }; + + var c1 = await testEnvironment.ConnectClient(new MqttClientOptionsBuilder()); + + await Task.Delay(1000); + + Assert.AreEqual(1, clientConnectedCalled); + Assert.AreEqual(0, clientDisconnectedCalled); + + await Task.Delay(1000); + + await c1.DisconnectAsync(); + + await Task.Delay(1000); + + Assert.AreEqual(1, clientConnectedCalled); + Assert.AreEqual(1, clientDisconnectedCalled); + } + + [TestMethod] + public async Task Handle_Lots_Of_Parallel_Retained_Messages() + { + const int clientCount = 50; + + using var testEnvironment = CreateTestEnvironment(); + var server = await testEnvironment.StartServer(); + + var tasks = new List(); + for (var i = 0; i < clientCount; i++) + { + var i2 = i; + var testEnvironment2 = testEnvironment; + + tasks.Add( + Task.Run( + async () => { - e.Response.ReasonCode = MqttSubscribeReasonCode.UnspecifiedError; - } - - return CompletedTask.Instance; - }; - - // Prepare some retained messages. - var client1 = await testEnvironment.ConnectClient(); - await client1.PublishAsync(new MqttApplicationMessageBuilder().WithTopic("y").WithPayload("x").WithRetainFlag().Build()); - await client1.PublishAsync(new MqttApplicationMessageBuilder().WithTopic("n").WithPayload("x").WithRetainFlag().Build()); - await client1.DisconnectAsync(); - - await Task.Delay(500); - - // Subscribe to all retained message types. - // It is important to do this in a range of filters to ensure that a subscription is not "hidden". - var client2 = await testEnvironment.ConnectClient(); - - var buffer = new StringBuilder(); - - client2.ApplicationMessageReceivedAsync += e => - { - lock (buffer) - { - buffer.Append(e.ApplicationMessage.Topic); - } - - return CompletedTask.Instance; - }; - - await client2.SubscribeAsync("y"); - await client2.SubscribeAsync("n"); - - await Task.Delay(500); - - // No 'n' in buffer! - Assert.AreEqual("y", buffer.ToString()); - } - } - - [TestMethod] - public async Task Handle_Clean_Disconnect() - { - using (var testEnvironment = CreateTestEnvironment()) - { - var server = await testEnvironment.StartServer(new MqttServerOptionsBuilder()); - - var clientConnectedCalled = 0; - var clientDisconnectedCalled = 0; - - server.ClientConnectedAsync += e => - { - Interlocked.Increment(ref clientConnectedCalled); - return CompletedTask.Instance; - }; - - server.ClientDisconnectedAsync += e => - { - Interlocked.Increment(ref clientDisconnectedCalled); - return CompletedTask.Instance; - }; - - var c1 = await testEnvironment.ConnectClient(new MqttClientOptionsBuilder()); - - await Task.Delay(1000); - - Assert.AreEqual(1, clientConnectedCalled); - Assert.AreEqual(0, clientDisconnectedCalled); - - await Task.Delay(1000); - - await c1.DisconnectAsync(); - - await Task.Delay(1000); - - Assert.AreEqual(1, clientConnectedCalled); - Assert.AreEqual(1, clientDisconnectedCalled); - } - } - - [TestMethod] - public async Task Handle_Lots_Of_Parallel_Retained_Messages() - { - const int clientCount = 50; - - using (var testEnvironment = CreateTestEnvironment()) - { - var server = await testEnvironment.StartServer(); - - var tasks = new List(); - for (var i = 0; i < clientCount; i++) - { - var i2 = i; - var testEnvironment2 = testEnvironment; - - tasks.Add( - Task.Run( - async () => - { - try - { - using (var client = await testEnvironment2.ConnectClient()) - { - // Clear retained message. - await client.PublishAsync( - new MqttApplicationMessageBuilder().WithTopic("r" + i2) - .WithPayload(EmptyBuffer.Array) - .WithRetainFlag() - .WithQualityOfServiceLevel(MqttQualityOfServiceLevel.AtLeastOnce) - .Build()); - - // Set retained message. - await client.PublishAsync( - new MqttApplicationMessageBuilder().WithTopic("r" + i2) - .WithPayload("value") - .WithRetainFlag() - .WithQualityOfServiceLevel(MqttQualityOfServiceLevel.AtLeastOnce) - .Build()); - - await client.DisconnectAsync(); - } - } - catch (Exception exception) - { - testEnvironment2.TrackException(exception); - } - })); - - await Task.Delay(10); - } - - await Task.WhenAll(tasks); - - await Task.Delay(1000); - - var retainedMessages = await server.GetRetainedMessagesAsync(); - - Assert.AreEqual(clientCount, retainedMessages.Count); - - for (var i = 0; i < clientCount; i++) - { - Assert.IsTrue(retainedMessages.Any(m => m.Topic == "r" + i)); - } - } - } - - [TestMethod] - public async Task Intercept_Application_Message() - { - using (var testEnvironment = CreateTestEnvironment()) - { - var server = await testEnvironment.StartServer(); - - server.InterceptingPublishAsync += e => - { - e.ApplicationMessage = new MqttApplicationMessage { Topic = "new_topic" }; - - return CompletedTask.Instance; - }; - - string receivedTopic = null; - var c1 = await testEnvironment.ConnectClient(); - await c1.SubscribeAsync("#"); - - c1.ApplicationMessageReceivedAsync += a => - { - receivedTopic = a.ApplicationMessage.Topic; - return CompletedTask.Instance; - }; - - await c1.PublishAsync(new MqttApplicationMessageBuilder().WithTopic("original_topic").Build()); - - await Task.Delay(500); - Assert.AreEqual("new_topic", receivedTopic); - } - } - - [TestMethod] - public async Task Intercept_Message() - { - using (var testEnvironment = CreateTestEnvironment()) - { - var server = await testEnvironment.StartServer(); - server.InterceptingPublishAsync += e => - { - e.ApplicationMessage.PayloadSegment = new ArraySegment(Encoding.ASCII.GetBytes("extended")); - return CompletedTask.Instance; - }; - - var c1 = await testEnvironment.ConnectClient(); - var c2 = await testEnvironment.ConnectClient(); - await c2.SubscribeAsync(new MqttTopicFilterBuilder().WithTopic("test").Build()); - - var isIntercepted = false; - c2.ApplicationMessageReceivedAsync += e => - { - isIntercepted = string.Compare("extended", Encoding.UTF8.GetString(e.ApplicationMessage.Payload), StringComparison.Ordinal) == 0; - return CompletedTask.Instance; - }; - - await c1.PublishAsync(new MqttApplicationMessageBuilder().WithTopic("test").Build()); - await c1.DisconnectAsync(); - - await Task.Delay(500); - - Assert.IsTrue(isIntercepted); - } - } - - [TestMethod] - public async Task Intercept_Undelivered() - { - using (var testEnvironment = CreateTestEnvironment()) - { - var undeliverd = string.Empty; - - var server = await testEnvironment.StartServer(); - server.ApplicationMessageNotConsumedAsync += e => - { - undeliverd = e.ApplicationMessage.Topic; - return CompletedTask.Instance; - }; - - var client = await testEnvironment.ConnectClient(); - - await client.SubscribeAsync("b"); - - await client.PublishStringAsync("a", null, MqttQualityOfServiceLevel.ExactlyOnce); - - await Task.Delay(500); - - Assert.AreEqual("a", undeliverd); - } - } - - [TestMethod] - public async Task No_Messages_If_No_Subscription() - { - using (var testEnvironment = CreateTestEnvironment()) - { - await testEnvironment.StartServer(); - - var client = await testEnvironment.ConnectClient(); - var receivedMessages = new List(); - - client.ConnectedAsync += async e => - { - await client.PublishStringAsync("Connected"); - }; - - client.ApplicationMessageReceivedAsync += e => - { - lock (receivedMessages) - { - receivedMessages.Add(e.ApplicationMessage); - } - - return CompletedTask.Instance; - }; - - await Task.Delay(500); - - await client.PublishStringAsync("Hello"); - - await Task.Delay(500); - - Assert.AreEqual(0, receivedMessages.Count); - } - } - - [TestMethod] - public async Task Persist_Retained_Message() - { - using (var testEnvironment = CreateTestEnvironment()) - { - List savedRetainedMessages = null; - - var s = await testEnvironment.StartServer(); - s.RetainedMessageChangedAsync += e => - { - savedRetainedMessages = e.StoredRetainedMessages; - return CompletedTask.Instance; - }; - - var c1 = await testEnvironment.ConnectClient(); - - await c1.PublishAsync(new MqttApplicationMessageBuilder().WithTopic("retained").WithPayload(new byte[3]).WithRetainFlag().Build()); - - await Task.Delay(500); - - Assert.AreEqual(1, savedRetainedMessages?.Count); - } - } - - [TestMethod] - public async Task Publish_After_Client_Connects() - { - using (var testEnvironment = CreateTestEnvironment()) - { - var server = await testEnvironment.StartServer(); - server.ClientConnectedAsync += async e => - { - await server.InjectApplicationMessage( - new InjectedMqttApplicationMessage( - new MqttApplicationMessage - { - Topic = "/test/1", - PayloadSegment = new ArraySegment(Encoding.UTF8.GetBytes("true")), - QualityOfServiceLevel = MqttQualityOfServiceLevel.ExactlyOnce - }) + try { - SenderClientId = "server" - }); - }; - - string receivedTopic = null; - - var c1 = await testEnvironment.ConnectClient(); - c1.ApplicationMessageReceivedAsync += e => - { - receivedTopic = e.ApplicationMessage.Topic; - return CompletedTask.Instance; - }; - - await c1.SubscribeAsync("#"); - - await testEnvironment.ConnectClient(); - await testEnvironment.ConnectClient(); - await testEnvironment.ConnectClient(); - await testEnvironment.ConnectClient(); - - await Task.Delay(500); - - Assert.AreEqual("/test/1", receivedTopic); - } - } - - [TestMethod] - public async Task Publish_At_Least_Once_0x01() - { - await TestPublishAsync("A/B/C", MqttQualityOfServiceLevel.AtLeastOnce, "A/B/C", MqttQualityOfServiceLevel.AtLeastOnce, 1); - } - - [TestMethod] - public async Task Publish_At_Most_Once_0x00() - { - await TestPublishAsync("A/B/C", MqttQualityOfServiceLevel.AtMostOnce, "A/B/C", MqttQualityOfServiceLevel.AtMostOnce, 1); - } - - [TestMethod] - public async Task Publish_Exactly_Once_0x02() - { - await TestPublishAsync("A/B/C", MqttQualityOfServiceLevel.ExactlyOnce, "A/B/C", MqttQualityOfServiceLevel.ExactlyOnce, 1); - } - - [TestMethod] - public async Task Publish_From_Server() - { - using (var testEnvironment = CreateTestEnvironment()) - { - var server = await testEnvironment.StartServer(); - - var receivedMessagesCount = 0; - - var client = await testEnvironment.ConnectClient(); - client.ApplicationMessageReceivedAsync += e => - { - Interlocked.Increment(ref receivedMessagesCount); - return CompletedTask.Instance; - }; - - var message = new MqttApplicationMessageBuilder().WithTopic("a").WithQualityOfServiceLevel(MqttQualityOfServiceLevel.AtLeastOnce).Build(); - await client.SubscribeAsync(new MqttTopicFilter { Topic = "a", QualityOfServiceLevel = MqttQualityOfServiceLevel.AtLeastOnce }); - - await server.InjectApplicationMessage( - new InjectedMqttApplicationMessage(message) - { - SenderClientId = "server" - }); - - await Task.Delay(1000); - - Assert.AreEqual(1, receivedMessagesCount); - } - } - - [TestMethod] - public async Task Publish_Multiple_Clients() - { - var receivedMessagesCount = 0; - - using (var testEnvironment = CreateTestEnvironment()) - { - await testEnvironment.StartServer(); - - var c1 = await testEnvironment.ConnectClient(); - var c2 = await testEnvironment.ConnectClient(); - var c3 = await testEnvironment.ConnectClient(); - - c2.ApplicationMessageReceivedAsync += e => - { - Interlocked.Increment(ref receivedMessagesCount); - return CompletedTask.Instance; - }; - - c3.ApplicationMessageReceivedAsync += e => - { - Interlocked.Increment(ref receivedMessagesCount); - return CompletedTask.Instance; - }; - - await c2.SubscribeAsync(new MqttTopicFilter { Topic = "a", QualityOfServiceLevel = MqttQualityOfServiceLevel.AtLeastOnce }).ConfigureAwait(false); - await c3.SubscribeAsync(new MqttTopicFilter { Topic = "a", QualityOfServiceLevel = MqttQualityOfServiceLevel.AtLeastOnce }).ConfigureAwait(false); - - var message = new MqttApplicationMessageBuilder().WithTopic("a").WithQualityOfServiceLevel(MqttQualityOfServiceLevel.AtLeastOnce).Build(); - - for (var i = 0; i < 500; i++) - { - await c1.PublishAsync(message).ConfigureAwait(false); - } - - SpinWait.SpinUntil(() => receivedMessagesCount == 1000, TimeSpan.FromSeconds(20)); - - Assert.AreEqual(1000, receivedMessagesCount); - } - } - - [TestMethod] - public async Task Remove_Session() - { - using (var testEnvironment = CreateTestEnvironment()) - { - var server = await testEnvironment.StartServer(new MqttServerOptionsBuilder()); - - var clientOptions = new MqttClientOptionsBuilder(); - var c1 = await testEnvironment.ConnectClient(clientOptions); - await Task.Delay(500); - Assert.AreEqual(1, (await server.GetClientsAsync()).Count); - - await c1.DisconnectAsync(); - await Task.Delay(500); - - Assert.AreEqual(0, (await server.GetClientsAsync()).Count); - } - } - - [TestMethod] - public async Task Same_Client_Id_Connect_Disconnect_Event_Order() - { - using (var testEnvironment = CreateTestEnvironment()) - { - var server = await testEnvironment.StartServer(); - - var events = new List(); - - server.ClientConnectedAsync += e => - { - lock (events) - { - events.Add("c"); - } - - return CompletedTask.Instance; - }; - - server.ClientDisconnectedAsync += e => - { - lock (events) - { - events.Add("d"); - } - - return CompletedTask.Instance; - }; - - var clientOptionsBuilder = new MqttClientOptionsBuilder().WithClientId(Guid.NewGuid().ToString()); - - // c - var c1 = await testEnvironment.ConnectClient(clientOptionsBuilder); - - await LongTestDelay(); - var flow = string.Join(string.Empty, events); - Assert.AreEqual("c", flow); - - // dc - // Connect client with same client ID. Should disconnect existing client. - var c2 = await testEnvironment.ConnectClient(clientOptionsBuilder); - - await LongTestDelay(); - flow = string.Join(string.Empty, events); - Assert.AreEqual("cdc", flow); - - c2.ApplicationMessageReceivedAsync += e => - { - lock (events) - { - events.Add("r"); - } - - return CompletedTask.Instance; - }; - - await c2.SubscribeAsync("topic"); - - // r - await c2.PublishStringAsync("topic"); - - await LongTestDelay(); - flow = string.Join(string.Empty, events); - Assert.AreEqual("cdcr", flow); - - // nothing - - Assert.AreEqual(false, c1.IsConnected); - await c1.TryDisconnectAsync(); - Assert.AreEqual(false, c1.IsConnected); - - await LongTestDelay(); - - // d - Assert.AreEqual(true, c2.IsConnected); - await c2.DisconnectAsync(); - - await LongTestDelay(); - - await server.StopAsync(); - - await LongTestDelay(); - flow = string.Join(string.Empty, events); - Assert.AreEqual("cdcrd", flow); - } - } - - [TestMethod] - public async Task Same_Client_Id_Refuse_Connection() - { - using (var testEnvironment = CreateTestEnvironment()) - { - testEnvironment.IgnoreClientLogErrors = true; - - _connected = new Dictionary(); - - var server = await testEnvironment.StartServer(); - - server.ValidatingConnectionAsync += e => - { - ConnectionValidationHandler(e); - return CompletedTask.Instance; - }; - - var events = new List(); - - server.ClientConnectedAsync += e => - { - lock (events) - { - events.Add("c"); - } - - return CompletedTask.Instance; - }; - - server.ClientDisconnectedAsync += e => - { - lock (events) - { - events.Add("d"); - } - - return CompletedTask.Instance; - }; - - var clientOptions = new MqttClientOptionsBuilder().WithClientId("same_id"); - - // c - var c1 = await testEnvironment.ConnectClient(clientOptions); - - c1.DisconnectedAsync += _ => - { - lock (events) - { - events.Add("x"); - } - - return CompletedTask.Instance; - }; - - c1.ApplicationMessageReceivedAsync += e => - { - lock (events) - { - events.Add("r"); - } - - return CompletedTask.Instance; - }; - - c1.SubscribeAsync("topic").Wait(); - - await Task.Delay(500); - - c1.PublishStringAsync("topic").Wait(); - - await Task.Delay(500); - - var flow = string.Join(string.Empty, events); - Assert.AreEqual("cr", flow); - - try - { - await testEnvironment.ConnectClient(clientOptions); - Assert.Fail("same id connection is expected to fail"); - } - catch - { - //same id connection is expected to fail - } - - await Task.Delay(500); - - flow = string.Join(string.Empty, events); - Assert.AreEqual("cr", flow); - - c1.PublishStringAsync("topic").Wait(); - - await Task.Delay(500); - - flow = string.Join(string.Empty, events); - Assert.AreEqual("crr", flow); - } - } - - [TestMethod] - public async Task Send_Long_Body() - { - using (var testEnvironment = CreateTestEnvironment()) - { - const int PayloadSizeInMB = 30; - const int CharCount = PayloadSizeInMB * 1024 * 1024; - - var longBody = new byte[CharCount]; - byte @char = 32; - - for (long i = 0; i < PayloadSizeInMB * 1024L * 1024L; i++) - { - longBody[i] = @char; - - @char++; - - if (@char > 126) - { - @char = 32; - } - } - - byte[] receivedBody = null; - - await testEnvironment.StartServer(); - - var client1 = await testEnvironment.ConnectClient(); - client1.ApplicationMessageReceivedAsync += e => - { - receivedBody = e.ApplicationMessage.Payload.ToArray(); - return CompletedTask.Instance; - }; - - await client1.SubscribeAsync("string"); - - var client2 = await testEnvironment.ConnectClient(); - await client2.PublishBinaryAsync("string", longBody); - - await Task.Delay(TimeSpan.FromSeconds(5)); - - Assert.IsTrue(longBody.SequenceEqual(receivedBody ?? new byte[0])); - } - } - - [TestMethod] - public async Task Set_Subscription_At_Server() - { - using (var testEnvironment = CreateTestEnvironment()) - { - var server = await testEnvironment.StartServer(); - - server.ClientConnectedAsync += async e => - { - // Every client will automatically subscribe to this topic. - await server.SubscribeAsync(e.ClientId, "topic1"); - }; - - var client = await testEnvironment.ConnectClient(); - var receivedMessages = new List(); - - client.ApplicationMessageReceivedAsync += e => - { - lock (receivedMessages) - { - receivedMessages.Add(e.ApplicationMessage); - } - - return CompletedTask.Instance; - }; - - await Task.Delay(500); - - await client.PublishStringAsync("Hello"); - await Task.Delay(100); - Assert.AreEqual(0, receivedMessages.Count); - - await client.PublishStringAsync("topic1"); - await Task.Delay(100); - Assert.AreEqual(1, receivedMessages.Count); - } - } - - [TestMethod] - public async Task Shutdown_Disconnects_Clients_Gracefully() - { - using (var testEnvironment = CreateTestEnvironment()) - { - var server = await testEnvironment.StartServer(new MqttServerOptionsBuilder()); - - var disconnectCalled = 0; - - var c1 = await testEnvironment.ConnectClient(new MqttClientOptionsBuilder()); - c1.DisconnectedAsync += e => - { - disconnectCalled++; - return CompletedTask.Instance; - }; - - // Stopping the server should disconnect the connection with the client - // which, it turn, will fire the disconnected event at the client. - await server.StopAsync(); - - await LongTestDelay(); - - Assert.AreEqual(1, disconnectCalled); - } - } - - [TestMethod] - public async Task Stop_And_Restart() - { - using (var testEnvironment = CreateTestEnvironment()) - { - testEnvironment.IgnoreClientLogErrors = true; - - var server = await testEnvironment.StartServer(); - - await testEnvironment.ConnectClient(); - await server.StopAsync(); - - try - { - await testEnvironment.ConnectClient(); - Assert.Fail("Connecting should fail."); - } - catch - { - // Ignore errors. - } - - await server.StartAsync(); - - // The client should be able to connect again. - await testEnvironment.ConnectClient(); - } - } - - [TestMethod] - [DataRow("", null)] - [DataRow("", "")] - [DataRow(null, null)] - public async Task Use_Admissible_Credentials(string username, string password) - { - using (var testEnvironment = CreateTestEnvironment()) - { - await testEnvironment.StartServer(); - - var client = testEnvironment.CreateClient(); - - var clientOptions = new MqttClientOptionsBuilder().WithTcpServer("localhost", testEnvironment.ServerPort).WithCredentials(username, password).Build(); - - var connectResult = await client.ConnectAsync(clientOptions); - - Assert.IsFalse(connectResult.IsSessionPresent); - Assert.IsTrue(client.IsConnected); - } - } - - [TestMethod] - public async Task Use_Empty_Client_ID() - { - using (var testEnvironment = CreateTestEnvironment()) - { - await testEnvironment.StartServer(); - var client2 = await testEnvironment.ConnectClient(new MqttClientOptionsBuilder().WithClientId("b").WithCleanSession(false)); - await client2.PublishStringAsync("x", "1"); - await client2.PublishStringAsync("x", "2"); - await client2.PublishStringAsync("x", "3"); - await client2.DisconnectAsync(); - - var client = testEnvironment.CreateClient(); - - var clientOptions = new MqttClientOptionsBuilder().WithTcpServer("localhost", testEnvironment.ServerPort).WithClientId(string.Empty).Build(); - - var connectResult = await client.ConnectAsync(clientOptions); - - Assert.IsFalse(connectResult.IsSessionPresent); - Assert.IsTrue(client.IsConnected); - } - } - - [TestMethod] - public async Task Disconnect_Client_with_Reason() - { - using (var testEnvironment = CreateTestEnvironment()) - { - var disconnectPacketReceived = false; - - string testClientId = null; - - await testEnvironment.StartServer(); - - testEnvironment.Server.ClientConnectedAsync += e => - { - testClientId = e.ClientId; - return CompletedTask.Instance; - }; - - var client = testEnvironment.CreateClient(); - - client.InspectPacketAsync += e => - { - if (e.Buffer.Length > 0) - { - if (e.Buffer[0] == (byte)MqttControlPacketType.Disconnect << 4) - { - disconnectPacketReceived = true; + using var client = await testEnvironment2.ConnectClient(); + // Clear retained message. + await client.PublishAsync( + new MqttApplicationMessageBuilder().WithTopic("r" + i2) + .WithPayload(EmptyBuffer.Array) + .WithRetainFlag() + .WithQualityOfServiceLevel(MqttQualityOfServiceLevel.AtLeastOnce) + .Build()); + + // Set retained message. + await client.PublishAsync( + new MqttApplicationMessageBuilder().WithTopic("r" + i2) + .WithPayload("value") + .WithRetainFlag() + .WithQualityOfServiceLevel(MqttQualityOfServiceLevel.AtLeastOnce) + .Build()); + + await client.DisconnectAsync(); } - } - return CompletedTask.Instance; - }; + catch (Exception exception) + { + testEnvironment2.TrackException(exception); + } + })); - await client.ConnectAsync( - new MqttClientOptionsBuilder().WithTcpServer("localhost", testEnvironment.ServerPort) - .WithProtocolVersion(MQTTnet.Formatter.MqttProtocolVersion.V311) - .Build() - ); - - await LongTestDelay(); - - // Test client should be connected now - - Assert.IsTrue(testClientId != null); - - // Have the server disconnect the client with SessionTakenOver reason; no DISCONNECT should be sent to MQTT 3.1.1 clients - - await testEnvironment.Server.DisconnectClientAsync(testClientId, Protocol.MqttDisconnectReasonCode.SessionTakenOver); - - await LongTestDelay(); - - // MQTT 3.1.1 client should not receive a disconnect packet - - Assert.IsFalse(disconnectPacketReceived, "Disconnect packet received when none is expected"); - } + await Task.Delay(10); } - void ConnectionValidationHandler(ValidatingConnectionEventArgs eventArgs) + await Task.WhenAll(tasks); + + await Task.Delay(1000); + + var retainedMessages = await server.GetRetainedMessagesAsync(); + + Assert.AreEqual(clientCount, retainedMessages.Count); + + for (var i = 0; i < clientCount; i++) { - if (_connected.ContainsKey(eventArgs.ClientId)) - { - eventArgs.ReasonCode = MqttConnectReasonCode.BadUserNameOrPassword; - return; - } - - _connected[eventArgs.ClientId] = true; - eventArgs.ReasonCode = MqttConnectReasonCode.Success; - } - - async Task TestPublishAsync( - string topic, - MqttQualityOfServiceLevel qualityOfServiceLevel, - string topicFilter, - MqttQualityOfServiceLevel filterQualityOfServiceLevel, - int expectedReceivedMessagesCount) - { - using (var testEnvironment = CreateTestEnvironment()) - { - await testEnvironment.StartServer(); - - var c1 = await testEnvironment.ConnectClient(new MqttClientOptionsBuilder().WithClientId("receiver")); - var c1MessageHandler = testEnvironment.CreateApplicationMessageHandler(c1); - await c1.SubscribeAsync(new MqttTopicFilterBuilder().WithTopic(topicFilter).WithQualityOfServiceLevel(filterQualityOfServiceLevel).Build()); - - var c2 = await testEnvironment.ConnectClient(new MqttClientOptionsBuilder().WithClientId("sender")); - await c2.PublishAsync(new MqttApplicationMessageBuilder().WithTopic(topic).WithPayload(new byte[0]).WithQualityOfServiceLevel(qualityOfServiceLevel).Build()); - await Task.Delay(500); - - await c2.DisconnectAsync().ConfigureAwait(false); - - await Task.Delay(500); - await c1.UnsubscribeAsync(topicFilter); - await Task.Delay(500); - - Assert.AreEqual(expectedReceivedMessagesCount, c1MessageHandler.ReceivedEventArgs.Count); - } + Assert.IsTrue(retainedMessages.Any(m => m.Topic == "r" + i)); } } + + [TestMethod] + public async Task Intercept_Application_Message() + { + using var testEnvironment = CreateTestEnvironment(); + var server = await testEnvironment.StartServer(); + + server.InterceptingPublishAsync += e => + { + e.ApplicationMessage = new MqttApplicationMessage { Topic = "new_topic" }; + + return CompletedTask.Instance; + }; + + string receivedTopic = null; + var c1 = await testEnvironment.ConnectClient(); + await c1.SubscribeAsync("#"); + + c1.ApplicationMessageReceivedAsync += a => + { + receivedTopic = a.ApplicationMessage.Topic; + return CompletedTask.Instance; + }; + + await c1.PublishAsync(new MqttApplicationMessageBuilder().WithTopic("original_topic").Build()); + + await Task.Delay(500); + Assert.AreEqual("new_topic", receivedTopic); + } + + [TestMethod] + public async Task Intercept_Message() + { + using var testEnvironment = CreateTestEnvironment(); + var server = await testEnvironment.StartServer(); + server.InterceptingPublishAsync += e => + { + e.ApplicationMessage.PayloadSegment = new ArraySegment(Encoding.ASCII.GetBytes("extended")); + return CompletedTask.Instance; + }; + + var c1 = await testEnvironment.ConnectClient(); + var c2 = await testEnvironment.ConnectClient(); + await c2.SubscribeAsync(new MqttTopicFilterBuilder().WithTopic("test").Build()); + + var isIntercepted = false; + c2.ApplicationMessageReceivedAsync += e => + { + isIntercepted = string.Compare("extended", Encoding.UTF8.GetString(e.ApplicationMessage.Payload), StringComparison.Ordinal) == 0; + return CompletedTask.Instance; + }; + + await c1.PublishAsync(new MqttApplicationMessageBuilder().WithTopic("test").Build()); + await c1.DisconnectAsync(); + + await Task.Delay(500); + + Assert.IsTrue(isIntercepted); + } + + [TestMethod] + public async Task Intercept_Undelivered() + { + using var testEnvironment = CreateTestEnvironment(); + var undeliverd = string.Empty; + + var server = await testEnvironment.StartServer(); + server.ApplicationMessageNotConsumedAsync += e => + { + undeliverd = e.ApplicationMessage.Topic; + return CompletedTask.Instance; + }; + + var client = await testEnvironment.ConnectClient(); + + await client.SubscribeAsync("b"); + + await client.PublishStringAsync("a", null, MqttQualityOfServiceLevel.ExactlyOnce); + + await Task.Delay(500); + + Assert.AreEqual("a", undeliverd); + } + + [TestMethod] + public async Task No_Messages_If_No_Subscription() + { + using var testEnvironment = CreateTestEnvironment(); + await testEnvironment.StartServer(); + + var client = await testEnvironment.ConnectClient(); + var receivedMessages = new List(); + + client.ConnectedAsync += async _ => + { + await client.PublishStringAsync("Connected").ConfigureAwait(false); + }; + + client.ApplicationMessageReceivedAsync += e => + { + lock (receivedMessages) + { + receivedMessages.Add(e.ApplicationMessage); + } + + return CompletedTask.Instance; + }; + + await Task.Delay(500); + + await client.PublishStringAsync("Hello"); + + await Task.Delay(500); + + Assert.AreEqual(0, receivedMessages.Count); + } + + [TestMethod] + public async Task Persist_Retained_Message() + { + using var testEnvironment = CreateTestEnvironment(); + List savedRetainedMessages = null; + + var s = await testEnvironment.StartServer(); + s.RetainedMessageChangedAsync += e => + { + savedRetainedMessages = e.StoredRetainedMessages; + return CompletedTask.Instance; + }; + + var c1 = await testEnvironment.ConnectClient(); + + await c1.PublishAsync(new MqttApplicationMessageBuilder().WithTopic("retained").WithPayload(new byte[3]).WithRetainFlag().Build()); + + await Task.Delay(500); + + Assert.AreEqual(1, savedRetainedMessages?.Count); + } + + [TestMethod] + public async Task Publish_After_Client_Connects() + { + using var testEnvironment = CreateTestEnvironment(); + var server = await testEnvironment.StartServer(); + server.ClientConnectedAsync += async _ => + { + await server.InjectApplicationMessage( + new InjectedMqttApplicationMessage( + new MqttApplicationMessage + { + Topic = "/test/1", + PayloadSegment = new ArraySegment("true"u8.ToArray()), + QualityOfServiceLevel = MqttQualityOfServiceLevel.ExactlyOnce + }) + { + SenderClientId = "server" + }).ConfigureAwait(false); + }; + + string receivedTopic = null; + + var c1 = await testEnvironment.ConnectClient(); + c1.ApplicationMessageReceivedAsync += e => + { + receivedTopic = e.ApplicationMessage.Topic; + return CompletedTask.Instance; + }; + + await c1.SubscribeAsync("#"); + + await testEnvironment.ConnectClient(); + await testEnvironment.ConnectClient(); + await testEnvironment.ConnectClient(); + await testEnvironment.ConnectClient(); + + await Task.Delay(500); + + Assert.AreEqual("/test/1", receivedTopic); + } + + [TestMethod] + public async Task Publish_At_Least_Once_0x01() + { + await TestPublishAsync("A/B/C", MqttQualityOfServiceLevel.AtLeastOnce, "A/B/C", MqttQualityOfServiceLevel.AtLeastOnce, 1).ConfigureAwait(false); + } + + [TestMethod] + public async Task Publish_At_Most_Once_0x00() + { + await TestPublishAsync("A/B/C", MqttQualityOfServiceLevel.AtMostOnce, "A/B/C", MqttQualityOfServiceLevel.AtMostOnce, 1).ConfigureAwait(false); + } + + [TestMethod] + public async Task Publish_Exactly_Once_0x02() + { + await TestPublishAsync("A/B/C", MqttQualityOfServiceLevel.ExactlyOnce, "A/B/C", MqttQualityOfServiceLevel.ExactlyOnce, 1).ConfigureAwait(false); + } + + [TestMethod] + public async Task Publish_From_Server() + { + using var testEnvironment = CreateTestEnvironment(); + var server = await testEnvironment.StartServer(); + + var receivedMessagesCount = 0; + + var client = await testEnvironment.ConnectClient(); + client.ApplicationMessageReceivedAsync += _ => + { + Interlocked.Increment(ref receivedMessagesCount); + return CompletedTask.Instance; + }; + + var message = new MqttApplicationMessageBuilder().WithTopic("a").WithQualityOfServiceLevel(MqttQualityOfServiceLevel.AtLeastOnce).Build(); + await client.SubscribeAsync(new MqttTopicFilter { Topic = "a", QualityOfServiceLevel = MqttQualityOfServiceLevel.AtLeastOnce }); + + await server.InjectApplicationMessage( + new InjectedMqttApplicationMessage(message) + { + SenderClientId = "server" + }); + + await Task.Delay(1000); + + Assert.AreEqual(1, receivedMessagesCount); + } + + [TestMethod] + public async Task Publish_Multiple_Clients() + { + var receivedMessagesCount = 0; + + using var testEnvironment = CreateTestEnvironment(); + await testEnvironment.StartServer(); + + var c1 = await testEnvironment.ConnectClient(); + var c2 = await testEnvironment.ConnectClient(); + var c3 = await testEnvironment.ConnectClient(); + + c2.ApplicationMessageReceivedAsync += _ => + { + Interlocked.Increment(ref receivedMessagesCount); + return CompletedTask.Instance; + }; + + c3.ApplicationMessageReceivedAsync += _ => + { + Interlocked.Increment(ref receivedMessagesCount); + return CompletedTask.Instance; + }; + + await c2.SubscribeAsync(new MqttTopicFilter { Topic = "a", QualityOfServiceLevel = MqttQualityOfServiceLevel.AtLeastOnce }).ConfigureAwait(false); + await c3.SubscribeAsync(new MqttTopicFilter { Topic = "a", QualityOfServiceLevel = MqttQualityOfServiceLevel.AtLeastOnce }).ConfigureAwait(false); + + var message = new MqttApplicationMessageBuilder().WithTopic("a").WithQualityOfServiceLevel(MqttQualityOfServiceLevel.AtLeastOnce).Build(); + + for (var i = 0; i < 500; i++) + { + await c1.PublishAsync(message).ConfigureAwait(false); + } + + SpinWait.SpinUntil(() => receivedMessagesCount == 1000, TimeSpan.FromSeconds(20)); + + Assert.AreEqual(1000, receivedMessagesCount); + } + + [TestMethod] + public async Task Remove_Session() + { + using var testEnvironment = CreateTestEnvironment(); + var server = await testEnvironment.StartServer(new MqttServerOptionsBuilder()); + + var clientOptions = new MqttClientOptionsBuilder(); + var c1 = await testEnvironment.ConnectClient(clientOptions); + await Task.Delay(500); + Assert.AreEqual(1, (await server.GetClientsAsync()).Count); + + await c1.DisconnectAsync(); + await Task.Delay(500); + + Assert.AreEqual(0, (await server.GetClientsAsync()).Count); + } + + [TestMethod] + public async Task Same_Client_Id_Connect_Disconnect_Event_Order() + { + using var testEnvironment = CreateTestEnvironment(); + var server = await testEnvironment.StartServer(); + + var events = new List(); + + server.ClientConnectedAsync += _ => + { + lock (events) + { + events.Add("c"); + } + + return CompletedTask.Instance; + }; + + server.ClientDisconnectedAsync += _ => + { + lock (events) + { + events.Add("d"); + } + + return CompletedTask.Instance; + }; + + var clientOptionsBuilder = new MqttClientOptionsBuilder().WithClientId(Guid.NewGuid().ToString()); + + // c + var c1 = await testEnvironment.ConnectClient(clientOptionsBuilder); + + await LongTestDelay(); + var flow = string.Join(string.Empty, events); + Assert.AreEqual("c", flow); + + // dc + // Connect client with same client ID. Should disconnect existing client. + var c2 = await testEnvironment.ConnectClient(clientOptionsBuilder); + + await LongTestDelay(); + flow = string.Join(string.Empty, events); + Assert.AreEqual("cdc", flow); + + c2.ApplicationMessageReceivedAsync += _ => + { + lock (events) + { + events.Add("r"); + } + + return CompletedTask.Instance; + }; + + await c2.SubscribeAsync("topic"); + + // r + await c2.PublishStringAsync("topic"); + + await LongTestDelay(); + flow = string.Join(string.Empty, events); + Assert.AreEqual("cdcr", flow); + + // nothing + + Assert.AreEqual(false, c1.IsConnected); + await c1.TryDisconnectAsync(); + Assert.AreEqual(false, c1.IsConnected); + + await LongTestDelay(); + + // d + Assert.AreEqual(true, c2.IsConnected); + await c2.DisconnectAsync(); + + await LongTestDelay(); + + await server.StopAsync(); + + await LongTestDelay(); + flow = string.Join(string.Empty, events); + Assert.AreEqual("cdcrd", flow); + } + + [TestMethod] + public async Task Same_Client_Id_Refuse_Connection() + { + using var testEnvironment = CreateTestEnvironment(); + testEnvironment.IgnoreClientLogErrors = true; + + _connected = []; + + var server = await testEnvironment.StartServer(); + + server.ValidatingConnectionAsync += e => + { + ConnectionValidationHandler(e); + return CompletedTask.Instance; + }; + + var events = new List(); + + server.ClientConnectedAsync += _ => + { + lock (events) + { + events.Add("c"); + } + + return CompletedTask.Instance; + }; + + server.ClientDisconnectedAsync += _ => + { + lock (events) + { + events.Add("d"); + } + + return CompletedTask.Instance; + }; + + var clientOptions = new MqttClientOptionsBuilder().WithClientId("same_id"); + + // c + var c1 = await testEnvironment.ConnectClient(clientOptions); + + c1.DisconnectedAsync += _ => + { + lock (events) + { + events.Add("x"); + } + + return CompletedTask.Instance; + }; + + c1.ApplicationMessageReceivedAsync += _ => + { + lock (events) + { + events.Add("r"); + } + + return CompletedTask.Instance; + }; + + c1.SubscribeAsync("topic").Wait(); + + await Task.Delay(500); + + c1.PublishStringAsync("topic").Wait(); + + await Task.Delay(500); + + var flow = string.Join(string.Empty, events); + Assert.AreEqual("cr", flow); + + try + { + await testEnvironment.ConnectClient(clientOptions); + Assert.Fail("same id connection is expected to fail"); + } + catch + { + //same id connection is expected to fail + } + + await Task.Delay(500); + + flow = string.Join(string.Empty, events); + Assert.AreEqual("cr", flow); + + c1.PublishStringAsync("topic").Wait(); + + await Task.Delay(500); + + flow = string.Join(string.Empty, events); + Assert.AreEqual("crr", flow); + } + + [TestMethod] + public async Task Send_Long_Body() + { + using var testEnvironment = CreateTestEnvironment(); + const int PayloadSizeInMB = 30; + const int CharCount = PayloadSizeInMB * 1024 * 1024; + + var longBody = new byte[CharCount]; + byte @char = 32; + + for (long i = 0; i < PayloadSizeInMB * 1024L * 1024L; i++) + { + longBody[i] = @char; + + @char++; + + if (@char > 126) + { + @char = 32; + } + } + + byte[] receivedBody = null; + + await testEnvironment.StartServer(); + + var client1 = await testEnvironment.ConnectClient(); + client1.ApplicationMessageReceivedAsync += e => + { + receivedBody = e.ApplicationMessage.Payload.ToArray(); + return CompletedTask.Instance; + }; + + await client1.SubscribeAsync("string"); + + var client2 = await testEnvironment.ConnectClient(); + await client2.PublishBinaryAsync("string", longBody); + + await Task.Delay(TimeSpan.FromSeconds(5)); + + Assert.IsTrue(longBody.SequenceEqual(receivedBody ?? [])); + } + + [TestMethod] + public async Task Set_Subscription_At_Server() + { + using var testEnvironment = CreateTestEnvironment(); + var server = await testEnvironment.StartServer(); + + server.ClientConnectedAsync += async e => + { + // Every client will automatically subscribe to this topic. + await server.SubscribeAsync(e.ClientId, "topic1").ConfigureAwait(false); + }; + + var client = await testEnvironment.ConnectClient(); + var receivedMessages = new List(); + + client.ApplicationMessageReceivedAsync += e => + { + lock (receivedMessages) + { + receivedMessages.Add(e.ApplicationMessage); + } + + return CompletedTask.Instance; + }; + + await Task.Delay(500); + + await client.PublishStringAsync("Hello"); + await Task.Delay(100); + Assert.AreEqual(0, receivedMessages.Count); + + await client.PublishStringAsync("topic1"); + await Task.Delay(100); + Assert.AreEqual(1, receivedMessages.Count); + } + + [TestMethod] + public async Task Shutdown_Disconnects_Clients_Gracefully() + { + using var testEnvironment = CreateTestEnvironment(); + var server = await testEnvironment.StartServer(new MqttServerOptionsBuilder()); + + var disconnectCalled = 0; + + var c1 = await testEnvironment.ConnectClient(new MqttClientOptionsBuilder()); + c1.DisconnectedAsync += _ => + { + disconnectCalled++; + return CompletedTask.Instance; + }; + + // Stopping the server should disconnect the connection with the client + // which, it turn, will fire the disconnected event at the client. + await server.StopAsync(); + + await LongTestDelay(); + + Assert.AreEqual(1, disconnectCalled); + } + + [TestMethod] + public async Task Stop_And_Restart() + { + using var testEnvironment = CreateTestEnvironment(); + testEnvironment.IgnoreClientLogErrors = true; + + var server = await testEnvironment.StartServer(); + + await testEnvironment.ConnectClient(); + await server.StopAsync(); + + try + { + await testEnvironment.ConnectClient(); + Assert.Fail("Connecting should fail."); + } + catch + { + // Ignore errors. + } + + await server.StartAsync(); + + // The client should be able to connect again. + await testEnvironment.ConnectClient(); + } + + [TestMethod] + [DataRow("", null)] + [DataRow("", "")] + [DataRow(null, null)] + public async Task Use_Admissible_Credentials(string username, string password) + { + using var testEnvironment = CreateTestEnvironment(); + await testEnvironment.StartServer(); + + var client = testEnvironment.CreateClient(); + + var clientOptions = new MqttClientOptionsBuilder().WithTcpServer("localhost", testEnvironment.ServerPort).WithCredentials(username, password).Build(); + + var connectResult = await client.ConnectAsync(clientOptions); + + Assert.IsFalse(connectResult.IsSessionPresent); + Assert.IsTrue(client.IsConnected); + } + + [TestMethod] + public async Task Use_Empty_Client_ID() + { + using var testEnvironment = CreateTestEnvironment(); + await testEnvironment.StartServer(); + var client2 = await testEnvironment.ConnectClient(new MqttClientOptionsBuilder().WithClientId("b").WithCleanSession(false)); + await client2.PublishStringAsync("x", "1"); + await client2.PublishStringAsync("x", "2"); + await client2.PublishStringAsync("x", "3"); + await client2.DisconnectAsync(); + + var client = testEnvironment.CreateClient(); + + var clientOptions = new MqttClientOptionsBuilder().WithTcpServer("localhost", testEnvironment.ServerPort).WithClientId(string.Empty).Build(); + + var connectResult = await client.ConnectAsync(clientOptions); + + Assert.IsFalse(connectResult.IsSessionPresent); + Assert.IsTrue(client.IsConnected); + } + + [TestMethod] + public async Task Disconnect_Client_with_Reason() + { + using var testEnvironment = CreateTestEnvironment(); + var disconnectPacketReceived = false; + + string testClientId = null; + + await testEnvironment.StartServer(); + + testEnvironment.Server.ClientConnectedAsync += e => + { + testClientId = e.ClientId; + return CompletedTask.Instance; + }; + + var client = testEnvironment.CreateClient(); + + client.InspectPacketAsync += e => + { + if (e.Buffer.Length > 0) + { + if (e.Buffer[0] == (byte)MqttControlPacketType.Disconnect << 4) + { + disconnectPacketReceived = true; + } + } + return CompletedTask.Instance; + }; + + await client.ConnectAsync( + new MqttClientOptionsBuilder().WithTcpServer("localhost", testEnvironment.ServerPort) + .WithProtocolVersion(MQTTnet.Formatter.MqttProtocolVersion.V311) + .Build() + ); + + await LongTestDelay(); + + // Test client should be connected now + + Assert.IsTrue(testClientId != null); + + // Have the server disconnect the client with SessionTakenOver reason; no DISCONNECT should be sent to MQTT 3.1.1 clients + + await testEnvironment.Server.DisconnectClientAsync(testClientId, MqttDisconnectReasonCode.SessionTakenOver); + + await LongTestDelay(); + + // MQTT 3.1.1 client should not receive a disconnect packet + + Assert.IsFalse(disconnectPacketReceived, "Disconnect packet received when none is expected"); + } + + void ConnectionValidationHandler(ValidatingConnectionEventArgs eventArgs) + { + if (!_connected.TryAdd(eventArgs.ClientId, true)) + { + eventArgs.ReasonCode = MqttConnectReasonCode.BadUserNameOrPassword; + return; + } + + eventArgs.ReasonCode = MqttConnectReasonCode.Success; + } + + async Task TestPublishAsync( + string topic, + MqttQualityOfServiceLevel qualityOfServiceLevel, + string topicFilter, + MqttQualityOfServiceLevel filterQualityOfServiceLevel, + int expectedReceivedMessagesCount) + { + using var testEnvironment = CreateTestEnvironment(); + await testEnvironment.StartServer(); + + var c1 = await testEnvironment.ConnectClient(new MqttClientOptionsBuilder().WithClientId("receiver")); + var c1MessageHandler = testEnvironment.CreateApplicationMessageHandler(c1); + await c1.SubscribeAsync(new MqttTopicFilterBuilder().WithTopic(topicFilter).WithQualityOfServiceLevel(filterQualityOfServiceLevel).Build()); + + var c2 = await testEnvironment.ConnectClient(new MqttClientOptionsBuilder().WithClientId("sender")); + await c2.PublishAsync(new MqttApplicationMessageBuilder().WithTopic(topic).WithPayload([]).WithQualityOfServiceLevel(qualityOfServiceLevel).Build()); + await Task.Delay(500); + + await c2.DisconnectAsync().ConfigureAwait(false); + + await Task.Delay(500); + await c1.UnsubscribeAsync(topicFilter); + await Task.Delay(500); + + Assert.AreEqual(expectedReceivedMessagesCount, c1MessageHandler.ReceivedEventArgs.Count); + } } \ No newline at end of file diff --git a/Source/MQTTnet.Tests/Server/HotSwapCerts_Tests.cs b/Source/MQTTnet.Tests/Server/HotSwapCerts_Tests.cs index 95caa260..74b69551 100644 --- a/Source/MQTTnet.Tests/Server/HotSwapCerts_Tests.cs +++ b/Source/MQTTnet.Tests/Server/HotSwapCerts_Tests.cs @@ -15,403 +15,390 @@ using MQTTnet.Formatter; using MQTTnet.Protocol; using MQTTnet.Server; -namespace MQTTnet.Tests.Server +namespace MQTTnet.Tests.Server; +// missing certificate builder api means tests won't work for older frameworks + +// ReSharper disable InconsistentNaming +[TestClass] +public sealed class HotSwapCerts_Tests { - // missing certificate builder api means tests won't work for older frameworks + static readonly TimeSpan DefaultTimeout = TimeSpan.FromSeconds(10); - [TestClass] - public sealed class HotSwapCerts_Tests + [TestMethod] + public async Task ClientCertChangeWithoutServerUpdateFailsReconnect() { - static readonly TimeSpan DefaultTimeout = TimeSpan.FromSeconds(10); + using var server = new ServerTestHarness(); + using var client01 = new ClientTestHarness(); + server.InstallNewClientCert(client01.GetCurrentClientCert()); + client01.InstallNewServerCert(server.GetCurrentServerCert()); - [TestMethod] - public async Task ClientCertChangeWithoutServerUpdateFailsReconnect() + await server.StartServer(); + + await client01.Connect(); + + client01.WaitForConnectOrFail(DefaultTimeout); + + client01.HotSwapClientCert(); + server.ForceDisconnectAsync(client01).Wait(DefaultTimeout); + client01.WaitForDisconnectOrFail(DefaultTimeout); + + client01.WaitForConnectToFail(DefaultTimeout); + } + + [TestMethod] + public async Task ClientCertChangeWithServerUpdateAcceptsReconnect() + { + using var server = new ServerTestHarness(); + using var client01 = new ClientTestHarness(); + server.InstallNewClientCert(client01.GetCurrentClientCert()); + client01.InstallNewServerCert(server.GetCurrentServerCert()); + + await server.StartServer(); + + await client01.Connect(); + + client01.WaitForConnectOrFail(DefaultTimeout); + + client01.HotSwapClientCert(); + server.ForceDisconnectAsync(client01).Wait(DefaultTimeout); + client01.WaitForDisconnectOrFail(DefaultTimeout); + + server.InstallNewClientCert(client01.GetCurrentClientCert()); + + client01.WaitForConnectOrFail(DefaultTimeout); + } + + [TestMethod] + public async Task ServerCertChangeWithClientCertUpdateAllowsReconnect() + { + using var server = new ServerTestHarness(); + using var client01 = new ClientTestHarness(); + server.InstallNewClientCert(client01.GetCurrentClientCert()); + client01.InstallNewServerCert(server.GetCurrentServerCert()); + + await server.StartServer(); + await client01.Connect(); + + client01.WaitForConnectOrFail(DefaultTimeout); + server.HotSwapServerCert(); + + server.ForceDisconnectAsync(client01).Wait(DefaultTimeout); + client01.WaitForDisconnectOrFail(DefaultTimeout); + client01.InstallNewServerCert(server.GetCurrentServerCert()); + + client01.WaitForConnectOrFail(DefaultTimeout); + } + + [TestMethod] + public async Task ServerCertChangeWithoutClientCertUpdateFailsReconnect() + { + using var server = new ServerTestHarness(); + using var client01 = new ClientTestHarness(); + server.InstallNewClientCert(client01.GetCurrentClientCert()); + client01.InstallNewServerCert(server.GetCurrentServerCert()); + + await server.StartServer(); + await client01.Connect(); + + client01.WaitForConnectOrFail(DefaultTimeout); + server.HotSwapServerCert(); + + server.ForceDisconnectAsync(client01).Wait(DefaultTimeout); + client01.WaitForDisconnectOrFail(DefaultTimeout); + + client01.WaitForConnectToFail(DefaultTimeout); + } + + static X509Certificate2 CreateSelfSignedCertificate(string oid) + { + var sanBuilder = new SubjectAlternativeNameBuilder(); + sanBuilder.AddIpAddress(IPAddress.Loopback); + sanBuilder.AddIpAddress(IPAddress.IPv6Loopback); + sanBuilder.AddDnsName("localhost"); + + using var rsa = RSA.Create(); + var certRequest = new CertificateRequest("CN=localhost", rsa, HashAlgorithmName.SHA512, RSASignaturePadding.Pkcs1); + + certRequest.CertificateExtensions.Add( + new X509KeyUsageExtension(X509KeyUsageFlags.DataEncipherment | X509KeyUsageFlags.KeyEncipherment | X509KeyUsageFlags.DigitalSignature, false)); + + certRequest.CertificateExtensions.Add(new X509EnhancedKeyUsageExtension(new OidCollection { new Oid(oid) }, false)); + + certRequest.CertificateExtensions.Add(sanBuilder.Build()); + + using var certificate = certRequest.CreateSelfSigned(DateTimeOffset.Now.AddMinutes(-10), DateTimeOffset.Now.AddMinutes(10)); + var pfxCertificate = new X509Certificate2( + certificate.Export(X509ContentType.Pfx), + (string)null, + X509KeyStorageFlags.MachineKeySet | X509KeyStorageFlags.Exportable); + + return pfxCertificate; + } + + sealed class ClientTestHarness : IDisposable + { + readonly HotSwappableClientCertProvider _hotSwapClient = new HotSwappableClientCertProvider(); + + IMqttClient _client; + + public string ClientId => _client.Options.ClientId; + + public Task Connect() { - using (var server = new ServerTestHarness()) - using (var client01 = new ClientTestHarness()) - { - server.InstallNewClientCert(client01.GetCurrentClientCert()); - client01.InstallNewServerCert(server.GetCurrentServerCert()); - - await server.StartServer(); - - await client01.Connect(); - - client01.WaitForConnectOrFail(DefaultTimeout); - - client01.HotSwapClientCert(); - server.ForceDisconnectAsync(client01).Wait(DefaultTimeout); - client01.WaitForDisconnectOrFail(DefaultTimeout); - - client01.WaitForConnectToFail(DefaultTimeout); - } + return Run_Client_Connection(); } - [TestMethod] - public async Task ClientCertChangeWithServerUpdateAcceptsReconnect() + public void Dispose() { - using (var server = new ServerTestHarness()) - using (var client01 = new ClientTestHarness()) - { - server.InstallNewClientCert(client01.GetCurrentClientCert()); - client01.InstallNewServerCert(server.GetCurrentServerCert()); - - await server.StartServer(); - - await client01.Connect(); - - client01.WaitForConnectOrFail(DefaultTimeout); - - client01.HotSwapClientCert(); - server.ForceDisconnectAsync(client01).Wait(DefaultTimeout); - client01.WaitForDisconnectOrFail(DefaultTimeout); - - server.InstallNewClientCert(client01.GetCurrentClientCert()); - - client01.WaitForConnectOrFail(DefaultTimeout); - } + _client.Dispose(); + _hotSwapClient.Dispose(); } - [TestMethod] - public async Task ServerCertChangeWithClientCertUpdateAllowsReconnect() + public X509Certificate2 GetCurrentClientCert() { - using (var server = new ServerTestHarness()) - using (var client01 = new ClientTestHarness()) - { - server.InstallNewClientCert(client01.GetCurrentClientCert()); - client01.InstallNewServerCert(server.GetCurrentServerCert()); - - await server.StartServer(); - await client01.Connect(); - - client01.WaitForConnectOrFail(DefaultTimeout); - server.HotSwapServerCert(); - - server.ForceDisconnectAsync(client01).Wait(DefaultTimeout); - client01.WaitForDisconnectOrFail(DefaultTimeout); - client01.InstallNewServerCert(server.GetCurrentServerCert()); - - client01.WaitForConnectOrFail(DefaultTimeout); - } + var result = _hotSwapClient.GetCertificates()[0]; + return new X509Certificate2(result); } - [TestMethod] - public async Task ServerCertChangeWithoutClientCertUpdateFailsReconnect() + public void HotSwapClientCert() { - using (var server = new ServerTestHarness()) - using (var client01 = new ClientTestHarness()) - { - server.InstallNewClientCert(client01.GetCurrentClientCert()); - client01.InstallNewServerCert(server.GetCurrentServerCert()); - - await server.StartServer(); - await client01.Connect(); - - client01.WaitForConnectOrFail(DefaultTimeout); - server.HotSwapServerCert(); - - server.ForceDisconnectAsync(client01).Wait(DefaultTimeout); - client01.WaitForDisconnectOrFail(DefaultTimeout); - - client01.WaitForConnectToFail(DefaultTimeout); - } + _hotSwapClient.HotSwapCert(); } - static X509Certificate2 CreateSelfSignedCertificate(string oid) + public void InstallNewServerCert(X509Certificate2 serverCert) { - var sanBuilder = new SubjectAlternativeNameBuilder(); - sanBuilder.AddIpAddress(IPAddress.Loopback); - sanBuilder.AddIpAddress(IPAddress.IPv6Loopback); - sanBuilder.AddDnsName("localhost"); - - using (var rsa = RSA.Create()) - { - var certRequest = new CertificateRequest("CN=localhost", rsa, HashAlgorithmName.SHA512, RSASignaturePadding.Pkcs1); - - certRequest.CertificateExtensions.Add( - new X509KeyUsageExtension(X509KeyUsageFlags.DataEncipherment | X509KeyUsageFlags.KeyEncipherment | X509KeyUsageFlags.DigitalSignature, false)); - - certRequest.CertificateExtensions.Add(new X509EnhancedKeyUsageExtension(new OidCollection { new Oid(oid) }, false)); - - certRequest.CertificateExtensions.Add(sanBuilder.Build()); - - using (var certificate = certRequest.CreateSelfSigned(DateTimeOffset.Now.AddMinutes(-10), DateTimeOffset.Now.AddMinutes(10))) - { - var pfxCertificate = new X509Certificate2( - certificate.Export(X509ContentType.Pfx), - (string)null, - X509KeyStorageFlags.MachineKeySet | X509KeyStorageFlags.Exportable); - - return pfxCertificate; - } - } + _hotSwapClient.InstallNewServerCert(serverCert); } - sealed class ClientTestHarness : IDisposable + public void WaitForConnectOrFail(TimeSpan timeout) { - readonly HotSwappableClientCertProvider _hotSwapClient = new HotSwappableClientCertProvider(); + Thread.Sleep(100); - IMqttClient _client; - - public string ClientId => _client.Options.ClientId; - - public Task Connect() + if (!_client.IsConnected) { - return Run_Client_Connection(); + _client.ReconnectAsync().Wait(timeout); } - public void Dispose() - { - _client.Dispose(); - _hotSwapClient.Dispose(); - } + WaitForConnect(timeout); - public X509Certificate2 GetCurrentClientCert() - { - var result = _hotSwapClient.GetCertificates()[0]; - return new X509Certificate2(result); - } + Assert.IsNotNull(_client, "Client was never initialized"); + Assert.IsTrue(_client.IsConnected, $"Client connection failed after {timeout}"); + } - public void HotSwapClientCert() - { - _hotSwapClient.HotSwapCert(); - } + public void WaitForConnectToFail(TimeSpan timeout) + { + Assert.IsFalse(_client.IsConnected, "Client should be disconnected before waiting for connect."); - public void InstallNewServerCert(X509Certificate2 serverCert) - { - _hotSwapClient.InstallNewServerCert(serverCert); - } + WaitForConnect(timeout); - public void WaitForConnectOrFail(TimeSpan timeout) + Assert.IsNotNull(_client, "Client was never initialized"); + Assert.IsFalse(_client.IsConnected, "Client connection success but test wanted fail"); + } + + void WaitForDisconnect(TimeSpan timeout) + { + var timer = Stopwatch.StartNew(); + while ((_client == null || _client.IsConnected) && timer.Elapsed < timeout) { Thread.Sleep(100); - - if (!_client.IsConnected) - { - _client.ReconnectAsync().Wait(timeout); - } - - WaitForConnect(timeout); - - Assert.IsNotNull(_client, "Client was never initialized"); - Assert.IsTrue(_client.IsConnected, $"Client connection failed after {timeout}"); - } - - public void WaitForConnectToFail(TimeSpan timeout) - { - Assert.IsFalse(_client.IsConnected, "Client should be disconnected before waiting for connect."); - - WaitForConnect(timeout); - - Assert.IsNotNull(_client, "Client was never initialized"); - Assert.IsFalse(_client.IsConnected, "Client connection success but test wanted fail"); - } - - void WaitForDisconnect(TimeSpan timeout) - { - var timer = Stopwatch.StartNew(); - while ((_client == null || _client.IsConnected) && timer.Elapsed < timeout) - { - Thread.Sleep(100); - } - } - - public void WaitForDisconnectOrFail(TimeSpan timeout) - { - WaitForDisconnect(timeout); - - Assert.IsNotNull(_client, "Client was never initialized"); - Assert.IsFalse(_client.IsConnected, $"Client connection should have disconnected after {timeout}"); - } - - async Task Run_Client_Connection() - { - var optionsBuilder = new MqttClientOptionsBuilder() - .WithTlsOptions( - o => o.WithClientCertificatesProvider(_hotSwapClient) - .WithCertificateValidationHandler(_hotSwapClient.OnCertificateValidation) - .WithSslProtocols(SslProtocols.Tls12)) - .WithTcpServer("localhost") - .WithCleanSession() - .WithProtocolVersion(MqttProtocolVersion.V500); - - var mqttClientOptions = optionsBuilder.Build(); - - var factory = new MqttClientFactory(); - var mqttClient = factory.CreateMqttClient(); - _client = mqttClient; - - await mqttClient.ConnectAsync(mqttClientOptions); - } - - void WaitForConnect(TimeSpan timeout) - { - var timer = Stopwatch.StartNew(); - while ((_client == null || !_client.IsConnected) && timer.Elapsed < timeout) - { - Thread.Sleep(100); - } } } - sealed class ServerTestHarness : IDisposable + public void WaitForDisconnectOrFail(TimeSpan timeout) { - readonly HotSwappableServerCertProvider _hotSwapServer = new HotSwappableServerCertProvider(); + WaitForDisconnect(timeout); - MqttServer _server; - - public void Dispose() - { - if (_server != null) - { - _server.StopAsync().Wait(); - _server.Dispose(); - } - - _hotSwapServer?.Dispose(); - } - - public Task ForceDisconnectAsync(ClientTestHarness client) - { - return _server.DisconnectClientAsync(client.ClientId, MqttDisconnectReasonCode.UnspecifiedError); - } - - public X509Certificate2 GetCurrentServerCert() - { - return _hotSwapServer.GetCertificate(); - } - - public void HotSwapServerCert() - { - _hotSwapServer.HotSwapCert(); - } - - public void InstallNewClientCert(X509Certificate2 serverCert) - { - _hotSwapServer.InstallNewClientCert(serverCert); - } - - public Task StartServer() - { - var mqttServerFactory = new MqttServerFactory(); - - var mqttServerOptions = new MqttServerOptionsBuilder().WithEncryptionCertificate(_hotSwapServer) - .WithRemoteCertificateValidationCallback(_hotSwapServer.RemoteCertificateValidationCallback) - .WithEncryptedEndpoint() - .Build(); - - mqttServerOptions.TlsEndpointOptions.ClientCertificateRequired = true; - - _server = mqttServerFactory.CreateMqttServer(mqttServerOptions); - return _server.StartAsync(); - } + Assert.IsNotNull(_client, "Client was never initialized"); + Assert.IsFalse(_client.IsConnected, $"Client connection should have disconnected after {timeout}"); } - class HotSwappableClientCertProvider : IMqttClientCertificatesProvider, IDisposable + async Task Run_Client_Connection() { - X509Certificate2Collection _certificates; - ConcurrentBag _serverCerts = new ConcurrentBag(); + var optionsBuilder = new MqttClientOptionsBuilder() + .WithTlsOptions( + o => o.WithClientCertificatesProvider(_hotSwapClient) + .WithCertificateValidationHandler(_hotSwapClient.OnCertificateValidation) + .WithSslProtocols(SslProtocols.Tls12)) + .WithTcpServer("localhost") + .WithCleanSession() + .WithProtocolVersion(MqttProtocolVersion.V500); - public HotSwappableClientCertProvider() - { - _certificates = new X509Certificate2Collection(CreateSelfSignedCertificate("1.3.6.1.5.5.7.3.2")); - } + var mqttClientOptions = optionsBuilder.Build(); - public void Dispose() - { - if (_certificates != null) - { - foreach (var certs in _certificates) - { - certs.Dispose(); - } - } - } + var factory = new MqttClientFactory(); + var mqttClient = factory.CreateMqttClient(); + _client = mqttClient; - public X509CertificateCollection GetCertificates() - { - return new X509Certificate2Collection(_certificates); - } - - public void HotSwapCert() - { - _certificates = new X509Certificate2Collection(CreateSelfSignedCertificate("1.3.6.1.5.5.7.3.2")); - } - - public void InstallNewServerCert(X509Certificate2 serverCert) - { - _serverCerts.Add(serverCert); - } - - public bool OnCertificateValidation(MqttClientCertificateValidationEventArgs certContext) - { - var serverCerts = _serverCerts.ToArray(); - - var providedCert = certContext.Certificate.GetRawCertData(); - for (int i = 0, n = serverCerts.Length; i < n; i++) - { - var currentcert = serverCerts[i]; - - if (currentcert.RawData.SequenceEqual(providedCert)) - { - return true; - } - } - - return false; - } + await mqttClient.ConnectAsync(mqttClientOptions); } - sealed class HotSwappableServerCertProvider : ICertificateProvider, IDisposable + void WaitForConnect(TimeSpan timeout) { - readonly ConcurrentBag _clientCerts = new ConcurrentBag(); - X509Certificate2 _certificate; - - public HotSwappableServerCertProvider() + var timer = Stopwatch.StartNew(); + while ((_client == null || !_client.IsConnected) && timer.Elapsed < timeout) { - _certificate = CreateSelfSignedCertificate("1.3.6.1.5.5.7.3.1"); - } - - public void Dispose() - { - _certificate.Dispose(); - } - - public X509Certificate2 GetCertificate() - { - return _certificate; - } - - public void HotSwapCert() - { - var newCert = CreateSelfSignedCertificate("1.3.6.1.5.5.7.3.1"); - var oldCert = Interlocked.Exchange(ref _certificate, newCert); - - oldCert.Dispose(); - } - - public void InstallNewClientCert(X509Certificate2 certificate) - { - _clientCerts.Add(certificate); - } - - public bool RemoteCertificateValidationCallback(object sender, X509Certificate certificate, X509Chain chain, SslPolicyErrors sslPolicyErrors) - { - var serverCerts = _clientCerts.ToArray(); - - var providedCert = certificate.GetRawCertData(); - for (int i = 0, n = serverCerts.Length; i < n; i++) - { - var currentCert = serverCerts[i]; - - if (currentCert.RawData.SequenceEqual(providedCert)) - { - return true; - } - } - - return false; + Thread.Sleep(100); } } } -} + + sealed class ServerTestHarness : IDisposable + { + readonly HotSwappableServerCertProvider _hotSwapServer = new HotSwappableServerCertProvider(); + + MqttServer _server; + + public void Dispose() + { + if (_server != null) + { + _server.StopAsync().Wait(); + _server.Dispose(); + } + + _hotSwapServer?.Dispose(); + } + + public Task ForceDisconnectAsync(ClientTestHarness client) + { + return _server.DisconnectClientAsync(client.ClientId, MqttDisconnectReasonCode.UnspecifiedError); + } + + public X509Certificate2 GetCurrentServerCert() + { + return _hotSwapServer.GetCertificate(); + } + + public void HotSwapServerCert() + { + _hotSwapServer.HotSwapCert(); + } + + public void InstallNewClientCert(X509Certificate2 serverCert) + { + _hotSwapServer.InstallNewClientCert(serverCert); + } + + public Task StartServer() + { + var mqttServerFactory = new MqttServerFactory(); + + var mqttServerOptions = new MqttServerOptionsBuilder().WithEncryptionCertificate(_hotSwapServer) + .WithRemoteCertificateValidationCallback(_hotSwapServer.RemoteCertificateValidationCallback) + .WithEncryptedEndpoint() + .Build(); + + mqttServerOptions.TlsEndpointOptions.ClientCertificateRequired = true; + + _server = mqttServerFactory.CreateMqttServer(mqttServerOptions); + return _server.StartAsync(); + } + } + + class HotSwappableClientCertProvider : IMqttClientCertificatesProvider, IDisposable + { + X509Certificate2Collection _certificates; + ConcurrentBag _serverCerts = new ConcurrentBag(); + + public HotSwappableClientCertProvider() + { + _certificates = new X509Certificate2Collection(CreateSelfSignedCertificate("1.3.6.1.5.5.7.3.2")); + } + + public void Dispose() + { + if (_certificates != null) + { + foreach (var certs in _certificates) + { + certs.Dispose(); + } + } + } + + public X509CertificateCollection GetCertificates() + { + return new X509Certificate2Collection(_certificates); + } + + public void HotSwapCert() + { + _certificates = new X509Certificate2Collection(CreateSelfSignedCertificate("1.3.6.1.5.5.7.3.2")); + } + + public void InstallNewServerCert(X509Certificate2 serverCert) + { + _serverCerts.Add(serverCert); + } + + public bool OnCertificateValidation(MqttClientCertificateValidationEventArgs certContext) + { + var serverCerts = _serverCerts.ToArray(); + + var providedCert = certContext.Certificate.GetRawCertData(); + for (int i = 0, n = serverCerts.Length; i < n; i++) + { + var currentcert = serverCerts[i]; + + if (currentcert.RawData.SequenceEqual(providedCert)) + { + return true; + } + } + + return false; + } + } + + sealed class HotSwappableServerCertProvider : ICertificateProvider, IDisposable + { + readonly ConcurrentBag _clientCerts = new ConcurrentBag(); + X509Certificate2 _certificate; + + public HotSwappableServerCertProvider() + { + _certificate = CreateSelfSignedCertificate("1.3.6.1.5.5.7.3.1"); + } + + public void Dispose() + { + _certificate.Dispose(); + } + + public X509Certificate2 GetCertificate() + { + return _certificate; + } + + public void HotSwapCert() + { + var newCert = CreateSelfSignedCertificate("1.3.6.1.5.5.7.3.1"); + var oldCert = Interlocked.Exchange(ref _certificate, newCert); + + oldCert.Dispose(); + } + + public void InstallNewClientCert(X509Certificate2 certificate) + { + _clientCerts.Add(certificate); + } + + public bool RemoteCertificateValidationCallback(object sender, X509Certificate certificate, X509Chain chain, SslPolicyErrors sslPolicyErrors) + { + var serverCerts = _clientCerts.ToArray(); + + var providedCert = certificate.GetRawCertData(); + for (int i = 0, n = serverCerts.Length; i < n; i++) + { + var currentCert = serverCerts[i]; + + if (currentCert.RawData.SequenceEqual(providedCert)) + { + return true; + } + } + + return false; + } + } +} \ No newline at end of file diff --git a/Source/MQTTnet.Tests/Server/Injection_Tests.cs b/Source/MQTTnet.Tests/Server/Injection_Tests.cs index 85ac5341..bc13fd56 100644 --- a/Source/MQTTnet.Tests/Server/Injection_Tests.cs +++ b/Source/MQTTnet.Tests/Server/Injection_Tests.cs @@ -7,449 +7,449 @@ using MQTTnet.Protocol; using MQTTnet.Server; using MQTTnet.Server.Exceptions; -namespace MQTTnet.Tests.Server +namespace MQTTnet.Tests.Server; + +// ReSharper disable InconsistentNaming +[TestClass] +public sealed class Injection_Tests : BaseTestClass { - [TestClass] - public sealed class Injection_Tests : BaseTestClass + [TestMethod] + public async Task Enqueue_Application_Message_At_Session_Level() { - [TestMethod] - public async Task Enqueue_Application_Message_At_Session_Level() + using var testEnvironment = CreateTestEnvironment(); + + var server = await testEnvironment.StartServer(); + var receiver1 = await testEnvironment.ConnectClient(); + var receiver2 = await testEnvironment.ConnectClient(); + var messageReceivedHandler1 = testEnvironment.CreateApplicationMessageHandler(receiver1); + var messageReceivedHandler2 = testEnvironment.CreateApplicationMessageHandler(receiver2); + + var status = await server.GetSessionsAsync(); + var clientStatus = status[0]; + + await receiver1.SubscribeAsync("#"); + await receiver2.SubscribeAsync("#"); + + var message = new MqttApplicationMessageBuilder() + .WithQualityOfServiceLevel(MqttQualityOfServiceLevel.AtLeastOnce) + .WithTopic("InjectedOne").Build(); + + var enqueued = clientStatus.TryEnqueueApplicationMessage(message, out var injectResult); + + Assert.IsTrue(enqueued); + + await LongTestDelay(); + + Assert.AreEqual(1, messageReceivedHandler1.ReceivedEventArgs.Count); + Assert.AreEqual(injectResult.PacketIdentifier, messageReceivedHandler1.ReceivedEventArgs[0].PacketIdentifier); + Assert.AreEqual("InjectedOne", messageReceivedHandler1.ReceivedEventArgs[0].ApplicationMessage.Topic); + + // The second receiver should NOT receive the message. + Assert.AreEqual(0, messageReceivedHandler2.ReceivedEventArgs.Count); + } + + [TestMethod] + public async Task Enqueue_Application_Message_At_Session_Level_QueueOverflow_DropNewMessageStrategy() + { + using var testEnvironment = CreateTestEnvironment(trackUnobservedTaskException: false); + + var server = await testEnvironment.StartServer( + builder => builder + .WithMaxPendingMessagesPerClient(1) + .WithPendingMessagesOverflowStrategy(MqttPendingMessagesOverflowStrategy.DropNewMessage)); + + var receiver = await testEnvironment.ConnectClient(); + + var firstMessageOutboundPacketInterceptedTcs = new TaskCompletionSource(); + server.InterceptingOutboundPacketAsync += async args => { - using var testEnvironment = CreateTestEnvironment(); + // - The first message is dequeued normally and calls this delay + // - The second message fills the outbound queue + // - The third message overflows the outbound queue + if (args.Packet is MqttPublishPacket) + { + firstMessageOutboundPacketInterceptedTcs.SetResult(); + await Task.Delay(TimeSpan.FromDays(1), args.CancellationToken); + } + }; - var server = await testEnvironment.StartServer(); - var receiver1 = await testEnvironment.ConnectClient(); - var receiver2 = await testEnvironment.ConnectClient(); - var messageReceivedHandler1 = testEnvironment.CreateApplicationMessageHandler(receiver1); - var messageReceivedHandler2 = testEnvironment.CreateApplicationMessageHandler(receiver2); + var firstMessageEvicted = false; + var secondMessageEvicted = false; + var thirdMessageEvicted = false; - var status = await server.GetSessionsAsync(); - var clientStatus = status[0]; - - await receiver1.SubscribeAsync("#"); - await receiver2.SubscribeAsync("#"); - - var message = new MqttApplicationMessageBuilder() - .WithQualityOfServiceLevel(MqttQualityOfServiceLevel.AtLeastOnce) - .WithTopic("InjectedOne").Build(); - - var enqueued = clientStatus.TryEnqueueApplicationMessage(message, out var injectResult); - - Assert.IsTrue(enqueued); - - await LongTestDelay(); - - Assert.AreEqual(1, messageReceivedHandler1.ReceivedEventArgs.Count); - Assert.AreEqual(injectResult.PacketIdentifier, messageReceivedHandler1.ReceivedEventArgs[0].PacketIdentifier); - Assert.AreEqual("InjectedOne", messageReceivedHandler1.ReceivedEventArgs[0].ApplicationMessage.Topic); - - // The second receiver should NOT receive the message. - Assert.AreEqual(0, messageReceivedHandler2.ReceivedEventArgs.Count); - } - - [TestMethod] - public async Task Enqueue_Application_Message_At_Session_Level_QueueOverflow_DropNewMessageStrategy() + server.QueuedApplicationMessageOverwrittenAsync += args => { - using var testEnvironment = CreateTestEnvironment(trackUnobservedTaskException: false); - - var server = await testEnvironment.StartServer( - builder => builder - .WithMaxPendingMessagesPerClient(1) - .WithPendingMessagesOverflowStrategy(MqttPendingMessagesOverflowStrategy.DropNewMessage)); - - var receiver = await testEnvironment.ConnectClient(); - - var firstMessageOutboundPacketInterceptedTcs = new TaskCompletionSource(); - server.InterceptingOutboundPacketAsync += async args => + if (args.Packet is not MqttPublishPacket publishPacket) { - // - The first message is dequeued normally and calls this delay - // - The second message fills the outbound queue - // - The third message overflows the outbound queue - if (args.Packet is MqttPublishPacket) - { - firstMessageOutboundPacketInterceptedTcs.SetResult(); - await Task.Delay(TimeSpan.FromDays(1), args.CancellationToken); - } - }; - - var firstMessageEvicted = false; - var secondMessageEvicted = false; - var thirdMessageEvicted = false; - - server.QueuedApplicationMessageOverwrittenAsync += args => - { - if (args.Packet is not MqttPublishPacket publishPacket) - { - return Task.CompletedTask; - } - - switch (publishPacket.Topic) - { - case "InjectedOne": - firstMessageEvicted = true; - break; - case "InjectedTwo": - secondMessageEvicted = true; - break; - case "InjectedThree": - thirdMessageEvicted = true; - break; - } - return Task.CompletedTask; - }; + } - var status = await server.GetSessionsAsync(); - var clientStatus = status[0]; - await receiver.SubscribeAsync("#"); + switch (publishPacket.Topic) + { + case "InjectedOne": + firstMessageEvicted = true; + break; + case "InjectedTwo": + secondMessageEvicted = true; + break; + case "InjectedThree": + thirdMessageEvicted = true; + break; + } - var firstMessageEnqueued = clientStatus.TryEnqueueApplicationMessage( - new MqttApplicationMessageBuilder().WithTopic("InjectedOne").Build(), out _); - await firstMessageOutboundPacketInterceptedTcs.Task; + return Task.CompletedTask; + }; - var secondMessageEnqueued = clientStatus.TryEnqueueApplicationMessage( - new MqttApplicationMessageBuilder().WithTopic("InjectedTwo").Build(), out _); + var status = await server.GetSessionsAsync(); + var clientStatus = status[0]; + await receiver.SubscribeAsync("#"); - var thirdMessageEnqueued = clientStatus.TryEnqueueApplicationMessage( - new MqttApplicationMessageBuilder().WithTopic("InjectedThree").Build(), out _); + var firstMessageEnqueued = clientStatus.TryEnqueueApplicationMessage( + new MqttApplicationMessageBuilder().WithTopic("InjectedOne").Build(), out _); + await firstMessageOutboundPacketInterceptedTcs.Task; - // Due to the DropNewMessage strategy the third message will not be enqueued. - // As a result, no existing messages in the queue will be dropped (evicted). - Assert.IsTrue(firstMessageEnqueued); - Assert.IsTrue(secondMessageEnqueued); - Assert.IsFalse(thirdMessageEnqueued); + var secondMessageEnqueued = clientStatus.TryEnqueueApplicationMessage( + new MqttApplicationMessageBuilder().WithTopic("InjectedTwo").Build(), out _); - Assert.IsFalse(firstMessageEvicted); - Assert.IsFalse(secondMessageEvicted); - Assert.IsFalse(thirdMessageEvicted); - } + var thirdMessageEnqueued = clientStatus.TryEnqueueApplicationMessage( + new MqttApplicationMessageBuilder().WithTopic("InjectedThree").Build(), out _); + + // Due to the DropNewMessage strategy the third message will not be enqueued. + // As a result, no existing messages in the queue will be dropped (evicted). + Assert.IsTrue(firstMessageEnqueued); + Assert.IsTrue(secondMessageEnqueued); + Assert.IsFalse(thirdMessageEnqueued); + + Assert.IsFalse(firstMessageEvicted); + Assert.IsFalse(secondMessageEvicted); + Assert.IsFalse(thirdMessageEvicted); + } - [TestMethod] - public async Task Enqueue_Application_Message_At_Session_Level_QueueOverflow_DropOldestQueuedMessageStrategy() + [TestMethod] + public async Task Enqueue_Application_Message_At_Session_Level_QueueOverflow_DropOldestQueuedMessageStrategy() + { + using var testEnvironment = CreateTestEnvironment(trackUnobservedTaskException: false); + + var server = await testEnvironment.StartServer( + builder => builder + .WithMaxPendingMessagesPerClient(1) + .WithPendingMessagesOverflowStrategy(MqttPendingMessagesOverflowStrategy.DropOldestQueuedMessage)); + + var receiver = await testEnvironment.ConnectClient(); + + var firstMessageOutboundPacketInterceptedTcs = new TaskCompletionSource(); + server.InterceptingOutboundPacketAsync += async args => { - using var testEnvironment = CreateTestEnvironment(trackUnobservedTaskException: false); - - var server = await testEnvironment.StartServer( - builder => builder - .WithMaxPendingMessagesPerClient(1) - .WithPendingMessagesOverflowStrategy(MqttPendingMessagesOverflowStrategy.DropOldestQueuedMessage)); - - var receiver = await testEnvironment.ConnectClient(); - - var firstMessageOutboundPacketInterceptedTcs = new TaskCompletionSource(); - server.InterceptingOutboundPacketAsync += async args => + // - The first message is dequeued normally and calls this delay + // - The second message fills the outbound queue + // - The third message overflows the outbound queue + if (args.Packet is MqttPublishPacket) { - // - The first message is dequeued normally and calls this delay - // - The second message fills the outbound queue - // - The third message overflows the outbound queue - if (args.Packet is MqttPublishPacket) - { - firstMessageOutboundPacketInterceptedTcs.SetResult(); - await Task.Delay(TimeSpan.FromDays(1), args.CancellationToken); - } - }; + firstMessageOutboundPacketInterceptedTcs.SetResult(); + await Task.Delay(TimeSpan.FromDays(1), args.CancellationToken); + } + }; - var firstMessageEvicted = false; - var secondMessageEvicted = false; - var thirdMessageEvicted = false; + var firstMessageEvicted = false; + var secondMessageEvicted = false; + var thirdMessageEvicted = false; - server.QueuedApplicationMessageOverwrittenAsync += args => + server.QueuedApplicationMessageOverwrittenAsync += args => + { + if (args.Packet is not MqttPublishPacket publishPacket) { - if (args.Packet is not MqttPublishPacket publishPacket) - { - return Task.CompletedTask; - } - - switch (publishPacket.Topic) - { - case "InjectedOne": - firstMessageEvicted = true; - break; - case "InjectedTwo": - secondMessageEvicted = true; - break; - case "InjectedThree": - thirdMessageEvicted = true; - break; - } - return Task.CompletedTask; - }; + } - var status = await server.GetSessionsAsync(); - var clientStatus = status[0]; - await receiver.SubscribeAsync("#"); - - var firstMessageEnqueued = clientStatus.TryEnqueueApplicationMessage( - new MqttApplicationMessageBuilder().WithTopic("InjectedOne").Build(), out _); - await firstMessageOutboundPacketInterceptedTcs.Task; - - var secondMessageEnqueued = clientStatus.TryEnqueueApplicationMessage( - new MqttApplicationMessageBuilder().WithTopic("InjectedTwo").Build(), out _); - - var thirdMessageEnqueued = clientStatus.TryEnqueueApplicationMessage( - new MqttApplicationMessageBuilder().WithTopic("InjectedThree").Build(), out _); - - // Due to the DropOldestQueuedMessage strategy, all messages will be enqueued initially. - // But the second message will eventually be dropped (evicted) to make room for the third one. - Assert.IsTrue(firstMessageEnqueued); - Assert.IsTrue(secondMessageEnqueued); - Assert.IsTrue(thirdMessageEnqueued); - - Assert.IsFalse(firstMessageEvicted); - Assert.IsTrue(secondMessageEvicted); - Assert.IsFalse(thirdMessageEvicted); - } - - [TestMethod] - public async Task Deliver_Application_Message_At_Session_Level() - { - using var testEnvironment = CreateTestEnvironment(); - - var server = await testEnvironment.StartServer(); - var receiver1 = await testEnvironment.ConnectClient(); - var receiver2 = await testEnvironment.ConnectClient(); - var messageReceivedHandler1 = testEnvironment.CreateApplicationMessageHandler(receiver1); - var messageReceivedHandler2 = testEnvironment.CreateApplicationMessageHandler(receiver2); - - var status = await server.GetSessionsAsync(); - var clientStatus = status[0]; - - await receiver1.SubscribeAsync("#"); - await receiver2.SubscribeAsync("#"); - - var mqttApplicationMessage = new MqttApplicationMessageBuilder() - .WithTopic("InjectedOne") - .WithQualityOfServiceLevel(MqttQualityOfServiceLevel.AtLeastOnce) - .Build(); - var injectResult = await clientStatus.DeliverApplicationMessageAsync(mqttApplicationMessage); - - await LongTestDelay(); - - Assert.AreEqual(1, messageReceivedHandler1.ReceivedEventArgs.Count); - Assert.AreEqual(injectResult.PacketIdentifier, messageReceivedHandler1.ReceivedEventArgs[0].PacketIdentifier); - Assert.AreEqual("InjectedOne", messageReceivedHandler1.ReceivedEventArgs[0].ApplicationMessage.Topic); - - // The second receiver should NOT receive the message. - Assert.AreEqual(0, messageReceivedHandler2.ReceivedEventArgs.Count); - } - - [TestMethod] - public async Task Deliver_Application_Message_At_Session_Level_QueueOverflow_DropNewMessageStrategy() - { - using var testEnvironment = CreateTestEnvironment(); - - var server = await testEnvironment.StartServer( - builder => builder - .WithMaxPendingMessagesPerClient(1) - .WithPendingMessagesOverflowStrategy(MqttPendingMessagesOverflowStrategy.DropNewMessage)); - - var receiver = await testEnvironment.ConnectClient(); - - var firstMessageOutboundPacketInterceptedTcs = new TaskCompletionSource(); - server.InterceptingOutboundPacketAsync += async args => + switch (publishPacket.Topic) { - // - The first message is dequeued normally and calls this delay - // - The second message fills the outbound queue - // - The third message overflows the outbound queue - if (args.Packet is MqttPublishPacket) - { - firstMessageOutboundPacketInterceptedTcs.SetResult(); - await Task.Delay(TimeSpan.FromDays(1), args.CancellationToken); - } - }; + case "InjectedOne": + firstMessageEvicted = true; + break; + case "InjectedTwo": + secondMessageEvicted = true; + break; + case "InjectedThree": + thirdMessageEvicted = true; + break; + } - var firstMessageEvicted = false; - var secondMessageEvicted = false; - var thirdMessageEvicted = false; + return Task.CompletedTask; + }; - server.QueuedApplicationMessageOverwrittenAsync += args => + var status = await server.GetSessionsAsync(); + var clientStatus = status[0]; + await receiver.SubscribeAsync("#"); + + var firstMessageEnqueued = clientStatus.TryEnqueueApplicationMessage( + new MqttApplicationMessageBuilder().WithTopic("InjectedOne").Build(), out _); + await firstMessageOutboundPacketInterceptedTcs.Task; + + var secondMessageEnqueued = clientStatus.TryEnqueueApplicationMessage( + new MqttApplicationMessageBuilder().WithTopic("InjectedTwo").Build(), out _); + + var thirdMessageEnqueued = clientStatus.TryEnqueueApplicationMessage( + new MqttApplicationMessageBuilder().WithTopic("InjectedThree").Build(), out _); + + // Due to the DropOldestQueuedMessage strategy, all messages will be enqueued initially. + // But the second message will eventually be dropped (evicted) to make room for the third one. + Assert.IsTrue(firstMessageEnqueued); + Assert.IsTrue(secondMessageEnqueued); + Assert.IsTrue(thirdMessageEnqueued); + + Assert.IsFalse(firstMessageEvicted); + Assert.IsTrue(secondMessageEvicted); + Assert.IsFalse(thirdMessageEvicted); + } + + [TestMethod] + public async Task Deliver_Application_Message_At_Session_Level() + { + using var testEnvironment = CreateTestEnvironment(); + + var server = await testEnvironment.StartServer(); + var receiver1 = await testEnvironment.ConnectClient(); + var receiver2 = await testEnvironment.ConnectClient(); + var messageReceivedHandler1 = testEnvironment.CreateApplicationMessageHandler(receiver1); + var messageReceivedHandler2 = testEnvironment.CreateApplicationMessageHandler(receiver2); + + var status = await server.GetSessionsAsync(); + var clientStatus = status[0]; + + await receiver1.SubscribeAsync("#"); + await receiver2.SubscribeAsync("#"); + + var mqttApplicationMessage = new MqttApplicationMessageBuilder() + .WithTopic("InjectedOne") + .WithQualityOfServiceLevel(MqttQualityOfServiceLevel.AtLeastOnce) + .Build(); + var injectResult = await clientStatus.DeliverApplicationMessageAsync(mqttApplicationMessage); + + await LongTestDelay(); + + Assert.AreEqual(1, messageReceivedHandler1.ReceivedEventArgs.Count); + Assert.AreEqual(injectResult.PacketIdentifier, messageReceivedHandler1.ReceivedEventArgs[0].PacketIdentifier); + Assert.AreEqual("InjectedOne", messageReceivedHandler1.ReceivedEventArgs[0].ApplicationMessage.Topic); + + // The second receiver should NOT receive the message. + Assert.AreEqual(0, messageReceivedHandler2.ReceivedEventArgs.Count); + } + + [TestMethod] + public async Task Deliver_Application_Message_At_Session_Level_QueueOverflow_DropNewMessageStrategy() + { + using var testEnvironment = CreateTestEnvironment(); + + var server = await testEnvironment.StartServer( + builder => builder + .WithMaxPendingMessagesPerClient(1) + .WithPendingMessagesOverflowStrategy(MqttPendingMessagesOverflowStrategy.DropNewMessage)); + + var receiver = await testEnvironment.ConnectClient(); + + var firstMessageOutboundPacketInterceptedTcs = new TaskCompletionSource(); + server.InterceptingOutboundPacketAsync += async args => + { + // - The first message is dequeued normally and calls this delay + // - The second message fills the outbound queue + // - The third message overflows the outbound queue + if (args.Packet is MqttPublishPacket) { - if (args.Packet is not MqttPublishPacket publishPacket) - { - return Task.CompletedTask; - } + firstMessageOutboundPacketInterceptedTcs.SetResult(); + await Task.Delay(TimeSpan.FromDays(1), args.CancellationToken); + } + }; - switch (publishPacket.Topic) - { - case "InjectedOne": - firstMessageEvicted = true; - break; - case "InjectedTwo": - secondMessageEvicted = true; - break; - case "InjectedThree": - thirdMessageEvicted = true; - break; - } + var firstMessageEvicted = false; + var secondMessageEvicted = false; + var thirdMessageEvicted = false; + server.QueuedApplicationMessageOverwrittenAsync += args => + { + if (args.Packet is not MqttPublishPacket publishPacket) + { return Task.CompletedTask; - }; + } - var status = await server.GetSessionsAsync(); - var clientStatus = status[0]; - await receiver.SubscribeAsync("#"); + switch (publishPacket.Topic) + { + case "InjectedOne": + firstMessageEvicted = true; + break; + case "InjectedTwo": + secondMessageEvicted = true; + break; + case "InjectedThree": + thirdMessageEvicted = true; + break; + } - var firstMessageTask = Task.Run( - () => clientStatus.DeliverApplicationMessageAsync( - new MqttApplicationMessageBuilder().WithTopic("InjectedOne").Build())); - await LongTestDelay(); - await firstMessageOutboundPacketInterceptedTcs.Task; + return Task.CompletedTask; + }; - var secondMessageTask = Task.Run( - () => clientStatus.DeliverApplicationMessageAsync( - new MqttApplicationMessageBuilder().WithTopic("InjectedTwo").Build())); - await LongTestDelay(); + var status = await server.GetSessionsAsync(); + var clientStatus = status[0]; + await receiver.SubscribeAsync("#"); - var thirdMessageTask = Task.Run( - () => clientStatus.DeliverApplicationMessageAsync( - new MqttApplicationMessageBuilder().WithTopic("InjectedThree").Build())); - await LongTestDelay(); + var firstMessageTask = Task.Run( + () => clientStatus.DeliverApplicationMessageAsync( + new MqttApplicationMessageBuilder().WithTopic("InjectedOne").Build())); + await LongTestDelay(); + await firstMessageOutboundPacketInterceptedTcs.Task; - Task.WaitAny(firstMessageTask, secondMessageTask, thirdMessageTask); + var secondMessageTask = Task.Run( + () => clientStatus.DeliverApplicationMessageAsync( + new MqttApplicationMessageBuilder().WithTopic("InjectedTwo").Build())); + await LongTestDelay(); - // Due to the DropNewMessage strategy the third message delivery will fail. - // As a result, no existing messages in the queue will be dropped (evicted). - Assert.AreEqual(firstMessageTask.Status, TaskStatus.WaitingForActivation); - Assert.AreEqual(secondMessageTask.Status, TaskStatus.WaitingForActivation); - Assert.AreEqual(thirdMessageTask.Status, TaskStatus.Faulted); - Assert.IsTrue(thirdMessageTask.Exception?.InnerException is MqttPendingMessagesOverflowException); + var thirdMessageTask = Task.Run( + () => clientStatus.DeliverApplicationMessageAsync( + new MqttApplicationMessageBuilder().WithTopic("InjectedThree").Build())); + await LongTestDelay(); - Assert.IsFalse(firstMessageEvicted); - Assert.IsFalse(secondMessageEvicted); - Assert.IsFalse(thirdMessageEvicted); - } + Task.WaitAny(firstMessageTask, secondMessageTask, thirdMessageTask); - [TestMethod] - public async Task Deliver_Application_Message_At_Session_Level_QueueOverflow_DropOldestQueuedMessageStrategy() + // Due to the DropNewMessage strategy the third message delivery will fail. + // As a result, no existing messages in the queue will be dropped (evicted). + Assert.AreEqual(firstMessageTask.Status, TaskStatus.WaitingForActivation); + Assert.AreEqual(secondMessageTask.Status, TaskStatus.WaitingForActivation); + Assert.AreEqual(thirdMessageTask.Status, TaskStatus.Faulted); + Assert.IsTrue(thirdMessageTask.Exception?.InnerException is MqttPendingMessagesOverflowException); + + Assert.IsFalse(firstMessageEvicted); + Assert.IsFalse(secondMessageEvicted); + Assert.IsFalse(thirdMessageEvicted); + } + + [TestMethod] + public async Task Deliver_Application_Message_At_Session_Level_QueueOverflow_DropOldestQueuedMessageStrategy() + { + using var testEnvironment = CreateTestEnvironment(trackUnobservedTaskException: false); + + var server = await testEnvironment.StartServer( + builder => builder + .WithMaxPendingMessagesPerClient(1) + .WithPendingMessagesOverflowStrategy(MqttPendingMessagesOverflowStrategy.DropOldestQueuedMessage)); + + var receiver = await testEnvironment.ConnectClient(); + + var firstMessageOutboundPacketInterceptedTcs = new TaskCompletionSource(); + server.InterceptingOutboundPacketAsync += async args => { - using var testEnvironment = CreateTestEnvironment(trackUnobservedTaskException: false); - - var server = await testEnvironment.StartServer( - builder => builder - .WithMaxPendingMessagesPerClient(1) - .WithPendingMessagesOverflowStrategy(MqttPendingMessagesOverflowStrategy.DropOldestQueuedMessage)); - - var receiver = await testEnvironment.ConnectClient(); - - var firstMessageOutboundPacketInterceptedTcs = new TaskCompletionSource(); - server.InterceptingOutboundPacketAsync += async args => + // - The first message is dequeued normally and calls this delay + // - The second message fills the outbound queue + // - The third message overflows the outbound queue + if (args.Packet is MqttPublishPacket) { - // - The first message is dequeued normally and calls this delay - // - The second message fills the outbound queue - // - The third message overflows the outbound queue - if (args.Packet is MqttPublishPacket) - { - firstMessageOutboundPacketInterceptedTcs.SetResult(); - await Task.Delay(TimeSpan.FromDays(1), args.CancellationToken); - } - }; + firstMessageOutboundPacketInterceptedTcs.SetResult(); + await Task.Delay(TimeSpan.FromDays(1), args.CancellationToken); + } + }; - var firstMessageEvicted = false; - var secondMessageEvicted = false; - var thirdMessageEvicted = false; + var firstMessageEvicted = false; + var secondMessageEvicted = false; + var thirdMessageEvicted = false; - server.QueuedApplicationMessageOverwrittenAsync += args => + server.QueuedApplicationMessageOverwrittenAsync += args => + { + if (args.Packet is not MqttPublishPacket publishPacket) { - if (args.Packet is not MqttPublishPacket publishPacket) - { - return Task.CompletedTask; - } - - switch (publishPacket.Topic) - { - case "InjectedOne": - firstMessageEvicted = true; - break; - case "InjectedTwo": - secondMessageEvicted = true; - break; - case "InjectedThree": - thirdMessageEvicted = true; - break; - } - return Task.CompletedTask; - }; + } - var status = await server.GetSessionsAsync(); - var clientStatus = status[0]; - await receiver.SubscribeAsync("#"); - - var firstMessageTask = Task.Run( - () => clientStatus.DeliverApplicationMessageAsync( - new MqttApplicationMessageBuilder().WithTopic("InjectedOne").Build())); - await LongTestDelay(); - await firstMessageOutboundPacketInterceptedTcs.Task; - - var secondMessageTask = Task.Run( - () => clientStatus.DeliverApplicationMessageAsync( - new MqttApplicationMessageBuilder().WithTopic("InjectedTwo").Build())); - await LongTestDelay(); - - var thirdMessageTask = Task.Run( - () => clientStatus.DeliverApplicationMessageAsync( - new MqttApplicationMessageBuilder().WithTopic("InjectedThree").Build())); - await LongTestDelay(); - - Task.WaitAny(firstMessageTask, secondMessageTask, thirdMessageTask); - - // Due to the DropOldestQueuedMessage strategy, the second message delivery will fail - // to make room for the third one. - Assert.AreEqual(firstMessageTask.Status, TaskStatus.WaitingForActivation); - Assert.AreEqual(secondMessageTask.Status, TaskStatus.Faulted); - Assert.IsTrue(secondMessageTask.Exception?.InnerException is MqttPendingMessagesOverflowException); - Assert.AreEqual(thirdMessageTask.Status, TaskStatus.WaitingForActivation); - - Assert.IsFalse(firstMessageEvicted); - Assert.IsTrue(secondMessageEvicted); - Assert.IsFalse(thirdMessageEvicted); - } - - [TestMethod] - public async Task Inject_ApplicationMessage_At_Server_Level() - { - using var testEnvironment = CreateTestEnvironment(); - - var server = await testEnvironment.StartServer(); - - var receiver = await testEnvironment.ConnectClient(); - - var messageReceivedHandler = testEnvironment.CreateApplicationMessageHandler(receiver); - - await receiver.SubscribeAsync("#"); - - var injectedApplicationMessage = new MqttApplicationMessageBuilder().WithTopic("InjectedOne").Build(); - - await server.InjectApplicationMessage(new InjectedMqttApplicationMessage(injectedApplicationMessage)); - - await LongTestDelay(); - - Assert.AreEqual(1, messageReceivedHandler.ReceivedEventArgs.Count); - Assert.AreEqual("InjectedOne", messageReceivedHandler.ReceivedEventArgs[0].ApplicationMessage.Topic); - } - - [TestMethod] - public async Task Intercept_Injected_Application_Message() - { - using var testEnvironment = CreateTestEnvironment(); - - var server = await testEnvironment.StartServer(); - - MqttApplicationMessage interceptedMessage = null; - server.InterceptingPublishAsync += eventArgs => + switch (publishPacket.Topic) { - interceptedMessage = eventArgs.ApplicationMessage; - return CompletedTask.Instance; - }; + case "InjectedOne": + firstMessageEvicted = true; + break; + case "InjectedTwo": + secondMessageEvicted = true; + break; + case "InjectedThree": + thirdMessageEvicted = true; + break; + } - var injectedApplicationMessage = new MqttApplicationMessageBuilder().WithTopic("InjectedOne").Build(); - await server.InjectApplicationMessage(new InjectedMqttApplicationMessage(injectedApplicationMessage)); + return Task.CompletedTask; + }; - await LongTestDelay(); + var status = await server.GetSessionsAsync(); + var clientStatus = status[0]; + await receiver.SubscribeAsync("#"); - Assert.IsNotNull(interceptedMessage); - } + var firstMessageTask = Task.Run( + () => clientStatus.DeliverApplicationMessageAsync( + new MqttApplicationMessageBuilder().WithTopic("InjectedOne").Build())); + await LongTestDelay(); + await firstMessageOutboundPacketInterceptedTcs.Task; + + var secondMessageTask = Task.Run( + () => clientStatus.DeliverApplicationMessageAsync( + new MqttApplicationMessageBuilder().WithTopic("InjectedTwo").Build())); + await LongTestDelay(); + + var thirdMessageTask = Task.Run( + () => clientStatus.DeliverApplicationMessageAsync( + new MqttApplicationMessageBuilder().WithTopic("InjectedThree").Build())); + await LongTestDelay(); + + Task.WaitAny(firstMessageTask, secondMessageTask, thirdMessageTask); + + // Due to the DropOldestQueuedMessage strategy, the second message delivery will fail + // to make room for the third one. + Assert.AreEqual(firstMessageTask.Status, TaskStatus.WaitingForActivation); + Assert.AreEqual(secondMessageTask.Status, TaskStatus.Faulted); + Assert.IsTrue(secondMessageTask.Exception?.InnerException is MqttPendingMessagesOverflowException); + Assert.AreEqual(thirdMessageTask.Status, TaskStatus.WaitingForActivation); + + Assert.IsFalse(firstMessageEvicted); + Assert.IsTrue(secondMessageEvicted); + Assert.IsFalse(thirdMessageEvicted); + } + + [TestMethod] + public async Task Inject_ApplicationMessage_At_Server_Level() + { + using var testEnvironment = CreateTestEnvironment(); + + var server = await testEnvironment.StartServer(); + + var receiver = await testEnvironment.ConnectClient(); + + var messageReceivedHandler = testEnvironment.CreateApplicationMessageHandler(receiver); + + await receiver.SubscribeAsync("#"); + + var injectedApplicationMessage = new MqttApplicationMessageBuilder().WithTopic("InjectedOne").Build(); + + await server.InjectApplicationMessage(new InjectedMqttApplicationMessage(injectedApplicationMessage)); + + await LongTestDelay(); + + Assert.AreEqual(1, messageReceivedHandler.ReceivedEventArgs.Count); + Assert.AreEqual("InjectedOne", messageReceivedHandler.ReceivedEventArgs[0].ApplicationMessage.Topic); + } + + [TestMethod] + public async Task Intercept_Injected_Application_Message() + { + using var testEnvironment = CreateTestEnvironment(); + + var server = await testEnvironment.StartServer(); + + MqttApplicationMessage interceptedMessage = null; + server.InterceptingPublishAsync += eventArgs => + { + interceptedMessage = eventArgs.ApplicationMessage; + return CompletedTask.Instance; + }; + + var injectedApplicationMessage = new MqttApplicationMessageBuilder().WithTopic("InjectedOne").Build(); + await server.InjectApplicationMessage(new InjectedMqttApplicationMessage(injectedApplicationMessage)); + + await LongTestDelay(); + + Assert.IsNotNull(interceptedMessage); } } \ No newline at end of file diff --git a/Source/MQTTnet.Tests/Server/Keep_Alive_Tests.cs b/Source/MQTTnet.Tests/Server/Keep_Alive_Tests.cs index 8bcd8b0a..070a396f 100644 --- a/Source/MQTTnet.Tests/Server/Keep_Alive_Tests.cs +++ b/Source/MQTTnet.Tests/Server/Keep_Alive_Tests.cs @@ -10,59 +10,57 @@ using MQTTnet.Formatter; using MQTTnet.Packets; using MQTTnet.Protocol; -namespace MQTTnet.Tests.Server +namespace MQTTnet.Tests.Server; + +// ReSharper disable InconsistentNaming +[TestClass] +public sealed class KeepAlive_Tests : BaseTestClass { - [TestClass] - public sealed class KeepAlive_Tests : BaseTestClass + [TestMethod] + public async Task Disconnect_Client_DueTo_KeepAlive() { - [TestMethod] - public async Task Disconnect_Client_DueTo_KeepAlive() + using var testEnvironment = CreateTestEnvironment(); + await testEnvironment.StartServer(); + + var client = await testEnvironment.ConnectLowLevelClient(o => o + .WithTimeout(TimeSpan.FromSeconds(1)) + .WithTimeout(TimeSpan.Zero) + .WithProtocolVersion(MqttProtocolVersion.V500)).ConfigureAwait(false); + + await client.SendAsync(new MqttConnectPacket { - using (var testEnvironment = CreateTestEnvironment()) - { - await testEnvironment.StartServer(); + CleanSession = true, + ClientId = "Disconnect_Client_DueTo_KeepAlive", + KeepAlivePeriod = 1 + }, CancellationToken.None).ConfigureAwait(false); - var client = await testEnvironment.ConnectLowLevelClient(o => o - .WithTimeout(TimeSpan.FromSeconds(1)) - .WithTimeout(TimeSpan.Zero) - .WithProtocolVersion(MqttProtocolVersion.V500)).ConfigureAwait(false); + var responsePacket = await client.ReceiveAsync(CancellationToken.None).ConfigureAwait(false); + Assert.IsTrue(responsePacket is MqttConnAckPacket); - await client.SendAsync(new MqttConnectPacket - { - CleanSession = true, - ClientId = "Disconnect_Client_DueTo_KeepAlive", - KeepAlivePeriod = 1 - }, CancellationToken.None).ConfigureAwait(false); + for (var i = 0; i < 6; i++) + { + await Task.Delay(500); - var responsePacket = await client.ReceiveAsync(CancellationToken.None).ConfigureAwait(false); - Assert.IsTrue(responsePacket is MqttConnAckPacket); - - for (var i = 0; i < 6; i++) - { - await Task.Delay(500); - - await client.SendAsync(MqttPingReqPacket.Instance, CancellationToken.None); - responsePacket = await client.ReceiveAsync(CancellationToken.None); - Assert.IsTrue(responsePacket is MqttPingRespPacket); - } - - // If we reach this point everything works as expected (server did not close the connection - // due to proper ping messages. - // Now we will wait 1.1 seconds because the server MUST wait 1.5 seconds in total (See spec). - - await Task.Delay(1100); - await client.SendAsync(MqttPingReqPacket.Instance, CancellationToken.None); - responsePacket = await client.ReceiveAsync(CancellationToken.None); - Assert.IsTrue(responsePacket is MqttPingRespPacket); - - // Now we will wait longer than 1.5 so that the server will close the connection. - responsePacket = await client.ReceiveAsync(CancellationToken.None); - - var disconnectPacket = responsePacket as MqttDisconnectPacket; - - Assert.IsTrue(disconnectPacket != null); - Assert.AreEqual(disconnectPacket.ReasonCode, MqttDisconnectReasonCode.KeepAliveTimeout); - } + await client.SendAsync(MqttPingReqPacket.Instance, CancellationToken.None); + responsePacket = await client.ReceiveAsync(CancellationToken.None); + Assert.IsTrue(responsePacket is MqttPingRespPacket); } + + // If we reach this point everything works as expected (server did not close the connection + // due to proper ping messages. + // Now we will wait 1.1 seconds because the server MUST wait 1.5 seconds in total (See spec). + + await Task.Delay(1100); + await client.SendAsync(MqttPingReqPacket.Instance, CancellationToken.None); + responsePacket = await client.ReceiveAsync(CancellationToken.None); + Assert.IsTrue(responsePacket is MqttPingRespPacket); + + // Now we will wait longer than 1.5 so that the server will close the connection. + responsePacket = await client.ReceiveAsync(CancellationToken.None); + + var disconnectPacket = responsePacket as MqttDisconnectPacket; + + Assert.IsTrue(disconnectPacket != null); + Assert.AreEqual(disconnectPacket.ReasonCode, MqttDisconnectReasonCode.KeepAliveTimeout); } } \ No newline at end of file diff --git a/Source/MQTTnet.Tests/Server/Load_Tests.cs b/Source/MQTTnet.Tests/Server/Load_Tests.cs index 466e1cfa..64e1b685 100644 --- a/Source/MQTTnet.Tests/Server/Load_Tests.cs +++ b/Source/MQTTnet.Tests/Server/Load_Tests.cs @@ -6,166 +6,154 @@ using MQTTnet.Internal; using MQTTnet.Packets; using MQTTnet.Protocol; -namespace MQTTnet.Tests.Server +namespace MQTTnet.Tests.Server; + +// ReSharper disable InconsistentNaming +#if DEBUG +[TestClass] +#endif +public sealed class Load_Tests : BaseTestClass { - [TestClass] - public sealed class Load_Tests : BaseTestClass + [TestMethod] + public async Task Handle_100_000_Messages_In_Low_Level_Client() { - [TestMethod] - public async Task Handle_100_000_Messages_In_Receiving_Client() + using var testEnvironment = CreateTestEnvironment(); + var server = await testEnvironment.StartServer(); + + var receivedMessages = 0; + + server.InterceptingPublishAsync += _ => { - using (var testEnvironment = CreateTestEnvironment()) - { - await testEnvironment.StartServer(); + Interlocked.Increment(ref receivedMessages); + return CompletedTask.Instance; + }; - var receivedMessages = 0; - - using (var receiverClient = await testEnvironment.ConnectClient()) + for (var i = 0; i < 100; i++) + { + _ = Task.Factory.StartNew( + async () => { - receiverClient.ApplicationMessageReceivedAsync += e => + try { - Interlocked.Increment(ref receivedMessages); - return CompletedTask.Instance; - }; + using var client = await testEnvironment.ConnectLowLevelClient(); - await receiverClient.SubscribeAsync("t/+"); - - - for (var i = 0; i < 100; i++) - { - _ = Task.Run( - async () => + await client.SendAsync( + new MqttConnectPacket { - using (var client = await testEnvironment.ConnectClient()) - { - var applicationMessageBuilder = new MqttApplicationMessageBuilder(); + ClientId = "Handle_100_000_Messages_In_Low_Level_Client_" + Guid.NewGuid() + }, + CancellationToken.None); - for (var j = 0; j < 1000; j++) - { - var message = applicationMessageBuilder.WithTopic("t/" + j) - .Build(); + var packet = await client.ReceiveAsync(CancellationToken.None); - await client.PublishAsync(message) - .ConfigureAwait(false); - } + var connAckPacket = packet as MqttConnAckPacket; - await client.DisconnectAsync(); - } - }); + Assert.IsTrue(connAckPacket != null); + Assert.AreEqual(MqttConnectReasonCode.Success, connAckPacket.ReasonCode); + + var publishPacket = new MqttPublishPacket(); + + for (var j = 0; j < 1000; j++) + { + publishPacket.Topic = j.ToString(); + + await client.SendAsync(publishPacket, CancellationToken.None).ConfigureAwait(false); + } + + await client.DisconnectAsync(CancellationToken.None); + } + catch (Exception exception) + { + Console.WriteLine(exception); + } + }, + TaskCreationOptions.LongRunning); + } + + var result = SpinWait.SpinUntil(() => receivedMessages >= 100000, TimeSpan.FromMinutes(5)); + + Assert.IsTrue(result); + } + + [TestMethod] + public async Task Handle_100_000_Messages_In_Receiving_Client() + { + using var testEnvironment = CreateTestEnvironment(); + await testEnvironment.StartServer(); + + var receivedMessages = 0; + + using var receiverClient = await testEnvironment.ConnectClient(); + + receiverClient.ApplicationMessageReceivedAsync += _ => + { + Interlocked.Increment(ref receivedMessages); + return CompletedTask.Instance; + }; + + await receiverClient.SubscribeAsync("t/+"); + + for (var i = 0; i < 100; i++) + { + _ = Task.Factory.StartNew( + async () => + { + using var client = await testEnvironment.ConnectClient(); + var applicationMessageBuilder = testEnvironment.ClientFactory.CreateApplicationMessageBuilder(); + + for (var j = 0; j < 1000; j++) + { + var message = applicationMessageBuilder.WithTopic("t/" + j).Build(); + + await client.PublishAsync(message).ConfigureAwait(false); } - SpinWait.SpinUntil(() => receivedMessages == 100000, TimeSpan.FromSeconds(120)); - - Assert.AreEqual(100000, receivedMessages); - } - } + await client.DisconnectAsync(); + }, + TaskCreationOptions.LongRunning); } - [TestMethod] - public async Task Handle_100_000_Messages_In_Low_Level_Client() + var result = SpinWait.SpinUntil(() => receivedMessages >= 100000, TimeSpan.FromMinutes(5)); + + Assert.IsTrue(result); + } + + [TestMethod] + public async Task Handle_100_000_Messages_In_Server() + { + using var testEnvironment = CreateTestEnvironment(); + var server = await testEnvironment.StartServer(); + + var receivedMessages = 0; + + server.InterceptingPublishAsync += _ => { - using (var testEnvironment = CreateTestEnvironment()) - { - var server = await testEnvironment.StartServer(); + Interlocked.Increment(ref receivedMessages); + return CompletedTask.Instance; + }; - var receivedMessages = 0; - - server.InterceptingPublishAsync += e => - { - Interlocked.Increment(ref receivedMessages); - return CompletedTask.Instance; - }; - - for (var i = 0; i < 100; i++) - { - _ = Task.Run( - async () => - { - try - { - using (var client = await testEnvironment.ConnectLowLevelClient()) - { - await client.SendAsync( - new MqttConnectPacket - { - ClientId = "Handle_100_000_Messages_In_Low_Level_Client_" + Guid.NewGuid() - }, CancellationToken.None); - - var packet = await client.ReceiveAsync(CancellationToken.None); - - var connAckPacket = packet as MqttConnAckPacket; - - Assert.IsTrue(connAckPacket != null); - Assert.AreEqual(MqttConnectReasonCode.Success, connAckPacket.ReasonCode); - - var publishPacket = new MqttPublishPacket(); - - for (var j = 0; j < 1000; j++) - { - publishPacket.Topic = j.ToString(); - - await client.SendAsync(publishPacket, CancellationToken.None) - .ConfigureAwait(false); - } - - await client.DisconnectAsync(CancellationToken.None); - } - } - catch (Exception exception) - { - Console.WriteLine(exception); - } - }); - } - - SpinWait.SpinUntil(() => receivedMessages == 100000, TimeSpan.FromSeconds(10)); - - Assert.AreEqual(100000, receivedMessages); - } - } - - [TestMethod] - public async Task Handle_100_000_Messages_In_Server() + for (var i = 0; i < 100; i++) { - using (var testEnvironment = CreateTestEnvironment()) - { - var server = await testEnvironment.StartServer(); - - var receivedMessages = 0; - - server.InterceptingPublishAsync += e => + _ = Task.Factory.StartNew( + async () => { - Interlocked.Increment(ref receivedMessages); - return CompletedTask.Instance; - }; + using var client = await testEnvironment.ConnectClient(); + var applicationMessageBuilder = new MqttApplicationMessageBuilder(); - for (var i = 0; i < 100; i++) - { - _ = Task.Run( - async () => - { - using (var client = await testEnvironment.ConnectClient()) - { - var applicationMessageBuilder = new MqttApplicationMessageBuilder(); + for (var j = 0; j < 1000; j++) + { + var message = applicationMessageBuilder.WithTopic(j.ToString()).Build(); - for (var j = 0; j < 1000; j++) - { - var message = applicationMessageBuilder.WithTopic(j.ToString()) - .Build(); + await client.PublishAsync(message).ConfigureAwait(false); + } - await client.PublishAsync(message) - .ConfigureAwait(false); - } - - await client.DisconnectAsync(); - } - }); - } - - SpinWait.SpinUntil(() => receivedMessages == 100000, TimeSpan.FromSeconds(10)); - - Assert.AreEqual(100000, receivedMessages); - } + await client.DisconnectAsync(); + }, + TaskCreationOptions.LongRunning); } + + var result = SpinWait.SpinUntil(() => receivedMessages >= 100000, TimeSpan.FromMinutes(5)); + + Assert.IsTrue(result); } } \ No newline at end of file diff --git a/Source/MQTTnet.Tests/Server/MqttRetainedMessageManager_Tests.cs b/Source/MQTTnet.Tests/Server/MqttRetainedMessageManager_Tests.cs index 227a664e..ca6f3d6d 100644 --- a/Source/MQTTnet.Tests/Server/MqttRetainedMessageManager_Tests.cs +++ b/Source/MQTTnet.Tests/Server/MqttRetainedMessageManager_Tests.cs @@ -1,26 +1,22 @@ using Microsoft.VisualStudio.TestTools.UnitTesting; -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; using System.Threading.Tasks; using MQTTnet.Server.Internal; -namespace MQTTnet.Tests.Server +namespace MQTTnet.Tests.Server; + +// ReSharper disable InconsistentNaming +[TestClass] +public sealed class MqttRetainedMessageManager_Tests { - [TestClass] - public sealed class MqttRetainedMessageManager_Tests + [TestMethod] + public async Task MqttRetainedMessageManager_GetUndefinedTopic() { - [TestMethod] - public async Task MqttRetainedMessageManager_GetUndefinedTopic() - { - var logger = new Mockups.TestLogger(); - var eventContainer = new MqttServerEventContainer(); - var retainedMessagesManager = new MqttRetainedMessagesManager(eventContainer, logger); - var task = retainedMessagesManager.GetMessage("undefined"); - Assert.IsNotNull(task, "Task should not be null"); - var result = await task; - Assert.IsNull(result, "Null result expected"); - } + var logger = new Mockups.TestLogger(); + var eventContainer = new MqttServerEventContainer(); + var retainedMessagesManager = new MqttRetainedMessagesManager(eventContainer, logger); + var task = retainedMessagesManager.GetMessage("undefined"); + Assert.IsNotNull(task, "Task should not be null"); + var result = await task; + Assert.IsNull(result, "Null result expected"); } -} +} \ No newline at end of file diff --git a/Source/MQTTnet.Tests/Server/MqttSubscriptionsManager_Tests.cs b/Source/MQTTnet.Tests/Server/MqttSubscriptionsManager_Tests.cs index 13a8cd9d..e188ea39 100644 --- a/Source/MQTTnet.Tests/Server/MqttSubscriptionsManager_Tests.cs +++ b/Source/MQTTnet.Tests/Server/MqttSubscriptionsManager_Tests.cs @@ -3,7 +3,6 @@ // See the LICENSE file in the project root for more information. using System.Collections.Concurrent; -using System.Collections.Generic; using System.Threading; using System.Threading.Tasks; using Microsoft.VisualStudio.TestTools.UnitTesting; @@ -13,227 +12,218 @@ using MQTTnet.Server; using MQTTnet.Server.Internal; using MQTTnet.Tests.Mockups; -namespace MQTTnet.Tests.Server +namespace MQTTnet.Tests.Server; + +// ReSharper disable InconsistentNaming +[TestClass] +public sealed class MqttSubscriptionsManager_Tests : BaseTestClass { - [TestClass] - public sealed class MqttSubscriptionsManager_Tests : BaseTestClass + MqttClientSubscriptionsManager _subscriptionsManager; + + [TestMethod] + public async Task MqttSubscriptionsManager_SubscribeAndUnsubscribeSingle() { - MqttClientSubscriptionsManager _subscriptionsManager; - - [TestMethod] - public async Task MqttSubscriptionsManager_SubscribeAndUnsubscribeSingle() + var sp = new MqttSubscribePacket { - var sp = new MqttSubscribePacket - { - TopicFilters = new List - { - new MqttTopicFilterBuilder().WithTopic("A/B/C").Build() - } - }; + TopicFilters = [new MqttTopicFilterBuilder().WithTopic("A/B/C").Build()] + }; - await _subscriptionsManager.Subscribe(sp, CancellationToken.None); + await _subscriptionsManager.Subscribe(sp, CancellationToken.None); - Assert.IsTrue(CheckSubscriptions("A/B/C", MqttQualityOfServiceLevel.AtMostOnce, "").IsSubscribed); + Assert.IsTrue(CheckSubscriptions("A/B/C", MqttQualityOfServiceLevel.AtMostOnce, "").IsSubscribed); - var up = new MqttUnsubscribePacket(); - up.TopicFilters.Add("A/B/C"); - await _subscriptionsManager.Unsubscribe(up, CancellationToken.None); + var up = new MqttUnsubscribePacket(); + up.TopicFilters.Add("A/B/C"); + await _subscriptionsManager.Unsubscribe(up, CancellationToken.None); - Assert.IsFalse(CheckSubscriptions("A/B/C", MqttQualityOfServiceLevel.AtMostOnce, "").IsSubscribed); - } + Assert.IsFalse(CheckSubscriptions("A/B/C", MqttQualityOfServiceLevel.AtMostOnce, "").IsSubscribed); + } - [TestMethod] - public async Task MqttSubscriptionsManager_SubscribeDifferentQoSSuccess() + [TestMethod] + public async Task MqttSubscriptionsManager_SubscribeDifferentQoSSuccess() + { + var sp = new MqttSubscribePacket { - var sp = new MqttSubscribePacket - { - TopicFilters = new List - { - new MqttTopicFilter { Topic = "A/B/C", QualityOfServiceLevel = MqttQualityOfServiceLevel.AtMostOnce } - } - }; + TopicFilters = + [ + new() { Topic = "A/B/C", QualityOfServiceLevel = MqttQualityOfServiceLevel.AtMostOnce } + ] + }; - await _subscriptionsManager.Subscribe(sp, CancellationToken.None); + await _subscriptionsManager.Subscribe(sp, CancellationToken.None); - var result = CheckSubscriptions("A/B/C", MqttQualityOfServiceLevel.ExactlyOnce, ""); - Assert.IsTrue(result.IsSubscribed); - Assert.AreEqual(result.QualityOfServiceLevel, MqttQualityOfServiceLevel.AtMostOnce); - } + var result = CheckSubscriptions("A/B/C", MqttQualityOfServiceLevel.ExactlyOnce, ""); + Assert.IsTrue(result.IsSubscribed); + Assert.AreEqual(result.QualityOfServiceLevel, MqttQualityOfServiceLevel.AtMostOnce); + } - [TestMethod] - public async Task MqttSubscriptionsManager_SubscribeSingleNoSuccess() + [TestMethod] + public async Task MqttSubscriptionsManager_SubscribeSingleNoSuccess() + { + var sp = new MqttSubscribePacket { - var sp = new MqttSubscribePacket - { - TopicFilters = new List - { - new MqttTopicFilterBuilder().WithTopic("A/B/C").Build() - } - }; + TopicFilters = [new MqttTopicFilterBuilder().WithTopic("A/B/C").Build()] + }; - await _subscriptionsManager.Subscribe(sp, CancellationToken.None); + await _subscriptionsManager.Subscribe(sp, CancellationToken.None); - Assert.IsFalse(CheckSubscriptions("A/B/X", MqttQualityOfServiceLevel.AtMostOnce, "").IsSubscribed); - } + Assert.IsFalse(CheckSubscriptions("A/B/X", MqttQualityOfServiceLevel.AtMostOnce, "").IsSubscribed); + } - [TestMethod] - public async Task MqttSubscriptionsManager_SubscribeSingleSuccess() + [TestMethod] + public async Task MqttSubscriptionsManager_SubscribeSingleSuccess() + { + var sp = new MqttSubscribePacket { - var sp = new MqttSubscribePacket - { - TopicFilters = new List - { - new MqttTopicFilterBuilder().WithTopic("A/B/C").Build() - } - }; + TopicFilters = [new MqttTopicFilterBuilder().WithTopic("A/B/C").Build()] + }; - await _subscriptionsManager.Subscribe(sp, CancellationToken.None); + await _subscriptionsManager.Subscribe(sp, CancellationToken.None); - var result = CheckSubscriptions("A/B/C", MqttQualityOfServiceLevel.AtMostOnce, ""); + var result = CheckSubscriptions("A/B/C", MqttQualityOfServiceLevel.AtMostOnce, ""); - Assert.IsTrue(result.IsSubscribed); - Assert.AreEqual(result.QualityOfServiceLevel, MqttQualityOfServiceLevel.AtMostOnce); - } + Assert.IsTrue(result.IsSubscribed); + Assert.AreEqual(result.QualityOfServiceLevel, MqttQualityOfServiceLevel.AtMostOnce); + } - [TestMethod] - public async Task MqttSubscriptionsManager_SubscribeTwoTimesSuccess() + [TestMethod] + public async Task MqttSubscriptionsManager_SubscribeTwoTimesSuccess() + { + var sp = new MqttSubscribePacket { - var sp = new MqttSubscribePacket - { - TopicFilters = new List - { - new MqttTopicFilter { Topic = "#", QualityOfServiceLevel = MqttQualityOfServiceLevel.AtMostOnce }, - new MqttTopicFilter { Topic = "A/B/C", QualityOfServiceLevel = MqttQualityOfServiceLevel.AtLeastOnce } - } - }; + TopicFilters = + [ + new MqttTopicFilter { Topic = "#", QualityOfServiceLevel = MqttQualityOfServiceLevel.AtMostOnce }, + new MqttTopicFilter { Topic = "A/B/C", QualityOfServiceLevel = MqttQualityOfServiceLevel.AtLeastOnce } + ] + }; - await _subscriptionsManager.Subscribe(sp, CancellationToken.None); + await _subscriptionsManager.Subscribe(sp, CancellationToken.None); - var result = CheckSubscriptions("A/B/C", MqttQualityOfServiceLevel.ExactlyOnce, ""); + var result = CheckSubscriptions("A/B/C", MqttQualityOfServiceLevel.ExactlyOnce, ""); - Assert.IsTrue(result.IsSubscribed); - Assert.AreEqual(result.QualityOfServiceLevel, MqttQualityOfServiceLevel.AtLeastOnce); - } + Assert.IsTrue(result.IsSubscribed); + Assert.AreEqual(result.QualityOfServiceLevel, MqttQualityOfServiceLevel.AtLeastOnce); + } - [TestMethod] - public async Task MqttSubscriptionsManager_SubscribeWildcard1() + [TestMethod] + public async Task MqttSubscriptionsManager_SubscribeWildcard1() + { + await SubscribeToTopic("house/+/room"); + await SubscribeToTopic("house/+/room/+"); + + CheckIsSubscribed("house/1/room"); + CheckIsSubscribed("house/1/room/bed"); + CheckIsSubscribed("house/1/room/chair"); + CheckIsSubscribed("house/2/room/bed"); + CheckIsSubscribed("house/2/room/chair"); + + CheckIsNotSubscribed("house/1/room/bed/cover"); + CheckIsNotSubscribed("house/1/study/bed"); + } + + [TestMethod] + public async Task MqttSubscriptionsManager_SubscribeWildcard2() + { + await SubscribeToTopic("house/+/room"); + await SubscribeToTopic("house/+/room/#"); + + CheckIsSubscribed("house/1/room"); + CheckIsSubscribed("house/1/room/bed"); + CheckIsSubscribed("house/2/room"); + CheckIsSubscribed("house/2/room/bed"); + CheckIsSubscribed("house/2/room/bed/cover"); + + CheckIsNotSubscribed("house/1/level"); + CheckIsNotSubscribed("house/1/level/door"); + } + + [TestMethod] + public async Task MqttSubscriptionsManager_SubscribeWildcard3() + { + await SubscribeToTopic("house/1/room"); + await SubscribeToTopic("house/1/room/+"); + + CheckIsSubscribed("house/1/room"); + CheckIsSubscribed("house/1/room/bed"); + CheckIsSubscribed("house/1/room/chair"); + + CheckIsNotSubscribed("house/2/room"); + CheckIsNotSubscribed("house/2/room/bed/cover"); + } + + [TestMethod] + public async Task MqttSubscriptionsManager_SubscribeWildcard4() + { + await SubscribeToTopic("house/1/+/+"); + + CheckIsSubscribed("house/1/room/bed"); + CheckIsSubscribed("house/1/room/chair"); + + CheckIsNotSubscribed("house/1/room"); + CheckIsNotSubscribed("house/1/room/bed/cover"); + } + + [TestMethod] + public async Task MqttSubscriptionsManager_SubscribeWildcard5() + { + await SubscribeToTopic("house/1/+/#"); + + CheckIsSubscribed("house/1/room/bed"); + CheckIsSubscribed("house/1/room/chair"); + CheckIsSubscribed("house/1/room/chair/leg"); + CheckIsSubscribed("house/1/level/window"); + CheckIsSubscribed("house/1/level/door"); + + CheckIsNotSubscribed("house/2/room/bed"); + } + + async Task SubscribeToTopic(string topic) + { + var sp = new MqttSubscribePacket { - await SubscribeToTopic("house/+/room"); - await SubscribeToTopic("house/+/room/+"); + TopicFilters = + [ + new MqttTopicFilter { Topic = topic, QualityOfServiceLevel = MqttQualityOfServiceLevel.AtMostOnce } + ] + }; - CheckIsSubscribed("house/1/room"); - CheckIsSubscribed("house/1/room/bed"); - CheckIsSubscribed("house/1/room/chair"); - CheckIsSubscribed("house/2/room/bed"); - CheckIsSubscribed("house/2/room/chair"); + await _subscriptionsManager.Subscribe(sp, CancellationToken.None).ConfigureAwait(false); + } - CheckIsNotSubscribed("house/1/room/bed/cover"); - CheckIsNotSubscribed("house/1/study/bed"); - } + void CheckIsSubscribed(string topic) + { + var result = CheckSubscriptions(topic, MqttQualityOfServiceLevel.AtMostOnce, ""); + Assert.IsTrue(result.IsSubscribed); + Assert.AreEqual(result.QualityOfServiceLevel, MqttQualityOfServiceLevel.AtMostOnce); + } - [TestMethod] - public async Task MqttSubscriptionsManager_SubscribeWildcard2() + void CheckIsNotSubscribed(string topic) + { + var result = CheckSubscriptions(topic, MqttQualityOfServiceLevel.AtMostOnce, ""); + Assert.IsFalse(result.IsSubscribed); + } + + [TestInitialize] + public void TestInitialize() + { + var logger = new TestLogger(); + var options = new MqttServerOptions(); + var retainedMessagesManager = new MqttRetainedMessagesManager(new MqttServerEventContainer(), logger); + var eventContainer = new MqttServerEventContainer(); + var clientSessionManager = new MqttClientSessionsManager(options, retainedMessagesManager, eventContainer, logger); + + var session = new MqttSession(new MqttConnectPacket { - await SubscribeToTopic("house/+/room"); - await SubscribeToTopic("house/+/room/#"); + ClientId = "" + }, new ConcurrentDictionary(), options, eventContainer, retainedMessagesManager, clientSessionManager); - CheckIsSubscribed("house/1/room"); - CheckIsSubscribed("house/1/room/bed"); - CheckIsSubscribed("house/2/room"); - CheckIsSubscribed("house/2/room/bed"); - CheckIsSubscribed("house/2/room/bed/cover"); + _subscriptionsManager = new MqttClientSubscriptionsManager(session, new MqttServerEventContainer(), retainedMessagesManager, clientSessionManager); + } - CheckIsNotSubscribed("house/1/level"); - CheckIsNotSubscribed("house/1/level/door"); - } - - [TestMethod] - public async Task MqttSubscriptionsManager_SubscribeWildcard3() - { - await SubscribeToTopic("house/1/room"); - await SubscribeToTopic("house/1/room/+"); - - CheckIsSubscribed("house/1/room"); - CheckIsSubscribed("house/1/room/bed"); - CheckIsSubscribed("house/1/room/chair"); - - CheckIsNotSubscribed("house/2/room"); - CheckIsNotSubscribed("house/2/room/bed/cover"); - } - - [TestMethod] - public async Task MqttSubscriptionsManager_SubscribeWildcard4() - { - await SubscribeToTopic("house/1/+/+"); - - CheckIsSubscribed("house/1/room/bed"); - CheckIsSubscribed("house/1/room/chair"); - - CheckIsNotSubscribed("house/1/room"); - CheckIsNotSubscribed("house/1/room/bed/cover"); - } - - [TestMethod] - public async Task MqttSubscriptionsManager_SubscribeWildcard5() - { - await SubscribeToTopic("house/1/+/#"); - - CheckIsSubscribed("house/1/room/bed"); - CheckIsSubscribed("house/1/room/chair"); - CheckIsSubscribed("house/1/room/chair/leg"); - CheckIsSubscribed("house/1/level/window"); - CheckIsSubscribed("house/1/level/door"); - - CheckIsNotSubscribed("house/2/room/bed"); - } - - async Task SubscribeToTopic(string topic) - { - var sp = new MqttSubscribePacket - { - TopicFilters = new List - { - new MqttTopicFilter { Topic = topic, QualityOfServiceLevel = MqttQualityOfServiceLevel.AtMostOnce }, - } - }; - - await _subscriptionsManager.Subscribe(sp, CancellationToken.None); - } - - void CheckIsSubscribed(string topic) - { - var result = CheckSubscriptions(topic, MqttQualityOfServiceLevel.AtMostOnce, ""); - Assert.IsTrue(result.IsSubscribed); - Assert.AreEqual(result.QualityOfServiceLevel, MqttQualityOfServiceLevel.AtMostOnce); - } - - void CheckIsNotSubscribed(string topic) - { - var result = CheckSubscriptions(topic, MqttQualityOfServiceLevel.AtMostOnce, ""); - Assert.IsFalse(result.IsSubscribed); - } - - [TestInitialize] - public void TestInitialize() - { - var logger = new TestLogger(); - var options = new MqttServerOptions(); - var retainedMessagesManager = new MqttRetainedMessagesManager(new MqttServerEventContainer(), logger); - var eventContainer = new MqttServerEventContainer(); - var clientSessionManager = new MqttClientSessionsManager(options, retainedMessagesManager, eventContainer, logger); - - var session = new MqttSession(new MqttConnectPacket - { - ClientId = "" - }, new ConcurrentDictionary(), options, eventContainer, retainedMessagesManager, clientSessionManager); - - _subscriptionsManager = new MqttClientSubscriptionsManager(session, new MqttServerEventContainer(), retainedMessagesManager, clientSessionManager); - } - - CheckSubscriptionsResult CheckSubscriptions(string topic, MqttQualityOfServiceLevel applicationMessageQoSLevel, string senderClientId) - { - MqttTopicHash.Calculate(topic, out var topicHash, out _, out _); - return _subscriptionsManager.CheckSubscriptions(topic, topicHash, applicationMessageQoSLevel, senderClientId); - } + CheckSubscriptionsResult CheckSubscriptions(string topic, MqttQualityOfServiceLevel applicationMessageQoSLevel, string senderClientId) + { + MqttTopicHash.Calculate(topic, out var topicHash, out _, out _); + return _subscriptionsManager.CheckSubscriptions(topic, topicHash, applicationMessageQoSLevel, senderClientId); } } \ No newline at end of file diff --git a/Source/MQTTnet.Tests/Server/No_Local_Tests.cs b/Source/MQTTnet.Tests/Server/No_Local_Tests.cs index 5a8627d2..7eaca607 100644 --- a/Source/MQTTnet.Tests/Server/No_Local_Tests.cs +++ b/Source/MQTTnet.Tests/Server/No_Local_Tests.cs @@ -6,45 +6,43 @@ using System.Threading.Tasks; using Microsoft.VisualStudio.TestTools.UnitTesting; using MQTTnet.Formatter; -namespace MQTTnet.Tests.Server +namespace MQTTnet.Tests.Server; + +// ReSharper disable InconsistentNaming +[TestClass] +public sealed class No_Local_Tests : BaseTestClass { - [TestClass] - public sealed class No_Local_Tests : BaseTestClass + [TestMethod] + public Task Subscribe_With_No_Local() { - [TestMethod] - public Task Subscribe_With_No_Local() - { - return ExecuteTest(true, 0); - } + return ExecuteTest(true, 0); + } - [TestMethod] - public Task Subscribe_Without_No_Local() - { - return ExecuteTest(false, 1); - } + [TestMethod] + public Task Subscribe_Without_No_Local() + { + return ExecuteTest(false, 1); + } - async Task ExecuteTest( - bool noLocal, - int expectedCountAfterPublish) - { - using (var testEnvironment = CreateTestEnvironment(MqttProtocolVersion.V500)) - { - await testEnvironment.StartServer(); + async Task ExecuteTest( + bool noLocal, + int expectedCountAfterPublish) + { + using var testEnvironment = CreateTestEnvironment(MqttProtocolVersion.V500); + await testEnvironment.StartServer(); - var client1 = await testEnvironment.ConnectClient(); - var applicationMessageHandler = testEnvironment.CreateApplicationMessageHandler(client1); - var topicFilter = testEnvironment.ClientFactory.CreateTopicFilterBuilder().WithTopic("Topic").WithNoLocal(noLocal).Build(); - await client1.SubscribeAsync(topicFilter); - await LongTestDelay(); + var client1 = await testEnvironment.ConnectClient(); + var applicationMessageHandler = testEnvironment.CreateApplicationMessageHandler(client1); + var topicFilter = testEnvironment.ClientFactory.CreateTopicFilterBuilder().WithTopic("Topic").WithNoLocal(noLocal).Build(); + await client1.SubscribeAsync(topicFilter); + await LongTestDelay(); - applicationMessageHandler.AssertReceivedCountEquals(0); + applicationMessageHandler.AssertReceivedCountEquals(0); - // The client will publish a message where it is itself subscribing to. - await client1.PublishStringAsync("Topic", "Payload", retain: true); - await LongTestDelay(); + // The client will publish a message where it is itself subscribing to. + await client1.PublishStringAsync("Topic", "Payload", retain: true); + await LongTestDelay(); - applicationMessageHandler.AssertReceivedCountEquals(expectedCountAfterPublish); - } - } + applicationMessageHandler.AssertReceivedCountEquals(expectedCountAfterPublish); } } \ No newline at end of file diff --git a/Source/MQTTnet.Tests/Server/Publishing_Tests.cs b/Source/MQTTnet.Tests/Server/Publishing_Tests.cs index e829e257..b10789eb 100644 --- a/Source/MQTTnet.Tests/Server/Publishing_Tests.cs +++ b/Source/MQTTnet.Tests/Server/Publishing_Tests.cs @@ -10,138 +10,128 @@ using MQTTnet.Internal; using MQTTnet.Protocol; using MQTTnet.Server; -namespace MQTTnet.Tests.Server +namespace MQTTnet.Tests.Server; + +// ReSharper disable InconsistentNaming +[TestClass] +public sealed class Publishing_Tests : BaseTestClass { - [TestClass] - public sealed class Publishing_Tests : BaseTestClass + [TestMethod] + [ExpectedException(typeof(MqttClientDisconnectedException))] + public async Task Disconnect_While_Publishing() { - [TestMethod] - [ExpectedException(typeof(MqttClientDisconnectedException))] - public async Task Disconnect_While_Publishing() + using var testEnvironment = CreateTestEnvironment(); + var server = await testEnvironment.StartServer(); + + // The client will be disconnected directly after subscribing! + server.InterceptingPublishAsync += ev => server.DisconnectClientAsync(ev.ClientId); + + var client = await testEnvironment.ConnectClient(); + await client.PublishStringAsync("test", qualityOfServiceLevel: MqttQualityOfServiceLevel.AtLeastOnce); + } + + [TestMethod] + public async Task Return_NoMatchingSubscribers_When_Not_Subscribed() + { + using var testEnvironment = CreateTestEnvironment(MqttProtocolVersion.V500); + await testEnvironment.StartServer(); + + var sender = await testEnvironment.ConnectClient(); + var receiver = await testEnvironment.ConnectClient(); + + await receiver.SubscribeAsync("A"); + + // AtLeastOnce is required to get an ACK packet from the server. + var publishResult = await sender.PublishStringAsync("B", "Payload", MqttQualityOfServiceLevel.AtLeastOnce); + + Assert.AreEqual(MqttClientPublishReasonCode.NoMatchingSubscribers, publishResult.ReasonCode); + + Assert.AreEqual(true, publishResult.IsSuccess); + } + + [TestMethod] + public async Task Return_Success_When_Subscribed() + { + using var testEnvironment = CreateTestEnvironment(MqttProtocolVersion.V500); + await testEnvironment.StartServer(); + + var sender = await testEnvironment.ConnectClient(); + var receiver = await testEnvironment.ConnectClient(); + + await receiver.SubscribeAsync("A"); + + // AtLeastOnce is required to get an ACK packet from the server. + var publishResult = await sender.PublishStringAsync("A", "Payload", MqttQualityOfServiceLevel.AtLeastOnce); + + Assert.AreEqual(MqttClientPublishReasonCode.Success, publishResult.ReasonCode); + + Assert.AreEqual(true, publishResult.IsSuccess); + } + + [TestMethod] + public async Task Intercept_Client_Enqueue() + { + using var testEnvironment = CreateTestEnvironment(); + var server = await testEnvironment.StartServer(); + + var sender = await testEnvironment.ConnectClient(); + + var receiver = await testEnvironment.ConnectClient(); + await receiver.SubscribeAsync("A"); + var receivedMessages = testEnvironment.CreateApplicationMessageHandler(receiver); + + await sender.PublishStringAsync("A", "Payload", MqttQualityOfServiceLevel.AtLeastOnce); + + await LongTestDelay(); + + receivedMessages.AssertReceivedCountEquals(1); + + server.InterceptingClientEnqueueAsync += e => { - using (var testEnvironment = CreateTestEnvironment()) - { - var server = await testEnvironment.StartServer(); + e.AcceptEnqueue = false; + return CompletedTask.Instance; + }; - // The client will be disconnect directly after subscribing! - server.InterceptingPublishAsync += ev => server.DisconnectClientAsync(ev.ClientId, MqttDisconnectReasonCode.NormalDisconnection); + await sender.PublishStringAsync("A", "Payload", MqttQualityOfServiceLevel.AtLeastOnce); - var client = await testEnvironment.ConnectClient(); - await client.PublishStringAsync("test", qualityOfServiceLevel: MqttQualityOfServiceLevel.AtLeastOnce); - } - } + await LongTestDelay(); - [TestMethod] - public async Task Return_NoMatchingSubscribers_When_Not_Subscribed() + // Do not increase because the internal enqueue to the target client is not accepted! + receivedMessages.AssertReceivedCountEquals(1); + } + + [TestMethod] + public async Task Intercept_Client_Enqueue_Multiple_Clients_Subscribed_Messages_Are_Filtered() + { + using var testEnvironment = CreateTestEnvironment(); + var server = await testEnvironment.StartServer(); + + var sender = await testEnvironment.ConnectClient(); + + var receiverOne = await testEnvironment.ConnectClient(o => o.WithClientId("One")); + await receiverOne.SubscribeAsync("A"); + var receivedMessagesOne = testEnvironment.CreateApplicationMessageHandler(receiverOne); + + var receiverTwo = await testEnvironment.ConnectClient(o => o.WithClientId("Two")); + await receiverTwo.SubscribeAsync("A"); + var receivedMessagesTwo = testEnvironment.CreateApplicationMessageHandler(receiverTwo); + + var receiverThree = await testEnvironment.ConnectClient(o => o.WithClientId("Three")); + await receiverThree.SubscribeAsync("A"); + var receivedMessagesThree = testEnvironment.CreateApplicationMessageHandler(receiverThree); + + server.InterceptingClientEnqueueAsync += e => { - using (var testEnvironment = CreateTestEnvironment(MqttProtocolVersion.V500)) - { - await testEnvironment.StartServer(); + if (e.ReceiverClientId.Contains("Two")) e.AcceptEnqueue = false; + return CompletedTask.Instance; + }; - var sender = await testEnvironment.ConnectClient(); - var receiver = await testEnvironment.ConnectClient(); + await sender.PublishStringAsync("A", "Payload", MqttQualityOfServiceLevel.AtLeastOnce); - await receiver.SubscribeAsync("A"); + await LongTestDelay(); - // AtLeastOnce is required to get an ACK packet from the server. - var publishResult = await sender.PublishStringAsync("B", "Payload", MqttQualityOfServiceLevel.AtLeastOnce); - - Assert.AreEqual(MqttClientPublishReasonCode.NoMatchingSubscribers, publishResult.ReasonCode); - - Assert.AreEqual(true, publishResult.IsSuccess); - } - } - - [TestMethod] - public async Task Return_Success_When_Subscribed() - { - using (var testEnvironment = CreateTestEnvironment(MqttProtocolVersion.V500)) - { - await testEnvironment.StartServer(); - - var sender = await testEnvironment.ConnectClient(); - var receiver = await testEnvironment.ConnectClient(); - - await receiver.SubscribeAsync("A"); - - // AtLeastOnce is required to get an ACK packet from the server. - var publishResult = await sender.PublishStringAsync("A", "Payload", MqttQualityOfServiceLevel.AtLeastOnce); - - Assert.AreEqual(MqttClientPublishReasonCode.Success, publishResult.ReasonCode); - - Assert.AreEqual(true, publishResult.IsSuccess); - } - } - - [TestMethod] - public async Task Intercept_Client_Enqueue() - { - using (var testEnvironment = CreateTestEnvironment()) - { - var server = await testEnvironment.StartServer(); - - var sender = await testEnvironment.ConnectClient(); - - var receiver = await testEnvironment.ConnectClient(); - await receiver.SubscribeAsync("A"); - var receivedMessages = testEnvironment.CreateApplicationMessageHandler(receiver); - - await sender.PublishStringAsync("A", "Payload", MqttQualityOfServiceLevel.AtLeastOnce); - - await LongTestDelay(); - - receivedMessages.AssertReceivedCountEquals(1); - - server.InterceptingClientEnqueueAsync += e => - { - e.AcceptEnqueue = false; - return CompletedTask.Instance; - }; - - await sender.PublishStringAsync("A", "Payload", MqttQualityOfServiceLevel.AtLeastOnce); - - await LongTestDelay(); - - // Do not increase because the internal enqueue to the target client is not accepted! - receivedMessages.AssertReceivedCountEquals(1); - } - } - - [TestMethod] - public async Task Intercept_Client_Enqueue_Multiple_Clients_Subscribed_Messages_Are_Filtered() - { - using (var testEnvironment = CreateTestEnvironment()) - { - var server = await testEnvironment.StartServer(); - - var sender = await testEnvironment.ConnectClient(); - - var receiverOne = await testEnvironment.ConnectClient(o => o.WithClientId("One")); - await receiverOne.SubscribeAsync("A"); - var receivedMessagesOne = testEnvironment.CreateApplicationMessageHandler(receiverOne); - - var receiverTwo = await testEnvironment.ConnectClient(o => o.WithClientId("Two")); - await receiverTwo.SubscribeAsync("A"); - var receivedMessagesTwo = testEnvironment.CreateApplicationMessageHandler(receiverTwo); - - var receiverThree = await testEnvironment.ConnectClient(o => o.WithClientId("Three")); - await receiverThree.SubscribeAsync("A"); - var receivedMessagesThree = testEnvironment.CreateApplicationMessageHandler(receiverThree); - - server.InterceptingClientEnqueueAsync += e => - { - if (e.ReceiverClientId.Contains("Two")) e.AcceptEnqueue = false; - return CompletedTask.Instance; - }; - - await sender.PublishStringAsync("A", "Payload", MqttQualityOfServiceLevel.AtLeastOnce); - - await LongTestDelay(); - - receivedMessagesOne.AssertReceivedCountEquals(1); - receivedMessagesTwo.AssertReceivedCountEquals(0); - receivedMessagesThree.AssertReceivedCountEquals(1); - } - } + receivedMessagesOne.AssertReceivedCountEquals(1); + receivedMessagesTwo.AssertReceivedCountEquals(0); + receivedMessagesThree.AssertReceivedCountEquals(1); } } \ No newline at end of file diff --git a/Source/MQTTnet.Tests/Server/QoS_Tests.cs b/Source/MQTTnet.Tests/Server/QoS_Tests.cs index b48a1a99..59c4a28c 100644 --- a/Source/MQTTnet.Tests/Server/QoS_Tests.cs +++ b/Source/MQTTnet.Tests/Server/QoS_Tests.cs @@ -11,157 +11,150 @@ using MQTTnet.Server; namespace MQTTnet.Tests.Server; +// ReSharper disable InconsistentNaming [TestClass] public sealed class QoS_Tests : BaseTestClass { [TestMethod] public async Task Fire_Event_On_Client_Acknowledges_QoS_0() { - using (var testEnvironment = CreateTestEnvironment()) + using var testEnvironment = CreateTestEnvironment(); + var server = await testEnvironment.StartServer(); + + ClientAcknowledgedPublishPacketEventArgs eventArgs = null; + server.ClientAcknowledgedPublishPacketAsync += args => { - var server = await testEnvironment.StartServer(); + eventArgs = args; + return CompletedTask.Instance; + }; - ClientAcknowledgedPublishPacketEventArgs eventArgs = null; - server.ClientAcknowledgedPublishPacketAsync += args => - { - eventArgs = args; - return CompletedTask.Instance; - }; + var client1 = await testEnvironment.ConnectClient(); + await client1.SubscribeAsync("A"); - var client1 = await testEnvironment.ConnectClient(); - await client1.SubscribeAsync("A"); + var client2 = await testEnvironment.ConnectClient(); + await client2.PublishStringAsync("A"); - var client2 = await testEnvironment.ConnectClient(); - await client2.PublishStringAsync("A"); + await LongTestDelay(); - await LongTestDelay(); - - // Must be null because no event should be fired for QoS 0. - Assert.IsNull(eventArgs); - } + // Must be null because no event should be fired for QoS 0. + Assert.IsNull(eventArgs); } [TestMethod] public async Task Fire_Event_On_Client_Acknowledges_QoS_1() { - using (var testEnvironment = CreateTestEnvironment()) + using var testEnvironment = CreateTestEnvironment(); + var server = await testEnvironment.StartServer(); + + ClientAcknowledgedPublishPacketEventArgs eventArgs = null; + server.ClientAcknowledgedPublishPacketAsync += args => { - var server = await testEnvironment.StartServer(); + eventArgs = args; + return CompletedTask.Instance; + }; - ClientAcknowledgedPublishPacketEventArgs eventArgs = null; - server.ClientAcknowledgedPublishPacketAsync += args => - { - eventArgs = args; - return CompletedTask.Instance; - }; + var client1 = await testEnvironment.ConnectClient(); + await client1.SubscribeAsync("A", MqttQualityOfServiceLevel.AtLeastOnce); - var client1 = await testEnvironment.ConnectClient(); - await client1.SubscribeAsync("A", MqttQualityOfServiceLevel.AtLeastOnce); + var client2 = await testEnvironment.ConnectClient(); + await client2.PublishStringAsync("A", qualityOfServiceLevel: MqttQualityOfServiceLevel.AtLeastOnce); - var client2 = await testEnvironment.ConnectClient(); - await client2.PublishStringAsync("A", qualityOfServiceLevel: MqttQualityOfServiceLevel.AtLeastOnce); + await LongTestDelay(); - await LongTestDelay(); + Assert.IsNotNull(eventArgs); + Assert.IsNotNull(eventArgs.PublishPacket); + Assert.IsNotNull(eventArgs.AcknowledgePacket); + Assert.IsTrue(eventArgs.IsCompleted); - Assert.IsNotNull(eventArgs); - Assert.IsNotNull(eventArgs.PublishPacket); - Assert.IsNotNull(eventArgs.AcknowledgePacket); - Assert.IsTrue(eventArgs.IsCompleted); - - Assert.AreEqual("A", eventArgs.PublishPacket.Topic); - } + Assert.AreEqual("A", eventArgs.PublishPacket.Topic); } [TestMethod] public async Task Fire_Event_On_Client_Acknowledges_QoS_2() { - using (var testEnvironment = CreateTestEnvironment()) + using var testEnvironment = CreateTestEnvironment(); + var server = await testEnvironment.StartServer(); + + var eventArgs = new List(); + server.ClientAcknowledgedPublishPacketAsync += args => { - var server = await testEnvironment.StartServer(); - - var eventArgs = new List(); - server.ClientAcknowledgedPublishPacketAsync += args => + lock (eventArgs) { - lock (eventArgs) - { - eventArgs.Add(args); - } + eventArgs.Add(args); + } - return CompletedTask.Instance; - }; + return CompletedTask.Instance; + }; - var client1 = await testEnvironment.ConnectClient(); - await client1.SubscribeAsync("A", MqttQualityOfServiceLevel.ExactlyOnce); + var client1 = await testEnvironment.ConnectClient(); + await client1.SubscribeAsync("A", MqttQualityOfServiceLevel.ExactlyOnce); - var client2 = await testEnvironment.ConnectClient(); - await client2.PublishStringAsync("A", qualityOfServiceLevel: MqttQualityOfServiceLevel.ExactlyOnce); + var client2 = await testEnvironment.ConnectClient(); + await client2.PublishStringAsync("A", qualityOfServiceLevel: MqttQualityOfServiceLevel.ExactlyOnce); - await LongTestDelay(); + await LongTestDelay(); - Assert.AreEqual(1, eventArgs.Count); + Assert.AreEqual(1, eventArgs.Count); - var firstEvent = eventArgs[0]; + var firstEvent = eventArgs[0]; - Assert.IsNotNull(firstEvent); - Assert.IsNotNull(firstEvent.PublishPacket); - Assert.IsNotNull(firstEvent.AcknowledgePacket); - Assert.IsTrue(firstEvent.IsCompleted); + Assert.IsNotNull(firstEvent); + Assert.IsNotNull(firstEvent.PublishPacket); + Assert.IsNotNull(firstEvent.AcknowledgePacket); + Assert.IsTrue(firstEvent.IsCompleted); - Assert.AreEqual("A", firstEvent.PublishPacket.Topic); - } + Assert.AreEqual("A", firstEvent.PublishPacket.Topic); } [TestMethod] public async Task Preserve_Message_Order_For_Queued_Messages() { - using (var testEnvironment = CreateTestEnvironment()) - { - var server = await testEnvironment.StartServer(o => o.WithPersistentSessions()); + using var testEnvironment = CreateTestEnvironment(); + var server = await testEnvironment.StartServer(o => o.WithPersistentSessions()); - // Create a session which will contain the messages. - var dummyClient = await testEnvironment.ConnectClient(o => o.WithClientId("A").WithCleanSession(false)); - await dummyClient.SubscribeAsync("#", MqttQualityOfServiceLevel.AtLeastOnce); - dummyClient.Dispose(); + // Create a session which will contain the messages. + var dummyClient = await testEnvironment.ConnectClient(o => o.WithClientId("A").WithCleanSession(false)); + await dummyClient.SubscribeAsync("#", MqttQualityOfServiceLevel.AtLeastOnce); + dummyClient.Dispose(); - await LongTestDelay(); - await LongTestDelay(); + await LongTestDelay(); + await LongTestDelay(); - // Now inject messages which are appended to the queue of the client. - await server.InjectApplicationMessage("T", "0", MqttQualityOfServiceLevel.AtLeastOnce); + // Now inject messages which are appended to the queue of the client. + await server.InjectApplicationMessage("T", "0", MqttQualityOfServiceLevel.AtLeastOnce); - await server.InjectApplicationMessage("T", "2", MqttQualityOfServiceLevel.AtLeastOnce); - await server.InjectApplicationMessage("T", "1", MqttQualityOfServiceLevel.AtLeastOnce); + await server.InjectApplicationMessage("T", "2", MqttQualityOfServiceLevel.AtLeastOnce); + await server.InjectApplicationMessage("T", "1", MqttQualityOfServiceLevel.AtLeastOnce); - await server.InjectApplicationMessage("T", "4", MqttQualityOfServiceLevel.AtLeastOnce); - await server.InjectApplicationMessage("T", "3", MqttQualityOfServiceLevel.AtLeastOnce); + await server.InjectApplicationMessage("T", "4", MqttQualityOfServiceLevel.AtLeastOnce); + await server.InjectApplicationMessage("T", "3", MqttQualityOfServiceLevel.AtLeastOnce); - await server.InjectApplicationMessage("T", "6", MqttQualityOfServiceLevel.AtLeastOnce); - await server.InjectApplicationMessage("T", "5", MqttQualityOfServiceLevel.AtLeastOnce); + await server.InjectApplicationMessage("T", "6", MqttQualityOfServiceLevel.AtLeastOnce); + await server.InjectApplicationMessage("T", "5", MqttQualityOfServiceLevel.AtLeastOnce); - await server.InjectApplicationMessage("T", "8", MqttQualityOfServiceLevel.AtLeastOnce); - await server.InjectApplicationMessage("T", "7", MqttQualityOfServiceLevel.AtLeastOnce); + await server.InjectApplicationMessage("T", "8", MqttQualityOfServiceLevel.AtLeastOnce); + await server.InjectApplicationMessage("T", "7", MqttQualityOfServiceLevel.AtLeastOnce); - await server.InjectApplicationMessage("T", "9", MqttQualityOfServiceLevel.AtLeastOnce); + await server.InjectApplicationMessage("T", "9", MqttQualityOfServiceLevel.AtLeastOnce); - await LongTestDelay(); + await LongTestDelay(); - // Create a new client for the existing message. - var client = await testEnvironment.ConnectClient(o => o.WithClientId("A").WithCleanSession(false)); - var messages = testEnvironment.CreateApplicationMessageHandler(client); + // Create a new client for the existing message. + var client = await testEnvironment.ConnectClient(o => o.WithClientId("A").WithCleanSession(false)); + var messages = testEnvironment.CreateApplicationMessageHandler(client); - await LongTestDelay(); + await LongTestDelay(); - var payloadSequence = messages.GeneratePayloadSequence(); - Assert.AreEqual("0214365879", payloadSequence); + var payloadSequence = messages.GeneratePayloadSequence(); + Assert.AreEqual("0214365879", payloadSequence); - // Disconnect and reconnect to make sure that the server will not send the messages twice. - await client.DisconnectAsync(); - await LongTestDelay(); - await client.ReconnectAsync(); - await LongTestDelay(); + // Disconnect and reconnect to make sure that the server will not send the messages twice. + await client.DisconnectAsync(); + await LongTestDelay(); + await client.ReconnectAsync(); + await LongTestDelay(); - payloadSequence = messages.GeneratePayloadSequence(); - Assert.AreEqual("0214365879", payloadSequence); - } + payloadSequence = messages.GeneratePayloadSequence(); + Assert.AreEqual("0214365879", payloadSequence); } } \ No newline at end of file diff --git a/Source/MQTTnet.Tests/Server/Retain_As_Published_Tests.cs b/Source/MQTTnet.Tests/Server/Retain_As_Published_Tests.cs index 43e70624..ba541608 100644 --- a/Source/MQTTnet.Tests/Server/Retain_As_Published_Tests.cs +++ b/Source/MQTTnet.Tests/Server/Retain_As_Published_Tests.cs @@ -6,42 +6,40 @@ using System.Threading.Tasks; using Microsoft.VisualStudio.TestTools.UnitTesting; using MQTTnet.Formatter; -namespace MQTTnet.Tests.Server +namespace MQTTnet.Tests.Server; + +// ReSharper disable InconsistentNaming +[TestClass] +public sealed class Retain_As_Published_Tests : BaseTestClass { - [TestClass] - public sealed class Retain_As_Published_Tests : BaseTestClass + [TestMethod] + public Task Subscribe_With_Retain_As_Published() { - [TestMethod] - public Task Subscribe_With_Retain_As_Published() - { - return ExecuteTest(true); - } + return ExecuteTest(true); + } - [TestMethod] - public Task Subscribe_Without_Retain_As_Published() - { - return ExecuteTest(false); - } + [TestMethod] + public Task Subscribe_Without_Retain_As_Published() + { + return ExecuteTest(false); + } - async Task ExecuteTest(bool retainAsPublished) - { - using (var testEnvironment = CreateTestEnvironment(MqttProtocolVersion.V500)) - { - await testEnvironment.StartServer(); + async Task ExecuteTest(bool retainAsPublished) + { + using var testEnvironment = CreateTestEnvironment(MqttProtocolVersion.V500); + await testEnvironment.StartServer(); - var client1 = await testEnvironment.ConnectClient(); - var applicationMessageHandler = testEnvironment.CreateApplicationMessageHandler(client1); - var topicFilter = testEnvironment.ClientFactory.CreateTopicFilterBuilder().WithTopic("Topic").WithRetainAsPublished(retainAsPublished).Build(); - await client1.SubscribeAsync(topicFilter); - await LongTestDelay(); + var client1 = await testEnvironment.ConnectClient(); + var applicationMessageHandler = testEnvironment.CreateApplicationMessageHandler(client1); + var topicFilter = testEnvironment.ClientFactory.CreateTopicFilterBuilder().WithTopic("Topic").WithRetainAsPublished(retainAsPublished).Build(); + await client1.SubscribeAsync(topicFilter); + await LongTestDelay(); - // The client will publish a message where it is itself subscribing to. - await client1.PublishStringAsync("Topic", "Payload", retain: true); - await LongTestDelay(); + // The client will publish a message where it is itself subscribing to. + await client1.PublishStringAsync("Topic", "Payload", retain: true); + await LongTestDelay(); - applicationMessageHandler.AssertReceivedCountEquals(1); - Assert.AreEqual(retainAsPublished, applicationMessageHandler.ReceivedEventArgs[0].ApplicationMessage.Retain); - } - } + applicationMessageHandler.AssertReceivedCountEquals(1); + Assert.AreEqual(retainAsPublished, applicationMessageHandler.ReceivedEventArgs[0].ApplicationMessage.Retain); } } \ No newline at end of file diff --git a/Source/MQTTnet.Tests/Server/Retain_Handling_Tests.cs b/Source/MQTTnet.Tests/Server/Retain_Handling_Tests.cs index aa4b7f22..50b625a1 100644 --- a/Source/MQTTnet.Tests/Server/Retain_Handling_Tests.cs +++ b/Source/MQTTnet.Tests/Server/Retain_Handling_Tests.cs @@ -7,63 +7,61 @@ using Microsoft.VisualStudio.TestTools.UnitTesting; using MQTTnet.Formatter; using MQTTnet.Protocol; -namespace MQTTnet.Tests.Server +namespace MQTTnet.Tests.Server; + +// ReSharper disable InconsistentNaming +[TestClass] +public sealed class Retain_Handling_Tests : BaseTestClass { - [TestClass] - public sealed class Retain_Handling_Tests : BaseTestClass + [TestMethod] + public Task Send_At_Subscribe() { - [TestMethod] - public Task Send_At_Subscribe() - { - return ExecuteTest(MqttRetainHandling.SendAtSubscribe, 1, 2, 3); - } + return ExecuteTest(MqttRetainHandling.SendAtSubscribe, 1, 2, 3); + } - [TestMethod] - public Task Do_Not_Send_On_Subscribe() - { - return ExecuteTest(MqttRetainHandling.DoNotSendOnSubscribe, 0, 1, 1); - } + [TestMethod] + public Task Do_Not_Send_On_Subscribe() + { + return ExecuteTest(MqttRetainHandling.DoNotSendOnSubscribe, 0, 1, 1); + } - [TestMethod] - public Task Send_At_Subscribe_If_New_Subscription_Only() - { - return ExecuteTest(MqttRetainHandling.SendAtSubscribeIfNewSubscriptionOnly, 1, 2, 2); - } + [TestMethod] + public Task Send_At_Subscribe_If_New_Subscription_Only() + { + return ExecuteTest(MqttRetainHandling.SendAtSubscribeIfNewSubscriptionOnly, 1, 2, 2); + } - async Task ExecuteTest( - MqttRetainHandling retainHandling, - int expectedCountAfterSubscribe, - int expectedCountAfterSecondPublish, - int expectedCountAfterSecondSubscribe) - { - using (var testEnvironment = CreateTestEnvironment(MqttProtocolVersion.V500)) - { - await testEnvironment.StartServer(); + async Task ExecuteTest( + MqttRetainHandling retainHandling, + int expectedCountAfterSubscribe, + int expectedCountAfterSecondPublish, + int expectedCountAfterSecondSubscribe) + { + using var testEnvironment = CreateTestEnvironment(MqttProtocolVersion.V500); + await testEnvironment.StartServer(); - var client1 = await testEnvironment.ConnectClient(); - await client1.PublishStringAsync("Topic", "Payload", retain: true); + var client1 = await testEnvironment.ConnectClient(); + await client1.PublishStringAsync("Topic", "Payload", retain: true); - await LongTestDelay(); + await LongTestDelay(); - var client2 = await testEnvironment.ConnectClient(); - var applicationMessageHandler = testEnvironment.CreateApplicationMessageHandler(client2); + var client2 = await testEnvironment.ConnectClient(); + var applicationMessageHandler = testEnvironment.CreateApplicationMessageHandler(client2); - var topicFilter = testEnvironment.ClientFactory.CreateTopicFilterBuilder().WithTopic("Topic").WithRetainHandling(retainHandling).Build(); - await client2.SubscribeAsync(topicFilter); - await LongTestDelay(); + var topicFilter = testEnvironment.ClientFactory.CreateTopicFilterBuilder().WithTopic("Topic").WithRetainHandling(retainHandling).Build(); + await client2.SubscribeAsync(topicFilter); + await LongTestDelay(); - applicationMessageHandler.AssertReceivedCountEquals(expectedCountAfterSubscribe); + applicationMessageHandler.AssertReceivedCountEquals(expectedCountAfterSubscribe); - await client1.PublishStringAsync("Topic", "Payload", retain: true); - await LongTestDelay(); + await client1.PublishStringAsync("Topic", "Payload", retain: true); + await LongTestDelay(); - applicationMessageHandler.AssertReceivedCountEquals(expectedCountAfterSecondPublish); + applicationMessageHandler.AssertReceivedCountEquals(expectedCountAfterSecondPublish); - await client2.SubscribeAsync(topicFilter); - await LongTestDelay(); + await client2.SubscribeAsync(topicFilter); + await LongTestDelay(); - applicationMessageHandler.AssertReceivedCountEquals(expectedCountAfterSecondSubscribe); - } - } + applicationMessageHandler.AssertReceivedCountEquals(expectedCountAfterSecondSubscribe); } } \ No newline at end of file diff --git a/Source/MQTTnet.Tests/Server/Retained_Messages_Tests.cs b/Source/MQTTnet.Tests/Server/Retained_Messages_Tests.cs index fbbe500d..562b1134 100644 --- a/Source/MQTTnet.Tests/Server/Retained_Messages_Tests.cs +++ b/Source/MQTTnet.Tests/Server/Retained_Messages_Tests.cs @@ -9,263 +9,243 @@ using MQTTnet.Formatter; using MQTTnet.Packets; using MQTTnet.Protocol; -namespace MQTTnet.Tests.Server +namespace MQTTnet.Tests.Server; + +// ReSharper disable InconsistentNaming +[TestClass] +public sealed class Retained_Messages_Tests : BaseTestClass { - [TestClass] - public sealed class Retained_Messages_Tests : BaseTestClass + [TestMethod] + public async Task Clear_Retained_Message_With_Empty_Payload() { - [TestMethod] - public async Task Clear_Retained_Message_With_Empty_Payload() + using var testEnvironment = CreateTestEnvironment(); + await testEnvironment.StartServer(); + + var c1 = await testEnvironment.ConnectClient(); + + await c1.PublishAsync(new MqttApplicationMessageBuilder().WithTopic("retained").WithPayload(new byte[3]).WithRetainFlag().Build()); + await c1.PublishAsync(new MqttApplicationMessageBuilder().WithTopic("retained").WithPayload(new byte[0]).WithRetainFlag().Build()); + + await c1.DisconnectAsync(); + + var c2 = await testEnvironment.ConnectClient(); + var messageHandler = testEnvironment.CreateApplicationMessageHandler(c2); + + await Task.Delay(200); + await c2.SubscribeAsync(new MqttTopicFilter { Topic = "retained", QualityOfServiceLevel = MqttQualityOfServiceLevel.AtMostOnce }); + await Task.Delay(500); + + messageHandler.AssertReceivedCountEquals(0); + } + + [TestMethod] + public async Task Clear_Retained_Message_With_Null_Payload() + { + using var testEnvironment = CreateTestEnvironment(); + await testEnvironment.StartServer(); + + var c1 = await testEnvironment.ConnectClient(); + + await c1.PublishAsync(new MqttApplicationMessageBuilder().WithTopic("retained").WithPayload(new byte[3]).WithRetainFlag().Build()); + await c1.PublishAsync(new MqttApplicationMessageBuilder().WithTopic("retained").WithPayload((byte[])null).WithRetainFlag().Build()); + + await c1.DisconnectAsync(); + + var c2 = await testEnvironment.ConnectClient(); + var messageHandler = testEnvironment.CreateApplicationMessageHandler(c2); + + await Task.Delay(200); + await c2.SubscribeAsync(new MqttTopicFilter { Topic = "retained", QualityOfServiceLevel = MqttQualityOfServiceLevel.AtMostOnce }); + await Task.Delay(500); + + messageHandler.AssertReceivedCountEquals(0); + } + + [TestMethod] + public async Task Downgrade_QoS_Level() + { + using var testEnvironment = CreateTestEnvironment(); + await testEnvironment.StartServer(); + + var c1 = await testEnvironment.ConnectClient(); + + // Add the retained message with QoS 2! + await c1.PublishAsync( + new MqttApplicationMessageBuilder().WithTopic("retained") + .WithPayload(new byte[3]) + .WithRetainFlag() + .WithQualityOfServiceLevel(MqttQualityOfServiceLevel.ExactlyOnce) + .Build()); + + // The second client uses QoS 1 so a downgrade is required. + var c2 = await testEnvironment.ConnectClient(); + var messageHandler = testEnvironment.CreateApplicationMessageHandler(c2); + await c2.SubscribeAsync(new MqttTopicFilter { Topic = "retained", QualityOfServiceLevel = MqttQualityOfServiceLevel.AtLeastOnce }); + + await LongTestDelay(); + + messageHandler.AssertReceivedCountEquals(1); + + Assert.AreEqual(MqttQualityOfServiceLevel.AtLeastOnce, messageHandler.ReceivedEventArgs.First().ApplicationMessage.QualityOfServiceLevel); + } + + [TestMethod] + public async Task No_Upgrade_QoS_Level() + { + using var testEnvironment = CreateTestEnvironment(); + await testEnvironment.StartServer(); + + var c1 = await testEnvironment.ConnectClient(); + + // Add the retained message with QoS 1! + await c1.PublishAsync( + new MqttApplicationMessageBuilder().WithTopic("retained") + .WithPayload(new byte[3]) + .WithRetainFlag() + .WithQualityOfServiceLevel(MqttQualityOfServiceLevel.AtLeastOnce) + .Build()); + + // The second client uses QoS 2 so an upgrade is expected but according to the MQTT spec this is not supported! + var c2 = await testEnvironment.ConnectClient(); + var messageHandler = testEnvironment.CreateApplicationMessageHandler(c2); + await c2.SubscribeAsync(new MqttTopicFilter { Topic = "retained", QualityOfServiceLevel = MqttQualityOfServiceLevel.ExactlyOnce }); + + await LongTestDelay(); + + messageHandler.AssertReceivedCountEquals(1); + + Assert.AreEqual(MqttQualityOfServiceLevel.AtLeastOnce, messageHandler.ReceivedEventArgs.First().ApplicationMessage.QualityOfServiceLevel); + } + + [TestMethod] + public async Task Receive_No_Retained_Message_After_Subscribe() + { + using var testEnvironment = CreateTestEnvironment(); + await testEnvironment.StartServer(); + + var c1 = await testEnvironment.ConnectClient(); + await c1.PublishAsync(new MqttApplicationMessageBuilder().WithTopic("retained").WithPayload(new byte[3]).WithRetainFlag().Build()); + await c1.DisconnectAsync(); + + var c2 = await testEnvironment.ConnectClient(); + var messageHandler = testEnvironment.CreateApplicationMessageHandler(c2); + await c2.SubscribeAsync(new MqttTopicFilterBuilder().WithTopic("retained_other").Build()); + + await Task.Delay(500); + + messageHandler.AssertReceivedCountEquals(0); + } + + [TestMethod] + public async Task Receive_Retained_Message_After_Subscribe() + { + using var testEnvironment = CreateTestEnvironment(); + await testEnvironment.StartServer(); + + var c1 = await testEnvironment.ConnectClient(); + await c1.PublishAsync(new MqttApplicationMessageBuilder().WithTopic("retained").WithPayload(new byte[3]).WithRetainFlag().Build()); + await c1.DisconnectAsync(); + + var c2 = await testEnvironment.ConnectClient(); + var messageHandler = testEnvironment.CreateApplicationMessageHandler(c2); + + await c2.SubscribeAsync(new MqttTopicFilterBuilder().WithTopic("retained").Build()); + + await Task.Delay(500); + + messageHandler.AssertReceivedCountEquals(1); + Assert.IsTrue(messageHandler.ReceivedEventArgs.First().ApplicationMessage.Retain); + } + + [TestMethod] + public async Task Receive_Retained_Messages_From_Higher_Qos_Level() + { + using var testEnvironment = CreateTestEnvironment(); + await testEnvironment.StartServer(); + + // Upload retained message. + var c1 = await testEnvironment.ConnectClient(); + await c1.PublishAsync( + new MqttApplicationMessageBuilder().WithTopic("retained") + .WithPayload(new byte[1]) + .WithQualityOfServiceLevel(MqttQualityOfServiceLevel.AtLeastOnce) + .WithRetainFlag() + .Build()); + + await c1.DisconnectAsync(); + + // Subscribe using a new client. + var c2 = await testEnvironment.ConnectClient(); + var messageHandler = testEnvironment.CreateApplicationMessageHandler(c2); + + await Task.Delay(200); + // Using QoS 2 will lead to 1 instead because the publish was made with QoS level 1 (see 3.8.4 SUBSCRIBE Actions)! + await c2.SubscribeAsync(new MqttTopicFilter { Topic = "retained", QualityOfServiceLevel = MqttQualityOfServiceLevel.ExactlyOnce }); + await Task.Delay(500); + + messageHandler.AssertReceivedCountEquals(1); + } + + [TestMethod] + public async Task Retained_Messages_Flow() + { + using var testEnvironment = CreateTestEnvironment(); + var retainedMessage = new MqttApplicationMessageBuilder().WithTopic("r").WithPayload("r").WithRetainFlag().Build(); + + await testEnvironment.StartServer(); + var c1 = await testEnvironment.ConnectClient(); + + var c2 = await testEnvironment.ConnectClient(); + var messageHandler = testEnvironment.CreateApplicationMessageHandler(c2); + + await c1.PublishAsync(retainedMessage); + await c1.DisconnectAsync(); + await LongTestDelay(); + + for (var i = 0; i < 5; i++) { - using (var testEnvironment = CreateTestEnvironment()) - { - await testEnvironment.StartServer(); + await c2.UnsubscribeAsync("r"); + await Task.Delay(100); + messageHandler.AssertReceivedCountEquals(i); - var c1 = await testEnvironment.ConnectClient(); - - await c1.PublishAsync(new MqttApplicationMessageBuilder().WithTopic("retained").WithPayload(new byte[3]).WithRetainFlag().Build()); - await c1.PublishAsync(new MqttApplicationMessageBuilder().WithTopic("retained").WithPayload(new byte[0]).WithRetainFlag().Build()); - - await c1.DisconnectAsync(); - - var c2 = await testEnvironment.ConnectClient(); - var messageHandler = testEnvironment.CreateApplicationMessageHandler(c2); - - await Task.Delay(200); - await c2.SubscribeAsync(new MqttTopicFilter { Topic = "retained", QualityOfServiceLevel = MqttQualityOfServiceLevel.AtMostOnce }); - await Task.Delay(500); - - messageHandler.AssertReceivedCountEquals(0); - } + await c2.SubscribeAsync("r"); + await Task.Delay(100); + messageHandler.AssertReceivedCountEquals(i + 1); } - [TestMethod] - public async Task Clear_Retained_Message_With_Null_Payload() - { - using (var testEnvironment = CreateTestEnvironment()) - { - await testEnvironment.StartServer(); + await c2.DisconnectAsync(); + } - var c1 = await testEnvironment.ConnectClient(); + [TestMethod] + public async Task Server_Reports_Retained_Messages_Supported_V3() + { + using var testEnvironment = CreateTestEnvironment(); + await testEnvironment.StartServer(); - await c1.PublishAsync(new MqttApplicationMessageBuilder().WithTopic("retained").WithPayload(new byte[3]).WithRetainFlag().Build()); - await c1.PublishAsync(new MqttApplicationMessageBuilder().WithTopic("retained").WithPayload((byte[])null).WithRetainFlag().Build()); + var client = testEnvironment.CreateClient(); - await c1.DisconnectAsync(); + var connectResult = await client.ConnectAsync( + testEnvironment.ClientFactory.CreateClientOptionsBuilder() + .WithProtocolVersion(MqttProtocolVersion.V311) + .WithTcpServer("127.0.0.1", testEnvironment.ServerPort) + .Build()); - var c2 = await testEnvironment.ConnectClient(); - var messageHandler = testEnvironment.CreateApplicationMessageHandler(c2); + Assert.IsTrue(connectResult.RetainAvailable); + } - await Task.Delay(200); - await c2.SubscribeAsync(new MqttTopicFilter { Topic = "retained", QualityOfServiceLevel = MqttQualityOfServiceLevel.AtMostOnce }); - await Task.Delay(500); + [TestMethod] + public async Task Server_Reports_Retained_Messages_Supported_V5() + { + using var testEnvironment = CreateTestEnvironment(MqttProtocolVersion.V500); + await testEnvironment.StartServer(); - messageHandler.AssertReceivedCountEquals(0); - } - } + var client = testEnvironment.CreateClient(); + var connectResult = await client.ConnectAsync( + testEnvironment.ClientFactory.CreateClientOptionsBuilder() + .WithProtocolVersion(MqttProtocolVersion.V500) + .WithTcpServer("127.0.0.1", testEnvironment.ServerPort) + .Build()); - [TestMethod] - public async Task Downgrade_QoS_Level() - { - using (var testEnvironment = CreateTestEnvironment()) - { - await testEnvironment.StartServer(); - - var c1 = await testEnvironment.ConnectClient(); - - // Add the retained message with QoS 2! - await c1.PublishAsync( - new MqttApplicationMessageBuilder().WithTopic("retained") - .WithPayload(new byte[3]) - .WithRetainFlag() - .WithQualityOfServiceLevel(MqttQualityOfServiceLevel.ExactlyOnce) - .Build()); - - // The second client uses QoS 1 so a downgrade is required. - var c2 = await testEnvironment.ConnectClient(); - var messageHandler = testEnvironment.CreateApplicationMessageHandler(c2); - await c2.SubscribeAsync(new MqttTopicFilter { Topic = "retained", QualityOfServiceLevel = MqttQualityOfServiceLevel.AtLeastOnce }); - - await LongTestDelay(); - - messageHandler.AssertReceivedCountEquals(1); - - Assert.AreEqual(MqttQualityOfServiceLevel.AtLeastOnce, messageHandler.ReceivedEventArgs.First().ApplicationMessage.QualityOfServiceLevel); - } - } - - [TestMethod] - public async Task No_Upgrade_QoS_Level() - { - using (var testEnvironment = CreateTestEnvironment()) - { - await testEnvironment.StartServer(); - - var c1 = await testEnvironment.ConnectClient(); - - // Add the retained message with QoS 1! - await c1.PublishAsync( - new MqttApplicationMessageBuilder().WithTopic("retained") - .WithPayload(new byte[3]) - .WithRetainFlag() - .WithQualityOfServiceLevel(MqttQualityOfServiceLevel.AtLeastOnce) - .Build()); - - // The second client uses QoS 2 so an upgrade is expected but according to the MQTT spec this is not supported! - var c2 = await testEnvironment.ConnectClient(); - var messageHandler = testEnvironment.CreateApplicationMessageHandler(c2); - await c2.SubscribeAsync(new MqttTopicFilter { Topic = "retained", QualityOfServiceLevel = MqttQualityOfServiceLevel.ExactlyOnce }); - - await LongTestDelay(); - - messageHandler.AssertReceivedCountEquals(1); - - Assert.AreEqual(MqttQualityOfServiceLevel.AtLeastOnce, messageHandler.ReceivedEventArgs.First().ApplicationMessage.QualityOfServiceLevel); - } - } - - [TestMethod] - public async Task Receive_No_Retained_Message_After_Subscribe() - { - using (var testEnvironment = CreateTestEnvironment()) - { - await testEnvironment.StartServer(); - - var c1 = await testEnvironment.ConnectClient(); - await c1.PublishAsync(new MqttApplicationMessageBuilder().WithTopic("retained").WithPayload(new byte[3]).WithRetainFlag().Build()); - await c1.DisconnectAsync(); - - var c2 = await testEnvironment.ConnectClient(); - var messageHandler = testEnvironment.CreateApplicationMessageHandler(c2); - await c2.SubscribeAsync(new MqttTopicFilterBuilder().WithTopic("retained_other").Build()); - - await Task.Delay(500); - - messageHandler.AssertReceivedCountEquals(0); - } - } - - [TestMethod] - public async Task Receive_Retained_Message_After_Subscribe() - { - using (var testEnvironment = CreateTestEnvironment()) - { - await testEnvironment.StartServer(); - - var c1 = await testEnvironment.ConnectClient(); - await c1.PublishAsync(new MqttApplicationMessageBuilder().WithTopic("retained").WithPayload(new byte[3]).WithRetainFlag().Build()); - await c1.DisconnectAsync(); - - var c2 = await testEnvironment.ConnectClient(); - var messageHandler = testEnvironment.CreateApplicationMessageHandler(c2); - - await c2.SubscribeAsync(new MqttTopicFilterBuilder().WithTopic("retained").Build()); - - await Task.Delay(500); - - messageHandler.AssertReceivedCountEquals(1); - Assert.IsTrue(messageHandler.ReceivedEventArgs.First().ApplicationMessage.Retain); - } - } - - [TestMethod] - public async Task Receive_Retained_Messages_From_Higher_Qos_Level() - { - using (var testEnvironment = CreateTestEnvironment()) - { - await testEnvironment.StartServer(); - - // Upload retained message. - var c1 = await testEnvironment.ConnectClient(); - await c1.PublishAsync( - new MqttApplicationMessageBuilder().WithTopic("retained") - .WithPayload(new byte[1]) - .WithQualityOfServiceLevel(MqttQualityOfServiceLevel.AtLeastOnce) - .WithRetainFlag() - .Build()); - - await c1.DisconnectAsync(); - - // Subscribe using a new client. - var c2 = await testEnvironment.ConnectClient(); - var messageHandler = testEnvironment.CreateApplicationMessageHandler(c2); - - await Task.Delay(200); - // Using QoS 2 will lead to 1 instead because the publish was made with QoS level 1 (see 3.8.4 SUBSCRIBE Actions)! - await c2.SubscribeAsync(new MqttTopicFilter { Topic = "retained", QualityOfServiceLevel = MqttQualityOfServiceLevel.ExactlyOnce }); - await Task.Delay(500); - - messageHandler.AssertReceivedCountEquals(1); - } - } - - [TestMethod] - public async Task Retained_Messages_Flow() - { - using (var testEnvironment = CreateTestEnvironment()) - { - var retainedMessage = new MqttApplicationMessageBuilder().WithTopic("r").WithPayload("r").WithRetainFlag().Build(); - - await testEnvironment.StartServer(); - var c1 = await testEnvironment.ConnectClient(); - - var c2 = await testEnvironment.ConnectClient(); - var messageHandler = testEnvironment.CreateApplicationMessageHandler(c2); - - await c1.PublishAsync(retainedMessage); - await c1.DisconnectAsync(); - await LongTestDelay(); - - for (var i = 0; i < 5; i++) - { - await c2.UnsubscribeAsync("r"); - await Task.Delay(100); - messageHandler.AssertReceivedCountEquals(i); - - await c2.SubscribeAsync("r"); - await Task.Delay(100); - messageHandler.AssertReceivedCountEquals(i + 1); - } - - await c2.DisconnectAsync(); - } - } - - [TestMethod] - public async Task Server_Reports_Retained_Messages_Supported_V3() - { - using (var testEnvironment = CreateTestEnvironment()) - { - await testEnvironment.StartServer(); - - var client = testEnvironment.CreateClient(); - - var connectResult = await client.ConnectAsync( - testEnvironment.ClientFactory.CreateClientOptionsBuilder() - .WithProtocolVersion(MqttProtocolVersion.V311) - .WithTcpServer("127.0.0.1", testEnvironment.ServerPort) - .Build()); - - Assert.IsTrue(connectResult.RetainAvailable); - } - } - - [TestMethod] - public async Task Server_Reports_Retained_Messages_Supported_V5() - { - using (var testEnvironment = CreateTestEnvironment(MqttProtocolVersion.V500)) - { - await testEnvironment.StartServer(); - - var client = testEnvironment.CreateClient(); - var connectResult = await client.ConnectAsync( - testEnvironment.ClientFactory.CreateClientOptionsBuilder() - .WithProtocolVersion(MqttProtocolVersion.V500) - .WithTcpServer("127.0.0.1", testEnvironment.ServerPort) - .Build()); - - Assert.IsTrue(connectResult.RetainAvailable); - } - } + Assert.IsTrue(connectResult.RetainAvailable); } } \ No newline at end of file diff --git a/Source/MQTTnet.Tests/Server/Security_Tests.cs b/Source/MQTTnet.Tests/Server/Security_Tests.cs index 404314c4..2e7eba29 100644 --- a/Source/MQTTnet.Tests/Server/Security_Tests.cs +++ b/Source/MQTTnet.Tests/Server/Security_Tests.cs @@ -10,161 +10,151 @@ using MQTTnet.Exceptions; using MQTTnet.Internal; using MQTTnet.Protocol; -namespace MQTTnet.Tests.Server +namespace MQTTnet.Tests.Server; + +// ReSharper disable InconsistentNaming +[TestClass] +public sealed class Security_Tests : BaseTestClass { - [TestClass] - public sealed class Security_Tests : BaseTestClass + [TestMethod] + public async Task Do_Not_Affect_Authorized_Clients() { - [TestMethod] - public async Task Do_Not_Affect_Authorized_Clients() + using var testEnvironment = CreateTestEnvironment(); + testEnvironment.IgnoreClientLogErrors = true; + + await testEnvironment.StartServer(); + + var publishedApplicationMessages = new List(); + testEnvironment.Server.InterceptingPublishAsync += eventArgs => { - using (var testEnvironment = CreateTestEnvironment()) + lock (publishedApplicationMessages) { - testEnvironment.IgnoreClientLogErrors = true; - - await testEnvironment.StartServer(); - - var publishedApplicationMessages = new List(); - testEnvironment.Server.InterceptingPublishAsync += eventArgs => - { - lock (publishedApplicationMessages) - { - publishedApplicationMessages.Add(eventArgs.ApplicationMessage); - } - - return CompletedTask.Instance; - }; - - testEnvironment.Server.ValidatingConnectionAsync += eventArgs => - { - if (eventArgs.UserName == "SECRET") - { - eventArgs.ReasonCode = MqttConnectReasonCode.Success; - } - else - { - eventArgs.ReasonCode = MqttConnectReasonCode.NotAuthorized; - } - - return CompletedTask.Instance; - }; - - using (var validClient = testEnvironment.CreateClient()) - { - await validClient.ConnectAsync( - testEnvironment.ClientFactory.CreateClientOptionsBuilder() - .WithTcpServer("localhost", testEnvironment.ServerPort) - .WithCredentials("SECRET") - .WithClientId("CLIENT") - .Build()); - - await validClient.PublishStringAsync("HELLO 1"); - - // The following code tries to connect a new client with the same client ID but invalid - // credentials. This should block the second client but keep the first one up and running. - try - { - using (var invalidClient = testEnvironment.CreateClient()) - { - await invalidClient.ConnectAsync( - testEnvironment.ClientFactory.CreateClientOptionsBuilder() - .WithTcpServer("localhost", testEnvironment.ServerPort) - .WithCredentials("???") - .WithClientId("CLIENT") - .Build()); - } - } - catch - { - // Ignore errors from the second client. - } - - await LongTestDelay(); - - await validClient.PublishStringAsync("HELLO 2"); - - await LongTestDelay(); - - await validClient.PublishStringAsync("HELLO 3"); - - await LongTestDelay(); - - Assert.AreEqual(3, publishedApplicationMessages.Count); - Assert.AreEqual(1, testEnvironment.Server.GetClientsAsync().GetAwaiter().GetResult().Count); - } + publishedApplicationMessages.Add(eventArgs.ApplicationMessage); } - } - [TestMethod] - public Task Handle_Wrong_Password() + return CompletedTask.Instance; + }; + + testEnvironment.Server.ValidatingConnectionAsync += eventArgs => { - return TestCredentials("UserName", "x"); - } - - [TestMethod] - public Task Handle_Wrong_UserName() - { - return TestCredentials("x", "Password1"); - } - - [TestMethod] - public Task Handle_Wrong_UserName_And_Password() - { - return TestCredentials("x", "x"); - } - - [TestMethod] - public async Task Use_Username_Null_Password_Empty() - { - string username = null; - var password = string.Empty; - - using (var testEnvironment = CreateTestEnvironment()) + if (eventArgs.UserName == "SECRET") { - testEnvironment.IgnoreClientLogErrors = true; - - await testEnvironment.StartServer(); - - var client = testEnvironment.CreateClient(); - - var clientOptions = new MqttClientOptionsBuilder().WithTcpServer("127.0.0.1", testEnvironment.ServerPort).WithCredentials(username, password).Build(); - - var ex = await Assert.ThrowsExceptionAsync(async () => await client.ConnectAsync(clientOptions)); - Assert.IsInstanceOfType(ex.InnerException, typeof(MqttProtocolViolationException)); - Assert.AreEqual("Error while authenticating. If the User Name Flag is set to 0, the Password Flag MUST be set to 0 [MQTT-3.1.2-22].", ex.Message, false); + eventArgs.ReasonCode = MqttConnectReasonCode.Success; } - } - - async Task TestCredentials(string userName, string password) - { - using (var testEnvironment = CreateTestEnvironment()) + else { - testEnvironment.IgnoreClientLogErrors = true; - - var server = await testEnvironment.StartServer(); - - server.ValidatingConnectionAsync += e => - { - if (e.UserName != "UserName1") - { - e.ReasonCode = MqttConnectReasonCode.BadUserNameOrPassword; - } - - if (e.Password != "Password1") - { - e.ReasonCode = MqttConnectReasonCode.BadUserNameOrPassword; - } - - return CompletedTask.Instance; - }; - - var client = testEnvironment.CreateClient(); - - var clientOptions = new MqttClientOptionsBuilder().WithTcpServer("127.0.0.1", testEnvironment.ServerPort).WithCredentials(userName, password).Build(); - - var response = await client.ConnectAsync(clientOptions); - Assert.AreEqual(MqttClientConnectResultCode.BadUserNameOrPassword, response.ResultCode); + eventArgs.ReasonCode = MqttConnectReasonCode.NotAuthorized; } + + return CompletedTask.Instance; + }; + + using var validClient = testEnvironment.CreateClient(); + await validClient.ConnectAsync( + testEnvironment.ClientFactory.CreateClientOptionsBuilder() + .WithTcpServer("localhost", testEnvironment.ServerPort) + .WithCredentials("SECRET") + .WithClientId("CLIENT") + .Build()); + + await validClient.PublishStringAsync("HELLO 1"); + + // The following code tries to connect a new client with the same client ID but invalid + // credentials. This should block the second client but keep the first one up and running. + try + { + using var invalidClient = testEnvironment.CreateClient(); + await invalidClient.ConnectAsync( + testEnvironment.ClientFactory.CreateClientOptionsBuilder() + .WithTcpServer("localhost", testEnvironment.ServerPort) + .WithCredentials("???") + .WithClientId("CLIENT") + .Build()); } + catch + { + // Ignore errors from the second client. + } + + await LongTestDelay(); + + await validClient.PublishStringAsync("HELLO 2"); + + await LongTestDelay(); + + await validClient.PublishStringAsync("HELLO 3"); + + await LongTestDelay(); + + Assert.AreEqual(3, publishedApplicationMessages.Count); + Assert.AreEqual(1, testEnvironment.Server.GetClientsAsync().GetAwaiter().GetResult().Count); + } + + [TestMethod] + public Task Handle_Wrong_Password() + { + return TestCredentials("UserName", "x"); + } + + [TestMethod] + public Task Handle_Wrong_UserName() + { + return TestCredentials("x", "Password1"); + } + + [TestMethod] + public Task Handle_Wrong_UserName_And_Password() + { + return TestCredentials("x", "x"); + } + + [TestMethod] + public async Task Use_Username_Null_Password_Empty() + { + string username = null; + var password = string.Empty; + + using var testEnvironment = CreateTestEnvironment(); + testEnvironment.IgnoreClientLogErrors = true; + + await testEnvironment.StartServer(); + + var client = testEnvironment.CreateClient(); + + var clientOptions = new MqttClientOptionsBuilder().WithTcpServer("127.0.0.1", testEnvironment.ServerPort).WithCredentials(username, password).Build(); + + var ex = await Assert.ThrowsExceptionAsync(async () => await client.ConnectAsync(clientOptions).ConfigureAwait(false)); + Assert.IsInstanceOfType(ex.InnerException, typeof(MqttProtocolViolationException)); + Assert.AreEqual("Error while authenticating. If the User Name Flag is set to 0, the Password Flag MUST be set to 0 [MQTT-3.1.2-22].", ex.Message, false); + } + + async Task TestCredentials(string userName, string password) + { + using var testEnvironment = CreateTestEnvironment(); + testEnvironment.IgnoreClientLogErrors = true; + + var server = await testEnvironment.StartServer(); + + server.ValidatingConnectionAsync += e => + { + if (e.UserName != "UserName1") + { + e.ReasonCode = MqttConnectReasonCode.BadUserNameOrPassword; + } + + if (e.Password != "Password1") + { + e.ReasonCode = MqttConnectReasonCode.BadUserNameOrPassword; + } + + return CompletedTask.Instance; + }; + + var client = testEnvironment.CreateClient(); + + var clientOptions = new MqttClientOptionsBuilder().WithTcpServer("127.0.0.1", testEnvironment.ServerPort).WithCredentials(userName, password).Build(); + + var response = await client.ConnectAsync(clientOptions); + Assert.AreEqual(MqttClientConnectResultCode.BadUserNameOrPassword, response.ResultCode); } } \ No newline at end of file diff --git a/Source/MQTTnet.Tests/Server/Server_Reference_Tests.cs b/Source/MQTTnet.Tests/Server/Server_Reference_Tests.cs index 2b4f96c1..295f7286 100644 --- a/Source/MQTTnet.Tests/Server/Server_Reference_Tests.cs +++ b/Source/MQTTnet.Tests/Server/Server_Reference_Tests.cs @@ -10,32 +10,31 @@ using MQTTnet.Protocol; namespace MQTTnet.Tests.Server; +// ReSharper disable InconsistentNaming [TestClass] public sealed class Server_Reference_Tests : BaseTestClass { [TestMethod] public async Task Server_Reports_With_Reference_Server() { - using (var testEnvironment = CreateTestEnvironment()) + using var testEnvironment = CreateTestEnvironment(); + testEnvironment.IgnoreClientLogErrors = true; + + var server = await testEnvironment.StartServer(); + + server.ValidatingConnectionAsync += e => { - testEnvironment.IgnoreClientLogErrors = true; + e.ReasonCode = MqttConnectReasonCode.ServerMoved; + e.ServerReference = "new_server"; + return CompletedTask.Instance; + }; - var server = await testEnvironment.StartServer(); + var client = testEnvironment.CreateClient(); - server.ValidatingConnectionAsync += e => - { - e.ReasonCode = MqttConnectReasonCode.ServerMoved; - e.ServerReference = "new_server"; - return CompletedTask.Instance; - }; + var response = await client.ConnectAsync( + new MqttClientOptionsBuilder().WithProtocolVersion(MqttProtocolVersion.V500).WithTcpServer("127.0.0.1", testEnvironment.ServerPort).Build()); - var client = testEnvironment.CreateClient(); - - var response = await client.ConnectAsync( - new MqttClientOptionsBuilder().WithProtocolVersion(MqttProtocolVersion.V500).WithTcpServer("127.0.0.1", testEnvironment.ServerPort).Build()); - - Assert.AreEqual(MqttClientConnectResultCode.ServerMoved, response.ResultCode); - Assert.AreEqual("new_server", response.ServerReference); - } + Assert.AreEqual(MqttClientConnectResultCode.ServerMoved, response.ResultCode); + Assert.AreEqual("new_server", response.ServerReference); } } \ No newline at end of file diff --git a/Source/MQTTnet.Tests/Server/Session_Tests.cs b/Source/MQTTnet.Tests/Server/Session_Tests.cs index 91345da5..2467adb5 100644 --- a/Source/MQTTnet.Tests/Server/Session_Tests.cs +++ b/Source/MQTTnet.Tests/Server/Session_Tests.cs @@ -13,377 +13,357 @@ using MQTTnet.Internal; using MQTTnet.Protocol; using MQTTnet.Tests.Mockups; -namespace MQTTnet.Tests.Server +namespace MQTTnet.Tests.Server; + +// ReSharper disable InconsistentNaming +[TestClass] +public sealed class Session_Tests : BaseTestClass { - [TestClass] - public sealed class Session_Tests : BaseTestClass + [TestMethod] + public async Task Clean_Session_Persistence() { - [TestMethod] - public async Task Clean_Session_Persistence() + using var testEnvironment = new TestEnvironment(TestContext); + // Create server with persistent sessions enabled + + await testEnvironment.StartServer(o => o.WithPersistentSessions()); + + const string clientId = "Client1"; + + // Create client with clean session and long session expiry interval + + var client1 = await testEnvironment.ConnectClient( + o => o.WithProtocolVersion(MqttProtocolVersion.V311) + .WithTcpServer("127.0.0.1", testEnvironment.ServerPort) + .WithSessionExpiryInterval(9999) // not relevant for v311 but testing impact + .WithCleanSession() // start and end with clean session + .WithClientId(clientId) + .Build()); + + // Disconnect; empty session should be removed from server + + await client1.DisconnectAsync(); + + // Simulate some time delay between connections + + await Task.Delay(1000); + + // Reconnect the same client ID without clean session + + var client2 = testEnvironment.CreateClient(); + var options = testEnvironment.ClientFactory.CreateClientOptionsBuilder() + .WithProtocolVersion(MqttProtocolVersion.V311) + .WithTcpServer("127.0.0.1", testEnvironment.ServerPort) + .WithSessionExpiryInterval(9999) // not relevant for v311 but testing impact + .WithCleanSession(false) // see if there is a session + .WithClientId(clientId) + .Build(); + + + var result = await client2.ConnectAsync(options).ConfigureAwait(false); + + await client2.DisconnectAsync(); + + // Session should NOT be present for MQTT v311 and initial CleanSession == true + + Assert.IsTrue(!result.IsSessionPresent, "Session present"); + } + + [TestMethod] + public async Task Do_Not_Use_Expired_Session() + { + using var testEnvironment = CreateTestEnvironment(MqttProtocolVersion.V500); + await testEnvironment.StartServer(o => o.WithPersistentSessions()); + + var c1 = await testEnvironment.ConnectClient(o => o.WithClientId("Client1").WithCleanSession(false).WithSessionExpiryInterval(3)); + + // Kill the client connection and ensure that the session will stay there but is expired after 3 seconds! + c1.Dispose(); + + await Task.Delay(TimeSpan.FromSeconds(6)); + + c1 = testEnvironment.CreateClient(); + var options = testEnvironment.CreateDefaultClientOptionsBuilder().WithClientId("Client1").WithCleanSession(false).WithSessionExpiryInterval(3).Build(); + + var connectResult = await c1.ConnectAsync(options); + Assert.AreEqual(false, connectResult.IsSessionPresent); + } + + [TestMethod] + public async Task Fire_Deleted_Event() + { + using var testEnvironment = CreateTestEnvironment(); + // Arrange client and server. + var server = await testEnvironment.StartServer(o => o.WithPersistentSessions(false)); + + var deletedEventFired = false; + server.SessionDeletedAsync += _ => { - using (var testEnvironment = new TestEnvironment(TestContext)) - { - // Create server with persistent sessions enabled + deletedEventFired = true; + return CompletedTask.Instance; + }; - await testEnvironment.StartServer(o => o.WithPersistentSessions()); + var client = await testEnvironment.ConnectClient(); - const string clientId = "Client1"; + // Act: Disconnect the client -> Event must be fired. + await client.DisconnectAsync(); - // Create client with clean session and long session expiry interval + await LongTestDelay(); - var client1 = await testEnvironment.ConnectClient( - o => o.WithProtocolVersion(MqttProtocolVersion.V311) - .WithTcpServer("127.0.0.1", testEnvironment.ServerPort) - .WithSessionExpiryInterval(9999) // not relevant for v311 but testing impact - .WithCleanSession() // start and end with clean session - .WithClientId(clientId) - .Build()); + // Assert that the event was fired properly. + Assert.IsTrue(deletedEventFired); + } - // Disconnect; empty session should be removed from server + [TestMethod] + public async Task Get_Session_Items_In_Status() + { + using var testEnvironment = CreateTestEnvironment(); + var server = await testEnvironment.StartServer(); - await client1.DisconnectAsync(); + server.ValidatingConnectionAsync += e => + { + // Don't validate anything. Just set some session items. + e.SessionItems["can_subscribe_x"] = true; + e.SessionItems["default_payload"] = "Hello World"; - // Simulate some time delay between connections + return CompletedTask.Instance; + }; - await Task.Delay(1000); + await testEnvironment.ConnectClient(); - // Reconnect the same client ID without clean session + var sessionStatus = await testEnvironment.Server.GetSessionsAsync(); + var session = sessionStatus.First(); - var client2 = testEnvironment.CreateClient(); - var options = testEnvironment.ClientFactory.CreateClientOptionsBuilder() - .WithProtocolVersion(MqttProtocolVersion.V311) - .WithTcpServer("127.0.0.1", testEnvironment.ServerPort) - .WithSessionExpiryInterval(9999) // not relevant for v311 but testing impact - .WithCleanSession(false) // see if there is a session - .WithClientId(clientId) - .Build(); + Assert.AreEqual(true, session.Items["can_subscribe_x"]); + } + [TestMethod] + [DataRow(MqttProtocolVersion.V310)] + [DataRow(MqttProtocolVersion.V311)] + [DataRow(MqttProtocolVersion.V500)] + public async Task Handle_Parallel_Connection_Attempts(MqttProtocolVersion protocolVersion) + { + using var testEnvironment = CreateTestEnvironment(); + testEnvironment.IgnoreClientLogErrors = true; - var result = await client2.ConnectAsync(options).ConfigureAwait(false); + await testEnvironment.StartServer(); - await client2.DisconnectAsync(); + var options = new MqttClientOptionsBuilder().WithClientId("1").WithTimeout(TimeSpan.FromSeconds(10)).WithProtocolVersion(protocolVersion); - // Session should NOT be present for MQTT v311 and initial CleanSession == true + var hasReceive = false; - Assert.IsTrue(!result.IsSessionPresent, "Session present"); - } + // Try to connect 50 clients at the same time. + var clients = await Task.WhenAll(Enumerable.Range(0, 50).Select(_ => ConnectAndSubscribe(testEnvironment, options, OnReceive))); + + var connectedClients = clients.Where(c => c?.TryPingAsync().GetAwaiter().GetResult() == true).ToList(); + + await LongTestDelay(); + + Assert.AreEqual(1, connectedClients.Count); + + var option2 = new MqttClientOptionsBuilder().WithClientId("2").WithKeepAlivePeriod(TimeSpan.FromSeconds(10)); + var sendClient = await testEnvironment.ConnectClient(option2); + await sendClient.PublishStringAsync("aaa", "1"); + + await LongTestDelay(); + + Assert.AreEqual(true, hasReceive); + return; + + void OnReceive() + { + hasReceive = true; } + } - [TestMethod] - public async Task Do_Not_Use_Expired_Session() + [TestMethod] + [DataRow(MqttQualityOfServiceLevel.ExactlyOnce)] + [DataRow(MqttQualityOfServiceLevel.AtLeastOnce)] + public async Task Retry_If_Not_PubAck(MqttQualityOfServiceLevel qos) + { + long count = 0; + using var testEnvironment = CreateTestEnvironment(); + await testEnvironment.StartServer(o => o.WithPersistentSessions()); + + var publisher = await testEnvironment.ConnectClient(); + + var subscriber = await testEnvironment.ConnectClient(o => o.WithClientId(qos.ToString()).WithCleanSession(false)); + + subscriber.ApplicationMessageReceivedAsync += c => { - using (var testEnvironment = CreateTestEnvironment(MqttProtocolVersion.V500)) - { - await testEnvironment.StartServer(o => o.WithPersistentSessions()); + c.AutoAcknowledge = false; + ++count; + Console.WriteLine("process"); + return CompletedTask.Instance; + }; - var c1 = await testEnvironment.ConnectClient(o => o.WithClientId("Client1").WithCleanSession(false).WithSessionExpiryInterval(3)); + await subscriber.SubscribeAsync("#", qos); - // Kill the client connection and ensure that the session will stay there but is expired after 3 seconds! - c1.Dispose(); + var pub = publisher.PublishStringAsync("a", null, qos); - await Task.Delay(TimeSpan.FromSeconds(6)); + await Task.Delay(100); + await subscriber.DisconnectAsync(); + await subscriber.ConnectAsync(subscriber.Options); + await Task.Delay(100); - c1 = testEnvironment.CreateClient(); - var options = testEnvironment.CreateDefaultClientOptionsBuilder().WithClientId("Client1").WithCleanSession(false).WithSessionExpiryInterval(3).Build(); + var res = await pub; - var connectResult = await c1.ConnectAsync(options); - Assert.AreEqual(false, connectResult.IsSessionPresent); - } - } + Assert.AreEqual(MqttClientPublishReasonCode.Success, res.ReasonCode); + Assert.AreEqual(2, count); + } - [TestMethod] - public async Task Fire_Deleted_Event() + [TestMethod] + public async Task Session_Takeover() + { + using var testEnvironment = CreateTestEnvironment(); + await testEnvironment.StartServer(); + + var options = new MqttClientOptionsBuilder().WithCleanSession(false) + .WithProtocolVersion(MqttProtocolVersion.V500) // Disconnect reason is only available in MQTT 5+ + .WithClientId("a"); + + var client1 = await testEnvironment.ConnectClient(options); + await Task.Delay(500); + + var disconnectReason = MqttClientDisconnectReason.NormalDisconnection; + client1.DisconnectedAsync += c => { - using (var testEnvironment = CreateTestEnvironment()) - { - // Arrange client and server. - var server = await testEnvironment.StartServer(o => o.WithPersistentSessions(false)); + disconnectReason = c.Reason; + return CompletedTask.Instance; + }; - var deletedEventFired = false; - server.SessionDeletedAsync += e => + var client2 = await testEnvironment.ConnectClient(options); + await Task.Delay(500); + + Assert.IsFalse(client1.IsConnected); + Assert.IsTrue(client2.IsConnected); + + Assert.AreEqual(MqttClientDisconnectReason.SessionTakenOver, disconnectReason); + } + + [TestMethod] + public async Task Set_Session_Item() + { + using var testEnvironment = CreateTestEnvironment(); + var server = await testEnvironment.StartServer(); + + server.ValidatingConnectionAsync += e => + { + // Don't validate anything. Just set some session items. + e.SessionItems["can_subscribe_x"] = true; + e.SessionItems["default_payload"] = "Hello World"; + + return CompletedTask.Instance; + }; + + server.InterceptingSubscriptionAsync += e => + { + if (e.TopicFilter.Topic == "x") + { + if (e.SessionItems["can_subscribe_x"] as bool? == false) { - deletedEventFired = true; - return CompletedTask.Instance; - }; - - var client = await testEnvironment.ConnectClient(); - - // Act: Disconnect the client -> Event must be fired. - await client.DisconnectAsync(); - - await LongTestDelay(); - - // Assert that the event was fired properly. - Assert.IsTrue(deletedEventFired); - } - } - - [TestMethod] - public async Task Get_Session_Items_In_Status() - { - using (var testEnvironment = CreateTestEnvironment()) - { - var server = await testEnvironment.StartServer(); - - server.ValidatingConnectionAsync += e => - { - // Don't validate anything. Just set some session items. - e.SessionItems["can_subscribe_x"] = true; - e.SessionItems["default_payload"] = "Hello World"; - - return CompletedTask.Instance; - }; - - await testEnvironment.ConnectClient(); - - var sessionStatus = await testEnvironment.Server.GetSessionsAsync(); - var session = sessionStatus.First(); - - Assert.AreEqual(true, session.Items["can_subscribe_x"]); - } - } - - [TestMethod] - [DataRow(MqttProtocolVersion.V310)] - [DataRow(MqttProtocolVersion.V311)] - [DataRow(MqttProtocolVersion.V500)] - public async Task Handle_Parallel_Connection_Attempts(MqttProtocolVersion protocolVersion) - { - using (var testEnvironment = CreateTestEnvironment()) - { - testEnvironment.IgnoreClientLogErrors = true; - - await testEnvironment.StartServer(); - - var options = new MqttClientOptionsBuilder().WithClientId("1").WithTimeout(TimeSpan.FromSeconds(10)).WithProtocolVersion(protocolVersion); - - var hasReceive = false; - void OnReceive() - { - hasReceive = true; + e.Response.ReasonCode = MqttSubscribeReasonCode.ImplementationSpecificError; } - - // Try to connect 50 clients at the same time. - var clients = await Task.WhenAll(Enumerable.Range(0, 50).Select(i => ConnectAndSubscribe(testEnvironment, options, OnReceive))); - - var connectedClients = clients.Where(c => c?.TryPingAsync().GetAwaiter().GetResult() == true).ToList(); - - await LongTestDelay(); - - Assert.AreEqual(1, connectedClients.Count); - - var option2 = new MqttClientOptionsBuilder().WithClientId("2").WithKeepAlivePeriod(TimeSpan.FromSeconds(10)); - var sendClient = await testEnvironment.ConnectClient(option2); - await sendClient.PublishStringAsync("aaa", "1"); - - await LongTestDelay(); - - Assert.AreEqual(true, hasReceive); } - } - [TestMethod] - [DataRow(MqttQualityOfServiceLevel.ExactlyOnce)] - [DataRow(MqttQualityOfServiceLevel.AtLeastOnce)] - public async Task Retry_If_Not_PubAck(MqttQualityOfServiceLevel qos) + return CompletedTask.Instance; + }; + + server.InterceptingPublishAsync += e => { - long count = 0; - using (var testEnvironment = CreateTestEnvironment()) - { - await testEnvironment.StartServer(o => o.WithPersistentSessions()); + e.ApplicationMessage.PayloadSegment = new ArraySegment(Encoding.UTF8.GetBytes(e.SessionItems["default_payload"] as string ?? string.Empty)); + return CompletedTask.Instance; + }; - var publisher = await testEnvironment.ConnectClient(); + string receivedPayload = null; - var subscriber = await testEnvironment.ConnectClient(o => o.WithClientId(qos.ToString()).WithCleanSession(false)); - - subscriber.ApplicationMessageReceivedAsync += c => - { - c.AutoAcknowledge = false; - ++count; - Console.WriteLine("process"); - return CompletedTask.Instance; - }; - - await subscriber.SubscribeAsync("#", qos); - - var pub = publisher.PublishStringAsync("a", null, qos); - - await Task.Delay(100); - await subscriber.DisconnectAsync(); - await subscriber.ConnectAsync(subscriber.Options); - await Task.Delay(100); - - var res = await pub; - - Assert.AreEqual(MqttClientPublishReasonCode.Success, res.ReasonCode); - Assert.AreEqual(2, count); - } - } - - [TestMethod] - public async Task Session_Takeover() + var client = await testEnvironment.ConnectClient(); + client.ApplicationMessageReceivedAsync += e => { - using (var testEnvironment = CreateTestEnvironment()) - { - await testEnvironment.StartServer(); + receivedPayload = e.ApplicationMessage.ConvertPayloadToString(); + return CompletedTask.Instance; + }; - var options = new MqttClientOptionsBuilder().WithCleanSession(false) - .WithProtocolVersion(MqttProtocolVersion.V500) // Disconnect reason is only available in MQTT 5+ - .WithClientId("a"); + var subscribeResult = await client.SubscribeAsync("x"); - var client1 = await testEnvironment.ConnectClient(options); - await Task.Delay(500); + Assert.AreEqual(MqttClientSubscribeResultCode.GrantedQoS0, subscribeResult.Items.First().ResultCode); - var disconnectReason = MqttClientDisconnectReason.NormalDisconnection; - client1.DisconnectedAsync += c => - { - disconnectReason = c.Reason; - return CompletedTask.Instance; - }; + var client2 = await testEnvironment.ConnectClient(); + await client2.PublishStringAsync("x"); - var client2 = await testEnvironment.ConnectClient(options); - await Task.Delay(500); + await Task.Delay(1000); - Assert.IsFalse(client1.IsConnected); - Assert.IsTrue(client2.IsConnected); + Assert.AreEqual("Hello World", receivedPayload); + } - Assert.AreEqual(MqttClientDisconnectReason.SessionTakenOver, disconnectReason); - } - } + [TestMethod] + public async Task Use_Clean_Session() + { + using var testEnvironment = CreateTestEnvironment(); + await testEnvironment.StartServer(); - [TestMethod] - public async Task Set_Session_Item() + var client = testEnvironment.CreateClient(); + var connectResult = await client.ConnectAsync(new MqttClientOptionsBuilder().WithTcpServer("localhost", testEnvironment.ServerPort).WithCleanSession().Build()); + + // Create the session including the subscription. + var client1 = await testEnvironment.ConnectClient(new MqttClientOptionsBuilder().WithClientId("a").WithCleanSession(false)); + await client1.SubscribeAsync("x"); + await client1.DisconnectAsync(); + await Task.Delay(500); + + Assert.IsFalse(connectResult.IsSessionPresent); + } + + [TestMethod] + public async Task Will_Message_Do_Not_Send_On_Takeover() + { + using var testEnvironment = CreateTestEnvironment(); + var receivedMessagesCount = 0; + + await testEnvironment.StartServer(); + + // C1 will receive the last will! + var c1 = await testEnvironment.ConnectClient(); + c1.ApplicationMessageReceivedAsync += _ => { - using (var testEnvironment = CreateTestEnvironment()) - { - var server = await testEnvironment.StartServer(); + Interlocked.Increment(ref receivedMessagesCount); + return CompletedTask.Instance; + }; - server.ValidatingConnectionAsync += e => - { - // Don't validate anything. Just set some session items. - e.SessionItems["can_subscribe_x"] = true; - e.SessionItems["default_payload"] = "Hello World"; + await c1.SubscribeAsync(new MqttTopicFilterBuilder().WithTopic("#").Build()); - return CompletedTask.Instance; - }; + // C2 has the last will defined. + var clientOptions = new MqttClientOptionsBuilder().WithWillTopic("My/last/will").WithClientId("WillOwner"); - server.InterceptingSubscriptionAsync += e => - { - if (e.TopicFilter.Topic == "x") - { - if (e.SessionItems["can_subscribe_x"] as bool? == false) - { - e.Response.ReasonCode = MqttSubscribeReasonCode.ImplementationSpecificError; - } - } + await testEnvironment.ConnectClient(clientOptions); - return CompletedTask.Instance; - }; + // C3 will do the connection takeover. + await testEnvironment.ConnectClient(clientOptions); - server.InterceptingPublishAsync += e => - { - e.ApplicationMessage.PayloadSegment = new ArraySegment(Encoding.UTF8.GetBytes(e.SessionItems["default_payload"] as string ?? string.Empty)); - return CompletedTask.Instance; - }; + await Task.Delay(1000); - string receivedPayload = null; + Assert.AreEqual(0, receivedMessagesCount); + } - var client = await testEnvironment.ConnectClient(); - client.ApplicationMessageReceivedAsync += e => - { - receivedPayload = e.ApplicationMessage.ConvertPayloadToString(); - return CompletedTask.Instance; - }; - - var subscribeResult = await client.SubscribeAsync("x"); - - Assert.AreEqual(MqttClientSubscribeResultCode.GrantedQoS0, subscribeResult.Items.First().ResultCode); - - var client2 = await testEnvironment.ConnectClient(); - await client2.PublishStringAsync("x"); - - await Task.Delay(1000); - - Assert.AreEqual("Hello World", receivedPayload); - } - } - - [TestMethod] - public async Task Use_Clean_Session() + static async Task ConnectAndSubscribe(TestEnvironment testEnvironment, MqttClientOptionsBuilder options, Action onReceive) + { + try { - using (var testEnvironment = CreateTestEnvironment()) + var client = await testEnvironment.ConnectClient(options).ConfigureAwait(false); + + client.ApplicationMessageReceivedAsync += _ => { - await testEnvironment.StartServer(); + onReceive(); + return CompletedTask.Instance; + }; - var client = testEnvironment.CreateClient(); - var connectResult = await client.ConnectAsync(new MqttClientOptionsBuilder().WithTcpServer("localhost", testEnvironment.ServerPort).WithCleanSession().Build()); + using var timeout = new CancellationTokenSource(TimeSpan.FromSeconds(30)); + await client.SubscribeAsync("aaa", MqttQualityOfServiceLevel.AtMostOnce, timeout.Token).ConfigureAwait(false); - // Create the session including the subscription. - var client1 = await testEnvironment.ConnectClient(new MqttClientOptionsBuilder().WithClientId("a").WithCleanSession(false)); - await client1.SubscribeAsync("x"); - await client1.DisconnectAsync(); - await Task.Delay(500); - - Assert.IsFalse(connectResult.IsSessionPresent); - } + return client; } - - [TestMethod] - public async Task Will_Message_Do_Not_Send_On_Takeover() + catch { - using (var testEnvironment = CreateTestEnvironment()) - { - var receivedMessagesCount = 0; - - await testEnvironment.StartServer(); - - // C1 will receive the last will! - var c1 = await testEnvironment.ConnectClient(); - c1.ApplicationMessageReceivedAsync += e => - { - Interlocked.Increment(ref receivedMessagesCount); - return CompletedTask.Instance; - }; - - await c1.SubscribeAsync(new MqttTopicFilterBuilder().WithTopic("#").Build()); - - // C2 has the last will defined. - var clientOptions = new MqttClientOptionsBuilder().WithWillTopic("My/last/will").WithClientId("WillOwner"); - - await testEnvironment.ConnectClient(clientOptions); - - // C3 will do the connection takeover. - await testEnvironment.ConnectClient(clientOptions); - - await Task.Delay(1000); - - Assert.AreEqual(0, receivedMessagesCount); - } - } - - static async Task ConnectAndSubscribe(TestEnvironment testEnvironment, MqttClientOptionsBuilder options, Action onReceive) - { - try - { - var client = await testEnvironment.ConnectClient(options).ConfigureAwait(false); - - client.ApplicationMessageReceivedAsync += e => - { - onReceive(); - return CompletedTask.Instance; - }; - - using (var timeout = new CancellationTokenSource(TimeSpan.FromSeconds(30))) - { - await client.SubscribeAsync("aaa", MqttQualityOfServiceLevel.AtMostOnce, timeout.Token).ConfigureAwait(false); - } - - return client; - } - catch (Exception) - { - return null; - } + return null; } } } \ No newline at end of file diff --git a/Source/MQTTnet.Tests/Server/Shared_Subscriptions_Tests.cs b/Source/MQTTnet.Tests/Server/Shared_Subscriptions_Tests.cs index c1a39289..f2f5fe55 100644 --- a/Source/MQTTnet.Tests/Server/Shared_Subscriptions_Tests.cs +++ b/Source/MQTTnet.Tests/Server/Shared_Subscriptions_Tests.cs @@ -7,43 +7,39 @@ using System.Threading.Tasks; using Microsoft.VisualStudio.TestTools.UnitTesting; using MQTTnet.Formatter; -namespace MQTTnet.Tests.Server +namespace MQTTnet.Tests.Server; + +// ReSharper disable InconsistentNaming +[TestClass] +public sealed class Shared_Subscriptions_Tests : BaseTestClass { - [TestClass] - public sealed class Shared_Subscriptions_Tests : BaseTestClass + [TestMethod] + public async Task Server_Reports_Shared_Subscriptions_Not_Supported() { - [TestMethod] - public async Task Server_Reports_Shared_Subscriptions_Not_Supported() - { - using (var testEnvironment = CreateTestEnvironment(MqttProtocolVersion.V500)) - { - await testEnvironment.StartServer(); + using var testEnvironment = CreateTestEnvironment(MqttProtocolVersion.V500); + await testEnvironment.StartServer(); - var client = testEnvironment.CreateClient(); - var connectResult = await client.ConnectAsync(testEnvironment.ClientFactory.CreateClientOptionsBuilder() - .WithProtocolVersion(MqttProtocolVersion.V500) - .WithTcpServer("127.0.0.1", testEnvironment.ServerPort).Build()); + var client = testEnvironment.CreateClient(); + var connectResult = await client.ConnectAsync(testEnvironment.ClientFactory.CreateClientOptionsBuilder() + .WithProtocolVersion(MqttProtocolVersion.V500) + .WithTcpServer("127.0.0.1", testEnvironment.ServerPort).Build()); - Assert.IsFalse(connectResult.SharedSubscriptionAvailable); - } - } + Assert.IsFalse(connectResult.SharedSubscriptionAvailable); + } - [TestMethod] - public async Task Subscription_Of_Shared_Subscription_Is_Denied() - { - using (var testEnvironment = CreateTestEnvironment(MqttProtocolVersion.V500)) - { - await testEnvironment.StartServer(); + [TestMethod] + public async Task Subscription_Of_Shared_Subscription_Is_Denied() + { + using var testEnvironment = CreateTestEnvironment(MqttProtocolVersion.V500); + await testEnvironment.StartServer(); - var client = testEnvironment.CreateClient(); - await client.ConnectAsync(testEnvironment.ClientFactory.CreateClientOptionsBuilder() - .WithProtocolVersion(MqttProtocolVersion.V500) - .WithTcpServer("127.0.0.1", testEnvironment.ServerPort).Build()); + var client = testEnvironment.CreateClient(); + await client.ConnectAsync(testEnvironment.ClientFactory.CreateClientOptionsBuilder() + .WithProtocolVersion(MqttProtocolVersion.V500) + .WithTcpServer("127.0.0.1", testEnvironment.ServerPort).Build()); - var subscribeResult = await client.SubscribeAsync("$share/A"); + var subscribeResult = await client.SubscribeAsync("$share/A"); - Assert.AreEqual(MqttClientSubscribeResultCode.SharedSubscriptionsNotSupported, subscribeResult.Items.First().ResultCode); - } - } + Assert.AreEqual(MqttClientSubscribeResultCode.SharedSubscriptionsNotSupported, subscribeResult.Items.First().ResultCode); } } \ No newline at end of file diff --git a/Source/MQTTnet.Tests/Server/Status_Tests.cs b/Source/MQTTnet.Tests/Server/Status_Tests.cs index ef9419eb..81670547 100644 --- a/Source/MQTTnet.Tests/Server/Status_Tests.cs +++ b/Source/MQTTnet.Tests/Server/Status_Tests.cs @@ -12,189 +12,178 @@ using MQTTnet.Tests.Mockups; namespace MQTTnet.Tests.Server; +// ReSharper disable InconsistentNaming [TestClass] public sealed class Status_Tests : BaseTestClass { [TestMethod] public async Task Disconnect_Client() { - using (var testEnvironment = new TestEnvironment(TestContext)) - { - var server = await testEnvironment.StartServer(); + using var testEnvironment = new TestEnvironment(TestContext); + var server = await testEnvironment.StartServer(); - var c1 = await testEnvironment.ConnectClient(new MqttClientOptionsBuilder().WithClientId("client1")); + var c1 = await testEnvironment.ConnectClient(new MqttClientOptionsBuilder().WithClientId("client1")); - await Task.Delay(1000); + await Task.Delay(1000); - var clientStatus = await server.GetClientsAsync(); + var clientStatus = await server.GetClientsAsync(); - Assert.AreEqual(1, clientStatus.Count); - Assert.IsTrue(clientStatus.Any(s => s.Id == c1.Options.ClientId)); + Assert.AreEqual(1, clientStatus.Count); + Assert.IsTrue(clientStatus.Any(s => s.Id == c1.Options.ClientId)); - await clientStatus.First().DisconnectAsync(); + await clientStatus[0].DisconnectAsync(); - await Task.Delay(500); + await Task.Delay(500); - Assert.IsFalse(c1.IsConnected); + Assert.IsFalse(c1.IsConnected); - clientStatus = await server.GetClientsAsync(); + clientStatus = await server.GetClientsAsync(); - Assert.AreEqual(0, clientStatus.Count); - } + Assert.AreEqual(0, clientStatus.Count); } [TestMethod] public async Task Keep_Persistent_Session_Version311() { - using (var testEnvironment = new TestEnvironment(TestContext)) - { - var server = await testEnvironment.StartServer(o => o.WithPersistentSessions()); + using var testEnvironment = new TestEnvironment(TestContext); + var server = await testEnvironment.StartServer(o => o.WithPersistentSessions()); - var c1 = await testEnvironment.ConnectClient( - new MqttClientOptionsBuilder().WithClientId("client1").WithCleanSession(false).WithProtocolVersion(MqttProtocolVersion.V311)); - var c2 = await testEnvironment.ConnectClient( - new MqttClientOptionsBuilder().WithClientId("client2").WithCleanSession(false).WithProtocolVersion(MqttProtocolVersion.V311)); + var c1 = await testEnvironment.ConnectClient( + new MqttClientOptionsBuilder().WithClientId("client1").WithCleanSession(false).WithProtocolVersion(MqttProtocolVersion.V311)); + var c2 = await testEnvironment.ConnectClient( + new MqttClientOptionsBuilder().WithClientId("client2").WithCleanSession(false).WithProtocolVersion(MqttProtocolVersion.V311)); - await c1.DisconnectAsync(); + await c1.DisconnectAsync(); - await LongTestDelay(); + await LongTestDelay(); - var clientStatus = await server.GetClientsAsync(); - var sessionStatus = await server.GetSessionsAsync(); + var clientStatus = await server.GetClientsAsync(); + var sessionStatus = await server.GetSessionsAsync(); - Assert.AreEqual(1, clientStatus.Count); - Assert.AreEqual(2, sessionStatus.Count); + Assert.AreEqual(1, clientStatus.Count); + Assert.AreEqual(2, sessionStatus.Count); - await c2.DisconnectAsync(); + await c2.DisconnectAsync(); - await LongTestDelay(); + await LongTestDelay(); - clientStatus = await server.GetClientsAsync(); - sessionStatus = await server.GetSessionsAsync(); + clientStatus = await server.GetClientsAsync(); + sessionStatus = await server.GetSessionsAsync(); - Assert.AreEqual(0, clientStatus.Count); - Assert.AreEqual(2, sessionStatus.Count); - } + Assert.AreEqual(0, clientStatus.Count); + Assert.AreEqual(2, sessionStatus.Count); } [TestMethod] public async Task Keep_Persistent_Session_Version500() { - using (var testEnvironment = new TestEnvironment(TestContext)) - { - var server = await testEnvironment.StartServer(o => o.WithPersistentSessions()); + using var testEnvironment = new TestEnvironment(TestContext); + var server = await testEnvironment.StartServer(o => o.WithPersistentSessions()); - var c1 = await testEnvironment.ConnectClient( - new MqttClientOptionsBuilder().WithClientId("client1").WithCleanSession(false).WithProtocolVersion(MqttProtocolVersion.V500)); - var c2 = await testEnvironment.ConnectClient( - new MqttClientOptionsBuilder().WithClientId("client2").WithCleanSession(false).WithProtocolVersion(MqttProtocolVersion.V500)); + var c1 = await testEnvironment.ConnectClient( + new MqttClientOptionsBuilder().WithClientId("client1").WithCleanSession(false).WithProtocolVersion(MqttProtocolVersion.V500)); + var c2 = await testEnvironment.ConnectClient( + new MqttClientOptionsBuilder().WithClientId("client2").WithCleanSession(false).WithProtocolVersion(MqttProtocolVersion.V500)); - // The session expiry interval is mandatory for MQTT5.0.0 in order keep session! - await c1.DisconnectAsync(sessionExpiryInterval: 60); + // The session expiry interval is mandatory for MQTT5.0.0 in order keep session! + await c1.DisconnectAsync(sessionExpiryInterval: 60); - await LongTestDelay(); + await LongTestDelay(); - var clientStatus = await server.GetClientsAsync(); - var sessionStatus = await server.GetSessionsAsync(); + var clientStatus = await server.GetClientsAsync(); + var sessionStatus = await server.GetSessionsAsync(); - Assert.AreEqual(1, clientStatus.Count); - Assert.AreEqual(2, sessionStatus.Count); + Assert.AreEqual(1, clientStatus.Count); + Assert.AreEqual(2, sessionStatus.Count); - // The session expiry interval is mandatory for MQTT5.0.0 in order keep session! - await c2.DisconnectAsync(sessionExpiryInterval: 60); + // The session expiry interval is mandatory for MQTT5.0.0 in order keep session! + await c2.DisconnectAsync(sessionExpiryInterval: 60); - await LongTestDelay(); + await LongTestDelay(); - clientStatus = await server.GetClientsAsync(); - sessionStatus = await server.GetSessionsAsync(); + clientStatus = await server.GetClientsAsync(); + sessionStatus = await server.GetSessionsAsync(); - Assert.AreEqual(0, clientStatus.Count); - Assert.AreEqual(2, sessionStatus.Count); - } + Assert.AreEqual(0, clientStatus.Count); + Assert.AreEqual(2, sessionStatus.Count); } [TestMethod] public async Task Show_Client_And_Session_Statistics() { - using (var testEnvironment = new TestEnvironment(TestContext)) - { - var server = await testEnvironment.StartServer(); + using var testEnvironment = new TestEnvironment(TestContext); + var server = await testEnvironment.StartServer(); - var c1 = await testEnvironment.ConnectClient(new MqttClientOptionsBuilder().WithClientId("client1")); - var c2 = await testEnvironment.ConnectClient(new MqttClientOptionsBuilder().WithClientId("client2")); + var c1 = await testEnvironment.ConnectClient(new MqttClientOptionsBuilder().WithClientId("client1")); + var c2 = await testEnvironment.ConnectClient(new MqttClientOptionsBuilder().WithClientId("client2")); - await Task.Delay(500); + await Task.Delay(500); - var clientStatus = await server.GetClientsAsync(); - var sessionStatus = await server.GetSessionsAsync(); + var clientStatus = await server.GetClientsAsync(); + var sessionStatus = await server.GetSessionsAsync(); - Assert.AreEqual(2, clientStatus.Count); - Assert.AreEqual(2, sessionStatus.Count); + Assert.AreEqual(2, clientStatus.Count); + Assert.AreEqual(2, sessionStatus.Count); - Assert.IsTrue(clientStatus.Any(s => s.Id == c1.Options.ClientId)); - Assert.IsTrue(clientStatus.Any(s => s.Id == c2.Options.ClientId)); + Assert.IsTrue(clientStatus.Any(s => s.Id == c1.Options.ClientId)); + Assert.IsTrue(clientStatus.Any(s => s.Id == c2.Options.ClientId)); - await c1.DisconnectAsync(); - await c2.DisconnectAsync(); + await c1.DisconnectAsync(); + await c2.DisconnectAsync(); - await Task.Delay(500); + await Task.Delay(500); - clientStatus = await server.GetClientsAsync(); - sessionStatus = await server.GetSessionsAsync(); + clientStatus = await server.GetClientsAsync(); + sessionStatus = await server.GetSessionsAsync(); - Assert.AreEqual(0, clientStatus.Count); - Assert.AreEqual(0, sessionStatus.Count); - } + Assert.AreEqual(0, clientStatus.Count); + Assert.AreEqual(0, sessionStatus.Count); } [TestMethod] public async Task Track_Sent_Application_Messages() { - using (var testEnvironment = new TestEnvironment(TestContext)) + using var testEnvironment = new TestEnvironment(TestContext); + var server = await testEnvironment.StartServer(new MqttServerOptionsBuilder().WithPersistentSessions()); + + var c1 = await testEnvironment.ConnectClient(); + + for (var i = 1; i < 25; i++) { - var server = await testEnvironment.StartServer(new MqttServerOptionsBuilder().WithPersistentSessions()); + await c1.PublishStringAsync("a"); + await Task.Delay(50); - var c1 = await testEnvironment.ConnectClient(); - - for (var i = 1; i < 25; i++) - { - await c1.PublishStringAsync("a"); - await Task.Delay(50); - - var clientStatus = await server.GetClientsAsync(); - Assert.AreEqual(i, clientStatus.First().SentApplicationMessagesCount); - Assert.AreEqual(0, clientStatus.First().ReceivedApplicationMessagesCount); - } + var clientStatus = await server.GetClientsAsync(); + Assert.AreEqual(i, clientStatus[0].SentApplicationMessagesCount); + Assert.AreEqual(0, clientStatus[0].ReceivedApplicationMessagesCount); } } [TestMethod] public async Task Track_Sent_Packets() { - using (var testEnvironment = new TestEnvironment(TestContext)) + using var testEnvironment = new TestEnvironment(TestContext); + var server = await testEnvironment.StartServer(new MqttServerOptionsBuilder().WithPersistentSessions()); + + var c1 = await testEnvironment.ConnectClient(new MqttClientOptionsBuilder().WithNoKeepAlive()); + + for (var i = 1; i < 25; i++) { - var server = await testEnvironment.StartServer(new MqttServerOptionsBuilder().WithPersistentSessions()); + // At most once will send one packet to the client and the server will reply + // with an additional ACK packet. + await c1.PublishStringAsync("a", string.Empty, MqttQualityOfServiceLevel.AtLeastOnce); - var c1 = await testEnvironment.ConnectClient(new MqttClientOptionsBuilder().WithNoKeepAlive()); + await Task.Delay(500); - for (var i = 1; i < 25; i++) - { - // At most once will send one packet to the client and the server will reply - // with an additional ACK packet. - await c1.PublishStringAsync("a", string.Empty, MqttQualityOfServiceLevel.AtLeastOnce); + var clientStatus = await server.GetClientsAsync(); - await Task.Delay(500); + Assert.AreEqual(i, clientStatus[0].SentApplicationMessagesCount, "SAMC invalid!"); - var clientStatus = await server.GetClientsAsync(); + // + 1 because CONNECT is also counted. + Assert.AreEqual(i + 1, clientStatus[0].SentPacketsCount, "SPC invalid!"); - Assert.AreEqual(i, clientStatus.First().SentApplicationMessagesCount, "SAMC invalid!"); - - // + 1 because CONNECT is also counted. - Assert.AreEqual(i + 1, clientStatus.First().SentPacketsCount, "SPC invalid!"); - - // +2 because ConnACK + PubAck package is already counted. - Assert.AreEqual(i + 2, clientStatus.First().ReceivedPacketsCount, "RPC invalid!"); - } + // +2 because ConnACK + PubAck package is already counted. + Assert.AreEqual(i + 2, clientStatus[0].ReceivedPacketsCount, "RPC invalid!"); } } } \ No newline at end of file diff --git a/Source/MQTTnet.Tests/Server/Subscribe_Tests.cs b/Source/MQTTnet.Tests/Server/Subscribe_Tests.cs index 9dd48c31..dfd95765 100644 --- a/Source/MQTTnet.Tests/Server/Subscribe_Tests.cs +++ b/Source/MQTTnet.Tests/Server/Subscribe_Tests.cs @@ -15,420 +15,396 @@ using MQTTnet.Protocol; using MQTTnet.Server; using MQTTnet.Tests.Helpers; -namespace MQTTnet.Tests.Server +namespace MQTTnet.Tests.Server; + +// ReSharper disable InconsistentNaming +[TestClass] +public sealed class Subscribe_Tests : BaseTestClass { - [TestClass] - public sealed class Subscribe_Tests : BaseTestClass + [TestMethod] + [DataRow("A", "A", true)] + [DataRow("A", "B", false)] + [DataRow("A", "#", true)] + [DataRow("A", "+", true)] + [DataRow("A/B", "A/B", true)] + [DataRow("A/B", "A/+", true)] + [DataRow("A/B", "A/#", true)] + [DataRow("A/B/C", "A/B/C", true)] + [DataRow("A/B/C", "A/+/C", true)] + [DataRow("A/B/C", "A/+/+", true)] + [DataRow("A/B/C", "A/+/#", true)] + [DataRow("A/B/C/D", "A/B/C/D", true)] + [DataRow("A/B/C/D", "A/+/C/+", true)] + [DataRow("A/B/C/D", "A/+/C/#", true)] + [DataRow("A/B/C", "A/B/+", true)] + [DataRow("A/B1/B2/C", "A/+/C", false)] + public async Task Subscription_Roundtrip(string topic, string filter, bool shouldWork) { - [TestMethod] - [DataRow("A", "A", true)] - [DataRow("A", "B", false)] - [DataRow("A", "#", true)] - [DataRow("A", "+", true)] - [DataRow("A/B", "A/B", true)] - [DataRow("A/B", "A/+", true)] - [DataRow("A/B", "A/#", true)] - [DataRow("A/B/C", "A/B/C", true)] - [DataRow("A/B/C", "A/+/C", true)] - [DataRow("A/B/C", "A/+/+", true)] - [DataRow("A/B/C", "A/+/#", true)] - [DataRow("A/B/C/D", "A/B/C/D", true)] - [DataRow("A/B/C/D", "A/+/C/+", true)] - [DataRow("A/B/C/D", "A/+/C/#", true)] - [DataRow("A/B/C", "A/B/+", true)] - [DataRow("A/B1/B2/C", "A/+/C", false)] - public async Task Subscription_Roundtrip(string topic, string filter, bool shouldWork) + using var testEnvironment = CreateTestEnvironment(); + await testEnvironment.StartServer().ConfigureAwait(false); + + var receiver = await testEnvironment.ConnectClient(); + await receiver.SubscribeAsync(filter).ConfigureAwait(false); + var receivedMessages = receiver.TrackReceivedMessages(); + + var sender = await testEnvironment.ConnectClient(); + await sender.PublishStringAsync(topic, "PAYLOAD").ConfigureAwait(false); + + await LongTestDelay().ConfigureAwait(false); + + if (shouldWork) { - using (var testEnvironment = CreateTestEnvironment()) - { - await testEnvironment.StartServer().ConfigureAwait(false); - - var receiver = await testEnvironment.ConnectClient(); - await receiver.SubscribeAsync(filter).ConfigureAwait(false); - var receivedMessages = receiver.TrackReceivedMessages(); - - var sender = await testEnvironment.ConnectClient(); - await sender.PublishStringAsync(topic, "PAYLOAD").ConfigureAwait(false); - - await LongTestDelay().ConfigureAwait(false); - - if (shouldWork) - { - Assert.AreEqual(1, receivedMessages.Count, message: "The filter should work!"); - } - else - { - Assert.AreEqual(0, receivedMessages.Count, message: "The filter should not work!"); - } - } + Assert.AreEqual(1, receivedMessages.Count, message: "The filter should work!"); } - - [TestMethod] - public async Task Deny_Invalid_Topic() + else { - using (var testEnvironment = CreateTestEnvironment(MqttProtocolVersion.V500)) - { - var server = await testEnvironment.StartServer(); - - server.InterceptingSubscriptionAsync += e => - { - if (e.TopicFilter.Topic == "not_allowed_topic") - { - e.Response.ReasonCode = MqttSubscribeReasonCode.TopicFilterInvalid; - } - - return CompletedTask.Instance; - }; - - var client = await testEnvironment.ConnectClient(); - - var subscribeResult = await client.SubscribeAsync("allowed_topic"); - Assert.AreEqual(MqttClientSubscribeResultCode.GrantedQoS0, subscribeResult.Items.First().ResultCode); - - subscribeResult = await client.SubscribeAsync("not_allowed_topic"); - Assert.AreEqual(MqttClientSubscribeResultCode.TopicFilterInvalid, subscribeResult.Items.First().ResultCode); - } - } - - [TestMethod] - public async Task Intercept_Subscribe_With_User_Properties() - { - using (var testEnvironment = CreateTestEnvironment(MqttProtocolVersion.V500)) - { - var server = await testEnvironment.StartServer(); - - InterceptingSubscriptionEventArgs eventArgs = null; - server.InterceptingSubscriptionAsync += e => - { - eventArgs = e; - return CompletedTask.Instance; - }; - - var client = await testEnvironment.ConnectClient(); - - var subscribeOptions = testEnvironment.ClientFactory.CreateSubscribeOptionsBuilder().WithTopicFilter("X").WithUserProperty("A", "1").Build(); - await client.SubscribeAsync(subscribeOptions); - - CollectionAssert.AreEqual(subscribeOptions.UserProperties.ToList(), eventArgs.UserProperties); - } - } - - [TestMethod] - [ExpectedException(typeof(MqttClientDisconnectedException))] - public async Task Disconnect_While_Subscribing() - { - using (var testEnvironment = CreateTestEnvironment()) - { - var server = await testEnvironment.StartServer(); - - // The client will be disconnect directly after subscribing! - server.ClientSubscribedTopicAsync += ev => server.DisconnectClientAsync(ev.ClientId, MqttDisconnectReasonCode.NormalDisconnection); - - var client = await testEnvironment.ConnectClient(); - await client.SubscribeAsync("#"); - } - } - - [TestMethod] - public async Task Enqueue_Message_After_Subscription() - { - using (var testEnvironment = CreateTestEnvironment()) - { - var server = await testEnvironment.StartServer(); - - server.ClientSubscribedTopicAsync += e => - { - server.InjectApplicationMessage(new InjectedMqttApplicationMessage(new MqttApplicationMessageBuilder().WithTopic("test_topic").Build())); - return CompletedTask.Instance; - }; - - var client = await testEnvironment.ConnectClient(); - var receivedMessages = testEnvironment.CreateApplicationMessageHandler(client); - - await client.SubscribeAsync("test_topic"); - - await LongTestDelay(); - - Assert.AreEqual(1, receivedMessages.ReceivedEventArgs.Count); - } - } - - [TestMethod] - public async Task Intercept_Subscription() - { - using (var testEnvironment = CreateTestEnvironment()) - { - var server = await testEnvironment.StartServer(); - - server.InterceptingSubscriptionAsync += e => - { - // Set the topic to "a" regards what the client wants to subscribe. - e.TopicFilter.Topic = "a"; - return CompletedTask.Instance; - }; - - var topicAReceived = false; - var topicBReceived = false; - - var client = await testEnvironment.ConnectClient(); - client.ApplicationMessageReceivedAsync += e => - { - if (e.ApplicationMessage.Topic == "a") - { - topicAReceived = true; - } - else if (e.ApplicationMessage.Topic == "b") - { - topicBReceived = true; - } - - return CompletedTask.Instance; - }; - - await client.SubscribeAsync("b"); - - await client.PublishStringAsync("a"); - - await Task.Delay(500); - - Assert.IsTrue(topicAReceived); - Assert.IsFalse(topicBReceived); - } - } - - [TestMethod] - public async Task Response_Contains_Equal_Reason_Codes() - { - using (var testEnvironment = CreateTestEnvironment()) - { - await testEnvironment.StartServer(); - var client = await testEnvironment.ConnectClient(); - - var subscribeOptions = new MqttClientSubscribeOptionsBuilder().WithTopicFilter("a") - .WithTopicFilter("b", MqttQualityOfServiceLevel.AtLeastOnce) - .WithTopicFilter("c", MqttQualityOfServiceLevel.ExactlyOnce) - .WithTopicFilter("d") - .Build(); - - var response = await client.SubscribeAsync(subscribeOptions); - - Assert.AreEqual(subscribeOptions.TopicFilters.Count, response.Items.Count); - } - } - - [TestMethod] - public async Task Subscribe_Lots_In_Multiple_Requests() - { - using (var testEnvironment = CreateTestEnvironment()) - { - var receivedMessagesCount = 0; - - await testEnvironment.StartServer(); - - var c1 = await testEnvironment.ConnectClient(); - c1.ApplicationMessageReceivedAsync += e => - { - Interlocked.Increment(ref receivedMessagesCount); - return CompletedTask.Instance; - }; - - for (var i = 0; i < 500; i++) - { - var so = new MqttClientSubscribeOptionsBuilder().WithTopicFilter(i.ToString()).Build(); - - await c1.SubscribeAsync(so).ConfigureAwait(false); - - await Task.Delay(10); - } - - var c2 = await testEnvironment.ConnectClient(); - - var messageBuilder = new MqttApplicationMessageBuilder(); - for (var i = 0; i < 500; i++) - { - messageBuilder.WithTopic(i.ToString()); - - await c2.PublishAsync(messageBuilder.Build()).ConfigureAwait(false); - - await Task.Delay(10); - } - - SpinWait.SpinUntil(() => receivedMessagesCount == 500, 5000); - - Assert.AreEqual(500, receivedMessagesCount); - } - } - - [TestMethod] - public async Task Subscribe_Lots_In_Single_Request() - { - using (var testEnvironment = CreateTestEnvironment()) - { - var receivedMessagesCount = 0; - - await testEnvironment.StartServer(); - - var c1 = await testEnvironment.ConnectClient(); - c1.ApplicationMessageReceivedAsync += e => - { - Interlocked.Increment(ref receivedMessagesCount); - return CompletedTask.Instance; - }; - - var optionsBuilder = new MqttClientSubscribeOptionsBuilder(); - for (var i = 0; i < 500; i++) - { - optionsBuilder.WithTopicFilter(i.ToString()); - } - - await c1.SubscribeAsync(optionsBuilder.Build()).ConfigureAwait(false); - - var c2 = await testEnvironment.ConnectClient(); - - var messageBuilder = new MqttApplicationMessageBuilder(); - for (var i = 0; i < 500; i++) - { - messageBuilder.WithTopic(i.ToString()); - - await c2.PublishAsync(messageBuilder.Build()).ConfigureAwait(false); - } - - SpinWait.SpinUntil(() => receivedMessagesCount == 500, TimeSpan.FromSeconds(20)); - - Assert.AreEqual(500, receivedMessagesCount); - } - } - - [TestMethod] - public async Task Subscribe_Multiple_In_Multiple_Request() - { - using (var testEnvironment = CreateTestEnvironment()) - { - var receivedMessagesCount = 0; - - await testEnvironment.StartServer(); - - var c1 = await testEnvironment.ConnectClient(); - c1.ApplicationMessageReceivedAsync += e => - { - Interlocked.Increment(ref receivedMessagesCount); - return CompletedTask.Instance; - }; - - await c1.SubscribeAsync(new MqttClientSubscribeOptionsBuilder().WithTopicFilter("a").Build()); - - await c1.SubscribeAsync(new MqttClientSubscribeOptionsBuilder().WithTopicFilter("b").Build()); - - await c1.SubscribeAsync(new MqttClientSubscribeOptionsBuilder().WithTopicFilter("c").Build()); - - var c2 = await testEnvironment.ConnectClient(); - - await c2.PublishStringAsync("a"); - await Task.Delay(100); - Assert.AreEqual(receivedMessagesCount, 1); - - await c2.PublishStringAsync("b"); - await Task.Delay(100); - Assert.AreEqual(receivedMessagesCount, 2); - - await c2.PublishStringAsync("c"); - await Task.Delay(100); - Assert.AreEqual(receivedMessagesCount, 3); - } - } - - [TestMethod] - public async Task Subscribe_Multiple_In_Single_Request() - { - using (var testEnvironment = CreateTestEnvironment()) - { - var receivedMessagesCount = 0; - - await testEnvironment.StartServer(); - - var c1 = await testEnvironment.ConnectClient(); - c1.ApplicationMessageReceivedAsync += e => - { - Interlocked.Increment(ref receivedMessagesCount); - return CompletedTask.Instance; - }; - - await c1.SubscribeAsync(new MqttClientSubscribeOptionsBuilder().WithTopicFilter("a").WithTopicFilter("b").WithTopicFilter("c").Build()); - - var c2 = await testEnvironment.ConnectClient(); - - await c2.PublishStringAsync("a"); - await Task.Delay(100); - Assert.AreEqual(receivedMessagesCount, 1); - - await c2.PublishStringAsync("b"); - await Task.Delay(100); - Assert.AreEqual(receivedMessagesCount, 2); - - await c2.PublishStringAsync("c"); - await Task.Delay(100); - Assert.AreEqual(receivedMessagesCount, 3); - } - } - - [TestMethod] - public async Task Subscribe_Unsubscribe() - { - using (var testEnvironment = CreateTestEnvironment()) - { - var receivedMessagesCount = 0; - - var server = await testEnvironment.StartServer(); - - var c1 = await testEnvironment.ConnectClient(new MqttClientOptionsBuilder().WithClientId("c1")); - c1.ApplicationMessageReceivedAsync += e => - { - Interlocked.Increment(ref receivedMessagesCount); - return CompletedTask.Instance; - }; - - var c2 = await testEnvironment.ConnectClient(new MqttClientOptionsBuilder().WithClientId("c2")); - - var message = new MqttApplicationMessageBuilder().WithTopic("a").WithQualityOfServiceLevel(MqttQualityOfServiceLevel.ExactlyOnce).Build(); - await c2.PublishAsync(message); - - await Task.Delay(500); - Assert.AreEqual(0, receivedMessagesCount); - - var subscribeEventCalled = false; - server.ClientSubscribedTopicAsync += e => - { - subscribeEventCalled = e.TopicFilter.Topic == "a" && e.ClientId == c1.Options.ClientId; - return CompletedTask.Instance; - }; - - await c1.SubscribeAsync(new MqttTopicFilter { Topic = "a", QualityOfServiceLevel = MqttQualityOfServiceLevel.AtLeastOnce }); - await Task.Delay(250); - Assert.IsTrue(subscribeEventCalled, "Subscribe event not called."); - - await c2.PublishAsync(message); - await Task.Delay(250); - Assert.AreEqual(1, receivedMessagesCount); - - var unsubscribeEventCalled = false; - server.ClientUnsubscribedTopicAsync += e => - { - unsubscribeEventCalled = e.TopicFilter == "a" && e.ClientId == c1.Options.ClientId; - return CompletedTask.Instance; - }; - - await c1.UnsubscribeAsync("a"); - await Task.Delay(250); - Assert.IsTrue(unsubscribeEventCalled, "Unsubscribe event not called."); - - await c2.PublishAsync(message); - await Task.Delay(500); - Assert.AreEqual(1, receivedMessagesCount); - - await Task.Delay(500); - - Assert.AreEqual(1, receivedMessagesCount); - } + Assert.AreEqual(0, receivedMessages.Count, message: "The filter should not work!"); } } + + [TestMethod] + public async Task Deny_Invalid_Topic() + { + using var testEnvironment = CreateTestEnvironment(MqttProtocolVersion.V500); + var server = await testEnvironment.StartServer(); + + server.InterceptingSubscriptionAsync += e => + { + if (e.TopicFilter.Topic == "not_allowed_topic") + { + e.Response.ReasonCode = MqttSubscribeReasonCode.TopicFilterInvalid; + } + + return CompletedTask.Instance; + }; + + var client = await testEnvironment.ConnectClient(); + + var subscribeResult = await client.SubscribeAsync("allowed_topic"); + Assert.AreEqual(MqttClientSubscribeResultCode.GrantedQoS0, subscribeResult.Items.First().ResultCode); + + subscribeResult = await client.SubscribeAsync("not_allowed_topic"); + Assert.AreEqual(MqttClientSubscribeResultCode.TopicFilterInvalid, subscribeResult.Items.First().ResultCode); + } + + [TestMethod] + public async Task Intercept_Subscribe_With_User_Properties() + { + using var testEnvironment = CreateTestEnvironment(MqttProtocolVersion.V500); + var server = await testEnvironment.StartServer(); + + InterceptingSubscriptionEventArgs eventArgs = null; + server.InterceptingSubscriptionAsync += e => + { + eventArgs = e; + return CompletedTask.Instance; + }; + + var client = await testEnvironment.ConnectClient(); + + var subscribeOptions = testEnvironment.ClientFactory.CreateSubscribeOptionsBuilder().WithTopicFilter("X").WithUserProperty("A", "1").Build(); + await client.SubscribeAsync(subscribeOptions); + + CollectionAssert.AreEqual(subscribeOptions.UserProperties.ToList(), eventArgs.UserProperties); + } + + [TestMethod] + [ExpectedException(typeof(MqttClientDisconnectedException))] + public async Task Disconnect_While_Subscribing() + { + using var testEnvironment = CreateTestEnvironment(); + var server = await testEnvironment.StartServer(); + + // The client will be disconnected directly after subscribing! + server.ClientSubscribedTopicAsync += ev => server.DisconnectClientAsync(ev.ClientId); + + var client = await testEnvironment.ConnectClient(); + await client.SubscribeAsync("#"); + } + + [TestMethod] + public async Task Enqueue_Message_After_Subscription() + { + using var testEnvironment = CreateTestEnvironment(); + var server = await testEnvironment.StartServer(); + + server.ClientSubscribedTopicAsync += _ => + { + server.InjectApplicationMessage(new InjectedMqttApplicationMessage(new MqttApplicationMessageBuilder().WithTopic("test_topic").Build())); + return CompletedTask.Instance; + }; + + var client = await testEnvironment.ConnectClient(); + var receivedMessages = testEnvironment.CreateApplicationMessageHandler(client); + + await client.SubscribeAsync("test_topic"); + + await LongTestDelay(); + + Assert.AreEqual(1, receivedMessages.ReceivedEventArgs.Count); + } + + [TestMethod] + public async Task Intercept_Subscription() + { + using var testEnvironment = CreateTestEnvironment(); + var server = await testEnvironment.StartServer(); + + server.InterceptingSubscriptionAsync += e => + { + // Set the topic to "a" regards what the client wants to subscribe. + e.TopicFilter.Topic = "a"; + return CompletedTask.Instance; + }; + + var topicAReceived = false; + var topicBReceived = false; + + var client = await testEnvironment.ConnectClient(); + client.ApplicationMessageReceivedAsync += e => + { + if (e.ApplicationMessage.Topic == "a") + { + topicAReceived = true; + } + else if (e.ApplicationMessage.Topic == "b") + { + topicBReceived = true; + } + + return CompletedTask.Instance; + }; + + await client.SubscribeAsync("b"); + + await client.PublishStringAsync("a"); + + await Task.Delay(500); + + Assert.IsTrue(topicAReceived); + Assert.IsFalse(topicBReceived); + } + + [TestMethod] + public async Task Response_Contains_Equal_Reason_Codes() + { + using var testEnvironment = CreateTestEnvironment(); + await testEnvironment.StartServer(); + var client = await testEnvironment.ConnectClient(); + + var subscribeOptions = new MqttClientSubscribeOptionsBuilder().WithTopicFilter("a") + .WithTopicFilter("b", MqttQualityOfServiceLevel.AtLeastOnce) + .WithTopicFilter("c", MqttQualityOfServiceLevel.ExactlyOnce) + .WithTopicFilter("d") + .Build(); + + var response = await client.SubscribeAsync(subscribeOptions); + + Assert.AreEqual(subscribeOptions.TopicFilters.Count, response.Items.Count); + } + + [TestMethod] + public async Task Subscribe_Lots_In_Multiple_Requests() + { + using var testEnvironment = CreateTestEnvironment(); + var receivedMessagesCount = 0; + + await testEnvironment.StartServer(); + + var c1 = await testEnvironment.ConnectClient(); + c1.ApplicationMessageReceivedAsync += _ => + { + Interlocked.Increment(ref receivedMessagesCount); + return CompletedTask.Instance; + }; + + for (var i = 0; i < 500; i++) + { + var so = new MqttClientSubscribeOptionsBuilder().WithTopicFilter(i.ToString()).Build(); + + await c1.SubscribeAsync(so).ConfigureAwait(false); + + await Task.Delay(10); + } + + var c2 = await testEnvironment.ConnectClient(); + + var messageBuilder = new MqttApplicationMessageBuilder(); + for (var i = 0; i < 500; i++) + { + messageBuilder.WithTopic(i.ToString()); + + await c2.PublishAsync(messageBuilder.Build()).ConfigureAwait(false); + + await Task.Delay(10); + } + + SpinWait.SpinUntil(() => receivedMessagesCount == 500, 5000); + + Assert.AreEqual(500, receivedMessagesCount); + } + + [TestMethod] + public async Task Subscribe_Lots_In_Single_Request() + { + using var testEnvironment = CreateTestEnvironment(); + var receivedMessagesCount = 0; + + await testEnvironment.StartServer(); + + var c1 = await testEnvironment.ConnectClient(); + c1.ApplicationMessageReceivedAsync += _ => + { + Interlocked.Increment(ref receivedMessagesCount); + return CompletedTask.Instance; + }; + + var optionsBuilder = new MqttClientSubscribeOptionsBuilder(); + for (var i = 0; i < 500; i++) + { + optionsBuilder.WithTopicFilter(i.ToString()); + } + + await c1.SubscribeAsync(optionsBuilder.Build()).ConfigureAwait(false); + + var c2 = await testEnvironment.ConnectClient(); + + var messageBuilder = new MqttApplicationMessageBuilder(); + for (var i = 0; i < 500; i++) + { + messageBuilder.WithTopic(i.ToString()); + + await c2.PublishAsync(messageBuilder.Build()).ConfigureAwait(false); + } + + SpinWait.SpinUntil(() => receivedMessagesCount == 500, TimeSpan.FromSeconds(20)); + + Assert.AreEqual(500, receivedMessagesCount); + } + + [TestMethod] + public async Task Subscribe_Multiple_In_Multiple_Request() + { + using var testEnvironment = CreateTestEnvironment(); + var receivedMessagesCount = 0; + + await testEnvironment.StartServer(); + + var c1 = await testEnvironment.ConnectClient(); + c1.ApplicationMessageReceivedAsync += _ => + { + Interlocked.Increment(ref receivedMessagesCount); + return CompletedTask.Instance; + }; + + await c1.SubscribeAsync(new MqttClientSubscribeOptionsBuilder().WithTopicFilter("a").Build()); + + await c1.SubscribeAsync(new MqttClientSubscribeOptionsBuilder().WithTopicFilter("b").Build()); + + await c1.SubscribeAsync(new MqttClientSubscribeOptionsBuilder().WithTopicFilter("c").Build()); + + var c2 = await testEnvironment.ConnectClient(); + + await c2.PublishStringAsync("a"); + await Task.Delay(100); + Assert.AreEqual(receivedMessagesCount, 1); + + await c2.PublishStringAsync("b"); + await Task.Delay(100); + Assert.AreEqual(receivedMessagesCount, 2); + + await c2.PublishStringAsync("c"); + await Task.Delay(100); + Assert.AreEqual(receivedMessagesCount, 3); + } + + [TestMethod] + public async Task Subscribe_Multiple_In_Single_Request() + { + using var testEnvironment = CreateTestEnvironment(); + var receivedMessagesCount = 0; + + await testEnvironment.StartServer(); + + var c1 = await testEnvironment.ConnectClient(); + c1.ApplicationMessageReceivedAsync += _ => + { + Interlocked.Increment(ref receivedMessagesCount); + return CompletedTask.Instance; + }; + + await c1.SubscribeAsync(new MqttClientSubscribeOptionsBuilder().WithTopicFilter("a").WithTopicFilter("b").WithTopicFilter("c").Build()); + + var c2 = await testEnvironment.ConnectClient(); + + await c2.PublishStringAsync("a"); + await Task.Delay(100); + Assert.AreEqual(receivedMessagesCount, 1); + + await c2.PublishStringAsync("b"); + await Task.Delay(100); + Assert.AreEqual(receivedMessagesCount, 2); + + await c2.PublishStringAsync("c"); + await Task.Delay(100); + Assert.AreEqual(receivedMessagesCount, 3); + } + + [TestMethod] + public async Task Subscribe_Unsubscribe() + { + using var testEnvironment = CreateTestEnvironment(); + var receivedMessagesCount = 0; + + var server = await testEnvironment.StartServer(); + + var c1 = await testEnvironment.ConnectClient(new MqttClientOptionsBuilder().WithClientId("c1")); + c1.ApplicationMessageReceivedAsync += _ => + { + Interlocked.Increment(ref receivedMessagesCount); + return CompletedTask.Instance; + }; + + var c2 = await testEnvironment.ConnectClient(new MqttClientOptionsBuilder().WithClientId("c2")); + + var message = new MqttApplicationMessageBuilder().WithTopic("a").WithQualityOfServiceLevel(MqttQualityOfServiceLevel.ExactlyOnce).Build(); + await c2.PublishAsync(message); + + await Task.Delay(500); + Assert.AreEqual(0, receivedMessagesCount); + + var subscribeEventCalled = false; + server.ClientSubscribedTopicAsync += e => + { + subscribeEventCalled = e.TopicFilter.Topic == "a" && e.ClientId == c1.Options.ClientId; + return CompletedTask.Instance; + }; + + await c1.SubscribeAsync(new MqttTopicFilter { Topic = "a", QualityOfServiceLevel = MqttQualityOfServiceLevel.AtLeastOnce }); + await Task.Delay(250); + Assert.IsTrue(subscribeEventCalled, "Subscribe event not called."); + + await c2.PublishAsync(message); + await Task.Delay(250); + Assert.AreEqual(1, receivedMessagesCount); + + var unsubscribeEventCalled = false; + server.ClientUnsubscribedTopicAsync += e => + { + unsubscribeEventCalled = e.TopicFilter == "a" && e.ClientId == c1.Options.ClientId; + return CompletedTask.Instance; + }; + + await c1.UnsubscribeAsync("a"); + await Task.Delay(250); + Assert.IsTrue(unsubscribeEventCalled, "Unsubscribe event not called."); + + await c2.PublishAsync(message); + await Task.Delay(500); + Assert.AreEqual(1, receivedMessagesCount); + + await Task.Delay(500); + + Assert.AreEqual(1, receivedMessagesCount); + } } \ No newline at end of file diff --git a/Source/MQTTnet.Tests/Server/Subscription_Identifier_Tests.cs b/Source/MQTTnet.Tests/Server/Subscription_Identifier_Tests.cs index 24b370d1..8fd8fed5 100644 --- a/Source/MQTTnet.Tests/Server/Subscription_Identifier_Tests.cs +++ b/Source/MQTTnet.Tests/Server/Subscription_Identifier_Tests.cs @@ -6,87 +6,81 @@ using System.Threading.Tasks; using Microsoft.VisualStudio.TestTools.UnitTesting; using MQTTnet.Formatter; -namespace MQTTnet.Tests.Server +namespace MQTTnet.Tests.Server; + +// ReSharper disable InconsistentNaming +[TestClass] +public sealed class Subscription_Identifier_Tests : BaseTestClass { - [TestClass] - public sealed class Subscription_Identifier_Tests : BaseTestClass + [TestMethod] + public async Task Server_Reports_Subscription_Identifiers_Supported() { - [TestMethod] - public async Task Server_Reports_Subscription_Identifiers_Supported() - { - using (var testEnvironment = CreateTestEnvironment(MqttProtocolVersion.V500)) - { - await testEnvironment.StartServer(); + using var testEnvironment = CreateTestEnvironment(MqttProtocolVersion.V500); + await testEnvironment.StartServer(); - var client = testEnvironment.CreateClient(); - var connectResult = await client.ConnectAsync(testEnvironment.ClientFactory.CreateClientOptionsBuilder() - .WithProtocolVersion(MqttProtocolVersion.V500) - .WithTcpServer("127.0.0.1", testEnvironment.ServerPort).Build()); + var client = testEnvironment.CreateClient(); + var connectResult = await client.ConnectAsync(testEnvironment.ClientFactory.CreateClientOptionsBuilder() + .WithProtocolVersion(MqttProtocolVersion.V500) + .WithTcpServer("127.0.0.1", testEnvironment.ServerPort).Build()); - Assert.IsTrue(connectResult.SubscriptionIdentifiersAvailable); - } - } + Assert.IsTrue(connectResult.SubscriptionIdentifiersAvailable); + } - [TestMethod] - public async Task Subscribe_With_Subscription_Identifier() - { - using (var testEnvironment = CreateTestEnvironment(MqttProtocolVersion.V500)) - { - await testEnvironment.StartServer(); + [TestMethod] + public async Task Subscribe_With_Subscription_Identifier() + { + using var testEnvironment = CreateTestEnvironment(MqttProtocolVersion.V500); + await testEnvironment.StartServer(); - var client1 = await testEnvironment.ConnectClient(); - var applicationMessageHandler = testEnvironment.CreateApplicationMessageHandler(client1); - var topicFilter = testEnvironment.ClientFactory.CreateTopicFilterBuilder().WithTopic("Topic").Build(); - var subscribeOptions = testEnvironment.ClientFactory.CreateSubscribeOptionsBuilder().WithSubscriptionIdentifier(456).WithTopicFilter(topicFilter).Build(); + var client1 = await testEnvironment.ConnectClient(); + var applicationMessageHandler = testEnvironment.CreateApplicationMessageHandler(client1); + var topicFilter = testEnvironment.ClientFactory.CreateTopicFilterBuilder().WithTopic("Topic").Build(); + var subscribeOptions = testEnvironment.ClientFactory.CreateSubscribeOptionsBuilder().WithSubscriptionIdentifier(456).WithTopicFilter(topicFilter).Build(); - await client1.SubscribeAsync(subscribeOptions); - await LongTestDelay(); + await client1.SubscribeAsync(subscribeOptions); + await LongTestDelay(); - applicationMessageHandler.AssertReceivedCountEquals(0); + applicationMessageHandler.AssertReceivedCountEquals(0); - // The client will publish a message where it is itself subscribing to. - await client1.PublishStringAsync("Topic", "Payload", retain: true); - await LongTestDelay(); + // The client will publish a message where it is itself subscribing to. + await client1.PublishStringAsync("Topic", "Payload", retain: true); + await LongTestDelay(); - applicationMessageHandler.AssertReceivedCountEquals(1); + applicationMessageHandler.AssertReceivedCountEquals(1); - applicationMessageHandler.ReceivedEventArgs[0].ApplicationMessage.SubscriptionIdentifiers.Contains(456); - } - } + Assert.IsTrue(applicationMessageHandler.ReceivedEventArgs[0].ApplicationMessage.SubscriptionIdentifiers.Contains(456)); + } - [TestMethod] - public async Task Subscribe_With_Multiple_Subscription_Identifiers() - { - using (var testEnvironment = CreateTestEnvironment(MqttProtocolVersion.V500)) - { - await testEnvironment.StartServer(); + [TestMethod] + public async Task Subscribe_With_Multiple_Subscription_Identifiers() + { + using var testEnvironment = CreateTestEnvironment(MqttProtocolVersion.V500); + await testEnvironment.StartServer(); - var client1 = await testEnvironment.ConnectClient(); - var applicationMessageHandler = testEnvironment.CreateApplicationMessageHandler(client1); + var client1 = await testEnvironment.ConnectClient(); + var applicationMessageHandler = testEnvironment.CreateApplicationMessageHandler(client1); - var topicFilter = testEnvironment.ClientFactory.CreateTopicFilterBuilder().WithTopic("Topic/A").Build(); - var subscribeOptions = testEnvironment.ClientFactory.CreateSubscribeOptionsBuilder().WithSubscriptionIdentifier(456).WithTopicFilter(topicFilter).Build(); - await client1.SubscribeAsync(subscribeOptions); + var topicFilter = testEnvironment.ClientFactory.CreateTopicFilterBuilder().WithTopic("Topic/A").Build(); + var subscribeOptions = testEnvironment.ClientFactory.CreateSubscribeOptionsBuilder().WithSubscriptionIdentifier(456).WithTopicFilter(topicFilter).Build(); + await client1.SubscribeAsync(subscribeOptions); - await LongTestDelay(); + await LongTestDelay(); - topicFilter = testEnvironment.ClientFactory.CreateTopicFilterBuilder().WithTopic("Topic/+").Build(); - subscribeOptions = testEnvironment.ClientFactory.CreateSubscribeOptionsBuilder().WithSubscriptionIdentifier(789).WithTopicFilter(topicFilter).Build(); - await client1.SubscribeAsync(subscribeOptions); + topicFilter = testEnvironment.ClientFactory.CreateTopicFilterBuilder().WithTopic("Topic/+").Build(); + subscribeOptions = testEnvironment.ClientFactory.CreateSubscribeOptionsBuilder().WithSubscriptionIdentifier(789).WithTopicFilter(topicFilter).Build(); + await client1.SubscribeAsync(subscribeOptions); - await LongTestDelay(); + await LongTestDelay(); - applicationMessageHandler.AssertReceivedCountEquals(0); + applicationMessageHandler.AssertReceivedCountEquals(0); - // The client will publish a message where it is itself subscribing to. - await client1.PublishStringAsync("Topic/A", "Payload", retain: true); - await LongTestDelay(); + // The client will publish a message where it is itself subscribing to. + await client1.PublishStringAsync("Topic/A", "Payload", retain: true); + await LongTestDelay(); - applicationMessageHandler.AssertReceivedCountEquals(1); + applicationMessageHandler.AssertReceivedCountEquals(1); - applicationMessageHandler.ReceivedEventArgs[0].ApplicationMessage.SubscriptionIdentifiers.Contains(456); - applicationMessageHandler.ReceivedEventArgs[0].ApplicationMessage.SubscriptionIdentifiers.Contains(789); - } - } + Assert.IsTrue(applicationMessageHandler.ReceivedEventArgs[0].ApplicationMessage.SubscriptionIdentifiers.Contains(456)); + Assert.IsTrue(applicationMessageHandler.ReceivedEventArgs[0].ApplicationMessage.SubscriptionIdentifiers.Contains(789)); } } \ No newline at end of file diff --git a/Source/MQTTnet.Tests/Server/Subscription_TopicHash_Tests.cs b/Source/MQTTnet.Tests/Server/Subscription_TopicHash_Tests.cs index c067d510..a022201d 100644 --- a/Source/MQTTnet.Tests/Server/Subscription_TopicHash_Tests.cs +++ b/Source/MQTTnet.Tests/Server/Subscription_TopicHash_Tests.cs @@ -11,589 +11,588 @@ using MQTTnet.Server; using MQTTnet.Server.Internal; using MQTTnet.Tests.Mockups; -namespace MQTTnet.Tests.Server +namespace MQTTnet.Tests.Server; + +[TestClass] +public sealed class SubscriptionTopicHashTests { - [TestClass] - public sealed class SubscriptionTopicHashTests + MqttSession _clientSession; + + /// + /// Even fairly regularly named topics as generated by the topic generator should result in shallow hash buckets + /// + /// + [TestMethod] + public void Check_Hash_Bucket_Depth() { - MqttSession _clientSession; + const int numPublishers = 5000; + const int numTopicsPerPublisher = 10; - /// - /// Even fairly regularly named topics as generated by the topic generator should result in shallow hash buckets - /// - /// - [TestMethod] - public void Check_Hash_Bucket_Depth() + TopicGenerator.Generate( + numPublishers, + numTopicsPerPublisher, + out var topicsByPublisher, + out _, + out _); + + // There will be many 'similar' topics ending with, i.e. "sensor100", "sensor101", ... + // Hash bucket depths should remain low. + var bucketDepths = new Dictionary(); + var maxBucketDepth = 0; + ulong maxBucketDepthHash = 0; + + var topicsByHash = new Dictionary>(); + + foreach (var t in topicsByPublisher) { - const int numPublishers = 5000; - const int numTopicsPerPublisher = 10; - - TopicGenerator.Generate( - numPublishers, - numTopicsPerPublisher, - out var topicsByPublisher, - out _, - out _); - - // There will be many 'similar' topics ending with, i.e. "sensor100", "sensor101", ... - // Hash bucket depths should remain low. - var bucketDepths = new Dictionary(); - var maxBucketDepth = 0; - ulong maxBucketDepthHash = 0; - - var topicsByHash = new Dictionary>(); - - foreach (var t in topicsByPublisher) + var topics = t.Value; + foreach (var topic in topics) { - var topics = t.Value; - foreach (var topic in topics) + MqttTopicHash.Calculate(topic, out var topicHash, out _, out _); + + bucketDepths.TryGetValue(topicHash, out var currentValue); + ++currentValue; + bucketDepths[topicHash] = currentValue; + + if (currentValue > maxBucketDepth) { - MqttTopicHash.Calculate(topic, out var topicHash, out var hashMask, out var hasWildcard); - - bucketDepths.TryGetValue(topicHash, out var currentValue); - ++currentValue; - bucketDepths[topicHash] = currentValue; - - if (currentValue > maxBucketDepth) - { - maxBucketDepth = currentValue; - maxBucketDepthHash = topicHash; - } - - if (!topicsByHash.TryGetValue(topicHash, out var topicList)) - { - topicList = new List(); - topicsByHash.Add(topicHash, topicList); - } - - topicList.Add(topic); - } - } - - var maxDepthTopics = topicsByHash[maxBucketDepthHash]; - - Console.Write("Max bucket depth is " + maxBucketDepth); - - // for the test case the bucket depth should be less than 100 - Assert.IsTrue(maxBucketDepth < 100, "Unexpected high topic hash bucket depth"); - } - - - [TestMethod] - public void Check_Selected_Topic_Hashes() - { - CheckTopicHash("client1/building1/level1/sensor1", 0x655D4AF100000000, 0xFFFFFFFFFFFFFFFF); - CheckTopicHash("client1/building1/+/sensor1", 0x655D00F100000000, 0xFFFF00FFFFFFFFFF); - CheckTopicHash("client1/+/level1/+", 0x65004A0000000000, 0xFF00FF00FFFFFFFF); - CheckTopicHash("client1/building1/level1/#", 0x655D4A0000000000, 0xFFFFFF0000000000); - CheckTopicHash("client1/+/level1/#", 0x65004A0000000000, 0xFF00FF0000000000); - } - - /// - /// Test long topic name with last level being # - /// - [TestMethod] - public void Match_Hash_Test_LongTopic_DetectMultiLevelWildcard() - { - var topic = "asdfasdf/asdfasdf/asdfasdf/asdfasdf/asdfas/dfaf/assfdgsdfgdf/#"; - - MqttTopicHash.Calculate(topic, out var topicHash, out var topicHashMask, out var topicHasWildcard); - - Assert.IsTrue(topicHasWildcard, "Wildcard not detected"); - } - - - /// - /// Test long topic name with last level being + - /// - [TestMethod] - public void Match_Hash_Test_LongTopic_DetectSingleLevelWildcard() - { - var topic = "asdfasdf/asdfasdf/asdfasdf/asdfasdf/asdfas/dfaf/assfdgsdfgdf/+"; - - MqttTopicHash.Calculate(topic, out var topicHash, out var topicHashMask, out var topicHasWildcard); - - Assert.IsTrue(topicHasWildcard, "Wildcard not detected"); - } - - /// - /// Test long topic name with last level being # - /// - [TestMethod] - public void Match_Hash_Test_LongTopic_MultiWildcard() - { - var sb = new StringBuilder(); - const int numLevels = 8; - var levelNames = new string[numLevels]; - for (var i = 0; i < numLevels; ++i) - { - if (i > 0) - { - sb.Append("/"); + maxBucketDepth = currentValue; + maxBucketDepthHash = topicHash; } - string levelName; - if (i == numLevels - 1) + if (!topicsByHash.TryGetValue(topicHash, out var topicList)) { - // last one is # - levelName = "#"; - } - else - { - levelName = "level" + i; + topicList = []; + topicsByHash.Add(topicHash, topicList); } - levelNames[i] = levelName; - sb.Append(levelName); - } - - var topic = sb.ToString(); - - // UInt64 is limited to 8 levels - - MqttTopicHash.Calculate(topic, out var topicHash, out var topicHashMask, out var topicHasWildcard); - - Assert.IsTrue(topicHasWildcard, "Wildcard not detected"); - - var hashBytes = GetBytes(topicHash); - // all bytes should contain checksum - var count = 0; - foreach (var h in hashBytes) - { - if (count < 7) - { - Assert.AreNotEqual(h, 0, "checksum mismatch"); - } - else - { - Assert.AreEqual(h, 0, "checksum mismatch"); - } - - ++count; - } - - // The mask should have ff except for last level - var hashMaskBytes = GetBytes(topicHashMask); - count = 0; - foreach (var h in hashMaskBytes) - { - if (count < 7) - { - Assert.AreEqual(h, 0xff, "mask mismatch"); - } - else - { - Assert.AreEqual(h, 0, "last mask mismatch"); - } - - ++count; + topicList.Add(topic); } } - /// - /// Test long topic name exceeding 8 levels - /// - [TestMethod] - public void Match_Hash_Test_LongTopic_NoWildCard() - { - var sb = new StringBuilder(); - const int numLevels = 9; - var levelNames = new string[numLevels]; - for (var i = 0; i < numLevels; ++i) - { - if (i > 0) - { - sb.Append("/"); - } + var maxDepthTopics = topicsByHash[maxBucketDepthHash]; - var levelName = "level" + i; - levelNames[i] = levelName; - sb.Append(levelName); + Console.Write("Max bucket depth is " + maxBucketDepth); + + // for the test case the bucket depth should be less than 100 + Assert.IsTrue(maxBucketDepth < 100, "Unexpected high topic hash bucket depth"); + } + + + [TestMethod] + public void Check_Selected_Topic_Hashes() + { + CheckTopicHash("client1/building1/level1/sensor1", 0x655D4AF100000000, 0xFFFFFFFFFFFFFFFF); + CheckTopicHash("client1/building1/+/sensor1", 0x655D00F100000000, 0xFFFF00FFFFFFFFFF); + CheckTopicHash("client1/+/level1/+", 0x65004A0000000000, 0xFF00FF00FFFFFFFF); + CheckTopicHash("client1/building1/level1/#", 0x655D4A0000000000, 0xFFFFFF0000000000); + CheckTopicHash("client1/+/level1/#", 0x65004A0000000000, 0xFF00FF0000000000); + } + + /// + /// Test long topic name with last level being # + /// + [TestMethod] + public void Match_Hash_Test_LongTopic_DetectMultiLevelWildcard() + { + var topic = "asdfasdf/asdfasdf/asdfasdf/asdfasdf/asdfas/dfaf/assfdgsdfgdf/#"; + + MqttTopicHash.Calculate(topic, out _, out _, out var topicHasWildcard); + + Assert.IsTrue(topicHasWildcard, "Wildcard not detected"); + } + + + /// + /// Test long topic name with last level being + + /// + [TestMethod] + public void Match_Hash_Test_LongTopic_DetectSingleLevelWildcard() + { + var topic = "asdfasdf/asdfasdf/asdfasdf/asdfasdf/asdfas/dfaf/assfdgsdfgdf/+"; + + MqttTopicHash.Calculate(topic, out _, out _, out var topicHasWildcard); + + Assert.IsTrue(topicHasWildcard, "Wildcard not detected"); + } + + /// + /// Test long topic name with last level being # + /// + [TestMethod] + public void Match_Hash_Test_LongTopic_MultiWildcard() + { + var sb = new StringBuilder(); + const int numLevels = 8; + var levelNames = new string[numLevels]; + for (var i = 0; i < numLevels; ++i) + { + if (i > 0) + { + sb.Append('/'); } - var topic = sb.ToString(); + string levelName; + if (i == numLevels - 1) + { + // last one is # + levelName = "#"; + } + else + { + levelName = "level" + i; + } - MqttTopicHash.Calculate(topic, out var topicHash, out var topicHashMask, out var topicHasWildcard); + levelNames[i] = levelName; + sb.Append(levelName); + } - Assert.IsFalse(topicHasWildcard, "Wildcard detected when not present"); + var topic = sb.ToString(); + // UInt64 is limited to 8 levels - var hashBytes = GetBytes(topicHash); - // all bytes should contain checksum - var count = 0; - foreach (var h in hashBytes) + MqttTopicHash.Calculate(topic, out var topicHash, out var topicHashMask, out var topicHasWildcard); + + Assert.IsTrue(topicHasWildcard, "Wildcard not detected"); + + var hashBytes = GetBytes(topicHash); + // all bytes should contain checksum + var count = 0; + foreach (var h in hashBytes) + { + if (count < 7) { Assert.AreNotEqual(h, 0, "checksum mismatch"); - ++count; + } + else + { + Assert.AreEqual(h, 0, "checksum mismatch"); } - // The mask should have ff - var hashMaskBytes = GetBytes(topicHashMask); - count = 0; - foreach (var h in hashMaskBytes) + ++count; + } + + // The mask should have ff except for last level + var hashMaskBytes = GetBytes(topicHashMask); + count = 0; + foreach (var h in hashMaskBytes) + { + if (count < 7) { Assert.AreEqual(h, 0xff, "mask mismatch"); - ++count; } - } - - /// - /// Test long topic name with last level being + - /// - [TestMethod] - public void Match_Hash_Test_LongTopic_SingleWildCard() - { - var sb = new StringBuilder(); - const int numLevels = 8; - var levelNames = new string[numLevels]; - for (var i = 0; i < numLevels; ++i) + else { - if (i > 0) - { - sb.Append("/"); - } - - string levelName; - if (i == numLevels - 1) - { - // last one is + - levelName = "+"; - } - else - { - levelName = "level" + i; - } - - levelNames[i] = levelName; - sb.Append(levelName); + Assert.AreEqual(h, 0, "last mask mismatch"); } - var topic = sb.ToString(); - - MqttTopicHash.Calculate(topic, out var topicHash, out var topicHashMask, out var topicHasWildcard); - - Assert.IsTrue(topicHasWildcard, "Wildcard not detected"); - - - var hashBytes = GetBytes(topicHash); - // all bytes should contain checksum - var count = 0; - foreach (var h in hashBytes) - { - if (count < 7) - { - Assert.AreNotEqual(h, 0, "checksum mismatch"); - } - else - { - // wildcard position - Assert.AreEqual(h, 0, "checksum mismatch"); - } - - ++count; - } - - // The mask should have ff - var hashMaskBytes = GetBytes(topicHashMask); - count = 0; - foreach (var h in hashMaskBytes) - { - if (count < 7) - { - Assert.AreEqual(h, 0xff, "mask mismatch"); - } - else - { - Assert.AreEqual(h, 0, "last mask mismatch"); - } - - ++count; - } - } - - [TestMethod] - public void Match_Hash_Test_MultiWildCard() - { - var l0 = "pub0"; - var l1 = "topic1"; - var l2 = "sub1"; - var l3 = "#"; - var topic = $"{l0}/{l1}/{l2}/{l3}"; - - MqttTopicHash.Calculate(topic, out var topicHash, out var topicHashMask, out var topicHasWildcard); - - Assert.IsTrue(topicHasWildcard, "Wildcard not detected"); - - var hashBytes = GetBytes(topicHash); - Assert.AreNotEqual(hashBytes[0], 0, "checksum 0 mismatch"); - Assert.AreNotEqual(hashBytes[1], 0, "checksum 1 mismatch"); - Assert.AreNotEqual(hashBytes[2], 0, "checksum 2 mismatch"); - Assert.AreEqual(hashBytes[3], 0, "checksum 3 mismatch"); - Assert.AreEqual(hashBytes[4], 0, "checksum 4 mismatch"); - Assert.AreEqual(hashBytes[5], 0, "checksum 5 mismatch"); - Assert.AreEqual(hashBytes[6], 0, "checksum 6 mismatch"); - Assert.AreEqual(hashBytes[7], 0, "checksum 7 mismatch"); - - // The mask should have zeroes where the wildcard is and zero onward - var hashMaskBytes = GetBytes(topicHashMask); - Assert.AreEqual(hashMaskBytes[0], 0xff, "mask 0 mismatch"); - Assert.AreEqual(hashMaskBytes[1], 0xff, "mask 1 mismatch"); - Assert.AreEqual(hashMaskBytes[2], 0xff, "mask 2 mismatch"); - Assert.AreEqual(hashMaskBytes[3], 0, "mask 3 mismatch"); - Assert.AreEqual(hashMaskBytes[4], 0, "mask 4 mismatch"); - Assert.AreEqual(hashMaskBytes[5], 0, "mask 5 mismatch"); - Assert.AreEqual(hashMaskBytes[6], 0, "mask 6 mismatch"); - Assert.AreEqual(hashMaskBytes[7], 0, "mask 7 mismatch"); - } - - [TestMethod] - public void Match_Hash_Test_NoWildCard() - { - var l0 = "pub0"; - var l1 = "topic1"; - var l2 = "sub1"; - var l3 = "prop1"; - var topic = $"{l0}/{l1}/{l2}/{l3}"; - - MqttTopicHash.Calculate(topic, out var topicHash, out var topicHashMask, out var topicHasWildcard); - - Assert.IsFalse(topicHasWildcard, "Wildcard detected when not wildcard present"); - - - var hashBytes = GetBytes(topicHash); - Assert.AreNotEqual(hashBytes[0], 0, "checksum 0 mismatch"); - Assert.AreNotEqual(hashBytes[1], 0, "checksum 1 mismatch"); - Assert.AreNotEqual(hashBytes[2], 0, "checksum 2 mismatch"); - Assert.AreNotEqual(hashBytes[3], 0, "checksum 3 mismatch"); - Assert.AreEqual(hashBytes[4], 0, "checksum 4 mismatch"); - Assert.AreEqual(hashBytes[5], 0, "checksum 5 mismatch"); - Assert.AreEqual(hashBytes[6], 0, "checksum 6 mismatch"); - Assert.AreEqual(hashBytes[7], 0, "checksum 7 mismatch"); - - // The mask should have ff - var hashMaskBytes = GetBytes(topicHashMask); - Assert.AreEqual(hashMaskBytes[0], 0xff, "mask 0 mismatch"); - Assert.AreEqual(hashMaskBytes[1], 0xff, "mask 1 mismatch"); - Assert.AreEqual(hashMaskBytes[2], 0xff, "mask 2 mismatch"); - Assert.AreEqual(hashMaskBytes[3], 0xff, "mask 3 mismatch"); - Assert.AreEqual(hashMaskBytes[4], 0xff, "mask 4 mismatch"); - Assert.AreEqual(hashMaskBytes[5], 0xff, "mask 5 mismatch"); - Assert.AreEqual(hashMaskBytes[6], 0xff, "mask 6 mismatch"); - Assert.AreEqual(hashMaskBytes[7], 0xff, "mask 7 mismatch"); - } - - [TestMethod] - public async Task Match_Hash_Test_Search_MultiWildcard() - { - var topics = await PrepareTopicHashSubscriptions(TopicHashSelector.MultiWildcard); - var matchCount = CheckTopicSubscriptions(_clientSession, topics, "Multi Wildcard"); - // Should match all topics - Assert.AreEqual(topics.Count, matchCount, "Topics not matched"); - } - - [TestMethod] - public async Task Match_Hash_Test_Search_NoWildcard() - { - var topics = await PrepareTopicHashSubscriptions(TopicHashSelector.NoWildcard); - - // all match lookup - var matchCount = CheckTopicSubscriptions(_clientSession, topics, "No Wildcard All Match"); - - Assert.AreEqual(topics.Count, matchCount, "Not all topics matched"); - - // no match lookup - { - { - var topicsToFind = new List(); - foreach (var t in topics) - { - topicsToFind.Add(t + "x"); - } - - matchCount = CheckTopicSubscriptions(_clientSession, topicsToFind, "No Wildcard No Match (Append X)"); - - Assert.AreEqual(0, matchCount, "Topic match count not zero"); - } - - { - var topicsToFind = new List(); - foreach (var t in topics) - { - // replace last letter with x - topicsToFind.Add(t.Substring(0, t.Length - 1) + "x"); - } - - matchCount = CheckTopicSubscriptions(_clientSession, topicsToFind, "No Wildcard No Match (Replace X)"); - - Assert.AreEqual(0, matchCount, "Topic match count not zero"); - } - } - } - - [TestMethod] - public async Task Match_Hash_Test_Search_SingleWildcard() - { - var topics = await PrepareTopicHashSubscriptions(TopicHashSelector.SingleWildcard); - var matchCount = CheckTopicSubscriptions(_clientSession, topics, "Single Wildcard"); - // Should match all topics - Assert.AreEqual(topics.Count, matchCount, "Topics not matched"); - } - - [TestMethod] - public void Match_Hash_Test_SingleWildCard() - { - var l0 = "pub0"; - var l1 = "topic1"; - var l2 = "+"; - var l3 = "prop1"; - var topic = $"{l0}/{l1}/{l2}/{l3}"; - - MqttTopicHash.Calculate(topic, out var topicHash, out var topicHashMask, out var topicHasWildcard); - - Assert.IsTrue(topicHasWildcard, "Wildcard not detected"); - - var hashBytes = GetBytes(topicHash); - Assert.AreNotEqual(hashBytes[0], 0, "checksum 0 mismatch"); - Assert.AreNotEqual(hashBytes[1], 0, "checksum 1 mismatch"); - Assert.AreEqual(hashBytes[2], 0, "checksum 2 mismatch"); - Assert.AreNotEqual(hashBytes[3], 0, "checksum 3 mismatch"); - Assert.AreEqual(hashBytes[4], 0, "checksum 4 mismatch"); - Assert.AreEqual(hashBytes[5], 0, "checksum 5 mismatch"); - Assert.AreEqual(hashBytes[6], 0, "checksum 6 mismatch"); - Assert.AreEqual(hashBytes[7], 0, "checksum 7 mismatch"); - - // The mask should have zeroes where the wildcard and ff at the end - var hashMaskBytes = GetBytes(topicHashMask); - Assert.AreEqual(hashMaskBytes[0], 0xff, "mask 0 mismatch"); - Assert.AreEqual(hashMaskBytes[1], 0xff, "mask 1 mismatch"); - Assert.AreEqual(hashMaskBytes[2], 0, "mask 2 mismatch"); - Assert.AreEqual(hashMaskBytes[3], 0xff, "mask 3 mismatch"); - Assert.AreEqual(hashMaskBytes[4], 0xff, "mask 4 mismatch"); - Assert.AreEqual(hashMaskBytes[5], 0xff, "mask 5 mismatch"); - Assert.AreEqual(hashMaskBytes[6], 0xff, "mask 6 mismatch"); - Assert.AreEqual(hashMaskBytes[7], 0xff, "mask 7 mismatch"); - } - - void CheckTopicHash(string topic, ulong expectedHash, ulong expectedHashMask) - { - MqttTopicHash.Calculate(topic, out var topicHash, out var hashMask, out var hasWildcard); - - Console.WriteLine(); - Console.WriteLine("Topic: " + topic); - Console.WriteLine($"Hash: {topicHash:X8}"); - Console.WriteLine($"Hash Mask: {hashMask:X8}"); - - Assert.AreEqual(expectedHash, topicHash, "Topic hash not as expected. Has the hash function changed?"); - Assert.AreEqual(expectedHashMask, hashMask, "Topic hash mask not as expected"); - } - - int CheckTopicSubscriptions(MqttSession clientSession, List topicsToFind, string subject) - { - var matchCount = 0; - - { - var resultCount = 0; - - var stopWatch = new Stopwatch(); - stopWatch.Start(); - var countUp = 0; - var countDown = topicsToFind.Count - 1; - for (; countUp < topicsToFind.Count; ++countUp, --countDown) - { - var topicToFind = topicsToFind[countDown]; - - MqttTopicHash.Calculate(topicToFind, out var topicHash, out _, out _); - - clientSession.TryCheckSubscriptions(topicToFind, topicHash, MqttQualityOfServiceLevel.AtMostOnce, "OtherClient", out var result); - if (result.IsSubscribed) - { - ++matchCount; - } - - ++resultCount; - } - - stopWatch.Stop(); - - Console.Write("Match count: " + matchCount + "; "); - - Console.WriteLine(subject + " lookup milliseconds: " + stopWatch.ElapsedMilliseconds); - } - - return matchCount; - } - - byte[] GetBytes(ulong value) - { - var bytes = BitConverter.GetBytes(value); - // Ensure that highest byte comes first for comparison left to right - if (BitConverter.IsLittleEndian) - { - return bytes.Reverse().ToArray(); - } - - return bytes; - } - - - async Task> PrepareTopicHashSubscriptions(TopicHashSelector selector) - { - const int numPublishers = 1; - const int numTopicsPerPublisher = 10000; - - TopicGenerator.Generate( - numPublishers, - numTopicsPerPublisher, - out var topicsByPublisher, - out var singleWildcardTopicsByPublisher, - out var multiWildcardTopicsByPublisher); - - var topics = topicsByPublisher.FirstOrDefault().Value; - var singleWildcardTopics = singleWildcardTopicsByPublisher.FirstOrDefault().Value; - var multiWildcardTopics = multiWildcardTopicsByPublisher.FirstOrDefault().Value; - - const string clientId = "Client1"; - var logger = new TestLogger(); - var serverOptions = new MqttServerOptions(); - var eventContainer = new MqttServerEventContainer(); - var retainedMessagesManager = new MqttRetainedMessagesManager(eventContainer, logger); - var sessionManager = new MqttClientSessionsManager(serverOptions, retainedMessagesManager, eventContainer, logger); - _clientSession = new MqttSession(new MqttConnectPacket{ ClientId = clientId }, new Dictionary(), serverOptions, eventContainer, retainedMessagesManager, sessionManager); - - List topicsToSubscribe; - - switch (selector) - { - case TopicHashSelector.SingleWildcard: - topicsToSubscribe = singleWildcardTopics; - break; - case TopicHashSelector.MultiWildcard: - topicsToSubscribe = multiWildcardTopics; - break; - default: - topicsToSubscribe = topics; - break; - } - - foreach (var t in topicsToSubscribe) - { - var subPacket = new MqttSubscribePacket(); - var filter = new MqttTopicFilter - { - Topic = t - }; - subPacket.TopicFilters.Add(filter); - await _clientSession.Subscribe(subPacket, default); - } - - return topics; - } - - enum TopicHashSelector - { - NoWildcard, - SingleWildcard, - MultiWildcard + ++count; } } + + /// + /// Test long topic name exceeding 8 levels + /// + [TestMethod] + public void Match_Hash_Test_LongTopic_NoWildCard() + { + var sb = new StringBuilder(); + const int numLevels = 9; + var levelNames = new string[numLevels]; + for (var i = 0; i < numLevels; ++i) + { + if (i > 0) + { + sb.Append('/'); + } + + var levelName = "level" + i; + levelNames[i] = levelName; + sb.Append(levelName); + } + + var topic = sb.ToString(); + + MqttTopicHash.Calculate(topic, out var topicHash, out var topicHashMask, out var topicHasWildcard); + + Assert.IsFalse(topicHasWildcard, "Wildcard detected when not present"); + + + var hashBytes = GetBytes(topicHash); + // all bytes should contain checksum + var count = 0; + foreach (var h in hashBytes) + { + Assert.AreNotEqual(h, 0, "checksum mismatch"); + ++count; + } + + // The mask should have ff + var hashMaskBytes = GetBytes(topicHashMask); + count = 0; + foreach (var h in hashMaskBytes) + { + Assert.AreEqual(h, 0xff, "mask mismatch"); + ++count; + } + } + + /// + /// Test long topic name with last level being + + /// + [TestMethod] + public void Match_Hash_Test_LongTopic_SingleWildCard() + { + var sb = new StringBuilder(); + const int numLevels = 8; + var levelNames = new string[numLevels]; + for (var i = 0; i < numLevels; ++i) + { + if (i > 0) + { + sb.Append('/'); + } + + string levelName; + if (i == numLevels - 1) + { + // last one is + + levelName = "+"; + } + else + { + levelName = "level" + i; + } + + levelNames[i] = levelName; + sb.Append(levelName); + } + + var topic = sb.ToString(); + + MqttTopicHash.Calculate(topic, out var topicHash, out var topicHashMask, out var topicHasWildcard); + + Assert.IsTrue(topicHasWildcard, "Wildcard not detected"); + + + var hashBytes = GetBytes(topicHash); + // all bytes should contain checksum + var count = 0; + foreach (var h in hashBytes) + { + if (count < 7) + { + Assert.AreNotEqual(h, 0, "checksum mismatch"); + } + else + { + // wildcard position + Assert.AreEqual(h, 0, "checksum mismatch"); + } + + ++count; + } + + // The mask should have ff + var hashMaskBytes = GetBytes(topicHashMask); + count = 0; + foreach (var h in hashMaskBytes) + { + if (count < 7) + { + Assert.AreEqual(h, 0xff, "mask mismatch"); + } + else + { + Assert.AreEqual(h, 0, "last mask mismatch"); + } + + ++count; + } + } + + [TestMethod] + public void Match_Hash_Test_MultiWildCard() + { + var l0 = "pub0"; + var l1 = "topic1"; + var l2 = "sub1"; + var l3 = "#"; + var topic = $"{l0}/{l1}/{l2}/{l3}"; + + MqttTopicHash.Calculate(topic, out var topicHash, out var topicHashMask, out var topicHasWildcard); + + Assert.IsTrue(topicHasWildcard, "Wildcard not detected"); + + var hashBytes = GetBytes(topicHash); + Assert.AreNotEqual(hashBytes[0], 0, "checksum 0 mismatch"); + Assert.AreNotEqual(hashBytes[1], 0, "checksum 1 mismatch"); + Assert.AreNotEqual(hashBytes[2], 0, "checksum 2 mismatch"); + Assert.AreEqual(hashBytes[3], 0, "checksum 3 mismatch"); + Assert.AreEqual(hashBytes[4], 0, "checksum 4 mismatch"); + Assert.AreEqual(hashBytes[5], 0, "checksum 5 mismatch"); + Assert.AreEqual(hashBytes[6], 0, "checksum 6 mismatch"); + Assert.AreEqual(hashBytes[7], 0, "checksum 7 mismatch"); + + // The mask should have zeroes where the wildcard is and zero onward + var hashMaskBytes = GetBytes(topicHashMask); + Assert.AreEqual(hashMaskBytes[0], 0xff, "mask 0 mismatch"); + Assert.AreEqual(hashMaskBytes[1], 0xff, "mask 1 mismatch"); + Assert.AreEqual(hashMaskBytes[2], 0xff, "mask 2 mismatch"); + Assert.AreEqual(hashMaskBytes[3], 0, "mask 3 mismatch"); + Assert.AreEqual(hashMaskBytes[4], 0, "mask 4 mismatch"); + Assert.AreEqual(hashMaskBytes[5], 0, "mask 5 mismatch"); + Assert.AreEqual(hashMaskBytes[6], 0, "mask 6 mismatch"); + Assert.AreEqual(hashMaskBytes[7], 0, "mask 7 mismatch"); + } + + [TestMethod] + public void Match_Hash_Test_NoWildCard() + { + var l0 = "pub0"; + var l1 = "topic1"; + var l2 = "sub1"; + var l3 = "prop1"; + var topic = $"{l0}/{l1}/{l2}/{l3}"; + + MqttTopicHash.Calculate(topic, out var topicHash, out var topicHashMask, out var topicHasWildcard); + + Assert.IsFalse(topicHasWildcard, "Wildcard detected when not wildcard present"); + + + var hashBytes = GetBytes(topicHash); + Assert.AreNotEqual(hashBytes[0], 0, "checksum 0 mismatch"); + Assert.AreNotEqual(hashBytes[1], 0, "checksum 1 mismatch"); + Assert.AreNotEqual(hashBytes[2], 0, "checksum 2 mismatch"); + Assert.AreNotEqual(hashBytes[3], 0, "checksum 3 mismatch"); + Assert.AreEqual(hashBytes[4], 0, "checksum 4 mismatch"); + Assert.AreEqual(hashBytes[5], 0, "checksum 5 mismatch"); + Assert.AreEqual(hashBytes[6], 0, "checksum 6 mismatch"); + Assert.AreEqual(hashBytes[7], 0, "checksum 7 mismatch"); + + // The mask should have ff + var hashMaskBytes = GetBytes(topicHashMask); + Assert.AreEqual(hashMaskBytes[0], 0xff, "mask 0 mismatch"); + Assert.AreEqual(hashMaskBytes[1], 0xff, "mask 1 mismatch"); + Assert.AreEqual(hashMaskBytes[2], 0xff, "mask 2 mismatch"); + Assert.AreEqual(hashMaskBytes[3], 0xff, "mask 3 mismatch"); + Assert.AreEqual(hashMaskBytes[4], 0xff, "mask 4 mismatch"); + Assert.AreEqual(hashMaskBytes[5], 0xff, "mask 5 mismatch"); + Assert.AreEqual(hashMaskBytes[6], 0xff, "mask 6 mismatch"); + Assert.AreEqual(hashMaskBytes[7], 0xff, "mask 7 mismatch"); + } + + [TestMethod] + public async Task Match_Hash_Test_Search_MultiWildcard() + { + var topics = await PrepareTopicHashSubscriptions(TopicHashSelector.MultiWildcard); + var matchCount = CheckTopicSubscriptions(_clientSession, topics, "Multi Wildcard"); + // Should match all topics + Assert.AreEqual(topics.Count, matchCount, "Topics not matched"); + } + + [TestMethod] + public async Task Match_Hash_Test_Search_NoWildcard() + { + var topics = await PrepareTopicHashSubscriptions(TopicHashSelector.NoWildcard); + + // all match lookup + var matchCount = CheckTopicSubscriptions(_clientSession, topics, "No Wildcard All Match"); + + Assert.AreEqual(topics.Count, matchCount, "Not all topics matched"); + + // no match lookup + { + { + var topicsToFind = new List(); + foreach (var t in topics) + { + topicsToFind.Add(t + "x"); + } + + matchCount = CheckTopicSubscriptions(_clientSession, topicsToFind, "No Wildcard No Match (Append X)"); + + Assert.AreEqual(0, matchCount, "Topic match count not zero"); + } + + { + var topicsToFind = new List(); + foreach (var t in topics) + { + // replace last letter with x + topicsToFind.Add(t.Substring(0, t.Length - 1) + "x"); + } + + matchCount = CheckTopicSubscriptions(_clientSession, topicsToFind, "No Wildcard No Match (Replace X)"); + + Assert.AreEqual(0, matchCount, "Topic match count not zero"); + } + } + } + + [TestMethod] + public async Task Match_Hash_Test_Search_SingleWildcard() + { + var topics = await PrepareTopicHashSubscriptions(TopicHashSelector.SingleWildcard); + var matchCount = CheckTopicSubscriptions(_clientSession, topics, "Single Wildcard"); + // Should match all topics + Assert.AreEqual(topics.Count, matchCount, "Topics not matched"); + } + + [TestMethod] + public void Match_Hash_Test_SingleWildCard() + { + var l0 = "pub0"; + var l1 = "topic1"; + var l2 = "+"; + var l3 = "prop1"; + var topic = $"{l0}/{l1}/{l2}/{l3}"; + + MqttTopicHash.Calculate(topic, out var topicHash, out var topicHashMask, out var topicHasWildcard); + + Assert.IsTrue(topicHasWildcard, "Wildcard not detected"); + + var hashBytes = GetBytes(topicHash); + Assert.AreNotEqual(hashBytes[0], 0, "checksum 0 mismatch"); + Assert.AreNotEqual(hashBytes[1], 0, "checksum 1 mismatch"); + Assert.AreEqual(hashBytes[2], 0, "checksum 2 mismatch"); + Assert.AreNotEqual(hashBytes[3], 0, "checksum 3 mismatch"); + Assert.AreEqual(hashBytes[4], 0, "checksum 4 mismatch"); + Assert.AreEqual(hashBytes[5], 0, "checksum 5 mismatch"); + Assert.AreEqual(hashBytes[6], 0, "checksum 6 mismatch"); + Assert.AreEqual(hashBytes[7], 0, "checksum 7 mismatch"); + + // The mask should have zeroes where the wildcard and ff at the end + var hashMaskBytes = GetBytes(topicHashMask); + Assert.AreEqual(hashMaskBytes[0], 0xff, "mask 0 mismatch"); + Assert.AreEqual(hashMaskBytes[1], 0xff, "mask 1 mismatch"); + Assert.AreEqual(hashMaskBytes[2], 0, "mask 2 mismatch"); + Assert.AreEqual(hashMaskBytes[3], 0xff, "mask 3 mismatch"); + Assert.AreEqual(hashMaskBytes[4], 0xff, "mask 4 mismatch"); + Assert.AreEqual(hashMaskBytes[5], 0xff, "mask 5 mismatch"); + Assert.AreEqual(hashMaskBytes[6], 0xff, "mask 6 mismatch"); + Assert.AreEqual(hashMaskBytes[7], 0xff, "mask 7 mismatch"); + } + + void CheckTopicHash(string topic, ulong expectedHash, ulong expectedHashMask) + { + MqttTopicHash.Calculate(topic, out var topicHash, out var hashMask, out _); + + Console.WriteLine(); + Console.WriteLine("Topic: " + topic); + Console.WriteLine($"Hash: {topicHash:X8}"); + Console.WriteLine($"Hash Mask: {hashMask:X8}"); + + Assert.AreEqual(expectedHash, topicHash, "Topic hash not as expected. Has the hash function changed?"); + Assert.AreEqual(expectedHashMask, hashMask, "Topic hash mask not as expected"); + } + + int CheckTopicSubscriptions(MqttSession clientSession, List topicsToFind, string subject) + { + var matchCount = 0; + + { + var resultCount = 0; + + var stopWatch = new Stopwatch(); + stopWatch.Start(); + var countUp = 0; + var countDown = topicsToFind.Count - 1; + for (; countUp < topicsToFind.Count; ++countUp, --countDown) + { + var topicToFind = topicsToFind[countDown]; + + MqttTopicHash.Calculate(topicToFind, out var topicHash, out _, out _); + + clientSession.TryCheckSubscriptions(topicToFind, topicHash, MqttQualityOfServiceLevel.AtMostOnce, "OtherClient", out var result); + if (result.IsSubscribed) + { + ++matchCount; + } + + ++resultCount; + } + + stopWatch.Stop(); + + Console.Write("Match count: " + matchCount + "; "); + + Console.WriteLine(subject + " lookup milliseconds: " + stopWatch.ElapsedMilliseconds); + } + + return matchCount; + } + + byte[] GetBytes(ulong value) + { + var bytes = BitConverter.GetBytes(value); + // Ensure that highest byte comes first for comparison left to right + if (BitConverter.IsLittleEndian) + { + return bytes.Reverse().ToArray(); + } + + return bytes; + } + + + async Task> PrepareTopicHashSubscriptions(TopicHashSelector selector) + { + const int numPublishers = 1; + const int numTopicsPerPublisher = 10000; + + TopicGenerator.Generate( + numPublishers, + numTopicsPerPublisher, + out var topicsByPublisher, + out var singleWildcardTopicsByPublisher, + out var multiWildcardTopicsByPublisher); + + var topics = topicsByPublisher.FirstOrDefault().Value; + var singleWildcardTopics = singleWildcardTopicsByPublisher.FirstOrDefault().Value; + var multiWildcardTopics = multiWildcardTopicsByPublisher.FirstOrDefault().Value; + + const string clientId = "Client1"; + var logger = new TestLogger(); + var serverOptions = new MqttServerOptions(); + var eventContainer = new MqttServerEventContainer(); + var retainedMessagesManager = new MqttRetainedMessagesManager(eventContainer, logger); + var sessionManager = new MqttClientSessionsManager(serverOptions, retainedMessagesManager, eventContainer, logger); + _clientSession = new MqttSession(new MqttConnectPacket{ ClientId = clientId }, new Dictionary(), serverOptions, eventContainer, retainedMessagesManager, sessionManager); + + List topicsToSubscribe; + + switch (selector) + { + case TopicHashSelector.SingleWildcard: + topicsToSubscribe = singleWildcardTopics; + break; + case TopicHashSelector.MultiWildcard: + topicsToSubscribe = multiWildcardTopics; + break; + default: + topicsToSubscribe = topics; + break; + } + + foreach (var t in topicsToSubscribe) + { + var subPacket = new MqttSubscribePacket(); + var filter = new MqttTopicFilter + { + Topic = t + }; + subPacket.TopicFilters.Add(filter); + await _clientSession.Subscribe(subPacket, default); + } + + return topics; + } + + enum TopicHashSelector + { + NoWildcard, + SingleWildcard, + MultiWildcard + } } \ No newline at end of file diff --git a/Source/MQTTnet.Tests/Server/Tls_Tests.cs b/Source/MQTTnet.Tests/Server/Tls_Tests.cs index a87b0dc4..b81657bd 100644 --- a/Source/MQTTnet.Tests/Server/Tls_Tests.cs +++ b/Source/MQTTnet.Tests/Server/Tls_Tests.cs @@ -12,188 +12,184 @@ using MQTTnet.Formatter; using MQTTnet.Server; using MQTTnet.Tests.Mockups; -namespace MQTTnet.Tests.Server +namespace MQTTnet.Tests.Server; + +// ReSharper disable InconsistentNaming +[TestClass] +public sealed class Tls_Tests : BaseTestClass { - [TestClass] - public sealed class Tls_Tests : BaseTestClass + static X509Certificate2 CreateCertificate(string oid) { - static X509Certificate2 CreateCertificate(string oid) + var sanBuilder = new SubjectAlternativeNameBuilder(); + sanBuilder.AddIpAddress(IPAddress.Loopback); + sanBuilder.AddIpAddress(IPAddress.IPv6Loopback); + sanBuilder.AddDnsName("localhost"); + + using var rsa = RSA.Create(); + var certRequest = new CertificateRequest("CN=localhost", rsa, HashAlgorithmName.SHA512, RSASignaturePadding.Pkcs1); + + certRequest.CertificateExtensions.Add( + new X509KeyUsageExtension(X509KeyUsageFlags.DataEncipherment | X509KeyUsageFlags.KeyEncipherment | X509KeyUsageFlags.DigitalSignature, false)); + + certRequest.CertificateExtensions.Add(new X509EnhancedKeyUsageExtension(new OidCollection { new Oid(oid) }, false)); + + certRequest.CertificateExtensions.Add(sanBuilder.Build()); + + using var certificate = certRequest.CreateSelfSigned(DateTimeOffset.Now.AddMinutes(-10), DateTimeOffset.Now.AddMinutes(10)); + var pfxCertificate = new X509Certificate2( + certificate.Export(X509ContentType.Pfx), + (string)null, + X509KeyStorageFlags.MachineKeySet | X509KeyStorageFlags.Exportable); + + return pfxCertificate; + } + + [TestMethod] + public async Task Tls_Swap_Test() + { + var testEnvironment = CreateTestEnvironment(MqttProtocolVersion.V500); + var serverOptionsBuilder = testEnvironment.ServerFactory.CreateServerOptionsBuilder(); + + var firstOid = "1.3.6.1.5.5.7.3.1"; + var secondOid = "1.3.6.1.5.5.7.3.2"; + + var certificateProvider = new CertificateProvider { - var sanBuilder = new SubjectAlternativeNameBuilder(); - sanBuilder.AddIpAddress(IPAddress.Loopback); - sanBuilder.AddIpAddress(IPAddress.IPv6Loopback); - sanBuilder.AddDnsName("localhost"); + CurrentCertificate = CreateCertificate(firstOid) + }; - using (var rsa = RSA.Create()) - { - var certRequest = new CertificateRequest("CN=localhost", rsa, HashAlgorithmName.SHA512, RSASignaturePadding.Pkcs1); + serverOptionsBuilder.WithoutDefaultEndpoint().WithEncryptedEndpoint().WithEncryptionSslProtocol(SslProtocols.Tls12).WithEncryptionCertificate(certificateProvider); - certRequest.CertificateExtensions.Add( - new X509KeyUsageExtension(X509KeyUsageFlags.DataEncipherment | X509KeyUsageFlags.KeyEncipherment | X509KeyUsageFlags.DigitalSignature, false)); + var serverOptions = serverOptionsBuilder.Build(); - certRequest.CertificateExtensions.Add(new X509EnhancedKeyUsageExtension(new OidCollection { new Oid(oid) }, false)); + var server = testEnvironment.CreateServer(serverOptions); - certRequest.CertificateExtensions.Add(sanBuilder.Build()); - - using (var certificate = certRequest.CreateSelfSigned(DateTimeOffset.Now.AddMinutes(-10), DateTimeOffset.Now.AddMinutes(10))) - { - var pfxCertificate = new X509Certificate2( - certificate.Export(X509ContentType.Pfx), - (string)null, - X509KeyStorageFlags.MachineKeySet | X509KeyStorageFlags.Exportable); - - return pfxCertificate; - } - } - } - - [TestMethod] - public async Task Tls_Swap_Test() + var publishedCount = 0; + server.InterceptingPublishAsync += _ => { - var testEnvironment = CreateTestEnvironment(MqttProtocolVersion.V500); - var serverOptionsBuilder = testEnvironment.ServerFactory.CreateServerOptionsBuilder(); + Interlocked.Increment(ref publishedCount); - var firstOid = "1.3.6.1.5.5.7.3.1"; - var secondOid = "1.3.6.1.5.5.7.3.2"; + return Task.CompletedTask; + }; - var certificateProvider = new CertificateProvider + await server.StartAsync(); + + var firstClient = await ConnectClientAsync( + testEnvironment, + args => { - CurrentCertificate = CreateCertificate(firstOid) - }; + Assert.AreEqual(firstOid, ((X509Certificate2)args.Certificate).Extensions.OfType().First().EnhancedKeyUsages[0].Value); + return true; + }); - serverOptionsBuilder.WithoutDefaultEndpoint().WithEncryptedEndpoint().WithEncryptionSslProtocol(SslProtocols.Tls12).WithEncryptionCertificate(certificateProvider); + var firstClientReceivedCount = 0; + firstClient.ApplicationMessageReceivedAsync += _ => + { + Interlocked.Increment(ref firstClientReceivedCount); - var serverOptions = serverOptionsBuilder.Build(); + return Task.CompletedTask; + }; - var server = testEnvironment.CreateServer(serverOptions); + await firstClient.SubscribeAsync("TestTopic1"); - var publishedCount = 0; - server.InterceptingPublishAsync += args => + await firstClient.PublishAsync( + new MqttApplicationMessage { - Interlocked.Increment(ref publishedCount); + Topic = "TestTopic1", + PayloadSegment = new ArraySegment([1, 2, 3, 4]) + }); - return Task.CompletedTask; - }; - - await server.StartAsync(); - - var firstClient = await ConnectClientAsync( - testEnvironment, - args => - { - Assert.AreEqual(firstOid, ((X509Certificate2)args.Certificate).Extensions.OfType().First().EnhancedKeyUsages[0].Value); - return true; - }); - - var firstClientReceivedCount = 0; - firstClient.ApplicationMessageReceivedAsync += args => - { - Interlocked.Increment(ref firstClientReceivedCount); - - return Task.CompletedTask; - }; - - await firstClient.SubscribeAsync("TestTopic1"); - - await firstClient.PublishAsync( + await testEnvironment.Server.InjectApplicationMessage( + new InjectedMqttApplicationMessage( new MqttApplicationMessage { Topic = "TestTopic1", - PayloadSegment = new ArraySegment(new byte[] { 1, 2, 3, 4 }) - }); + PayloadSegment = new ArraySegment([1, 2, 3, 4]) + })); - await testEnvironment.Server.InjectApplicationMessage( - new InjectedMqttApplicationMessage( - new MqttApplicationMessage - { - Topic = "TestTopic1", - PayloadSegment = new ArraySegment(new byte[] { 1, 2, 3, 4 }) - })); + certificateProvider.CurrentCertificate = CreateCertificate(secondOid); - certificateProvider.CurrentCertificate = CreateCertificate(secondOid); - - // Validate that the certificate was switched - var secondClient = await ConnectClientAsync( - testEnvironment, - args => - { - Assert.AreEqual(secondOid, ((X509Certificate2)args.Certificate).Extensions.OfType().First().EnhancedKeyUsages[0].Value); - return true; - }); - - var secondClientReceivedCount = 0; - secondClient.ApplicationMessageReceivedAsync += args => + // Validate that the certificate was switched + var secondClient = await ConnectClientAsync( + testEnvironment, + args => { - Interlocked.Increment(ref secondClientReceivedCount); + Assert.AreEqual(secondOid, ((X509Certificate2)args.Certificate).Extensions.OfType().First().EnhancedKeyUsages[0].Value); + return true; + }); - return Task.CompletedTask; - }; + var secondClientReceivedCount = 0; + secondClient.ApplicationMessageReceivedAsync += _ => + { + Interlocked.Increment(ref secondClientReceivedCount); - await secondClient.SubscribeAsync("TestTopic2"); + return Task.CompletedTask; + }; - await firstClient.PublishAsync( + await secondClient.SubscribeAsync("TestTopic2"); + + await firstClient.PublishAsync( + new MqttApplicationMessage + { + Topic = "TestTopic2", + PayloadSegment = new ArraySegment([1, 2, 3, 4]) + }); + + await testEnvironment.Server.InjectApplicationMessage( + new InjectedMqttApplicationMessage( new MqttApplicationMessage { Topic = "TestTopic2", - PayloadSegment = new ArraySegment(new byte[] { 1, 2, 3, 4 }) - }); + PayloadSegment = new ArraySegment([1, 2, 3, 4]) + })); - await testEnvironment.Server.InjectApplicationMessage( - new InjectedMqttApplicationMessage( - new MqttApplicationMessage - { - Topic = "TestTopic2", - PayloadSegment = new ArraySegment(new byte[] { 1, 2, 3, 4 }) - })); + // Ensure first client still works + await firstClient.PublishAsync( + new MqttApplicationMessage + { + Topic = "TestTopic1", + PayloadSegment = new ArraySegment([1, 2, 3, 4]) + }); - // Ensure first client still works - await firstClient.PublishAsync( + await testEnvironment.Server.InjectApplicationMessage( + new InjectedMqttApplicationMessage( new MqttApplicationMessage { Topic = "TestTopic1", - PayloadSegment = new ArraySegment(new byte[] { 1, 2, 3, 4 }) + PayloadSegment = new ArraySegment([1, 2, 3, 4]) + })); + + await Task.Delay(1000); + + Assert.AreEqual(6, publishedCount); + Assert.AreEqual(4, firstClientReceivedCount); + Assert.AreEqual(2, secondClientReceivedCount); + + await server.StopAsync().ConfigureAwait(false); + } + + static async Task ConnectClientAsync(TestEnvironment testEnvironment, Func certValidator) + { + var clientOptionsBuilder = testEnvironment.ClientFactory.CreateClientOptionsBuilder(); + clientOptionsBuilder.WithClientId(Guid.NewGuid().ToString()) + .WithTcpServer("localhost", 8883) + .WithTlsOptions( + o => + { + o.WithSslProtocols(SslProtocols.Tls12).WithCertificateValidationHandler(certValidator); }); - await testEnvironment.Server.InjectApplicationMessage( - new InjectedMqttApplicationMessage( - new MqttApplicationMessage - { - Topic = "TestTopic1", - PayloadSegment = new ArraySegment(new byte[] { 1, 2, 3, 4 }) - })); + var clientOptions = clientOptionsBuilder.Build(); + return await testEnvironment.ConnectClient(clientOptions).ConfigureAwait(false); + } - await Task.Delay(1000); + sealed class CertificateProvider : ICertificateProvider + { + public X509Certificate2 CurrentCertificate { get; set; } - Assert.AreEqual(6, publishedCount); - Assert.AreEqual(4, firstClientReceivedCount); - Assert.AreEqual(2, secondClientReceivedCount); - - await server.StopAsync(); - } - - static async Task ConnectClientAsync(TestEnvironment testEnvironment, Func certValidator) + public X509Certificate2 GetCertificate() { - var clientOptionsBuilder = testEnvironment.ClientFactory.CreateClientOptionsBuilder(); - clientOptionsBuilder.WithClientId(Guid.NewGuid().ToString()) - .WithTcpServer("localhost", 8883) - .WithTlsOptions( - o => - { - o.WithSslProtocols(SslProtocols.Tls12).WithCertificateValidationHandler(certValidator); - }); - - var clientOptions = clientOptionsBuilder.Build(); - return await testEnvironment.ConnectClient(clientOptions); - } - - sealed class CertificateProvider : ICertificateProvider - { - public X509Certificate2 CurrentCertificate { get; set; } - - public X509Certificate2 GetCertificate() - { - return CurrentCertificate; - } + return CurrentCertificate; } } -} +} \ No newline at end of file diff --git a/Source/MQTTnet.Tests/Server/Topic_Alias_Tests.cs b/Source/MQTTnet.Tests/Server/Topic_Alias_Tests.cs index 024fca16..41a42daf 100644 --- a/Source/MQTTnet.Tests/Server/Topic_Alias_Tests.cs +++ b/Source/MQTTnet.Tests/Server/Topic_Alias_Tests.cs @@ -9,72 +9,68 @@ using Microsoft.VisualStudio.TestTools.UnitTesting; using MQTTnet.Formatter; using MQTTnet.Internal; -namespace MQTTnet.Tests.Server +namespace MQTTnet.Tests.Server; + +// ReSharper disable InconsistentNaming +[TestClass] +public sealed class Topic_Alias_Tests : BaseTestClass { - [TestClass] - public sealed class Topic_Alias_Tests : BaseTestClass + [TestMethod] + public async Task Server_Reports_Topic_Alias_Supported() { - [TestMethod] - public async Task Server_Reports_Topic_Alias_Supported() - { - using (var testEnvironment = CreateTestEnvironment()) - { - await testEnvironment.StartServer(); + using var testEnvironment = CreateTestEnvironment(); + await testEnvironment.StartServer(); - var client = testEnvironment.CreateClient(); + var client = testEnvironment.CreateClient(); - var connectResult = await client.ConnectAsync(new MqttClientOptionsBuilder() - .WithProtocolVersion(MqttProtocolVersion.V500) - .WithTcpServer("127.0.0.1", testEnvironment.ServerPort) - .Build()); + var connectResult = await client.ConnectAsync(new MqttClientOptionsBuilder() + .WithProtocolVersion(MqttProtocolVersion.V500) + .WithTcpServer("127.0.0.1", testEnvironment.ServerPort) + .Build()); - Assert.AreEqual(connectResult.TopicAliasMaximum, ushort.MaxValue); - } - } - - [TestMethod] - public async Task Publish_With_Topic_Alias() - { - using (var testEnvironment = CreateTestEnvironment()) - { - await testEnvironment.StartServer(); - - var receivedTopics = new List(); - - var c1 = await testEnvironment.ConnectClient(options => options.WithProtocolVersion(MqttProtocolVersion.V500)); - c1.ApplicationMessageReceivedAsync += e => - { - lock (receivedTopics) - { - receivedTopics.Add(e.ApplicationMessage.Topic); - } - - return CompletedTask.Instance; - }; - - await c1.SubscribeAsync("#"); - - var c2 = await testEnvironment.ConnectClient(options => options.WithProtocolVersion(MqttProtocolVersion.V500)); - - var message = new MqttApplicationMessage - { - Topic = "this_is_the_topic", - TopicAlias = 22 - }; - - await c2.PublishAsync(message); - - message.Topic = null; - - await c2.PublishAsync(message); - await c2.PublishAsync(message); - - await Task.Delay(500); - - Assert.AreEqual(3, receivedTopics.Count); - CollectionAssert.AllItemsAreNotNull(receivedTopics); - Assert.IsTrue(receivedTopics.All(t => t.Equals("this_is_the_topic"))); - } - } + Assert.AreEqual(connectResult.TopicAliasMaximum, ushort.MaxValue); } -} + + [TestMethod] + public async Task Publish_With_Topic_Alias() + { + using var testEnvironment = CreateTestEnvironment(); + await testEnvironment.StartServer(); + + var receivedTopics = new List(); + + var c1 = await testEnvironment.ConnectClient(options => options.WithProtocolVersion(MqttProtocolVersion.V500)); + c1.ApplicationMessageReceivedAsync += e => + { + lock (receivedTopics) + { + receivedTopics.Add(e.ApplicationMessage.Topic); + } + + return CompletedTask.Instance; + }; + + await c1.SubscribeAsync("#"); + + var c2 = await testEnvironment.ConnectClient(options => options.WithProtocolVersion(MqttProtocolVersion.V500)); + + var message = new MqttApplicationMessage + { + Topic = "this_is_the_topic", + TopicAlias = 22 + }; + + await c2.PublishAsync(message); + + message.Topic = null; + + await c2.PublishAsync(message); + await c2.PublishAsync(message); + + await Task.Delay(500); + + Assert.AreEqual(3, receivedTopics.Count); + CollectionAssert.AllItemsAreNotNull(receivedTopics); + Assert.IsTrue(receivedTopics.All(t => t.Equals("this_is_the_topic"))); + } +} \ No newline at end of file diff --git a/Source/MQTTnet.Tests/Server/Unsubscribe_Tests.cs b/Source/MQTTnet.Tests/Server/Unsubscribe_Tests.cs index 9f97ad88..01ef9ecd 100644 --- a/Source/MQTTnet.Tests/Server/Unsubscribe_Tests.cs +++ b/Source/MQTTnet.Tests/Server/Unsubscribe_Tests.cs @@ -8,52 +8,47 @@ using Microsoft.VisualStudio.TestTools.UnitTesting; using MQTTnet.Exceptions; using MQTTnet.Formatter; using MQTTnet.Internal; -using MQTTnet.Protocol; using MQTTnet.Server; -namespace MQTTnet.Tests.Server +namespace MQTTnet.Tests.Server; + +// ReSharper disable InconsistentNaming +[TestClass] +public sealed class Unsubscribe_Tests : BaseTestClass { - [TestClass] - public sealed class Unsubscribe_Tests : BaseTestClass + [TestMethod] + [ExpectedException(typeof(MqttClientDisconnectedException))] + public async Task Disconnect_While_Unsubscribing() { - [TestMethod] - [ExpectedException(typeof(MqttClientDisconnectedException))] - public async Task Disconnect_While_Unsubscribing() + using var testEnvironment = CreateTestEnvironment(); + var server = await testEnvironment.StartServer(); + + // The client will be disconnected directly after subscribing! + server.ClientUnsubscribedTopicAsync += ev => server.DisconnectClientAsync(ev.ClientId); + + var client = await testEnvironment.ConnectClient(); + await client.SubscribeAsync("#"); + await client.UnsubscribeAsync("#"); + } + + [TestMethod] + public async Task Intercept_Unsubscribe_With_User_Properties() + { + using var testEnvironment = CreateTestEnvironment(MqttProtocolVersion.V500); + var server = await testEnvironment.StartServer(); + + InterceptingUnsubscriptionEventArgs eventArgs = null; + server.InterceptingUnsubscriptionAsync += e => { - using (var testEnvironment = CreateTestEnvironment()) - { - var server = await testEnvironment.StartServer(); + eventArgs = e; + return CompletedTask.Instance; + }; - // The client will be disconnect directly after subscribing! - server.ClientUnsubscribedTopicAsync += ev => server.DisconnectClientAsync(ev.ClientId, MqttDisconnectReasonCode.NormalDisconnection); + var client = await testEnvironment.ConnectClient(); - var client = await testEnvironment.ConnectClient(); - await client.SubscribeAsync("#"); - await client.UnsubscribeAsync("#"); - } - } + var unsubscribeOptions = testEnvironment.ClientFactory.CreateUnsubscribeOptionsBuilder().WithTopicFilter("X").WithUserProperty("A", "1").Build(); + await client.UnsubscribeAsync(unsubscribeOptions); - [TestMethod] - public async Task Intercept_Unsubscribe_With_User_Properties() - { - using (var testEnvironment = CreateTestEnvironment(MqttProtocolVersion.V500)) - { - var server = await testEnvironment.StartServer(); - - InterceptingUnsubscriptionEventArgs eventArgs = null; - server.InterceptingUnsubscriptionAsync += e => - { - eventArgs = e; - return CompletedTask.Instance; - }; - - var client = await testEnvironment.ConnectClient(); - - var unsubscribeOptions = testEnvironment.ClientFactory.CreateUnsubscribeOptionsBuilder().WithTopicFilter("X").WithUserProperty("A", "1").Build(); - await client.UnsubscribeAsync(unsubscribeOptions); - - CollectionAssert.AreEqual(unsubscribeOptions.UserProperties.ToList(), eventArgs.UserProperties); - } - } + CollectionAssert.AreEqual(unsubscribeOptions.UserProperties.ToList(), eventArgs.UserProperties); } } \ No newline at end of file diff --git a/Source/MQTTnet.Tests/Server/User_Properties_Tests.cs b/Source/MQTTnet.Tests/Server/User_Properties_Tests.cs index 2a6f0713..a84849a9 100644 --- a/Source/MQTTnet.Tests/Server/User_Properties_Tests.cs +++ b/Source/MQTTnet.Tests/Server/User_Properties_Tests.cs @@ -2,64 +2,56 @@ // The .NET Foundation licenses this file to you under the MIT license. // See the LICENSE file in the project root for more information. -using System.Collections.Generic; using System.Threading; using System.Threading.Tasks; using Microsoft.VisualStudio.TestTools.UnitTesting; using MQTTnet.Formatter; -using MQTTnet.Implementations; using MQTTnet.Internal; using MQTTnet.Packets; using MQTTnet.Tests.Mockups; -namespace MQTTnet.Tests.Server +namespace MQTTnet.Tests.Server; + +// ReSharper disable InconsistentNaming +[TestClass] +public class Feature_Tests { - [TestClass] - public class Feature_Tests + public TestContext TestContext { get; set; } + + [TestMethod] + public async Task Use_User_Properties() { - public TestContext TestContext { get; set; } + using var testEnvironment = new TestEnvironment(TestContext); + await testEnvironment.StartServer(); - [TestMethod] - public async Task Use_User_Properties() + var sender = await testEnvironment.ConnectClient(new MqttClientOptionsBuilder().WithProtocolVersion(MqttProtocolVersion.V500)); + var receiver = await testEnvironment.ConnectClient(new MqttClientOptionsBuilder().WithProtocolVersion(MqttProtocolVersion.V500)); + + var message = new MqttApplicationMessageBuilder() + .WithTopic("A") + .WithUserProperty("x", "1") + .WithUserProperty("y", "2") + .WithUserProperty("z", "3") + .WithUserProperty("z", "4"); // z is here two times to test list of items + + await receiver.SubscribeAsync(new MqttClientSubscribeOptions { - using (var testEnvironment = new TestEnvironment(TestContext)) - { - await testEnvironment.StartServer(); + TopicFilters = [new MqttTopicFilter { Topic = "#" }] + }, CancellationToken.None); - var sender = await testEnvironment.ConnectClient(new MqttClientOptionsBuilder().WithProtocolVersion(MqttProtocolVersion.V500)); - var receiver = await testEnvironment.ConnectClient(new MqttClientOptionsBuilder().WithProtocolVersion(MqttProtocolVersion.V500)); + MqttApplicationMessage receivedMessage = null; + receiver.ApplicationMessageReceivedAsync += e => + { + receivedMessage = e.ApplicationMessage; + return CompletedTask.Instance; + }; - var message = new MqttApplicationMessageBuilder() - .WithTopic("A") - .WithUserProperty("x", "1") - .WithUserProperty("y", "2") - .WithUserProperty("z", "3") - .WithUserProperty("z", "4"); // z is here two times to test list of items + await sender.PublishAsync(message.Build(), CancellationToken.None); - await receiver.SubscribeAsync(new MqttClientSubscribeOptions - { - TopicFilters = new List - { - new MqttTopicFilter { Topic = "#" } - } - }, CancellationToken.None); + await Task.Delay(1000); - MqttApplicationMessage receivedMessage = null; - receiver.ApplicationMessageReceivedAsync += e => - { - receivedMessage = e.ApplicationMessage; - return CompletedTask.Instance; - }; - - await sender.PublishAsync(message.Build(), CancellationToken.None); - - await Task.Delay(1000); - - Assert.IsNotNull(receivedMessage); - Assert.AreEqual("A", receivedMessage.Topic); - Assert.AreEqual(4, receivedMessage.UserProperties.Count); - - } - } + Assert.IsNotNull(receivedMessage); + Assert.AreEqual("A", receivedMessage.Topic); + Assert.AreEqual(4, receivedMessage.UserProperties.Count); } -} +} \ No newline at end of file diff --git a/Source/MQTTnet.Tests/Server/Wildcard_Subscription_Available_Tests.cs b/Source/MQTTnet.Tests/Server/Wildcard_Subscription_Available_Tests.cs index 85b224d6..8f90f2ef 100644 --- a/Source/MQTTnet.Tests/Server/Wildcard_Subscription_Available_Tests.cs +++ b/Source/MQTTnet.Tests/Server/Wildcard_Subscription_Available_Tests.cs @@ -6,41 +6,37 @@ using System.Threading.Tasks; using Microsoft.VisualStudio.TestTools.UnitTesting; using MQTTnet.Formatter; -namespace MQTTnet.Tests.Server +namespace MQTTnet.Tests.Server; + +// ReSharper disable InconsistentNaming +[TestClass] +public sealed class Wildcard_Subscription_Available_Tests : BaseTestClass { - [TestClass] - public sealed class Wildcard_Subscription_Available_Tests : BaseTestClass + [TestMethod] + public async Task Server_Reports_Wildcard_Subscription_Available_Tests_Supported_V3() { - [TestMethod] - public async Task Server_Reports_Wildcard_Subscription_Available_Tests_Supported_V3() - { - using (var testEnvironment = CreateTestEnvironment()) - { - await testEnvironment.StartServer(); + using var testEnvironment = CreateTestEnvironment(); + await testEnvironment.StartServer(); - var client = testEnvironment.CreateClient(); - var connectResult = await client.ConnectAsync(testEnvironment.ClientFactory.CreateClientOptionsBuilder() - .WithProtocolVersion(MqttProtocolVersion.V311) - .WithTcpServer("127.0.0.1", testEnvironment.ServerPort).Build()); + var client = testEnvironment.CreateClient(); + var connectResult = await client.ConnectAsync(testEnvironment.ClientFactory.CreateClientOptionsBuilder() + .WithProtocolVersion(MqttProtocolVersion.V311) + .WithTcpServer("127.0.0.1", testEnvironment.ServerPort).Build()); - Assert.IsTrue(connectResult.WildcardSubscriptionAvailable); - } - } + Assert.IsTrue(connectResult.WildcardSubscriptionAvailable); + } - [TestMethod] - public async Task Server_Reports_Wildcard_Subscription_Available_Tests_Supported_V5() - { - using (var testEnvironment = CreateTestEnvironment(MqttProtocolVersion.V500)) - { - await testEnvironment.StartServer(); + [TestMethod] + public async Task Server_Reports_Wildcard_Subscription_Available_Tests_Supported_V5() + { + using var testEnvironment = CreateTestEnvironment(MqttProtocolVersion.V500); + await testEnvironment.StartServer(); - var client = testEnvironment.CreateClient(); - var connectResult = await client.ConnectAsync(testEnvironment.ClientFactory.CreateClientOptionsBuilder() - .WithProtocolVersion(MqttProtocolVersion.V500) - .WithTcpServer("127.0.0.1", testEnvironment.ServerPort).Build()); + var client = testEnvironment.CreateClient(); + var connectResult = await client.ConnectAsync(testEnvironment.ClientFactory.CreateClientOptionsBuilder() + .WithProtocolVersion(MqttProtocolVersion.V500) + .WithTcpServer("127.0.0.1", testEnvironment.ServerPort).Build()); - Assert.IsTrue(connectResult.WildcardSubscriptionAvailable); - } - } + Assert.IsTrue(connectResult.WildcardSubscriptionAvailable); } } \ No newline at end of file diff --git a/Source/MQTTnet.Tests/Server/Will_Tests.cs b/Source/MQTTnet.Tests/Server/Will_Tests.cs index 0e823bcd..e4e96ce0 100644 --- a/Source/MQTTnet.Tests/Server/Will_Tests.cs +++ b/Source/MQTTnet.Tests/Server/Will_Tests.cs @@ -3,78 +3,72 @@ using Microsoft.VisualStudio.TestTools.UnitTesting; using MQTTnet.Internal; using MQTTnet.Protocol; -namespace MQTTnet.Tests.Server +namespace MQTTnet.Tests.Server; + +// ReSharper disable InconsistentNaming +[TestClass] +public sealed class Will_Tests : BaseTestClass { - [TestClass] - public sealed class Will_Tests : BaseTestClass + [TestMethod] + public async Task Intercept_Will_Message() { - [TestMethod] - public async Task Intercept_Will_Message() + using var testEnvironment = CreateTestEnvironment(); + var server = await testEnvironment.StartServer().ConfigureAwait(false); + + MqttApplicationMessage willMessage = null; + server.InterceptingPublishAsync += eventArgs => { - using (var testEnvironment = CreateTestEnvironment()) - { - var server = await testEnvironment.StartServer().ConfigureAwait(false); + willMessage = eventArgs.ApplicationMessage; + return CompletedTask.Instance; + }; - MqttApplicationMessage willMessage = null; - server.InterceptingPublishAsync += eventArgs => - { - willMessage = eventArgs.ApplicationMessage; - return CompletedTask.Instance; - }; + await testEnvironment.ConnectClient(new MqttClientOptionsBuilder()).ConfigureAwait(false); + var clientOptions = new MqttClientOptionsBuilder().WithWillTopic("My/last/will").WithWillQualityOfServiceLevel(MqttQualityOfServiceLevel.AtMostOnce); + var takeOverClient = await testEnvironment.ConnectClient(clientOptions).ConfigureAwait(false); + takeOverClient.Dispose(); // Dispose will not send a DISCONNECT pattern first so the will message must be sent. - await testEnvironment.ConnectClient(new MqttClientOptionsBuilder()).ConfigureAwait(false); - var clientOptions = new MqttClientOptionsBuilder().WithWillTopic("My/last/will").WithWillQualityOfServiceLevel(MqttQualityOfServiceLevel.AtMostOnce); - var takeOverClient = await testEnvironment.ConnectClient(clientOptions).ConfigureAwait(false); - takeOverClient.Dispose(); // Dispose will not send a DISCONNECT pattern first so the will message must be sent. + await LongTestDelay().ConfigureAwait(false); - await LongTestDelay().ConfigureAwait(false); + Assert.IsNotNull(willMessage); + } - Assert.IsNotNull(willMessage); - } - } + [TestMethod] + public async Task Will_Message_Do_Not_Send_On_Clean_Disconnect() + { + using var testEnvironment = CreateTestEnvironment(); + await testEnvironment.StartServer(); - [TestMethod] - public async Task Will_Message_Do_Not_Send_On_Clean_Disconnect() - { - using (var testEnvironment = CreateTestEnvironment()) - { - await testEnvironment.StartServer(); + var receiver = await testEnvironment.ConnectClient().ConfigureAwait(false); - var receiver = await testEnvironment.ConnectClient().ConfigureAwait(false); + var receivedMessages = testEnvironment.CreateApplicationMessageHandler(receiver); - var receivedMessages = testEnvironment.CreateApplicationMessageHandler(receiver); + await receiver.SubscribeAsync(new MqttTopicFilterBuilder().WithTopic("#").Build()); - await receiver.SubscribeAsync(new MqttTopicFilterBuilder().WithTopic("#").Build()); + var clientOptions = new MqttClientOptionsBuilder().WithWillTopic("My/last/will"); + var sender = await testEnvironment.ConnectClient(clientOptions).ConfigureAwait(false); + await sender.DisconnectAsync().ConfigureAwait(false); - var clientOptions = new MqttClientOptionsBuilder().WithWillTopic("My/last/will"); - var sender = await testEnvironment.ConnectClient(clientOptions).ConfigureAwait(false); - await sender.DisconnectAsync().ConfigureAwait(false); + await LongTestDelay().ConfigureAwait(false); - await LongTestDelay().ConfigureAwait(false); + Assert.AreEqual(0, receivedMessages.ReceivedEventArgs.Count); + } - Assert.AreEqual(0, receivedMessages.ReceivedEventArgs.Count); - } - } + [TestMethod] + public async Task Will_Message_Send() + { + using var testEnvironment = CreateTestEnvironment(); + await testEnvironment.StartServer(); - [TestMethod] - public async Task Will_Message_Send() - { - using (var testEnvironment = CreateTestEnvironment()) - { - await testEnvironment.StartServer(); + var receiver = await testEnvironment.ConnectClient(new MqttClientOptionsBuilder()).ConfigureAwait(false); + var receivedMessages = testEnvironment.CreateApplicationMessageHandler(receiver); + await receiver.SubscribeAsync(new MqttTopicFilterBuilder().WithTopic("#").Build()); - var receiver = await testEnvironment.ConnectClient(new MqttClientOptionsBuilder()).ConfigureAwait(false); - var receivedMessages = testEnvironment.CreateApplicationMessageHandler(receiver); - await receiver.SubscribeAsync(new MqttTopicFilterBuilder().WithTopic("#").Build()); + var clientOptions = new MqttClientOptionsBuilder().WithWillTopic("My/last/will").WithWillQualityOfServiceLevel(MqttQualityOfServiceLevel.AtMostOnce); + var takeOverClient = await testEnvironment.ConnectClient(clientOptions).ConfigureAwait(false); + takeOverClient.Dispose(); // Dispose will not send a DISCONNECT pattern first so the will message must be sent. - var clientOptions = new MqttClientOptionsBuilder().WithWillTopic("My/last/will").WithWillQualityOfServiceLevel(MqttQualityOfServiceLevel.AtMostOnce); - var takeOverClient = await testEnvironment.ConnectClient(clientOptions).ConfigureAwait(false); - takeOverClient.Dispose(); // Dispose will not send a DISCONNECT pattern first so the will message must be sent. + await LongTestDelay().ConfigureAwait(false); - await LongTestDelay().ConfigureAwait(false); - - Assert.AreEqual(1, receivedMessages.ReceivedEventArgs.Count); - } - } + Assert.AreEqual(1, receivedMessages.ReceivedEventArgs.Count); } } \ No newline at end of file diff --git a/Source/MQTTnet.Tests/TopicFilterComparer_Tests.cs b/Source/MQTTnet.Tests/TopicFilterComparer_Tests.cs index 61f8987c..d7d6f7c2 100644 --- a/Source/MQTTnet.Tests/TopicFilterComparer_Tests.cs +++ b/Source/MQTTnet.Tests/TopicFilterComparer_Tests.cs @@ -3,162 +3,161 @@ // See the LICENSE file in the project root for more information. using Microsoft.VisualStudio.TestTools.UnitTesting; -using MQTTnet.Server; using MQTTnet.Server.Internal; -namespace MQTTnet.Tests +namespace MQTTnet.Tests; + +// ReSharper disable InconsistentNaming +[TestClass] +public sealed class MqttTopicFilterComparer_Tests { - [TestClass] - public sealed class MqttTopicFilterComparer_Tests + [TestMethod] + public void AllLevelsWildcardMatch() { - [TestMethod] - public void AllLevelsWildcardMatch() + CompareAndAssert("A/B/C/D", "#", MqttTopicFilterCompareResult.IsMatch); + } + + [TestMethod] + public void BeginningOneLevelWildcardMatch() + { + CompareAndAssert("A/B/C", "+/B/C", MqttTopicFilterCompareResult.IsMatch); + } + + [TestMethod] + public void Compare_UTF8_String_Match() + { + CompareAndAssert("öäüß", "öäüß", MqttTopicFilterCompareResult.IsMatch); + } + + [TestMethod] + public void Compare_UTF8_String_No_Match() + { + CompareAndAssert("ae", "ä", MqttTopicFilterCompareResult.NoMatch); + } + + [TestMethod] + public void DirectMatch() + { + CompareAndAssert("A/B/C", "A/B/C", MqttTopicFilterCompareResult.IsMatch); + } + + [TestMethod] + public void DirectNoMatch() + { + CompareAndAssert("A/B/X", "A/B/C", MqttTopicFilterCompareResult.NoMatch); + } + + [TestMethod] + public void EndMultipleLevelsWildcardMatch() + { + CompareAndAssert("A/B/C", "A/#", MqttTopicFilterCompareResult.IsMatch); + } + + [TestMethod] + public void EndMultipleLevelsWildcardMatchEmptyLevel() + { + CompareAndAssert("A/", "A/#", MqttTopicFilterCompareResult.IsMatch); + } + + [TestMethod] + public void EndMultipleLevelsWildcardNoMatch() + { + CompareAndAssert("A/B/C/D", "A/C/#", MqttTopicFilterCompareResult.NoMatch); + } + + [TestMethod] + public void EndOneLevelWildcardMatch() + { + CompareAndAssert("A/B/C", "A/B/+", MqttTopicFilterCompareResult.IsMatch); + } + + [TestMethod] + public void Hash_Match_With_Separator_Only() + { + //A Topic Name or Topic Filter consisting only of the ‘/’ character is valid + CompareAndAssert("/", "#", MqttTopicFilterCompareResult.IsMatch); + } + + [TestMethod] + public void MiddleOneLevelWildcardMatch() + { + CompareAndAssert("A/B/C", "A/+/C", MqttTopicFilterCompareResult.IsMatch); + } + + [TestMethod] + public void MiddleOneLevelWildcardNoMatch() + { + CompareAndAssert("A/B/C/D", "A/+/C", MqttTopicFilterCompareResult.NoMatch); + } + + [TestMethod] + public void MultiLevel_Sport() + { + // Tests from official MQTT spec (4.7.1.2 Multi-level wildcard) + CompareAndAssert("sport/tennis/player1", "sport/tennis/player1/#", MqttTopicFilterCompareResult.IsMatch); + CompareAndAssert("sport/tennis/player1/ranking", "sport/tennis/player1/#", MqttTopicFilterCompareResult.IsMatch); + CompareAndAssert("sport/tennis/player1/score/wimbledon", "sport/tennis/player1/#", MqttTopicFilterCompareResult.IsMatch); + + CompareAndAssert("sport/tennis/player1", "sport/tennis/+", MqttTopicFilterCompareResult.IsMatch); + CompareAndAssert("sport/tennis/player2", "sport/tennis/+", MqttTopicFilterCompareResult.IsMatch); + CompareAndAssert("sport/tennis/player1/ranking", "sport/tennis/+", MqttTopicFilterCompareResult.NoMatch); + + CompareAndAssert("sport", "sport/#", MqttTopicFilterCompareResult.IsMatch); + CompareAndAssert("sport", "sport/+", MqttTopicFilterCompareResult.NoMatch); + CompareAndAssert("sport/", "sport/+", MqttTopicFilterCompareResult.IsMatch); + } + + [TestMethod] + public void Plus_Match_With_Separator_Only() + { + //A Topic Name or Topic Filter consisting only of the ‘/’ character is valid + CompareAndAssert("A", "+", MqttTopicFilterCompareResult.IsMatch); + } + + [TestMethod] + public void Reserved_Multi_Level_Wildcard_Only() + { + CompareAndAssert("$SPECIAL/TOPIC", "#", MqttTopicFilterCompareResult.NoMatch); + } + + [TestMethod] + public void Reserved_Single_Level_Wildcard() + { + CompareAndAssert("$SYS/monitor/Clients", "$SYS/#", MqttTopicFilterCompareResult.IsMatch); + } + + [TestMethod] + public void Reserved_Single_Level_Wildcard_Prefix() + { + CompareAndAssert("$SYS/monitor/Clients", "+/monitor/Clients", MqttTopicFilterCompareResult.NoMatch); + } + + [TestMethod] + public void Reserved_Single_Level_Wildcard_Suffix() + { + CompareAndAssert("$SYS/monitor/Clients", "$SYS/monitor/+", MqttTopicFilterCompareResult.IsMatch); + } + + [TestMethod] + public void SingleLevel_Finance() + { + // Tests from official MQTT spec (4.7.1.3 Single level wildcard) + CompareAndAssert("/finance", "+/+", MqttTopicFilterCompareResult.IsMatch); + CompareAndAssert("/finance", "/+", MqttTopicFilterCompareResult.IsMatch); + CompareAndAssert("/finance", "+", MqttTopicFilterCompareResult.NoMatch); + } + + static void CompareAndAssert(string topic, string filter, MqttTopicFilterCompareResult expectedResult) + { + Assert.AreEqual(expectedResult, MqttTopicFilterComparer.Compare(topic, filter)); + + MqttTopicHash.Calculate(topic, out var topicHash, out _, out _); + MqttTopicHash.Calculate(filter, out var filterTopicHash, out var filterTopicHashMask, out _); + + if (expectedResult == MqttTopicFilterCompareResult.IsMatch) { - CompareAndAssert("A/B/C/D", "#", MqttTopicFilterCompareResult.IsMatch); - } - - [TestMethod] - public void BeginningOneLevelWildcardMatch() - { - CompareAndAssert("A/B/C", "+/B/C", MqttTopicFilterCompareResult.IsMatch); - } - - [TestMethod] - public void Compare_UTF8_String_Match() - { - CompareAndAssert("öäüß", "öäüß", MqttTopicFilterCompareResult.IsMatch); - } - - [TestMethod] - public void Compare_UTF8_String_No_Match() - { - CompareAndAssert("ae", "ä", MqttTopicFilterCompareResult.NoMatch); - } - - [TestMethod] - public void DirectMatch() - { - CompareAndAssert("A/B/C", "A/B/C", MqttTopicFilterCompareResult.IsMatch); - } - - [TestMethod] - public void DirectNoMatch() - { - CompareAndAssert("A/B/X", "A/B/C", MqttTopicFilterCompareResult.NoMatch); - } - - [TestMethod] - public void EndMultipleLevelsWildcardMatch() - { - CompareAndAssert("A/B/C", "A/#", MqttTopicFilterCompareResult.IsMatch); - } - - [TestMethod] - public void EndMultipleLevelsWildcardMatchEmptyLevel() - { - CompareAndAssert("A/", "A/#", MqttTopicFilterCompareResult.IsMatch); - } - - [TestMethod] - public void EndMultipleLevelsWildcardNoMatch() - { - CompareAndAssert("A/B/C/D", "A/C/#", MqttTopicFilterCompareResult.NoMatch); - } - - [TestMethod] - public void EndOneLevelWildcardMatch() - { - CompareAndAssert("A/B/C", "A/B/+", MqttTopicFilterCompareResult.IsMatch); - } - - [TestMethod] - public void Hash_Match_With_Separator_Only() - { - //A Topic Name or Topic Filter consisting only of the ‘/’ character is valid - CompareAndAssert("/", "#", MqttTopicFilterCompareResult.IsMatch); - } - - [TestMethod] - public void MiddleOneLevelWildcardMatch() - { - CompareAndAssert("A/B/C", "A/+/C", MqttTopicFilterCompareResult.IsMatch); - } - - [TestMethod] - public void MiddleOneLevelWildcardNoMatch() - { - CompareAndAssert("A/B/C/D", "A/+/C", MqttTopicFilterCompareResult.NoMatch); - } - - [TestMethod] - public void MultiLevel_Sport() - { - // Tests from official MQTT spec (4.7.1.2 Multi-level wildcard) - CompareAndAssert("sport/tennis/player1", "sport/tennis/player1/#", MqttTopicFilterCompareResult.IsMatch); - CompareAndAssert("sport/tennis/player1/ranking", "sport/tennis/player1/#", MqttTopicFilterCompareResult.IsMatch); - CompareAndAssert("sport/tennis/player1/score/wimbledon", "sport/tennis/player1/#", MqttTopicFilterCompareResult.IsMatch); - - CompareAndAssert("sport/tennis/player1", "sport/tennis/+", MqttTopicFilterCompareResult.IsMatch); - CompareAndAssert("sport/tennis/player2", "sport/tennis/+", MqttTopicFilterCompareResult.IsMatch); - CompareAndAssert("sport/tennis/player1/ranking", "sport/tennis/+", MqttTopicFilterCompareResult.NoMatch); - - CompareAndAssert("sport", "sport/#", MqttTopicFilterCompareResult.IsMatch); - CompareAndAssert("sport", "sport/+", MqttTopicFilterCompareResult.NoMatch); - CompareAndAssert("sport/", "sport/+", MqttTopicFilterCompareResult.IsMatch); - } - - [TestMethod] - public void Plus_Match_With_Separator_Only() - { - //A Topic Name or Topic Filter consisting only of the ‘/’ character is valid - CompareAndAssert("A", "+", MqttTopicFilterCompareResult.IsMatch); - } - - [TestMethod] - public void Reserved_Multi_Level_Wildcard_Only() - { - CompareAndAssert("$SPECIAL/TOPIC", "#", MqttTopicFilterCompareResult.NoMatch); - } - - [TestMethod] - public void Reserved_Single_Level_Wildcard() - { - CompareAndAssert("$SYS/monitor/Clients", "$SYS/#", MqttTopicFilterCompareResult.IsMatch); - } - - [TestMethod] - public void Reserved_Single_Level_Wildcard_Prefix() - { - CompareAndAssert("$SYS/monitor/Clients", "+/monitor/Clients", MqttTopicFilterCompareResult.NoMatch); - } - - [TestMethod] - public void Reserved_Single_Level_Wildcard_Suffix() - { - CompareAndAssert("$SYS/monitor/Clients", "$SYS/monitor/+", MqttTopicFilterCompareResult.IsMatch); - } - - [TestMethod] - public void SingleLevel_Finance() - { - // Tests from official MQTT spec (4.7.1.3 Single level wildcard) - CompareAndAssert("/finance", "+/+", MqttTopicFilterCompareResult.IsMatch); - CompareAndAssert("/finance", "/+", MqttTopicFilterCompareResult.IsMatch); - CompareAndAssert("/finance", "+", MqttTopicFilterCompareResult.NoMatch); - } - - static void CompareAndAssert(string topic, string filter, MqttTopicFilterCompareResult expectedResult) - { - Assert.AreEqual(expectedResult, MqttTopicFilterComparer.Compare(topic, filter)); - - MqttTopicHash.Calculate(topic, out var topicHash, out _, out _); - MqttTopicHash.Calculate(filter, out var filterTopicHash, out var filterTopicHashMask, out _); - - if (expectedResult == MqttTopicFilterCompareResult.IsMatch) - { - // If it matches then the hash evaluation should also indicate a match - Assert.IsTrue((topicHash & filterTopicHashMask) == filterTopicHash, "Incorrect topic hash (is equal)"); - } + // If it matches then the hash evaluation should also indicate a match + Assert.IsTrue((topicHash & filterTopicHashMask) == filterTopicHash, "Incorrect topic hash (is equal)"); } } } \ No newline at end of file diff --git a/Source/MQTTnet.Tests/TopicGenerator.cs b/Source/MQTTnet.Tests/TopicGenerator.cs index d623d107..ee413ab7 100644 --- a/Source/MQTTnet.Tests/TopicGenerator.cs +++ b/Source/MQTTnet.Tests/TopicGenerator.cs @@ -2,92 +2,91 @@ using System; using System.Collections.Generic; using MQTTnet.Extensions.TopicTemplate; -namespace MQTTnet.Tests +namespace MQTTnet.Tests; + +public class TopicGenerator { - public class TopicGenerator + public static void Generate( + int numPublishers, int numTopicsPerPublisher, + out Dictionary> topicsByPublisher, + out Dictionary> singleWildcardTopicsByPublisher, + out Dictionary> multiWildcardTopicsByPublisher + ) { - public static void Generate( - int numPublishers, int numTopicsPerPublisher, - out Dictionary> topicsByPublisher, - out Dictionary> singleWildcardTopicsByPublisher, - out Dictionary> multiWildcardTopicsByPublisher - ) + topicsByPublisher = new Dictionary>(); + singleWildcardTopicsByPublisher = new Dictionary>(); + multiWildcardTopicsByPublisher = new Dictionary>(); + + // Find some reasonable distribution across three topic levels + var topicsPerLevel = (int)Math.Pow(numTopicsPerPublisher, (1.0 / 3.0)); + if (topicsPerLevel <= 0) { - topicsByPublisher = new Dictionary>(); - singleWildcardTopicsByPublisher = new Dictionary>(); - multiWildcardTopicsByPublisher = new Dictionary>(); + topicsPerLevel = 1; + } - // Find some reasonable distribution across three topic levels - var topicsPerLevel = (int)Math.Pow(numTopicsPerPublisher, (1.0 / 3.0)); - if (topicsPerLevel <= 0) + int numLevel1Topics = topicsPerLevel; + int numLevel2Topics = topicsPerLevel; + + var maxNumLevel3Topics = 1 + (int)((double)numTopicsPerPublisher / numLevel1Topics / numLevel2Topics); + if (maxNumLevel3Topics <= 0) + { + maxNumLevel3Topics = 1; + } + + MqttTopicTemplate baseTemplate = new MqttTopicTemplate("{publisher}/{building}/{level}/{sensor}"); + + for (var p = 0; p < numPublishers; ++p) + { + int publisherTopicCount = 0; + var publisherName = "pub" + p; + var publisherTemplate = baseTemplate + .WithParameter("publisher", publisherName); + + for (var l1 = 0; l1 < numLevel1Topics; ++l1) { - topicsPerLevel = 1; - } - - int numLevel1Topics = topicsPerLevel; - int numLevel2Topics = topicsPerLevel; - - var maxNumLevel3Topics = 1 + (int)((double)numTopicsPerPublisher / numLevel1Topics / numLevel2Topics); - if (maxNumLevel3Topics <= 0) - { - maxNumLevel3Topics = 1; - } - - MqttTopicTemplate baseTemplate = new MqttTopicTemplate("{publisher}/{building}/{level}/{sensor}"); - - for (var p = 0; p < numPublishers; ++p) - { - int publisherTopicCount = 0; - var publisherName = "pub" + p; - var publisherTemplate = baseTemplate - .WithParameter("publisher", publisherName); - - for (var l1 = 0; l1 < numLevel1Topics; ++l1) + var l1Template = publisherTemplate.WithParameter("building", "building" + (l1 + 1)); + for (var l2 = 0; l2 < numLevel2Topics; ++l2) { - var l1Template = publisherTemplate.WithParameter("building", "building" + (l1 + 1)); - for (var l2 = 0; l2 < numLevel2Topics; ++l2) + var l2Template = l1Template.WithParameter("level", "level" + (l2 + 1)); + for (var l3 = 0; l3 < maxNumLevel3Topics; ++l3) { - var l2Template = l1Template.WithParameter("level", "level" + (l2 + 1)); - for (var l3 = 0; l3 < maxNumLevel3Topics; ++l3) + if (publisherTopicCount >= numTopicsPerPublisher) + break; + + var l3Template = l2Template + .WithParameter("sensor", "sensor" + (l3 + 1)); + AddPublisherTopic(publisherName, l3Template.TopicFilter, topicsByPublisher); + + if (l2 == 0) { - if (publisherTopicCount >= numTopicsPerPublisher) - break; - - var l3Template = l2Template - .WithParameter("sensor", "sensor" + (l3 + 1)); - AddPublisherTopic(publisherName, l3Template.TopicFilter, topicsByPublisher); - - if (l2 == 0) - { - var singleWildcardTopic = l1Template - .WithParameter("sensor", "sensor" + (l3 + 1)) - .TopicFilter; - AddPublisherTopic(publisherName, singleWildcardTopic, singleWildcardTopicsByPublisher); - } - if ((l1 == 0) && (l3 == 0)) - { - var multiWildcardTopic = publisherTemplate - .WithParameter("level", "level" + (l2 + 1)) - .TopicFilter; - AddPublisherTopic(publisherName, multiWildcardTopic, multiWildcardTopicsByPublisher); - } - - ++publisherTopicCount; + var singleWildcardTopic = l1Template + .WithParameter("sensor", "sensor" + (l3 + 1)) + .TopicFilter; + AddPublisherTopic(publisherName, singleWildcardTopic, singleWildcardTopicsByPublisher); } + if ((l1 == 0) && (l3 == 0)) + { + var multiWildcardTopic = publisherTemplate + .WithParameter("level", "level" + (l2 + 1)) + .TopicFilter; + AddPublisherTopic(publisherName, multiWildcardTopic, multiWildcardTopicsByPublisher); + } + + ++publisherTopicCount; } } } } - - static void AddPublisherTopic(string publisherName, string topic, Dictionary> topicsByPublisher) - { - List topicList; - if (!topicsByPublisher.TryGetValue(publisherName, out topicList)) - { - topicList = new List(); - topicsByPublisher.Add(publisherName, topicList); - } - topicList.Add(topic); - } } -} + + static void AddPublisherTopic(string publisherName, string topic, Dictionary> topicsByPublisher) + { + List topicList; + if (!topicsByPublisher.TryGetValue(publisherName, out topicList)) + { + topicList = new List(); + topicsByPublisher.Add(publisherName, topicList); + } + topicList.Add(topic); + } +} \ No newline at end of file diff --git a/Source/MQTTnet/Adapter/MqttChannelAdapter.cs b/Source/MQTTnet/Adapter/MqttChannelAdapter.cs index 28d72bcf..724d47df 100644 --- a/Source/MQTTnet/Adapter/MqttChannelAdapter.cs +++ b/Source/MQTTnet/Adapter/MqttChannelAdapter.cs @@ -74,7 +74,7 @@ public sealed class MqttChannelAdapter : Disposable, IMqttChannelAdapter */ var timeout = new TaskCompletionSource(); - using (cancellationToken.Register(() => timeout.TrySetResult(null))) + await using (cancellationToken.Register(() => timeout.TrySetResult(null))) { var connectTask = Task.Run( async () => diff --git a/Source/MQTTnet/Adapter/MqttPacketInspector.cs b/Source/MQTTnet/Adapter/MqttPacketInspector.cs index f4ea7753..63296840 100644 --- a/Source/MQTTnet/Adapter/MqttPacketInspector.cs +++ b/Source/MQTTnet/Adapter/MqttPacketInspector.cs @@ -35,11 +35,7 @@ public sealed class MqttPacketInspector return; } - if (_receivedPacketBuffer == null) - { - _receivedPacketBuffer = new MemoryStream(); - } - + _receivedPacketBuffer ??= new MemoryStream(); _receivedPacketBuffer?.SetLength(0); } diff --git a/Source/MQTTnet/Adapter/ReceivedMqttPacket.cs b/Source/MQTTnet/Adapter/ReceivedMqttPacket.cs index f290e565..de49b9ba 100644 --- a/Source/MQTTnet/Adapter/ReceivedMqttPacket.cs +++ b/Source/MQTTnet/Adapter/ReceivedMqttPacket.cs @@ -8,7 +8,7 @@ namespace MQTTnet.Adapter; public readonly struct ReceivedMqttPacket { - public static readonly ReceivedMqttPacket Empty = new(); + public static readonly ReceivedMqttPacket Empty; public ReceivedMqttPacket(byte fixedHeader, ArraySegment body, int totalLength) { diff --git a/Source/MQTTnet/Certificates/BlobCertificateProvider.cs b/Source/MQTTnet/Certificates/BlobCertificateProvider.cs index be2024e2..7675125c 100644 --- a/Source/MQTTnet/Certificates/BlobCertificateProvider.cs +++ b/Source/MQTTnet/Certificates/BlobCertificateProvider.cs @@ -5,28 +5,22 @@ using System; using System.Security.Cryptography.X509Certificates; -namespace MQTTnet.Certificates +namespace MQTTnet.Certificates; + +public class BlobCertificateProvider(byte[] blob) : ICertificateProvider { - public class BlobCertificateProvider : ICertificateProvider + public byte[] Blob { get; } = blob ?? throw new ArgumentNullException(nameof(blob)); + + public string Password { get; set; } + + public X509Certificate2 GetCertificate() { - public BlobCertificateProvider(byte[] blob) + if (string.IsNullOrEmpty(Password)) { - Blob = blob ?? throw new ArgumentNullException(nameof(blob)); + // Use a different overload when no password is specified. Otherwise, the constructor will fail. + return new X509Certificate2(Blob); } - public byte[] Blob { get; } - - public string Password { get; set; } - - public X509Certificate2 GetCertificate() - { - if (string.IsNullOrEmpty(Password)) - { - // Use a different overload when no password is specified. Otherwise the constructor will fail. - return new X509Certificate2(Blob); - } - - return new X509Certificate2(Blob, Password); - } + return new X509Certificate2(Blob, Password); } -} +} \ No newline at end of file diff --git a/Source/MQTTnet/Certificates/ICertificateProvider.cs b/Source/MQTTnet/Certificates/ICertificateProvider.cs index 58b4a908..99ae9530 100644 --- a/Source/MQTTnet/Certificates/ICertificateProvider.cs +++ b/Source/MQTTnet/Certificates/ICertificateProvider.cs @@ -4,10 +4,9 @@ using System.Security.Cryptography.X509Certificates; -namespace MQTTnet.Certificates +namespace MQTTnet.Certificates; + +public interface ICertificateProvider { - public interface ICertificateProvider - { - X509Certificate2 GetCertificate(); - } -} + X509Certificate2 GetCertificate(); +} \ No newline at end of file diff --git a/Source/MQTTnet/Certificates/X509CertificateProvider.cs b/Source/MQTTnet/Certificates/X509CertificateProvider.cs index e62f7f32..62463dc4 100644 --- a/Source/MQTTnet/Certificates/X509CertificateProvider.cs +++ b/Source/MQTTnet/Certificates/X509CertificateProvider.cs @@ -5,20 +5,14 @@ using System; using System.Security.Cryptography.X509Certificates; -namespace MQTTnet.Certificates +namespace MQTTnet.Certificates; + +public class X509CertificateProvider(X509Certificate2 certificate) : ICertificateProvider { - public class X509CertificateProvider : ICertificateProvider + readonly X509Certificate2 _certificate = certificate ?? throw new ArgumentNullException(nameof(certificate)); + + public X509Certificate2 GetCertificate() { - readonly X509Certificate2 _certificate; - - public X509CertificateProvider(X509Certificate2 certificate) - { - _certificate = certificate ?? throw new ArgumentNullException(nameof(certificate)); - } - - public X509Certificate2 GetCertificate() - { - return _certificate; - } + return _certificate; } -} +} \ No newline at end of file diff --git a/Source/MQTTnet/Connecting/MqttClientConnectResult.cs b/Source/MQTTnet/Connecting/MqttClientConnectResult.cs index a64a4c29..66cf0300 100644 --- a/Source/MQTTnet/Connecting/MqttClientConnectResult.cs +++ b/Source/MQTTnet/Connecting/MqttClientConnectResult.cs @@ -14,57 +14,57 @@ public sealed class MqttClientConnectResult /// Gets the client identifier which was chosen by the server. /// MQTTv5 only. /// - public string AssignedClientIdentifier { get; internal set; } + public string AssignedClientIdentifier { get; init; } /// /// Gets the authentication data. /// MQTTv5 only. /// - public byte[] AuthenticationData { get; internal set; } + public byte[] AuthenticationData { get; init; } /// /// Gets the authentication method. /// MQTTv5 only. /// - public string AuthenticationMethod { get; internal set; } + public string AuthenticationMethod { get; init; } /// /// Gets a value indicating whether a session was already available or not. /// - public bool IsSessionPresent { get; internal set; } + public bool IsSessionPresent { get; init; } - public uint? MaximumPacketSize { get; internal set; } + public uint? MaximumPacketSize { get; init; } /// /// Gets the maximum QoS which is supported by the server. /// MQTTv5 only. /// - public MqttQualityOfServiceLevel MaximumQoS { get; internal set; } + public MqttQualityOfServiceLevel MaximumQoS { get; init; } /// /// Gets the reason string. /// MQTTv5 only. /// - public string ReasonString { get; internal set; } + public string ReasonString { get; init; } - public ushort? ReceiveMaximum { get; internal set; } + public ushort? ReceiveMaximum { get; init; } /// /// Gets the response information. /// MQTTv5 only. /// - public string ResponseInformation { get; internal set; } + public string ResponseInformation { get; init; } /// /// Gets the result code. /// - public MqttClientConnectResultCode ResultCode { get; internal set; } + public MqttClientConnectResultCode ResultCode { get; init; } /// /// Gets whether the server supports retained messages. /// MQTTv5 only. /// - public bool RetainAvailable { get; internal set; } + public bool RetainAvailable { get; init; } /// /// MQTTv5 only. @@ -72,33 +72,33 @@ public sealed class MqttClientConnectResult /// keep alive interval from the client CONNECT packet. /// A value of 0 indicates that the feature is not used. /// - public ushort ServerKeepAlive { get; internal set; } + public ushort ServerKeepAlive { get; init; } /// /// Gets an alternate server which should be used instead of the current one. /// MQTTv5 only. /// - public string ServerReference { get; internal set; } + public string ServerReference { get; init; } - public uint? SessionExpiryInterval { get; internal set; } + public uint? SessionExpiryInterval { get; init; } /// /// Gets a value indicating whether the shared subscriptions are available or not. /// MQTTv5 only. /// - public bool SharedSubscriptionAvailable { get; internal set; } + public bool SharedSubscriptionAvailable { get; init; } /// /// Gets a value indicating whether the subscription identifiers are available or not. /// MQTTv5 only. /// - public bool SubscriptionIdentifiersAvailable { get; internal set; } + public bool SubscriptionIdentifiersAvailable { get; init; } /// /// Gets the maximum value for a topic alias. 0 means not supported. /// MQTTv5 only. /// - public ushort TopicAliasMaximum { get; internal set; } + public ushort TopicAliasMaximum { get; init; } /// /// Gets the user properties. @@ -109,11 +109,11 @@ public sealed class MqttClientConnectResult /// The feature is very similar to the HTTP header concept. /// MQTTv5 only. /// - public List UserProperties { get; internal set; } + public List UserProperties { get; init; } /// /// Gets a value indicating whether wildcards can be used in subscriptions at the current server. /// MQTTv5 only. /// - public bool WildcardSubscriptionAvailable { get; internal set; } + public bool WildcardSubscriptionAvailable { get; init; } } diff --git a/Source/MQTTnet/Connecting/MqttClientConnectResultFactory.cs b/Source/MQTTnet/Connecting/MqttClientConnectResultFactory.cs index b9e44273..373b4bc5 100644 --- a/Source/MQTTnet/Connecting/MqttClientConnectResultFactory.cs +++ b/Source/MQTTnet/Connecting/MqttClientConnectResultFactory.cs @@ -10,9 +10,9 @@ using MQTTnet.Protocol; namespace MQTTnet; -public sealed class MqttClientConnectResultFactory +public static class MqttClientConnectResultFactory { - public MqttClientConnectResult Create(MqttConnAckPacket connAckPacket, MqttProtocolVersion protocolVersion) + public static MqttClientConnectResult Create(MqttConnAckPacket connAckPacket, MqttProtocolVersion protocolVersion) { ArgumentNullException.ThrowIfNull(connAckPacket); @@ -26,41 +26,16 @@ public sealed class MqttClientConnectResultFactory static MqttClientConnectResultCode ConvertReturnCodeToResultCode(MqttConnectReturnCode connectReturnCode) { - switch (connectReturnCode) + return connectReturnCode switch { - case MqttConnectReturnCode.ConnectionAccepted: - { - return MqttClientConnectResultCode.Success; - } - - case MqttConnectReturnCode.ConnectionRefusedUnacceptableProtocolVersion: - { - return MqttClientConnectResultCode.UnsupportedProtocolVersion; - } - - case MqttConnectReturnCode.ConnectionRefusedNotAuthorized: - { - return MqttClientConnectResultCode.NotAuthorized; - } - - case MqttConnectReturnCode.ConnectionRefusedBadUsernameOrPassword: - { - return MqttClientConnectResultCode.BadUserNameOrPassword; - } - - case MqttConnectReturnCode.ConnectionRefusedIdentifierRejected: - { - return MqttClientConnectResultCode.ClientIdentifierNotValid; - } - - case MqttConnectReturnCode.ConnectionRefusedServerUnavailable: - { - return MqttClientConnectResultCode.ServerUnavailable; - } - - default: - throw new MqttProtocolViolationException("Received unexpected return code."); - } + MqttConnectReturnCode.ConnectionAccepted => MqttClientConnectResultCode.Success, + MqttConnectReturnCode.ConnectionRefusedUnacceptableProtocolVersion => MqttClientConnectResultCode.UnsupportedProtocolVersion, + MqttConnectReturnCode.ConnectionRefusedNotAuthorized => MqttClientConnectResultCode.NotAuthorized, + MqttConnectReturnCode.ConnectionRefusedBadUsernameOrPassword => MqttClientConnectResultCode.BadUserNameOrPassword, + MqttConnectReturnCode.ConnectionRefusedIdentifierRejected => MqttClientConnectResultCode.ClientIdentifierNotValid, + MqttConnectReturnCode.ConnectionRefusedServerUnavailable => MqttClientConnectResultCode.ServerUnavailable, + _ => throw new MqttProtocolViolationException("Received unexpected return code.") + }; } static MqttClientConnectResult CreateForMqtt311(MqttConnAckPacket connAckPacket) diff --git a/Source/MQTTnet/Diagnostics/Logger/IMqttNetLogger.cs b/Source/MQTTnet/Diagnostics/Logger/IMqttNetLogger.cs index a4bab322..e1c98aa3 100644 --- a/Source/MQTTnet/Diagnostics/Logger/IMqttNetLogger.cs +++ b/Source/MQTTnet/Diagnostics/Logger/IMqttNetLogger.cs @@ -4,12 +4,11 @@ using System; -namespace MQTTnet.Diagnostics.Logger +namespace MQTTnet.Diagnostics.Logger; + +public interface IMqttNetLogger { - public interface IMqttNetLogger - { - bool IsEnabled { get; } - - void Publish(MqttNetLogLevel logLevel, string source, string message, object[] parameters, Exception exception); - } -} + bool IsEnabled { get; } + + void Publish(MqttNetLogLevel level, string source, string message, object[] parameters, Exception exception); +} \ No newline at end of file diff --git a/Source/MQTTnet/Diagnostics/Logger/MqttNetEventLogger.cs b/Source/MQTTnet/Diagnostics/Logger/MqttNetEventLogger.cs index 15c7b3aa..8361ac06 100644 --- a/Source/MQTTnet/Diagnostics/Logger/MqttNetEventLogger.cs +++ b/Source/MQTTnet/Diagnostics/Logger/MqttNetEventLogger.cs @@ -4,61 +4,57 @@ using System; -namespace MQTTnet.Diagnostics.Logger +namespace MQTTnet.Diagnostics.Logger; + +/// +/// This logger fires an event when a new message was published. +/// +public sealed class MqttNetEventLogger(string logId = null) : IMqttNetLogger { - /// - /// This logger fires an event when a new message was published. - /// - public sealed class MqttNetEventLogger : IMqttNetLogger + public event EventHandler LogMessagePublished; + + public bool IsEnabled => LogMessagePublished != null; + + public string LogId { get; } = logId; + + public IFormatProvider FormatProvider { get; set; } + + public void Publish(MqttNetLogLevel level, string source, string message, object[] parameters, Exception exception) { - public MqttNetEventLogger(string logId = null) + var eventHandler = LogMessagePublished; + if (eventHandler == null) { - LogId = logId; + // No listener is attached so we can step out. + // Keep a reference to the handler because the handler + // might be null after preparing the message. + return; } - public event EventHandler LogMessagePublished; - - public bool IsEnabled => LogMessagePublished != null; - - public string LogId { get; } - - public void Publish(MqttNetLogLevel level, string source, string message, object[] parameters, Exception exception) + if (parameters?.Length > 0 && message?.Length > 0) { - var eventHandler = LogMessagePublished; - if (eventHandler == null) + try { - // No listener is attached so we can step out. - // Keep a reference to the handler because the handler - // might be null after preparing the message. - return; + message = string.Format(FormatProvider, message, parameters); } - - if (parameters?.Length > 0 && message?.Length > 0) + catch (FormatException) { - try - { - message = string.Format(message, parameters); - } - catch (FormatException) - { - message = "MESSAGE FORMAT INVALID: " + message; - } + message = "MESSAGE FORMAT INVALID: " + message; } - - // We only use UTC here to improve performance. Using a local date time - // would require to load the time zone settings! - var logMessage = new MqttNetLogMessage - { - LogId = LogId, - Timestamp = DateTime.UtcNow, - Source = source, - ThreadId = Environment.CurrentManagedThreadId, - Level = level, - Message = message, - Exception = exception - }; - - eventHandler.Invoke(this, new MqttNetLogMessagePublishedEventArgs(logMessage)); } + + // We only use UTC here to improve performance. Using a local date time + // would require to load the time zone settings! + var logMessage = new MqttNetLogMessage + { + LogId = LogId, + Timestamp = DateTime.UtcNow, + Source = source, + ThreadId = Environment.CurrentManagedThreadId, + Level = level, + Message = message, + Exception = exception + }; + + eventHandler(this, new MqttNetLogMessagePublishedEventArgs(logMessage)); } } \ No newline at end of file diff --git a/Source/MQTTnet/Diagnostics/Logger/MqttNetLogLevel.cs b/Source/MQTTnet/Diagnostics/Logger/MqttNetLogLevel.cs index b848b89f..58cef6de 100644 --- a/Source/MQTTnet/Diagnostics/Logger/MqttNetLogLevel.cs +++ b/Source/MQTTnet/Diagnostics/Logger/MqttNetLogLevel.cs @@ -2,16 +2,15 @@ // The .NET Foundation licenses this file to you under the MIT license. // See the LICENSE file in the project root for more information. -namespace MQTTnet.Diagnostics.Logger +namespace MQTTnet.Diagnostics.Logger; + +public enum MqttNetLogLevel { - public enum MqttNetLogLevel - { - Verbose, + Verbose, - Info, + Info, - Warning, + Warning, - Error - } -} + Error +} \ No newline at end of file diff --git a/Source/MQTTnet/Diagnostics/Logger/MqttNetLogMessage.cs b/Source/MQTTnet/Diagnostics/Logger/MqttNetLogMessage.cs index aea7480e..b6d928f7 100644 --- a/Source/MQTTnet/Diagnostics/Logger/MqttNetLogMessage.cs +++ b/Source/MQTTnet/Diagnostics/Logger/MqttNetLogMessage.cs @@ -4,33 +4,32 @@ using System; -namespace MQTTnet.Diagnostics.Logger +namespace MQTTnet.Diagnostics.Logger; + +public sealed class MqttNetLogMessage { - public sealed class MqttNetLogMessage + public string LogId { get; set; } + + public DateTime Timestamp { get; set; } + + public int ThreadId { get; set; } + + public string Source { get; set; } + + public MqttNetLogLevel Level { get; set; } + + public string Message { get; set; } + + public Exception Exception { get; set; } + + public override string ToString() { - public string LogId { get; set; } - - public DateTime Timestamp { get; set; } - - public int ThreadId { get; set; } - - public string Source { get; set; } - - public MqttNetLogLevel Level { get; set; } - - public string Message { get; set; } - - public Exception Exception { get; set; } - - public override string ToString() + var result = $"[{Timestamp:O}] [{LogId}] [{ThreadId}] [{Source}] [{Level}]: {Message}"; + if (Exception != null) { - var result = $"[{Timestamp:O}] [{LogId}] [{ThreadId}] [{Source}] [{Level}]: {Message}"; - if (Exception != null) - { - result += Environment.NewLine + Exception; - } - - return result; + result += Environment.NewLine + Exception; } + + return result; } -} +} \ No newline at end of file diff --git a/Source/MQTTnet/Diagnostics/Logger/MqttNetLogMessagePublishedEventArgs.cs b/Source/MQTTnet/Diagnostics/Logger/MqttNetLogMessagePublishedEventArgs.cs index c5b206d0..ca2674f2 100644 --- a/Source/MQTTnet/Diagnostics/Logger/MqttNetLogMessagePublishedEventArgs.cs +++ b/Source/MQTTnet/Diagnostics/Logger/MqttNetLogMessagePublishedEventArgs.cs @@ -4,15 +4,9 @@ using System; -namespace MQTTnet.Diagnostics.Logger -{ - public sealed class MqttNetLogMessagePublishedEventArgs : EventArgs - { - public MqttNetLogMessagePublishedEventArgs(MqttNetLogMessage logMessage) - { - LogMessage = logMessage ?? throw new ArgumentNullException(nameof(logMessage)); - } +namespace MQTTnet.Diagnostics.Logger; - public MqttNetLogMessage LogMessage { get; } - } -} +public sealed class MqttNetLogMessagePublishedEventArgs(MqttNetLogMessage logMessage) : EventArgs +{ + public MqttNetLogMessage LogMessage { get; } = logMessage ?? throw new ArgumentNullException(nameof(logMessage)); +} \ No newline at end of file diff --git a/Source/MQTTnet/Diagnostics/Logger/MqttNetNullLogger.cs b/Source/MQTTnet/Diagnostics/Logger/MqttNetNullLogger.cs index ad3a8f12..e1140a4b 100644 --- a/Source/MQTTnet/Diagnostics/Logger/MqttNetNullLogger.cs +++ b/Source/MQTTnet/Diagnostics/Logger/MqttNetNullLogger.cs @@ -4,24 +4,18 @@ using System; -namespace MQTTnet.Diagnostics.Logger +namespace MQTTnet.Diagnostics.Logger; + +/// +/// This logger does nothing with the messages. +/// +public sealed class MqttNetNullLogger : IMqttNetLogger { - /// - /// This logger does nothing with the messages. - /// - public sealed class MqttNetNullLogger : IMqttNetLogger + public static MqttNetNullLogger Instance { get; } = new(); + + public bool IsEnabled { get; } + + public void Publish(MqttNetLogLevel level, string source, string message, object[] parameters, Exception exception) { - public MqttNetNullLogger() - { - IsEnabled = false; - } - - public static MqttNetNullLogger Instance { get; } = new MqttNetNullLogger(); - - public bool IsEnabled { get; } - - public void Publish(MqttNetLogLevel logLevel, string source, string message, object[] parameters, Exception exception) - { - } } } \ No newline at end of file diff --git a/Source/MQTTnet/Diagnostics/Logger/MqttNetSourceLogger.cs b/Source/MQTTnet/Diagnostics/Logger/MqttNetSourceLogger.cs index bba77f7a..e7ea7dca 100644 --- a/Source/MQTTnet/Diagnostics/Logger/MqttNetSourceLogger.cs +++ b/Source/MQTTnet/Diagnostics/Logger/MqttNetSourceLogger.cs @@ -4,24 +4,16 @@ using System; -namespace MQTTnet.Diagnostics.Logger +namespace MQTTnet.Diagnostics.Logger; + +public sealed class MqttNetSourceLogger(IMqttNetLogger logger, string source) { - public sealed class MqttNetSourceLogger + readonly IMqttNetLogger _logger = logger ?? throw new ArgumentNullException(nameof(logger)); + + public bool IsEnabled => _logger.IsEnabled; + + public void Publish(MqttNetLogLevel logLevel, string message, object[] parameters, Exception exception) { - readonly IMqttNetLogger _logger; - readonly string _source; - - public MqttNetSourceLogger(IMqttNetLogger logger, string source) - { - _logger = logger ?? throw new ArgumentNullException(nameof(logger)); - _source = source; - } - - public bool IsEnabled => _logger.IsEnabled; - - public void Publish(MqttNetLogLevel logLevel, string message, object[] parameters, Exception exception) - { - _logger.Publish(logLevel, _source, message, parameters, exception); - } + _logger.Publish(logLevel, source, message, parameters, exception); } } \ No newline at end of file diff --git a/Source/MQTTnet/Diagnostics/Logger/MqttNetSourceLoggerExtensions.cs b/Source/MQTTnet/Diagnostics/Logger/MqttNetSourceLoggerExtensions.cs index 1c3fb176..8b460cd4 100644 --- a/Source/MQTTnet/Diagnostics/Logger/MqttNetSourceLoggerExtensions.cs +++ b/Source/MQTTnet/Diagnostics/Logger/MqttNetSourceLoggerExtensions.cs @@ -4,214 +4,212 @@ using System; -namespace MQTTnet.Diagnostics.Logger +namespace MQTTnet.Diagnostics.Logger; +/* + * The logger uses generic parameters in order to avoid boxing of parameter values like integers etc. + */ + +public static class MqttNetSourceLoggerExtensions { - /* - * The logger uses generic parameters in order to avoid boxing of parameter values like integers etc. - */ - - public static class MqttNetSourceLoggerExtensions + public static void Error(this MqttNetSourceLogger logger, Exception exception, string message, TParameter1 parameter1) { - public static void Error(this MqttNetSourceLogger logger, Exception exception, string message, TParameter1 parameter1) + if (!logger.IsEnabled) { - if (!logger.IsEnabled) - { - return; - } - - logger.Publish(MqttNetLogLevel.Error, message, new object[] { parameter1 }, exception); + return; } - public static void Error(this MqttNetSourceLogger logger, Exception exception, string message, TParameter1 parameter1, TParameter2 parameter2) - { - if (!logger.IsEnabled) - { - return; - } + logger.Publish(MqttNetLogLevel.Error, message, [parameter1], exception); + } - logger.Publish(MqttNetLogLevel.Error, message, new object[] { parameter1, parameter2 }, exception); + public static void Error(this MqttNetSourceLogger logger, Exception exception, string message, TParameter1 parameter1, TParameter2 parameter2) + { + if (!logger.IsEnabled) + { + return; } - public static void Error(this MqttNetSourceLogger logger, Exception exception, string message) - { - if (!logger.IsEnabled) - { - return; - } + logger.Publish(MqttNetLogLevel.Error, message, [parameter1, parameter2], exception); + } - logger.Publish(MqttNetLogLevel.Error, message, null, exception); + public static void Error(this MqttNetSourceLogger logger, Exception exception, string message) + { + if (!logger.IsEnabled) + { + return; } - public static void Error(this MqttNetSourceLogger logger, string message) - { - if (!logger.IsEnabled) - { - return; - } + logger.Publish(MqttNetLogLevel.Error, message, null, exception); + } - logger.Publish(MqttNetLogLevel.Error, message, null, null); + public static void Error(this MqttNetSourceLogger logger, string message) + { + if (!logger.IsEnabled) + { + return; } - public static void Info(this MqttNetSourceLogger logger, string message, TParameter1 parameter1) - { - if (!logger.IsEnabled) - { - return; - } + logger.Publish(MqttNetLogLevel.Error, message, null, null); + } - logger.Publish(MqttNetLogLevel.Info, message, new object[] { parameter1 }, null); + public static void Info(this MqttNetSourceLogger logger, string message, TParameter1 parameter1) + { + if (!logger.IsEnabled) + { + return; } - public static void Info(this MqttNetSourceLogger logger, string message, TParameter1 parameter1, TParameter2 parameter2) - { - if (!logger.IsEnabled) - { - return; - } + logger.Publish(MqttNetLogLevel.Info, message, [parameter1], null); + } - logger.Publish(MqttNetLogLevel.Info, message, new object[] { parameter1, parameter2 }, null); + public static void Info(this MqttNetSourceLogger logger, string message, TParameter1 parameter1, TParameter2 parameter2) + { + if (!logger.IsEnabled) + { + return; } - public static void Info(this MqttNetSourceLogger logger, string message) - { - if (!logger.IsEnabled) - { - return; - } + logger.Publish(MqttNetLogLevel.Info, message, [parameter1, parameter2], null); + } - logger.Publish(MqttNetLogLevel.Info, message, null, null); + public static void Info(this MqttNetSourceLogger logger, string message) + { + if (!logger.IsEnabled) + { + return; } - public static void Publish(this MqttNetSourceLogger logger, MqttNetLogLevel logLevel, Exception exception, string message, TParameter1 parameter1) - { - if (!logger.IsEnabled) - { - return; - } + logger.Publish(MqttNetLogLevel.Info, message, null, null); + } - logger.Publish(logLevel, message, new object[] { parameter1 }, exception); + public static void Publish(this MqttNetSourceLogger logger, MqttNetLogLevel logLevel, Exception exception, string message, TParameter1 parameter1) + { + if (!logger.IsEnabled) + { + return; } - public static void Publish(this MqttNetSourceLogger logger, MqttNetLogLevel logLevel, Exception exception, string message, TParameter1 parameter1, TParameter2 parameter2) - { - if (!logger.IsEnabled) - { - return; - } + logger.Publish(logLevel, message, [parameter1], exception); + } - logger.Publish(logLevel, message, new object[] { parameter1, parameter2 }, exception); + public static void Publish(this MqttNetSourceLogger logger, MqttNetLogLevel logLevel, Exception exception, string message, TParameter1 parameter1, TParameter2 parameter2) + { + if (!logger.IsEnabled) + { + return; } - public static void Verbose(this MqttNetSourceLogger logger, string message, TParameter1 parameter1) - { - if (!logger.IsEnabled) - { - return; - } + logger.Publish(logLevel, message, [parameter1, parameter2], exception); + } - logger.Publish(MqttNetLogLevel.Verbose, message, new object[] { parameter1 }, null); + public static void Verbose(this MqttNetSourceLogger logger, string message, TParameter1 parameter1) + { + if (!logger.IsEnabled) + { + return; } - public static void Verbose(this MqttNetSourceLogger logger, string message, TParameter1 parameter1, TParameter2 parameter2) - { - if (!logger.IsEnabled) - { - return; - } + logger.Publish(MqttNetLogLevel.Verbose, message, [parameter1], null); + } - logger.Publish(MqttNetLogLevel.Verbose, message, new object[] { parameter1, parameter2 }, null); + public static void Verbose(this MqttNetSourceLogger logger, string message, TParameter1 parameter1, TParameter2 parameter2) + { + if (!logger.IsEnabled) + { + return; } - public static void Verbose( - this MqttNetSourceLogger logger, - string message, - TParameter1 parameter1, - TParameter2 parameter2, - TParameter3 parameter3) - { - if (!logger.IsEnabled) - { - return; - } + logger.Publish(MqttNetLogLevel.Verbose, message, [parameter1, parameter2], null); + } - logger.Publish(MqttNetLogLevel.Verbose, message, new object[] { parameter1, parameter2, parameter3 }, null); + public static void Verbose( + this MqttNetSourceLogger logger, + string message, + TParameter1 parameter1, + TParameter2 parameter2, + TParameter3 parameter3) + { + if (!logger.IsEnabled) + { + return; } - public static void Verbose(this MqttNetSourceLogger logger, string message) - { - if (!logger.IsEnabled) - { - return; - } + logger.Publish(MqttNetLogLevel.Verbose, message, [parameter1, parameter2, parameter3], null); + } - logger.Publish(MqttNetLogLevel.Verbose, message, null, null); + public static void Verbose(this MqttNetSourceLogger logger, string message) + { + if (!logger.IsEnabled) + { + return; } - public static void Warning(this MqttNetSourceLogger logger, Exception exception, string message, TParameter1 parameter1) - { - if (!logger.IsEnabled) - { - return; - } + logger.Publish(MqttNetLogLevel.Verbose, message, null, null); + } - logger.Publish(MqttNetLogLevel.Warning, message, new object[] { parameter1 }, exception); + public static void Warning(this MqttNetSourceLogger logger, Exception exception, string message, TParameter1 parameter1) + { + if (!logger.IsEnabled) + { + return; } - public static void Warning(this MqttNetSourceLogger logger, Exception exception, string message, TParameter1 parameter1, TParameter2 parameter2) - { - if (!logger.IsEnabled) - { - return; - } + logger.Publish(MqttNetLogLevel.Warning, message, [parameter1], exception); + } - logger.Publish(MqttNetLogLevel.Warning, message, new object[] { parameter1, parameter2 }, exception); + public static void Warning(this MqttNetSourceLogger logger, Exception exception, string message, TParameter1 parameter1, TParameter2 parameter2) + { + if (!logger.IsEnabled) + { + return; } - public static void Warning(this MqttNetSourceLogger logger, Exception exception, string message) - { - if (!logger.IsEnabled) - { - return; - } + logger.Publish(MqttNetLogLevel.Warning, message, [parameter1, parameter2], exception); + } - logger.Publish(MqttNetLogLevel.Warning, message, null, exception); + public static void Warning(this MqttNetSourceLogger logger, Exception exception, string message) + { + if (!logger.IsEnabled) + { + return; } - public static void Warning(this MqttNetSourceLogger logger, string message, TParameter1 parameter1) - { - if (!logger.IsEnabled) - { - return; - } + logger.Publish(MqttNetLogLevel.Warning, message, null, exception); + } - logger.Publish(MqttNetLogLevel.Warning, message, new object[] { parameter1 }, null); + public static void Warning(this MqttNetSourceLogger logger, string message, TParameter1 parameter1) + { + if (!logger.IsEnabled) + { + return; } - public static void Warning(this MqttNetSourceLogger logger, string message, TParameter1 parameter1, TParameter2 parameter2) - { - if (!logger.IsEnabled) - { - return; - } + logger.Publish(MqttNetLogLevel.Warning, message, [parameter1], null); + } - logger.Publish(MqttNetLogLevel.Warning, message, new object[] { parameter1, parameter2 }, null); + public static void Warning(this MqttNetSourceLogger logger, string message, TParameter1 parameter1, TParameter2 parameter2) + { + if (!logger.IsEnabled) + { + return; } - public static void Warning(this MqttNetSourceLogger logger, string message) - { - if (!logger.IsEnabled) - { - return; - } + logger.Publish(MqttNetLogLevel.Warning, message, [parameter1, parameter2], null); + } - logger.Publish(MqttNetLogLevel.Warning, message, null, null); + public static void Warning(this MqttNetSourceLogger logger, string message) + { + if (!logger.IsEnabled) + { + return; } - public static MqttNetSourceLogger WithSource(this IMqttNetLogger logger, string source) - { - ArgumentNullException.ThrowIfNull(logger); + logger.Publish(MqttNetLogLevel.Warning, message, null, null); + } - return new MqttNetSourceLogger(logger, source); - } + public static MqttNetSourceLogger WithSource(this IMqttNetLogger logger, string source) + { + ArgumentNullException.ThrowIfNull(logger); + + return new MqttNetSourceLogger(logger, source); } } \ No newline at end of file diff --git a/Source/MQTTnet/Diagnostics/PacketInspection/InspectMqttPacketEventArgs.cs b/Source/MQTTnet/Diagnostics/PacketInspection/InspectMqttPacketEventArgs.cs index ec7ec733..c85784ee 100644 --- a/Source/MQTTnet/Diagnostics/PacketInspection/InspectMqttPacketEventArgs.cs +++ b/Source/MQTTnet/Diagnostics/PacketInspection/InspectMqttPacketEventArgs.cs @@ -4,18 +4,17 @@ using System; -namespace MQTTnet.Diagnostics.PacketInspection +namespace MQTTnet.Diagnostics.PacketInspection; + +public sealed class InspectMqttPacketEventArgs : EventArgs { - public sealed class InspectMqttPacketEventArgs : EventArgs + public InspectMqttPacketEventArgs(MqttPacketFlowDirection direction, byte[] buffer) { - public InspectMqttPacketEventArgs(MqttPacketFlowDirection direction, byte[] buffer) - { - Direction = direction; - Buffer = buffer ?? throw new ArgumentNullException(nameof(buffer)); - } - - public byte[] Buffer { get; } - - public MqttPacketFlowDirection Direction { get; } + Direction = direction; + Buffer = buffer ?? throw new ArgumentNullException(nameof(buffer)); } + + public byte[] Buffer { get; } + + public MqttPacketFlowDirection Direction { get; } } \ No newline at end of file diff --git a/Source/MQTTnet/Diagnostics/PacketInspection/MqttPacketFlowDirection.cs b/Source/MQTTnet/Diagnostics/PacketInspection/MqttPacketFlowDirection.cs index 3d5909c1..1daee52a 100644 --- a/Source/MQTTnet/Diagnostics/PacketInspection/MqttPacketFlowDirection.cs +++ b/Source/MQTTnet/Diagnostics/PacketInspection/MqttPacketFlowDirection.cs @@ -2,12 +2,11 @@ // The .NET Foundation licenses this file to you under the MIT license. // See the LICENSE file in the project root for more information. -namespace MQTTnet.Diagnostics.PacketInspection -{ - public enum MqttPacketFlowDirection - { - Inbound, +namespace MQTTnet.Diagnostics.PacketInspection; - Outbound - } +public enum MqttPacketFlowDirection +{ + Inbound, + + Outbound } \ No newline at end of file diff --git a/Source/MQTTnet/Disconnecting/MqttClientDisconnectOptionsBuilder.cs b/Source/MQTTnet/Disconnecting/MqttClientDisconnectOptionsBuilder.cs index a953207b..c8a7b88d 100644 --- a/Source/MQTTnet/Disconnecting/MqttClientDisconnectOptionsBuilder.cs +++ b/Source/MQTTnet/Disconnecting/MqttClientDisconnectOptionsBuilder.cs @@ -51,11 +51,7 @@ public sealed class MqttClientDisconnectOptionsBuilder public MqttClientDisconnectOptionsBuilder WithUserProperty(string name, string value) { - if (_userProperties == null) - { - _userProperties = new List(); - } - + _userProperties ??= []; _userProperties.Add(new MqttUserProperty(name, value)); return this; } diff --git a/Source/MQTTnet/Disconnecting/MqttClientDisconnectOptionsValidator.cs b/Source/MQTTnet/Disconnecting/MqttClientDisconnectOptionsValidator.cs index b8da24ea..c151bdee 100644 --- a/Source/MQTTnet/Disconnecting/MqttClientDisconnectOptionsValidator.cs +++ b/Source/MQTTnet/Disconnecting/MqttClientDisconnectOptionsValidator.cs @@ -3,7 +3,6 @@ // See the LICENSE file in the project root for more information. using System; -using System.Linq; using MQTTnet.Formatter; namespace MQTTnet; @@ -20,7 +19,7 @@ public static class MqttClientDisconnectOptionsValidator return; } - if (options.ReasonString?.Any() == true) + if (options.ReasonString?.Length > 0) { Throw(nameof(options.ReasonString)); } diff --git a/Source/MQTTnet/Exceptions/MqttClientNotConnectedException.cs b/Source/MQTTnet/Exceptions/MqttClientNotConnectedException.cs index 716ead70..e03a1b5e 100644 --- a/Source/MQTTnet/Exceptions/MqttClientNotConnectedException.cs +++ b/Source/MQTTnet/Exceptions/MqttClientNotConnectedException.cs @@ -2,18 +2,11 @@ // The .NET Foundation licenses this file to you under the MIT license. // See the LICENSE file in the project root for more information. -using System; +namespace MQTTnet.Exceptions; -namespace MQTTnet.Exceptions +public class MqttClientNotConnectedException : MqttCommunicationException { - public class MqttClientNotConnectedException : MqttCommunicationException + public MqttClientNotConnectedException() : base("The MQTT client is not connected.") { - public MqttClientNotConnectedException() : base("The MQTT client is not connected.") - { - } - - public MqttClientNotConnectedException(Exception innerException) : base("The MQTT client is not connected.", innerException) - { - } } -} +} \ No newline at end of file diff --git a/Source/MQTTnet/Exceptions/MqttClientUnexpectedDisconnectReceivedException.cs b/Source/MQTTnet/Exceptions/MqttClientUnexpectedDisconnectReceivedException.cs index 5fa74595..9880e751 100644 --- a/Source/MQTTnet/Exceptions/MqttClientUnexpectedDisconnectReceivedException.cs +++ b/Source/MQTTnet/Exceptions/MqttClientUnexpectedDisconnectReceivedException.cs @@ -9,26 +9,17 @@ using MQTTnet.Protocol; namespace MQTTnet.Exceptions; -public sealed class MqttClientUnexpectedDisconnectReceivedException : MqttCommunicationException +public sealed class MqttClientUnexpectedDisconnectReceivedException(MqttDisconnectPacket disconnectPacket, Exception innerException = null) : MqttCommunicationException( + $"Unexpected DISCONNECT (Reason code={disconnectPacket.ReasonCode}) received.", + innerException) { - public MqttClientUnexpectedDisconnectReceivedException(MqttDisconnectPacket disconnectPacket, Exception innerExcpetion = null) : base( - $"Unexpected DISCONNECT (Reason code={disconnectPacket.ReasonCode}) received.", - innerExcpetion) - { - ReasonCode = disconnectPacket.ReasonCode; - SessionExpiryInterval = disconnectPacket.SessionExpiryInterval; - ReasonString = disconnectPacket.ReasonString; - ServerReference = disconnectPacket.ServerReference; - UserProperties = disconnectPacket.UserProperties; - } + public MqttDisconnectReasonCode? ReasonCode { get; } = disconnectPacket.ReasonCode; - public MqttDisconnectReasonCode? ReasonCode { get; } + public string ReasonString { get; } = disconnectPacket.ReasonString; - public string ReasonString { get; } + public string ServerReference { get; } = disconnectPacket.ServerReference; - public string ServerReference { get; } + public uint? SessionExpiryInterval { get; } = disconnectPacket.SessionExpiryInterval; - public uint? SessionExpiryInterval { get; } - - public List UserProperties { get; } + public List UserProperties { get; } = disconnectPacket.UserProperties; } \ No newline at end of file diff --git a/Source/MQTTnet/Exceptions/MqttCommunicationException.cs b/Source/MQTTnet/Exceptions/MqttCommunicationException.cs index a78c1f7c..7f8a3ffd 100644 --- a/Source/MQTTnet/Exceptions/MqttCommunicationException.cs +++ b/Source/MQTTnet/Exceptions/MqttCommunicationException.cs @@ -4,18 +4,17 @@ using System; -namespace MQTTnet.Exceptions -{ - public class MqttCommunicationException : Exception - { - public MqttCommunicationException(Exception innerException) - : base(innerException?.Message ?? "MQTT communication failed.", innerException) - { - } +namespace MQTTnet.Exceptions; - public MqttCommunicationException(string message, Exception innerException = null) - : base(message, innerException) - { - } +public class MqttCommunicationException : Exception +{ + public MqttCommunicationException(Exception innerException) + : base(innerException?.Message ?? "MQTT communication failed.", innerException) + { } -} + + public MqttCommunicationException(string message, Exception innerException = null) + : base(message, innerException) + { + } +} \ No newline at end of file diff --git a/Source/MQTTnet/Exceptions/MqttCommunicationTimedOutException.cs b/Source/MQTTnet/Exceptions/MqttCommunicationTimedOutException.cs index 5d41c1b6..566a5871 100644 --- a/Source/MQTTnet/Exceptions/MqttCommunicationTimedOutException.cs +++ b/Source/MQTTnet/Exceptions/MqttCommunicationTimedOutException.cs @@ -4,16 +4,15 @@ using System; -namespace MQTTnet.Exceptions -{ - public sealed class MqttCommunicationTimedOutException : MqttCommunicationException - { - public MqttCommunicationTimedOutException() : base("The operation has timed out.") - { - } +namespace MQTTnet.Exceptions; - public MqttCommunicationTimedOutException(Exception innerException) : base("The operation has timed out.", innerException) - { - } +public sealed class MqttCommunicationTimedOutException : MqttCommunicationException +{ + public MqttCommunicationTimedOutException() : base("The operation has timed out.") + { } -} + + public MqttCommunicationTimedOutException(Exception innerException) : base("The operation has timed out.", innerException) + { + } +} \ No newline at end of file diff --git a/Source/MQTTnet/Exceptions/MqttConfigurationException.cs b/Source/MQTTnet/Exceptions/MqttConfigurationException.cs index 9e2bc580..145ee82a 100644 --- a/Source/MQTTnet/Exceptions/MqttConfigurationException.cs +++ b/Source/MQTTnet/Exceptions/MqttConfigurationException.cs @@ -4,22 +4,21 @@ using System; -namespace MQTTnet.Exceptions +namespace MQTTnet.Exceptions; + +public class MqttConfigurationException : Exception { - public class MqttConfigurationException : Exception + protected MqttConfigurationException() { - protected MqttConfigurationException() - { - } - - public MqttConfigurationException(Exception innerException) - : base(innerException.Message, innerException) - { - } - - public MqttConfigurationException(string message) - : base(message) - { - } } -} + + public MqttConfigurationException(Exception innerException) + : base(innerException.Message, innerException) + { + } + + public MqttConfigurationException(string message) + : base(message) + { + } +} \ No newline at end of file diff --git a/Source/MQTTnet/Exceptions/MqttProtocolViolationException.cs b/Source/MQTTnet/Exceptions/MqttProtocolViolationException.cs index e8229a89..f373d643 100644 --- a/Source/MQTTnet/Exceptions/MqttProtocolViolationException.cs +++ b/Source/MQTTnet/Exceptions/MqttProtocolViolationException.cs @@ -4,13 +4,6 @@ using System; -namespace MQTTnet.Exceptions -{ - public class MqttProtocolViolationException : Exception - { - public MqttProtocolViolationException(string message) - : base(message) - { - } - } -} +namespace MQTTnet.Exceptions; + +public class MqttProtocolViolationException(string message) : Exception(message); \ No newline at end of file diff --git a/Source/MQTTnet/Formatter/IMqttPacketFormatter.cs b/Source/MQTTnet/Formatter/IMqttPacketFormatter.cs index a6e8c555..7754d8b2 100644 --- a/Source/MQTTnet/Formatter/IMqttPacketFormatter.cs +++ b/Source/MQTTnet/Formatter/IMqttPacketFormatter.cs @@ -5,12 +5,11 @@ using MQTTnet.Adapter; using MQTTnet.Packets; -namespace MQTTnet.Formatter -{ - public interface IMqttPacketFormatter - { - MqttPacket Decode(ReceivedMqttPacket receivedMqttPacket); +namespace MQTTnet.Formatter; - MqttPacketBuffer Encode(MqttPacket mqttPacket); - } +public interface IMqttPacketFormatter +{ + MqttPacket Decode(ReceivedMqttPacket receivedPacket); + + MqttPacketBuffer Encode(MqttPacket packet); } \ No newline at end of file diff --git a/Source/MQTTnet/Formatter/MqttApplicationMessageFactory.cs b/Source/MQTTnet/Formatter/MqttApplicationMessageFactory.cs index 60cbcbb1..8c81d990 100644 --- a/Source/MQTTnet/Formatter/MqttApplicationMessageFactory.cs +++ b/Source/MQTTnet/Formatter/MqttApplicationMessageFactory.cs @@ -5,30 +5,29 @@ using System; using MQTTnet.Packets; -namespace MQTTnet.Formatter -{ - public static class MqttApplicationMessageFactory - { - public static MqttApplicationMessage Create(MqttPublishPacket publishPacket) - { - ArgumentNullException.ThrowIfNull(publishPacket); +namespace MQTTnet.Formatter; - return new MqttApplicationMessage - { - Topic = publishPacket.Topic, - Payload = publishPacket.Payload, - QualityOfServiceLevel = publishPacket.QualityOfServiceLevel, - Retain = publishPacket.Retain, - Dup = publishPacket.Dup, - ResponseTopic = publishPacket.ResponseTopic, - ContentType = publishPacket.ContentType, - CorrelationData = publishPacket.CorrelationData, - MessageExpiryInterval = publishPacket.MessageExpiryInterval, - SubscriptionIdentifiers = publishPacket.SubscriptionIdentifiers, - TopicAlias = publishPacket.TopicAlias, - PayloadFormatIndicator = publishPacket.PayloadFormatIndicator, - UserProperties = publishPacket.UserProperties - }; - } +public static class MqttApplicationMessageFactory +{ + public static MqttApplicationMessage Create(MqttPublishPacket publishPacket) + { + ArgumentNullException.ThrowIfNull(publishPacket); + + return new MqttApplicationMessage + { + Topic = publishPacket.Topic, + Payload = publishPacket.Payload, + QualityOfServiceLevel = publishPacket.QualityOfServiceLevel, + Retain = publishPacket.Retain, + Dup = publishPacket.Dup, + ResponseTopic = publishPacket.ResponseTopic, + ContentType = publishPacket.ContentType, + CorrelationData = publishPacket.CorrelationData, + MessageExpiryInterval = publishPacket.MessageExpiryInterval, + SubscriptionIdentifiers = publishPacket.SubscriptionIdentifiers, + TopicAlias = publishPacket.TopicAlias, + PayloadFormatIndicator = publishPacket.PayloadFormatIndicator, + UserProperties = publishPacket.UserProperties + }; } } \ No newline at end of file diff --git a/Source/MQTTnet/Formatter/MqttBufferReader.cs b/Source/MQTTnet/Formatter/MqttBufferReader.cs index c8102be7..c6ca138e 100644 --- a/Source/MQTTnet/Formatter/MqttBufferReader.cs +++ b/Source/MQTTnet/Formatter/MqttBufferReader.cs @@ -10,146 +10,145 @@ using MQTTnet.Internal; using System.Buffers.Binary; -namespace MQTTnet.Formatter +namespace MQTTnet.Formatter; + +public sealed class MqttBufferReader { - public sealed class MqttBufferReader + byte[] _buffer = EmptyBuffer.Array; + int _maxPosition; + int _offset; + int _position; + + public int BytesLeft => _maxPosition - _position; + + public bool EndOfStream => BytesLeft == 0; + + public int Position => _position - _offset; + + public byte[] ReadBinaryData() { - byte[] _buffer = EmptyBuffer.Array; - int _maxPosition; - int _offset; - int _position; + var length = ReadTwoByteInteger(); - public int BytesLeft => _maxPosition - _position; - - public bool EndOfStream => BytesLeft == 0; - - public int Position => _position - _offset; - - public byte[] ReadBinaryData() + if (length == 0) { - var length = ReadTwoByteInteger(); + return EmptyBuffer.Array; + } - if (length == 0) + ValidateReceiveBuffer(length); + + var result = GC.AllocateUninitializedArray(length); + MqttMemoryHelper.Copy(_buffer, _position, result, 0, length); + _position += length; + + return result; + } + + public byte ReadByte() + { + ValidateReceiveBuffer(1); + return _buffer[_position++]; + } + + public uint ReadFourByteInteger() + { + ValidateReceiveBuffer(4); + + var value = BinaryPrimitives.ReadUInt32BigEndian(_buffer.AsSpan(_position)); + + _position += 4; + return value; + } + + public byte[] ReadRemainingData() + { + var bufferLength = BytesLeft; + if (bufferLength == 0) + { + return EmptyBuffer.Array; + } + + var buffer = GC.AllocateUninitializedArray(bufferLength); + MqttMemoryHelper.Copy(_buffer, _position, buffer, 0, bufferLength); + _position += bufferLength; + + return buffer; + } + + public string ReadString() + { + var length = ReadTwoByteInteger(); + + if (length == 0) + { + return string.Empty; + } + + ValidateReceiveBuffer(length); + + // AsSpan() version is slightly faster. Not much but at least a little bit. + var result = Encoding.UTF8.GetString(_buffer.AsSpan(_position, length)); + + _position += length; + return result; + } + + public ushort ReadTwoByteInteger() + { + ValidateReceiveBuffer(2); + + var value = BinaryPrimitives.ReadUInt16BigEndian(_buffer.AsSpan(_position)); + + _position += 2; + return value; + } + + public uint ReadVariableByteInteger() + { + var multiplier = 1; + var value = 0U; + byte encodedByte; + + do + { + encodedByte = ReadByte(); + value += (uint)((encodedByte & 127) * multiplier); + + if (multiplier > 2097152) { - return EmptyBuffer.Array; + throw new MqttProtocolViolationException("Variable length integer is invalid."); } - ValidateReceiveBuffer(length); + multiplier *= 128; + } while ((encodedByte & 128) != 0); - var result = GC.AllocateUninitializedArray(length); - MqttMemoryHelper.Copy(_buffer, _position, result, 0, length); - _position += length; + return value; + } - return result; - } + public void Seek(int position) + { + _position = _offset + position; + } - public byte ReadByte() + public void SetBuffer(ArraySegment buffer) + { + SetBuffer(buffer.Array, buffer.Offset, buffer.Count); + } + + public void SetBuffer(byte[] buffer, int offset, int length) + { + _buffer = buffer ?? throw new ArgumentNullException(nameof(buffer)); + _offset = offset; + _position = offset; + _maxPosition = offset + length; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + void ValidateReceiveBuffer(int length) + { + var newPosition = _position + length; + if (_maxPosition < newPosition) { - ValidateReceiveBuffer(1); - return _buffer[_position++]; - } - - public uint ReadFourByteInteger() - { - ValidateReceiveBuffer(4); - - var value = BinaryPrimitives.ReadUInt32BigEndian(_buffer.AsSpan(_position)); - - _position += 4; - return value; - } - - public byte[] ReadRemainingData() - { - var bufferLength = BytesLeft; - if (bufferLength == 0) - { - return EmptyBuffer.Array; - } - - var buffer = GC.AllocateUninitializedArray(bufferLength); - MqttMemoryHelper.Copy(_buffer, _position, buffer, 0, bufferLength); - _position += bufferLength; - - return buffer; - } - - public string ReadString() - { - var length = ReadTwoByteInteger(); - - if (length == 0) - { - return string.Empty; - } - - ValidateReceiveBuffer(length); - - // AsSpan() version is slightly faster. Not much but at least a little bit. - var result = Encoding.UTF8.GetString(_buffer.AsSpan(_position, length)); - - _position += length; - return result; - } - - public ushort ReadTwoByteInteger() - { - ValidateReceiveBuffer(2); - - var value = BinaryPrimitives.ReadUInt16BigEndian(_buffer.AsSpan(_position)); - - _position += 2; - return value; - } - - public uint ReadVariableByteInteger() - { - var multiplier = 1; - var value = 0U; - byte encodedByte; - - do - { - encodedByte = ReadByte(); - value += (uint)((encodedByte & 127) * multiplier); - - if (multiplier > 2097152) - { - throw new MqttProtocolViolationException("Variable length integer is invalid."); - } - - multiplier *= 128; - } while ((encodedByte & 128) != 0); - - return value; - } - - public void Seek(int position) - { - _position = _offset + position; - } - - public void SetBuffer(ArraySegment buffer) - { - SetBuffer(buffer.Array, buffer.Offset, buffer.Count); - } - - public void SetBuffer(byte[] buffer, int offset, int length) - { - _buffer = buffer ?? throw new ArgumentNullException(nameof(buffer)); - _offset = offset; - _position = offset; - _maxPosition = offset + length; - } - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - void ValidateReceiveBuffer(int length) - { - var newPosition = _position + length; - if (_maxPosition < newPosition) - { - throw new MqttProtocolViolationException($"Expected at least {newPosition} bytes but there are only {_maxPosition} bytes"); - } + throw new MqttProtocolViolationException($"Expected at least {newPosition} bytes but there are only {_maxPosition} bytes"); } } } \ No newline at end of file diff --git a/Source/MQTTnet/Formatter/MqttBufferWriter.cs b/Source/MQTTnet/Formatter/MqttBufferWriter.cs index c89ccad7..abaca7da 100644 --- a/Source/MQTTnet/Formatter/MqttBufferWriter.cs +++ b/Source/MQTTnet/Formatter/MqttBufferWriter.cs @@ -9,287 +9,286 @@ using MQTTnet.Exceptions; using MQTTnet.Internal; using MQTTnet.Protocol; -namespace MQTTnet.Formatter +namespace MQTTnet.Formatter; + +/// +/// This is a custom implementation of a memory stream which provides only MQTTnet relevant features. +/// The goal is to avoid lots of argument checks like in the original stream. The growth rule is the +/// same as for the original MemoryStream in .net. Also this implementation allows accessing the internal +/// buffer for all platforms and .net framework versions (which is not available at the regular MemoryStream). +/// +public sealed class MqttBufferWriter { - /// - /// This is a custom implementation of a memory stream which provides only MQTTnet relevant features. - /// The goal is to avoid lots of argument checks like in the original stream. The growth rule is the - /// same as for the original MemoryStream in .net. Also this implementation allows accessing the internal - /// buffer for all platforms and .net framework versions (which is not available at the regular MemoryStream). - /// - public sealed class MqttBufferWriter + const uint VariableByteIntegerMaxValue = 268435455; + const int EncodedStringMaxLength = 65535; + + readonly int _maxBufferSize; + + byte[] _buffer; + int _position; + + public MqttBufferWriter(int bufferSize, int maxBufferSize) { - const uint VariableByteIntegerMaxValue = 268435455; - const int EncodedStringMaxLength = 65535; + _buffer = new byte[bufferSize]; + _maxBufferSize = maxBufferSize; + } - readonly int _maxBufferSize; + public int Length { get; private set; } - byte[] _buffer; - int _position; + public static byte BuildFixedHeader(MqttControlPacketType packetType, byte flags = 0) + { + var fixedHeader = (int)packetType << 4; + fixedHeader |= flags; + return (byte)fixedHeader; + } - public MqttBufferWriter(int bufferSize, int maxBufferSize) + public void Cleanup() + { + // This method frees the used memory by shrinking the buffer. This is required because the buffer + // is used across several messages. In general this is not a big issue because subsequent Ping packages + // have the same size but a very big publish package with 100 MB of payload will increase the buffer + // a lot and the size will never reduced. So this method tries to find a size which can be held in + // memory for a long time without causing troubles. + + if (_buffer.Length <= _maxBufferSize) { - _buffer = new byte[bufferSize]; - _maxBufferSize = maxBufferSize; + return; } - public int Length { get; private set; } + // Create a new and empty buffer. Do not use Array.Resize because it will copy all data from + // the old array to the new one which is not required in this case. + _buffer = new byte[_maxBufferSize]; + } - public static byte BuildFixedHeader(MqttControlPacketType packetType, byte flags = 0) + public byte[] GetBuffer() + { + return _buffer; + } + + public static int GetVariableByteIntegerSize(uint value) + { + // From RFC: Table 2.4 Size of Remaining Length field + + // 0 (0x00) to 127 (0x7F) + if (value <= 127) { - var fixedHeader = (int)packetType << 4; - fixedHeader |= flags; - return (byte)fixedHeader; + return 1; } - public void Cleanup() + // 128 (0x80, 0x01) to 16 383 (0xFF, 0x7F) + if (value <= 16383) { - // This method frees the used memory by shrinking the buffer. This is required because the buffer - // is used across several messages. In general this is not a big issue because subsequent Ping packages - // have the same size but a very big publish package with 100 MB of payload will increase the buffer - // a lot and the size will never reduced. So this method tries to find a size which can be held in - // memory for a long time without causing troubles. - - if (_buffer.Length <= _maxBufferSize) - { - return; - } - - // Create a new and empty buffer. Do not use Array.Resize because it will copy all data from - // the old array to the new one which is not required in this case. - _buffer = new byte[_maxBufferSize]; + return 2; } - public byte[] GetBuffer() + // 16 384 (0x80, 0x80, 0x01) to 2 097 151 (0xFF, 0xFF, 0x7F) + if (value <= 2097151) { - return _buffer; + return 3; } - public static int GetVariableByteIntegerSize(uint value) - { - // From RFC: Table 2.4 Size of Remaining Length field + // 2 097 152 (0x80, 0x80, 0x80, 0x01) to 268 435 455 (0xFF, 0xFF, 0xFF, 0x7F) + return 4; + } - // 0 (0x00) to 127 (0x7F) - if (value <= 127) - { - return 1; - } + public void Reset(int length) + { + _position = 0; + Length = length; + } - // 128 (0x80, 0x01) to 16 383 (0xFF, 0x7F) - if (value <= 16383) - { - return 2; - } + public void Seek(int position) + { + EnsureCapacity(position); + _position = position; + } - // 16 384 (0x80, 0x80, 0x01) to 2 097 151 (0xFF, 0xFF, 0x7F) - if (value <= 2097151) - { - return 3; - } + public void Write(MqttBufferWriter propertyWriter) + { + ArgumentNullException.ThrowIfNull(propertyWriter); - // 2 097 152 (0x80, 0x80, 0x80, 0x01) to 268 435 455 (0xFF, 0xFF, 0xFF, 0x7F) - return 4; - } + WriteBinary(propertyWriter._buffer, 0, propertyWriter.Length); + } - public void Reset(int length) - { - _position = 0; - Length = length; - } - - public void Seek(int position) - { - EnsureCapacity(position); - _position = position; - } - - public void Write(MqttBufferWriter propertyWriter) - { - ArgumentNullException.ThrowIfNull(propertyWriter); - - WriteBinary(propertyWriter._buffer, 0, propertyWriter.Length); - } - - public void WriteBinary(byte[] value) - { - if (value == null || value.Length == 0) - { - EnsureAdditionalCapacity(2); - - _buffer[_position] = 0; - _buffer[_position + 1] = 0; - - IncreasePosition(2); - } - else - { - var valueLength = value.Length; - - EnsureAdditionalCapacity(valueLength + 2); - - _buffer[_position] = (byte)(valueLength >> 8); - _buffer[_position + 1] = (byte)valueLength; - - MqttMemoryHelper.Copy(value, 0, _buffer, _position + 2, valueLength); - IncreasePosition(valueLength + 2); - } - } - - public void WriteBinary(byte[] buffer, int offset, int count) - { - ArgumentNullException.ThrowIfNull(buffer); - - if (count == 0) - { - return; - } - - EnsureAdditionalCapacity(count); - - MqttMemoryHelper.Copy(buffer, offset, _buffer, _position, count); - IncreasePosition(count); - } - - public void WriteByte(byte @byte) - { - EnsureAdditionalCapacity(1); - - _buffer[_position] = @byte; - IncreasePosition(1); - } - - public void WriteString(string value) - { - if (string.IsNullOrEmpty(value)) - { - EnsureAdditionalCapacity(2); - - _buffer[_position] = 0; - _buffer[_position + 1] = 0; - - IncreasePosition(2); - } - else - { - // Do not use Encoding.UTF8.GetByteCount(value); - // UTF8 chars can have a max length of 4 and the used buffer increase *2 every time. - // So the buffer should always have much more capacity left so that a correct value - // here is only waste of CPU cycles. - var byteCount = value.Length * 4; - - EnsureAdditionalCapacity(byteCount + 2); - - var writtenBytes = Encoding.UTF8.GetBytes(value, 0, value.Length, _buffer, _position + 2); - - // From RFC: 1.5.4 UTF-8 Encoded String - // Unless stated otherwise all UTF-8 encoded strings can have any length in the range 0 to 65,535 bytes. - if (writtenBytes > EncodedStringMaxLength) - { - throw new MqttProtocolViolationException($"The maximum string length is 65535. The current string has a length of {writtenBytes}."); - } - - _buffer[_position] = (byte)(writtenBytes >> 8); - _buffer[_position + 1] = (byte)writtenBytes; - - IncreasePosition(writtenBytes + 2); - } - } - - public void WriteTwoByteInteger(ushort value) + public void WriteBinary(byte[] value) + { + if (value == null || value.Length == 0) { EnsureAdditionalCapacity(2); - _buffer[_position] = (byte)(value >> 8); + _buffer[_position] = 0; + _buffer[_position + 1] = 0; + + IncreasePosition(2); + } + else + { + var valueLength = value.Length; + + EnsureAdditionalCapacity(valueLength + 2); + + _buffer[_position] = (byte)(valueLength >> 8); + _buffer[_position + 1] = (byte)valueLength; + + MqttMemoryHelper.Copy(value, 0, _buffer, _position + 2, valueLength); + IncreasePosition(valueLength + 2); + } + } + + public void WriteBinary(byte[] buffer, int offset, int count) + { + ArgumentNullException.ThrowIfNull(buffer); + + if (count == 0) + { + return; + } + + EnsureAdditionalCapacity(count); + + MqttMemoryHelper.Copy(buffer, offset, _buffer, _position, count); + IncreasePosition(count); + } + + public void WriteByte(byte @byte) + { + EnsureAdditionalCapacity(1); + + _buffer[_position] = @byte; + IncreasePosition(1); + } + + public void WriteString(string value) + { + if (string.IsNullOrEmpty(value)) + { + EnsureAdditionalCapacity(2); + + _buffer[_position] = 0; + _buffer[_position + 1] = 0; + + IncreasePosition(2); + } + else + { + // Do not use Encoding.UTF8.GetByteCount(value); + // UTF8 chars can have a max length of 4 and the used buffer increase *2 every time. + // So the buffer should always have much more capacity left so that a correct value + // here is only waste of CPU cycles. + var byteCount = value.Length * 4; + + EnsureAdditionalCapacity(byteCount + 2); + + var writtenBytes = Encoding.UTF8.GetBytes(value, 0, value.Length, _buffer, _position + 2); + + // From RFC: 1.5.4 UTF-8 Encoded String + // Unless stated otherwise all UTF-8 encoded strings can have any length in the range 0 to 65,535 bytes. + if (writtenBytes > EncodedStringMaxLength) + { + throw new MqttProtocolViolationException($"The maximum string length is 65535. The current string has a length of {writtenBytes}."); + } + + _buffer[_position] = (byte)(writtenBytes >> 8); + _buffer[_position + 1] = (byte)writtenBytes; + + IncreasePosition(writtenBytes + 2); + } + } + + public void WriteTwoByteInteger(ushort value) + { + EnsureAdditionalCapacity(2); + + _buffer[_position] = (byte)(value >> 8); + IncreasePosition(1); + _buffer[_position] = (byte)value; + IncreasePosition(1); + } + + public void WriteVariableByteInteger(uint value) + { + if (value == 0) + { + _buffer[_position] = 0; IncreasePosition(1); + + return; + } + + if (value <= 127) + { _buffer[_position] = (byte)value; IncreasePosition(1); + + return; } - public void WriteVariableByteInteger(uint value) + if (value > VariableByteIntegerMaxValue) { - if (value == 0) - { - _buffer[_position] = 0; - IncreasePosition(1); - - return; - } - - if (value <= 127) - { - _buffer[_position] = (byte)value; - IncreasePosition(1); - - return; - } - - if (value > VariableByteIntegerMaxValue) - { - throw new MqttProtocolViolationException($"The specified value ({value}) is too large for a variable byte integer."); - } - - var size = 0; - var x = value; - do - { - var encodedByte = x % 128; - x /= 128; - if (x > 0) - { - encodedByte |= 128; - } - - _buffer[_position + size] = (byte)encodedByte; - size++; - } while (x > 0); - - IncreasePosition(size); + throw new MqttProtocolViolationException($"The specified value ({value}) is too large for a variable byte integer."); } - [MethodImpl(MethodImplOptions.AggressiveInlining)] - void EnsureAdditionalCapacity(int additionalCapacity) + var size = 0; + var x = value; + do { - var bufferLength = _buffer.Length; - - var freeSpace = bufferLength - _position; - if (freeSpace >= additionalCapacity) + var encodedByte = x % 128; + x /= 128; + if (x > 0) { - return; + encodedByte |= 128; } - EnsureCapacity(bufferLength + additionalCapacity - freeSpace); + _buffer[_position + size] = (byte)encodedByte; + size++; + } while (x > 0); + + IncreasePosition(size); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + void EnsureAdditionalCapacity(int additionalCapacity) + { + var bufferLength = _buffer.Length; + + var freeSpace = bufferLength - _position; + if (freeSpace >= additionalCapacity) + { + return; } - [MethodImpl(MethodImplOptions.AggressiveInlining)] - void EnsureCapacity(int capacity) + EnsureCapacity(bufferLength + additionalCapacity - freeSpace); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + void EnsureCapacity(int capacity) + { + var newBufferLength = _buffer.Length; + + if (newBufferLength >= capacity) { - var newBufferLength = _buffer.Length; - - if (newBufferLength >= capacity) - { - return; - } - - while (newBufferLength < capacity) - { - newBufferLength *= 2; - } - - // Array.Resize will create a new array and copy the existing data to the new one. - Array.Resize(ref _buffer, newBufferLength); + return; } - [MethodImpl(MethodImplOptions.AggressiveInlining)] - void IncreasePosition(int length) + while (newBufferLength < capacity) { - _position += length; + newBufferLength *= 2; + } - if (_position > Length) - { - // Also extend the position because we reached the end of the - // pre allocated buffer. - Length = _position; - } + // Array.Resize will create a new array and copy the existing data to the new one. + Array.Resize(ref _buffer, newBufferLength); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + void IncreasePosition(int length) + { + _position += length; + + if (_position > Length) + { + // Also extend the position because we reached the end of the + // pre allocated buffer. + Length = _position; } } } \ No newline at end of file diff --git a/Source/MQTTnet/Formatter/MqttConnectPacketFactory.cs b/Source/MQTTnet/Formatter/MqttConnectPacketFactory.cs index 89dda059..a93e070f 100644 --- a/Source/MQTTnet/Formatter/MqttConnectPacketFactory.cs +++ b/Source/MQTTnet/Formatter/MqttConnectPacketFactory.cs @@ -33,22 +33,24 @@ public static class MqttConnectPacketFactory TryPrivate = clientOptions.TryPrivate }; - if (!string.IsNullOrEmpty(clientOptions.WillTopic)) + if (string.IsNullOrEmpty(clientOptions.WillTopic)) { - connectPacket.WillFlag = true; - connectPacket.WillTopic = clientOptions.WillTopic; - connectPacket.WillQoS = clientOptions.WillQualityOfServiceLevel; - connectPacket.WillMessage = clientOptions.WillPayload; - connectPacket.WillRetain = clientOptions.WillRetain; - connectPacket.WillDelayInterval = clientOptions.WillDelayInterval; - connectPacket.WillContentType = clientOptions.WillContentType; - connectPacket.WillCorrelationData = clientOptions.WillCorrelationData; - connectPacket.WillResponseTopic = clientOptions.WillResponseTopic; - connectPacket.WillMessageExpiryInterval = clientOptions.WillMessageExpiryInterval; - connectPacket.WillPayloadFormatIndicator = clientOptions.WillPayloadFormatIndicator; - connectPacket.WillUserProperties = clientOptions.WillUserProperties; + return connectPacket; } + connectPacket.WillFlag = true; + connectPacket.WillTopic = clientOptions.WillTopic; + connectPacket.WillQoS = clientOptions.WillQualityOfServiceLevel; + connectPacket.WillMessage = clientOptions.WillPayload; + connectPacket.WillRetain = clientOptions.WillRetain; + connectPacket.WillDelayInterval = clientOptions.WillDelayInterval; + connectPacket.WillContentType = clientOptions.WillContentType; + connectPacket.WillCorrelationData = clientOptions.WillCorrelationData; + connectPacket.WillResponseTopic = clientOptions.WillResponseTopic; + connectPacket.WillMessageExpiryInterval = clientOptions.WillMessageExpiryInterval; + connectPacket.WillPayloadFormatIndicator = clientOptions.WillPayloadFormatIndicator; + connectPacket.WillUserProperties = clientOptions.WillUserProperties; + return connectPacket; } } \ No newline at end of file diff --git a/Source/MQTTnet/Formatter/MqttConnectReasonCodeConverter.cs b/Source/MQTTnet/Formatter/MqttConnectReasonCodeConverter.cs index cad1799b..a4bf70a6 100644 --- a/Source/MQTTnet/Formatter/MqttConnectReasonCodeConverter.cs +++ b/Source/MQTTnet/Formatter/MqttConnectReasonCodeConverter.cs @@ -4,52 +4,51 @@ using MQTTnet.Protocol; -namespace MQTTnet.Formatter +namespace MQTTnet.Formatter; + +public static class MqttConnectReasonCodeConverter { - public static class MqttConnectReasonCodeConverter + public static MqttConnectReturnCode ToConnectReturnCode(MqttConnectReasonCode reasonCode) { - public static MqttConnectReturnCode ToConnectReturnCode(MqttConnectReasonCode reasonCode) + switch (reasonCode) { - switch (reasonCode) + case MqttConnectReasonCode.Success: { - case MqttConnectReasonCode.Success: - { - return MqttConnectReturnCode.ConnectionAccepted; - } - - case MqttConnectReasonCode.Banned: - case MqttConnectReasonCode.NotAuthorized: - { - return MqttConnectReturnCode.ConnectionRefusedNotAuthorized; - } - - case MqttConnectReasonCode.BadAuthenticationMethod: - case MqttConnectReasonCode.BadUserNameOrPassword: - { - return MqttConnectReturnCode.ConnectionRefusedBadUsernameOrPassword; - } - - case MqttConnectReasonCode.ClientIdentifierNotValid: - { - return MqttConnectReturnCode.ConnectionRefusedIdentifierRejected; - } - - case MqttConnectReasonCode.UnsupportedProtocolVersion: - { - return MqttConnectReturnCode.ConnectionRefusedUnacceptableProtocolVersion; - } - - case MqttConnectReasonCode.UseAnotherServer: - case MqttConnectReasonCode.ServerUnavailable: - case MqttConnectReasonCode.ServerBusy: - case MqttConnectReasonCode.ServerMoved: - { - return MqttConnectReturnCode.ConnectionRefusedServerUnavailable; - } - - default: - return MqttConnectReturnCode.ConnectionRefusedUnacceptableProtocolVersion; + return MqttConnectReturnCode.ConnectionAccepted; } + + case MqttConnectReasonCode.Banned: + case MqttConnectReasonCode.NotAuthorized: + { + return MqttConnectReturnCode.ConnectionRefusedNotAuthorized; + } + + case MqttConnectReasonCode.BadAuthenticationMethod: + case MqttConnectReasonCode.BadUserNameOrPassword: + { + return MqttConnectReturnCode.ConnectionRefusedBadUsernameOrPassword; + } + + case MqttConnectReasonCode.ClientIdentifierNotValid: + { + return MqttConnectReturnCode.ConnectionRefusedIdentifierRejected; + } + + case MqttConnectReasonCode.UnsupportedProtocolVersion: + { + return MqttConnectReturnCode.ConnectionRefusedUnacceptableProtocolVersion; + } + + case MqttConnectReasonCode.UseAnotherServer: + case MqttConnectReasonCode.ServerUnavailable: + case MqttConnectReasonCode.ServerBusy: + case MqttConnectReasonCode.ServerMoved: + { + return MqttConnectReturnCode.ConnectionRefusedServerUnavailable; + } + + default: + return MqttConnectReturnCode.ConnectionRefusedUnacceptableProtocolVersion; } } } \ No newline at end of file diff --git a/Source/MQTTnet/Formatter/MqttFixedHeader.cs b/Source/MQTTnet/Formatter/MqttFixedHeader.cs index cb446d62..3460a47d 100644 --- a/Source/MQTTnet/Formatter/MqttFixedHeader.cs +++ b/Source/MQTTnet/Formatter/MqttFixedHeader.cs @@ -2,21 +2,13 @@ // The .NET Foundation licenses this file to you under the MIT license. // See the LICENSE file in the project root for more information. -namespace MQTTnet.Formatter +namespace MQTTnet.Formatter; + +public struct MqttFixedHeader(byte flags, int remainingLength, int totalLength) { - public struct MqttFixedHeader - { - public MqttFixedHeader(byte flags, int remainingLength, int totalLength) - { - Flags = flags; - RemainingLength = remainingLength; - TotalLength = totalLength; - } + public byte Flags { get; } = flags; - public byte Flags { get; } + public int RemainingLength { get; } = remainingLength; - public int RemainingLength { get; } - - public int TotalLength { get; } - } + public int TotalLength { get; } = totalLength; } \ No newline at end of file diff --git a/Source/MQTTnet/Formatter/MqttPacketBuffer.cs b/Source/MQTTnet/Formatter/MqttPacketBuffer.cs index b87939e7..91376e35 100644 --- a/Source/MQTTnet/Formatter/MqttPacketBuffer.cs +++ b/Source/MQTTnet/Formatter/MqttPacketBuffer.cs @@ -6,53 +6,53 @@ using MQTTnet.Internal; using System; using System.Buffers; -namespace MQTTnet.Formatter +namespace MQTTnet.Formatter; + +public readonly struct MqttPacketBuffer { - public readonly struct MqttPacketBuffer + public MqttPacketBuffer(ArraySegment packet, ReadOnlySequence payload) { - public MqttPacketBuffer(ArraySegment packet, ReadOnlySequence payload) - { - Packet = packet; - Payload = payload; + Packet = packet; + Payload = payload; - Length = Packet.Count + (int)Payload.Length; + Length = Packet.Count + (int)Payload.Length; + } + + public MqttPacketBuffer(ArraySegment packet) + { + Packet = packet; + Payload = EmptyBuffer.ReadOnlySequence; + + Length = Packet.Count; + } + + public int Length { get; } + + public ArraySegment Packet { get; } + + public ReadOnlySequence Payload { get; } + + public byte[] ToArray() + { + if (Payload.Length == 0) + { + return Packet.ToArray(); } - public MqttPacketBuffer(ArraySegment packet) - { - Packet = packet; - Payload = EmptyBuffer.ReadOnlySequence; + var buffer = GC.AllocateUninitializedArray(Length); + MqttMemoryHelper.Copy(Packet.Array, Packet.Offset, buffer, 0, Packet.Count); + MqttMemoryHelper.Copy(Payload, 0, buffer, Packet.Count, (int)Payload.Length); - Length = Packet.Count; + return buffer; + } + + public ArraySegment Join() + { + if (Payload.Length == 0) + { + return Packet; } - public int Length { get; } - - public ArraySegment Packet { get; } - - public ReadOnlySequence Payload { get; } - - public byte[] ToArray() - { - if (Payload.Length == 0) - { - return Packet.ToArray(); - } - - var buffer = GC.AllocateUninitializedArray(Length); - MqttMemoryHelper.Copy(Packet.Array, Packet.Offset, buffer, 0, Packet.Count); - MqttMemoryHelper.Copy(Payload, 0, buffer, Packet.Count, (int)Payload.Length); - - return buffer; - } - - public ArraySegment Join() - { - if (Payload.Length == 0) - { - return Packet; - } - return new ArraySegment(this.ToArray()); - } + return new ArraySegment(ToArray()); } } \ No newline at end of file diff --git a/Source/MQTTnet/Formatter/MqttPacketFormatterAdapter.cs b/Source/MQTTnet/Formatter/MqttPacketFormatterAdapter.cs index 7022740a..4f974369 100644 --- a/Source/MQTTnet/Formatter/MqttPacketFormatterAdapter.cs +++ b/Source/MQTTnet/Formatter/MqttPacketFormatterAdapter.cs @@ -10,141 +10,130 @@ using MQTTnet.Formatter.V3; using MQTTnet.Formatter.V5; using MQTTnet.Packets; -namespace MQTTnet.Formatter +namespace MQTTnet.Formatter; + +public sealed class MqttPacketFormatterAdapter { - public sealed class MqttPacketFormatterAdapter + readonly MqttBufferReader _bufferReader = new(); + readonly MqttBufferWriter _bufferWriter; + + IMqttPacketFormatter _formatter; + + public MqttPacketFormatterAdapter(MqttBufferWriter mqttBufferWriter) { - readonly MqttBufferReader _bufferReader = new MqttBufferReader(); - readonly MqttBufferWriter _bufferWriter; + _bufferWriter = mqttBufferWriter ?? throw new ArgumentNullException(nameof(mqttBufferWriter)); + } - IMqttPacketFormatter _formatter; + public MqttPacketFormatterAdapter(MqttProtocolVersion protocolVersion, MqttBufferWriter bufferWriter) + : this(bufferWriter) + { + UseProtocolVersion(protocolVersion); + } - public MqttPacketFormatterAdapter(MqttBufferWriter mqttBufferWriter) + public MqttProtocolVersion ProtocolVersion { get; private set; } = MqttProtocolVersion.Unknown; + + public void Cleanup() + { + _bufferWriter.Cleanup(); + } + + public MqttPacket Decode(ReceivedMqttPacket receivedMqttPacket) + { + ThrowIfFormatterNotSet(); + return _formatter.Decode(receivedMqttPacket); + } + + public void DetectProtocolVersion(ReceivedMqttPacket receivedMqttPacket) + { + var protocolVersion = ParseProtocolVersion(receivedMqttPacket); + UseProtocolVersion(protocolVersion); + } + + public MqttPacketBuffer Encode(MqttPacket packet) + { + ThrowIfFormatterNotSet(); + return _formatter.Encode(packet); + } + + public static IMqttPacketFormatter GetMqttPacketFormatter(MqttProtocolVersion protocolVersion, MqttBufferWriter bufferWriter) + { + if (protocolVersion == MqttProtocolVersion.Unknown) { - _bufferWriter = mqttBufferWriter ?? throw new ArgumentNullException(nameof(mqttBufferWriter)); + throw new InvalidOperationException("MQTT protocol version is invalid."); } - public MqttPacketFormatterAdapter(MqttProtocolVersion protocolVersion, MqttBufferWriter bufferWriter) - : this(bufferWriter) + return protocolVersion switch { - UseProtocolVersion(protocolVersion); + MqttProtocolVersion.V500 => new MqttV5PacketFormatter(bufferWriter), + MqttProtocolVersion.V310 or MqttProtocolVersion.V311 => new MqttV3PacketFormatter(bufferWriter, protocolVersion), + _ => throw new NotSupportedException() + }; + } + + MqttProtocolVersion ParseProtocolVersion(ReceivedMqttPacket receivedMqttPacket) + { + if (receivedMqttPacket.Body.Count < 7) + { + // 2 byte protocol name length + // at least 4 byte protocol name + // 1 byte protocol level + throw new MqttProtocolViolationException("CONNECT packet must have at least 7 bytes."); } - public MqttProtocolVersion ProtocolVersion { get; private set; } = MqttProtocolVersion.Unknown; + _bufferReader.SetBuffer(receivedMqttPacket.Body.Array, receivedMqttPacket.Body.Offset, receivedMqttPacket.Body.Count); - public void Cleanup() - { - _bufferWriter.Cleanup(); - } + var protocolName = _bufferReader.ReadString(); + var protocolLevel = _bufferReader.ReadByte(); - public MqttPacket Decode(ReceivedMqttPacket receivedMqttPacket) - { - ThrowIfFormatterNotSet(); - return _formatter.Decode(receivedMqttPacket); - } + // Remove the mosquitto try_private flag (MQTT 3.1.1 Bridge). + // This flag is accepted but not yet used. + protocolLevel &= 0x7F; - public void DetectProtocolVersion(ReceivedMqttPacket receivedMqttPacket) + if (protocolName == "MQTT") { - var protocolVersion = ParseProtocolVersion(receivedMqttPacket); - UseProtocolVersion(protocolVersion); - } - - public MqttPacketBuffer Encode(MqttPacket packet) - { - ThrowIfFormatterNotSet(); - return _formatter.Encode(packet); - } - - public static IMqttPacketFormatter GetMqttPacketFormatter(MqttProtocolVersion protocolVersion, MqttBufferWriter bufferWriter) - { - if (protocolVersion == MqttProtocolVersion.Unknown) + if (protocolLevel == 5) { - throw new InvalidOperationException("MQTT protocol version is invalid."); + return MqttProtocolVersion.V500; } - switch (protocolVersion) + if (protocolLevel == 4) { - case MqttProtocolVersion.V500: - { - return new MqttV5PacketFormatter(bufferWriter); - } - case MqttProtocolVersion.V310: - case MqttProtocolVersion.V311: - { - return new MqttV3PacketFormatter(bufferWriter, protocolVersion); - } - default: - { - throw new NotSupportedException(); - } + return MqttProtocolVersion.V311; } + + throw new MqttProtocolViolationException($"Protocol level '{protocolLevel}' not supported."); } - MqttProtocolVersion ParseProtocolVersion(ReceivedMqttPacket receivedMqttPacket) + if (protocolName == "MQIsdp") { - if (receivedMqttPacket.Body.Count < 7) + if (protocolLevel == 3) { - // 2 byte protocol name length - // at least 4 byte protocol name - // 1 byte protocol level - throw new MqttProtocolViolationException("CONNECT packet must have at least 7 bytes."); + return MqttProtocolVersion.V310; } - _bufferReader.SetBuffer(receivedMqttPacket.Body.Array, receivedMqttPacket.Body.Offset, receivedMqttPacket.Body.Count); - - var protocolName = _bufferReader.ReadString(); - var protocolLevel = _bufferReader.ReadByte(); - - // Remove the mosquitto try_private flag (MQTT 3.1.1 Bridge). - // This flag is accepted but not yet used. - protocolLevel &= 0x7F; - - if (protocolName == "MQTT") - { - if (protocolLevel == 5) - { - return MqttProtocolVersion.V500; - } - - if (protocolLevel == 4) - { - return MqttProtocolVersion.V311; - } - - throw new MqttProtocolViolationException($"Protocol level '{protocolLevel}' not supported."); - } - - if (protocolName == "MQIsdp") - { - if (protocolLevel == 3) - { - return MqttProtocolVersion.V310; - } - - throw new MqttProtocolViolationException($"Protocol level '{protocolLevel}' not supported."); - } - - throw new MqttProtocolViolationException($"Protocol '{protocolName}' not supported."); + throw new MqttProtocolViolationException($"Protocol level '{protocolLevel}' not supported."); } - [MethodImpl(MethodImplOptions.AggressiveInlining)] - void ThrowIfFormatterNotSet() - { - if (_formatter == null) - { - throw new InvalidOperationException("Protocol version not set or detected."); - } - } + throw new MqttProtocolViolationException($"Protocol '{protocolName}' not supported."); + } - void UseProtocolVersion(MqttProtocolVersion protocolVersion) + [MethodImpl(MethodImplOptions.AggressiveInlining)] + void ThrowIfFormatterNotSet() + { + if (_formatter == null) { - if (protocolVersion == MqttProtocolVersion.Unknown) - { - throw new InvalidOperationException("MQTT protocol version is invalid."); - } - - ProtocolVersion = protocolVersion; - _formatter = GetMqttPacketFormatter(protocolVersion, _bufferWriter); + throw new InvalidOperationException("Protocol version not set or detected."); } } + + void UseProtocolVersion(MqttProtocolVersion protocolVersion) + { + if (protocolVersion == MqttProtocolVersion.Unknown) + { + throw new InvalidOperationException("MQTT protocol version is invalid."); + } + + ProtocolVersion = protocolVersion; + _formatter = GetMqttPacketFormatter(protocolVersion, _bufferWriter); + } } \ No newline at end of file diff --git a/Source/MQTTnet/Formatter/MqttProtocolVersion.cs b/Source/MQTTnet/Formatter/MqttProtocolVersion.cs index 436951de..ad852489 100644 --- a/Source/MQTTnet/Formatter/MqttProtocolVersion.cs +++ b/Source/MQTTnet/Formatter/MqttProtocolVersion.cs @@ -2,14 +2,13 @@ // The .NET Foundation licenses this file to you under the MIT license. // See the LICENSE file in the project root for more information. -namespace MQTTnet.Formatter -{ - public enum MqttProtocolVersion - { - Unknown = 0, +namespace MQTTnet.Formatter; - V310 = 3, - V311 = 4, - V500 = 5 - } +public enum MqttProtocolVersion +{ + Unknown = 0, + + V310 = 3, + V311 = 4, + V500 = 5 } \ No newline at end of file diff --git a/Source/MQTTnet/Formatter/MqttPublishPacketFactory.cs b/Source/MQTTnet/Formatter/MqttPublishPacketFactory.cs index daf0237b..685e4d15 100644 --- a/Source/MQTTnet/Formatter/MqttPublishPacketFactory.cs +++ b/Source/MQTTnet/Formatter/MqttPublishPacketFactory.cs @@ -3,37 +3,35 @@ // See the LICENSE file in the project root for more information. using System; -using MQTTnet.Exceptions; using MQTTnet.Packets; -namespace MQTTnet.Formatter +namespace MQTTnet.Formatter; + +public static class MqttPublishPacketFactory { - public static class MqttPublishPacketFactory + public static MqttPublishPacket Create(MqttApplicationMessage applicationMessage) { - public static MqttPublishPacket Create(MqttApplicationMessage applicationMessage) + ArgumentNullException.ThrowIfNull(applicationMessage); + + // Copy all values to their matching counterparts. + // The not supported values in MQTT 3.1.1 are not serialized (excluded) later. + var packet = new MqttPublishPacket { - ArgumentNullException.ThrowIfNull(applicationMessage); + Topic = applicationMessage.Topic, + Payload = applicationMessage.Payload, + QualityOfServiceLevel = applicationMessage.QualityOfServiceLevel, + Retain = applicationMessage.Retain, + Dup = applicationMessage.Dup, + ContentType = applicationMessage.ContentType, + CorrelationData = applicationMessage.CorrelationData, + MessageExpiryInterval = applicationMessage.MessageExpiryInterval, + PayloadFormatIndicator = applicationMessage.PayloadFormatIndicator, + ResponseTopic = applicationMessage.ResponseTopic, + TopicAlias = applicationMessage.TopicAlias, + SubscriptionIdentifiers = applicationMessage.SubscriptionIdentifiers, + UserProperties = applicationMessage.UserProperties + }; - // Copy all values to their matching counterparts. - // The not supported values in MQTT 3.1.1 are not serialized (excluded) later. - var packet = new MqttPublishPacket - { - Topic = applicationMessage.Topic, - Payload = applicationMessage.Payload, - QualityOfServiceLevel = applicationMessage.QualityOfServiceLevel, - Retain = applicationMessage.Retain, - Dup = applicationMessage.Dup, - ContentType = applicationMessage.ContentType, - CorrelationData = applicationMessage.CorrelationData, - MessageExpiryInterval = applicationMessage.MessageExpiryInterval, - PayloadFormatIndicator = applicationMessage.PayloadFormatIndicator, - ResponseTopic = applicationMessage.ResponseTopic, - TopicAlias = applicationMessage.TopicAlias, - SubscriptionIdentifiers = applicationMessage.SubscriptionIdentifiers, - UserProperties = applicationMessage.UserProperties - }; - - return packet; - } + return packet; } } \ No newline at end of file diff --git a/Source/MQTTnet/Formatter/V3/MqttV3PacketFormatter.cs b/Source/MQTTnet/Formatter/V3/MqttV3PacketFormatter.cs index 20dd59f4..4e30ec90 100644 --- a/Source/MQTTnet/Formatter/V3/MqttV3PacketFormatter.cs +++ b/Source/MQTTnet/Formatter/V3/MqttV3PacketFormatter.cs @@ -5,809 +5,808 @@ using System; using System.Buffers; using System.Collections.Generic; -using System.Linq; using System.Runtime.CompilerServices; using MQTTnet.Adapter; using MQTTnet.Exceptions; using MQTTnet.Packets; using MQTTnet.Protocol; -namespace MQTTnet.Formatter.V3 +namespace MQTTnet.Formatter.V3; + +public sealed class MqttV3PacketFormatter : IMqttPacketFormatter { - public sealed class MqttV3PacketFormatter : IMqttPacketFormatter + const int FixedHeaderSize = 1; + + static readonly MqttDisconnectPacket DisconnectPacket = new(); + + readonly MqttBufferReader _bufferReader = new(); + readonly MqttBufferWriter _bufferWriter; + readonly MqttProtocolVersion _mqttProtocolVersion; + + public MqttV3PacketFormatter(MqttBufferWriter bufferWriter, MqttProtocolVersion mqttProtocolVersion) { - const int FixedHeaderSize = 1; + _bufferWriter = bufferWriter; + _mqttProtocolVersion = mqttProtocolVersion; + } - static readonly MqttDisconnectPacket DisconnectPacket = new(); - - readonly MqttBufferReader _bufferReader = new(); - readonly MqttBufferWriter _bufferWriter; - readonly MqttProtocolVersion _mqttProtocolVersion; - - public MqttV3PacketFormatter(MqttBufferWriter bufferWriter, MqttProtocolVersion mqttProtocolVersion) + public MqttPacket Decode(ReceivedMqttPacket receivedPacket) + { + if (receivedPacket.TotalLength == 0) { - _bufferWriter = bufferWriter; - _mqttProtocolVersion = mqttProtocolVersion; + return null; } - public MqttPacket Decode(ReceivedMqttPacket receivedMqttPacket) + var controlPacketType = receivedPacket.FixedHeader >> 4; + if (controlPacketType is < 1 or > 14) { - if (receivedMqttPacket.TotalLength == 0) - { - return null; - } - - var controlPacketType = receivedMqttPacket.FixedHeader >> 4; - if (controlPacketType < 1 || controlPacketType > 14) - { - throw new MqttProtocolViolationException($"The packet type is invalid ({controlPacketType})."); - } - - switch ((MqttControlPacketType)controlPacketType) - { - case MqttControlPacketType.Publish: - return DecodePublishPacket(receivedMqttPacket); - case MqttControlPacketType.PubAck: - return DecodePubAckPacket(receivedMqttPacket.Body); - case MqttControlPacketType.PubRec: - return DecodePubRecPacket(receivedMqttPacket.Body); - case MqttControlPacketType.PubRel: - return DecodePubRelPacket(receivedMqttPacket.Body); - case MqttControlPacketType.PubComp: - return DecodePubCompPacket(receivedMqttPacket.Body); - - case MqttControlPacketType.PingReq: - return MqttPingReqPacket.Instance; - case MqttControlPacketType.PingResp: - return MqttPingRespPacket.Instance; - - case MqttControlPacketType.Connect: - return DecodeConnectPacket(receivedMqttPacket.Body); - case MqttControlPacketType.ConnAck: - if (_mqttProtocolVersion == MqttProtocolVersion.V311) - { - return DecodeConnAckPacketV311(receivedMqttPacket.Body); - } - - return DecodeConnAckPacket(receivedMqttPacket.Body); - case MqttControlPacketType.Disconnect: - return DisconnectPacket; - - case MqttControlPacketType.Subscribe: - return DecodeSubscribePacket(receivedMqttPacket.Body); - case MqttControlPacketType.SubAck: - return DecodeSubAckPacket(receivedMqttPacket.Body); - case MqttControlPacketType.Unsubscribe: - return DecodeUnsubscribePacket(receivedMqttPacket.Body); - case MqttControlPacketType.UnsubAck: - return DecodeUnsubAckPacket(receivedMqttPacket.Body); - - default: - throw new MqttProtocolViolationException($"Packet type ({controlPacketType}) not supported."); - } + throw new MqttProtocolViolationException($"The packet type is invalid ({controlPacketType})."); } - public MqttPacketBuffer Encode(MqttPacket packet) + switch ((MqttControlPacketType)controlPacketType) { - ArgumentNullException.ThrowIfNull(packet); + case MqttControlPacketType.Publish: + return DecodePublishPacket(receivedPacket); + case MqttControlPacketType.PubAck: + return DecodePubAckPacket(receivedPacket.Body); + case MqttControlPacketType.PubRec: + return DecodePubRecPacket(receivedPacket.Body); + case MqttControlPacketType.PubRel: + return DecodePubRelPacket(receivedPacket.Body); + case MqttControlPacketType.PubComp: + return DecodePubCompPacket(receivedPacket.Body); - // Leave enough head space for max header size (fixed + 4 variable remaining length = 5 bytes) - _bufferWriter.Reset(5); - _bufferWriter.Seek(5); + case MqttControlPacketType.PingReq: + return MqttPingReqPacket.Instance; + case MqttControlPacketType.PingResp: + return MqttPingRespPacket.Instance; - var fixedHeader = EncodePacket(packet, _bufferWriter); - var remainingLength = (uint)(_bufferWriter.Length - 5); - - ReadOnlySequence payload = default; - if (packet is MqttPublishPacket publishPacket) - { - payload = publishPacket.Payload; - remainingLength += (uint)payload.Length; - } - - var remainingLengthSize = MqttBufferWriter.GetVariableByteIntegerSize(remainingLength); - - var headerSize = FixedHeaderSize + remainingLengthSize; - var headerOffset = 5 - headerSize; - - // Position cursor on correct offset on beginning of array (has leading 0x0) - _bufferWriter.Seek(headerOffset); - _bufferWriter.WriteByte(fixedHeader); - _bufferWriter.WriteVariableByteInteger(remainingLength); - - var firstSegment = new ArraySegment(_bufferWriter.GetBuffer(), headerOffset, _bufferWriter.Length - headerOffset); - - return payload.Length == 0 - ? new MqttPacketBuffer(firstSegment) - : new MqttPacketBuffer(firstSegment, payload); - } - - MqttPacket DecodeConnAckPacket(ArraySegment body) - { - ThrowIfBodyIsEmpty(body); - - _bufferReader.SetBuffer(body.Array, body.Offset, body.Count); - - var packet = new MqttConnAckPacket(); - - _bufferReader.ReadByte(); // Reserved. - packet.ReturnCode = (MqttConnectReturnCode)_bufferReader.ReadByte(); - - return packet; - } - - MqttPacket DecodeConnAckPacketV311(ArraySegment body) - { - ThrowIfBodyIsEmpty(body); - - _bufferReader.SetBuffer(body.Array, body.Offset, body.Count); - - var packet = new MqttConnAckPacket(); - - var acknowledgeFlags = _bufferReader.ReadByte(); - - packet.IsSessionPresent = (acknowledgeFlags & 0x1) > 0; - packet.ReturnCode = (MqttConnectReturnCode)_bufferReader.ReadByte(); - - return packet; - } - - MqttPacket DecodeConnectPacket(ArraySegment body) - { - ThrowIfBodyIsEmpty(body); - - _bufferReader.SetBuffer(body.Array, body.Offset, body.Count); - - var protocolName = _bufferReader.ReadString(); - var protocolVersion = _bufferReader.ReadByte(); - - if (protocolName != "MQTT" && protocolName != "MQIsdp") - { - throw new MqttProtocolViolationException("MQTT protocol name do not match MQTT v3."); - } - - var tryPrivate = (protocolVersion & 0x80) > 0; - protocolVersion &= 0x7F; - - if (protocolVersion != 3 && protocolVersion != 4) - { - throw new MqttProtocolViolationException("MQTT protocol version do not match MQTT v3."); - } - - var packet = new MqttConnectPacket - { - TryPrivate = tryPrivate - }; - - var connectFlags = _bufferReader.ReadByte(); - if ((connectFlags & 0x1) > 0) - { - throw new MqttProtocolViolationException("The first bit of the Connect Flags must be set to 0."); - } - - packet.CleanSession = (connectFlags & 0x2) > 0; - - var willFlag = (connectFlags & 0x4) > 0; - var willQoS = (connectFlags & 0x18) >> 3; - var willRetain = (connectFlags & 0x20) > 0; - var passwordFlag = (connectFlags & 0x40) > 0; - var usernameFlag = (connectFlags & 0x80) > 0; - - packet.KeepAlivePeriod = _bufferReader.ReadTwoByteInteger(); - packet.ClientId = _bufferReader.ReadString(); - - if (willFlag) - { - packet.WillFlag = true; - packet.WillQoS = (MqttQualityOfServiceLevel)willQoS; - packet.WillRetain = willRetain; - - packet.WillTopic = _bufferReader.ReadString(); - packet.WillMessage = _bufferReader.ReadBinaryData(); - } - - if (usernameFlag) - { - packet.Username = _bufferReader.ReadString(); - } - - if (passwordFlag) - { - packet.Password = _bufferReader.ReadBinaryData(); - } - - ValidateConnectPacket(packet); - return packet; - } - - MqttPacket DecodePubAckPacket(ArraySegment body) - { - ThrowIfBodyIsEmpty(body); - - _bufferReader.SetBuffer(body.Array, body.Offset, body.Count); - - return new MqttPubAckPacket - { - PacketIdentifier = _bufferReader.ReadTwoByteInteger() - }; - } - - MqttPacket DecodePubCompPacket(ArraySegment body) - { - ThrowIfBodyIsEmpty(body); - - _bufferReader.SetBuffer(body.Array, body.Offset, body.Count); - - return new MqttPubCompPacket - { - PacketIdentifier = _bufferReader.ReadTwoByteInteger() - }; - } - - MqttPacket DecodePublishPacket(ReceivedMqttPacket receivedMqttPacket) - { - ThrowIfBodyIsEmpty(receivedMqttPacket.Body); - - _bufferReader.SetBuffer(receivedMqttPacket.Body.Array, receivedMqttPacket.Body.Offset, receivedMqttPacket.Body.Count); - - var retain = (receivedMqttPacket.FixedHeader & 0x1) > 0; - var qualityOfServiceLevel = (MqttQualityOfServiceLevel)((receivedMqttPacket.FixedHeader >> 1) & 0x3); - var dup = (receivedMqttPacket.FixedHeader & 0x8) > 0; - - var topic = _bufferReader.ReadString(); - - ushort packetIdentifier = 0; - if (qualityOfServiceLevel > MqttQualityOfServiceLevel.AtMostOnce) - { - packetIdentifier = _bufferReader.ReadTwoByteInteger(); - } - - var packet = new MqttPublishPacket - { - PacketIdentifier = packetIdentifier, - Retain = retain, - Topic = topic, - QualityOfServiceLevel = qualityOfServiceLevel, - Dup = dup - }; - - if (!_bufferReader.EndOfStream) - { - packet.PayloadSegment = new ArraySegment(_bufferReader.ReadRemainingData()); - } - - return packet; - } - - MqttPacket DecodePubRecPacket(ArraySegment body) - { - ThrowIfBodyIsEmpty(body); - - _bufferReader.SetBuffer(body.Array, body.Offset, body.Count); - - return new MqttPubRecPacket - { - PacketIdentifier = _bufferReader.ReadTwoByteInteger() - }; - } - - MqttPacket DecodePubRelPacket(ArraySegment body) - { - ThrowIfBodyIsEmpty(body); - - _bufferReader.SetBuffer(body.Array, body.Offset, body.Count); - - return new MqttPubRelPacket - { - PacketIdentifier = _bufferReader.ReadTwoByteInteger() - }; - } - - MqttPacket DecodeSubAckPacket(ArraySegment body) - { - ThrowIfBodyIsEmpty(body); - - _bufferReader.SetBuffer(body.Array, body.Offset, body.Count); - - var packet = new MqttSubAckPacket - { - PacketIdentifier = _bufferReader.ReadTwoByteInteger(), - ReasonCodes = new List(_bufferReader.BytesLeft) - }; - - while (!_bufferReader.EndOfStream) - { - packet.ReasonCodes.Add((MqttSubscribeReasonCode)_bufferReader.ReadByte()); - } - - return packet; - } - - MqttPacket DecodeSubscribePacket(ArraySegment body) - { - ThrowIfBodyIsEmpty(body); - - _bufferReader.SetBuffer(body.Array, body.Offset, body.Count); - - var packet = new MqttSubscribePacket - { - PacketIdentifier = _bufferReader.ReadTwoByteInteger() - }; - - while (!_bufferReader.EndOfStream) - { - var topicFilter = new MqttTopicFilter + case MqttControlPacketType.Connect: + return DecodeConnectPacket(receivedPacket.Body); + case MqttControlPacketType.ConnAck: + if (_mqttProtocolVersion == MqttProtocolVersion.V311) { - Topic = _bufferReader.ReadString(), - QualityOfServiceLevel = (MqttQualityOfServiceLevel)_bufferReader.ReadByte() - }; - - packet.TopicFilters.Add(topicFilter); - } - - return packet; - } - - MqttPacket DecodeUnsubAckPacket(ArraySegment body) - { - ThrowIfBodyIsEmpty(body); - - _bufferReader.SetBuffer(body.Array, body.Offset, body.Count); - - return new MqttUnsubAckPacket - { - PacketIdentifier = _bufferReader.ReadTwoByteInteger() - }; - } - - MqttPacket DecodeUnsubscribePacket(ArraySegment body) - { - ThrowIfBodyIsEmpty(body); - - _bufferReader.SetBuffer(body.Array, body.Offset, body.Count); - - var packet = new MqttUnsubscribePacket - { - PacketIdentifier = _bufferReader.ReadTwoByteInteger() - }; - - while (!_bufferReader.EndOfStream) - { - packet.TopicFilters.Add(_bufferReader.ReadString()); - } - - return packet; - } - - byte EncodeConnAckPacket(MqttConnAckPacket packet, MqttBufferWriter bufferWriter) - { - bufferWriter.WriteByte(0); // Reserved. - bufferWriter.WriteByte((byte)packet.ReturnCode); - - return MqttBufferWriter.BuildFixedHeader(MqttControlPacketType.ConnAck); - } - - byte EncodeConnAckPacketV311(MqttConnAckPacket packet, MqttBufferWriter bufferWriter) - { - byte connectAcknowledgeFlags = 0x0; - if (packet.IsSessionPresent) - { - connectAcknowledgeFlags |= 0x1; - } - - bufferWriter.WriteByte(connectAcknowledgeFlags); - bufferWriter.WriteByte((byte)packet.ReturnCode); - - return MqttBufferWriter.BuildFixedHeader(MqttControlPacketType.ConnAck); - } - - byte EncodeConnectPacket(MqttConnectPacket packet, MqttBufferWriter bufferWriter) - { - ValidateConnectPacket(packet); - - bufferWriter.WriteString("MQIsdp"); - - var protocolVersion = 3; - if (packet.TryPrivate) - { - protocolVersion |= 0x80; - } - - bufferWriter.WriteByte((byte)protocolVersion); - - byte connectFlags = 0x0; - if (packet.CleanSession) - { - connectFlags |= 0x2; - } - - if (packet.WillFlag) - { - connectFlags |= 0x4; - connectFlags |= (byte)((byte)packet.WillQoS << 3); - - if (packet.WillRetain) - { - connectFlags |= 0x20; - } - } - - if (packet.Password != null && packet.Username == null) - { - throw new MqttProtocolViolationException("If the User Name Flag is set to 0, the Password Flag MUST be set to 0 [MQTT-3.1.2-22]."); - } - - if (packet.Password != null) - { - connectFlags |= 0x40; - } - - if (packet.Username != null) - { - connectFlags |= 0x80; - } - - bufferWriter.WriteByte(connectFlags); - bufferWriter.WriteTwoByteInteger(packet.KeepAlivePeriod); - bufferWriter.WriteString(packet.ClientId); - - if (packet.WillFlag) - { - bufferWriter.WriteString(packet.WillTopic); - bufferWriter.WriteBinary(packet.WillMessage); - } - - if (packet.Username != null) - { - bufferWriter.WriteString(packet.Username); - } - - if (packet.Password != null) - { - bufferWriter.WriteBinary(packet.Password); - } - - return MqttBufferWriter.BuildFixedHeader(MqttControlPacketType.Connect); - } - - byte EncodeConnectPacketV311(MqttConnectPacket packet, MqttBufferWriter bufferWriter) - { - ValidateConnectPacket(packet); - - bufferWriter.WriteString("MQTT"); - - // 3.1.2.2 Protocol Level 4 - var protocolVersion = 4; - - if (packet.TryPrivate) - { - protocolVersion |= 0x80; - } - - bufferWriter.WriteByte((byte)protocolVersion); - - byte connectFlags = 0x0; - if (packet.CleanSession) - { - connectFlags |= 0x2; - } - - if (packet.WillFlag) - { - connectFlags |= 0x4; - connectFlags |= (byte)((byte)packet.WillQoS << 3); - - if (packet.WillRetain) - { - connectFlags |= 0x20; - } - } - - if (packet.Password != null && packet.Username == null) - { - throw new MqttProtocolViolationException("If the User Name Flag is set to 0, the Password Flag MUST be set to 0 [MQTT-3.1.2-22]."); - } - - if (packet.Password != null) - { - connectFlags |= 0x40; - } - - if (packet.Username != null) - { - connectFlags |= 0x80; - } - - bufferWriter.WriteByte(connectFlags); - bufferWriter.WriteTwoByteInteger(packet.KeepAlivePeriod); - bufferWriter.WriteString(packet.ClientId); - - if (packet.WillFlag) - { - bufferWriter.WriteString(packet.WillTopic); - bufferWriter.WriteBinary(packet.WillMessage); - } - - if (packet.Username != null) - { - bufferWriter.WriteString(packet.Username); - } - - if (packet.Password != null) - { - bufferWriter.WriteBinary(packet.Password); - } - - return MqttBufferWriter.BuildFixedHeader(MqttControlPacketType.Connect); - } - - static byte EncodeEmptyPacket(MqttControlPacketType type) - { - return MqttBufferWriter.BuildFixedHeader(type); - } - - byte EncodePacket(MqttPacket packet, MqttBufferWriter bufferWriter) - { - switch (packet) - { - case MqttConnectPacket connectPacket: - if (_mqttProtocolVersion == MqttProtocolVersion.V311) - { - return EncodeConnectPacketV311(connectPacket, bufferWriter); - } - - return EncodeConnectPacket(connectPacket, bufferWriter); - case MqttConnAckPacket connAckPacket: - if (_mqttProtocolVersion == MqttProtocolVersion.V311) - { - return EncodeConnAckPacketV311(connAckPacket, bufferWriter); - } - - return EncodeConnAckPacket(connAckPacket, bufferWriter); - case MqttDisconnectPacket _: - return EncodeEmptyPacket(MqttControlPacketType.Disconnect); - case MqttPingReqPacket _: - return EncodeEmptyPacket(MqttControlPacketType.PingReq); - case MqttPingRespPacket _: - return EncodeEmptyPacket(MqttControlPacketType.PingResp); - case MqttPublishPacket publishPacket: - return EncodePublishPacket(publishPacket, bufferWriter); - case MqttPubAckPacket pubAckPacket: - return EncodePubAckPacket(pubAckPacket, bufferWriter); - case MqttPubRecPacket pubRecPacket: - return EncodePubRecPacket(pubRecPacket, bufferWriter); - case MqttPubRelPacket pubRelPacket: - return EncodePubRelPacket(pubRelPacket, bufferWriter); - case MqttPubCompPacket pubCompPacket: - return EncodePubCompPacket(pubCompPacket, bufferWriter); - case MqttSubscribePacket subscribePacket: - return EncodeSubscribePacket(subscribePacket, bufferWriter); - case MqttSubAckPacket subAckPacket: - return EncodeSubAckPacket(subAckPacket, bufferWriter); - case MqttUnsubscribePacket unsubscribePacket: - return EncodeUnsubscribePacket(unsubscribePacket, bufferWriter); - case MqttUnsubAckPacket unsubAckPacket: - return EncodeUnsubAckPacket(unsubAckPacket, bufferWriter); - - default: - throw new MqttProtocolViolationException("Packet type invalid."); - } - } - - static byte EncodePubAckPacket(MqttPubAckPacket packet, MqttBufferWriter bufferWriter) - { - if (packet.PacketIdentifier == 0) - { - throw new MqttProtocolViolationException("PubAck packet has no packet identifier."); - } - - bufferWriter.WriteTwoByteInteger(packet.PacketIdentifier); - - return MqttBufferWriter.BuildFixedHeader(MqttControlPacketType.PubAck); - } - - static byte EncodePubCompPacket(MqttPubCompPacket packet, MqttBufferWriter bufferWriter) - { - if (packet.PacketIdentifier == 0) - { - throw new MqttProtocolViolationException("PubComp packet has no packet identifier."); - } - - bufferWriter.WriteTwoByteInteger(packet.PacketIdentifier); - - return MqttBufferWriter.BuildFixedHeader(MqttControlPacketType.PubComp); - } - - static byte EncodePublishPacket(MqttPublishPacket packet, MqttBufferWriter bufferWriter) - { - ValidatePublishPacket(packet); - - bufferWriter.WriteString(packet.Topic); - - if (packet.QualityOfServiceLevel > MqttQualityOfServiceLevel.AtMostOnce) - { - if (packet.PacketIdentifier == 0) - { - throw new MqttProtocolViolationException("Publish packet has no packet identifier."); + return DecodeConnAckPacketV311(receivedPacket.Body); } - bufferWriter.WriteTwoByteInteger(packet.PacketIdentifier); - } - else + return DecodeConnAckPacket(receivedPacket.Body); + case MqttControlPacketType.Disconnect: + return DisconnectPacket; + + case MqttControlPacketType.Subscribe: + return DecodeSubscribePacket(receivedPacket.Body); + case MqttControlPacketType.SubAck: + return DecodeSubAckPacket(receivedPacket.Body); + case MqttControlPacketType.Unsubscribe: + return DecodeUnsubscribePacket(receivedPacket.Body); + case MqttControlPacketType.UnsubAck: + return DecodeUnsubAckPacket(receivedPacket.Body); + + case MqttControlPacketType.Auth: + default: + throw new MqttProtocolViolationException($"Packet type ({controlPacketType}) not supported."); + } + } + + public MqttPacketBuffer Encode(MqttPacket packet) + { + ArgumentNullException.ThrowIfNull(packet); + + // Leave enough headspace for max header size (fixed + 4 variable remaining length = 5 bytes) + _bufferWriter.Reset(5); + _bufferWriter.Seek(5); + + var fixedHeader = EncodePacket(packet, _bufferWriter); + var remainingLength = (uint)(_bufferWriter.Length - 5); + + ReadOnlySequence payload = default; + if (packet is MqttPublishPacket publishPacket) + { + payload = publishPacket.Payload; + remainingLength += (uint)payload.Length; + } + + var remainingLengthSize = MqttBufferWriter.GetVariableByteIntegerSize(remainingLength); + + var headerSize = FixedHeaderSize + remainingLengthSize; + var headerOffset = 5 - headerSize; + + // Position cursor on correct offset on beginning of array (has leading 0x0) + _bufferWriter.Seek(headerOffset); + _bufferWriter.WriteByte(fixedHeader); + _bufferWriter.WriteVariableByteInteger(remainingLength); + + var firstSegment = new ArraySegment(_bufferWriter.GetBuffer(), headerOffset, _bufferWriter.Length - headerOffset); + + return payload.Length == 0 + ? new MqttPacketBuffer(firstSegment) + : new MqttPacketBuffer(firstSegment, payload); + } + + MqttConnAckPacket DecodeConnAckPacket(ArraySegment body) + { + ThrowIfBodyIsEmpty(body); + + _bufferReader.SetBuffer(body.Array, body.Offset, body.Count); + + var packet = new MqttConnAckPacket(); + + _bufferReader.ReadByte(); // Reserved. + packet.ReturnCode = (MqttConnectReturnCode)_bufferReader.ReadByte(); + + return packet; + } + + MqttConnAckPacket DecodeConnAckPacketV311(ArraySegment body) + { + ThrowIfBodyIsEmpty(body); + + _bufferReader.SetBuffer(body.Array, body.Offset, body.Count); + + var packet = new MqttConnAckPacket(); + + var acknowledgeFlags = _bufferReader.ReadByte(); + + packet.IsSessionPresent = (acknowledgeFlags & 0x1) > 0; + packet.ReturnCode = (MqttConnectReturnCode)_bufferReader.ReadByte(); + + return packet; + } + + MqttConnectPacket DecodeConnectPacket(ArraySegment body) + { + ThrowIfBodyIsEmpty(body); + + _bufferReader.SetBuffer(body.Array, body.Offset, body.Count); + + var protocolName = _bufferReader.ReadString(); + var protocolVersion = _bufferReader.ReadByte(); + + if (protocolName != "MQTT" && protocolName != "MQIsdp") + { + throw new MqttProtocolViolationException("MQTT protocol name do not match MQTT v3."); + } + + var tryPrivate = (protocolVersion & 0x80) > 0; + protocolVersion &= 0x7F; + + if (protocolVersion != 3 && protocolVersion != 4) + { + throw new MqttProtocolViolationException("MQTT protocol version do not match MQTT v3."); + } + + var packet = new MqttConnectPacket + { + TryPrivate = tryPrivate + }; + + var connectFlags = _bufferReader.ReadByte(); + if ((connectFlags & 0x1) > 0) + { + throw new MqttProtocolViolationException("The first bit of the Connect Flags must be set to 0."); + } + + packet.CleanSession = (connectFlags & 0x2) > 0; + + var willFlag = (connectFlags & 0x4) > 0; + var willQoS = (connectFlags & 0x18) >> 3; + var willRetain = (connectFlags & 0x20) > 0; + var passwordFlag = (connectFlags & 0x40) > 0; + var usernameFlag = (connectFlags & 0x80) > 0; + + packet.KeepAlivePeriod = _bufferReader.ReadTwoByteInteger(); + packet.ClientId = _bufferReader.ReadString(); + + if (willFlag) + { + packet.WillFlag = true; + packet.WillQoS = (MqttQualityOfServiceLevel)willQoS; + packet.WillRetain = willRetain; + + packet.WillTopic = _bufferReader.ReadString(); + packet.WillMessage = _bufferReader.ReadBinaryData(); + } + + if (usernameFlag) + { + packet.Username = _bufferReader.ReadString(); + } + + if (passwordFlag) + { + packet.Password = _bufferReader.ReadBinaryData(); + } + + ValidateConnectPacket(packet); + return packet; + } + + MqttPubAckPacket DecodePubAckPacket(ArraySegment body) + { + ThrowIfBodyIsEmpty(body); + + _bufferReader.SetBuffer(body.Array, body.Offset, body.Count); + + return new MqttPubAckPacket + { + PacketIdentifier = _bufferReader.ReadTwoByteInteger() + }; + } + + MqttPubCompPacket DecodePubCompPacket(ArraySegment body) + { + ThrowIfBodyIsEmpty(body); + + _bufferReader.SetBuffer(body.Array, body.Offset, body.Count); + + return new MqttPubCompPacket + { + PacketIdentifier = _bufferReader.ReadTwoByteInteger() + }; + } + + MqttPublishPacket DecodePublishPacket(ReceivedMqttPacket receivedMqttPacket) + { + ThrowIfBodyIsEmpty(receivedMqttPacket.Body); + + _bufferReader.SetBuffer(receivedMqttPacket.Body.Array, receivedMqttPacket.Body.Offset, receivedMqttPacket.Body.Count); + + var retain = (receivedMqttPacket.FixedHeader & 0x1) > 0; + var qualityOfServiceLevel = (MqttQualityOfServiceLevel)((receivedMqttPacket.FixedHeader >> 1) & 0x3); + var dup = (receivedMqttPacket.FixedHeader & 0x8) > 0; + + var topic = _bufferReader.ReadString(); + + ushort packetIdentifier = 0; + if (qualityOfServiceLevel > MqttQualityOfServiceLevel.AtMostOnce) + { + packetIdentifier = _bufferReader.ReadTwoByteInteger(); + } + + var packet = new MqttPublishPacket + { + PacketIdentifier = packetIdentifier, + Retain = retain, + Topic = topic, + QualityOfServiceLevel = qualityOfServiceLevel, + Dup = dup + }; + + if (!_bufferReader.EndOfStream) + { + packet.PayloadSegment = new ArraySegment(_bufferReader.ReadRemainingData()); + } + + return packet; + } + + MqttPubRecPacket DecodePubRecPacket(ArraySegment body) + { + ThrowIfBodyIsEmpty(body); + + _bufferReader.SetBuffer(body.Array, body.Offset, body.Count); + + return new MqttPubRecPacket + { + PacketIdentifier = _bufferReader.ReadTwoByteInteger() + }; + } + + MqttPubRelPacket DecodePubRelPacket(ArraySegment body) + { + ThrowIfBodyIsEmpty(body); + + _bufferReader.SetBuffer(body.Array, body.Offset, body.Count); + + return new MqttPubRelPacket + { + PacketIdentifier = _bufferReader.ReadTwoByteInteger() + }; + } + + MqttSubAckPacket DecodeSubAckPacket(ArraySegment body) + { + ThrowIfBodyIsEmpty(body); + + _bufferReader.SetBuffer(body.Array, body.Offset, body.Count); + + var packet = new MqttSubAckPacket + { + PacketIdentifier = _bufferReader.ReadTwoByteInteger(), + ReasonCodes = new List(_bufferReader.BytesLeft) + }; + + while (!_bufferReader.EndOfStream) + { + packet.ReasonCodes.Add((MqttSubscribeReasonCode)_bufferReader.ReadByte()); + } + + return packet; + } + + MqttSubscribePacket DecodeSubscribePacket(ArraySegment body) + { + ThrowIfBodyIsEmpty(body); + + _bufferReader.SetBuffer(body.Array, body.Offset, body.Count); + + var packet = new MqttSubscribePacket + { + PacketIdentifier = _bufferReader.ReadTwoByteInteger() + }; + + while (!_bufferReader.EndOfStream) + { + var topicFilter = new MqttTopicFilter { - if (packet.PacketIdentifier > 0) + Topic = _bufferReader.ReadString(), + QualityOfServiceLevel = (MqttQualityOfServiceLevel)_bufferReader.ReadByte() + }; + + packet.TopicFilters.Add(topicFilter); + } + + return packet; + } + + MqttUnsubAckPacket DecodeUnsubAckPacket(ArraySegment body) + { + ThrowIfBodyIsEmpty(body); + + _bufferReader.SetBuffer(body.Array, body.Offset, body.Count); + + return new MqttUnsubAckPacket + { + PacketIdentifier = _bufferReader.ReadTwoByteInteger() + }; + } + + MqttUnsubscribePacket DecodeUnsubscribePacket(ArraySegment body) + { + ThrowIfBodyIsEmpty(body); + + _bufferReader.SetBuffer(body.Array, body.Offset, body.Count); + + var packet = new MqttUnsubscribePacket + { + PacketIdentifier = _bufferReader.ReadTwoByteInteger() + }; + + while (!_bufferReader.EndOfStream) + { + packet.TopicFilters.Add(_bufferReader.ReadString()); + } + + return packet; + } + + static byte EncodeConnAckPacket(MqttConnAckPacket packet, MqttBufferWriter bufferWriter) + { + bufferWriter.WriteByte(0); // Reserved. + bufferWriter.WriteByte((byte)packet.ReturnCode); + + return MqttBufferWriter.BuildFixedHeader(MqttControlPacketType.ConnAck); + } + + static byte EncodeConnAckPacketV311(MqttConnAckPacket packet, MqttBufferWriter bufferWriter) + { + byte connectAcknowledgeFlags = 0x0; + if (packet.IsSessionPresent) + { + connectAcknowledgeFlags |= 0x1; + } + + bufferWriter.WriteByte(connectAcknowledgeFlags); + bufferWriter.WriteByte((byte)packet.ReturnCode); + + return MqttBufferWriter.BuildFixedHeader(MqttControlPacketType.ConnAck); + } + + static byte EncodeConnectPacket(MqttConnectPacket packet, MqttBufferWriter bufferWriter) + { + ValidateConnectPacket(packet); + + bufferWriter.WriteString("MQIsdp"); + + var protocolVersion = 3; + if (packet.TryPrivate) + { + protocolVersion |= 0x80; + } + + bufferWriter.WriteByte((byte)protocolVersion); + + byte connectFlags = 0x0; + if (packet.CleanSession) + { + connectFlags |= 0x2; + } + + if (packet.WillFlag) + { + connectFlags |= 0x4; + connectFlags |= (byte)((byte)packet.WillQoS << 3); + + if (packet.WillRetain) + { + connectFlags |= 0x20; + } + } + + if (packet.Password != null && packet.Username == null) + { + throw new MqttProtocolViolationException("If the User Name Flag is set to 0, the Password Flag MUST be set to 0 [MQTT-3.1.2-22]."); + } + + if (packet.Password != null) + { + connectFlags |= 0x40; + } + + if (packet.Username != null) + { + connectFlags |= 0x80; + } + + bufferWriter.WriteByte(connectFlags); + bufferWriter.WriteTwoByteInteger(packet.KeepAlivePeriod); + bufferWriter.WriteString(packet.ClientId); + + if (packet.WillFlag) + { + bufferWriter.WriteString(packet.WillTopic); + bufferWriter.WriteBinary(packet.WillMessage); + } + + if (packet.Username != null) + { + bufferWriter.WriteString(packet.Username); + } + + if (packet.Password != null) + { + bufferWriter.WriteBinary(packet.Password); + } + + return MqttBufferWriter.BuildFixedHeader(MqttControlPacketType.Connect); + } + + static byte EncodeConnectPacketV311(MqttConnectPacket packet, MqttBufferWriter bufferWriter) + { + ValidateConnectPacket(packet); + + bufferWriter.WriteString("MQTT"); + + // 3.1.2.2 Protocol Level 4 + var protocolVersion = 4; + + if (packet.TryPrivate) + { + protocolVersion |= 0x80; + } + + bufferWriter.WriteByte((byte)protocolVersion); + + byte connectFlags = 0x0; + if (packet.CleanSession) + { + connectFlags |= 0x2; + } + + if (packet.WillFlag) + { + connectFlags |= 0x4; + connectFlags |= (byte)((byte)packet.WillQoS << 3); + + if (packet.WillRetain) + { + connectFlags |= 0x20; + } + } + + if (packet.Password != null && packet.Username == null) + { + throw new MqttProtocolViolationException("If the User Name Flag is set to 0, the Password Flag MUST be set to 0 [MQTT-3.1.2-22]."); + } + + if (packet.Password != null) + { + connectFlags |= 0x40; + } + + if (packet.Username != null) + { + connectFlags |= 0x80; + } + + bufferWriter.WriteByte(connectFlags); + bufferWriter.WriteTwoByteInteger(packet.KeepAlivePeriod); + bufferWriter.WriteString(packet.ClientId); + + if (packet.WillFlag) + { + bufferWriter.WriteString(packet.WillTopic); + bufferWriter.WriteBinary(packet.WillMessage); + } + + if (packet.Username != null) + { + bufferWriter.WriteString(packet.Username); + } + + if (packet.Password != null) + { + bufferWriter.WriteBinary(packet.Password); + } + + return MqttBufferWriter.BuildFixedHeader(MqttControlPacketType.Connect); + } + + static byte EncodeEmptyPacket(MqttControlPacketType type) + { + return MqttBufferWriter.BuildFixedHeader(type); + } + + byte EncodePacket(MqttPacket packet, MqttBufferWriter bufferWriter) + { + switch (packet) + { + case MqttConnectPacket connectPacket: + if (_mqttProtocolVersion == MqttProtocolVersion.V311) { - throw new MqttProtocolViolationException("Packet identifier must be empty if QoS == 0 [MQTT-2.3.1-5]."); + return EncodeConnectPacketV311(connectPacket, bufferWriter); + } + + return EncodeConnectPacket(connectPacket, bufferWriter); + case MqttConnAckPacket connAckPacket: + if (_mqttProtocolVersion == MqttProtocolVersion.V311) + { + return EncodeConnAckPacketV311(connAckPacket, bufferWriter); + } + + return EncodeConnAckPacket(connAckPacket, bufferWriter); + case MqttDisconnectPacket _: + return EncodeEmptyPacket(MqttControlPacketType.Disconnect); + case MqttPingReqPacket _: + return EncodeEmptyPacket(MqttControlPacketType.PingReq); + case MqttPingRespPacket _: + return EncodeEmptyPacket(MqttControlPacketType.PingResp); + case MqttPublishPacket publishPacket: + return EncodePublishPacket(publishPacket, bufferWriter); + case MqttPubAckPacket pubAckPacket: + return EncodePubAckPacket(pubAckPacket, bufferWriter); + case MqttPubRecPacket pubRecPacket: + return EncodePubRecPacket(pubRecPacket, bufferWriter); + case MqttPubRelPacket pubRelPacket: + return EncodePubRelPacket(pubRelPacket, bufferWriter); + case MqttPubCompPacket pubCompPacket: + return EncodePubCompPacket(pubCompPacket, bufferWriter); + case MqttSubscribePacket subscribePacket: + return EncodeSubscribePacket(subscribePacket, bufferWriter); + case MqttSubAckPacket subAckPacket: + return EncodeSubAckPacket(subAckPacket, bufferWriter); + case MqttUnsubscribePacket unsubscribePacket: + return EncodeUnsubscribePacket(unsubscribePacket, bufferWriter); + case MqttUnsubAckPacket unsubAckPacket: + return EncodeUnsubAckPacket(unsubAckPacket, bufferWriter); + + default: + throw new MqttProtocolViolationException("Packet type invalid."); + } + } + + static byte EncodePubAckPacket(MqttPubAckPacket packet, MqttBufferWriter bufferWriter) + { + if (packet.PacketIdentifier == 0) + { + throw new MqttProtocolViolationException("PubAck packet has no packet identifier."); + } + + bufferWriter.WriteTwoByteInteger(packet.PacketIdentifier); + + return MqttBufferWriter.BuildFixedHeader(MqttControlPacketType.PubAck); + } + + static byte EncodePubCompPacket(MqttPubCompPacket packet, MqttBufferWriter bufferWriter) + { + if (packet.PacketIdentifier == 0) + { + throw new MqttProtocolViolationException("PubComp packet has no packet identifier."); + } + + bufferWriter.WriteTwoByteInteger(packet.PacketIdentifier); + + return MqttBufferWriter.BuildFixedHeader(MqttControlPacketType.PubComp); + } + + static byte EncodePublishPacket(MqttPublishPacket packet, MqttBufferWriter bufferWriter) + { + ValidatePublishPacket(packet); + + bufferWriter.WriteString(packet.Topic); + + if (packet.QualityOfServiceLevel > MqttQualityOfServiceLevel.AtMostOnce) + { + if (packet.PacketIdentifier == 0) + { + throw new MqttProtocolViolationException("Publish packet has no packet identifier."); + } + + bufferWriter.WriteTwoByteInteger(packet.PacketIdentifier); + } + else + { + if (packet.PacketIdentifier > 0) + { + throw new MqttProtocolViolationException("Packet identifier must be empty if QoS == 0 [MQTT-2.3.1-5]."); + } + } + + // The payload is the past part of the packet. But it is not added here in order to keep + // memory allocation low. + + byte fixedHeader = 0; + + if (packet.Retain) + { + fixedHeader |= 0x01; + } + + fixedHeader |= (byte)((byte)packet.QualityOfServiceLevel << 1); + + if (packet.Dup) + { + fixedHeader |= 0x08; + } + + return MqttBufferWriter.BuildFixedHeader(MqttControlPacketType.Publish, fixedHeader); + } + + static byte EncodePubRecPacket(MqttPubRecPacket packet, MqttBufferWriter bufferWriter) + { + if (packet.PacketIdentifier == 0) + { + throw new MqttProtocolViolationException("PubRec packet has no packet identifier."); + } + + bufferWriter.WriteTwoByteInteger(packet.PacketIdentifier); + + return MqttBufferWriter.BuildFixedHeader(MqttControlPacketType.PubRec); + } + + static byte EncodePubRelPacket(MqttPubRelPacket packet, MqttBufferWriter bufferWriter) + { + if (packet.PacketIdentifier == 0) + { + throw new MqttProtocolViolationException("PubRel packet has no packet identifier."); + } + + bufferWriter.WriteTwoByteInteger(packet.PacketIdentifier); + + return MqttBufferWriter.BuildFixedHeader(MqttControlPacketType.PubRel, 0x02); + } + + static byte EncodeSubAckPacket(MqttSubAckPacket packet, MqttBufferWriter bufferWriter) + { + if (packet.PacketIdentifier == 0) + { + throw new MqttProtocolViolationException("SubAck packet has no packet identifier."); + } + + bufferWriter.WriteTwoByteInteger(packet.PacketIdentifier); + + if (packet.ReasonCodes.Count != 0) + { + foreach (var packetSubscribeReturnCode in packet.ReasonCodes) + { + if (packetSubscribeReturnCode == MqttSubscribeReasonCode.GrantedQoS0) + { + bufferWriter.WriteByte((byte)MqttSubscribeReturnCode.SuccessMaximumQoS0); + } + else if (packetSubscribeReturnCode == MqttSubscribeReasonCode.GrantedQoS1) + { + bufferWriter.WriteByte((byte)MqttSubscribeReturnCode.SuccessMaximumQoS1); + } + else if (packetSubscribeReturnCode == MqttSubscribeReasonCode.GrantedQoS2) + { + bufferWriter.WriteByte((byte)MqttSubscribeReturnCode.SuccessMaximumQoS2); + } + else + { + bufferWriter.WriteByte((byte)MqttSubscribeReturnCode.Failure); } } - - // The payload is the past part of the packet. But it is not added here in order to keep - // memory allocation low. - - byte fixedHeader = 0; - - if (packet.Retain) - { - fixedHeader |= 0x01; - } - - fixedHeader |= (byte)((byte)packet.QualityOfServiceLevel << 1); - - if (packet.Dup) - { - fixedHeader |= 0x08; - } - - return MqttBufferWriter.BuildFixedHeader(MqttControlPacketType.Publish, fixedHeader); } - static byte EncodePubRecPacket(MqttPubRecPacket packet, MqttBufferWriter bufferWriter) + return MqttBufferWriter.BuildFixedHeader(MqttControlPacketType.SubAck); + } + + static byte EncodeSubscribePacket(MqttSubscribePacket packet, MqttBufferWriter bufferWriter) + { + if (packet.TopicFilters.Count == 0) { - if (packet.PacketIdentifier == 0) - { - throw new MqttProtocolViolationException("PubRec packet has no packet identifier."); - } - - bufferWriter.WriteTwoByteInteger(packet.PacketIdentifier); - - return MqttBufferWriter.BuildFixedHeader(MqttControlPacketType.PubRec); + throw new MqttProtocolViolationException("At least one topic filter must be set [MQTT-3.8.3-3]."); } - static byte EncodePubRelPacket(MqttPubRelPacket packet, MqttBufferWriter bufferWriter) + if (packet.PacketIdentifier == 0) { - if (packet.PacketIdentifier == 0) - { - throw new MqttProtocolViolationException("PubRel packet has no packet identifier."); - } - - bufferWriter.WriteTwoByteInteger(packet.PacketIdentifier); - - return MqttBufferWriter.BuildFixedHeader(MqttControlPacketType.PubRel, 0x02); + throw new MqttProtocolViolationException("Subscribe packet has no packet identifier."); } - static byte EncodeSubAckPacket(MqttSubAckPacket packet, MqttBufferWriter bufferWriter) + bufferWriter.WriteTwoByteInteger(packet.PacketIdentifier); + + if (packet.TopicFilters?.Count > 0) { - if (packet.PacketIdentifier == 0) + foreach (var topicFilter in packet.TopicFilters) { - throw new MqttProtocolViolationException("SubAck packet has no packet identifier."); - } - - bufferWriter.WriteTwoByteInteger(packet.PacketIdentifier); - - if (packet.ReasonCodes.Any()) - { - foreach (var packetSubscribeReturnCode in packet.ReasonCodes) - { - if (packetSubscribeReturnCode == MqttSubscribeReasonCode.GrantedQoS0) - { - bufferWriter.WriteByte((byte)MqttSubscribeReturnCode.SuccessMaximumQoS0); - } - else if (packetSubscribeReturnCode == MqttSubscribeReasonCode.GrantedQoS1) - { - bufferWriter.WriteByte((byte)MqttSubscribeReturnCode.SuccessMaximumQoS1); - } - else if (packetSubscribeReturnCode == MqttSubscribeReasonCode.GrantedQoS2) - { - bufferWriter.WriteByte((byte)MqttSubscribeReturnCode.SuccessMaximumQoS2); - } - else - { - bufferWriter.WriteByte((byte)MqttSubscribeReturnCode.Failure); - } - } - } - - return MqttBufferWriter.BuildFixedHeader(MqttControlPacketType.SubAck); - } - - static byte EncodeSubscribePacket(MqttSubscribePacket packet, MqttBufferWriter bufferWriter) - { - if (!packet.TopicFilters.Any()) - { - throw new MqttProtocolViolationException("At least one topic filter must be set [MQTT-3.8.3-3]."); - } - - if (packet.PacketIdentifier == 0) - { - throw new MqttProtocolViolationException("Subscribe packet has no packet identifier."); - } - - bufferWriter.WriteTwoByteInteger(packet.PacketIdentifier); - - if (packet.TopicFilters?.Count > 0) - { - foreach (var topicFilter in packet.TopicFilters) - { - bufferWriter.WriteString(topicFilter.Topic); - bufferWriter.WriteByte((byte)topicFilter.QualityOfServiceLevel); - } - } - - return MqttBufferWriter.BuildFixedHeader(MqttControlPacketType.Subscribe, 0x02); - } - - static byte EncodeUnsubAckPacket(MqttUnsubAckPacket packet, MqttBufferWriter bufferWriter) - { - if (packet.PacketIdentifier == 0) - { - throw new MqttProtocolViolationException("UnsubAck packet has no packet identifier."); - } - - bufferWriter.WriteTwoByteInteger(packet.PacketIdentifier); - return MqttBufferWriter.BuildFixedHeader(MqttControlPacketType.UnsubAck); - } - - static byte EncodeUnsubscribePacket(MqttUnsubscribePacket packet, MqttBufferWriter bufferWriter) - { - if (!packet.TopicFilters.Any()) - { - throw new MqttProtocolViolationException("At least one topic filter must be set [MQTT-3.10.3-2]."); - } - - if (packet.PacketIdentifier == 0) - { - throw new MqttProtocolViolationException("Unsubscribe packet has no packet identifier."); - } - - bufferWriter.WriteTwoByteInteger(packet.PacketIdentifier); - - if (packet.TopicFilters?.Any() == true) - { - foreach (var topicFilter in packet.TopicFilters) - { - bufferWriter.WriteString(topicFilter); - } - } - - return MqttBufferWriter.BuildFixedHeader(MqttControlPacketType.Unsubscribe, 0x02); - } - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - static void ThrowIfBodyIsEmpty(ArraySegment body) - { - if (body.Count == 0) - { - throw new MqttProtocolViolationException("Data from the body is required but not present."); + bufferWriter.WriteString(topicFilter.Topic); + bufferWriter.WriteByte((byte)topicFilter.QualityOfServiceLevel); } } - void ValidateConnectPacket(MqttConnectPacket packet) - { - ArgumentNullException.ThrowIfNull(packet); + return MqttBufferWriter.BuildFixedHeader(MqttControlPacketType.Subscribe, 0x02); + } - if (string.IsNullOrEmpty(packet.ClientId) && !packet.CleanSession) + static byte EncodeUnsubAckPacket(MqttUnsubAckPacket packet, MqttBufferWriter bufferWriter) + { + if (packet.PacketIdentifier == 0) + { + throw new MqttProtocolViolationException("UnsubAck packet has no packet identifier."); + } + + bufferWriter.WriteTwoByteInteger(packet.PacketIdentifier); + return MqttBufferWriter.BuildFixedHeader(MqttControlPacketType.UnsubAck); + } + + static byte EncodeUnsubscribePacket(MqttUnsubscribePacket packet, MqttBufferWriter bufferWriter) + { + if (packet.TopicFilters.Count == 0) + { + throw new MqttProtocolViolationException("At least one topic filter must be set [MQTT-3.10.3-2]."); + } + + if (packet.PacketIdentifier == 0) + { + throw new MqttProtocolViolationException("Unsubscribe packet has no packet identifier."); + } + + bufferWriter.WriteTwoByteInteger(packet.PacketIdentifier); + + if (packet.TopicFilters?.Count > 0) + { + foreach (var topicFilter in packet.TopicFilters) { - throw new MqttProtocolViolationException("CleanSession must be set if ClientId is empty [MQTT-3.1.3-7]."); + bufferWriter.WriteString(topicFilter); } } - // ReSharper disable once ParameterOnlyUsedForPreconditionCheck.Local - static void ValidatePublishPacket(MqttPublishPacket packet) + return MqttBufferWriter.BuildFixedHeader(MqttControlPacketType.Unsubscribe, 0x02); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + static void ThrowIfBodyIsEmpty(ArraySegment body) + { + if (body.Count == 0) { - if (packet.QualityOfServiceLevel == 0 && packet.Dup) - { - throw new MqttProtocolViolationException("Dup flag must be false for QoS 0 packets [MQTT-3.3.1-2]."); - } + throw new MqttProtocolViolationException("Data from the body is required but not present."); + } + } + + static void ValidateConnectPacket(MqttConnectPacket packet) + { + ArgumentNullException.ThrowIfNull(packet); + + if (string.IsNullOrEmpty(packet.ClientId) && !packet.CleanSession) + { + throw new MqttProtocolViolationException("CleanSession must be set if ClientId is empty [MQTT-3.1.3-7]."); + } + } + + // ReSharper disable once ParameterOnlyUsedForPreconditionCheck.Local + static void ValidatePublishPacket(MqttPublishPacket packet) + { + if (packet.QualityOfServiceLevel == 0 && packet.Dup) + { + throw new MqttProtocolViolationException("Dup flag must be false for QoS 0 packets [MQTT-3.3.1-2]."); } } } \ No newline at end of file diff --git a/Source/MQTTnet/Formatter/V5/MqttV5PacketDecoder.cs b/Source/MQTTnet/Formatter/V5/MqttV5PacketDecoder.cs index 7f6a11f1..8997ae8f 100644 --- a/Source/MQTTnet/Formatter/V5/MqttV5PacketDecoder.cs +++ b/Source/MQTTnet/Formatter/V5/MqttV5PacketDecoder.cs @@ -9,774 +9,756 @@ using MQTTnet.Exceptions; using MQTTnet.Packets; using MQTTnet.Protocol; -namespace MQTTnet.Formatter.V5 +namespace MQTTnet.Formatter.V5; + +public sealed class MqttV5PacketDecoder { - public sealed class MqttV5PacketDecoder + readonly MqttBufferReader _bufferReader = new(); + + public MqttPacket Decode(ReceivedMqttPacket receivedMqttPacket) { - readonly MqttBufferReader _bufferReader = new(); - - public MqttPacket Decode(ReceivedMqttPacket receivedMqttPacket) + if (receivedMqttPacket.TotalLength == 0) { - if (receivedMqttPacket.TotalLength == 0) + return null; + } + + var controlPacketType = receivedMqttPacket.FixedHeader >> 4; + if (controlPacketType < 1) + { + throw new MqttProtocolViolationException($"The packet type is invalid ({controlPacketType})."); + } + + return (MqttControlPacketType)controlPacketType switch + { + MqttControlPacketType.Connect => DecodeConnectPacket(receivedMqttPacket.Body), + MqttControlPacketType.ConnAck => DecodeConnAckPacket(receivedMqttPacket.Body), + MqttControlPacketType.Disconnect => DecodeDisconnectPacket(receivedMqttPacket.Body), + MqttControlPacketType.Publish => DecodePublishPacket(receivedMqttPacket.FixedHeader, receivedMqttPacket.Body), + MqttControlPacketType.PubAck => DecodePubAckPacket(receivedMqttPacket.Body), + MqttControlPacketType.PubRec => DecodePubRecPacket(receivedMqttPacket.Body), + MqttControlPacketType.PubRel => DecodePubRelPacket(receivedMqttPacket.Body), + MqttControlPacketType.PubComp => DecodePubCompPacket(receivedMqttPacket.Body), + MqttControlPacketType.PingReq => MqttPingReqPacket.Instance, + MqttControlPacketType.PingResp => MqttPingRespPacket.Instance, + MqttControlPacketType.Subscribe => DecodeSubscribePacket(receivedMqttPacket.Body), + MqttControlPacketType.SubAck => DecodeSubAckPacket(receivedMqttPacket.Body), + MqttControlPacketType.Unsubscribe => DecodeUnsubscribePacket(receivedMqttPacket.Body), + MqttControlPacketType.UnsubAck => DecodeUnsubAckPacket(receivedMqttPacket.Body), + MqttControlPacketType.Auth => DecodeAuthPacket(receivedMqttPacket.Body), + _ => throw new MqttProtocolViolationException($"Packet type ({controlPacketType}) not supported.") + }; + } + + MqttAuthPacket DecodeAuthPacket(ArraySegment body) + { + _bufferReader.SetBuffer(body.Array, body.Offset, body.Count); + + var packet = new MqttAuthPacket(); + + // MQTT spec: The Reason Code and Property Length can be omitted if the Reason Code is 0x00 (Success) and there are no Properties. + // In this case the AUTH has a Remaining Length of 0. + if (_bufferReader.EndOfStream) + { + packet.ReasonCode = MqttAuthenticateReasonCode.Success; + return packet; + } + + packet.ReasonCode = (MqttAuthenticateReasonCode)_bufferReader.ReadByte(); + + var propertiesReader = new MqttV5PropertiesReader(_bufferReader); + while (propertiesReader.MoveNext()) + { + if (propertiesReader.CurrentPropertyId == MqttPropertyId.AuthenticationMethod) { - return null; + packet.AuthenticationMethod = propertiesReader.ReadAuthenticationMethod(); } - - var controlPacketType = receivedMqttPacket.FixedHeader >> 4; - if (controlPacketType < 1) + else if (propertiesReader.CurrentPropertyId == MqttPropertyId.AuthenticationData) { - throw new MqttProtocolViolationException($"The packet type is invalid ({controlPacketType})."); + packet.AuthenticationData = propertiesReader.ReadAuthenticationData(); } - - switch ((MqttControlPacketType)controlPacketType) + else if (propertiesReader.CurrentPropertyId == MqttPropertyId.ReasonString) { - case MqttControlPacketType.Connect: - return DecodeConnectPacket(receivedMqttPacket.Body); - case MqttControlPacketType.ConnAck: - return DecodeConnAckPacket(receivedMqttPacket.Body); - case MqttControlPacketType.Disconnect: - return DecodeDisconnectPacket(receivedMqttPacket.Body); - case MqttControlPacketType.Publish: - return DecodePublishPacket(receivedMqttPacket.FixedHeader, receivedMqttPacket.Body); - case MqttControlPacketType.PubAck: - return DecodePubAckPacket(receivedMqttPacket.Body); - case MqttControlPacketType.PubRec: - return DecodePubRecPacket(receivedMqttPacket.Body); - case MqttControlPacketType.PubRel: - return DecodePubRelPacket(receivedMqttPacket.Body); - case MqttControlPacketType.PubComp: - return DecodePubCompPacket(receivedMqttPacket.Body); - case MqttControlPacketType.PingReq: - return MqttPingReqPacket.Instance; - case MqttControlPacketType.PingResp: - return MqttPingRespPacket.Instance; - case MqttControlPacketType.Subscribe: - return DecodeSubscribePacket(receivedMqttPacket.Body); - case MqttControlPacketType.SubAck: - return DecodeSubAckPacket(receivedMqttPacket.Body); - case MqttControlPacketType.Unsubscribe: - return DecodeUnsubscribePacket(receivedMqttPacket.Body); - case MqttControlPacketType.UnsubAck: - return DecodeUnsubAckPacket(receivedMqttPacket.Body); - case MqttControlPacketType.Auth: - return DecodeAuthPacket(receivedMqttPacket.Body); - - default: - throw new MqttProtocolViolationException($"Packet type ({controlPacketType}) not supported."); + packet.ReasonString = propertiesReader.ReadReasonString(); + } + else + { + propertiesReader.ThrowInvalidPropertyIdException(typeof(MqttAuthPacket)); } } - MqttPacket DecodeAuthPacket(ArraySegment body) + packet.UserProperties = propertiesReader.CollectedUserProperties; + + return packet; + } + + MqttConnAckPacket DecodeConnAckPacket(ArraySegment body) + { + ThrowIfBodyIsEmpty(body); + + _bufferReader.SetBuffer(body.Array, body.Offset, body.Count); + + var acknowledgeFlags = _bufferReader.ReadByte(); + + var packet = new MqttConnAckPacket { - _bufferReader.SetBuffer(body.Array, body.Offset, body.Count); + IsSessionPresent = (acknowledgeFlags & 0x1) > 0, + ReasonCode = (MqttConnectReasonCode)_bufferReader.ReadByte(), + // indicate that a feature is available. + // Set all default values according to specification. When they are missing the often + RetainAvailable = true, + SharedSubscriptionAvailable = true, + SubscriptionIdentifiersAvailable = true, + WildcardSubscriptionAvailable = true, + // Absence indicates max QoS level. + MaximumQoS = MqttQualityOfServiceLevel.ExactlyOnce + }; - var packet = new MqttAuthPacket(); + // Also set the return code of MQTT 3.1.1 for backward compatibility and debugging purposes. + packet.ReturnCode = MqttConnectReasonCodeConverter.ToConnectReturnCode(packet.ReasonCode); - // MQTT spec: The Reason Code and Property Length can be omitted if the Reason Code is 0x00 (Success) and there are no Properties. - // In this case the AUTH has a Remaining Length of 0. - if (_bufferReader.EndOfStream) + var propertiesReader = new MqttV5PropertiesReader(_bufferReader); + while (propertiesReader.MoveNext()) + { + if (propertiesReader.CurrentPropertyId == MqttPropertyId.SessionExpiryInterval) { - packet.ReasonCode = MqttAuthenticateReasonCode.Success; - return packet; + packet.SessionExpiryInterval = propertiesReader.ReadSessionExpiryInterval(); } - - packet.ReasonCode = (MqttAuthenticateReasonCode)_bufferReader.ReadByte(); - - var propertiesReader = new MqttV5PropertiesReader(_bufferReader); - while (propertiesReader.MoveNext()) + else if (propertiesReader.CurrentPropertyId == MqttPropertyId.AuthenticationMethod) { - if (propertiesReader.CurrentPropertyId == MqttPropertyId.AuthenticationMethod) + packet.AuthenticationMethod = propertiesReader.ReadAuthenticationMethod(); + } + else if (propertiesReader.CurrentPropertyId == MqttPropertyId.AuthenticationData) + { + packet.AuthenticationData = propertiesReader.ReadAuthenticationData(); + } + else if (propertiesReader.CurrentPropertyId == MqttPropertyId.RetainAvailable) + { + packet.RetainAvailable = propertiesReader.ReadRetainAvailable(); + } + else if (propertiesReader.CurrentPropertyId == MqttPropertyId.ReceiveMaximum) + { + packet.ReceiveMaximum = propertiesReader.ReadReceiveMaximum(); + } + else if (propertiesReader.CurrentPropertyId == MqttPropertyId.MaximumQoS) + { + packet.MaximumQoS = propertiesReader.ReadMaximumQoS(); + } + else if (propertiesReader.CurrentPropertyId == MqttPropertyId.AssignedClientIdentifier) + { + packet.AssignedClientIdentifier = propertiesReader.ReadAssignedClientIdentifier(); + } + else if (propertiesReader.CurrentPropertyId == MqttPropertyId.TopicAliasMaximum) + { + packet.TopicAliasMaximum = propertiesReader.ReadTopicAliasMaximum(); + } + else if (propertiesReader.CurrentPropertyId == MqttPropertyId.ReasonString) + { + packet.ReasonString = propertiesReader.ReadReasonString(); + } + else if (propertiesReader.CurrentPropertyId == MqttPropertyId.MaximumPacketSize) + { + packet.MaximumPacketSize = propertiesReader.ReadMaximumPacketSize(); + } + else if (propertiesReader.CurrentPropertyId == MqttPropertyId.WildcardSubscriptionAvailable) + { + packet.WildcardSubscriptionAvailable = propertiesReader.ReadWildcardSubscriptionAvailable(); + } + else if (propertiesReader.CurrentPropertyId == MqttPropertyId.SubscriptionIdentifiersAvailable) + { + packet.SubscriptionIdentifiersAvailable = propertiesReader.ReadSubscriptionIdentifiersAvailable(); + } + else if (propertiesReader.CurrentPropertyId == MqttPropertyId.SharedSubscriptionAvailable) + { + packet.SharedSubscriptionAvailable = propertiesReader.ReadSharedSubscriptionAvailable(); + } + else if (propertiesReader.CurrentPropertyId == MqttPropertyId.ServerKeepAlive) + { + packet.ServerKeepAlive = propertiesReader.ReadServerKeepAlive(); + } + else if (propertiesReader.CurrentPropertyId == MqttPropertyId.ResponseInformation) + { + packet.ResponseInformation = propertiesReader.ReadResponseInformation(); + } + else if (propertiesReader.CurrentPropertyId == MqttPropertyId.ServerReference) + { + packet.ServerReference = propertiesReader.ReadServerReference(); + } + else + { + propertiesReader.ThrowInvalidPropertyIdException(typeof(MqttConnAckPacket)); + } + } + + packet.UserProperties = propertiesReader.CollectedUserProperties; + + return packet; + } + + MqttConnectPacket DecodeConnectPacket(ArraySegment body) + { + ThrowIfBodyIsEmpty(body); + + _bufferReader.SetBuffer(body.Array, body.Offset, body.Count); + + var packet = new MqttConnectPacket + { + // If the Request Problem Information is absent, the value of 1 is used. + RequestProblemInformation = true + }; + + var protocolName = _bufferReader.ReadString(); + var protocolVersion = _bufferReader.ReadByte(); + + if (protocolName != "MQTT" && protocolVersion != 5) + { + throw new MqttProtocolViolationException("MQTT protocol name and version do not match MQTT v5."); + } + + var connectFlags = _bufferReader.ReadByte(); + + var cleanSessionFlag = (connectFlags & 0x02) > 0; + var willMessageFlag = (connectFlags & 0x04) > 0; + var willMessageQoS = (byte)((connectFlags >> 3) & 3); + var willMessageRetainFlag = (connectFlags & 0x20) > 0; + var passwordFlag = (connectFlags & 0x40) > 0; + var usernameFlag = (connectFlags & 0x80) > 0; + + packet.CleanSession = cleanSessionFlag; + + if (willMessageFlag) + { + packet.WillFlag = true; + packet.WillQoS = (MqttQualityOfServiceLevel)willMessageQoS; + packet.WillRetain = willMessageRetainFlag; + } + + packet.KeepAlivePeriod = _bufferReader.ReadTwoByteInteger(); + + var propertiesReader = new MqttV5PropertiesReader(_bufferReader); + while (propertiesReader.MoveNext()) + { + if (propertiesReader.CurrentPropertyId == MqttPropertyId.SessionExpiryInterval) + { + packet.SessionExpiryInterval = propertiesReader.ReadSessionExpiryInterval(); + } + else if (propertiesReader.CurrentPropertyId == MqttPropertyId.AuthenticationMethod) + { + packet.AuthenticationMethod = propertiesReader.ReadAuthenticationMethod(); + } + else if (propertiesReader.CurrentPropertyId == MqttPropertyId.AuthenticationData) + { + packet.AuthenticationData = propertiesReader.ReadAuthenticationData(); + } + else if (propertiesReader.CurrentPropertyId == MqttPropertyId.ReceiveMaximum) + { + packet.ReceiveMaximum = propertiesReader.ReadReceiveMaximum(); + } + else if (propertiesReader.CurrentPropertyId == MqttPropertyId.TopicAliasMaximum) + { + packet.TopicAliasMaximum = propertiesReader.ReadTopicAliasMaximum(); + } + else if (propertiesReader.CurrentPropertyId == MqttPropertyId.MaximumPacketSize) + { + packet.MaximumPacketSize = propertiesReader.ReadMaximumPacketSize(); + } + else if (propertiesReader.CurrentPropertyId == MqttPropertyId.RequestResponseInformation) + { + packet.RequestResponseInformation = propertiesReader.RequestResponseInformation(); + } + else if (propertiesReader.CurrentPropertyId == MqttPropertyId.RequestProblemInformation) + { + packet.RequestProblemInformation = propertiesReader.RequestProblemInformation(); + } + else + { + propertiesReader.ThrowInvalidPropertyIdException(typeof(MqttConnectPacket)); + } + } + + packet.UserProperties = propertiesReader.CollectedUserProperties; + + packet.ClientId = _bufferReader.ReadString(); + + if (packet.WillFlag) + { + var willPropertiesReader = new MqttV5PropertiesReader(_bufferReader); + + while (willPropertiesReader.MoveNext()) + { + if (willPropertiesReader.CurrentPropertyId == MqttPropertyId.PayloadFormatIndicator) { - packet.AuthenticationMethod = propertiesReader.ReadAuthenticationMethod(); + packet.WillPayloadFormatIndicator = willPropertiesReader.ReadPayloadFormatIndicator(); } - else if (propertiesReader.CurrentPropertyId == MqttPropertyId.AuthenticationData) + else if (willPropertiesReader.CurrentPropertyId == MqttPropertyId.MessageExpiryInterval) { - packet.AuthenticationData = propertiesReader.ReadAuthenticationData(); + packet.WillMessageExpiryInterval = willPropertiesReader.ReadMessageExpiryInterval(); } - else if (propertiesReader.CurrentPropertyId == MqttPropertyId.ReasonString) + else if (willPropertiesReader.CurrentPropertyId == MqttPropertyId.ResponseTopic) { - packet.ReasonString = propertiesReader.ReadReasonString(); + packet.WillResponseTopic = willPropertiesReader.ReadResponseTopic(); + } + else if (willPropertiesReader.CurrentPropertyId == MqttPropertyId.CorrelationData) + { + packet.WillCorrelationData = willPropertiesReader.ReadCorrelationData(); + } + else if (willPropertiesReader.CurrentPropertyId == MqttPropertyId.ContentType) + { + packet.WillContentType = willPropertiesReader.ReadContentType(); + } + else if (willPropertiesReader.CurrentPropertyId == MqttPropertyId.WillDelayInterval) + { + packet.WillDelayInterval = willPropertiesReader.ReadWillDelayInterval(); } else { - propertiesReader.ThrowInvalidPropertyIdException(typeof(MqttAuthPacket)); + willPropertiesReader.ThrowInvalidPropertyIdException(typeof(MqttPublishPacket)); } } - packet.UserProperties = propertiesReader.CollectedUserProperties; - - return packet; + packet.WillTopic = _bufferReader.ReadString(); + packet.WillMessage = _bufferReader.ReadBinaryData(); + packet.WillUserProperties = willPropertiesReader.CollectedUserProperties; } - MqttPacket DecodeConnAckPacket(ArraySegment body) + if (usernameFlag) { - ThrowIfBodyIsEmpty(body); + packet.Username = _bufferReader.ReadString(); + } - _bufferReader.SetBuffer(body.Array, body.Offset, body.Count); + if (passwordFlag) + { + packet.Password = _bufferReader.ReadBinaryData(); + } - var acknowledgeFlags = _bufferReader.ReadByte(); + return packet; + } - var packet = new MqttConnAckPacket + MqttDisconnectPacket DecodeDisconnectPacket(ArraySegment body) + { + // From RFC: 3.14.2.1 Disconnect Reason Code + // Byte 1 in the Variable Header is the Disconnect Reason Code. + // If the Remaining Length is less than 1 the value of 0x00 (Normal disconnection) is used. + if (body.Count == 0) + { + return new MqttDisconnectPacket { - IsSessionPresent = (acknowledgeFlags & 0x1) > 0, - ReasonCode = (MqttConnectReasonCode)_bufferReader.ReadByte(), - // indicate that a feature is available. - // Set all default values according to specification. When they are missing the often - RetainAvailable = true, - SharedSubscriptionAvailable = true, - SubscriptionIdentifiersAvailable = true, - WildcardSubscriptionAvailable = true, - // Absence indicates max QoS level. - MaximumQoS = MqttQualityOfServiceLevel.ExactlyOnce + ReasonCode = MqttDisconnectReasonCode.NormalDisconnection }; + } - // Also set the return code of MQTT 3.1.1 for backward compatibility and debugging purposes. - packet.ReturnCode = MqttConnectReasonCodeConverter.ToConnectReturnCode(packet.ReasonCode); + _bufferReader.SetBuffer(body.Array, body.Offset, body.Count); - var propertiesReader = new MqttV5PropertiesReader(_bufferReader); - while (propertiesReader.MoveNext()) + var packet = new MqttDisconnectPacket + { + ReasonCode = (MqttDisconnectReasonCode)_bufferReader.ReadByte() + }; + + var propertiesReader = new MqttV5PropertiesReader(_bufferReader); + while (propertiesReader.MoveNext()) + { + if (propertiesReader.CurrentPropertyId == MqttPropertyId.SessionExpiryInterval) { - if (propertiesReader.CurrentPropertyId == MqttPropertyId.SessionExpiryInterval) - { - packet.SessionExpiryInterval = propertiesReader.ReadSessionExpiryInterval(); - } - else if (propertiesReader.CurrentPropertyId == MqttPropertyId.AuthenticationMethod) - { - packet.AuthenticationMethod = propertiesReader.ReadAuthenticationMethod(); - } - else if (propertiesReader.CurrentPropertyId == MqttPropertyId.AuthenticationData) - { - packet.AuthenticationData = propertiesReader.ReadAuthenticationData(); - } - else if (propertiesReader.CurrentPropertyId == MqttPropertyId.RetainAvailable) - { - packet.RetainAvailable = propertiesReader.ReadRetainAvailable(); - } - else if (propertiesReader.CurrentPropertyId == MqttPropertyId.ReceiveMaximum) - { - packet.ReceiveMaximum = propertiesReader.ReadReceiveMaximum(); - } - else if (propertiesReader.CurrentPropertyId == MqttPropertyId.MaximumQoS) - { - packet.MaximumQoS = propertiesReader.ReadMaximumQoS(); - } - else if (propertiesReader.CurrentPropertyId == MqttPropertyId.AssignedClientIdentifier) - { - packet.AssignedClientIdentifier = propertiesReader.ReadAssignedClientIdentifier(); - } - else if (propertiesReader.CurrentPropertyId == MqttPropertyId.TopicAliasMaximum) - { - packet.TopicAliasMaximum = propertiesReader.ReadTopicAliasMaximum(); - } - else if (propertiesReader.CurrentPropertyId == MqttPropertyId.ReasonString) - { - packet.ReasonString = propertiesReader.ReadReasonString(); - } - else if (propertiesReader.CurrentPropertyId == MqttPropertyId.MaximumPacketSize) - { - packet.MaximumPacketSize = propertiesReader.ReadMaximumPacketSize(); - } - else if (propertiesReader.CurrentPropertyId == MqttPropertyId.WildcardSubscriptionAvailable) - { - packet.WildcardSubscriptionAvailable = propertiesReader.ReadWildcardSubscriptionAvailable(); - } - else if (propertiesReader.CurrentPropertyId == MqttPropertyId.SubscriptionIdentifiersAvailable) - { - packet.SubscriptionIdentifiersAvailable = propertiesReader.ReadSubscriptionIdentifiersAvailable(); - } - else if (propertiesReader.CurrentPropertyId == MqttPropertyId.SharedSubscriptionAvailable) - { - packet.SharedSubscriptionAvailable = propertiesReader.ReadSharedSubscriptionAvailable(); - } - else if (propertiesReader.CurrentPropertyId == MqttPropertyId.ServerKeepAlive) - { - packet.ServerKeepAlive = propertiesReader.ReadServerKeepAlive(); - } - else if (propertiesReader.CurrentPropertyId == MqttPropertyId.ResponseInformation) - { - packet.ResponseInformation = propertiesReader.ReadResponseInformation(); - } - else if (propertiesReader.CurrentPropertyId == MqttPropertyId.ServerReference) - { - packet.ServerReference = propertiesReader.ReadServerReference(); - } - else - { - propertiesReader.ThrowInvalidPropertyIdException(typeof(MqttConnAckPacket)); - } + packet.SessionExpiryInterval = propertiesReader.ReadSessionExpiryInterval(); } + else if (propertiesReader.CurrentPropertyId == MqttPropertyId.ReasonString) + { + packet.ReasonString = propertiesReader.ReadReasonString(); + } + else if (propertiesReader.CurrentPropertyId == MqttPropertyId.ServerReference) + { + packet.ServerReference = propertiesReader.ReadServerReference(); + } + else + { + propertiesReader.ThrowInvalidPropertyIdException(typeof(MqttDisconnectPacket)); + } + } - packet.UserProperties = propertiesReader.CollectedUserProperties; + packet.UserProperties = propertiesReader.CollectedUserProperties; + return packet; + } + + MqttPubAckPacket DecodePubAckPacket(ArraySegment body) + { + ThrowIfBodyIsEmpty(body); + + _bufferReader.SetBuffer(body.Array, body.Offset, body.Count); + + var packet = new MqttPubAckPacket + { + PacketIdentifier = _bufferReader.ReadTwoByteInteger() + }; + + if (_bufferReader.EndOfStream) + { + packet.ReasonCode = MqttPubAckReasonCode.Success; return packet; } - MqttPacket DecodeConnectPacket(ArraySegment body) + packet.ReasonCode = (MqttPubAckReasonCode)_bufferReader.ReadByte(); + + var propertiesReader = new MqttV5PropertiesReader(_bufferReader); + while (propertiesReader.MoveNext()) { - ThrowIfBodyIsEmpty(body); - - _bufferReader.SetBuffer(body.Array, body.Offset, body.Count); - - var packet = new MqttConnectPacket + if (propertiesReader.CurrentPropertyId == MqttPropertyId.ReasonString) { - // If the Request Problem Information is absent, the value of 1 is used. - RequestProblemInformation = true - }; - - var protocolName = _bufferReader.ReadString(); - var protocolVersion = _bufferReader.ReadByte(); - - if (protocolName != "MQTT" && protocolVersion != 5) - { - throw new MqttProtocolViolationException("MQTT protocol name and version do not match MQTT v5."); + packet.ReasonString = propertiesReader.ReadReasonString(); } - - var connectFlags = _bufferReader.ReadByte(); - - var cleanSessionFlag = (connectFlags & 0x02) > 0; - var willMessageFlag = (connectFlags & 0x04) > 0; - var willMessageQoS = (byte)((connectFlags >> 3) & 3); - var willMessageRetainFlag = (connectFlags & 0x20) > 0; - var passwordFlag = (connectFlags & 0x40) > 0; - var usernameFlag = (connectFlags & 0x80) > 0; - - packet.CleanSession = cleanSessionFlag; - - if (willMessageFlag) + else { - packet.WillFlag = true; - packet.WillQoS = (MqttQualityOfServiceLevel)willMessageQoS; - packet.WillRetain = willMessageRetainFlag; + propertiesReader.ThrowInvalidPropertyIdException(typeof(MqttPubAckPacket)); } + } - packet.KeepAlivePeriod = _bufferReader.ReadTwoByteInteger(); + packet.UserProperties = propertiesReader.CollectedUserProperties; - var propertiesReader = new MqttV5PropertiesReader(_bufferReader); - while (propertiesReader.MoveNext()) - { - if (propertiesReader.CurrentPropertyId == MqttPropertyId.SessionExpiryInterval) - { - packet.SessionExpiryInterval = propertiesReader.ReadSessionExpiryInterval(); - } - else if (propertiesReader.CurrentPropertyId == MqttPropertyId.AuthenticationMethod) - { - packet.AuthenticationMethod = propertiesReader.ReadAuthenticationMethod(); - } - else if (propertiesReader.CurrentPropertyId == MqttPropertyId.AuthenticationData) - { - packet.AuthenticationData = propertiesReader.ReadAuthenticationData(); - } - else if (propertiesReader.CurrentPropertyId == MqttPropertyId.ReceiveMaximum) - { - packet.ReceiveMaximum = propertiesReader.ReadReceiveMaximum(); - } - else if (propertiesReader.CurrentPropertyId == MqttPropertyId.TopicAliasMaximum) - { - packet.TopicAliasMaximum = propertiesReader.ReadTopicAliasMaximum(); - } - else if (propertiesReader.CurrentPropertyId == MqttPropertyId.MaximumPacketSize) - { - packet.MaximumPacketSize = propertiesReader.ReadMaximumPacketSize(); - } - else if (propertiesReader.CurrentPropertyId == MqttPropertyId.RequestResponseInformation) - { - packet.RequestResponseInformation = propertiesReader.RequestResponseInformation(); - } - else if (propertiesReader.CurrentPropertyId == MqttPropertyId.RequestProblemInformation) - { - packet.RequestProblemInformation = propertiesReader.RequestProblemInformation(); - } - else - { - propertiesReader.ThrowInvalidPropertyIdException(typeof(MqttConnectPacket)); - } - } + return packet; + } - packet.UserProperties = propertiesReader.CollectedUserProperties; + MqttPubCompPacket DecodePubCompPacket(ArraySegment body) + { + ThrowIfBodyIsEmpty(body); - packet.ClientId = _bufferReader.ReadString(); + _bufferReader.SetBuffer(body.Array, body.Offset, body.Count); - if (packet.WillFlag) - { - var willPropertiesReader = new MqttV5PropertiesReader(_bufferReader); - - while (willPropertiesReader.MoveNext()) - { - if (willPropertiesReader.CurrentPropertyId == MqttPropertyId.PayloadFormatIndicator) - { - packet.WillPayloadFormatIndicator = willPropertiesReader.ReadPayloadFormatIndicator(); - } - else if (willPropertiesReader.CurrentPropertyId == MqttPropertyId.MessageExpiryInterval) - { - packet.WillMessageExpiryInterval = willPropertiesReader.ReadMessageExpiryInterval(); - } - else if (willPropertiesReader.CurrentPropertyId == MqttPropertyId.ResponseTopic) - { - packet.WillResponseTopic = willPropertiesReader.ReadResponseTopic(); - } - else if (willPropertiesReader.CurrentPropertyId == MqttPropertyId.CorrelationData) - { - packet.WillCorrelationData = willPropertiesReader.ReadCorrelationData(); - } - else if (willPropertiesReader.CurrentPropertyId == MqttPropertyId.ContentType) - { - packet.WillContentType = willPropertiesReader.ReadContentType(); - } - else if (willPropertiesReader.CurrentPropertyId == MqttPropertyId.WillDelayInterval) - { - packet.WillDelayInterval = willPropertiesReader.ReadWillDelayInterval(); - } - else - { - willPropertiesReader.ThrowInvalidPropertyIdException(typeof(MqttPublishPacket)); - } - } - - packet.WillTopic = _bufferReader.ReadString(); - packet.WillMessage = _bufferReader.ReadBinaryData(); - packet.WillUserProperties = willPropertiesReader.CollectedUserProperties; - } - - if (usernameFlag) - { - packet.Username = _bufferReader.ReadString(); - } - - if (passwordFlag) - { - packet.Password = _bufferReader.ReadBinaryData(); - } + var packet = new MqttPubCompPacket + { + PacketIdentifier = _bufferReader.ReadTwoByteInteger() + }; + if (_bufferReader.EndOfStream) + { + packet.ReasonCode = MqttPubCompReasonCode.Success; return packet; } - MqttPacket DecodeDisconnectPacket(ArraySegment body) + packet.ReasonCode = (MqttPubCompReasonCode)_bufferReader.ReadByte(); + + var propertiesReader = new MqttV5PropertiesReader(_bufferReader); + while (propertiesReader.MoveNext()) { - // From RFC: 3.14.2.1 Disconnect Reason Code - // Byte 1 in the Variable Header is the Disconnect Reason Code. - // If the Remaining Length is less than 1 the value of 0x00 (Normal disconnection) is used. - if (body.Count == 0) + if (propertiesReader.CurrentPropertyId == MqttPropertyId.ReasonString) { - return new MqttDisconnectPacket - { - ReasonCode = MqttDisconnectReasonCode.NormalDisconnection - }; + packet.ReasonString = propertiesReader.ReadReasonString(); } - - _bufferReader.SetBuffer(body.Array, body.Offset, body.Count); - - var packet = new MqttDisconnectPacket + else { - ReasonCode = (MqttDisconnectReasonCode)_bufferReader.ReadByte() - }; - - var propertiesReader = new MqttV5PropertiesReader(_bufferReader); - while (propertiesReader.MoveNext()) - { - if (propertiesReader.CurrentPropertyId == MqttPropertyId.SessionExpiryInterval) - { - packet.SessionExpiryInterval = propertiesReader.ReadSessionExpiryInterval(); - } - else if (propertiesReader.CurrentPropertyId == MqttPropertyId.ReasonString) - { - packet.ReasonString = propertiesReader.ReadReasonString(); - } - else if (propertiesReader.CurrentPropertyId == MqttPropertyId.ServerReference) - { - packet.ServerReference = propertiesReader.ReadServerReference(); - } - else - { - propertiesReader.ThrowInvalidPropertyIdException(typeof(MqttDisconnectPacket)); - } + propertiesReader.ThrowInvalidPropertyIdException(typeof(MqttPubCompPacket)); } + } - packet.UserProperties = propertiesReader.CollectedUserProperties; + packet.UserProperties = propertiesReader.CollectedUserProperties; + return packet; + } + + + MqttPublishPacket DecodePublishPacket(byte header, ArraySegment body) + { + ThrowIfBodyIsEmpty(body); + + _bufferReader.SetBuffer(body.Array, body.Offset, body.Count); + + var retain = (header & 1) > 0; + var qos = (MqttQualityOfServiceLevel)((header >> 1) & 3); + var dup = ((header >> 3) & 1) > 0; + + var packet = new MqttPublishPacket + { + Topic = _bufferReader.ReadString(), + Retain = retain, + QualityOfServiceLevel = qos, + Dup = dup + }; + + if (qos > 0) + { + packet.PacketIdentifier = _bufferReader.ReadTwoByteInteger(); + } + + var propertiesReader = new MqttV5PropertiesReader(_bufferReader); + while (propertiesReader.MoveNext()) + { + if (propertiesReader.CurrentPropertyId == MqttPropertyId.PayloadFormatIndicator) + { + packet.PayloadFormatIndicator = propertiesReader.ReadPayloadFormatIndicator(); + } + else if (propertiesReader.CurrentPropertyId == MqttPropertyId.MessageExpiryInterval) + { + packet.MessageExpiryInterval = propertiesReader.ReadMessageExpiryInterval(); + } + else if (propertiesReader.CurrentPropertyId == MqttPropertyId.TopicAlias) + { + packet.TopicAlias = propertiesReader.ReadTopicAlias(); + } + else if (propertiesReader.CurrentPropertyId == MqttPropertyId.ResponseTopic) + { + packet.ResponseTopic = propertiesReader.ReadResponseTopic(); + } + else if (propertiesReader.CurrentPropertyId == MqttPropertyId.CorrelationData) + { + packet.CorrelationData = propertiesReader.ReadCorrelationData(); + } + else if (propertiesReader.CurrentPropertyId == MqttPropertyId.SubscriptionIdentifier) + { + if (packet.SubscriptionIdentifiers == null) + { + packet.SubscriptionIdentifiers = new List(); + } + + packet.SubscriptionIdentifiers.Add(propertiesReader.ReadSubscriptionIdentifier()); + } + else if (propertiesReader.CurrentPropertyId == MqttPropertyId.ContentType) + { + packet.ContentType = propertiesReader.ReadContentType(); + } + else + { + propertiesReader.ThrowInvalidPropertyIdException(typeof(MqttPublishPacket)); + } + } + + packet.UserProperties = propertiesReader.CollectedUserProperties; + + if (!_bufferReader.EndOfStream) + { + packet.PayloadSegment = new ArraySegment(_bufferReader.ReadRemainingData()); + } + + return packet; + } + + MqttPubRecPacket DecodePubRecPacket(ArraySegment body) + { + ThrowIfBodyIsEmpty(body); + + _bufferReader.SetBuffer(body.Array, body.Offset, body.Count); + + var packet = new MqttPubRecPacket + { + PacketIdentifier = _bufferReader.ReadTwoByteInteger() + }; + + if (_bufferReader.EndOfStream) + { + packet.ReasonCode = MqttPubRecReasonCode.Success; return packet; } - MqttPacket DecodePubAckPacket(ArraySegment body) + packet.ReasonCode = (MqttPubRecReasonCode)_bufferReader.ReadByte(); + + var propertiesReader = new MqttV5PropertiesReader(_bufferReader); + while (propertiesReader.MoveNext()) { - ThrowIfBodyIsEmpty(body); - - _bufferReader.SetBuffer(body.Array, body.Offset, body.Count); - - var packet = new MqttPubAckPacket + if (propertiesReader.CurrentPropertyId == MqttPropertyId.ReasonString) { - PacketIdentifier = _bufferReader.ReadTwoByteInteger() - }; - - if (_bufferReader.EndOfStream) - { - packet.ReasonCode = MqttPubAckReasonCode.Success; - return packet; + packet.ReasonString = propertiesReader.ReadReasonString(); } - - packet.ReasonCode = (MqttPubAckReasonCode)_bufferReader.ReadByte(); - - var propertiesReader = new MqttV5PropertiesReader(_bufferReader); - while (propertiesReader.MoveNext()) + else { - if (propertiesReader.CurrentPropertyId == MqttPropertyId.ReasonString) - { - packet.ReasonString = propertiesReader.ReadReasonString(); - } - else - { - propertiesReader.ThrowInvalidPropertyIdException(typeof(MqttPubAckPacket)); - } + propertiesReader.ThrowInvalidPropertyIdException(typeof(MqttPubRecPacket)); } + } - packet.UserProperties = propertiesReader.CollectedUserProperties; + packet.UserProperties = propertiesReader.CollectedUserProperties; + return packet; + } + + MqttPubRelPacket DecodePubRelPacket(ArraySegment body) + { + ThrowIfBodyIsEmpty(body); + + _bufferReader.SetBuffer(body.Array, body.Offset, body.Count); + + var packet = new MqttPubRelPacket + { + PacketIdentifier = _bufferReader.ReadTwoByteInteger() + }; + + if (_bufferReader.EndOfStream) + { + packet.ReasonCode = MqttPubRelReasonCode.Success; return packet; } - MqttPacket DecodePubCompPacket(ArraySegment body) + packet.ReasonCode = (MqttPubRelReasonCode)_bufferReader.ReadByte(); + + var propertiesReader = new MqttV5PropertiesReader(_bufferReader); + while (propertiesReader.MoveNext()) { - ThrowIfBodyIsEmpty(body); - - _bufferReader.SetBuffer(body.Array, body.Offset, body.Count); - - var packet = new MqttPubCompPacket + if (propertiesReader.CurrentPropertyId == MqttPropertyId.ReasonString) { - PacketIdentifier = _bufferReader.ReadTwoByteInteger() - }; - - if (_bufferReader.EndOfStream) - { - packet.ReasonCode = MqttPubCompReasonCode.Success; - return packet; + packet.ReasonString = propertiesReader.ReadReasonString(); } - - packet.ReasonCode = (MqttPubCompReasonCode)_bufferReader.ReadByte(); - - var propertiesReader = new MqttV5PropertiesReader(_bufferReader); - while (propertiesReader.MoveNext()) + else { - if (propertiesReader.CurrentPropertyId == MqttPropertyId.ReasonString) - { - packet.ReasonString = propertiesReader.ReadReasonString(); - } - else - { - propertiesReader.ThrowInvalidPropertyIdException(typeof(MqttPubCompPacket)); - } + propertiesReader.ThrowInvalidPropertyIdException(typeof(MqttPubRelPacket)); } - - packet.UserProperties = propertiesReader.CollectedUserProperties; - - return packet; } + packet.UserProperties = propertiesReader.CollectedUserProperties; - MqttPacket DecodePublishPacket(byte header, ArraySegment body) + return packet; + } + + MqttSubAckPacket DecodeSubAckPacket(ArraySegment body) + { + ThrowIfBodyIsEmpty(body); + + _bufferReader.SetBuffer(body.Array, body.Offset, body.Count); + + var packet = new MqttSubAckPacket { - ThrowIfBodyIsEmpty(body); + PacketIdentifier = _bufferReader.ReadTwoByteInteger() + }; - _bufferReader.SetBuffer(body.Array, body.Offset, body.Count); - - var retain = (header & 1) > 0; - var qos = (MqttQualityOfServiceLevel)((header >> 1) & 3); - var dup = ((header >> 3) & 1) > 0; - - var packet = new MqttPublishPacket + var propertiesReader = new MqttV5PropertiesReader(_bufferReader); + while (propertiesReader.MoveNext()) + { + if (propertiesReader.CurrentPropertyId == MqttPropertyId.ReasonString) { - Topic = _bufferReader.ReadString(), - Retain = retain, - QualityOfServiceLevel = qos, - Dup = dup - }; - - if (qos > 0) - { - packet.PacketIdentifier = _bufferReader.ReadTwoByteInteger(); + packet.ReasonString = propertiesReader.ReadReasonString(); } - - var propertiesReader = new MqttV5PropertiesReader(_bufferReader); - while (propertiesReader.MoveNext()) + else { - if (propertiesReader.CurrentPropertyId == MqttPropertyId.PayloadFormatIndicator) - { - packet.PayloadFormatIndicator = propertiesReader.ReadPayloadFormatIndicator(); - } - else if (propertiesReader.CurrentPropertyId == MqttPropertyId.MessageExpiryInterval) - { - packet.MessageExpiryInterval = propertiesReader.ReadMessageExpiryInterval(); - } - else if (propertiesReader.CurrentPropertyId == MqttPropertyId.TopicAlias) - { - packet.TopicAlias = propertiesReader.ReadTopicAlias(); - } - else if (propertiesReader.CurrentPropertyId == MqttPropertyId.ResponseTopic) - { - packet.ResponseTopic = propertiesReader.ReadResponseTopic(); - } - else if (propertiesReader.CurrentPropertyId == MqttPropertyId.CorrelationData) - { - packet.CorrelationData = propertiesReader.ReadCorrelationData(); - } - else if (propertiesReader.CurrentPropertyId == MqttPropertyId.SubscriptionIdentifier) - { - if (packet.SubscriptionIdentifiers == null) - { - packet.SubscriptionIdentifiers = new List(); - } - - packet.SubscriptionIdentifiers.Add(propertiesReader.ReadSubscriptionIdentifier()); - } - else if (propertiesReader.CurrentPropertyId == MqttPropertyId.ContentType) - { - packet.ContentType = propertiesReader.ReadContentType(); - } - else - { - propertiesReader.ThrowInvalidPropertyIdException(typeof(MqttPublishPacket)); - } + propertiesReader.ThrowInvalidPropertyIdException(typeof(MqttSubAckPacket)); } - - packet.UserProperties = propertiesReader.CollectedUserProperties; - - if (!_bufferReader.EndOfStream) - { - packet.PayloadSegment = new ArraySegment(_bufferReader.ReadRemainingData()); - } - - return packet; } - MqttPacket DecodePubRecPacket(ArraySegment body) + packet.UserProperties = propertiesReader.CollectedUserProperties; + + packet.ReasonCodes = new List(_bufferReader.BytesLeft); + while (!_bufferReader.EndOfStream) { - ThrowIfBodyIsEmpty(body); - - _bufferReader.SetBuffer(body.Array, body.Offset, body.Count); - - var packet = new MqttPubRecPacket - { - PacketIdentifier = _bufferReader.ReadTwoByteInteger() - }; - - if (_bufferReader.EndOfStream) - { - packet.ReasonCode = MqttPubRecReasonCode.Success; - return packet; - } - - packet.ReasonCode = (MqttPubRecReasonCode)_bufferReader.ReadByte(); - - var propertiesReader = new MqttV5PropertiesReader(_bufferReader); - while (propertiesReader.MoveNext()) - { - if (propertiesReader.CurrentPropertyId == MqttPropertyId.ReasonString) - { - packet.ReasonString = propertiesReader.ReadReasonString(); - } - else - { - propertiesReader.ThrowInvalidPropertyIdException(typeof(MqttPubRecPacket)); - } - } - - packet.UserProperties = propertiesReader.CollectedUserProperties; - - return packet; + var reasonCode = (MqttSubscribeReasonCode)_bufferReader.ReadByte(); + packet.ReasonCodes.Add(reasonCode); } - MqttPacket DecodePubRelPacket(ArraySegment body) + return packet; + } + + MqttSubscribePacket DecodeSubscribePacket(ArraySegment body) + { + ThrowIfBodyIsEmpty(body); + + _bufferReader.SetBuffer(body.Array, body.Offset, body.Count); + + var packet = new MqttSubscribePacket { - ThrowIfBodyIsEmpty(body); + PacketIdentifier = _bufferReader.ReadTwoByteInteger() + }; - _bufferReader.SetBuffer(body.Array, body.Offset, body.Count); - - var packet = new MqttPubRelPacket + var propertiesReader = new MqttV5PropertiesReader(_bufferReader); + while (propertiesReader.MoveNext()) + { + if (propertiesReader.CurrentPropertyId == MqttPropertyId.SubscriptionIdentifier) { - PacketIdentifier = _bufferReader.ReadTwoByteInteger() - }; - - if (_bufferReader.EndOfStream) - { - packet.ReasonCode = MqttPubRelReasonCode.Success; - return packet; + packet.SubscriptionIdentifier = propertiesReader.ReadSubscriptionIdentifier(); } - - packet.ReasonCode = (MqttPubRelReasonCode)_bufferReader.ReadByte(); - - var propertiesReader = new MqttV5PropertiesReader(_bufferReader); - while (propertiesReader.MoveNext()) + else { - if (propertiesReader.CurrentPropertyId == MqttPropertyId.ReasonString) - { - packet.ReasonString = propertiesReader.ReadReasonString(); - } - else - { - propertiesReader.ThrowInvalidPropertyIdException(typeof(MqttPubRelPacket)); - } + propertiesReader.ThrowInvalidPropertyIdException(typeof(MqttSubscribePacket)); } - - packet.UserProperties = propertiesReader.CollectedUserProperties; - - return packet; } - MqttPacket DecodeSubAckPacket(ArraySegment body) + packet.UserProperties = propertiesReader.CollectedUserProperties; + + while (!_bufferReader.EndOfStream) { - ThrowIfBodyIsEmpty(body); + var topic = _bufferReader.ReadString(); + var options = _bufferReader.ReadByte(); - _bufferReader.SetBuffer(body.Array, body.Offset, body.Count); + var qos = (MqttQualityOfServiceLevel)(options & 3); + var noLocal = (options & (1 << 2)) > 0; + var retainAsPublished = (options & (1 << 3)) > 0; + var retainHandling = (MqttRetainHandling)((options >> 4) & 3); - var packet = new MqttSubAckPacket - { - PacketIdentifier = _bufferReader.ReadTwoByteInteger() - }; - - var propertiesReader = new MqttV5PropertiesReader(_bufferReader); - while (propertiesReader.MoveNext()) - { - if (propertiesReader.CurrentPropertyId == MqttPropertyId.ReasonString) + packet.TopicFilters.Add( + new MqttTopicFilter { - packet.ReasonString = propertiesReader.ReadReasonString(); - } - else - { - propertiesReader.ThrowInvalidPropertyIdException(typeof(MqttSubAckPacket)); - } - } - - packet.UserProperties = propertiesReader.CollectedUserProperties; - - packet.ReasonCodes = new List(_bufferReader.BytesLeft); - while (!_bufferReader.EndOfStream) - { - var reasonCode = (MqttSubscribeReasonCode)_bufferReader.ReadByte(); - packet.ReasonCodes.Add(reasonCode); - } - - return packet; + Topic = topic, + QualityOfServiceLevel = qos, + NoLocal = noLocal, + RetainAsPublished = retainAsPublished, + RetainHandling = retainHandling + }); } - MqttPacket DecodeSubscribePacket(ArraySegment body) + return packet; + } + + MqttUnsubAckPacket DecodeUnsubAckPacket(ArraySegment body) + { + ThrowIfBodyIsEmpty(body); + + _bufferReader.SetBuffer(body.Array, body.Offset, body.Count); + + var packet = new MqttUnsubAckPacket { - ThrowIfBodyIsEmpty(body); + PacketIdentifier = _bufferReader.ReadTwoByteInteger() + }; - _bufferReader.SetBuffer(body.Array, body.Offset, body.Count); - - var packet = new MqttSubscribePacket + var propertiesReader = new MqttV5PropertiesReader(_bufferReader); + while (propertiesReader.MoveNext()) + { + if (propertiesReader.CurrentPropertyId == MqttPropertyId.ReasonString) { - PacketIdentifier = _bufferReader.ReadTwoByteInteger() - }; - - var propertiesReader = new MqttV5PropertiesReader(_bufferReader); - while (propertiesReader.MoveNext()) - { - if (propertiesReader.CurrentPropertyId == MqttPropertyId.SubscriptionIdentifier) - { - packet.SubscriptionIdentifier = propertiesReader.ReadSubscriptionIdentifier(); - } - else - { - propertiesReader.ThrowInvalidPropertyIdException(typeof(MqttSubscribePacket)); - } + packet.ReasonString = propertiesReader.ReadReasonString(); } - - packet.UserProperties = propertiesReader.CollectedUserProperties; - - while (!_bufferReader.EndOfStream) + else { - var topic = _bufferReader.ReadString(); - var options = _bufferReader.ReadByte(); - - var qos = (MqttQualityOfServiceLevel)(options & 3); - var noLocal = (options & (1 << 2)) > 0; - var retainAsPublished = (options & (1 << 3)) > 0; - var retainHandling = (MqttRetainHandling)((options >> 4) & 3); - - packet.TopicFilters.Add( - new MqttTopicFilter - { - Topic = topic, - QualityOfServiceLevel = qos, - NoLocal = noLocal, - RetainAsPublished = retainAsPublished, - RetainHandling = retainHandling - }); + propertiesReader.ThrowInvalidPropertyIdException(typeof(MqttUnsubAckPacket)); } - - return packet; } - MqttPacket DecodeUnsubAckPacket(ArraySegment body) + packet.UserProperties = propertiesReader.CollectedUserProperties; + + packet.ReasonCodes = new List(_bufferReader.BytesLeft); + + while (!_bufferReader.EndOfStream) { - ThrowIfBodyIsEmpty(body); - - _bufferReader.SetBuffer(body.Array, body.Offset, body.Count); - - var packet = new MqttUnsubAckPacket - { - PacketIdentifier = _bufferReader.ReadTwoByteInteger() - }; - - var propertiesReader = new MqttV5PropertiesReader(_bufferReader); - while (propertiesReader.MoveNext()) - { - if (propertiesReader.CurrentPropertyId == MqttPropertyId.ReasonString) - { - packet.ReasonString = propertiesReader.ReadReasonString(); - } - else - { - propertiesReader.ThrowInvalidPropertyIdException(typeof(MqttUnsubAckPacket)); - } - } - - packet.UserProperties = propertiesReader.CollectedUserProperties; - - packet.ReasonCodes = new List(_bufferReader.BytesLeft); - - while (!_bufferReader.EndOfStream) - { - var reasonCode = (MqttUnsubscribeReasonCode)_bufferReader.ReadByte(); - packet.ReasonCodes.Add(reasonCode); - } - - return packet; + var reasonCode = (MqttUnsubscribeReasonCode)_bufferReader.ReadByte(); + packet.ReasonCodes.Add(reasonCode); } - MqttPacket DecodeUnsubscribePacket(ArraySegment body) + return packet; + } + + MqttUnsubscribePacket DecodeUnsubscribePacket(ArraySegment body) + { + ThrowIfBodyIsEmpty(body); + + _bufferReader.SetBuffer(body.Array, body.Offset, body.Count); + + var packet = new MqttUnsubscribePacket { - ThrowIfBodyIsEmpty(body); + PacketIdentifier = _bufferReader.ReadTwoByteInteger() + }; - _bufferReader.SetBuffer(body.Array, body.Offset, body.Count); - - var packet = new MqttUnsubscribePacket - { - PacketIdentifier = _bufferReader.ReadTwoByteInteger() - }; - - var propertiesReader = new MqttV5PropertiesReader(_bufferReader); - while (propertiesReader.MoveNext()) - { - propertiesReader.ThrowInvalidPropertyIdException(typeof(MqttUnsubscribePacket)); - } - - packet.UserProperties = propertiesReader.CollectedUserProperties; - - while (!_bufferReader.EndOfStream) - { - packet.TopicFilters.Add(_bufferReader.ReadString()); - } - - return packet; + var propertiesReader = new MqttV5PropertiesReader(_bufferReader); + while (propertiesReader.MoveNext()) + { + propertiesReader.ThrowInvalidPropertyIdException(typeof(MqttUnsubscribePacket)); } - // ReSharper disable once ParameterOnlyUsedForPreconditionCheck.Local - static void ThrowIfBodyIsEmpty(ArraySegment body) + packet.UserProperties = propertiesReader.CollectedUserProperties; + + while (!_bufferReader.EndOfStream) { - if (body.Count == 0) - { - throw new MqttProtocolViolationException("Data from the body is required but not present."); - } + packet.TopicFilters.Add(_bufferReader.ReadString()); + } + + return packet; + } + + // ReSharper disable once ParameterOnlyUsedForPreconditionCheck.Local + static void ThrowIfBodyIsEmpty(ArraySegment body) + { + if (body.Count == 0) + { + throw new MqttProtocolViolationException("Data from the body is required but not present."); } } -} +} \ No newline at end of file diff --git a/Source/MQTTnet/Formatter/V5/MqttV5PacketEncoder.cs b/Source/MQTTnet/Formatter/V5/MqttV5PacketEncoder.cs index 9d5a6d09..9c4e9013 100644 --- a/Source/MQTTnet/Formatter/V5/MqttV5PacketEncoder.cs +++ b/Source/MQTTnet/Formatter/V5/MqttV5PacketEncoder.cs @@ -3,7 +3,6 @@ // See the LICENSE file in the project root for more information. using System; -using System.Buffers; using System.Linq; using MQTTnet.Exceptions; using MQTTnet.Packets; @@ -11,34 +10,29 @@ using MQTTnet.Protocol; namespace MQTTnet.Formatter.V5; -public sealed class MqttV5PacketEncoder +public sealed class MqttV5PacketEncoder(MqttBufferWriter bufferWriter) { const int FixedHeaderSize = 1; - readonly MqttBufferWriter _bufferWriter; + readonly MqttBufferWriter _bufferWriter = bufferWriter ?? throw new ArgumentNullException(nameof(bufferWriter)); readonly MqttV5PropertiesWriter _propertiesWriter = new(new MqttBufferWriter(1024, 4096)); - public MqttV5PacketEncoder(MqttBufferWriter bufferWriter) - { - _bufferWriter = bufferWriter ?? throw new ArgumentNullException(nameof(bufferWriter)); - } - public MqttPacketBuffer Encode(MqttPacket packet) { ArgumentNullException.ThrowIfNull(packet); - // Leave enough head space for max header size (fixed + 4 variable remaining length = 5 bytes) - const int ReservedHeaderSize = 5; - _bufferWriter.Reset(ReservedHeaderSize); - _bufferWriter.Seek(ReservedHeaderSize); + // Leave enough headspace for max header size (fixed + 4 variable remaining length = 5 bytes) + const int reservedHeaderSize = 5; + + _bufferWriter.Reset(reservedHeaderSize); + _bufferWriter.Seek(reservedHeaderSize); var fixedHeader = EncodePacket(packet); - var remainingLength = (uint)_bufferWriter.Length - ReservedHeaderSize; + var remainingLength = (uint)_bufferWriter.Length - reservedHeaderSize; - ReadOnlySequence payload = default; if (packet is MqttPublishPacket publishPacket) { - payload = publishPacket.Payload; + var payload = publishPacket.Payload; remainingLength += (uint)payload.Length; } else @@ -226,42 +220,25 @@ public sealed class MqttV5PacketEncoder byte EncodePacket(MqttPacket packet) { - switch (packet) + return packet switch { - case MqttConnectPacket connectPacket: - return EncodeConnectPacket(connectPacket); - case MqttConnAckPacket connAckPacket: - return EncodeConnAckPacket(connAckPacket); - case MqttDisconnectPacket disconnectPacket: - return EncodeDisconnectPacket(disconnectPacket); - case MqttPingReqPacket _: - return EncodePingReqPacket(); - case MqttPingRespPacket _: - return EncodePingRespPacket(); - case MqttPublishPacket publishPacket: - return EncodePublishPacket(publishPacket); - case MqttPubAckPacket pubAckPacket: - return EncodePubAckPacket(pubAckPacket); - case MqttPubRecPacket pubRecPacket: - return EncodePubRecPacket(pubRecPacket); - case MqttPubRelPacket pubRelPacket: - return EncodePubRelPacket(pubRelPacket); - case MqttPubCompPacket pubCompPacket: - return EncodePubCompPacket(pubCompPacket); - case MqttSubscribePacket subscribePacket: - return EncodeSubscribePacket(subscribePacket); - case MqttSubAckPacket subAckPacket: - return EncodeSubAckPacket(subAckPacket); - case MqttUnsubscribePacket unsubscribePacket: - return EncodeUnsubscribePacket(unsubscribePacket); - case MqttUnsubAckPacket unsubAckPacket: - return EncodeUnsubAckPacket(unsubAckPacket); - case MqttAuthPacket authPacket: - return EncodeAuthPacket(authPacket); - - default: - throw new MqttProtocolViolationException("Packet type invalid."); - } + MqttConnectPacket connectPacket => EncodeConnectPacket(connectPacket), + MqttConnAckPacket connAckPacket => EncodeConnAckPacket(connAckPacket), + MqttDisconnectPacket disconnectPacket => EncodeDisconnectPacket(disconnectPacket), + MqttPingReqPacket _ => EncodePingReqPacket(), + MqttPingRespPacket _ => EncodePingRespPacket(), + MqttPublishPacket publishPacket => EncodePublishPacket(publishPacket), + MqttPubAckPacket pubAckPacket => EncodePubAckPacket(pubAckPacket), + MqttPubRecPacket pubRecPacket => EncodePubRecPacket(pubRecPacket), + MqttPubRelPacket pubRelPacket => EncodePubRelPacket(pubRelPacket), + MqttPubCompPacket pubCompPacket => EncodePubCompPacket(pubCompPacket), + MqttSubscribePacket subscribePacket => EncodeSubscribePacket(subscribePacket), + MqttSubAckPacket subAckPacket => EncodeSubAckPacket(subAckPacket), + MqttUnsubscribePacket unsubscribePacket => EncodeUnsubscribePacket(unsubscribePacket), + MqttUnsubAckPacket unsubAckPacket => EncodeUnsubAckPacket(unsubAckPacket), + MqttAuthPacket authPacket => EncodeAuthPacket(authPacket), + _ => throw new MqttProtocolViolationException("Packet type invalid.") + }; } static byte EncodePingReqPacket() @@ -413,7 +390,7 @@ public sealed class MqttV5PacketEncoder byte EncodeSubAckPacket(MqttSubAckPacket packet) { - if (packet.ReasonCodes?.Any() != true) + if (packet.ReasonCodes?.Count == 0) { throw new MqttProtocolViolationException("At least one reason code must be set[MQTT - 3.8.3 - 3]."); } @@ -438,7 +415,7 @@ public sealed class MqttV5PacketEncoder byte EncodeSubscribePacket(MqttSubscribePacket packet) { - if (packet.TopicFilters?.Any() != true) + if (packet.TopicFilters?.Count == 0) { throw new MqttProtocolViolationException("At least one topic filter must be set [MQTT-3.8.3-3]."); } @@ -509,7 +486,7 @@ public sealed class MqttV5PacketEncoder byte EncodeUnsubscribePacket(MqttUnsubscribePacket packet) { - if (packet.TopicFilters?.Any() != true) + if (packet.TopicFilters?.Count == 0) { throw new MqttProtocolViolationException("At least one topic filter must be set [MQTT-3.10.3-2]."); } diff --git a/Source/MQTTnet/Formatter/V5/MqttV5PacketFormatter.cs b/Source/MQTTnet/Formatter/V5/MqttV5PacketFormatter.cs index 3cf4776c..ec6a1229 100644 --- a/Source/MQTTnet/Formatter/V5/MqttV5PacketFormatter.cs +++ b/Source/MQTTnet/Formatter/V5/MqttV5PacketFormatter.cs @@ -5,27 +5,26 @@ using MQTTnet.Adapter; using MQTTnet.Packets; -namespace MQTTnet.Formatter.V5 +namespace MQTTnet.Formatter.V5; + +public sealed class MqttV5PacketFormatter : IMqttPacketFormatter { - public sealed class MqttV5PacketFormatter : IMqttPacketFormatter + readonly MqttV5PacketDecoder _decoder; + readonly MqttV5PacketEncoder _encoder; + + public MqttV5PacketFormatter(MqttBufferWriter bufferWriter) { - readonly MqttV5PacketDecoder _decoder; - readonly MqttV5PacketEncoder _encoder; + _decoder = new MqttV5PacketDecoder(); + _encoder = new MqttV5PacketEncoder(bufferWriter); + } - public MqttV5PacketFormatter(MqttBufferWriter bufferWriter) - { - _decoder = new MqttV5PacketDecoder(); - _encoder = new MqttV5PacketEncoder(bufferWriter); - } + public MqttPacket Decode(ReceivedMqttPacket receivedPacket) + { + return _decoder.Decode(receivedPacket); + } - public MqttPacket Decode(ReceivedMqttPacket receivedMqttPacket) - { - return _decoder.Decode(receivedMqttPacket); - } - - public MqttPacketBuffer Encode(MqttPacket mqttPacket) - { - return _encoder.Encode(mqttPacket); - } + public MqttPacketBuffer Encode(MqttPacket packet) + { + return _encoder.Encode(packet); } } \ No newline at end of file diff --git a/Source/MQTTnet/Formatter/V5/MqttV5PropertiesReader.cs b/Source/MQTTnet/Formatter/V5/MqttV5PropertiesReader.cs index e5a3efe7..84983088 100644 --- a/Source/MQTTnet/Formatter/V5/MqttV5PropertiesReader.cs +++ b/Source/MQTTnet/Formatter/V5/MqttV5PropertiesReader.cs @@ -8,213 +8,212 @@ using MQTTnet.Exceptions; using MQTTnet.Packets; using MQTTnet.Protocol; -namespace MQTTnet.Formatter.V5 +namespace MQTTnet.Formatter.V5; + +public struct MqttV5PropertiesReader { - public struct MqttV5PropertiesReader + readonly MqttBufferReader _body; + readonly int _length; + readonly int _targetOffset; + + public MqttV5PropertiesReader(MqttBufferReader body) { - readonly MqttBufferReader _body; - readonly int _length; - readonly int _targetOffset; + _body = body ?? throw new ArgumentNullException(nameof(body)); - public MqttV5PropertiesReader(MqttBufferReader body) + if (!body.EndOfStream) { - _body = body ?? throw new ArgumentNullException(nameof(body)); - - if (!body.EndOfStream) - { - _length = (int)body.ReadVariableByteInteger(); - } - else - { - _length = 0; - } - - _targetOffset = body.Position + _length; - - CollectedUserProperties = null; - CurrentPropertyId = MqttPropertyId.None; + _length = (int)body.ReadVariableByteInteger(); + } + else + { + _length = 0; } - public List CollectedUserProperties { get; private set; } + _targetOffset = body.Position + _length; - public MqttPropertyId CurrentPropertyId { get; private set; } + CollectedUserProperties = null; + CurrentPropertyId = MqttPropertyId.None; + } - public bool MoveNext() + public List CollectedUserProperties { get; private set; } + + public MqttPropertyId CurrentPropertyId { get; private set; } + + public bool MoveNext() + { + while (true) { - while (true) + if (_length == 0) { - if (_length == 0) + return false; + } + + if (_body.Position >= _targetOffset) + { + return false; + } + + CurrentPropertyId = (MqttPropertyId)_body.ReadByte(); + + // User properties are special because they can appear multiple times in the + // buffer and at any position. So we collect them here to expose them as a + // final result list. + if (CurrentPropertyId == MqttPropertyId.UserProperty) + { + var name = _body.ReadString(); + var value = _body.ReadString(); + + if (CollectedUserProperties == null) { - return false; + CollectedUserProperties = new List(); } - if (_body.Position >= _targetOffset) - { - return false; - } - - CurrentPropertyId = (MqttPropertyId)_body.ReadByte(); - - // User properties are special because they can appear multiple times in the - // buffer and at any position. So we collect them here to expose them as a - // final result list. - if (CurrentPropertyId == MqttPropertyId.UserProperty) - { - var name = _body.ReadString(); - var value = _body.ReadString(); - - if (CollectedUserProperties == null) - { - CollectedUserProperties = new List(); - } - - CollectedUserProperties.Add(new MqttUserProperty(name, value)); - continue; - } - - return true; - } - } - - public string ReadAssignedClientIdentifier() - { - return _body.ReadString(); - } - - public byte[] ReadAuthenticationData() - { - return _body.ReadBinaryData(); - } - - public string ReadAuthenticationMethod() - { - return _body.ReadString(); - } - - public string ReadContentType() - { - return _body.ReadString(); - } - - public byte[] ReadCorrelationData() - { - return _body.ReadBinaryData(); - } - - public uint ReadMaximumPacketSize() - { - return _body.ReadFourByteInteger(); - } - - public MqttQualityOfServiceLevel ReadMaximumQoS() - { - var value = _body.ReadByte(); - if (value > 1) - { - throw new MqttProtocolViolationException($"Unexpected Maximum QoS value: {value}"); + CollectedUserProperties.Add(new MqttUserProperty(name, value)); + continue; } - return (MqttQualityOfServiceLevel)value; - } - - public uint ReadMessageExpiryInterval() - { - return _body.ReadFourByteInteger(); - } - - public MqttPayloadFormatIndicator ReadPayloadFormatIndicator() - { - return (MqttPayloadFormatIndicator)_body.ReadByte(); - } - - public string ReadReasonString() - { - return _body.ReadString(); - } - - public ushort ReadReceiveMaximum() - { - return _body.ReadTwoByteInteger(); - } - - public string ReadResponseInformation() - { - return _body.ReadString(); - } - - public string ReadResponseTopic() - { - return _body.ReadString(); - } - - public bool ReadRetainAvailable() - { - return _body.ReadByte() == 1; - } - - public ushort ReadServerKeepAlive() - { - return _body.ReadTwoByteInteger(); - } - - public string ReadServerReference() - { - return _body.ReadString(); - } - - public uint ReadSessionExpiryInterval() - { - return _body.ReadFourByteInteger(); - } - - public bool ReadSharedSubscriptionAvailable() - { - return _body.ReadByte() == 1; - } - - public uint ReadSubscriptionIdentifier() - { - return _body.ReadVariableByteInteger(); - } - - public bool ReadSubscriptionIdentifiersAvailable() - { - return _body.ReadByte() == 1; - } - - public ushort ReadTopicAlias() - { - return _body.ReadTwoByteInteger(); - } - - public ushort ReadTopicAliasMaximum() - { - return _body.ReadTwoByteInteger(); - } - - public bool ReadWildcardSubscriptionAvailable() - { - return _body.ReadByte() == 1; - } - - public uint ReadWillDelayInterval() - { - return _body.ReadFourByteInteger(); - } - - public bool RequestProblemInformation() - { - return _body.ReadByte() == 1; - } - - public bool RequestResponseInformation() - { - return _body.ReadByte() == 1; - } - - public void ThrowInvalidPropertyIdException(Type type) - { - throw new MqttProtocolViolationException($"Property ID '{CurrentPropertyId}' is not supported for package type '{type.Name}'."); + return true; } } + + public string ReadAssignedClientIdentifier() + { + return _body.ReadString(); + } + + public byte[] ReadAuthenticationData() + { + return _body.ReadBinaryData(); + } + + public string ReadAuthenticationMethod() + { + return _body.ReadString(); + } + + public string ReadContentType() + { + return _body.ReadString(); + } + + public byte[] ReadCorrelationData() + { + return _body.ReadBinaryData(); + } + + public uint ReadMaximumPacketSize() + { + return _body.ReadFourByteInteger(); + } + + public MqttQualityOfServiceLevel ReadMaximumQoS() + { + var value = _body.ReadByte(); + if (value > 1) + { + throw new MqttProtocolViolationException($"Unexpected Maximum QoS value: {value}"); + } + + return (MqttQualityOfServiceLevel)value; + } + + public uint ReadMessageExpiryInterval() + { + return _body.ReadFourByteInteger(); + } + + public MqttPayloadFormatIndicator ReadPayloadFormatIndicator() + { + return (MqttPayloadFormatIndicator)_body.ReadByte(); + } + + public string ReadReasonString() + { + return _body.ReadString(); + } + + public ushort ReadReceiveMaximum() + { + return _body.ReadTwoByteInteger(); + } + + public string ReadResponseInformation() + { + return _body.ReadString(); + } + + public string ReadResponseTopic() + { + return _body.ReadString(); + } + + public bool ReadRetainAvailable() + { + return _body.ReadByte() == 1; + } + + public ushort ReadServerKeepAlive() + { + return _body.ReadTwoByteInteger(); + } + + public string ReadServerReference() + { + return _body.ReadString(); + } + + public uint ReadSessionExpiryInterval() + { + return _body.ReadFourByteInteger(); + } + + public bool ReadSharedSubscriptionAvailable() + { + return _body.ReadByte() == 1; + } + + public uint ReadSubscriptionIdentifier() + { + return _body.ReadVariableByteInteger(); + } + + public bool ReadSubscriptionIdentifiersAvailable() + { + return _body.ReadByte() == 1; + } + + public ushort ReadTopicAlias() + { + return _body.ReadTwoByteInteger(); + } + + public ushort ReadTopicAliasMaximum() + { + return _body.ReadTwoByteInteger(); + } + + public bool ReadWildcardSubscriptionAvailable() + { + return _body.ReadByte() == 1; + } + + public uint ReadWillDelayInterval() + { + return _body.ReadFourByteInteger(); + } + + public bool RequestProblemInformation() + { + return _body.ReadByte() == 1; + } + + public bool RequestResponseInformation() + { + return _body.ReadByte() == 1; + } + + public void ThrowInvalidPropertyIdException(Type type) + { + throw new MqttProtocolViolationException($"Property ID '{CurrentPropertyId}' is not supported for package type '{type.Name}'."); + } } \ No newline at end of file diff --git a/Source/MQTTnet/Formatter/V5/MqttV5PropertiesWriter.cs b/Source/MQTTnet/Formatter/V5/MqttV5PropertiesWriter.cs index 25546d33..5d138718 100644 --- a/Source/MQTTnet/Formatter/V5/MqttV5PropertiesWriter.cs +++ b/Source/MQTTnet/Formatter/V5/MqttV5PropertiesWriter.cs @@ -7,347 +7,339 @@ using System.Collections.Generic; using MQTTnet.Packets; using MQTTnet.Protocol; -namespace MQTTnet.Formatter.V5 +namespace MQTTnet.Formatter.V5; + +public sealed class MqttV5PropertiesWriter { - public sealed class MqttV5PropertiesWriter + readonly MqttBufferWriter _bufferWriter; + + public MqttV5PropertiesWriter(MqttBufferWriter bufferWriter) { - readonly MqttBufferWriter _bufferWriter; + _bufferWriter = bufferWriter ?? throw new ArgumentNullException(nameof(bufferWriter)); + } - public MqttV5PropertiesWriter(MqttBufferWriter bufferWriter) + public int Length => _bufferWriter.Length; + + public void Reset() + { + _bufferWriter.Reset(0); + _bufferWriter.Cleanup(); + } + + public void WriteAssignedClientIdentifier(string value) + { + Write(MqttPropertyId.AssignedClientIdentifier, value); + } + + public void WriteAuthenticationData(byte[] value) + { + Write(MqttPropertyId.AuthenticationData, value); + } + + public void WriteAuthenticationMethod(string value) + { + Write(MqttPropertyId.AuthenticationMethod, value); + } + + public void WriteContentType(string value) + { + Write(MqttPropertyId.ContentType, value); + } + + public void WriteCorrelationData(byte[] value) + { + Write(MqttPropertyId.CorrelationData, value); + } + + public void WriteMaximumPacketSize(uint value) + { + // It is a Protocol Error to include the Maximum Packet Size more than once, or for the value to be set to zero. + if (value == 0) { - _bufferWriter = bufferWriter ?? throw new ArgumentNullException(nameof(bufferWriter)); + return; } - public int Length => _bufferWriter.Length; + WriteAsFourByteInteger(MqttPropertyId.MaximumPacketSize, value); + } - public void Reset() + public void WriteMaximumQoS(MqttQualityOfServiceLevel value) + { + // It is a Protocol Error to include Maximum QoS more than once, or to have a value other than 0 or 1. If the Maximum QoS is absent, the Client uses a Maximum QoS of 2. + if (value == MqttQualityOfServiceLevel.ExactlyOnce) { - _bufferWriter.Reset(0); - _bufferWriter.Cleanup(); + return; } - public void WriteAssignedClientIdentifier(string value) + Write(MqttPropertyId.MaximumQoS, value == MqttQualityOfServiceLevel.AtLeastOnce ? (byte)0x1 : (byte)0x0); + } + + public void WriteMessageExpiryInterval(uint value) + { + // If absent, the Application Message does not expire. + // This library uses 0 to indicate no expiration. + if (value == 0) { - Write(MqttPropertyId.AssignedClientIdentifier, value); + return; } - public void WriteAuthenticationData(byte[] value) + WriteAsFourByteInteger(MqttPropertyId.MessageExpiryInterval, value); + } + + public void WritePayloadFormatIndicator(MqttPayloadFormatIndicator value) + { + // 0 (0x00) Byte Indicates that the Payload is unspecified bytes, which is equivalent to not sending a Payload Format Indicator. + if (value == MqttPayloadFormatIndicator.Unspecified) { - Write(MqttPropertyId.AuthenticationData, value); + return; } - public void WriteAuthenticationMethod(string value) + Write(MqttPropertyId.PayloadFormatIndicator, (byte)value); + } + + public void WriteReasonString(string value) + { + Write(MqttPropertyId.ReasonString, value); + } + + public void WriteReceiveMaximum(ushort value) + { + // It is a Protocol Error to include the Receive Maximum value more than once or for it to have the value 0. + if (value == 0) { - Write(MqttPropertyId.AuthenticationMethod, value); + return; } - public void WriteContentType(string value) + Write(MqttPropertyId.ReceiveMaximum, value); + } + + public void WriteRequestProblemInformation(bool value) + { + // If the Request Problem Information is absent, the value of 1 is used. + if (value) { - Write(MqttPropertyId.ContentType, value); + return; } - public void WriteCorrelationData(byte[] value) + Write(MqttPropertyId.RequestProblemInformation, false); + } + + public void WriteRequestResponseInformation(bool value) + { + // If the Request Response Information is absent, the value of 0 is used. + if (!value) { - Write(MqttPropertyId.CorrelationData, value); + return; } - public void WriteMaximumPacketSize(uint value) - { - // It is a Protocol Error to include the Maximum Packet Size more than once, or for the value to be set to zero. - if (value == 0) - { - return; - } + Write(MqttPropertyId.RequestResponseInformation, true); + } - WriteAsFourByteInteger(MqttPropertyId.MaximumPacketSize, value); + public void WriteResponseInformation(string value) + { + Write(MqttPropertyId.ResponseInformation, value); + } + + public void WriteResponseTopic(string value) + { + Write(MqttPropertyId.ResponseTopic, value); + } + + public void WriteRetainAvailable(bool value) + { + if (value) + { + // Absence of the flag means it is supported! + return; } - public void WriteMaximumQoS(MqttQualityOfServiceLevel value) - { - // It is a Protocol Error to include Maximum QoS more than once, or to have a value other than 0 or 1. If the Maximum QoS is absent, the Client uses a Maximum QoS of 2. - if (value == MqttQualityOfServiceLevel.ExactlyOnce) - { - return; - } + Write(MqttPropertyId.RetainAvailable, false); + } - if (value == MqttQualityOfServiceLevel.AtLeastOnce) - { - Write(MqttPropertyId.MaximumQoS, 0x1); - } - else - { - Write(MqttPropertyId.MaximumQoS, 0x0); - } + public void WriteServerKeepAlive(ushort value) + { + if (value == 0) + { + return; } - public void WriteMessageExpiryInterval(uint value) + Write(MqttPropertyId.ServerKeepAlive, value); + } + + public void WriteServerReference(string value) + { + Write(MqttPropertyId.ServerReference, value); + } + + public void WriteSessionExpiryInterval(uint value) + { + // If the Session Expiry Interval is absent the value 0 is used. + if (value == 0) { - // If absent, the Application Message does not expire. - // This library uses 0 to indicate no expiration. - if (value == 0) - { - return; - } - - WriteAsFourByteInteger(MqttPropertyId.MessageExpiryInterval, value); + return; } - public void WritePayloadFormatIndicator(MqttPayloadFormatIndicator value) - { - // 0 (0x00) Byte Indicates that the Payload is unspecified bytes, which is equivalent to not sending a Payload Format Indicator. - if (value == MqttPayloadFormatIndicator.Unspecified) - { - return; - } + WriteAsFourByteInteger(MqttPropertyId.SessionExpiryInterval, value); + } - Write(MqttPropertyId.PayloadFormatIndicator, (byte)value); + public void WriteSharedSubscriptionAvailable(bool value) + { + if (value) + { + // Absence of the flag means it is supported! + return; } - public void WriteReasonString(string value) + Write(MqttPropertyId.SharedSubscriptionAvailable, false); + } + + public void WriteSubscriptionIdentifier(uint value) + { + WriteAsVariableByteInteger(MqttPropertyId.SubscriptionIdentifier, value); + } + + public void WriteSubscriptionIdentifiers(ICollection value) + { + if (value == null) { - Write(MqttPropertyId.ReasonString, value); + return; } - public void WriteReceiveMaximum(ushort value) + foreach (var subscriptionIdentifier in value) { - // It is a Protocol Error to include the Receive Maximum value more than once or for it to have the value 0. - if (value == 0) - { - return; - } - - Write(MqttPropertyId.ReceiveMaximum, value); - } - - public void WriteRequestProblemInformation(bool value) - { - // If the Request Problem Information is absent, the value of 1 is used. - if (value) - { - return; - } - - Write(MqttPropertyId.RequestProblemInformation, false); - } - - public void WriteRequestResponseInformation(bool value) - { - // If the Request Response Information is absent, the value of 0 is used. - if (!value) - { - return; - } - - Write(MqttPropertyId.RequestResponseInformation, true); - } - - public void WriteResponseInformation(string value) - { - Write(MqttPropertyId.ResponseInformation, value); - } - - public void WriteResponseTopic(string value) - { - Write(MqttPropertyId.ResponseTopic, value); - } - - public void WriteRetainAvailable(bool value) - { - if (value) - { - // Absence of the flag means it is supported! - return; - } - - Write(MqttPropertyId.RetainAvailable, false); - } - - public void WriteServerKeepAlive(ushort value) - { - if (value == 0) - { - return; - } - - Write(MqttPropertyId.ServerKeepAlive, value); - } - - public void WriteServerReference(string value) - { - Write(MqttPropertyId.ServerReference, value); - } - - public void WriteSessionExpiryInterval(uint value) - { - // If the Session Expiry Interval is absent the value 0 is used. - if (value == 0) - { - return; - } - - WriteAsFourByteInteger(MqttPropertyId.SessionExpiryInterval, value); - } - - public void WriteSharedSubscriptionAvailable(bool value) - { - if (value) - { - // Absence of the flag means it is supported! - return; - } - - Write(MqttPropertyId.SharedSubscriptionAvailable, false); - } - - public void WriteSubscriptionIdentifier(uint value) - { - WriteAsVariableByteInteger(MqttPropertyId.SubscriptionIdentifier, value); - } - - public void WriteSubscriptionIdentifiers(ICollection value) - { - if (value == null) - { - return; - } - - foreach (var subscriptionIdentifier in value) - { - WriteAsVariableByteInteger(MqttPropertyId.SubscriptionIdentifier, subscriptionIdentifier); - } - } - - public void WriteSubscriptionIdentifiersAvailable(bool value) - { - if (value) - { - // Absence of the flag means it is supported! - return; - } - - Write(MqttPropertyId.SubscriptionIdentifiersAvailable, false); - } - - public void WriteTo(MqttBufferWriter target) - { - ArgumentNullException.ThrowIfNull(target); - - target.WriteVariableByteInteger((uint)_bufferWriter.Length); - target.Write(_bufferWriter); - } - - public void WriteTopicAlias(ushort value) - { - // A Topic Alias of 0 is not permitted. A sender MUST NOT send a PUBLISH packet containing a Topic Alias which has the value 0. - if (value == 0) - { - return; - } - - Write(MqttPropertyId.TopicAlias, value); - } - - public void WriteTopicAliasMaximum(ushort value) - { - // If the Topic Alias Maximum property is absent, the default value is 0. - if (value == 0) - { - return; - } - - Write(MqttPropertyId.TopicAliasMaximum, value); - } - - public void WriteUserProperties(List userProperties) - { - if (userProperties == null || userProperties.Count == 0) - { - return; - } - - foreach (var property in userProperties) - { - _bufferWriter.WriteByte((byte)MqttPropertyId.UserProperty); - _bufferWriter.WriteString(property.Name); - _bufferWriter.WriteString(property.Value); - } - } - - public void WriteWildcardSubscriptionAvailable(bool value) - { - // If not present, then Wildcard Subscriptions are supported. - if (value) - { - return; - } - - Write(MqttPropertyId.WildcardSubscriptionAvailable, false); - } - - public void WriteWillDelayInterval(uint value) - { - // If the Will Delay Interval is absent, the default value is 0 and there is no delay before the Will Message is published. - if (value == 0) - { - return; - } - - WriteAsFourByteInteger(MqttPropertyId.WillDelayInterval, value); - } - - void Write(MqttPropertyId id, bool value) - { - _bufferWriter.WriteByte((byte)id); - _bufferWriter.WriteByte(value ? (byte)0x1 : (byte)0x0); - } - - void Write(MqttPropertyId id, byte value) - { - _bufferWriter.WriteByte((byte)id); - _bufferWriter.WriteByte(value); - } - - void Write(MqttPropertyId id, ushort value) - { - _bufferWriter.WriteByte((byte)id); - _bufferWriter.WriteTwoByteInteger(value); - } - - void Write(MqttPropertyId id, string value) - { - if (string.IsNullOrEmpty(value)) - { - return; - } - - _bufferWriter.WriteByte((byte)id); - _bufferWriter.WriteString(value); - } - - void Write(MqttPropertyId id, byte[] value) - { - if (value == null) - { - return; - } - - _bufferWriter.WriteByte((byte)id); - _bufferWriter.WriteBinary(value); - } - - void WriteAsFourByteInteger(MqttPropertyId id, uint value) - { - _bufferWriter.WriteByte((byte)id); - _bufferWriter.WriteByte((byte)(value >> 24)); - _bufferWriter.WriteByte((byte)(value >> 16)); - _bufferWriter.WriteByte((byte)(value >> 8)); - _bufferWriter.WriteByte((byte)value); - } - - void WriteAsVariableByteInteger(MqttPropertyId id, uint value) - { - _bufferWriter.WriteByte((byte)id); - _bufferWriter.WriteVariableByteInteger(value); + WriteAsVariableByteInteger(MqttPropertyId.SubscriptionIdentifier, subscriptionIdentifier); } } + + public void WriteSubscriptionIdentifiersAvailable(bool value) + { + if (value) + { + // Absence of the flag means it is supported! + return; + } + + Write(MqttPropertyId.SubscriptionIdentifiersAvailable, false); + } + + public void WriteTo(MqttBufferWriter target) + { + ArgumentNullException.ThrowIfNull(target); + + target.WriteVariableByteInteger((uint)_bufferWriter.Length); + target.Write(_bufferWriter); + } + + public void WriteTopicAlias(ushort value) + { + // A Topic Alias of 0 is not permitted. A sender MUST NOT send a PUBLISH packet containing a Topic Alias which has the value 0. + if (value == 0) + { + return; + } + + Write(MqttPropertyId.TopicAlias, value); + } + + public void WriteTopicAliasMaximum(ushort value) + { + // If the Topic Alias Maximum property is absent, the default value is 0. + if (value == 0) + { + return; + } + + Write(MqttPropertyId.TopicAliasMaximum, value); + } + + public void WriteUserProperties(List userProperties) + { + if (userProperties == null || userProperties.Count == 0) + { + return; + } + + foreach (var property in userProperties) + { + _bufferWriter.WriteByte((byte)MqttPropertyId.UserProperty); + _bufferWriter.WriteString(property.Name); + _bufferWriter.WriteString(property.Value); + } + } + + public void WriteWildcardSubscriptionAvailable(bool value) + { + // If not present, then Wildcard Subscriptions are supported. + if (value) + { + return; + } + + Write(MqttPropertyId.WildcardSubscriptionAvailable, false); + } + + public void WriteWillDelayInterval(uint value) + { + // If the Will Delay Interval is absent, the default value is 0 and there is no delay before the Will Message is published. + if (value == 0) + { + return; + } + + WriteAsFourByteInteger(MqttPropertyId.WillDelayInterval, value); + } + + void Write(MqttPropertyId id, bool value) + { + _bufferWriter.WriteByte((byte)id); + _bufferWriter.WriteByte(value ? (byte)0x1 : (byte)0x0); + } + + void Write(MqttPropertyId id, byte value) + { + _bufferWriter.WriteByte((byte)id); + _bufferWriter.WriteByte(value); + } + + void Write(MqttPropertyId id, ushort value) + { + _bufferWriter.WriteByte((byte)id); + _bufferWriter.WriteTwoByteInteger(value); + } + + void Write(MqttPropertyId id, string value) + { + if (string.IsNullOrEmpty(value)) + { + return; + } + + _bufferWriter.WriteByte((byte)id); + _bufferWriter.WriteString(value); + } + + void Write(MqttPropertyId id, byte[] value) + { + if (value == null) + { + return; + } + + _bufferWriter.WriteByte((byte)id); + _bufferWriter.WriteBinary(value); + } + + void WriteAsFourByteInteger(MqttPropertyId id, uint value) + { + _bufferWriter.WriteByte((byte)id); + _bufferWriter.WriteByte((byte)(value >> 24)); + _bufferWriter.WriteByte((byte)(value >> 16)); + _bufferWriter.WriteByte((byte)(value >> 8)); + _bufferWriter.WriteByte((byte)value); + } + + void WriteAsVariableByteInteger(MqttPropertyId id, uint value) + { + _bufferWriter.WriteByte((byte)id); + _bufferWriter.WriteVariableByteInteger(value); + } } \ No newline at end of file diff --git a/Source/MQTTnet/Implementations/MqttClientAdapterFactory.cs b/Source/MQTTnet/Implementations/MqttClientAdapterFactory.cs index 0a4031f3..ef8f80ac 100644 --- a/Source/MQTTnet/Implementations/MqttClientAdapterFactory.cs +++ b/Source/MQTTnet/Implementations/MqttClientAdapterFactory.cs @@ -8,43 +8,42 @@ using System; using MQTTnet.Channel; using MQTTnet.Diagnostics.Logger; -namespace MQTTnet.Implementations +namespace MQTTnet.Implementations; + +public sealed class MqttClientAdapterFactory : IMqttClientAdapterFactory { - public sealed class MqttClientAdapterFactory : IMqttClientAdapterFactory + public IMqttChannelAdapter CreateClientAdapter(MqttClientOptions options, MqttPacketInspector packetInspector, IMqttNetLogger logger) { - public IMqttChannelAdapter CreateClientAdapter(MqttClientOptions options, MqttPacketInspector packetInspector, IMqttNetLogger logger) + ArgumentNullException.ThrowIfNull(options); + + IMqttChannel channel; + switch (options.ChannelOptions) { - ArgumentNullException.ThrowIfNull(options); - - IMqttChannel channel; - switch (options.ChannelOptions) + case MqttClientTcpOptions: { - case MqttClientTcpOptions _: - { - channel = new MqttTcpChannel(options); - break; - } - - case MqttClientWebSocketOptions webSocketOptions: - { - channel = new MqttWebSocketChannel(webSocketOptions); - break; - } - - default: - { - throw new NotSupportedException(); - } + channel = new MqttTcpChannel(options); + break; } - var bufferWriter = new MqttBufferWriter(options.WriterBufferSize, options.WriterBufferSizeMax); - var packetFormatterAdapter = new MqttPacketFormatterAdapter(options.ProtocolVersion, bufferWriter); - - return new MqttChannelAdapter(channel, packetFormatterAdapter, logger) + case MqttClientWebSocketOptions webSocketOptions: { - AllowPacketFragmentation = options.AllowPacketFragmentation, - PacketInspector = packetInspector - }; + channel = new MqttWebSocketChannel(webSocketOptions); + break; + } + + default: + { + throw new NotSupportedException(); + } } + + var bufferWriter = new MqttBufferWriter(options.WriterBufferSize, options.WriterBufferSizeMax); + var packetFormatterAdapter = new MqttPacketFormatterAdapter(options.ProtocolVersion, bufferWriter); + + return new MqttChannelAdapter(channel, packetFormatterAdapter, logger) + { + AllowPacketFragmentation = options.AllowPacketFragmentation, + PacketInspector = packetInspector + }; } -} +} \ No newline at end of file diff --git a/Source/MQTTnet/Implementations/MqttTcpChannel.cs b/Source/MQTTnet/Implementations/MqttTcpChannel.cs index 5ef20c1a..7333261c 100644 --- a/Source/MQTTnet/Implementations/MqttTcpChannel.cs +++ b/Source/MQTTnet/Implementations/MqttTcpChannel.cs @@ -16,313 +16,312 @@ using MQTTnet.Channel; using MQTTnet.Exceptions; using MQTTnet.Internal; -namespace MQTTnet.Implementations +namespace MQTTnet.Implementations; + +public sealed class MqttTcpChannel : IMqttChannel { - public sealed class MqttTcpChannel : IMqttChannel + readonly MqttClientOptions _clientOptions; + readonly MqttClientTcpOptions _tcpOptions; + + Stream _stream; + + public MqttTcpChannel() { - readonly MqttClientOptions _clientOptions; - readonly MqttClientTcpOptions _tcpOptions; + } - Stream _stream; + public MqttTcpChannel(MqttClientOptions clientOptions) : this() + { + _clientOptions = clientOptions ?? throw new ArgumentNullException(nameof(clientOptions)); + _tcpOptions = (MqttClientTcpOptions)clientOptions.ChannelOptions; - public MqttTcpChannel() + IsSecureConnection = clientOptions.ChannelOptions?.TlsOptions?.UseTls == true; + } + + public MqttTcpChannel(Stream stream, EndPoint remoteEndPoint, X509Certificate2 clientCertificate) : this() + { + _stream = stream ?? throw new ArgumentNullException(nameof(stream)); + + RemoteEndPoint = remoteEndPoint; + + IsSecureConnection = stream is SslStream; + ClientCertificate = clientCertificate; + } + + public X509Certificate2 ClientCertificate { get; } + + public EndPoint RemoteEndPoint { get; private set; } + + public bool IsSecureConnection { get; } + + public async Task ConnectAsync(CancellationToken cancellationToken) + { + CrossPlatformSocket socket = null; + try { - } - - public MqttTcpChannel(MqttClientOptions clientOptions) : this() - { - _clientOptions = clientOptions ?? throw new ArgumentNullException(nameof(clientOptions)); - _tcpOptions = (MqttClientTcpOptions)clientOptions.ChannelOptions; - - IsSecureConnection = clientOptions.ChannelOptions?.TlsOptions?.UseTls == true; - } - - public MqttTcpChannel(Stream stream, EndPoint remoteEndPoint, X509Certificate2 clientCertificate) : this() - { - _stream = stream ?? throw new ArgumentNullException(nameof(stream)); - - RemoteEndPoint = remoteEndPoint; - - IsSecureConnection = stream is SslStream; - ClientCertificate = clientCertificate; - } - - public X509Certificate2 ClientCertificate { get; } - - public EndPoint RemoteEndPoint { get; private set; } - - public bool IsSecureConnection { get; } - - public async Task ConnectAsync(CancellationToken cancellationToken) - { - CrossPlatformSocket socket = null; - try + if (_tcpOptions.AddressFamily == AddressFamily.Unspecified) { - if (_tcpOptions.AddressFamily == AddressFamily.Unspecified) + socket = new CrossPlatformSocket(_tcpOptions.ProtocolType); + } + else + { + socket = new CrossPlatformSocket(_tcpOptions.AddressFamily, _tcpOptions.ProtocolType); + } + + if (_tcpOptions.LocalEndpoint != null) + { + socket.Bind(_tcpOptions.LocalEndpoint); + } + + socket.ReceiveBufferSize = _tcpOptions.BufferSize; + socket.SendBufferSize = _tcpOptions.BufferSize; + socket.SendTimeout = (int)_clientOptions.Timeout.TotalMilliseconds; + + if (_tcpOptions.ProtocolType == ProtocolType.Tcp) + { + // Other protocol types do not support the Nagle algorithm. + socket.NoDelay = _tcpOptions.NoDelay; + } + + if (socket.LingerState != null) + { + socket.LingerState = _tcpOptions.LingerState; + } + + if (_tcpOptions.DualMode.HasValue) + { + // It is important to avoid setting the flag if no specific value is set by the user + // because on IPv4 only networks the setter will always throw an exception. Regardless + // of the actual value. + socket.DualMode = _tcpOptions.DualMode.Value; + } + + await socket.ConnectAsync(_tcpOptions.RemoteEndpoint, cancellationToken).ConfigureAwait(false); + + cancellationToken.ThrowIfCancellationRequested(); + + var networkStream = socket.GetStream(); + + if (_tcpOptions.TlsOptions?.UseTls == true) + { + var targetHost = _tcpOptions.TlsOptions.TargetHost; + if (string.IsNullOrEmpty(targetHost)) { - socket = new CrossPlatformSocket(_tcpOptions.ProtocolType); + if (_tcpOptions.RemoteEndpoint is DnsEndPoint dns) + { + targetHost = dns.Host; + } + } + + SslStream sslStream; + if (_tcpOptions.TlsOptions.CertificateSelectionHandler != null) + { + sslStream = new SslStream( + networkStream, + false, + InternalUserCertificateValidationCallback, + InternalUserCertificateSelectionCallback); } else { - socket = new CrossPlatformSocket(_tcpOptions.AddressFamily, _tcpOptions.ProtocolType); + // Use a different constructor depending on the options for MQTTnet so that we do not have + // to copy the exact same behavior of the selection handler. + sslStream = new SslStream( + networkStream, + false, + InternalUserCertificateValidationCallback); } - if (_tcpOptions.LocalEndpoint != null) + try { - socket.Bind(_tcpOptions.LocalEndpoint); - } - - socket.ReceiveBufferSize = _tcpOptions.BufferSize; - socket.SendBufferSize = _tcpOptions.BufferSize; - socket.SendTimeout = (int)_clientOptions.Timeout.TotalMilliseconds; - - if (_tcpOptions.ProtocolType == ProtocolType.Tcp) - { - // Other protocol types do not support the Nagle algorithm. - socket.NoDelay = _tcpOptions.NoDelay; - } - - if (socket.LingerState != null) - { - socket.LingerState = _tcpOptions.LingerState; - } - - if (_tcpOptions.DualMode.HasValue) - { - // It is important to avoid setting the flag if no specific value is set by the user - // because on IPv4 only networks the setter will always throw an exception. Regardless - // of the actual value. - socket.DualMode = _tcpOptions.DualMode.Value; - } - - await socket.ConnectAsync(_tcpOptions.RemoteEndpoint, cancellationToken).ConfigureAwait(false); - - cancellationToken.ThrowIfCancellationRequested(); - - var networkStream = socket.GetStream(); - - if (_tcpOptions.TlsOptions?.UseTls == true) - { - var targetHost = _tcpOptions.TlsOptions.TargetHost; - if (string.IsNullOrEmpty(targetHost)) + var sslOptions = new SslClientAuthenticationOptions { - if (_tcpOptions.RemoteEndpoint is DnsEndPoint dns) + ApplicationProtocols = _tcpOptions.TlsOptions.ApplicationProtocols, + ClientCertificates = LoadCertificates(), + EnabledSslProtocols = _tcpOptions.TlsOptions.SslProtocol, + CertificateRevocationCheckMode = _tcpOptions.TlsOptions.IgnoreCertificateRevocationErrors + ? X509RevocationMode.NoCheck + : _tcpOptions.TlsOptions.RevocationMode, + TargetHost = targetHost, + CipherSuitesPolicy = _tcpOptions.TlsOptions.CipherSuitesPolicy, + EncryptionPolicy = _tcpOptions.TlsOptions.EncryptionPolicy, + AllowRenegotiation = _tcpOptions.TlsOptions.AllowRenegotiation + }; + + if (_tcpOptions.TlsOptions.TrustChain?.Count > 0) + { + sslOptions.CertificateChainPolicy = new X509ChainPolicy { - targetHost = dns.Host; - } - } - - SslStream sslStream; - if (_tcpOptions.TlsOptions.CertificateSelectionHandler != null) - { - sslStream = new SslStream( - networkStream, - false, - InternalUserCertificateValidationCallback, - InternalUserCertificateSelectionCallback); - } - else - { - // Use a different constructor depending on the options for MQTTnet so that we do not have - // to copy the exact same behavior of the selection handler. - sslStream = new SslStream( - networkStream, - false, - InternalUserCertificateValidationCallback); - } - - try - { - var sslOptions = new SslClientAuthenticationOptions - { - ApplicationProtocols = _tcpOptions.TlsOptions.ApplicationProtocols, - ClientCertificates = LoadCertificates(), - EnabledSslProtocols = _tcpOptions.TlsOptions.SslProtocol, - CertificateRevocationCheckMode = _tcpOptions.TlsOptions.IgnoreCertificateRevocationErrors - ? X509RevocationMode.NoCheck - : _tcpOptions.TlsOptions.RevocationMode, - TargetHost = targetHost, - CipherSuitesPolicy = _tcpOptions.TlsOptions.CipherSuitesPolicy, - EncryptionPolicy = _tcpOptions.TlsOptions.EncryptionPolicy, - AllowRenegotiation = _tcpOptions.TlsOptions.AllowRenegotiation + TrustMode = X509ChainTrustMode.CustomRootTrust, + VerificationFlags = X509VerificationFlags.IgnoreEndRevocationUnknown, + RevocationMode = _tcpOptions.TlsOptions.IgnoreCertificateRevocationErrors ? X509RevocationMode.NoCheck : _tcpOptions.TlsOptions.RevocationMode }; - if (_tcpOptions.TlsOptions.TrustChain?.Count > 0) - { - sslOptions.CertificateChainPolicy = new X509ChainPolicy - { - TrustMode = X509ChainTrustMode.CustomRootTrust, - VerificationFlags = X509VerificationFlags.IgnoreEndRevocationUnknown, - RevocationMode = _tcpOptions.TlsOptions.IgnoreCertificateRevocationErrors ? X509RevocationMode.NoCheck : _tcpOptions.TlsOptions.RevocationMode - }; - - sslOptions.CertificateChainPolicy.CustomTrustStore.AddRange(_tcpOptions.TlsOptions.TrustChain); - } - - await sslStream.AuthenticateAsClientAsync(sslOptions, cancellationToken).ConfigureAwait(false); - } - catch - { - await sslStream.DisposeAsync().ConfigureAwait(false); - - throw; + sslOptions.CertificateChainPolicy.CustomTrustStore.AddRange(_tcpOptions.TlsOptions.TrustChain); } - _stream = sslStream; + await sslStream.AuthenticateAsClientAsync(sslOptions, cancellationToken).ConfigureAwait(false); } - else + catch { - _stream = networkStream; + await sslStream.DisposeAsync().ConfigureAwait(false); + + throw; } - RemoteEndPoint = socket.RemoteEndPoint; + _stream = sslStream; } - catch (Exception) + else { - socket?.Dispose(); - throw; + _stream = networkStream; } + + RemoteEndPoint = socket.RemoteEndPoint; } - - public Task DisconnectAsync(CancellationToken cancellationToken) + catch { - Dispose(); - return CompletedTask.Instance; + socket?.Dispose(); + throw; } + } - public void Dispose() + public Task DisconnectAsync(CancellationToken cancellationToken) + { + Dispose(); + return CompletedTask.Instance; + } + + public void Dispose() + { + // When the stream is disposed it will also close the socket and this will also dispose it. + // So there is no need to dispose the socket again. + // https://stackoverflow.com/questions/3601521/should-i-manually-dispose-the-socket-after-closing-it + try { - // When the stream is disposed it will also close the socket and this will also dispose it. - // So there is no need to dispose the socket again. - // https://stackoverflow.com/questions/3601521/should-i-manually-dispose-the-socket-after-closing-it - try - { - _stream?.Close(); - _stream?.Dispose(); - } - catch (ObjectDisposedException) - { - } - catch (NullReferenceException) - { - } - finally - { - _stream = null; - } + _stream?.Close(); + _stream?.Dispose(); } - - public async Task ReadAsync(byte[] buffer, int offset, int count, CancellationToken cancellationToken) + catch (ObjectDisposedException) { - cancellationToken.ThrowIfCancellationRequested(); + } + catch (NullReferenceException) + { + } + finally + { + _stream = null; + } + } - try + public async Task ReadAsync(byte[] buffer, int offset, int count, CancellationToken cancellationToken) + { + cancellationToken.ThrowIfCancellationRequested(); + + try + { + var stream = _stream; + + if (stream == null) { - var stream = _stream; - - if (stream == null) - { - return 0; - } - - if (!stream.CanRead) - { - return 0; - } - - return await stream.ReadAsync(buffer.AsMemory(offset, count), cancellationToken).ConfigureAwait(false); - } - catch (ObjectDisposedException) - { - // Indicate a graceful socket close. return 0; } - catch (IOException exception) - { - if (exception.InnerException is SocketException socketException) - { - ExceptionDispatchInfo.Capture(socketException).Throw(); - } - throw; + if (!stream.CanRead) + { + return 0; } + + return await stream.ReadAsync(buffer.AsMemory(offset, count), cancellationToken).ConfigureAwait(false); } - - public async Task WriteAsync(ReadOnlySequence buffer, bool isEndOfPacket, CancellationToken cancellationToken) + catch (ObjectDisposedException) { - cancellationToken.ThrowIfCancellationRequested(); - - try + // Indicate a graceful socket close. + return 0; + } + catch (IOException exception) + { + if (exception.InnerException is SocketException socketException) { - var stream = _stream; - - if (stream == null) - { - throw new MqttCommunicationException("The TCP connection is closed."); - } - - foreach (var segment in buffer) - { - await stream.WriteAsync(segment, cancellationToken).ConfigureAwait(false); - } + ExceptionDispatchInfo.Capture(socketException).Throw(); } - catch (ObjectDisposedException) + + throw; + } + } + + public async Task WriteAsync(ReadOnlySequence buffer, bool isEndOfPacket, CancellationToken cancellationToken) + { + cancellationToken.ThrowIfCancellationRequested(); + + try + { + var stream = _stream; + + if (stream == null) { throw new MqttCommunicationException("The TCP connection is closed."); } - catch (IOException exception) - { - if (exception.InnerException is SocketException socketException) - { - ExceptionDispatchInfo.Capture(socketException).Throw(); - } - throw; + foreach (var segment in buffer) + { + await stream.WriteAsync(segment, cancellationToken).ConfigureAwait(false); } } - - X509Certificate InternalUserCertificateSelectionCallback( - object sender, - string targetHost, - X509CertificateCollection localCertificates, - X509Certificate remoteCertificate, - string[] acceptableIssuers) + catch (ObjectDisposedException) { - var certificateSelectionHandler = _tcpOptions?.TlsOptions?.CertificateSelectionHandler; - if (certificateSelectionHandler != null) - { - var eventArgs = new MqttClientCertificateSelectionEventArgs(targetHost, localCertificates, remoteCertificate, acceptableIssuers, _tcpOptions); - return certificateSelectionHandler(eventArgs); - } - - if (localCertificates?.Count > 0) - { - return localCertificates[0]; - } - - return null; + throw new MqttCommunicationException("The TCP connection is closed."); } - - bool InternalUserCertificateValidationCallback(object sender, X509Certificate x509Certificate, X509Chain chain, SslPolicyErrors sslPolicyErrors) + catch (IOException exception) { - var certificateValidationHandler = _tcpOptions?.TlsOptions?.CertificateValidationHandler; - if (certificateValidationHandler != null) + if (exception.InnerException is SocketException socketException) { - var eventArgs = new MqttClientCertificateValidationEventArgs(x509Certificate, chain, sslPolicyErrors, _tcpOptions); - return certificateValidationHandler(eventArgs); + ExceptionDispatchInfo.Capture(socketException).Throw(); } - if (_tcpOptions?.TlsOptions?.IgnoreCertificateChainErrors ?? false) - { - sslPolicyErrors &= ~SslPolicyErrors.RemoteCertificateChainErrors; - } - - return sslPolicyErrors == SslPolicyErrors.None; - } - - X509CertificateCollection LoadCertificates() - { - return _tcpOptions.TlsOptions.ClientCertificatesProvider?.GetCertificates(); + throw; } } + + X509Certificate InternalUserCertificateSelectionCallback( + object sender, + string targetHost, + X509CertificateCollection localCertificates, + X509Certificate remoteCertificate, + string[] acceptableIssuers) + { + var certificateSelectionHandler = _tcpOptions?.TlsOptions?.CertificateSelectionHandler; + if (certificateSelectionHandler != null) + { + var eventArgs = new MqttClientCertificateSelectionEventArgs(targetHost, localCertificates, remoteCertificate, acceptableIssuers, _tcpOptions); + return certificateSelectionHandler(eventArgs); + } + + if (localCertificates?.Count > 0) + { + return localCertificates[0]; + } + + return null; + } + + bool InternalUserCertificateValidationCallback(object sender, X509Certificate x509Certificate, X509Chain chain, SslPolicyErrors sslPolicyErrors) + { + var certificateValidationHandler = _tcpOptions?.TlsOptions?.CertificateValidationHandler; + if (certificateValidationHandler != null) + { + var eventArgs = new MqttClientCertificateValidationEventArgs(x509Certificate, chain, sslPolicyErrors, _tcpOptions); + return certificateValidationHandler(eventArgs); + } + + if (_tcpOptions?.TlsOptions?.IgnoreCertificateChainErrors ?? false) + { + sslPolicyErrors &= ~SslPolicyErrors.RemoteCertificateChainErrors; + } + + return sslPolicyErrors == SslPolicyErrors.None; + } + + X509CertificateCollection LoadCertificates() + { + return _tcpOptions.TlsOptions.ClientCertificatesProvider?.GetCertificates(); + } } \ No newline at end of file diff --git a/Source/MQTTnet/Implementations/MqttWebSocketChannel.cs b/Source/MQTTnet/Implementations/MqttWebSocketChannel.cs index c8346011..701cc279 100644 --- a/Source/MQTTnet/Implementations/MqttWebSocketChannel.cs +++ b/Source/MQTTnet/Implementations/MqttWebSocketChannel.cs @@ -12,230 +12,229 @@ using System.Threading.Tasks; using MQTTnet.Channel; using MQTTnet.Internal; -namespace MQTTnet.Implementations +namespace MQTTnet.Implementations; + +public sealed class MqttWebSocketChannel : IMqttChannel { - public sealed class MqttWebSocketChannel : IMqttChannel + readonly MqttClientWebSocketOptions _options; + + AsyncLock _sendLock = new(); + WebSocket _webSocket; + + public MqttWebSocketChannel(MqttClientWebSocketOptions options) { - readonly MqttClientWebSocketOptions _options; + _options = options ?? throw new ArgumentNullException(nameof(options)); + } - AsyncLock _sendLock = new AsyncLock(); - WebSocket _webSocket; + public MqttWebSocketChannel(WebSocket webSocket, EndPoint remoteEndPoint, bool isSecureConnection, X509Certificate2 clientCertificate) + { + _webSocket = webSocket ?? throw new ArgumentNullException(nameof(webSocket)); - public MqttWebSocketChannel(MqttClientWebSocketOptions options) + RemoteEndPoint = remoteEndPoint; + IsSecureConnection = isSecureConnection; + ClientCertificate = clientCertificate; + } + + public X509Certificate2 ClientCertificate { get; } + + public EndPoint RemoteEndPoint { get; } + + public bool IsSecureConnection { get; private set; } + + public async Task ConnectAsync(CancellationToken cancellationToken) + { + var uri = _options.Uri; + if (!uri.StartsWith("ws://", StringComparison.OrdinalIgnoreCase) && !uri.StartsWith("wss://", StringComparison.OrdinalIgnoreCase)) { - _options = options ?? throw new ArgumentNullException(nameof(options)); - } - - public MqttWebSocketChannel(WebSocket webSocket, EndPoint remoteEndPoint, bool isSecureConnection, X509Certificate2 clientCertificate) - { - _webSocket = webSocket ?? throw new ArgumentNullException(nameof(webSocket)); - - RemoteEndPoint = remoteEndPoint; - IsSecureConnection = isSecureConnection; - ClientCertificate = clientCertificate; - } - - public X509Certificate2 ClientCertificate { get; } - - public EndPoint RemoteEndPoint { get; } - - public bool IsSecureConnection { get; private set; } - - public async Task ConnectAsync(CancellationToken cancellationToken) - { - var uri = _options.Uri; - if (!uri.StartsWith("ws://", StringComparison.OrdinalIgnoreCase) && !uri.StartsWith("wss://", StringComparison.OrdinalIgnoreCase)) + if (_options.TlsOptions?.UseTls == true) { - if (_options.TlsOptions?.UseTls == true) - { - uri = "wss://" + uri; - } - else - { - uri = "ws://" + uri; - } - } - - var clientWebSocket = new ClientWebSocket(); - try - { - SetupClientWebSocket(clientWebSocket); - - await clientWebSocket.ConnectAsync(new Uri(uri), cancellationToken).ConfigureAwait(false); - } - catch - { - // Prevent a memory leak when always creating new instance which will fail while connecting. - clientWebSocket.Dispose(); - throw; - } - - _webSocket = clientWebSocket; - IsSecureConnection = uri.StartsWith("wss://", StringComparison.OrdinalIgnoreCase); - } - - public async Task DisconnectAsync(CancellationToken cancellationToken) - { - if (_webSocket == null) - { - return; - } - - if (_webSocket.State == WebSocketState.Open || _webSocket.State == WebSocketState.Connecting) - { - await _webSocket.CloseAsync(WebSocketCloseStatus.NormalClosure, string.Empty, cancellationToken).ConfigureAwait(false); - } - - Cleanup(); - } - - public void Dispose() - { - Cleanup(); - } - - public async Task ReadAsync(byte[] buffer, int offset, int count, CancellationToken cancellationToken) - { - var response = await _webSocket.ReceiveAsync(new ArraySegment(buffer, offset, count), cancellationToken).ConfigureAwait(false); - return response.Count; - } - - public async Task WriteAsync(ReadOnlySequence buffer, bool isEndOfPacket, CancellationToken cancellationToken) - { - cancellationToken.ThrowIfCancellationRequested(); - - // MQTT Control Packets MUST be sent in WebSocket binary data frames. If any other type of data frame is received the recipient MUST close the Network Connection [MQTT-6.0.0-1]. - // A single WebSocket data frame can contain multiple or partial MQTT Control Packets. The receiver MUST NOT assume that MQTT Control Packets are aligned on WebSocket frame boundaries [MQTT-6.0.0-2]. - long length = buffer.Length; - foreach (var segment in buffer) - { - length -= segment.Length; - bool endOfPacket = isEndOfPacket && length == 0; - await _webSocket.SendAsync(segment, WebSocketMessageType.Binary, endOfPacket, cancellationToken).ConfigureAwait(false); - } - } - - void Cleanup() - { - _sendLock?.Dispose(); - _sendLock = null; - - try - { - _webSocket?.Dispose(); - } - catch (ObjectDisposedException) - { - } - finally - { - _webSocket = null; - } - } - - IWebProxy CreateProxy() - { - if (string.IsNullOrEmpty(_options.ProxyOptions?.Address)) - { - return null; - } - - var proxyUri = new Uri(_options.ProxyOptions.Address); - WebProxy webProxy; - - if (!string.IsNullOrEmpty(_options.ProxyOptions.Username) && !string.IsNullOrEmpty(_options.ProxyOptions.Password)) - { - var credentials = new NetworkCredential(_options.ProxyOptions.Username, _options.ProxyOptions.Password, _options.ProxyOptions.Domain); - webProxy = new WebProxy(proxyUri, _options.ProxyOptions.BypassOnLocal, _options.ProxyOptions.BypassList, credentials); + uri = "wss://" + uri; } else { - webProxy = new WebProxy(proxyUri, _options.ProxyOptions.BypassOnLocal, _options.ProxyOptions.BypassList); + uri = "ws://" + uri; } - - if (_options.ProxyOptions.UseDefaultCredentials) - { - // Only update the property if required because setting it to false will alter - // the used credentials internally! - webProxy.UseDefaultCredentials = true; - } - - return webProxy; } - void SetupClientWebSocket(ClientWebSocket clientWebSocket) + var clientWebSocket = new ClientWebSocket(); + try { - if (_options.ProxyOptions != null) + SetupClientWebSocket(clientWebSocket); + + await clientWebSocket.ConnectAsync(new Uri(uri), cancellationToken).ConfigureAwait(false); + } + catch + { + // Prevent a memory leak when always creating new instance which will fail while connecting. + clientWebSocket.Dispose(); + throw; + } + + _webSocket = clientWebSocket; + IsSecureConnection = uri.StartsWith("wss://", StringComparison.OrdinalIgnoreCase); + } + + public async Task DisconnectAsync(CancellationToken cancellationToken) + { + if (_webSocket == null) + { + return; + } + + if (_webSocket.State is WebSocketState.Open or WebSocketState.Connecting) + { + await _webSocket.CloseAsync(WebSocketCloseStatus.NormalClosure, string.Empty, cancellationToken).ConfigureAwait(false); + } + + Cleanup(); + } + + public void Dispose() + { + Cleanup(); + } + + public async Task ReadAsync(byte[] buffer, int offset, int count, CancellationToken cancellationToken) + { + var response = await _webSocket.ReceiveAsync(new ArraySegment(buffer, offset, count), cancellationToken).ConfigureAwait(false); + return response.Count; + } + + public async Task WriteAsync(ReadOnlySequence buffer, bool isEndOfPacket, CancellationToken cancellationToken) + { + cancellationToken.ThrowIfCancellationRequested(); + + // MQTT Control Packets MUST be sent in WebSocket binary data frames. If any other type of data frame is received the recipient MUST close the Network Connection [MQTT-6.0.0-1]. + // A single WebSocket data frame can contain multiple or partial MQTT Control Packets. The receiver MUST NOT assume that MQTT Control Packets are aligned on WebSocket frame boundaries [MQTT-6.0.0-2]. + var length = buffer.Length; + foreach (var segment in buffer) + { + length -= segment.Length; + bool endOfPacket = isEndOfPacket && length == 0; + await _webSocket.SendAsync(segment, WebSocketMessageType.Binary, endOfPacket, cancellationToken).ConfigureAwait(false); + } + } + + void Cleanup() + { + _sendLock?.Dispose(); + _sendLock = null; + + try + { + _webSocket?.Dispose(); + } + catch (ObjectDisposedException) + { + } + finally + { + _webSocket = null; + } + } + + WebProxy CreateProxy() + { + if (string.IsNullOrEmpty(_options.ProxyOptions?.Address)) + { + return null; + } + + var proxyUri = new Uri(_options.ProxyOptions.Address); + WebProxy webProxy; + + if (!string.IsNullOrEmpty(_options.ProxyOptions.Username) && !string.IsNullOrEmpty(_options.ProxyOptions.Password)) + { + var credentials = new NetworkCredential(_options.ProxyOptions.Username, _options.ProxyOptions.Password, _options.ProxyOptions.Domain); + webProxy = new WebProxy(proxyUri, _options.ProxyOptions.BypassOnLocal, _options.ProxyOptions.BypassList, credentials); + } + else + { + webProxy = new WebProxy(proxyUri, _options.ProxyOptions.BypassOnLocal, _options.ProxyOptions.BypassList); + } + + if (_options.ProxyOptions.UseDefaultCredentials) + { + // Only update the property if required because setting it to false will alter + // the used credentials internally! + webProxy.UseDefaultCredentials = true; + } + + return webProxy; + } + + void SetupClientWebSocket(ClientWebSocket clientWebSocket) + { + if (_options.ProxyOptions != null) + { + clientWebSocket.Options.Proxy = CreateProxy(); + } + + if (_options.RequestHeaders != null) + { + foreach (var requestHeader in _options.RequestHeaders) { - clientWebSocket.Options.Proxy = CreateProxy(); + clientWebSocket.Options.SetRequestHeader(requestHeader.Key, requestHeader.Value); } + } - if (_options.RequestHeaders != null) + if (_options.SubProtocols != null) + { + foreach (var subProtocol in _options.SubProtocols) { - foreach (var requestHeader in _options.RequestHeaders) - { - clientWebSocket.Options.SetRequestHeader(requestHeader.Key, requestHeader.Value); - } + clientWebSocket.Options.AddSubProtocol(subProtocol); } + } - if (_options.SubProtocols != null) + if (_options.CookieContainer != null) + { + clientWebSocket.Options.Cookies = _options.CookieContainer; + } + + if (_options.TlsOptions?.UseTls == true) + { + var certificates = _options.TlsOptions?.ClientCertificatesProvider?.GetCertificates(); + if (certificates?.Count > 0) { - foreach (var subProtocol in _options.SubProtocols) - { - clientWebSocket.Options.AddSubProtocol(subProtocol); - } + clientWebSocket.Options.ClientCertificates = certificates; } + } - if (_options.CookieContainer != null) + if (_options.DangerousDeflateOptions != null) + clientWebSocket.Options.DangerousDeflateOptions = _options.DangerousDeflateOptions; + + // Only set the value if it is actually true. This property is not supported on all platforms + // and will throw a _PlatformNotSupported_ (i.e. WASM) exception when being used regardless of the actual value. + if (_options.UseDefaultCredentials) + { + clientWebSocket.Options.UseDefaultCredentials = _options.UseDefaultCredentials; + } + + if (_options.KeepAliveInterval != WebSocket.DefaultKeepAliveInterval) + { + clientWebSocket.Options.KeepAliveInterval = _options.KeepAliveInterval; + } + + if (_options.Credentials != null) + { + clientWebSocket.Options.Credentials = _options.Credentials; + } + + var certificateValidationHandler = _options.TlsOptions?.CertificateValidationHandler; + if (certificateValidationHandler != null) + { + clientWebSocket.Options.RemoteCertificateValidationCallback = (_, certificate, chain, sslPolicyErrors) => { - clientWebSocket.Options.Cookies = _options.CookieContainer; - } + // TODO: Find a way to add client options to same callback. Problem is that they have a different type. + var context = new MqttClientCertificateValidationEventArgs(certificate, chain, sslPolicyErrors, _options); + return certificateValidationHandler(context); + }; - if (_options.TlsOptions?.UseTls == true) + var certificateSelectionHandler = _options.TlsOptions?.CertificateSelectionHandler; + if (certificateSelectionHandler != null) { - var certificates = _options.TlsOptions?.ClientCertificatesProvider?.GetCertificates(); - if (certificates?.Count > 0) - { - clientWebSocket.Options.ClientCertificates = certificates; - } - } - - if (_options.DangerousDeflateOptions != null) - clientWebSocket.Options.DangerousDeflateOptions = _options.DangerousDeflateOptions; - - // Only set the value if it is actually true. This property is not supported on all platforms - // and will throw a _PlatformNotSupported_ (i.e. WASM) exception when being used regardless of the actual value. - if (_options.UseDefaultCredentials) - { - clientWebSocket.Options.UseDefaultCredentials = _options.UseDefaultCredentials; - } - - if (_options.KeepAliveInterval != WebSocket.DefaultKeepAliveInterval) - { - clientWebSocket.Options.KeepAliveInterval = _options.KeepAliveInterval; - } - - if (_options.Credentials != null) - { - clientWebSocket.Options.Credentials = _options.Credentials; - } - - var certificateValidationHandler = _options.TlsOptions?.CertificateValidationHandler; - if (certificateValidationHandler != null) - { - clientWebSocket.Options.RemoteCertificateValidationCallback = (_, certificate, chain, sslPolicyErrors) => - { - // TODO: Find a way to add client options to same callback. Problem is that they have a different type. - var context = new MqttClientCertificateValidationEventArgs(certificate, chain, sslPolicyErrors, _options); - return certificateValidationHandler(context); - }; - - var certificateSelectionHandler = _options.TlsOptions?.CertificateSelectionHandler; - if (certificateSelectionHandler != null) - { - throw new NotSupportedException("Remote certificate selection callback is not supported for WebSocket connections."); - } + throw new NotSupportedException("Remote certificate selection callback is not supported for WebSocket connections."); } } } -} +} \ No newline at end of file diff --git a/Source/MQTTnet/Internal/AsyncEvent.cs b/Source/MQTTnet/Internal/AsyncEvent.cs index cd38105b..c86073e3 100644 --- a/Source/MQTTnet/Internal/AsyncEvent.cs +++ b/Source/MQTTnet/Internal/AsyncEvent.cs @@ -7,105 +7,104 @@ using System.Collections.Generic; using System.Threading.Tasks; using MQTTnet.Diagnostics.Logger; -namespace MQTTnet.Internal +namespace MQTTnet.Internal; + +public sealed class AsyncEvent where TEventArgs : EventArgs { - public sealed class AsyncEvent where TEventArgs : EventArgs + readonly List> _handlers = []; + + ICollection> _handlersForInvoke; + + public AsyncEvent() { - readonly List> _handlers = new List>(); + _handlersForInvoke = _handlers; + } - ICollection> _handlersForInvoke; + // Track the existence of handlers in a separate field so that checking it all the time will not + // require locking the actual list (_handlers). + public bool HasHandlers { get; private set; } - public AsyncEvent() + public void AddHandler(Func handler) + { + ArgumentNullException.ThrowIfNull(handler); + + lock (_handlers) { - _handlersForInvoke = _handlers; + _handlers.Add(new AsyncEventInvocator(null, handler)); + + HasHandlers = true; + _handlersForInvoke = new List>(_handlers); + } + } + + public void AddHandler(Action handler) + { + ArgumentNullException.ThrowIfNull(handler); + + lock (_handlers) + { + _handlers.Add(new AsyncEventInvocator(handler, null)); + + HasHandlers = true; + _handlersForInvoke = new List>(_handlers); + } + } + + public async Task InvokeAsync(TEventArgs eventArgs) + { + if (!HasHandlers) + { + return; } - // Track the existence of handlers in a separate field so that checking it all the time will not - // require locking the actual list (_handlers). - public bool HasHandlers { get; private set; } - - public void AddHandler(Func handler) + // Adding or removing handlers will produce a new list instance all the time. + // So locking here is not required since only the reference to an immutable list + // of handlers is used. + var handlers = _handlersForInvoke; + foreach (var handler in handlers) { - ArgumentNullException.ThrowIfNull(handler); - - lock (_handlers) - { - _handlers.Add(new AsyncEventInvocator(null, handler)); - - HasHandlers = true; - _handlersForInvoke = new List>(_handlers); - } + await handler.InvokeAsync(eventArgs).ConfigureAwait(false); } + } - public void AddHandler(Action handler) + public void RemoveHandler(Func handler) + { + ArgumentNullException.ThrowIfNull(handler); + + lock (_handlers) { - ArgumentNullException.ThrowIfNull(handler); + _handlers.RemoveAll(h => h.WrapsHandler(handler)); - lock (_handlers) - { - _handlers.Add(new AsyncEventInvocator(handler, null)); - - HasHandlers = true; - _handlersForInvoke = new List>(_handlers); - } + HasHandlers = _handlers.Count > 0; + _handlersForInvoke = [.._handlers]; } + } - public async Task InvokeAsync(TEventArgs eventArgs) + public void RemoveHandler(Action handler) + { + ArgumentNullException.ThrowIfNull(handler); + + lock (_handlers) { - if (!HasHandlers) - { - return; - } + _handlers.RemoveAll(h => h.WrapsHandler(handler)); - // Adding or removing handlers will produce a new list instance all the time. - // So locking here is not required since only the reference to an immutable list - // of handlers is used. - var handlers = _handlersForInvoke; - foreach (var handler in handlers) - { - await handler.InvokeAsync(eventArgs).ConfigureAwait(false); - } + HasHandlers = _handlers.Count > 0; + _handlersForInvoke = [.._handlers]; } + } - public void RemoveHandler(Func handler) + public async Task TryInvokeAsync(TEventArgs eventArgs, MqttNetSourceLogger logger) + { + ArgumentNullException.ThrowIfNull(eventArgs); + ArgumentNullException.ThrowIfNull(logger); + + try { - ArgumentNullException.ThrowIfNull(handler); - - lock (_handlers) - { - _handlers.RemoveAll(h => h.WrapsHandler(handler)); - - HasHandlers = _handlers.Count > 0; - _handlersForInvoke = new List>(_handlers); - } + await InvokeAsync(eventArgs).ConfigureAwait(false); } - - public void RemoveHandler(Action handler) + catch (Exception exception) { - ArgumentNullException.ThrowIfNull(handler); - - lock (_handlers) - { - _handlers.RemoveAll(h => h.WrapsHandler(handler)); - - HasHandlers = _handlers.Count > 0; - _handlersForInvoke = new List>(_handlers); - } - } - - public async Task TryInvokeAsync(TEventArgs eventArgs, MqttNetSourceLogger logger) - { - ArgumentNullException.ThrowIfNull(eventArgs); - ArgumentNullException.ThrowIfNull(logger); - - try - { - await InvokeAsync(eventArgs).ConfigureAwait(false); - } - catch (Exception exception) - { - logger.Warning(exception, $"Error while invoking event with arguments of type {typeof(TEventArgs)}."); - } + logger.Warning(exception, $"Error while invoking event with arguments of type {typeof(TEventArgs)}."); } } } \ No newline at end of file diff --git a/Source/MQTTnet/Internal/AsyncEventInvocator.cs b/Source/MQTTnet/Internal/AsyncEventInvocator.cs index f344cb1e..d2527245 100644 --- a/Source/MQTTnet/Internal/AsyncEventInvocator.cs +++ b/Source/MQTTnet/Internal/AsyncEventInvocator.cs @@ -4,47 +4,45 @@ using System; using System.Threading.Tasks; -using MQTTnet.Implementations; -namespace MQTTnet.Internal +namespace MQTTnet.Internal; + +public readonly struct AsyncEventInvocator { - public readonly struct AsyncEventInvocator + readonly Action _handler; + readonly Func _asyncHandler; + + public AsyncEventInvocator(Action handler, Func asyncHandler) { - readonly Action _handler; - readonly Func _asyncHandler; - - public AsyncEventInvocator(Action handler, Func asyncHandler) + _handler = handler; + _asyncHandler = asyncHandler; + } + + public bool WrapsHandler(Action handler1) + { + // Do not use ReferenceEquals! It will not work with delegates. + return handler1 == _handler; + } + + public bool WrapsHandler(Func handler) + { + // Do not use ReferenceEquals! It will not work with delegates. + return handler == _asyncHandler; + } + + public Task InvokeAsync(TEventArgs eventArgs) + { + if (_handler != null) { - _handler = handler; - _asyncHandler = asyncHandler; + _handler(eventArgs); + return CompletedTask.Instance; } - public bool WrapsHandler(Action handler) + if (_asyncHandler != null) { - // Do not use ReferenceEquals! It will not work with delegates. - return handler == _handler; + return _asyncHandler(eventArgs); } - - public bool WrapsHandler(Func handler) - { - // Do not use ReferenceEquals! It will not work with delegates. - return handler == _asyncHandler; - } - - public Task InvokeAsync(TEventArgs eventArgs) - { - if (_handler != null) - { - _handler.Invoke(eventArgs); - return CompletedTask.Instance; - } - if (_asyncHandler != null) - { - return _asyncHandler.Invoke(eventArgs); - } - - throw new InvalidOperationException(); - } + throw new InvalidOperationException(); } } \ No newline at end of file diff --git a/Source/MQTTnet/Internal/AsyncLock.cs b/Source/MQTTnet/Internal/AsyncLock.cs index 0d129b10..de646ede 100644 --- a/Source/MQTTnet/Internal/AsyncLock.cs +++ b/Source/MQTTnet/Internal/AsyncLock.cs @@ -4,157 +4,152 @@ using System; using System.Collections.Generic; -using System.Linq; using System.Threading; using System.Threading.Tasks; -namespace MQTTnet.Internal +namespace MQTTnet.Internal; + +public sealed class AsyncLock : IDisposable { - public sealed class AsyncLock : IDisposable + readonly Task _completedTask; + readonly IDisposable _releaser; + readonly object _syncRoot = new(); + readonly Queue _waiters = new(64); + + volatile bool _isDisposed; + bool _isLocked; + + public AsyncLock() { - readonly Task _completedTask; - readonly IDisposable _releaser; - readonly object _syncRoot = new object(); - readonly Queue _waiters = new Queue(64); + _releaser = new Releaser(this); + _completedTask = Task.FromResult(_releaser); + } - volatile bool _isDisposed; - bool _isLocked; - - public AsyncLock() + public void Dispose() + { + lock (_syncRoot) { - _releaser = new Releaser(this); - _completedTask = Task.FromResult(_releaser); + _isDisposed = true; + + while (_waiters.Count != 0) + { + _waiters.Dequeue().Dispose(); + } + } + } + + public Task EnterAsync(CancellationToken cancellationToken = default) + { + cancellationToken.ThrowIfCancellationRequested(); + + ObjectDisposedException.ThrowIf(_isDisposed, this); + + lock (_syncRoot) + { + if (!_isLocked) + { + _isLocked = true; + return _completedTask; + } + + var waiter = new AsyncLockWaiter(cancellationToken); + _waiters.Enqueue(waiter); + + return waiter.Task; + } + } + + void Release() + { + lock (_syncRoot) + { + if (_isDisposed) + { + // All waiters have been canceled with a ObjectDisposedException. + // So there is nothing left to do. + return; + } + + // Assume that there is no waiter left first. + _isLocked = false; + + // Try to find the next waiter which can be approved. + // Some of them might be canceled already so it is not + // guaranteed that the very next waiter is the correct one. + while (_waiters.Count != 0) + { + var waiter = _waiters.Dequeue(); + var isApproved = waiter.Approve(_releaser); + waiter.Dispose(); + + if (isApproved) + { + _isLocked = true; + return; + } + } + } + } + + sealed class AsyncLockWaiter : IDisposable + { + readonly CancellationTokenRegistration _cancellationRegistration; + readonly bool _hasCancellationRegistration; + readonly AsyncTaskCompletionSource _promise = new AsyncTaskCompletionSource(); + + public AsyncLockWaiter(CancellationToken cancellationToken) + { + cancellationToken.ThrowIfCancellationRequested(); + + if (cancellationToken.CanBeCanceled) + { + _cancellationRegistration = cancellationToken.Register(Cancel); + _hasCancellationRegistration = true; + } + } + + public Task Task => _promise.Task; + + public bool Approve(IDisposable scope) + { + ArgumentNullException.ThrowIfNull(scope); + + if (_promise.Task.IsCompleted) + { + return false; + } + + return _promise.TrySetResult(scope); } public void Dispose() { - lock (_syncRoot) + if (_hasCancellationRegistration) { - _isDisposed = true; - - while (_waiters.Any()) - { - _waiters.Dequeue().Dispose(); - } + _cancellationRegistration.Dispose(); } + + _promise.TrySetException(new ObjectDisposedException(nameof(AsyncLockWaiter))); } - public Task EnterAsync(CancellationToken cancellationToken = default) + void Cancel() { - cancellationToken.ThrowIfCancellationRequested(); - - if (_isDisposed) - { - throw new ObjectDisposedException(nameof(AsyncLock)); - } + _promise.TrySetCanceled(); + } + } - lock (_syncRoot) - { - if (!_isLocked) - { - _isLocked = true; - return _completedTask; - } + readonly struct Releaser : IDisposable + { + readonly AsyncLock _asyncLock; - var waiter = new AsyncLockWaiter(cancellationToken); - _waiters.Enqueue(waiter); - - return waiter.Task; - } + public Releaser(AsyncLock asyncLock) + { + _asyncLock = asyncLock ?? throw new ArgumentNullException(nameof(asyncLock)); } - void Release() + public void Dispose() { - lock (_syncRoot) - { - if (_isDisposed) - { - // All waiters have been canceled with a ObjectDisposedException. - // So there is nothing left to do. - return; - } - - // Assume that there is no waiter left first. - _isLocked = false; - - // Try to find the next waiter which can be approved. - // Some of them might be canceled already so it is not - // guaranteed that the very next waiter is the correct one. - while (_waiters.Any()) - { - var waiter = _waiters.Dequeue(); - var isApproved = waiter.Approve(_releaser); - waiter.Dispose(); - - if (isApproved) - { - _isLocked = true; - return; - } - } - } - } - - sealed class AsyncLockWaiter : IDisposable - { - readonly CancellationTokenRegistration _cancellationRegistration; - readonly bool _hasCancellationRegistration; - readonly AsyncTaskCompletionSource _promise = new AsyncTaskCompletionSource(); - - public AsyncLockWaiter(CancellationToken cancellationToken) - { - cancellationToken.ThrowIfCancellationRequested(); - - if (cancellationToken.CanBeCanceled) - { - _cancellationRegistration = cancellationToken.Register(Cancel); - _hasCancellationRegistration = true; - } - } - - public Task Task => _promise.Task; - - public bool Approve(IDisposable scope) - { - ArgumentNullException.ThrowIfNull(scope); - - if (_promise.Task.IsCompleted) - { - return false; - } - - return _promise.TrySetResult(scope); - } - - public void Dispose() - { - if (_hasCancellationRegistration) - { - _cancellationRegistration.Dispose(); - } - - _promise.TrySetException(new ObjectDisposedException(nameof(AsyncLockWaiter))); - } - - void Cancel() - { - _promise.TrySetCanceled(); - } - } - - readonly struct Releaser : IDisposable - { - readonly AsyncLock _asyncLock; - - public Releaser(AsyncLock asyncLock) - { - _asyncLock = asyncLock ?? throw new ArgumentNullException(nameof(asyncLock)); - } - - public void Dispose() - { - _asyncLock.Release(); - } + _asyncLock.Release(); } } } \ No newline at end of file diff --git a/Source/MQTTnet/Internal/AsyncQueue.cs b/Source/MQTTnet/Internal/AsyncQueue.cs index 8824b07b..f86a0902 100644 --- a/Source/MQTTnet/Internal/AsyncQueue.cs +++ b/Source/MQTTnet/Internal/AsyncQueue.cs @@ -7,103 +7,104 @@ using System.Collections.Concurrent; using System.Threading; using System.Threading.Tasks; -namespace MQTTnet.Internal +namespace MQTTnet.Internal; + +#pragma warning disable CA1711 + +public sealed class AsyncQueue : IDisposable { - public sealed class AsyncQueue : IDisposable + readonly AsyncSignal _signal = new(); + readonly object _syncRoot = new(); + + readonly ConcurrentQueue _queue = []; + + bool _isDisposed; + + public int Count => _queue.Count; + + public void Clear() { - readonly AsyncSignal _signal = new AsyncSignal(); - readonly object _syncRoot = new object(); + _queue.Clear(); + } - ConcurrentQueue _queue = new ConcurrentQueue(); - - bool _isDisposed; - - public int Count => _queue.Count; - - public void Clear() + public void Dispose() + { + lock (_syncRoot) { - _queue.Clear(); - } + _signal.Dispose(); - public void Dispose() - { - lock (_syncRoot) + _isDisposed = true; + + if (typeof(IDisposable).IsAssignableFrom(typeof(TItem))) { - _signal.Dispose(); - - _isDisposed = true; - - if (typeof(IDisposable).IsAssignableFrom(typeof(TItem))) + while (_queue.TryDequeue(out var item)) { - while (_queue.TryDequeue(out TItem item)) - { - (item as IDisposable).Dispose(); - } + (item as IDisposable)?.Dispose(); } } } + } - public void Enqueue(TItem item) + public void Enqueue(TItem item) + { + lock (_syncRoot) { - lock (_syncRoot) - { - _queue.Enqueue(item); - _signal.Set(); - } + _queue.Enqueue(item); + _signal.Set(); + } + } + + public AsyncQueueDequeueResult TryDequeue() + { + if (_queue.TryDequeue(out var item)) + { + return new AsyncQueueDequeueResult(true, item); } - public AsyncQueueDequeueResult TryDequeue() - { - if (_queue.TryDequeue(out var item)) - { - return AsyncQueueDequeueResult.Success(item); - } + return AsyncQueueDequeueResult.NonSuccess; + } - return AsyncQueueDequeueResult.NonSuccess; - } - - public async Task> TryDequeueAsync(CancellationToken cancellationToken) + public async Task> TryDequeueAsync(CancellationToken cancellationToken) + { + while (!cancellationToken.IsCancellationRequested) { - while (!cancellationToken.IsCancellationRequested) + try { - try + Task task = null; + lock (_syncRoot) { - Task task = null; - lock (_syncRoot) - { - if (_isDisposed) - { - return AsyncQueueDequeueResult.NonSuccess; - } - - if (_queue.IsEmpty) - { - task = _signal.WaitAsync(cancellationToken); - } - } - - if (task != null) - { - await task.ConfigureAwait(false); - } - - if (cancellationToken.IsCancellationRequested) + if (_isDisposed) { return AsyncQueueDequeueResult.NonSuccess; } - if (_queue.TryDequeue(out var item)) + if (_queue.IsEmpty) { - return AsyncQueueDequeueResult.Success(item); + task = _signal.WaitAsync(cancellationToken); } } - catch (OperationCanceledException) + + if (task != null) + { + await task.ConfigureAwait(false); + } + + if (cancellationToken.IsCancellationRequested) { return AsyncQueueDequeueResult.NonSuccess; } - } - return AsyncQueueDequeueResult.NonSuccess; + if (_queue.TryDequeue(out var item)) + { + return new AsyncQueueDequeueResult(true, item); + } + } + catch (OperationCanceledException) + { + return AsyncQueueDequeueResult.NonSuccess; + } } + + return AsyncQueueDequeueResult.NonSuccess; } } \ No newline at end of file diff --git a/Source/MQTTnet/Internal/AsyncQueueDequeueResult.cs b/Source/MQTTnet/Internal/AsyncQueueDequeueResult.cs index 09543238..bdbf4f1b 100644 --- a/Source/MQTTnet/Internal/AsyncQueueDequeueResult.cs +++ b/Source/MQTTnet/Internal/AsyncQueueDequeueResult.cs @@ -2,25 +2,13 @@ // The .NET Foundation licenses this file to you under the MIT license. // See the LICENSE file in the project root for more information. -namespace MQTTnet.Internal +namespace MQTTnet.Internal; + +public sealed class AsyncQueueDequeueResult(bool isSuccess, TItem item) { - public sealed class AsyncQueueDequeueResult - { - public static readonly AsyncQueueDequeueResult NonSuccess = new AsyncQueueDequeueResult(false, default); + public static readonly AsyncQueueDequeueResult NonSuccess = new(false, default); - public AsyncQueueDequeueResult(bool isSuccess, TItem item) - { - IsSuccess = isSuccess; - Item = item; - } + public bool IsSuccess { get; } = isSuccess; - public bool IsSuccess { get; } - - public TItem Item { get; } - - public static AsyncQueueDequeueResult Success(TItem item) - { - return new AsyncQueueDequeueResult(true, item); - } - } + public TItem Item { get; } = item; } \ No newline at end of file diff --git a/Source/MQTTnet/Internal/AsyncSignal.cs b/Source/MQTTnet/Internal/AsyncSignal.cs index bc3bcb86..d5f1589b 100644 --- a/Source/MQTTnet/Internal/AsyncSignal.cs +++ b/Source/MQTTnet/Internal/AsyncSignal.cs @@ -6,150 +6,146 @@ using System; using System.Threading; using System.Threading.Tasks; -namespace MQTTnet.Internal +namespace MQTTnet.Internal; + +public sealed class AsyncSignal : IDisposable { - public sealed class AsyncSignal : IDisposable + readonly object _syncRoot = new(); + + bool _isDisposed; + bool _isSignaled; + AsyncSignalWaiter _waiter; + + public void Dispose() { - readonly object _syncRoot = new object(); - - bool _isDisposed; - bool _isSignaled; - AsyncSignalWaiter _waiter; - - public void Dispose() + lock (_syncRoot) { - lock (_syncRoot) + _waiter?.Dispose(); + _waiter = null; + + _isDisposed = true; + } + } + + public void Set() + { + lock (_syncRoot) + { + _isSignaled = true; + + Cleanup(); + + // If there is already a waiting task let it run. + if (_waiter != null) { - _waiter?.Dispose(); + _waiter.Approve(); + _waiter.Dispose(); _waiter = null; - _isDisposed = true; - } - } - - public void Set() - { - lock (_syncRoot) - { - _isSignaled = true; - - Cleanup(); - - // If there is already a waiting task let it run. - if (_waiter != null) - { - _waiter.Approve(); - _waiter.Dispose(); - _waiter = null; - - // Since we already got a waiter the signal must be reset right now! - _isSignaled = false; - } - } - } - - public Task WaitAsync(CancellationToken cancellationToken = default) - { - cancellationToken.ThrowIfCancellationRequested(); - - lock (_syncRoot) - { - ThrowIfDisposed(); - - Cleanup(); - - if (_isSignaled) - { - _isSignaled = false; - return CompletedTask.Instance; - } - - if (_waiter != null) - { - if (!_waiter.Task.IsCompleted) - { - throw new InvalidOperationException("Only one waiting task is permitted per async signal."); - } - - _waiter.Dispose(); - } - - _waiter = new AsyncSignalWaiter(cancellationToken); - return _waiter.Task; - } - } - - void Cleanup() - { - // Cleanup if the previous waiter was cancelled. - if (_waiter != null && _waiter.Task.IsCanceled) - { - _waiter = null; - } - } - - void ThrowIfDisposed() - { - if (_isDisposed) - { - throw new ObjectDisposedException(nameof(AsyncSignal)); - } - } - - sealed class AsyncSignalWaiter : IDisposable - { - readonly AsyncTaskCompletionSource _promise = new AsyncTaskCompletionSource(); - - // ReSharper disable once FieldCanBeMadeReadOnly.Local - CancellationTokenRegistration _cancellationTokenRegistration; - - volatile bool _isCompleted; - - public AsyncSignalWaiter(CancellationToken cancellationToken) - { - if (cancellationToken.CanBeCanceled) - { - _cancellationTokenRegistration = cancellationToken.Register(Cancel); - } - } - - public Task Task => _promise.Task; - - public void Approve() - { - if (_isCompleted) - { - return; - } - - _isCompleted = true; - _promise.TrySetResult(true); - } - - public void Dispose() - { - _cancellationTokenRegistration.Dispose(); - - if (_isCompleted) - { - // Avoid allocation of _ObjectDisposedException_ which may not be used. - return; - } - - _isCompleted = true; - _promise.TrySetException(new ObjectDisposedException(nameof(AsyncSignalWaiter))); - } - - void Cancel() - { - if (_isCompleted) - { - return; - } - - _isCompleted = true; - _promise.TrySetCanceled(); + // Since we already got a waiter the signal must be reset right now! + _isSignaled = false; } } } + + public Task WaitAsync(CancellationToken cancellationToken = default) + { + cancellationToken.ThrowIfCancellationRequested(); + + lock (_syncRoot) + { + ThrowIfDisposed(); + + Cleanup(); + + if (_isSignaled) + { + _isSignaled = false; + return CompletedTask.Instance; + } + + if (_waiter != null) + { + if (!_waiter.Task.IsCompleted) + { + throw new InvalidOperationException("Only one waiting task is permitted per async signal."); + } + + _waiter.Dispose(); + } + + _waiter = new AsyncSignalWaiter(cancellationToken); + return _waiter.Task; + } + } + + void Cleanup() + { + // Cleanup if the previous waiter was cancelled. + if (_waiter != null && _waiter.Task.IsCanceled) + { + _waiter = null; + } + } + + void ThrowIfDisposed() + { + ObjectDisposedException.ThrowIf(_isDisposed, this); + } + + sealed class AsyncSignalWaiter : IDisposable + { + readonly AsyncTaskCompletionSource _promise = new(); + + // ReSharper disable once FieldCanBeMadeReadOnly.Local + CancellationTokenRegistration _cancellationTokenRegistration; + + volatile bool _isCompleted; + + public AsyncSignalWaiter(CancellationToken cancellationToken) + { + if (cancellationToken.CanBeCanceled) + { + _cancellationTokenRegistration = cancellationToken.Register(Cancel); + } + } + + public Task Task => _promise.Task; + + public void Approve() + { + if (_isCompleted) + { + return; + } + + _isCompleted = true; + _promise.TrySetResult(true); + } + + public void Dispose() + { + _cancellationTokenRegistration.Dispose(); + + if (_isCompleted) + { + // Avoid allocation of _ObjectDisposedException_ which may not be used. + return; + } + + _isCompleted = true; + _promise.TrySetException(new ObjectDisposedException(nameof(AsyncSignalWaiter))); + } + + void Cancel() + { + if (_isCompleted) + { + return; + } + + _isCompleted = true; + _promise.TrySetCanceled(); + } + } } \ No newline at end of file diff --git a/Source/MQTTnet/Internal/AsyncTaskCompletionSource.cs b/Source/MQTTnet/Internal/AsyncTaskCompletionSource.cs index 7c0b9f4d..f7f2092c 100644 --- a/Source/MQTTnet/Internal/AsyncTaskCompletionSource.cs +++ b/Source/MQTTnet/Internal/AsyncTaskCompletionSource.cs @@ -5,32 +5,26 @@ using System; using System.Threading.Tasks; -namespace MQTTnet.Internal +namespace MQTTnet.Internal; + +public sealed class AsyncTaskCompletionSource { - public sealed class AsyncTaskCompletionSource + readonly TaskCompletionSource _taskCompletionSource = new(TaskCreationOptions.RunContinuationsAsynchronously); + + public Task Task => _taskCompletionSource.Task; + + public void TrySetCanceled() { - readonly TaskCompletionSource _taskCompletionSource; + _taskCompletionSource.TrySetCanceled(); + } - public AsyncTaskCompletionSource() - { - _taskCompletionSource = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); - } + public void TrySetException(Exception exception) + { + _taskCompletionSource.TrySetException(exception); + } - public Task Task => _taskCompletionSource.Task; - - public void TrySetCanceled() - { - _taskCompletionSource.TrySetCanceled(); - } - - public void TrySetException(Exception exception) - { - _taskCompletionSource.TrySetException(exception); - } - - public bool TrySetResult(TResult result) - { - return _taskCompletionSource.TrySetResult(result); - } + public bool TrySetResult(TResult result) + { + return _taskCompletionSource.TrySetResult(result); } } \ No newline at end of file diff --git a/Source/MQTTnet/Internal/BlockingQueue.cs b/Source/MQTTnet/Internal/BlockingQueue.cs index e4122dfe..142c331f 100644 --- a/Source/MQTTnet/Internal/BlockingQueue.cs +++ b/Source/MQTTnet/Internal/BlockingQueue.cs @@ -6,122 +6,123 @@ using System; using System.Collections.Generic; using System.Threading; -namespace MQTTnet.Internal +namespace MQTTnet.Internal; + +#pragma warning disable CA1711 + +public sealed class BlockingQueue : IDisposable { - public sealed class BlockingQueue : IDisposable + readonly LinkedList _items = []; + readonly object _syncRoot = new(); + + ManualResetEventSlim _gate = new(false); + + public int Count { - readonly object _syncRoot = new object(); - readonly LinkedList _items = new LinkedList(); - - ManualResetEventSlim _gate = new ManualResetEventSlim(false); - - public int Count - { - get - { - lock (_syncRoot) - { - return _items.Count; - } - } - } - - public void Enqueue(TItem item) - { - ArgumentNullException.ThrowIfNull(item); - - lock (_syncRoot) - { - _items.AddLast(item); - _gate?.Set(); - } - } - - public TItem Dequeue(CancellationToken cancellationToken = default) - { - while (!cancellationToken.IsCancellationRequested) - { - lock (_syncRoot) - { - if (_items.Count > 0) - { - var item = _items.First.Value; - _items.RemoveFirst(); - - return item; - } - - if (_items.Count == 0) - { - _gate?.Reset(); - } - } - - _gate?.Wait(cancellationToken); - } - - throw new OperationCanceledException(); - } - - public TItem PeekAndWait(CancellationToken cancellationToken = default) - { - while (!cancellationToken.IsCancellationRequested) - { - lock (_syncRoot) - { - if (_items.Count > 0) - { - return _items.First.Value; - } - - if (_items.Count == 0) - { - _gate?.Reset(); - } - } - - _gate?.Wait(cancellationToken); - } - - throw new OperationCanceledException(); - } - - public void RemoveFirst(Predicate match) - { - ArgumentNullException.ThrowIfNull(match); - - lock (_syncRoot) - { - if (_items.Count > 0 && match(_items.First.Value)) - { - _items.RemoveFirst(); - } - } - } - - public TItem RemoveFirst() + get { lock (_syncRoot) { - var item = _items.First; - _items.RemoveFirst(); - - return item.Value; + return _items.Count; } } - - public void Clear() - { - lock (_syncRoot) - { - _items.Clear(); - } - } - - public void Dispose() - { - _gate?.Dispose(); - _gate = null; - } } -} + + public void Clear() + { + lock (_syncRoot) + { + _items.Clear(); + } + } + + public TItem Dequeue(CancellationToken cancellationToken = default) + { + while (!cancellationToken.IsCancellationRequested) + { + lock (_syncRoot) + { + if (_items.Count > 0) + { + var item = _items.First!.Value; + _items.RemoveFirst(); + + return item; + } + + if (_items.Count == 0) + { + _gate?.Reset(); + } + } + + _gate?.Wait(cancellationToken); + } + + throw new OperationCanceledException(); + } + + public void Dispose() + { + _gate?.Dispose(); + _gate = null; + } + + public void Enqueue(TItem item) + { + ArgumentNullException.ThrowIfNull(item); + + lock (_syncRoot) + { + _items.AddLast(item); + _gate?.Set(); + } + } + + public TItem PeekAndWait(CancellationToken cancellationToken = default) + { + while (!cancellationToken.IsCancellationRequested) + { + lock (_syncRoot) + { + if (_items.Count > 0) + { + return _items.First!.Value; + } + + if (_items.Count == 0) + { + _gate?.Reset(); + } + } + + _gate?.Wait(cancellationToken); + } + + throw new OperationCanceledException(); + } + + public void RemoveFirst(Predicate match) + { + ArgumentNullException.ThrowIfNull(match); + + lock (_syncRoot) + { + if (_items.Count > 0 && match(_items.First!.Value)) + { + _items.RemoveFirst(); + } + } + } + + public TItem RemoveFirst() + { + lock (_syncRoot) + { + var item = _items.First; + _items.RemoveFirst(); + + return item!.Value; + } + } +} \ No newline at end of file diff --git a/Source/MQTTnet/Internal/CancellationTokenSourceExtensions.cs b/Source/MQTTnet/Internal/CancellationTokenSourceExtensions.cs index 94c3febc..ed076d54 100644 --- a/Source/MQTTnet/Internal/CancellationTokenSourceExtensions.cs +++ b/Source/MQTTnet/Internal/CancellationTokenSourceExtensions.cs @@ -5,33 +5,32 @@ using System; using System.Threading; -namespace MQTTnet.Internal +namespace MQTTnet.Internal; + +public static class CancellationTokenSourceExtensions { - public static class CancellationTokenSourceExtensions + public static bool TryCancel(this CancellationTokenSource cancellationTokenSource, bool throwOnFirstException = false) { - public static bool TryCancel(this CancellationTokenSource cancellationTokenSource, bool throwOnFirstException = false) + if (cancellationTokenSource == null) { - if (cancellationTokenSource == null) + return false; + } + + try + { + // Checking the _IsCancellationRequested_ here will not throw an + // "ObjectDisposedException" as the getter of the property "Token" will do! + if (cancellationTokenSource.IsCancellationRequested) { return false; } - try - { - // Checking the _IsCancellationRequested_ here will not throw an - // "ObjectDisposedException" as the getter of the property "Token" will do! - if (cancellationTokenSource.IsCancellationRequested) - { - return false; - } - - cancellationTokenSource.Cancel(throwOnFirstException); - return true; - } - catch (ObjectDisposedException) - { - return false; - } + cancellationTokenSource.Cancel(throwOnFirstException); + return true; + } + catch (ObjectDisposedException) + { + return false; } } } \ No newline at end of file diff --git a/Source/MQTTnet/Internal/CompletedTask.cs b/Source/MQTTnet/Internal/CompletedTask.cs index 4bd129eb..202f2c92 100644 --- a/Source/MQTTnet/Internal/CompletedTask.cs +++ b/Source/MQTTnet/Internal/CompletedTask.cs @@ -4,10 +4,9 @@ using System.Threading.Tasks; -namespace MQTTnet.Internal +namespace MQTTnet.Internal; + +public static class CompletedTask { - public static class CompletedTask - { - public static readonly Task Instance = Task.CompletedTask; - } + public static readonly Task Instance = Task.CompletedTask; } \ No newline at end of file diff --git a/Source/MQTTnet/Internal/Disposable.cs b/Source/MQTTnet/Internal/Disposable.cs index 0adb3478..a35a3214 100644 --- a/Source/MQTTnet/Internal/Disposable.cs +++ b/Source/MQTTnet/Internal/Disposable.cs @@ -4,38 +4,34 @@ using System; -namespace MQTTnet.Internal +namespace MQTTnet.Internal; + +public abstract class Disposable : IDisposable { - public abstract class Disposable : IDisposable + protected bool IsDisposed { get; private set; } + + protected void ThrowIfDisposed() { - protected bool IsDisposed { get; private set; } - - protected void ThrowIfDisposed() - { - if (IsDisposed) - { - throw new ObjectDisposedException(GetType().Name); - } - } - - protected virtual void Dispose(bool disposing) - { - } - - // This code added to correctly implement the disposable pattern. - public void Dispose() - { - // Do not change this code. Put cleanup code in Dispose(bool disposing) above. - - if (IsDisposed) - { - return; - } - - IsDisposed = true; - - Dispose(true); - GC.SuppressFinalize(this); - } + ObjectDisposedException.ThrowIf(IsDisposed, GetType()); } -} + + protected virtual void Dispose(bool disposing) + { + } + + // This code added to correctly implement the disposable pattern. + public void Dispose() + { + // Do not change this code. Put cleanup code in Dispose(bool disposing) above. + + if (IsDisposed) + { + return; + } + + IsDisposed = true; + + Dispose(true); + GC.SuppressFinalize(this); + } +} \ No newline at end of file diff --git a/Source/MQTTnet/Internal/EmptyBuffer.cs b/Source/MQTTnet/Internal/EmptyBuffer.cs index eb0506e9..dd319aef 100644 --- a/Source/MQTTnet/Internal/EmptyBuffer.cs +++ b/Source/MQTTnet/Internal/EmptyBuffer.cs @@ -5,14 +5,13 @@ using System; using System.Buffers; -namespace MQTTnet.Internal +namespace MQTTnet.Internal; + +public static class EmptyBuffer { - public static class EmptyBuffer - { - public static readonly byte[] Array = System.Array.Empty(); + public static readonly byte[] Array = []; - public static readonly ArraySegment ArraySegment = new ArraySegment(Array, 0, 0); + public static readonly ArraySegment ArraySegment = new(Array, 0, 0); - public static readonly ReadOnlySequence ReadOnlySequence = ReadOnlySequence.Empty; - } + public static readonly ReadOnlySequence ReadOnlySequence = ReadOnlySequence.Empty; } \ No newline at end of file diff --git a/Source/MQTTnet/Internal/MqttClientResultFactory.cs b/Source/MQTTnet/Internal/MqttClientResultFactory.cs deleted file mode 100644 index bba11ab1..00000000 --- a/Source/MQTTnet/Internal/MqttClientResultFactory.cs +++ /dev/null @@ -1,13 +0,0 @@ -// Licensed to the .NET Foundation under one or more agreements. -// The .NET Foundation licenses this file to you under the MIT license. -// See the LICENSE file in the project root for more information. - -namespace MQTTnet.Internal; - -public static class MqttClientResultFactory -{ - public static readonly MqttClientConnectResultFactory ConnectResult = new(); - public static readonly MqttClientPublishResultFactory PublishResult = new(); - public static readonly MqttClientSubscribeResultFactory SubscribeResult = new(); - public static readonly MqttClientUnsubscribeResultFactory UnsubscribeResult = new(); -} \ No newline at end of file diff --git a/Source/MQTTnet/Internal/MqttMemoryHelper.cs b/Source/MQTTnet/Internal/MqttMemoryHelper.cs index 8f3b1008..3e61495b 100644 --- a/Source/MQTTnet/Internal/MqttMemoryHelper.cs +++ b/Source/MQTTnet/Internal/MqttMemoryHelper.cs @@ -2,86 +2,85 @@ using System; using System.Buffers; using System.Runtime.CompilerServices; -namespace MQTTnet.Internal +namespace MQTTnet.Internal; + +public static class MqttMemoryHelper { - public static class MqttMemoryHelper + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static void Copy(byte[] source, int sourceIndex, byte[] destination, int destinationIndex, int length) { - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static void Copy(byte[] source, int sourceIndex, byte[] destination, int destinationIndex, int length) + source.AsSpan(sourceIndex, length).CopyTo(destination.AsSpan(destinationIndex, length)); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static void Copy(ReadOnlySequence sequence, int sourceIndex, byte[] destination, int destinationIndex, int length) + { + sequence.Slice(sourceIndex).CopyTo(destination.AsSpan(destinationIndex, length)); + } + + public static bool SequenceEqual(ArraySegment source, ArraySegment target) + { + return source.AsSpan().SequenceEqual(target); + } + + public static bool SequenceEqual(ReadOnlySequence source, ReadOnlySequence target) + { + if (source.Length != target.Length) { - source.AsSpan(sourceIndex, length).CopyTo(destination.AsSpan(destinationIndex, length)); + return false; } - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static void Copy(ReadOnlySequence sequence, int sourceIndex, byte[] destination, int destinationIndex, int length) - { - sequence.Slice(sourceIndex).CopyTo(destination.AsSpan(destinationIndex, length)); - } + long comparedLength = 0; + long length = source.Length; - public static bool SequenceEqual(ArraySegment source, ArraySegment target) - { - return source.AsSpan().SequenceEqual(target); - } + int sourceOffset = 0; + int targetOffset = 0; - public static bool SequenceEqual(ReadOnlySequence source, ReadOnlySequence target) + var sourceEnumerator = source.GetEnumerator(); + var targetEnumerator = target.GetEnumerator(); + + var sourceSegment = sourceEnumerator.Current; + var targetSegment = targetEnumerator.Current; + + while (true) { - if (source.Length != target.Length) + var compareLength = Math.Min(sourceSegment.Length - sourceOffset, targetSegment.Length - targetOffset); + + if (compareLength > 0 && + !sourceSegment.Span.Slice(sourceOffset, compareLength).SequenceEqual(targetSegment.Span.Slice(targetOffset, compareLength))) { return false; } - long comparedLength = 0; - long length = source.Length; - - int sourceOffset = 0; - int targetOffset = 0; - - var sourceEnumerator = source.GetEnumerator(); - var targetEnumerator = target.GetEnumerator(); - - ReadOnlyMemory sourceSegment = sourceEnumerator.Current; - ReadOnlyMemory targetSegment = targetEnumerator.Current; - - while (true) + comparedLength += compareLength; + if (comparedLength >= length) { - int compareLength = Math.Min(sourceSegment.Length - sourceOffset, targetSegment.Length - targetOffset); + return true; + } - if (compareLength > 0 && - !sourceSegment.Span.Slice(sourceOffset, compareLength).SequenceEqual(targetSegment.Span.Slice(targetOffset, compareLength))) + sourceOffset += compareLength; + if (sourceOffset >= sourceSegment.Length) + { + if (!sourceEnumerator.MoveNext()) { return false; } - comparedLength += compareLength; - if (comparedLength >= length) + sourceSegment = sourceEnumerator.Current; + sourceOffset = 0; + } + + targetOffset += compareLength; + if (targetOffset >= targetSegment.Length) + { + if (!targetEnumerator.MoveNext()) { - return true; + return false; } - sourceOffset += compareLength; - if (sourceOffset >= sourceSegment.Length) - { - if (!sourceEnumerator.MoveNext()) - { - return false; - } - - sourceSegment = sourceEnumerator.Current; - sourceOffset = 0; - } - - targetOffset += compareLength; - if (targetOffset >= targetSegment.Length) - { - if (!targetEnumerator.MoveNext()) - { - return false; - } - - targetSegment = targetEnumerator.Current; - targetOffset = 0; - } + targetSegment = targetEnumerator.Current; + targetOffset = 0; } } } -} +} \ No newline at end of file diff --git a/Source/MQTTnet/Internal/MqttPacketBus.cs b/Source/MQTTnet/Internal/MqttPacketBus.cs index 46e2b349..e0b0652e 100644 --- a/Source/MQTTnet/Internal/MqttPacketBus.cs +++ b/Source/MQTTnet/Internal/MqttPacketBus.cs @@ -9,156 +9,155 @@ using System.Threading; using System.Threading.Tasks; using MQTTnet.Packets; -namespace MQTTnet.Internal +namespace MQTTnet.Internal; + +public sealed class MqttPacketBus : IDisposable { - public sealed class MqttPacketBus : IDisposable + readonly LinkedList[] _partitions = + [ + [], + [], + [] + ]; + + readonly AsyncSignal _signal = new(); + readonly object _syncRoot = new(); + + int _activePartition = (int)MqttPacketBusPartition.Health; + + public int TotalItemsCount { - readonly LinkedList[] _partitions = - { - new LinkedList(), - new LinkedList(), - new LinkedList() - }; - - readonly AsyncSignal _signal = new AsyncSignal(); - readonly object _syncRoot = new object(); - - int _activePartition = (int)MqttPacketBusPartition.Health; - - public int TotalItemsCount - { - get - { - lock (_syncRoot) - { - return _partitions.Sum(p => p.Count); - } - } - } - - public void Clear() + get { lock (_syncRoot) { - foreach (var partition in _partitions) - { - partition.Clear(); - } - } - } - - public async Task DequeueItemAsync(CancellationToken cancellationToken) - { - while (!cancellationToken.IsCancellationRequested) - { - lock (_syncRoot) - { - for (var i = 0; i < 3; i++) - { - // Iterate through the partitions in order to ensure processing of health packets - // even if lots of data packets are enqueued. - - // Partition | Messages (left = oldest). - // DATA | [#]######################### - // CONTROL | [#]############# - // HEALTH | [#]#### - - // In this sample the 3 oldest messages from the partitions are processed in a row. - // Then the next 3 from all 3 partitions. - - MoveActivePartition(); - - var activePartition = _partitions[_activePartition]; - - if (activePartition.First != null) - { - var item = activePartition.First; - activePartition.RemoveFirst(); - - return item.Value; - } - } - } - - // No partition contains data so that we have to wait and put - // the worker back to the thread pool. - try - { - await _signal.WaitAsync(cancellationToken).ConfigureAwait(false); - } - catch (ObjectDisposedException) - { - // The cancelled token should "hide" the disposal of the signal. - cancellationToken.ThrowIfCancellationRequested(); - throw; - } - } - - cancellationToken.ThrowIfCancellationRequested(); - - throw new InvalidOperationException("MqttPacketBus is broken."); - } - - public void Dispose() - { - _signal.Dispose(); - } - - public MqttPacketBusItem DropFirstItem(MqttPacketBusPartition partition) - { - lock (_syncRoot) - { - var partitionInstance = _partitions[(int)partition]; - - if (partitionInstance.Count > 0) - { - var firstItem = partitionInstance.First.Value; - partitionInstance.RemoveFirst(); - - return firstItem; - } - } - - return null; - } - - public void EnqueueItem(MqttPacketBusItem item, MqttPacketBusPartition partition) - { - ArgumentNullException.ThrowIfNull(item); - - lock (_syncRoot) - { - _partitions[(int)partition].AddLast(item); - _signal.Set(); - } - } - - public List ExportPackets(MqttPacketBusPartition partition) - { - lock (_syncRoot) - { - return _partitions[(int)partition].Select(i => i.Packet).ToList(); - } - } - - public int PartitionItemsCount(MqttPacketBusPartition partition) - { - lock (_syncRoot) - { - return _partitions[(int)partition].Count; - } - } - - void MoveActivePartition() - { - if (_activePartition >= _partitions.Length - 1) - { - _activePartition = 0; - } - else - { - _activePartition++; + return _partitions.Sum(p => p.Count); } } } + + public void Clear() + { + lock (_syncRoot) + { + foreach (var partition in _partitions) + { + partition.Clear(); + } + } + } + + public async Task DequeueItemAsync(CancellationToken cancellationToken) + { + while (!cancellationToken.IsCancellationRequested) + { + lock (_syncRoot) + { + for (var i = 0; i < 3; i++) + { + // Iterate through the partitions in order to ensure processing of health packets + // even if lots of data packets are enqueued. + + // Partition | Messages (left = oldest). + // DATA | [#]######################### + // CONTROL | [#]############# + // HEALTH | [#]#### + + // In this sample the 3 oldest messages from the partitions are processed in a row. + // Then the next 3 from all 3 partitions. + + MoveActivePartition(); + + var activePartition = _partitions[_activePartition]; + + if (activePartition.First != null) + { + var item = activePartition.First; + activePartition.RemoveFirst(); + + return item.Value; + } + } + } + + // No partition contains data so that we have to wait and put + // the worker back to the thread pool. + try + { + await _signal.WaitAsync(cancellationToken).ConfigureAwait(false); + } + catch (ObjectDisposedException) + { + // The cancelled token should "hide" the disposal of the signal. + cancellationToken.ThrowIfCancellationRequested(); + throw; + } + } + + cancellationToken.ThrowIfCancellationRequested(); + + throw new InvalidOperationException("MqttPacketBus is broken."); + } + + public void Dispose() + { + _signal.Dispose(); + } + + public MqttPacketBusItem DropFirstItem(MqttPacketBusPartition partition) + { + lock (_syncRoot) + { + var partitionInstance = _partitions[(int)partition]; + + if (partitionInstance.Count > 0) + { + var firstItem = partitionInstance.First!.Value; + partitionInstance.RemoveFirst(); + + return firstItem; + } + } + + return null; + } + + public void EnqueueItem(MqttPacketBusItem item, MqttPacketBusPartition partition) + { + ArgumentNullException.ThrowIfNull(item); + + lock (_syncRoot) + { + _partitions[(int)partition].AddLast(item); + _signal.Set(); + } + } + + public List ExportPackets(MqttPacketBusPartition partition) + { + lock (_syncRoot) + { + return _partitions[(int)partition].Select(i => i.Packet).ToList(); + } + } + + public int PartitionItemsCount(MqttPacketBusPartition partition) + { + lock (_syncRoot) + { + return _partitions[(int)partition].Count; + } + } + + void MoveActivePartition() + { + if (_activePartition >= _partitions.Length - 1) + { + _activePartition = 0; + } + else + { + _activePartition++; + } + } } \ No newline at end of file diff --git a/Source/MQTTnet/Internal/MqttPacketBusItem.cs b/Source/MQTTnet/Internal/MqttPacketBusItem.cs index 6fb5b114..df02f726 100644 --- a/Source/MQTTnet/Internal/MqttPacketBusItem.cs +++ b/Source/MQTTnet/Internal/MqttPacketBusItem.cs @@ -6,40 +6,39 @@ using System; using System.Threading.Tasks; using MQTTnet.Packets; -namespace MQTTnet.Internal +namespace MQTTnet.Internal; + +public sealed class MqttPacketBusItem { - public sealed class MqttPacketBusItem + readonly AsyncTaskCompletionSource _promise = new(); + + public MqttPacketBusItem(MqttPacket packet) { - readonly AsyncTaskCompletionSource _promise = new AsyncTaskCompletionSource(); + Packet = packet ?? throw new ArgumentNullException(nameof(packet)); + } - public MqttPacketBusItem(MqttPacket packet) - { - Packet = packet ?? throw new ArgumentNullException(nameof(packet)); - } + public event EventHandler Completed; - public event EventHandler Completed; + public MqttPacket Packet { get; } - public MqttPacket Packet { get; } + public void Cancel() + { + _promise.TrySetCanceled(); + } - public void Cancel() - { - _promise.TrySetCanceled(); - } + public void Complete() + { + _promise.TrySetResult(Packet); + Completed?.Invoke(this, EventArgs.Empty); + } - public void Complete() - { - _promise.TrySetResult(Packet); - Completed?.Invoke(this, EventArgs.Empty); - } + public void Fail(Exception exception) + { + _promise.TrySetException(exception); + } - public void Fail(Exception exception) - { - _promise.TrySetException(exception); - } - - public Task WaitAsync() - { - return _promise.Task; - } + public Task WaitAsync() + { + return _promise.Task; } } \ No newline at end of file diff --git a/Source/MQTTnet/Internal/MqttPacketBusPartition.cs b/Source/MQTTnet/Internal/MqttPacketBusPartition.cs index d8700399..f7f241e2 100644 --- a/Source/MQTTnet/Internal/MqttPacketBusPartition.cs +++ b/Source/MQTTnet/Internal/MqttPacketBusPartition.cs @@ -2,14 +2,13 @@ // The .NET Foundation licenses this file to you under the MIT license. // See the LICENSE file in the project root for more information. -namespace MQTTnet.Internal +namespace MQTTnet.Internal; + +public enum MqttPacketBusPartition { - public enum MqttPacketBusPartition - { - Data, - - Control, - - Health - } + Data, + + Control, + + Health } \ No newline at end of file diff --git a/Source/MQTTnet/Internal/TaskExtensions.cs b/Source/MQTTnet/Internal/TaskExtensions.cs index 5ab14f75..753a6446 100644 --- a/Source/MQTTnet/Internal/TaskExtensions.cs +++ b/Source/MQTTnet/Internal/TaskExtensions.cs @@ -6,53 +6,52 @@ using System; using System.Threading.Tasks; using MQTTnet.Diagnostics.Logger; -namespace MQTTnet.Internal +namespace MQTTnet.Internal; + +public static class TaskExtensions { - public static class TaskExtensions + public static void RunInBackground(this Task task, MqttNetSourceLogger logger = null) { - public static void RunInBackground(this Task task, MqttNetSourceLogger logger = null) + task?.ContinueWith( + t => + { + // Consume the exception first so that we get no exception regarding the not observed exception. + var exception = t.Exception; + logger?.Error(exception, "Unhandled exception in background task."); + }, + TaskContinuationOptions.OnlyOnFaulted); + } + + public static async Task WaitAsync(this Task task, Task sender, MqttNetSourceLogger logger) + { + ArgumentNullException.ThrowIfNull(logger); + + if (task == null) { - task?.ContinueWith( - t => - { - // Consume the exception first so that we get no exception regarding the not observed exception. - var exception = t.Exception; - logger?.Error(exception, "Unhandled exception in background task."); - }, - TaskContinuationOptions.OnlyOnFaulted); + return; } - public static async Task WaitAsync(this Task task, Task sender, MqttNetSourceLogger logger) + if (task == sender) { - ArgumentNullException.ThrowIfNull(logger); - - if (task == null) + // Return here to avoid deadlocks, but first any eventual exception in the task + // must be handled to avoid not getting an unhandled task exception + if (!task.IsFaulted) { return; } - if (task == sender) - { - // Return here to avoid deadlocks, but first any eventual exception in the task - // must be handled to avoid not getting an unhandled task exception - if (!task.IsFaulted) - { - return; - } + // By accessing the Exception property the exception is considered handled and will + // not result in an unhandled task exception later by the finalizer + logger.Warning(task.Exception, "Error while waiting for background task."); + return; + } - // By accessing the Exception property the exception is considered handled and will - // not result in an unhandled task exception later by the finalizer - logger.Warning(task.Exception, "Error while waiting for background task."); - return; - } - - try - { - await task.ConfigureAwait(false); - } - catch (OperationCanceledException) - { - } + try + { + await task.ConfigureAwait(false); + } + catch (OperationCanceledException) + { } } } \ No newline at end of file diff --git a/Source/MQTTnet/LowLevelClient/ILowLevelMqttClient.cs b/Source/MQTTnet/LowLevelClient/ILowLevelMqttClient.cs index e6d7620b..bdd3687f 100644 --- a/Source/MQTTnet/LowLevelClient/ILowLevelMqttClient.cs +++ b/Source/MQTTnet/LowLevelClient/ILowLevelMqttClient.cs @@ -4,20 +4,19 @@ using System.Threading.Tasks; using MQTTnet.Diagnostics.PacketInspection; using MQTTnet.Packets; -namespace MQTTnet.LowLevelClient +namespace MQTTnet.LowLevelClient; + +public interface ILowLevelMqttClient : IDisposable { - public interface ILowLevelMqttClient : IDisposable - { - event Func InspectPacketAsync; + event Func InspectPacketAsync; - bool IsConnected { get; } + bool IsConnected { get; } - Task ConnectAsync(MqttClientOptions options, CancellationToken cancellationToken = default); + Task ConnectAsync(MqttClientOptions options, CancellationToken cancellationToken = default); - Task DisconnectAsync(CancellationToken cancellationToken = default); + Task DisconnectAsync(CancellationToken cancellationToken = default); - Task ReceiveAsync(CancellationToken cancellationToken = default); + Task ReceiveAsync(CancellationToken cancellationToken = default); - Task SendAsync(MqttPacket packet, CancellationToken cancellationToken = default); - } + Task SendAsync(MqttPacket packet, CancellationToken cancellationToken = default); } \ No newline at end of file diff --git a/Source/MQTTnet/LowLevelClient/LowLevelMqttClient.cs b/Source/MQTTnet/LowLevelClient/LowLevelMqttClient.cs index 186e8bfd..eb839d8a 100644 --- a/Source/MQTTnet/LowLevelClient/LowLevelMqttClient.cs +++ b/Source/MQTTnet/LowLevelClient/LowLevelMqttClient.cs @@ -12,135 +12,134 @@ using MQTTnet.Exceptions; using MQTTnet.Internal; using MQTTnet.Packets; -namespace MQTTnet.LowLevelClient +namespace MQTTnet.LowLevelClient; + +public sealed class LowLevelMqttClient : ILowLevelMqttClient { - public sealed class LowLevelMqttClient : ILowLevelMqttClient + readonly IMqttClientAdapterFactory _clientAdapterFactory; + readonly AsyncEvent _inspectPacketEvent = new(); + readonly MqttNetSourceLogger _logger; + readonly IMqttNetLogger _rootLogger; + + IMqttChannelAdapter _adapter; + + public LowLevelMqttClient(IMqttClientAdapterFactory clientAdapterFactory, IMqttNetLogger logger) { - readonly IMqttClientAdapterFactory _clientAdapterFactory; - readonly AsyncEvent _inspectPacketEvent = new AsyncEvent(); - readonly MqttNetSourceLogger _logger; - readonly IMqttNetLogger _rootLogger; + _clientAdapterFactory = clientAdapterFactory ?? throw new ArgumentNullException(nameof(clientAdapterFactory)); - IMqttChannelAdapter _adapter; + _rootLogger = logger ?? throw new ArgumentNullException(nameof(logger)); + _logger = logger.WithSource(nameof(LowLevelMqttClient)); + } - public LowLevelMqttClient(IMqttClientAdapterFactory clientAdapterFactory, IMqttNetLogger logger) + public event Func InspectPacketAsync + { + add => _inspectPacketEvent.AddHandler(value); + remove => _inspectPacketEvent.RemoveHandler(value); + } + + public bool IsConnected => _adapter != null; + + public async Task ConnectAsync(MqttClientOptions options, CancellationToken cancellationToken) + { + ArgumentNullException.ThrowIfNull(options); + + if (_adapter != null) { - _clientAdapterFactory = clientAdapterFactory ?? throw new ArgumentNullException(nameof(clientAdapterFactory)); - - _rootLogger = logger ?? throw new ArgumentNullException(nameof(logger)); - _logger = logger.WithSource(nameof(LowLevelMqttClient)); + throw new InvalidOperationException("Low level MQTT client is already connected. Disconnect first before connecting again."); } - public event Func InspectPacketAsync + MqttPacketInspector packetInspector = null; + if (_inspectPacketEvent.HasHandlers) { - add => _inspectPacketEvent.AddHandler(value); - remove => _inspectPacketEvent.RemoveHandler(value); + packetInspector = new MqttPacketInspector(_inspectPacketEvent, _rootLogger); } - public bool IsConnected => _adapter != null; + var newAdapter = _clientAdapterFactory.CreateClientAdapter(options, packetInspector, _rootLogger); - public async Task ConnectAsync(MqttClientOptions options, CancellationToken cancellationToken) + try { - ArgumentNullException.ThrowIfNull(options); - - if (_adapter != null) - { - throw new InvalidOperationException("Low level MQTT client is already connected. Disconnect first before connecting again."); - } - - MqttPacketInspector packetInspector = null; - if (_inspectPacketEvent.HasHandlers) - { - packetInspector = new MqttPacketInspector(_inspectPacketEvent, _rootLogger); - } - - var newAdapter = _clientAdapterFactory.CreateClientAdapter(options, packetInspector, _rootLogger); - - try - { - _logger.Verbose("Trying to connect with server '{0}'", options.ChannelOptions); - await newAdapter.ConnectAsync(cancellationToken).ConfigureAwait(false); - _logger.Verbose("Connection with server established"); - } - catch (Exception) - { - _adapter?.Dispose(); - throw; - } - - _adapter = newAdapter; + _logger.Verbose("Trying to connect with server '{0}'", options.ChannelOptions); + await newAdapter.ConnectAsync(cancellationToken).ConfigureAwait(false); + _logger.Verbose("Connection with server established"); } - - public async Task DisconnectAsync(CancellationToken cancellationToken) - { - var adapter = _adapter; - if (adapter == null) - { - throw new InvalidOperationException("Low level MQTT client is not connected."); - } - - try - { - await adapter.DisconnectAsync(cancellationToken).ConfigureAwait(false); - } - catch - { - Dispose(); - throw; - } - } - - public void Dispose() + catch { _adapter?.Dispose(); - _adapter = null; + throw; } - public async Task ReceiveAsync(CancellationToken cancellationToken) + _adapter = newAdapter; + } + + public async Task DisconnectAsync(CancellationToken cancellationToken) + { + var adapter = _adapter; + if (adapter == null) { - var adapter = _adapter; - if (adapter == null) - { - throw new InvalidOperationException("Low level MQTT client is not connected."); - } - - try - { - var receivedPacket = await adapter.ReceivePacketAsync(cancellationToken).ConfigureAwait(false); - if (receivedPacket == null) - { - // Graceful socket close. - throw new MqttCommunicationException("The connection is closed."); - } - - return receivedPacket; - } - catch - { - Dispose(); - throw; - } + throw new InvalidOperationException("Low level MQTT client is not connected."); } - public async Task SendAsync(MqttPacket packet, CancellationToken cancellationToken) + try { - ArgumentNullException.ThrowIfNull(packet); + await adapter.DisconnectAsync(cancellationToken).ConfigureAwait(false); + } + catch + { + Dispose(); + throw; + } + } - var adapter = _adapter; - if (adapter == null) + public void Dispose() + { + _adapter?.Dispose(); + _adapter = null; + } + + public async Task ReceiveAsync(CancellationToken cancellationToken) + { + var adapter = _adapter; + if (adapter == null) + { + throw new InvalidOperationException("Low level MQTT client is not connected."); + } + + try + { + var receivedPacket = await adapter.ReceivePacketAsync(cancellationToken).ConfigureAwait(false); + if (receivedPacket == null) { - throw new InvalidOperationException("Low level MQTT client is not connected."); + // Graceful socket close. + throw new MqttCommunicationException("The connection is closed."); } - try - { - await adapter.SendPacketAsync(packet, cancellationToken).ConfigureAwait(false); - } - catch - { - Dispose(); - throw; - } + return receivedPacket; + } + catch + { + Dispose(); + throw; + } + } + + public async Task SendAsync(MqttPacket packet, CancellationToken cancellationToken) + { + ArgumentNullException.ThrowIfNull(packet); + + var adapter = _adapter; + if (adapter == null) + { + throw new InvalidOperationException("Low level MQTT client is not connected."); + } + + try + { + await adapter.SendPacketAsync(packet, cancellationToken).ConfigureAwait(false); + } + catch + { + Dispose(); + throw; } } } \ No newline at end of file diff --git a/Source/MQTTnet/MQTTnet.csproj b/Source/MQTTnet/MQTTnet.csproj index 1b1f3987..380beef9 100644 --- a/Source/MQTTnet/MQTTnet.csproj +++ b/Source/MQTTnet/MQTTnet.csproj @@ -31,7 +31,7 @@ git MQTT Message Queue Telemetry Transport MQTTClient MQTTServer Server MQTTBroker Broker NETStandard IoT InternetOfThings Messaging Hardware Arduino Sensor Actuator M2M ESP Smart Home Cities Automation Xamarin Blazor en-US - false + true false nuget.png true diff --git a/Source/MQTTnet/MqttApplicationMessage.cs b/Source/MQTTnet/MqttApplicationMessage.cs index eb402e15..ca7264f9 100644 --- a/Source/MQTTnet/MqttApplicationMessage.cs +++ b/Source/MQTTnet/MqttApplicationMessage.cs @@ -9,138 +9,137 @@ using System; using System.Buffers; using System.Collections.Generic; -namespace MQTTnet +namespace MQTTnet; + +public sealed class MqttApplicationMessage { - public sealed class MqttApplicationMessage + /// + /// Gets or sets the content type. + /// The content type must be a UTF-8 encoded string. The content type value identifies the kind of UTF-8 encoded + /// payload. + /// + public string ContentType { get; set; } + + /// + /// Gets or sets the correlation data. + /// In order for the sender to know what sent message the response refers to it can also send correlation data with the + /// published message. + /// Hint: MQTT 5 feature only. + /// + public byte[] CorrelationData { get; set; } + + /// + /// If the DUP flag is set to 0, it indicates that this is the first occasion that the Client or Server has attempted + /// to send this MQTT PUBLISH Packet. + /// If the DUP flag is set to 1, it indicates that this might be re-delivery of an earlier attempt to send the Packet. + /// The DUP flag MUST be set to 1 by the Client or Server when it attempts to re-deliver a PUBLISH Packet + /// [MQTT-3.3.1.-1]. + /// The DUP flag MUST be set to 0 for all QoS 0 messages [MQTT-3.3.1-2]. + /// + public bool Dup { get; set; } + + /// + /// Gets or sets the message expiry interval. + /// A client can set the message expiry interval in seconds for each PUBLISH message individually. + /// This interval defines the period of time that the broker stores the PUBLISH message for any matching subscribers + /// that are not currently connected. + /// When no message expiry interval is set, the broker must store the message for matching subscribers indefinitely. + /// When the retained=true option is set on the PUBLISH message, this interval also defines how long a message is + /// retained on a topic. + /// Hint: MQTT 5 feature only. + /// + public uint MessageExpiryInterval { get; set; } + + /// + /// Set an ArraySegment as Payload. + /// + public ArraySegment PayloadSegment { - /// - /// Gets or sets the content type. - /// The content type must be a UTF-8 encoded string. The content type value identifies the kind of UTF-8 encoded - /// payload. - /// - public string ContentType { get; set; } - - /// - /// Gets or sets the correlation data. - /// In order for the sender to know what sent message the response refers to it can also send correlation data with the - /// published message. - /// Hint: MQTT 5 feature only. - /// - public byte[] CorrelationData { get; set; } - - /// - /// If the DUP flag is set to 0, it indicates that this is the first occasion that the Client or Server has attempted - /// to send this MQTT PUBLISH Packet. - /// If the DUP flag is set to 1, it indicates that this might be re-delivery of an earlier attempt to send the Packet. - /// The DUP flag MUST be set to 1 by the Client or Server when it attempts to re-deliver a PUBLISH Packet - /// [MQTT-3.3.1.-1]. - /// The DUP flag MUST be set to 0 for all QoS 0 messages [MQTT-3.3.1-2]. - /// - public bool Dup { get; set; } - - /// - /// Gets or sets the message expiry interval. - /// A client can set the message expiry interval in seconds for each PUBLISH message individually. - /// This interval defines the period of time that the broker stores the PUBLISH message for any matching subscribers - /// that are not currently connected. - /// When no message expiry interval is set, the broker must store the message for matching subscribers indefinitely. - /// When the retained=true option is set on the PUBLISH message, this interval also defines how long a message is - /// retained on a topic. - /// Hint: MQTT 5 feature only. - /// - public uint MessageExpiryInterval { get; set; } - - /// - /// Set an ArraySegment as Payload. - /// - public ArraySegment PayloadSegment - { - set { Payload = new ReadOnlySequence(value); } - } - - /// - /// Get or set ReadOnlySequence style of Payload. - /// This payload type is used internally and is recommended for public use. - /// It can be used in combination with a RecyclableMemoryStream to publish - /// large buffered messages without allocating large chunks of memory. - /// - public ReadOnlySequence Payload { get; set; } = EmptyBuffer.ReadOnlySequence; - - /// - /// Gets or sets the payload format indicator. - /// The payload format indicator is part of any MQTT packet that can contain a payload. The indicator is an optional - /// byte value. - /// A value of 0 indicates an “unspecified byte stream”. - /// A value of 1 indicates a "UTF-8 encoded payload". - /// If no payload format indicator is provided, the default value is 0. - /// Hint: MQTT 5 feature only. - /// - public MqttPayloadFormatIndicator PayloadFormatIndicator { get; set; } = MqttPayloadFormatIndicator.Unspecified; - - /// - /// Gets or sets the quality of service level. - /// The Quality of Service (QoS) level is an agreement between the sender of a message and the receiver of a message - /// that defines the guarantee of delivery for a specific message. - /// There are 3 QoS levels in MQTT: - /// - At most once (0): Message gets delivered no time, once or multiple times. - /// - At least once (1): Message gets delivered at least once (one time or more often). - /// - Exactly once (2): Message gets delivered exactly once (It's ensured that the message only comes once). - /// - public MqttQualityOfServiceLevel QualityOfServiceLevel { get; set; } - - /// - /// Gets or sets the response topic. - /// In MQTT 5 the ability to publish a response topic was added in the publish message which allows you to implement - /// the request/response pattern between clients that is common in web applications. - /// Hint: MQTT 5 feature only. - /// - public string ResponseTopic { get; set; } - - /// - /// Gets or sets a value indicating whether the message should be retained or not. - /// A retained message is a normal MQTT message with the retained flag set to true. - /// The broker stores the last retained message and the corresponding QoS for that topic. - /// - public bool Retain { get; set; } - - /// - /// Gets or sets the subscription identifiers. - /// The client can specify a subscription identifier when subscribing. - /// The broker will establish and store the mapping relationship between this subscription and subscription identifier - /// when successfully create or modify subscription. - /// The broker will return the subscription identifier associated with this PUBLISH packet and the PUBLISH packet to - /// the client when need to forward PUBLISH packets matching this subscription to this client. - /// Hint: MQTT 5 feature only. - /// - public List SubscriptionIdentifiers { get; set; } - - /// - /// Gets or sets the MQTT topic. - /// In MQTT, the word topic refers to an UTF-8 string that the broker uses to filter messages for each connected - /// client. - /// The topic consists of one or more topic levels. Each topic level is separated by a forward slash (topic level - /// separator). - /// - public string Topic { get; set; } - - /// - /// Gets or sets the topic alias. - /// Topic aliases were introduced are a mechanism for reducing the size of published packets by reducing the size of - /// the topic field. - /// A value of 0 indicates no topic alias is used. - /// Hint: MQTT 5 feature only. - /// - public ushort TopicAlias { get; set; } - - /// - /// Gets or sets the user properties. - /// In MQTT 5, user properties are basic UTF-8 string key-value pairs that you can append to almost every type of MQTT - /// packet. - /// As long as you don’t exceed the maximum message size, you can use an unlimited number of user properties to add - /// metadata to MQTT messages and pass information between publisher, broker, and subscriber. - /// The feature is very similar to the HTTP header concept. - /// Hint: MQTT 5 feature only. - /// - public List UserProperties { get; set; } + set => Payload = new ReadOnlySequence(value); } + + /// + /// Get or set ReadOnlySequence style of Payload. + /// This payload type is used internally and is recommended for public use. + /// It can be used in combination with a RecyclableMemoryStream to publish + /// large buffered messages without allocating large chunks of memory. + /// + public ReadOnlySequence Payload { get; set; } = EmptyBuffer.ReadOnlySequence; + + /// + /// Gets or sets the payload format indicator. + /// The payload format indicator is part of any MQTT packet that can contain a payload. The indicator is an optional + /// byte value. + /// A value of 0 indicates an “unspecified byte stream”. + /// A value of 1 indicates a "UTF-8 encoded payload". + /// If no payload format indicator is provided, the default value is 0. + /// Hint: MQTT 5 feature only. + /// + public MqttPayloadFormatIndicator PayloadFormatIndicator { get; set; } = MqttPayloadFormatIndicator.Unspecified; + + /// + /// Gets or sets the quality of service level. + /// The Quality of Service (QoS) level is an agreement between the sender of a message and the receiver of a message + /// that defines the guarantee of delivery for a specific message. + /// There are 3 QoS levels in MQTT: + /// - At most once (0): Message gets delivered no time, once or multiple times. + /// - At least once (1): Message gets delivered at least once (one time or more often). + /// - Exactly once (2): Message gets delivered exactly once (It's ensured that the message only comes once). + /// + public MqttQualityOfServiceLevel QualityOfServiceLevel { get; set; } + + /// + /// Gets or sets the response topic. + /// In MQTT 5 the ability to publish a response topic was added in the publish message which allows you to implement + /// the request/response pattern between clients that is common in web applications. + /// Hint: MQTT 5 feature only. + /// + public string ResponseTopic { get; set; } + + /// + /// Gets or sets a value indicating whether the message should be retained or not. + /// A retained message is a normal MQTT message with the retained flag set to true. + /// The broker stores the last retained message and the corresponding QoS for that topic. + /// + public bool Retain { get; set; } + + /// + /// Gets or sets the subscription identifiers. + /// The client can specify a subscription identifier when subscribing. + /// The broker will establish and store the mapping relationship between this subscription and subscription identifier + /// when successfully create or modify subscription. + /// The broker will return the subscription identifier associated with this PUBLISH packet and the PUBLISH packet to + /// the client when need to forward PUBLISH packets matching this subscription to this client. + /// Hint: MQTT 5 feature only. + /// + public List SubscriptionIdentifiers { get; set; } + + /// + /// Gets or sets the MQTT topic. + /// In MQTT, the word topic refers to an UTF-8 string that the broker uses to filter messages for each connected + /// client. + /// The topic consists of one or more topic levels. Each topic level is separated by a forward slash (topic level + /// separator). + /// + public string Topic { get; set; } + + /// + /// Gets or sets the topic alias. + /// Topic aliases were introduced are a mechanism for reducing the size of published packets by reducing the size of + /// the topic field. + /// A value of 0 indicates no topic alias is used. + /// Hint: MQTT 5 feature only. + /// + public ushort TopicAlias { get; set; } + + /// + /// Gets or sets the user properties. + /// In MQTT 5, user properties are basic UTF-8 string key-value pairs that you can append to almost every type of MQTT + /// packet. + /// As long as you don’t exceed the maximum message size, you can use an unlimited number of user properties to add + /// metadata to MQTT messages and pass information between publisher, broker, and subscriber. + /// The feature is very similar to the HTTP header concept. + /// Hint: MQTT 5 feature only. + /// + public List UserProperties { get; set; } } \ No newline at end of file diff --git a/Source/MQTTnet/MqttApplicationMessageBuilder.cs b/Source/MQTTnet/MqttApplicationMessageBuilder.cs index 2c5c2dd7..08a2a4a7 100644 --- a/Source/MQTTnet/MqttApplicationMessageBuilder.cs +++ b/Source/MQTTnet/MqttApplicationMessageBuilder.cs @@ -14,265 +14,260 @@ using MQTTnet.Internal; using MQTTnet.Packets; using MQTTnet.Protocol; -namespace MQTTnet +namespace MQTTnet; + +public sealed class MqttApplicationMessageBuilder { - public sealed class MqttApplicationMessageBuilder + string _contentType; + byte[] _correlationData; + uint _messageExpiryInterval; + + MqttPayloadFormatIndicator _payloadFormatIndicator; + ReadOnlySequence _payload; + MqttQualityOfServiceLevel _qualityOfServiceLevel = MqttQualityOfServiceLevel.AtMostOnce; + string _responseTopic; + bool _retain; + List _subscriptionIdentifiers; + string _topic; + ushort _topicAlias; + List _userProperties; + + public MqttApplicationMessage Build() { - string _contentType; - byte[] _correlationData; - uint _messageExpiryInterval; - - MqttPayloadFormatIndicator _payloadFormatIndicator; - ReadOnlySequence _payload; - MqttQualityOfServiceLevel _qualityOfServiceLevel = MqttQualityOfServiceLevel.AtMostOnce; - string _responseTopic; - bool _retain; - List _subscriptionIdentifiers; - string _topic; - ushort _topicAlias; - List _userProperties; - - public MqttApplicationMessage Build() + if (_topicAlias == 0 && string.IsNullOrEmpty(_topic)) { - if (_topicAlias == 0 && string.IsNullOrEmpty(_topic)) + throw new MqttProtocolViolationException("Topic or TopicAlias is not set."); + } + + var applicationMessage = new MqttApplicationMessage + { + Topic = _topic, + Payload = _payload, + QualityOfServiceLevel = _qualityOfServiceLevel, + Retain = _retain, + ContentType = _contentType, + ResponseTopic = _responseTopic, + CorrelationData = _correlationData, + TopicAlias = _topicAlias, + SubscriptionIdentifiers = _subscriptionIdentifiers, + MessageExpiryInterval = _messageExpiryInterval, + PayloadFormatIndicator = _payloadFormatIndicator, + UserProperties = _userProperties + }; + + return applicationMessage; + } + + /// + /// Adds the content type to the message. + /// MQTT 5.0.0+ feature. + /// + public MqttApplicationMessageBuilder WithContentType(string contentType) + { + _contentType = contentType; + return this; + } + + /// + /// Adds the correlation data to the message. + /// MQTT 5.0.0+ feature. + /// + public MqttApplicationMessageBuilder WithCorrelationData(byte[] correlationData) + { + _correlationData = correlationData; + return this; + } + + /// + /// Adds the message expiry interval in seconds to the message. + /// MQTT 5.0.0+ feature. + /// + public MqttApplicationMessageBuilder WithMessageExpiryInterval(uint messageExpiryInterval) + { + _messageExpiryInterval = messageExpiryInterval; + return this; + } + + public MqttApplicationMessageBuilder WithPayload(byte[] payload) + { + _payload = payload == null || payload.Length == 0 ? EmptyBuffer.ReadOnlySequence : new ReadOnlySequence(payload); + return this; + } + + public MqttApplicationMessageBuilder WithPayload(ArraySegment payloadSegment) + { + _payload = new ReadOnlySequence(payloadSegment); + return this; + } + + public MqttApplicationMessageBuilder WithPayload(IEnumerable payload) + { + if (payload == null) + { + return WithPayload(default(byte[])); + } + + if (payload is byte[] byteArray) + { + return WithPayload(byteArray); + } + + if (payload is ArraySegment arraySegment) + { + return WithPayloadSegment(arraySegment); + } + + return WithPayload(payload.ToArray()); + } + + public MqttApplicationMessageBuilder WithPayload(Stream payload) + { + return payload == null ? WithPayload(default(byte[])) : WithPayload(payload, payload.Length - payload.Position); + } + + public MqttApplicationMessageBuilder WithPayload(Stream payload, long length) + { + if (payload == null || length == 0) + { + return WithPayload(default(byte[])); + } + + var payloadBuffer = new byte[length]; + var totalRead = 0; + do + { + var bytesRead = payload.Read(payloadBuffer, totalRead, payloadBuffer.Length - totalRead); + if (bytesRead == 0) { - throw new MqttProtocolViolationException("Topic or TopicAlias is not set."); + break; } - var applicationMessage = new MqttApplicationMessage - { - Topic = _topic, - Payload = _payload, - QualityOfServiceLevel = _qualityOfServiceLevel, - Retain = _retain, - ContentType = _contentType, - ResponseTopic = _responseTopic, - CorrelationData = _correlationData, - TopicAlias = _topicAlias, - SubscriptionIdentifiers = _subscriptionIdentifiers, - MessageExpiryInterval = _messageExpiryInterval, - PayloadFormatIndicator = _payloadFormatIndicator, - UserProperties = _userProperties - }; + totalRead += bytesRead; + } while (totalRead < length); - return applicationMessage; - } + return WithPayload(payloadBuffer); + } - /// - /// Adds the content type to the message. - /// MQTT 5.0.0+ feature. - /// - public MqttApplicationMessageBuilder WithContentType(string contentType) + public MqttApplicationMessageBuilder WithPayload(string payload) + { + if (string.IsNullOrEmpty(payload)) { - _contentType = contentType; - return this; + return WithPayload(default(byte[])); } - /// - /// Adds the correlation data to the message. - /// MQTT 5.0.0+ feature. - /// - public MqttApplicationMessageBuilder WithCorrelationData(byte[] correlationData) + var payloadBuffer = Encoding.UTF8.GetBytes(payload); + return WithPayload(payloadBuffer); + } + + public MqttApplicationMessageBuilder WithPayload(ReadOnlySequence payload) + { + _payload = payload; + return this; + } + + /// + /// Adds the payload format indicator to the message. + /// MQTT 5.0.0+ feature. + /// + public MqttApplicationMessageBuilder WithPayloadFormatIndicator(MqttPayloadFormatIndicator payloadFormatIndicator) + { + _payloadFormatIndicator = payloadFormatIndicator; + return this; + } + + public MqttApplicationMessageBuilder WithPayloadSegment(ArraySegment payloadSegment) + { + _payload = new ReadOnlySequence(payloadSegment); + return this; + } + + public MqttApplicationMessageBuilder WithPayloadSegment(ReadOnlyMemory payloadSegment) + { + return MemoryMarshal.TryGetArray(payloadSegment, out var segment) ? WithPayloadSegment(segment) : WithPayload(payloadSegment.ToArray()); + } + + /// + /// The quality of service level. + /// The Quality of Service (QoS) level is an agreement between the sender of a message and the receiver of a message + /// that defines the guarantee of delivery for a specific message. + /// There are 3 QoS levels in MQTT: + /// - At most once (0): Message gets delivered no time, once or multiple times. + /// - At least once (1): Message gets delivered at least once (one time or more often). + /// - Exactly once (2): Message gets delivered exactly once (It's ensured that the message only comes once). + /// + public MqttApplicationMessageBuilder WithQualityOfServiceLevel(MqttQualityOfServiceLevel qualityOfServiceLevel) + { + _qualityOfServiceLevel = qualityOfServiceLevel; + return this; + } + + /// + /// Adds the response topic to the message. + /// MQTT 5.0.0+ feature. + /// + public MqttApplicationMessageBuilder WithResponseTopic(string responseTopic) + { + _responseTopic = responseTopic; + return this; + } + + /// + /// A value indicating whether the message should be retained or not. + /// A retained message is a normal MQTT message with the retained flag set to true. + /// The broker stores the last retained message and the corresponding QoS for that topic. + /// + public MqttApplicationMessageBuilder WithRetainFlag(bool value = true) + { + _retain = value; + return this; + } + + /// + /// Adds the subscription identifier to the message. + /// MQTT 5.0.0+ feature. + /// + public MqttApplicationMessageBuilder WithSubscriptionIdentifier(uint subscriptionIdentifier) + { + if (_subscriptionIdentifiers == null) { - _correlationData = correlationData; - return this; + _subscriptionIdentifiers = new List(); } - /// - /// Adds the message expiry interval in seconds to the message. - /// MQTT 5.0.0+ feature. - /// - public MqttApplicationMessageBuilder WithMessageExpiryInterval(uint messageExpiryInterval) - { - _messageExpiryInterval = messageExpiryInterval; - return this; - } + _subscriptionIdentifiers.Add(subscriptionIdentifier); + return this; + } - public MqttApplicationMessageBuilder WithPayload(byte[] payload) - { - _payload = payload == null || payload.Length == 0 ? EmptyBuffer.ReadOnlySequence : new ReadOnlySequence(payload); - return this; - } + /// + /// The MQTT topic. + /// In MQTT, the word topic refers to an UTF-8 string that the broker uses to filter messages for each connected + /// client. + /// The topic consists of one or more topic levels. Each topic level is separated by a forward slash (topic level + /// separator). + /// + public MqttApplicationMessageBuilder WithTopic(string topic) + { + _topic = topic; + return this; + } - public MqttApplicationMessageBuilder WithPayload(ArraySegment payloadSegment) - { - _payload = new ReadOnlySequence(payloadSegment); - return this; - } + /// + /// Adds the topic alias to the message. + /// MQTT 5.0.0+ feature. + /// + public MqttApplicationMessageBuilder WithTopicAlias(ushort topicAlias) + { + _topicAlias = topicAlias; + return this; + } - public MqttApplicationMessageBuilder WithPayload(IEnumerable payload) - { - if (payload == null) - { - return WithPayload(default(byte[])); - } - - if (payload is byte[] byteArray) - { - return WithPayload(byteArray); - } - - if (payload is ArraySegment arraySegment) - { - return WithPayloadSegment(arraySegment); - } - - return WithPayload(payload.ToArray()); - } - - public MqttApplicationMessageBuilder WithPayload(Stream payload) - { - return payload == null ? WithPayload(default(byte[])) : WithPayload(payload, payload.Length - payload.Position); - } - - public MqttApplicationMessageBuilder WithPayload(Stream payload, long length) - { - if (payload == null || length == 0) - { - return WithPayload(default(byte[])); - } - - var payloadBuffer = new byte[length]; - var totalRead = 0; - do - { - var bytesRead = payload.Read(payloadBuffer, totalRead, payloadBuffer.Length - totalRead); - if (bytesRead == 0) - { - break; - } - - totalRead += bytesRead; - } while (totalRead < length); - - return WithPayload(payloadBuffer); - } - - public MqttApplicationMessageBuilder WithPayload(string payload) - { - if (string.IsNullOrEmpty(payload)) - { - return WithPayload(default(byte[])); - } - - var payloadBuffer = Encoding.UTF8.GetBytes(payload); - return WithPayload(payloadBuffer); - } - - public MqttApplicationMessageBuilder WithPayload(ReadOnlySequence payload) - { - _payload = payload; - return this; - } - - /// - /// Adds the payload format indicator to the message. - /// MQTT 5.0.0+ feature. - /// - public MqttApplicationMessageBuilder WithPayloadFormatIndicator(MqttPayloadFormatIndicator payloadFormatIndicator) - { - _payloadFormatIndicator = payloadFormatIndicator; - return this; - } - - public MqttApplicationMessageBuilder WithPayloadSegment(ArraySegment payloadSegment) - { - _payload = new ReadOnlySequence(payloadSegment); - return this; - } - - public MqttApplicationMessageBuilder WithPayloadSegment(ReadOnlyMemory payloadSegment) - { - return MemoryMarshal.TryGetArray(payloadSegment, out var segment) ? WithPayloadSegment(segment) : WithPayload(payloadSegment.ToArray()); - } - - /// - /// The quality of service level. - /// The Quality of Service (QoS) level is an agreement between the sender of a message and the receiver of a message - /// that defines the guarantee of delivery for a specific message. - /// There are 3 QoS levels in MQTT: - /// - At most once (0): Message gets delivered no time, once or multiple times. - /// - At least once (1): Message gets delivered at least once (one time or more often). - /// - Exactly once (2): Message gets delivered exactly once (It's ensured that the message only comes once). - /// - public MqttApplicationMessageBuilder WithQualityOfServiceLevel(MqttQualityOfServiceLevel qualityOfServiceLevel) - { - _qualityOfServiceLevel = qualityOfServiceLevel; - return this; - } - - /// - /// Adds the response topic to the message. - /// MQTT 5.0.0+ feature. - /// - public MqttApplicationMessageBuilder WithResponseTopic(string responseTopic) - { - _responseTopic = responseTopic; - return this; - } - - /// - /// A value indicating whether the message should be retained or not. - /// A retained message is a normal MQTT message with the retained flag set to true. - /// The broker stores the last retained message and the corresponding QoS for that topic. - /// - public MqttApplicationMessageBuilder WithRetainFlag(bool value = true) - { - _retain = value; - return this; - } - - /// - /// Adds the subscription identifier to the message. - /// MQTT 5.0.0+ feature. - /// - public MqttApplicationMessageBuilder WithSubscriptionIdentifier(uint subscriptionIdentifier) - { - if (_subscriptionIdentifiers == null) - { - _subscriptionIdentifiers = new List(); - } - - _subscriptionIdentifiers.Add(subscriptionIdentifier); - return this; - } - - /// - /// The MQTT topic. - /// In MQTT, the word topic refers to an UTF-8 string that the broker uses to filter messages for each connected - /// client. - /// The topic consists of one or more topic levels. Each topic level is separated by a forward slash (topic level - /// separator). - /// - public MqttApplicationMessageBuilder WithTopic(string topic) - { - _topic = topic; - return this; - } - - /// - /// Adds the topic alias to the message. - /// MQTT 5.0.0+ feature. - /// - public MqttApplicationMessageBuilder WithTopicAlias(ushort topicAlias) - { - _topicAlias = topicAlias; - return this; - } - - /// - /// Adds the user property to the message. - /// MQTT 5.0.0+ feature. - /// - public MqttApplicationMessageBuilder WithUserProperty(string name, string value) - { - if (_userProperties == null) - { - _userProperties = new List(); - } - - _userProperties.Add(new MqttUserProperty(name, value)); - return this; - } + /// + /// Adds the user property to the message. + /// MQTT 5.0.0+ feature. + /// + public MqttApplicationMessageBuilder WithUserProperty(string name, string value) + { + _userProperties ??= []; + _userProperties.Add(new MqttUserProperty(name, value)); + return this; } } \ No newline at end of file diff --git a/Source/MQTTnet/MqttApplicationMessageValidator.cs b/Source/MQTTnet/MqttApplicationMessageValidator.cs index d7eb3b61..e1ed2ffe 100644 --- a/Source/MQTTnet/MqttApplicationMessageValidator.cs +++ b/Source/MQTTnet/MqttApplicationMessageValidator.cs @@ -3,63 +3,61 @@ // See the LICENSE file in the project root for more information. using System; -using System.Linq; using MQTTnet.Formatter; using MQTTnet.Protocol; -namespace MQTTnet +namespace MQTTnet; + +public static class MqttApplicationMessageValidator { - public static class MqttApplicationMessageValidator + public static void ThrowIfNotSupported(MqttApplicationMessage applicationMessage, MqttProtocolVersion protocolVersion) { - public static void ThrowIfNotSupported(MqttApplicationMessage applicationMessage, MqttProtocolVersion protocolVersion) + ArgumentNullException.ThrowIfNull(applicationMessage); + + if (protocolVersion == MqttProtocolVersion.V500) { - ArgumentNullException.ThrowIfNull(applicationMessage); - - if (protocolVersion == MqttProtocolVersion.V500) - { - // Everything is supported. - return; - } - - if (applicationMessage.ContentType?.Any() == true) - { - Throw(nameof(applicationMessage.ContentType)); - } - - if (applicationMessage.UserProperties?.Any() == true) - { - Throw(nameof(applicationMessage.UserProperties)); - } - - if (applicationMessage.CorrelationData?.Any() == true) - { - Throw(nameof(applicationMessage.CorrelationData)); - } - - if (applicationMessage.ResponseTopic?.Any() == true) - { - Throw(nameof(applicationMessage.ResponseTopic)); - } - - if (applicationMessage.SubscriptionIdentifiers?.Any() == true) - { - Throw(nameof(applicationMessage.SubscriptionIdentifiers)); - } - - if (applicationMessage.TopicAlias > 0) - { - Throw(nameof(applicationMessage.TopicAlias)); - } - - if (applicationMessage.PayloadFormatIndicator != MqttPayloadFormatIndicator.Unspecified) - { - Throw(nameof(applicationMessage.PayloadFormatIndicator)); - } + // Everything is supported. + return; } - static void Throw(string featureName) + if (applicationMessage.ContentType?.Length > 0) { - throw new NotSupportedException($"Feature {featureName} requires MQTT version 5.0.0."); + Throw(nameof(applicationMessage.ContentType)); + } + + if (applicationMessage.UserProperties?.Count > 0) + { + Throw(nameof(applicationMessage.UserProperties)); + } + + if (applicationMessage.CorrelationData?.Length > 0) + { + Throw(nameof(applicationMessage.CorrelationData)); + } + + if (applicationMessage.ResponseTopic?.Length > 0) + { + Throw(nameof(applicationMessage.ResponseTopic)); + } + + if (applicationMessage.SubscriptionIdentifiers?.Count > 0) + { + Throw(nameof(applicationMessage.SubscriptionIdentifiers)); + } + + if (applicationMessage.TopicAlias > 0) + { + Throw(nameof(applicationMessage.TopicAlias)); + } + + if (applicationMessage.PayloadFormatIndicator != MqttPayloadFormatIndicator.Unspecified) + { + Throw(nameof(applicationMessage.PayloadFormatIndicator)); } } + + static void Throw(string featureName) + { + throw new NotSupportedException($"Feature {featureName} requires MQTT version 5.0.0."); + } } \ No newline at end of file diff --git a/Source/MQTTnet/MqttClient.cs b/Source/MQTTnet/MqttClient.cs index 9dbe960e..8f278b85 100644 --- a/Source/MQTTnet/MqttClient.cs +++ b/Source/MQTTnet/MqttClient.cs @@ -134,10 +134,8 @@ public sealed class MqttClient : Disposable, IMqttClient { // Fall back to the general timeout specified in the options if the user passed // CancellationToken.None or similar. - using (var timeout = new CancellationTokenSource(Options.Timeout)) - { - connectResult = await ConnectInternal(adapter, timeout.Token).ConfigureAwait(false); - } + using var timeout = new CancellationTokenSource(Options.Timeout); + connectResult = await ConnectInternal(adapter, timeout.Token).ConfigureAwait(false); } if (connectResult.ResultCode != MqttClientConnectResultCode.Success) @@ -219,10 +217,8 @@ public sealed class MqttClient : Disposable, IMqttClient } else { - using (var timeout = new CancellationTokenSource(Options.Timeout)) - { - await Send(disconnectPacket, timeout.Token).ConfigureAwait(false); - } + using var timeout = new CancellationTokenSource(Options.Timeout); + await Send(disconnectPacket, timeout.Token).ConfigureAwait(false); } } finally @@ -244,10 +240,8 @@ public sealed class MqttClient : Disposable, IMqttClient } else { - using (var timeout = new CancellationTokenSource(Options.Timeout)) - { - await Request(MqttPingReqPacket.Instance, timeout.Token).ConfigureAwait(false); - } + using var timeout = new CancellationTokenSource(Options.Timeout); + await Request(MqttPingReqPacket.Instance, timeout.Token).ConfigureAwait(false); } } @@ -335,13 +329,11 @@ public sealed class MqttClient : Disposable, IMqttClient } else { - using (var timeout = new CancellationTokenSource(Options.Timeout)) - { - subAckPacket = await Request(subscribePacket, timeout.Token).ConfigureAwait(false); - } + using var timeout = new CancellationTokenSource(Options.Timeout); + subAckPacket = await Request(subscribePacket, timeout.Token).ConfigureAwait(false); } - return MqttClientResultFactory.SubscribeResult.Create(subscribePacket, subAckPacket); + return MqttClientSubscribeResultFactory.Create(subscribePacket, subAckPacket); } public async Task UnsubscribeAsync(MqttClientUnsubscribeOptions options, CancellationToken cancellationToken = default) @@ -371,13 +363,11 @@ public sealed class MqttClient : Disposable, IMqttClient } else { - using (var timeout = new CancellationTokenSource(Options.Timeout)) - { - unsubAckPacket = await Request(unsubscribePacket, timeout.Token).ConfigureAwait(false); - } + using var timeout = new CancellationTokenSource(Options.Timeout); + unsubAckPacket = await Request(unsubscribePacket, timeout.Token).ConfigureAwait(false); } - return MqttClientResultFactory.UnsubscribeResult.Create(unsubscribePacket, unsubAckPacket); + return MqttClientUnsubscribeResultFactory.Create(unsubscribePacket, unsubAckPacket); } protected override void Dispose(bool disposing) @@ -451,7 +441,7 @@ public sealed class MqttClient : Disposable, IMqttClient if (receivedPacket is MqttConnAckPacket connAckPacket) { - result = MqttClientResultFactory.ConnectResult.Create(connAckPacket, channelAdapter.PacketFormatterAdapter.ProtocolVersion); + result = MqttClientConnectResultFactory.Create(connAckPacket, channelAdapter.PacketFormatterAdapter.ProtocolVersion); break; } @@ -473,10 +463,10 @@ public sealed class MqttClient : Disposable, IMqttClient return result; } - async Task HandleEnhancedAuthentication(MqttAuthPacket authPacket, CancellationToken cancellationToken) + Task HandleEnhancedAuthentication(MqttAuthPacket authPacket, CancellationToken cancellationToken) { var eventArgs = new MqttEnhancedAuthenticationEventArgs(authPacket, _adapter, cancellationToken); - await Options.EnhancedAuthenticationHandler.HandleEnhancedAuthenticationAsync(eventArgs); + return Options.EnhancedAuthenticationHandler.HandleEnhancedAuthenticationAsync(eventArgs); } void Cleanup() @@ -510,24 +500,24 @@ public sealed class MqttClient : Disposable, IMqttClient { var backgroundCancellationToken = _mqttClientAlive.Token; - using (var effectiveCancellationToken = CancellationTokenSource.CreateLinkedTokenSource(backgroundCancellationToken, cancellationToken)) + using var effectiveCancellationToken = CancellationTokenSource.CreateLinkedTokenSource(backgroundCancellationToken, cancellationToken); + _logger.Verbose("Trying to connect with server '{0}'", Options.ChannelOptions); + await channelAdapter.ConnectAsync(effectiveCancellationToken.Token).ConfigureAwait(false); + _logger.Verbose("Connection with server established"); + + _publishPacketReceiverQueue?.Dispose(); + _publishPacketReceiverQueue = new AsyncQueue(); + + var connectResult = await Authenticate(channelAdapter, Options, effectiveCancellationToken.Token).ConfigureAwait(false); + if (connectResult.ResultCode != MqttClientConnectResultCode.Success) { - _logger.Verbose("Trying to connect with server '{0}'", Options.ChannelOptions); - await channelAdapter.ConnectAsync(effectiveCancellationToken.Token).ConfigureAwait(false); - _logger.Verbose("Connection with server established"); - - _publishPacketReceiverQueue?.Dispose(); - _publishPacketReceiverQueue = new AsyncQueue(); - - var connectResult = await Authenticate(channelAdapter, Options, effectiveCancellationToken.Token).ConfigureAwait(false); - if (connectResult.ResultCode == MqttClientConnectResultCode.Success) - { - _publishPacketReceiverTask = Task.Run(() => ProcessReceivedPublishPackets(backgroundCancellationToken), backgroundCancellationToken); - _packetReceiverTask = Task.Run(() => ReceivePacketsLoop(backgroundCancellationToken), backgroundCancellationToken); - } - return connectResult; } + + _publishPacketReceiverTask = Task.Run(() => ProcessReceivedPublishPackets(backgroundCancellationToken), backgroundCancellationToken); + _packetReceiverTask = Task.Run(() => ReceivePacketsLoop(backgroundCancellationToken), backgroundCancellationToken); + + return connectResult; } async Task DisconnectCore(Task sender, Exception exception, MqttClientConnectResult connectResult, bool clientWasConnected) @@ -540,10 +530,8 @@ public sealed class MqttClient : Disposable, IMqttClient { _logger.Verbose("Disconnecting [Timeout={0}]", Options.Timeout); - using (var timeout = new CancellationTokenSource(Options.Timeout)) - { - await _adapter.DisconnectAsync(timeout.Token).ConfigureAwait(false); - } + using var timeout = new CancellationTokenSource(Options.Timeout); + await _adapter.DisconnectAsync(timeout.Token).ConfigureAwait(false); } _logger.Verbose("Disconnected from adapter."); @@ -747,7 +735,7 @@ public sealed class MqttClient : Disposable, IMqttClient publishPacket.PacketIdentifier = _packetIdentifierProvider.GetNextPacketIdentifier(); var pubAckPacket = await Request(publishPacket, cancellationToken).ConfigureAwait(false); - return MqttClientResultFactory.PublishResult.Create(pubAckPacket); + return MqttClientPublishResultFactory.Create(pubAckPacket); } async Task PublishAtMostOnce(MqttPublishPacket publishPacket, CancellationToken cancellationToken) @@ -757,7 +745,7 @@ public sealed class MqttClient : Disposable, IMqttClient // No packet identifier is used for QoS 0 [3.3.2.2 Packet Identifier] await Send(publishPacket, cancellationToken).ConfigureAwait(false); - return MqttClientResultFactory.PublishResult.Create(null); + return MqttClientPublishResultFactory.Create(null); } catch (Exception exception) { @@ -785,7 +773,7 @@ public sealed class MqttClient : Disposable, IMqttClient var pubCompPacket = await Request(pubRelPacket, cancellationToken).ConfigureAwait(false); - return MqttClientResultFactory.PublishResult.Create(pubRecPacket, pubCompPacket); + return MqttClientPublishResultFactory.Create(pubRecPacket, pubCompPacket); } async Task Receive(CancellationToken cancellationToken) @@ -875,31 +863,29 @@ public sealed class MqttClient : Disposable, IMqttClient packetIdentifier = packetWithIdentifier.PacketIdentifier; } - using (var packetAwaitable = _packetDispatcher.AddAwaitable(packetIdentifier)) + using var packetAwaitable = _packetDispatcher.AddAwaitable(packetIdentifier); + try { - try + await Send(requestPacket, cancellationToken).ConfigureAwait(false); + } + catch (Exception exception) + { + _logger.Warning(exception, "Error when sending {0} request packet", requestPacket.GetType().Name); + packetAwaitable.Fail(exception); + } + + try + { + return await packetAwaitable.WaitOneAsync(cancellationToken).ConfigureAwait(false); + } + catch (Exception exception) + { + if (exception is MqttCommunicationTimedOutException) { - await Send(requestPacket, cancellationToken).ConfigureAwait(false); - } - catch (Exception exception) - { - _logger.Warning(exception, "Error when sending {0} request packet", requestPacket.GetType().Name); - packetAwaitable.Fail(exception); + _logger.Warning("Timeout while waiting for {0} response packet", typeof(TResponsePacket).Name); } - try - { - return await packetAwaitable.WaitOneAsync(cancellationToken).ConfigureAwait(false); - } - catch (Exception exception) - { - if (exception is MqttCommunicationTimedOutException) - { - _logger.Warning("Timeout while waiting for {0} response packet", typeof(TResponsePacket).Name); - } - - throw; - } + throw; } } @@ -1066,9 +1052,9 @@ public sealed class MqttClient : Disposable, IMqttClient switch (exception) { - case OperationCanceledException _: + case OperationCanceledException: return; - case MqttCommunicationException _: + case MqttCommunicationException: _logger.Warning(exception, "Communication error while sending/receiving keep alive packets"); break; default: diff --git a/Source/MQTTnet/MqttClientFactory.cs b/Source/MQTTnet/MqttClientFactory.cs index 8994ee98..746d16dd 100644 --- a/Source/MQTTnet/MqttClientFactory.cs +++ b/Source/MQTTnet/MqttClientFactory.cs @@ -11,6 +11,8 @@ using MQTTnet.LowLevelClient; namespace MQTTnet; +#pragma warning disable CA1822 + public class MqttClientFactory { readonly IMqttNetLogger _logger; @@ -48,6 +50,7 @@ public class MqttClientFactory } public MqttClientOptionsBuilder CreateClientOptionsBuilder() + { return new MqttClientOptionsBuilder(); } diff --git a/Source/MQTTnet/MqttTopicFilterBuilder.cs b/Source/MQTTnet/MqttTopicFilterBuilder.cs index 2140745a..a927e5a7 100644 --- a/Source/MQTTnet/MqttTopicFilterBuilder.cs +++ b/Source/MQTTnet/MqttTopicFilterBuilder.cs @@ -6,93 +6,92 @@ using MQTTnet.Exceptions; using MQTTnet.Protocol; using MQTTnet.Packets; -namespace MQTTnet +namespace MQTTnet; + +public sealed class MqttTopicFilterBuilder { - public sealed class MqttTopicFilterBuilder + /// + /// The quality of service level. + /// The Quality of Service (QoS) level is an agreement between the sender of a message and the receiver of a message that defines the guarantee of delivery for a specific message. + /// There are 3 QoS levels in MQTT: + /// - At most once (0): Message gets delivered no time, once or multiple times. + /// - At least once (1): Message gets delivered at least once (one time or more often). + /// - Exactly once (2): Message gets delivered exactly once (It's ensured that the message only comes once). + /// + MqttQualityOfServiceLevel _qualityOfServiceLevel = MqttQualityOfServiceLevel.AtMostOnce; + + /// + /// The MQTT topic. + /// In MQTT, the word topic refers to an UTF-8 string that the broker uses to filter messages for each connected client. + /// The topic consists of one or more topic levels. Each topic level is separated by a forward slash (topic level separator). + /// + string _topic; + bool _noLocal; + bool _retainAsPublished; + MqttRetainHandling _retainHandling = MqttRetainHandling.SendAtSubscribe; + + public MqttTopicFilterBuilder WithTopic(string topic) { - /// - /// The quality of service level. - /// The Quality of Service (QoS) level is an agreement between the sender of a message and the receiver of a message that defines the guarantee of delivery for a specific message. - /// There are 3 QoS levels in MQTT: - /// - At most once (0): Message gets delivered no time, once or multiple times. - /// - At least once (1): Message gets delivered at least once (one time or more often). - /// - Exactly once (2): Message gets delivered exactly once (It's ensured that the message only comes once). - /// - MqttQualityOfServiceLevel _qualityOfServiceLevel = MqttQualityOfServiceLevel.AtMostOnce; - - /// - /// The MQTT topic. - /// In MQTT, the word topic refers to an UTF-8 string that the broker uses to filter messages for each connected client. - /// The topic consists of one or more topic levels. Each topic level is separated by a forward slash (topic level separator). - /// - string _topic; - bool _noLocal; - bool _retainAsPublished; - MqttRetainHandling _retainHandling = MqttRetainHandling.SendAtSubscribe; - - public MqttTopicFilterBuilder WithTopic(string topic) - { - _topic = topic; - return this; - } - - public MqttTopicFilterBuilder WithQualityOfServiceLevel(MqttQualityOfServiceLevel qualityOfServiceLevel) - { - _qualityOfServiceLevel = qualityOfServiceLevel; - return this; - } - - public MqttTopicFilterBuilder WithAtLeastOnceQoS() - { - _qualityOfServiceLevel = MqttQualityOfServiceLevel.AtLeastOnce; - return this; - } - - public MqttTopicFilterBuilder WithAtMostOnceQoS() - { - _qualityOfServiceLevel = MqttQualityOfServiceLevel.AtMostOnce; - return this; - } - - public MqttTopicFilterBuilder WithExactlyOnceQoS() - { - _qualityOfServiceLevel = MqttQualityOfServiceLevel.ExactlyOnce; - return this; - } - - public MqttTopicFilterBuilder WithNoLocal(bool value = true) - { - _noLocal = value; - return this; - } - - public MqttTopicFilterBuilder WithRetainAsPublished(bool value = true) - { - _retainAsPublished = value; - return this; - } - - public MqttTopicFilterBuilder WithRetainHandling(MqttRetainHandling value) - { - _retainHandling = value; - return this; - } - - public MqttTopicFilter Build() - { - if (string.IsNullOrEmpty(_topic)) - { - throw new MqttProtocolViolationException("Topic is not set."); - } - - return new MqttTopicFilter - { - Topic = _topic, - QualityOfServiceLevel = _qualityOfServiceLevel, - NoLocal = _noLocal, - RetainAsPublished = _retainAsPublished, - RetainHandling = _retainHandling - }; - } + _topic = topic; + return this; } -} + + public MqttTopicFilterBuilder WithQualityOfServiceLevel(MqttQualityOfServiceLevel qualityOfServiceLevel) + { + _qualityOfServiceLevel = qualityOfServiceLevel; + return this; + } + + public MqttTopicFilterBuilder WithAtLeastOnceQoS() + { + _qualityOfServiceLevel = MqttQualityOfServiceLevel.AtLeastOnce; + return this; + } + + public MqttTopicFilterBuilder WithAtMostOnceQoS() + { + _qualityOfServiceLevel = MqttQualityOfServiceLevel.AtMostOnce; + return this; + } + + public MqttTopicFilterBuilder WithExactlyOnceQoS() + { + _qualityOfServiceLevel = MqttQualityOfServiceLevel.ExactlyOnce; + return this; + } + + public MqttTopicFilterBuilder WithNoLocal(bool value = true) + { + _noLocal = value; + return this; + } + + public MqttTopicFilterBuilder WithRetainAsPublished(bool value = true) + { + _retainAsPublished = value; + return this; + } + + public MqttTopicFilterBuilder WithRetainHandling(MqttRetainHandling value) + { + _retainHandling = value; + return this; + } + + public MqttTopicFilter Build() + { + if (string.IsNullOrEmpty(_topic)) + { + throw new MqttProtocolViolationException("Topic is not set."); + } + + return new MqttTopicFilter + { + Topic = _topic, + QualityOfServiceLevel = _qualityOfServiceLevel, + NoLocal = _noLocal, + RetainAsPublished = _retainAsPublished, + RetainHandling = _retainHandling + }; + } +} \ No newline at end of file diff --git a/Source/MQTTnet/MqttTopicFilterCompareResult.cs b/Source/MQTTnet/MqttTopicFilterCompareResult.cs index 5ce59a5c..bc2b7021 100644 --- a/Source/MQTTnet/MqttTopicFilterCompareResult.cs +++ b/Source/MQTTnet/MqttTopicFilterCompareResult.cs @@ -2,16 +2,15 @@ // The .NET Foundation licenses this file to you under the MIT license. // See the LICENSE file in the project root for more information. -namespace MQTTnet +namespace MQTTnet; + +public enum MqttTopicFilterCompareResult { - public enum MqttTopicFilterCompareResult - { - NoMatch, - - IsMatch, - - FilterInvalid, - - TopicInvalid - } + NoMatch, + + IsMatch, + + FilterInvalid, + + TopicInvalid } \ No newline at end of file diff --git a/Source/MQTTnet/MqttTopicFilterComparer.cs b/Source/MQTTnet/MqttTopicFilterComparer.cs index 72285a82..4ba748d5 100644 --- a/Source/MQTTnet/MqttTopicFilterComparer.cs +++ b/Source/MQTTnet/MqttTopicFilterComparer.cs @@ -2,177 +2,176 @@ // The .NET Foundation licenses this file to you under the MIT license. // See the LICENSE file in the project root for more information. -namespace MQTTnet +namespace MQTTnet; + +public static class MqttTopicFilterComparer { - public static class MqttTopicFilterComparer + public const char LevelSeparator = '/'; + public const char MultiLevelWildcard = '#'; + public const char SingleLevelWildcard = '+'; + public const char ReservedTopicPrefix = '$'; + + public static unsafe MqttTopicFilterCompareResult Compare(string topic, string filter) { - public const char LevelSeparator = '/'; - public const char MultiLevelWildcard = '#'; - public const char SingleLevelWildcard = '+'; - public const char ReservedTopicPrefix = '$'; - - public static unsafe MqttTopicFilterCompareResult Compare(string topic, string filter) + if (string.IsNullOrEmpty(topic)) { - if (string.IsNullOrEmpty(topic)) - { - return MqttTopicFilterCompareResult.TopicInvalid; - } + return MqttTopicFilterCompareResult.TopicInvalid; + } - if (string.IsNullOrEmpty(filter)) - { - return MqttTopicFilterCompareResult.FilterInvalid; - } + if (string.IsNullOrEmpty(filter)) + { + return MqttTopicFilterCompareResult.FilterInvalid; + } - var filterOffset = 0; - var filterLength = filter.Length; + var filterOffset = 0; + var filterLength = filter.Length; - var topicOffset = 0; - var topicLength = topic.Length; - - fixed (char* topicPointer = topic) - fixed (char* filterPointer = filter) + var topicOffset = 0; + var topicLength = topic.Length; + + fixed (char* topicPointer = topic) + fixed (char* filterPointer = filter) + { + if (filterLength > topicLength) { - if (filterLength > topicLength) + // It is impossible to create a filter which is longer than the actual topic. + // The only way this can happen is when the last char is a wildcard char. + // sensor/7/temperature >> sensor/7/temperature = Equal + // sensor/+/temperature >> sensor/7/temperature = Equal + // sensor/7/+ >> sensor/7/temperature = Shorter + // sensor/# >> sensor/7/temperature = Shorter + var lastFilterChar = filterPointer[filterLength - 1]; + if (lastFilterChar != MultiLevelWildcard && lastFilterChar != SingleLevelWildcard) { - // It is impossible to create a filter which is longer than the actual topic. - // The only way this can happen is when the last char is a wildcard char. - // sensor/7/temperature >> sensor/7/temperature = Equal - // sensor/+/temperature >> sensor/7/temperature = Equal - // sensor/7/+ >> sensor/7/temperature = Shorter - // sensor/# >> sensor/7/temperature = Shorter - var lastFilterChar = filterPointer[filterLength - 1]; - if (lastFilterChar != MultiLevelWildcard && lastFilterChar != SingleLevelWildcard) - { - return MqttTopicFilterCompareResult.NoMatch; - } - } - - var isMultiLevelFilter = filterPointer[filterLength - 1] == MultiLevelWildcard; - var isReservedTopic = topicPointer[0] == ReservedTopicPrefix; - - if (isReservedTopic && filterLength == 1 && isMultiLevelFilter) - { - // It is not allowed to receive i.e. '$foo/bar' with filter '#'. return MqttTopicFilterCompareResult.NoMatch; } + } - if (isReservedTopic && filterPointer[0] == SingleLevelWildcard) + var isMultiLevelFilter = filterPointer[filterLength - 1] == MultiLevelWildcard; + var isReservedTopic = topicPointer[0] == ReservedTopicPrefix; + + if (isReservedTopic && filterLength == 1 && isMultiLevelFilter) + { + // It is not allowed to receive i.e. '$foo/bar' with filter '#'. + return MqttTopicFilterCompareResult.NoMatch; + } + + if (isReservedTopic && filterPointer[0] == SingleLevelWildcard) + { + // It is not allowed to receive i.e. '$SYS/monitor/Clients' with filter '+/monitor/Clients'. + return MqttTopicFilterCompareResult.NoMatch; + } + + if (filterLength == 1 && isMultiLevelFilter) + { + // Filter '#' matches basically everything. + return MqttTopicFilterCompareResult.IsMatch; + } + + // Go through the filter char by char. + while (filterOffset < filterLength && topicOffset < topicLength) + { + // Check if the current char is a multi level wildcard. The char is only allowed + // at the very las position. + if (filterPointer[filterOffset] == MultiLevelWildcard && filterOffset != filterLength - 1) { - // It is not allowed to receive i.e. '$SYS/monitor/Clients' with filter '+/monitor/Clients'. - return MqttTopicFilterCompareResult.NoMatch; + return MqttTopicFilterCompareResult.FilterInvalid; } - if (filterLength == 1 && isMultiLevelFilter) + if (filterPointer[filterOffset] == topicPointer[topicOffset]) { - // Filter '#' matches basically everything. - return MqttTopicFilterCompareResult.IsMatch; - } - - // Go through the filter char by char. - while (filterOffset < filterLength && topicOffset < topicLength) - { - // Check if the current char is a multi level wildcard. The char is only allowed - // at the very las position. - if (filterPointer[filterOffset] == MultiLevelWildcard && filterOffset != filterLength - 1) + if (topicOffset == topicLength - 1) { - return MqttTopicFilterCompareResult.FilterInvalid; - } - - if (filterPointer[filterOffset] == topicPointer[topicOffset]) - { - if (topicOffset == topicLength - 1) + // Check for e.g. "foo" matching "foo/#" + if (filterOffset == filterLength - 3 && filterPointer[filterOffset + 1] == LevelSeparator && isMultiLevelFilter) { - // Check for e.g. "foo" matching "foo/#" - if (filterOffset == filterLength - 3 && filterPointer[filterOffset + 1] == LevelSeparator && isMultiLevelFilter) - { - return MqttTopicFilterCompareResult.IsMatch; - } + return MqttTopicFilterCompareResult.IsMatch; + } - // Check for e.g. "foo/" matching "foo/#" - if (filterOffset == filterLength - 2 && filterPointer[filterOffset] == LevelSeparator && isMultiLevelFilter) - { - return MqttTopicFilterCompareResult.IsMatch; - } + // Check for e.g. "foo/" matching "foo/#" + if (filterOffset == filterLength - 2 && filterPointer[filterOffset] == LevelSeparator && isMultiLevelFilter) + { + return MqttTopicFilterCompareResult.IsMatch; + } + } + + filterOffset++; + topicOffset++; + + // Check if the end was reached and i.e. "foo/bar" matches "foo/bar" + if (filterOffset == filterLength && topicOffset == topicLength) + { + return MqttTopicFilterCompareResult.IsMatch; + } + + var endOfTopic = topicOffset == topicLength; + + if (endOfTopic && filterOffset == filterLength - 1 && filterPointer[filterOffset] == SingleLevelWildcard) + { + if (filterOffset > 0 && filterPointer[filterOffset - 1] != LevelSeparator) + { + return MqttTopicFilterCompareResult.FilterInvalid; + } + + return MqttTopicFilterCompareResult.IsMatch; + } + } + else + { + if (filterPointer[filterOffset] == SingleLevelWildcard) + { + // Check for invalid "+foo" or "a/+foo" subscription + if (filterOffset > 0 && filterPointer[filterOffset - 1] != LevelSeparator) + { + return MqttTopicFilterCompareResult.FilterInvalid; + } + + // Check for bad "foo+" or "foo+/a" subscription + if (filterOffset < filterLength - 1 && filterPointer[filterOffset + 1] != LevelSeparator) + { + return MqttTopicFilterCompareResult.FilterInvalid; } filterOffset++; - topicOffset++; + while (topicOffset < topicLength && topicPointer[topicOffset] != LevelSeparator) + { + topicOffset++; + } - // Check if the end was reached and i.e. "foo/bar" matches "foo/bar" - if (filterOffset == filterLength && topicOffset == topicLength) + if (topicOffset == topicLength && filterOffset == filterLength) { return MqttTopicFilterCompareResult.IsMatch; } - - var endOfTopic = topicOffset == topicLength; - - if (endOfTopic && filterOffset == filterLength - 1 && filterPointer[filterOffset] == SingleLevelWildcard) + } + else if (filterPointer[filterOffset] == MultiLevelWildcard) + { + if (filterOffset > 0 && filterPointer[filterOffset - 1] != LevelSeparator) { - if (filterOffset > 0 && filterPointer[filterOffset - 1] != LevelSeparator) - { - return MqttTopicFilterCompareResult.FilterInvalid; - } - - return MqttTopicFilterCompareResult.IsMatch; + return MqttTopicFilterCompareResult.FilterInvalid; } + + if (filterOffset + 1 != filterLength) + { + return MqttTopicFilterCompareResult.FilterInvalid; + } + + return MqttTopicFilterCompareResult.IsMatch; } else { - if (filterPointer[filterOffset] == SingleLevelWildcard) + // Check for e.g. "foo/bar" matching "foo/+/#". + if (filterOffset > 0 && filterOffset + 2 == filterLength && topicOffset == topicLength && filterPointer[filterOffset - 1] == SingleLevelWildcard && + filterPointer[filterOffset] == LevelSeparator && isMultiLevelFilter) { - // Check for invalid "+foo" or "a/+foo" subscription - if (filterOffset > 0 && filterPointer[filterOffset - 1] != LevelSeparator) - { - return MqttTopicFilterCompareResult.FilterInvalid; - } - - // Check for bad "foo+" or "foo+/a" subscription - if (filterOffset < filterLength - 1 && filterPointer[filterOffset + 1] != LevelSeparator) - { - return MqttTopicFilterCompareResult.FilterInvalid; - } - - filterOffset++; - while (topicOffset < topicLength && topicPointer[topicOffset] != LevelSeparator) - { - topicOffset++; - } - - if (topicOffset == topicLength && filterOffset == filterLength) - { - return MqttTopicFilterCompareResult.IsMatch; - } - } - else if (filterPointer[filterOffset] == MultiLevelWildcard) - { - if (filterOffset > 0 && filterPointer[filterOffset - 1] != LevelSeparator) - { - return MqttTopicFilterCompareResult.FilterInvalid; - } - - if (filterOffset + 1 != filterLength) - { - return MqttTopicFilterCompareResult.FilterInvalid; - } - return MqttTopicFilterCompareResult.IsMatch; } - else - { - // Check for e.g. "foo/bar" matching "foo/+/#". - if (filterOffset > 0 && filterOffset + 2 == filterLength && topicOffset == topicLength && filterPointer[filterOffset - 1] == SingleLevelWildcard && - filterPointer[filterOffset] == LevelSeparator && isMultiLevelFilter) - { - return MqttTopicFilterCompareResult.IsMatch; - } - return MqttTopicFilterCompareResult.NoMatch; - } + return MqttTopicFilterCompareResult.NoMatch; } } } - - return MqttTopicFilterCompareResult.NoMatch; } + + return MqttTopicFilterCompareResult.NoMatch; } } \ No newline at end of file diff --git a/Source/MQTTnet/Options/DefaultMqttCertificatesProvider.cs b/Source/MQTTnet/Options/DefaultMqttCertificatesProvider.cs index 3e608464..5285a2a1 100644 --- a/Source/MQTTnet/Options/DefaultMqttCertificatesProvider.cs +++ b/Source/MQTTnet/Options/DefaultMqttCertificatesProvider.cs @@ -20,7 +20,7 @@ public sealed class DefaultMqttCertificatesProvider : IMqttClientCertificatesPro { if (certificates != null) { - _certificates = new X509Certificate2Collection(); + _certificates = []; foreach (var certificate in certificates) { _certificates.Add(certificate); diff --git a/Source/MQTTnet/Options/MqttClientDefaultCertificateValidationHandler.cs b/Source/MQTTnet/Options/MqttClientDefaultCertificateValidationHandler.cs index 608a0216..28ba4eaa 100644 --- a/Source/MQTTnet/Options/MqttClientDefaultCertificateValidationHandler.cs +++ b/Source/MQTTnet/Options/MqttClientDefaultCertificateValidationHandler.cs @@ -18,7 +18,7 @@ public sealed class MqttClientDefaultCertificateValidationHandler } if (eventArgs.Chain.ChainStatus.Any( - c => c.Status == X509ChainStatusFlags.RevocationStatusUnknown || c.Status == X509ChainStatusFlags.Revoked || c.Status == X509ChainStatusFlags.OfflineRevocation)) + c => c.Status is X509ChainStatusFlags.RevocationStatusUnknown or X509ChainStatusFlags.Revoked or X509ChainStatusFlags.OfflineRevocation)) { if (eventArgs.ClientOptions?.TlsOptions?.IgnoreCertificateRevocationErrors != true) { diff --git a/Source/MQTTnet/Options/MqttClientOptionsBuilder.cs b/Source/MQTTnet/Options/MqttClientOptionsBuilder.cs index a6e92c60..3c2f589f 100644 --- a/Source/MQTTnet/Options/MqttClientOptionsBuilder.cs +++ b/Source/MQTTnet/Options/MqttClientOptionsBuilder.cs @@ -127,7 +127,7 @@ public sealed class MqttClientOptionsBuilder ArgumentNullException.ThrowIfNull(uri); var port = uri.IsDefaultPort ? null : (int?)uri.Port; - switch (uri.Scheme.ToLower()) + switch (uri.Scheme.ToLowerInvariant()) { case "tcp": case "mqtt": @@ -307,7 +307,7 @@ public sealed class MqttClientOptionsBuilder ArgumentNullException.ThrowIfNull(optionsBuilder); _tcpOptions = new MqttClientTcpOptions(); - optionsBuilder.Invoke(_tcpOptions); + optionsBuilder(_tcpOptions); return this; } @@ -466,11 +466,7 @@ public sealed class MqttClientOptionsBuilder public MqttClientOptionsBuilder WithWillUserProperty(string name, string value) { - if (_options.WillUserProperties == null) - { - _options.WillUserProperties = new List(); - } - + _options.WillUserProperties ??= []; _options.WillUserProperties.Add(new MqttUserProperty(name, value)); return this; } diff --git a/Source/MQTTnet/Options/MqttClientOptionsValidator.cs b/Source/MQTTnet/Options/MqttClientOptionsValidator.cs index 3da66175..5b40616c 100644 --- a/Source/MQTTnet/Options/MqttClientOptionsValidator.cs +++ b/Source/MQTTnet/Options/MqttClientOptionsValidator.cs @@ -3,7 +3,6 @@ // See the LICENSE file in the project root for more information. using System; -using System.Linq; using MQTTnet.Formatter; using MQTTnet.Protocol; @@ -25,12 +24,12 @@ public static class MqttClientOptionsValidator return; } - if (options.WillContentType?.Any() == true) + if (options.WillContentType?.Length > 0) { Throw(nameof(options.WillContentType)); } - if (options.UserProperties?.Any() == true) + if (options.UserProperties?.Count > 0) { Throw(nameof(options.UserProperties)); } @@ -59,12 +58,12 @@ public static class MqttClientOptionsValidator // Authentication relevant properties. - if (options.AuthenticationData?.Any() == true) + if (options.AuthenticationData?.Length > 0) { Throw(nameof(options.AuthenticationData)); } - if (options.AuthenticationMethod?.Any() == true) + if (options.AuthenticationMethod?.Length > 0) { Throw(nameof(options.AuthenticationMethod)); } @@ -76,17 +75,17 @@ public static class MqttClientOptionsValidator Throw(nameof(options.WillPayloadFormatIndicator)); } - if (options.WillContentType?.Any() == true) + if (options.WillContentType?.Length > 0) { Throw(nameof(options.WillContentType)); } - if (options.WillCorrelationData?.Any() == true) + if (options.WillCorrelationData?.Length > 0) { Throw(nameof(options.WillCorrelationData)); } - if (options.WillResponseTopic?.Any() == true) + if (options.WillResponseTopic?.Length > 0) { Throw(nameof(options.WillResponseTopic)); } @@ -101,7 +100,7 @@ public static class MqttClientOptionsValidator Throw(nameof(options.WillMessageExpiryInterval)); } - if (options.WillUserProperties?.Any() == true) + if (options.WillUserProperties?.Count > 0) { Throw(nameof(options.WillUserProperties)); } diff --git a/Source/MQTTnet/Options/MqttClientWebSocketOptions.cs b/Source/MQTTnet/Options/MqttClientWebSocketOptions.cs index 1e2d9811..b88e57b0 100644 --- a/Source/MQTTnet/Options/MqttClientWebSocketOptions.cs +++ b/Source/MQTTnet/Options/MqttClientWebSocketOptions.cs @@ -12,6 +12,7 @@ namespace MQTTnet; public sealed class MqttClientWebSocketOptions : IMqttClientChannelOptions { public CookieContainer CookieContainer { get; set; } + public WebSocketDeflateOptions DangerousDeflateOptions { get; set; } public ICredentials Credentials { get; set; } @@ -26,7 +27,7 @@ public sealed class MqttClientWebSocketOptions : IMqttClientChannelOptions public IDictionary RequestHeaders { get; set; } - public ICollection SubProtocols { get; set; } = new List { "mqtt" }; + public ICollection SubProtocols { get; set; } = ["mqtt"]; public MqttClientTlsOptions TlsOptions { get; set; } = new(); diff --git a/Source/MQTTnet/Options/MqttClientWebSocketOptionsBuilder.cs b/Source/MQTTnet/Options/MqttClientWebSocketOptionsBuilder.cs index fffc77ef..090c7257 100644 --- a/Source/MQTTnet/Options/MqttClientWebSocketOptionsBuilder.cs +++ b/Source/MQTTnet/Options/MqttClientWebSocketOptionsBuilder.cs @@ -59,7 +59,7 @@ public sealed class MqttClientWebSocketOptionsBuilder ArgumentNullException.ThrowIfNull(configure); var proxyOptionsBuilder = new MqttClientWebSocketProxyOptionsBuilder(); - configure.Invoke(proxyOptionsBuilder); + configure(proxyOptionsBuilder); _webSocketOptions.ProxyOptions = proxyOptionsBuilder.Build(); return this; diff --git a/Source/MQTTnet/Options/MqttEnhancedAuthenticationEventArgs.cs b/Source/MQTTnet/Options/MqttEnhancedAuthenticationEventArgs.cs index 0b9ac404..5d9b508a 100644 --- a/Source/MQTTnet/Options/MqttEnhancedAuthenticationEventArgs.cs +++ b/Source/MQTTnet/Options/MqttEnhancedAuthenticationEventArgs.cs @@ -89,10 +89,7 @@ public class MqttEnhancedAuthenticationEventArgs : EventArgs public Task SendAsync(SendMqttEnhancedAuthenticationDataOptions options, CancellationToken cancellationToken = default) { - if (options == null) - { - throw new ArgumentNullException(nameof(options)); - } + ArgumentNullException.ThrowIfNull(options); var authPacket = new MqttAuthPacket { diff --git a/Source/MQTTnet/Options/MqttEnhancedAuthenticationExchangeData.cs b/Source/MQTTnet/Options/MqttEnhancedAuthenticationExchangeData.cs index 0e7ceafb..6fac2d76 100644 --- a/Source/MQTTnet/Options/MqttEnhancedAuthenticationExchangeData.cs +++ b/Source/MQTTnet/Options/MqttEnhancedAuthenticationExchangeData.cs @@ -41,5 +41,5 @@ public class MqttEnhancedAuthenticationExchangeData /// The feature is very similar to the HTTP header concept. /// Hint: MQTT 5 feature only. /// - public List UserProperties { get; } + public List UserProperties { get; set; } } \ No newline at end of file diff --git a/Source/MQTTnet/PacketDispatcher/IMqttPacketAwaitable.cs b/Source/MQTTnet/PacketDispatcher/IMqttPacketAwaitable.cs index a467edb8..1a63eeb7 100644 --- a/Source/MQTTnet/PacketDispatcher/IMqttPacketAwaitable.cs +++ b/Source/MQTTnet/PacketDispatcher/IMqttPacketAwaitable.cs @@ -5,16 +5,15 @@ using MQTTnet.Packets; using System; -namespace MQTTnet.PacketDispatcher +namespace MQTTnet.PacketDispatcher; + +public interface IMqttPacketAwaitable : IDisposable { - public interface IMqttPacketAwaitable : IDisposable - { - MqttPacketAwaitableFilter Filter { get; } - - void Complete(MqttPacket packet); + MqttPacketAwaitableFilter Filter { get; } - void Fail(Exception exception); + void Complete(MqttPacket packet); - void Cancel(); - } + void Fail(Exception exception); + + void Cancel(); } \ No newline at end of file diff --git a/Source/MQTTnet/PacketDispatcher/MqttPacketAwaitable.cs b/Source/MQTTnet/PacketDispatcher/MqttPacketAwaitable.cs index 895e2b53..f751a907 100644 --- a/Source/MQTTnet/PacketDispatcher/MqttPacketAwaitable.cs +++ b/Source/MQTTnet/PacketDispatcher/MqttPacketAwaitable.cs @@ -9,57 +9,56 @@ using System.Threading; using System.Threading.Tasks; using MQTTnet.Internal; -namespace MQTTnet.PacketDispatcher +namespace MQTTnet.PacketDispatcher; + +public sealed class MqttPacketAwaitable : IMqttPacketAwaitable where TPacket : MqttPacket { - public sealed class MqttPacketAwaitable : IMqttPacketAwaitable where TPacket : MqttPacket + readonly AsyncTaskCompletionSource _promise = new(); + readonly MqttPacketDispatcher _owningPacketDispatcher; + + public MqttPacketAwaitable(ushort packetIdentifier, MqttPacketDispatcher owningPacketDispatcher) { - readonly AsyncTaskCompletionSource _promise = new AsyncTaskCompletionSource(); - readonly MqttPacketDispatcher _owningPacketDispatcher; - - public MqttPacketAwaitable(ushort packetIdentifier, MqttPacketDispatcher owningPacketDispatcher) + Filter = new MqttPacketAwaitableFilter { - Filter = new MqttPacketAwaitableFilter - { - Type = typeof(TPacket), - Identifier = packetIdentifier - }; - - _owningPacketDispatcher = owningPacketDispatcher ?? throw new ArgumentNullException(nameof(owningPacketDispatcher)); - } - - public MqttPacketAwaitableFilter Filter { get; } - - public async Task WaitOneAsync(CancellationToken cancellationToken) - { - using (cancellationToken.Register(() => Fail(new MqttCommunicationTimedOutException()))) - { - var packet = await _promise.Task.ConfigureAwait(false); - return (TPacket)packet; - } - } + Type = typeof(TPacket), + Identifier = packetIdentifier + }; - public void Complete(MqttPacket packet) - { - if (packet == null) throw new ArgumentNullException(nameof(packet)); + _owningPacketDispatcher = owningPacketDispatcher ?? throw new ArgumentNullException(nameof(owningPacketDispatcher)); + } - _promise.TrySetResult(packet); - } + public MqttPacketAwaitableFilter Filter { get; } - public void Fail(Exception exception) + public async Task WaitOneAsync(CancellationToken cancellationToken) + { + await using (cancellationToken.Register(() => Fail(new MqttCommunicationTimedOutException()))) { - if (exception == null) throw new ArgumentNullException(nameof(exception)); - - _promise.TrySetException(exception); - } - - public void Cancel() - { - _promise.TrySetCanceled(); - } - - public void Dispose() - { - _owningPacketDispatcher.RemoveAwaitable(this); + var packet = await _promise.Task.ConfigureAwait(false); + return (TPacket)packet; } } + + public void Complete(MqttPacket packet) + { + ArgumentNullException.ThrowIfNull(packet); + + _promise.TrySetResult(packet); + } + + public void Fail(Exception exception) + { + ArgumentNullException.ThrowIfNull(exception); + + _promise.TrySetException(exception); + } + + public void Cancel() + { + _promise.TrySetCanceled(); + } + + public void Dispose() + { + _owningPacketDispatcher.RemoveAwaitable(this); + } } \ No newline at end of file diff --git a/Source/MQTTnet/PacketDispatcher/MqttPacketAwaitableFilter.cs b/Source/MQTTnet/PacketDispatcher/MqttPacketAwaitableFilter.cs index ab94969f..6c551589 100644 --- a/Source/MQTTnet/PacketDispatcher/MqttPacketAwaitableFilter.cs +++ b/Source/MQTTnet/PacketDispatcher/MqttPacketAwaitableFilter.cs @@ -4,12 +4,11 @@ using System; -namespace MQTTnet.PacketDispatcher +namespace MQTTnet.PacketDispatcher; + +public sealed class MqttPacketAwaitableFilter { - public sealed class MqttPacketAwaitableFilter - { - public Type Type { get; set; } - - public ushort Identifier { get; set; } - } + public Type Type { get; set; } + + public ushort Identifier { get; set; } } \ No newline at end of file diff --git a/Source/MQTTnet/PacketDispatcher/MqttPacketDispatcher.cs b/Source/MQTTnet/PacketDispatcher/MqttPacketDispatcher.cs index fd71406d..ae4df439 100644 --- a/Source/MQTTnet/PacketDispatcher/MqttPacketDispatcher.cs +++ b/Source/MQTTnet/PacketDispatcher/MqttPacketDispatcher.cs @@ -6,131 +6,127 @@ using System; using System.Collections.Generic; using MQTTnet.Packets; -namespace MQTTnet.PacketDispatcher +namespace MQTTnet.PacketDispatcher; + +public sealed class MqttPacketDispatcher : IDisposable { - public sealed class MqttPacketDispatcher : IDisposable + readonly List _waiters = []; + + bool _isDisposed; + + public MqttPacketAwaitable AddAwaitable(ushort packetIdentifier) where TResponsePacket : MqttPacket { - readonly List _waiters = new List(); + var awaitable = new MqttPacketAwaitable(packetIdentifier, this); - bool _isDisposed; - - public MqttPacketAwaitable AddAwaitable(ushort packetIdentifier) where TResponsePacket : MqttPacket + lock (_waiters) { - var awaitable = new MqttPacketAwaitable(packetIdentifier, this); - - lock (_waiters) - { - _waiters.Add(awaitable); - } - - return awaitable; + _waiters.Add(awaitable); } - public void CancelAll() + return awaitable; + } + + public void CancelAll() + { + lock (_waiters) { - lock (_waiters) + foreach (var awaitable in _waiters) { - foreach (var awaitable in _waiters) - { - awaitable.Cancel(); - } - - _waiters.Clear(); - } - } - - public void Dispose() - { - Dispose(new ObjectDisposedException(nameof(MqttPacketDispatcher))); - } - - public void Dispose(Exception exception) - { - ArgumentNullException.ThrowIfNull(exception); - - lock (_waiters) - { - FailAll(exception); - - // Make sure that no task can start waiting after this instance is already disposed. - // This will prevent unexpected freezes. - _isDisposed = true; - } - } - - public void FailAll(Exception exception) - { - ArgumentNullException.ThrowIfNull(exception); - - lock (_waiters) - { - foreach (var awaitable in _waiters) - { - awaitable.Fail(exception); - } - - _waiters.Clear(); - } - } - - public void RemoveAwaitable(IMqttPacketAwaitable awaitable) - { - ArgumentNullException.ThrowIfNull(awaitable); - - lock (_waiters) - { - _waiters.Remove(awaitable); - } - } - - public bool TryDispatch(MqttPacket packet) - { - ArgumentNullException.ThrowIfNull(packet); - - ushort identifier = 0; - if (packet is MqttPacketWithIdentifier packetWithIdentifier) - { - identifier = packetWithIdentifier.PacketIdentifier; + awaitable.Cancel(); } - var packetType = packet.GetType(); - var waiters = new List(); - - lock (_waiters) - { - ThrowIfDisposed(); - - for (var i = _waiters.Count - 1; i >= 0; i--) - { - var entry = _waiters[i]; - - // Note: The PingRespPacket will also arrive here and has NO identifier but there - // is code which waits for it. So the code must be able to deal with filters which - // are referring to the type only (identifier is 0)! - if (entry.Filter.Type != packetType || entry.Filter.Identifier != identifier) - { - continue; - } - - waiters.Add(entry); - _waiters.RemoveAt(i); - } - } - - foreach (var matchingEntry in waiters) - { - matchingEntry.Complete(packet); - } - - return waiters.Count > 0; - } - - void ThrowIfDisposed() - { - if (_isDisposed) - { - throw new ObjectDisposedException(nameof(MqttPacketDispatcher)); - } + _waiters.Clear(); } } + + public void Dispose() + { + Dispose(new ObjectDisposedException(nameof(MqttPacketDispatcher))); + } + + public void Dispose(Exception exception) + { + ArgumentNullException.ThrowIfNull(exception); + + lock (_waiters) + { + FailAll(exception); + + // Make sure that no task can start waiting after this instance is already disposed. + // This will prevent unexpected freezes. + _isDisposed = true; + } + } + + public void FailAll(Exception exception) + { + ArgumentNullException.ThrowIfNull(exception); + + lock (_waiters) + { + foreach (var awaitable in _waiters) + { + awaitable.Fail(exception); + } + + _waiters.Clear(); + } + } + + public void RemoveAwaitable(IMqttPacketAwaitable awaitable) + { + ArgumentNullException.ThrowIfNull(awaitable); + + lock (_waiters) + { + _waiters.Remove(awaitable); + } + } + + public bool TryDispatch(MqttPacket packet) + { + ArgumentNullException.ThrowIfNull(packet); + + ushort identifier = 0; + if (packet is MqttPacketWithIdentifier packetWithIdentifier) + { + identifier = packetWithIdentifier.PacketIdentifier; + } + + var packetType = packet.GetType(); + var waiters = new List(); + + lock (_waiters) + { + ThrowIfDisposed(); + + for (var i = _waiters.Count - 1; i >= 0; i--) + { + var entry = _waiters[i]; + + // Note: The PingRespPacket will also arrive here and has NO identifier but there + // is code which waits for it. So the code must be able to deal with filters which + // are referring to the type only (identifier is 0)! + if (entry.Filter.Type != packetType || entry.Filter.Identifier != identifier) + { + continue; + } + + waiters.Add(entry); + _waiters.RemoveAt(i); + } + } + + foreach (var matchingEntry in waiters) + { + matchingEntry.Complete(packet); + } + + return waiters.Count > 0; + } + + void ThrowIfDisposed() + { + ObjectDisposedException.ThrowIf(_isDisposed, this); + } } \ No newline at end of file diff --git a/Source/MQTTnet/Packets/MqttConnectPacket.cs b/Source/MQTTnet/Packets/MqttConnectPacket.cs index f9e83f35..b91ee9f3 100644 --- a/Source/MQTTnet/Packets/MqttConnectPacket.cs +++ b/Source/MQTTnet/Packets/MqttConnectPacket.cs @@ -5,77 +5,76 @@ using System.Collections.Generic; using MQTTnet.Protocol; -namespace MQTTnet.Packets +namespace MQTTnet.Packets; + +public sealed class MqttConnectPacket : MqttPacket { - public sealed class MqttConnectPacket : MqttPacket + public byte[] AuthenticationData { get; set; } + + public string AuthenticationMethod { get; set; } + + /// + /// Also called "Clean Start" in MQTTv5. + /// + public bool CleanSession { get; set; } + + public string ClientId { get; set; } + + public byte[] WillCorrelationData { get; set; } + + public ushort KeepAlivePeriod { get; set; } + + public uint MaximumPacketSize { get; set; } + + public byte[] Password { get; set; } + + public ushort ReceiveMaximum { get; set; } + + public bool RequestProblemInformation { get; set; } + + public bool RequestResponseInformation { get; set; } + + public string WillResponseTopic { get; set; } + + public uint SessionExpiryInterval { get; set; } + + public ushort TopicAliasMaximum { get; set; } + + public string Username { get; set; } + + public List UserProperties { get; set; } + + public string WillContentType { get; set; } + + public uint WillDelayInterval { get; set; } + + public bool WillFlag { get; set; } + + public byte[] WillMessage { get; set; } + + public uint WillMessageExpiryInterval { get; set; } + + public MqttPayloadFormatIndicator WillPayloadFormatIndicator { get; set; } = MqttPayloadFormatIndicator.Unspecified; + + public MqttQualityOfServiceLevel WillQoS { get; set; } = MqttQualityOfServiceLevel.AtMostOnce; + + public bool WillRetain { get; set; } + + public string WillTopic { get; set; } + + public List WillUserProperties { get; set; } + + public bool TryPrivate { get; set; } + + public override string ToString() { - public byte[] AuthenticationData { get; set; } + var passwordText = string.Empty; - public string AuthenticationMethod { get; set; } - - /// - /// Also called "Clean Start" in MQTTv5. - /// - public bool CleanSession { get; set; } - - public string ClientId { get; set; } - - public byte[] WillCorrelationData { get; set; } - - public ushort KeepAlivePeriod { get; set; } - - public uint MaximumPacketSize { get; set; } - - public byte[] Password { get; set; } - - public ushort ReceiveMaximum { get; set; } - - public bool RequestProblemInformation { get; set; } - - public bool RequestResponseInformation { get; set; } - - public string WillResponseTopic { get; set; } - - public uint SessionExpiryInterval { get; set; } - - public ushort TopicAliasMaximum { get; set; } - - public string Username { get; set; } - - public List UserProperties { get; set; } - - public string WillContentType { get; set; } - - public uint WillDelayInterval { get; set; } - - public bool WillFlag { get; set; } - - public byte[] WillMessage { get; set; } - - public uint WillMessageExpiryInterval { get; set; } - - public MqttPayloadFormatIndicator WillPayloadFormatIndicator { get; set; } = MqttPayloadFormatIndicator.Unspecified; - - public MqttQualityOfServiceLevel WillQoS { get; set; } = MqttQualityOfServiceLevel.AtMostOnce; - - public bool WillRetain { get; set; } - - public string WillTopic { get; set; } - - public List WillUserProperties { get; set; } - - public bool TryPrivate { get; set; } - - public override string ToString() + if (Password != null) { - var passwordText = string.Empty; - - if (Password != null) - { - passwordText = "****"; - } - - return $"Connect: [ClientId={ClientId}] [Username={Username}] [Password={passwordText}] [KeepAlivePeriod={KeepAlivePeriod}] [CleanSession={CleanSession}]"; + passwordText = "****"; } + + return $"Connect: [ClientId={ClientId}] [Username={Username}] [Password={passwordText}] [KeepAlivePeriod={KeepAlivePeriod}] [CleanSession={CleanSession}]"; } } \ No newline at end of file diff --git a/Source/MQTTnet/Packets/MqttDisconnectPacket.cs b/Source/MQTTnet/Packets/MqttDisconnectPacket.cs index 8fe505a3..54698170 100644 --- a/Source/MQTTnet/Packets/MqttDisconnectPacket.cs +++ b/Source/MQTTnet/Packets/MqttDisconnectPacket.cs @@ -5,38 +5,37 @@ using System.Collections.Generic; using MQTTnet.Protocol; -namespace MQTTnet.Packets +namespace MQTTnet.Packets; + +public sealed class MqttDisconnectPacket : MqttPacket { - public sealed class MqttDisconnectPacket : MqttPacket + /// + /// Added in MQTTv5. + /// + public MqttDisconnectReasonCode ReasonCode { get; set; } = MqttDisconnectReasonCode.NormalDisconnection; + + /// + /// Added in MQTTv5. + /// + public string ReasonString { get; set; } + + /// + /// Added in MQTTv5. + /// + public string ServerReference { get; set; } + + /// + /// Added in MQTTv5. + /// + public uint SessionExpiryInterval { get; set; } + + /// + /// Added in MQTTv5. + /// + public List UserProperties { get; set; } + + public override string ToString() { - /// - /// Added in MQTTv5. - /// - public MqttDisconnectReasonCode ReasonCode { get; set; } = MqttDisconnectReasonCode.NormalDisconnection; - - /// - /// Added in MQTTv5. - /// - public string ReasonString { get; set; } - - /// - /// Added in MQTTv5. - /// - public string ServerReference { get; set; } - - /// - /// Added in MQTTv5. - /// - public uint SessionExpiryInterval { get; set; } - - /// - /// Added in MQTTv5. - /// - public List UserProperties { get; set; } - - public override string ToString() - { - return $"Disconnect: [ReasonCode={ReasonCode}] [ReasonString={ReasonString}] [ServerReference={ServerReference}] [SessionExpiryInterval={SessionExpiryInterval}]"; - } + return $"Disconnect: [ReasonCode={ReasonCode}] [ReasonString={ReasonString}] [ServerReference={ServerReference}] [SessionExpiryInterval={SessionExpiryInterval}]"; } } \ No newline at end of file diff --git a/Source/MQTTnet/Packets/MqttPacket.cs b/Source/MQTTnet/Packets/MqttPacket.cs index 1e07e42f..cfcaa0cc 100644 --- a/Source/MQTTnet/Packets/MqttPacket.cs +++ b/Source/MQTTnet/Packets/MqttPacket.cs @@ -4,6 +4,4 @@ namespace MQTTnet.Packets; -public abstract class MqttPacket -{ -} \ No newline at end of file +public abstract class MqttPacket; \ No newline at end of file diff --git a/Source/MQTTnet/Packets/MqttPacketExtensions.cs b/Source/MQTTnet/Packets/MqttPacketExtensions.cs index 90be0ef7..d2c55aea 100644 --- a/Source/MQTTnet/Packets/MqttPacketExtensions.cs +++ b/Source/MQTTnet/Packets/MqttPacketExtensions.cs @@ -12,87 +12,24 @@ public static class MqttPacketExtensions { ArgumentNullException.ThrowIfNull(packet); - switch (packet) + return packet switch { - case MqttConnectPacket _: - { - return "CONNECT"; - } - - case MqttConnAckPacket _: - { - return "CONNACK"; - } - - case MqttAuthPacket _: - { - return "AUTH"; - } - - case MqttDisconnectPacket _: - { - return "DISCONNECT"; - } - - case MqttPingReqPacket _: - { - return "PINGREQ"; - } - - case MqttPingRespPacket _: - { - return "PINGRESP"; - } - - case MqttSubscribePacket _: - { - return "SUBSCRIBE"; - } - - case MqttSubAckPacket _: - { - return "SUBACK"; - } - - case MqttUnsubscribePacket _: - { - return "UNSUBSCRIBE"; - } - - case MqttUnsubAckPacket _: - { - return "UNSUBACK"; - } - - case MqttPublishPacket _: - { - return "PUBLISH"; - } - - case MqttPubAckPacket _: - { - return "PUBACK"; - } - - case MqttPubRelPacket _: - { - return "PUBREL"; - } - - case MqttPubRecPacket _: - { - return "PUBREC"; - } - - case MqttPubCompPacket _: - { - return "PUBCOMP"; - } - - default: - { - return packet.GetType().Name; - } - } + MqttConnectPacket => "CONNECT", + MqttConnAckPacket => "CONNACK", + MqttAuthPacket => "AUTH", + MqttDisconnectPacket => "DISCONNECT", + MqttPingReqPacket => "PINGREQ", + MqttPingRespPacket => "PINGRESP", + MqttSubscribePacket => "SUBSCRIBE", + MqttSubAckPacket => "SUBACK", + MqttUnsubscribePacket => "UNSUBSCRIBE", + MqttUnsubAckPacket => "UNSUBACK", + MqttPublishPacket => "PUBLISH", + MqttPubAckPacket => "PUBACK", + MqttPubRelPacket => "PUBREL", + MqttPubRecPacket => "PUBREC", + MqttPubCompPacket => "PUBCOMP", + _ => packet.GetType().Name + }; } } \ No newline at end of file diff --git a/Source/MQTTnet/Packets/MqttPacketWithIdentifier.cs b/Source/MQTTnet/Packets/MqttPacketWithIdentifier.cs index 99caa7b7..4589e2c8 100644 --- a/Source/MQTTnet/Packets/MqttPacketWithIdentifier.cs +++ b/Source/MQTTnet/Packets/MqttPacketWithIdentifier.cs @@ -2,10 +2,9 @@ // The .NET Foundation licenses this file to you under the MIT license. // See the LICENSE file in the project root for more information. -namespace MQTTnet.Packets +namespace MQTTnet.Packets; + +public abstract class MqttPacketWithIdentifier : MqttPacket { - public abstract class MqttPacketWithIdentifier : MqttPacket - { - public ushort PacketIdentifier { get; set; } - } -} + public ushort PacketIdentifier { get; set; } +} \ No newline at end of file diff --git a/Source/MQTTnet/Packets/MqttPingReqPacket.cs b/Source/MQTTnet/Packets/MqttPingReqPacket.cs index 9ca47649..de002289 100644 --- a/Source/MQTTnet/Packets/MqttPingReqPacket.cs +++ b/Source/MQTTnet/Packets/MqttPingReqPacket.cs @@ -2,16 +2,15 @@ // The .NET Foundation licenses this file to you under the MIT license. // See the LICENSE file in the project root for more information. -namespace MQTTnet.Packets -{ - public sealed class MqttPingReqPacket : MqttPacket - { - // This is a minor performance improvement. - public static readonly MqttPingReqPacket Instance = new MqttPingReqPacket(); +namespace MQTTnet.Packets; - public override string ToString() - { - return "PingReq"; - } +public sealed class MqttPingReqPacket : MqttPacket +{ + // This is a minor performance improvement. + public static readonly MqttPingReqPacket Instance = new(); + + public override string ToString() + { + return "PingReq"; } -} +} \ No newline at end of file diff --git a/Source/MQTTnet/Packets/MqttPubRecPacket.cs b/Source/MQTTnet/Packets/MqttPubRecPacket.cs index 3e080e7c..624c3315 100644 --- a/Source/MQTTnet/Packets/MqttPubRecPacket.cs +++ b/Source/MQTTnet/Packets/MqttPubRecPacket.cs @@ -5,28 +5,27 @@ using System.Collections.Generic; using MQTTnet.Protocol; -namespace MQTTnet.Packets +namespace MQTTnet.Packets; + +public sealed class MqttPubRecPacket : MqttPacketWithIdentifier { - public sealed class MqttPubRecPacket : MqttPacketWithIdentifier + /// + /// Added in MQTTv5. + /// + public MqttPubRecReasonCode ReasonCode { get; set; } = MqttPubRecReasonCode.Success; + + /// + /// Added in MQTTv5. + /// + public string ReasonString { get; set; } + + /// + /// Added in MQTTv5. + /// + public List UserProperties { get; set; } + + public override string ToString() { - /// - /// Added in MQTTv5. - /// - public MqttPubRecReasonCode ReasonCode { get; set; } = MqttPubRecReasonCode.Success; - - /// - /// Added in MQTTv5. - /// - public string ReasonString { get; set; } - - /// - /// Added in MQTTv5. - /// - public List UserProperties { get; set; } - - public override string ToString() - { - return $"PubRec: [PacketIdentifier={PacketIdentifier}] [ReasonCode={ReasonCode}]"; - } + return $"PubRec: [PacketIdentifier={PacketIdentifier}] [ReasonCode={ReasonCode}]"; } } \ No newline at end of file diff --git a/Source/MQTTnet/Packets/MqttPubRelPacket.cs b/Source/MQTTnet/Packets/MqttPubRelPacket.cs index 9631da37..190fde9c 100644 --- a/Source/MQTTnet/Packets/MqttPubRelPacket.cs +++ b/Source/MQTTnet/Packets/MqttPubRelPacket.cs @@ -5,28 +5,27 @@ using System.Collections.Generic; using MQTTnet.Protocol; -namespace MQTTnet.Packets +namespace MQTTnet.Packets; + +public sealed class MqttPubRelPacket : MqttPacketWithIdentifier { - public sealed class MqttPubRelPacket : MqttPacketWithIdentifier + /// + /// Added in MQTTv5. + /// + public MqttPubRelReasonCode ReasonCode { get; set; } = MqttPubRelReasonCode.Success; + + /// + /// Added in MQTTv5. + /// + public string ReasonString { get; set; } + + /// + /// Added in MQTTv5. + /// + public List UserProperties { get; set; } + + public override string ToString() { - /// - /// Added in MQTTv5. - /// - public MqttPubRelReasonCode ReasonCode { get; set; } = MqttPubRelReasonCode.Success; - - /// - /// Added in MQTTv5. - /// - public string ReasonString { get; set; } - - /// - /// Added in MQTTv5. - /// - public List UserProperties { get; set; } - - public override string ToString() - { - return $"PubRel: [PacketIdentifier={PacketIdentifier}] [ReasonCode={ReasonCode}]"; - } + return $"PubRel: [PacketIdentifier={PacketIdentifier}] [ReasonCode={ReasonCode}]"; } } \ No newline at end of file diff --git a/Source/MQTTnet/Packets/MqttPublishPacket.cs b/Source/MQTTnet/Packets/MqttPublishPacket.cs index 9edc789e..11e70969 100644 --- a/Source/MQTTnet/Packets/MqttPublishPacket.cs +++ b/Source/MQTTnet/Packets/MqttPublishPacket.cs @@ -21,7 +21,7 @@ public sealed class MqttPublishPacket : MqttPacketWithIdentifier public MqttPayloadFormatIndicator PayloadFormatIndicator { get; set; } = MqttPayloadFormatIndicator.Unspecified; - public ArraySegment PayloadSegment { set { Payload = new ReadOnlySequence(value); } } + public ArraySegment PayloadSegment { set => Payload = new ReadOnlySequence(value); } public ReadOnlySequence Payload { get; set; } diff --git a/Source/MQTTnet/Packets/MqttSubAckPacket.cs b/Source/MQTTnet/Packets/MqttSubAckPacket.cs index e9887c81..013d7c25 100644 --- a/Source/MQTTnet/Packets/MqttSubAckPacket.cs +++ b/Source/MQTTnet/Packets/MqttSubAckPacket.cs @@ -6,30 +6,29 @@ using System.Collections.Generic; using System.Linq; using MQTTnet.Protocol; -namespace MQTTnet.Packets +namespace MQTTnet.Packets; + +public sealed class MqttSubAckPacket : MqttPacketWithIdentifier { - public sealed class MqttSubAckPacket : MqttPacketWithIdentifier + /// + /// Reason Code is used in MQTTv5.0.0 and backward compatible to v.3.1.1. Return Code is used in MQTTv3.1.1 + /// + public List ReasonCodes { get; set; } + + /// + /// Added in MQTTv5. + /// + public string ReasonString { get; set; } + + /// + /// Added in MQTTv5. + /// + public List UserProperties { get; set; } + + public override string ToString() { - /// - /// Reason Code is used in MQTTv5.0.0 and backward compatible to v.3.1.1. Return Code is used in MQTTv3.1.1 - /// - public List ReasonCodes { get; set; } + var reasonCodesText = string.Join(",", ReasonCodes.Select(f => f.ToString())); - /// - /// Added in MQTTv5. - /// - public string ReasonString { get; set; } - - /// - /// Added in MQTTv5. - /// - public List UserProperties { get; set; } - - public override string ToString() - { - var reasonCodesText = string.Join(",", ReasonCodes.Select(f => f.ToString())); - - return $"SubAck: [PacketIdentifier={PacketIdentifier}] [ReasonCode={reasonCodesText}]"; - } + return $"SubAck: [PacketIdentifier={PacketIdentifier}] [ReasonCode={reasonCodesText}]"; } } \ No newline at end of file diff --git a/Source/MQTTnet/Packets/MqttSubscribePacket.cs b/Source/MQTTnet/Packets/MqttSubscribePacket.cs index d1024f4b..853037ed 100644 --- a/Source/MQTTnet/Packets/MqttSubscribePacket.cs +++ b/Source/MQTTnet/Packets/MqttSubscribePacket.cs @@ -14,7 +14,7 @@ public sealed class MqttSubscribePacket : MqttPacketWithIdentifier /// public uint SubscriptionIdentifier { get; set; } - public List TopicFilters { get; set; } = new(); + public List TopicFilters { get; set; } = []; /// /// Added in MQTTv5. diff --git a/Source/MQTTnet/Packets/MqttTopicFilter.cs b/Source/MQTTnet/Packets/MqttTopicFilter.cs index 41876e71..196be2aa 100644 --- a/Source/MQTTnet/Packets/MqttTopicFilter.cs +++ b/Source/MQTTnet/Packets/MqttTopicFilter.cs @@ -4,52 +4,51 @@ using MQTTnet.Protocol; -namespace MQTTnet.Packets +namespace MQTTnet.Packets; + +public sealed class MqttTopicFilter { - public sealed class MqttTopicFilter + /// + /// Gets or sets a value indicating whether the sender will not receive its own published application messages. + /// MQTT 5.0.0+ feature. + /// + public bool NoLocal { get; set; } + + /// + /// Gets or sets the quality of service level. + /// The Quality of Service (QoS) level is an agreement between the sender of a message and the receiver of a message + /// that defines the guarantee of delivery for a specific message. + /// There are 3 QoS levels in MQTT: + /// - At most once (0): Message gets delivered no time, once or multiple times. + /// - At least once (1): Message gets delivered at least once (one time or more often). + /// - Exactly once (2): Message gets delivered exactly once (It's ensured that the message only comes once). + /// + public MqttQualityOfServiceLevel QualityOfServiceLevel { get; set; } + + /// + /// Gets or sets a value indicating whether messages are retained as published or not. + /// MQTT 5.0.0+ feature. + /// + public bool RetainAsPublished { get; set; } + + /// + /// Gets or sets the retain handling. + /// MQTT 5.0.0+ feature. + /// + public MqttRetainHandling RetainHandling { get; set; } = MqttRetainHandling.SendAtSubscribe; + + /// + /// Gets or sets the MQTT topic. + /// In MQTT, the word topic refers to an UTF-8 string that the broker uses to filter messages for each connected + /// client. + /// The topic consists of one or more topic levels. Each topic level is separated by a forward slash (topic level + /// separator). + /// + public string Topic { get; set; } + + public override string ToString() { - /// - /// Gets or sets a value indicating whether the sender will not receive its own published application messages. - /// MQTT 5.0.0+ feature. - /// - public bool NoLocal { get; set; } - - /// - /// Gets or sets the quality of service level. - /// The Quality of Service (QoS) level is an agreement between the sender of a message and the receiver of a message - /// that defines the guarantee of delivery for a specific message. - /// There are 3 QoS levels in MQTT: - /// - At most once (0): Message gets delivered no time, once or multiple times. - /// - At least once (1): Message gets delivered at least once (one time or more often). - /// - Exactly once (2): Message gets delivered exactly once (It's ensured that the message only comes once). - /// - public MqttQualityOfServiceLevel QualityOfServiceLevel { get; set; } - - /// - /// Gets or sets a value indicating whether messages are retained as published or not. - /// MQTT 5.0.0+ feature. - /// - public bool RetainAsPublished { get; set; } - - /// - /// Gets or sets the retain handling. - /// MQTT 5.0.0+ feature. - /// - public MqttRetainHandling RetainHandling { get; set; } = MqttRetainHandling.SendAtSubscribe; - - /// - /// Gets or sets the MQTT topic. - /// In MQTT, the word topic refers to an UTF-8 string that the broker uses to filter messages for each connected - /// client. - /// The topic consists of one or more topic levels. Each topic level is separated by a forward slash (topic level - /// separator). - /// - public string Topic { get; set; } - - public override string ToString() - { - return - $"TopicFilter: [Topic={Topic}] [QualityOfServiceLevel={QualityOfServiceLevel}] [NoLocal={NoLocal}] [RetainAsPublished={RetainAsPublished}] [RetainHandling={RetainHandling}]"; - } + return + $"TopicFilter: [Topic={Topic}] [QualityOfServiceLevel={QualityOfServiceLevel}] [NoLocal={NoLocal}] [RetainAsPublished={RetainAsPublished}] [RetainHandling={RetainHandling}]"; } } \ No newline at end of file diff --git a/Source/MQTTnet/Packets/MqttUnsubAckPacket.cs b/Source/MQTTnet/Packets/MqttUnsubAckPacket.cs index 1d2d32f5..1c79ebfb 100644 --- a/Source/MQTTnet/Packets/MqttUnsubAckPacket.cs +++ b/Source/MQTTnet/Packets/MqttUnsubAckPacket.cs @@ -6,34 +6,33 @@ using System.Collections.Generic; using System.Linq; using MQTTnet.Protocol; -namespace MQTTnet.Packets +namespace MQTTnet.Packets; + +public sealed class MqttUnsubAckPacket : MqttPacketWithIdentifier { - public sealed class MqttUnsubAckPacket : MqttPacketWithIdentifier + /// + /// Added in MQTTv5. + /// + public List ReasonCodes { get; set; } + + /// + /// Added in MQTTv5. + /// + public string ReasonString { get; set; } + + /// + /// Added in MQTTv5. + /// + public List UserProperties { get; set; } + + public override string ToString() { - /// - /// Added in MQTTv5. - /// - public List ReasonCodes { get; set; } - - /// - /// Added in MQTTv5. - /// - public string ReasonString { get; set; } - - /// - /// Added in MQTTv5. - /// - public List UserProperties { get; set; } - - public override string ToString() + var reasonCodesText = string.Empty; + if (ReasonCodes != null) { - var reasonCodesText = string.Empty; - if (ReasonCodes != null) - { - reasonCodesText = string.Join(",", ReasonCodes?.Select(f => f.ToString())); - } - - return $"UnsubAck: [PacketIdentifier={PacketIdentifier}] [ReasonCodes={reasonCodesText}] [ReasonString={ReasonString}]"; + reasonCodesText = string.Join(",", ReasonCodes?.Select(f => f.ToString())); } + + return $"UnsubAck: [PacketIdentifier={PacketIdentifier}] [ReasonCodes={reasonCodesText}] [ReasonString={ReasonString}]"; } } \ No newline at end of file diff --git a/Source/MQTTnet/Packets/MqttUnsubscribePacket.cs b/Source/MQTTnet/Packets/MqttUnsubscribePacket.cs index 698bb330..4053c1fe 100644 --- a/Source/MQTTnet/Packets/MqttUnsubscribePacket.cs +++ b/Source/MQTTnet/Packets/MqttUnsubscribePacket.cs @@ -8,7 +8,7 @@ namespace MQTTnet.Packets; public sealed class MqttUnsubscribePacket : MqttPacketWithIdentifier { - public List TopicFilters { get; set; } = new(); + public List TopicFilters { get; set; } = []; /// /// Added in MQTTv5. diff --git a/Source/MQTTnet/Packets/MqttUserProperty.cs b/Source/MQTTnet/Packets/MqttUserProperty.cs index 1486dd86..43cbad35 100644 --- a/Source/MQTTnet/Packets/MqttUserProperty.cs +++ b/Source/MQTTnet/Packets/MqttUserProperty.cs @@ -18,9 +18,9 @@ public sealed class MqttUserProperty public string Value { get; } - public override bool Equals(object other) + public override bool Equals(object obj) { - return Equals(other as MqttUserProperty); + return Equals(obj as MqttUserProperty); } public bool Equals(MqttUserProperty other) diff --git a/Source/MQTTnet/Protocol/MqttAuthenticateReasonCode.cs b/Source/MQTTnet/Protocol/MqttAuthenticateReasonCode.cs index 1f59e1dd..8834608d 100644 --- a/Source/MQTTnet/Protocol/MqttAuthenticateReasonCode.cs +++ b/Source/MQTTnet/Protocol/MqttAuthenticateReasonCode.cs @@ -2,12 +2,11 @@ // The .NET Foundation licenses this file to you under the MIT license. // See the LICENSE file in the project root for more information. -namespace MQTTnet.Protocol +namespace MQTTnet.Protocol; + +public enum MqttAuthenticateReasonCode { - public enum MqttAuthenticateReasonCode - { - Success = 0, - ContinueAuthentication = 24, - ReAuthenticate = 25 - } -} + Success = 0, + ContinueAuthentication = 24, + ReAuthenticate = 25 +} \ No newline at end of file diff --git a/Source/MQTTnet/Protocol/MqttConnectReasonCode.cs b/Source/MQTTnet/Protocol/MqttConnectReasonCode.cs index 74977df9..929d3df7 100644 --- a/Source/MQTTnet/Protocol/MqttConnectReasonCode.cs +++ b/Source/MQTTnet/Protocol/MqttConnectReasonCode.cs @@ -2,31 +2,30 @@ // The .NET Foundation licenses this file to you under the MIT license. // See the LICENSE file in the project root for more information. -namespace MQTTnet.Protocol +namespace MQTTnet.Protocol; + +public enum MqttConnectReasonCode { - public enum MqttConnectReasonCode - { - Success = 0, - UnspecifiedError = 128, - MalformedPacket = 129, - ProtocolError = 130, - ImplementationSpecificError = 131, - UnsupportedProtocolVersion = 132, - ClientIdentifierNotValid = 133, - BadUserNameOrPassword = 134, - NotAuthorized = 135, - ServerUnavailable = 136, - ServerBusy = 137, - Banned = 138, - BadAuthenticationMethod = 140, - TopicNameInvalid = 144, - PacketTooLarge = 149, - QuotaExceeded = 151, - PayloadFormatInvalid = 153, - RetainNotSupported = 154, - QoSNotSupported = 155, - UseAnotherServer = 156, - ServerMoved = 157, - ConnectionRateExceeded = 159 - } -} + Success = 0, + UnspecifiedError = 128, + MalformedPacket = 129, + ProtocolError = 130, + ImplementationSpecificError = 131, + UnsupportedProtocolVersion = 132, + ClientIdentifierNotValid = 133, + BadUserNameOrPassword = 134, + NotAuthorized = 135, + ServerUnavailable = 136, + ServerBusy = 137, + Banned = 138, + BadAuthenticationMethod = 140, + TopicNameInvalid = 144, + PacketTooLarge = 149, + QuotaExceeded = 151, + PayloadFormatInvalid = 153, + RetainNotSupported = 154, + QoSNotSupported = 155, + UseAnotherServer = 156, + ServerMoved = 157, + ConnectionRateExceeded = 159 +} \ No newline at end of file diff --git a/Source/MQTTnet/Protocol/MqttConnectReturnCode.cs b/Source/MQTTnet/Protocol/MqttConnectReturnCode.cs index 0687fe70..5b5c7fa4 100644 --- a/Source/MQTTnet/Protocol/MqttConnectReturnCode.cs +++ b/Source/MQTTnet/Protocol/MqttConnectReturnCode.cs @@ -2,15 +2,14 @@ // The .NET Foundation licenses this file to you under the MIT license. // See the LICENSE file in the project root for more information. -namespace MQTTnet.Protocol +namespace MQTTnet.Protocol; + +public enum MqttConnectReturnCode { - public enum MqttConnectReturnCode - { - ConnectionAccepted = 0x00, - ConnectionRefusedUnacceptableProtocolVersion = 0x01, - ConnectionRefusedIdentifierRejected = 0x02, - ConnectionRefusedServerUnavailable = 0x03, - ConnectionRefusedBadUsernameOrPassword = 0x04, - ConnectionRefusedNotAuthorized = 0x05 - } -} + ConnectionAccepted = 0x00, + ConnectionRefusedUnacceptableProtocolVersion = 0x01, + ConnectionRefusedIdentifierRejected = 0x02, + ConnectionRefusedServerUnavailable = 0x03, + ConnectionRefusedBadUsernameOrPassword = 0x04, + ConnectionRefusedNotAuthorized = 0x05 +} \ No newline at end of file diff --git a/Source/MQTTnet/Protocol/MqttControlPacketType.cs b/Source/MQTTnet/Protocol/MqttControlPacketType.cs index 37a3fc5d..d2d312e2 100644 --- a/Source/MQTTnet/Protocol/MqttControlPacketType.cs +++ b/Source/MQTTnet/Protocol/MqttControlPacketType.cs @@ -2,24 +2,23 @@ // The .NET Foundation licenses this file to you under the MIT license. // See the LICENSE file in the project root for more information. -namespace MQTTnet.Protocol +namespace MQTTnet.Protocol; + +public enum MqttControlPacketType { - public enum MqttControlPacketType - { - Connect = 1, - ConnAck = 2, - Publish = 3, - PubAck = 4, - PubRec = 5, - PubRel = 6, - PubComp = 7, - Subscribe = 8, - SubAck = 9, - Unsubscribe = 10, - UnsubAck = 11, - PingReq = 12, - PingResp = 13, - Disconnect = 14, - Auth = 15 - } -} + Connect = 1, + ConnAck = 2, + Publish = 3, + PubAck = 4, + PubRec = 5, + PubRel = 6, + PubComp = 7, + Subscribe = 8, + SubAck = 9, + Unsubscribe = 10, + UnsubAck = 11, + PingReq = 12, + PingResp = 13, + Disconnect = 14, + Auth = 15 +} \ No newline at end of file diff --git a/Source/MQTTnet/Protocol/MqttDisconnectReasonCode.cs b/Source/MQTTnet/Protocol/MqttDisconnectReasonCode.cs index b3307f7a..4e9849df 100644 --- a/Source/MQTTnet/Protocol/MqttDisconnectReasonCode.cs +++ b/Source/MQTTnet/Protocol/MqttDisconnectReasonCode.cs @@ -2,38 +2,37 @@ // The .NET Foundation licenses this file to you under the MIT license. // See the LICENSE file in the project root for more information. -namespace MQTTnet.Protocol +namespace MQTTnet.Protocol; + +public enum MqttDisconnectReasonCode { - public enum MqttDisconnectReasonCode - { - NormalDisconnection = 0, - DisconnectWithWillMessage = 4, - UnspecifiedError = 128, - MalformedPacket = 129, - ProtocolError = 130, - ImplementationSpecificError = 131, - NotAuthorized = 135, - ServerBusy = 137, - ServerShuttingDown = 139, - KeepAliveTimeout = 141, - SessionTakenOver = 142, - TopicFilterInvalid = 143, - TopicNameInvalid = 144, - ReceiveMaximumExceeded = 147, - TopicAliasInvalid = 148, - PacketTooLarge = 149, - MessageRateTooHigh = 150, - QuotaExceeded = 151, - AdministrativeAction = 152, - PayloadFormatInvalid = 153, - RetainNotSupported = 154, - QoSNotSupported = 155, - UseAnotherServer = 156, - ServerMoved = 157, - SharedSubscriptionsNotSupported = 158, - ConnectionRateExceeded = 159, - MaximumConnectTime = 160, - SubscriptionIdentifiersNotSupported = 161, - WildcardSubscriptionsNotSupported = 162 - } -} + NormalDisconnection = 0, + DisconnectWithWillMessage = 4, + UnspecifiedError = 128, + MalformedPacket = 129, + ProtocolError = 130, + ImplementationSpecificError = 131, + NotAuthorized = 135, + ServerBusy = 137, + ServerShuttingDown = 139, + KeepAliveTimeout = 141, + SessionTakenOver = 142, + TopicFilterInvalid = 143, + TopicNameInvalid = 144, + ReceiveMaximumExceeded = 147, + TopicAliasInvalid = 148, + PacketTooLarge = 149, + MessageRateTooHigh = 150, + QuotaExceeded = 151, + AdministrativeAction = 152, + PayloadFormatInvalid = 153, + RetainNotSupported = 154, + QoSNotSupported = 155, + UseAnotherServer = 156, + ServerMoved = 157, + SharedSubscriptionsNotSupported = 158, + ConnectionRateExceeded = 159, + MaximumConnectTime = 160, + SubscriptionIdentifiersNotSupported = 161, + WildcardSubscriptionsNotSupported = 162 +} \ No newline at end of file diff --git a/Source/MQTTnet/Protocol/MqttPayloadFormatIndicator.cs b/Source/MQTTnet/Protocol/MqttPayloadFormatIndicator.cs index a4446c4b..f43a22e9 100644 --- a/Source/MQTTnet/Protocol/MqttPayloadFormatIndicator.cs +++ b/Source/MQTTnet/Protocol/MqttPayloadFormatIndicator.cs @@ -2,11 +2,10 @@ // The .NET Foundation licenses this file to you under the MIT license. // See the LICENSE file in the project root for more information. -namespace MQTTnet.Protocol +namespace MQTTnet.Protocol; + +public enum MqttPayloadFormatIndicator { - public enum MqttPayloadFormatIndicator - { - Unspecified = 0, - CharacterData = 1 - } -} + Unspecified = 0, + CharacterData = 1 +} \ No newline at end of file diff --git a/Source/MQTTnet/Protocol/MqttPorts.cs b/Source/MQTTnet/Protocol/MqttPorts.cs index b2ea8f04..74dc2786 100644 --- a/Source/MQTTnet/Protocol/MqttPorts.cs +++ b/Source/MQTTnet/Protocol/MqttPorts.cs @@ -2,12 +2,11 @@ // The .NET Foundation licenses this file to you under the MIT license. // See the LICENSE file in the project root for more information. -namespace MQTTnet.Protocol -{ - public static class MqttPorts - { - public const int Default = 1883; +namespace MQTTnet.Protocol; - public const int Secure = 8883; - } +public static class MqttPorts +{ + public const int Default = 1883; + + public const int Secure = 8883; } \ No newline at end of file diff --git a/Source/MQTTnet/Protocol/MqttPropertyId.cs b/Source/MQTTnet/Protocol/MqttPropertyId.cs index bce6c230..4113dc4f 100644 --- a/Source/MQTTnet/Protocol/MqttPropertyId.cs +++ b/Source/MQTTnet/Protocol/MqttPropertyId.cs @@ -2,38 +2,37 @@ // The .NET Foundation licenses this file to you under the MIT license. // See the LICENSE file in the project root for more information. -namespace MQTTnet.Protocol +namespace MQTTnet.Protocol; + +public enum MqttPropertyId { - public enum MqttPropertyId - { - None = 0, - - PayloadFormatIndicator = 1, - MessageExpiryInterval = 2, - ContentType = 3, - ResponseTopic = 8, - CorrelationData = 9, - SubscriptionIdentifier = 11, - SessionExpiryInterval = 17, - AssignedClientIdentifier = 18, - ServerKeepAlive = 19, - AuthenticationMethod = 21, - AuthenticationData = 22, - RequestProblemInformation = 23, - WillDelayInterval = 24, - RequestResponseInformation = 25, - ResponseInformation = 26, - ServerReference = 28, - ReasonString = 31, - ReceiveMaximum = 33, - TopicAliasMaximum = 34, - TopicAlias = 35, - MaximumQoS = 36, - RetainAvailable = 37, - UserProperty = 38, - MaximumPacketSize = 39, - WildcardSubscriptionAvailable = 40, - SubscriptionIdentifiersAvailable = 41, - SharedSubscriptionAvailable = 42 - } -} + None = 0, + + PayloadFormatIndicator = 1, + MessageExpiryInterval = 2, + ContentType = 3, + ResponseTopic = 8, + CorrelationData = 9, + SubscriptionIdentifier = 11, + SessionExpiryInterval = 17, + AssignedClientIdentifier = 18, + ServerKeepAlive = 19, + AuthenticationMethod = 21, + AuthenticationData = 22, + RequestProblemInformation = 23, + WillDelayInterval = 24, + RequestResponseInformation = 25, + ResponseInformation = 26, + ServerReference = 28, + ReasonString = 31, + ReceiveMaximum = 33, + TopicAliasMaximum = 34, + TopicAlias = 35, + MaximumQoS = 36, + RetainAvailable = 37, + UserProperty = 38, + MaximumPacketSize = 39, + WildcardSubscriptionAvailable = 40, + SubscriptionIdentifiersAvailable = 41, + SharedSubscriptionAvailable = 42 +} \ No newline at end of file diff --git a/Source/MQTTnet/Protocol/MqttPubAckReasonCode.cs b/Source/MQTTnet/Protocol/MqttPubAckReasonCode.cs index 50de497f..f596c019 100644 --- a/Source/MQTTnet/Protocol/MqttPubAckReasonCode.cs +++ b/Source/MQTTnet/Protocol/MqttPubAckReasonCode.cs @@ -2,23 +2,22 @@ // The .NET Foundation licenses this file to you under the MIT license. // See the LICENSE file in the project root for more information. -namespace MQTTnet.Protocol +namespace MQTTnet.Protocol; + +public enum MqttPubAckReasonCode { - public enum MqttPubAckReasonCode - { - Success = 0, - - /// - /// The message is accepted but there are no subscribers. This is sent only by the Server. If the Server knows that there are no matching subscribers, it MAY use this Reason Code instead of 0x00 (Success). - /// - NoMatchingSubscribers = 16, - - UnspecifiedError = 128, - ImplementationSpecificError = 131, - NotAuthorized = 135, - TopicNameInvalid = 144, - PacketIdentifierInUse = 145, - QuotaExceeded = 151, - PayloadFormatInvalid = 153 - } -} + Success = 0, + + /// + /// The message is accepted but there are no subscribers. This is sent only by the Server. If the Server knows that there are no matching subscribers, it MAY use this Reason Code instead of 0x00 (Success). + /// + NoMatchingSubscribers = 16, + + UnspecifiedError = 128, + ImplementationSpecificError = 131, + NotAuthorized = 135, + TopicNameInvalid = 144, + PacketIdentifierInUse = 145, + QuotaExceeded = 151, + PayloadFormatInvalid = 153 +} \ No newline at end of file diff --git a/Source/MQTTnet/Protocol/MqttPubCompReasonCode.cs b/Source/MQTTnet/Protocol/MqttPubCompReasonCode.cs index 464b90a2..a4ec6f68 100644 --- a/Source/MQTTnet/Protocol/MqttPubCompReasonCode.cs +++ b/Source/MQTTnet/Protocol/MqttPubCompReasonCode.cs @@ -2,11 +2,10 @@ // The .NET Foundation licenses this file to you under the MIT license. // See the LICENSE file in the project root for more information. -namespace MQTTnet.Protocol +namespace MQTTnet.Protocol; + +public enum MqttPubCompReasonCode { - public enum MqttPubCompReasonCode - { - Success = 0, - PacketIdentifierNotFound = 146 - } -} + Success = 0, + PacketIdentifierNotFound = 146 +} \ No newline at end of file diff --git a/Source/MQTTnet/Protocol/MqttPubRecReasonCode.cs b/Source/MQTTnet/Protocol/MqttPubRecReasonCode.cs index 9299b643..954990ae 100644 --- a/Source/MQTTnet/Protocol/MqttPubRecReasonCode.cs +++ b/Source/MQTTnet/Protocol/MqttPubRecReasonCode.cs @@ -2,18 +2,17 @@ // The .NET Foundation licenses this file to you under the MIT license. // See the LICENSE file in the project root for more information. -namespace MQTTnet.Protocol +namespace MQTTnet.Protocol; + +public enum MqttPubRecReasonCode { - public enum MqttPubRecReasonCode - { - Success = 0, - NoMatchingSubscribers = 16, - UnspecifiedError = 128, - ImplementationSpecificError = 131, - NotAuthorized = 135, - TopicNameInvalid = 144, - PacketIdentifierInUse = 145, - QuotaExceeded = 151, - PayloadFormatInvalid = 153 - } -} + Success = 0, + NoMatchingSubscribers = 16, + UnspecifiedError = 128, + ImplementationSpecificError = 131, + NotAuthorized = 135, + TopicNameInvalid = 144, + PacketIdentifierInUse = 145, + QuotaExceeded = 151, + PayloadFormatInvalid = 153 +} \ No newline at end of file diff --git a/Source/MQTTnet/Protocol/MqttPubRelReasonCode.cs b/Source/MQTTnet/Protocol/MqttPubRelReasonCode.cs index 5bca4b48..1a49d983 100644 --- a/Source/MQTTnet/Protocol/MqttPubRelReasonCode.cs +++ b/Source/MQTTnet/Protocol/MqttPubRelReasonCode.cs @@ -2,11 +2,10 @@ // The .NET Foundation licenses this file to you under the MIT license. // See the LICENSE file in the project root for more information. -namespace MQTTnet.Protocol +namespace MQTTnet.Protocol; + +public enum MqttPubRelReasonCode { - public enum MqttPubRelReasonCode - { - Success = 0, - PacketIdentifierNotFound = 146 - } -} + Success = 0, + PacketIdentifierNotFound = 146 +} \ No newline at end of file diff --git a/Source/MQTTnet/Protocol/MqttQualityOfServiceLevel.cs b/Source/MQTTnet/Protocol/MqttQualityOfServiceLevel.cs index e4134b7a..ba2260d8 100644 --- a/Source/MQTTnet/Protocol/MqttQualityOfServiceLevel.cs +++ b/Source/MQTTnet/Protocol/MqttQualityOfServiceLevel.cs @@ -2,12 +2,11 @@ // The .NET Foundation licenses this file to you under the MIT license. // See the LICENSE file in the project root for more information. -namespace MQTTnet.Protocol +namespace MQTTnet.Protocol; + +public enum MqttQualityOfServiceLevel { - public enum MqttQualityOfServiceLevel - { - AtMostOnce = 0x00, - AtLeastOnce = 0x01, - ExactlyOnce = 0x02 - } -} + AtMostOnce = 0x00, + AtLeastOnce = 0x01, + ExactlyOnce = 0x02 +} \ No newline at end of file diff --git a/Source/MQTTnet/Protocol/MqttRetainHandling.cs b/Source/MQTTnet/Protocol/MqttRetainHandling.cs index 7604ea56..de7a3ba1 100644 --- a/Source/MQTTnet/Protocol/MqttRetainHandling.cs +++ b/Source/MQTTnet/Protocol/MqttRetainHandling.cs @@ -2,14 +2,13 @@ // The .NET Foundation licenses this file to you under the MIT license. // See the LICENSE file in the project root for more information. -namespace MQTTnet.Protocol +namespace MQTTnet.Protocol; + +public enum MqttRetainHandling { - public enum MqttRetainHandling - { - SendAtSubscribe = 0, - - SendAtSubscribeIfNewSubscriptionOnly = 1, - - DoNotSendOnSubscribe = 2 - } -} + SendAtSubscribe = 0, + + SendAtSubscribeIfNewSubscriptionOnly = 1, + + DoNotSendOnSubscribe = 2 +} \ No newline at end of file diff --git a/Source/MQTTnet/Protocol/MqttSubscribeReasonCode.cs b/Source/MQTTnet/Protocol/MqttSubscribeReasonCode.cs index 48cc7f83..71499fec 100644 --- a/Source/MQTTnet/Protocol/MqttSubscribeReasonCode.cs +++ b/Source/MQTTnet/Protocol/MqttSubscribeReasonCode.cs @@ -2,24 +2,23 @@ // The .NET Foundation licenses this file to you under the MIT license. // See the LICENSE file in the project root for more information. -namespace MQTTnet.Protocol +namespace MQTTnet.Protocol; + +public enum MqttSubscribeReasonCode { - public enum MqttSubscribeReasonCode - { - // Compatible with MQTTv3.1.1. - GrantedQoS0 = 0x00, - GrantedQoS1 = 0x01, - GrantedQoS2 = 0x02, - UnspecifiedError = 0x80, - - // New in MQTTv5. - ImplementationSpecificError = 131, - NotAuthorized = 135, - TopicFilterInvalid = 143, - PacketIdentifierInUse = 145, - QuotaExceeded = 151, - SharedSubscriptionsNotSupported = 158, - SubscriptionIdentifiersNotSupported = 161, - WildcardSubscriptionsNotSupported = 162 - } -} + // Compatible with MQTTv3.1.1. + GrantedQoS0 = 0x00, + GrantedQoS1 = 0x01, + GrantedQoS2 = 0x02, + UnspecifiedError = 0x80, + + // New in MQTTv5. + ImplementationSpecificError = 131, + NotAuthorized = 135, + TopicFilterInvalid = 143, + PacketIdentifierInUse = 145, + QuotaExceeded = 151, + SharedSubscriptionsNotSupported = 158, + SubscriptionIdentifiersNotSupported = 161, + WildcardSubscriptionsNotSupported = 162 +} \ No newline at end of file diff --git a/Source/MQTTnet/Protocol/MqttSubscribeReturnCode.cs b/Source/MQTTnet/Protocol/MqttSubscribeReturnCode.cs index da569b6a..34ed9137 100644 --- a/Source/MQTTnet/Protocol/MqttSubscribeReturnCode.cs +++ b/Source/MQTTnet/Protocol/MqttSubscribeReturnCode.cs @@ -2,13 +2,12 @@ // The .NET Foundation licenses this file to you under the MIT license. // See the LICENSE file in the project root for more information. -namespace MQTTnet.Protocol +namespace MQTTnet.Protocol; + +public enum MqttSubscribeReturnCode { - public enum MqttSubscribeReturnCode - { - SuccessMaximumQoS0 = 0x00, - SuccessMaximumQoS1 = 0x01, - SuccessMaximumQoS2 = 0x02, - Failure = 0x80 - } -} + SuccessMaximumQoS0 = 0x00, + SuccessMaximumQoS1 = 0x01, + SuccessMaximumQoS2 = 0x02, + Failure = 0x80 +} \ No newline at end of file diff --git a/Source/MQTTnet/Protocol/MqttTopicValidator.cs b/Source/MQTTnet/Protocol/MqttTopicValidator.cs index efdeeafa..32baef71 100644 --- a/Source/MQTTnet/Protocol/MqttTopicValidator.cs +++ b/Source/MQTTnet/Protocol/MqttTopicValidator.cs @@ -5,53 +5,52 @@ using System; using MQTTnet.Exceptions; -namespace MQTTnet.Protocol +namespace MQTTnet.Protocol; + +public static class MqttTopicValidator { - public static class MqttTopicValidator + public static void ThrowIfInvalid(MqttApplicationMessage applicationMessage) { - public static void ThrowIfInvalid(MqttApplicationMessage applicationMessage) - { - ArgumentNullException.ThrowIfNull(applicationMessage); + ArgumentNullException.ThrowIfNull(applicationMessage); - if (applicationMessage.TopicAlias == 0) - { - ThrowIfInvalid(applicationMessage.Topic); - } + if (applicationMessage.TopicAlias == 0) + { + ThrowIfInvalid(applicationMessage.Topic); + } + } + + public static void ThrowIfInvalid(string topic) + { + if (string.IsNullOrEmpty(topic)) + { + throw new MqttProtocolViolationException("Topic should not be empty."); } - public static void ThrowIfInvalid(string topic) + foreach (var @char in topic) { - if (string.IsNullOrEmpty(topic)) + if (@char == '+') { - throw new MqttProtocolViolationException("Topic should not be empty."); + throw new MqttProtocolViolationException("The character '+' is not allowed in topics."); } - foreach (var @char in topic) + if (@char == '#') { - if (@char == '+') - { - throw new MqttProtocolViolationException("The character '+' is not allowed in topics."); - } - - if (@char == '#') - { - throw new MqttProtocolViolationException("The character '#' is not allowed in topics."); - } - } - } - - public static void ThrowIfInvalidSubscribe(string topic) - { - if (string.IsNullOrEmpty(topic)) - { - throw new MqttProtocolViolationException("Topic should not be empty."); - } - - var indexOfHash = topic.IndexOf("#", StringComparison.Ordinal); - if (indexOfHash != -1 && indexOfHash != topic.Length - 1) - { - throw new MqttProtocolViolationException("The character '#' is only allowed as last character"); + throw new MqttProtocolViolationException("The character '#' is not allowed in topics."); } } } + + public static void ThrowIfInvalidSubscribe(string topic) + { + if (string.IsNullOrEmpty(topic)) + { + throw new MqttProtocolViolationException("Topic should not be empty."); + } + + var indexOfHash = topic.IndexOf('#', StringComparison.Ordinal); + if (indexOfHash != -1 && indexOfHash != topic.Length - 1) + { + throw new MqttProtocolViolationException("The character '#' is only allowed as last character"); + } + } } \ No newline at end of file diff --git a/Source/MQTTnet/Protocol/MqttUnsubscribeReasonCode.cs b/Source/MQTTnet/Protocol/MqttUnsubscribeReasonCode.cs index cd85c8ed..957282b5 100644 --- a/Source/MQTTnet/Protocol/MqttUnsubscribeReasonCode.cs +++ b/Source/MQTTnet/Protocol/MqttUnsubscribeReasonCode.cs @@ -2,16 +2,15 @@ // The .NET Foundation licenses this file to you under the MIT license. // See the LICENSE file in the project root for more information. -namespace MQTTnet.Protocol +namespace MQTTnet.Protocol; + +public enum MqttUnsubscribeReasonCode { - public enum MqttUnsubscribeReasonCode - { - Success = 0, - NoSubscriptionExisted = 17, - UnspecifiedError = 128, - ImplementationSpecificError = 131, - NotAuthorized = 135, - TopicFilterInvalid = 143, - PacketIdentifierInUse = 145 - } -} + Success = 0, + NoSubscriptionExisted = 17, + UnspecifiedError = 128, + ImplementationSpecificError = 131, + NotAuthorized = 135, + TopicFilterInvalid = 143, + PacketIdentifierInUse = 145 +} \ No newline at end of file diff --git a/Source/MQTTnet/Publishing/MqttClientPublishResult.cs b/Source/MQTTnet/Publishing/MqttClientPublishResult.cs index 89fd676a..c85a1a3c 100644 --- a/Source/MQTTnet/Publishing/MqttClientPublishResult.cs +++ b/Source/MQTTnet/Publishing/MqttClientPublishResult.cs @@ -23,7 +23,7 @@ public sealed class MqttClientPublishResult /// topic but overall transmit /// to the server etc. was a success. /// - public bool IsSuccess => ReasonCode == MqttClientPublishReasonCode.Success || ReasonCode == MqttClientPublishReasonCode.NoMatchingSubscribers; + public bool IsSuccess => ReasonCode is MqttClientPublishReasonCode.Success or MqttClientPublishReasonCode.NoMatchingSubscribers; /// /// Gets the packet identifier which was used for this publish. diff --git a/Source/MQTTnet/Publishing/MqttClientPublishResultFactory.cs b/Source/MQTTnet/Publishing/MqttClientPublishResultFactory.cs index 4109cc6f..7a947ef9 100644 --- a/Source/MQTTnet/Publishing/MqttClientPublishResultFactory.cs +++ b/Source/MQTTnet/Publishing/MqttClientPublishResultFactory.cs @@ -10,10 +10,10 @@ namespace MQTTnet; public sealed class MqttClientPublishResultFactory { - static readonly IReadOnlyCollection EmptyUserProperties = new List(); + static readonly IReadOnlyCollection EmptyUserProperties = []; static readonly MqttClientPublishResult AtMostOnceSuccessResult = new(null, MqttClientPublishReasonCode.Success, null, EmptyUserProperties); - public MqttClientPublishResult Create(MqttPubAckPacket pubAckPacket) + public static MqttClientPublishResult Create(MqttPubAckPacket pubAckPacket) { // QoS 0 has no response. So we treat it as a success always. if (pubAckPacket == null) @@ -31,7 +31,7 @@ public sealed class MqttClientPublishResultFactory return result; } - public MqttClientPublishResult Create(MqttPubRecPacket pubRecPacket, MqttPubCompPacket pubCompPacket) + public static MqttClientPublishResult Create(MqttPubRecPacket pubRecPacket, MqttPubCompPacket pubCompPacket) { if (pubRecPacket == null || pubCompPacket == null) { diff --git a/Source/MQTTnet/Receiving/MqttApplicationMessageReceivedEventArgs.cs b/Source/MQTTnet/Receiving/MqttApplicationMessageReceivedEventArgs.cs index e323bdc3..d1a2c499 100644 --- a/Source/MQTTnet/Receiving/MqttApplicationMessageReceivedEventArgs.cs +++ b/Source/MQTTnet/Receiving/MqttApplicationMessageReceivedEventArgs.cs @@ -71,7 +71,7 @@ public sealed class MqttApplicationMessageReceivedEventArgs : EventArgs /// /// Gets or sets the user properties which will be sent to the server in the ACK packet etc. /// - public List ResponseUserProperties { get; } = new(); + public List ResponseUserProperties { get; } = []; public object Tag { get; set; } diff --git a/Source/MQTTnet/Subscribing/MqttClientSubscribeOptions.cs b/Source/MQTTnet/Subscribing/MqttClientSubscribeOptions.cs index b26eb01d..2b1434a6 100644 --- a/Source/MQTTnet/Subscribing/MqttClientSubscribeOptions.cs +++ b/Source/MQTTnet/Subscribing/MqttClientSubscribeOptions.cs @@ -24,7 +24,7 @@ public sealed class MqttClientSubscribeOptions /// Gets or sets a list of topic filters the client wants to subscribe to. /// Topic filters can include regular topics or wild cards. /// - public List TopicFilters { get; set; } = new(); + public List TopicFilters { get; set; } = []; /// /// Gets or sets the user properties. diff --git a/Source/MQTTnet/Subscribing/MqttClientSubscribeOptionsBuilder.cs b/Source/MQTTnet/Subscribing/MqttClientSubscribeOptionsBuilder.cs index 486b86cc..0fdd3f23 100644 --- a/Source/MQTTnet/Subscribing/MqttClientSubscribeOptionsBuilder.cs +++ b/Source/MQTTnet/Subscribing/MqttClientSubscribeOptionsBuilder.cs @@ -3,7 +3,6 @@ // See the LICENSE file in the project root for more information. using System; -using System.Collections.Generic; using MQTTnet.Exceptions; using MQTTnet.Packets; using MQTTnet.Protocol; @@ -69,11 +68,7 @@ public sealed class MqttClientSubscribeOptionsBuilder { ArgumentNullException.ThrowIfNull(topicFilter); - if (_subscribeOptions.TopicFilters == null) - { - _subscribeOptions.TopicFilters = new List(); - } - + _subscribeOptions.TopicFilters ??= []; _subscribeOptions.TopicFilters.Add(topicFilter); return this; @@ -85,11 +80,7 @@ public sealed class MqttClientSubscribeOptionsBuilder /// public MqttClientSubscribeOptionsBuilder WithUserProperty(string name, string value) { - if (_subscribeOptions.UserProperties == null) - { - _subscribeOptions.UserProperties = new List(); - } - + _subscribeOptions.UserProperties ??= []; _subscribeOptions.UserProperties.Add(new MqttUserProperty(name, value)); return this; diff --git a/Source/MQTTnet/Subscribing/MqttClientSubscribeOptionsValidator.cs b/Source/MQTTnet/Subscribing/MqttClientSubscribeOptionsValidator.cs index c490642b..80153960 100644 --- a/Source/MQTTnet/Subscribing/MqttClientSubscribeOptionsValidator.cs +++ b/Source/MQTTnet/Subscribing/MqttClientSubscribeOptionsValidator.cs @@ -21,7 +21,7 @@ public static class MqttClientSubscribeOptionsValidator return; } - if (options.UserProperties?.Any() == true) + if (options.UserProperties?.Count > 0) { Throw(nameof(options.UserProperties)); } diff --git a/Source/MQTTnet/Subscribing/MqttClientSubscribeResultFactory.cs b/Source/MQTTnet/Subscribing/MqttClientSubscribeResultFactory.cs index b7ec5fde..e21636c1 100644 --- a/Source/MQTTnet/Subscribing/MqttClientSubscribeResultFactory.cs +++ b/Source/MQTTnet/Subscribing/MqttClientSubscribeResultFactory.cs @@ -4,7 +4,6 @@ using System; using System.Collections.Generic; -using System.Linq; using MQTTnet.Exceptions; using MQTTnet.Packets; @@ -12,15 +11,15 @@ namespace MQTTnet; public sealed class MqttClientSubscribeResultFactory { - static readonly IReadOnlyCollection EmptyUserProperties = new List(); + static readonly IReadOnlyCollection EmptyUserProperties = []; - public MqttClientSubscribeResult Create(MqttSubscribePacket subscribePacket, MqttSubAckPacket subAckPacket) + public static MqttClientSubscribeResult Create(MqttSubscribePacket subscribePacket, MqttSubAckPacket subAckPacket) { ArgumentNullException.ThrowIfNull(subscribePacket); ArgumentNullException.ThrowIfNull(subAckPacket); // MQTTv5.0.0 handling. - if (subAckPacket.ReasonCodes.Any() && subAckPacket.ReasonCodes.Count != subscribePacket.TopicFilters.Count) + if (subAckPacket.ReasonCodes.Count != 0 && subAckPacket.ReasonCodes.Count != subscribePacket.TopicFilters.Count) { throw new MqttProtocolViolationException("The reason codes are not matching the topic filters [MQTT-3.9.3-1]."); } diff --git a/Source/MQTTnet/Unsubscribing/MqttClientUnsubscribeOptions.cs b/Source/MQTTnet/Unsubscribing/MqttClientUnsubscribeOptions.cs index 3c0f61bc..f2c0e7ba 100644 --- a/Source/MQTTnet/Unsubscribing/MqttClientUnsubscribeOptions.cs +++ b/Source/MQTTnet/Unsubscribing/MqttClientUnsubscribeOptions.cs @@ -13,7 +13,7 @@ public sealed class MqttClientUnsubscribeOptions /// Gets or sets a list of topic filters the client wants to unsubscribe from. /// Topic filters can include regular topics or wild cards. /// - public List TopicFilters { get; set; } = new(); + public List TopicFilters { get; set; } = []; /// /// Gets or sets the user properties. diff --git a/Source/MQTTnet/Unsubscribing/MqttClientUnsubscribeOptionsBuilder.cs b/Source/MQTTnet/Unsubscribing/MqttClientUnsubscribeOptionsBuilder.cs index c17f3081..f77fc49d 100644 --- a/Source/MQTTnet/Unsubscribing/MqttClientUnsubscribeOptionsBuilder.cs +++ b/Source/MQTTnet/Unsubscribing/MqttClientUnsubscribeOptionsBuilder.cs @@ -3,7 +3,6 @@ // See the LICENSE file in the project root for more information. using System; -using System.Collections.Generic; using MQTTnet.Packets; namespace MQTTnet; @@ -21,11 +20,7 @@ public sealed class MqttClientUnsubscribeOptionsBuilder { ArgumentNullException.ThrowIfNull(topic); - if (_unsubscribeOptions.TopicFilters is null) - { - _unsubscribeOptions.TopicFilters = new List(); - } - + _unsubscribeOptions.TopicFilters ??= []; _unsubscribeOptions.TopicFilters.Add(topic); return this; @@ -55,10 +50,7 @@ public sealed class MqttClientUnsubscribeOptionsBuilder { ArgumentNullException.ThrowIfNull(userProperty); - if (_unsubscribeOptions.UserProperties is null) - { - _unsubscribeOptions.UserProperties = new List(); - } + _unsubscribeOptions.UserProperties ??= []; _unsubscribeOptions.UserProperties.Add(userProperty); diff --git a/Source/MQTTnet/Unsubscribing/MqttClientUnsubscribeOptionsValidator.cs b/Source/MQTTnet/Unsubscribing/MqttClientUnsubscribeOptionsValidator.cs index 78fbb8ed..11fb9271 100644 --- a/Source/MQTTnet/Unsubscribing/MqttClientUnsubscribeOptionsValidator.cs +++ b/Source/MQTTnet/Unsubscribing/MqttClientUnsubscribeOptionsValidator.cs @@ -3,7 +3,6 @@ // See the LICENSE file in the project root for more information. using System; -using System.Linq; using MQTTnet.Formatter; namespace MQTTnet; @@ -20,7 +19,7 @@ public static class MqttClientUnsubscribeOptionsValidator return; } - if (options.UserProperties?.Any() == true) + if (options.UserProperties?.Count > 0) { Throw(nameof(options.UserProperties)); } diff --git a/Source/MQTTnet/Unsubscribing/MqttClientUnsubscribeResultFactory.cs b/Source/MQTTnet/Unsubscribing/MqttClientUnsubscribeResultFactory.cs index 202460e3..c18ba3fa 100644 --- a/Source/MQTTnet/Unsubscribing/MqttClientUnsubscribeResultFactory.cs +++ b/Source/MQTTnet/Unsubscribing/MqttClientUnsubscribeResultFactory.cs @@ -11,9 +11,9 @@ namespace MQTTnet; public sealed class MqttClientUnsubscribeResultFactory { - static readonly IReadOnlyCollection EmptyUserProperties = new List(); + static readonly IReadOnlyCollection EmptyUserProperties = []; - public MqttClientUnsubscribeResult Create(MqttUnsubscribePacket unsubscribePacket, MqttUnsubAckPacket unsubAckPacket) + public static MqttClientUnsubscribeResult Create(MqttUnsubscribePacket unsubscribePacket, MqttUnsubAckPacket unsubAckPacket) { ArgumentNullException.ThrowIfNull(unsubscribePacket); ArgumentNullException.ThrowIfNull(unsubAckPacket);