Skip to content
Open
Show file tree
Hide file tree
Changes from 6 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 9 additions & 3 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -22,15 +22,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:
Expand All @@ -53,12 +59,12 @@ jobs:
uses: actions/checkout@v4

- name: Execute tests
run: dotnet test --framework net8.0 --configuration Release Source/MQTTnet.Tests/MQTTnet.Tests.csproj
run: dotnet test --framework net8.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
Expand Down
20 changes: 20 additions & 0 deletions Samples/Client/Client_Publish_Samples.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -84,4 +87,21 @@ public static async Task Publish_Multiple_Application_Messages()

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();
}
}
12 changes: 9 additions & 3 deletions Source/MQTTnet.Tests/MQTTnet.Tests.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -11,9 +11,9 @@

<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.TestFramework" Version="4.0.2" />
<PackageReference Include="MSTest" Version="4.0.2" />
<PackageReference Include="MSTest.TestAdapter" Version="4.0.2" />
<FrameworkReference Include="Microsoft.AspNetCore.App" />
</ItemGroup>

Expand All @@ -25,4 +25,10 @@
<ProjectReference Include="..\MQTTnet.Extensions.TopicTemplate\MQTTnet.Extensions.TopicTemplate.csproj" />
</ItemGroup>


<ItemGroup>
<None Include=".\..\..\global.json"
Link="global.json"
CopyToOutputDirectory="PreserveNewest"/>
</ItemGroup>
</Project>
65 changes: 65 additions & 0 deletions Source/MQTTnet.Tests/MqttApplicationMessageBuilder_Tests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -59,4 +64,64 @@ public void CreateApplicationMessage_QosLevel2()
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];
Assert.IsTrue(userProperty.HasValueBuffer);
CollectionAssert.AreEqual(buffer, userProperty.ValueBuffer.ToArray());
Assert.AreEqual(value, userProperty.Value);
}

[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];
Assert.IsTrue(userProperty.HasValueBuffer);
CollectionAssert.AreEqual(buffer, userProperty.ValueBuffer.ToArray());
Assert.AreEqual("segment", userProperty.Value);
}

[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);
stringPropertiesWriter.WriteUserProperties(new List<MqttUserProperty> { new(name, value) });

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;
}
}
5 changes: 3 additions & 2 deletions Source/MQTTnet.Tests/Server/Load_Tests.cs
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
#if DEBUG

using System.Globalization;
using MQTTnet.Internal;
using MQTTnet.Packets;
using MQTTnet.Protocol;
Expand Down Expand Up @@ -52,7 +53,7 @@ await client.SendAsync(

for (var j = 0; j < 1000; j++)
{
publishPacket.Topic = j.ToString();
publishPacket.Topic = j.ToString(CultureInfo.InvariantCulture);

await client.SendAsync(publishPacket, CancellationToken.None);
}
Expand Down Expand Up @@ -139,7 +140,7 @@ public async Task Handle_100_000_Messages_In_Server()

for (var j = 0; j < 1000; j++)
{
var message = applicationMessageBuilder.WithTopic(j.ToString()).Build();
var message = applicationMessageBuilder.WithTopic(j.ToString(CultureInfo.InvariantCulture)).Build();

await client.PublishAsync(message);
}
Expand Down
4 changes: 3 additions & 1 deletion Source/MQTTnet.Tests/Server/Subscription_TopicHash_Tests.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
using System.Diagnostics;
using System.Linq;
using System.Text;
using MQTTnet.Packets;
using MQTTnet.Protocol;
Expand Down Expand Up @@ -523,7 +524,8 @@ static byte[] GetBytes(ulong value)
// Ensure that highest byte comes first for comparison left to right
if (BitConverter.IsLittleEndian)
{
return bytes.Reverse().ToArray();
Array.Reverse(bytes);
return bytes;
}

return bytes;
Expand Down
15 changes: 15 additions & 0 deletions Source/MQTTnet/Disconnecting/MqttClientDisconnectOptionsBuilder.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -54,4 +55,18 @@ public MqttClientDisconnectOptionsBuilder WithUserProperty(string name, string v
_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;
}
}
24 changes: 24 additions & 0 deletions Source/MQTTnet/Formatter/MqttBufferWriter.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -193,6 +194,29 @@ public void WriteString(string value)
}
}

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);
Expand Down
9 changes: 8 additions & 1 deletion Source/MQTTnet/Formatter/V5/MqttV5PropertiesWriter.cs
Original file line number Diff line number Diff line change
Expand Up @@ -260,7 +260,14 @@ public void WriteUserProperties(List<MqttUserProperty> userProperties)
{
_bufferWriter.WriteByte((byte)MqttPropertyId.UserProperty);
_bufferWriter.WriteString(property.Name);
_bufferWriter.WriteString(property.Value);
if (property.HasValueBuffer)
{
_bufferWriter.WriteString(property.ValueBuffer);
}
else
{
_bufferWriter.WriteString(property.Value);
}
}
}

Expand Down
23 changes: 23 additions & 0 deletions Source/MQTTnet/MqttApplicationMessageBuilder.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -269,4 +270,26 @@ public MqttApplicationMessageBuilder WithUserProperty(string name, string value)
_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;
}
}
37 changes: 37 additions & 0 deletions Source/MQTTnet/Options/MqttClientOptionsBuilder.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -368,6 +369,28 @@ public MqttClientOptionsBuilder WithUserProperty(string name, string value)
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);
Expand Down Expand Up @@ -468,4 +491,18 @@ public MqttClientOptionsBuilder WithWillUserProperty(string name, string value)
_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;
}
}
Loading