This little project is a practical implementation of a blog post I wrote about implementing daemons in .NET Core. This daemon is a .NET Core console app that is using a Generic Host to host an MQTT Server based on the code in MQTTNet, which has become my go-to library for MQTT for .NET apps of all kinds. In reality, this really isn’t creating the server. MQTTNet already supplies much of the plumbing needed to do this. This is putting a wrapper around MQTTNet’s server API, which by itself can act as a service because it does implement the IHostedService interface we talked about in our last post.

Download the complete project here.

To make this work, I started with the same basic structure from the previous project with a service class, a config class, and the Program.cs that acts as the entry point for the daemon. I renamed the project to MQTTDaemon, the service to MQTTService and the config to MQTTConfig. Likewise, I added the needed dependencies from Nuget to include MQTTNet. Here are the files respectively:

Program.cs

This simply wired up the renamed services with their new names. Notice the config still gets it’s configuration from the CLI or Environment. In this case, it’s looking for a port.

using System;
using System.Threading.Tasks;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging;
using MQTTnet.Server;

namespace MQTTDaemon
{
    class Program
    {
        public static async Task Main(string[] args)
        {
            var builder = new HostBuilder()
                .ConfigureAppConfiguration((hostingContext, config) =>
                {
                    config.AddEnvironmentVariables();
                    if (args != null)
                    {
                        config.AddCommandLine(args);
                    }
                })
                .ConfigureServices((hostContext, services) =>
                {
                    services.AddOptions();
                    services.Configure<MQTTConfig>(hostContext.Configuration.GetSection("MQTT"));
                    services.AddSingleton<IHostedService, MQTTService>();
                })
                .ConfigureLogging((hostingContext, logging) => {
                    logging.AddConsole();
                });
            await builder.RunConsoleAsync();
        }
    }
}

MQTTConfig.cs

Again, this is just another POCO class that has some fields in it that carry the configuration for the server.

using System;

namespace MQTTDaemon
{
    public class MQTTConfig 
    {
        public int Port { get; set; } 

    }
}

MQTTService.cs

This class is not doing anything particularly special, but take a moment to notice the StartAsync. Rather than return a Task.Completed, this is returning the MQTTNet’s server’s StartAsync task.

using System;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
using MQTTnet;
using MQTTnet.Server;

namespace MQTTDaemon
{

    public class MQTTService : IHostedService, IDisposable
    {
        private readonly ILogger _logger;
        private readonly IOptions<MQTTConfig> _config;
        private IMqttServer _mqttServer;
        public MQTTService(ILogger<MQTTService> logger, IOptions<MQTTConfig> config)
        {
            _logger = logger;
            _config = config;
        }

        public Task StartAsync(CancellationToken cancellationToken)
        {
            _logger.LogInformation("Starting MQTT Daemon on port " + _config.Value.Port);

            //Building the config
            var optionsBuilder = new MqttServerOptionsBuilder()
                .WithConnectionBacklog(1000)
                .WithDefaultEndpointPort(_config.Value.Port);


            //Getting an MQTT Instance
            _mqttServer = new MqttFactory().CreateMqttServer();

            //Wiring up all the events...

            _mqttServer.ClientSubscribedTopic += _mqttServer_ClientSubscribedTopic;
            _mqttServer.ClientUnsubscribedTopic += _mqttServer_ClientUnsubscribedTopic;
            _mqttServer.ClientConnected += _mqttServer_ClientConnected;
            _mqttServer.ClientDisconnected += _mqttServer_ClientDisconnected;
            _mqttServer.ApplicationMessageReceived += _mqttServer_ApplicationMessageReceived;

            //Now, start the server -- Notice this is resturning the MQTT Server's StartAsync, which is a task.
            return _mqttServer.StartAsync(optionsBuilder.Build());
        }

        private void _mqttServer_ApplicationMessageReceived(object sender, MqttApplicationMessageReceivedEventArgs e)
        {
            _logger.LogInformation(e.ClientId + " published message to topic " + e.ApplicationMessage.Topic);
        }

        private void _mqttServer_ClientDisconnected(object sender, MqttClientDisconnectedEventArgs e)
        {
            _logger.LogInformation(e.ClientId + " Disonnected.");
        }

        private void _mqttServer_ClientConnected(object sender, MqttClientConnectedEventArgs e)
        {
            _logger.LogInformation(e.ClientId + " Connected.");
        }

        private void _mqttServer_ClientUnsubscribedTopic(object sender, MqttClientUnsubscribedTopicEventArgs e)
        {
            _logger.LogInformation(e.ClientId + " unsubscribed to " + e.TopicFilter);
        }

        private void _mqttServer_ClientSubscribedTopic(object sender, MqttClientSubscribedTopicEventArgs e)
        {
            _logger.LogInformation(e.ClientId + " subscribed to " + e.TopicFilter);
        }

        public Task StopAsync(CancellationToken cancellationToken)
        {
            _logger.LogInformation("Stopping MQTT Daemon.");
            return _mqttServer.StopAsync();
        }

        public void Dispose()
        {
            _logger.LogInformation("Disposing....");

        }
    }
}

Using the Daemon with a Client

Now, you can build and run  the app. To do so, first run dotnet build in the root directory of the app. Next, run dotnet run --MQTT:Port=1883. This will start the server on port 1883, the standard port for a MQTT Server.

With that running you can test it. There are dozens of tools for testing MQTT Servers and one such tool is MQTTLens, which is a Chrome plugin.  The app is pretty straightforward. Click the + button next to Connections to add a connection, then set the Hostname to localhost and the Port to 1883. Click Create Connection at the bottom of the config to connect to the server.

MQTTLens Config
MQTTLens Config

You’ll notice in the shell where your server is running that a new connection added. Now, you can subscribe to a topic. Enter the name of a topic and click Subscribe. This will create a new topic in the server and MQTTLens is now listening on this topic.

info: MQTTDaemon.MQTTService[0]
      Starting MQTT Daemon on port 1883
Application started. Press Ctrl+C to shut down.
Hosting environment: Production
Content root path: D:\source\dotnet-daemon\mydaemon\bin\Debug\netcoreapp2.1\
info: MQTTDaemon.MQTTService[0]
      lens_tlcrOhH5HWEGFb3egEdX0uTRTzm Connected.

With MQTTLens you can also publish to the topic you created. Simply enter the same topic name, enter a message, then click Publish. The message will show up in MQTTLens.

MQTTLens

MQTTLensBack in the shell running the server, you can see where the topic was subscribed to and where the message was published.

info: MQTTDaemon.MQTTService[0]
      lens_tlcrOhH5HWEGFb3egEdX0uTRTzm subscribed to MyTopic@AtMostOnce
info: MQTTDaemon.MQTTService[0]
      lens_tlcrOhH5HWEGFb3egEdX0uTRTzm published message to topic MyTopic

Conclusion

This little demo shows you how to wire up a daemon and do something practical with it. As mentioned, MQTTnet itself can act as a service too. You don’t need the wrapper, as the library has its own configuration classes, logger interfaces, service classes, etc. so you can can wire it up right in the Program.cs and literally have a 1 class MQTT daemon at your disposal. In our next installment in this series, we’ll look at how to use an .NET Core daemon in Docker containers.