asyncapi / saunter

Saunter is a code-first AsyncAPI documentation generator for dotnet.
https://www.asyncapi.com/
MIT License
199 stars 56 forks source link

✨ Feature - MQTTnet.AspNetCore.AttributeRouting Support #101

Open Rikj000 opened 3 years ago

Rikj000 commented 3 years ago

I've been struggling to get Saunter v0.3.1 to work in combination with:

My project has these 2 MQTTnet libraries implemented successfully in ASP.NET 5 now. I tried to follow Saunters documentation to the letter too. However with MQTTnet there was no need for me to implement MessageBus(Interface)s or Event (model-like) classes.

I can send/receive payloads from the MQTTnet.App client test application, however Saunter AsyncAPI Documentation generation keeps failing, the only thing I see hosted under /asyncapi/ui/index.html is the info defined in new AsyncApiDocument() of Startup.cs.

Which lead me to wondering:

Any help would be truly welcome!

m-wild commented 3 years ago

In theory this is supported. Currently you'd have a couple of options for generating the AsyncAPI components:

  1. Use the saunter-provided attributes. These should be added to your MQTT controller alongside the exiting MQTT AttributeRouting attributes (a bit redundant πŸ™).
  2. Implement an IDocumentFilter to automatically scan the code for the MQTT AttributeRouting attributes.

Option 2 would be ideal as an extension to saunter as an MQTT AttributeRouting support package.

Unfortunately I don't have any experience with MQTTnet or MQTT AttributeRouting, if you could provide a simple example project (or an existing open source project) using these packages, I am happy to spend some time trying to get it working, and prototype a package implementing option 2 above.

I'm not aware of any existing examples using MQTTnet and Saunter.

Saunter doesn't provide any message broker, but has support for documenting them by using bindings. We currently only have binding implemented for amqp, http, and Kafka, but I am happy to add MQTT. These have to be added to the document using filters today, autogenerating them from the MQTT AttributeRouting attributes (as above) would be ideal.

Rikj000 commented 3 years ago

Option 2 would be ideal as an extension to saunter as an MQTT AttributeRouting support package.

This also sounds like the cleanest implementation to me!πŸ™‚ However I'm unsure if I have enough knowledge of MQTT, AsyncAPI and C# Attributes on my own to make this happen..

Unfortunately I don't have any experience with MQTTnet or MQTT AttributeRouting, if you could provide a simple example project (or an existing open source project) using these packages, I am happy to spend some time trying to get it working, and prototype a package implementing option 2 above.

I just poured my current work into a de-branded example project and invited you as a collaborator, also added a quick readme page to quickly help find your way through the project:

Saunter-MQTTnet-AspNet5-AttributeRouting-ExampleProject

I'll continue to try and implement this during my working hours for as long as my boss allows me to work on it, however in my free time I won't have time to work on this, so I hope we can see this through in time! πŸš€

Rikj000 commented 3 years ago

Managed to get Option 1 working! πŸŽ‰

However the Channel name string conversion (Different for MQTTnet Attribute Routing & Saunter) is still HardCoded, I will write up a conversion method and push it to the example project.

Feel free to reference my project in your Documentation or to include the conversion method in Saunter once I finished it up!

m-wild commented 3 years ago

Thanks, I will take a look over the next few days.

m-wild commented 3 years ago

Ok, there are a couple of issues with Option 2...

  1. Saunter DocumentFilterContext does not expose the ISchemaGenerator -- easy fix from my side.
  2. MQTTnet.AspNetCore.AttributeRouting does not provide any public types/methods for inspecting the routing table.

We can either

  1. Re-implement the assembly scanning behaviour of MqttRouteTableFactory
  2. Use reflection to get into the MqttRouteTable
  3. Raise an issue with MQTTnet.AspNetCore.AttributeRouting to make the route table public.
I have a proof of concept using reflection...

