diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 030f1186..6932e6e7 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -23,15 +23,21 @@ jobs: uses: actions/checkout@v4 - name: Load strong name certificate + if: ${{ github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == github.repository }} run: | echo "$SNC_BASE64" | base64 --decode > "${{ github.workspace }}/certificate.snk" shell: bash env: SNC_BASE64: ${{ secrets.SNC_BASE64 }} - - name: Build package + - name: Build package (signed) + if: ${{ github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == github.repository }} run: dotnet build MQTTnet.sln --configuration Release /p:FileVersion=${{ env.VERSION }} /p:AssemblyVersion=${{ env.VERSION }} /p:PackageVersion=${{ env.VERSION }}${{ env.PACKAGE_SUFFIX }} /p:SignAssembly=true /p:AssemblyOriginatorKeyFile=${{ github.workspace }}/certificate.snk + - name: Build package (unsigned) + if: ${{ github.event_name == 'pull_request' && github.event.pull_request.head.repo.full_name != github.repository }} + run: dotnet build MQTTnet.sln --configuration Release /p:FileVersion=${{ env.VERSION }} /p:AssemblyVersion=${{ env.VERSION }} /p:PackageVersion=${{ env.VERSION }}${{ env.PACKAGE_SUFFIX }} + - name: Upload nuget packages uses: actions/upload-artifact@v4 with: @@ -55,12 +61,12 @@ jobs: uses: actions/checkout@v4 - name: Execute tests - run: dotnet test --framework net10.0 --configuration Release Source/MQTTnet.Tests/MQTTnet.Tests.csproj + run: dotnet test --framework net10.0 --configuration Release --project Source/MQTTnet.Tests/MQTTnet.Tests.csproj sign: needs: build runs-on: windows-latest # Code signing must run on a Windows agent for Authenticode signing (dll/exe) - if: github.repository == 'dotnet/MQTTnet' + if: github.repository == 'dotnet/MQTTnet' && (github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == github.repository) steps: - name: Setup .NET SDK uses: actions/setup-dotnet@v4 diff --git a/Samples/Client/Client_Publish_Samples.cs b/Samples/Client/Client_Publish_Samples.cs index 957baf95..ef4eb881 100644 --- a/Samples/Client/Client_Publish_Samples.cs +++ b/Samples/Client/Client_Publish_Samples.cs @@ -6,6 +6,9 @@ // ReSharper disable UnusedMember.Global // ReSharper disable InconsistentNaming +using System; +using System.Text; + namespace MQTTnet.Samples.Client; public static class Client_Publish_Samples @@ -84,4 +87,21 @@ public static class Client_Publish_Samples Console.WriteLine("MQTT application message is published."); } + + public static MqttApplicationMessage Create_Message_With_Binary_User_Property() + { + /* + * MQTT v5 user properties are encoded as UTF-8 strings. When the UTF-8 payload is already available + * as a byte buffer, the builder APIs can avoid creating intermediate strings. + */ + + var encodedValue = Encoding.UTF8.GetBytes("sensor-01"); + + return new MqttApplicationMessageBuilder() + .WithTopic("samples/metadata/binary") + .WithUserProperty("client-id", encodedValue.AsMemory()) + .WithUserProperty("checksum", new ArraySegment(encodedValue)) + .WithPayload("metadata") + .Build(); + } } \ No newline at end of file diff --git a/Samples/Client/Client_Subscribe_Samples.cs b/Samples/Client/Client_Subscribe_Samples.cs index 38cf67fc..81c2c1f3 100644 --- a/Samples/Client/Client_Subscribe_Samples.cs +++ b/Samples/Client/Client_Subscribe_Samples.cs @@ -7,6 +7,7 @@ // ReSharper disable InconsistentNaming // ReSharper disable UnusedMember.Local +using System.Text; using MQTTnet.Packets; using MQTTnet.Protocol; using MQTTnet.Samples.Helpers; @@ -67,7 +68,7 @@ public static class Client_Subscribe_Samples args.ResponseReasonString = "That did not work!"; // User properties require MQTT v5! - args.ResponseUserProperties.Add(new MqttUserProperty("My", "Data")); + args.ResponseUserProperties.Add(new MqttUserProperty("My", Encoding.UTF8.GetBytes("Data"))); // Now the broker will resend the message again. return Task.CompletedTask; diff --git a/Source/MQTTnet.Server/Disconnecting/MqttServerClientDisconnectOptionsBuilder.cs b/Source/MQTTnet.Server/Disconnecting/MqttServerClientDisconnectOptionsBuilder.cs index 8cac4369..4034e558 100644 --- a/Source/MQTTnet.Server/Disconnecting/MqttServerClientDisconnectOptionsBuilder.cs +++ b/Source/MQTTnet.Server/Disconnecting/MqttServerClientDisconnectOptionsBuilder.cs @@ -40,6 +40,7 @@ public sealed class MqttServerClientDisconnectOptionsBuilder return this; } + [Obsolete("Please use more performance `WithUserProperty` with ArraySegment or ReadOnlyMemory for the value.")] public MqttServerClientDisconnectOptionsBuilder WithUserProperty(string name, string value) { if (_options.UserProperties == null) @@ -50,4 +51,26 @@ public sealed class MqttServerClientDisconnectOptionsBuilder _options.UserProperties.Add(new MqttUserProperty(name, value)); return this; } + + public MqttServerClientDisconnectOptionsBuilder WithUserProperty(string name, ReadOnlyMemory value) + { + if (_options.UserProperties == null) + { + _options.UserProperties = new List(); + } + + _options.UserProperties.Add(new MqttUserProperty(name, value)); + return this; + } + + public MqttServerClientDisconnectOptionsBuilder WithUserProperty(string name, ArraySegment value) + { + if (_options.UserProperties == null) + { + _options.UserProperties = new List(); + } + + _options.UserProperties.Add(new MqttUserProperty(name, value)); + return this; + } } \ No newline at end of file diff --git a/Source/MQTTnet.Server/EnhancedAuthentication/ExchangeEnhancedAuthenticationOptionsFactory.cs b/Source/MQTTnet.Server/EnhancedAuthentication/ExchangeEnhancedAuthenticationOptionsFactory.cs index 5396d3f6..f163f68e 100644 --- a/Source/MQTTnet.Server/EnhancedAuthentication/ExchangeEnhancedAuthenticationOptionsFactory.cs +++ b/Source/MQTTnet.Server/EnhancedAuthentication/ExchangeEnhancedAuthenticationOptionsFactory.cs @@ -51,6 +51,7 @@ public sealed class ExchangeEnhancedAuthenticationOptionsFactory return this; } + [Obsolete("Please use more performance `WithUserProperty` with ArraySegment or ReadOnlyMemory for the value.")] public ExchangeEnhancedAuthenticationOptionsFactory WithUserProperty(string name, string value) { ArgumentNullException.ThrowIfNull(name); @@ -64,4 +65,32 @@ public sealed class ExchangeEnhancedAuthenticationOptionsFactory return this; } + + public ExchangeEnhancedAuthenticationOptionsFactory WithUserProperty(string name, ReadOnlyMemory value) + { + ArgumentNullException.ThrowIfNull(name); + + if (_options.UserProperties == null) + { + _options.UserProperties = new List(); + } + + _options.UserProperties.Add(new MqttUserProperty(name, value)); + + return this; + } + + public ExchangeEnhancedAuthenticationOptionsFactory WithUserProperty(string name, ArraySegment value) + { + ArgumentNullException.ThrowIfNull(name); + + if (_options.UserProperties == null) + { + _options.UserProperties = new List(); + } + + _options.UserProperties.Add(new MqttUserProperty(name, value)); + + return this; + } } \ 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 c722d9cc..da135f05 100644 --- a/Source/MQTTnet.Tests/Clients/MqttClient/MqttClient_Connection_Tests.cs +++ b/Source/MQTTnet.Tests/Clients/MqttClient/MqttClient_Connection_Tests.cs @@ -6,6 +6,7 @@ using System.Text; using MQTTnet.Exceptions; using MQTTnet.Formatter; using MQTTnet.Internal; +using MQTTnet.Packets; using MQTTnet.Protocol; using MQTTnet.Server; using MQTTnet.Server.EnhancedAuthentication; @@ -143,7 +144,7 @@ public sealed class MqttClient_Connection_Tests : BaseTestClass var client = await testEnvironment.ConnectClient(); - var disconnectOptions = testEnvironment.ClientFactory.CreateClientDisconnectOptionsBuilder().WithUserProperty("test_name", "test_value").Build(); + var disconnectOptions = testEnvironment.ClientFactory.CreateClientDisconnectOptionsBuilder().WithUserProperty("test_name", Encoding.UTF8.GetBytes("test_value")).Build(); // Perform a clean disconnect. await client.DisconnectAsync(disconnectOptions); @@ -154,7 +155,7 @@ public sealed class MqttClient_Connection_Tests : BaseTestClass Assert.IsNotNull(eventArgs.UserProperties); Assert.HasCount(1, eventArgs.UserProperties); Assert.AreEqual("test_name", eventArgs.UserProperties[0].Name); - Assert.AreEqual("test_value", eventArgs.UserProperties[0].Value); + Assert.AreEqual("test_value", eventArgs.UserProperties[0].ReadValueAsString()); } sealed class TestClientKerberosAuthenticationHandler : IMqttEnhancedAuthenticationHandler @@ -260,7 +261,7 @@ public sealed class MqttClient_Connection_Tests : BaseTestClass server.ValidatingConnectionAsync += args => { - args.ResponseUserProperties = [new("Property", "Value")]; + args.ResponseUserProperties = [new("Property", Encoding.UTF8.GetBytes("Value"))]; args.ReasonCode = MqttConnectReasonCode.QuotaExceeded; @@ -274,7 +275,7 @@ public sealed class MqttClient_Connection_Tests : BaseTestClass Assert.IsNotNull(response); Assert.AreEqual(MqttClientConnectResultCode.QuotaExceeded, response.ResultCode); Assert.AreEqual("Property", response.UserProperties[0].Name); - Assert.AreEqual("Value", response.UserProperties[0].Value); + Assert.AreEqual("Value", response.UserProperties[0].ReadValueAsString()); } [TestMethod] diff --git a/Source/MQTTnet.Tests/Formatter/MqttPacketSerialization_V3_Tests.cs b/Source/MQTTnet.Tests/Formatter/MqttPacketSerialization_V3_Tests.cs index 1507197b..968d7970 100644 --- a/Source/MQTTnet.Tests/Formatter/MqttPacketSerialization_V3_Tests.cs +++ b/Source/MQTTnet.Tests/Formatter/MqttPacketSerialization_V3_Tests.cs @@ -3,6 +3,7 @@ // See the LICENSE file in the project root for more information. using System.Buffers; +using System.Text; using MQTTnet.Exceptions; using MQTTnet.Formatter; using MQTTnet.Packets; @@ -45,7 +46,7 @@ public sealed class MqttPacketSerialization_V3_Tests SubscriptionIdentifiersAvailable = true, TopicAliasMaximum = 963, WildcardSubscriptionAvailable = true, - UserProperties = [new("Foo", "Bar")] + UserProperties = [new("Foo", Encoding.UTF8.GetBytes("Bar"))] }; var deserialized = MqttPacketSerializationHelper.EncodeAndDecodePacket(connAckPacket, MqttProtocolVersion.V311); @@ -96,7 +97,7 @@ public sealed class MqttPacketSerialization_V3_Tests SubscriptionIdentifiersAvailable = true, TopicAliasMaximum = 963, WildcardSubscriptionAvailable = true, - UserProperties = [new MqttUserProperty("Foo", "Bar")] + UserProperties = [new MqttUserProperty("Foo", Encoding.UTF8.GetBytes("Bar"))] }; var deserialized = MqttPacketSerializationHelper.EncodeAndDecodePacket(connAckPacket, MqttProtocolVersion.V310); @@ -152,8 +153,8 @@ public sealed class MqttPacketSerialization_V3_Tests WillResponseTopic = "WillResponseTopic", WillMessageExpiryInterval = 542, WillPayloadFormatIndicator = MqttPayloadFormatIndicator.CharacterData, - UserProperties = [new("Foo", "Bar")], - WillUserProperties = [new("WillFoo", "WillBar")] + UserProperties = [new("Foo", Encoding.UTF8.GetBytes("Bar"))], + WillUserProperties = [new("WillFoo", Encoding.UTF8.GetBytes("WillBar"))] }; var deserialized = MqttPacketSerializationHelper.EncodeAndDecodePacket(connectPacket, MqttProtocolVersion.V311); @@ -195,7 +196,7 @@ public sealed class MqttPacketSerialization_V3_Tests ReasonString = "ReasonString", ServerReference = "ServerReference", SessionExpiryInterval = 234, - UserProperties = [new("Foo", "Bar")] + UserProperties = [new("Foo", Encoding.UTF8.GetBytes("Bar"))] }; var deserialized = MqttPacketSerializationHelper.EncodeAndDecodePacket(disconnectPacket, MqttProtocolVersion.V311); @@ -235,7 +236,7 @@ public sealed class MqttPacketSerialization_V3_Tests PacketIdentifier = 123, ReasonCode = MqttPubAckReasonCode.NoMatchingSubscribers, ReasonString = "ReasonString", - UserProperties = [new("Foo", "Bar")] + UserProperties = [new("Foo", Encoding.UTF8.GetBytes("Bar"))] }; var deserialized = MqttPacketSerializationHelper.EncodeAndDecodePacket(pubAckPacket, MqttProtocolVersion.V311); @@ -254,7 +255,7 @@ public sealed class MqttPacketSerialization_V3_Tests PacketIdentifier = 123, ReasonCode = MqttPubCompReasonCode.PacketIdentifierNotFound, ReasonString = "ReasonString", - UserProperties = [new("Foo", "Bar")] + UserProperties = [new("Foo", Encoding.UTF8.GetBytes("Bar"))] }; var deserialized = MqttPacketSerializationHelper.EncodeAndDecodePacket(pubCompPacket, MqttProtocolVersion.V311); @@ -284,7 +285,7 @@ public sealed class MqttPacketSerialization_V3_Tests SubscriptionIdentifiers = [123], MessageExpiryInterval = 38, PayloadFormatIndicator = MqttPayloadFormatIndicator.CharacterData, - UserProperties = [new("Foo", "Bar")] + UserProperties = [new("Foo", Encoding.UTF8.GetBytes("Bar"))] }; var deserialized = MqttPacketSerializationHelper.EncodeAndDecodePacket(publishPacket, MqttProtocolVersion.V311); @@ -313,7 +314,7 @@ public sealed class MqttPacketSerialization_V3_Tests PacketIdentifier = 123, ReasonCode = MqttPubRecReasonCode.UnspecifiedError, ReasonString = "ReasonString", - UserProperties = [new("Foo", "Bar")] + UserProperties = [new("Foo", Encoding.UTF8.GetBytes("Bar"))] }; var deserialized = MqttPacketSerializationHelper.EncodeAndDecodePacket(pubRecPacket, MqttProtocolVersion.V311); @@ -333,7 +334,7 @@ public sealed class MqttPacketSerialization_V3_Tests PacketIdentifier = 123, ReasonCode = MqttPubRelReasonCode.PacketIdentifierNotFound, ReasonString = "ReasonString", - UserProperties = [new("Foo", "Bar")] + UserProperties = [new("Foo", Encoding.UTF8.GetBytes("Bar"))] }; var deserialized = MqttPacketSerializationHelper.EncodeAndDecodePacket(pubRelPacket, MqttProtocolVersion.V311); @@ -353,7 +354,7 @@ public sealed class MqttPacketSerialization_V3_Tests PacketIdentifier = 123, ReasonString = "ReasonString", ReasonCodes = [MqttSubscribeReasonCode.GrantedQoS1], - UserProperties = [new("Foo", "Bar")] + UserProperties = [new("Foo", Encoding.UTF8.GetBytes("Bar"))] }; var deserialized = MqttPacketSerializationHelper.EncodeAndDecodePacket(subAckPacket, MqttProtocolVersion.V311); @@ -383,7 +384,7 @@ public sealed class MqttPacketSerialization_V3_Tests QualityOfServiceLevel = MqttQualityOfServiceLevel.AtMostOnce } ], - UserProperties = [new("Foo", "Bar")] + UserProperties = [new("Foo", Encoding.UTF8.GetBytes("Bar"))] }; var deserialized = MqttPacketSerializationHelper.EncodeAndDecodePacket(subscribePacket, MqttProtocolVersion.V311); @@ -407,7 +408,7 @@ public sealed class MqttPacketSerialization_V3_Tests PacketIdentifier = 123, ReasonCodes = [MqttUnsubscribeReasonCode.ImplementationSpecificError], ReasonString = "ReasonString", - UserProperties = [new("Foo", "Bar")] + UserProperties = [new("Foo", Encoding.UTF8.GetBytes("Bar"))] }; var deserialized = MqttPacketSerializationHelper.EncodeAndDecodePacket(unsubAckPacket, MqttProtocolVersion.V311); @@ -425,7 +426,7 @@ public sealed class MqttPacketSerialization_V3_Tests { PacketIdentifier = 123, TopicFilters = ["TopicFilter1"], - UserProperties = [new("Foo", "Bar")] + UserProperties = [new("Foo", Encoding.UTF8.GetBytes("Bar"))] }; var deserialized = MqttPacketSerializationHelper.EncodeAndDecodePacket(unsubscribePacket, MqttProtocolVersion.V311); diff --git a/Source/MQTTnet.Tests/Formatter/MqttPacketSerialization_V5_Tests.cs b/Source/MQTTnet.Tests/Formatter/MqttPacketSerialization_V5_Tests.cs index af216c5c..570fc1af 100644 --- a/Source/MQTTnet.Tests/Formatter/MqttPacketSerialization_V5_Tests.cs +++ b/Source/MQTTnet.Tests/Formatter/MqttPacketSerialization_V5_Tests.cs @@ -3,6 +3,7 @@ // See the LICENSE file in the project root for more information. using System.Buffers; +using System.Text; using MQTTnet.Formatter; using MQTTnet.Packets; using MQTTnet.Protocol; @@ -32,7 +33,7 @@ public sealed class MqttPacketSerialization_V5_Tests AuthenticationMethod = "AuthenticationMethod", ReasonCode = MqttAuthenticateReasonCode.ContinueAuthentication, ReasonString = "ReasonString", - UserProperties = [new MqttUserProperty("Foo", "Bar")] + UserProperties = [new MqttUserProperty("Foo", Encoding.UTF8.GetBytes("Bar"))] }; var deserialized = MqttPacketSerializationHelper.EncodeAndDecodePacket(authPacket, MqttProtocolVersion.V500); @@ -68,7 +69,7 @@ public sealed class MqttPacketSerialization_V5_Tests SubscriptionIdentifiersAvailable = true, TopicAliasMaximum = 963, WildcardSubscriptionAvailable = true, - UserProperties = [new MqttUserProperty("Foo", "Bar")] + UserProperties = [new MqttUserProperty("Foo", Encoding.UTF8.GetBytes("Bar"))] }; var deserialized = MqttPacketSerializationHelper.EncodeAndDecodePacket(connAckPacket, MqttProtocolVersion.V500); @@ -124,8 +125,8 @@ public sealed class MqttPacketSerialization_V5_Tests WillResponseTopic = "WillResponseTopic", WillMessageExpiryInterval = 542, WillPayloadFormatIndicator = MqttPayloadFormatIndicator.CharacterData, - UserProperties = [new MqttUserProperty("Foo", "Bar")], - WillUserProperties = [new MqttUserProperty("WillFoo", "WillBar")] + UserProperties = [new MqttUserProperty("Foo", Encoding.UTF8.GetBytes("Bar"))], + WillUserProperties = [new MqttUserProperty("WillFoo", Encoding.UTF8.GetBytes("WillBar"))] }; var deserialized = MqttPacketSerializationHelper.EncodeAndDecodePacket(connectPacket, MqttProtocolVersion.V500); @@ -167,7 +168,7 @@ public sealed class MqttPacketSerialization_V5_Tests ReasonString = "ReasonString", ServerReference = "ServerReference", SessionExpiryInterval = 234, - UserProperties = [new MqttUserProperty("Foo", "Bar")] + UserProperties = [new MqttUserProperty("Foo", Encoding.UTF8.GetBytes("Bar"))] }; var deserialized = MqttPacketSerializationHelper.EncodeAndDecodePacket(disconnectPacket, MqttProtocolVersion.V500); @@ -207,7 +208,7 @@ public sealed class MqttPacketSerialization_V5_Tests PacketIdentifier = 123, ReasonCode = MqttPubAckReasonCode.NoMatchingSubscribers, ReasonString = "ReasonString", - UserProperties = [new MqttUserProperty("Foo", "Bar")] + UserProperties = [new MqttUserProperty("Foo", Encoding.UTF8.GetBytes("Bar"))] }; var deserialized = MqttPacketSerializationHelper.EncodeAndDecodePacket(pubAckPacket, MqttProtocolVersion.V500); @@ -226,7 +227,7 @@ public sealed class MqttPacketSerialization_V5_Tests PacketIdentifier = 123, ReasonCode = MqttPubCompReasonCode.PacketIdentifierNotFound, ReasonString = "ReasonString", - UserProperties = [new MqttUserProperty("Foo", "Bar")] + UserProperties = [new MqttUserProperty("Foo", Encoding.UTF8.GetBytes("Bar"))] }; var deserialized = MqttPacketSerializationHelper.EncodeAndDecodePacket(pubCompPacket, MqttProtocolVersion.V500); @@ -255,7 +256,7 @@ public sealed class MqttPacketSerialization_V5_Tests SubscriptionIdentifiers = [123], MessageExpiryInterval = 38, PayloadFormatIndicator = MqttPayloadFormatIndicator.CharacterData, - UserProperties = [new MqttUserProperty("Foo", "Bar")] + UserProperties = [new MqttUserProperty("Foo", Encoding.UTF8.GetBytes("Bar"))] }; var deserialized = MqttPacketSerializationHelper.EncodeAndDecodePacket(publishPacket, MqttProtocolVersion.V500); @@ -284,7 +285,7 @@ public sealed class MqttPacketSerialization_V5_Tests PacketIdentifier = 123, ReasonCode = MqttPubRecReasonCode.UnspecifiedError, ReasonString = "ReasonString", - UserProperties = [new MqttUserProperty("Foo", "Bar")] + UserProperties = [new MqttUserProperty("Foo", Encoding.UTF8.GetBytes("Bar"))] }; var deserialized = MqttPacketSerializationHelper.EncodeAndDecodePacket(pubRecPacket, MqttProtocolVersion.V500); @@ -303,7 +304,7 @@ public sealed class MqttPacketSerialization_V5_Tests PacketIdentifier = 123, ReasonCode = MqttPubRelReasonCode.PacketIdentifierNotFound, ReasonString = "ReasonString", - UserProperties = [new MqttUserProperty("Foo", "Bar")] + UserProperties = [new MqttUserProperty("Foo", Encoding.UTF8.GetBytes("Bar"))] }; var deserialized = MqttPacketSerializationHelper.EncodeAndDecodePacket(pubRelPacket, MqttProtocolVersion.V500); @@ -322,7 +323,7 @@ public sealed class MqttPacketSerialization_V5_Tests PacketIdentifier = 123, ReasonString = "ReasonString", ReasonCodes = [MqttSubscribeReasonCode.GrantedQoS1], - UserProperties = [new MqttUserProperty("Foo", "Bar")] + UserProperties = [new MqttUserProperty("Foo", Encoding.UTF8.GetBytes("Bar"))] }; var deserialized = MqttPacketSerializationHelper.EncodeAndDecodePacket(subAckPacket, MqttProtocolVersion.V500); @@ -352,7 +353,7 @@ public sealed class MqttPacketSerialization_V5_Tests QualityOfServiceLevel = MqttQualityOfServiceLevel.AtMostOnce } ], - UserProperties = [new MqttUserProperty("Foo", "Bar")] + UserProperties = [new MqttUserProperty("Foo", Encoding.UTF8.GetBytes("Bar"))] }; var deserialized = MqttPacketSerializationHelper.EncodeAndDecodePacket(subscribePacket, MqttProtocolVersion.V500); @@ -376,7 +377,7 @@ public sealed class MqttPacketSerialization_V5_Tests PacketIdentifier = 123, ReasonCodes = [MqttUnsubscribeReasonCode.ImplementationSpecificError], ReasonString = "ReasonString", - UserProperties = [new MqttUserProperty("Foo", "Bar")] + UserProperties = [new MqttUserProperty("Foo", Encoding.UTF8.GetBytes("Bar"))] }; var deserialized = MqttPacketSerializationHelper.EncodeAndDecodePacket(unsubAckPacket, MqttProtocolVersion.V500); @@ -395,7 +396,7 @@ public sealed class MqttPacketSerialization_V5_Tests { PacketIdentifier = 123, TopicFilters = ["TopicFilter1"], - UserProperties = [new MqttUserProperty("Foo", "Bar")] + UserProperties = [new MqttUserProperty("Foo", Encoding.UTF8.GetBytes("Bar"))] }; var deserialized = MqttPacketSerializationHelper.EncodeAndDecodePacket(unsubscribePacket, MqttProtocolVersion.V500); diff --git a/Source/MQTTnet.Tests/MQTTnet.Tests.csproj b/Source/MQTTnet.Tests/MQTTnet.Tests.csproj index 7183f0ce..ad7fb0eb 100644 --- a/Source/MQTTnet.Tests/MQTTnet.Tests.csproj +++ b/Source/MQTTnet.Tests/MQTTnet.Tests.csproj @@ -6,13 +6,12 @@ default disable $(NoWarn);CA1707 + true - - - + @@ -24,4 +23,10 @@ + + + + \ No newline at end of file diff --git a/Source/MQTTnet.Tests/MQTTv5/Client_Tests.cs b/Source/MQTTnet.Tests/MQTTv5/Client_Tests.cs index 0251a745..f68323ba 100644 --- a/Source/MQTTnet.Tests/MQTTv5/Client_Tests.cs +++ b/Source/MQTTnet.Tests/MQTTv5/Client_Tests.cs @@ -4,6 +4,7 @@ using System.Buffers; using System.Diagnostics.CodeAnalysis; +using System.Text; using Microsoft.IO; using MQTTnet.Formatter; using MQTTnet.Internal; @@ -64,8 +65,8 @@ public sealed class Client_Tests : BaseTestClass await client.PublishAsync( new MqttApplicationMessageBuilder().WithTopic("a") .WithPayload("x") - .WithUserProperty("a", "1") - .WithUserProperty("b", "2") + .WithUserProperty("a", Encoding.UTF8.GetBytes("1")) + .WithUserProperty("b", Encoding.UTF8.GetBytes("2")) .WithPayloadFormatIndicator(MqttPayloadFormatIndicator.CharacterData) .WithQualityOfServiceLevel(MqttQualityOfServiceLevel.AtLeastOnce) .Build()); @@ -98,8 +99,8 @@ public sealed class Client_Tests : BaseTestClass var applicationMessage = new MqttApplicationMessageBuilder().WithTopic("Hello") .WithPayload("World") .WithQualityOfServiceLevel(MqttQualityOfServiceLevel.AtMostOnce) - .WithUserProperty("x", "1") - .WithUserProperty("y", "2") + .WithUserProperty("x", Encoding.UTF8.GetBytes("1")) + .WithUserProperty("y", Encoding.UTF8.GetBytes("2")) .WithResponseTopic("response") .WithContentType("text") .WithMessageExpiryInterval(50) @@ -214,8 +215,8 @@ public sealed class Client_Tests : BaseTestClass var applicationMessage = new MqttApplicationMessageBuilder().WithTopic("Hello") .WithPayload("World") .WithQualityOfServiceLevel(MqttQualityOfServiceLevel.AtMostOnce) - .WithUserProperty("x", "1") - .WithUserProperty("y", "2") + .WithUserProperty("x", Encoding.UTF8.GetBytes("1")) + .WithUserProperty("y", Encoding.UTF8.GetBytes("2")) .WithResponseTopic("response") .WithContentType("text") .WithMessageExpiryInterval(50) @@ -250,8 +251,8 @@ public sealed class Client_Tests : BaseTestClass var applicationMessage = new MqttApplicationMessageBuilder().WithTopic("Hello") .WithPayload(memoryStream.GetReadOnlySequence()) .WithQualityOfServiceLevel(MqttQualityOfServiceLevel.AtMostOnce) - .WithUserProperty("x", "1") - .WithUserProperty("y", "2") + .WithUserProperty("x", Encoding.UTF8.GetBytes("1")) + .WithUserProperty("y", Encoding.UTF8.GetBytes("2")) .WithResponseTopic("response") .WithContentType("text") .WithMessageExpiryInterval(50) diff --git a/Source/MQTTnet.Tests/MqttApplicationMessageBuilder_Tests.cs b/Source/MQTTnet.Tests/MqttApplicationMessageBuilder_Tests.cs index b1689648..8f447710 100644 --- a/Source/MQTTnet.Tests/MqttApplicationMessageBuilder_Tests.cs +++ b/Source/MQTTnet.Tests/MqttApplicationMessageBuilder_Tests.cs @@ -2,7 +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. +using System; +using System.Collections.Generic; using System.Text; +using MQTTnet.Formatter; +using MQTTnet.Formatter.V5; +using MQTTnet.Packets; using MQTTnet.Protocol; namespace MQTTnet.Tests; @@ -59,4 +64,64 @@ public sealed class MqttApplicationMessageBuilder_Tests Assert.IsTrue(message.Retain); Assert.AreEqual(MqttQualityOfServiceLevel.ExactlyOnce, message.QualityOfServiceLevel); } + + [TestMethod] + public void CreateApplicationMessage_UserProperty_ReadOnlyMemoryValue() + { + var value = "utf8"; + var buffer = Encoding.UTF8.GetBytes(value); + + var message = new MqttApplicationMessageBuilder().WithTopic("topic").WithUserProperty("name", buffer.AsMemory()).Build(); + + Assert.IsNotNull(message.UserProperties); + Assert.HasCount(1, message.UserProperties); + + var userProperty = message.UserProperties[0]; + CollectionAssert.AreEqual(buffer, userProperty.ValueBuffer.ToArray()); + Assert.AreEqual(value, userProperty.ReadValueAsString()); + } + + [TestMethod] + public void CreateApplicationMessage_UserProperty_ArraySegmentValue() + { + var buffer = Encoding.UTF8.GetBytes("segment"); + var segment = new ArraySegment(buffer); + + var message = new MqttApplicationMessageBuilder().WithTopic("topic").WithUserProperty("name", segment).Build(); + + Assert.IsNotNull(message.UserProperties); + Assert.HasCount(1, message.UserProperties); + + var userProperty = message.UserProperties[0]; + CollectionAssert.AreEqual(buffer, userProperty.ValueBuffer.ToArray()); + Assert.AreEqual("segment", userProperty.ReadValueAsString()); + } + + [TestMethod] + public void WriteUserProperty_FromBinaryBuffer_EqualsStringEncoding() + { + var name = "name"; + var value = "value"; + var encoded = Encoding.UTF8.GetBytes(value); + + var binaryWriter = new MqttBufferWriter(32, 256); + var binaryPropertiesWriter = new MqttV5PropertiesWriter(binaryWriter); + binaryPropertiesWriter.WriteUserProperties(new List { new(name, encoded.AsMemory()) }); + + var stringWriter = new MqttBufferWriter(32, 256); + var stringPropertiesWriter = new MqttV5PropertiesWriter(stringWriter); +#pragma warning disable CS0618 // Type or member is obsolete + stringPropertiesWriter.WriteUserProperties(new List { new(name, value) }); +#pragma warning restore CS0618 // Type or member is obsolete + + CollectionAssert.AreEqual(GetWrittenBytes(stringWriter), GetWrittenBytes(binaryWriter)); + } + + static byte[] GetWrittenBytes(MqttBufferWriter writer) + { + var length = writer.Length; + var copy = new byte[length]; + Array.Copy(writer.GetBuffer(), copy, length); + return copy; + } } \ No newline at end of file diff --git a/Source/MQTTnet.Tests/MqttApplicationMessageValidator_Tests.cs b/Source/MQTTnet.Tests/MqttApplicationMessageValidator_Tests.cs index 441f125a..2ad9c732 100644 --- a/Source/MQTTnet.Tests/MqttApplicationMessageValidator_Tests.cs +++ b/Source/MQTTnet.Tests/MqttApplicationMessageValidator_Tests.cs @@ -2,6 +2,7 @@ // 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.Text; using MQTTnet.Formatter; namespace MQTTnet.Tests; @@ -27,7 +28,7 @@ public sealed class MqttApplicationMessageValidator_Tests public void Succeed_When_Using_UserProperties_And_MQTT_500() { MqttApplicationMessageValidator.ThrowIfNotSupported( - new MqttApplicationMessageBuilder().WithTopic("A").WithUserProperty("User", "Property").Build(), + new MqttApplicationMessageBuilder().WithTopic("A").WithUserProperty("User", Encoding.UTF8.GetBytes("Property")).Build(), MqttProtocolVersion.V500); } @@ -35,7 +36,7 @@ public sealed class MqttApplicationMessageValidator_Tests public void Succeed_When_Using_WillUserProperties_And_MQTT_311() { Assert.ThrowsExactly(() => MqttApplicationMessageValidator.ThrowIfNotSupported( - new MqttApplicationMessageBuilder().WithTopic("B").WithUserProperty("User", "Property").Build(), + new MqttApplicationMessageBuilder().WithTopic("B").WithUserProperty("User", Encoding.UTF8.GetBytes("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 65b04f8f..9ae42284 100644 --- a/Source/MQTTnet.Tests/MqttClientOptionsValidator_Tests.cs +++ b/Source/MQTTnet.Tests/MqttClientOptionsValidator_Tests.cs @@ -2,6 +2,7 @@ // 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.Text; using MQTTnet.Formatter; namespace MQTTnet.Tests; @@ -13,27 +14,27 @@ public sealed class MqttClientOptionsValidator_Tests [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", Encoding.UTF8.GetBytes("Property")).WithTcpServer("FAKE").Build(); } [TestMethod] public void Succeed_When_Using_WillUserProperties_And_MQTT_311() { Assert.ThrowsExactly(() => - new MqttClientOptionsBuilder().WithProtocolVersion(MqttProtocolVersion.V311).WithWillUserProperty("User", "Property").WithTcpServer("FAKE").Build()); + new MqttClientOptionsBuilder().WithProtocolVersion(MqttProtocolVersion.V311).WithWillUserProperty("User", Encoding.UTF8.GetBytes("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(); + new MqttClientOptionsBuilder().WithProtocolVersion(MqttProtocolVersion.V500).WithWillUserProperty("User", Encoding.UTF8.GetBytes("Property")).WithTcpServer("FAKE").Build(); } [TestMethod] public void Throw_When_Using_UserProperties_And_MQTT_311() { Assert.ThrowsExactly(() => - new MqttClientOptionsBuilder().WithProtocolVersion(MqttProtocolVersion.V311).WithUserProperty("User", "Property").WithTcpServer("FAKE").Build()); + new MqttClientOptionsBuilder().WithProtocolVersion(MqttProtocolVersion.V311).WithUserProperty("User", Encoding.UTF8.GetBytes("Property")).WithTcpServer("FAKE").Build()); } [TestMethod] diff --git a/Source/MQTTnet.Tests/MqttTcpChannel_Tests.cs b/Source/MQTTnet.Tests/MqttTcpChannel_Tests.cs index 4fb8bef4..a0742425 100644 --- a/Source/MQTTnet.Tests/MqttTcpChannel_Tests.cs +++ b/Source/MQTTnet.Tests/MqttTcpChannel_Tests.cs @@ -20,7 +20,8 @@ public class MqttTcpChannel_Tests try { - serverSocket.Bind(new IPEndPoint(IPAddress.Any, 50001)); + serverSocket.Bind(new IPEndPoint(IPAddress.Loopback, 0)); + var serverPort = ((IPEndPoint)serverSocket.LocalEndPoint).Port; serverSocket.Listen(0); #pragma warning disable 4014 @@ -37,7 +38,7 @@ public class MqttTcpChannel_Tests }, ct.Token); - var remoteEndPoint = new DnsEndPoint("localhost", 50001); + var remoteEndPoint = new DnsEndPoint("localhost", serverPort); using var clientSocket = new CrossPlatformSocket(AddressFamily.InterNetwork, ProtocolType.Tcp); await clientSocket.ConnectAsync(remoteEndPoint, CancellationToken.None); diff --git a/Source/MQTTnet.Tests/Server/Cross_Version_Tests.cs b/Source/MQTTnet.Tests/Server/Cross_Version_Tests.cs index 32563c35..78de6135 100644 --- a/Source/MQTTnet.Tests/Server/Cross_Version_Tests.cs +++ b/Source/MQTTnet.Tests/Server/Cross_Version_Tests.cs @@ -43,7 +43,7 @@ public sealed class Cross_Version_Tests : BaseTestClass var applicationMessage = new MqttApplicationMessageBuilder().WithTopic("My/Message") .WithPayload("My_Payload") - .WithUserProperty("A", "B") + .WithUserProperty("A", Encoding.UTF8.GetBytes("B")) .WithResponseTopic("Response") .WithCorrelationData(Encoding.UTF8.GetBytes("Correlation")) .Build(); diff --git a/Source/MQTTnet.Tests/Server/HotSwapCerts_Tests.cs b/Source/MQTTnet.Tests/Server/HotSwapCerts_Tests.cs index 8771b55d..7d0afc9e 100644 --- a/Source/MQTTnet.Tests/Server/HotSwapCerts_Tests.cs +++ b/Source/MQTTnet.Tests/Server/HotSwapCerts_Tests.cs @@ -2,6 +2,7 @@ using System.Collections.Concurrent; using System.Diagnostics; using System.Net; using System.Net.Security; +using System.Net.Sockets; using System.Security.Authentication; using System.Security.Cryptography; using System.Security.Cryptography.X509Certificates; @@ -26,6 +27,7 @@ public sealed class HotSwapCerts_Tests using var client01 = new ClientTestHarness(); server.InstallNewClientCert(client01.GetCurrentClientCert()); client01.InstallNewServerCert(server.GetCurrentServerCert()); + client01.SetServerPort(server.EncryptedPort); await server.StartServer(); @@ -47,6 +49,7 @@ public sealed class HotSwapCerts_Tests using var client01 = new ClientTestHarness(); server.InstallNewClientCert(client01.GetCurrentClientCert()); client01.InstallNewServerCert(server.GetCurrentServerCert()); + client01.SetServerPort(server.EncryptedPort); await server.StartServer(); @@ -70,6 +73,7 @@ public sealed class HotSwapCerts_Tests using var client01 = new ClientTestHarness(); server.InstallNewClientCert(client01.GetCurrentClientCert()); client01.InstallNewServerCert(server.GetCurrentServerCert()); + client01.SetServerPort(server.EncryptedPort); await server.StartServer(); await client01.Connect(); @@ -91,6 +95,7 @@ public sealed class HotSwapCerts_Tests using var client01 = new ClientTestHarness(); server.InstallNewClientCert(client01.GetCurrentClientCert()); client01.InstallNewServerCert(server.GetCurrentServerCert()); + client01.SetServerPort(server.EncryptedPort); await server.StartServer(); await client01.Connect(); @@ -135,10 +140,11 @@ public sealed class HotSwapCerts_Tests sealed class ClientTestHarness : IDisposable { readonly HotSwappableClientCertProvider _hotSwapClient = new HotSwappableClientCertProvider(); + int _serverPort = 8883; IMqttClient _client; - public string ClientId => _client.Options.ClientId; + public string ClientId => _client?.Options?.ClientId; public Task Connect() { @@ -147,10 +153,15 @@ public sealed class HotSwapCerts_Tests public void Dispose() { - _client.Dispose(); + _client?.Dispose(); _hotSwapClient.Dispose(); } + public void SetServerPort(int port) + { + _serverPort = port; + } + public X509Certificate2 GetCurrentClientCert() { var result = _hotSwapClient.GetCertificates()[0]; @@ -216,7 +227,7 @@ public sealed class HotSwapCerts_Tests o => o.WithClientCertificatesProvider(_hotSwapClient) .WithCertificateValidationHandler(_hotSwapClient.OnCertificateValidation) .WithSslProtocols(SslProtocols.Tls12)) - .WithTcpServer("localhost") + .WithTcpServer("localhost", _serverPort) .WithCleanSession() .WithProtocolVersion(MqttProtocolVersion.V500); @@ -242,6 +253,7 @@ public sealed class HotSwapCerts_Tests sealed class ServerTestHarness : IDisposable { readonly HotSwappableServerCertProvider _hotSwapServer = new HotSwappableServerCertProvider(); + readonly int _encryptedPort = GetFreeTcpPort(); MqttServer _server; @@ -261,6 +273,8 @@ public sealed class HotSwapCerts_Tests return _server.DisconnectClientAsync(client.ClientId, MqttDisconnectReasonCode.UnspecifiedError); } + public int EncryptedPort => _encryptedPort; + public X509Certificate2 GetCurrentServerCert() { return _hotSwapServer.GetCertificate(); @@ -281,6 +295,7 @@ public sealed class HotSwapCerts_Tests var mqttServerFactory = new MqttServerFactory(); var mqttServerOptions = new MqttServerOptionsBuilder().WithEncryptionCertificate(_hotSwapServer) + .WithEncryptedEndpointPort(_encryptedPort) .WithRemoteCertificateValidationCallback(_hotSwapServer.RemoteCertificateValidationCallback) .WithEncryptedEndpoint() .Build(); @@ -290,6 +305,17 @@ public sealed class HotSwapCerts_Tests _server = mqttServerFactory.CreateMqttServer(mqttServerOptions); return _server.StartAsync(); } + + static int GetFreeTcpPort() + { + var listener = new TcpListener(IPAddress.Loopback, 0); + listener.Start(); + + var port = ((IPEndPoint)listener.LocalEndpoint).Port; + listener.Stop(); + + return port; + } } sealed class HotSwappableClientCertProvider : IMqttClientCertificatesProvider, IDisposable diff --git a/Source/MQTTnet.Tests/Server/Subscribe_Tests.cs b/Source/MQTTnet.Tests/Server/Subscribe_Tests.cs index 991159c5..9ac1b49c 100644 --- a/Source/MQTTnet.Tests/Server/Subscribe_Tests.cs +++ b/Source/MQTTnet.Tests/Server/Subscribe_Tests.cs @@ -3,6 +3,7 @@ // See the LICENSE file in the project root for more information. using System.Globalization; +using System.Text; using MQTTnet.Exceptions; using MQTTnet.Formatter; using MQTTnet.Internal; @@ -98,7 +99,7 @@ public sealed class Subscribe_Tests : BaseTestClass var client = await testEnvironment.ConnectClient(); - var subscribeOptions = testEnvironment.ClientFactory.CreateSubscribeOptionsBuilder().WithTopicFilter("X").WithUserProperty("A", "1").Build(); + var subscribeOptions = testEnvironment.ClientFactory.CreateSubscribeOptionsBuilder().WithTopicFilter("X").WithUserProperty("A", Encoding.UTF8.GetBytes("1")).Build(); await client.SubscribeAsync(subscribeOptions); CollectionAssert.AreEqual(subscribeOptions.UserProperties.ToList(), eventArgs.UserProperties); diff --git a/Source/MQTTnet.Tests/Server/Subscription_TopicHash_Tests.cs b/Source/MQTTnet.Tests/Server/Subscription_TopicHash_Tests.cs index 905bc7fa..0b580785 100644 --- a/Source/MQTTnet.Tests/Server/Subscription_TopicHash_Tests.cs +++ b/Source/MQTTnet.Tests/Server/Subscription_TopicHash_Tests.cs @@ -1,4 +1,5 @@ using System.Diagnostics; +using System.Linq; using System.Text; using MQTTnet.Packets; using MQTTnet.Protocol; diff --git a/Source/MQTTnet.Tests/Server/Unsubscribe_Tests.cs b/Source/MQTTnet.Tests/Server/Unsubscribe_Tests.cs index a6008ee3..b6b61eeb 100644 --- a/Source/MQTTnet.Tests/Server/Unsubscribe_Tests.cs +++ b/Source/MQTTnet.Tests/Server/Unsubscribe_Tests.cs @@ -2,6 +2,7 @@ // 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.Text; using MQTTnet.Exceptions; using MQTTnet.Formatter; using MQTTnet.Internal; @@ -45,7 +46,7 @@ public sealed class Unsubscribe_Tests : BaseTestClass var client = await testEnvironment.ConnectClient(); - var unsubscribeOptions = testEnvironment.ClientFactory.CreateUnsubscribeOptionsBuilder().WithTopicFilter("X").WithUserProperty("A", "1").Build(); + var unsubscribeOptions = testEnvironment.ClientFactory.CreateUnsubscribeOptionsBuilder().WithTopicFilter("X").WithUserProperty("A", Encoding.UTF8.GetBytes("1")).Build(); await client.UnsubscribeAsync(unsubscribeOptions); CollectionAssert.AreEqual(unsubscribeOptions.UserProperties.ToList(), eventArgs.UserProperties); diff --git a/Source/MQTTnet.Tests/Server/User_Properties_Tests.cs b/Source/MQTTnet.Tests/Server/User_Properties_Tests.cs index cf111c39..afe08efb 100644 --- a/Source/MQTTnet.Tests/Server/User_Properties_Tests.cs +++ b/Source/MQTTnet.Tests/Server/User_Properties_Tests.cs @@ -2,6 +2,7 @@ // 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.Text; using MQTTnet.Formatter; using MQTTnet.Internal; using MQTTnet.Packets; @@ -26,10 +27,10 @@ public class Feature_Tests 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 + .WithUserProperty("x", Encoding.UTF8.GetBytes("1")) + .WithUserProperty("y", Encoding.UTF8.GetBytes("2")) + .WithUserProperty("z", Encoding.UTF8.GetBytes("3")) + .WithUserProperty("z", Encoding.UTF8.GetBytes("4")); // z is here two times to test list of items await receiver.SubscribeAsync(new MqttClientSubscribeOptions { diff --git a/Source/MQTTnet/Disconnecting/MqttClientDisconnectOptionsBuilder.cs b/Source/MQTTnet/Disconnecting/MqttClientDisconnectOptionsBuilder.cs index c0a8b006..d906518f 100644 --- a/Source/MQTTnet/Disconnecting/MqttClientDisconnectOptionsBuilder.cs +++ b/Source/MQTTnet/Disconnecting/MqttClientDisconnectOptionsBuilder.cs @@ -2,6 +2,7 @@ // 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; @@ -48,10 +49,25 @@ public sealed class MqttClientDisconnectOptionsBuilder return this; } + [Obsolete("Please use more performance `WithUserProperty` with ArraySegment or ReadOnlyMemory for the value.")] public MqttClientDisconnectOptionsBuilder WithUserProperty(string name, string value) { _userProperties ??= []; _userProperties.Add(new MqttUserProperty(name, value)); return this; } + + public MqttClientDisconnectOptionsBuilder WithUserProperty(string name, ReadOnlyMemory value) + { + _userProperties ??= []; + _userProperties.Add(new MqttUserProperty(name, value)); + return this; + } + + public MqttClientDisconnectOptionsBuilder WithUserProperty(string name, ArraySegment value) + { + _userProperties ??= []; + _userProperties.Add(new MqttUserProperty(name, value)); + return this; + } } \ No newline at end of file diff --git a/Source/MQTTnet/Formatter/MqttBufferWriter.cs b/Source/MQTTnet/Formatter/MqttBufferWriter.cs index c31f56f9..9b7bdc0a 100644 --- a/Source/MQTTnet/Formatter/MqttBufferWriter.cs +++ b/Source/MQTTnet/Formatter/MqttBufferWriter.cs @@ -2,6 +2,7 @@ // 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.Runtime.CompilerServices; using System.Text; using MQTTnet.Exceptions; @@ -193,6 +194,29 @@ public sealed class MqttBufferWriter } } + public void WriteString(ReadOnlyMemory value) + { + var span = value.Span; + var length = span.Length; + + if (length > EncodedStringMaxLength) + { + throw new MqttProtocolViolationException($"The maximum string length is 65535. The current string has a length of {length}."); + } + + EnsureAdditionalCapacity(length + 2); + + _buffer[_position] = (byte)(length >> 8); + _buffer[_position + 1] = (byte)length; + + if (length > 0) + { + span.CopyTo(_buffer.AsSpan(_position + 2)); + } + + IncreasePosition(length + 2); + } + public void WriteTwoByteInteger(ushort value) { EnsureAdditionalCapacity(2); diff --git a/Source/MQTTnet/Formatter/V5/MqttV5PropertiesReader.cs b/Source/MQTTnet/Formatter/V5/MqttV5PropertiesReader.cs index 5dbf2630..ce98eea5 100644 --- a/Source/MQTTnet/Formatter/V5/MqttV5PropertiesReader.cs +++ b/Source/MQTTnet/Formatter/V5/MqttV5PropertiesReader.cs @@ -2,6 +2,7 @@ // 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.Exceptions; using MQTTnet.Packets; using MQTTnet.Protocol; @@ -59,14 +60,14 @@ public struct MqttV5PropertiesReader if (CurrentPropertyId == MqttPropertyId.UserProperty) { var name = _body.ReadString(); - var value = _body.ReadString(); + var valueBuffer = ReadUserPropertyValueBuffer(); if (CollectedUserProperties == null) { CollectedUserProperties = new List(); } - CollectedUserProperties.Add(new MqttUserProperty(name, value)); + CollectedUserProperties.Add(new MqttUserProperty(name, valueBuffer)); continue; } @@ -190,6 +191,11 @@ public struct MqttV5PropertiesReader return _body.ReadTwoByteInteger(); } + public ReadOnlyMemory ReadUserPropertyValueBuffer() + { + return _body.ReadBinaryData(); + } + public bool ReadWildcardSubscriptionAvailable() { return _body.ReadByte() == 1; diff --git a/Source/MQTTnet/Formatter/V5/MqttV5PropertiesWriter.cs b/Source/MQTTnet/Formatter/V5/MqttV5PropertiesWriter.cs index 60c02d5f..a6027329 100644 --- a/Source/MQTTnet/Formatter/V5/MqttV5PropertiesWriter.cs +++ b/Source/MQTTnet/Formatter/V5/MqttV5PropertiesWriter.cs @@ -260,7 +260,7 @@ public sealed class MqttV5PropertiesWriter { _bufferWriter.WriteByte((byte)MqttPropertyId.UserProperty); _bufferWriter.WriteString(property.Name); - _bufferWriter.WriteString(property.Value); + _bufferWriter.WriteString(property.ValueBuffer); } } diff --git a/Source/MQTTnet/MqttApplicationMessageBuilder.cs b/Source/MQTTnet/MqttApplicationMessageBuilder.cs index 95e2cef3..9df5a951 100644 --- a/Source/MQTTnet/MqttApplicationMessageBuilder.cs +++ b/Source/MQTTnet/MqttApplicationMessageBuilder.cs @@ -2,6 +2,7 @@ // 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.Runtime.InteropServices; using System.Text; @@ -263,10 +264,33 @@ public sealed class MqttApplicationMessageBuilder /// Adds the user property to the message. /// MQTT 5.0.0+ feature. /// + [Obsolete("Use the WithUserProperty accepting a ReadOnlyMemory for better performance.")] public MqttApplicationMessageBuilder WithUserProperty(string name, string value) { _userProperties ??= []; _userProperties.Add(new MqttUserProperty(name, value)); return this; } + + /// + /// Adds the user property to the message using a pre-encoded UTF-8 value buffer. + /// MQTT 5.0.0+ feature. + /// + public MqttApplicationMessageBuilder WithUserProperty(string name, ReadOnlyMemory value) + { + _userProperties ??= []; + _userProperties.Add(new MqttUserProperty(name, value)); + return this; + } + + /// + /// Adds the user property to the message using a pre-encoded UTF-8 value buffer. + /// MQTT 5.0.0+ feature. + /// + public MqttApplicationMessageBuilder WithUserProperty(string name, ArraySegment value) + { + _userProperties ??= []; + _userProperties.Add(new MqttUserProperty(name, value)); + return this; + } } \ No newline at end of file diff --git a/Source/MQTTnet/Options/MqttClientOptionsBuilder.cs b/Source/MQTTnet/Options/MqttClientOptionsBuilder.cs index 9b5f39a1..8c7e99bc 100644 --- a/Source/MQTTnet/Options/MqttClientOptionsBuilder.cs +++ b/Source/MQTTnet/Options/MqttClientOptionsBuilder.cs @@ -2,6 +2,7 @@ // 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.Sockets; using System.Text; @@ -357,6 +358,7 @@ public sealed class MqttClientOptionsBuilder return this; } + [Obsolete("Please use more performance `WithUserProperty` with ArraySegment or ReadOnlyMemory for the value.")] public MqttClientOptionsBuilder WithUserProperty(string name, string value) { if (_options.UserProperties == null) @@ -368,6 +370,28 @@ public sealed class MqttClientOptionsBuilder return this; } + public MqttClientOptionsBuilder WithUserProperty(string name, ReadOnlyMemory value) + { + if (_options.UserProperties == null) + { + _options.UserProperties = new List(); + } + + _options.UserProperties.Add(new MqttUserProperty(name, value)); + return this; + } + + public MqttClientOptionsBuilder WithUserProperty(string name, ArraySegment value) + { + if (_options.UserProperties == null) + { + _options.UserProperties = new List(); + } + + _options.UserProperties.Add(new MqttUserProperty(name, value)); + return this; + } + public MqttClientOptionsBuilder WithWebSocketServer(Action configure) { ArgumentNullException.ThrowIfNull(configure); @@ -462,10 +486,25 @@ public sealed class MqttClientOptionsBuilder return this; } + [Obsolete("Please use more performance `WithWillUserProperty` with ArraySegment or ReadOnlyMemory for the value.")] public MqttClientOptionsBuilder WithWillUserProperty(string name, string value) { _options.WillUserProperties ??= []; _options.WillUserProperties.Add(new MqttUserProperty(name, value)); return this; } + + public MqttClientOptionsBuilder WithWillUserProperty(string name, ReadOnlyMemory value) + { + _options.WillUserProperties ??= []; + _options.WillUserProperties.Add(new MqttUserProperty(name, value)); + return this; + } + + public MqttClientOptionsBuilder WithWillUserProperty(string name, ArraySegment value) + { + _options.WillUserProperties ??= []; + _options.WillUserProperties.Add(new MqttUserProperty(name, value)); + return this; + } } \ No newline at end of file diff --git a/Source/MQTTnet/Packets/MqttUserProperty.cs b/Source/MQTTnet/Packets/MqttUserProperty.cs index eb02fb70..401d45e0 100644 --- a/Source/MQTTnet/Packets/MqttUserProperty.cs +++ b/Source/MQTTnet/Packets/MqttUserProperty.cs @@ -2,19 +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. +using System.Text; + namespace MQTTnet.Packets; public sealed class MqttUserProperty { + readonly ReadOnlyMemory _valueBuffer; + + [Obsolete("Please use more performance constructor with ArraySegment or ReadOnlyMemory for the value.")] public MqttUserProperty(string name, string value) + : this(name, new ReadOnlyMemory(Encoding.UTF8.GetBytes(value ?? throw new ArgumentNullException(nameof(value))))) + { + } + + public MqttUserProperty(string name, ArraySegment value) + : this(name, CreateMemory(value)) + { + } + + public MqttUserProperty(string name, ReadOnlyMemory value) { Name = name ?? throw new ArgumentNullException(nameof(name)); - Value = value ?? throw new ArgumentNullException(nameof(value)); + _valueBuffer = value; } public string Name { get; } - public string Value { get; } + public ReadOnlyMemory ValueBuffer => _valueBuffer; + + [Obsolete("Please use more performance property ValueBuffer or the MqttUserPropertyExtensionMethod `ReadValueAsString`")] + public string Value => this.ReadValueAsString(); public override bool Equals(object obj) { @@ -33,16 +51,46 @@ public sealed class MqttUserProperty return true; } - return string.Equals(Name, other.Name, StringComparison.Ordinal) && string.Equals(Value, other.Value, StringComparison.Ordinal); + if (!string.Equals(Name, other.Name, StringComparison.Ordinal)) + { + return false; + } + + return _valueBuffer.Span.SequenceEqual(other._valueBuffer.Span); } + public override int GetHashCode() { - return Name.GetHashCode() ^ Value.GetHashCode(); + var hashCode = new HashCode(); + + if (!string.IsNullOrEmpty(Name)) + { + hashCode.Add(Name, StringComparer.Ordinal); + } + else + { + hashCode.Add(0); + } + + hashCode.AddBytes(_valueBuffer.Span); + + return hashCode.ToHashCode(); } + public override string ToString() { - return $"{Name} = {Value}"; + return $"{Name} = {this.ReadValueAsString()}"; + } + + static ReadOnlyMemory CreateMemory(ArraySegment value) + { + if (value.Array == null) + { + throw new ArgumentNullException(nameof(value)); + } + + return new ReadOnlyMemory(value.Array, value.Offset, value.Count); } } \ No newline at end of file diff --git a/Source/MQTTnet/Packets/MqttUserPropertyExtensions.cs b/Source/MQTTnet/Packets/MqttUserPropertyExtensions.cs new file mode 100644 index 00000000..03173e36 --- /dev/null +++ b/Source/MQTTnet/Packets/MqttUserPropertyExtensions.cs @@ -0,0 +1,27 @@ +// 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. + +using System; +using System.Text; + +namespace MQTTnet.Packets; + +public static class MqttUserPropertyExtensions +{ + /// + /// Reads the value of the user property as a UTF-8 string. + /// + public static string ReadValueAsString(this MqttUserProperty userProperty) + { + ArgumentNullException.ThrowIfNull(userProperty); + + var buffer = userProperty.ValueBuffer; + if (buffer.IsEmpty) + { + return string.Empty; + } + + return Encoding.UTF8.GetString(buffer.Span); + } +} diff --git a/Source/MQTTnet/Subscribing/MqttClientSubscribeOptionsBuilder.cs b/Source/MQTTnet/Subscribing/MqttClientSubscribeOptionsBuilder.cs index e49af713..5586d436 100644 --- a/Source/MQTTnet/Subscribing/MqttClientSubscribeOptionsBuilder.cs +++ b/Source/MQTTnet/Subscribing/MqttClientSubscribeOptionsBuilder.cs @@ -2,6 +2,7 @@ // 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.Exceptions; using MQTTnet.Packets; using MQTTnet.Protocol; @@ -77,6 +78,7 @@ public sealed class MqttClientSubscribeOptionsBuilder /// Adds the user property to the subscribe options. /// MQTT 5.0.0+ feature. /// + [Obsolete("Use the WithUserProperty accepting a ReadOnlyMemory for better performance.")] public MqttClientSubscribeOptionsBuilder WithUserProperty(string name, string value) { _subscribeOptions.UserProperties ??= []; @@ -84,4 +86,28 @@ public sealed class MqttClientSubscribeOptionsBuilder return this; } + + /// + /// Adds the user property to the subscribe options. + /// MQTT 5.0.0+ feature. + /// + public MqttClientSubscribeOptionsBuilder WithUserProperty(string name, ReadOnlyMemory value) + { + _subscribeOptions.UserProperties ??= []; + _subscribeOptions.UserProperties.Add(new MqttUserProperty(name, value)); + + return this; + } + + /// + /// Adds the user property to the subscribe options. + /// MQTT 5.0.0+ feature. + /// + public MqttClientSubscribeOptionsBuilder WithUserProperty(string name, ArraySegment value) + { + _subscribeOptions.UserProperties ??= []; + _subscribeOptions.UserProperties.Add(new MqttUserProperty(name, value)); + + return this; + } } \ No newline at end of file diff --git a/Source/MQTTnet/Unsubscribing/MqttClientUnsubscribeOptionsBuilder.cs b/Source/MQTTnet/Unsubscribing/MqttClientUnsubscribeOptionsBuilder.cs index abec6c35..aee2b973 100644 --- a/Source/MQTTnet/Unsubscribing/MqttClientUnsubscribeOptionsBuilder.cs +++ b/Source/MQTTnet/Unsubscribing/MqttClientUnsubscribeOptionsBuilder.cs @@ -2,6 +2,7 @@ // 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; @@ -36,11 +37,30 @@ public sealed class MqttClientUnsubscribeOptionsBuilder /// Adds the user property to the unsubscribe options. /// MQTT 5.0.0+ feature. /// + [Obsolete("Use the WithUserProperty accepting a ReadOnlyMemory for better performance.")] public MqttClientUnsubscribeOptionsBuilder WithUserProperty(string name, string value) { return WithUserProperty(new MqttUserProperty(name, value)); } + /// + /// Adds the user property to the unsubscribe options. + /// MQTT 5.0.0+ feature. + /// + public MqttClientUnsubscribeOptionsBuilder WithUserProperty(string name, ReadOnlyMemory value) + { + return WithUserProperty(new MqttUserProperty(name, value)); + } + + /// + /// Adds the user property to the unsubscribe options. + /// MQTT 5.0.0+ feature. + /// + public MqttClientUnsubscribeOptionsBuilder WithUserProperty(string name, ArraySegment value) + { + return WithUserProperty(new MqttUserProperty(name, value)); + } + /// /// Adds the user property to the unsubscribe options. /// MQTT 5.0.0+ feature. diff --git a/global.json b/global.json new file mode 100644 index 00000000..8f73781c --- /dev/null +++ b/global.json @@ -0,0 +1,5 @@ +{ + "test": { + "runner": "Microsoft.Testing.Platform" + } +} \ No newline at end of file