Saunter

repository·main·Indexed 42 days ago

https://github.com/asyncapi/saunter

A tool for generating AsyncAPI documentation from .NET code, allowing developers to maintain a single source of truth for event-driven architectures. It provides attributes for annotating classes and methods, middleware to expose AsyncAPI JSON and UI, and support for protocol-specific bindings. Version 1.0.0.

Tokens
3.9K
Snippets
10
Records
12
Agent score
3%

What's inside Saunter

  1. Understand the generated AsyncAPI document structure

    main

    Saunter automatically generates an AsyncAPI document based on your code. A key behavior to note is that schemas are automatically moved to the components/schemas section of the document to ensure valid AsyncAPI structure.

    The document includes:

    • channels: Defines the communication paths (e.g., publish/light/measured, subscribe/light/measured).
    • components/schemas: Contains the data structures (e.g., lightMeasuredEvent).
    • components/messages: Defines the messages used in channels, referencing the schemas.
    • components/bindings: Contains channel and operation bindings (e.g., amqp or http bindings).
    {
      "asyncapi": "2.6.0",
      "channels": {
        "publish/light/measured": {
          "message": { "$ref": "#/components/messages/lightMeasuredEvent" }
        }
      },
      "components": {
        "schemas": {
          "lightMeasuredEvent": {
            "type": "object",
            "properties": {
              "id": { "type": "integer", "format": "int32" },
              "lumens": { "type": "integer", "format": "int32" }
            }
          }
        }
      }
    }
  2. Use Bindings to describe protocol-specific information

    main

    Bindings allow you to include protocol-specific details (like AMQP exchanges or HTTP methods) in your AsyncAPI document.

    1. Define the bindings in the AsyncApiDocument.Components section within AddAsyncApiSchemaGeneration.
    2. Reference these bindings in your attributes ([OperationAttribute], [MessageAttribute], or [ChannelAttribute]) using the BindingsRef property.
    // 1. Define bindings in Startup.cs
    services.AddAsyncApiSchemaGeneration(options =>
    {
        options.AsyncApi = new AsyncApiDocument
        {
            Components = 
            {
                ChannelBindings =
                {
                    ["amqpDev"] = new()
                    {
                        new AMQPChannelBinding
                        {
                            Is = ChannelType.Queue,
                            Exchange = new()
                            {
                                Name = "example-exchange",
                                Vhost = "/development"
                            }
                        }
                    }
                },
                OperationBindings =
                {
                    {
                        "postBind",
                        new()
                        {
                            new HttpOperationBinding
                            {
                                Method = "POST",
                                Type = HttpOperationType.Response,
                            }
                        }
                    }
                }
            }
        }
    });
    
    // 2. Reference bindings in attributes
    [Channel("light.measured", BindingsRef = "amqpDev")]
    public void PublishLightMeasuredEvent(Streetlight streetlight, int lumens) {}
    
    [PublishOperation(typeof(LightMeasuredEvent), "Light", BindingsRef = "postBind")]
    public void MeasureLight([FromBody] LightMeasuredEvent lightMeasuredEvent)
  3. Annotate classes and methods with AsyncAPI attributes

    main

    To generate the documentation, decorate your classes and methods with Saunter attributes:

    1. Use [AsyncApi] on a class to tell Saunter to scan it.
    2. Use [Channel] to define a channel and its associated server.
    3. Use [SubscribeOperation] or [PublishOperation] on methods to define operations within that channel.
    [AsyncApi] // Tells Saunter to scan this class.
    public class StreetlightMessageBus : IStreetlightMessageBus
    {
        private const string SubscribeLightMeasuredTopic = "subscribe/light/measured";
        
        [Channel(SubscribeLightMeasuredTopic, Servers = new[] { "mosquitto" })]
        [SubscribeOperation(typeof(LightMeasuredEvent), "Light", Summary = "Subscribe to environmental lighting conditions for a particular streetlight.")]
        public void PublishLightMeasuredEvent(Streetlight streetlight, int lumens) {}
    }
    [AsyncApi] // Tells Saunter to scan this class.
    public class StreetlightMessageBus : IStreetlightMessageBus
    {
        private const string SubscribeLightMeasuredTopic = "subscribe/light/measured";
        
        [Channel(SubscribeLightMeasuredTopic, Servers = new[] { "mosquitto" })]
        [SubscribeOperation(typeof(LightMeasuredEvent), "Light", Summary = "Subscribe to environmental lighting conditions for a particular streetlight.")]
         public void PublishLightMeasuredEvent(Streetlight streetlight, int lumens) {}
    }
  4. Configure Saunter in Startup.cs

    main

    Register Saunter in your application services using AddAsyncApiSchemaGeneration. You must specify AssemblyMarkerTypes to tell Saunter which assemblies to scan for AsyncAPI attributes. You can also manually define parts of the AsyncApiDocument such as Info and Servers.

    // Add Saunter to the application services. 
    services.AddAsyncApiSchemaGeneration(options =>
    {
        // Specify example type(s) from assemblies to scan.
        options.AssemblyMarkerTypes = new[] { typeof(StreetlightMessageBus) };
        
        // Build as much (or as little) of the AsyncApi document as you like.
        options.Middleware.UiTitle = "Streetlights API";
        options.AsyncApi = new AsyncApiDocument
        {
            Info = new AsyncApiInfo()
            {
                Title = "Streetlights API",
                Version = "1.0.0",
                Description = "The Smartylighting Streetlights API allows you to remotely manage the city lights.",
                License = new AsyncApiLicense()
                {
                    Name = "Apache 2.0",
                    Url = new("https://www.apache.org/licenses/LICENSE-2.0"),
                }
            },
            Servers =
            {
                ["mosquitto"] = new AsyncApiServer(){ Url = "test.mosquitto.org",  Protocol = "mqtt"},
                ["webapi"] = new AsyncApiServer(){ Url = "localhost:5000",  Protocol = "http"},
            },
        };
    });
  5. Run the Streetlights API Example

    main

    To run the Streetlights API example project, you must first manually install the UI assets if you are referencing the Saunter project directly for debugging. If you are using the NuGet package in a standard project, these steps are not required.

    1. Install UI assets (only if referencing Saunter source directly): Navigate to ~/saunter/src/Saunter.UI and run npm install.

    2. Run the example: Navigate to the example directory and use dotnet run.

    Once running, the application will expose the AsyncAPI documentation and the UI at specific endpoints (typically on port 5000).

    # Install UI assets (if referencing Saunter source directly)
    $ cd ~/saunter/src/Saunter.UI
    $ npm install
    
    # Run the example
    $ cd ~/saunter/examples/StreetlightsAPI
    $ dotnet run
  6. Generate multiple AsyncAPI documents

    main

    You can generate multiple distinct AsyncAPI documents by using ConfigureNamedAsyncApi.

    1. In Startup.cs, use ConfigureNamedAsyncApi(name, callback) to define the configuration for each document.
    2. Decorate your classes with [AsyncApi(name)] to specify which document they belong to.

    Each document is accessible via a unique URL: /asyncapi/{name}/asyncapi.json.

    // Startup.cs
    services.AddAsyncApiSchemaGeneration(options =>
    {
        options.AssemblyMarkerTypes = new[] {typeof(FooMessageBus)};
    });
    
    services.ConfigureNamedAsyncApi("Foo", asyncApi => 
    {
        asyncApi.Info = new Info("Foo API", "1.0.0");
    });
    
    services.ConfigureNamedAsyncApi("Bar", asyncApi => 
    {
        asyncApi.Info = new Info("Bar API", "1.0.0");
    });
    
    // Classes
    [AsyncApi("Foo")]
    public class FooMessageBus 
    {
        // Channels here go to "Foo" document
    }
    
    [AsyncApi("Bar")]
    public class BarMessageBus 
    {
        // Channels here go to "Bar" document
    }
  7. Expose AsyncAPI JSON and UI via Middleware

    main

    In the Configure method of Startup.cs, add the Saunter middleware to your endpoint routing to host the generated JSON document and the AsyncAPI UI.

    app.UseEndpoints(endpoints =>
    {
        endpoints.MapAsyncApiDocuments();
        endpoints.MapAsyncApiUi();
    
        endpoints.MapControllers();
    });
  8. Access AsyncAPI Documentation and UI

    main

    After running a Saunter-enabled application, you can access the generated AsyncAPI assets via HTTP. By default, the following paths are used:

    • AsyncAPI JSON Document: http://localhost:5000/asyncapi/asyncapi.json
    • AsyncAPI UI: http://localhost:5000/asyncapi/ui/

    Note: The port (e.g., 5000) may vary depending on your application's configuration.

  9. Install Saunter via dotnet CLI

    main

    To use Saunter in your .NET project, install the Saunter NuGet package using the dotnet CLI.

    Note: Versions prior to 1.0.0 have an unstable API and may introduce breaking changes.

    dotnet add package Saunter
  10. Configure Saunter middleware options

    main

    You can customize the behavior of the Saunter middleware using the AsyncApiOptions object in AddAsyncApiSchemaGeneration.

    Common options include:

    • AssemblyMarkerTypes: Types used to identify assemblies to scan.
    • AddChannelFilter<T>: Adds a dynamic filter to update ChannelItems.
    • AddOperationFilter<T>: Adds a dynamic filter to update Operations.
    • Middleware.Route: The URL path for the AsyncAPI JSON document (default is /asyncapi/asyncapi.json).
    • Middleware.UiBaseRoute: The URL path for the AsyncAPI UI (default is /asyncapi/ui/).
    • Middleware.UiTitle: The title displayed on the AsyncAPI UI page.
    services.AddAsyncApiSchemaGeneration(options =>
    {
        options.AssemblyMarkerTypes = new[] { typeof(Startup) };   // Tell Saunter where to scan for your classes.
        
        options.AddChannelFilter<MyAsyncApiChannelFilter>();       // Dynamically update ChanelItems
        options.AddOperationFilter<MyOperationFilter>();           // Dynamically update Operations
        
        options.Middleware.Route = "/asyncapi/asyncapi.json";      // AsyncAPI JSON document URL
        options.Middleware.UiBaseRoute = "/asyncapi/ui/";          // AsyncAPI UI URL
        options.Middleware.UiTitle = "My AsyncAPI Documentation";  // AsyncAPI UI page title
    }
  11. Migration guide: Moving to LEGO AsyncApi.Net

    main

    If you are migrating from an older version of Saunter to the version using LEGO AsyncApi.Net, be aware of the following breaking changes:

    • Namespace Changes:
      • Saunter.AsyncApiSchema.v2 $\rightarrow$ LEGO.AsyncAPI.Models
      • Saunter.Attributes $\rightarrow$ Saunter.AttributeProvider.Attributes
    • Data Structure Renaming: Most data structures now have an AsyncApi prefix (e.g., Info is now AsyncApiInfo).
    • Constructors: All data structure constructors now use parameterless constructors.
  12. Test the Streetlights API via HTTP

    main

    You can interact with the Streetlights API using standard HTTP tools like curl or PowerShell's Invoke-WebRequest.

    Get streetlights

    Retrieve the current list of streetlights:

    Invoke-WebRequest 'http://localhost:5000/api/streetlights' | ConvertFrom-Json

    Publish a light measurement

    Send a POST request to publish a new light measurement to the system:

    Invoke-WebRequest -Method POST -Uri 'http://localhost:5000/publish/light/measured' -Body '{"id":1, "lumens":400}' -ContentType 'application/json' | Select StatusCode
    # Get streetlights
    Invoke-WebRequest 'http://localhost:5000/api/streetlights' | ConvertFrom-Json
    
    # Publish an example light measurement
    Invoke-WebRequest -Method POST -Uri 'http://localhost:5000/publish/light/measured' -Body '{"id":1, "lumens":400}' -ContentType 'application/json' | Select StatusCode