From 3334bac7290fe74e150598f7216b3cc0e58a398f Mon Sep 17 00:00:00 2001 From: Scott Holodak Date: Mon, 18 Dec 2023 11:55:36 -0500 Subject: [PATCH] WiP --- .vscode/launch.json | 6 +- Directory.Build.props | 6 +- Samples/Directory.Build.props | 2 +- .../Mqtt.Sample/Controllers/TestController.cs | 4 +- Samples/Mqtt.Sample/GlobalUsings.cs | 3 +- Samples/Mqtt.Sample/Program.cs | 5 +- Samples/Mqtt.Sample/Services/ClientService.cs | 121 +++++++ .../Mqtt.Sample/Services/FakeClientService.cs | 71 ---- Samples/Mqtt.Sample/appsettings.json | 2 +- Source/Directory.Build.props | 2 +- ...opicPatternPropertyConfigurationBuilder.cs | 2 +- .../NewtonsoftJsonPayloadTypeConverter.cs | 2 +- .../Builder/MqttApplicationBuilder.cs | 3 +- .../Provider/MqttApplicationProvider.cs | 2 +- Source/Sholo.Mqtt/Endpoint.cs | 46 +-- Source/Sholo.Mqtt/IMqttRequestContext.cs | 17 +- Source/Sholo.Mqtt/IRouteProvider.cs | 6 +- ...r.cs => DefaultMqttControllerActivator.cs} | 12 +- ...tivator.cs => IMqttControllerActivator.cs} | 6 +- .../MqttApplicationBuilderExtensions.cs | 9 +- .../BindingProviders/IMqttParameterBinder.cs | 17 + .../MqttAttributeParameterBinder.cs | 32 ++ .../MqttCancellationTokenParameterBinder.cs | 26 ++ .../MqttCorrelationDataParameterBinder.cs | 32 ++ .../MqttPayloadParameterBinder.cs | 32 ++ .../MqttRequestContextParameterBinder.cs | 25 ++ .../MqttServiceProviderParameterBinder.cs | 26 ++ .../MqttTopicArgumentParameterBinder.cs | 27 ++ .../MqttTopicFilterParameterBinder.cs | 26 ++ .../MqttTopicParameterBinder.cs | 26 ++ .../ParameterBindingResult.cs | 15 + .../Context/IMqttModelBindingContext.cs | 4 +- .../ModelBinding/IMqttModelBinder.cs | 83 ++--- .../ModelBinding/IMqttModelBindingContext.cs | 42 +-- .../ModelBinding/IMqttModelBindingResult.cs | 44 +++ .../ModelBinding/MqttModelBinder.cs | 327 ++++++++---------- .../ModelBinding/MqttModelBindingContext.cs | 106 +----- .../ModelBinding/MqttModelBindingResult.cs | 125 ++----- .../MqttRequestContextExtensions.cs | 44 +++ .../Sholo.Mqtt/ModelBinding/ParameterState.cs | 145 +++----- .../ModelBinding/ParameterValidationResult.cs | 9 + .../BaseFromMqttConverterAttribute.cs | 56 +++ .../FromMqttCorrelationDataAttribute.cs | 41 ++- .../Attributes/FromMqttPayloadAttribute.cs | 41 ++- .../Attributes/FromMqttTopicAttribute.cs | 41 ++- .../FromMqttUserPropertiesAttribute.cs | 50 +++ .../Attributes/IFromMqttConverter.cs | 9 + ...eConverters.cs => DefaultTypeConverter.cs} | 41 ++- .../IMqttCorrelationDataTypeConverter.cs | 5 +- .../IMqttParameterTypeConverter.cs | 6 - .../IMqttPayloadTypeConverter.cs | 3 +- .../IMqttTopicArgumentTypeConverter.cs | 9 + .../TypeConverters/IMqttTypeConverter.cs | 5 +- .../IMqttUserPropertiesTypeConverter.cs | 11 + .../TypeConverters/Json/JsonTypeConverter.cs | 7 +- .../Json/SnakeCaseNamingPolicy.cs | 4 +- .../LambdaMqttParameterTypeConverter.cs | 35 +- .../MqttValidationStateDictionary.cs | 1 - .../CancellationTokenModelBinder.cs | 4 +- .../IMqttCorrelationDataValueProvider.cs | 2 +- .../IMqttPayloadValueProvider.cs | 4 +- .../ValueProviders/IMqttValueProvider.cs | 3 +- .../MqttCorrelationDataValueProvider.cs | 2 +- .../MqttPayloadValueProvider.cs | 8 +- .../MqttTopicArgumentValueProvider.cs | 13 +- .../MqttUserPropertyValueProvider.cs | 5 +- .../ValueProviders/ServiceValueProvider.cs | 39 --- Source/Sholo.Mqtt/MqttRequestContext.cs | 53 +-- .../MqttRequestContextExtensions.cs | 7 - Source/Sholo.Mqtt/MqttRequestDelegate.cs | 2 + Source/Sholo.Mqtt/RouteProvider.cs | 255 -------------- .../Routing/AnonymousRouteProvider.cs | 64 ++++ .../MqttApplicationBuilderExtensions.cs | 53 +++ Source/Sholo.Mqtt/Routing/RouteProvider.cs | 154 +++++++++ .../RoutingMiddleware.cs | 7 +- .../Sholo.Mqtt/ServiceCollectionExtensions.cs | 21 +- Source/Sholo.Mqtt/Settings/MqttSettings.cs | 9 +- Source/Sholo.Mqtt/Sholo.Mqtt.csproj | 12 +- .../Topics/Filter/IMqttTopicFilter.cs | 1 + .../FilterBuilder/IMqttTopicFilterBuilder.cs | 10 +- .../FilterBuilder/MqttTopicFilterBuilder.cs | 62 +++- .../ComplexTopicPatternMatcher.cs | 47 ++- .../SimpleTopicPatternMatcher.cs | 15 +- .../TopicPatternMatcherFactory.cs | 17 +- .../Utilities/ReadOnlyDictionary.cs | 12 + Source/Sholo.Mqtt/Utilities/ReadOnlySet.cs | 36 ++ Source/Sholo.Mqtt/Utilities/SetExtensions.cs | 9 + .../Sholo.Mqtt/Utilities/ValidationHelper.cs | 10 +- Tests/Directory.Build.props | 2 +- .../DefaultTypeConverterTests.cs | 8 +- .../LambdaMqttParameterTypeConverterTests.cs | 21 +- .../MqttCorrelationDataValueProviderTests.cs | 7 +- .../MqttPayloadValueProviderTests.cs | 8 +- .../MqttTopicArgumentValueProviderTests.cs | 8 +- .../MqttUserPropertyValueProviderTests.cs | 15 +- global.json | 2 +- 96 files changed, 1664 insertions(+), 1248 deletions(-) create mode 100644 Samples/Mqtt.Sample/Services/ClientService.cs delete mode 100644 Samples/Mqtt.Sample/Services/FakeClientService.cs rename Source/Sholo.Mqtt/Internal/{DefaultControllerActivator.cs => DefaultMqttControllerActivator.cs} (74%) rename Source/Sholo.Mqtt/Internal/{IControllerActivator.cs => IMqttControllerActivator.cs} (84%) rename Source/Sholo.Mqtt/{Application/Builder => Middleware}/MqttApplicationBuilderExtensions.cs (80%) create mode 100644 Source/Sholo.Mqtt/ModelBinding/BindingProviders/IMqttParameterBinder.cs create mode 100644 Source/Sholo.Mqtt/ModelBinding/BindingProviders/MqttAttributeParameterBinder.cs create mode 100644 Source/Sholo.Mqtt/ModelBinding/BindingProviders/MqttCancellationTokenParameterBinder.cs create mode 100644 Source/Sholo.Mqtt/ModelBinding/BindingProviders/MqttCorrelationDataParameterBinder.cs create mode 100644 Source/Sholo.Mqtt/ModelBinding/BindingProviders/MqttPayloadParameterBinder.cs create mode 100644 Source/Sholo.Mqtt/ModelBinding/BindingProviders/MqttRequestContextParameterBinder.cs create mode 100644 Source/Sholo.Mqtt/ModelBinding/BindingProviders/MqttServiceProviderParameterBinder.cs create mode 100644 Source/Sholo.Mqtt/ModelBinding/BindingProviders/MqttTopicArgumentParameterBinder.cs create mode 100644 Source/Sholo.Mqtt/ModelBinding/BindingProviders/MqttTopicFilterParameterBinder.cs create mode 100644 Source/Sholo.Mqtt/ModelBinding/BindingProviders/MqttTopicParameterBinder.cs create mode 100644 Source/Sholo.Mqtt/ModelBinding/BindingProviders/ParameterBindingResult.cs create mode 100644 Source/Sholo.Mqtt/ModelBinding/IMqttModelBindingResult.cs create mode 100644 Source/Sholo.Mqtt/ModelBinding/MqttRequestContextExtensions.cs create mode 100644 Source/Sholo.Mqtt/ModelBinding/ParameterValidationResult.cs create mode 100644 Source/Sholo.Mqtt/ModelBinding/TypeConverters/Attributes/BaseFromMqttConverterAttribute.cs create mode 100644 Source/Sholo.Mqtt/ModelBinding/TypeConverters/Attributes/FromMqttUserPropertiesAttribute.cs create mode 100644 Source/Sholo.Mqtt/ModelBinding/TypeConverters/Attributes/IFromMqttConverter.cs rename Source/Sholo.Mqtt/ModelBinding/TypeConverters/{DefaultTypeConverters.cs => DefaultTypeConverter.cs} (89%) delete mode 100644 Source/Sholo.Mqtt/ModelBinding/TypeConverters/IMqttParameterTypeConverter.cs create mode 100644 Source/Sholo.Mqtt/ModelBinding/TypeConverters/IMqttTopicArgumentTypeConverter.cs create mode 100644 Source/Sholo.Mqtt/ModelBinding/TypeConverters/IMqttUserPropertiesTypeConverter.cs delete mode 100644 Source/Sholo.Mqtt/ModelBinding/ValueProviders/ServiceValueProvider.cs delete mode 100644 Source/Sholo.Mqtt/RouteProvider.cs create mode 100644 Source/Sholo.Mqtt/Routing/AnonymousRouteProvider.cs create mode 100644 Source/Sholo.Mqtt/Routing/MqttApplicationBuilderExtensions.cs create mode 100644 Source/Sholo.Mqtt/Routing/RouteProvider.cs rename Source/Sholo.Mqtt/{Middleware => Routing}/RoutingMiddleware.cs (62%) create mode 100644 Source/Sholo.Mqtt/Utilities/ReadOnlyDictionary.cs create mode 100644 Source/Sholo.Mqtt/Utilities/ReadOnlySet.cs create mode 100644 Source/Sholo.Mqtt/Utilities/SetExtensions.cs 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(serviceProvider, controllerType); } diff --git a/Source/Sholo.Mqtt/Internal/IControllerActivator.cs b/Source/Sholo.Mqtt/Internal/IMqttControllerActivator.cs similarity index 84% rename from Source/Sholo.Mqtt/Internal/IControllerActivator.cs rename to Source/Sholo.Mqtt/Internal/IMqttControllerActivator.cs index 5a53aa5..6f42f53 100644 --- a/Source/Sholo.Mqtt/Internal/IControllerActivator.cs +++ b/Source/Sholo.Mqtt/Internal/IMqttControllerActivator.cs @@ -9,15 +9,15 @@ namespace Sholo.Mqtt.Internal; /// /// Provides methods to create a controller. /// -public interface IControllerActivator +public interface IMqttControllerActivator { /// /// Creates a controller. /// - /// The for the executing action. + /// The for the executing action. /// The controller type to create. /// An instance of the controller type specified - object Create(IMqttRequestContext context, Type controllerType); + object Create(IMqttRequestContext requestContext, Type controllerType); /// /// Releases a controller. diff --git a/Source/Sholo.Mqtt/Application/Builder/MqttApplicationBuilderExtensions.cs b/Source/Sholo.Mqtt/Middleware/MqttApplicationBuilderExtensions.cs similarity index 80% rename from Source/Sholo.Mqtt/Application/Builder/MqttApplicationBuilderExtensions.cs rename to Source/Sholo.Mqtt/Middleware/MqttApplicationBuilderExtensions.cs index 2c244e7..68be9b8 100644 --- a/Source/Sholo.Mqtt/Application/Builder/MqttApplicationBuilderExtensions.cs +++ b/Source/Sholo.Mqtt/Middleware/MqttApplicationBuilderExtensions.cs @@ -1,17 +1,12 @@ using System; using Microsoft.Extensions.DependencyInjection; -using Sholo.Mqtt.Middleware; +using Sholo.Mqtt.Application.Builder; -namespace Sholo.Mqtt.Application.Builder; +namespace Sholo.Mqtt.Middleware; [PublicAPI] public static class MqttApplicationBuilderExtensions { - public static IMqttApplicationBuilder UseRouting(this IMqttApplicationBuilder mqttApplicationBuilder) - { - return mqttApplicationBuilder.UseMiddleware(); - } - public static IMqttApplicationBuilder UseMiddleware(this IMqttApplicationBuilder mqttApplicationBuilder) where TMqttMiddleware : IMqttMiddleware { diff --git a/Source/Sholo.Mqtt/ModelBinding/BindingProviders/IMqttParameterBinder.cs b/Source/Sholo.Mqtt/ModelBinding/BindingProviders/IMqttParameterBinder.cs new file mode 100644 index 0000000..6bbb8c7 --- /dev/null +++ b/Source/Sholo.Mqtt/ModelBinding/BindingProviders/IMqttParameterBinder.cs @@ -0,0 +1,17 @@ +using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; +using Microsoft.Extensions.Primitives; + +namespace Sholo.Mqtt.ModelBinding.BindingProviders; + +[PublicAPI] +public interface IMqttParameterBinder +{ + bool TryBind( + IMqttModelBindingContext modelBindingContext, + IMqttRequestContext requestContext, + IReadOnlyDictionary topicArguments, + ParameterState parameterState, + [MaybeNullWhen(false)] out ParameterBindingResult result + ); +} diff --git a/Source/Sholo.Mqtt/ModelBinding/BindingProviders/MqttAttributeParameterBinder.cs b/Source/Sholo.Mqtt/ModelBinding/BindingProviders/MqttAttributeParameterBinder.cs new file mode 100644 index 0000000..345f123 --- /dev/null +++ b/Source/Sholo.Mqtt/ModelBinding/BindingProviders/MqttAttributeParameterBinder.cs @@ -0,0 +1,32 @@ +using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; +using System.Linq; +using Microsoft.Extensions.Primitives; +using Sholo.Mqtt.ModelBinding.TypeConverters.Attributes; + +namespace Sholo.Mqtt.ModelBinding.BindingProviders; + +public class MqttAttributeParameterBinder : IMqttParameterBinder +{ + public bool TryBind( + IMqttModelBindingContext modelBindingContext, + IMqttRequestContext requestContext, + IReadOnlyDictionary topicArguments, + ParameterState parameterState, + [MaybeNullWhen(false)] out ParameterBindingResult result) + { + var fromMqttConverters = parameterState.ParameterInfo.GetCustomAttributes(false).OfType(); + + foreach (var fromMqttConverter in fromMqttConverters) + { + if (fromMqttConverter.TryBind(requestContext, parameterState, out var resultObj)) + { + result = new ParameterBindingResult(fromMqttConverter.BindingSource, resultObj); + return true; + } + } + + result = null; + return false; + } +} diff --git a/Source/Sholo.Mqtt/ModelBinding/BindingProviders/MqttCancellationTokenParameterBinder.cs b/Source/Sholo.Mqtt/ModelBinding/BindingProviders/MqttCancellationTokenParameterBinder.cs new file mode 100644 index 0000000..c807aa1 --- /dev/null +++ b/Source/Sholo.Mqtt/ModelBinding/BindingProviders/MqttCancellationTokenParameterBinder.cs @@ -0,0 +1,26 @@ +using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; +using System.Threading; +using Microsoft.Extensions.Primitives; + +namespace Sholo.Mqtt.ModelBinding.BindingProviders; + +public class MqttCancellationTokenParameterBinder : IMqttParameterBinder +{ + public bool TryBind( + IMqttModelBindingContext modelBindingContext, + IMqttRequestContext requestContext, + IReadOnlyDictionary topicArguments, + ParameterState parameterState, + [MaybeNullWhen(false)] out ParameterBindingResult result) + { + if (parameterState.TargetType == typeof(CancellationToken)) + { + result = new ParameterBindingResult(MqttBindingSource.Context, requestContext.ShutdownToken, bypassValidation: true); + return true; + } + + result = null; + return false; + } +} diff --git a/Source/Sholo.Mqtt/ModelBinding/BindingProviders/MqttCorrelationDataParameterBinder.cs b/Source/Sholo.Mqtt/ModelBinding/BindingProviders/MqttCorrelationDataParameterBinder.cs new file mode 100644 index 0000000..39bde41 --- /dev/null +++ b/Source/Sholo.Mqtt/ModelBinding/BindingProviders/MqttCorrelationDataParameterBinder.cs @@ -0,0 +1,32 @@ +using System; +using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; +using Microsoft.Extensions.Primitives; + +namespace Sholo.Mqtt.ModelBinding.BindingProviders; + +public class MqttCorrelationDataParameterBinder : IMqttParameterBinder +{ + public bool TryBind( + IMqttModelBindingContext modelBindingContext, + IMqttRequestContext requestContext, + IReadOnlyDictionary topicArguments, + ParameterState parameterState, + [MaybeNullWhen(false)] out ParameterBindingResult result) + { + if (parameterState.TargetType == typeof(byte[]) && parameterState.ParameterName.Equals("correlationData", StringComparison.Ordinal)) + { + result = new ParameterBindingResult(MqttBindingSource.CorrelationData, requestContext.CorrelationData); + return true; + } + + if (parameterState.TargetType == typeof(ArraySegment) && parameterState.ParameterName.Equals("correlationData", StringComparison.Ordinal)) + { + result = new ParameterBindingResult(MqttBindingSource.CorrelationData, requestContext.CorrelationData); + return true; + } + + result = null; + return false; + } +} diff --git a/Source/Sholo.Mqtt/ModelBinding/BindingProviders/MqttPayloadParameterBinder.cs b/Source/Sholo.Mqtt/ModelBinding/BindingProviders/MqttPayloadParameterBinder.cs new file mode 100644 index 0000000..7662210 --- /dev/null +++ b/Source/Sholo.Mqtt/ModelBinding/BindingProviders/MqttPayloadParameterBinder.cs @@ -0,0 +1,32 @@ +using System; +using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; +using Microsoft.Extensions.Primitives; + +namespace Sholo.Mqtt.ModelBinding.BindingProviders; + +public class MqttPayloadParameterBinder : IMqttParameterBinder +{ + public bool TryBind( + IMqttModelBindingContext modelBindingContext, + IMqttRequestContext requestContext, + IReadOnlyDictionary topicArguments, + ParameterState parameterState, + [MaybeNullWhen(false)] out ParameterBindingResult result) + { + if (parameterState.TargetType == typeof(byte[]) && parameterState.ParameterName.Equals("payload", StringComparison.Ordinal)) + { + result = new ParameterBindingResult(MqttBindingSource.Payload, requestContext.Payload.ToArray()); + return true; + } + + if (parameterState.TargetType == typeof(ArraySegment) && parameterState.ParameterName.Equals("payload", StringComparison.Ordinal)) + { + result = new ParameterBindingResult(MqttBindingSource.Payload, requestContext.Payload); + return true; + } + + result = null; + return false; + } +} diff --git a/Source/Sholo.Mqtt/ModelBinding/BindingProviders/MqttRequestContextParameterBinder.cs b/Source/Sholo.Mqtt/ModelBinding/BindingProviders/MqttRequestContextParameterBinder.cs new file mode 100644 index 0000000..247aa3b --- /dev/null +++ b/Source/Sholo.Mqtt/ModelBinding/BindingProviders/MqttRequestContextParameterBinder.cs @@ -0,0 +1,25 @@ +using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; +using Microsoft.Extensions.Primitives; + +namespace Sholo.Mqtt.ModelBinding.BindingProviders; + +public class MqttRequestContextParameterBinder : IMqttParameterBinder +{ + public bool TryBind( + IMqttModelBindingContext modelBindingContext, + IMqttRequestContext requestContext, + IReadOnlyDictionary topicArguments, + ParameterState parameterState, + [MaybeNullWhen(false)] out ParameterBindingResult result) + { + if (parameterState.TargetType == typeof(IMqttRequestContext)) + { + result = new ParameterBindingResult(MqttBindingSource.Context, requestContext, bypassValidation: true); + return true; + } + + result = null; + return false; + } +} diff --git a/Source/Sholo.Mqtt/ModelBinding/BindingProviders/MqttServiceProviderParameterBinder.cs b/Source/Sholo.Mqtt/ModelBinding/BindingProviders/MqttServiceProviderParameterBinder.cs new file mode 100644 index 0000000..31eaad7 --- /dev/null +++ b/Source/Sholo.Mqtt/ModelBinding/BindingProviders/MqttServiceProviderParameterBinder.cs @@ -0,0 +1,26 @@ +using System; +using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; +using Microsoft.Extensions.Primitives; + +namespace Sholo.Mqtt.ModelBinding.BindingProviders; + +public class MqttServiceProviderParameterBinder : IMqttParameterBinder +{ + public bool TryBind( + IMqttModelBindingContext modelBindingContext, + IMqttRequestContext requestContext, + IReadOnlyDictionary topicArguments, + ParameterState parameterState, + [MaybeNullWhen(false)] out ParameterBindingResult result) + { + if (parameterState.TargetType == typeof(IServiceProvider)) + { + result = new ParameterBindingResult(MqttBindingSource.Context, requestContext.ServiceProvider, bypassValidation: true); + return true; + } + + result = null; + return false; + } +} diff --git a/Source/Sholo.Mqtt/ModelBinding/BindingProviders/MqttTopicArgumentParameterBinder.cs b/Source/Sholo.Mqtt/ModelBinding/BindingProviders/MqttTopicArgumentParameterBinder.cs new file mode 100644 index 0000000..a37b7a8 --- /dev/null +++ b/Source/Sholo.Mqtt/ModelBinding/BindingProviders/MqttTopicArgumentParameterBinder.cs @@ -0,0 +1,27 @@ +using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; +using Microsoft.Extensions.Primitives; +using Sholo.Mqtt.ModelBinding.TypeConverters; + +namespace Sholo.Mqtt.ModelBinding.BindingProviders; + +public class MqttTopicArgumentParameterBinder : IMqttParameterBinder +{ + public bool TryBind( + IMqttModelBindingContext modelBindingContext, + IMqttRequestContext requestContext, + IReadOnlyDictionary topicArguments, + ParameterState parameterState, + [MaybeNullWhen(false)] out ParameterBindingResult result) + { + if (topicArguments.TryGetValue(parameterState.ParameterName, out var stringValues) && + DefaultTypeConverter.Instance.TryConvertTopicArgument(stringValues!, parameterState.ParameterInfo.ParameterType, out var resultObj)) + { + result = new ParameterBindingResult(MqttBindingSource.Topic, resultObj); + return true; + } + + result = null; + return false; + } +} diff --git a/Source/Sholo.Mqtt/ModelBinding/BindingProviders/MqttTopicFilterParameterBinder.cs b/Source/Sholo.Mqtt/ModelBinding/BindingProviders/MqttTopicFilterParameterBinder.cs new file mode 100644 index 0000000..6cf61bc --- /dev/null +++ b/Source/Sholo.Mqtt/ModelBinding/BindingProviders/MqttTopicFilterParameterBinder.cs @@ -0,0 +1,26 @@ +using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; +using Microsoft.Extensions.Primitives; +using Sholo.Mqtt.Topics.Filter; + +namespace Sholo.Mqtt.ModelBinding.BindingProviders; + +public class MqttTopicFilterParameterBinder : IMqttParameterBinder +{ + public bool TryBind( + IMqttModelBindingContext modelBindingContext, + IMqttRequestContext requestContext, + IReadOnlyDictionary topicArguments, + ParameterState parameterState, + [MaybeNullWhen(false)] out ParameterBindingResult result) + { + if (parameterState.TargetType == typeof(IMqttTopicFilter)) + { + result = new ParameterBindingResult(MqttBindingSource.Context, modelBindingContext.TopicFilter, bypassValidation: true); + return true; + } + + result = null; + return false; + } +} diff --git a/Source/Sholo.Mqtt/ModelBinding/BindingProviders/MqttTopicParameterBinder.cs b/Source/Sholo.Mqtt/ModelBinding/BindingProviders/MqttTopicParameterBinder.cs new file mode 100644 index 0000000..508565e --- /dev/null +++ b/Source/Sholo.Mqtt/ModelBinding/BindingProviders/MqttTopicParameterBinder.cs @@ -0,0 +1,26 @@ +using System; +using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; +using Microsoft.Extensions.Primitives; + +namespace Sholo.Mqtt.ModelBinding.BindingProviders; + +public class MqttTopicParameterBinder : IMqttParameterBinder +{ + public bool TryBind( + IMqttModelBindingContext modelBindingContext, + IMqttRequestContext requestContext, + IReadOnlyDictionary topicArguments, + ParameterState parameterState, + [MaybeNullWhen(false)] out ParameterBindingResult result) + { + if (parameterState.TargetType == typeof(string) && parameterState.ParameterName.Equals("topic", StringComparison.Ordinal)) + { + result = new ParameterBindingResult(MqttBindingSource.Topic, requestContext.Topic); + return true; + } + + result = null; + return false; + } +} diff --git a/Source/Sholo.Mqtt/ModelBinding/BindingProviders/ParameterBindingResult.cs b/Source/Sholo.Mqtt/ModelBinding/BindingProviders/ParameterBindingResult.cs new file mode 100644 index 0000000..32b0b20 --- /dev/null +++ b/Source/Sholo.Mqtt/ModelBinding/BindingProviders/ParameterBindingResult.cs @@ -0,0 +1,15 @@ +namespace Sholo.Mqtt.ModelBinding.BindingProviders; + +public class ParameterBindingResult +{ + public MqttBindingSource BindingSource { get; } + public object? Value { get; } + public bool BypassValidation { get; } + + public ParameterBindingResult(MqttBindingSource bindingSource, object? value, bool bypassValidation = false) + { + BindingSource = bindingSource; + Value = value; + BypassValidation = bypassValidation; + } +} diff --git a/Source/Sholo.Mqtt/ModelBinding/Context/IMqttModelBindingContext.cs b/Source/Sholo.Mqtt/ModelBinding/Context/IMqttModelBindingContext.cs index 8fd0e28..7089902 100644 --- a/Source/Sholo.Mqtt/ModelBinding/Context/IMqttModelBindingContext.cs +++ b/Source/Sholo.Mqtt/ModelBinding/Context/IMqttModelBindingContext.cs @@ -11,7 +11,7 @@ public interface IMqttModelBindingContext // Parameters binding IReadOnlyDictionary TopicArguments { get; } - IMqttParameterTypeConverter[] ParameterTypeConverters { get; } + IMqttUserPropertiesTypeConverter[] ParameterTypeConverters { get; } // Correlation Data IMqttCorrelationDataTypeConverter CorrelationDataTypeConverter { get; } @@ -19,6 +19,6 @@ public interface IMqttModelBindingContext // Payload binding IMqttPayloadTypeConverter PayloadTypeConverter { get; } - bool TryConvertParameter(string? input, IMqttParameterTypeConverter? explicitParameterTypeConverter, ParameterInfo actionParameter, Type targetType, out object? result); + bool TryConvertUserProperties(string? input, IMqttUserPropertiesTypeConverter? explicitParameterTypeConverter, ParameterInfo actionParameter, Type targetType, out object? result); } */ diff --git a/Source/Sholo.Mqtt/ModelBinding/IMqttModelBinder.cs b/Source/Sholo.Mqtt/ModelBinding/IMqttModelBinder.cs index ea9f595..1b3b092 100644 --- a/Source/Sholo.Mqtt/ModelBinding/IMqttModelBinder.cs +++ b/Source/Sholo.Mqtt/ModelBinding/IMqttModelBinder.cs @@ -1,73 +1,30 @@ using System.Collections.Generic; -using System.Diagnostics.CodeAnalysis; -using System.Reflection; -using System.Threading.Tasks; -using Sholo.Mqtt.Topics.Filter; +using Microsoft.Extensions.Primitives; namespace Sholo.Mqtt.ModelBinding; -/* -public interface IMqttModelBinder -{ - bool CanBind(Type targetType); -} - -// ReSharper disable once UnusedTypeParameter - Used to disambiguate in DI -public interface IMqttModelBinder : IMqttModelBinder -{ -} - -public interface IMqttModelBinder : IMqttModelBinder -{ - bool TryGetValue(TSource source, out TTarget target); -} -*/ - public interface IMqttModelBinder { /// - /// Attempts to bind a model. + /// Attempts to bind the request details to the matched action method's arguments. The results + /// of the binding operation are stored in the 's + /// property."/> /// - /// The . - /// - /// - /// A which will complete when the model binding process completes. - /// - /// - /// If model binding was successful, the should have - /// set to true. - /// - /// - /// A model binder that completes successfully should set to - /// a value returned from . - /// - /// - Task BindModelAsync(IMqttModelBindingContext bindingContext); - bool TryPerformModelBinding( + /// + /// The associated with the endpoint or action. + /// + /// + /// An associated with the incoming request + /// + /// + /// An containing the arguments + /// extracted from the incoming using the + /// 's + /// topic filter during the route matching operation. + /// + void TryPerformModelBinding( + IMqttModelBindingContext modelBindingContext, IMqttRequestContext requestContext, - IMqttTopicFilter topicPatternFilter, - MethodInfo action, - [MaybeNullWhen(false)] out IDictionary actionArguments); -} - -public class CancellationTokenModelBinder : IMqttModelBinder -{ - public Task BindModelAsync(IMqttModelBindingContext bindingContext) - { - bindingContext.Result = MqttModelBindingResult.Success(bindingContext.Request.ShutdownToken); - throw new System.NotImplementedException(); - } - - public bool TryPerformModelBinding(IMqttRequestContext requestContext, IMqttTopicFilter topicPatternFilter, MethodInfo action, out IDictionary actionArguments) - { - throw new System.NotImplementedException(); - } -} - -public class ServicesMqttModelBinder : IMqttModelBinder -{ - public bool TryPerformModelBinding(IMqttRequestContext requestContext, IMqttTopicFilter topicPatternFilter, MethodInfo action, out IDictionary actionArguments) - { - throw new System.NotImplementedException(); - } + IReadOnlyDictionary topicArguments + ); } diff --git a/Source/Sholo.Mqtt/ModelBinding/IMqttModelBindingContext.cs b/Source/Sholo.Mqtt/ModelBinding/IMqttModelBindingContext.cs index 0119193..5f7be76 100644 --- a/Source/Sholo.Mqtt/ModelBinding/IMqttModelBindingContext.cs +++ b/Source/Sholo.Mqtt/ModelBinding/IMqttModelBindingContext.cs @@ -1,12 +1,7 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. -using System; -using System.Collections.Generic; using System.Reflection; -using Microsoft.Extensions.Primitives; -using Sholo.Mqtt.ModelBinding.TypeConverters; -using Sholo.Mqtt.ModelBinding.Validation; using Sholo.Mqtt.Topics.Filter; namespace Sholo.Mqtt.ModelBinding; @@ -18,19 +13,8 @@ namespace Sholo.Mqtt.ModelBinding; /// This is largely copy-and-paste from https://github.com/dotnet/aspnetcore /// [PublicAPI] -public interface IMqttModelBindingContext : IServiceProvider +public interface IMqttModelBindingContext { - /// - /// Gets a value which represents the associated with the - /// request - /// - MqttBindingSource? BindingSource { get; } - - /// - /// Gets the associated with this context. - /// - IMqttRequestContext Request { get; } - /// /// Gets a value which represents the associated with the /// request @@ -38,31 +22,13 @@ public interface IMqttModelBindingContext : IServiceProvider IMqttTopicFilter TopicFilter { get; } /// - /// Gets a value which represents the arguments extracted from the + /// Gets the object which contains the method in the . If the action is + /// anonymous, this will be null. /// - IReadOnlyDictionary TopicArguments { get; } + TypeInfo? Instance { get; } /// /// Gets the associated with the request handler (Controller action, , etc.) /// MethodInfo Action { get; } - - /// - /// Gets a dictionary containing the values to bind to the 's parameters. - /// Configuring the values of this dictionary is the responsibility of s, - /// the result of which is used by s before executing the - /// . - /// - IReadOnlyDictionary ActionArguments { get; } - - // Parameters binding - IMqttParameterTypeConverter[] ParameterTypeConverters { get; } - - // Correlation Data - IMqttCorrelationDataTypeConverter CorrelationDataTypeConverter { get; } - - // Payload binding - IMqttPayloadTypeConverter PayloadTypeConverter { get; } - - bool TryConvertParameter(string? input, IMqttParameterTypeConverter? explicitParameterTypeConverter, ParameterInfo actionParameter, Type targetType, out object? result); } diff --git a/Source/Sholo.Mqtt/ModelBinding/IMqttModelBindingResult.cs b/Source/Sholo.Mqtt/ModelBinding/IMqttModelBindingResult.cs new file mode 100644 index 0000000..bddfe46 --- /dev/null +++ b/Source/Sholo.Mqtt/ModelBinding/IMqttModelBindingResult.cs @@ -0,0 +1,44 @@ +using System.Collections.Generic; +using System.Reflection; +using System.Threading.Tasks; +using Sholo.Mqtt.ModelBinding.Validation; +using Sholo.Mqtt.Topics.Filter; + +namespace Sholo.Mqtt.ModelBinding; + +[PublicAPI] +public interface IMqttModelBindingResult +{ + /// + /// Gets a value which represents the associated with the + /// request + /// + IMqttTopicFilter TopicFilter { get; } + + /// + /// Gets the object which contains the method in the . If the action is + /// anonymous, this will be null. + /// + TypeInfo? Instance { get; } + + /// + /// Gets the associated with the request handler (Controller action, , etc.) + /// + MethodInfo Action { get; } + + /// + /// Gets a dictionary containing the values to bind to the 's parameters. + /// Configuring the values of this dictionary is the responsibility of s, + /// the result of which is used by s before executing the + /// . + /// + IReadOnlyDictionary ActionArguments { get; } + + /// + /// Gets a value indicating whether all parameters of the action were successfully bound + /// and validated. The action will only execute if this returns true. + /// + bool Success { get; } + + Task Invoke(); +} diff --git a/Source/Sholo.Mqtt/ModelBinding/MqttModelBinder.cs b/Source/Sholo.Mqtt/ModelBinding/MqttModelBinder.cs index 76cd5a2..c332fc9 100644 --- a/Source/Sholo.Mqtt/ModelBinding/MqttModelBinder.cs +++ b/Source/Sholo.Mqtt/ModelBinding/MqttModelBinder.cs @@ -1,131 +1,180 @@ -using System; using System.Collections.Generic; -using System.Diagnostics.CodeAnalysis; using System.Linq; -using System.Reflection; -using System.Threading; -using Microsoft.Extensions.DependencyInjection; -using Microsoft.Extensions.Logging; -using Sholo.Mqtt.ModelBinding.TypeConverters; -using Sholo.Mqtt.ModelBinding.TypeConverters.Attributes; -using Sholo.Mqtt.Topics.Filter; -using Sholo.Mqtt.Utilities; +using Microsoft.Extensions.Primitives; +using Sholo.Mqtt.ModelBinding.BindingProviders; namespace Sholo.Mqtt.ModelBinding; public class MqttModelBinder : IMqttModelBinder { - public MqttModelBinder() + private IMqttParameterBinder[] ParameterBinders { get; } + + public MqttModelBinder(IMqttParameterBinder[] parameterBinders) { + ParameterBinders = parameterBinders; } - public bool TryPerformModelBinding( - IMqttRequestContext requestContext, - IMqttTopicFilter topicPatternFilter, - MethodInfo action, - [MaybeNullWhen(false)] out IDictionary actionArguments) + public void TryPerformModelBinding(IMqttModelBindingContext modelBindingContext, IMqttRequestContext requestContext, IReadOnlyDictionary topicArguments) { - var logger = requestContext.ServiceProvider.GetService>(); + var parameters = modelBindingContext.Action + .GetParameters() + .ToDictionary( + x => x, + x => new ParameterState(modelBindingContext, x) + ); - // See if the request message's topic matches the pattern & extract arguments - if (!topicPatternFilter.IsMatch(requestContext, out var topicArguments)) + var allParametersSet = true; + foreach (var (_, parameterState) in parameters) { - actionArguments = null; - return false; + if (!TryBind(modelBindingContext, requestContext, topicArguments, parameterState)) + { + allParametersSet = false; + } } - var actionParameters = action.GetParameters(); - - var modelBindingContext = new MqttModelBindingContext( - action, - topicPatternFilter.TopicPattern, - requestContext, - topicArguments!, - logger - ); - - // Attempt to bind the topic arguments, services, etc. to the action/method parameters - if (!TryBindActionParameters(modelBindingContext, out actionArguments)) + if (allParametersSet) { - return false; + foreach (var (_, parameterState) in parameters) + { + if (parameterState is { IsModelSet: true, ValidationStatus: not ParameterValidationResult.ValidationSuppressed }) + { + parameterState.TryValidate(); + } + } } - // Handle the case where we couldn't match 100% of the action arguments to parameters - if (actionArguments.Count != actionParameters.Length) - { - var unmatchedParameters = string.Join(", ", actionArguments.Keys.Except(actionParameters).Select(x => x.Name)); + requestContext.ModelBindingResult = new MqttModelBindingResult(modelBindingContext, parameters); + } - logger?.LogDebug( - "Evaluating candidate handler {TopicPattern}: Failed to bind the following parameters: {UnmatchedParameters}", - topicPatternFilter.TopicPattern, - unmatchedParameters - ); - return false; + public bool TryBind( + IMqttModelBindingContext modelBindingContext, + IMqttRequestContext requestContext, + IReadOnlyDictionary topicArguments, + ParameterState parameterState + ) + { + foreach (var parameterBinder in ParameterBinders) + { + if (parameterBinder.TryBind(modelBindingContext, requestContext, topicArguments, parameterState, out var parameterBindingResult)) + { + parameterState.SetBindingSuccess(parameterBindingResult.BindingSource, parameterBindingResult.Value, parameterBindingResult.BypassValidation); + return true; + } } - logger?.LogDebug( - "Executing {TopicPattern}", - topicPatternFilter.TopicPattern - ); - - return true; + parameterState.SetBindingFailure(); + return false; } - [ExcludeFromCodeCoverage] - private bool TryBindActionParameters(IMqttModelBindingContext mqttModelBindingContext, out IDictionary actionArguments) + /* + public bool TryBindParameters(IMqttUserPropertiesTypeConverter? explicitParameterTypeConverter, ParameterInfo actionParameter, Type targetType, out object? result) { - actionArguments = new Dictionary(); + if (input == null) + { + if (actionParameter.ParameterType.IsClass) + { + result = default; + return true; + } + + if (actionParameter.ParameterType.IsValueType && Nullable.GetUnderlyingType(actionParameter.ParameterType) != null) + { + result = null; + return true; + } - ParameterInfo? unboundParameter = null; - var actionParameters = mqttModelBindingContext.Action.GetParameters(); + result = null; + return false; + } - foreach (var actionParameter in actionParameters) + if (explicitParameterTypeConverter != null) { - if (TryBindCancellationToken(mqttModelBindingContext, actionArguments, actionParameter) || - TryBindParameter(mqttModelBindingContext, actionArguments, actionParameter) || - TryBindCorrelationData(mqttModelBindingContext, actionArguments, actionParameter) || - TryBindUserProperty(mqttModelBindingContext, actionArguments, actionParameter) || - TryBindService(mqttModelBindingContext, actionArguments, actionParameter)) + if (explicitParameterTypeConverter.TryConvertUserProperties(input, targetType, out result)) { - continue; + return true; } - - if (unboundParameter != null) + else { - // There can only be 1 unmatched parameter (the one that might be the payload) - return false; + throw new InvalidOperationException( + $"The converter {explicitParameterTypeConverter.GetType().Name} cannot convert parameters of type {actionParameter.ParameterType.Name}"); } + } - // Need to hold the slot. We'll supply the value below. - actionArguments.Add(actionParameter, null); - unboundParameter = actionParameter; + foreach (var converter in ParameterTypeConverters) + { + if (converter.TryConvertUserProperties(input, targetType, out result)) + { + return true; + } } - if (unboundParameter == null) + if (DefaultTypeConverter.TryConvert(input, actionParameter.ParameterType, out result)) { - mqttModelBindingContext.Logger?.LogWarning("No remaining parameters available for payload binding"); - return false; + return true; } - var payloadParameter = unboundParameter; - if (!TryBindPayload(mqttModelBindingContext, payloadParameter, out var payload)) + result = default; + return false; + } + */ + + /* + +[ExcludeFromCodeCoverage] +private bool TryBindActionParameters(IMqttModelBindingContext mqttModelBindingContext, out IDictionary actionArguments) +{ + actionArguments = new Dictionary(); + + ParameterInfo? unboundParameter = null; + var actionParameters = mqttModelBindingContext.Action.GetParameters(); + + foreach (var actionParameter in actionParameters) + { + if (TryBindCancellationToken(mqttModelBindingContext, actionArguments, actionParameter) || + TryBindParameter(mqttModelBindingContext, actionArguments, actionParameter) || + TryBindCorrelationData(mqttModelBindingContext, actionArguments, actionParameter) || + TryBindUserProperty(mqttModelBindingContext, actionArguments, actionParameter) || + TryBindService(mqttModelBindingContext, actionArguments, actionParameter)) { - mqttModelBindingContext.Logger?.LogWarning("Failed to bind payload"); - return false; + continue; } - if (!ValidationHelper.IsValid(payload!, out var validationResults)) + if (unboundParameter != null) { - mqttModelBindingContext.Logger?.LogWarning( - "The message payload failed validation:{NewLine}{ValidationErrors}", - Environment.NewLine, - string.Join(Environment.NewLine, validationResults.Select(x => $"{x.ErrorMessage} ({string.Join(", ", x.MemberNames)})")) - ); + // There can only be 1 unmatched parameter (the one that might be the payload) return false; } - actionArguments[payloadParameter] = payload; - return true; + // Need to hold the slot. We'll supply the value below. + actionArguments.Add(actionParameter, null); + unboundParameter = actionParameter; + } + + if (unboundParameter == null) + { + mqttModelBindingContext.Logger?.LogWarning("No remaining parameters available for payload binding"); + return false; + } + + var payloadParameter = unboundParameter; + if (!TryBindPayload(mqttModelBindingContext, payloadParameter, out var payload)) + { + mqttModelBindingContext.Logger?.LogWarning("Failed to bind payload"); + return false; + } + + if (!ValidationHelper.IsValid(payload!, out var validationResults)) + { + mqttModelBindingContext.Logger?.LogWarning( + "The message payload failed validation:{NewLine}{ValidationErrors}", + Environment.NewLine, + string.Join(Environment.NewLine, validationResults.Select(x => $"{x.ErrorMessage} ({string.Join(", ", x.MemberNames)})")) + ); + return false; + } + + actionArguments[payloadParameter] = payload; + return true; } [ExcludeFromCodeCoverage] @@ -145,7 +194,7 @@ private bool TryBindCancellationToken(IMqttModelBindingContext mqttModelBindingC [ExcludeFromCodeCoverage] private bool TryBindParameter(IMqttModelBindingContext mqttModelBindingContext, IDictionary actionArguments, ParameterInfo actionParameter) { - var explicitTypeConverter = CreateRequestedTypeConverter( + var explicitTypeConverter = CreateRequestedTypeConverter( mqttModelBindingContext, actionParameter, a => a.TypeConverterType); @@ -168,7 +217,7 @@ private bool TryBindParameter(IMqttModelBindingContext mqttModelBindingContext, var array = Array.CreateInstance(elementType, argumentValueStrings.Length); for (var i = 0; i < argumentValueStrings.Length; i++) { - if (!mqttModelBindingContext.TryConvertParameter(argumentValueStrings[i], explicitTypeConverter, actionParameter, elementType, out var typedArgumentValue)) + if (!mqttModelBindingContext.TryConvertUserProperties(argumentValueStrings[i], explicitTypeConverter, actionParameter, elementType, out var typedArgumentValue)) { mqttModelBindingContext.Logger?.LogWarning( "Unable to convert parameter {ParameterName} value to {ParameterType}", @@ -186,7 +235,7 @@ private bool TryBindParameter(IMqttModelBindingContext mqttModelBindingContext, return true; } - if (mqttModelBindingContext.TryConvertParameter(argumentValueStrings.Single(), explicitTypeConverter, actionParameter, actionParameterParameterType, out var argumentValue)) + if (mqttModelBindingContext.TryConvertUserProperties(argumentValueStrings.Single(), explicitTypeConverter, actionParameter, actionParameterParameterType, out var argumentValue)) { actionArguments[actionParameter] = argumentValue; return true; @@ -200,107 +249,5 @@ private bool TryBindParameter(IMqttModelBindingContext mqttModelBindingContext, return false; } - - [ExcludeFromCodeCoverage] - private bool TryBindCorrelationData(IMqttModelBindingContext mqttModelBindingContext, IDictionary actionArguments, ParameterInfo actionParameter) - { - var parameterType = actionParameter.ParameterType; - - var explicitTypeConverter = CreateRequestedTypeConverter( - mqttModelBindingContext, - actionParameter, - a => a.TypeConverterType); - - var correlationData = new ArraySegment(mqttModelBindingContext.Request.CorrelationData ?? Array.Empty()); - - if (explicitTypeConverter != null && explicitTypeConverter.TryConvertPayload(correlationData, parameterType, out var correlationDataResult)) - { - actionArguments[actionParameter] = correlationDataResult; - return true; - } - - if (DefaultTypeConverters.TryConvert(correlationData, parameterType, out correlationDataResult)) - { - actionArguments[actionParameter] = correlationDataResult; - return true; - } - - if (mqttModelBindingContext.CorrelationDataTypeConverter.TryConvertCorrelationData(mqttModelBindingContext.Request.CorrelationData, parameterType, out correlationDataResult)) - { - actionArguments[actionParameter] = correlationDataResult; - return true; - } - - actionArguments[actionParameter] = null!; - return false; - } - - // ReSharper disable once UnusedParameter.Local - [ExcludeFromCodeCoverage] - private bool TryBindUserProperty(IMqttModelBindingContext mqttModelBindingContext, IDictionary actionArguments, ParameterInfo actionParameter) - { - return false; - } - - [ExcludeFromCodeCoverage] - private bool TryBindService(IMqttModelBindingContext mqttModelBindingContext, IDictionary actionArguments, ParameterInfo actionParameter) - { - var parameterType = actionParameter.ParameterType; - if (parameterType.IsEnum || - parameterType.IsPrimitive || - parameterType == typeof(string) || - Nullable.GetUnderlyingType(parameterType) != null) - { - return false; - } - - var argumentValues = mqttModelBindingContext.Request.ServiceProvider - .GetServices(parameterType) - .ToArray(); - - if (argumentValues.Length == 0) return false; - - var argumentValue = argumentValues.Last(); - actionArguments[actionParameter] = argumentValue; - - return true; - } - - [ExcludeFromCodeCoverage] - private bool TryBindPayload(IMqttModelBindingContext mqttModelBindingContext, ParameterInfo? payloadParameter, out object? payload) - { - if (payloadParameter == null) - { - payload = null; - return false; - } - - var parameterType = payloadParameter.ParameterType; - - var explicitTypeConverter = CreateRequestedTypeConverter( - mqttModelBindingContext, - payloadParameter, - a => a.TypeConverterType); - - if (explicitTypeConverter != null && explicitTypeConverter.TryConvertPayload(mqttModelBindingContext.Request.Payload, parameterType, out var payloadResult)) - { - payload = payloadResult; - return true; - } - - if (DefaultTypeConverters.TryConvert(mqttModelBindingContext.Request.Payload, parameterType, out payloadResult)) - { - payload = payloadResult; - return true; - } - - if (mqttModelBindingContext.PayloadTypeConverter.TryConvertPayload(mqttModelBindingContext.Request.Payload, parameterType, out payloadResult)) - { - payload = payloadResult; - return true; - } - - payload = null!; - return false; - } + */ } diff --git a/Source/Sholo.Mqtt/ModelBinding/MqttModelBindingContext.cs b/Source/Sholo.Mqtt/ModelBinding/MqttModelBindingContext.cs index 9fce454..f6a7716 100644 --- a/Source/Sholo.Mqtt/ModelBinding/MqttModelBindingContext.cs +++ b/Source/Sholo.Mqtt/ModelBinding/MqttModelBindingContext.cs @@ -1,118 +1,22 @@ -using System.Collections.Generic; using System.Reflection; -using Sholo.Mqtt.ModelBinding.Validation; -using Sholo.Mqtt.ModelBinding.ValueProviders; using Sholo.Mqtt.Topics.Filter; namespace Sholo.Mqtt.ModelBinding; public class MqttModelBindingContext : IMqttModelBindingContext { - public MqttBindingSource? BindingSource { get; } - public IMqttRequestContext RequestContext { get; } public IMqttTopicFilter TopicFilter { get; } - public IReadOnlyDictionary TopicArguments { get; } + public TypeInfo? Instance { get; } public MethodInfo Action { get; } - public IDictionary ActionArguments { get; } - public IDictionary ParameterValueProviders { get; } - public MqttValidationStateDictionary MqttValidationState { get; } - public MqttModelBindingResult Result { get; set; } - - /* - public MethodInfo Action { get; } - public string TopicPattern { get; } - public IReadOnlyDictionary TopicArguments { get; } - public IMqttRequestContext Request { get; } - public ILogger? Logger { get; } - public IMqttParameterTypeConverter[] ParameterTypeConverters => LazyParameterTypeConverters.Value; - public IMqttCorrelationDataTypeConverter CorrelationDataTypeConverter => LazyCorrelationDataTypeConverter.Value; - public IMqttPayloadTypeConverter PayloadTypeConverter => LazyPayloadTypeConverter.Value; - - private Lazy LazyParameterTypeConverters { get; } - private Lazy LazyCorrelationDataTypeConverter { get; } - private Lazy LazyPayloadTypeConverter { get; } public MqttModelBindingContext( + TypeInfo? instance, MethodInfo action, - string topicPattern, - IMqttRequestContext request, - IReadOnlyDictionary topicArguments, - ILogger? logger + IMqttTopicFilter topicFilter ) { + Instance = instance; Action = action; - TopicPattern = topicPattern; - Request = request; - Logger = logger; - TopicArguments = topicArguments; - - LazyParameterTypeConverters = new Lazy(RetrieveParameterTypeConverters); - LazyCorrelationDataTypeConverter = new Lazy(RetrieveCorrelationDataTypeConverter); - LazyPayloadTypeConverter = new Lazy(RetrievePayloadTypeConverter); - } - - public bool TryConvertParameter(string? input, IMqttParameterTypeConverter? explicitParameterTypeConverter, ParameterInfo actionParameter, Type targetType, out object? result) - { - if (input == null) - { - if (actionParameter.ParameterType.IsClass) - { - result = default; - return true; - } - - if (actionParameter.ParameterType.IsValueType && Nullable.GetUnderlyingType(actionParameter.ParameterType) != null) - { - result = null; - return true; - } - - result = null; - return false; - } - - if (explicitParameterTypeConverter != null) - { - if (explicitParameterTypeConverter.TryConvertParameter(input, targetType, out result)) - { - return true; - } - else - { - throw new InvalidOperationException( - $"The converter {explicitParameterTypeConverter.GetType().Name} cannot convert parameters of type {actionParameter.ParameterType.Name}"); - } - } - - foreach (var converter in ParameterTypeConverters) - { - if (converter.TryConvertParameter(input, targetType, out result)) - { - return true; - } - } - - if (DefaultTypeConverters.TryConvert(input, actionParameter.ParameterType, out result)) - { - return true; - } - - result = default; - return false; + TopicFilter = topicFilter; } - - private IMqttParameterTypeConverter[] RetrieveParameterTypeConverters() => - Request.ServiceProvider.GetService>() - ?.Reverse() - .ToArray() - ?? new IMqttParameterTypeConverter[] { DefaultTypeConverters.Instance }; - - private IMqttCorrelationDataTypeConverter RetrieveCorrelationDataTypeConverter() => - Request.ServiceProvider.GetService() - ?? DefaultTypeConverters.Instance; - - private IMqttPayloadTypeConverter RetrievePayloadTypeConverter() => - Request.ServiceProvider.GetService() - ?? DefaultTypeConverters.Instance; - */ } diff --git a/Source/Sholo.Mqtt/ModelBinding/MqttModelBindingResult.cs b/Source/Sholo.Mqtt/ModelBinding/MqttModelBindingResult.cs index 20ab8f5..38c4251 100644 --- a/Source/Sholo.Mqtt/ModelBinding/MqttModelBindingResult.cs +++ b/Source/Sholo.Mqtt/ModelBinding/MqttModelBindingResult.cs @@ -1,118 +1,53 @@ -// Licensed to the .NET Foundation under one or more agreements. -// The .NET Foundation licenses this file to you under the MIT license. - using System; +using System.Collections.Generic; +using System.Collections.ObjectModel; +using System.Linq; +using System.Reflection; +using System.Threading.Tasks; +using Sholo.Mqtt.Topics.Filter; namespace Sholo.Mqtt.ModelBinding; -/// -/// Contains the result of model binding. -/// [PublicAPI] -public readonly struct MqttModelBindingResult : IEquatable +public class MqttModelBindingResult : IMqttModelBindingResult { - /// - /// Creates a representing a failed model binding operation. - /// - /// A representing a failed model binding operation. - public static MqttModelBindingResult Failed() - { - return new MqttModelBindingResult(model: null, isModelSet: false); - } - - /// - /// Creates a representing a successful model binding operation. - /// - /// The model value. May be null. - /// A representing a successful model bind. - public static MqttModelBindingResult Success(object? model) - { - return new MqttModelBindingResult(model, isModelSet: true); - } - - private MqttModelBindingResult(object? model, bool isModelSet) - { - Model = model; - IsModelSet = isModelSet; - } - - /// - /// Gets the model associated with this context. - /// - public object? Model { get; } - - /// - /// - /// Gets a value indicating whether or not the value has been set. - /// - /// - /// This property can be used to distinguish between a model binder which does not find a value and - /// the case where a model binder sets the null value. - /// - /// - public bool IsModelSet { get; } + public IMqttTopicFilter TopicFilter { get; } + public TypeInfo? Instance { get; } + public MethodInfo Action { get; } /// - public override bool Equals(object? obj) - { - var other = obj as MqttModelBindingResult?; - if (other == null) - { - return false; - } - else - { - return Equals(other.Value); - } - } + public IReadOnlyDictionary ActionArguments { get; private set; } /// - public override int GetHashCode() - { - return HashCode.Combine(IsModelSet, Model); - } + public bool Success => ActionArguments.All(x => x.Value is { IsModelSet: true, ValidationStatus: ParameterValidationResult.ValidationSuppressed or ParameterValidationResult.Valid }); - /// - public bool Equals(MqttModelBindingResult other) + public MqttModelBindingResult(IMqttModelBindingContext modelBindingContext, IDictionary actionArguments) { - // ReSharper disable once RedundantNameQualifier - return - IsModelSet == other.IsModelSet && - object.Equals(Model, other.Model); + TopicFilter = modelBindingContext.TopicFilter; + Instance = modelBindingContext.Instance; + Action = modelBindingContext.Action; + ActionArguments = new ReadOnlyDictionary(actionArguments); } - /// - public override string ToString() + public Task Invoke() { - if (IsModelSet) + if (!Success) { - return $"Success '{Model}'"; + throw new InvalidOperationException("The model binding did not complete successfully. Invocation is impossible."); } - else + + var arguments = ActionArguments.Values.Select(x => x.Value).ToArray(); + + if (Action.ReturnType == typeof(Task)) { - return "Failed"; + return (Task)Action.Invoke(null, arguments)!; } - } - /// - /// Compares objects for equality. - /// - /// A . - /// A to compare with. - /// true if the objects are equal, otherwise false. - public static bool operator ==(MqttModelBindingResult x, MqttModelBindingResult y) - { - return x.Equals(y); - } + if (Action.ReturnType == typeof(bool)) + { + return Task.FromResult((bool)Action.Invoke(null, arguments)!); + } - /// - /// Compares objects for inequality. - /// - /// A . - /// A to compare with. - /// true if the objects are not equal, otherwise false. - public static bool operator !=(MqttModelBindingResult x, MqttModelBindingResult y) - { - return !x.Equals(y); + throw new InvalidOperationException("Expecting either a Task or a bool return type"); } } diff --git a/Source/Sholo.Mqtt/ModelBinding/MqttRequestContextExtensions.cs b/Source/Sholo.Mqtt/ModelBinding/MqttRequestContextExtensions.cs new file mode 100644 index 0000000..a8f9fe6 --- /dev/null +++ b/Source/Sholo.Mqtt/ModelBinding/MqttRequestContextExtensions.cs @@ -0,0 +1,44 @@ +using System; +using System.Linq; +using System.Threading.Tasks; + +namespace Sholo.Mqtt.ModelBinding; + +internal static class MqttRequestContextExtensions +{ + // ReSharper disable once ParameterOnlyUsedForPreconditionCheck.Global + public static MqttRequestDelegate GetRequestDelegate(this IMqttRequestContext context, object? instance) + { + return ctx => + { + if (!ReferenceEquals(ctx, context)) + { + throw new InvalidOperationException($"This ${nameof(MqttRequestDelegate)} was created from a different {nameof(IMqttRequestContext)}"); + } + + if (ctx.ModelBindingResult is not { Success: true }) + { + throw new InvalidOperationException("The model binding did not complete successfully. Invocation is impossible."); + } + + var arguments = ctx.ModelBindingResult.ActionArguments.Values.Select(x => x.Value).ToArray(); + + if (ctx.ModelBindingResult.Action.ReturnType == typeof(Task)) + { + return (Task)ctx.ModelBindingResult.Action.Invoke(instance, arguments)!; + } + + if (ctx.ModelBindingResult.Action.ReturnType == typeof(ValueTask)) + { + return ((ValueTask)ctx.ModelBindingResult.Action.Invoke(instance, arguments)!).AsTask(); + } + + if (ctx.ModelBindingResult.Action.ReturnType == typeof(bool)) + { + return Task.FromResult((bool)ctx.ModelBindingResult.Action.Invoke(instance, arguments)!); + } + + throw new InvalidOperationException("Expecting action to have a Task, ValueTask, or a bool return type"); + }; + } +} diff --git a/Source/Sholo.Mqtt/ModelBinding/ParameterState.cs b/Source/Sholo.Mqtt/ModelBinding/ParameterState.cs index 256d7a1..60b9d9e 100644 --- a/Source/Sholo.Mqtt/ModelBinding/ParameterState.cs +++ b/Source/Sholo.Mqtt/ModelBinding/ParameterState.cs @@ -4,30 +4,22 @@ using System.Diagnostics.CodeAnalysis; using System.Linq; using System.Reflection; -using System.Threading; -using Sholo.Mqtt.ModelBinding.TypeConverters; -using Sholo.Mqtt.ModelBinding.TypeConverters.Attributes; -using Sholo.Mqtt.Topics.Filter; namespace Sholo.Mqtt.ModelBinding; -public enum ParameterValidationResult -{ - NotYetValidated, - Valid, - Invalid, - ValidationSuppressed -} - [PublicAPI] public class ParameterState { + /// + /// Gets a value which represents the associated with the + /// request + /// public MqttBindingSource? BindingSource { get; private set; } public bool IsModelSet { get; private set; } public object? Value { get; private set; } public ParameterInfo ParameterInfo { get; } - public string? ParameterName => ParameterInfo.Name!; + public string ParameterName => ParameterInfo.Name!; public Type TargetType => ParameterInfo.ParameterType; /// @@ -37,36 +29,55 @@ public class ParameterState public ParameterValidationResult ValidationStatus { get; private set; } = ParameterValidationResult.NotYetValidated; public ValidationResult[]? ValidationResults { get; private set; } - private IMqttModelBindingContext ModelBindingContext { get; } + public IMqttModelBindingContext ModelBindingContext { get; } + + private bool _bindingAttempted; - internal void SetBindingSuccess(object? value) + internal void SetBindingSuccess(MqttBindingSource bindingSource, object? value, bool bypassValidation = false) { + if (_bindingAttempted) + { + throw new InvalidOperationException("Binding has already been attempted."); + } + + if (ValidationResults != null) + { + throw new InvalidOperationException("Validation has already occurred."); + } + + _bindingAttempted = true; + + BindingSource = bindingSource; Value = value; IsModelSet = true; + + if (bypassValidation) + { + ValidationStatus = ParameterValidationResult.ValidationSuppressed; + } } internal void SetBindingFailure() { - Value = null; - IsModelSet = false; - } + if (_bindingAttempted) + { + throw new InvalidOperationException("Binding has already been attempted."); + } - internal void SetValidationSuppressed() - { if (ValidationResults != null) { - throw new InvalidOperationException("The validation has already occurred."); + throw new InvalidOperationException("Validation has already occurred."); } - ValidationStatus = ParameterValidationResult.ValidationSuppressed; + _bindingAttempted = true; + + Value = null; + IsModelSet = false; } internal void SetValidationResults(IEnumerable validationResults) { - if (validationResults == null) - { - throw new ArgumentNullException(nameof(validationResults)); - } + ArgumentNullException.ThrowIfNull(validationResults); ValidationResults = validationResults.ToArray(); ValidationStatus = ValidationResults.Length == 0 ? ParameterValidationResult.Valid : ParameterValidationResult.Invalid; @@ -78,90 +89,22 @@ public ParameterState(IMqttModelBindingContext modelBindingContext, ParameterInf ParameterInfo = parameterInfo; } - private IMqttCorrelationDataTypeConverter GetTypeConverter() + public bool TryValidate() { - if (TryGetTypeConverter(tc => tc.TypeConverterType, out _)) - { - BindingSource = MqttBindingSource.CorrelationData; - } - else if (TryGetTypeConverter(tc => tc.TypeConverterType, out _)) - { - BindingSource = MqttBindingSource.Payload; - } - else if (TryGetTypeConverter(tc => tc.TypeConverterType, out _)) - { - BindingSource = MqttBindingSource.Topic; - } - else if (TargetType == typeof(IMqttRequestContext)) - { - BindingSource = MqttBindingSource.Context; - SetBindingSuccess(ModelBindingContext.Request); - SetValidationSuppressed(); - } - else if (TargetType == typeof(CancellationToken)) - { - BindingSource = MqttBindingSource.Context; - SetBindingSuccess(ModelBindingContext.Request.ShutdownToken); - SetValidationSuppressed(); - } - else if (TargetType == typeof(IServiceProvider)) - { - BindingSource = MqttBindingSource.Context; - SetBindingSuccess(ModelBindingContext.Request.ServiceProvider); - SetValidationSuppressed(); - } - else if (TargetType == typeof(IMqttTopicFilter)) - { - BindingSource = MqttBindingSource.Context; - SetBindingSuccess(ModelBindingContext.TopicFilter); - SetValidationSuppressed(); - } - else if (TargetType == typeof(string) && (ParameterName?.Equals("topic", StringComparison.Ordinal) ?? false)) - { - BindingSource = MqttBindingSource.Context; - SetBindingSuccess(ModelBindingContext.Request.Topic); - } - else if (TargetType == typeof(byte[]) && (ParameterName?.Equals("correlationData", StringComparison.Ordinal) ?? false)) - { - BindingSource = MqttBindingSource.Context; - SetBindingSuccess(ModelBindingContext.Request.CorrelationData); - } - else if (TargetType == typeof(byte[]) && (ParameterName?.Equals("payload", StringComparison.Ordinal) ?? false)) - { - BindingSource = MqttBindingSource.Context; - SetBindingSuccess(ModelBindingContext.Request.Payload.ToArray()); - } - else if (TargetType == typeof(ArraySegment) && (ParameterName?.Equals("payload", StringComparison.Ordinal) ?? false)) - { - BindingSource = MqttBindingSource.Context; - SetBindingSuccess(ModelBindingContext.Request.Payload); - } - else if (TargetType == typeof(ArraySegment) && (ParameterName?.Equals("payload", StringComparison.Ordinal) ?? false)) - { - BindingSource = MqttBindingSource.Context; - SetBindingSuccess(ModelBindingContext.Request.); - } - else - { - // Need to fall back to "figure-it-out" mode - // MqttBindingSource.Topic - // MqttBindingSource.Context - // MqttBindingSource.CorrelationData - // MqttBindingSource.Payload - // MqttBindingSource.Services - // MqttBindingSource.UserProperties - - } + // TODO + ValidationStatus = ParameterValidationResult.Valid; + return true; } private bool TryGetTypeConverter( + IMqttRequestContext requestContext, Func typeConverterTypeSelector, [MaybeNullWhen(false)] out TTypeConverter typeConverter ) where TAttribute : Attribute where TTypeConverter : class { - var customAttribute = ParameterInfo.GetCustomAttribute(); + var customAttribute = ParameterInfo.GetCustomAttributes().OfType().SingleOrDefault(); if (customAttribute == null) { typeConverter = null!; @@ -175,7 +118,7 @@ private bool TryGetTypeConverter( return false; } - var serviceProvider = ModelBindingContext.Request.ServiceProvider; + var serviceProvider = requestContext.ServiceProvider; var typeConverterInstance = serviceProvider.GetService(explicitTypeConverterType) ?? Activator.CreateInstance(explicitTypeConverterType); diff --git a/Source/Sholo.Mqtt/ModelBinding/ParameterValidationResult.cs b/Source/Sholo.Mqtt/ModelBinding/ParameterValidationResult.cs new file mode 100644 index 0000000..6fe3af2 --- /dev/null +++ b/Source/Sholo.Mqtt/ModelBinding/ParameterValidationResult.cs @@ -0,0 +1,9 @@ +namespace Sholo.Mqtt.ModelBinding; + +public enum ParameterValidationResult +{ + NotYetValidated, + Valid, + Invalid, + ValidationSuppressed +} diff --git a/Source/Sholo.Mqtt/ModelBinding/TypeConverters/Attributes/BaseFromMqttConverterAttribute.cs b/Source/Sholo.Mqtt/ModelBinding/TypeConverters/Attributes/BaseFromMqttConverterAttribute.cs new file mode 100644 index 0000000..b993c4f --- /dev/null +++ b/Source/Sholo.Mqtt/ModelBinding/TypeConverters/Attributes/BaseFromMqttConverterAttribute.cs @@ -0,0 +1,56 @@ +using System; +using System.Diagnostics.CodeAnalysis; +using Microsoft.Extensions.DependencyInjection; +using Sholo.Mqtt.Internal; + +namespace Sholo.Mqtt.ModelBinding.TypeConverters.Attributes; + +[PublicAPI] +public abstract class BaseFromMqttConverterAttribute : Attribute, IFromMqttConverter + where TMqttTypeConverterInterface : class, IMqttTypeConverter + where TMqttTypeConverter : class, TMqttTypeConverterInterface +{ + public MqttBindingSource BindingSource { get; } + public string? ServiceKey { get; } + + protected BaseFromMqttConverterAttribute(MqttBindingSource bindingSource, string? serviceKey = null) + { + BindingSource = bindingSource; + ServiceKey = serviceKey; + } + + public bool TryBind(IMqttRequestContext requestContext, ParameterState parameterState, out object? result) + { + if (TryGetTypeConverter(requestContext.ServiceProvider, ServiceKey, out var typeConverterInstance) + && TryConvert(requestContext, parameterState, typeConverterInstance, out result)) + { + return true; + } + + result = null; + return false; + } + + protected abstract bool TryConvert( + IMqttRequestContext requestContext, + ParameterState parameterState, + TMqttTypeConverterInterface typeConverter, + out object? result + ); + + protected virtual bool TryGetTypeConverter(IServiceProvider serviceProvider, string? serviceKey, [MaybeNullWhen(false)] out TMqttTypeConverterInterface typeConverter) + { + typeConverter = !string.IsNullOrEmpty(serviceKey) ? serviceProvider.GetKeyedService(serviceKey) : null; + if (typeConverter != null) return true; + + typeConverter = serviceProvider.GetService(); + if (typeConverter != null) return true; + + var typeActivatorCache = serviceProvider.GetService(); + typeConverter = typeActivatorCache?.CreateInstance(serviceProvider, typeof(TMqttTypeConverter)); + if (typeConverter != null) return true; + + typeConverter = null; + return false; + } +} diff --git a/Source/Sholo.Mqtt/ModelBinding/TypeConverters/Attributes/FromMqttCorrelationDataAttribute.cs b/Source/Sholo.Mqtt/ModelBinding/TypeConverters/Attributes/FromMqttCorrelationDataAttribute.cs index b72bb54..1b8e04f 100644 --- a/Source/Sholo.Mqtt/ModelBinding/TypeConverters/Attributes/FromMqttCorrelationDataAttribute.cs +++ b/Source/Sholo.Mqtt/ModelBinding/TypeConverters/Attributes/FromMqttCorrelationDataAttribute.cs @@ -4,22 +4,39 @@ namespace Sholo.Mqtt.ModelBinding.TypeConverters.Attributes; [PublicAPI] [AttributeUsage(AttributeTargets.Parameter)] -public sealed class FromMqttCorrelationDataAttribute : Attribute +public sealed class FromMqttCorrelationDataAttribute : BaseFromMqttConverterAttribute { - public Type TypeConverterType { get; } + public FromMqttCorrelationDataAttribute() + : base(MqttBindingSource.CorrelationData) + { + } - public FromMqttCorrelationDataAttribute(Type typeConverterType) + protected override bool TryConvert(IMqttRequestContext requestContext, ParameterState parameterState, IMqttCorrelationDataTypeConverter typeConverter, out object? result) { - ArgumentNullException.ThrowIfNull(typeConverterType, nameof(typeConverterType)); + return typeConverter.TryConvertCorrelationData( + requestContext.CorrelationData, + parameterState.ParameterInfo.ParameterType, + out result + ); + } +} - if (!typeof(IMqttCorrelationDataTypeConverter).IsAssignableFrom(typeConverterType)) - { - throw new ArgumentException( - $"The type {typeConverterType.Name} does not implement {nameof(IMqttCorrelationDataTypeConverter)}", - nameof(typeConverterType) - ); - } +[PublicAPI] +[AttributeUsage(AttributeTargets.Parameter)] +public sealed class FromMqttCorrelationDataAttribute : BaseFromMqttConverterAttribute + where TMqttCorrelationDataTypeConverter : class, IMqttCorrelationDataTypeConverter +{ + public FromMqttCorrelationDataAttribute() + : base(MqttBindingSource.CorrelationData) + { + } - TypeConverterType = typeConverterType; + protected override bool TryConvert(IMqttRequestContext requestContext, ParameterState parameterState, IMqttCorrelationDataTypeConverter typeConverter, out object? result) + { + return typeConverter.TryConvertCorrelationData( + requestContext.CorrelationData, + parameterState.ParameterInfo.ParameterType, + out result + ); } } diff --git a/Source/Sholo.Mqtt/ModelBinding/TypeConverters/Attributes/FromMqttPayloadAttribute.cs b/Source/Sholo.Mqtt/ModelBinding/TypeConverters/Attributes/FromMqttPayloadAttribute.cs index d7b182b..287a409 100644 --- a/Source/Sholo.Mqtt/ModelBinding/TypeConverters/Attributes/FromMqttPayloadAttribute.cs +++ b/Source/Sholo.Mqtt/ModelBinding/TypeConverters/Attributes/FromMqttPayloadAttribute.cs @@ -4,22 +4,39 @@ namespace Sholo.Mqtt.ModelBinding.TypeConverters.Attributes; [PublicAPI] [AttributeUsage(AttributeTargets.Parameter)] -public sealed class FromMqttPayloadAttribute : Attribute +public sealed class FromMqttPayloadAttribute : BaseFromMqttConverterAttribute { - public Type TypeConverterType { get; } + public FromMqttPayloadAttribute() + : base(MqttBindingSource.Payload) + { + } - public FromMqttPayloadAttribute(Type typeConverterType) + protected override bool TryConvert(IMqttRequestContext requestContext, ParameterState parameterState, IMqttPayloadTypeConverter typeConverter, out object? result) { - ArgumentNullException.ThrowIfNull(typeConverterType, nameof(typeConverterType)); + return typeConverter.TryConvertPayload( + requestContext.Payload, + parameterState.ParameterInfo.ParameterType, + out result + ); + } +} - if (!typeof(IMqttPayloadTypeConverter).IsAssignableFrom(typeConverterType)) - { - throw new ArgumentException( - $"The type {typeConverterType.Name} does not implement {nameof(IMqttPayloadTypeConverter)}", - nameof(typeConverterType) - ); - } +[PublicAPI] +[AttributeUsage(AttributeTargets.Parameter)] +public sealed class FromMqttPayloadAttribute : BaseFromMqttConverterAttribute + where TMqttPayloadTypeConverter : class, IMqttPayloadTypeConverter +{ + public FromMqttPayloadAttribute(string? serviceKey = null) + : base(MqttBindingSource.Payload, serviceKey) + { + } - TypeConverterType = typeConverterType; + protected override bool TryConvert(IMqttRequestContext requestContext, ParameterState parameterState, IMqttPayloadTypeConverter typeConverter, out object? result) + { + return typeConverter.TryConvertPayload( + requestContext.Payload, + parameterState.ParameterInfo.ParameterType, + out result + ); } } diff --git a/Source/Sholo.Mqtt/ModelBinding/TypeConverters/Attributes/FromMqttTopicAttribute.cs b/Source/Sholo.Mqtt/ModelBinding/TypeConverters/Attributes/FromMqttTopicAttribute.cs index 34a0e6c..14719e2 100644 --- a/Source/Sholo.Mqtt/ModelBinding/TypeConverters/Attributes/FromMqttTopicAttribute.cs +++ b/Source/Sholo.Mqtt/ModelBinding/TypeConverters/Attributes/FromMqttTopicAttribute.cs @@ -4,22 +4,39 @@ namespace Sholo.Mqtt.ModelBinding.TypeConverters.Attributes; [PublicAPI] [AttributeUsage(AttributeTargets.Parameter)] -public sealed class FromMqttTopicAttribute : Attribute +public sealed class FromMqttTopicAttribute : BaseFromMqttConverterAttribute { - public Type TypeConverterType { get; } + public FromMqttTopicAttribute() + : base(MqttBindingSource.Topic) + { + } - public FromMqttTopicAttribute(Type typeConverterType) + protected override bool TryConvert(IMqttRequestContext requestContext, ParameterState parameterState, IMqttTopicArgumentTypeConverter typeConverter, out object? result) { - ArgumentNullException.ThrowIfNull(typeConverterType, nameof(typeConverterType)); + return typeConverter.TryConvertTopicArgument( + parameterState.ParameterName, + parameterState.ParameterInfo.ParameterType, + out result + ); + } +} - if (!typeof(IMqttParameterTypeConverter).IsAssignableFrom(typeConverterType)) - { - throw new ArgumentException( - $"The type {typeConverterType.Name} does not implement {nameof(IMqttParameterTypeConverter)}", - nameof(typeConverterType) - ); - } +[PublicAPI] +[AttributeUsage(AttributeTargets.Parameter)] +public sealed class FromMqttTopicAttribute : BaseFromMqttConverterAttribute + where TMqttTopicArgumentTypeConverter : class, IMqttTopicArgumentTypeConverter +{ + public FromMqttTopicAttribute() + : base(MqttBindingSource.Topic) + { + } - TypeConverterType = typeConverterType; + protected override bool TryConvert(IMqttRequestContext requestContext, ParameterState parameterState, IMqttTopicArgumentTypeConverter typeConverter, out object? result) + { + return typeConverter.TryConvertTopicArgument( + parameterState.ParameterName, + parameterState.ParameterInfo.ParameterType, + out result + ); } } diff --git a/Source/Sholo.Mqtt/ModelBinding/TypeConverters/Attributes/FromMqttUserPropertiesAttribute.cs b/Source/Sholo.Mqtt/ModelBinding/TypeConverters/Attributes/FromMqttUserPropertiesAttribute.cs new file mode 100644 index 0000000..e0bcf90 --- /dev/null +++ b/Source/Sholo.Mqtt/ModelBinding/TypeConverters/Attributes/FromMqttUserPropertiesAttribute.cs @@ -0,0 +1,50 @@ +using System; + +namespace Sholo.Mqtt.ModelBinding.TypeConverters.Attributes; + +[PublicAPI] +[AttributeUsage(AttributeTargets.Parameter)] +public sealed class FromMqttUserPropertiesAttribute : BaseFromMqttConverterAttribute +{ + public FromMqttUserPropertiesAttribute() + : base(MqttBindingSource.UserProperties) + { + } + + protected override bool TryConvert(IMqttRequestContext requestContext, ParameterState parameterState, IMqttUserPropertiesTypeConverter typeConverter, out object? result) + { + if (requestContext.UserProperties.TryGetValue(parameterState.ParameterName, out var stringValues)) + { + typeConverter.TryConvertUserPropertyValues(stringValues, parameterState.TargetType, out var resultList); + result = resultList; + return true; + } + + result = null; + return false; + } +} + +[PublicAPI] +[AttributeUsage(AttributeTargets.Parameter)] +public sealed class FromMqttUserPropertiesAttribute : BaseFromMqttConverterAttribute + where TMqttUserPropertiesTypeConverter : class, IMqttUserPropertiesTypeConverter +{ + public FromMqttUserPropertiesAttribute() + : base(MqttBindingSource.UserProperties) + { + } + + protected override bool TryConvert(IMqttRequestContext requestContext, ParameterState parameterState, IMqttUserPropertiesTypeConverter typeConverter, out object? result) + { + if (requestContext.UserProperties.TryGetValue(parameterState.ParameterName, out var stringValues)) + { + typeConverter.TryConvertUserPropertyValues(stringValues, parameterState.TargetType, out var resultList); + result = resultList; + return true; + } + + result = null; + return false; + } +} diff --git a/Source/Sholo.Mqtt/ModelBinding/TypeConverters/Attributes/IFromMqttConverter.cs b/Source/Sholo.Mqtt/ModelBinding/TypeConverters/Attributes/IFromMqttConverter.cs new file mode 100644 index 0000000..11d10a5 --- /dev/null +++ b/Source/Sholo.Mqtt/ModelBinding/TypeConverters/Attributes/IFromMqttConverter.cs @@ -0,0 +1,9 @@ +namespace Sholo.Mqtt.ModelBinding.TypeConverters.Attributes; + +[PublicAPI] +public interface IFromMqttConverter +{ + MqttBindingSource BindingSource { get; } + + bool TryBind(IMqttRequestContext requestContext, ParameterState parameterState, out object? result); +} diff --git a/Source/Sholo.Mqtt/ModelBinding/TypeConverters/DefaultTypeConverters.cs b/Source/Sholo.Mqtt/ModelBinding/TypeConverters/DefaultTypeConverter.cs similarity index 89% rename from Source/Sholo.Mqtt/ModelBinding/TypeConverters/DefaultTypeConverters.cs rename to Source/Sholo.Mqtt/ModelBinding/TypeConverters/DefaultTypeConverter.cs index 957499f..d8e1f68 100644 --- a/Source/Sholo.Mqtt/ModelBinding/TypeConverters/DefaultTypeConverters.cs +++ b/Source/Sholo.Mqtt/ModelBinding/TypeConverters/DefaultTypeConverter.cs @@ -2,23 +2,50 @@ using System.Collections.Generic; using System.Globalization; using System.Text; +using Microsoft.Extensions.Primitives; namespace Sholo.Mqtt.ModelBinding.TypeConverters; [PublicAPI] -public class DefaultTypeConverters : IMqttCorrelationDataTypeConverter, IMqttPayloadTypeConverter, IMqttParameterTypeConverter +public class DefaultTypeConverter : IMqttCorrelationDataTypeConverter, IMqttPayloadTypeConverter, IMqttTopicArgumentTypeConverter, IMqttUserPropertiesTypeConverter { - public static DefaultTypeConverters Instance => InstanceFactory.Value; - private static Lazy InstanceFactory { get; } = new(() => new DefaultTypeConverters()); + public static DefaultTypeConverter Instance => InstanceFactory.Value; + private static Lazy InstanceFactory { get; } = new(() => new DefaultTypeConverter()); public bool TryConvertCorrelationData(byte[]? correlationData, Type targetType, out object? result) => TryConvert(new ArraySegment(correlationData ?? Array.Empty()), targetType, out result); - 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); - public bool TryConvertParameter(string? value, Type targetType, out object? result) - => TryConvert(value, targetType, out result); + public bool TryConvertTopicArgument(string argument, Type targetType, out object? result) + => TryConvert(argument, targetType, out result); + + public bool TryConvertUserPropertyValues(StringValues? values, Type targetType, out IList? result) + { + if (!values.HasValue) + { + result = null; + return false; + } + + var list = new List(); + foreach (var value in values) + { + if (!TryConvert(values, targetType, out var itemResult)) + { + result = null; + return false; + } + else + { + list.Add(itemResult); + } + } + + result = list; + return true; + } private static Dictionary> StringTypeConverters { get; } = new() diff --git a/Source/Sholo.Mqtt/ModelBinding/TypeConverters/IMqttCorrelationDataTypeConverter.cs b/Source/Sholo.Mqtt/ModelBinding/TypeConverters/IMqttCorrelationDataTypeConverter.cs index 816863a..6a435fa 100644 --- a/Source/Sholo.Mqtt/ModelBinding/TypeConverters/IMqttCorrelationDataTypeConverter.cs +++ b/Source/Sholo.Mqtt/ModelBinding/TypeConverters/IMqttCorrelationDataTypeConverter.cs @@ -1,6 +1,9 @@ +using System; + namespace Sholo.Mqtt.ModelBinding.TypeConverters; [PublicAPI] -public interface IMqttCorrelationDataTypeConverter : IMqttTypeConverter +public interface IMqttCorrelationDataTypeConverter : IMqttTypeConverter { + bool TryConvertCorrelationData(byte[]? correlationData, Type targetType, out object? result); } diff --git a/Source/Sholo.Mqtt/ModelBinding/TypeConverters/IMqttParameterTypeConverter.cs b/Source/Sholo.Mqtt/ModelBinding/TypeConverters/IMqttParameterTypeConverter.cs deleted file mode 100644 index b103993..0000000 --- a/Source/Sholo.Mqtt/ModelBinding/TypeConverters/IMqttParameterTypeConverter.cs +++ /dev/null @@ -1,6 +0,0 @@ -namespace Sholo.Mqtt.ModelBinding.TypeConverters; - -[PublicAPI] -public interface IMqttParameterTypeConverter : IMqttTypeConverter -{ -} diff --git a/Source/Sholo.Mqtt/ModelBinding/TypeConverters/IMqttPayloadTypeConverter.cs b/Source/Sholo.Mqtt/ModelBinding/TypeConverters/IMqttPayloadTypeConverter.cs index 3702a69..0e37118 100644 --- a/Source/Sholo.Mqtt/ModelBinding/TypeConverters/IMqttPayloadTypeConverter.cs +++ b/Source/Sholo.Mqtt/ModelBinding/TypeConverters/IMqttPayloadTypeConverter.cs @@ -3,6 +3,7 @@ namespace Sholo.Mqtt.ModelBinding.TypeConverters; [PublicAPI] -public interface IMqttPayloadTypeConverter : IMqttTypeConverter> +public interface IMqttPayloadTypeConverter : IMqttTypeConverter { + bool TryConvertPayload(ArraySegment payload, Type targetType, out object? result); } diff --git a/Source/Sholo.Mqtt/ModelBinding/TypeConverters/IMqttTopicArgumentTypeConverter.cs b/Source/Sholo.Mqtt/ModelBinding/TypeConverters/IMqttTopicArgumentTypeConverter.cs new file mode 100644 index 0000000..fc0d5e6 --- /dev/null +++ b/Source/Sholo.Mqtt/ModelBinding/TypeConverters/IMqttTopicArgumentTypeConverter.cs @@ -0,0 +1,9 @@ +using System; + +namespace Sholo.Mqtt.ModelBinding.TypeConverters; + +[PublicAPI] +public interface IMqttTopicArgumentTypeConverter : IMqttTypeConverter +{ + bool TryConvertTopicArgument(string argument, Type targetType, out object? result); +} diff --git a/Source/Sholo.Mqtt/ModelBinding/TypeConverters/IMqttTypeConverter.cs b/Source/Sholo.Mqtt/ModelBinding/TypeConverters/IMqttTypeConverter.cs index b9da26f..4801281 100644 --- a/Source/Sholo.Mqtt/ModelBinding/TypeConverters/IMqttTypeConverter.cs +++ b/Source/Sholo.Mqtt/ModelBinding/TypeConverters/IMqttTypeConverter.cs @@ -1,9 +1,6 @@ -using System; - namespace Sholo.Mqtt.ModelBinding.TypeConverters; [PublicAPI] -public interface IMqttTypeConverter +public interface IMqttTypeConverter { - bool TryConvert(TInput? input, Type targetType, out object? result); } diff --git a/Source/Sholo.Mqtt/ModelBinding/TypeConverters/IMqttUserPropertiesTypeConverter.cs b/Source/Sholo.Mqtt/ModelBinding/TypeConverters/IMqttUserPropertiesTypeConverter.cs new file mode 100644 index 0000000..2f981c2 --- /dev/null +++ b/Source/Sholo.Mqtt/ModelBinding/TypeConverters/IMqttUserPropertiesTypeConverter.cs @@ -0,0 +1,11 @@ +using System; +using System.Collections.Generic; +using Microsoft.Extensions.Primitives; + +namespace Sholo.Mqtt.ModelBinding.TypeConverters; + +[PublicAPI] +public interface IMqttUserPropertiesTypeConverter : IMqttTypeConverter +{ + bool TryConvertUserPropertyValues(StringValues? values, Type targetType, out IList? result); +} diff --git a/Source/Sholo.Mqtt/ModelBinding/TypeConverters/Json/JsonTypeConverter.cs b/Source/Sholo.Mqtt/ModelBinding/TypeConverters/Json/JsonTypeConverter.cs index 4415c56..a016cbc 100644 --- a/Source/Sholo.Mqtt/ModelBinding/TypeConverters/Json/JsonTypeConverter.cs +++ b/Source/Sholo.Mqtt/ModelBinding/TypeConverters/Json/JsonTypeConverter.cs @@ -20,8 +20,11 @@ ILogger logger Logger = logger; } - 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); + + public bool TryConvertCorrelationData(byte[]? correlationData, Type targetType, out object? result) + => TryConvert(correlationData, targetType, out result); public bool TryConvert(byte[]? input, Type targetType, out object? result) { diff --git a/Source/Sholo.Mqtt/ModelBinding/TypeConverters/Json/SnakeCaseNamingPolicy.cs b/Source/Sholo.Mqtt/ModelBinding/TypeConverters/Json/SnakeCaseNamingPolicy.cs index dda1180..4a6b6fc 100644 --- a/Source/Sholo.Mqtt/ModelBinding/TypeConverters/Json/SnakeCaseNamingPolicy.cs +++ b/Source/Sholo.Mqtt/ModelBinding/TypeConverters/Json/SnakeCaseNamingPolicy.cs @@ -33,7 +33,7 @@ private ReadOnlySpan ToSnakeCase(string name) { var upperCaseCushionLength = name[1..].Count(t => t is >= 'A' and <= 'Z'); var remainingLength = name.Length - upperCaseCushionLength; - var bufferSize = upperCaseCushionLength * 2 + remainingLength; + var bufferSize = (upperCaseCushionLength * 2) + remainingLength; var buffer = new char[bufferSize]; var bufferPosition = 0; @@ -70,7 +70,7 @@ private ReadOnlySpan ToSnakeCase(string name) var isPreviousLower = spanName[position - 1] > 96 && spanName[position - 1] < 123; var isPreviousNumber = spanName[position - 1] > 47 && spanName[position - 1] < 58; - if (isCurrentUpper && (isPreviousLower || isPreviousNumber || isNextLower || isNextSpace || isNextLower && !isPreviousSpace)) + if (isCurrentUpper && (isPreviousLower || isPreviousNumber || isNextLower || isNextSpace || (isNextLower && !isPreviousSpace))) { buffer[bufferPosition] = '_'; bufferPosition++; diff --git a/Source/Sholo.Mqtt/ModelBinding/TypeConverters/LambdaMqttParameterTypeConverter.cs b/Source/Sholo.Mqtt/ModelBinding/TypeConverters/LambdaMqttParameterTypeConverter.cs index 0c1496a..3d9dc2d 100644 --- a/Source/Sholo.Mqtt/ModelBinding/TypeConverters/LambdaMqttParameterTypeConverter.cs +++ b/Source/Sholo.Mqtt/ModelBinding/TypeConverters/LambdaMqttParameterTypeConverter.cs @@ -1,27 +1,46 @@ using System; +using System.Collections.Generic; +using Microsoft.Extensions.Primitives; namespace Sholo.Mqtt.ModelBinding.TypeConverters; -internal class LambdaMqttParameterTypeConverter : IMqttParameterTypeConverter +internal class LambdaMqttParameterTypeConverter : IMqttUserPropertiesTypeConverter { - private Func Converter { get; } + private Func Converter { get; } - public LambdaMqttParameterTypeConverter(Func converter) + public LambdaMqttParameterTypeConverter(Func converter) { Converter = converter; } - public bool TryConvertParameter(string? value, Type targetType, out object? result) + public bool TryConvertUserPropertyValues(StringValues? values, Type targetType, out IList? result) { + var results = new List(); try { - var (success, typedResult) = Converter.Invoke(value); - result = typedResult; - return success; + var allSuccess = true; + var (success, typedResult) = Converter.Invoke(values); + if (success) + { + results.Add(typedResult); + } + else + { + allSuccess = false; + } + + if (allSuccess) + { + result = results; + return true; + } + + result = null; + return false; } catch { - result = default; + result = null; return false; } } diff --git a/Source/Sholo.Mqtt/ModelBinding/Validation/MqttValidationStateDictionary.cs b/Source/Sholo.Mqtt/ModelBinding/Validation/MqttValidationStateDictionary.cs index 504da49..cace81c 100644 --- a/Source/Sholo.Mqtt/ModelBinding/Validation/MqttValidationStateDictionary.cs +++ b/Source/Sholo.Mqtt/ModelBinding/Validation/MqttValidationStateDictionary.cs @@ -50,7 +50,6 @@ MqttValidationStateEntry IDictionary.this[obje /// The key of the item to get MqttValidationStateEntry IReadOnlyDictionary.this[object key] => this[key]!; - /// public int Count => _inner.Count; /// diff --git a/Source/Sholo.Mqtt/ModelBinding/ValueProviders/CancellationTokenModelBinder.cs b/Source/Sholo.Mqtt/ModelBinding/ValueProviders/CancellationTokenModelBinder.cs index 288b341..871744c 100644 --- a/Source/Sholo.Mqtt/ModelBinding/ValueProviders/CancellationTokenModelBinder.cs +++ b/Source/Sholo.Mqtt/ModelBinding/ValueProviders/CancellationTokenModelBinder.cs @@ -1,7 +1,6 @@ -using System.Reflection; - namespace Sholo.Mqtt.ModelBinding.ValueProviders; +/* [PublicAPI] public class CancellationTokenModelBinder : IMqttModelBinder { @@ -10,3 +9,4 @@ public MqttValueProviderResult GetValue(IMqttModelBindingContext mqttModelBindin return new MqttValueProviderResult(mqttModelBindingContext.Request.ShutdownToken); } } +*/ diff --git a/Source/Sholo.Mqtt/ModelBinding/ValueProviders/IMqttCorrelationDataValueProvider.cs b/Source/Sholo.Mqtt/ModelBinding/ValueProviders/IMqttCorrelationDataValueProvider.cs index 7be4c6b..d564a9a 100644 --- a/Source/Sholo.Mqtt/ModelBinding/ValueProviders/IMqttCorrelationDataValueProvider.cs +++ b/Source/Sholo.Mqtt/ModelBinding/ValueProviders/IMqttCorrelationDataValueProvider.cs @@ -3,5 +3,5 @@ namespace Sholo.Mqtt.ModelBinding.ValueProviders; [PublicAPI] public interface IMqttCorrelationDataValueProvider { - public byte[]? GetCorrelationData(IMqttModelBindingContext mqttModelBindingContext); + public byte[]? GetCorrelationData(IMqttModelBindingContext modelBindingContext, IMqttRequestContext requestContext); } diff --git a/Source/Sholo.Mqtt/ModelBinding/ValueProviders/IMqttPayloadValueProvider.cs b/Source/Sholo.Mqtt/ModelBinding/ValueProviders/IMqttPayloadValueProvider.cs index ecca437..3d90716 100644 --- a/Source/Sholo.Mqtt/ModelBinding/ValueProviders/IMqttPayloadValueProvider.cs +++ b/Source/Sholo.Mqtt/ModelBinding/ValueProviders/IMqttPayloadValueProvider.cs @@ -1,8 +1,6 @@ -using System; - namespace Sholo.Mqtt.ModelBinding.ValueProviders; [PublicAPI] -public interface IMqttPayloadValueProvider : IMqttValueProvider> +public interface IMqttPayloadValueProvider : IMqttValueProvider { } diff --git a/Source/Sholo.Mqtt/ModelBinding/ValueProviders/IMqttValueProvider.cs b/Source/Sholo.Mqtt/ModelBinding/ValueProviders/IMqttValueProvider.cs index ee894c4..fd35569 100644 --- a/Source/Sholo.Mqtt/ModelBinding/ValueProviders/IMqttValueProvider.cs +++ b/Source/Sholo.Mqtt/ModelBinding/ValueProviders/IMqttValueProvider.cs @@ -1,7 +1,8 @@ using System.Reflection; namespace Sholo.Mqtt.ModelBinding.ValueProviders; + public interface IMqttValueProvider { - MqttValueProviderResult GetValue(IMqttModelBindingContext mqttModelBindingContext, ParameterInfo actionParameter, out object? value); + MqttValueProviderResult GetValue(IMqttModelBindingContext mqttModelBindingContext, IMqttRequestContext requestContext, ParameterInfo actionParameter, out object? value); } diff --git a/Source/Sholo.Mqtt/ModelBinding/ValueProviders/MqttCorrelationDataValueProvider.cs b/Source/Sholo.Mqtt/ModelBinding/ValueProviders/MqttCorrelationDataValueProvider.cs index 8358715..5416f49 100644 --- a/Source/Sholo.Mqtt/ModelBinding/ValueProviders/MqttCorrelationDataValueProvider.cs +++ b/Source/Sholo.Mqtt/ModelBinding/ValueProviders/MqttCorrelationDataValueProvider.cs @@ -3,5 +3,5 @@ namespace Sholo.Mqtt.ModelBinding.ValueProviders; [PublicAPI] public class MqttCorrelationDataValueProvider : IMqttCorrelationDataValueProvider { - public byte[]? GetValueSource(IMqttModelBindingContext mqttModelBindingContext) => mqttModelBindingContext.Request.CorrelationData; + public byte[]? GetCorrelationData(IMqttModelBindingContext modelBindingContext, IMqttRequestContext requestContext) => requestContext.CorrelationData; } diff --git a/Source/Sholo.Mqtt/ModelBinding/ValueProviders/MqttPayloadValueProvider.cs b/Source/Sholo.Mqtt/ModelBinding/ValueProviders/MqttPayloadValueProvider.cs index f0e5278..9c26695 100644 --- a/Source/Sholo.Mqtt/ModelBinding/ValueProviders/MqttPayloadValueProvider.cs +++ b/Source/Sholo.Mqtt/ModelBinding/ValueProviders/MqttPayloadValueProvider.cs @@ -1,8 +1,12 @@ -using System; +using System.Reflection; namespace Sholo.Mqtt.ModelBinding.ValueProviders; public class MqttPayloadValueProvider : IMqttPayloadValueProvider { - public ArraySegment GetValueSource(IMqttModelBindingContext mqttModelBindingContext) => mqttModelBindingContext.Request.Payload; + public MqttValueProviderResult GetValue(IMqttModelBindingContext mqttModelBindingContext, IMqttRequestContext requestContext, ParameterInfo actionParameter, out object? value) + { + value = requestContext.Payload; + return MqttValueProviderResult.None; + } } diff --git a/Source/Sholo.Mqtt/ModelBinding/ValueProviders/MqttTopicArgumentValueProvider.cs b/Source/Sholo.Mqtt/ModelBinding/ValueProviders/MqttTopicArgumentValueProvider.cs index 9259dcb..6f8896c 100644 --- a/Source/Sholo.Mqtt/ModelBinding/ValueProviders/MqttTopicArgumentValueProvider.cs +++ b/Source/Sholo.Mqtt/ModelBinding/ValueProviders/MqttTopicArgumentValueProvider.cs @@ -1,7 +1,6 @@ -using System.Reflection; - namespace Sholo.Mqtt.ModelBinding.ValueProviders; +/* public class MqttTopicArgumentValueProvider : IMqttTopicArgumentValueProvider { public string ParameterName { get; } @@ -11,18 +10,14 @@ public MqttTopicArgumentValueProvider(string parameterName) ParameterName = parameterName; } - public string[]? GetValueSource(IMqttModelBindingContext mqttModelBindingContext) + public MqttValueProviderResult GetValue(IMqttModelBindingContext mqttModelBindingContext, IMqttRequestContext requestContext, ParameterInfo actionParameter, out object? value) { - if (mqttModelBindingContext.TopicArguments.TryGetValue(ParameterName, out var values)) + if (requestContext.TopicArguments.TryGetValue(ParameterName, out var values)) { return values; } return null; } - - public MqttValueProviderResult GetValue(IMqttModelBindingContext mqttModelBindingContext, ParameterInfo actionParameter, out object? value) - { - throw new System.NotImplementedException(); - } } +*/ diff --git a/Source/Sholo.Mqtt/ModelBinding/ValueProviders/MqttUserPropertyValueProvider.cs b/Source/Sholo.Mqtt/ModelBinding/ValueProviders/MqttUserPropertyValueProvider.cs index dedcc26..f65d246 100644 --- a/Source/Sholo.Mqtt/ModelBinding/ValueProviders/MqttUserPropertyValueProvider.cs +++ b/Source/Sholo.Mqtt/ModelBinding/ValueProviders/MqttUserPropertyValueProvider.cs @@ -1,8 +1,6 @@ -using System; -using System.Linq; - namespace Sholo.Mqtt.ModelBinding.ValueProviders; +/* public class MqttUserPropertyValueProvider : IMqttUserPropertyValueProvider { public string PropertyName { get; } @@ -22,3 +20,4 @@ public string[] GetValueSource(IMqttModelBindingContext mqttModelBindingContext) .ToArray(); } } +*/ diff --git a/Source/Sholo.Mqtt/ModelBinding/ValueProviders/ServiceValueProvider.cs b/Source/Sholo.Mqtt/ModelBinding/ValueProviders/ServiceValueProvider.cs deleted file mode 100644 index ff9f69f..0000000 --- a/Source/Sholo.Mqtt/ModelBinding/ValueProviders/ServiceValueProvider.cs +++ /dev/null @@ -1,39 +0,0 @@ -using System; -using System.Linq; -using System.Reflection; -using Microsoft.Extensions.DependencyInjection; - -namespace Sholo.Mqtt.ModelBinding.ValueProviders; - -[PublicAPI] -public class ServiceValueProvider : IMqttValueProvider -{ - public virtual bool TryGetValue(IMqttModelBindingContext mqttModelBindingContext, ParameterInfo actionParameter, out object? value) - { - var parameterType = actionParameter.ParameterType; - - if (parameterType.IsEnum || - parameterType.IsPrimitive || - parameterType == typeof(string) || - Nullable.GetUnderlyingType(parameterType) != null) - { - value = null; - return false; - } - - // TODO: test with IEnumerable. Do we want Last? - - var argumentValues = mqttModelBindingContext.Request.ServiceProvider - .GetServices(parameterType) - .ToArray(); - - if (argumentValues.Length == 0) - { - value = null; - return false; - } - - value = argumentValues.Last(); - return true; - } -} diff --git a/Source/Sholo.Mqtt/MqttRequestContext.cs b/Source/Sholo.Mqtt/MqttRequestContext.cs index 54417dc..b8f34a3 100644 --- a/Source/Sholo.Mqtt/MqttRequestContext.cs +++ b/Source/Sholo.Mqtt/MqttRequestContext.cs @@ -1,12 +1,17 @@ using System; +using System.Collections.Generic; +using System.Collections.ObjectModel; +using System.Linq; using System.Threading; using System.Threading.Tasks; using Microsoft.Extensions.DependencyInjection; 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; @@ -18,7 +23,7 @@ internal class MqttRequestContext : IMqttRequestContext public ArraySegment Payload { get; } public MqttQualityOfServiceLevel QualityOfServiceLevel { get; } public bool Retain { get; } - public MqttUserProperty[] UserProperties { get; } = null!; + public IReadOnlyDictionary UserProperties { get; } = null!; public string? ContentType { get; } public bool Dup { get; } public string? ResponseTopic { get; } @@ -29,6 +34,7 @@ internal class MqttRequestContext : IMqttRequestContext public uint[]? SubscriptionIdentifiers { get; } public string ClientId { get; } = null!; public CancellationToken ShutdownToken => ShutdownTokenFactory.Value; + public IMqttModelBindingResult? ModelBindingResult { get; set; } private Lazy ClientFactory { get; } private Lazy HostApplicationLifetimeFactory { get; } @@ -36,27 +42,6 @@ internal class MqttRequestContext : IMqttRequestContext private IManagedMqttClient Client => ClientFactory.Value; - public MqttRequestContext(MqttRequestContext context) - : this( - context.ServiceProvider, - context.Topic, - context.Payload, - context.QualityOfServiceLevel, - context.Retain, - context.UserProperties, - context.ContentType, - context.Dup, - context.ResponseTopic, - context.PayloadFormatIndicator, - context.MessageExpiryInterval, - context.TopicAlias, - context.CorrelationData, - context.SubscriptionIdentifiers, - context.ClientId - ) - { - } - internal MqttRequestContext(IServiceProvider serviceProvider, MqttApplicationMessage message, string clientId) : this( serviceProvider, @@ -73,7 +58,8 @@ internal MqttRequestContext(IServiceProvider serviceProvider, MqttApplicationMes message.TopicAlias, message.CorrelationData, message.SubscriptionIdentifiers?.ToArray(), - clientId + clientId, + StringComparer.Ordinal // TODO: Configuration ) { } @@ -93,15 +79,30 @@ internal MqttRequestContext( ushort? topicAlias, byte[]? correlationData, uint[]? subscriptionIdentifiers, - string clientId - ) + string clientId, + IEqualityComparer? userPropertiesKeyEqualityComparer) : this(serviceProvider) { Topic = topic; Payload = payload; QualityOfServiceLevel = qualityOfServiceLevel; Retain = retain; - UserProperties = userProperties ?? Array.Empty(); + + userProperties ??= Array.Empty(); + userPropertiesKeyEqualityComparer ??= StringComparer.Ordinal; + UserProperties = new ReadOnlyDictionary( + userProperties + .GroupBy( + x => x.Name, + userPropertiesKeyEqualityComparer + ) + .ToDictionary( + x => x.Key, + x => new StringValues(x.Select(y => y.Value).ToArray()), + userPropertiesKeyEqualityComparer + ) + ); + ContentType = contentType; Dup = dup; ResponseTopic = responseTopic; diff --git a/Source/Sholo.Mqtt/MqttRequestContextExtensions.cs b/Source/Sholo.Mqtt/MqttRequestContextExtensions.cs index b778102..ed8335e 100644 --- a/Source/Sholo.Mqtt/MqttRequestContextExtensions.cs +++ b/Source/Sholo.Mqtt/MqttRequestContextExtensions.cs @@ -1,7 +1,6 @@ using System; using System.Threading; using System.Threading.Tasks; -using Microsoft.Extensions.DependencyInjection; using MQTTnet; namespace Sholo.Mqtt; @@ -52,10 +51,4 @@ public static Task RespondAsync( return requestContext.RespondAsync(message, cancellationToken); } - - internal static Endpoint? GetEndpoint(this IMqttRequestContext context) - { - var routeProvider = context.ServiceProvider.GetRequiredService(); - return routeProvider.GetEndpoint(context); - } } diff --git a/Source/Sholo.Mqtt/MqttRequestDelegate.cs b/Source/Sholo.Mqtt/MqttRequestDelegate.cs index a467d91..26ef284 100644 --- a/Source/Sholo.Mqtt/MqttRequestDelegate.cs +++ b/Source/Sholo.Mqtt/MqttRequestDelegate.cs @@ -3,6 +3,8 @@ namespace Sholo.Mqtt; +// TODO: CancellationToken + /// /// A function that can process an MQTT request. /// diff --git a/Source/Sholo.Mqtt/RouteProvider.cs b/Source/Sholo.Mqtt/RouteProvider.cs deleted file mode 100644 index 2b6be35..0000000 --- a/Source/Sholo.Mqtt/RouteProvider.cs +++ /dev/null @@ -1,255 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Diagnostics.CodeAnalysis; -using System.Linq; -using System.Reflection; -using System.Threading.Tasks; -using Microsoft.Extensions.DependencyInjection; -using Microsoft.Extensions.Logging; -using Sholo.Mqtt.Controllers; -using Sholo.Mqtt.Internal; -using Sholo.Mqtt.ModelBinding; -using Sholo.Mqtt.Topics.Filter; -using Sholo.Mqtt.Topics.FilterBuilder; - -namespace Sholo.Mqtt; - -public class RouteProvider : IRouteProvider -{ - public Endpoint[] Endpoints { get; } - - private IMqttModelBinder ModelBinder { get; } - private IControllerActivator ControllerActivator { get; } - - public Endpoint? GetEndpoint(IMqttRequestContext context) - { - return Endpoints.FirstOrDefault(endpoint => endpoint.IsMatch(context)); - } - - public RouteProvider( - IMqttModelBinder modelBinder, - IControllerActivator controllerActivator, - IEnumerable mqttApplicationParts) - { - ModelBinder = modelBinder; - ControllerActivator = controllerActivator; - - var assemblies = AppDomain.CurrentDomain.GetAssemblies() - .Where(asm => asm.GetCustomAttributes().Any()) - .Union(mqttApplicationParts.Select(ap => ap.Assembly)) - .Distinct(); - - var controllers = assemblies - .SelectMany(asm => asm - .GetExportedTypes() - .Where(c => c.IsClass) - .Where(c => c.IsPublic) - .Where(c => c.GetCustomAttributes().Any())); - - var endpoints = controllers - .SelectMany(ctrl => ctrl - .GetMethods() - .Where(m => m.IsPublic) - .Where(m => !m.IsGenericMethod) - .Where(m => m.ReturnType == typeof(bool) || m.ReturnType == typeof(Task)) - .Select(m => GetEndpoint(ctrl, m))) - .Where(x => x != null) - .Select(x => x!) - .ToArray(); - - Endpoints = endpoints; - } - - [ExcludeFromCodeCoverage] - private Endpoint? GetEndpoint( - Type controller, - MethodInfo action) - { - var topicPrefixAttribute = controller.GetCustomAttribute(); - - var topicAttribute = action.GetCustomAttribute() ?? - controller.GetCustomAttribute(); - - if (topicAttribute == null) - { - return null; - } - - var noLocalAttribute = action.GetCustomAttribute() ?? - controller.GetCustomAttribute(); - - var qualityOfServiceAttribute = action.GetCustomAttribute() ?? - controller.GetCustomAttribute(); - - var retainAsPublishedAttribute = action.GetCustomAttribute() ?? - controller.GetCustomAttribute(); - - var retainHandlingAttribute = action.GetCustomAttribute() ?? - controller.GetCustomAttribute(); - - var topicFilter = CreateTopicFilter( - topicPrefixAttribute, - topicAttribute, - noLocalAttribute, - qualityOfServiceAttribute, - retainAsPublishedAttribute, - retainHandlingAttribute); - - var controllerName = controller.Name.EndsWith("Controller", StringComparison.Ordinal) - ? controller.Name[..^"Controller".Length] - : controller.Name; - - var requestDelegate = CreateControllerRequestDelegate( - controller, - action, - controllerName, - topicFilter - ); - - return new Endpoint( - action, - topicFilter, - requestDelegate); - } - - [ExcludeFromCodeCoverage] - private MqttRequestDelegate CreateAnonymousRequestDelegate( - MethodInfo action, - IMqttTopicFilter topicFilter) - { - return async requestContext => - { - if (!ModelBinder.TryPerformModelBinding(requestContext, topicFilter, action, out var actionArguments)) - { - return false; - } - - var arguments = actionArguments.Values.ToArray(); - var logger = requestContext.ServiceProvider.GetService>(); - - Func> actionRunner; - if (action.ReturnType == typeof(Task)) - { - actionRunner = () => (Task)action.Invoke(null, arguments)!; - } - else if (action.ReturnType == typeof(bool)) - { - actionRunner = () => Task.FromResult((bool)action.Invoke(null, arguments)!); - } - else - { - throw new InvalidOperationException("Expecting either a Task or a bool return type"); - } - - using var scope = logger?.BeginScope(new Dictionary - { - ["TopicPattern"] = topicFilter.TopicPattern - }); - - return await actionRunner.Invoke(); - }; - } - - [ExcludeFromCodeCoverage] - private MqttRequestDelegate CreateControllerRequestDelegate( - Type controllerType, - MethodInfo action, - string controllerName, - IMqttTopicFilter topicFilter) - { - return async requestContext => - { - if (!ModelBinder.TryPerformModelBinding(requestContext, topicFilter, action, out var actionArguments)) - { - return false; - } - - var controllerInstance = ControllerActivator.Create(requestContext, controllerType); - if (controllerInstance is MqttControllerBase controllerBase) - { - controllerBase.Request = requestContext; - } - - var arguments = actionArguments.Values.ToArray(); - var logger = requestContext.ServiceProvider.GetService>(); - - Func> actionRunner; - if (action.ReturnType == typeof(Task)) - { - actionRunner = () => (Task)action.Invoke(controllerInstance, arguments)!; - } - else if (action.ReturnType == typeof(bool)) - { - actionRunner = () => Task.FromResult((bool)action.Invoke(controllerInstance, arguments)!); - } - else - { - throw new InvalidOperationException("Expecting either a Task or a bool return type"); - } - - try - { - using var scope = logger?.BeginScope(new Dictionary - { - ["TopicPattern"] = topicFilter.TopicPattern, - ["Controller"] = controllerName, - ["ActionName"] = action.Name - }); - return await actionRunner.Invoke(); - } - finally - { - await ControllerActivator.ReleaseAsync(requestContext, controllerInstance); - } - }; - } - - [ExcludeFromCodeCoverage] - private IMqttTopicFilter CreateTopicFilter( - TopicPrefixAttribute? topicPrefixAttribute, - TopicAttribute topicAttribute, - NoLocalAttribute? noLocalAttribute, - QualityOfServiceAttribute? qualityOfServiceAttribute, - RetainAsPublishedAttribute? retainAsPublishedAttribute, - RetainHandlingAttribute? retainHandlingAttribute) - { - var topicPrefix = topicPrefixAttribute?.TopicPrefix.TrimEnd('/'); - var topicPattern = topicAttribute.TopicPattern.TrimStart('/'); - var effectiveTopicPattern = !string.IsNullOrEmpty(topicPrefix) - ? $"{topicPrefix}/{topicPattern}" - : topicPattern; - - var noLocal = noLocalAttribute?.NoLocal; - var qualityOfServiceLevel = qualityOfServiceAttribute?.QualityOfServiceLevel; - var retainAsPublished = retainAsPublishedAttribute?.RetainAsPublished; - var retainHandling = retainHandlingAttribute?.RetainHandling; - - var mqttTopicFilterBuilder = new MqttTopicFilterBuilder(); - - if (noLocal.HasValue) - { - mqttTopicFilterBuilder.WithNoLocal(noLocal.Value); - } - - if (qualityOfServiceLevel.HasValue) - { - mqttTopicFilterBuilder.WithQualityOfServiceLevel(qualityOfServiceLevel.Value); - } - - if (retainAsPublished.HasValue) - { - mqttTopicFilterBuilder.WithRetainAsPublished(retainAsPublished.Value); - } - - if (retainHandling.HasValue) - { - mqttTopicFilterBuilder.WithRetainHandling(retainHandling.Value); - } - - mqttTopicFilterBuilder.WithTopicPattern(effectiveTopicPattern); - - var mqttTopicFilter = mqttTopicFilterBuilder.Build(); - - return mqttTopicFilter; - } -} diff --git a/Source/Sholo.Mqtt/Routing/AnonymousRouteProvider.cs b/Source/Sholo.Mqtt/Routing/AnonymousRouteProvider.cs new file mode 100644 index 0000000..c5c391d --- /dev/null +++ b/Source/Sholo.Mqtt/Routing/AnonymousRouteProvider.cs @@ -0,0 +1,64 @@ +namespace Sholo.Mqtt.Routing; + +/* +public class AnonymousRouteProvider : BaseRouteProvider +{ + public AnonymousRouteProvider(Endpoint endpoint) + { + Endpoints = new[] { endpoint }; + } + + public override Endpoint? GetEndpoint(IMqttRequestContext requestContext) + { + throw new NotImplementedException(); + } + + private Endpoint? CreateEndpoint( + TypeInfo? instance, + MethodInfo action) + { + if (!TryCreateTopicFilterFromAttributes(instance, action, out var topicFilter)) + { + return null; + } + + var requestDelegate = CreateAnonymousRequestDelegate( + instance, + action, + topicFilter + ); + + return new Endpoint( + instance, + action, + topicFilter, + requestDelegate + ); + } + + private MqttRequestDelegate CreateAnonymousRequestDelegate( + Type? instance, + MethodInfo action, + IMqttTopicFilter topicFilter) + { + return async requestContext => + { + if (requestContext.ModelBindingResult is not { Success: true }) + { + throw new InvalidOperationException("Model binding did not complete successfully"); + } + + var logger = requestContext.ServiceProvider.GetService>(); + var requestDelegate = requestContext.GetRequestDelegate(instance); + + using var scope = logger?.BeginScope(new Dictionary + { + ["TopicPattern"] = topicFilter.TopicPattern, + ["ActionName"] = action.Name + }); + + return await requestDelegate.Invoke(requestContext); + }; + } +} +*/ diff --git a/Source/Sholo.Mqtt/Routing/MqttApplicationBuilderExtensions.cs b/Source/Sholo.Mqtt/Routing/MqttApplicationBuilderExtensions.cs new file mode 100644 index 0000000..142f306 --- /dev/null +++ b/Source/Sholo.Mqtt/Routing/MqttApplicationBuilderExtensions.cs @@ -0,0 +1,53 @@ +using Sholo.Mqtt.Application.Builder; +using Sholo.Mqtt.Middleware; + +namespace Sholo.Mqtt.Routing; + +[PublicAPI] +public static class MqttApplicationBuilderExtensions +{ + public static IMqttApplicationBuilder UseRouting(this IMqttApplicationBuilder mqttApplicationBuilder) + { + return mqttApplicationBuilder.UseMiddleware(); + } +} + +/* +[PublicAPI] +public static class MqttApplicationBuilderExtensions +{ + public static IMqttApplicationBuilder UseRouting(this IMqttApplicationBuilder mqttApplicationBuilder) + { + return mqttApplicationBuilder.UseMiddleware(); + } + + public static IMqttApplicationBuilder Use(this IMqttApplicationBuilder mqttApplicationBuilder, Action topicFilterBuilderConfig, MqttRequestDelegate requestDelegate) + { + var mqttTopicFilterBuilder = new MqttTopicFilterBuilder(); + topicFilterBuilderConfig.Invoke(mqttTopicFilterBuilder); + var mqttTopicFilter = mqttTopicFilterBuilder.Build(); + // var endpoint = new Endpoint(null, requestDelegate.Method, mqttTopicFilter, ctx => ctx.ModelBindingResult.Invoke()) + + /* + new AnonymousRouteProvider() + var endpoint = new Endpoint(null, requestDelegate.Method, mqttTopicFilter, ) + // mqttApplicationBuilder.UseMiddleware(new Anon); + // mqttApplicationBuilder.UseMiddleware(); + * / + } +} + +public class AnonymousRouteProvider : IRouteProvider +{ + public IMqttTopicFilter[] TopicFilters { get; } + public Endpoint? GetEndpoint(IMqttRequestContext requestContext) + { + throw new NotImplementedException(); + } + + public AnonymousRouteProvider(IMqttTopicFilter topicFilter) + { + TopicFilters = new[] { topicFilter }; + } +} +*/ diff --git a/Source/Sholo.Mqtt/Routing/RouteProvider.cs b/Source/Sholo.Mqtt/Routing/RouteProvider.cs new file mode 100644 index 0000000..339cd87 --- /dev/null +++ b/Source/Sholo.Mqtt/Routing/RouteProvider.cs @@ -0,0 +1,154 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Reflection; +using System.Threading.Tasks; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Logging; +using Sholo.Mqtt.Controllers; +using Sholo.Mqtt.Internal; +using Sholo.Mqtt.ModelBinding; +using Sholo.Mqtt.Topics.Filter; +using Sholo.Mqtt.Topics.FilterBuilder; + +namespace Sholo.Mqtt.Routing; + +public class RouteProvider : IRouteProvider +{ + public IMqttTopicFilter[] TopicFilters { get; } + private Endpoint[] Endpoints { get; } + + public Endpoint? GetEndpoint(IMqttRequestContext requestContext) + { + var modelBinder = requestContext.ServiceProvider.GetRequiredService(); + + foreach (var endpoint in Endpoints) + { + if (!endpoint.TopicFilter.IsMatch(requestContext, out var topicArguments)) + { + continue; + } + + modelBinder.TryPerformModelBinding(endpoint, requestContext, topicArguments); + + if (requestContext.ModelBindingResult is { Success: true }) + { + return endpoint; + } + } + + return null; + } + + public RouteProvider(IEnumerable mqttApplicationParts) + { + Endpoints = BuildEndpoints(mqttApplicationParts); + TopicFilters = Endpoints.Select(x => x.TopicFilter).ToArray(); + } + + private Endpoint[] BuildEndpoints(IEnumerable mqttApplicationParts) + { + var assemblies = AppDomain.CurrentDomain.GetAssemblies() + .Where(asm => asm.GetCustomAttributes().Any()) + .Union(mqttApplicationParts.Select(ap => ap.Assembly)) + .Distinct(); + + var controllers = assemblies + .SelectMany(asm => asm + .GetExportedTypes() + .Where(c => c.IsClass) + .Where(c => c.IsPublic) + .Where(c => c.GetCustomAttributes().Any())); + + var endpoints = controllers + .SelectMany(ctrl => ctrl + .GetMethods() + .Where(m => m.IsPublic) + .Where(m => !m.IsGenericMethod) + .Where(m => m.ReturnType == typeof(bool) || m.ReturnType == typeof(Task)) + .Select(m => CreateEndpoint(ctrl.GetTypeInfo(), m))) + .Where(x => x != null) + .Select(x => x!) + .ToArray(); + + return endpoints; + } + + private Endpoint CreateEndpoint( + TypeInfo instance, + MethodInfo action) + { + if (instance == null) + { + throw new ArgumentException("An instance is required for controller-based routes"); + } + + var mqttTopicFilter = MqttTopicFilterBuilder + .FromActionAttributes(instance, action) + .Build(); + + var controllerName = instance.Name.EndsWith("Controller", StringComparison.Ordinal) + ? instance.Name[..^"Controller".Length] + : instance.Name; + + var requestDelegate = CreateRequestDelegate( + instance, + action, + controllerName, + mqttTopicFilter + ); + + return new Endpoint( + instance, + action, + mqttTopicFilter, + requestDelegate + ); + } + + private MqttRequestDelegate CreateRequestDelegate( + Type controllerType, + MethodInfo action, + string controllerName, + IMqttTopicFilter topicFilter) + { + return async requestContext => + { + if (requestContext.ModelBindingResult is not { Success: true }) + { + throw new InvalidOperationException("Model binding did not complete successfully"); + } + + object? controllerInstance = null; + var controllerActivator = requestContext.ServiceProvider.GetRequiredService(); + + try + { + controllerInstance = controllerActivator.Create(requestContext, controllerType); + if (controllerInstance is MqttControllerBase controllerBase) + { + controllerBase.Request = requestContext; + } + + var logger = requestContext.ServiceProvider.GetService>(); + var requestDelegate = requestContext.GetRequestDelegate(controllerInstance); + + using var scope = logger?.BeginScope(new Dictionary + { + ["TopicPattern"] = topicFilter.TopicPattern, + ["Controller"] = controllerName, + ["ActionName"] = action.Name + }); + + return await requestDelegate.Invoke(requestContext); + } + finally + { + if (controllerInstance != null) + { + await controllerActivator.ReleaseAsync(requestContext, controllerInstance); + } + } + }; + } +} diff --git a/Source/Sholo.Mqtt/Middleware/RoutingMiddleware.cs b/Source/Sholo.Mqtt/Routing/RoutingMiddleware.cs similarity index 62% rename from Source/Sholo.Mqtt/Middleware/RoutingMiddleware.cs rename to Source/Sholo.Mqtt/Routing/RoutingMiddleware.cs index 3d4b409..8eccbd9 100644 --- a/Source/Sholo.Mqtt/Middleware/RoutingMiddleware.cs +++ b/Source/Sholo.Mqtt/Routing/RoutingMiddleware.cs @@ -1,13 +1,16 @@ using System.Threading.Tasks; +using Microsoft.Extensions.DependencyInjection; +using Sholo.Mqtt.Middleware; -namespace Sholo.Mqtt.Middleware; +namespace Sholo.Mqtt.Routing; [PublicAPI] public class RoutingMiddleware : IMqttMiddleware { public async Task InvokeAsync(IMqttRequestContext context, MqttRequestDelegate next) { - var endpoint = context.GetEndpoint(); + var routeProvider = context.ServiceProvider.GetService(); + var endpoint = routeProvider?.GetEndpoint(context); var requestDelegate = endpoint?.RequestDelegate; if (requestDelegate == null) diff --git a/Source/Sholo.Mqtt/ServiceCollectionExtensions.cs b/Source/Sholo.Mqtt/ServiceCollectionExtensions.cs index f70b308..6bdd937 100644 --- a/Source/Sholo.Mqtt/ServiceCollectionExtensions.cs +++ b/Source/Sholo.Mqtt/ServiceCollectionExtensions.cs @@ -1,3 +1,5 @@ +using System.Collections.Generic; +using System.Linq; using System.Security.Authentication; using System.Security.Cryptography.X509Certificates; using Microsoft.Extensions.DependencyInjection; @@ -12,6 +14,8 @@ using Sholo.Mqtt.DependencyInjection; using Sholo.Mqtt.Internal; using Sholo.Mqtt.ModelBinding; +using Sholo.Mqtt.ModelBinding.BindingProviders; +using Sholo.Mqtt.Routing; using Sholo.Mqtt.Settings; namespace Sholo.Mqtt; @@ -33,7 +37,8 @@ public static IMqttServiceCollection AddMqttServices(this IServic var mqttClientOptionsBuilder = new MqttClientOptionsBuilder() .WithTcpServer(mqttSettings.Host, mqttSettings.Port) - .WithProtocolVersion(mqttSettings.MqttProtocolVersion ?? MqttProtocolVersion.V500); + .WithProtocolVersion(mqttSettings.MqttProtocolVersion ?? MqttProtocolVersion.V500) + .WithCleanSession(mqttSettings.CleanSession ?? true); if (mqttSettings.UseTls) { @@ -112,9 +117,21 @@ public static IMqttServiceCollection AddMqttServices(this IServic public static IMqttServiceCollection AddMqttConsumerService(this IServiceCollection services, string configSectionPath) where TMqttSettings : ManagedMqttSettings, new() { + services.AddSingleton(); + services.AddSingleton(); + services.AddSingleton(); + services.AddSingleton(); + services.AddSingleton(); + services.AddSingleton(); + services.AddSingleton(); + services.AddSingleton(); + services.AddSingleton(); + services.AddSingleton(sp => sp.GetRequiredService>().ToArray()); + services.TryAddSingleton(); + services.TryAddSingleton(); - services.TryAddSingleton(); + services.TryAddSingleton(); services.TryAddSingleton(); services.TryAddSingleton(); diff --git a/Source/Sholo.Mqtt/Settings/MqttSettings.cs b/Source/Sholo.Mqtt/Settings/MqttSettings.cs index 4a2662d..b9d283b 100644 --- a/Source/Sholo.Mqtt/Settings/MqttSettings.cs +++ b/Source/Sholo.Mqtt/Settings/MqttSettings.cs @@ -77,12 +77,12 @@ public class MqttSettings : IValidatableObject /// /// Gets or sets an optional message to publish when the client connects or reconnects to the broker. /// - public MqttMessageSettings? OnlineMessage { get; set; } + public MqttMessageSettings OnlineMessage { get; set; } = new(); /// /// Gets or sets an optional message that the broker will automatically publish if the client disconnects without sending a DISCONNECT packet. /// - public MqttMessageSettings? LastWillAndTestament { get; set; } + public MqttMessageSettings LastWillAndTestament { get; set; } = new(); /// /// Gets or sets the timeout which will be applied at socket level and internal operations. @@ -95,6 +95,11 @@ public class MqttSettings : IValidatableObject /// public TimeSpan? KeepAliveInterval { get; set; } + /// + /// Gets or sets whether a clean session is used. This determines whether the broker will store and attempt delivery by session persistence and QoS messages + /// + public bool? CleanSession { get; set; } + public virtual IEnumerable Validate(ValidationContext validationContext) { if (ClientCertificatePublicKeyPemFile == null && ClientCertificatePrivateKeyPemFile != null) diff --git a/Source/Sholo.Mqtt/Sholo.Mqtt.csproj b/Source/Sholo.Mqtt/Sholo.Mqtt.csproj index 176ff63..3411308 100644 --- a/Source/Sholo.Mqtt/Sholo.Mqtt.csproj +++ b/Source/Sholo.Mqtt/Sholo.Mqtt.csproj @@ -7,12 +7,12 @@ enable - - - - - - + + + + + + diff --git a/Source/Sholo.Mqtt/Topics/Filter/IMqttTopicFilter.cs b/Source/Sholo.Mqtt/Topics/Filter/IMqttTopicFilter.cs index bf495a3..439a0e3 100644 --- a/Source/Sholo.Mqtt/Topics/Filter/IMqttTopicFilter.cs +++ b/Source/Sholo.Mqtt/Topics/Filter/IMqttTopicFilter.cs @@ -2,6 +2,7 @@ using System.Diagnostics.CodeAnalysis; using Microsoft.Extensions.Primitives; using MQTTnet.Protocol; +using Sholo.Mqtt.Routing; namespace Sholo.Mqtt.Topics.Filter; diff --git a/Source/Sholo.Mqtt/Topics/FilterBuilder/IMqttTopicFilterBuilder.cs b/Source/Sholo.Mqtt/Topics/FilterBuilder/IMqttTopicFilterBuilder.cs index 9ec418d..13a7efc 100644 --- a/Source/Sholo.Mqtt/Topics/FilterBuilder/IMqttTopicFilterBuilder.cs +++ b/Source/Sholo.Mqtt/Topics/FilterBuilder/IMqttTopicFilterBuilder.cs @@ -17,9 +17,13 @@ public interface IMqttTopicFilterBuilder /// The topic (or topic mask) to subscribe to. The builder will remove Sholo.Mqtt library parameter variable names from the mask /// (e.g., test/#topic_part/parts/*topic_parts will be changed to test/#/parts/*) /// - /// The topic or topic mask to subscribe to - /// The being configured - IMqttTopicFilterBuilder WithTopicPattern(string topicPattern); + /// + /// The topic or topic mask to subscribe to + /// + /// + /// The being configured + /// + IMqttTopicFilterBuilder WithTopicPattern(string topicPattern); // TODO: bool caseSensitive /// /// Requests broker to not send application messages forwarded with a ClientID equal to the ClientID of the publishing connection. diff --git a/Source/Sholo.Mqtt/Topics/FilterBuilder/MqttTopicFilterBuilder.cs b/Source/Sholo.Mqtt/Topics/FilterBuilder/MqttTopicFilterBuilder.cs index 6dc39c7..bf832ff 100644 --- a/Source/Sholo.Mqtt/Topics/FilterBuilder/MqttTopicFilterBuilder.cs +++ b/Source/Sholo.Mqtt/Topics/FilterBuilder/MqttTopicFilterBuilder.cs @@ -1,4 +1,6 @@ +using System.Reflection; using MQTTnet.Protocol; +using Sholo.Mqtt.Controllers; using Sholo.Mqtt.Topics.Filter; using Sholo.Mqtt.Topics.PatternMatcherFactory; @@ -7,11 +9,68 @@ namespace Sholo.Mqtt.Topics.FilterBuilder; internal class MqttTopicFilterBuilder : IMqttTopicFilterBuilder { private string? TopicPattern { get; set; } + private bool CaseSensitive { get; set; } = true; private MqttQualityOfServiceLevel? QualityOfServiceLevel { get; set; } private bool? NoLocal { get; set; } private bool? RetainAsPublished { get; set; } private MqttRetainHandling? RetainHandling { get; set; } + public static IMqttTopicFilterBuilder FromActionAttributes(TypeInfo? instance, MethodInfo action) + { + var topicPrefixAttribute = instance?.GetCustomAttribute(); + + var topicAttribute = action.GetCustomAttribute() ?? + instance?.GetCustomAttribute(); + + var noLocalAttribute = action.GetCustomAttribute() ?? + instance?.GetCustomAttribute(); + + var qualityOfServiceAttribute = action.GetCustomAttribute() ?? + instance?.GetCustomAttribute(); + + var retainAsPublishedAttribute = action.GetCustomAttribute() ?? + instance?.GetCustomAttribute(); + + var retainHandlingAttribute = action.GetCustomAttribute() ?? + instance?.GetCustomAttribute(); + + var mqttTopicFilterBuilder = new MqttTopicFilterBuilder(); + + if (noLocalAttribute?.NoLocal != null) + { + mqttTopicFilterBuilder.WithNoLocal(noLocalAttribute.NoLocal); + } + + if (qualityOfServiceAttribute?.QualityOfServiceLevel != null) + { + mqttTopicFilterBuilder.WithQualityOfServiceLevel(qualityOfServiceAttribute.QualityOfServiceLevel); + } + + if (retainAsPublishedAttribute?.RetainAsPublished != null) + { + mqttTopicFilterBuilder.WithRetainAsPublished(retainAsPublishedAttribute.RetainAsPublished); + } + + if (retainHandlingAttribute?.RetainHandling != null) + { + mqttTopicFilterBuilder.WithRetainHandling(retainHandlingAttribute.RetainHandling); + } + + var topicPrefix = topicPrefixAttribute?.TopicPrefix.TrimEnd('/'); + var topicPattern = topicAttribute?.TopicPattern.TrimStart('/'); + var effectiveTopicPattern = !string.IsNullOrEmpty(topicPrefix) + ? $"{topicPrefix}/{topicPattern}" + : topicPattern; + + if (effectiveTopicPattern != null) + { + // TODO: Case sensitive -> configurable + mqttTopicFilterBuilder.WithTopicPattern(effectiveTopicPattern); + } + + return mqttTopicFilterBuilder; + } + public IMqttTopicFilterBuilder WithQualityOfServiceLevel(MqttQualityOfServiceLevel qualityOfServiceLevel) { QualityOfServiceLevel = qualityOfServiceLevel; @@ -21,6 +80,7 @@ public IMqttTopicFilterBuilder WithQualityOfServiceLevel(MqttQualityOfServiceLev public IMqttTopicFilterBuilder WithTopicPattern(string topicPattern) { TopicPattern = topicPattern; + CaseSensitive = true; // TODO return this; } @@ -45,7 +105,7 @@ public IMqttTopicFilterBuilder WithRetainHandling(MqttRetainHandling retainHandl public IMqttTopicFilter Build() { var topicPatternMatcher = new TopicPatternMatcherFactory() - .CreateTopicPatternMatcher(TopicPattern!); + .CreateTopicPatternMatcher(TopicPattern!); // , CaseSensitive var result = new MqttTopicFilter( topicPatternMatcher, diff --git a/Source/Sholo.Mqtt/Topics/PatternMatcher/ComplexTopicPatternMatcher.cs b/Source/Sholo.Mqtt/Topics/PatternMatcher/ComplexTopicPatternMatcher.cs index 2a38335..c029b32 100644 --- a/Source/Sholo.Mqtt/Topics/PatternMatcher/ComplexTopicPatternMatcher.cs +++ b/Source/Sholo.Mqtt/Topics/PatternMatcher/ComplexTopicPatternMatcher.cs @@ -1,7 +1,9 @@ +using System; using System.Collections.Generic; using System.Collections.ObjectModel; using System.Linq; using System.Text.RegularExpressions; +using Microsoft.Extensions.Primitives; using Sholo.Mqtt.Topics.FilterSanitizer; namespace Sholo.Mqtt.Topics.PatternMatcher; @@ -13,10 +15,26 @@ internal class ComplexTopicPatternMatcher : ITopicPatternMatcher public string TopicPattern { get; } public IReadOnlySet TopicParameterNames { get; } public string? MutliLevelWildcardParameterName { get; } + private bool CaseSensitive { get; } private Regex? PatternMatcher { get; } - public bool IsTopicMatch(string topic, out IReadOnlyDictionary? topicArguments) + public ComplexTopicPatternMatcher( + string topicPattern, + Regex patternMatcher, + IReadOnlySet topicParameterNames, + string? mutliLevelWildcardParameterName + ) + { + Topic = TopicFilterSanitizer.SanitizeTopic(topicPattern); + TopicPattern = topicPattern; + PatternMatcher = patternMatcher; + TopicParameterNames = topicParameterNames; + MutliLevelWildcardParameterName = mutliLevelWildcardParameterName; + CaseSensitive = true; // TODO + } + + public bool IsTopicMatch(string topic, out IReadOnlyDictionary? topicArguments) { var match = PatternMatcher!.Match(topic); if (!match.Success) @@ -25,7 +43,9 @@ public bool IsTopicMatch(string topic, out IReadOnlyDictionary return false; } - var result = new Dictionary>(); + var stringComparer = CaseSensitive ? StringComparer.Ordinal : StringComparer.OrdinalIgnoreCase; + + var result = new Dictionary>(stringComparer); foreach (var topicParameterName in TopicParameterNames) { @@ -49,21 +69,14 @@ public bool IsTopicMatch(string topic, out IReadOnlyDictionary } } - topicArguments = new ReadOnlyDictionary(result.ToDictionary(x => x.Key, x => x.Value.ToArray())); - return true; - } + topicArguments = new ReadOnlyDictionary( + result.ToDictionary( + x => x.Key, + x => new StringValues(x.Value.ToArray()), + stringComparer + ) + ); - public ComplexTopicPatternMatcher( - string topicPattern, - Regex patternMatcher, - IReadOnlySet topicParameterNames, - string? mutliLevelWildcardParameterName - ) - { - Topic = TopicFilterSanitizer.SanitizeTopic(topicPattern); - TopicPattern = topicPattern; - PatternMatcher = patternMatcher; - TopicParameterNames = topicParameterNames; - MutliLevelWildcardParameterName = mutliLevelWildcardParameterName; + return true; } } diff --git a/Source/Sholo.Mqtt/Topics/PatternMatcher/SimpleTopicPatternMatcher.cs b/Source/Sholo.Mqtt/Topics/PatternMatcher/SimpleTopicPatternMatcher.cs index bd1d92a..fc0cac8 100644 --- a/Source/Sholo.Mqtt/Topics/PatternMatcher/SimpleTopicPatternMatcher.cs +++ b/Source/Sholo.Mqtt/Topics/PatternMatcher/SimpleTopicPatternMatcher.cs @@ -1,31 +1,36 @@ using System; using System.Collections.Generic; using System.Collections.ObjectModel; +using Microsoft.Extensions.Primitives; using Sholo.Mqtt.Topics.FilterSanitizer; +using Sholo.Mqtt.Utilities; namespace Sholo.Mqtt.Topics.PatternMatcher; [PublicAPI] internal class SimpleTopicPatternMatcher : ITopicPatternMatcher { - private static readonly IReadOnlyDictionary EmptyDictionary = new ReadOnlyDictionary(new Dictionary()); - private static readonly IReadOnlySet EmptySet = new HashSet(); + private static readonly IReadOnlyDictionary EmptyDictionary = ReadOnlyDictionary.Empty; + private static readonly IReadOnlySet EmptySet = ReadOnlySet.Empty(); public string Topic { get; } public string TopicPattern { get; } public IReadOnlySet TopicParameterNames { get; } public string? MutliLevelWildcardParameterName { get; } - public SimpleTopicPatternMatcher(string topicPattern) + private StringComparison StringComparison { get; } + + public SimpleTopicPatternMatcher(string topicPattern) // TODO: , bool caseSensitive { Topic = TopicFilterSanitizer.SanitizeTopic(topicPattern); TopicPattern = topicPattern; + StringComparison = StringComparison.Ordinal; // TODO: configurable TopicParameterNames = EmptySet; } - public bool IsTopicMatch(string topic, out IReadOnlyDictionary? topicArguments) + public bool IsTopicMatch(string topic, out IReadOnlyDictionary? topicArguments) { - if (topic.Equals(TopicPattern, StringComparison.Ordinal)) + if (topic.Equals(TopicPattern, StringComparison)) { topicArguments = EmptyDictionary; return true; diff --git a/Source/Sholo.Mqtt/Topics/PatternMatcherFactory/TopicPatternMatcherFactory.cs b/Source/Sholo.Mqtt/Topics/PatternMatcherFactory/TopicPatternMatcherFactory.cs index 9b72e94..7410336 100644 --- a/Source/Sholo.Mqtt/Topics/PatternMatcherFactory/TopicPatternMatcherFactory.cs +++ b/Source/Sholo.Mqtt/Topics/PatternMatcherFactory/TopicPatternMatcherFactory.cs @@ -11,7 +11,7 @@ namespace Sholo.Mqtt.Topics.PatternMatcherFactory; // TODO: In the meantime, I opted to compile the regular expressions created since the ratio of instance usage to instance creation is likely high enough to justify the setup cost. internal class TopicPatternMatcherFactory : ITopicPatternMatcherFactory { - public ITopicPatternMatcher CreateTopicPatternMatcher(string topicPattern) + public ITopicPatternMatcher CreateTopicPatternMatcher(string topicPattern) // TODO: , bool caseSensitive { if (topicPattern == null) throw new ArgumentNullException(nameof(topicPattern), $"{nameof(topicPattern)} is required."); if (string.IsNullOrEmpty(topicPattern)) throw new ArgumentException($"{nameof(topicPattern)} must be non-empty.", nameof(topicPattern)); @@ -25,7 +25,9 @@ public ITopicPatternMatcher CreateTopicPatternMatcher(string topicPattern) var regBuilder = new StringBuilder(); string? mutliLevelWildcardVariableName = null; - var topicParameterNames = new HashSet(); + var stringComparer = StringComparer.Ordinal; + + var topicParameterNames = new HashSet(stringComparer); for (var i = 0; i < topicParts.Length; i++) { var topicPart = topicParts[i]; @@ -82,7 +84,16 @@ public ITopicPatternMatcher CreateTopicPatternMatcher(string topicPattern) regBuilder.Append('$'); - var regex = new Regex(regBuilder.ToString(), RegexOptions.Compiled); + var regexOptions = RegexOptions.Compiled; + + /* + if (!caseSensitive) + { + regexOptions |= RegexOptions.IgnoreCase; + } + */ + + var regex = new Regex(regBuilder.ToString(), regexOptions); return new ComplexTopicPatternMatcher(topicPattern, regex, topicParameterNames, mutliLevelWildcardVariableName); } diff --git a/Source/Sholo.Mqtt/Utilities/ReadOnlyDictionary.cs b/Source/Sholo.Mqtt/Utilities/ReadOnlyDictionary.cs new file mode 100644 index 0000000..781ea36 --- /dev/null +++ b/Source/Sholo.Mqtt/Utilities/ReadOnlyDictionary.cs @@ -0,0 +1,12 @@ +namespace Sholo.Mqtt.Utilities; + +/* +public static class ReadOnlyDictionary +{ + private static readonly ConcurrentDictionary<(Type, Type), object> Instances = new(); + + public static IReadOnlyDictionary Empty() where TKey : notnull => (IReadOnlyDictionary)Instances.GetOrAdd((typeof(TKey), typeof(TValue)), _ => new ReadOnlyDictionary()); + + public static IReadOnlyDictionary => new ReadOnlyDictionary(items.ToDictionary(x => x.Key, x => x.Value));> +} +*/ diff --git a/Source/Sholo.Mqtt/Utilities/ReadOnlySet.cs b/Source/Sholo.Mqtt/Utilities/ReadOnlySet.cs new file mode 100644 index 0000000..0e45b59 --- /dev/null +++ b/Source/Sholo.Mqtt/Utilities/ReadOnlySet.cs @@ -0,0 +1,36 @@ +using System; +using System.Collections; +using System.Collections.Concurrent; +using System.Collections.Generic; + +namespace Sholo.Mqtt.Utilities; + +[PublicAPI] +public static class ReadOnlySet +{ + private static readonly ConcurrentDictionary Instances = new(); + + public static IReadOnlySet Empty() => (IReadOnlySet)Instances.GetOrAdd(typeof(T), _ => new ReadOnlySet(new HashSet())); +} + +[PublicAPI] +public class ReadOnlySet : IReadOnlySet +{ + private ISet Set { get; } + + public ReadOnlySet(ISet set) + { + Set = set ?? throw new ArgumentNullException(nameof(set)); + } + + public int Count => Set.Count; + public bool Contains(T item) => Set.Contains(item); + public bool IsProperSubsetOf(IEnumerable other) => Set.IsProperSubsetOf(other); + public bool IsProperSupersetOf(IEnumerable other) => Set.IsProperSupersetOf(other); + public bool IsSubsetOf(IEnumerable other) => Set.IsSubsetOf(other); + public bool IsSupersetOf(IEnumerable other) => Set.IsSupersetOf(other); + public bool Overlaps(IEnumerable other) => Set.Overlaps(other); + public bool SetEquals(IEnumerable other) => Set.SetEquals(other); + public IEnumerator GetEnumerator() => Set.GetEnumerator(); + IEnumerator IEnumerable.GetEnumerator() => GetEnumerator(); +} diff --git a/Source/Sholo.Mqtt/Utilities/SetExtensions.cs b/Source/Sholo.Mqtt/Utilities/SetExtensions.cs new file mode 100644 index 0000000..4767321 --- /dev/null +++ b/Source/Sholo.Mqtt/Utilities/SetExtensions.cs @@ -0,0 +1,9 @@ +using System.Collections.Generic; + +namespace Sholo.Mqtt.Utilities; + +[PublicAPI] +public static class SetExtensions +{ + public static IReadOnlySet AsReadOnly(this ISet set) => new ReadOnlySet(set); +} diff --git a/Source/Sholo.Mqtt/Utilities/ValidationHelper.cs b/Source/Sholo.Mqtt/Utilities/ValidationHelper.cs index ec1d33a..2974d60 100644 --- a/Source/Sholo.Mqtt/Utilities/ValidationHelper.cs +++ b/Source/Sholo.Mqtt/Utilities/ValidationHelper.cs @@ -9,7 +9,7 @@ namespace Sholo.Mqtt.Utilities; [PublicAPI] internal static class ValidationHelper { - private static IServiceProvider DefaultServiceProvider { get; } + private static ServiceProvider DefaultServiceProvider { get; } static ValidationHelper() { @@ -30,8 +30,8 @@ public static bool IsValid(object obj, [MaybeNullWhen(true)] out ValidationResul public static bool IsValid(object obj, IList validationResults, IFileAbstraction? fileAbstraction = null) { - var serviceProvider = fileAbstraction != null - ? CreateServiceProvider(services => services.AddSingleton(fileAbstraction)) + using var serviceProvider = fileAbstraction != null + ? CreateServiceProvider(services => services.AddSingleton(fileAbstraction))! : DefaultServiceProvider; var validationContext = new ValidationContext(obj, serviceProvider, null); @@ -46,11 +46,11 @@ public static bool IsValid(object obj, IList validationResults return success; } - private static IServiceProvider CreateServiceProvider(Action config) + private static ServiceProvider CreateServiceProvider(Action config) { var serviceCollection = new ServiceCollection(); config.Invoke(serviceCollection); var serviceProvider = serviceCollection.BuildServiceProvider(); - return serviceProvider; + return serviceProvider!; } } diff --git a/Tests/Directory.Build.props b/Tests/Directory.Build.props index 4733e59..49df38d 100644 --- a/Tests/Directory.Build.props +++ b/Tests/Directory.Build.props @@ -12,7 +12,7 @@ all runtime; build; native; contentfiles; analyzers; buildtransitive - + diff --git a/Tests/Sholo.Mqtt.Test/TypeConverters/DefaultTypeConverterTests.cs b/Tests/Sholo.Mqtt.Test/TypeConverters/DefaultTypeConverterTests.cs index 5a1b5b7..adbfc05 100644 --- a/Tests/Sholo.Mqtt.Test/TypeConverters/DefaultTypeConverterTests.cs +++ b/Tests/Sholo.Mqtt.Test/TypeConverters/DefaultTypeConverterTests.cs @@ -1033,7 +1033,7 @@ private void TestTryConvertStringHappyPath(string? payload, T expectedResult, private void TestTryConvertStringHappyPath(string? payload, T expectedResult, Func equalityComparer) { - var converted = DefaultTypeConverters.TryConvert(payload, typeof(T), out var result); + var converted = DefaultTypeConverter.Instance.TryConvert(payload, typeof(T), out var result); Assert.True(converted); @@ -1066,7 +1066,7 @@ private void TestTryConvertStringHappyPath(string? payload, T expectedResult, private void TestTryConvertStringSadPath(string? payload) { - var converted = DefaultTypeConverters.TryConvert(payload, typeof(T), out var result); + var converted = DefaultTypeConverter.Instance.TryConvert(payload, typeof(T), out var result); Assert.False(converted); Assert.Null(result); } @@ -1079,7 +1079,7 @@ private void TestTryConvertArraySegmentOfBytesHappyPath(ArraySegment pa private void TestTryConvertArraySegmentOfBytesHappyPath(ArraySegment payload, T expectedResult, Func equalityComparer) { - var converted = DefaultTypeConverters.TryConvert(payload, typeof(T), out var result); + var converted = DefaultTypeConverter.Instance.TryConvert(payload, typeof(T), out var result); Assert.True(converted); @@ -1112,7 +1112,7 @@ private void TestTryConvertArraySegmentOfBytesHappyPath(ArraySegment pa private void TestTryConvertArraySegmentOfBytesSadPath(ArraySegment payload) { - var converted = DefaultTypeConverters.TryConvert(payload, typeof(T), out var result); + var converted = DefaultTypeConverter.Instance.TryConvert(payload, typeof(T), out var result); Assert.False(converted); Assert.Null(result); } diff --git a/Tests/Sholo.Mqtt.Test/TypeConverters/Parameter/LambdaMqttParameterTypeConverterTests.cs b/Tests/Sholo.Mqtt.Test/TypeConverters/Parameter/LambdaMqttParameterTypeConverterTests.cs index 42d2d5a..e4e5784 100644 --- a/Tests/Sholo.Mqtt.Test/TypeConverters/Parameter/LambdaMqttParameterTypeConverterTests.cs +++ b/Tests/Sholo.Mqtt.Test/TypeConverters/Parameter/LambdaMqttParameterTypeConverterTests.cs @@ -1,15 +1,13 @@ using System; using Sholo.Mqtt.ModelBinding.TypeConverters; -using Sholo.Mqtt.TypeConverters; -using Sholo.Mqtt.TypeConverters.Parameter; -using Xunit; namespace Sholo.Mqtt.Test.TypeConverters.Parameter; public class LambdaMqttParameterTypeConverterTests { - private IMqttParameterTypeConverter BrokenTypeConverter { get; } = new LambdaMqttParameterTypeConverter(s => throw new NotImplementedException("Oops")); + private IMqttUserPropertiesTypeConverter BrokenTypeConverter { get; } = new LambdaMqttParameterTypeConverter(s => throw new NotImplementedException("Oops")); - private IMqttParameterTypeConverter FuzzyBooleanTypeConverter { get; } = new LambdaMqttParameterTypeConverter( + /* + private IMqttUserPropertiesTypeConverter FuzzyBooleanTypeConverter { get; } = new LambdaMqttParameterTypeConverter( s => { if (s == null) @@ -76,7 +74,7 @@ public class LambdaMqttParameterTypeConverterTests [Fact] public void TryConvert_WhenInputIsNull_ReturnsTrueWithNullResult() { - var success = FuzzyBooleanTypeConverter.TryConvertParameter(null, typeof(bool), out var objResult); + var success = FuzzyBooleanTypeConverter.TryConvertUserProperties(null, typeof(bool), out var objResult); Assert.True(success); Assert.Null(objResult); } @@ -84,7 +82,7 @@ public void TryConvert_WhenInputIsNull_ReturnsTrueWithNullResult() [Fact] public void TryConvert_WhenInputIsEmptyString_ReturnsFalseWithNullResult() { - var success = FuzzyBooleanTypeConverter.TryConvertParameter(string.Empty, typeof(bool), out var objResult); + var success = FuzzyBooleanTypeConverter.TryConvertUserProperties(string.Empty, typeof(bool), out var objResult); Assert.False(success); Assert.Null(objResult); } @@ -108,7 +106,7 @@ public void TryConvert_WhenInputIsEmptyString_ReturnsFalseWithNullResult() public void TryConvert_WhenInputIsTruthy_ReturnsTrueWithTrueResult(string value) { - var success = FuzzyBooleanTypeConverter.TryConvertParameter(value, typeof(bool), out var objResult); + var success = FuzzyBooleanTypeConverter.TryConvertUserProperties(value, typeof(bool), out var objResult); Assert.True(success); var result = Assert.IsAssignableFrom(objResult); @@ -134,7 +132,7 @@ public void TryConvert_WhenInputIsTruthy_ReturnsTrueWithTrueResult(string value) public void TryConvert_WhenInputIsFalsey_ReturnsTrueWithFalseResult(string value) { - var success = FuzzyBooleanTypeConverter.TryConvertParameter(value, typeof(bool), out var objResult); + var success = FuzzyBooleanTypeConverter.TryConvertUserProperties(value, typeof(bool), out var objResult); Assert.True(success); var result = Assert.IsAssignableFrom(objResult); @@ -147,7 +145,7 @@ public void TryConvert_WhenInputIsFalsey_ReturnsTrueWithFalseResult(string value public void TryConvert_WhenInputIsSomethingElse_ReturnsFalseWithNullResult(string value) { - var success = FuzzyBooleanTypeConverter.TryConvertParameter(value, typeof(bool), out var objResult); + var success = FuzzyBooleanTypeConverter.TryConvertUserProperties(value, typeof(bool), out var objResult); Assert.False(success); Assert.Null(objResult); @@ -156,9 +154,10 @@ public void TryConvert_WhenInputIsSomethingElse_ReturnsFalseWithNullResult(strin [Fact] public void TryConvert_WhenTypeConverterThrows_ReturnsFalseWithNullResult() { - var success = BrokenTypeConverter.TryConvertParameter("abc", typeof(bool), out var objResult); + var success = BrokenTypeConverter.TryConvertUserProperties("abc", typeof(bool), out var objResult); Assert.False(success); Assert.Null(objResult); } + */ } diff --git a/Tests/Sholo.Mqtt.Test/ValueProviders/MqttCorrelationDataValueProviderTests.cs b/Tests/Sholo.Mqtt.Test/ValueProviders/MqttCorrelationDataValueProviderTests.cs index 2fe8137..d1ea4c9 100644 --- a/Tests/Sholo.Mqtt.Test/ValueProviders/MqttCorrelationDataValueProviderTests.cs +++ b/Tests/Sholo.Mqtt.Test/ValueProviders/MqttCorrelationDataValueProviderTests.cs @@ -1,10 +1,6 @@ -using System.Linq; -using Moq; -using Sholo.Mqtt.ModelBinding.ValueProviders; -using Xunit; - namespace Sholo.Mqtt.Test.ValueProviders; +/* public class MqttCorrelationDataValueProviderTests { private IMqttCorrelationDataValueProvider MqttCorrelationDataValueProvider { get; } = new MqttCorrelationDataValueProvider(); @@ -47,3 +43,4 @@ public void GetValueSource_WhenRequestHasCorrelationData_ReturnsNull() Assert.Null(correlationData); } } +*/ diff --git a/Tests/Sholo.Mqtt.Test/ValueProviders/MqttPayloadValueProviderTests.cs b/Tests/Sholo.Mqtt.Test/ValueProviders/MqttPayloadValueProviderTests.cs index 25b5b36..12e5db5 100644 --- a/Tests/Sholo.Mqtt.Test/ValueProviders/MqttPayloadValueProviderTests.cs +++ b/Tests/Sholo.Mqtt.Test/ValueProviders/MqttPayloadValueProviderTests.cs @@ -1,11 +1,6 @@ -using System; -using System.Linq; -using Moq; -using Sholo.Mqtt.ModelBinding.ValueProviders; -using Xunit; - namespace Sholo.Mqtt.Test.ValueProviders; +/* public class MqttPayloadValueProviderTests { private IMqttPayloadValueProvider MqttPayloadValueProvider { get; } = new MqttPayloadValueProvider(); @@ -49,3 +44,4 @@ public void GetValueSource_WhenRequestHasCorrelationData_ReturnsNull() Assert.Equal(0, payload.Count); } } +*/ diff --git a/Tests/Sholo.Mqtt.Test/ValueProviders/MqttTopicArgumentValueProviderTests.cs b/Tests/Sholo.Mqtt.Test/ValueProviders/MqttTopicArgumentValueProviderTests.cs index 7ab5b6e..633f63b 100644 --- a/Tests/Sholo.Mqtt.Test/ValueProviders/MqttTopicArgumentValueProviderTests.cs +++ b/Tests/Sholo.Mqtt.Test/ValueProviders/MqttTopicArgumentValueProviderTests.cs @@ -1,11 +1,5 @@ -using System.Collections.Generic; -using System.Collections.ObjectModel; -using System.Linq; -using Microsoft.Extensions.Primitives; using Moq; using Sholo.Mqtt.ModelBinding; -using Sholo.Mqtt.ModelBinding.ValueProviders; -using Xunit; namespace Sholo.Mqtt.Test.ValueProviders; @@ -13,6 +7,7 @@ public class MqttTopicArgumentValueProviderTests { private Mock MockModelBindingContext { get; } = new(MockBehavior.Strict); + /* [Fact] public void GetValueSource_WhenTopicHasParameter_ReturnsValue() { @@ -54,4 +49,5 @@ public void GetValueSource_WhenTopicHasParameter_ReturnsNull() Assert.Equal(null, result); } + */ } diff --git a/Tests/Sholo.Mqtt.Test/ValueProviders/MqttUserPropertyValueProviderTests.cs b/Tests/Sholo.Mqtt.Test/ValueProviders/MqttUserPropertyValueProviderTests.cs index 1804ecd..6b46337 100644 --- a/Tests/Sholo.Mqtt.Test/ValueProviders/MqttUserPropertyValueProviderTests.cs +++ b/Tests/Sholo.Mqtt.Test/ValueProviders/MqttUserPropertyValueProviderTests.cs @@ -1,12 +1,6 @@ -using System; -using Moq; -using MQTTnet.Packets; -using Sholo.Mqtt.ModelBinding; -using Sholo.Mqtt.ModelBinding.ValueProviders; -using Xunit; - namespace Sholo.Mqtt.Test.ValueProviders; +/* public class MqttUserPropertyValueProviderTests { private Mock MockModelBindingContext { get; } = new(MockBehavior.Strict); @@ -29,7 +23,7 @@ public MqttUserPropertyValueProviderTests() [Theory] [InlineData("key1", StringComparison.Ordinal)] [InlineData("KEY1", StringComparison.OrdinalIgnoreCase)] - public void GetValueSource_WhenRequestHasUserPropertyMatchkingKeyAndStringComparer_ReturnsSingleValue(string propertyName, StringComparison stringComparison) + public void GetValueSource_WhenRequestHasUserPropertyMatchingKeyAndStringComparer_ReturnsSingleValue(string propertyName, StringComparison stringComparison) { var mqttUserPropertyValueProvider = new MqttUserPropertyValueProvider(propertyName, stringComparison); @@ -49,7 +43,7 @@ public void GetValueSource_WhenRequestHasUserPropertyMatchkingKeyAndStringCompar [Theory] [InlineData("key2", StringComparison.Ordinal)] [InlineData("KEY2", StringComparison.OrdinalIgnoreCase)] - public void GetValueSource_WhenRequestHasUserPropertiesMatchkingKeyAndStringComparer_ReturnsMultipleValues(string propertyName, StringComparison stringComparison) + public void GetValueSource_WhenRequestHasUserPropertiesMatchingKeyAndStringComparer_ReturnsMultipleValues(string propertyName, StringComparison stringComparison) { var mqttUserPropertyValueProvider = new MqttUserPropertyValueProvider(propertyName, stringComparison); @@ -70,7 +64,7 @@ public void GetValueSource_WhenRequestHasUserPropertiesMatchkingKeyAndStringComp [Theory] [InlineData("key3", StringComparison.Ordinal)] [InlineData("KEY3", StringComparison.OrdinalIgnoreCase)] - public void GetValueSource_WhenRequestHasUserPropertyMatchkingKeyButNotStringComparer_ReturnsEmptyArray(string propertyName, StringComparison stringComparison) + public void GetValueSource_WhenRequestHasUserPropertyMatchingKeyButNotStringComparer_ReturnsEmptyArray(string propertyName, StringComparison stringComparison) { var mqttUserPropertyValueProvider = new MqttUserPropertyValueProvider(propertyName, stringComparison); @@ -84,3 +78,4 @@ public void GetValueSource_WhenRequestHasUserPropertyMatchkingKeyButNotStringCom Assert.Empty(values); } } +*/ diff --git a/global.json b/global.json index 2773016..f7fb55b 100644 --- a/global.json +++ b/global.json @@ -1,6 +1,6 @@ { "sdk": { - "version": "7.0.403", + "version": "8.0.100", "rollForward": "latestMinor", "allowPrerelease": false }