diff --git a/README.md b/README.md index c5961d4..e7b9402 100644 --- a/README.md +++ b/README.md @@ -9,6 +9,7 @@ This repository offers a wide collection of .NET packages for use in microservic # Sections - [RabbitMQ Consumer](#Consumer) +- [JsonSchema generator for Rabbit events](#jsonschema-generator-for-rabbit-events) - [Logging](#Logging) - [API Middlewares](#Middlewares) - [API Documentation](#Documentation) @@ -159,6 +160,42 @@ services .AddPlatformRabbitMqSqlServerConsumerLock(configuration.GetConnectionString("ms sql connection string")); ``` +### JsonSchema Generator for Rabbit events + +Allow to generate json schema for consuming and producing types, +based on [NJsonSchema](https://github.com/RicoSuter/NJsonSchema) + +To use just install +[Luxoft.Bss.Platform.RabbitMq.JsonSchemaGenerator](https://www.nuget.org/packages/Luxoft.Bss.Platform.RabbitMq.JsonSchemaGenerator) +package and add a new middleware via the extension: +```C# +if (app.Environment.IsDevelopment()) +{ + app.UseRabbitJsonSchemaGenerator(opt => + { + opt.Path = "/api/rabbit-json-schema"; + opt.SystemPrefix = "TSS."; + opt.ProducedEventTypes = typeof(IEvent).Assembly.GetTypes() + .Where(x => x.IsPublic && !x.IsAbstract && !x.IsInterface) + .Where(x => x.GetInterfaces().Contains(typeof(IEvent))); + }); +} +``` +The next two options are required only for produced events: +- `SystemPrefix` - the prefix added to the produced type names (`SaveEmployee` -> `TSS.SaveEmployee`) when exporting types in the schema +- `ProducedEventTypes` - collection of all produced types, that need to be exported in the schema + +Consumed events are automatically added if you use the new way to register consumer with +method [AddPlatformRabbitMqConsumerWithMessages](#Register-event-with-builder) + +For more complex cases please use middleware without extensions: +```C# +app.UseMiddleware("/api/rabbit-json-schema", allEventsDict); +``` +where `Dictionary allEventsDict` contains the mapping between routing keys +and types (for both types - consumed and produced) + + ## Logging To use platform logger, first install the [NuGet package](https://www.nuget.org/packages/Luxoft.Bss.Platform.Logging): diff --git a/src/Bss.Platform.RabbitMq.JsonSchemaGenerator/Bss.Platform.RabbitMq.JsonSchemaGenerator.csproj b/src/Bss.Platform.RabbitMq.JsonSchemaGenerator/Bss.Platform.RabbitMq.JsonSchemaGenerator.csproj new file mode 100644 index 0000000..6577468 --- /dev/null +++ b/src/Bss.Platform.RabbitMq.JsonSchemaGenerator/Bss.Platform.RabbitMq.JsonSchemaGenerator.csproj @@ -0,0 +1,13 @@ + + + Luxoft.Bss.Platform.RabbitMq.JsonSchemaGenerator + Bss.Platform.RabbitMq.JsonSchemaGenerator + + + + + + + + + \ No newline at end of file diff --git a/src/Bss.Platform.RabbitMq.JsonSchemaGenerator/DependencyInjection.cs b/src/Bss.Platform.RabbitMq.JsonSchemaGenerator/DependencyInjection.cs new file mode 100644 index 0000000..4376982 --- /dev/null +++ b/src/Bss.Platform.RabbitMq.JsonSchemaGenerator/DependencyInjection.cs @@ -0,0 +1,29 @@ +using Microsoft.AspNetCore.Builder; +using Microsoft.Extensions.DependencyInjection; + +namespace Bss.Platform.RabbitMq.JsonSchemaGenerator; + +public static class DependencyInjection +{ + public static IApplicationBuilder UseRabbitJsonSchemaGenerator( + this IApplicationBuilder app, + Action? setup = null) + { + var options = new GenerateSchemaOptions(); + setup?.Invoke(options); + + var consumedEvents = + app.ApplicationServices + .GetKeyedService>(Consumer.DependencyInjection.RoutingMessageProviderKey) + ?.Select(x => (x.Key, x.Value)) + ?? []; + + var producedEvents = options.ProducedEventTypes.Select(x => ($"{options.SystemPrefix}{x.Name}", x)); + + var allEventsDict = producedEvents.Concat(consumedEvents) + .DistinctBy(x => x.Item1, StringComparer.OrdinalIgnoreCase) + .ToDictionary(x => x.Item1, x => x.Item2); + + return app.UseMiddleware(options.Path, allEventsDict); + } +} diff --git a/src/Bss.Platform.RabbitMq.JsonSchemaGenerator/GenerateSchemaMiddleware.cs b/src/Bss.Platform.RabbitMq.JsonSchemaGenerator/GenerateSchemaMiddleware.cs new file mode 100644 index 0000000..2a37277 --- /dev/null +++ b/src/Bss.Platform.RabbitMq.JsonSchemaGenerator/GenerateSchemaMiddleware.cs @@ -0,0 +1,54 @@ +using Microsoft.AspNetCore.Http; + +using NJsonSchema; +using NJsonSchema.Generation; + +namespace Bss.Platform.RabbitMq.JsonSchemaGenerator; + +public class GenerateSchemaMiddleware(RequestDelegate next, string path, Dictionary eventsDict) +{ + public async Task InvokeAsync(HttpContext context) + { + if (context.Request.Method == "GET" + && context.Request.Path.Value.Equals(path, StringComparison.InvariantCultureIgnoreCase)) + { + await this.GenerateSchema(context); + return; + } + + await next(context); + } + + private async Task GenerateSchema(HttpContext context) + { + var settings = new SystemTextJsonSchemaGeneratorSettings + { + FlattenInheritanceHierarchy = true, + GenerateAbstractProperties = false, + AllowReferencesWithProperties = false + }; + + var schemaContainer = new JsonSchema(); + var appender = new JsonSchemaAppender(schemaContainer, new MappedNameGenerator(eventsDict)); + var generator = new NJsonSchema.Generation.JsonSchemaGenerator(settings); + + var jsonSchemas = eventsDict.Select(x => x.Value).Select(generator.Generate); + foreach (var schema in jsonSchemas) + { + appender.AppendSchema(schema, null); + } + + context.Response.StatusCode = 200; + context.Response.ContentType = "application/json"; + await context.Response.WriteAsync(schemaContainer.ToJson()); + } +} + +file class MappedNameGenerator(Dictionary eventsDict) : ITypeNameGenerator +{ + private readonly Dictionary mapping = eventsDict.DistinctBy(x => x.Value) + .ToDictionary(x => x.Value.Name, x => x.Key); + + public string Generate(JsonSchema schema, string? typeNameHint, IEnumerable reservedTypeNames) => + this.mapping.GetValueOrDefault(schema.Title ?? throw new("JsonSchema title is null"), schema.Title); +} diff --git a/src/Bss.Platform.RabbitMq.JsonSchemaGenerator/GenerateSchemaOptions.cs b/src/Bss.Platform.RabbitMq.JsonSchemaGenerator/GenerateSchemaOptions.cs new file mode 100644 index 0000000..0f83430 --- /dev/null +++ b/src/Bss.Platform.RabbitMq.JsonSchemaGenerator/GenerateSchemaOptions.cs @@ -0,0 +1,17 @@ +namespace Bss.Platform.RabbitMq.JsonSchemaGenerator; + +public class GenerateSchemaOptions +{ + public string Path { get; set; } = "/api/rabbit-json-schema"; + + + /// + /// Collection of produced events + /// + public IEnumerable ProducedEventTypes { get; set; } = []; + + /// + /// Prefix to add to produced event type + /// + public string SystemPrefix { get; set; } = string.Empty; +} diff --git a/src/Bss.Platform.sln b/src/Bss.Platform.sln index f4652d8..1d04266 100644 --- a/src/Bss.Platform.sln +++ b/src/Bss.Platform.sln @@ -40,6 +40,8 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Notifications", "Notificati EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Bss.Platform.Notifications.Audit", "Bss.Platform.Notifications.Audit\Bss.Platform.Notifications.Audit.csproj", "{03CD2FDC-2BF1-497D-BF18-F15170C0AD85}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Bss.Platform.RabbitMq.JsonSchemaGenerator", "Bss.Platform.RabbitMq.JsonSchemaGenerator\Bss.Platform.RabbitMq.JsonSchemaGenerator.csproj", "{0FC70E24-62A9-44BF-A433-804C98514A09}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -58,6 +60,7 @@ Global {BE2E219F-3F3B-48E8-B13B-A71321BB6372} = {80AA8C01-842D-4A5F-BCBE-48E0361783A0} {A97421A9-58B7-4948-8231-2028B5D11F36} = {94640F4D-8BC6-407F-835E-73B454972CAD} {03CD2FDC-2BF1-497D-BF18-F15170C0AD85} = {94640F4D-8BC6-407F-835E-73B454972CAD} + {0FC70E24-62A9-44BF-A433-804C98514A09} = {0F54786D-AB46-46AC-88DF-BEB789A62C1F} EndGlobalSection GlobalSection(ProjectConfigurationPlatforms) = postSolution {860BFBD8-26EA-44F9-980E-21B828FC8F72}.Debug|Any CPU.ActiveCfg = Debug|Any CPU @@ -108,5 +111,9 @@ Global {03CD2FDC-2BF1-497D-BF18-F15170C0AD85}.Debug|Any CPU.Build.0 = Debug|Any CPU {03CD2FDC-2BF1-497D-BF18-F15170C0AD85}.Release|Any CPU.ActiveCfg = Release|Any CPU {03CD2FDC-2BF1-497D-BF18-F15170C0AD85}.Release|Any CPU.Build.0 = Release|Any CPU + {0FC70E24-62A9-44BF-A433-804C98514A09}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {0FC70E24-62A9-44BF-A433-804C98514A09}.Debug|Any CPU.Build.0 = Debug|Any CPU + {0FC70E24-62A9-44BF-A433-804C98514A09}.Release|Any CPU.ActiveCfg = Release|Any CPU + {0FC70E24-62A9-44BF-A433-804C98514A09}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection EndGlobal diff --git a/src/Directory.Build.props b/src/Directory.Build.props index 0faceeb..c854181 100644 --- a/src/Directory.Build.props +++ b/src/Directory.Build.props @@ -26,11 +26,6 @@ NU1901;NU1902;NU1903;NU1904 - - None - False - - diff --git a/src/Directory.Packages.props b/src/Directory.Packages.props index 8fb7a7f..e8edfdb 100644 --- a/src/Directory.Packages.props +++ b/src/Directory.Packages.props @@ -9,6 +9,7 @@ +