diff --git a/Samples/Client/Client_Subscribe_Samples.cs b/Samples/Client/Client_Subscribe_Samples.cs
index 8e9f742e2..bebecc99e 100644
--- a/Samples/Client/Client_Subscribe_Samples.cs
+++ b/Samples/Client/Client_Subscribe_Samples.cs
@@ -17,8 +17,8 @@ namespace MQTTnet.Samples.Client;
public static class Client_Subscribe_Samples
{
- static MqttTopicTemplate sampleTemplate = new MqttTopicTemplate("mqttnet/samples/topic/{id}");
-
+ static readonly MqttTopicTemplate sampleTemplate = new("mqttnet/samples/topic/{id}");
+
public static async Task Handle_Received_Application_Message()
{
/*
@@ -44,9 +44,7 @@ public static async Task Handle_Received_Application_Message()
await mqttClient.ConnectAsync(mqttClientOptions, CancellationToken.None);
- var mqttSubscribeOptions = mqttFactory.CreateSubscribeOptionsBuilder()
- .WithTopicTemplate(sampleTemplate.WithParameter("id", "2"))
- .Build();
+ var mqttSubscribeOptions = mqttFactory.CreateSubscribeOptionsBuilder().WithTopicTemplate(sampleTemplate.WithParameter("id", "2")).Build();
await mqttClient.SubscribeAsync(mqttSubscribeOptions, CancellationToken.None);
@@ -86,10 +84,7 @@ public static async Task Send_Responses()
await mqttClient.ConnectAsync(mqttClientOptions, CancellationToken.None);
- var mqttSubscribeOptions = mqttFactory.CreateSubscribeOptionsBuilder()
- .WithTopicTemplate(
- sampleTemplate.WithParameter("id", "1"))
- .Build();
+ var mqttSubscribeOptions = mqttFactory.CreateSubscribeOptionsBuilder().WithTopicTemplate(sampleTemplate.WithParameter("id", "1")).Build();
var response = await mqttClient.SubscribeAsync(mqttSubscribeOptions, CancellationToken.None);
@@ -117,12 +112,9 @@ public static async Task Subscribe_Multiple_Topics()
// Create the subscribe options including several topics with different options.
// It is also possible to all of these topics using a dedicated call of _SubscribeAsync_ per topic.
var mqttSubscribeOptions = mqttFactory.CreateSubscribeOptionsBuilder()
- .WithTopicTemplate(
- sampleTemplate.WithParameter("id", "1"))
- .WithTopicTemplate(
- sampleTemplate.WithParameter("id", "2"), noLocal: true)
- .WithTopicTemplate(
- sampleTemplate.WithParameter("id", "3"), retainHandling: MqttRetainHandling.SendAtSubscribe)
+ .WithTopicTemplate(sampleTemplate.WithParameter("id", "1"))
+ .WithTopicTemplate(sampleTemplate.WithParameter("id", "2"), noLocal: true)
+ .WithTopicTemplate(sampleTemplate.WithParameter("id", "3"), retainHandling: MqttRetainHandling.SendAtSubscribe)
.Build();
var response = await mqttClient.SubscribeAsync(mqttSubscribeOptions, CancellationToken.None);
@@ -148,9 +140,7 @@ public static async Task Subscribe_Topic()
await mqttClient.ConnectAsync(mqttClientOptions, CancellationToken.None);
- var mqttSubscribeOptions = mqttFactory.CreateSubscribeOptionsBuilder()
- .WithTopicTemplate(sampleTemplate.WithParameter("id", "1"))
- .Build();
+ var mqttSubscribeOptions = mqttFactory.CreateSubscribeOptionsBuilder().WithTopicTemplate(sampleTemplate.WithParameter("id", "1")).Build();
var response = await mqttClient.SubscribeAsync(mqttSubscribeOptions, CancellationToken.None);
diff --git a/Samples/MQTTnet.Samples.csproj b/Samples/MQTTnet.Samples.csproj
index 88e39be5e..6d3920a22 100644
--- a/Samples/MQTTnet.Samples.csproj
+++ b/Samples/MQTTnet.Samples.csproj
@@ -20,6 +20,7 @@
+
diff --git a/Source/MQTTnet.AspnetCore/MqttHostedServer.cs b/Source/MQTTnet.AspnetCore/MqttHostedServer.cs
index b34adc3d5..4c74f6a43 100644
--- a/Source/MQTTnet.AspnetCore/MqttHostedServer.cs
+++ b/Source/MQTTnet.AspnetCore/MqttHostedServer.cs
@@ -7,60 +7,42 @@
using System.Threading;
using System.Threading.Tasks;
using Microsoft.Extensions.Hosting;
-using MQTTnet.Adapter;
-using MQTTnet.Diagnostics;
+using MQTTnet.Diagnostics.Logger;
using MQTTnet.Server;
-namespace MQTTnet.AspNetCore
+namespace MQTTnet.AspNetCore;
+
+public sealed class MqttHostedServer : MqttServer, IHostedService
{
- public sealed class MqttHostedServer : MqttServer, IHostedService
- {
- readonly MqttFactory _mqttFactory;
-#if NETCOREAPP3_1_OR_GREATER
- readonly IHostApplicationLifetime _hostApplicationLifetime;
- public MqttHostedServer(IHostApplicationLifetime hostApplicationLifetime, MqttFactory mqttFactory,
- MqttServerOptions options, IEnumerable adapters, IMqttNetLogger logger) : base(
- options,
- adapters,
- logger)
- {
- _mqttFactory = mqttFactory ?? throw new ArgumentNullException(nameof(mqttFactory));
- _hostApplicationLifetime = hostApplicationLifetime;
- }
-#else
- public MqttHostedServer(MqttFactory mqttFactory,
- MqttServerOptions options, IEnumerable adapters, IMqttNetLogger logger) : base(
- options,
- adapters,
- logger)
- {
- _mqttFactory = mqttFactory ?? throw new ArgumentNullException(nameof(mqttFactory));
- }
-#endif
+ readonly IHostApplicationLifetime _hostApplicationLifetime;
+ readonly MqttServerFactory _mqttFactory;
+ public MqttHostedServer(
+ IHostApplicationLifetime hostApplicationLifetime,
+ MqttServerFactory mqttFactory,
+ MqttServerOptions options,
+ IEnumerable adapters,
+ IMqttNetLogger logger) : base(options, adapters, logger)
+ {
+ _mqttFactory = mqttFactory ?? throw new ArgumentNullException(nameof(mqttFactory));
+ _hostApplicationLifetime = hostApplicationLifetime;
+ }
- public async Task StartAsync(CancellationToken cancellationToken)
- {
- // The yield makes sure that the hosted service is considered up and running.
- await Task.Yield();
-#if NETCOREAPP3_1_OR_GREATER
- _hostApplicationLifetime.ApplicationStarted.Register(OnStarted);
-#else
- _ = StartAsync();
-#endif
+ public async Task StartAsync(CancellationToken cancellationToken)
+ {
+ // The yield makes sure that the hosted service is considered up and running.
+ await Task.Yield();
- }
+ _hostApplicationLifetime.ApplicationStarted.Register(OnStarted);
+ }
- public Task StopAsync(CancellationToken cancellationToken)
- {
- return StopAsync(_mqttFactory.CreateMqttServerStopOptionsBuilder().Build());
- }
+ public Task StopAsync(CancellationToken cancellationToken)
+ {
+ return StopAsync(_mqttFactory.CreateMqttServerStopOptionsBuilder().Build());
+ }
-#if NETCOREAPP3_1_OR_GREATER
- private void OnStarted()
- {
- _ = StartAsync();
- }
-#endif
+ void OnStarted()
+ {
+ _ = StartAsync();
}
}
\ No newline at end of file
diff --git a/Source/MQTTnet.Extensions.TopicTemplate/MQTTnet.Extensions.TopicTemplate.csproj b/Source/MQTTnet.Extensions.TopicTemplate/MQTTnet.Extensions.TopicTemplate.csproj
index 14d653c04..be72adc34 100644
--- a/Source/MQTTnet.Extensions.TopicTemplate/MQTTnet.Extensions.TopicTemplate.csproj
+++ b/Source/MQTTnet.Extensions.TopicTemplate/MQTTnet.Extensions.TopicTemplate.csproj
@@ -1,10 +1,7 @@
- netstandard1.3;netstandard2.0;netstandard2.1;netcoreapp3.1;net5.0;net6.0;net7.0
- $(TargetFrameworks);net452;net461;net48
- $(TargetFrameworks);uap10.0
-
+ net8.0
MQTTnet.Extensions.TopicTemplate
MQTTnet.Extensions.TopicTemplate
True
@@ -60,7 +57,7 @@
\
-
+
diff --git a/Source/MQTTnet.Extensions.TopicTemplate/MqttTopicTemplate.cs b/Source/MQTTnet.Extensions.TopicTemplate/MqttTopicTemplate.cs
index f5dc48f11..25b3f3eaa 100644
--- a/Source/MQTTnet.Extensions.TopicTemplate/MqttTopicTemplate.cs
+++ b/Source/MQTTnet.Extensions.TopicTemplate/MqttTopicTemplate.cs
@@ -9,349 +9,347 @@
using System.Threading;
using MQTTnet.Protocol;
-namespace MQTTnet.Extensions.TopicTemplate
+namespace MQTTnet.Extensions.TopicTemplate;
+
+///
+/// A topic template is an MQTT topic filter string that may contain
+/// segments in curly braces called parameters. This well-known
+/// 'moustache' syntax also matches AsyncAPI Channel Address Expressions.
+/// The topic template is designed to support dynamic subscription/publication,
+/// message-topic matching and routing. It is intended to be more safe and
+/// convenient than String.Format() for aforementioned purposes.
+///
+///
+/// topic/subtopic/{parameter}/{otherParameter}
+///
+public sealed class MqttTopicTemplate : IEquatable
{
+ static readonly Regex MoustacheRegex = new("{([^/]+?)}", RegexOptions.Compiled);
+
+ readonly string[] _parameterSegments;
+
+ string _topicFilter;
+
///
- /// A topic template is an MQTT topic filter string that may contain
- /// segments in curly braces called parameters. This well-known
- /// 'moustache' syntax also matches AsyncAPI Channel Address Expressions.
- /// The topic template is designed to support dynamic subscription/publication,
- /// message-topic matching and routing. It is intended to be more safe and
- /// convenient than String.Format() for aforementioned purposes.
+ /// Create a topic template from an mqtt topic filter with moustache placeholders.
///
- ///
- /// topic/subtopic/{parameter}/{otherParameter}
- ///
- public sealed class MqttTopicTemplate : IEquatable
+ ///
+ ///
+ ///
+ ///
+ public MqttTopicTemplate(string topicTemplate)
{
- static readonly Regex MoustacheRegex = new Regex("{([^/]+?)}", RegexOptions.Compiled);
-
- readonly string[] _parameterSegments;
-
- string _topicFilter;
-
- ///
- /// Create a topic template from an mqtt topic filter with moustache placeholders.
- ///
- ///
- ///
- ///
- ///
- public MqttTopicTemplate(string topicTemplate)
+ if (topicTemplate == null)
{
- if (topicTemplate == null)
- {
- throw new ArgumentNullException(nameof(topicTemplate));
- }
-
- MqttTopicValidator.ThrowIfInvalidSubscribe(topicTemplate);
-
- Template = topicTemplate;
- _parameterSegments = topicTemplate.Split(MqttTopicFilterComparer.LevelSeparator)
- .Select(segment => MoustacheRegex.Match(segment).Groups[1].Value)
- .Select(s => s.Length > 0 ? s : null)
- .ToArray();
+ throw new ArgumentNullException(nameof(topicTemplate));
}
-
- ///
- /// Yield the template parameter names.
- ///
- public IEnumerable Parameters => _parameterSegments.Where(s => s != null);
-
- ///
- /// The topic template string representation, e.g. A/B/{foo}/D.
- ///
- public string Template { get; }
-
- ///
- /// The topic template as an MQTT topic filter (+ substituted for all parameters). If the template
- /// ends with a multi-level wildcard (hash), this will be reflected here.
- ///
- public string TopicFilter
+
+ MqttTopicValidator.ThrowIfInvalidSubscribe(topicTemplate);
+
+ Template = topicTemplate;
+ _parameterSegments = topicTemplate.Split(MqttTopicFilterComparer.LevelSeparator)
+ .Select(segment => MoustacheRegex.Match(segment).Groups[1].Value)
+ .Select(s => s.Length > 0 ? s : null)
+ .ToArray();
+ }
+
+ ///
+ /// Yield the template parameter names.
+ ///
+ public IEnumerable Parameters => _parameterSegments.Where(s => s != null);
+
+ ///
+ /// The topic template string representation, e.g. A/B/{foo}/D.
+ ///
+ public string Template { get; }
+
+ ///
+ /// The topic template as an MQTT topic filter (+ substituted for all parameters). If the template
+ /// ends with a multi-level wildcard (hash), this will be reflected here.
+ ///
+ public string TopicFilter
+ {
+ get
{
- get
- {
- LazyInitializer.EnsureInitialized(ref _topicFilter, () => MoustacheRegex.Replace(Template, MqttTopicFilterComparer.SingleLevelWildcard.ToString()));
- return _topicFilter;
- }
+ LazyInitializer.EnsureInitialized(ref _topicFilter, () => MoustacheRegex.Replace(Template, MqttTopicFilterComparer.SingleLevelWildcard.ToString()));
+ return _topicFilter;
}
-
- ///
- /// Return the topic filter of this template, ending with a multi-level wildcard (hash).
- ///
- public string TopicTreeRootFilter
+ }
+
+ ///
+ /// Return the topic filter of this template, ending with a multi-level wildcard (hash).
+ ///
+ public string TopicTreeRootFilter
+ {
+ get
{
- get
+ var filter = TopicFilter;
+ // append slash if neccessary
+ if (filter.Length > 0 && !filter.EndsWith(MqttTopicFilterComparer.LevelSeparator.ToString()) && !filter.EndsWith(MqttTopicFilterComparer.MultiLevelWildcard.ToString()))
{
- var filter = TopicFilter;
- // append slash if neccessary
- if (filter.Length > 0 && !filter.EndsWith(MqttTopicFilterComparer.LevelSeparator.ToString()) &&
- !filter.EndsWith(MqttTopicFilterComparer.MultiLevelWildcard.ToString()))
- {
- filter += MqttTopicFilterComparer.LevelSeparator;
- }
-
- // append hash if neccessary
- if (!filter.EndsWith(MqttTopicFilterComparer.MultiLevelWildcard.ToString()))
- {
- filter += MqttTopicFilterComparer.MultiLevelWildcard;
- }
-
- return filter;
+ filter += MqttTopicFilterComparer.LevelSeparator;
+ }
+
+ // append hash if neccessary
+ if (!filter.EndsWith(MqttTopicFilterComparer.MultiLevelWildcard.ToString()))
+ {
+ filter += MqttTopicFilterComparer.MultiLevelWildcard;
}
+
+ return filter;
}
-
- public bool Equals(MqttTopicTemplate other)
+ }
+
+ public bool Equals(MqttTopicTemplate other)
+ {
+ return other != null && Template == other.Template;
+ }
+
+ public override bool Equals(object obj)
+ {
+ if (ReferenceEquals(null, obj))
{
- return other != null && Template == other.Template;
+ return false;
}
-
- public override bool Equals(object obj)
+
+ if (ReferenceEquals(this, obj))
{
- if (ReferenceEquals(null, obj))
- {
- return false;
- }
-
- if (ReferenceEquals(this, obj))
- {
- return true;
- }
-
- if (obj.GetType() != GetType())
- {
- return false;
- }
-
- return Equals((MqttTopicTemplate)obj);
+ return true;
}
-
- ///
- /// Determine the shortest common prefix of the given templates. Partial segments
- /// are not returned.
- ///
- ///
- /// topic templates
- ///
- ///
- public static MqttTopicTemplate FindCanonicalPrefix(IEnumerable templates)
+
+ if (obj.GetType() != GetType())
{
- string root = null;
-
- string CommonPrefix(string a, string b)
+ return false;
+ }
+
+ return Equals((MqttTopicTemplate)obj);
+ }
+
+ ///
+ /// Determine the shortest common prefix of the given templates. Partial segments
+ /// are not returned.
+ ///
+ ///
+ /// topic templates
+ ///
+ ///
+ public static MqttTopicTemplate FindCanonicalPrefix(IEnumerable templates)
+ {
+ string root = null;
+
+ string CommonPrefix(string a, string b)
+ {
+ var maxIndex = Math.Min(a.Length, b.Length) - 1;
+ for (var i = 0; i <= maxIndex; i++)
{
- var maxIndex = Math.Min(a.Length, b.Length) - 1;
- for (var i = 0; i <= maxIndex; i++)
+ if (a[i] != b[i])
{
- if (a[i] != b[i])
- {
- return a.Substring(0, i);
- }
+ return a.Substring(0, i);
}
-
- return a.Substring(0, maxIndex+1);
- }
-
- foreach (string topic in from template in templates select template.Template)
- {
- root = root == null ? topic : CommonPrefix(root, topic);
- }
-
- if (string.IsNullOrEmpty(root))
- return new MqttTopicTemplate(MqttTopicFilterComparer.MultiLevelWildcard.ToString());
-
- if (root.Contains(MqttTopicFilterComparer.LevelSeparator) &&
- !root.EndsWith(MqttTopicFilterComparer.LevelSeparator.ToString()) &&
- !root.EndsWith("}"))
- {
- root = root.Substring(0, root.LastIndexOf(MqttTopicFilterComparer.LevelSeparator)+1);
}
-
- if (root.EndsWith(MqttTopicFilterComparer.LevelSeparator.ToString()))
- root += MqttTopicFilterComparer.SingleLevelWildcard;
- return new MqttTopicTemplate(root);
+ return a.Substring(0, maxIndex + 1);
}
-
- public override int GetHashCode()
+
+ foreach (var topic in from template in templates select template.Template)
{
- return Template.GetHashCode();
+ root = root == null ? topic : CommonPrefix(root, topic);
}
-
- ///
- /// Test if this topic template matches a given topic.
- ///
- ///
- /// a fully specified topic
- ///
- ///
- /// true to match including the subtree (multi-level wildcard)
- ///
- /// true iff the topic matches the template's filter
- ///
- ///
- ///
- /// if the topic is invalid
- ///
- public bool MatchesTopic(string topic, bool subtree = false)
+
+ if (string.IsNullOrEmpty(root))
{
- var comparison = MqttTopicFilterComparer.Compare(topic, subtree ? TopicTreeRootFilter : TopicFilter);
- if (comparison == MqttTopicFilterCompareResult.FilterInvalid)
- {
- throw new InvalidOperationException("Invalid filter");
- }
-
- if (comparison == MqttTopicFilterCompareResult.TopicInvalid)
- {
- throw new ArgumentException("Invalid topic", nameof(topic));
- }
-
- return comparison == MqttTopicFilterCompareResult.IsMatch;
+ return new MqttTopicTemplate(MqttTopicFilterComparer.MultiLevelWildcard.ToString());
}
-
- ///
- /// Extract the parameter values from a topic corresponding to the template
- /// parameters. The topic has to match this template.
- ///
- ///
- /// the topic
- ///
- /// an enumeration of (parameter, index, value)
- public IEnumerable<(string parameter, int index, string value)> ParseParameterValues(string topic)
+
+ if (root.Contains(MqttTopicFilterComparer.LevelSeparator) && !root.EndsWith(MqttTopicFilterComparer.LevelSeparator.ToString()) && !root.EndsWith("}"))
{
- if (!MatchesTopic(topic))
- {
- throw new ArgumentException("the topic has to match this template", nameof(topic));
- }
-
- return parseParameterValuesInternal(topic);
+ root = root.Substring(0, root.LastIndexOf(MqttTopicFilterComparer.LevelSeparator) + 1);
}
-
- ///
- /// Extract the parameter values from the message topic corresponding to the template
- /// parameters. The message topic has to match this topic template.
- ///
- ///
- /// the message
- ///
- /// an enumeration of (parameter, index, value)
- public IEnumerable<(string parameter, int index, string value)> ParseParameterValues(MqttApplicationMessage message)
+
+ if (root.EndsWith(MqttTopicFilterComparer.LevelSeparator.ToString()))
{
- return ParseParameterValues(message.Topic);
+ root += MqttTopicFilterComparer.SingleLevelWildcard;
}
-
- ///
- /// Try to set a parameter to a given value. If the parameter is not present,
- /// this is returned. The value must not contain slashes.
- ///
- ///
- /// a template parameter
- ///
- ///
- /// a string
- ///
- ///
- public MqttTopicTemplate TrySetParameter(string parameter, string value)
+
+ return new MqttTopicTemplate(root);
+ }
+
+ public override int GetHashCode()
+ {
+ return Template.GetHashCode();
+ }
+
+ ///
+ /// Test if this topic template matches a given topic.
+ ///
+ ///
+ /// a fully specified topic
+ ///
+ ///
+ /// true to match including the subtree (multi-level wildcard)
+ ///
+ /// true iff the topic matches the template's filter
+ ///
+ ///
+ ///
+ /// if the topic is invalid
+ ///
+ public bool MatchesTopic(string topic, bool subtree = false)
+ {
+ var comparison = MqttTopicFilterComparer.Compare(topic, subtree ? TopicTreeRootFilter : TopicFilter);
+ if (comparison == MqttTopicFilterCompareResult.FilterInvalid)
{
- if (parameter != null && _parameterSegments.Contains(parameter))
- {
- return WithParameter(parameter, value);
- }
-
- return this;
+ throw new InvalidOperationException("Invalid filter");
}
-
- ///
- /// Replace the given parameter with a single-level wildcard (plus sign).
- ///
- ///
- /// parameter name
- ///
- /// the topic template (without the parameter)
- public MqttTopicTemplate WithoutParameter(string parameter)
+
+ if (comparison == MqttTopicFilterCompareResult.TopicInvalid)
{
- if (string.IsNullOrEmpty(parameter) || !_parameterSegments.Contains(parameter))
- {
- throw new ArgumentException("topic template parameter must exist.");
- }
-
- return ReplaceInternal(parameter, MqttTopicFilterComparer.SingleLevelWildcard.ToString());
+ throw new ArgumentException("Invalid topic", nameof(topic));
}
-
- ///
- /// Substitute a parameter with a given value, thus removing the parameter. If the parameter is not present,
- /// the method trows. The value must not contain slashes or wildcards.
- ///
- ///
- /// a template parameter
- ///
- ///
- /// a string
- ///
- ///
- /// when the parameter is not present
- ///
- /// the topic template (without the parameter)
- public MqttTopicTemplate WithParameter(string parameter, string value)
+
+ return comparison == MqttTopicFilterCompareResult.IsMatch;
+ }
+
+ ///
+ /// Extract the parameter values from a topic corresponding to the template
+ /// parameters. The topic has to match this template.
+ ///
+ ///
+ /// the topic
+ ///
+ /// an enumeration of (parameter, index, value)
+ public IEnumerable<(string parameter, int index, string value)> ParseParameterValues(string topic)
+ {
+ if (!MatchesTopic(topic))
{
- if (value == null || string.IsNullOrEmpty(parameter) || !_parameterSegments.Contains(parameter) ||
- value.Contains(MqttTopicFilterComparer.LevelSeparator) ||
- value.Contains(MqttTopicFilterComparer.SingleLevelWildcard) ||
- value.Contains(MqttTopicFilterComparer.MultiLevelWildcard))
- {
- throw new ArgumentException("parameter must exist and value must not contain slashes or wildcard.");
- }
-
- return ReplaceInternal(parameter, value);
+ throw new ArgumentException("the topic has to match this template", nameof(topic));
+ }
+
+ return parseParameterValuesInternal(topic);
+ }
+
+ ///
+ /// Extract the parameter values from the message topic corresponding to the template
+ /// parameters. The message topic has to match this topic template.
+ ///
+ ///
+ /// the message
+ ///
+ /// an enumeration of (parameter, index, value)
+ public IEnumerable<(string parameter, int index, string value)> ParseParameterValues(MqttApplicationMessage message)
+ {
+ return ParseParameterValues(message.Topic);
+ }
+
+ ///
+ /// Try to set a parameter to a given value. If the parameter is not present,
+ /// this is returned. The value must not contain slashes.
+ ///
+ ///
+ /// a template parameter
+ ///
+ ///
+ /// a string
+ ///
+ ///
+ public MqttTopicTemplate TrySetParameter(string parameter, string value)
+ {
+ if (parameter != null && _parameterSegments.Contains(parameter))
+ {
+ return WithParameter(parameter, value);
}
-
- private MqttTopicTemplate ReplaceInternal(string parameter, string value)
+
+ return this;
+ }
+
+ ///
+ /// Replace the given parameter with a single-level wildcard (plus sign).
+ ///
+ ///
+ /// parameter name
+ ///
+ /// the topic template (without the parameter)
+ public MqttTopicTemplate WithoutParameter(string parameter)
+ {
+ if (string.IsNullOrEmpty(parameter) || !_parameterSegments.Contains(parameter))
{
- var moustache = "{" + parameter + "}";
- return new MqttTopicTemplate(Template.Replace(moustache, value));
+ throw new ArgumentException("topic template parameter must exist.");
}
-
- ///
- /// Reuse parameters as they are extracted using another topic template on this template
- /// when the parameter name matches. Useful
- /// for compatibility routing.
- ///
- ///
- ///
- ///
- public MqttTopicTemplate WithParameterValuesFrom(IEnumerable<(string parameter, int index, string value)> parameters)
+
+ return ReplaceInternal(parameter, MqttTopicFilterComparer.SingleLevelWildcard.ToString());
+ }
+
+ ///
+ /// Substitute a parameter with a given value, thus removing the parameter. If the parameter is not present,
+ /// the method trows. The value must not contain slashes or wildcards.
+ ///
+ ///
+ /// a template parameter
+ ///
+ ///
+ /// a string
+ ///
+ ///
+ /// when the parameter is not present
+ ///
+ /// the topic template (without the parameter)
+ public MqttTopicTemplate WithParameter(string parameter, string value)
+ {
+ if (value == null || string.IsNullOrEmpty(parameter) || !_parameterSegments.Contains(parameter) || value.Contains(MqttTopicFilterComparer.LevelSeparator) ||
+ value.Contains(MqttTopicFilterComparer.SingleLevelWildcard) || value.Contains(MqttTopicFilterComparer.MultiLevelWildcard))
{
- return parameters.Aggregate(this, (t, p) => t.TrySetParameter(p.parameter, p.value));
+ throw new ArgumentException("parameter must exist and value must not contain slashes or wildcard.");
}
-
- IEnumerable<(string parameter, int index, string value)> parseParameterValuesInternal(string topic)
+
+ return ReplaceInternal(parameter, value);
+ }
+
+ ///
+ /// Reuse parameters as they are extracted using another topic template on this template
+ /// when the parameter name matches. Useful
+ /// for compatibility routing.
+ ///
+ ///
+ ///
+ ///
+ public MqttTopicTemplate WithParameterValuesFrom(IEnumerable<(string parameter, int index, string value)> parameters)
+ {
+ return parameters.Aggregate(this, (t, p) => t.TrySetParameter(p.parameter, p.value));
+ }
+
+ IEnumerable<(string parameter, int index, string value)> parseParameterValuesInternal(string topic)
+ {
+ // because we have a match, we know the segment array is at least the template's length
+ var segments = topic.Split(MqttTopicFilterComparer.LevelSeparator);
+ for (var i = 0; i < _parameterSegments.Length; i++)
{
- // because we have a match, we know the segment array is at least the template's length
- var segments = topic.Split(MqttTopicFilterComparer.LevelSeparator);
- for (var i = 0; i < _parameterSegments.Length; i++)
+ var name = _parameterSegments[i];
+ if (name != null)
{
- var name = _parameterSegments[i];
- if (name != null)
- {
- yield return (name, i, segments[i]);
- }
+ yield return (name, i, segments[i]);
}
}
}
+
+ MqttTopicTemplate ReplaceInternal(string parameter, string value)
+ {
+ var moustache = "{" + parameter + "}";
+ return new MqttTopicTemplate(Template.Replace(moustache, value));
+ }
}
\ No newline at end of file
diff --git a/Source/MQTTnet.Extensions.TopicTemplate/TopicTemplateExtensions.cs b/Source/MQTTnet.Extensions.TopicTemplate/TopicTemplateExtensions.cs
index a4d49e99a..f46d2ef13 100644
--- a/Source/MQTTnet.Extensions.TopicTemplate/TopicTemplateExtensions.cs
+++ b/Source/MQTTnet.Extensions.TopicTemplate/TopicTemplateExtensions.cs
@@ -8,183 +8,182 @@
using MQTTnet.Packets;
using MQTTnet.Protocol;
-namespace MQTTnet.Extensions.TopicTemplate
+namespace MQTTnet.Extensions.TopicTemplate;
+
+public static class TopicTemplateExtensions
{
- public static class TopicTemplateExtensions
+ ///
+ /// Modify this message builder to respond to a given message. The
+ /// message's response topic and correlation data are included
+ /// in the message builder.
+ ///
+ ///
+ /// a message builder
+ ///
+ ///
+ /// a message with a response topic
+ ///
+ /// a message builder
+ ///
+ public static MqttApplicationMessageBuilder AsResponseTo(this MqttApplicationMessageBuilder builder, MqttApplicationMessage message)
{
- ///
- /// Modify this message builder to respond to a given message. The
- /// message's response topic and correlation data are included
- /// in the message builder.
- ///
- ///
- /// a message builder
- ///
- ///
- /// a message with a response topic
- ///
- /// a message builder
- ///
- public static MqttApplicationMessageBuilder AsResponseTo(this MqttApplicationMessageBuilder builder, MqttApplicationMessage message)
+ if (!string.IsNullOrEmpty(message.ResponseTopic))
{
- if (!string.IsNullOrEmpty(message.ResponseTopic))
- {
- throw new ArgumentException("message does not have a response topic");
- }
-
- return builder.WithTopic(message.ResponseTopic).WithCorrelationData(message.CorrelationData);
+ throw new ArgumentException("message does not have a response topic");
}
- ///
- /// Set the filter topic according to the template, with
- /// remaining template parameters substituted by single-level
- /// wildcard.
- ///
- ///
- /// a topic template
- ///
- ///
- /// whether to subscribe to the whole topic tree
- ///
- /// the modified topic filter
- public static MqttTopicFilterBuilder BuildFilter(this MqttTopicTemplate topicTemplate, bool subscribeTreeRoot = false)
- {
- return new MqttTopicFilterBuilder().WithTopicTemplate(topicTemplate, subscribeTreeRoot);
- }
+ return builder.WithTopic(message.ResponseTopic).WithCorrelationData(message.CorrelationData);
+ }
- ///
- /// Create a message builder from this template. The template must not have
- /// remaining parameters.
- ///
- ///
- /// a parameterless topic template
- ///
- /// a new message builder
- ///
- /// if the topic template has parameters
- ///
- public static MqttApplicationMessageBuilder BuildMessage(this MqttTopicTemplate topicTemplate)
- {
- return new MqttApplicationMessageBuilder().WithTopicTemplate(topicTemplate);
- }
-
- ///
- /// Return a message builder to respond to this message. The
- /// message's response topic and correlation data are included
- /// in the response message builder.
- ///
- ///
- /// a message with a response topic
- ///
- /// a message builder
- ///
- public static MqttApplicationMessageBuilder BuildResponse(this MqttApplicationMessage message)
- {
- return new MqttApplicationMessageBuilder().AsResponseTo(message);
- }
+ ///
+ /// Set the filter topic according to the template, with
+ /// remaining template parameters substituted by single-level
+ /// wildcard.
+ ///
+ ///
+ /// a topic template
+ ///
+ ///
+ /// whether to subscribe to the whole topic tree
+ ///
+ /// the modified topic filter
+ public static MqttTopicFilterBuilder BuildFilter(this MqttTopicTemplate topicTemplate, bool subscribeTreeRoot = false)
+ {
+ return new MqttTopicFilterBuilder().WithTopicTemplate(topicTemplate, subscribeTreeRoot);
+ }
- ///
- /// Return whether the message matches the given topic template.
- ///
- ///
- /// a message
- ///
- ///
- /// a topic template
- ///
- ///
- /// whether to include the topic subtree
- ///
- ///
- public static bool MatchesTopicTemplate(this MqttApplicationMessage message, MqttTopicTemplate topicTemplate, bool subtree = false)
- {
- return topicTemplate.MatchesTopic(message.Topic, subtree);
- }
+ ///
+ /// Create a message builder from this template. The template must not have
+ /// remaining parameters.
+ ///
+ ///
+ /// a parameterless topic template
+ ///
+ /// a new message builder
+ ///
+ /// if the topic template has parameters
+ ///
+ public static MqttApplicationMessageBuilder BuildMessage(this MqttTopicTemplate topicTemplate)
+ {
+ return new MqttApplicationMessageBuilder().WithTopicTemplate(topicTemplate);
+ }
- ///
- /// Set the filter topic according to the template, with
- /// template parameters substituted by a single-level
- /// wildcard.
- ///
- ///
- /// a filter builder
- ///
- ///
- /// a topic template
- ///
- ///
- /// whether to subscribe to the whole topic tree
- ///
- /// the modified topic filter
- public static MqttTopicFilterBuilder WithTopicTemplate(this MqttTopicFilterBuilder builder, MqttTopicTemplate topicTemplate, bool subscribeTreeRoot = false)
- {
- return builder.WithTopic(subscribeTreeRoot ? topicTemplate.TopicTreeRootFilter : topicTemplate.TopicFilter);
- }
+ ///
+ /// Return a message builder to respond to this message. The
+ /// message's response topic and correlation data are included
+ /// in the response message builder.
+ ///
+ ///
+ /// a message with a response topic
+ ///
+ /// a message builder
+ ///
+ public static MqttApplicationMessageBuilder BuildResponse(this MqttApplicationMessage message)
+ {
+ return new MqttApplicationMessageBuilder().AsResponseTo(message);
+ }
- ///
- /// Set the subscription to the template's topic filter.
- ///
- /// the builder
- public static MqttClientSubscribeOptionsBuilder WithTopicTemplate(
- this MqttClientSubscribeOptionsBuilder builder,
- MqttTopicTemplate topicTemplate,
- MqttQualityOfServiceLevel qualityOfServiceLevel = MqttQualityOfServiceLevel.AtMostOnce,
- bool noLocal = false,
- bool retainAsPublished = false,
- MqttRetainHandling retainHandling = MqttRetainHandling.SendAtSubscribe)
- {
- return builder.WithTopicFilter(
- new MqttTopicFilter
- {
- Topic = topicTemplate.TopicFilter,
- QualityOfServiceLevel = qualityOfServiceLevel,
- NoLocal = noLocal,
- RetainAsPublished = retainAsPublished,
- RetainHandling = retainHandling
- });
- }
-
- ///
- /// Set the publication topic according to the topic template. The template
- /// must not have remaining (unset) parameters or contain wildcards.
- ///
- ///
- /// a message builder
- ///
- ///
- /// a parameterless topic template
- ///
- /// the modified message builder
- ///
- /// if the topic template has parameters
- ///
- public static MqttApplicationMessageBuilder WithTopicTemplate(this MqttApplicationMessageBuilder builder, MqttTopicTemplate topicTemplate)
- {
- if (topicTemplate.Parameters.Any())
+ ///
+ /// Return whether the message matches the given topic template.
+ ///
+ ///
+ /// a message
+ ///
+ ///
+ /// a topic template
+ ///
+ ///
+ /// whether to include the topic subtree
+ ///
+ ///
+ public static bool MatchesTopicTemplate(this MqttApplicationMessage message, MqttTopicTemplate topicTemplate, bool subtree = false)
+ {
+ return topicTemplate.MatchesTopic(message.Topic, subtree);
+ }
+
+ ///
+ /// Set the filter topic according to the template, with
+ /// template parameters substituted by a single-level
+ /// wildcard.
+ ///
+ ///
+ /// a filter builder
+ ///
+ ///
+ /// a topic template
+ ///
+ ///
+ /// whether to subscribe to the whole topic tree
+ ///
+ /// the modified topic filter
+ public static MqttTopicFilterBuilder WithTopicTemplate(this MqttTopicFilterBuilder builder, MqttTopicTemplate topicTemplate, bool subscribeTreeRoot = false)
+ {
+ return builder.WithTopic(subscribeTreeRoot ? topicTemplate.TopicTreeRootFilter : topicTemplate.TopicFilter);
+ }
+
+ ///
+ /// Set the subscription to the template's topic filter.
+ ///
+ /// the builder
+ public static MqttClientSubscribeOptionsBuilder WithTopicTemplate(
+ this MqttClientSubscribeOptionsBuilder builder,
+ MqttTopicTemplate topicTemplate,
+ MqttQualityOfServiceLevel qualityOfServiceLevel = MqttQualityOfServiceLevel.AtMostOnce,
+ bool noLocal = false,
+ bool retainAsPublished = false,
+ MqttRetainHandling retainHandling = MqttRetainHandling.SendAtSubscribe)
+ {
+ return builder.WithTopicFilter(
+ new MqttTopicFilter
{
- throw new ArgumentException("topic templates must be parameter-less when sending " + topicTemplate.Template);
- }
+ Topic = topicTemplate.TopicFilter,
+ QualityOfServiceLevel = qualityOfServiceLevel,
+ NoLocal = noLocal,
+ RetainAsPublished = retainAsPublished,
+ RetainHandling = retainHandling
+ });
+ }
- MqttTopicValidator.ThrowIfInvalid(topicTemplate.Template);
- return builder.WithTopic(topicTemplate.Template);
+ ///
+ /// Set the publication topic according to the topic template. The template
+ /// must not have remaining (unset) parameters or contain wildcards.
+ ///
+ ///
+ /// a message builder
+ ///
+ ///
+ /// a parameterless topic template
+ ///
+ /// the modified message builder
+ ///
+ /// if the topic template has parameters
+ ///
+ public static MqttApplicationMessageBuilder WithTopicTemplate(this MqttApplicationMessageBuilder builder, MqttTopicTemplate topicTemplate)
+ {
+ if (topicTemplate.Parameters.Any())
+ {
+ throw new ArgumentException("topic templates must be parameter-less when sending " + topicTemplate.Template);
}
+
+ MqttTopicValidator.ThrowIfInvalid(topicTemplate.Template);
+ return builder.WithTopic(topicTemplate.Template);
}
}
\ No newline at end of file
diff --git a/Source/MQTTnet.TestApp/MQTTnet.TestApp.csproj b/Source/MQTTnet.TestApp/MQTTnet.TestApp.csproj
index f5b32a5a9..45da3ce1e 100644
--- a/Source/MQTTnet.TestApp/MQTTnet.TestApp.csproj
+++ b/Source/MQTTnet.TestApp/MQTTnet.TestApp.csproj
@@ -20,6 +20,7 @@
+
diff --git a/Source/MQTTnet.Tests/Extensions/MqttTopicTemplate_Tests.cs b/Source/MQTTnet.Tests/Extensions/MqttTopicTemplate_Tests.cs
index 3a95ebc81..e3c7ad201 100644
--- a/Source/MQTTnet.Tests/Extensions/MqttTopicTemplate_Tests.cs
+++ b/Source/MQTTnet.Tests/Extensions/MqttTopicTemplate_Tests.cs
@@ -61,21 +61,21 @@ public void RejectsReservedChars3()
var template = new MqttTopicTemplate("A/B/{foo}/D");
template.WithParameter("foo", "a/b");
}
-
+
[TestMethod]
public void AcceptsEmptyValue()
{
var template = new MqttTopicTemplate("A/B/{foo}/D");
template.WithParameter("foo", "");
}
-
+
[TestMethod]
[ExpectedException(typeof(MqttProtocolViolationException))]
public void RejectsEmptyTemplate()
{
var _ = new MqttTopicTemplate("");
}
-
+
[TestMethod]
[ExpectedException(typeof(ArgumentNullException))]
public void RejectsNullTemplate()
@@ -83,14 +83,13 @@ public void RejectsNullTemplate()
var _ = new MqttTopicTemplate(null);
}
-
[TestMethod]
public void IgnoresEmptyParameters()
{
var template = new MqttTopicTemplate("A/B/{}/D");
Assert.IsFalse(template.Parameters.Any());
}
-
+
[TestMethod]
public void AcceptsValidTopics()
{
@@ -127,6 +126,7 @@ public void SubscriptionSupport()
.WithAtLeastOnceQoS()
.WithNoLocal()
.Build();
+
Assert.AreEqual("A/v1/+/F", filter.Topic);
}
@@ -134,11 +134,12 @@ public void SubscriptionSupport()
public void SubscriptionSupport2()
{
var template = new MqttTopicTemplate("A/v1/{param}/F");
-
- var subscribeOptions = new MqttFactory().CreateSubscribeOptionsBuilder()
+
+ var subscribeOptions = new MqttClientFactory().CreateSubscribeOptionsBuilder()
.WithTopicTemplate(template)
.WithSubscriptionIdentifier(5)
.Build();
+
Assert.AreEqual("A/v1/+/F", subscribeOptions.TopicFilters[0].Topic);
}
@@ -170,7 +171,7 @@ public void SendAndSubscribeSupport()
public void SendAndSubscribeSupport2()
{
var template = new MqttTopicTemplate("App/v1/{sender}/message");
- Assert.ThrowsException(() =>
+ Assert.ThrowsException(() =>
template.BuildMessage());
}
@@ -184,7 +185,7 @@ public void CanonicalPrefixFilter()
// possible improvement: Assert.AreEqual("A/v1/+/F", canonicalFilter.TopicFilter);
Assert.AreEqual("A/v1/+", canonicalFilter.TopicFilter);
Assert.AreEqual("A/v1/+/#", canonicalFilter.TopicTreeRootFilter);
-
+
var template2b = new MqttTopicTemplate("A/v1/E/X");
canonicalFilter = MqttTopicTemplate.FindCanonicalPrefix(new[] { template1, template2, template3, template2b });
Assert.AreEqual("A/v1/+", canonicalFilter.Template);
@@ -197,7 +198,7 @@ public void CanonicalPrefixFilter()
Assert.AreEqual("A/+", canonicalFilter2.TopicFilter);
Assert.AreEqual("A/+/#", canonicalFilter2.TopicTreeRootFilter);
}
-
+
[TestMethod]
public void CanonicalPrefixFilterSimple1()
{
@@ -209,7 +210,7 @@ public void CanonicalPrefixFilterSimple1()
Assert.AreEqual("#", template.TopicFilter);
Assert.AreEqual("#", template.Template);
}
-
+
[TestMethod]
public void CanonicalPrefixFilterSimple2()
{
@@ -220,7 +221,7 @@ public void CanonicalPrefixFilterSimple2()
});
Assert.AreEqual("A/v1/+", template.TopicFilter);
}
-
+
[TestMethod]
public void CanonicalPrefixFilterSimple3()
{
@@ -232,7 +233,7 @@ public void CanonicalPrefixFilterSimple3()
Assert.AreEqual("A/v1/+/+", template.TopicFilter);
Assert.AreEqual("A/v1/{param}/+", template.Template);
}
-
+
[TestMethod]
public void CanonicalPrefixFilterSimple4()
{
@@ -244,8 +245,7 @@ public void CanonicalPrefixFilterSimple4()
Assert.AreEqual("A/v1/+/+", template.TopicFilter);
Assert.AreEqual("A/v1/{param}/+", template.Template);
}
-
-
+
[TestMethod]
public void CanonicalPrefixFilterSimple5()
{
@@ -257,7 +257,7 @@ public void CanonicalPrefixFilterSimple5()
Assert.AreEqual("A/+", template.TopicFilter);
Assert.AreEqual("A/+/#", template.TopicTreeRootFilter);
}
-
+
[TestMethod]
public void CanonicalPrefixFilterSimple6()
{