提交 f6b8013a 编写于 作者: A Anton Vishnyak

Added readme and incorporated feedback from @JanEggers

上级 17684ada
using System;
using MQTTnet.Server;
namespace MQTTnet.AspNetCore.AttributeRouting.Attributes
{
/// <summary>
/// When creating a custom controller that does not inherit from <see cref="MqttBaseController"/>, this attribute
/// tells the activator which property the <see cref="MqttApplicationMessageInterceptorContext"/> should be assigned to.
/// </summary>
[AttributeUsage(AttributeTargets.Property, AllowMultiple = false, Inherited = true)]
public class MqttControllerContextAttribute : Attribute
{
}
}
\ No newline at end of file
......@@ -24,8 +24,8 @@ namespace Example.MqttControllers
// We have access to the MqttContext
if (zipCode != 90210) { MqttContext.CloseConnection = true; }
// We have access to the request context
var temperature = BitConverter.ToDouble(Request.Payload);
// We have access to the raw message
var temperature = BitConverter.ToDouble(Message.Payload);
_logger.LogInformation($"It's {temperature} degrees in Hollywood");
......
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging;
using MQTTnet.AspNetCore;
using MQTTnet.AspNetCore.AttributeRouting;
using MQTTnet.AspNetCore.Extensions;
......@@ -30,12 +24,17 @@ namespace Example
// Configure AspNetCore controllers
services.AddControllers();
// Configure MQTT controllers
// Identify and build routes for the current assembly
services.AddMqttControllers();
services
.AddHostedMqttServerWithAttributeRouting(s =>
.AddHostedMqttServerWithServices(s =>
{
// Optionally set server options here
s.WithoutDefaultEndpoint();
// Enable Attribute routing
s.WithAttributeRouting();
})
.AddMqttConnectionHandler()
.AddConnections();
......
using Microsoft.Extensions.DependencyInjection;
using MQTTnet.AspNetCore.Extensions;
// Copyright (c) Atlas Lift Tech Inc. All rights reserved.
using Microsoft.Extensions.DependencyInjection;
using MQTTnet.AspNetCore.AttributeRouting.Routing;
using MQTTnet.Server;
using System;
using System.Diagnostics;
using System.Linq;
using System.Reflection;
namespace MQTTnet.AspNetCore.AttributeRouting
{
public static class ServiceCollectionExtensions
{
public static IServiceCollection AddHostedMqttServerWithAttributeRouting(this IServiceCollection services, Action<AspNetMqttServerOptionsBuilder> configure)
public static IServiceCollection AddMqttControllers(this IServiceCollection services)
{
var assemblies = new Assembly[] { Assembly.GetEntryAssembly() };
var routeTable = MqttRouteTableFactory.Create(assemblies);
services.AddSingleton(routeTable);
services.AddHostedMqttServerWithServices(options =>
services.AddSingleton(c =>
{
configure(options);
var rt = options.ServiceProvider.GetRequiredService<MqttRouteTable>();
options.WithApplicationMessageInterceptor(new MqttServerApplicationMessageInterceptorDelegate(context =>
{
var ctx = new MqttRouteContext(context.ApplicationMessage.Topic);
rt.Route(ctx);
if (ctx.Handler == null)
{
context.AcceptPublish = false;
}
else
{
object result = null;
ParameterInfo[] parameters = ctx.Handler.GetParameters();
// future enhancement: scan for other AppParts, if needed
using (var scope = options.ServiceProvider.CreateScope())
{
object classInstance = ActivatorUtilities.GetServiceOrCreateInstance(scope.ServiceProvider, ctx.Handler.DeclaringType);
var assemblies = new Assembly[] { Assembly.GetEntryAssembly() };
// TODO: Handle instance where we get a non MqttBaseController for some reason
((MqttBaseController)classInstance).MqttContext = context;
return MqttRouteTableFactory.Create(assemblies);
});
try
{
if (parameters.Length == 0)
{
result = ctx.Handler.Invoke(classInstance, null);
}
else
{
// TODO: Better error messages if parameters don't align properly
object[] paramArray = parameters.Select(p => ctx.Parameters[p.Name]).ToArray();
services.AddSingleton<ITypeActivatorCache>(new TypeActivatorCache());
services.AddSingleton<MqttRouter>();
result = ctx.Handler.Invoke(classInstance, paramArray);
}
}
catch (Exception ex)
{
Debug.WriteLine(ex);
}
}
return services;
}
context.AcceptPublish = true;
public static AspNetMqttServerOptionsBuilder WithAttributeRouting(this AspNetMqttServerOptionsBuilder options)
{
var router = options.ServiceProvider.GetRequiredService<MqttRouter>();
var interceptor = new MqttServerApplicationMessageInterceptorDelegate(context => router.OnIncomingApplicationMessage(options, context));
Debug.WriteLine($"Matched route ${context.ApplicationMessage.Topic} to handler ${ctx.Handler.DeclaringType.FullName}.{ctx.Handler.Name}");
}
}));
});
options.WithApplicationMessageInterceptor(interceptor);
return services;
return options;
}
}
}
\ No newline at end of file
......@@ -3,22 +3,25 @@
<PropertyGroup>
<TargetFrameworks>netstandard2.0;netcoreapp3.1</TargetFrameworks>
<GeneratePackageOnBuild>true</GeneratePackageOnBuild>
<Description>
This is a support library to integrate AttributeRouting into MQTTnet with AspNetCore.
<Description>This is a support library to integrate AttributeRouting into MQTTnet with AspNetCore.
Easily create Controllers and Actions to process incoming MQTT messages using a familiar paradigm. Bring your WebAPI development experience directly into processing MQTT messages.
</Description>
<Copyright>Copyright (c) Anton Vishnyak 2020</Copyright>
Easily create Controllers and Actions to process incoming MQTT messages using attribute-based routing against the incoming message topic.</Description>
<Copyright>Copyright (c) Atlas Lift Tech Inc. 2020</Copyright>
<PackageLicenseExpression>MIT</PackageLicenseExpression>
<PackageTags>MQTT Message Queue Telemetry Transport MQTTClient MQTTServer Server MQTTBroker Broker NETStandard IoT InternetOfThings Messaging Hardware Arduino Sensor Actuator M2M ESP Smart Home Cities Automation Xamarin Blazor AspNetCore</PackageTags>
<Company>Atlas Lift Tech Inc.</Company>
<Authors>Anton Vishnyak</Authors>
<AssemblyVersion>0.0.0.0</AssemblyVersion>
<FileVersion>0.0.0.0</FileVersion>
<AssemblyVersion>0.1.0.0</AssemblyVersion>
<FileVersion>0.1.0.0</FileVersion>
<SignAssembly>true</SignAssembly>
<DelaySign>false</DelaySign>
<AssemblyOriginatorKeyFile>codeSigningKey.pfx</AssemblyOriginatorKeyFile>
<LangVersion>default</LangVersion>
<RepositoryUrl>https://github.com/Atlas-LiftTech/MQTTnet.AspNetCore.AttributeRouting</RepositoryUrl>
<RepositoryType>GIT</RepositoryType>
<PackageReleaseNotes>Beta release</PackageReleaseNotes>
<Version>0.1.0</Version>
<PackageRequireLicenseAcceptance>false</PackageRequireLicenseAcceptance>
</PropertyGroup>
<ItemGroup Condition="'$(TargetFramework)' == 'netcoreapp3.1'">
......
<p align="center">
<img src="https://github.com/chkr1011/MQTTnet/blob/master/Images/icon_det_256.png?raw=true" width="196">
<br/>
Attribute Routing
<br/>
</p>
[![NuGet Badge](https://buildstats.info/nuget/MQTTnet.AspNetCore.AttributeRouting)](https://www.nuget.org/packages/MQTTnet.AspNetCore.AttributeRouting)
[![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://github.com/Atlas-LiftTech/MQTTnet.AspNetCore.AttributeRouting/LICENSE)
# MQTTnet AspNetCore AttributeRouting
This addon to MQTTnet provides the ability to define controllers and use attribute-based routing against message topics in a manner that is very similar to AspNet Core.
## Features
* Encapsulate your incoming message logic in controllers
* Use familiar paradigms from AspNetCore in your MQTT logic
* First-class support for dependency injection using existing ServiceProvider implementaiton in your AspNetCore project
* Use together with any other MQTTnet options
## Performance Note
This library has not been tested against a very high-load environment yet. Ensure you do your own load testing prior to use in production. All performance improvement PRs are welcome.
## Supported frameworks
* .NET Standard 2.0+
* .NET Core 3.1+
## Supported MQTT versions
* 5.0.0
* 3.1.1
* 3.1.0
## Nuget
This library is available as a nuget package: <https://www.nuget.org/packages/MQTTnet.AspNetCore.AttributeRouting/>
## Usage
Install this package and MQTTnet from nuget.
Modify your `Startup.cs` with the following options:
```csharp
public void ConfigureServices(IServiceCollection services)
{
// ... All your other configuration ...
// Identify and build routes for the current assembly
services.AddMqttControllers();
services
.AddHostedMqttServerWithServices(s =>
{
// Optionally set server options here
s.WithoutDefaultEndpoint();
// Enable Attribute routing
s.WithAttributeRouting();
})
.AddMqttConnectionHandler()
.AddConnections();
}
```
Create your controllers by inheriting from MqttBaseController and adding actions to it like so:
```csharp
[MqttController]
[MqttRoute("[controller]")] // Optional route prefix
public class MqttWeatherForecastController : MqttBaseController
{
private readonly ILogger<MqttWeatherForecastController> _logger;
// Controllers have full support for dependency injection just like AspNetCore controllers
public MqttWeatherForecastController(ILogger<MqttWeatherForecastController> logger)
{
_logger = logger;
}
// Supports template routing with typed constraints just like AspNetCore
// Action routes compose together with the route prefix on the controller level
[MqttRoute("{zipCode:int}/temperature")]
public async Task WeatherReport(int zipCode)
{
// We have access to the MqttContext
if (zipCode != 90210) { MqttContext.CloseConnection = true; }
// We have access to the raw message
var temperature = BitConverter.ToDouble(Message.Payload);
_logger.LogInformation($"It's {temperature} degrees in Hollywood");
if (temperature <= 0 && temperature >= 130)
{
// Example validation
MqttContext.AcceptPublish = false;
}
}
}
```
[See a full example project here](https://github.com/Atlas-LiftTech/MQTTnet.AspNetCore.AttributeRouting/tree/master/Example)
## Contributions
Contributions are welcome. Please open an issue to discuss your idea prior to sending a PR.
## MIT License
See https://github.com/Atlas-LiftTech/MQTTnet.AspNetCore.AttributeRouting/LICENSE.
// Copyright (c) .NET Foundation. All rights reserved. Licensed under the Apache License, Version 2.0. See License.txt
// in the project root for license information.
using System;
namespace MQTTnet.AspNetCore.AttributeRouting.Routing
{
internal interface ITypeActivatorCache
{
TInstance CreateInstance<TInstance>(IServiceProvider serviceProvider, Type implementationType);
}
}
\ No newline at end of file
// Copyright (c) Atlas Lift Tech Inc. All rights reserved.
using MQTTnet.AspNetCore.AttributeRouting.Attributes;
using MQTTnet.Server;
namespace MQTTnet.AspNetCore.AttributeRouting
......@@ -7,8 +8,15 @@ namespace MQTTnet.AspNetCore.AttributeRouting
[MqttController]
public abstract class MqttBaseController
{
/// <summary>
/// Connection context is set by controller activator. If this class is instantiated directly, it will be null.
/// </summary>
[MqttControllerContext]
public MqttApplicationMessageInterceptorContext MqttContext { get; set; }
public MqttApplicationMessage Request => MqttContext.ApplicationMessage;
/// <summary>
/// Gets the <see cref="MqttApplicationMessage"/> for the executing action.
/// </summary>
public MqttApplicationMessage Message => MqttContext.ApplicationMessage;
}
}
\ No newline at end of file
......@@ -35,7 +35,7 @@ namespace MQTTnet.AspNetCore.AttributeRouting
var asm = assemblies ?? new Assembly[] { Assembly.GetExecutingAssembly() };
var actions = asm.SelectMany(a => a.GetTypes())
.Where(type => type.IsSubclassOf(typeof(MqttBaseController)))
.Where(type => type.GetCustomAttribute(typeof(MqttControllerAttribute), true) != null)
.SelectMany(type => type.GetMethods(BindingFlags.Instance | BindingFlags.DeclaredOnly | BindingFlags.Public))
.Where(m => !m.GetCustomAttributes(typeof(System.Runtime.CompilerServices.CompilerGeneratedAttribute), true).Any() && !m.IsDefined(typeof(NonActionAttribute)));
......
// Copyright (c) Atlas Lift Tech Inc. All rights reserved.
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
using MQTTnet.AspNetCore.AttributeRouting.Attributes;
using MQTTnet.Server;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Reflection;
namespace MQTTnet.AspNetCore.AttributeRouting.Routing
{
internal class MqttRouter
{
private readonly ILogger<MqttRouter> logger;
public MqttRouter(ILogger<MqttRouter> logger)
{
this.logger = logger;
}
internal void OnIncomingApplicationMessage(AspNetMqttServerOptionsBuilder options, MqttApplicationMessageInterceptorContext context)
{
var routeTable = options.ServiceProvider.GetRequiredService<MqttRouteTable>();
var typeActivator = options.ServiceProvider.GetRequiredService<ITypeActivatorCache>();
var routeContext = new MqttRouteContext(context.ApplicationMessage.Topic);
routeTable.Route(routeContext);
if (routeContext.Handler == null)
{
// Route not found
logger.LogDebug($"Rejecting message publish because '{context.ApplicationMessage.Topic}' did not match any known routes.");
context.AcceptPublish = false;
}
else
{
object result = null;
using (var scope = options.ServiceProvider.CreateScope())
{
var classInstance = typeActivator.CreateInstance<object>(scope.ServiceProvider, routeContext.Handler.DeclaringType);
// Potential perf improvement is to cache this reflection work in the future.
var activateProperties = routeContext.Handler.DeclaringType.GetRuntimeProperties()
.Where((property) =>
{
return
property.IsDefined(typeof(MqttControllerContextAttribute)) &&
property.GetIndexParameters().Length == 0 &&
property.SetMethod != null &&
!property.SetMethod.IsStatic;
})
.ToArray();
if (activateProperties.Length == 0)
{
logger.LogDebug($"MqttController '{routeContext.Handler.DeclaringType.FullName}' does not have a property that can accept a controller context. You may want to add a [{nameof(MqttControllerContextAttribute)}] to a pubilc property.");
}
foreach (var property in activateProperties)
{
property.SetValue(classInstance, context);
}
ParameterInfo[] parameters = routeContext.Handler.GetParameters();
if (parameters.Length == 0)
{
result = routeContext.Handler.Invoke(classInstance, null);
context.AcceptPublish = true;
}
else
{
object[] paramArray;
try
{
paramArray = parameters.Select(p => MatchParameterOrThrow(p, routeContext.Parameters)).ToArray();
result = routeContext.Handler.Invoke(classInstance, paramArray);
context.AcceptPublish = true;
}
catch (ArgumentException ex)
{
logger.LogError(ex, $"Unable to match route parameters to all arguments. See inner exception for details.");
context.AcceptPublish = false;
}
catch (Exception ex)
{
logger.LogError(ex, $"Unhandled MQTT action exception. See inner exception for details.");
// This is an unandled exception from the invoked action
context.AcceptPublish = false;
}
}
}
}
}
private static object MatchParameterOrThrow(ParameterInfo param, IReadOnlyDictionary<string, object> availableParmeters)
{
if (!availableParmeters.TryGetValue(param.Name, out object value))
{
if (param.IsOptional)
{
return null;
}
else
{
throw new ArgumentException($"No matching route parameter for \"{param.ParameterType.Name} {param.Name}\"", param.Name);
}
}
if (!param.ParameterType.IsAssignableFrom(value.GetType()))
{
throw new ArgumentException($"Cannot assign type \"{value.GetType()}\" to parameter \"{param.ParameterType.Name} {param.Name}\"", param.Name);
}
return value;
}
}
}
\ No newline at end of file
// Copyright (c) .NET Foundation. All rights reserved. Licensed under the Apache License, Version 2.0. See License.txt
// in the project root for license information.
// Modifications Copyright (c) Atlas Lift Tech Inc. All rights reserved.
using Microsoft.Extensions.DependencyInjection;
using System;
using System.Collections.Concurrent;
namespace MQTTnet.AspNetCore.AttributeRouting.Routing
{
/// <summary>
/// Caches <see cref="ObjectFactory"/> instances produced by <see cref="ActivatorUtilities.CreateFactory(Type, Type[])"/>.
/// </summary>
internal class TypeActivatorCache : ITypeActivatorCache
{
private readonly Func<Type, ObjectFactory> _createFactory = (type) => ActivatorUtilities.CreateFactory(type, Type.EmptyTypes);
private readonly ConcurrentDictionary<Type, ObjectFactory> _typeActivatorCache = new ConcurrentDictionary<Type, ObjectFactory>();
/// <inheritdoc/>
public TInstance CreateInstance<TInstance>(IServiceProvider serviceProvider, Type implementationType)
{
if (serviceProvider == null)
{
throw new ArgumentNullException(nameof(serviceProvider));
}
if (implementationType == null)
{
throw new ArgumentNullException(nameof(implementationType));
}
var createFactory = _typeActivatorCache.GetOrAdd(implementationType, _createFactory);
return (TInstance)createFactory(serviceProvider, arguments: null);
}
}
}
\ No newline at end of file
Markdown is supported
0% .
You are about to add 0 people to the discussion. Proceed with caution.
先完成此消息的编辑!
想要评论请 注册