diff --git a/.vscode/launch.json b/.vscode/launch.json
index ac61726..6b22338 100644
--- a/.vscode/launch.json
+++ b/.vscode/launch.json
@@ -5,8 +5,8 @@
"name": "Sample (console)",
"type": "coreclr",
"request": "launch",
- "preLaunchTask": "build",
- "program": "${workspaceFolder}/Samples/Mqtt.Sample/bin/Debug/net7.0/Mqtt.Sample.dll",
+ "preLaunchTask": "build",
+ "program": "${workspaceFolder}/Samples/Mqtt.Sample/bin/Debug/net8.0/Mqtt.Sample.dll",
"args": [],
"cwd": "${workspaceFolder}/Samples/Mqtt.Sample",
"console": "integratedTerminal",
@@ -18,4 +18,4 @@
"request": "attach"
}
]
-}
\ No newline at end of file
+}
diff --git a/Directory.Build.props b/Directory.Build.props
index bb3068b..a1e4da4 100644
--- a/Directory.Build.props
+++ b/Directory.Build.props
@@ -6,7 +6,7 @@
- net7.0
+ net8.0
latest
true
latest
@@ -41,8 +41,8 @@
-
-
+
+
diff --git a/Samples/Directory.Build.props b/Samples/Directory.Build.props
index 9cdffa3..2c4c7c5 100644
--- a/Samples/Directory.Build.props
+++ b/Samples/Directory.Build.props
@@ -17,7 +17,7 @@
-
+
diff --git a/Samples/Mqtt.Sample/Controllers/TestController.cs b/Samples/Mqtt.Sample/Controllers/TestController.cs
index 7654e33..446de31 100644
--- a/Samples/Mqtt.Sample/Controllers/TestController.cs
+++ b/Samples/Mqtt.Sample/Controllers/TestController.cs
@@ -18,10 +18,10 @@ public TestController(ILogger logger)
[Topic("run/+user/+count")]
public Task RunAsync(
string user,
- int count,
+ float count,
CancellationToken cancellationToken)
{
- Logger.LogInformation("Running {User}: {Count} {Payload}", user, count, Encoding.ASCII.GetString(Request.Payload));
+ Logger.LogInformation("Running {User}: {Count} {Payload}", user, count, Encoding.UTF8.GetString(Request.Payload));
cancellationToken.ThrowIfCancellationRequested();
return Task.FromResult(true);
}
diff --git a/Samples/Mqtt.Sample/GlobalUsings.cs b/Samples/Mqtt.Sample/GlobalUsings.cs
index f3b5eaa..477880d 100644
--- a/Samples/Mqtt.Sample/GlobalUsings.cs
+++ b/Samples/Mqtt.Sample/GlobalUsings.cs
@@ -1,3 +1,4 @@
// Global using directives
-global using JetBrains.Annotations;
\ No newline at end of file
+global using JetBrains.Annotations;
+global using Sholo.Mqtt;
diff --git a/Samples/Mqtt.Sample/Program.cs b/Samples/Mqtt.Sample/Program.cs
index 99b8189..e0e7e36 100644
--- a/Samples/Mqtt.Sample/Program.cs
+++ b/Samples/Mqtt.Sample/Program.cs
@@ -6,9 +6,8 @@
using Mqtt.Sample.Services;
using Newtonsoft.Json;
using Newtonsoft.Json.Serialization;
-using Sholo.Mqtt;
-using Sholo.Mqtt.Application.Builder;
using Sholo.Mqtt.Hosting;
+using Sholo.Mqtt.Routing;
using Sholo.Mqtt.TypeConverters.NewtonsoftJson;
namespace Mqtt.Sample;
@@ -38,7 +37,7 @@ await Host.CreateDefaultBuilder(args)
};
});
- services.AddHostedService();
+ services.AddHostedService();
})
.ConfigureMqttHost(app =>
{
diff --git a/Samples/Mqtt.Sample/Services/ClientService.cs b/Samples/Mqtt.Sample/Services/ClientService.cs
new file mode 100644
index 0000000..fcd1200
--- /dev/null
+++ b/Samples/Mqtt.Sample/Services/ClientService.cs
@@ -0,0 +1,121 @@
+using Microsoft.Extensions.Hosting;
+using Microsoft.Extensions.Logging;
+using MQTTnet;
+using MQTTnet.Extensions.ManagedClient;
+using MQTTnet.Protocol;
+
+namespace Mqtt.Sample.Services;
+
+internal sealed class ClientService : BackgroundService
+{
+ private SortedDictionary Items { get; }
+ private IManagedMqttClient MqttClient { get; }
+ private ILogger Logger { get; }
+
+ public ClientService(
+ IManagedMqttClient mqttClient,
+ ILogger logger
+ )
+ {
+ MqttClient = mqttClient;
+ Logger = logger;
+
+ Items = new SortedDictionary(Comparer.Create((a, b) => char.ToLowerInvariant(a).CompareTo(char.ToLowerInvariant(b))))
+ {
+ ['0'] = new("Send a message", ct => SendMessage(
+ b => b
+ .WithTopic("test/run/scott/26.2")
+ .WithQualityOfServiceLevel(MqttQualityOfServiceLevel.AtMostOnce)
+ .WithPayload("this is a test")
+ .WithRetainFlag(false),
+ ct
+ )),
+ ['1'] = new("Send a message", ct => SendMessage(
+ b => b
+ .WithTopic("test/run/scott/25/2")
+ .WithQualityOfServiceLevel(MqttQualityOfServiceLevel.AtMostOnce)
+ .WithPayload("{\"hello\":\"world\",\"test\":\"123\"}"u8.ToArray())
+ .WithRetainFlag(false),
+ ct
+ )),
+ };
+ }
+
+ protected override async Task ExecuteAsync(CancellationToken stoppingToken)
+ {
+ using var cts = CancellationTokenSource.CreateLinkedTokenSource(stoppingToken);
+
+ async Task Quit(CancellationTokenSource c)
+ {
+ await c.CancelAsync();
+ }
+
+ await WriteMenu(false);
+
+ var cancellationToken = cts.Token;
+ while (!cancellationToken.IsCancellationRequested)
+ {
+ Console.WriteLine("Choose an option (? for menu, q to quit):");
+ var c = Console.ReadKey(true);
+ var lc = char.ToLowerInvariant(c.KeyChar);
+
+ var task = lc switch
+ {
+ '?' => WriteMenu(false),
+ 'q' => Quit(cts),
+ _ => Items.TryGetValue(lc, out var item) ? item.Action.Invoke(cancellationToken) : WriteMenu(true)
+ };
+
+ await task;
+ }
+ }
+
+ private async Task SendMessage(Action configuration, CancellationToken cancellationToken)
+ {
+ Logger.LogInformation("Sending a message");
+
+ var mqttApplicationMessageBuilder = new MqttApplicationMessageBuilder();
+
+ configuration.Invoke(mqttApplicationMessageBuilder);
+
+ var mqttApplicationMessage = mqttApplicationMessageBuilder.Build();
+
+ await MqttClient.InternalClient.PublishAsync(mqttApplicationMessage, cancellationToken);
+ }
+
+ private Task WriteMenu(bool showErrorMessage)
+ {
+ if (showErrorMessage)
+ {
+ Console.WriteLine("Invalid option");
+ Console.WriteLine();
+ }
+
+ foreach (var (c, item) in Items)
+ {
+ if (c is '?' or 'q')
+ {
+ throw new InvalidOperationException("? and q are reserved");
+ }
+
+ Console.WriteLine($" {c} {item.Description}");
+ }
+
+ Console.WriteLine(" ? Help");
+ Console.WriteLine(" q Quit");
+
+ return Task.CompletedTask;
+ }
+
+ private sealed class MenuItem
+ {
+ public string Description { get; }
+ public Func Action { get; }
+
+ public MenuItem(string description, Func action)
+ {
+ Description = description;
+ Action = action;
+ }
+ }
+}
diff --git a/Samples/Mqtt.Sample/Services/FakeClientService.cs b/Samples/Mqtt.Sample/Services/FakeClientService.cs
deleted file mode 100644
index 56bedf0..0000000
--- a/Samples/Mqtt.Sample/Services/FakeClientService.cs
+++ /dev/null
@@ -1,71 +0,0 @@
-using Microsoft.Extensions.Hosting;
-
-namespace Mqtt.Sample.Services;
-
-internal class FakeClientService : BackgroundService
-{
- private SortedDictionary Items { get; } = new(Comparer.Create((a, b) => char.ToLowerInvariant(a).CompareTo(char.ToLowerInvariant(b))))
- {
- ['c'] = new MenuItem("c", (ct) =>
- {
- Console.WriteLine("you pressed c");
- return Task.CompletedTask;
- })
- };
-
- protected override async Task ExecuteAsync(CancellationToken stoppingToken)
- {
- using var cts = CancellationTokenSource.CreateLinkedTokenSource(stoppingToken);
-
- Task Quit(CancellationTokenSource c)
- {
- c.Cancel();
- return Task.CompletedTask;
- }
-
- await WriteMenu();
-
- while (!cts.Token.IsCancellationRequested)
- {
- Console.WriteLine("Choose an option (? for menu, q to quit):");
- var c = Console.ReadKey(true);
- var lc = char.ToLowerInvariant(c.KeyChar);
-
- var task = lc switch
- {
- '?' => WriteMenu(),
- 'q' => Quit(cts),
- _ => WriteMenu()
- };
-
- await task;
- }
- }
-
- private Task Handle(char c)
- {
-
- }
-
- private Task WriteMenu()
- {
- foreach (var (c, item) in Items)
- {
- Console.WriteLine($" {c} {item.Description}");
- }
-
- return Task.CompletedTask;
- }
-
- public class MenuItem
- {
- public string Description { get; }
- public Func Action { get; }
-
- public MenuItem(string description, Func action)
- {
- Description = description;
- Action = action;
- }
- }
-}
diff --git a/Samples/Mqtt.Sample/appsettings.json b/Samples/Mqtt.Sample/appsettings.json
index 2dca383..65b8596 100644
--- a/Samples/Mqtt.Sample/appsettings.json
+++ b/Samples/Mqtt.Sample/appsettings.json
@@ -1,6 +1,6 @@
{
"mqtt": {
- "host": "mosquitto",
+ "host": "localhost",
"port": 1883
}
}
diff --git a/Source/Directory.Build.props b/Source/Directory.Build.props
index d50ab92..7f08f66 100644
--- a/Source/Directory.Build.props
+++ b/Source/Directory.Build.props
@@ -23,7 +23,7 @@
-
+
diff --git a/Source/Sholo.Mqtt.Sandbox/Topics/PatternPropertyConfigurationBuilder/MqttTopicPatternPropertyConfigurationBuilder.cs b/Source/Sholo.Mqtt.Sandbox/Topics/PatternPropertyConfigurationBuilder/MqttTopicPatternPropertyConfigurationBuilder.cs
index 92a8c14..b41255d 100644
--- a/Source/Sholo.Mqtt.Sandbox/Topics/PatternPropertyConfigurationBuilder/MqttTopicPatternPropertyConfigurationBuilder.cs
+++ b/Source/Sholo.Mqtt.Sandbox/Topics/PatternPropertyConfigurationBuilder/MqttTopicPatternPropertyConfigurationBuilder.cs
@@ -16,7 +16,7 @@ public MqttTopicPatternPropertyConfigurationBuilder(string initialParameterName,
ParameterName = initialParameterName;
ValueSetter = valueSetter;
- if (DefaultTypeConverters.TryGetStringTypeConverter(parameterType, out var typeConverter))
+ if (DefaultTypeConverter.TryGetStringTypeConverter(parameterType, out var typeConverter))
{
TypeConverter = typeConverter!;
}
diff --git a/Source/Sholo.Mqtt.TypeConverters.NewtonsoftJson/NewtonsoftJsonPayloadTypeConverter.cs b/Source/Sholo.Mqtt.TypeConverters.NewtonsoftJson/NewtonsoftJsonPayloadTypeConverter.cs
index af6bd57..5fb9704 100644
--- a/Source/Sholo.Mqtt.TypeConverters.NewtonsoftJson/NewtonsoftJsonPayloadTypeConverter.cs
+++ b/Source/Sholo.Mqtt.TypeConverters.NewtonsoftJson/NewtonsoftJsonPayloadTypeConverter.cs
@@ -25,7 +25,7 @@ ILogger logger
Serializer = JsonSerializer.Create(options.Value.SerializerSettings);
}
- public bool TryConvertPayload(ArraySegment payloadData, Type targetType, out object result) => TryConvert(payloadData, targetType, out result);
+ public bool TryConvertPayload(ArraySegment payload, Type targetType, out object result) => TryConvert(payload, targetType, out result);
private bool TryConvert(ArraySegment data, Type targetType, out object result)
{
diff --git a/Source/Sholo.Mqtt/Application/Builder/MqttApplicationBuilder.cs b/Source/Sholo.Mqtt/Application/Builder/MqttApplicationBuilder.cs
index acfc05c..b0fced8 100644
--- a/Source/Sholo.Mqtt/Application/Builder/MqttApplicationBuilder.cs
+++ b/Source/Sholo.Mqtt/Application/Builder/MqttApplicationBuilder.cs
@@ -1,6 +1,5 @@
using System;
using System.Collections.Generic;
-using System.Linq;
using System.Threading.Tasks;
using Microsoft.Extensions.DependencyInjection;
using Sholo.Mqtt.Internal;
@@ -50,7 +49,7 @@ public IMqttApplication Build()
var routeProvider = ApplicationServices.GetRequiredService();
- var topicFilters = routeProvider.Endpoints.Select(x => x.TopicFilter);
+ var topicFilters = routeProvider.TopicFilters;
return new MqttApplication(topicFilters, app);
}
diff --git a/Source/Sholo.Mqtt/Application/Provider/MqttApplicationProvider.cs b/Source/Sholo.Mqtt/Application/Provider/MqttApplicationProvider.cs
index 1010e15..e0307ee 100644
--- a/Source/Sholo.Mqtt/Application/Provider/MqttApplicationProvider.cs
+++ b/Source/Sholo.Mqtt/Application/Provider/MqttApplicationProvider.cs
@@ -17,7 +17,7 @@ public IMqttApplication? Current
get => _current;
private set
{
- var previous = _current ?? throw new InvalidOperationException();
+ var previous = _current;
_current = value;
OnApplicationChanged(previous, _current!);
diff --git a/Source/Sholo.Mqtt/Endpoint.cs b/Source/Sholo.Mqtt/Endpoint.cs
index fd8a0e5..cd1a962 100644
--- a/Source/Sholo.Mqtt/Endpoint.cs
+++ b/Source/Sholo.Mqtt/Endpoint.cs
@@ -1,58 +1,26 @@
using System.Reflection;
-using System.Threading;
+using Sholo.Mqtt.ModelBinding;
using Sholo.Mqtt.Topics.Filter;
namespace Sholo.Mqtt;
[PublicAPI]
-public class Endpoint
+public class Endpoint : IMqttModelBindingContext
{
+ public TypeInfo? Instance { get; }
public MethodInfo Action { get; }
public IMqttTopicFilter TopicFilter { get; }
public MqttRequestDelegate RequestDelegate { get; }
public Endpoint(
+ TypeInfo? instance,
MethodInfo action,
- IMqttTopicFilter topicPatternFilter,
+ IMqttTopicFilter topicFilter,
MqttRequestDelegate requestDelegate)
{
+ Instance = instance;
Action = action;
- TopicFilter = topicPatternFilter;
+ TopicFilter = topicFilter;
RequestDelegate = requestDelegate;
}
-
- public bool IsMatch(IMqttRequestContext context)
- {
- if (TopicFilter.IsMatch(context, out var topicArguments) && TopicFilter.QualityOfServiceLevel == context.QualityOfServiceLevel)
- {
- var actionParameters = Action.GetParameters();
- var requiredArguments = actionParameters.Length;
-
- foreach (var actionParameter in actionParameters)
- {
- var parameterName = actionParameter.Name!;
-
- if (topicArguments?.TryGetValue(parameterName, out _) ?? false)
- {
- requiredArguments--;
- }
- else if (actionParameter.ParameterType == typeof(CancellationToken))
- {
- requiredArguments--;
- }
- else if (context.ServiceProvider.GetService(actionParameter.ParameterType) != null)
- {
- requiredArguments--;
- }
-
- // TODO: Better handling for when the request has a model (was break instead of continue above, requiredArguments == 0)
- if (requiredArguments <= 1)
- {
- return true;
- }
- }
- }
-
- return false;
- }
}
diff --git a/Source/Sholo.Mqtt/IMqttRequestContext.cs b/Source/Sholo.Mqtt/IMqttRequestContext.cs
index 45bd5ba..8ff6940 100644
--- a/Source/Sholo.Mqtt/IMqttRequestContext.cs
+++ b/Source/Sholo.Mqtt/IMqttRequestContext.cs
@@ -1,11 +1,13 @@
using System;
+using System.Collections.Generic;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.Extensions.Hosting;
+using Microsoft.Extensions.Primitives;
using MQTTnet;
using MQTTnet.Extensions.ManagedClient;
-using MQTTnet.Packets;
using MQTTnet.Protocol;
+using Sholo.Mqtt.ModelBinding;
namespace Sholo.Mqtt;
@@ -68,14 +70,14 @@ public interface IMqttRequestContext
/// 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
+ /// 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.
///
///
/// MQTT 5 feature only.
///
- MqttUserProperty[] UserProperties { get; }
+ IReadOnlyDictionary UserProperties { get; }
///
/// Gets the content type.
@@ -176,6 +178,15 @@ public interface IMqttRequestContext
///
CancellationToken ShutdownToken { get; }
+ ///
+ /// Gets or sets the for the current request.
+ ///
+ ///
+ /// indicates that all action parameters were bound & validated and that the
+ /// action can be invoked
+ ///
+ IMqttModelBindingResult? ModelBindingResult { get; set; }
+
///
/// Publishes a message using the connection on which this message was received
///
diff --git a/Source/Sholo.Mqtt/IRouteProvider.cs b/Source/Sholo.Mqtt/IRouteProvider.cs
index d0ca495..e2a5c83 100644
--- a/Source/Sholo.Mqtt/IRouteProvider.cs
+++ b/Source/Sholo.Mqtt/IRouteProvider.cs
@@ -1,8 +1,10 @@
+using Sholo.Mqtt.Topics.Filter;
+
namespace Sholo.Mqtt;
public interface IRouteProvider
{
- Endpoint[] Endpoints { get; }
+ IMqttTopicFilter[] TopicFilters { get; }
- Endpoint? GetEndpoint(IMqttRequestContext context);
+ Endpoint? GetEndpoint(IMqttRequestContext requestContext);
}
diff --git a/Source/Sholo.Mqtt/Internal/DefaultControllerActivator.cs b/Source/Sholo.Mqtt/Internal/DefaultMqttControllerActivator.cs
similarity index 74%
rename from Source/Sholo.Mqtt/Internal/DefaultControllerActivator.cs
rename to Source/Sholo.Mqtt/Internal/DefaultMqttControllerActivator.cs
index add4464..c65dd03 100644
--- a/Source/Sholo.Mqtt/Internal/DefaultControllerActivator.cs
+++ b/Source/Sholo.Mqtt/Internal/DefaultMqttControllerActivator.cs
@@ -7,23 +7,23 @@
namespace Sholo.Mqtt.Internal;
///
-/// that uses type activation to create controllers.
+/// that uses type activation to create controllers.
///
-internal class DefaultControllerActivator : IControllerActivator
+internal class DefaultMqttControllerActivator : IMqttControllerActivator
{
private readonly ITypeActivatorCache _typeActivatorCache;
- public DefaultControllerActivator(ITypeActivatorCache typeActivatorCache)
+ public DefaultMqttControllerActivator(ITypeActivatorCache typeActivatorCache)
{
_typeActivatorCache = typeActivatorCache ?? throw new ArgumentNullException(nameof(typeActivatorCache));
}
///
- public object Create(IMqttRequestContext controllerContext, Type controllerType)
+ public object Create(IMqttRequestContext requestContext, Type controllerType)
{
- ArgumentNullException.ThrowIfNull(controllerContext, nameof(controllerContext));
+ ArgumentNullException.ThrowIfNull(requestContext, nameof(requestContext));
- var serviceProvider = controllerContext.ServiceProvider;
+ var serviceProvider = requestContext.ServiceProvider;
return _typeActivatorCache.CreateInstance