diff --git a/MQTTnet.sln.DotSettings b/MQTTnet.sln.DotSettings index 7eed6537..b039fe14 100644 --- a/MQTTnet.sln.DotSettings +++ b/MQTTnet.sln.DotSettings @@ -1,4 +1,67 @@  + <?xml version="1.0" encoding="utf-16"?><Profile name="MQTTnet"><CppReformatCode>True</CppReformatCode><ShaderLabReformatCode>True</ShaderLabReformatCode><XMLReformatCode>True</XMLReformatCode><HtmlReformatCode>True</HtmlReformatCode><VBReformatCode>True</VBReformatCode><CSReformatCode>True</CSReformatCode><CSCodeStyleAttributes ArrangeVarStyle="True" ArrangeTypeAccessModifier="True" ArrangeTypeMemberAccessModifier="True" SortModifiers="True" ArrangeArgumentsStyle="True" RemoveRedundantParentheses="True" AddMissingParentheses="True" ArrangeBraces="True" ArrangeAttributes="True" ArrangeCodeBodyStyle="True" ArrangeTrailingCommas="True" ArrangeObjectCreation="True" ArrangeDefaultValue="True" ArrangeNamespaces="True" /><CSArrangeQualifiers>True</CSArrangeQualifiers><CSFixBuiltinTypeReferences>True</CSFixBuiltinTypeReferences><CppCodeStyleCleanupDescriptor ArrangeBraces="True" ArrangeAuto="True" ArrangeFunctionDeclarations="True" ArrangeNestedNamespaces="True" ArrangeTypeAliases="True" ArrangeCVQualifiers="True" ArrangeSlashesInIncludeDirectives="True" ArrangeOverridingFunctions="True" SortIncludeDirectives="True" SortMemberInitializers="True" /><FormatAttributeQuoteDescriptor>True</FormatAttributeQuoteDescriptor><CSUpdateFileHeader>True</CSUpdateFileHeader><CppUpdateFileHeader>True</CppUpdateFileHeader><IDEA_SETTINGS>&lt;profile version="1.0"&gt; + &lt;option name="myName" value="MQTTnet" /&gt; + &lt;inspection_tool class="ConditionalExpressionWithIdenticalBranchesJS" enabled="true" level="WARNING" enabled_by_default="true" /&gt; + &lt;inspection_tool class="ES6ShorthandObjectProperty" enabled="true" level="WARNING" enabled_by_default="true" /&gt; + &lt;inspection_tool class="JSArrowFunctionBracesCanBeRemoved" enabled="true" level="WARNING" enabled_by_default="true" /&gt; + &lt;inspection_tool class="JSRemoveUnnecessaryParentheses" enabled="true" level="WARNING" enabled_by_default="true" /&gt; + &lt;inspection_tool class="TypeScriptExplicitMemberType" enabled="true" level="WARNING" enabled_by_default="true" /&gt; + &lt;inspection_tool class="UnterminatedStatementJS" enabled="true" level="WARNING" enabled_by_default="true" /&gt; + &lt;inspection_tool class="WrongPropertyKeyValueDelimiter" enabled="true" level="WARNING" enabled_by_default="true" /&gt; +&lt;/profile&gt;</IDEA_SETTINGS><RIDER_SETTINGS>&lt;profile&gt; + &lt;Language id="CSS"&gt; + &lt;Reformat&gt;true&lt;/Reformat&gt; + &lt;Rearrange&gt;true&lt;/Rearrange&gt; + &lt;/Language&gt; + &lt;Language id="EditorConfig"&gt; + &lt;Reformat&gt;true&lt;/Reformat&gt; + &lt;/Language&gt; + &lt;Language id="HTML"&gt; + &lt;Reformat&gt;true&lt;/Reformat&gt; + &lt;Rearrange&gt;true&lt;/Rearrange&gt; + &lt;OptimizeImports&gt;true&lt;/OptimizeImports&gt; + &lt;/Language&gt; + &lt;Language id="HTTP Request"&gt; + &lt;Reformat&gt;true&lt;/Reformat&gt; + &lt;/Language&gt; + &lt;Language id="Handlebars"&gt; + &lt;Reformat&gt;true&lt;/Reformat&gt; + &lt;/Language&gt; + &lt;Language id="Ini"&gt; + &lt;Reformat&gt;true&lt;/Reformat&gt; + &lt;/Language&gt; + &lt;Language id="JSON"&gt; + &lt;Reformat&gt;true&lt;/Reformat&gt; + &lt;/Language&gt; + &lt;Language id="Jade"&gt; + &lt;Reformat&gt;true&lt;/Reformat&gt; + &lt;/Language&gt; + &lt;Language id="JavaScript"&gt; + &lt;Reformat&gt;true&lt;/Reformat&gt; + &lt;Rearrange&gt;true&lt;/Rearrange&gt; + &lt;OptimizeImports&gt;true&lt;/OptimizeImports&gt; + &lt;/Language&gt; + &lt;Language id="Markdown"&gt; + &lt;Reformat&gt;true&lt;/Reformat&gt; + &lt;/Language&gt; + &lt;Language id="Properties"&gt; + &lt;Reformat&gt;true&lt;/Reformat&gt; + &lt;/Language&gt; + &lt;Language id="RELAX-NG"&gt; + &lt;Reformat&gt;true&lt;/Reformat&gt; + &lt;/Language&gt; + &lt;Language id="SQL"&gt; + &lt;Reformat&gt;true&lt;/Reformat&gt; + &lt;/Language&gt; + &lt;Language id="XML"&gt; + &lt;Reformat&gt;true&lt;/Reformat&gt; + &lt;Rearrange&gt;true&lt;/Rearrange&gt; + &lt;OptimizeImports&gt;true&lt;/OptimizeImports&gt; + &lt;/Language&gt; + &lt;Language id="yaml"&gt; + &lt;Reformat&gt;true&lt;/Reformat&gt; + &lt;/Language&gt; +&lt;/profile&gt;</RIDER_SETTINGS><CSOptimizeUsings><OptimizeUsings>True</OptimizeUsings></CSOptimizeUsings><CSShortenReferences>True</CSShortenReferences><VBOptimizeImports>True</VBOptimizeImports><VBShortenReferences>True</VBShortenReferences><Xaml.RemoveRedundantNamespaceAlias>True</Xaml.RemoveRedundantNamespaceAlias><AspOptimizeRegisterDirectives>True</AspOptimizeRegisterDirectives><CSReorderTypeMembers>True</CSReorderTypeMembers><RemoveCodeRedundancies>True</RemoveCodeRedundancies><CSUseAutoProperty>True</CSUseAutoProperty><CSMakeFieldReadonly>True</CSMakeFieldReadonly><CSMakeAutoPropertyGetOnly>True</CSMakeAutoPropertyGetOnly><CppAddTypenameTemplateKeywords>True</CppAddTypenameTemplateKeywords><CppJoinDeclarationAndAssignmentDescriptor>True</CppJoinDeclarationAndAssignmentDescriptor><CppMakeLocalVarConstDescriptor>True</CppMakeLocalVarConstDescriptor><CppMakeMethodConst>True</CppMakeMethodConst><CppMakeMethodStatic>True</CppMakeMethodStatic><CppRemoveCastDescriptor>True</CppRemoveCastDescriptor><CppRemoveElseKeyword>True</CppRemoveElseKeyword><CppRemoveElaboratedTypeSpecifierDescriptor>True</CppRemoveElaboratedTypeSpecifierDescriptor><CppRemoveRedundantLambdaParameterListDescriptor>True</CppRemoveRedundantLambdaParameterListDescriptor><CppRemoveRedundantMemberInitializerDescriptor>True</CppRemoveRedundantMemberInitializerDescriptor><CppRemoveRedundantParentheses>True</CppRemoveRedundantParentheses><CppShortenQualifiedName>True</CppShortenQualifiedName><CppDeleteRedundantSpecifier>True</CppDeleteRedundantSpecifier><CppRemoveStatement>True</CppRemoveStatement><CppRemoveTemplateArgumentsDescriptor>True</CppRemoveTemplateArgumentsDescriptor><CppDeleteRedundantTypenameTemplateKeywords>True</CppDeleteRedundantTypenameTemplateKeywords><CppRemoveUnreachableCode>True</CppRemoveUnreachableCode><CppRemoveUnusedIncludes>True</CppRemoveUnusedIncludes><CppRemoveUnusedLambdaCaptures>True</CppRemoveUnusedLambdaCaptures><CppCStyleToStaticCastDescriptor>True</CppCStyleToStaticCastDescriptor><CppReplaceExpressionWithBooleanConst>True</CppReplaceExpressionWithBooleanConst><CppMakeIfConstexpr>True</CppMakeIfConstexpr><CppMakePostfixOperatorPrefix>True</CppMakePostfixOperatorPrefix><CppChangeSmartPointerToMakeFunction>True</CppChangeSmartPointerToMakeFunction><CppReplaceThrowWithRethrowFix>True</CppReplaceThrowWithRethrowFix><CppTypeTraitAliasDescriptor>True</CppTypeTraitAliasDescriptor><CppReplaceExpressionWithNullptr>True</CppReplaceExpressionWithNullptr><CppReplaceTieWithStructuredBindingDescriptor>True</CppReplaceTieWithStructuredBindingDescriptor><CppUseAssociativeContainsDescriptor>True</CppUseAssociativeContainsDescriptor><CppUseEraseAlgorithmDescriptor>True</CppUseEraseAlgorithmDescriptor><RemoveCodeRedundanciesVB>True</RemoveCodeRedundanciesVB><Xaml.RedundantFreezeAttribute>True</Xaml.RedundantFreezeAttribute><Xaml.RemoveRedundantModifiersAttribute>True</Xaml.RemoveRedundantModifiersAttribute><Xaml.RemoveRedundantNameAttribute>True</Xaml.RemoveRedundantNameAttribute><Xaml.RemoveRedundantResource>True</Xaml.RemoveRedundantResource><Xaml.RemoveRedundantCollectionProperty>True</Xaml.RemoveRedundantCollectionProperty><Xaml.RemoveRedundantAttachedPropertySetter>True</Xaml.RemoveRedundantAttachedPropertySetter><Xaml.RemoveRedundantStyledValue>True</Xaml.RemoveRedundantStyledValue><Xaml.RemoveForbiddenResourceName>True</Xaml.RemoveForbiddenResourceName><Xaml.RemoveRedundantGridDefinitionsAttribute>True</Xaml.RemoveRedundantGridDefinitionsAttribute><Xaml.RemoveRedundantUpdateSourceTriggerAttribute>True</Xaml.RemoveRedundantUpdateSourceTriggerAttribute><Xaml.RemoveRedundantBindingModeAttribute>True</Xaml.RemoveRedundantBindingModeAttribute><Xaml.RemoveRedundantGridSpanAttribut>True</Xaml.RemoveRedundantGridSpanAttribut><CSharpFormatDocComments>True</CSharpFormatDocComments><VBFormatDocComments>True</VBFormatDocComments></Profile> Required Required Required @@ -8,23 +71,28 @@ False False False + False False NEVER + False False NEVER False False + True False False False True True CHOP_IF_LONG - CHOP_IF_LONG + CHOP_ALWAYS CHOP_IF_LONG CHOP_IF_LONG 180 + CHOP_ALWAYS CHOP_IF_LONG + CHOP_ALWAYS True OnDifferentLines OnDifferentLines diff --git a/Samples/ManagedClient/Managed_Client_Simple_Samples.cs b/Samples/ManagedClient/Managed_Client_Simple_Samples.cs index 723a33a6..fb7cbdf9 100644 --- a/Samples/ManagedClient/Managed_Client_Simple_Samples.cs +++ b/Samples/ManagedClient/Managed_Client_Simple_Samples.cs @@ -44,9 +44,9 @@ public sealed class Managed_Client_Simple_Samples Console.WriteLine("The managed MQTT client is connected."); // Wait until the queue is fully processed. - SpinWait.SpinUntil(() => managedMqttClient.PendingApplicationMessagesCount == 0, 10000); + SpinWait.SpinUntil(() => managedMqttClient.EnqueuedApplicationMessagesCount == 0, 10000); - Console.WriteLine($"Pending messages = {managedMqttClient.PendingApplicationMessagesCount}"); + Console.WriteLine($"Pending messages = {managedMqttClient.EnqueuedApplicationMessagesCount}"); } } } \ No newline at end of file diff --git a/Source/MQTTnet.Extensions.ManagedClient/IManagedMqttClient.cs b/Source/MQTTnet.Extensions.ManagedClient/IManagedMqttClient.cs index 20f51d83..e6c4758d 100644 --- a/Source/MQTTnet.Extensions.ManagedClient/IManagedMqttClient.cs +++ b/Source/MQTTnet.Extensions.ManagedClient/IManagedMqttClient.cs @@ -1,52 +1,56 @@ +// 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.Collections.Generic; using System.Threading; using System.Threading.Tasks; using MQTTnet.Client; -using MQTTnet.Packets; namespace MQTTnet.Extensions.ManagedClient { public interface IManagedMqttClient : IDisposable { + event Func ApplicationMessageDroppedAsync; + + event Func ApplicationMessageEnqueueingAsync; + event Func ApplicationMessageProcessedAsync; - + event Func ApplicationMessageReceivedAsync; - - event Func ApplicationMessageSkippedAsync; - + event Func ConnectedAsync; - + event Func ConnectingFailedAsync; - - event Func ConnectionStateChangedAsync; - + event Func DisconnectedAsync; - - event Func SynchronizingSubscriptionsFailedAsync; - + + event Func SubscribeProcessedAsync; + + event Func UnsubscribeProcessedAsync; + + int EnqueuedApplicationMessagesCount { get; } + IMqttClient InternalClient { get; } - + bool IsConnected { get; } - + bool IsStarted { get; } - + ManagedMqttClientOptions Options { get; } - - int PendingApplicationMessagesCount { get; } - + Task EnqueueAsync(MqttApplicationMessage applicationMessage); - + Task EnqueueAsync(ManagedMqttApplicationMessage applicationMessage); - + Task PingAsync(CancellationToken cancellationToken = default); - + Task StartAsync(ManagedMqttClientOptions options); - - Task StopAsync(); - - Task SubscribeAsync(ICollection topicFilters); - - Task UnsubscribeAsync(ICollection topics); + + Task StopAsync(MqttClientDisconnectOptions options, CancellationToken cancellationToken); + + Task SubscribeAsync(MqttClientSubscribeOptions options); + + Task UnsubscribeAsync(MqttClientUnsubscribeOptions options); } } \ No newline at end of file diff --git a/Source/MQTTnet.Extensions.ManagedClient/MQTTnet.Extensions.ManagedClient.csproj b/Source/MQTTnet.Extensions.ManagedClient/MQTTnet.Extensions.ManagedClient.csproj index 2ef5c783..c6894546 100644 --- a/Source/MQTTnet.Extensions.ManagedClient/MQTTnet.Extensions.ManagedClient.csproj +++ b/Source/MQTTnet.Extensions.ManagedClient/MQTTnet.Extensions.ManagedClient.csproj @@ -4,7 +4,8 @@ netstandard1.3;netstandard2.0;netstandard2.1;netcoreapp3.1;net5.0;net6.0 $(TargetFrameworks);net452;net461 $(TargetFrameworks);uap10.0 - + + 7.3 MQTTnet.Extensions.ManagedClient MQTTnet.Extensions.ManagedClient True diff --git a/Source/MQTTnet.Extensions.ManagedClient/MQTTnet.Extensions.ManagedClient.csproj.DotSettings b/Source/MQTTnet.Extensions.ManagedClient/MQTTnet.Extensions.ManagedClient.csproj.DotSettings new file mode 100644 index 00000000..99b27e7c --- /dev/null +++ b/Source/MQTTnet.Extensions.ManagedClient/MQTTnet.Extensions.ManagedClient.csproj.DotSettings @@ -0,0 +1,4 @@ + + True + True + True \ No newline at end of file diff --git a/Source/MQTTnet.Extensions.ManagedClient/ManagedMqttApplicationMessage.cs b/Source/MQTTnet.Extensions.ManagedClient/ManagedMqttApplicationMessage.cs index 7ef38ebd..f80bd788 100644 --- a/Source/MQTTnet.Extensions.ManagedClient/ManagedMqttApplicationMessage.cs +++ b/Source/MQTTnet.Extensions.ManagedClient/ManagedMqttApplicationMessage.cs @@ -6,10 +6,16 @@ using System; namespace MQTTnet.Extensions.ManagedClient { - public class ManagedMqttApplicationMessage + public sealed class ManagedMqttApplicationMessage { - public Guid Id { get; set; } = Guid.NewGuid(); + public ManagedMqttApplicationMessage(string id, MqttApplicationMessage applicationMessage) + { + Id = id ?? throw new ArgumentNullException(nameof(id)); + ApplicationMessage = applicationMessage ?? throw new ArgumentNullException(nameof(applicationMessage)); + } + + public string Id { get; } - public MqttApplicationMessage ApplicationMessage { get; set; } + public MqttApplicationMessage ApplicationMessage { get; } } } diff --git a/Source/MQTTnet.Extensions.ManagedClient/ManagedMqttApplicationMessageBuilder.cs b/Source/MQTTnet.Extensions.ManagedClient/ManagedMqttApplicationMessageBuilder.cs index 96d0a42b..4f919cdf 100644 --- a/Source/MQTTnet.Extensions.ManagedClient/ManagedMqttApplicationMessageBuilder.cs +++ b/Source/MQTTnet.Extensions.ManagedClient/ManagedMqttApplicationMessageBuilder.cs @@ -6,15 +6,19 @@ using System; namespace MQTTnet.Extensions.ManagedClient { - public class ManagedMqttApplicationMessageBuilder + public sealed class ManagedMqttApplicationMessageBuilder { - private Guid _id = Guid.NewGuid(); - private MqttApplicationMessage _applicationMessage; + MqttApplicationMessage _applicationMessage; + string _id = Guid.NewGuid().ToString(); - public ManagedMqttApplicationMessageBuilder WithId(Guid id) + public ManagedMqttApplicationMessage Build() { - _id = id; - return this; + if (_applicationMessage == null) + { + throw new InvalidOperationException("The ApplicationMessage cannot be null."); + } + + return new ManagedMqttApplicationMessage(_id, _applicationMessage); } public ManagedMqttApplicationMessageBuilder WithApplicationMessage(MqttApplicationMessage applicationMessage) @@ -25,7 +29,10 @@ namespace MQTTnet.Extensions.ManagedClient public ManagedMqttApplicationMessageBuilder WithApplicationMessage(Action builder) { - if (builder == null) throw new ArgumentNullException(nameof(builder)); + if (builder == null) + { + throw new ArgumentNullException(nameof(builder)); + } var internalBuilder = new MqttApplicationMessageBuilder(); builder(internalBuilder); @@ -34,18 +41,16 @@ namespace MQTTnet.Extensions.ManagedClient return this; } - public ManagedMqttApplicationMessage Build() + public ManagedMqttApplicationMessageBuilder WithId(Guid id) { - if (_applicationMessage == null) - { - throw new InvalidOperationException("The ApplicationMessage cannot be null."); - } + _id = id.ToString(); + return this; + } - return new ManagedMqttApplicationMessage - { - Id = _id, - ApplicationMessage = _applicationMessage - }; + public ManagedMqttApplicationMessageBuilder WithId(string id) + { + _id = id ?? throw new ArgumentNullException(nameof(id)); + return this; } } -} +} \ No newline at end of file diff --git a/Source/MQTTnet.Extensions.ManagedClient/ManagedMqttClient.cs b/Source/MQTTnet.Extensions.ManagedClient/ManagedMqttClient.cs index b4fa02c2..6fc49fc7 100644 --- a/Source/MQTTnet.Extensions.ManagedClient/ManagedMqttClient.cs +++ b/Source/MQTTnet.Extensions.ManagedClient/ManagedMqttClient.cs @@ -17,10 +17,10 @@ using MQTTnet.Server; namespace MQTTnet.Extensions.ManagedClient { - public sealed class ManagedMqttClient : Disposable, IManagedMqttClient + public sealed class ManagedMqttClient : Disposable { readonly AsyncEvent _applicationMessageProcessedEvent = new AsyncEvent(); - readonly AsyncEvent _applicationMessageSkippedEvent = new AsyncEvent(); + readonly AsyncEvent _applicationMessageSkippedEvent = new AsyncEvent(); readonly AsyncEvent _connectingFailedEvent = new AsyncEvent(); readonly AsyncEvent _connectionStateChangedEvent = new AsyncEvent(); readonly AsyncEvent _synchronizingSubscriptionsFailedEvent = new AsyncEvent(); @@ -75,7 +75,7 @@ namespace MQTTnet.Extensions.ManagedClient _logger = logger.WithSource(nameof(ManagedMqttClient)); } - public event Func ApplicationMessageSkippedAsync + public event Func ApplicationMessageSkippedAsync { add => _applicationMessageSkippedEvent.AddHandler(value); remove => _applicationMessageSkippedEvent.RemoveHandler(value); @@ -117,12 +117,18 @@ namespace MQTTnet.Extensions.ManagedClient remove => InternalClient.DisconnectedAsync -= value; } - public event Func SynchronizingSubscriptionsFailedAsync + public event Func SubscribeProcessedAsync; + + public event Func UnsubscribeProcessedAsync; + + public event Func SubscribedAsync { add => _synchronizingSubscriptionsFailedEvent.AddHandler(value); remove => _synchronizingSubscriptionsFailedEvent.RemoveHandler(value); } + public event Func ApplicationMessageEnqueued; + public IMqttClient InternalClient { get; } public bool IsConnected => InternalClient.IsConnected; @@ -163,7 +169,7 @@ namespace MQTTnet.Extensions.ManagedClient MqttTopicValidator.ThrowIfInvalid(applicationMessage.ApplicationMessage.Topic); ManagedMqttApplicationMessage removedMessage = null; - ApplicationMessageSkippedEventArgs applicationMessageSkippedEventArgs = null; + ApplicationMessageDroppedEventArgs applicationMessageDroppedEventArgs = null; try { @@ -174,7 +180,7 @@ namespace MQTTnet.Extensions.ManagedClient if (Options.PendingMessagesOverflowStrategy == MqttPendingMessagesOverflowStrategy.DropNewMessage) { _logger.Verbose("Skipping publish of new application message because internal queue is full."); - applicationMessageSkippedEventArgs = new ApplicationMessageSkippedEventArgs(applicationMessage); + applicationMessageDroppedEventArgs = new ApplicationMessageDroppedEventArgs(applicationMessage); return; } @@ -182,7 +188,7 @@ namespace MQTTnet.Extensions.ManagedClient { removedMessage = _messageQueue.RemoveFirst(); _logger.Verbose("Removed oldest application message from internal queue because it is full."); - applicationMessageSkippedEventArgs = new ApplicationMessageSkippedEventArgs(removedMessage); + applicationMessageDroppedEventArgs = new ApplicationMessageDroppedEventArgs(removedMessage); } } @@ -201,9 +207,9 @@ namespace MQTTnet.Extensions.ManagedClient } finally { - if (applicationMessageSkippedEventArgs != null && _applicationMessageSkippedEvent.HasHandlers) + if (applicationMessageDroppedEventArgs != null && _applicationMessageSkippedEvent.HasHandlers) { - await _applicationMessageSkippedEvent.InvokeAsync(applicationMessageSkippedEventArgs).ConfigureAwait(false); + await _applicationMessageSkippedEvent.InvokeAsync(applicationMessageDroppedEventArgs).ConfigureAwait(false); } } } diff --git a/Source/MQTTnet.Extensions.ManagedClient/ManagedMqttClientExtensions.cs b/Source/MQTTnet.Extensions.ManagedClient/ManagedMqttClientExtensions.cs index 54dd39e7..1135471b 100644 --- a/Source/MQTTnet.Extensions.ManagedClient/ManagedMqttClientExtensions.cs +++ b/Source/MQTTnet.Extensions.ManagedClient/ManagedMqttClientExtensions.cs @@ -3,8 +3,9 @@ // See the LICENSE file in the project root for more information. using System; -using System.Collections.Generic; +using System.Threading; using System.Threading.Tasks; +using MQTTnet.Client; using MQTTnet.Packets; using MQTTnet.Protocol; @@ -38,6 +39,32 @@ namespace MQTTnet.Extensions.ManagedClient return managedMqttClient.EnqueueAsync(applicationMessage); } + public static Task StopAsync(this IManagedMqttClient managedMqttClient, CancellationToken cancellationToken = default) + { + if (managedMqttClient == null) + { + throw new ArgumentNullException(nameof(managedMqttClient)); + } + + return managedMqttClient.StopAsync(new MqttClientDisconnectOptions(), cancellationToken); + } + + public static Task SubscribeAsync(this IManagedMqttClient managedMqttClient, MqttTopicFilter topicFilter) + { + if (managedMqttClient == null) + { + throw new ArgumentNullException(nameof(managedMqttClient)); + } + + if (topicFilter == null) + { + throw new ArgumentNullException(nameof(topicFilter)); + } + + var subscribeOptions = new MqttClientSubscribeOptionsBuilder().WithTopicFilter(topicFilter).Build(); + return managedMqttClient.SubscribeAsync(subscribeOptions); + } + public static Task SubscribeAsync( this IManagedMqttClient managedMqttClient, string topic, @@ -53,11 +80,8 @@ namespace MQTTnet.Extensions.ManagedClient throw new ArgumentNullException(nameof(topic)); } - return managedMqttClient.SubscribeAsync( - new List - { - new MqttTopicFilterBuilder().WithTopic(topic).WithQualityOfServiceLevel(qualityOfServiceLevel).Build() - }); + var subscribeOptions = new MqttClientSubscribeOptionsBuilder().WithTopicFilter(topic, qualityOfServiceLevel).Build(); + return managedMqttClient.SubscribeAsync(subscribeOptions); } public static Task UnsubscribeAsync(this IManagedMqttClient managedMqttClient, string topic) @@ -72,7 +96,8 @@ namespace MQTTnet.Extensions.ManagedClient throw new ArgumentNullException(nameof(topic)); } - return managedMqttClient.UnsubscribeAsync(new List { topic }); + var unsubscribeOptions = new MqttClientUnsubscribeOptionsBuilder().WithTopicFilter(topic).Build(); + return managedMqttClient.UnsubscribeAsync(unsubscribeOptions); } } } \ No newline at end of file diff --git a/Source/MQTTnet.Extensions.ManagedClient/MqttFactoryExtensions.cs b/Source/MQTTnet.Extensions.ManagedClient/MqttFactoryExtensions.cs index 3349ca5d..884742f9 100644 --- a/Source/MQTTnet.Extensions.ManagedClient/MqttFactoryExtensions.cs +++ b/Source/MQTTnet.Extensions.ManagedClient/MqttFactoryExtensions.cs @@ -10,24 +10,54 @@ namespace MQTTnet.Extensions.ManagedClient { public static class MqttFactoryExtensions { - public static IManagedMqttClient CreateManagedMqttClient(this MqttFactory factory, IMqttClient mqttClient = null) + public static IManagedMqttClient CreateManagedMqttClient(this MqttFactory factory, IMqttClient mqttClient, IMqttNetLogger logger) { - if (factory == null) throw new ArgumentNullException(nameof(factory)); + if (factory == null) + { + throw new ArgumentNullException(nameof(factory)); + } if (mqttClient == null) { - return new ManagedMqttClient(factory.CreateMqttClient(), factory.DefaultLogger); + return new NewManagedMqttClient(factory.CreateMqttClient(), logger); } - - return new ManagedMqttClient(mqttClient, factory.DefaultLogger); + + return new NewManagedMqttClient(mqttClient, logger); } - + + public static IManagedMqttClient CreateManagedMqttClient(this MqttFactory factory, IMqttClient mqttClient = null) + { + if (factory == null) + { + throw new ArgumentNullException(nameof(factory)); + } + + if (mqttClient == null) + { + return new NewManagedMqttClient(factory.CreateMqttClient(), factory.DefaultLogger); + } + + return new NewManagedMqttClient(mqttClient, factory.DefaultLogger); + } + public static IManagedMqttClient CreateManagedMqttClient(this MqttFactory factory, IMqttNetLogger logger) { - if (factory == null) throw new ArgumentNullException(nameof(factory)); - if (logger == null) throw new ArgumentNullException(nameof(logger)); + if (factory == null) + { + throw new ArgumentNullException(nameof(factory)); + } - return new ManagedMqttClient(factory.CreateMqttClient(logger), logger); + if (logger == null) + { + throw new ArgumentNullException(nameof(logger)); + } + + return new NewManagedMqttClient(factory.CreateMqttClient(logger), logger); + } + + public static ManagedMqttClientOptionsBuilder CreateManagedMqttClientOptionsBuilder(this MqttFactory factory) + { + return new ManagedMqttClientOptionsBuilder(); } } } \ No newline at end of file diff --git a/Source/MQTTnet.Extensions.ManagedClient/New/ApplicationMessagesManager.cs b/Source/MQTTnet.Extensions.ManagedClient/New/ApplicationMessagesManager.cs new file mode 100644 index 00000000..5d33b9ee --- /dev/null +++ b/Source/MQTTnet.Extensions.ManagedClient/New/ApplicationMessagesManager.cs @@ -0,0 +1,95 @@ +// 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.Collections.Generic; +using System.Threading.Tasks; +using MQTTnet.Client; +using MQTTnet.Diagnostics; +using MQTTnet.Internal; + +namespace MQTTnet.Extensions.ManagedClient +{ + public sealed class ApplicationMessagesManager + { + readonly MqttNetSourceLogger _logger; + + readonly Queue _messageQueue = new Queue(); + readonly IMqttClient _mqttClient; + + public ApplicationMessagesManager(IMqttClient mqttClient, IMqttNetLogger logger) + { + _mqttClient = mqttClient ?? throw new ArgumentNullException(nameof(mqttClient)); + + if (logger == null) + { + throw new ArgumentNullException(nameof(logger)); + } + + _logger = logger.WithSource(); + } + + public AsyncEvent ApplicationMessageDroppedEvent { get; } = new AsyncEvent(); + + public AsyncEvent ApplicationMessageEnqueueingEvent { get; } = new AsyncEvent(); + + public int EnqueuedApplicationMessagesCount + { + get + { + lock (_messageQueue) + { + return _messageQueue.Count; + } + } + } + + public void Dequeue() + { + lock (_messageQueue) + { + _messageQueue.Dequeue(); + } + } + + public async Task Enqueue(ManagedMqttApplicationMessage applicationMessage) + { + if (applicationMessage == null) + { + throw new ArgumentNullException(nameof(applicationMessage)); + } + + // Fire the event first so that the handler can make sure that this message is + // properly backed up in a suitable storage. + if (ApplicationMessageEnqueueingEvent.HasHandlers) + { + var eventArgs = new ApplicationMessageEnqueueingEventArgs(applicationMessage, 0); + await ApplicationMessageEnqueueingEvent.InvokeAsync(eventArgs).ConfigureAwait(false); + } + + lock (_messageQueue) + { + + // TODO: Drop if too much! + + _messageQueue.Enqueue(applicationMessage); + } + } + + public bool TryPeek(out ManagedMqttApplicationMessage applicationMessage) + { + lock (_messageQueue) + { + if (_messageQueue.Count == 0) + { + applicationMessage = null; + return false; + } + + applicationMessage = _messageQueue.Peek(); + return true; + } + } + } +} \ No newline at end of file diff --git a/Source/MQTTnet.Extensions.ManagedClient/ApplicationMessageSkippedEventArgs.cs b/Source/MQTTnet.Extensions.ManagedClient/New/Events/ApplicationMessageDroppedEventArgs.cs similarity index 80% rename from Source/MQTTnet.Extensions.ManagedClient/ApplicationMessageSkippedEventArgs.cs rename to Source/MQTTnet.Extensions.ManagedClient/New/Events/ApplicationMessageDroppedEventArgs.cs index 0afd96a4..13d925ff 100644 --- a/Source/MQTTnet.Extensions.ManagedClient/ApplicationMessageSkippedEventArgs.cs +++ b/Source/MQTTnet.Extensions.ManagedClient/New/Events/ApplicationMessageDroppedEventArgs.cs @@ -6,13 +6,13 @@ using System; namespace MQTTnet.Extensions.ManagedClient { - public sealed class ApplicationMessageSkippedEventArgs : EventArgs + public sealed class ApplicationMessageDroppedEventArgs : EventArgs { - public ApplicationMessageSkippedEventArgs(ManagedMqttApplicationMessage applicationMessage) + public ApplicationMessageDroppedEventArgs(ManagedMqttApplicationMessage applicationMessage) { ApplicationMessage = applicationMessage ?? throw new ArgumentNullException(nameof(applicationMessage)); } public ManagedMqttApplicationMessage ApplicationMessage { get; } } -} +} \ No newline at end of file diff --git a/Source/MQTTnet.Extensions.ManagedClient/New/Events/ApplicationMessageEnqueueingEventArgs.cs b/Source/MQTTnet.Extensions.ManagedClient/New/Events/ApplicationMessageEnqueueingEventArgs.cs new file mode 100644 index 00000000..67db5582 --- /dev/null +++ b/Source/MQTTnet.Extensions.ManagedClient/New/Events/ApplicationMessageEnqueueingEventArgs.cs @@ -0,0 +1,21 @@ +// 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; + +namespace MQTTnet.Extensions.ManagedClient +{ + public sealed class ApplicationMessageEnqueueingEventArgs : EventArgs + { + public ApplicationMessageEnqueueingEventArgs(ManagedMqttApplicationMessage applicationMessage, ushort packetIdentifier) + { + ApplicationMessage = applicationMessage ?? throw new ArgumentNullException(nameof(applicationMessage)); + PacketIdentifier = packetIdentifier; + } + + public ManagedMqttApplicationMessage ApplicationMessage { get; } + + public ushort PacketIdentifier { get; } + } +} \ No newline at end of file diff --git a/Source/MQTTnet.Extensions.ManagedClient/ApplicationMessageProcessedEventArgs.cs b/Source/MQTTnet.Extensions.ManagedClient/New/Events/ApplicationMessageProcessedEventArgs.cs similarity index 85% rename from Source/MQTTnet.Extensions.ManagedClient/ApplicationMessageProcessedEventArgs.cs rename to Source/MQTTnet.Extensions.ManagedClient/New/Events/ApplicationMessageProcessedEventArgs.cs index 2efaa493..4b402056 100644 --- a/Source/MQTTnet.Extensions.ManagedClient/ApplicationMessageProcessedEventArgs.cs +++ b/Source/MQTTnet.Extensions.ManagedClient/New/Events/ApplicationMessageProcessedEventArgs.cs @@ -15,10 +15,12 @@ namespace MQTTnet.Extensions.ManagedClient } public ManagedMqttApplicationMessage ApplicationMessage { get; } - + /// - /// Then this is _null_ the message was processed successfully without any error. + /// Gets or sets the exception which was thrown while sending the message. /// public Exception Exception { get; } + + public bool HasFailed => Exception != null; } -} +} \ No newline at end of file diff --git a/Source/MQTTnet.Extensions.ManagedClient/ConnectingFailedEventArgs.cs b/Source/MQTTnet.Extensions.ManagedClient/New/Events/ConnectingFailedEventArgs.cs similarity index 62% rename from Source/MQTTnet.Extensions.ManagedClient/ConnectingFailedEventArgs.cs rename to Source/MQTTnet.Extensions.ManagedClient/New/Events/ConnectingFailedEventArgs.cs index 5fd7bdd7..241d1f68 100644 --- a/Source/MQTTnet.Extensions.ManagedClient/ConnectingFailedEventArgs.cs +++ b/Source/MQTTnet.Extensions.ManagedClient/New/Events/ConnectingFailedEventArgs.cs @@ -1,3 +1,7 @@ +// 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 MQTTnet.Client; @@ -12,7 +16,7 @@ namespace MQTTnet.Extensions.ManagedClient } /// - /// This is null when the connection was failing and the server was not reachable. + /// This is null when the server was not reachable and thus no MQTT response was transmitted. /// public MqttClientConnectResult ConnectResult { get; } diff --git a/Source/MQTTnet.Extensions.ManagedClient/ManagedProcessFailedEventArgs.cs b/Source/MQTTnet.Extensions.ManagedClient/New/Events/ManagedProcessFailedEventArgs.cs similarity index 100% rename from Source/MQTTnet.Extensions.ManagedClient/ManagedProcessFailedEventArgs.cs rename to Source/MQTTnet.Extensions.ManagedClient/New/Events/ManagedProcessFailedEventArgs.cs diff --git a/Source/MQTTnet.Extensions.ManagedClient/New/Events/SubscribeProcessedEventArgs.cs b/Source/MQTTnet.Extensions.ManagedClient/New/Events/SubscribeProcessedEventArgs.cs new file mode 100644 index 00000000..23e4c443 --- /dev/null +++ b/Source/MQTTnet.Extensions.ManagedClient/New/Events/SubscribeProcessedEventArgs.cs @@ -0,0 +1,27 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System; +using System.Collections.Generic; +using MQTTnet.Client; +using MQTTnet.Packets; + +namespace MQTTnet.Extensions.ManagedClient +{ + public sealed class SubscribeProcessedEventArgs : EventArgs + { + public SubscribeProcessedEventArgs(MqttClientSubscribeOptions options, MqttClientSubscribeResultCode result, IReadOnlyCollection userProperties) + { + Options = options ?? throw new ArgumentNullException(nameof(options)); + Result = result; + UserProperties = userProperties; + } + + public MqttClientSubscribeOptions Options { get; } + + public MqttClientSubscribeResultCode Result { get; } + + public IReadOnlyCollection UserProperties { get; } + } +} \ No newline at end of file diff --git a/Source/MQTTnet.Extensions.ManagedClient/New/Events/UnsubscribeProcessedEventArgs.cs b/Source/MQTTnet.Extensions.ManagedClient/New/Events/UnsubscribeProcessedEventArgs.cs new file mode 100644 index 00000000..9a435005 --- /dev/null +++ b/Source/MQTTnet.Extensions.ManagedClient/New/Events/UnsubscribeProcessedEventArgs.cs @@ -0,0 +1,27 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System; +using System.Collections.Generic; +using MQTTnet.Client; +using MQTTnet.Packets; + +namespace MQTTnet.Extensions.ManagedClient +{ + public sealed class UnsubscribeProcessedEventArgs : EventArgs + { + public UnsubscribeProcessedEventArgs(MqttClientUnsubscribeOptions options, MqttClientUnsubscribeResultCode result, IReadOnlyCollection userProperties) + { + Options = options ?? throw new ArgumentNullException(nameof(options)); + Result = result; + UserProperties = userProperties; + } + + public MqttClientUnsubscribeOptions Options { get; } + + public MqttClientUnsubscribeResultCode Result { get; } + + public IReadOnlyCollection UserProperties { get; } + } +} \ No newline at end of file diff --git a/Source/MQTTnet.Extensions.ManagedClient/New/MaintainConnectionResult.cs b/Source/MQTTnet.Extensions.ManagedClient/New/MaintainConnectionResult.cs new file mode 100644 index 00000000..8f39dad2 --- /dev/null +++ b/Source/MQTTnet.Extensions.ManagedClient/New/MaintainConnectionResult.cs @@ -0,0 +1,15 @@ +// 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. + +namespace MQTTnet.Extensions.ManagedClient +{ + public enum MaintainConnectionResult + { + NoChange, + + ConnectedEstablished, + + NotConnected + } +} \ No newline at end of file diff --git a/Source/MQTTnet.Extensions.ManagedClient/New/ManagedMqttSubscription.cs b/Source/MQTTnet.Extensions.ManagedClient/New/ManagedMqttSubscription.cs new file mode 100644 index 00000000..5b71f716 --- /dev/null +++ b/Source/MQTTnet.Extensions.ManagedClient/New/ManagedMqttSubscription.cs @@ -0,0 +1,35 @@ +// 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.Linq; +using MQTTnet.Client; + +namespace MQTTnet.Extensions.ManagedClient +{ + public sealed class ManagedMqttSubscription + { + public ManagedMqttSubscription(MqttClientSubscribeOptions subscribeOptions) + { + SubscribeOptions = subscribeOptions ?? throw new ArgumentNullException(nameof(subscribeOptions)); + + Topic = subscribeOptions.TopicFilters.First().Topic; + } + + public bool IsObsolete => UnsubscribeOptions != null; + + public bool IsPending { get; set; } = true; + + public MqttClientSubscribeOptions SubscribeOptions { get; } + + public string Topic { get; } + + public MqttClientUnsubscribeOptions UnsubscribeOptions { get; private set; } + + public void MarkAsUnsubscribed(MqttClientUnsubscribeOptions options) + { + UnsubscribeOptions = options ?? throw new ArgumentNullException(nameof(options)); + } + } +} \ No newline at end of file diff --git a/Source/MQTTnet.Extensions.ManagedClient/New/ManagedMqttSubscriptionsManager.cs b/Source/MQTTnet.Extensions.ManagedClient/New/ManagedMqttSubscriptionsManager.cs new file mode 100644 index 00000000..e54b0c12 --- /dev/null +++ b/Source/MQTTnet.Extensions.ManagedClient/New/ManagedMqttSubscriptionsManager.cs @@ -0,0 +1,155 @@ +// 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.Collections.Generic; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using MQTTnet.Client; +using MQTTnet.Diagnostics; +using MQTTnet.Internal; +using MQTTnet.Protocol; + +namespace MQTTnet.Extensions.ManagedClient +{ + public sealed class ManagedMqttSubscriptionsManager + { + readonly MqttNetSourceLogger _logger; + readonly IMqttClient _mqttClient; + + // Do not use a dictionary in order to preserve subscription ordering (retained messages). + readonly List _subscriptions = new List(); + + public ManagedMqttSubscriptionsManager(IMqttClient mqttClient, IMqttNetLogger logger) + { + _mqttClient = mqttClient ?? throw new ArgumentNullException(nameof(mqttClient)); + + if (logger == null) + { + throw new ArgumentNullException(nameof(logger)); + } + + _logger = logger.WithSource(); + } + + public AsyncEvent SubscribeProcessedEvent { get; } = new AsyncEvent(); + + public AsyncEvent UnsubscribeProcessedEvent { get; } = new AsyncEvent(); + + public void ResubscribeAll() + { + lock (_subscriptions) + { + foreach (var subscription in _subscriptions) + { + subscription.IsPending = true; + } + } + } + + public void Subscribe(MqttClientSubscribeOptions options) + { + if (options == null) + { + throw new ArgumentNullException(nameof(options)); + } + + if (options.TopicFilters.Count > 1) + { + throw new NotSupportedException("The managed client only supports subscribing to one topic at the same time."); + } + + var topic = options.TopicFilters.First().Topic; + + lock (_subscriptions) + { + MqttTopicValidator.ThrowIfInvalidSubscribe(topic); + + _subscriptions.RemoveAll(s => s.Topic.Equals(topic)); + _subscriptions.Add(new ManagedMqttSubscription(options)); + } + } + + public async Task Synchronize(CancellationToken cancellationToken) + { + // TODO: Remember real changes. + + List pendingSubscriptions; + List obsoleteSubscriptions; + lock (_subscriptions) + { + pendingSubscriptions = _subscriptions.Where(s => s.IsPending).ToList(); + obsoleteSubscriptions = _subscriptions.Where(s => s.IsObsolete).ToList(); + } + + if (pendingSubscriptions.Count == 0 && obsoleteSubscriptions.Count == 0) + { + _logger.Verbose("No subscriptions to synchronize"); + return; + } + + foreach (var subscription in pendingSubscriptions) + { + var subscribeResult = await _mqttClient.SubscribeAsync(subscription.SubscribeOptions, cancellationToken).ConfigureAwait(false); + subscription.IsPending = false; + await FireSubscribeProcessedEvent(subscription, subscribeResult).ConfigureAwait(false); + } + + foreach (var subscription in obsoleteSubscriptions) + { + var unsubscribeResult = await _mqttClient.UnsubscribeAsync(subscription.UnsubscribeOptions, cancellationToken).ConfigureAwait(false); + lock (_subscriptions) + { + _subscriptions.Remove(subscription); + } + + await FireUnsubscribeProcessedEvent(subscription, unsubscribeResult).ConfigureAwait(false); + } + } + + public void Unsubscribe(MqttClientUnsubscribeOptions options) + { + if (options == null) + { + throw new ArgumentNullException(nameof(options)); + } + + lock (_subscriptions) + { + var topic = options.TopicFilters.First(); + var existingSubscription = _subscriptions.FirstOrDefault(s => s.Topic.Equals(topic)); + + if (existingSubscription != null) + { + existingSubscription.MarkAsUnsubscribed(options); + } + } + } + + async Task FireSubscribeProcessedEvent(ManagedMqttSubscription subscription, MqttClientSubscribeResult subscribeResult) + { + _logger.Info("Subscribed topic '{0}'.", subscription.Topic); + + if (SubscribeProcessedEvent.HasHandlers) + { + var resultCode = subscribeResult?.Items.First().ResultCode ?? MqttClientSubscribeResultCode.UnspecifiedError; + var eventArgs = new SubscribeProcessedEventArgs(subscription.SubscribeOptions, resultCode, subscribeResult?.UserProperties); + await SubscribeProcessedEvent.InvokeAsync(eventArgs).ConfigureAwait(false); + } + } + + async Task FireUnsubscribeProcessedEvent(ManagedMqttSubscription subscription, MqttClientUnsubscribeResult unsubscribeResult) + { + _logger.Info("Unsubscribed topic '{0}'.", subscription.Topic); + + if (UnsubscribeProcessedEvent.HasHandlers) + { + var resultCode = unsubscribeResult?.Items.First().ResultCode ?? MqttClientUnsubscribeResultCode.UnspecifiedError; + var eventArgs = new UnsubscribeProcessedEventArgs(subscription.UnsubscribeOptions, resultCode, unsubscribeResult?.UserProperties); + await UnsubscribeProcessedEvent.InvokeAsync(eventArgs).ConfigureAwait(false); + } + } + } +} \ No newline at end of file diff --git a/Source/MQTTnet.Extensions.ManagedClient/New/MqttConnectionManager.cs b/Source/MQTTnet.Extensions.ManagedClient/New/MqttConnectionManager.cs new file mode 100644 index 00000000..290efab7 --- /dev/null +++ b/Source/MQTTnet.Extensions.ManagedClient/New/MqttConnectionManager.cs @@ -0,0 +1,103 @@ +// 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.Threading; +using System.Threading.Tasks; +using MQTTnet.Client; +using MQTTnet.Diagnostics; +using MQTTnet.Internal; + +namespace MQTTnet.Extensions.ManagedClient +{ + public sealed class MqttConnectionManager + { + readonly MqttNetSourceLogger _logger; + readonly IMqttClient _mqttClient; + + bool _wasConnected; + + public MqttConnectionManager(IMqttClient mqttClient, IMqttNetLogger logger) + { + _mqttClient = mqttClient ?? throw new ArgumentNullException(nameof(mqttClient)); + + if (logger == null) + { + throw new ArgumentNullException(nameof(logger)); + } + + _logger = logger.WithSource(); + + _mqttClient.DisconnectedAsync += OnClientDisconnected; + } + + public AsyncEvent ConnectedEvent { get; } = new AsyncEvent(); + + public AsyncEvent ConnectingFailedEvent { get; } = new AsyncEvent(); + + public AsyncEvent DisconnectedEvent { get; } = new AsyncEvent(); + + public async Task MaintainConnection(ManagedMqttClientOptions options, CancellationToken cancellationToken) + { + cancellationToken.ThrowIfCancellationRequested(); + + if (_wasConnected) + { + try + { + // The only valid way for testing the connection is to send a PING. So we start with this + // in order to check if reconnecting etc. is require or not. + await _mqttClient.PingAsync(cancellationToken).ConfigureAwait(false); + + // The client is properly connection. No further action required. + return MaintainConnectionResult.NoChange; + } + catch (Exception exception) + { + _logger.Error(exception, "Error while checking connection."); + } + } + + MqttClientConnectResult connectResult = null; + try + { + // The PING was not successful so that there is no working connection. So we connect. + connectResult = await _mqttClient.ConnectAsync(options.ClientOptions, cancellationToken).ConfigureAwait(false); + + _wasConnected = true; + _logger.Info("Connected."); + + if (ConnectedEvent.HasHandlers) + { + var eventArgs = new MqttClientConnectedEventArgs(connectResult); + await ConnectedEvent.TryInvokeAsync(eventArgs, _logger).ConfigureAwait(false); + } + + return MaintainConnectionResult.ConnectedEstablished; + } + catch (Exception exception) + { + if (!(exception is OperationCanceledException)) + { + _logger.Error(exception, "Error while connecting."); + } + + if (ConnectingFailedEvent.HasHandlers) + { + var eventArgs = new ConnectingFailedEventArgs(connectResult, exception); + await ConnectingFailedEvent.InvokeAsync(eventArgs).ConfigureAwait(false); + } + } + + _wasConnected = false; + return MaintainConnectionResult.NotConnected; + } + + Task OnClientDisconnected(MqttClientDisconnectedEventArgs arg) + { + _wasConnected = false; + return DisconnectedEvent?.InvokeAsync(arg); + } + } +} \ No newline at end of file diff --git a/Source/MQTTnet.Extensions.ManagedClient/New/NewManagedMqttClient.cs b/Source/MQTTnet.Extensions.ManagedClient/New/NewManagedMqttClient.cs new file mode 100644 index 00000000..4b66ed09 --- /dev/null +++ b/Source/MQTTnet.Extensions.ManagedClient/New/NewManagedMqttClient.cs @@ -0,0 +1,310 @@ +// 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.Threading; +using System.Threading.Tasks; +using MQTTnet.Client; +using MQTTnet.Diagnostics; +using MQTTnet.Implementations; +using MQTTnet.Internal; + +namespace MQTTnet.Extensions.ManagedClient +{ + public sealed class NewManagedMqttClient : Disposable, IManagedMqttClient + { + readonly AsyncEvent _applicationMessageProcessedEvent = new AsyncEvent(); + readonly ApplicationMessagesManager _applicationMessagesManager; + readonly MqttConnectionManager _connectionManager; + readonly AsyncEvent _isConnectedChangedEvent = new AsyncEvent(); + readonly MqttNetSourceLogger _logger; + readonly ManagedMqttSubscriptionsManager _subscriptionsManager; + + readonly ManualResetEventSlim _workSignal = new ManualResetEventSlim(); + + CancellationTokenSource _workerCancellationToken; + Task _workerTask; + + public NewManagedMqttClient(IMqttClient mqttClient, IMqttNetLogger logger) + { + InternalClient = mqttClient ?? throw new ArgumentNullException(nameof(mqttClient)); + + if (logger == null) + { + throw new ArgumentNullException(nameof(logger)); + } + + InternalClient.DisconnectedAsync += OnInternalClientDisconnected; + + _logger = logger.WithSource(nameof(NewManagedMqttClient)); + + _connectionManager = new MqttConnectionManager(mqttClient, logger); + _subscriptionsManager = new ManagedMqttSubscriptionsManager(mqttClient, logger); + _applicationMessagesManager = new ApplicationMessagesManager(mqttClient, logger); + + // TODO: Start timer for periodical connection checking. + } + + public event Func ApplicationMessageEnqueueingAsync + { + add => _applicationMessagesManager.ApplicationMessageEnqueueingEvent.AddHandler(value); + remove => _applicationMessagesManager.ApplicationMessageEnqueueingEvent.RemoveHandler(value); + } + + public event Func ApplicationMessageProcessedAsync + { + add => _applicationMessageProcessedEvent.AddHandler(value); + remove => _applicationMessageProcessedEvent.RemoveHandler(value); + } + + public event Func ApplicationMessageReceivedAsync + { + add => InternalClient.ApplicationMessageReceivedAsync += value; + remove => InternalClient.ApplicationMessageReceivedAsync += value; + } + + public event Func ApplicationMessageDroppedAsync + { + add => _applicationMessagesManager.ApplicationMessageDroppedEvent.AddHandler(value); + remove => _applicationMessagesManager.ApplicationMessageDroppedEvent.RemoveHandler(value); + } + + public event Func ConnectedAsync + { + add => _connectionManager.ConnectedEvent.AddHandler(value); + remove => _connectionManager.ConnectedEvent.RemoveHandler(value); + } + + public event Func ConnectingFailedAsync + { + add => _connectionManager.ConnectingFailedEvent.AddHandler(value); + remove => _connectionManager.ConnectingFailedEvent.RemoveHandler(value); + } + + public event Func DisconnectedAsync + { + add => _connectionManager.DisconnectedEvent.AddHandler(value); + remove => _connectionManager.DisconnectedEvent.RemoveHandler(value); + } + + public event Func SubscribeProcessedAsync + { + add => _subscriptionsManager.SubscribeProcessedEvent.AddHandler(value); + remove => _subscriptionsManager.SubscribeProcessedEvent.RemoveHandler(value); + } + + public event Func UnsubscribeProcessedAsync + { + add => _subscriptionsManager.UnsubscribeProcessedEvent.AddHandler(value); + remove => _subscriptionsManager.UnsubscribeProcessedEvent.RemoveHandler(value); + } + + public IMqttClient InternalClient { get; } + + public bool IsConnected { get; private set; } + + public bool IsStarted { get; private set; } + + public ManagedMqttClientOptions Options { get; private set; } + + public int EnqueuedApplicationMessagesCount => _applicationMessagesManager.EnqueuedApplicationMessagesCount; + + public Task EnqueueAsync(MqttApplicationMessage applicationMessage) + { + return EnqueueAsync(new ManagedMqttApplicationMessage(Guid.NewGuid().ToString(), applicationMessage)); + } + + public async Task EnqueueAsync(ManagedMqttApplicationMessage applicationMessage) + { + if (applicationMessage == null) + { + throw new ArgumentNullException(nameof(applicationMessage)); + } + + await _applicationMessagesManager.Enqueue(applicationMessage).ConfigureAwait(false); + _workSignal.Set(); + } + + public Task PingAsync(CancellationToken cancellationToken = default) + { + return InternalClient.PingAsync(cancellationToken); + } + + public Task StartAsync(ManagedMqttClientOptions options) + { + Options = options ?? throw new ArgumentNullException(nameof(options)); + + _workerCancellationToken = new CancellationTokenSource(); + + _workerTask = Task.Factory.StartNew( + () => WorkerLoop(_workerCancellationToken.Token), + _workerCancellationToken.Token, + TaskCreationOptions.PreferFairness, + TaskScheduler.Default); + + _workSignal.Set(); + + return PlatformAbstractionLayer.CompletedTask; + } + + public async Task StopAsync(MqttClientDisconnectOptions options, CancellationToken cancellationToken = default) + { + if (options == null) + { + throw new ArgumentNullException(nameof(options)); + } + + // TODO: Send proper Disconnect signal. + + try + { + await InternalClient.DisconnectAsync(options, cancellationToken).ConfigureAwait(false); + } + catch (Exception exception) + { + _logger.Error(exception, "Error while disconnecting."); + } + + _workerCancellationToken?.Cancel(false); + _workerCancellationToken?.Dispose(); + + _workerCancellationToken = null; + + if (_workerTask != null) + { + } + } + + public Task SubscribeAsync(MqttClientSubscribeOptions options) + { + ThrowIfDisposed(); + + _subscriptionsManager.Subscribe(options); + _workSignal.Set(); + + return PlatformAbstractionLayer.CompletedTask; + } + + public Task UnsubscribeAsync(MqttClientUnsubscribeOptions options) + { + ThrowIfDisposed(); + + _subscriptionsManager.Unsubscribe(options); + _workSignal.Set(); + + return PlatformAbstractionLayer.CompletedTask; + } + + protected override void Dispose(bool disposing) + { + base.Dispose(disposing); + + if (disposing) + { + } + } + + Task OnInternalClientDisconnected(MqttClientDisconnectedEventArgs eventArgs) + { + _workSignal.Set(); + return PlatformAbstractionLayer.CompletedTask; + } + + async Task UpdateConnectionStatus(MaintainConnectionResult maintainConnectionResult) + { + var isConnected = maintainConnectionResult != MaintainConnectionResult.NotConnected; + + if (IsConnected.Equals(isConnected)) + { + return; + } + + IsConnected = isConnected; + + if (_isConnectedChangedEvent.HasHandlers) + { + await _isConnectedChangedEvent.InvokeAsync(EventArgs.Empty).ConfigureAwait(false); + } + } + + async Task WorkerLoop(CancellationToken cancellationToken) + { + try + { + IsStarted = true; + + while (!cancellationToken.IsCancellationRequested) + { + _workSignal.Wait(cancellationToken); + _workSignal.Reset(); + + var maintainConnectionResult = await _connectionManager.MaintainConnection(Options, cancellationToken).ConfigureAwait(false); + await UpdateConnectionStatus(maintainConnectionResult).ConfigureAwait(false); + + if (maintainConnectionResult == MaintainConnectionResult.ConnectedEstablished && Options.ClientOptions.CleanSession) + { + // Since the session is clean the server will now know any subscription so that + // we have to subscribe to all of them again. + _subscriptionsManager.ResubscribeAll(); + } + + if (IsConnected) + { + await _subscriptionsManager.Synchronize(cancellationToken).ConfigureAwait(false); + } + + if (IsConnected) + { + // TODO: Move into pending messages manager + locking. + while (_applicationMessagesManager.TryPeek(out var nextPendingMessage)) + { + Exception processingException = null; + try + { + _logger.Verbose("Publishing managed application message {0}.", nextPendingMessage.Id); + + await InternalClient.PublishAsync(nextPendingMessage.ApplicationMessage, cancellationToken).ConfigureAwait(false); + + // The message was delivered successfully. So we can get rid of it. + _applicationMessagesManager.Dequeue(); + } + catch (Exception exception) + { + processingException = exception; + } + finally + { + if (_applicationMessageProcessedEvent.HasHandlers) + { + var eventArgs = new ApplicationMessageProcessedEventArgs(nextPendingMessage, processingException); + await _applicationMessageProcessedEvent.InvokeAsync(eventArgs).ConfigureAwait(false); + } + } + } + } + } + } + catch (OperationCanceledException) + { + } +#if !NETSTANDARD1_3 + catch (ThreadAbortException) + { + } +#endif + catch (Exception exception) + { + _logger.Error(exception, "Error while maintaining connection."); + } + finally + { + IsStarted = false; + + await UpdateConnectionStatus(MaintainConnectionResult.NotConnected).ConfigureAwait(false); + } + + // TODO: Disconnected! + } + } +} \ No newline at end of file diff --git a/Source/MQTTnet.Extensions.ManagedClient/ManagedMqttClientOptions.cs b/Source/MQTTnet.Extensions.ManagedClient/New/Options/ManagedMqttClientOptions.cs similarity index 85% rename from Source/MQTTnet.Extensions.ManagedClient/ManagedMqttClientOptions.cs rename to Source/MQTTnet.Extensions.ManagedClient/New/Options/ManagedMqttClientOptions.cs index dcd0c119..bc77dc07 100644 --- a/Source/MQTTnet.Extensions.ManagedClient/ManagedMqttClientOptions.cs +++ b/Source/MQTTnet.Extensions.ManagedClient/New/Options/ManagedMqttClientOptions.cs @@ -10,6 +10,15 @@ namespace MQTTnet.Extensions.ManagedClient { public sealed class ManagedMqttClientOptions { + public ManagedMqttClientOptions() + { + } + + public ManagedMqttClientOptions(MqttClientOptions clientOptions) + { + ClientOptions = clientOptions; + } + public MqttClientOptions ClientOptions { get; set; } public TimeSpan AutoReconnectDelay { get; set; } = TimeSpan.FromSeconds(5); diff --git a/Source/MQTTnet.Extensions.ManagedClient/ManagedMqttClientOptionsBuilder.cs b/Source/MQTTnet.Extensions.ManagedClient/New/Options/ManagedMqttClientOptionsBuilder.cs similarity index 98% rename from Source/MQTTnet.Extensions.ManagedClient/ManagedMqttClientOptionsBuilder.cs rename to Source/MQTTnet.Extensions.ManagedClient/New/Options/ManagedMqttClientOptionsBuilder.cs index 72c192b5..4841cfe3 100644 --- a/Source/MQTTnet.Extensions.ManagedClient/ManagedMqttClientOptionsBuilder.cs +++ b/Source/MQTTnet.Extensions.ManagedClient/New/Options/ManagedMqttClientOptionsBuilder.cs @@ -8,7 +8,7 @@ using MQTTnet.Server; namespace MQTTnet.Extensions.ManagedClient { - public class ManagedMqttClientOptionsBuilder + public sealed class ManagedMqttClientOptionsBuilder { readonly ManagedMqttClientOptions _options = new ManagedMqttClientOptions(); MqttClientOptionsBuilder _clientOptionsBuilder; diff --git a/Source/MQTTnet.Tests/Client/ManagedMqttClient_Tests.cs b/Source/MQTTnet.Tests/ManagedMqttClient/ManagedMqttClient_Connection_Tests.cs similarity index 68% rename from Source/MQTTnet.Tests/Client/ManagedMqttClient_Tests.cs rename to Source/MQTTnet.Tests/ManagedMqttClient/ManagedMqttClient_Connection_Tests.cs index a590027d..14c767ed 100644 --- a/Source/MQTTnet.Tests/Client/ManagedMqttClient_Tests.cs +++ b/Source/MQTTnet.Tests/ManagedMqttClient/ManagedMqttClient_Connection_Tests.cs @@ -13,121 +13,67 @@ using MQTTnet.Diagnostics; using MQTTnet.Extensions.ManagedClient; using MQTTnet.Implementations; using MQTTnet.Internal; -using MQTTnet.Protocol; using MQTTnet.Server; using MQTTnet.Tests.Mockups; -using MqttClient = MQTTnet.Client.MqttClient; -namespace MQTTnet.Tests.Client +namespace MQTTnet.Tests.ManagedMqttClient { [TestClass] - public sealed class ManagedMqttClient_Tests : BaseTestClass + public sealed class ManagedMqttClient_Connection_Tests : BaseTestClass { + + [TestMethod] - public async Task Connect_To_Invalid_Server() + public async Task Connect_To_Valid_Server() { using (var testEnvironment = CreateTestEnvironment()) { - testEnvironment.IgnoreClientLogErrors = true; + await testEnvironment.StartServer(); + var managedClient = await testEnvironment.StartManagedClient(); - var mqttClient = testEnvironment.CreateClient(); + await LongTestDelay(); - var managedClient = testEnvironment.Factory.CreateManagedMqttClient(mqttClient); - - ConnectingFailedEventArgs connectingFailedEventArgs = null; - - managedClient.ConnectingFailedAsync += args => - { - connectingFailedEventArgs = args; - return PlatformAbstractionLayer.CompletedTask; - }; - - await managedClient.StartAsync( - new ManagedMqttClientOptions - { - ClientOptions = testEnvironment.Factory.CreateClientOptionsBuilder().WithTimeout(TimeSpan.FromSeconds(2)).WithTcpServer("wrong_server", 1234).Build() - }); - - await managedClient.EnqueueAsync("test_topic_2"); - - SpinWait.SpinUntil(() => connectingFailedEventArgs != null, 10000); - - // The wrong server must be reported in general. - Assert.IsNotNull(connectingFailedEventArgs); - Assert.IsNotNull(connectingFailedEventArgs.Exception); - Assert.IsNull(connectingFailedEventArgs.ConnectResult); + Assert.IsTrue(managedClient.IsStarted); + Assert.IsTrue(managedClient.IsConnected); } } [TestMethod] - public async Task Drop_New_Messages_On_Full_Queue() - { - var factory = new MqttFactory(); - var managedClient = factory.CreateManagedMqttClient(); - try - { - var clientOptions = new ManagedMqttClientOptionsBuilder().WithMaxPendingMessages(5) - .WithPendingMessagesOverflowStrategy(MqttPendingMessagesOverflowStrategy.DropNewMessage); - - clientOptions.WithClientOptions(o => o.WithTcpServer("localhost")); - - await managedClient.StartAsync(clientOptions.Build()); - - await managedClient.EnqueueAsync("1"); - await managedClient.EnqueueAsync("2"); - await managedClient.EnqueueAsync("3"); - await managedClient.EnqueueAsync("4"); - await managedClient.EnqueueAsync("5"); - await managedClient.EnqueueAsync("6"); - await managedClient.EnqueueAsync("7"); - await managedClient.EnqueueAsync("8"); - - Assert.AreEqual(5, managedClient.PendingApplicationMessagesCount); - } - finally - { - await managedClient.StopAsync(); - } - } - - [TestMethod] - public async Task ManagedClients_Will_Message_Send() + public async Task Will_Message_Send() { using (var testEnvironment = CreateTestEnvironment()) { await testEnvironment.StartServer(); + // Prepare the receiving client. var receivedMessagesCount = 0; - - var clientOptions = new MqttClientOptionsBuilder().WithTcpServer("127.0.0.1", testEnvironment.ServerPort) - .WithWillTopic("My/last/will") - .WithWillQualityOfServiceLevel(MqttQualityOfServiceLevel.AtMostOnce) - .Build(); - - var dyingClient = testEnvironment.CreateClient(); - var dyingManagedClient = new ManagedMqttClient(dyingClient, testEnvironment.ClientLogger); - - await dyingManagedClient.StartAsync(new ManagedMqttClientOptionsBuilder().WithClientOptions(clientOptions).Build()); - - // Wait until the managed client is fully set up and running. - await Task.Delay(1000); - var receivingClient = await testEnvironment.ConnectClient(); - receivingClient.ApplicationMessageReceivedAsync += e => { Interlocked.Increment(ref receivedMessagesCount); return PlatformAbstractionLayer.CompletedTask; }; - + await receivingClient.SubscribeAsync("My/last/will"); + + // Prepare the managed client. + var clientOptions = new MqttClientOptionsBuilder().WithTcpServer("127.0.0.1", testEnvironment.ServerPort) + .WithWillTopic("My/last/will") + .Build(); + var dyingClient = testEnvironment.CreateClient(); + var dyingManagedClient = testEnvironment.CreateManagedMqttClient(dyingClient); + await dyingManagedClient.StartAsync(new ManagedMqttClientOptionsBuilder().WithClientOptions(clientOptions).Build()); + + // Wait until the managed client is fully set up and running. + await LongTestDelay(); + // Disposing the client will not sent a DISCONNECT packet so that connection is terminated // which will lead to the will publish. dyingManagedClient.Dispose(); // Wait for arrival of the will message at the receiver. - await Task.Delay(5000); + await LongTestDelay(); Assert.AreEqual(1, receivedMessagesCount); } @@ -140,18 +86,17 @@ namespace MQTTnet.Tests.Client { var server = await testEnvironment.StartServer(); - var managedClient = new ManagedMqttClient(testEnvironment.CreateClient(), new MqttNetEventLogger()); - var clientOptions = new MqttClientOptionsBuilder().WithTcpServer("localhost", testEnvironment.ServerPort); + var managedClient = testEnvironment.CreateManagedClient(); var connected = GetConnectedTask(managedClient); - await managedClient.StartAsync(new ManagedMqttClientOptionsBuilder().WithClientOptions(clientOptions).Build()); + await managedClient.StartAsync(testEnvironment.CreateDefaultManagedMqttClientOptions()); await connected; await managedClient.StopAsync(); - await Task.Delay(500); + await LongTestDelay(); Assert.AreEqual(0, (await server.GetClientsAsync()).Count); } @@ -167,7 +112,7 @@ namespace MQTTnet.Tests.Client await testEnvironment.StartServer(); - var managedClient = new ManagedMqttClient(testEnvironment.CreateClient(), new MqttNetEventLogger()); + var managedClient = testEnvironment.CreateManagedClient(); var clientOptions = new MqttClientOptionsBuilder().WithTcpServer("localhost", testEnvironment.ServerPort); var storage = new ManagedMqttClientTestStorage(); @@ -207,6 +152,8 @@ namespace MQTTnet.Tests.Client { using (var testEnvironment = CreateTestEnvironment()) { + await testEnvironment.StartServer(); + var unmanagedClient = testEnvironment.CreateClient(); var managedClient = await CreateManagedClientAsync(testEnvironment, unmanagedClient); @@ -223,7 +170,7 @@ namespace MQTTnet.Tests.Client await managedClient.SubscribeAsync("unsubscribedThenSubscribed"); //wait a bit for the subscriptions to become established before the messages are published - await Task.Delay(500); + await LongTestDelay(); var sendingClient = await testEnvironment.ConnectClient(); @@ -253,7 +200,7 @@ namespace MQTTnet.Tests.Client await connected; // wait a bit so that the managed client can reestablish the subscriptions - await Task.Delay(500); + await LongTestDelay(); received = SetupReceivingOfMessages(managedClient, 2); @@ -269,28 +216,14 @@ namespace MQTTnet.Tests.Client { using (var testEnvironment = CreateTestEnvironment()) { - await testEnvironment.StartServer().ConfigureAwait(false); - - var sendingClient = await testEnvironment.ConnectClient().ConfigureAwait(false); - await sendingClient.PublishAsync( - new MqttApplicationMessage - { - Topic = "topic", - Payload = new byte[] { 1 }, - Retain = true - }); - - // Wait a bit for the retained message to be available - await Task.Delay(1000); - - await sendingClient.DisconnectAsync(); + var server = await testEnvironment.StartServer(); + // Prepare retained messages. + await server.InjectApplicationMessage(testEnvironment.Factory.CreateApplicationMessageBuilder().WithTopic("topic").WithPayload("payload").WithRetainFlag().Build()); + // Now use the managed client and check if subscriptions get cleared properly. - - var clientOptions = new MqttClientOptionsBuilder().WithTcpServer("127.0.0.1", testEnvironment.ServerPort); - + var managedClient = testEnvironment.CreateManagedMqttClient(); var receivedManagedMessages = new List(); - var managedClient = new ManagedMqttClient(testEnvironment.CreateClient(), new MqttNetEventLogger()); managedClient.ApplicationMessageReceivedAsync += e => { receivedManagedMessages.Add(e.ApplicationMessage); @@ -299,19 +232,21 @@ namespace MQTTnet.Tests.Client await managedClient.SubscribeAsync("topic"); - await managedClient.StartAsync(new ManagedMqttClientOptionsBuilder().WithClientOptions(clientOptions).WithAutoReconnectDelay(TimeSpan.FromSeconds(1)).Build()); + var managedClientOptions = testEnvironment.CreateDefaultManagedMqttClientOptions(); + managedClientOptions.ClientOptions.CleanSession = false; + await managedClient.StartAsync(managedClientOptions); - await Task.Delay(1000); + await LongTestDelay(); Assert.AreEqual(1, receivedManagedMessages.Count); await managedClient.StopAsync(); - await Task.Delay(500); + await LongTestDelay(); - await managedClient.StartAsync(new ManagedMqttClientOptionsBuilder().WithClientOptions(clientOptions).WithAutoReconnectDelay(TimeSpan.FromSeconds(1)).Build()); + await managedClient.StartAsync(managedClientOptions); - await Task.Delay(1000); + await LongTestDelay(); // After reconnect and then some delay, the retained message must not be received, // showing that the subscriptions were cleared @@ -319,7 +254,9 @@ namespace MQTTnet.Tests.Client // Make sure that it gets received after subscribing again. await managedClient.SubscribeAsync("topic"); - await Task.Delay(500); + + await LongTestDelay(); + Assert.AreEqual(2, receivedManagedMessages.Count); } } @@ -331,6 +268,8 @@ namespace MQTTnet.Tests.Client { using (var testEnvironment = CreateTestEnvironment()) { + await testEnvironment.StartServer(); + // Use a long connection check interval to verify that the subscriptions // do not depend on the connection check interval anymore var connectionCheckInterval = TimeSpan.FromSeconds(10); @@ -361,6 +300,8 @@ namespace MQTTnet.Tests.Client { using (var testEnvironment = CreateTestEnvironment()) { + await testEnvironment.StartServer(); + var managedClient = await CreateManagedClientAsync(testEnvironment); var sendingClient = await testEnvironment.ConnectClient(); @@ -387,14 +328,12 @@ namespace MQTTnet.Tests.Client } } - async Task CreateManagedClientAsync( + async Task CreateManagedClientAsync( TestEnvironment testEnvironment, IMqttClient underlyingClient = null, TimeSpan? connectionCheckInterval = null, string host = "localhost") { - await testEnvironment.StartServer(); - var clientOptions = new MqttClientOptionsBuilder().WithTcpServer(host, testEnvironment.ServerPort); var managedOptions = new ManagedMqttClientOptionsBuilder().WithClientOptions(clientOptions).Build(); @@ -404,46 +343,31 @@ namespace MQTTnet.Tests.Client // at connection check intervals managedOptions.ConnectionCheckInterval = connectionCheckInterval ?? TimeSpan.FromSeconds(0.1); - var managedClient = new ManagedMqttClient(underlyingClient ?? testEnvironment.CreateClient(), new MqttNetEventLogger()); + var managedClient = testEnvironment.CreateManagedMqttClient(underlyingClient ?? testEnvironment.CreateClient()); var connected = GetConnectedTask(managedClient); await managedClient.StartAsync(managedOptions); - + await connected; return managedClient; } - /// - /// Returns a task that will finish when the - /// - /// has connected - /// - Task GetConnectedTask(ManagedMqttClient managedClient) + static Task GetConnectedTask(IManagedMqttClient managedClient) { - var connected = new TaskCompletionSource(); + var promise = new TaskCompletionSource(); managedClient.ConnectedAsync += e => { - connected.TrySetResult(true); + promise.TrySetResult(true); return PlatformAbstractionLayer.CompletedTask; }; - return connected.Task; + return promise.Task; } - /// - /// Returns a task that will return the messages received on - /// - /// when - /// - /// have been received - /// - Task> SetupReceivingOfMessages(ManagedMqttClient managedClient, int expectedNumberOfMessages) + Task> SetupReceivingOfMessages(IManagedMqttClient managedClient, int expectedNumberOfMessages) { var receivedMessages = new List(); var result = new AsyncTaskCompletionSource>(); @@ -466,7 +390,7 @@ namespace MQTTnet.Tests.Client public class ManagedMqttClientTestStorage : IManagedMqttClientStorage { - IList _messages; + IList _messages = new List(); public int GetMessageCount() { @@ -475,11 +399,6 @@ namespace MQTTnet.Tests.Client public Task> LoadQueuedMessagesAsync() { - if (_messages == null) - { - _messages = new List(); - } - return Task.FromResult(_messages); } diff --git a/Source/MQTTnet.Tests/ManagedMqttClient/ManagedMqttClient_Event_Tests.cs b/Source/MQTTnet.Tests/ManagedMqttClient/ManagedMqttClient_Event_Tests.cs new file mode 100644 index 00000000..ae877dd2 --- /dev/null +++ b/Source/MQTTnet.Tests/ManagedMqttClient/ManagedMqttClient_Event_Tests.cs @@ -0,0 +1,178 @@ +// 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.Threading; +using System.Threading.Tasks; +using Microsoft.VisualStudio.TestTools.UnitTesting; +using MQTTnet.Extensions.ManagedClient; +using MQTTnet.Implementations; + +namespace MQTTnet.Tests.ManagedMqttClient +{ + [TestClass] + public sealed class ManagedMqttClient_Event_Tests : BaseTestClass + { + [TestMethod] + public async Task Connecting_Should_Fire_Event() + { + using (var testEnvironment = CreateTestEnvironment()) + { + await testEnvironment.StartServer(); + + var managedClient = testEnvironment.CreateManagedClient(); + + var eventFired = false; + managedClient.ConnectedAsync += e => + { + eventFired = true; + return PlatformAbstractionLayer.CompletedTask; + }; + + await managedClient.StartAsync(testEnvironment.CreateDefaultManagedMqttClientOptions()); + + await LongTestDelay(); + + Assert.IsTrue(eventFired); + } + } + + [TestMethod] + public async Task Connecting_To_Invalid_Server_Should_Fire_Event() + { + using (var testEnvironment = CreateTestEnvironment()) + { + testEnvironment.IgnoreClientLogErrors = true; + + var managedClient = testEnvironment.CreateManagedMqttClient(); + + ConnectingFailedEventArgs connectingFailedEventArgs = null; + managedClient.ConnectingFailedAsync += args => + { + connectingFailedEventArgs = args; + return PlatformAbstractionLayer.CompletedTask; + }; + + await managedClient.StartAsync( + new ManagedMqttClientOptions + { + ClientOptions = testEnvironment.Factory.CreateClientOptionsBuilder().WithTimeout(TimeSpan.FromSeconds(2)).WithTcpServer("wrong_server", 1234).Build() + }); + + SpinWait.SpinUntil(() => connectingFailedEventArgs != null, 10000); + + // The wrong server must be reported in general. + Assert.IsNotNull(connectingFailedEventArgs); + Assert.IsNotNull(connectingFailedEventArgs.Exception); + Assert.IsNull(connectingFailedEventArgs.ConnectResult); + } + } + + [TestMethod] + public async Task Disconnect_Should_Fire_Event() + { + using (var testEnvironment = CreateTestEnvironment()) + { + var server = await testEnvironment.StartServer(); + + var managedClient = testEnvironment.CreateManagedClient(); + + var eventFired = false; + managedClient.DisconnectedAsync += e => + { + eventFired = true; + return PlatformAbstractionLayer.CompletedTask; + }; + + await managedClient.StartAsync(testEnvironment.CreateDefaultManagedMqttClientOptions()); + + await LongTestDelay(); + + await server.StopAsync(); + + await LongTestDelay(); + + Assert.IsTrue(eventFired); + } + } + + [TestMethod] + public async Task Enqueueing_Should_Fire_Event() + { + using (var testEnvironment = CreateTestEnvironment()) + { + await testEnvironment.StartServer(); + + var managedClient = testEnvironment.CreateManagedClient(); + + var eventFired = false; + managedClient.ApplicationMessageEnqueueingAsync += e => + { + eventFired = true; + return PlatformAbstractionLayer.CompletedTask; + }; + + await managedClient.EnqueueAsync("A"); + + Assert.IsTrue(eventFired); + } + } + + [TestMethod] + public async Task Subscription_Should_Fire_Event() + { + using (var testEnvironment = CreateTestEnvironment()) + { + await testEnvironment.StartServer(); + + var managedClient = testEnvironment.CreateManagedClient(); + + await managedClient.SubscribeAsync("A"); + + var eventFired = false; + managedClient.SubscribeProcessedAsync += e => + { + eventFired = e.Options.TopicFilters[0].Topic == "A"; + return PlatformAbstractionLayer.CompletedTask; + }; + + await managedClient.StartAsync(testEnvironment.CreateDefaultManagedMqttClientOptions()); + + await LongTestDelay(); + + Assert.IsTrue(eventFired); + } + } + + [TestMethod] + public async Task Unsubscription_Should_Fire_Event() + { + using (var testEnvironment = CreateTestEnvironment()) + { + await testEnvironment.StartServer(); + + var managedClient = testEnvironment.CreateManagedClient(); + + await managedClient.SubscribeAsync("A"); + + var eventFired = false; + managedClient.UnsubscribeProcessedAsync += e => + { + eventFired = e.Options.TopicFilters[0] == "A"; + return PlatformAbstractionLayer.CompletedTask; + }; + + await managedClient.StartAsync(testEnvironment.CreateDefaultManagedMqttClientOptions()); + + await LongTestDelay(); + + await managedClient.UnsubscribeAsync("A"); + + await LongTestDelay(); + + Assert.IsTrue(eventFired); + } + } + } +} \ No newline at end of file diff --git a/Source/MQTTnet.Tests/ManagedMqttClient/ManagedMqttClient_Messages_Tests.cs b/Source/MQTTnet.Tests/ManagedMqttClient/ManagedMqttClient_Messages_Tests.cs new file mode 100644 index 00000000..44c9f390 --- /dev/null +++ b/Source/MQTTnet.Tests/ManagedMqttClient/ManagedMqttClient_Messages_Tests.cs @@ -0,0 +1,134 @@ +// 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.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using Microsoft.VisualStudio.TestTools.UnitTesting; +using MQTTnet.Extensions.ManagedClient; +using MQTTnet.Implementations; +using MQTTnet.Server; + +namespace MQTTnet.Tests.ManagedMqttClient +{ + [TestClass] + public sealed class ManagedMqttClient_Messages_Tests : BaseTestClass + { + [TestMethod] + public async Task Drop_New_Messages_On_Full_Queue() + { + using (var testEnvironment = CreateTestEnvironment()) + { + await testEnvironment.StartServer(); + + var managedClient = testEnvironment.CreateManagedMqttClient(); + + var clientOptions = new ManagedMqttClientOptionsBuilder().WithClientOptions(o => o.WithTcpServer("localhost")) + .WithMaxPendingMessages(5) + .WithPendingMessagesOverflowStrategy(MqttPendingMessagesOverflowStrategy.DropNewMessage); + + await managedClient.StartAsync(clientOptions.Build()); + + await managedClient.EnqueueAsync("1"); + await managedClient.EnqueueAsync("2"); + await managedClient.EnqueueAsync("3"); + await managedClient.EnqueueAsync("4"); + await managedClient.EnqueueAsync("5"); + await managedClient.EnqueueAsync("6"); + await managedClient.EnqueueAsync("7"); + await managedClient.EnqueueAsync("8"); + + Assert.AreEqual(5, managedClient.EnqueuedApplicationMessagesCount); + } + } + + [TestMethod] + public async Task Drop_Old_Messages_On_Full_Queue() + { + using (var testEnvironment = CreateTestEnvironment()) + { + await testEnvironment.StartServer(); + + var managedClient = testEnvironment.CreateManagedMqttClient(); + + var clientOptions = new ManagedMqttClientOptionsBuilder().WithClientOptions(o => o.WithTcpServer("localhost")) + .WithMaxPendingMessages(5) + .WithPendingMessagesOverflowStrategy(MqttPendingMessagesOverflowStrategy.DropOldestQueuedMessage); + + await managedClient.StartAsync(clientOptions.Build()); + + await managedClient.EnqueueAsync("1"); + await managedClient.EnqueueAsync("2"); + await managedClient.EnqueueAsync("3"); + await managedClient.EnqueueAsync("4"); + await managedClient.EnqueueAsync("5"); + await managedClient.EnqueueAsync("6"); + await managedClient.EnqueueAsync("7"); + await managedClient.EnqueueAsync("8"); + + Assert.AreEqual(5, managedClient.EnqueuedApplicationMessagesCount); + } + } + + [TestMethod] + public async Task Preserve_Order_Of_Subscriptions() + { + using (var testEnvironment = CreateTestEnvironment()) + { + var server = await testEnvironment.StartServer(); + + // Prepare the retained messages. + await server.InjectApplicationMessage(testEnvironment.Factory.CreateApplicationMessageBuilder().WithTopic("A").WithPayload("P").WithRetainFlag().Build()); + await server.InjectApplicationMessage(testEnvironment.Factory.CreateApplicationMessageBuilder().WithTopic("B").WithPayload("P").WithRetainFlag().Build()); + await server.InjectApplicationMessage(testEnvironment.Factory.CreateApplicationMessageBuilder().WithTopic("C").WithPayload("P").WithRetainFlag().Build()); + await server.InjectApplicationMessage(testEnvironment.Factory.CreateApplicationMessageBuilder().WithTopic("3").WithPayload("P").WithRetainFlag().Build()); + await server.InjectApplicationMessage(testEnvironment.Factory.CreateApplicationMessageBuilder().WithTopic("2").WithPayload("P").WithRetainFlag().Build()); + await server.InjectApplicationMessage(testEnvironment.Factory.CreateApplicationMessageBuilder().WithTopic("1").WithPayload("P").WithRetainFlag().Build()); + + // Subscribe to the retained messages in a defined order. + var managedClient = testEnvironment.CreateManagedMqttClient(); + + var applicationMessages = new List(); + managedClient.ApplicationMessageReceivedAsync += e => + { + applicationMessages.Add(e.ApplicationMessage); + return PlatformAbstractionLayer.CompletedTask; + }; + + await managedClient.SubscribeAsync("B"); + await managedClient.SubscribeAsync("C"); + await managedClient.SubscribeAsync("A"); + await managedClient.SubscribeAsync("2"); + await managedClient.SubscribeAsync("1"); + await managedClient.SubscribeAsync("3"); + + await managedClient.StartAsync(testEnvironment.CreateDefaultManagedMqttClientOptions()).ConfigureAwait(false); + + await LongTestDelay(); + + var order = string.Join(string.Empty, applicationMessages.Select(a => a.Topic)); + + Assert.AreEqual("BCA213", order); + } + } + + [TestMethod] + public async Task Publish() + { + using (var testEnvironment = CreateTestEnvironment()) + { + testEnvironment.TrackServerReceivedApplicationMessages = true; + await testEnvironment.StartServer(); + + var managedClient = await testEnvironment.StartManagedClient(); + + await managedClient.EnqueueAsync(testEnvironment.Factory.CreateApplicationMessageBuilder().WithTopic("TEST").Build()); + + await LongTestDelay(); + + Assert.AreEqual(1, testEnvironment.ServerReceivedApplicationMessages.Count); + } + } + } +} \ No newline at end of file diff --git a/Source/MQTTnet.Tests/Mockups/TestEnvironment.cs b/Source/MQTTnet.Tests/Mockups/TestEnvironment.cs index c3da9717..fc54397e 100644 --- a/Source/MQTTnet.Tests/Mockups/TestEnvironment.cs +++ b/Source/MQTTnet.Tests/Mockups/TestEnvironment.cs @@ -2,6 +2,8 @@ // The .NET Foundation licenses this file to you under the MIT license. // See the LICENSE file in the project root for more information. +#region + using System; using System.Collections.Generic; using System.Diagnostics; @@ -11,6 +13,7 @@ using System.Threading.Tasks; using Microsoft.VisualStudio.TestTools.UnitTesting; using MQTTnet.Client; using MQTTnet.Diagnostics; +using MQTTnet.Extensions.ManagedClient; using MQTTnet.Extensions.Rpc; using MQTTnet.Formatter; using MQTTnet.Implementations; @@ -18,6 +21,8 @@ using MQTTnet.LowLevelClient; using MQTTnet.Protocol; using MQTTnet.Server; +#endregion + namespace MQTTnet.Tests.Mockups { public sealed class TestEnvironment : IDisposable @@ -27,6 +32,7 @@ namespace MQTTnet.Tests.Mockups readonly List _exceptions = new List(); readonly List _lowLevelClients = new List(); + readonly List _managedClients = new List(); readonly MqttProtocolVersion _protocolVersion; readonly List _serverErrors = new List(); @@ -88,8 +94,12 @@ namespace MQTTnet.Tests.Mockups public int ServerPort { get; set; } + public List ServerReceivedApplicationMessages { get; } = new List(); + public TestContext TestContext { get; } + public bool TrackServerReceivedApplicationMessages { get; set; } + public Task ConnectClient() { return ConnectClient(Factory.CreateClientOptionsBuilder().WithProtocolVersion(_protocolVersion)); @@ -109,7 +119,7 @@ namespace MQTTnet.Tests.Mockups configureOptions.Invoke(optionsBuilder); var options = optionsBuilder.Build(); - + var client = CreateClient(); if (timeout == TimeSpan.Zero) @@ -137,7 +147,7 @@ namespace MQTTnet.Tests.Mockups options = options.WithTcpServer("127.0.0.1", ServerPort); var client = CreateClient(); - + if (timeout == TimeSpan.Zero) { await client.ConnectAsync(options.Build()).ConfigureAwait(false); @@ -161,7 +171,7 @@ namespace MQTTnet.Tests.Mockups } var client = CreateClient(); - + if (timeout == TimeSpan.Zero) { await client.ConnectAsync(options).ConfigureAwait(false); @@ -227,6 +237,11 @@ namespace MQTTnet.Tests.Mockups } } + public ManagedMqttClientOptions CreateDefaultManagedMqttClientOptions() + { + return Factory.CreateManagedMqttClientOptionsBuilder().WithClientOptions(Factory.CreateClientOptionsBuilder().WithTcpServer("localhost", ServerPort).Build()).Build(); + } + public ILowLevelMqttClient CreateLowLevelClient() { lock (_clients) @@ -238,6 +253,21 @@ namespace MQTTnet.Tests.Mockups } } + public IManagedMqttClient CreateManagedClient() + { + return Factory.CreateManagedMqttClient(CreateClient()); + } + + public IManagedMqttClient CreateManagedMqttClient() + { + return Factory.CreateManagedMqttClient(CreateClient(), ClientLogger); + } + + public IManagedMqttClient CreateManagedMqttClient(IMqttClient client) + { + return Factory.CreateManagedMqttClient(client, ClientLogger); + } + public MqttServer CreateServer(MqttServerOptions options) { if (Server != null) @@ -249,6 +279,11 @@ namespace MQTTnet.Tests.Mockups Server = Factory.CreateMqttServer(options, logger); + if (TrackServerReceivedApplicationMessages) + { + Server.InterceptingPublishAsync += ServerOnInterceptingPublishAsync; + } + Server.ValidatingConnectionAsync += e => { if (TestContext != null) @@ -285,6 +320,18 @@ namespace MQTTnet.Tests.Mockups } } + foreach (var managedMqttClient in _managedClients) + { + try + { + managedMqttClient.StopAsync().Wait(); + } + finally + { + managedMqttClient.Dispose(); + } + } + foreach (var lowLevelMqttClient in _lowLevelClients) { lowLevelMqttClient.Dispose(); @@ -311,6 +358,13 @@ namespace MQTTnet.Tests.Mockups } } + public async Task StartManagedClient() + { + var managedClient = Factory.CreateManagedMqttClient(CreateClient()); + await managedClient.StartAsync(CreateDefaultManagedMqttClientOptions()); + return managedClient; + } + public Task StartServer() { return StartServer(Factory.CreateServerOptionsBuilder()); @@ -325,7 +379,7 @@ namespace MQTTnet.Tests.Mockups var options = optionsBuilder.Build(); var server = CreateServer(options); await server.StartAsync(); - + // The OS has chosen the port to we have to properly expose it to the tests. ServerPort = options.DefaultEndpointOptions.Port; return server; @@ -344,7 +398,7 @@ namespace MQTTnet.Tests.Mockups var options = optionsBuilder.Build(); var server = CreateServer(options); await server.StartAsync(); - + // The OS has chosen the port to we have to properly expose it to the tests. ServerPort = options.DefaultEndpointOptions.Port; return server; @@ -380,5 +434,15 @@ namespace MQTTnet.Tests.Mockups _exceptions.Add(exception); } } + + Task ServerOnInterceptingPublishAsync(InterceptingPublishEventArgs arg) + { + lock (ServerReceivedApplicationMessages) + { + ServerReceivedApplicationMessages.Add(arg.ApplicationMessage); + } + + return PlatformAbstractionLayer.CompletedTask; + } } } \ No newline at end of file diff --git a/Source/MQTTnet/Client/Connecting/MqttClientConnectResult.cs b/Source/MQTTnet/Client/Connecting/MqttClientConnectResult.cs index 831d14cb..df4930d2 100644 --- a/Source/MQTTnet/Client/Connecting/MqttClientConnectResult.cs +++ b/Source/MQTTnet/Client/Connecting/MqttClientConnectResult.cs @@ -11,110 +11,112 @@ namespace MQTTnet.Client public sealed class MqttClientConnectResult { /// - /// Gets the result code. - /// MQTTv5 only. - /// - public MqttClientConnectResultCode ResultCode { get; internal set; } - - /// - /// Gets a value indicating whether a session was already available or not. - /// MQTTv5 only. - /// - public bool IsSessionPresent { get; internal set; } - - /// - /// Gets a value indicating whether wildcards can be used in subscriptions at the current server. - /// MQTTv5 only. - /// - public bool WildcardSubscriptionAvailable { get; internal set; } - - /// - /// Gets whether the server supports retained messages. - /// MQTTv5 only. - /// - public bool RetainAvailable { get; internal set; } - - /// - /// Gets the client identifier which was chosen by the server. - /// MQTTv5 only. + /// Gets the client identifier which was chosen by the server. + /// MQTTv5 only. /// public string AssignedClientIdentifier { get; internal set; } /// - /// Gets the authentication method. - /// MQTTv5 only. + /// Gets the authentication data. + /// MQTTv5 only. + /// + public byte[] AuthenticationData { get; internal set; } + + /// + /// Gets the authentication method. + /// MQTTv5 only. /// public string AuthenticationMethod { get; internal set; } /// - /// Gets the authentication data. - /// MQTTv5 only. + /// Gets a value indicating whether a session was already available or not. + /// MQTTv5 only. /// - public byte[] AuthenticationData { get; internal set; } + public bool IsSessionPresent { get; internal set; } public uint? MaximumPacketSize { get; internal set; } /// - /// Gets the reason string. - /// MQTTv5 only. - /// - public string ReasonString { get; internal set; } - - public ushort? ReceiveMaximum { get; internal set; } - - /// - /// Gets the maximum QoS which is supported by the server. - /// MQTTv5 only. + /// Gets the maximum QoS which is supported by the server. + /// MQTTv5 only. /// public MqttQualityOfServiceLevel MaximumQoS { get; internal set; } /// - /// Gets the response information. - /// MQTTv5 only. + /// Gets the reason string. + /// MQTTv5 only. + /// + public string ReasonString { get; internal set; } + + public ushort? ReceiveMaximum { get; internal set; } + + /// + /// Gets the response information. + /// MQTTv5 only. /// public string ResponseInformation { get; internal set; } /// - /// Gets the maximum value for a topic alias. 0 means not supported. - /// MQTTv5 only. + /// Gets the result code. + /// MQTTv5 only. /// - public ushort TopicAliasMaximum { get; internal set; } + public MqttClientConnectResultCode ResultCode { get; internal set; } /// - /// Gets an alternate server which should be used instead of the current one. - /// MQTTv5 only. + /// Gets whether the server supports retained messages. + /// MQTTv5 only. /// - public string ServerReference { get; internal set; } + public bool RetainAvailable { get; internal set; } /// - /// MQTTv5 only. - /// Gets the keep alive interval which was chosen by the server instead of the - /// keep alive interval from the client CONNECT packet. - /// A value of 0 indicates that the feature is not used. + /// MQTTv5 only. + /// Gets the keep alive interval which was chosen by the server instead of the + /// keep alive interval from the client CONNECT packet. + /// A value of 0 indicates that the feature is not used. /// public ushort ServerKeepAlive { get; internal set; } + /// + /// Gets an alternate server which should be used instead of the current one. + /// MQTTv5 only. + /// + public string ServerReference { get; internal set; } + public uint? SessionExpiryInterval { get; internal set; } /// - /// Gets a value indicating whether the subscription identifiers are available or not. - /// MQTTv5 only. - /// - public bool SubscriptionIdentifiersAvailable { get; internal set; } - - /// - /// Gets a value indicating whether the shared subscriptions are available or not. - /// MQTTv5 only. + /// Gets a value indicating whether the shared subscriptions are available or not. + /// MQTTv5 only. /// public bool SharedSubscriptionAvailable { get; internal set; } /// - /// Gets the user properties. - /// In MQTT 5, user properties are basic UTF-8 string key-value pairs that you can append to almost every type of MQTT packet. - /// As long as you don’t exceed the maximum message size, you can use an unlimited number of user properties to add metadata to MQTT messages and pass information between publisher, broker, and subscriber. - /// The feature is very similar to the HTTP header concept. - /// MQTTv5 only. + /// Gets a value indicating whether the subscription identifiers are available or not. + /// MQTTv5 only. + /// + public bool SubscriptionIdentifiersAvailable { get; internal set; } + + /// + /// Gets the maximum value for a topic alias. 0 means not supported. + /// MQTTv5 only. + /// + public ushort TopicAliasMaximum { get; internal set; } + + /// + /// Gets the user properties. + /// In MQTT 5, user properties are basic UTF-8 string key-value pairs that you can append to almost every type of MQTT + /// packet. + /// As long as you don’t exceed the maximum message size, you can use an unlimited number of user properties to add + /// metadata to MQTT messages and pass information between publisher, broker, and subscriber. + /// The feature is very similar to the HTTP header concept. + /// MQTTv5 only. /// public List UserProperties { get; internal set; } + + /// + /// Gets a value indicating whether wildcards can be used in subscriptions at the current server. + /// MQTTv5 only. + /// + public bool WildcardSubscriptionAvailable { get; internal set; } } -} +} \ No newline at end of file diff --git a/Source/MQTTnet/Client/Disconnecting/MqttClientDisconnectOptions.cs b/Source/MQTTnet/Client/Disconnecting/MqttClientDisconnectOptions.cs index 3f4e88aa..7dd7e0f2 100644 --- a/Source/MQTTnet/Client/Disconnecting/MqttClientDisconnectOptions.cs +++ b/Source/MQTTnet/Client/Disconnecting/MqttClientDisconnectOptions.cs @@ -7,15 +7,15 @@ namespace MQTTnet.Client public sealed class MqttClientDisconnectOptions { /// - /// Gets or sets the reason code. - /// Hint: MQTT 5 feature only. + /// Gets or sets the reason code. + /// Hint: MQTT 5 feature only. /// public MqttClientDisconnectReason Reason { get; set; } = MqttClientDisconnectReason.NormalDisconnection; /// - /// Gets or sets the reason string. - /// Hint: MQTT 5 feature only. + /// Gets or sets the reason string. + /// Hint: MQTT 5 feature only. /// public string ReasonString { get; set; } } -} +} \ No newline at end of file diff --git a/Source/MQTTnet/Client/Disconnecting/MqttClientDisconnectedEventArgs.cs b/Source/MQTTnet/Client/Disconnecting/MqttClientDisconnectedEventArgs.cs index 009e0208..169998c9 100644 --- a/Source/MQTTnet/Client/Disconnecting/MqttClientDisconnectedEventArgs.cs +++ b/Source/MQTTnet/Client/Disconnecting/MqttClientDisconnectedEventArgs.cs @@ -10,20 +10,20 @@ namespace MQTTnet.Client { public bool ClientWasConnected { get; internal set; } - public Exception Exception { get; internal set; } - /// - /// Gets the authentication result. - /// Hint: MQTT 5 feature only. + /// Gets the authentication result. + /// Hint: MQTT 5 feature only. /// public MqttClientConnectResult ConnectResult { get; internal set; } + public Exception Exception { get; internal set; } + /// - /// Gets or sets the reason. - /// Hint: MQTT 5 feature only. + /// Gets or sets the reason. + /// Hint: MQTT 5 feature only. /// public MqttClientDisconnectReason Reason { get; internal set; } - + public string ReasonString { get; internal set; } } -} +} \ No newline at end of file diff --git a/Source/MQTTnet/Client/MqttClient.cs b/Source/MQTTnet/Client/MqttClient.cs index 00b0e3aa..d7435daf 100644 --- a/Source/MQTTnet/Client/MqttClient.cs +++ b/Source/MQTTnet/Client/MqttClient.cs @@ -91,8 +91,8 @@ namespace MQTTnet.Client add => _inspectPacketEvent.AddHandler(value); remove => _inspectPacketEvent.RemoveHandler(value); } - - public bool IsConnected => (MqttClientConnectionStatus)_connectionStatus == MqttClientConnectionStatus.Connected; + + public bool IsConnected => _adapter != null && (MqttClientConnectionStatus)_connectionStatus == MqttClientConnectionStatus.Connected; public MqttClientOptions Options { get; private set; } @@ -125,7 +125,7 @@ namespace MQTTnet.Client if (_connectingEvent.HasHandlers) { - await _connectingEvent.InvokeAsync(new MqttClientConnectingEventArgs(options)); + await _connectingEvent.InvokeAsync(new MqttClientConnectingEventArgs(options)).ConfigureAwait(false); } _packetIdentifierProvider.Reset(); @@ -182,7 +182,10 @@ namespace MQTTnet.Client { _disconnectReason = MqttClientDisconnectReason.UnspecifiedError; - _logger.Error(exception, "Error while connecting with server."); + if (!(exception is OperationCanceledException)) + { + _logger.Error(exception, "Error while connecting with server."); + } await DisconnectInternalAsync(null, exception, connectResult).ConfigureAwait(false); @@ -517,7 +520,8 @@ namespace MQTTnet.Client connectionStatus = curStatus; break; } - } while (true); + } + while (true); } void EnqueueReceivedPublishPacket(MqttPublishPacket publishPacket) @@ -646,6 +650,7 @@ namespace MQTTnet.Client async Task SendAndReceiveAsync(MqttPacket requestPacket, CancellationToken cancellationToken) where TResponsePacket : MqttPacket { cancellationToken.ThrowIfCancellationRequested(); + ThrowIfNoAdapter(); ushort packetIdentifier = 0; if (requestPacket is MqttPacketWithIdentifier packetWithIdentifier) @@ -684,9 +689,9 @@ namespace MQTTnet.Client Task SendAsync(MqttPacket packet, CancellationToken cancellationToken) { cancellationToken.ThrowIfCancellationRequested(); + ThrowIfNoAdapter(); _lastPacketSentTimestamp = DateTime.UtcNow; - return _adapter.SendPacketAsync(packet, cancellationToken); } @@ -698,14 +703,29 @@ namespace MQTTnet.Client } } + void ThrowIfNoAdapter() + { + // Having no adapter indicates that the connection is closed or still being created. + // Thus the client is disconnected. + if (_adapter == null) + { + ThrowNotConnected(); + } + } + void ThrowIfNotConnected() { if (!IsConnected) { - throw new MqttCommunicationException("The client is not connected."); + ThrowNotConnected(); } } + void ThrowNotConnected() + { + throw new MqttCommunicationException("The client is not connected."); + } + void TryInitiateDisconnect() { lock (_disconnectLock) diff --git a/Source/MQTTnet/Client/Subscribing/MqttClientSubscribeOptions.cs b/Source/MQTTnet/Client/Subscribing/MqttClientSubscribeOptions.cs index 5c1c1ee3..5d468150 100644 --- a/Source/MQTTnet/Client/Subscribing/MqttClientSubscribeOptions.cs +++ b/Source/MQTTnet/Client/Subscribing/MqttClientSubscribeOptions.cs @@ -10,27 +10,31 @@ namespace MQTTnet.Client public sealed class MqttClientSubscribeOptions { /// - /// Gets or sets a list of topic filters the client wants to subscribe to. - /// Topic filters can include regular topics or wild cards. - /// - public List TopicFilters { get; set; } = new List(); - - /// - /// Gets or sets the subscription identifier. - /// The client can specify a subscription identifier when subscribing. - /// The broker will establish and store the mapping relationship between this subscription and subscription identifier when successfully create or modify subscription. - /// The broker will return the subscription identifier associated with this PUBLISH packet and the PUBLISH packet to the client when need to forward PUBLISH packets matching this subscription to this client. - /// Hint: MQTT 5 feature only. + /// Gets or sets the subscription identifier. + /// The client can specify a subscription identifier when subscribing. + /// The broker will establish and store the mapping relationship between this subscription and subscription identifier + /// when successfully create or modify subscription. + /// The broker will return the subscription identifier associated with this PUBLISH packet and the PUBLISH packet to + /// the client when need to forward PUBLISH packets matching this subscription to this client. + /// Hint: MQTT 5 feature only. /// public uint SubscriptionIdentifier { get; set; } /// - /// Gets or sets the user properties. - /// In MQTT 5, user properties are basic UTF-8 string key-value pairs that you can append to almost every type of MQTT packet. - /// As long as you don’t exceed the maximum message size, you can use an unlimited number of user properties to add metadata to MQTT messages and pass information between publisher, broker, and subscriber. - /// The feature is very similar to the HTTP header concept. - /// Hint: MQTT 5 feature only. + /// Gets or sets a list of topic filters the client wants to subscribe to. + /// Topic filters can include regular topics or wild cards. + /// + public List TopicFilters { get; set; } = new List(); + + /// + /// Gets or sets the user properties. + /// In MQTT 5, user properties are basic UTF-8 string key-value pairs that you can append to almost every type of MQTT + /// packet. + /// As long as you don’t exceed the maximum message size, you can use an unlimited number of user properties to add + /// metadata to MQTT messages and pass information between publisher, broker, and subscriber. + /// The feature is very similar to the HTTP header concept. + /// Hint: MQTT 5 feature only. /// public List UserProperties { get; set; } } -} +} \ No newline at end of file diff --git a/Source/MQTTnet/Client/Subscribing/MqttClientSubscribeResult.cs b/Source/MQTTnet/Client/Subscribing/MqttClientSubscribeResult.cs index 67b52600..3f579c23 100644 --- a/Source/MQTTnet/Client/Subscribing/MqttClientSubscribeResult.cs +++ b/Source/MQTTnet/Client/Subscribing/MqttClientSubscribeResult.cs @@ -10,22 +10,22 @@ namespace MQTTnet.Client public sealed class MqttClientSubscribeResult { public IReadOnlyCollection Items { get; internal set; } - + /// - /// Gets the user properties which were part of the SUBACK packet. - /// MQTTv5 only. + /// Gets the packet identifier which was used. /// - public IReadOnlyCollection UserProperties { get; internal set; } - + public ushort PacketIdentifier { get; internal set; } + /// - /// Gets the reason string. - /// MQTTv5 only. + /// Gets the reason string. + /// MQTTv5 only. /// public string ReasonString { get; internal set; } /// - /// Gets the packet identifier which was used. + /// Gets the user properties which were part of the SUBACK packet. + /// MQTTv5 only. /// - public ushort PacketIdentifier { get; internal set; } + public IReadOnlyCollection UserProperties { get; internal set; } } } \ No newline at end of file diff --git a/Source/MQTTnet/Client/Subscribing/MqttClientSubscribeResultItem.cs b/Source/MQTTnet/Client/Subscribing/MqttClientSubscribeResultItem.cs index f1fcee87..eadd4c7b 100644 --- a/Source/MQTTnet/Client/Subscribing/MqttClientSubscribeResultItem.cs +++ b/Source/MQTTnet/Client/Subscribing/MqttClientSubscribeResultItem.cs @@ -9,15 +9,15 @@ namespace MQTTnet.Client public sealed class MqttClientSubscribeResultItem { /// - /// Gets or sets the topic filter. - /// The topic filter can contain topics and wildcards. - /// - public MqttTopicFilter TopicFilter { get; internal set; } - - /// - /// Gets or sets the result code. - /// Hint: MQTT 5 feature only. + /// Gets or sets the result code. + /// Hint: MQTT 5 feature only. /// public MqttClientSubscribeResultCode ResultCode { get; internal set; } + + /// + /// Gets or sets the topic filter. + /// The topic filter can contain topics and wildcards. + /// + public MqttTopicFilter TopicFilter { get; internal set; } } -} +} \ No newline at end of file diff --git a/Source/MQTTnet/Client/Unsubscribing/MqttClientUnsubscribeOptions.cs b/Source/MQTTnet/Client/Unsubscribing/MqttClientUnsubscribeOptions.cs index da331087..97b7c5b9 100644 --- a/Source/MQTTnet/Client/Unsubscribing/MqttClientUnsubscribeOptions.cs +++ b/Source/MQTTnet/Client/Unsubscribing/MqttClientUnsubscribeOptions.cs @@ -10,18 +10,20 @@ namespace MQTTnet.Client public sealed class MqttClientUnsubscribeOptions { /// - /// Gets or sets a list of topic filters the client wants to unsubscribe from. - /// Topic filters can include regular topics or wild cards. + /// Gets or sets a list of topic filters the client wants to unsubscribe from. + /// Topic filters can include regular topics or wild cards. /// public List TopicFilters { get; set; } = new List(); /// - /// Gets or sets the user properties. - /// In MQTT 5, user properties are basic UTF-8 string key-value pairs that you can append to almost every type of MQTT packet. - /// As long as you don’t exceed the maximum message size, you can use an unlimited number of user properties to add metadata to MQTT messages and pass information between publisher, broker, and subscriber. - /// The feature is very similar to the HTTP header concept. - /// Hint: MQTT 5 feature only. + /// Gets or sets the user properties. + /// In MQTT 5, user properties are basic UTF-8 string key-value pairs that you can append to almost every type of MQTT + /// packet. + /// As long as you don’t exceed the maximum message size, you can use an unlimited number of user properties to add + /// metadata to MQTT messages and pass information between publisher, broker, and subscriber. + /// The feature is very similar to the HTTP header concept. + /// Hint: MQTT 5 feature only. /// public List UserProperties { get; set; } = new List(); } -} +} \ No newline at end of file diff --git a/Source/MQTTnet/Client/Unsubscribing/MqttClientUnsubscribeResult.cs b/Source/MQTTnet/Client/Unsubscribing/MqttClientUnsubscribeResult.cs index 6bd69401..0f93e403 100644 --- a/Source/MQTTnet/Client/Unsubscribing/MqttClientUnsubscribeResult.cs +++ b/Source/MQTTnet/Client/Unsubscribing/MqttClientUnsubscribeResult.cs @@ -12,20 +12,20 @@ namespace MQTTnet.Client public IReadOnlyCollection Items { get; internal set; } /// - /// Gets the user properties which were part of the UNSUBACK packet. - /// MQTTv5 only. - /// - public IReadOnlyCollection UserProperties { get; internal set; } - - /// - /// Gets the reason string. - /// MQTTv5 only. - /// - public string ReasonString { get; internal set; } - - /// - /// Gets the packet identifier which was used. + /// Gets the packet identifier which was used. /// public ushort PacketIdentifier { get; internal set; } + + /// + /// Gets the reason string. + /// MQTTv5 only. + /// + public string ReasonString { get; internal set; } + + /// + /// Gets the user properties which were part of the UNSUBACK packet. + /// MQTTv5 only. + /// + public IReadOnlyCollection UserProperties { get; internal set; } } -} +} \ No newline at end of file diff --git a/Source/MQTTnet/Client/Unsubscribing/MqttClientUnsubscribeResultItem.cs b/Source/MQTTnet/Client/Unsubscribing/MqttClientUnsubscribeResultItem.cs index 9f390357..fe0e4549 100644 --- a/Source/MQTTnet/Client/Unsubscribing/MqttClientUnsubscribeResultItem.cs +++ b/Source/MQTTnet/Client/Unsubscribing/MqttClientUnsubscribeResultItem.cs @@ -7,15 +7,15 @@ namespace MQTTnet.Client public sealed class MqttClientUnsubscribeResultItem { /// - /// Gets or sets the topic filter. - /// The topic filter can contain topics and wildcards. - /// - public string TopicFilter { get; internal set; } - - /// - /// Gets or sets the result code. - /// Hint: MQTT 5 feature only. + /// Gets or sets the result code. + /// Hint: MQTT 5 feature only. /// public MqttClientUnsubscribeResultCode ResultCode { get; internal set; } + + /// + /// Gets or sets the topic filter. + /// The topic filter can contain topics and wildcards. + /// + public string TopicFilter { get; internal set; } } } \ No newline at end of file diff --git a/Source/MQTTnet/Diagnostics/Logger/MqttNetSourceLoggerExtensions.cs b/Source/MQTTnet/Diagnostics/Logger/MqttNetSourceLoggerExtensions.cs index 18d70a5f..bbe93e32 100644 --- a/Source/MQTTnet/Diagnostics/Logger/MqttNetSourceLoggerExtensions.cs +++ b/Source/MQTTnet/Diagnostics/Logger/MqttNetSourceLoggerExtensions.cs @@ -18,6 +18,13 @@ namespace MQTTnet.Diagnostics return new MqttNetSourceLogger(logger, source); } + + public static MqttNetSourceLogger WithSource(this IMqttNetLogger logger) + { + if (logger == null) throw new ArgumentNullException(nameof(logger)); + + return new MqttNetSourceLogger(logger, typeof(TSource).Name); + } public static void Verbose(this MqttNetSourceLogger logger, string message, TParameter1 parameter1) { diff --git a/Source/MQTTnet/Internal/AsyncLock.cs b/Source/MQTTnet/Internal/AsyncLock.cs index 17384ef3..eb5e70b3 100644 --- a/Source/MQTTnet/Internal/AsyncLock.cs +++ b/Source/MQTTnet/Internal/AsyncLock.cs @@ -64,7 +64,7 @@ namespace MQTTnet.Internal // So we can approve the current task directly. releaser = _releaserWithDirectApproval; hasDirectApproval = true; - Debug.WriteLine("AsyncLock: Task -1 directly approved."); + //Debug.WriteLine("AsyncLock: Task -1 directly approved."); } else { @@ -114,7 +114,7 @@ namespace MQTTnet.Internal return; } - Debug.WriteLine("AsyncLock: No Task pending."); + //Debug.WriteLine("AsyncLock: No Task pending."); } } @@ -141,7 +141,7 @@ namespace MQTTnet.Internal _id = promise?.Task.Id ?? -1; - Debug.WriteLine($"AsyncLock: Task {_id} queued."); + //Debug.WriteLine($"AsyncLock: Task {_id} queued."); } public bool IsPending => _promise != null && !_promise.Task.IsCanceled && !_promise.Task.IsFaulted && !_promise.Task.IsCompleted; @@ -152,7 +152,7 @@ namespace MQTTnet.Internal { _promise?.TrySetResult(this); - Debug.WriteLine($"AsyncLock: Task {_id} approved."); + //Debug.WriteLine($"AsyncLock: Task {_id} approved."); } public void Dispose() @@ -162,7 +162,7 @@ namespace MQTTnet.Internal _cancellationTokenRegistration.Dispose(); } - Debug.WriteLine($"AsyncLock: Task {_id} completed."); + //Debug.WriteLine($"AsyncLock: Task {_id} completed."); _asyncLock.Release(this); } @@ -171,14 +171,14 @@ namespace MQTTnet.Internal { _promise?.TrySetException(exception); - Debug.WriteLine($"AsyncLock: Task {_id} failed ({exception.GetType().Name})."); + //Debug.WriteLine($"AsyncLock: Task {_id} failed ({exception.GetType().Name})."); } void Cancel() { _promise?.TrySetCanceled(); - Debug.WriteLine($"AsyncLock: Task {_id} canceled."); + //Debug.WriteLine($"AsyncLock: Task {_id} canceled."); } } } diff --git a/Source/MQTTnet/Server/MqttServerExtensions.cs b/Source/MQTTnet/Server/MqttServerExtensions.cs index 22d8538c..28374fbc 100644 --- a/Source/MQTTnet/Server/MqttServerExtensions.cs +++ b/Source/MQTTnet/Server/MqttServerExtensions.cs @@ -10,22 +10,62 @@ namespace MQTTnet.Server { public static class MqttServerExtensions { + public static Task InjectApplicationMessage(this MqttServer server, MqttApplicationMessage applicationMessage) + { + if (server == null) + { + throw new ArgumentNullException(nameof(server)); + } + + if (applicationMessage == null) + { + throw new ArgumentNullException(nameof(applicationMessage)); + } + + return server.InjectApplicationMessage(new InjectedMqttApplicationMessage(applicationMessage) + { + SenderClientId = string.Empty + }); + } + public static Task SubscribeAsync(this MqttServer server, string clientId, params MqttTopicFilter[] topicFilters) { - if (server == null) throw new ArgumentNullException(nameof(server)); - if (clientId == null) throw new ArgumentNullException(nameof(clientId)); - if (topicFilters == null) throw new ArgumentNullException(nameof(topicFilters)); + if (server == null) + { + throw new ArgumentNullException(nameof(server)); + } + + if (clientId == null) + { + throw new ArgumentNullException(nameof(clientId)); + } + + if (topicFilters == null) + { + throw new ArgumentNullException(nameof(topicFilters)); + } return server.SubscribeAsync(clientId, topicFilters); } - + public static Task SubscribeAsync(this MqttServer server, string clientId, string topic) { - if (server == null) throw new ArgumentNullException(nameof(server)); - if (clientId == null) throw new ArgumentNullException(nameof(clientId)); - if (topic == null) throw new ArgumentNullException(nameof(topic)); + if (server == null) + { + throw new ArgumentNullException(nameof(server)); + } + + if (clientId == null) + { + throw new ArgumentNullException(nameof(clientId)); + } + + if (topic == null) + { + throw new ArgumentNullException(nameof(topic)); + } return server.SubscribeAsync(clientId, new MqttTopicFilterBuilder().WithTopic(topic).Build()); } } -} +} \ No newline at end of file