```csharp public class MqttNetAspNetCoreAttributeRoutingDocumentFilter : IDocumentFilter { public void Apply(AsyncApiDocument document, DocumentFilterContext context) { var mqttRouteTableFactory = Type.GetType("MQTTnet.AspNetCore.AttributeRouting.MqttRouteTableFactory, MQTTnet.AspNetCore.AttributeRouting"); var mqttRouteTable = Type.GetType("MQTTnet.AspNetCore.AttributeRouting.MqttRouteTable, MQTTnet.AspNetCore.AttributeRouting"); var mqttRoute = Type.GetType("MQTTnet.AspNetCore.AttributeRouting.MqttRoute, MQTTnet.AspNetCore.AttributeRouting"); var mqttRouteTemplate = Type.GetType("MQTTnet.AspNetCore.AttributeRouting.RouteTemplate, MQTTnet.AspNetCore.AttributeRouting"); var mqttTemplateSegment = Type.GetType("MQTTnet.AspNetCore.AttributeRouting.TemplateSegment, MQTTnet.AspNetCore.AttributeRouting"); var create = mqttRouteTableFactory.GetMethod("Create", BindingFlags.Static | BindingFlags.NonPublic, null, CallingConventions.Any, new[] {typeof(IEnumerable)}, null); var routeTable = create.Invoke(null, new object[] { new [] {Assembly.GetEntryAssembly() } }); var routes = mqttRouteTable.GetProperty("Routes").GetValue(routeTable); foreach (var route in (IEnumerable) routes) { var template = mqttRoute.GetProperty("Template").GetValue(route); var segments = mqttRouteTemplate.GetProperty("Segments").GetValue(template); // MQTTnet route templates are not uri safe, which is required by the asyncapi spec. var channelItemName = new List(); foreach (var segment in (IEnumerable)segments) { var isParameter = (bool) mqttTemplateSegment.GetProperty("IsParameter").GetValue(segment); var isCatchAll = (bool) mqttTemplateSegment.GetProperty("IsCatchAll").GetValue(segment); var value = (string) mqttTemplateSegment.GetProperty("Value").GetValue(segment); channelItemName.Add(isCatchAll ? "#" : isParameter ? "+" : value); } var handler = (MethodInfo) mqttRoute.GetProperty("Handler").GetValue(route); var parameters = handler.GetParameters(); ISchema payload = null; if (parameters != null && parameters.Any()) { payload = context.SchemaGenerator.GenerateSchema(parameters.First().ParameterType, context.SchemaRepository); } document.Channels.Add(string.Join("/", channelItemName), new ChannelItem { Publish = new Operation { OperationId = handler.Name, Summary = handler.GetXmlDocsSummary(), Message = new Message { Payload = payload, } } }); } } } ```

If we had access to the MQTTnet RouteTable, it would look something like this...

```csharp public class MqttNetAspNetCoreAttributeRoutingDocumentFilter : IDocumentFilter { private readonly MqttRouteTable _mqttRouteTable; public MqttNetAspNetCoreAttributeRoutingDocumentFilter(MqttRouteTable mqttRouteTable) { _mqttRouteTable = mqttRouteTable; } public void Apply(AsyncApiDocument document, DocumentFilterContext context) { foreach (var route in _mqttRouteTable.Routes) { // MQTTnet route templates are not uri safe, which is required by the asyncapi spec. var channelItemName = string.Join("/", route.Template.Segments.Select(s => s.IsCatchAll ? "#" : s.IsParameter ? "+" : s.Value)); var handler = route.Handler; var parameters = handler.GetParameters(); ISchema payload = null; if (parameters != null && parameters.Any()) { payload = context.SchemaGenerator.GenerateSchema(parameters.First().ParameterType, context.SchemaRepository); } document.Channels.Add(channelItemName, new ChannelItem { Publish = new Operation { OperationId = handler.Name, Summary = handler.GetXmlDocsSummary(), Message = new Message { Payload = payload, } } }); } } } ```

I will raise an issue on the MQTTnet.AspNetCore.AttributeRouting project.