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() {