Mqttnet: Attribute-based routing for handling incoming server messages (similar to AspnetCore)

Created on 4 Aug 2020  路  9Comments  路  Source: chkr1011/MQTTnet

Describe the feature request

Since the server has the capability to listen to messages directly as they come in, I think it would be a good idea to borrow the concept of attribute-based routing from the AspnetCore framework. The idea would be that we create a MqttController and MqttRoute attribute which works in a way that is similar to the AspNet framework. The matching would be done against the path of the Mqtt messages and the appropriate action would be invoked by the MqttServer.

Which project is your feature request related to?

  • MQTTnet.AspnetCore

Describe the solution you'd like

  • Create a base MqttController
  • Create MqttController attribute
  • Create MqttRoute attribute
  • Add MapMqttControllers configuration as a chainable config to UseMqtt Endpoint config in MQTTnet.AspnetCore.
  • If there are any configured controllers, match them against a routing table and invoke the appropriate Action

Describe alternatives you've considered

Alternatives are:

  • Do nothing (everyone implements whatever they want in user space)
  • Issue guidance in the form of a separate GitHub project and have everyone re-implement as needed on their own project
  • Plug into the AspNet endpoint routing directly (not a 100% fit of the paradigm)

Additional context

If this feature is part of the AspNet integration package and optional (needs to use .MapMqttControllers to opt into this behavior, I believe it will be fully backwards compatible and offer a nicer onboarding for new users who are used to the AspNet routing paradigm.

I would be happy to do the implementation if this is something the project would like to add.

feature-request

Most helpful comment

@avishnyak Sure we already mentioned other libs in the readme etc.

All 9 comments

I like the idea from a high perspective. But I am not able to support the code for such a huge extension. So if you want to build that you can start this and we will see how much effort is required to maintain that code. The problem is that I cannot maintain more code in this project.

You can also push a nuget package in the form of "MQTTnet.Extensions.Controllers" etc. If you need some additional interfaces etc. we can discuss how they can be added to the main library.

How does that sounds?

Yes, that sounds great. I will keep it a separate package for now and reach back out with any extensions that may be needed.

Thanks for all the time you put in to this. Cheers!

If you don't mind, I'll keep this open until I have a working implementation and comment here for your review.

Thanks, @JanEggers thats a very helpful implementation. I would love it if you could take a look at my implementation when it's done to compare notes.

@JanEggers @chkr1011 I have an initial working implementation for you to consider at Atlas-LiftTech/MQTTnet.AspNetCore.AttributeRouting

Usage turned out to be very ergonomic if the workload is predominantly processing incoming messages. First, you opt into the behavior with the following snippet:

// This method gets called by the runtime. Use this method to add services to the container.
        public void ConfigureServices(IServiceCollection services)
        {
            // Configure AspNetCore controllers
            services.AddControllers();

            // Configure MQTT controllers
            services
                .AddHostedMqttServerWithAttributeRouting(s =>
                {
                    // Optionally set server options here
                    s.WithoutDefaultEndpoint();
                })
                .AddMqttConnectionHandler()
                .AddConnections();
        }

Then you are free to add any number of controllers to your project like so:

    [MqttController]
    [MqttRoute("[controller]")]
    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
        [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 request context
            var temperature = BitConverter.ToDouble(Request.Payload);

            _logger.LogInformation($"It's {temperature} degrees in Hollywood");

            if (temperature <= 0 && temperature >= 130)
            {
                // Example validation
                MqttContext.AcceptPublish = false;
            }
        }
    }

One enhancement that I might ask for in the core library is the ability to add an extension to the AspNetCore that would let me use ServiceDepenencies to provide the route table to the configuration so that I don't have to jump through quite so many hoops in ServiceCollectionExtensions.cs. I would love to be able to provide a AddMqttControllers() extension which injects a routing table to services and consume that from within AspNetCore AddHostedMqttServerWithServices() instead of creating yet another confusing override.

Looking forward to your feedback.

instead of your servicecollection extension you could

public static IServiceCollection AddMqttAttributeRouting(this IServiceCollection services)
        {
            services.AddSingleton(c => {
               var assemblies = new Assembly[] { Assembly.GetEntryAssembly() };
               return MqttRouteTableFactory.Create(assemblies);
            });

            return services;
        }
    }

public static AspNetMqttServerOptionsBuilder WithAttributeRouting(this AspNetMqttServerOptionsBuilder options)
{
                options.WithApplicationMessageInterceptor(new MqttServerApplicationMessageInterceptorDelegate(context =>
                        {
                            var rt = options.ServiceProvider.GetRequiredService<MqttRouteTable>();
                            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();

                                using (var scope = options.ServiceProvider.CreateScope())
                                {
                                    object classInstance = ActivatorUtilities.GetServiceOrCreateInstance(scope.ServiceProvider, ctx.Handler.DeclaringType);

                                    // TODO: Handle instance where we get a non MqttBaseController for some reason
                                    ((MqttBaseController)classInstance).MqttContext = context;

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

                                            result = ctx.Handler.Invoke(classInstance, paramArray);
                                        }
                                    }
                                    catch (Exception ex)
                                    {
                                        Debug.WriteLine(ex);
                                    }
                                }

                                context.AcceptPublish = true;

                                Debug.WriteLine($"Matched route ${context.ApplicationMessage.Topic} to handler ${ctx.Handler.DeclaringType.FullName}.{ctx.Handler.Name}");
                            }
                        }));
}

in startup:

services.AddMqttAttributeRouting();
services.AddHostedMqttServerWithServices(options => options.WithAttributeRouting());

this is a little more composable and I changed the instantiation to be as lazy as possible. also I would move the MqttServerApplicationMessageInterceptorDelegate into another class as you may want to reuse that code.

Regarding: MqttBaseController, from my point of view it would be nice if the baseclass is not neccesary. If removing it is not possible at least rename Request to Message as MQTT does not have requests.

aside from that nice work

I have incorporated all of this feedback in the latest commits and published the beta NuGet package at https://www.nuget.org/packages/MQTTnet.AspNetCore.AttributeRouting.

I have moved the MqttServerApplicationMessageInterceptorDelegate to it's own singleton called MqttRouter.cs.

Regarding the base controller, I have taken the same approach as AspNetCore. You can either inherit from the MqttBaseController or create your own controller with a MqttController attribute and a property marked with an MqttControllerContext attribute for injection of the incoming message context.

Would you both be ok with me submitting a PR here to add some documentation directing folks who may be interested in this style of programming to the examples and the other repo? I'm thinking some wiki entries and maybe the readme.

Would you both be ok with me submitting a PR here to add some documentation directing folks who may be interested in this style of programming to the examples and the other repo? I'm thinking some wiki entries and maybe the readme.

@chkr1011

@avishnyak Sure we already mentioned other libs in the readme etc.

Was this page helpful?
0 / 5 - 0 ratings