Skip to content

Commit

Permalink
Add AsyncAPI support via Saunter
Browse files Browse the repository at this point in the history
Signed-off-by: Tomasz Maruszak <maruszaktomasz@gmail.com>
  • Loading branch information
zarusz committed Apr 5, 2023
1 parent fc718dc commit 9277108
Show file tree
Hide file tree
Showing 29 changed files with 902 additions and 7 deletions.
2 changes: 2 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,7 @@ SlimMessageBus is a client façade for message brokers for .NET. It comes with i
- [Serialization](docs/serialization.md)
- [Transactional Outbox](docs/plugin_outbox.md)
- [Validation using FluentValidation](docs/plugin_fluent_validation.md)
- [AsyncAPI specification generation](docs/plugin_asyncapi.md)

## Packages

Expand All @@ -76,6 +77,7 @@ SlimMessageBus is a client façade for message brokers for .NET. It comes with i
| `.Host.FluentValidation` | Validation for messages based on [FluentValidation](https://www.nuget.org/packages/FluentValidation) | [![NuGet](https://img.shields.io/nuget/v/SlimMessageBus.Host.FluentValidation.svg)](https://www.nuget.org/packages/SlimMessageBus.Host.FluentValidation) |
| `.Host.Outbox.Sql` | Transactional Outbox using SQL | [![NuGet](https://img.shields.io/nuget/v/SlimMessageBus.Host.Outbox.Sql.svg)](https://www.nuget.org/packages/SlimMessageBus.Host.Outbox.Sql) |
| `.Host.Outbox.DbContext` | Transactional Outbox using EF DbContext | [![NuGet](https://img.shields.io/nuget/v/SlimMessageBus.Host.Outbox.DbContext.svg)](https://www.nuget.org/packages/SlimMessageBus.Host.Outbox.DbContext) |
| `.Host.AsyncApi` | [AsyncAPI](https://www.asyncapi.com/) specification generation via [Saunter](https://github.com/tehmantra/saunter) | [![NuGet](https://img.shields.io/nuget/v/SlimMessageBus.Host.AsyncApi.svg)](https://www.nuget.org/packages/SlimMessageBus.Host.AsyncApi) |

Typically the application layers (domain model, business logic) only need to depend on `SlimMessageBus` which is the facade, and ultimately the application hosting layer (ASP.NET, Console App, Windows Service) will reference and configure the other packages (`SlimMessageBus.Host.*`) which are the messaging transport providers and additional plugins.

Expand Down
4 changes: 3 additions & 1 deletion build/tasks.ps1
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,9 @@ $projects = @(

"SlimMessageBus.Host.Outbox",
"SlimMessageBus.Host.Outbox.Sql",
"SlimMessageBus.Host.Outbox.DbContext"
"SlimMessageBus.Host.Outbox.DbContext",

"SlimMessageBus.Host.AsyncApi"
)

# msbuild.exe https://msdn.microsoft.com/pl-pl/library/ms164311(v=vs.80).aspx
Expand Down
92 changes: 92 additions & 0 deletions docs/plugin_asyncapi.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
# AsyncAPI Specification Plugin for SlimMessageBus <!-- omit in toc -->

Please read the [Introduction](intro.md) before reading this provider documentation.

- [Introduction](#introduction)
- [Configuration](#configuration)
- [Sample AsyncAPI document](#sample-asyncapi-document)
- [Documentation](#documentation)

## Introduction

The [`SlimMessageBus.Host.AsyncApi`](https://www.nuget.org/packages/SlimMessageBus.Host.AsyncApi) introduces a document generator for [Saunter](https://github.com/tehmantra/saunter), which enables to generate an [AsyncAPI specification](https://www.asyncapi.com/) from SlimMessageBus.

## Configuration

On the SMB setup, use the `mbb.AddAsyncApiDocumentGenerator()` to add the `IDocumentGenerator` for Saunter library:

```cs
services.AddSlimMessageBus(mbb =>
{
// Register the IDocumentGenerator for Saunter library
mbb.AddAsyncApiDocumentGenerator();
});
```

Then register the Saunter services (in that order):

```cs
services.AddAsyncApiSchemaGeneration(options =>
{
options.AsyncApi = new AsyncApiDocument
{
Info = new Info("SlimMessageBus AsyncAPI Sample API", "1.0.0")
{
Description = "This is a sample of the SlimMessageBus AsyncAPI plugin",
License = new License("Apache 2.0")
{
Url = "https://www.apache.org/licenses/LICENSE-2.0"
}
}
};
});
```

Saunter also requires to add the following endpoints (consult the Saunter docs):

```cs
// Register AsyncAPI docs via Sauter
app.MapAsyncApiDocuments();
app.MapAsyncApiUi();
```

See the [Sample.AsyncApi.Service](../src/Samples/Sample.AsyncApi.Service/) for a complete setup.

## Sample AsyncAPI document

When running the mentioned sample, the AsyncAPI document can be obtained via the following link:
https://localhost:7252/asyncapi/asyncapi.json

The generated document for the sample is available [here](../src/Samples/Sample.AsyncApi.Service/asyncapi.json) as well.

## Documentation

The comment and remarks are being taken from the code (for the consumer method and message type):

```cs
/// <summary>
/// Event when a customer is created within the domain.
/// </summary>
/// <param name="Id"></param>
/// <param name="Firstname"></param>
/// <param name="Lastname"></param>
public record CustomerCreatedEvent(Guid Id, string Firstname, string Lastname) : CustomerEvent(Id);

public class CustomerCreatedEventConsumer : IConsumer<CustomerCreatedEvent>
{
/// <summary>
/// Upon the <see cref="CustomerCreatedEvent"/> will store it with the database.
/// </summary>
/// <param name="message"></param>
/// <returns></returns>
public Task OnHandle(CustomerCreatedEvent message) { }
}
```

Ensure that your project has the `GenerateDocumentationFile` enabled (more [here](https://learn.microsoft.com/en-us/aspnet/core/tutorials/getting-started-with-swashbuckle?view=aspnetcore-7.0&tabs=visual-studio#xml-comments)):

```xml
<PropertyGroup>
<GenerateDocumentationFile>true</GenerateDocumentationFile>
</PropertyGroup>
```
File renamed without changes.
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
namespace Sample.AsyncApi.Service.Controllers;

using Microsoft.AspNetCore.Mvc;

[ApiController]
[Route("[controller]")]
public class WeatherForecastController : ControllerBase
{
private static readonly string[] Summaries = new[]
{
"Freezing", "Bracing", "Chilly", "Cool", "Mild", "Warm", "Balmy", "Hot", "Sweltering", "Scorching"
};

private readonly ILogger<WeatherForecastController> _logger;

public WeatherForecastController(ILogger<WeatherForecastController> logger)
{
_logger = logger;
}

[HttpGet(Name = "GetWeatherForecast")]
public IEnumerable<WeatherForecast> Get()
{
return Enumerable.Range(1, 5).Select(index => new WeatherForecast
{
Date = DateOnly.FromDateTime(DateTime.Now.AddDays(index)),
TemperatureC = Random.Shared.Next(-20, 55),
Summary = Summaries[Random.Shared.Next(Summaries.Length)]
})
.ToArray();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
namespace Sample.AsyncApi.Service.Messages;

/// <summary>
/// Event when a customer is created within the domain.
/// </summary>
/// <param name="Id"></param>
/// <param name="Firstname"></param>
/// <param name="Lastname"></param>
public record CustomerCreatedEvent(Guid Id, string Firstname, string Lastname) : CustomerEvent(Id);
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
namespace Sample.AsyncApi.Service.Messages;

public class CustomerCreatedEventConsumer : IConsumer<CustomerCreatedEvent>
{
/// <summary>
/// Upon the <see cref="CustomerCreatedEvent"/> will store it with the database.
/// </summary>
/// <param name="message"></param>
/// <returns></returns>
public Task OnHandle(CustomerCreatedEvent message)
{
throw new NotImplementedException();
}
}
10 changes: 10 additions & 0 deletions src/Samples/Sample.AsyncApi.Service/Messages/CustomerEvent.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
namespace Sample.AsyncApi.Service.Messages;

/// <summary>
/// Customer related events that notify about interesting events around customer lifecycle.
/// </summary>
/// <param name="Id"></param>
public record CustomerEvent(Guid Id)
{
public DateTime Timestamp { get; init; } = DateTime.UtcNow;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
namespace Sample.AsyncApi.Service.Messages;

public class CustomerEventConsumer : IConsumer<CustomerEvent>
{
/// <summary>
/// Process the <see cref="CustomerEvent"/> and acts accordingly.
/// </summary>
/// <remarks>
/// This will create an customer entry in the local databse for the created customer.
/// </remarks>
/// <param name="message"></param>
/// <returns></returns>
public Task OnHandle(CustomerEvent message)
{
throw new NotImplementedException();
}
}
94 changes: 94 additions & 0 deletions src/Samples/Sample.AsyncApi.Service/Program.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
using System.Reflection;

using Sample.AsyncApi.Service.Messages;

using Saunter;
using Saunter.AsyncApiSchema.v2;
using Saunter.Generation;

using SecretStore;

using SlimMessageBus.Host;
using SlimMessageBus.Host.AsyncApi;
using SlimMessageBus.Host.AzureServiceBus;
using SlimMessageBus.Host.Memory;
using SlimMessageBus.Host.Serialization.Json;

// Local file with secrets
Secrets.Load(@"..\..\secrets.txt");

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddControllers();
// Learn more about configuring Swagger/OpenAPI at https://aka.ms/aspnetcore/swashbuckle
//builder.Services.AddEndpointsApiExplorer();
//builder.Services.AddSwaggerGen();
builder.Services.AddSwaggerDocument();

builder.Services.AddSlimMessageBus(mbb =>
{
var configuration = builder.Configuration;

mbb
.AddChildBus("Memory", mbb =>
{
mbb
.WithProviderMemory()
.AutoDeclareFrom(Assembly.GetExecutingAssembly(), consumerTypeFilter: t => t.Name.Contains("Command"));
})
.AddChildBus("AzureSB", mbb =>
{
mbb
.WithProviderServiceBus(cfg => cfg.ConnectionString = Secrets.Service.PopulateSecrets(configuration["Azure:ServiceBus"]))
.Produce<CustomerEvent>(x =>
{
x.DefaultTopic("samples.asyncapi/customer-events");
})
.Consume<CustomerEvent>(x =>
{
x.Topic("samples.asyncapi/customer-events");
x.SubscriptionName("asyncapi-service");
x.WithConsumer<CustomerCreatedEventConsumer, CustomerCreatedEvent>();
});
})
.AddServicesFromAssembly(Assembly.GetExecutingAssembly())
.AddJsonSerializer()
.AddAsyncApiDocumentGenerator();
});

// Add Saunter to the application services.
builder.Services.AddAsyncApiSchemaGeneration(options =>
{
options.AsyncApi = new AsyncApiDocument
{
Info = new Info("SlimMessageBus AsyncAPI Sample API", "1.0.0")
{
Description = "This is a sample of the SlimMessageBus AsyncAPI plugin",
License = new License("Apache 2.0")
{
Url = "https://www.apache.org/licenses/LICENSE-2.0"
}
}
};
});

var app = builder.Build();

// Configure the HTTP request pipeline.
if (app.Environment.IsDevelopment())
{
app.UseOpenApi();
app.UseSwaggerUi3();
}

app.UseHttpsRedirection();

app.UseAuthorization();

app.MapControllers();

// Register AsyncAPI docs via Sauter
app.MapAsyncApiDocuments();
app.MapAsyncApiUi();

app.Run();
41 changes: 41 additions & 0 deletions src/Samples/Sample.AsyncApi.Service/Properties/launchSettings.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
{
"$schema": "https://json.schemastore.org/launchsettings.json",
"iisSettings": {
"windowsAuthentication": false,
"anonymousAuthentication": true,
"iisExpress": {
"applicationUrl": "http://localhost:37649",
"sslPort": 44341
}
},
"profiles": {
"http": {
"commandName": "Project",
"dotnetRunMessages": true,
"launchBrowser": true,
"launchUrl": "swagger",
"applicationUrl": "http://localhost:5134",
"environmentVariables": {
"ASPNETCORE_ENVIRONMENT": "Development"
}
},
"https": {
"commandName": "Project",
"dotnetRunMessages": true,
"launchBrowser": true,
"launchUrl": "swagger",
"applicationUrl": "https://localhost:7252;http://localhost:5134",
"environmentVariables": {
"ASPNETCORE_ENVIRONMENT": "Development"
}
},
"IIS Express": {
"commandName": "IISExpress",
"launchBrowser": true,
"launchUrl": "swagger",
"environmentVariables": {
"ASPNETCORE_ENVIRONMENT": "Development"
}
}
}
}
22 changes: 22 additions & 0 deletions src/Samples/Sample.AsyncApi.Service/Sample.AsyncApi.Service.csproj
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
<Project Sdk="Microsoft.NET.Sdk.Web">

<PropertyGroup>
<TargetFramework>net7.0</TargetFramework>
<Nullable>enable</Nullable>
<ImplicitUsings>enable</ImplicitUsings>
<GenerateDocumentationFile>true</GenerateDocumentationFile>
<NoWarn>$(NoWarn);1591</NoWarn>
</PropertyGroup>

<ItemGroup>
<PackageReference Include="Microsoft.AspNetCore.OpenApi" Version="7.0.4" />
<PackageReference Include="NSwag.AspNetCore" Version="13.18.2" />
<ProjectReference Include="..\..\SlimMessageBus.Host.AsyncApi\SlimMessageBus.Host.AsyncApi.csproj" />
<ProjectReference Include="..\..\SlimMessageBus.Host.AzureServiceBus\SlimMessageBus.Host.AzureServiceBus.csproj" />
<ProjectReference Include="..\..\SlimMessageBus.Host.Kafka\SlimMessageBus.Host.Kafka.csproj" />
<ProjectReference Include="..\..\SlimMessageBus.Host.Memory\SlimMessageBus.Host.Memory.csproj" />
<ProjectReference Include="..\..\SlimMessageBus.Host.Serialization.Json\SlimMessageBus.Host.Serialization.Json.csproj" />
<ProjectReference Include="..\..\Tools\SecretStore\SecretStore.csproj" />
</ItemGroup>

</Project>
1 change: 1 addition & 0 deletions src/Samples/Sample.AsyncApi.Service/Usings.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
global using SlimMessageBus;
13 changes: 13 additions & 0 deletions src/Samples/Sample.AsyncApi.Service/WeatherForecast.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
namespace Sample.AsyncApi.Service
{
public class WeatherForecast
{
public DateOnly Date { get; set; }

public int TemperatureC { get; set; }

public int TemperatureF => 32 + (int)(TemperatureC / 0.5556);

public string? Summary { get; set; }
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
{
"Logging": {
"LogLevel": {
"Default": "Information",
"Microsoft.AspNetCore": "Warning"
}
}
}
Loading

0 comments on commit 9277108

Please sign in to comment.