From 41adfd7ea80f85104a80814068cda581364dc904 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jakub=20Fija=C5=82kowski?= Date: Fri, 3 Jan 2025 15:36:31 +0100 Subject: [PATCH 1/4] Adjust `CQRSApiDescriptionProvider` so that it matches how Microsoft.AspNetCore.OpenApi operates --- .../CQRSApiDescriptionConfiguration.cs | 16 ++++---- .../CQRSApiDescriptionProvider.cs | 12 ++++-- .../Registration/EndpointMetadata.cs | 29 ++++++++++++++ .../CQRSApiDescriptionProviderTests.cs | 40 ++++++++++++------- 4 files changed, 70 insertions(+), 27 deletions(-) create mode 100644 src/CQRS/LeanCode.CQRS.AspNetCore/Registration/EndpointMetadata.cs diff --git a/src/CQRS/LeanCode.CQRS.AspNetCore/Registration/CQRSApiDescriptionConfiguration.cs b/src/CQRS/LeanCode.CQRS.AspNetCore/Registration/CQRSApiDescriptionConfiguration.cs index 60a60e6b..9d059f98 100644 --- a/src/CQRS/LeanCode.CQRS.AspNetCore/Registration/CQRSApiDescriptionConfiguration.cs +++ b/src/CQRS/LeanCode.CQRS.AspNetCore/Registration/CQRSApiDescriptionConfiguration.cs @@ -1,16 +1,16 @@ -using System.Collections.Frozen; +using LeanCode.CQRS.Execution; using Microsoft.AspNetCore.Routing; namespace LeanCode.CQRS.AspNetCore.Registration; public class CQRSApiDescriptionConfiguration { - // Required by Swagger (Swashbuckle) - public static readonly FrozenDictionary DefaultRouteValues = new Dictionary - { - ["controller"] = "CQRS", - }.ToFrozenDictionary(); + public Func> TagsMapping { get; init; } = + static (_, _) => Array.Empty(); - public Func> RouteValuesMapping { get; init; } = - _ => DefaultRouteValues; + public Func SummaryMapping { get; init; } = + static (_, m) => m.ObjectType.FullName ?? ""; + + public Func DescriptionMapping { get; init; } = + static (_, m) => $"Executed by {m.HandlerType.FullName ?? "unknown handler"}"; } diff --git a/src/CQRS/LeanCode.CQRS.AspNetCore/Registration/CQRSApiDescriptionProvider.cs b/src/CQRS/LeanCode.CQRS.AspNetCore/Registration/CQRSApiDescriptionProvider.cs index 8905f7d4..a9886c1a 100644 --- a/src/CQRS/LeanCode.CQRS.AspNetCore/Registration/CQRSApiDescriptionProvider.cs +++ b/src/CQRS/LeanCode.CQRS.AspNetCore/Registration/CQRSApiDescriptionProvider.cs @@ -2,6 +2,7 @@ using System.Text; using LeanCode.Contracts; using LeanCode.CQRS.Execution; +using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc.Abstractions; using Microsoft.AspNetCore.Mvc.ApiExplorer; using Microsoft.AspNetCore.Mvc.ModelBinding; @@ -55,7 +56,13 @@ private ApiDescription CreateApiDescription(RouteEndpoint routeEndpoint, CQRSObj ActionDescriptor = new ActionDescriptor { DisplayName = routeEndpoint.DisplayName, - RouteValues = configuration.RouteValuesMapping(routeEndpoint), + EndpointMetadata = + [ + metadata, + new EndpointTags(configuration.TagsMapping(routeEndpoint, metadata), metadata), + new EndpointSummary(configuration.SummaryMapping(routeEndpoint, metadata)), + new EndpointDescription(configuration.DescriptionMapping(routeEndpoint, metadata)), + ], }, }; apiDescription.SupportedRequestFormats.Add(new() { MediaType = ApplicationJson }); @@ -181,7 +188,6 @@ private static void AddCommonResponses(ApiDescription apiDescription) ModelMetadata = CreateModelMetadata(typeof(void)), ApiResponseFormats = [new() { MediaType = ApplicationJson }], StatusCode = 400, - Type = typeof(void), } ); @@ -191,7 +197,6 @@ private static void AddCommonResponses(ApiDescription apiDescription) ModelMetadata = CreateModelMetadata(typeof(void)), ApiResponseFormats = [new() { MediaType = ApplicationJson }], StatusCode = 401, - Type = typeof(void), } ); @@ -201,7 +206,6 @@ private static void AddCommonResponses(ApiDescription apiDescription) ModelMetadata = CreateModelMetadata(typeof(void)), ApiResponseFormats = [new() { MediaType = ApplicationJson }], StatusCode = 403, - Type = typeof(void), } ); } diff --git a/src/CQRS/LeanCode.CQRS.AspNetCore/Registration/EndpointMetadata.cs b/src/CQRS/LeanCode.CQRS.AspNetCore/Registration/EndpointMetadata.cs new file mode 100644 index 00000000..e243ff98 --- /dev/null +++ b/src/CQRS/LeanCode.CQRS.AspNetCore/Registration/EndpointMetadata.cs @@ -0,0 +1,29 @@ +using LeanCode.CQRS.Execution; +using Microsoft.AspNetCore.Http.Metadata; + +namespace LeanCode.CQRS.AspNetCore.Registration; + +internal class EndpointTags : ITagsMetadata +{ + public IReadOnlyList Tags { get; } + + public EndpointTags(IReadOnlyCollection userTags, CQRSObjectMetadata metadata) + { + Tags = userTags.Concat(GetPredefinedTags(metadata)).ToList(); + } + + private static IEnumerable GetPredefinedTags(CQRSObjectMetadata metadata) + { + yield return metadata.ObjectKind.ToString(); + } +} + +internal class EndpointSummary(string summary) : IEndpointSummaryMetadata +{ + public string Summary => summary; +} + +internal class EndpointDescription(string description) : IEndpointDescriptionMetadata +{ + public string Description => description; +} diff --git a/test/CQRS/LeanCode.CQRS.AspNetCore.Tests/Registration/CQRSApiDescriptionProviderTests.cs b/test/CQRS/LeanCode.CQRS.AspNetCore.Tests/Registration/CQRSApiDescriptionProviderTests.cs index 038b97f7..f60326f7 100644 --- a/test/CQRS/LeanCode.CQRS.AspNetCore.Tests/Registration/CQRSApiDescriptionProviderTests.cs +++ b/test/CQRS/LeanCode.CQRS.AspNetCore.Tests/Registration/CQRSApiDescriptionProviderTests.cs @@ -4,6 +4,7 @@ using LeanCode.CQRS.AspNetCore.Registration; using LeanCode.CQRS.Execution; using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Http.Metadata; using Microsoft.AspNetCore.Mvc.ApiExplorer; using Microsoft.AspNetCore.Mvc.ModelBinding; using Microsoft.AspNetCore.Routing; @@ -17,7 +18,6 @@ public class CQRSApiDescriptionProviderTests { private const string BasePath = "cqrs"; - private static readonly Dictionary ExpectedRouteValues = new() { ["controller"] = "CQRS" }; private static readonly List ExpectedRequestFormat = [ new ApiRequestFormat { MediaType = "application/json" }, @@ -45,8 +45,9 @@ public void Describes_base_query_parameters() query.HttpMethod.Should().Be("POST"); query.RelativePath.Should().Be($"{BasePath}/query/{typeof(Query).FullName}"); query.ActionDescriptor.DisplayName.Should().Be($"Query {typeof(Query).FullName}"); - query.ActionDescriptor.RouteValues.Should().BeEquivalentTo(ExpectedRouteValues); query.SupportedRequestFormats.Should().BeEquivalentTo(ExpectedRequestFormat); + + ShouldContainMetadata(query); } [Fact] @@ -81,13 +82,14 @@ public void Describes_base_command_parameters() { var allDescriptors = ListApisFor(); - var query = allDescriptors.Should().ContainSingle().Which; + var command = allDescriptors.Should().ContainSingle().Which; - query.HttpMethod.Should().Be("POST"); - query.RelativePath.Should().Be($"{BasePath}/command/{typeof(Command).FullName}"); - query.ActionDescriptor.DisplayName.Should().Be($"Command {typeof(Command).FullName}"); - query.ActionDescriptor.RouteValues.Should().BeEquivalentTo(ExpectedRouteValues); - query.SupportedRequestFormats.Should().BeEquivalentTo(ExpectedRequestFormat); + command.HttpMethod.Should().Be("POST"); + command.RelativePath.Should().Be($"{BasePath}/command/{typeof(Command).FullName}"); + command.ActionDescriptor.DisplayName.Should().Be($"Command {typeof(Command).FullName}"); + command.SupportedRequestFormats.Should().BeEquivalentTo(ExpectedRequestFormat); + + ShouldContainMetadata(command); } [Fact] @@ -128,13 +130,14 @@ public void Describes_base_operation_parameters() { var allDescriptors = ListApisFor(); - var query = allDescriptors.Should().ContainSingle().Which; + var operation = allDescriptors.Should().ContainSingle().Which; - query.HttpMethod.Should().Be("POST"); - query.RelativePath.Should().Be($"{BasePath}/operation/{typeof(Operation).FullName}"); - query.ActionDescriptor.DisplayName.Should().Be($"Operation {typeof(Operation).FullName}"); - query.ActionDescriptor.RouteValues.Should().BeEquivalentTo(ExpectedRouteValues); - query.SupportedRequestFormats.Should().BeEquivalentTo(ExpectedRequestFormat); + operation.HttpMethod.Should().Be("POST"); + operation.RelativePath.Should().Be($"{BasePath}/operation/{typeof(Operation).FullName}"); + operation.ActionDescriptor.DisplayName.Should().Be($"Operation {typeof(Operation).FullName}"); + operation.SupportedRequestFormats.Should().BeEquivalentTo(ExpectedRequestFormat); + + ShouldContainMetadata(operation); } [Fact] @@ -164,6 +167,14 @@ public void Operation_defines_all_responses() ); } + private static void ShouldContainMetadata(ApiDescription obj) => + obj + .ActionDescriptor.EndpointMetadata.Should() + .ContainItemsAssignableTo() + .And.ContainItemsAssignableTo() + .And.ContainItemsAssignableTo() + .And.ContainItemsAssignableTo(); + private static object RequestOf() { return new @@ -192,7 +203,6 @@ private static object ResponseOfVoid(int statusCode) { ApiResponseFormats = new[] { new { MediaType = "application/json" } }, StatusCode = statusCode, - Type = typeof(void), ModelMetadata = new { Identity = new { ModelType = typeof(void) } }, }; } From 5bac52b4b3a1a63f47f72df1a4a77002c7be2dd0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jakub=20Fija=C5=82kowski?= Date: Fri, 3 Jan 2025 15:37:37 +0100 Subject: [PATCH 2/4] Add test-bed project for manual testing of the packages --- Directory.Packages.props | 4 +++ LeanCode.CoreLibrary.sln | 14 ++++++++++ test-bed/Api/TestCommand.cs | 44 ++++++++++++++++++++++++++++++++ test-bed/Api/TestQuery.cs | 29 +++++++++++++++++++++ test-bed/LeanCode.TestBed.csproj | 27 ++++++++++++++++++++ test-bed/Program.cs | 39 ++++++++++++++++++++++++++++ 6 files changed, 157 insertions(+) create mode 100644 test-bed/Api/TestCommand.cs create mode 100644 test-bed/Api/TestQuery.cs create mode 100644 test-bed/LeanCode.TestBed.csproj create mode 100644 test-bed/Program.cs diff --git a/Directory.Packages.props b/Directory.Packages.props index fc3533da..63d186f2 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -63,6 +63,7 @@ + @@ -108,6 +109,9 @@ + + + diff --git a/LeanCode.CoreLibrary.sln b/LeanCode.CoreLibrary.sln index 9018c56e..3885deb1 100755 --- a/LeanCode.CoreLibrary.sln +++ b/LeanCode.CoreLibrary.sln @@ -189,6 +189,8 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Helpers", "Helpers", "{29CC EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "LeanCode.UserIdExtractors.Tests", "test\Helpers\LeanCode.UserIdExtractors.Tests\LeanCode.UserIdExtractors.Tests.csproj", "{8E57E9DE-108B-46D8-A7FF-65D28C21A3DF}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "LeanCode.TestBed", "test-bed\LeanCode.TestBed.csproj", "{E0B2F070-425B-4453-80AE-EE23680F41D8}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -1123,6 +1125,18 @@ Global {8E57E9DE-108B-46D8-A7FF-65D28C21A3DF}.Release|x64.Build.0 = Release|Any CPU {8E57E9DE-108B-46D8-A7FF-65D28C21A3DF}.Release|x86.ActiveCfg = Release|Any CPU {8E57E9DE-108B-46D8-A7FF-65D28C21A3DF}.Release|x86.Build.0 = Release|Any CPU + {E0B2F070-425B-4453-80AE-EE23680F41D8}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {E0B2F070-425B-4453-80AE-EE23680F41D8}.Debug|Any CPU.Build.0 = Debug|Any CPU + {E0B2F070-425B-4453-80AE-EE23680F41D8}.Debug|x64.ActiveCfg = Debug|Any CPU + {E0B2F070-425B-4453-80AE-EE23680F41D8}.Debug|x64.Build.0 = Debug|Any CPU + {E0B2F070-425B-4453-80AE-EE23680F41D8}.Debug|x86.ActiveCfg = Debug|Any CPU + {E0B2F070-425B-4453-80AE-EE23680F41D8}.Debug|x86.Build.0 = Debug|Any CPU + {E0B2F070-425B-4453-80AE-EE23680F41D8}.Release|Any CPU.ActiveCfg = Release|Any CPU + {E0B2F070-425B-4453-80AE-EE23680F41D8}.Release|Any CPU.Build.0 = Release|Any CPU + {E0B2F070-425B-4453-80AE-EE23680F41D8}.Release|x64.ActiveCfg = Release|Any CPU + {E0B2F070-425B-4453-80AE-EE23680F41D8}.Release|x64.Build.0 = Release|Any CPU + {E0B2F070-425B-4453-80AE-EE23680F41D8}.Release|x86.ActiveCfg = Release|Any CPU + {E0B2F070-425B-4453-80AE-EE23680F41D8}.Release|x86.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE diff --git a/test-bed/Api/TestCommand.cs b/test-bed/Api/TestCommand.cs new file mode 100644 index 00000000..9da145e9 --- /dev/null +++ b/test-bed/Api/TestCommand.cs @@ -0,0 +1,44 @@ +using System.ComponentModel; +using FluentValidation; +using LeanCode.Contracts; +using LeanCode.Contracts.Security; +using LeanCode.CQRS.Execution; +using LeanCode.CQRS.Validation.Fluent; +using Serilog; +using ILogger = Serilog.ILogger; + +namespace LeanCode.TestBed.Api; + +[AllowUnauthorized] +public class TestCommand : ICommand +{ + public required bool TriggerCode1 { get; set; } + public required bool TriggerCode2 { get; set; } + public required string OtherParameter { get; set; } + + public static class ErrorCodes + { + public const int Code1 = 1; + public const int Code2 = 2; + } +} + +public class TestCommandCV : AbstractValidator +{ + public TestCommandCV() + { + RuleFor(e => e.TriggerCode1).Equal(false).WithCode(TestCommand.ErrorCodes.Code1); + RuleFor(e => e.TriggerCode2).Equal(false).WithCode(TestCommand.ErrorCodes.Code2); + } +} + +public class TestCommandCH : ICommandHandler +{ + private readonly ILogger logger = Log.ForContext(); + + public Task ExecuteAsync(HttpContext context, TestCommand command) + { + logger.Information("Executing command {Command}", command); + return Task.CompletedTask; + } +} diff --git a/test-bed/Api/TestQuery.cs b/test-bed/Api/TestQuery.cs new file mode 100644 index 00000000..70ae9153 --- /dev/null +++ b/test-bed/Api/TestQuery.cs @@ -0,0 +1,29 @@ +using LeanCode.Contracts; +using LeanCode.CQRS.Execution; + +namespace LeanCode.TestBed.Api; + +[Obsolete] +public class TestQuery : IQuery { } + +public class TestQueryResult +{ + public Guid Id { get; set; } + public string Property1 { get; set; } + public TestQueryResult? Inner { get; set; } +} + +public class TestQueryQH : IQueryHandler +{ + public Task ExecuteAsync(HttpContext context, TestQuery query) + { + return Task.FromResult( + new TestQueryResult + { + Id = Guid.NewGuid(), + Property1 = "Some value", + Inner = new() { Id = Guid.NewGuid(), Property1 = "Other value" }, + } + ); + } +} diff --git a/test-bed/LeanCode.TestBed.csproj b/test-bed/LeanCode.TestBed.csproj new file mode 100644 index 00000000..9676d065 --- /dev/null +++ b/test-bed/LeanCode.TestBed.csproj @@ -0,0 +1,27 @@ + + + + net9.0 + enable + enable + false + + + + + + + + + + + + + + + + + + + + diff --git a/test-bed/Program.cs b/test-bed/Program.cs new file mode 100644 index 00000000..d9848201 --- /dev/null +++ b/test-bed/Program.cs @@ -0,0 +1,39 @@ +using System.Text.Json; +using LeanCode.Components; +using LeanCode.CQRS.AspNetCore; +using LeanCode.CQRS.Validation.Fluent; +using LeanCode.TestBed.Api; +using Microsoft.OpenApi.Models; +using Serilog; + +var builder = WebApplication.CreateBuilder(args); + +builder.Logging.AddSerilog(); +builder.Services.AddFluentValidation(TypesCatalog.Of()); +builder.Services.AddCQRS(TypesCatalog.Of(), TypesCatalog.Of()); +builder.Services.AddCQRSApiExplorer(); +builder.Services.AddOpenApi(); + +var app = builder.Build(); + +app.MapOpenApi(); +app.MapRemoteCQRS( + "/", + c => + { + c.Commands = p => p.Secure().Validate(); + c.Queries = p => p.Secure(); + } +); +app.UseReDoc(opt => +{ + opt.SpecUrl("/openapi/v1.json"); + opt.RoutePrefix = "redoc"; +}); +app.UseSwaggerUI(opt => +{ + opt.SwaggerEndpoint("/openapi/v1.json", "Test API"); + opt.RoutePrefix = "swagger"; +}); + +app.Run(); From 35b703c0247912bb7d21a82e62b65aa9c52930e3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jakub=20Fija=C5=82kowski?= Date: Fri, 3 Jan 2025 16:04:57 +0100 Subject: [PATCH 3/4] Improve tag handling --- .../Registration/ApiDescriptionTags.cs | 21 +++++++++++ .../CQRSApiDescriptionConfiguration.cs | 4 +-- .../CQRSApiDescriptionProvider.cs | 2 +- .../Registration/EndpointMetadata.cs | 15 ++------ test-bed/Program.cs | 2 -- .../Registration/ApiDescriptionTagsTests.cs | 36 +++++++++++++++++++ 6 files changed, 62 insertions(+), 18 deletions(-) create mode 100644 src/CQRS/LeanCode.CQRS.AspNetCore/Registration/ApiDescriptionTags.cs create mode 100644 test/CQRS/LeanCode.CQRS.AspNetCore.Tests/Registration/ApiDescriptionTagsTests.cs diff --git a/src/CQRS/LeanCode.CQRS.AspNetCore/Registration/ApiDescriptionTags.cs b/src/CQRS/LeanCode.CQRS.AspNetCore/Registration/ApiDescriptionTags.cs new file mode 100644 index 00000000..6a9ed517 --- /dev/null +++ b/src/CQRS/LeanCode.CQRS.AspNetCore/Registration/ApiDescriptionTags.cs @@ -0,0 +1,21 @@ +using LeanCode.CQRS.Execution; +using Microsoft.AspNetCore.Routing; + +namespace LeanCode.CQRS.AspNetCore.Registration; + +public static class ApiDescriptionTags +{ + public static IReadOnlyList FullNamespace(RouteEndpoint _, CQRSObjectMetadata m) + { + return [(m.ObjectType.Namespace ?? "").Replace(".", " - ", StringComparison.InvariantCulture)]; + } + + public static Func> SkipNamespacePrefix(int skipFirst) + { + return (_, m) => + { + var parts = (m.ObjectType.Namespace ?? "").Split('.').Skip(skipFirst); + return [string.Join(" - ", parts)]; + }; + } +} diff --git a/src/CQRS/LeanCode.CQRS.AspNetCore/Registration/CQRSApiDescriptionConfiguration.cs b/src/CQRS/LeanCode.CQRS.AspNetCore/Registration/CQRSApiDescriptionConfiguration.cs index 9d059f98..c865251d 100644 --- a/src/CQRS/LeanCode.CQRS.AspNetCore/Registration/CQRSApiDescriptionConfiguration.cs +++ b/src/CQRS/LeanCode.CQRS.AspNetCore/Registration/CQRSApiDescriptionConfiguration.cs @@ -5,8 +5,8 @@ namespace LeanCode.CQRS.AspNetCore.Registration; public class CQRSApiDescriptionConfiguration { - public Func> TagsMapping { get; init; } = - static (_, _) => Array.Empty(); + public Func> TagsMapping { get; init; } = + ApiDescriptionTags.FullNamespace; public Func SummaryMapping { get; init; } = static (_, m) => m.ObjectType.FullName ?? ""; diff --git a/src/CQRS/LeanCode.CQRS.AspNetCore/Registration/CQRSApiDescriptionProvider.cs b/src/CQRS/LeanCode.CQRS.AspNetCore/Registration/CQRSApiDescriptionProvider.cs index a9886c1a..23dcfb27 100644 --- a/src/CQRS/LeanCode.CQRS.AspNetCore/Registration/CQRSApiDescriptionProvider.cs +++ b/src/CQRS/LeanCode.CQRS.AspNetCore/Registration/CQRSApiDescriptionProvider.cs @@ -59,7 +59,7 @@ private ApiDescription CreateApiDescription(RouteEndpoint routeEndpoint, CQRSObj EndpointMetadata = [ metadata, - new EndpointTags(configuration.TagsMapping(routeEndpoint, metadata), metadata), + new EndpointTags(configuration.TagsMapping(routeEndpoint, metadata)), new EndpointSummary(configuration.SummaryMapping(routeEndpoint, metadata)), new EndpointDescription(configuration.DescriptionMapping(routeEndpoint, metadata)), ], diff --git a/src/CQRS/LeanCode.CQRS.AspNetCore/Registration/EndpointMetadata.cs b/src/CQRS/LeanCode.CQRS.AspNetCore/Registration/EndpointMetadata.cs index e243ff98..22d77b05 100644 --- a/src/CQRS/LeanCode.CQRS.AspNetCore/Registration/EndpointMetadata.cs +++ b/src/CQRS/LeanCode.CQRS.AspNetCore/Registration/EndpointMetadata.cs @@ -1,21 +1,10 @@ -using LeanCode.CQRS.Execution; using Microsoft.AspNetCore.Http.Metadata; namespace LeanCode.CQRS.AspNetCore.Registration; -internal class EndpointTags : ITagsMetadata +internal class EndpointTags(IReadOnlyList tags) : ITagsMetadata { - public IReadOnlyList Tags { get; } - - public EndpointTags(IReadOnlyCollection userTags, CQRSObjectMetadata metadata) - { - Tags = userTags.Concat(GetPredefinedTags(metadata)).ToList(); - } - - private static IEnumerable GetPredefinedTags(CQRSObjectMetadata metadata) - { - yield return metadata.ObjectKind.ToString(); - } + public IReadOnlyList Tags => tags; } internal class EndpointSummary(string summary) : IEndpointSummaryMetadata diff --git a/test-bed/Program.cs b/test-bed/Program.cs index d9848201..78aad49d 100644 --- a/test-bed/Program.cs +++ b/test-bed/Program.cs @@ -1,9 +1,7 @@ -using System.Text.Json; using LeanCode.Components; using LeanCode.CQRS.AspNetCore; using LeanCode.CQRS.Validation.Fluent; using LeanCode.TestBed.Api; -using Microsoft.OpenApi.Models; using Serilog; var builder = WebApplication.CreateBuilder(args); diff --git a/test/CQRS/LeanCode.CQRS.AspNetCore.Tests/Registration/ApiDescriptionTagsTests.cs b/test/CQRS/LeanCode.CQRS.AspNetCore.Tests/Registration/ApiDescriptionTagsTests.cs new file mode 100644 index 00000000..0ad65f7d --- /dev/null +++ b/test/CQRS/LeanCode.CQRS.AspNetCore.Tests/Registration/ApiDescriptionTagsTests.cs @@ -0,0 +1,36 @@ +using FluentAssertions; +using LeanCode.CQRS.AspNetCore.Registration; +using LeanCode.CQRS.Execution; +using Xunit; + +namespace LeanCode.CQRS.AspNetCore.Tests.Registration; + +public class ApiDescriptionTagsTests +{ + private static readonly CQRSObjectMetadata Metadata = new CQRSObjectMetadata( + CQRSObjectKind.Command, + typeof(ApiDescriptionTagsTests), + typeof(ApiDescriptionTagsTests), + typeof(ApiDescriptionTagsTests), + (_, _) => Task.FromResult(null) + ); + + [Fact] + public void FullNamespace_returns_full_namespace_with_dot_replaced() + { + var tags = ApiDescriptionTags.FullNamespace(null!, Metadata); + + tags.Should().BeEquivalentTo("LeanCode - CQRS - AspNetCore - Tests - Registration"); + } + + [Fact] + public void SkipNamespacePrefix_skips_initial_namespace_parts() + { + Tag(skip: 0).Should().BeEquivalentTo("LeanCode - CQRS - AspNetCore - Tests - Registration"); + Tag(skip: 1).Should().BeEquivalentTo("CQRS - AspNetCore - Tests - Registration"); + Tag(skip: 2).Should().BeEquivalentTo("AspNetCore - Tests - Registration"); + Tag(skip: 10).Should().BeEquivalentTo(""); + + IReadOnlyList Tag(int skip) => ApiDescriptionTags.SkipNamespacePrefix(skip)(null!, Metadata); + } +} From 50df4047d1fbfea79271ad3a921db63e4f1c8851 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jakub=20Fija=C5=82kowski?= Date: Fri, 3 Jan 2025 16:12:09 +0100 Subject: [PATCH 4/4] Update CHANGELOG and docs --- CHANGELOG.md | 1 + .../api_explorer/index.md | 29 ++++++------------- 2 files changed, 10 insertions(+), 20 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 386b556d..54f4bd11 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -14,6 +14,7 @@ but this project DOES NOT adhere to [Semantic Versioning](http://semver.org/). * Remove LeanCode.PdfRocket * Remove StyleCop completely * JSON serializer for CQRS now shares options with ASP.NET Core's `JsonOptions` by default +* Integrates with Microsoft.AspNetCore.OpenApi better ## 8.1 diff --git a/docs/external_integrations/api_explorer/index.md b/docs/external_integrations/api_explorer/index.md index 02020188..690c388c 100644 --- a/docs/external_integrations/api_explorer/index.md +++ b/docs/external_integrations/api_explorer/index.md @@ -1,8 +1,6 @@ # API Explorer/Swagger integration -[CQRS](../../cqrs/index.md) implementation integrates seamlessly with the [API Explorer](https://learn.microsoft.com/en-us/dotnet/api/microsoft.aspnetcore.mvc.apiexplorer) functionality of ASP.NET Core. This means that every endpoint can be automatically documented by other tools that leverage it, e.g. [Swashbuckle](https://github.com/domaindrivendev/Swashbuckle.AspNetCore) or [NSwag](https://github.com/RicoSuter/NSwag). - -By using these tools, you get OpenAPI definitions, along the [Swagger UI](https://swagger.io/tools/swagger-ui/) tooling. +[CQRS](../../cqrs/index.md) implementation integrates seamlessly with the [OpenAPI](https://learn.microsoft.com/en-us/aspnet/core/fundamentals/openapi/overview) support of ASP.NET Core. This means that every endpoint can be automatically documented by other tools that leverage it, e.g. [SwaggerUI](https://swagger.io/tools/swaggerhub/) or [ReDoc](https://github.com/Redocly/redoc). ## Packages @@ -22,24 +20,15 @@ public override void ConfigureServices(IServiceCollection services) ``` The `AddCQRSApiExplorer` method has an optional parameter that accepts a configuration override. -For now, it only allows to set a custom mapping of the `RouteValues` parameter, -which is used by OpenAPI generation tools to pass some metadata, by which the endpoints -could be grouped on the OpenAPI UI, for example. Usage of this metadata varies between -different OpenAPI UI implementations, so please check the tools documentation before overriding it. - -## JSON Casing +It allows configuring what metadata is passed to the OpenAPI tooling, including: -Every integration uses different configuration for the generated payload types. It is highly probable that the default configuration of JSON serializer for these integrations will use wrong property casing. Unfortunately, every tool is configured differently. +1. Tags, +2. Summary, +3. Description. -### Swashbuckle +See the [implementation] for defaults. -To configure Swashbuckle, you need to register custom `ISerializerDataContractResolver` that has the same configurtion as the `ISerializer` of CQRS. +To better accomodate common tag patterns, we also provide [ApiDescriptionTags] class with predefined tag generation based on the namespace of the command/query/operation. -```csharp -// This will use default JSON serialization, with unchanged property names (this is the default of System.Text.Json) & CQRS -builder - .Services - .AddTransient( - _ => new JsonSerializerDataContractResolver(new JsonSerializerOptions { PropertyNamingPolicy = null }) - ); -``` +[implementation]: https://github.com/leancodepl/corelibrary/blob/HEAD/src/CQRS/LeanCode.CQRS.AspNetCore/Registration/CQRSApiDescriptionConfiguration.cs +[ApiDescriptionTags]: https://github.com/leancodepl/corelibrary/blob/HEAD/src/CQRS/LeanCode.CQRS.AspNetCore/Registration/ApiDescriptionTags.cs