Allow user properties as pre-encoded UTF-8 binary buffer (#2228)

* Add support for binary user properties in MQTT 5.0

* Optimize byte array reversal in SubscriptionTopicHashTests

* Use InvariantCulture for topic string conversion in Load_Tests

* switched to `Microsoft.Testing.Platform`

* Refactor CI workflow to separate signed and unsigned package builds

* Add condition to sign job to prevent execution on external pull requests

* Remove fallback to value as string in MqttUserProperty

* Refactor MqttUserProperty handling to use byte arrays for improved performance and add deprecation warnings for legacy methods
This commit is contained in:
Alexander Köpke
2025-12-18 17:46:35 +01:00
committed by GitHub
parent 1a3e97afec
commit 07409e3a35
31 changed files with 494 additions and 73 deletions
+9 -3
View File
@@ -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
+20
View File
@@ -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<byte>(encodedValue))
.WithPayload("metadata")
.Build();
}
}
+2 -1
View File
@@ -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;
@@ -40,6 +40,7 @@ public sealed class MqttServerClientDisconnectOptionsBuilder
return this;
}
[Obsolete("Please use more performance `WithUserProperty` with ArraySegment<byte> or ReadOnlyMemory<byte> 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<byte> value)
{
if (_options.UserProperties == null)
{
_options.UserProperties = new List<MqttUserProperty>();
}
_options.UserProperties.Add(new MqttUserProperty(name, value));
return this;
}
public MqttServerClientDisconnectOptionsBuilder WithUserProperty(string name, ArraySegment<byte> value)
{
if (_options.UserProperties == null)
{
_options.UserProperties = new List<MqttUserProperty>();
}
_options.UserProperties.Add(new MqttUserProperty(name, value));
return this;
}
}
@@ -51,6 +51,7 @@ public sealed class ExchangeEnhancedAuthenticationOptionsFactory
return this;
}
[Obsolete("Please use more performance `WithUserProperty` with ArraySegment<byte> or ReadOnlyMemory<byte> 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<byte> value)
{
ArgumentNullException.ThrowIfNull(name);
if (_options.UserProperties == null)
{
_options.UserProperties = new List<MqttUserProperty>();
}
_options.UserProperties.Add(new MqttUserProperty(name, value));
return this;
}
public ExchangeEnhancedAuthenticationOptionsFactory WithUserProperty(string name, ArraySegment<byte> value)
{
ArgumentNullException.ThrowIfNull(name);
if (_options.UserProperties == null)
{
_options.UserProperties = new List<MqttUserProperty>();
}
_options.UserProperties.Add(new MqttUserProperty(name, value));
return this;
}
}
@@ -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]
@@ -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);
@@ -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);
+8 -3
View File
@@ -6,13 +6,12 @@
<LangVersion>default</LangVersion>
<Nullable>disable</Nullable>
<NoWarn>$(NoWarn);CA1707</NoWarn>
<EnableMSTestRunner>true</EnableMSTestRunner>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.IO.RecyclableMemoryStream" Version="3.0.1" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="18.0.0" />
<PackageReference Include="MSTest" Version="4.0.1" />
<PackageReference Include="MSTest" Version="4.0.2" />
<FrameworkReference Include="Microsoft.AspNetCore.App" />
</ItemGroup>
@@ -24,4 +23,10 @@
<ProjectReference Include="..\MQTTnet.Extensions.TopicTemplate\MQTTnet.Extensions.TopicTemplate.csproj" />
</ItemGroup>
<ItemGroup>
<None Include=".\..\..\global.json"
Link="global.json"
CopyToOutputDirectory="PreserveNewest"/>
</ItemGroup>
</Project>
+9 -8
View File
@@ -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)
@@ -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<byte>(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<MqttUserProperty> { 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<MqttUserProperty> { 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;
}
}
@@ -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<NotSupportedException>(() => MqttApplicationMessageValidator.ThrowIfNotSupported(
new MqttApplicationMessageBuilder().WithTopic("B").WithUserProperty("User", "Property").Build(),
new MqttApplicationMessageBuilder().WithTopic("B").WithUserProperty("User", Encoding.UTF8.GetBytes("Property")).Build(),
MqttProtocolVersion.V311));
}
}
@@ -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<NotSupportedException>(() =>
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<NotSupportedException>(() =>
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]
+3 -2
View File
@@ -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);
@@ -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();
@@ -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
@@ -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);
@@ -1,4 +1,5 @@
using System.Diagnostics;
using System.Linq;
using System.Text;
using MQTTnet.Packets;
using MQTTnet.Protocol;
@@ -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);
@@ -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
{
@@ -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<byte> or ReadOnlyMemory<byte> 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<byte> value)
{
_userProperties ??= [];
_userProperties.Add(new MqttUserProperty(name, value));
return this;
}
public MqttClientDisconnectOptionsBuilder WithUserProperty(string name, ArraySegment<byte> value)
{
_userProperties ??= [];
_userProperties.Add(new MqttUserProperty(name, value));
return this;
}
}
@@ -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<byte> 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);
@@ -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<MqttUserProperty>();
}
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<byte> ReadUserPropertyValueBuffer()
{
return _body.ReadBinaryData();
}
public bool ReadWildcardSubscriptionAvailable()
{
return _body.ReadByte() == 1;
@@ -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);
}
}
@@ -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.
/// <remarks>MQTT 5.0.0+ feature.</remarks>
/// </summary>
[Obsolete("Use the WithUserProperty accepting a ReadOnlyMemory<byte> for better performance.")]
public MqttApplicationMessageBuilder WithUserProperty(string name, string value)
{
_userProperties ??= [];
_userProperties.Add(new MqttUserProperty(name, value));
return this;
}
/// <summary>
/// Adds the user property to the message using a pre-encoded UTF-8 value buffer.
/// <remarks>MQTT 5.0.0+ feature.</remarks>
/// </summary>
public MqttApplicationMessageBuilder WithUserProperty(string name, ReadOnlyMemory<byte> value)
{
_userProperties ??= [];
_userProperties.Add(new MqttUserProperty(name, value));
return this;
}
/// <summary>
/// Adds the user property to the message using a pre-encoded UTF-8 value buffer.
/// <remarks>MQTT 5.0.0+ feature.</remarks>
/// </summary>
public MqttApplicationMessageBuilder WithUserProperty(string name, ArraySegment<byte> value)
{
_userProperties ??= [];
_userProperties.Add(new MqttUserProperty(name, value));
return this;
}
}
@@ -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<byte> or ReadOnlyMemory<byte> 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<byte> value)
{
if (_options.UserProperties == null)
{
_options.UserProperties = new List<MqttUserProperty>();
}
_options.UserProperties.Add(new MqttUserProperty(name, value));
return this;
}
public MqttClientOptionsBuilder WithUserProperty(string name, ArraySegment<byte> value)
{
if (_options.UserProperties == null)
{
_options.UserProperties = new List<MqttUserProperty>();
}
_options.UserProperties.Add(new MqttUserProperty(name, value));
return this;
}
public MqttClientOptionsBuilder WithWebSocketServer(Action<MqttClientWebSocketOptionsBuilder> configure)
{
ArgumentNullException.ThrowIfNull(configure);
@@ -462,10 +486,25 @@ public sealed class MqttClientOptionsBuilder
return this;
}
[Obsolete("Please use more performance `WithWillUserProperty` with ArraySegment<byte> or ReadOnlyMemory<byte> 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<byte> value)
{
_options.WillUserProperties ??= [];
_options.WillUserProperties.Add(new MqttUserProperty(name, value));
return this;
}
public MqttClientOptionsBuilder WithWillUserProperty(string name, ArraySegment<byte> value)
{
_options.WillUserProperties ??= [];
_options.WillUserProperties.Add(new MqttUserProperty(name, value));
return this;
}
}
+53 -5
View File
@@ -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<byte> _valueBuffer;
[Obsolete("Please use more performance constructor with ArraySegment<byte> or ReadOnlyMemory<byte> for the value.")]
public MqttUserProperty(string name, string value)
: this(name, new ReadOnlyMemory<byte>(Encoding.UTF8.GetBytes(value ?? throw new ArgumentNullException(nameof(value)))))
{
}
public MqttUserProperty(string name, ArraySegment<byte> value)
: this(name, CreateMemory(value))
{
}
public MqttUserProperty(string name, ReadOnlyMemory<byte> 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<byte> 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<byte> CreateMemory(ArraySegment<byte> value)
{
if (value.Array == null)
{
throw new ArgumentNullException(nameof(value));
}
return new ReadOnlyMemory<byte>(value.Array, value.Offset, value.Count);
}
}
@@ -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
{
/// <summary>
/// Reads the value of the user property as a UTF-8 string.
/// </summary>
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);
}
}
@@ -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.
/// <remarks>MQTT 5.0.0+ feature.</remarks>
/// </summary>
[Obsolete("Use the WithUserProperty accepting a ReadOnlyMemory<byte> for better performance.")]
public MqttClientSubscribeOptionsBuilder WithUserProperty(string name, string value)
{
_subscribeOptions.UserProperties ??= [];
@@ -84,4 +86,28 @@ public sealed class MqttClientSubscribeOptionsBuilder
return this;
}
/// <summary>
/// Adds the user property to the subscribe options.
/// <remarks>MQTT 5.0.0+ feature.</remarks>
/// </summary>
public MqttClientSubscribeOptionsBuilder WithUserProperty(string name, ReadOnlyMemory<byte> value)
{
_subscribeOptions.UserProperties ??= [];
_subscribeOptions.UserProperties.Add(new MqttUserProperty(name, value));
return this;
}
/// <summary>
/// Adds the user property to the subscribe options.
/// <remarks>MQTT 5.0.0+ feature.</remarks>
/// </summary>
public MqttClientSubscribeOptionsBuilder WithUserProperty(string name, ArraySegment<byte> value)
{
_subscribeOptions.UserProperties ??= [];
_subscribeOptions.UserProperties.Add(new MqttUserProperty(name, value));
return this;
}
}
@@ -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.
/// <remarks>MQTT 5.0.0+ feature.</remarks>
/// </summary>
[Obsolete("Use the WithUserProperty accepting a ReadOnlyMemory<byte> for better performance.")]
public MqttClientUnsubscribeOptionsBuilder WithUserProperty(string name, string value)
{
return WithUserProperty(new MqttUserProperty(name, value));
}
/// <summary>
/// Adds the user property to the unsubscribe options.
/// <remarks>MQTT 5.0.0+ feature.</remarks>
/// </summary>
public MqttClientUnsubscribeOptionsBuilder WithUserProperty(string name, ReadOnlyMemory<byte> value)
{
return WithUserProperty(new MqttUserProperty(name, value));
}
/// <summary>
/// Adds the user property to the unsubscribe options.
/// <remarks>MQTT 5.0.0+ feature.</remarks>
/// </summary>
public MqttClientUnsubscribeOptionsBuilder WithUserProperty(string name, ArraySegment<byte> value)
{
return WithUserProperty(new MqttUserProperty(name, value));
}
/// <summary>
/// Adds the user property to the unsubscribe options.
/// <remarks>MQTT 5.0.0+ feature.</remarks>
+5
View File
@@ -0,0 +1,5 @@
{
"test": {
"runner": "Microsoft.Testing.Platform"
}
}