From dfa8204db73d148e4cca9cf0230b42bea6595ba3 Mon Sep 17 00:00:00 2001 From: Jezz Santos Date: Fri, 3 May 2024 19:50:10 +1200 Subject: [PATCH] Added basic Swagger UI (+Swashbuckle) documentation. #4 --- docs/decisions/0140-openapi-docs.md | 35 +++ .../0140-developer-tooling.md | 9 + .../ImagesApiSpec.cs | 2 +- .../External/GravatarHttpServiceClientSpec.cs | 2 +- .../FlagsmithHttpServiceClient.TestingOnly.cs | 2 +- .../Endpoints/ContentNegotiationFilterSpec.cs | 36 +++ .../Endpoints/RequestCorrelationFilterSpec.cs | 1 + .../Extensions/HttpResponseExtensionsSpec.cs | 1 + .../FileUploadServiceSpec.cs | 1 + .../Endpoints/ContentNegotiationFilter.cs | 6 + .../Endpoints/RequestCorrelationFilter.cs | 1 + .../Extensions/ErrorExtensions.cs | 11 + .../Extensions/HandlerExtensions.cs | 38 +-- .../Extensions/HttpRequestExtensions.cs | 3 +- .../Extensions/HttpResponseExtensions.cs | 1 + .../Extensions/OperationMethodExtensions.cs | 22 ++ .../Extensions/ResponseExtensions.cs | 41 +++ .../Extensions/ValidationResultExtensions.cs | 7 +- .../Pipeline/XmlHttpResult.cs | 1 + .../ValidationResources.Designer.cs | 9 - .../Validation/ValidationResources.resx | 3 - .../ContentNegotiationApiSpec.cs | 2 +- .../DataFormatsApiSpec.cs | 2 +- .../RequestCorrelationApiSpec.cs | 2 +- .../ValidationApiSpec.cs | 8 +- .../HttpConstants.cs | 19 +- .../HttpError.cs | 1 + .../IWebRequest.cs | 15 +- .../IWebSearchRequest.cs | 2 +- .../ResponseCodeOptions.cs | 17 ++ .../SearchRequest.cs | 2 +- .../TenantedRequests.cs | 2 +- .../UnTenantedRequests.cs | 2 +- .../Gravatar/GravatarGetImageRequest.cs | 2 +- .../Clients/JsonClientSpec.cs | 2 - .../ResponseProblemExtensionsSpec.cs | 1 - .../Clients/InterHostServiceClient.cs | 2 +- .../Clients/JsonClient.cs | 3 +- .../Extensions/ResponseProblemExtensions.cs | 1 - src/Infrastructure.Web.Common/WebConstants.cs | 1 + .../AspNetCallerContextSpec.cs | 2 +- .../Auth/APIKeyAuthenticationHandlerSpec.cs | 2 +- .../Auth/HMACAuthenticationHandlerSpec.cs | 1 + .../Pipeline/CSRFMiddlewareSpec.cs | 4 +- .../Auth/APIKeyAuthenticationHandler.cs | 2 +- .../Auth/HMACAuthenticationHandler.cs | 2 +- .../Documentation/DefaultResponsesFilter.cs | 263 ++++++++++++++++++ .../Documentation/FromFormMultiPartFilter.cs | 142 ++++++++++ .../Extensions/HostExtensions.cs | 82 ++++++ .../Extensions/WebApplicationExtensions.cs | 28 +- .../Infrastructure.Web.Hosting.Common.csproj | 2 + .../Pipeline/CSRFConstants.cs | 2 +- .../Pipeline/CSRFMiddleware.cs | 2 +- .../Pipeline/ReverseProxyMiddleware.cs | 8 +- .../Resources.Designer.cs | 72 +++++ .../Resources.resx | 24 ++ .../WebHostOptions.cs | 16 +- .../WebsiteTestingExtensions.cs | 1 - .../OrganizationsApiSpec.cs | 4 +- .../MinimalApiMediatRGeneratorSpec.cs | 152 ++++++++-- .../MinimalApiMediatRGenerator.cs | 65 ++++- .../Reference/System.Net.Http.HttpMethod.cs | 14 + .../Tools.Generators.Web.Api.csproj | 4 + .../UserProfileApiSpec.cs | 2 +- src/WebsiteHost/Controllers/CSRFController.cs | 2 +- .../Controllers/Home/HomeController.cs | 2 +- 66 files changed, 1085 insertions(+), 133 deletions(-) create mode 100644 docs/decisions/0140-openapi-docs.md create mode 100644 src/Infrastructure.Web.Api.Common/Extensions/OperationMethodExtensions.cs create mode 100644 src/Infrastructure.Web.Api.Common/Extensions/ResponseExtensions.cs rename src/{Infrastructure.Web.Api.Common => Infrastructure.Web.Api.Interfaces}/HttpConstants.cs (76%) create mode 100644 src/Infrastructure.Web.Api.Interfaces/ResponseCodeOptions.cs create mode 100644 src/Infrastructure.Web.Hosting.Common/Documentation/DefaultResponsesFilter.cs create mode 100644 src/Infrastructure.Web.Hosting.Common/Documentation/FromFormMultiPartFilter.cs create mode 100644 src/Tools.Generators.Web.Api/Reference/System.Net.Http.HttpMethod.cs diff --git a/docs/decisions/0140-openapi-docs.md b/docs/decisions/0140-openapi-docs.md new file mode 100644 index 00000000..4091b6f7 --- /dev/null +++ b/docs/decisions/0140-openapi-docs.md @@ -0,0 +1,35 @@ +# OpenApi Docs + +* status: accepted +* date: 2024-05-02 +* deciders: jezzsantos + +# Context and Problem Statement + +Adding OpenAPI documentation to the project will help developers better understand the API and how to use it. + +In .net8.0 there is partial support for OpenAPI by ASP.NET Core, but it is not as feature rich as the Swashbuckle or NSwag libraries. +That wil ultimately change in .net9.0 as Swashbuckle has gone out of support. As per [this announcement](https://github.com/dotnet/aspnetcore/issues/54599) + +We need the following capabilities. + +1. Provide a developer fo the API some documentation about the API. +2. Enables the developer to try out the API with a built-in tool +3. Allows the backend to publish to other tools that can derive code from the API documentation. For example, JS/TS classes to be consumed by the WebsiteHost + +## Considered Options + +The options are: + +1. Swashbuckle +2. NSwag +3. AspNetCore OpenAPI + +## Decision Outcome + +`Swashbuckle` + +- It is mostly supported in .net8.0. +- It does the basics of what we need now, with some custom extensions +- NSwag has serious issues with correctly generating "multipart/form-data" data requests +- We will switch to .net9.0 when released \ No newline at end of file diff --git a/docs/design-principles/0140-developer-tooling.md b/docs/design-principles/0140-developer-tooling.md index d8efbcd2..345b033c 100644 --- a/docs/design-principles/0140-developer-tooling.md +++ b/docs/design-principles/0140-developer-tooling.md @@ -105,3 +105,12 @@ Specific class properties: TBD +## Swagger UI + +> a.k.a OpenAPI documentation + +We provide Swagger UI tooling for both the frontend and backend APIs. + +For Backend API: you can find Swagger UI a the root URL of the site `/` + +From Frontend API: you can find the swagger UI at : `/swagger` diff --git a/src/ImagesInfrastructure.IntegrationTests/ImagesApiSpec.cs b/src/ImagesInfrastructure.IntegrationTests/ImagesApiSpec.cs index 2d8a81ee..57e75ee5 100644 --- a/src/ImagesInfrastructure.IntegrationTests/ImagesApiSpec.cs +++ b/src/ImagesInfrastructure.IntegrationTests/ImagesApiSpec.cs @@ -3,8 +3,8 @@ using Application.Resources.Shared; using Common.Extensions; using FluentAssertions; -using Infrastructure.Web.Api.Common; using Infrastructure.Web.Api.Common.Extensions; +using Infrastructure.Web.Api.Interfaces; using Infrastructure.Web.Api.Operations.Shared.Images; using Infrastructure.Web.Interfaces.Clients; using IntegrationTesting.WebApi.Common; diff --git a/src/Infrastructure.Shared.IntegrationTests/ApplicationServices/External/GravatarHttpServiceClientSpec.cs b/src/Infrastructure.Shared.IntegrationTests/ApplicationServices/External/GravatarHttpServiceClientSpec.cs index 47874032..608f855d 100644 --- a/src/Infrastructure.Shared.IntegrationTests/ApplicationServices/External/GravatarHttpServiceClientSpec.cs +++ b/src/Infrastructure.Shared.IntegrationTests/ApplicationServices/External/GravatarHttpServiceClientSpec.cs @@ -2,7 +2,7 @@ using Common.Recording; using FluentAssertions; using Infrastructure.Shared.ApplicationServices.External; -using Infrastructure.Web.Api.Common; +using Infrastructure.Web.Api.Interfaces; using IntegrationTesting.WebApi.Common; using Microsoft.Extensions.DependencyInjection; using UnitTesting.Common; diff --git a/src/Infrastructure.Shared/ApplicationServices/External/FlagsmithHttpServiceClient.TestingOnly.cs b/src/Infrastructure.Shared/ApplicationServices/External/FlagsmithHttpServiceClient.TestingOnly.cs index 59d68bcf..202463dd 100644 --- a/src/Infrastructure.Shared/ApplicationServices/External/FlagsmithHttpServiceClient.TestingOnly.cs +++ b/src/Infrastructure.Shared/ApplicationServices/External/FlagsmithHttpServiceClient.TestingOnly.cs @@ -1,5 +1,5 @@ #if TESTINGONLY -using Infrastructure.Web.Api.Common; +using Infrastructure.Web.Api.Interfaces; using Infrastructure.Web.Api.Operations.Shared._3rdParties.Flagsmith; using Infrastructure.Web.Interfaces.Clients; using Flag = Common.FeatureFlags.Flag; diff --git a/src/Infrastructure.Web.Api.Common.UnitTests/Endpoints/ContentNegotiationFilterSpec.cs b/src/Infrastructure.Web.Api.Common.UnitTests/Endpoints/ContentNegotiationFilterSpec.cs index 196d0359..e8938f97 100644 --- a/src/Infrastructure.Web.Api.Common.UnitTests/Endpoints/ContentNegotiationFilterSpec.cs +++ b/src/Infrastructure.Web.Api.Common.UnitTests/Endpoints/ContentNegotiationFilterSpec.cs @@ -4,6 +4,7 @@ using FluentAssertions; using Infrastructure.Web.Api.Common.Endpoints; using Infrastructure.Web.Api.Common.Pipeline; +using Infrastructure.Web.Api.Interfaces; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Http.HttpResults; using Microsoft.AspNetCore.Mvc; @@ -228,6 +229,41 @@ public async Task WhenInvokeAsyncWithNullValueIResultResponse_ThenReturnsJsonCon .StatusCode.Should().Be(200); } + [Fact] + public async Task WhenInvokeAsyncWithNakedObjectResponseAndAcceptEverything_ThenReturnsJson() + { + var response = new TestResponse(); + var httpContext = SetupHttpContext(FormatMechanism.AcceptHeader(HttpContentTypes.Everything)); + var context = new DefaultEndpointFilterInvocationContext(httpContext.Object); + var next = new EndpointFilterDelegate(_ => new ValueTask(response)); + + var result = await _filter.InvokeAsync(context, next); + + result.Should().BeOfType>(); + result.As>() + .StatusCode.Should().Be((int)HttpStatusCode.OK); + result.As>() + .Value.Should().Be(response); + } + + [Fact] + public async Task WhenInvokeAsyncWithIResultResponseAndAcceptEverything_ThenReturnsJson() + { + var payload = new TestResponse(); + var response = Results.Ok(payload); + var httpContext = SetupHttpContext(FormatMechanism.AcceptHeader(HttpContentTypes.Everything)); + var context = new DefaultEndpointFilterInvocationContext(httpContext.Object); + var next = new EndpointFilterDelegate(_ => new ValueTask(response)); + + var result = await _filter.InvokeAsync(context, next); + + result.Should().BeOfType>(); + result.As>() + .StatusCode.Should().Be((int)HttpStatusCode.OK); + result.As>() + .Value.Should().Be(payload); + } + [Fact] public async Task WhenInvokeAsyncWithNakedObjectResponseAndAcceptJson_ThenReturnsJson() { diff --git a/src/Infrastructure.Web.Api.Common.UnitTests/Endpoints/RequestCorrelationFilterSpec.cs b/src/Infrastructure.Web.Api.Common.UnitTests/Endpoints/RequestCorrelationFilterSpec.cs index ed588a21..7246697a 100644 --- a/src/Infrastructure.Web.Api.Common.UnitTests/Endpoints/RequestCorrelationFilterSpec.cs +++ b/src/Infrastructure.Web.Api.Common.UnitTests/Endpoints/RequestCorrelationFilterSpec.cs @@ -1,5 +1,6 @@ using FluentAssertions; using Infrastructure.Web.Api.Common.Endpoints; +using Infrastructure.Web.Api.Interfaces; using Microsoft.AspNetCore.Http; using Xunit; diff --git a/src/Infrastructure.Web.Api.Common.UnitTests/Extensions/HttpResponseExtensionsSpec.cs b/src/Infrastructure.Web.Api.Common.UnitTests/Extensions/HttpResponseExtensionsSpec.cs index 9a3c02dc..4f60117b 100644 --- a/src/Infrastructure.Web.Api.Common.UnitTests/Extensions/HttpResponseExtensionsSpec.cs +++ b/src/Infrastructure.Web.Api.Common.UnitTests/Extensions/HttpResponseExtensionsSpec.cs @@ -1,5 +1,6 @@ using FluentAssertions; using Infrastructure.Web.Api.Common.Extensions; +using Infrastructure.Web.Api.Interfaces; using Xunit; namespace Infrastructure.Web.Api.Common.UnitTests.Extensions; diff --git a/src/Infrastructure.Web.Api.Common.UnitTests/FileUploadServiceSpec.cs b/src/Infrastructure.Web.Api.Common.UnitTests/FileUploadServiceSpec.cs index 9bff44e6..07b64d51 100644 --- a/src/Infrastructure.Web.Api.Common.UnitTests/FileUploadServiceSpec.cs +++ b/src/Infrastructure.Web.Api.Common.UnitTests/FileUploadServiceSpec.cs @@ -2,6 +2,7 @@ using Common; using Common.Extensions; using FluentAssertions; +using Infrastructure.Web.Api.Interfaces; using JetBrains.Annotations; using UnitTesting.Common; using Xunit; diff --git a/src/Infrastructure.Web.Api.Common/Endpoints/ContentNegotiationFilter.cs b/src/Infrastructure.Web.Api.Common/Endpoints/ContentNegotiationFilter.cs index f282bdd7..b7b60dd4 100644 --- a/src/Infrastructure.Web.Api.Common/Endpoints/ContentNegotiationFilter.cs +++ b/src/Infrastructure.Web.Api.Common/Endpoints/ContentNegotiationFilter.cs @@ -3,6 +3,7 @@ using Common.Extensions; using Infrastructure.Web.Api.Common.Extensions; using Infrastructure.Web.Api.Common.Pipeline; +using Infrastructure.Web.Api.Interfaces; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Http.HttpResults; using Microsoft.AspNetCore.Mvc; @@ -110,6 +111,11 @@ public class ContentNegotiationFilter : IEndpointFilter if (accepts.HasAny()) { + if (accepts.ContainsIgnoreCase(HttpContentTypes.Everything)) + { + return NegotiatedMimeType.Json; + } + if (accepts.ContainsIgnoreCase(HttpContentTypes.Json)) { return NegotiatedMimeType.Json; diff --git a/src/Infrastructure.Web.Api.Common/Endpoints/RequestCorrelationFilter.cs b/src/Infrastructure.Web.Api.Common/Endpoints/RequestCorrelationFilter.cs index befa985b..40005fe7 100644 --- a/src/Infrastructure.Web.Api.Common/Endpoints/RequestCorrelationFilter.cs +++ b/src/Infrastructure.Web.Api.Common/Endpoints/RequestCorrelationFilter.cs @@ -1,4 +1,5 @@ using Application.Common; +using Infrastructure.Web.Api.Interfaces; using Microsoft.AspNetCore.Http; using Microsoft.Extensions.Primitives; diff --git a/src/Infrastructure.Web.Api.Common/Extensions/ErrorExtensions.cs b/src/Infrastructure.Web.Api.Common/Extensions/ErrorExtensions.cs index 33839a47..bd950eb3 100644 --- a/src/Infrastructure.Web.Api.Common/Extensions/ErrorExtensions.cs +++ b/src/Infrastructure.Web.Api.Common/Extensions/ErrorExtensions.cs @@ -1,5 +1,7 @@ using Common; using Infrastructure.Web.Api.Interfaces; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Http.HttpResults; namespace Infrastructure.Web.Api.Common.Extensions; @@ -28,4 +30,13 @@ public static HttpError ToHttpError(this Error error) return new HttpError(code, error.Message); } + + /// + /// Converts the specified error to a with a problem detail + /// + public static ProblemHttpResult ToProblem(this Error error) + { + var httpError = error.ToHttpError(); + return (ProblemHttpResult)Results.Problem(statusCode: (int)httpError.Code, detail: httpError.Message); + } } \ No newline at end of file diff --git a/src/Infrastructure.Web.Api.Common/Extensions/HandlerExtensions.cs b/src/Infrastructure.Web.Api.Common/Extensions/HandlerExtensions.cs index acd003c4..10856e64 100644 --- a/src/Infrastructure.Web.Api.Common/Extensions/HandlerExtensions.cs +++ b/src/Infrastructure.Web.Api.Common/Extensions/HandlerExtensions.cs @@ -1,4 +1,5 @@ using System.Diagnostics.CodeAnalysis; +using System.Net; using Common; using Common.Extensions; using Infrastructure.Web.Api.Interfaces; @@ -159,36 +160,23 @@ private static IResult ToResult(this PostResult postResult { var response = postResult.Response; var location = postResult.Location; - switch (method) - { - case OperationMethod.Get: - case OperationMethod.Search: - return Results.Ok(response); - - case OperationMethod.Post: - { - return location.HasValue() - ? Results.Created(location, response) - : Results.Ok(response); - } - case OperationMethod.PutPatch: - return Results.Accepted(null, response); + var options = + new ResponseCodeOptions(response is not EmptyResponse, location.HasValue()); + var statusCode = method.ToHttpMethod().GetDefaultResponseCode(options); - case OperationMethod.Delete: - var hasResponse = response is not EmptyResponse; - return hasResponse - ? Results.Accepted(null, response) - : Results.NoContent(); - - default: - return Results.Ok(response); - } + return statusCode switch + { + HttpStatusCode.OK => Results.Ok(response), + HttpStatusCode.Accepted => Results.Accepted(null, response), + HttpStatusCode.Created => Results.Created(location, response), + HttpStatusCode.NoContent => Results.NoContent(), + _ => Results.Ok(response) + }; } private static IResult ToResult(this Error error) { - var httpError = error.ToHttpError(); - return Results.Problem(statusCode: (int)httpError.Code, detail: httpError.Message); + return error.ToProblem(); } } \ No newline at end of file diff --git a/src/Infrastructure.Web.Api.Common/Extensions/HttpRequestExtensions.cs b/src/Infrastructure.Web.Api.Common/Extensions/HttpRequestExtensions.cs index cc0d0f22..733fcd54 100644 --- a/src/Infrastructure.Web.Api.Common/Extensions/HttpRequestExtensions.cs +++ b/src/Infrastructure.Web.Api.Common/Extensions/HttpRequestExtensions.cs @@ -6,6 +6,7 @@ using Common.Extensions; using Infrastructure.Web.Api.Interfaces; using Microsoft.AspNetCore.Http; +using HttpHeaders = Infrastructure.Web.Api.Interfaces.HttpHeaders; namespace Infrastructure.Web.Api.Common.Extensions; @@ -50,8 +51,6 @@ public static Optional GetAPIKeyAuth(this HttpRequest request) /// /// Returns the values of the BASIC authentication from the request (if any) /// - /// - /// public static (Optional Username, Optional Password) GetBasicAuth(this HttpRequest request) { var fromBasicAuth = AuthenticationHeaderValue.TryParse(request.Headers.Authorization, out var result) diff --git a/src/Infrastructure.Web.Api.Common/Extensions/HttpResponseExtensions.cs b/src/Infrastructure.Web.Api.Common/Extensions/HttpResponseExtensions.cs index b8369eb6..00ecc1b4 100644 --- a/src/Infrastructure.Web.Api.Common/Extensions/HttpResponseExtensions.cs +++ b/src/Infrastructure.Web.Api.Common/Extensions/HttpResponseExtensions.cs @@ -3,6 +3,7 @@ using Application.Common; using Common.Extensions; using Infrastructure.Web.Api.Common.Endpoints; +using Infrastructure.Web.Api.Interfaces; using Microsoft.AspNetCore.Mvc; namespace Infrastructure.Web.Api.Common.Extensions; diff --git a/src/Infrastructure.Web.Api.Common/Extensions/OperationMethodExtensions.cs b/src/Infrastructure.Web.Api.Common/Extensions/OperationMethodExtensions.cs new file mode 100644 index 00000000..309641fa --- /dev/null +++ b/src/Infrastructure.Web.Api.Common/Extensions/OperationMethodExtensions.cs @@ -0,0 +1,22 @@ +using Infrastructure.Web.Api.Interfaces; + +namespace Infrastructure.Web.Api.Common.Extensions; + +public static class OperationMethodExtensions +{ + /// + /// Converts the to an appropriate + /// + public static HttpMethod ToHttpMethod(this OperationMethod method) + { + return method switch + { + OperationMethod.Get => HttpMethod.Get, + OperationMethod.Search => HttpMethod.Get, + OperationMethod.Post => HttpMethod.Post, + OperationMethod.PutPatch => HttpMethod.Put, + OperationMethod.Delete => HttpMethod.Delete, + _ => HttpMethod.Get + }; + } +} \ No newline at end of file diff --git a/src/Infrastructure.Web.Api.Common/Extensions/ResponseExtensions.cs b/src/Infrastructure.Web.Api.Common/Extensions/ResponseExtensions.cs new file mode 100644 index 00000000..d5487702 --- /dev/null +++ b/src/Infrastructure.Web.Api.Common/Extensions/ResponseExtensions.cs @@ -0,0 +1,41 @@ +using System.Net; +using Infrastructure.Web.Api.Interfaces; + +namespace Infrastructure.Web.Api.Common.Extensions; + +public static class ResponseExtensions +{ + /// + /// Returns the default for the specified + /// and + /// + public static HttpStatusCode GetDefaultResponseCode(this HttpMethod method, ResponseCodeOptions options) + { + if (method == HttpMethod.Get) + { + return HttpStatusCode.OK; + } + + if (method == HttpMethod.Post) + { + return options.HasLocation + ? HttpStatusCode.Created + : HttpStatusCode.OK; + } + + if (method == HttpMethod.Put + || method == HttpMethod.Patch) + { + return HttpStatusCode.Accepted; + } + + if (method == HttpMethod.Delete) + { + return options.HasContent + ? HttpStatusCode.Accepted + : HttpStatusCode.NoContent; + } + + return HttpStatusCode.OK; + } +} \ No newline at end of file diff --git a/src/Infrastructure.Web.Api.Common/Extensions/ValidationResultExtensions.cs b/src/Infrastructure.Web.Api.Common/Extensions/ValidationResultExtensions.cs index 4424df92..961d6a01 100644 --- a/src/Infrastructure.Web.Api.Common/Extensions/ValidationResultExtensions.cs +++ b/src/Infrastructure.Web.Api.Common/Extensions/ValidationResultExtensions.cs @@ -1,6 +1,5 @@ using System.Net; using FluentValidation.Results; -using Infrastructure.Web.Api.Common.Validation; using Infrastructure.Web.Api.Interfaces; using Microsoft.AspNetCore.Mvc; @@ -23,13 +22,11 @@ public static ProblemDetails ToRfc7807(this ValidationResult result, string requ .ToList(); var firstMessage = result.Errors.Select(error => error.ErrorMessage) .First(); - var firstCode = result.Errors.Select(error => error.ErrorCode) - .First(); var details = new ProblemDetails { - Type = firstCode, - Title = ValidationResources.ValidationBehavior_ErrorTitle, + Type = "https://datatracker.ietf.org/doc/html/rfc9110#section-15.5", + Title = "Bad Request", Status = (int)HttpStatusCode.BadRequest, Detail = firstMessage, Instance = requestUrl diff --git a/src/Infrastructure.Web.Api.Common/Pipeline/XmlHttpResult.cs b/src/Infrastructure.Web.Api.Common/Pipeline/XmlHttpResult.cs index 6c74c630..846d356a 100644 --- a/src/Infrastructure.Web.Api.Common/Pipeline/XmlHttpResult.cs +++ b/src/Infrastructure.Web.Api.Common/Pipeline/XmlHttpResult.cs @@ -1,6 +1,7 @@ using System.Diagnostics.CodeAnalysis; using System.Xml; using Infrastructure.Web.Api.Common.Extensions; +using Infrastructure.Web.Api.Interfaces; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Http.HttpResults; using Microsoft.AspNetCore.Mvc; diff --git a/src/Infrastructure.Web.Api.Common/Validation/ValidationResources.Designer.cs b/src/Infrastructure.Web.Api.Common/Validation/ValidationResources.Designer.cs index 89297421..5b576a98 100644 --- a/src/Infrastructure.Web.Api.Common/Validation/ValidationResources.Designer.cs +++ b/src/Infrastructure.Web.Api.Common/Validation/ValidationResources.Designer.cs @@ -149,15 +149,6 @@ internal static string UrlValidator_ErrorMessage { } } - /// - /// Looks up a localized string similar to Validation Error. - /// - internal static string ValidationBehavior_ErrorTitle { - get { - return ResourceManager.GetString("ValidationBehavior_ErrorTitle", resourceCulture); - } - } - /// /// Looks up a localized string similar to The property was invalid. /// diff --git a/src/Infrastructure.Web.Api.Common/Validation/ValidationResources.resx b/src/Infrastructure.Web.Api.Common/Validation/ValidationResources.resx index f6c6f428..402e4ca8 100644 --- a/src/Infrastructure.Web.Api.Common/Validation/ValidationResources.resx +++ b/src/Infrastructure.Web.Api.Common/Validation/ValidationResources.resx @@ -24,9 +24,6 @@ PublicKeyToken=b77a5c561934e089 - - Validation Error - The format of the Embed expression is invalid diff --git a/src/Infrastructure.Web.Api.IntegrationTests/ContentNegotiationApiSpec.cs b/src/Infrastructure.Web.Api.IntegrationTests/ContentNegotiationApiSpec.cs index fcdf4dea..e9d372d9 100644 --- a/src/Infrastructure.Web.Api.IntegrationTests/ContentNegotiationApiSpec.cs +++ b/src/Infrastructure.Web.Api.IntegrationTests/ContentNegotiationApiSpec.cs @@ -2,7 +2,7 @@ using System.Net; using ApiHost1; using FluentAssertions; -using Infrastructure.Web.Api.Common; +using Infrastructure.Web.Api.Interfaces; using Infrastructure.Web.Api.Operations.Shared.TestingOnly; using IntegrationTesting.WebApi.Common; using Xunit; diff --git a/src/Infrastructure.Web.Api.IntegrationTests/DataFormatsApiSpec.cs b/src/Infrastructure.Web.Api.IntegrationTests/DataFormatsApiSpec.cs index bdcb8d0d..5dbf7676 100644 --- a/src/Infrastructure.Web.Api.IntegrationTests/DataFormatsApiSpec.cs +++ b/src/Infrastructure.Web.Api.IntegrationTests/DataFormatsApiSpec.cs @@ -3,7 +3,7 @@ using ApiHost1; using Common.Extensions; using FluentAssertions; -using Infrastructure.Web.Api.Common; +using Infrastructure.Web.Api.Interfaces; using Infrastructure.Web.Api.Operations.Shared.TestingOnly; using IntegrationTesting.WebApi.Common; using Xunit; diff --git a/src/Infrastructure.Web.Api.IntegrationTests/RequestCorrelationApiSpec.cs b/src/Infrastructure.Web.Api.IntegrationTests/RequestCorrelationApiSpec.cs index 634b5090..9e1c25ae 100644 --- a/src/Infrastructure.Web.Api.IntegrationTests/RequestCorrelationApiSpec.cs +++ b/src/Infrastructure.Web.Api.IntegrationTests/RequestCorrelationApiSpec.cs @@ -2,7 +2,7 @@ using System.Net; using ApiHost1; using FluentAssertions; -using Infrastructure.Web.Api.Common; +using Infrastructure.Web.Api.Interfaces; using Infrastructure.Web.Api.Operations.Shared.TestingOnly; using IntegrationTesting.WebApi.Common; using JetBrains.Annotations; diff --git a/src/Infrastructure.Web.Api.IntegrationTests/ValidationApiSpec.cs b/src/Infrastructure.Web.Api.IntegrationTests/ValidationApiSpec.cs index 0a9f2a08..9dc17db3 100644 --- a/src/Infrastructure.Web.Api.IntegrationTests/ValidationApiSpec.cs +++ b/src/Infrastructure.Web.Api.IntegrationTests/ValidationApiSpec.cs @@ -32,8 +32,8 @@ public async Task WhenGetValidatedRequestWithInvalidFields_ThenReturnsValidation var result = await Api.GetAsync(new ValidationsValidatedTestingOnlyRequest { Id = "1234" }); result.StatusCode.Should().Be(HttpStatusCode.BadRequest); - result.Content.Error.Type.Should().Be("NotEmptyValidator"); - result.Content.Error.Title.Should().Be("Validation Error"); + result.Content.Error.Type.Should().Be("https://datatracker.ietf.org/doc/html/rfc9110#section-15.5"); + result.Content.Error.Title.Should().Be("Bad Request"); result.Content.Error.Status.Should().Be(400); result.Content.Error.Detail.Should().Be("'Field1' must not be empty."); result.Content.Error.Instance.Should().Match(@"https://localhost:?????/testingonly/validations/validated/1234"); @@ -51,8 +51,8 @@ public async Task WhenGetValidatedRequestWithPartialInvalidFields_ThenReturnsVal var result = await Api.GetAsync(new ValidationsValidatedTestingOnlyRequest { Id = "1234", Field1 = "123" }); result.StatusCode.Should().Be(HttpStatusCode.BadRequest); - result.Content.Error.Type.Should().Be("NotEmptyValidator"); - result.Content.Error.Title.Should().Be("Validation Error"); + result.Content.Error.Type.Should().Be("https://datatracker.ietf.org/doc/html/rfc9110#section-15.5"); + result.Content.Error.Title.Should().Be("Bad Request"); result.Content.Error.Status.Should().Be(400); result.Content.Error.Detail.Should().Be("'Field2' must not be empty."); result.Content.Error.Instance.Should() diff --git a/src/Infrastructure.Web.Api.Common/HttpConstants.cs b/src/Infrastructure.Web.Api.Interfaces/HttpConstants.cs similarity index 76% rename from src/Infrastructure.Web.Api.Common/HttpConstants.cs rename to src/Infrastructure.Web.Api.Interfaces/HttpConstants.cs index 212302e9..529ee2d1 100644 --- a/src/Infrastructure.Web.Api.Common/HttpConstants.cs +++ b/src/Infrastructure.Web.Api.Interfaces/HttpConstants.cs @@ -1,10 +1,13 @@ -namespace Infrastructure.Web.Api.Common; +using System.Net; + +namespace Infrastructure.Web.Api.Interfaces; /// /// Common HTTP MimeTypes /// public static class HttpContentTypes { + public const string Everything = "*/*"; public const string FormUrlEncoded = "application/x-www-form-urlencoded"; public const string FormUrlEncodedWithCharset = "application/x-www-form-urlencoded; charset=utf-8"; public const string Html = "text/html"; @@ -71,4 +74,18 @@ public static class Extensions public const string ValidationErrorPropertyName = "errors"; } } +} + +public static class HttpStatusCodes +{ + public static readonly Dictionary SupportedCodes = new() + { + { HttpStatusCode.BadRequest, HttpStatusCode.BadRequest }, + { HttpStatusCode.Unauthorized, HttpStatusCode.Unauthorized }, + { HttpStatusCode.PaymentRequired, HttpStatusCode.PaymentRequired }, + { HttpStatusCode.Forbidden, HttpStatusCode.Forbidden }, + { HttpStatusCode.NotFound, HttpStatusCode.NotFound }, + { HttpStatusCode.MethodNotAllowed, HttpStatusCode.MethodNotAllowed }, + { HttpStatusCode.Conflict, HttpStatusCode.Conflict } + }; } \ No newline at end of file diff --git a/src/Infrastructure.Web.Api.Interfaces/HttpError.cs b/src/Infrastructure.Web.Api.Interfaces/HttpError.cs index 8f51e7c9..a31eab3e 100644 --- a/src/Infrastructure.Web.Api.Interfaces/HttpError.cs +++ b/src/Infrastructure.Web.Api.Interfaces/HttpError.cs @@ -24,6 +24,7 @@ public HttpError(HttpErrorCode code, string? message) /// public enum HttpErrorCode { + //EXTEND with others from HttpStatusCodes.SupportedCodes BadRequest = HttpStatusCode.BadRequest, Unauthorized = HttpStatusCode.Unauthorized, PaymentRequired = HttpStatusCode.PaymentRequired, diff --git a/src/Infrastructure.Web.Api.Interfaces/IWebRequest.cs b/src/Infrastructure.Web.Api.Interfaces/IWebRequest.cs index 006165f8..a9f619a2 100644 --- a/src/Infrastructure.Web.Api.Interfaces/IWebRequest.cs +++ b/src/Infrastructure.Web.Api.Interfaces/IWebRequest.cs @@ -1,20 +1,25 @@ namespace Infrastructure.Web.Api.Interfaces; /// -/// Defines a incoming REST request and response. -/// Note: we have split this interface definition so it can be reused in Roslyn components +/// Defines an incoming REST request. +/// Note: we have split this interface definition so that it can be reused in Roslyn components /// // ReSharper disable once PartialTypeWithSinglePart public partial interface IWebRequest; /// -/// Defines a incoming REST request and response. +/// Defines an incoming REST request with response. /// // ReSharper disable once UnusedTypeParameter public interface IWebRequest : IWebRequest where TResponse : IWebResponse; /// -/// Defines a incoming REST request and response. +/// Defines an incoming REST request with empty response. /// -public interface IWebRequestVoid : IWebRequest; \ No newline at end of file +public interface IWebRequestVoid : IWebRequest; + +/// +/// Defines an incoming REST request with a stream response. +/// +public interface IWebRequestStream : IWebRequest; \ No newline at end of file diff --git a/src/Infrastructure.Web.Api.Interfaces/IWebSearchRequest.cs b/src/Infrastructure.Web.Api.Interfaces/IWebSearchRequest.cs index c6d9e250..5eb79c49 100644 --- a/src/Infrastructure.Web.Api.Interfaces/IWebSearchRequest.cs +++ b/src/Infrastructure.Web.Api.Interfaces/IWebSearchRequest.cs @@ -4,4 +4,4 @@ namespace Infrastructure.Web.Api.Interfaces; /// Defines the request of a SEARCH API /// public interface IWebSearchRequest : IWebRequest, IHasSearchOptions - where TResponse : IWebResponse; \ No newline at end of file + where TResponse : IWebSearchResponse; \ No newline at end of file diff --git a/src/Infrastructure.Web.Api.Interfaces/ResponseCodeOptions.cs b/src/Infrastructure.Web.Api.Interfaces/ResponseCodeOptions.cs new file mode 100644 index 00000000..794e0040 --- /dev/null +++ b/src/Infrastructure.Web.Api.Interfaces/ResponseCodeOptions.cs @@ -0,0 +1,17 @@ +namespace Infrastructure.Web.Api.Interfaces; + +/// +/// Defines options for determining the response code +/// +public class ResponseCodeOptions +{ + public ResponseCodeOptions(bool hasContent, bool hasLocation) + { + HasContent = hasContent; + HasLocation = hasLocation; + } + + public bool HasContent { get; } + + public bool HasLocation { get; } +} \ No newline at end of file diff --git a/src/Infrastructure.Web.Api.Interfaces/SearchRequest.cs b/src/Infrastructure.Web.Api.Interfaces/SearchRequest.cs index 9caa68f6..94ae1ca8 100644 --- a/src/Infrastructure.Web.Api.Interfaces/SearchRequest.cs +++ b/src/Infrastructure.Web.Api.Interfaces/SearchRequest.cs @@ -4,7 +4,7 @@ namespace Infrastructure.Web.Api.Interfaces; /// Defines the request of a SEARCH API /// public class SearchRequest : IWebSearchRequest - where TResponse : IWebResponse + where TResponse : IWebSearchResponse { public string? Embed { get; set; } diff --git a/src/Infrastructure.Web.Api.Interfaces/TenantedRequests.cs b/src/Infrastructure.Web.Api.Interfaces/TenantedRequests.cs index 3ede1066..202e159b 100644 --- a/src/Infrastructure.Web.Api.Interfaces/TenantedRequests.cs +++ b/src/Infrastructure.Web.Api.Interfaces/TenantedRequests.cs @@ -37,7 +37,7 @@ public class TenantedDeleteRequest : IWebRequestVoid, ITenantedRequest /// /// Defines the request of a GET API for a stream for an Organization /// -public class TenantedStreamRequest : IWebRequestVoid, ITenantedRequest +public class TenantedStreamRequest : IWebRequestStream, ITenantedRequest { public string? OrganizationId { get; set; } } \ No newline at end of file diff --git a/src/Infrastructure.Web.Api.Interfaces/UnTenantedRequests.cs b/src/Infrastructure.Web.Api.Interfaces/UnTenantedRequests.cs index 15f00e67..9b4547d8 100644 --- a/src/Infrastructure.Web.Api.Interfaces/UnTenantedRequests.cs +++ b/src/Infrastructure.Web.Api.Interfaces/UnTenantedRequests.cs @@ -25,4 +25,4 @@ public class UnTenantedDeleteRequest : IWebRequestVoid, IUnTenantedRequest; /// /// Defines the request of a GET API for a stream not for an Organization /// -public class UnTenantedStreamRequest : IWebRequestVoid, IUnTenantedRequest; \ No newline at end of file +public class UnTenantedStreamRequest : IWebRequestStream, IUnTenantedRequest; \ No newline at end of file diff --git a/src/Infrastructure.Web.Api.Operations.Shared/3rdParties/Gravatar/GravatarGetImageRequest.cs b/src/Infrastructure.Web.Api.Operations.Shared/3rdParties/Gravatar/GravatarGetImageRequest.cs index 7264b5ee..6a573ec2 100644 --- a/src/Infrastructure.Web.Api.Operations.Shared/3rdParties/Gravatar/GravatarGetImageRequest.cs +++ b/src/Infrastructure.Web.Api.Operations.Shared/3rdParties/Gravatar/GravatarGetImageRequest.cs @@ -6,7 +6,7 @@ namespace Infrastructure.Web.Api.Operations.Shared._3rdParties.Gravatar; [Route("/avatar/{Hash}", OperationMethod.Get)] [UsedImplicitly] -public class GravatarGetImageRequest : IWebRequestVoid +public class GravatarGetImageRequest : IWebRequestStream { [JsonPropertyName("d")] public string? Default { get; set; } diff --git a/src/Infrastructure.Web.Common.UnitTests/Clients/JsonClientSpec.cs b/src/Infrastructure.Web.Common.UnitTests/Clients/JsonClientSpec.cs index 391040b7..ed17f729 100644 --- a/src/Infrastructure.Web.Common.UnitTests/Clients/JsonClientSpec.cs +++ b/src/Infrastructure.Web.Common.UnitTests/Clients/JsonClientSpec.cs @@ -2,7 +2,6 @@ using System.Net.Http.Headers; using System.Net.Http.Json; using FluentAssertions; -using Infrastructure.Web.Api.Common; using Infrastructure.Web.Api.Interfaces; using Infrastructure.Web.Common.Clients; using JetBrains.Annotations; @@ -440,7 +439,6 @@ public async Task WhenGetStringResponseAsyncAndContentTypeIsFileForSuccess_ThenR result.IsSuccessful.Should().BeTrue(); result.HasValue.Should().BeFalse(); } - } } diff --git a/src/Infrastructure.Web.Common.UnitTests/Extensions/ResponseProblemExtensionsSpec.cs b/src/Infrastructure.Web.Common.UnitTests/Extensions/ResponseProblemExtensionsSpec.cs index 2d21f1b4..a83f01b6 100644 --- a/src/Infrastructure.Web.Common.UnitTests/Extensions/ResponseProblemExtensionsSpec.cs +++ b/src/Infrastructure.Web.Common.UnitTests/Extensions/ResponseProblemExtensionsSpec.cs @@ -2,7 +2,6 @@ using Common; using Common.Extensions; using FluentAssertions; -using Infrastructure.Web.Api.Common; using Infrastructure.Web.Api.Interfaces; using Infrastructure.Web.Common.Extensions; using Infrastructure.Web.Interfaces.Clients; diff --git a/src/Infrastructure.Web.Common/Clients/InterHostServiceClient.cs b/src/Infrastructure.Web.Common/Clients/InterHostServiceClient.cs index c228366a..8b35f0b4 100644 --- a/src/Infrastructure.Web.Common/Clients/InterHostServiceClient.cs +++ b/src/Infrastructure.Web.Common/Clients/InterHostServiceClient.cs @@ -3,8 +3,8 @@ using Application.Common.Extensions; using Application.Interfaces; using Common.Extensions; -using Infrastructure.Web.Api.Common; using Infrastructure.Web.Api.Common.Extensions; +using Infrastructure.Web.Api.Interfaces; namespace Infrastructure.Web.Common.Clients; diff --git a/src/Infrastructure.Web.Common/Clients/JsonClient.cs b/src/Infrastructure.Web.Common/Clients/JsonClient.cs index 4bc04e03..b77b2d9e 100644 --- a/src/Infrastructure.Web.Common/Clients/JsonClient.cs +++ b/src/Infrastructure.Web.Common/Clients/JsonClient.cs @@ -4,13 +4,12 @@ using System.Text.Json; using Common; using Common.Extensions; -using Infrastructure.Web.Api.Common; using Infrastructure.Web.Api.Common.Extensions; using Infrastructure.Web.Api.Interfaces; using Infrastructure.Web.Common.Extensions; using Infrastructure.Web.Interfaces.Clients; using Microsoft.AspNetCore.Mvc; -using HttpHeaders = Infrastructure.Web.Api.Common.HttpHeaders; +using HttpHeaders = Infrastructure.Web.Api.Interfaces.HttpHeaders; using JsonException = System.Text.Json.JsonException; using Task = System.Threading.Tasks.Task; diff --git a/src/Infrastructure.Web.Common/Extensions/ResponseProblemExtensions.cs b/src/Infrastructure.Web.Common/Extensions/ResponseProblemExtensions.cs index fe0b95c0..a240e59d 100644 --- a/src/Infrastructure.Web.Common/Extensions/ResponseProblemExtensions.cs +++ b/src/Infrastructure.Web.Common/Extensions/ResponseProblemExtensions.cs @@ -1,7 +1,6 @@ using System.Net; using Common; using Common.Extensions; -using Infrastructure.Web.Api.Common; using Infrastructure.Web.Api.Interfaces; using Infrastructure.Web.Common.Clients; using Infrastructure.Web.Interfaces.Clients; diff --git a/src/Infrastructure.Web.Common/WebConstants.cs b/src/Infrastructure.Web.Common/WebConstants.cs index 05252883..9ab70f96 100644 --- a/src/Infrastructure.Web.Common/WebConstants.cs +++ b/src/Infrastructure.Web.Common/WebConstants.cs @@ -3,4 +3,5 @@ namespace Infrastructure.Web.Common; public static class WebConstants { public const string BackEndForFrontEndBasePath = "/api"; + public const string BackEndForFrontEndDocsPath = "/swagger"; } \ No newline at end of file diff --git a/src/Infrastructure.Web.Hosting.Common.UnitTests/AspNetCallerContextSpec.cs b/src/Infrastructure.Web.Hosting.Common.UnitTests/AspNetCallerContextSpec.cs index faed7c4a..80319458 100644 --- a/src/Infrastructure.Web.Hosting.Common.UnitTests/AspNetCallerContextSpec.cs +++ b/src/Infrastructure.Web.Hosting.Common.UnitTests/AspNetCallerContextSpec.cs @@ -5,8 +5,8 @@ using FluentAssertions; using Infrastructure.Common.Extensions; using Infrastructure.Interfaces; -using Infrastructure.Web.Api.Common; using Infrastructure.Web.Api.Common.Endpoints; +using Infrastructure.Web.Api.Interfaces; using Infrastructure.Web.Hosting.Common.Auth; using Microsoft.AspNetCore.Authentication; using Microsoft.AspNetCore.Authentication.JwtBearer; diff --git a/src/Infrastructure.Web.Hosting.Common.UnitTests/Auth/APIKeyAuthenticationHandlerSpec.cs b/src/Infrastructure.Web.Hosting.Common.UnitTests/Auth/APIKeyAuthenticationHandlerSpec.cs index 6f8d94ec..f099cb90 100644 --- a/src/Infrastructure.Web.Hosting.Common.UnitTests/Auth/APIKeyAuthenticationHandlerSpec.cs +++ b/src/Infrastructure.Web.Hosting.Common.UnitTests/Auth/APIKeyAuthenticationHandlerSpec.cs @@ -6,7 +6,7 @@ using Common.Extensions; using FluentAssertions; using Infrastructure.Interfaces; -using Infrastructure.Web.Api.Common; +using Infrastructure.Web.Api.Interfaces; using Infrastructure.Web.Hosting.Common.Auth; using Microsoft.AspNetCore.Authentication; using Microsoft.AspNetCore.Http; diff --git a/src/Infrastructure.Web.Hosting.Common.UnitTests/Auth/HMACAuthenticationHandlerSpec.cs b/src/Infrastructure.Web.Hosting.Common.UnitTests/Auth/HMACAuthenticationHandlerSpec.cs index ddb2f65d..036dfce5 100644 --- a/src/Infrastructure.Web.Hosting.Common.UnitTests/Auth/HMACAuthenticationHandlerSpec.cs +++ b/src/Infrastructure.Web.Hosting.Common.UnitTests/Auth/HMACAuthenticationHandlerSpec.cs @@ -8,6 +8,7 @@ using FluentAssertions; using Infrastructure.Interfaces; using Infrastructure.Web.Api.Common; +using Infrastructure.Web.Api.Interfaces; using Infrastructure.Web.Hosting.Common.Auth; using Microsoft.AspNetCore.Authentication; using Microsoft.AspNetCore.Http; diff --git a/src/Infrastructure.Web.Hosting.Common.UnitTests/Pipeline/CSRFMiddlewareSpec.cs b/src/Infrastructure.Web.Hosting.Common.UnitTests/Pipeline/CSRFMiddlewareSpec.cs index 16b623db..40b8f31d 100644 --- a/src/Infrastructure.Web.Hosting.Common.UnitTests/Pipeline/CSRFMiddlewareSpec.cs +++ b/src/Infrastructure.Web.Hosting.Common.UnitTests/Pipeline/CSRFMiddlewareSpec.cs @@ -6,7 +6,7 @@ using Common; using Common.Extensions; using Infrastructure.Interfaces; -using Infrastructure.Web.Api.Common; +using Infrastructure.Web.Api.Interfaces; using Infrastructure.Web.Hosting.Common.Pipeline; using Microsoft.AspNetCore.Http; using Microsoft.Extensions.DependencyInjection; @@ -20,12 +20,12 @@ namespace Infrastructure.Web.Hosting.Common.UnitTests.Pipeline; [Trait("Category", "Unit")] public class CSRFMiddlewareSpec { + private readonly Mock _callerContextFactory; private readonly Mock _csrfService; private readonly Mock _hostSettings; private readonly CSRFMiddleware _middleware; private readonly Mock _next; private readonly ServiceProvider _serviceProvider; - private readonly Mock _callerContextFactory; public CSRFMiddlewareSpec() { diff --git a/src/Infrastructure.Web.Hosting.Common/Auth/APIKeyAuthenticationHandler.cs b/src/Infrastructure.Web.Hosting.Common/Auth/APIKeyAuthenticationHandler.cs index 4bf2b100..d3407bda 100644 --- a/src/Infrastructure.Web.Hosting.Common/Auth/APIKeyAuthenticationHandler.cs +++ b/src/Infrastructure.Web.Hosting.Common/Auth/APIKeyAuthenticationHandler.cs @@ -6,8 +6,8 @@ using Common.Extensions; using Infrastructure.Common.Extensions; using Infrastructure.Interfaces; -using Infrastructure.Web.Api.Common; using Infrastructure.Web.Api.Common.Extensions; +using Infrastructure.Web.Api.Interfaces; using Microsoft.AspNetCore.Authentication; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; diff --git a/src/Infrastructure.Web.Hosting.Common/Auth/HMACAuthenticationHandler.cs b/src/Infrastructure.Web.Hosting.Common/Auth/HMACAuthenticationHandler.cs index 6f5a7766..e3159eec 100644 --- a/src/Infrastructure.Web.Hosting.Common/Auth/HMACAuthenticationHandler.cs +++ b/src/Infrastructure.Web.Hosting.Common/Auth/HMACAuthenticationHandler.cs @@ -6,8 +6,8 @@ using Common.Extensions; using Infrastructure.Common.Extensions; using Infrastructure.Interfaces; -using Infrastructure.Web.Api.Common; using Infrastructure.Web.Api.Common.Extensions; +using Infrastructure.Web.Api.Interfaces; using Microsoft.AspNetCore.Authentication; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; diff --git a/src/Infrastructure.Web.Hosting.Common/Documentation/DefaultResponsesFilter.cs b/src/Infrastructure.Web.Hosting.Common/Documentation/DefaultResponsesFilter.cs new file mode 100644 index 00000000..bbd948a2 --- /dev/null +++ b/src/Infrastructure.Web.Hosting.Common/Documentation/DefaultResponsesFilter.cs @@ -0,0 +1,263 @@ +using System.Net; +using System.Reflection; +using Common; +using Common.Extensions; +using FluentValidation.Results; +using Infrastructure.Web.Api.Common.Extensions; +using Infrastructure.Web.Api.Interfaces; +using JetBrains.Annotations; +using Microsoft.AspNetCore.Mvc; +using Microsoft.OpenApi.Any; +using Microsoft.OpenApi.Models; +using Swashbuckle.AspNetCore.SwaggerGen; + +namespace Infrastructure.Web.Hosting.Common.Documentation; + +/// +/// Provides a that adds the responses for any request. +/// +[UsedImplicitly] +public sealed class DefaultResponsesFilter : IOperationFilter +{ + public void Apply(OpenApiOperation operation, OperationFilterContext context) + { + var parameter = GetRequestType(context); + if (!parameter.HasValue) + { + return; + } + + var requestType = parameter.Value; + operation.Responses = BuildResponses(context, requestType); + } + + private static OpenApiResponses BuildResponses(OperationFilterContext context, Type requestType) + { + var responseType = ConvertRequestTypeToResponseType(requestType); + var defaultResponseContent = ConvertResponseTypeToContent(context, responseType); + var method = HttpMethod.Parse(context.ApiDescription.HttpMethod ?? HttpMethod.Get.Method); + var options = new ResponseCodeOptions(false, true); + var defaultResponseCode = method.GetDefaultResponseCode(options); + + var defaultResponse = new Dictionary + { + { + ((int)defaultResponseCode).ToString(), new OpenApiResponse + { + Description = defaultResponseCode.ToString(), + Content = defaultResponseContent + } + } + }.ToList(); + + var validationErrors = new Dictionary + { + { + ((int)HttpStatusCode.BadRequest).ToString(), new OpenApiResponse + { + Description = HttpStatusCode.BadRequest.ToString(), + Content = new Dictionary + { + [HttpContentTypes.JsonProblem] = new() + { + Schema = context.SchemaGenerator.GenerateSchema(typeof(ProblemDetails), + context.SchemaRepository), + Examples = new Dictionary + { + { + ErrorCode.Validation.ToString(), new OpenApiExample + { + Description = Resources + .DefaultResponsesFilter_Example_ValidationError_Description, + Summary = Resources.DefaultResponsesFilter_Example_ValidationError_Summary, + Value = new OpenApiString(new ValidationResult(new List + { + new("FieldA", "The error for FieldA") + { ErrorCode = "The Code for FieldA" }, + new("FieldB", "The error for FieldB") + { ErrorCode = "The Code for FieldB" } + }) + .ToRfc7807("https://api.server.com/resource/123") + .ToJson(true, StringExtensions.JsonCasing.Camel)) + } + }, + { + ErrorCode.RuleViolation.ToString(), new OpenApiExample + { + Description = Resources + .DefaultResponsesFilter_Example_RuleViolationError_Description, + Summary = Resources.DefaultResponsesFilter_Example_RuleViolationError_Summary, + Value = new OpenApiString(Error.RuleViolation("A description of the violation") + .ToProblem().ProblemDetails.ToJson(true, StringExtensions.JsonCasing.Camel)) + } + } + } + } + } + } + } + }.ToList(); + + var generalErrors = HttpStatusCodes.SupportedCodes + .Where(pair => pair.Key != HttpStatusCode.BadRequest) + .Select(pair => pair.Key) + .ToDictionary(code => ((int)code).ToString(), code => new OpenApiResponse + { + Description = code.ToString() + }).ToList(); + + var otherErrors = new Dictionary + { + { + ((int)HttpStatusCode.InternalServerError).ToString(), new OpenApiResponse + { + Description = HttpStatusCode.InternalServerError.ToString(), + Content = new Dictionary + { + [HttpContentTypes.JsonProblem] = new() + { + Schema = context.SchemaGenerator.GenerateSchema(typeof(ProblemDetails), + context.SchemaRepository), + Examples = new Dictionary + { + { + ErrorCode.Unexpected.ToString(), new OpenApiExample + { + Description = Resources + .DefaultResponsesFilter_Example_UnexpectedError_Description, + Summary = Resources.DefaultResponsesFilter_Example_UnexpectedError_Summary, + Value = new OpenApiString(Error.Unexpected("A description of the error") + .ToProblem().ProblemDetails.ToJson(true, StringExtensions.JsonCasing.Camel)) + } + } + } + } + } + } + } + }.ToList(); + + var responses = new OpenApiResponses(); + defaultResponse.ForEach(pair => responses.Add(pair.Key, pair.Value)); + validationErrors.ForEach(pair => responses.Add(pair.Key, pair.Value)); + generalErrors.ForEach(pair => responses.Add(pair.Key, pair.Value)); + otherErrors.ForEach(pair => responses.Add(pair.Key, pair.Value)); + //TODO: add the specific error responses for this API, which a need to be documented declaratively on the RequestDto class + + return responses; + } + + private static Dictionary? ConvertResponseTypeToContent(OperationFilterContext context, + Type responseType) + { + if (responseType == typeof(void)) + { + return null; + } + + if (responseType == typeof(Stream)) + { + return new Dictionary + { + [HttpContentTypes.OctetStream] = new() + { + Schema = new OpenApiSchema + { + Type = "string", + Format = "binary" + } + }, + [HttpContentTypes.ImageGif] = new() + { + Schema = new OpenApiSchema + { + Type = "string", + Format = "binary" + } + }, + [HttpContentTypes.ImageJpeg] = new() + { + Schema = new OpenApiSchema + { + Type = "string", + Format = "binary" + } + }, + [HttpContentTypes.ImagePng] = new() + { + Schema = new OpenApiSchema + { + Type = "string", + Format = "binary" + } + } + }; + } + + return new Dictionary + { + [HttpContentTypes.Json] = new() + { + Schema = context.SchemaGenerator.GenerateSchema(responseType, context.SchemaRepository) + }, + [HttpContentTypes.Xml] = new() + { + Schema = context.SchemaGenerator.GenerateSchema(responseType, context.SchemaRepository) + } + }; + } + + private static Type ConvertRequestTypeToResponseType(Type requestType) + { + if (TryGetResponseType(requestType, out var responseType)) + { + return responseType; + } + + if (typeof(IWebRequestStream).IsAssignableFrom(requestType)) + { + return typeof(Stream); + } + + return typeof(void); + } + + private static bool TryGetResponseType(Type requestType, out Type responseType) + { + responseType = default!; + var interfaces = requestType.GetInterfaces(); + var typedRequest = interfaces.FirstOrDefault(@interface => + @interface.IsGenericType && @interface.GetGenericTypeDefinition() == typeof(IWebRequest<>)); + if (typedRequest.NotExists()) + { + return false; + } + + responseType = typedRequest.GetGenericArguments().First(); + return true; + } + + private static Optional GetRequestType(OperationFilterContext context) + { + var requestParameters = context.MethodInfo.GetParameters() + .Where(IsWebRequest) + .ToList(); + if (requestParameters.HasNone()) + { + return Optional.None; + } + + return requestParameters.First().ParameterType; + + static bool IsWebRequest(ParameterInfo requestParameter) + { + var type = requestParameter.ParameterType; + if (type.NotExists()) + { + return false; + } + + return typeof(IWebRequest).IsAssignableFrom(type); + } + } +} \ No newline at end of file diff --git a/src/Infrastructure.Web.Hosting.Common/Documentation/FromFormMultiPartFilter.cs b/src/Infrastructure.Web.Hosting.Common/Documentation/FromFormMultiPartFilter.cs new file mode 100644 index 00000000..8cae1dfa --- /dev/null +++ b/src/Infrastructure.Web.Hosting.Common/Documentation/FromFormMultiPartFilter.cs @@ -0,0 +1,142 @@ +using System.Reflection; +using Common; +using Common.Extensions; +using Infrastructure.Web.Api.Interfaces; +using JetBrains.Annotations; +using Microsoft.AspNetCore.Mvc.ApiExplorer; +using Microsoft.AspNetCore.Mvc.ModelBinding; +using Microsoft.OpenApi.Models; +using Swashbuckle.AspNetCore.SwaggerGen; + +namespace Infrastructure.Web.Hosting.Common.Documentation; + +/// +/// Provides a that fixes the Swagger UI to display tooling for a request that is +/// marked as , +/// that was source generated from a request. +/// +[UsedImplicitly] +public sealed class FromFormMultiPartFilter : IOperationFilter +{ + private const string FormFilesFieldName = "files"; + + public void Apply(OpenApiOperation operation, OperationFilterContext context) + { + var parameter = IsMultiPartFormRequest(context); + if (!parameter.HasValue) + { + return; + } + + var parts = new Dictionary + { + { + FormFilesFieldName, new OpenApiSchema + { + Type = "array", + Items = new OpenApiSchema + { + Type = "string", + Format = "binary" + } + } + } + }; + var requiredParts = new HashSet + { + FormFilesFieldName + }; + + var requestProperties = parameter.Value.Type.GetProperties() + .Select(prop => prop) + .ToList(); + foreach (var property in requestProperties) + { + parts.Add(property.Name, new OpenApiSchema + { + Type = ConvertToSchemaType(context, property) + }); + var isRequired = IsPropertyRequired(property); + if (isRequired) + { + requiredParts.Add(property.Name); + } + } + + operation.RequestBody = new OpenApiRequestBody + { + Content = + { + [HttpContentTypes.MultiPartFormData] = new OpenApiMediaType + { + Schema = new OpenApiSchema + { + Type = "object", + Properties = parts, + Required = requiredParts + } + } + } + }; + } + + private static bool IsPropertyRequired(PropertyInfo property) + { + var nullabilityContext = new NullabilityInfoContext(); + var nullability = nullabilityContext.Create(property); + + return nullability.ReadState == NullabilityState.NotNull; + } + + private static bool RequestHasBody(OperationFilterContext context) + { + var method = context.ApiDescription.HttpMethod; + return method == HttpMethod.Post.Method + || method == HttpMethod.Put.Method + || method == HttpMethod.Patch.Method; + } + + /// + /// Converts the into the respective schema type. + /// + private static string ConvertToSchemaType(OperationFilterContext context, PropertyInfo field) + { + var type = context.SchemaGenerator.GenerateSchema(field.PropertyType, context.SchemaRepository); + return type.Type; + } + + private static Optional IsMultiPartFormRequest(OperationFilterContext context) + { + var requestParameters = context.ApiDescription.ParameterDescriptions + .Where(IsFromFormRequest) + .ToList(); + if (requestParameters.HasNone()) + { + return Optional.None; + } + + if (!RequestHasBody(context)) + { + return Optional.None; + } + + return requestParameters.First(); + + static bool IsFromFormRequest(ApiParameterDescription requestParameter) + { + var type = requestParameter.Type; + if (type.NotExists()) + { + return false; + } + + var source = requestParameter.Source; + if (source != BindingSource.FormFile) + { + return false; + } + + return typeof(IHasMultipartForm).IsAssignableFrom(type); + } + } +} \ No newline at end of file diff --git a/src/Infrastructure.Web.Hosting.Common/Extensions/HostExtensions.cs b/src/Infrastructure.Web.Hosting.Common/Extensions/HostExtensions.cs index 8b34d89a..f200ecf4 100644 --- a/src/Infrastructure.Web.Hosting.Common/Extensions/HostExtensions.cs +++ b/src/Infrastructure.Web.Hosting.Common/Extensions/HostExtensions.cs @@ -36,6 +36,7 @@ using Infrastructure.Web.Api.Interfaces; using Infrastructure.Web.Hosting.Common.ApplicationServices; using Infrastructure.Web.Hosting.Common.Auth; +using Infrastructure.Web.Hosting.Common.Documentation; using Microsoft.AspNetCore.Authentication.JwtBearer; using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Builder; @@ -43,6 +44,7 @@ using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; using Microsoft.IdentityModel.Tokens; +using Microsoft.OpenApi.Models; #if TESTINGONLY using Infrastructure.Persistence.Interfaces.ApplicationServices; using Infrastructure.Web.Api.Operations.Shared.Ancillary; @@ -89,6 +91,7 @@ public static WebApplication ConfigureApiHost(this WebApplicationBuilder appBuil RegisterAuthenticationAuthorization(hostOptions.Authorization, hostOptions.IsMultiTenanted); RegisterWireFormats(); RegisterApiRequests(); + RegisterApiDocumentation(hostOptions.HostName, hostOptions.HostVersion, hostOptions.UsesApiDocumentation); RegisterNotifications(hostOptions.UsesNotifications); modules.RegisterServices(appBuilder.Configuration, services); RegisterApplicationServices(hostOptions.IsMultiTenanted); @@ -322,6 +325,85 @@ void RegisterApiRequests() }); } + void RegisterApiDocumentation(string name, string version, bool usesApiDocumentation) + { + if (!usesApiDocumentation) + { + return; + } + + services.AddEndpointsApiExplorer(); + services.AddSwaggerGen(options => + { + options.OperationFilter(); + options.OperationFilter(); + options.SwaggerDoc(version, new OpenApiInfo + { + Version = version, + Title = name, + Description = name + }); + + if (hostOptions.Authorization.UsesTokens) + { + options.AddSecurityDefinition(JwtBearerDefaults.AuthenticationScheme, new OpenApiSecurityScheme + { + Type = SecuritySchemeType.ApiKey, + Name = HttpHeaders.Authorization, + Description = + Resources.HostExtensions_ApiDocumentation_TokenDescription.Format(JwtBearerDefaults + .AuthenticationScheme), + In = ParameterLocation.Header, + Scheme = JwtBearerDefaults.AuthenticationScheme, + BearerFormat = "JWT" + }); + options.AddSecurityRequirement(new OpenApiSecurityRequirement + { + { + new OpenApiSecurityScheme + { + Reference = new OpenApiReference + { + Type = ReferenceType.SecurityScheme, + Id = JwtBearerDefaults.AuthenticationScheme + } + }, + Array.Empty() + } + }); + } + + if (hostOptions.Authorization.UsesApiKeys) + { + options.AddSecurityDefinition(APIKeyAuthenticationHandler.AuthenticationScheme, + new OpenApiSecurityScheme + { + Type = SecuritySchemeType.ApiKey, + Name = HttpQueryParams.APIKey, + Description = + Resources.HostExtensions_ApiDocumentation_APIKeyQueryDescription.Format(HttpQueryParams + .APIKey), + In = ParameterLocation.Query, + Scheme = APIKeyAuthenticationHandler.AuthenticationScheme + }); + options.AddSecurityRequirement(new OpenApiSecurityRequirement + { + { + new OpenApiSecurityScheme + { + Reference = new OpenApiReference + { + Type = ReferenceType.SecurityScheme, + Id = APIKeyAuthenticationHandler.AuthenticationScheme + } + }, + Array.Empty() + } + }); + } + }); + } + void RegisterNotifications(bool usesNotifications) { if (usesNotifications) diff --git a/src/Infrastructure.Web.Hosting.Common/Extensions/WebApplicationExtensions.cs b/src/Infrastructure.Web.Hosting.Common/Extensions/WebApplicationExtensions.cs index 3f321905..94cdebcf 100644 --- a/src/Infrastructure.Web.Hosting.Common/Extensions/WebApplicationExtensions.cs +++ b/src/Infrastructure.Web.Hosting.Common/Extensions/WebApplicationExtensions.cs @@ -5,15 +5,16 @@ using Infrastructure.Eventing.Interfaces.Projections; using Infrastructure.Hosting.Common.Extensions; using Infrastructure.Persistence.Interfaces; -using Infrastructure.Web.Api.Common; using Infrastructure.Web.Api.Common.Endpoints; using Infrastructure.Web.Api.Common.Extensions; +using Infrastructure.Web.Api.Interfaces; using Infrastructure.Web.Common; using Infrastructure.Web.Hosting.Common.ApplicationServices; using Infrastructure.Web.Hosting.Common.Pipeline; using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Cors.Infrastructure; using Microsoft.AspNetCore.Diagnostics; +using Microsoft.AspNetCore.Hosting; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Http.Extensions; using Microsoft.AspNetCore.Http.Features; @@ -205,11 +206,34 @@ public static void EnableOtherFeatures(this WebApplication builder, //Nothing to register }, "Feature: Recording with -> {Recorder}", recorder)); + if (hostOptions.UsesApiDocumentation) + { + var prefix = hostOptions.IsBackendForFrontEnd + ? WebConstants.BackEndForFrontEndDocsPath.Trim('/') + : string.Empty; + var url = builder.Configuration.GetValue(WebHostDefaults.ServerUrlsKey); + var path = prefix.HasValue() + ? $"{url}/{prefix}" + : url!; + middlewares.Add(new MiddlewareRegistration(-50, + app => + { + app.MapSwagger(); + app.UseSwaggerUI(options => + { + var endPoint = $"/swagger/{hostOptions.HostVersion}/swagger.json"; + options.SwaggerEndpoint(endPoint, hostOptions.HostName); + //Note: puts the swagger docs at the root of the API + options.RoutePrefix = prefix; + }); + }, "Feature: Open API documentation enabled with Swagger UI -> {Path}", path)); + } + var dataStore = builder.Services.GetRequiredServiceForPlatform().GetType().Name; var eventStore = builder.Services.GetRequiredServiceForPlatform().GetType().Name; var queueStore = builder.Services.GetRequiredServiceForPlatform().GetType().Name; var blobStore = builder.Services.GetRequiredServiceForPlatform().GetType().Name; - middlewares.Add(new MiddlewareRegistration(-50, _ => + middlewares.Add(new MiddlewareRegistration(-40, _ => { //Nothing to register }, diff --git a/src/Infrastructure.Web.Hosting.Common/Infrastructure.Web.Hosting.Common.csproj b/src/Infrastructure.Web.Hosting.Common/Infrastructure.Web.Hosting.Common.csproj index a04053f8..24da2288 100644 --- a/src/Infrastructure.Web.Hosting.Common/Infrastructure.Web.Hosting.Common.csproj +++ b/src/Infrastructure.Web.Hosting.Common/Infrastructure.Web.Hosting.Common.csproj @@ -21,6 +21,8 @@ + + diff --git a/src/Infrastructure.Web.Hosting.Common/Pipeline/CSRFConstants.cs b/src/Infrastructure.Web.Hosting.Common/Pipeline/CSRFConstants.cs index dc833804..06086df2 100644 --- a/src/Infrastructure.Web.Hosting.Common/Pipeline/CSRFConstants.cs +++ b/src/Infrastructure.Web.Hosting.Common/Pipeline/CSRFConstants.cs @@ -1,4 +1,4 @@ -using Infrastructure.Web.Api.Common; +using Infrastructure.Web.Api.Interfaces; namespace Infrastructure.Web.Hosting.Common.Pipeline; diff --git a/src/Infrastructure.Web.Hosting.Common/Pipeline/CSRFMiddleware.cs b/src/Infrastructure.Web.Hosting.Common/Pipeline/CSRFMiddleware.cs index eea9072d..f6bd6de9 100644 --- a/src/Infrastructure.Web.Hosting.Common/Pipeline/CSRFMiddleware.cs +++ b/src/Infrastructure.Web.Hosting.Common/Pipeline/CSRFMiddleware.cs @@ -4,8 +4,8 @@ using Common; using Common.Extensions; using Infrastructure.Interfaces; -using Infrastructure.Web.Api.Common; using Infrastructure.Web.Api.Common.Extensions; +using Infrastructure.Web.Api.Interfaces; using Infrastructure.Web.Hosting.Common.Extensions; using Microsoft.AspNetCore.Http; diff --git a/src/Infrastructure.Web.Hosting.Common/Pipeline/ReverseProxyMiddleware.cs b/src/Infrastructure.Web.Hosting.Common/Pipeline/ReverseProxyMiddleware.cs index 9af2e237..fffecd0e 100644 --- a/src/Infrastructure.Web.Hosting.Common/Pipeline/ReverseProxyMiddleware.cs +++ b/src/Infrastructure.Web.Hosting.Common/Pipeline/ReverseProxyMiddleware.cs @@ -1,7 +1,7 @@ using Application.Interfaces.Services; using Common.Extensions; using Infrastructure.Interfaces; -using Infrastructure.Web.Api.Common; +using Infrastructure.Web.Api.Interfaces; using Infrastructure.Web.Common; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Http.Extensions; @@ -43,6 +43,12 @@ public async Task InvokeAsync(HttpContext context) return; } + if (context.Request.Path.StartsWithSegments(WebConstants.BackEndForFrontEndDocsPath)) + { + await _next(context); //Continue down the pipeline + return; + } + await ForwardMessageToBackendAsync(context); } diff --git a/src/Infrastructure.Web.Hosting.Common/Resources.Designer.cs b/src/Infrastructure.Web.Hosting.Common/Resources.Designer.cs index 8749c9dc..5202cc74 100644 --- a/src/Infrastructure.Web.Hosting.Common/Resources.Designer.cs +++ b/src/Infrastructure.Web.Hosting.Common/Resources.Designer.cs @@ -203,6 +203,60 @@ internal static string CSRFTokenPair_FailedDecryptUserId { } } + /// + /// Looks up a localized string similar to A violation of a rule about whether this operation can be executed by this caller, or in the state the resource is in, at this time.. + /// + internal static string DefaultResponsesFilter_Example_RuleViolationError_Description { + get { + return ResourceManager.GetString("DefaultResponsesFilter_Example_RuleViolationError_Description", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Rule Violation. + /// + internal static string DefaultResponsesFilter_Example_RuleViolationError_Summary { + get { + return ResourceManager.GetString("DefaultResponsesFilter_Example_RuleViolationError_Summary", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to An unexpected error occured on the server, which should not have happened in normal operation. Please report this error, and the conditions under which it was discovered, to the support team.. + /// + internal static string DefaultResponsesFilter_Example_UnexpectedError_Description { + get { + return ResourceManager.GetString("DefaultResponsesFilter_Example_UnexpectedError_Description", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Unexpected Error. + /// + internal static string DefaultResponsesFilter_Example_UnexpectedError_Summary { + get { + return ResourceManager.GetString("DefaultResponsesFilter_Example_UnexpectedError_Summary", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to The incoming request is invalid in some way. The 'errors' will contain the details of each of the validation errors.. + /// + internal static string DefaultResponsesFilter_Example_ValidationError_Description { + get { + return ResourceManager.GetString("DefaultResponsesFilter_Example_ValidationError_Description", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Validation Error. + /// + internal static string DefaultResponsesFilter_Example_ValidationError_Summary { + get { + return ResourceManager.GetString("DefaultResponsesFilter_Example_ValidationError_Summary", resourceCulture); + } + } + /// /// Looks up a localized string similar to Failed HMAC authentication. /// @@ -221,6 +275,24 @@ internal static string HMACAuthenticationHandler_MissingHeader { } } + /// + /// Looks up a localized string similar to API Key Authorization using the '{0}' query parameter.. + /// + internal static string HostExtensions_ApiDocumentation_APIKeyQueryDescription { + get { + return ResourceManager.GetString("HostExtensions_ApiDocumentation_APIKeyQueryDescription", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Token Authorization using the {0} scheme. Example: "{0} {{yourtoken}}". + /// + internal static string HostExtensions_ApiDocumentation_TokenDescription { + get { + return ResourceManager.GetString("HostExtensions_ApiDocumentation_TokenDescription", resourceCulture); + } + } + /// /// Looks up a localized string similar to The 'OrganizationId' of the request is invalid. /// diff --git a/src/Infrastructure.Web.Hosting.Common/Resources.resx b/src/Infrastructure.Web.Hosting.Common/Resources.resx index 955cdf2b..8883cc29 100644 --- a/src/Infrastructure.Web.Hosting.Common/Resources.resx +++ b/src/Infrastructure.Web.Hosting.Common/Resources.resx @@ -96,4 +96,28 @@ The authenticated user is not a member of organization '{0}' + + Token Authorization using the {0} scheme. Example: "{0} {{yourtoken}}" + + + API Key Authorization using the '{0}' query parameter. + + + The incoming request is invalid in some way. The 'errors' will contain the details of each of the validation errors. + + + Validation Error + + + A violation of a rule about whether this operation can be executed by this caller, or in the state the resource is in, at this time. + + + Rule Violation + + + An unexpected error occured on the server, which should not have happened in normal operation. Please report this error, and the conditions under which it was discovered, to the support team. + + + Unexpected Error + \ No newline at end of file diff --git a/src/Infrastructure.Web.Hosting.Common/WebHostOptions.cs b/src/Infrastructure.Web.Hosting.Common/WebHostOptions.cs index 7916914e..ecc32822 100644 --- a/src/Infrastructure.Web.Hosting.Common/WebHostOptions.cs +++ b/src/Infrastructure.Web.Hosting.Common/WebHostOptions.cs @@ -17,7 +17,8 @@ public class WebHostOptions : HostOptions UsesHMAC = true }, IsBackendForFrontEnd = false, - UsesNotifications = true + UsesNotifications = true, + UsesApiDocumentation = true }; public new static readonly WebHostOptions BackEndApiHost = new(HostOptions.BackEndApiHost) { @@ -29,7 +30,8 @@ public class WebHostOptions : HostOptions UsesHMAC = true }, IsBackendForFrontEnd = false, - UsesNotifications = true + UsesNotifications = true, + UsesApiDocumentation = true }; public new static readonly WebHostOptions BackEndForFrontEndWebHost = new(HostOptions.BackEndForFrontEndWebHost) @@ -42,7 +44,8 @@ public class WebHostOptions : HostOptions UsesHMAC = false }, IsBackendForFrontEnd = true, - UsesNotifications = false + UsesNotifications = false, + UsesApiDocumentation = true }; public new static readonly WebHostOptions TestingStubsHost = new(HostOptions.TestingStubsHost) @@ -55,7 +58,8 @@ public class WebHostOptions : HostOptions UsesHMAC = false }, IsBackendForFrontEnd = false, - UsesNotifications = false + UsesNotifications = false, + UsesApiDocumentation = false }; private WebHostOptions(HostOptions options) : base(options) @@ -69,8 +73,12 @@ private WebHostOptions(HostOptions options) : base(options) public CORSOption CORS { get; private init; } + public string HostVersion { get; set; } = "v1"; + public bool IsBackendForFrontEnd { get; set; } + public bool UsesApiDocumentation { get; set; } + public bool UsesNotifications { get; set; } } diff --git a/src/Infrastructure.Web.Website.IntegrationTests/WebsiteTestingExtensions.cs b/src/Infrastructure.Web.Website.IntegrationTests/WebsiteTestingExtensions.cs index 7bb32692..44ea7ad0 100644 --- a/src/Infrastructure.Web.Website.IntegrationTests/WebsiteTestingExtensions.cs +++ b/src/Infrastructure.Web.Website.IntegrationTests/WebsiteTestingExtensions.cs @@ -4,7 +4,6 @@ using Common; using Common.Extensions; using Infrastructure.Interfaces; -using Infrastructure.Web.Api.Common; using Infrastructure.Web.Api.Common.Extensions; using Infrastructure.Web.Api.Interfaces; using Infrastructure.Web.Api.Operations.Shared.BackEndForFrontEnd; diff --git a/src/OrganizationsInfrastructure.IntegrationTests/OrganizationsApiSpec.cs b/src/OrganizationsInfrastructure.IntegrationTests/OrganizationsApiSpec.cs index 21eb84d8..9f55821e 100644 --- a/src/OrganizationsInfrastructure.IntegrationTests/OrganizationsApiSpec.cs +++ b/src/OrganizationsInfrastructure.IntegrationTests/OrganizationsApiSpec.cs @@ -3,8 +3,8 @@ using Application.Resources.Shared; using Domain.Interfaces.Authorization; using FluentAssertions; -using Infrastructure.Web.Api.Common; using Infrastructure.Web.Api.Common.Extensions; +using Infrastructure.Web.Api.Interfaces; using Infrastructure.Web.Api.Operations.Shared.EndUsers; using Infrastructure.Web.Api.Operations.Shared.Identities; using Infrastructure.Web.Api.Operations.Shared.Organizations; @@ -569,8 +569,6 @@ await Api.PutAsync(new UnassignRolesFromOrganizationRequest memberships2.Content.Value.Memberships![1].Roles.Should().ContainInOrder(TenantRoles.Member.Name); } - - [Fact] public async Task WhenDeleteAndHasMembers_ThenReturnsError() { diff --git a/src/Tools.Generators.Web.Api.UnitTests/MinimalApiMediatRGeneratorSpec.cs b/src/Tools.Generators.Web.Api.UnitTests/MinimalApiMediatRGeneratorSpec.cs index 60c78818..b93403d6 100644 --- a/src/Tools.Generators.Web.Api.UnitTests/MinimalApiMediatRGeneratorSpec.cs +++ b/src/Tools.Generators.Web.Api.UnitTests/MinimalApiMediatRGeneratorSpec.cs @@ -139,14 +139,21 @@ public static class MinimalApiRegistration public static void RegisterRoutes(this global::Microsoft.AspNetCore.Builder.WebApplication app) { var aserviceclassGroup = app.MapGroup(string.Empty) - .WithGroupName("AServiceClass") + .WithTags("AServiceClass") .RequireCors("__DefaultCorsPolicy") .AddEndpointFilter() .AddEndpointFilter() .AddEndpointFilter(); aserviceclassGroup.MapGet("aroute", async (global::MediatR.IMediator mediator, [global::Microsoft.AspNetCore.Http.AsParameters] global::ANamespace.ARequest request) => - await mediator.Send(request, global::System.Threading.CancellationToken.None)); + await mediator.Send(request, global::System.Threading.CancellationToken.None)) + .WithOpenApi(op => + { + op.OperationId = "A"; + op.Description = "ARequest"; + op.Summary = "ARequest"; + return op; + }); } } @@ -215,14 +222,21 @@ public static class MinimalApiRegistration public static void RegisterRoutes(this global::Microsoft.AspNetCore.Builder.WebApplication app) { var aserviceclassGroup = app.MapGroup(string.Empty) - .WithGroupName("AServiceClass") + .WithTags("AServiceClass") .RequireCors("__DefaultCorsPolicy") .AddEndpointFilter() .AddEndpointFilter() .AddEndpointFilter(); aserviceclassGroup.MapGet("aroute", async (global::MediatR.IMediator mediator, [global::Microsoft.AspNetCore.Http.AsParameters] global::ANamespace.ARequest request) => - await mediator.Send(request, global::System.Threading.CancellationToken.None)); + await mediator.Send(request, global::System.Threading.CancellationToken.None)) + .WithOpenApi(op => + { + op.OperationId = "A"; + op.Description = "ARequest"; + op.Summary = "ARequest"; + return op; + }); } } @@ -292,14 +306,21 @@ public static class MinimalApiRegistration public static void RegisterRoutes(this global::Microsoft.AspNetCore.Builder.WebApplication app) { var aserviceclassGroup = app.MapGroup(string.Empty) - .WithGroupName("AServiceClass") + .WithTags("AServiceClass") .RequireCors("__DefaultCorsPolicy") .AddEndpointFilter() .AddEndpointFilter() .AddEndpointFilter(); aserviceclassGroup.MapGet("aroute", async (global::MediatR.IMediator mediator, [global::Microsoft.AspNetCore.Http.AsParameters] global::ANamespace.ARequest request) => - await mediator.Send(request, global::System.Threading.CancellationToken.None)); + await mediator.Send(request, global::System.Threading.CancellationToken.None)) + .WithOpenApi(op => + { + op.OperationId = "A"; + op.Description = "ARequest"; + op.Summary = "ARequest"; + return op; + }); } } @@ -369,7 +390,7 @@ public static class MinimalApiRegistration public static void RegisterRoutes(this global::Microsoft.AspNetCore.Builder.WebApplication app) { var aserviceclassGroup = app.MapGroup(string.Empty) - .WithGroupName("AServiceClass") + .WithTags("AServiceClass") .RequireCors("__DefaultCorsPolicy") .AddEndpointFilter() .AddEndpointFilter() @@ -377,7 +398,14 @@ public static void RegisterRoutes(this global::Microsoft.AspNetCore.Builder.WebA #if TESTINGONLY aserviceclassGroup.MapGet("aroute", async (global::MediatR.IMediator mediator, [global::Microsoft.AspNetCore.Http.AsParameters] global::ANamespace.ARequest request) => - await mediator.Send(request, global::System.Threading.CancellationToken.None)); + await mediator.Send(request, global::System.Threading.CancellationToken.None)) + .WithOpenApi(op => + { + op.OperationId = "A"; + op.Description = "ARequest"; + op.Summary = "ARequest"; + return op; + }); #endif } @@ -450,7 +478,7 @@ public static class MinimalApiRegistration public static void RegisterRoutes(this global::Microsoft.AspNetCore.Builder.WebApplication app) { var aserviceclassGroup = app.MapGroup(string.Empty) - .WithGroupName("AServiceClass") + .WithTags("AServiceClass") .RequireCors("__DefaultCorsPolicy") .AddEndpointFilter() .AddEndpointFilter() @@ -459,7 +487,14 @@ public static void RegisterRoutes(this global::Microsoft.AspNetCore.Builder.WebA aserviceclassGroup.MapGet("aroute", async (global::MediatR.IMediator mediator, [global::Microsoft.AspNetCore.Http.AsParameters] global::ANamespace.ARequest request) => await mediator.Send(request, global::System.Threading.CancellationToken.None)) - .RequireAuthorization("HMAC"); + .RequireAuthorization("HMAC") + .WithOpenApi(op => + { + op.OperationId = "A"; + op.Description = "ARequest"; + op.Summary = "ARequest"; + return op; + }); #endif } @@ -532,7 +567,7 @@ public static class MinimalApiRegistration public static void RegisterRoutes(this global::Microsoft.AspNetCore.Builder.WebApplication app) { var aserviceclassGroup = app.MapGroup(string.Empty) - .WithGroupName("AServiceClass") + .WithTags("AServiceClass") .RequireCors("__DefaultCorsPolicy") .AddEndpointFilter() .AddEndpointFilter() @@ -541,7 +576,14 @@ public static void RegisterRoutes(this global::Microsoft.AspNetCore.Builder.WebA aserviceclassGroup.MapGet("aroute", async (global::MediatR.IMediator mediator, [global::Microsoft.AspNetCore.Http.AsParameters] global::ANamespace.ARequest request) => await mediator.Send(request, global::System.Threading.CancellationToken.None)) - .RequireAuthorization("Token"); + .RequireAuthorization("Token") + .WithOpenApi(op => + { + op.OperationId = "A"; + op.Description = "ARequest"; + op.Summary = "ARequest"; + return op; + }); #endif } @@ -624,14 +666,21 @@ public static class MinimalApiRegistration public static void RegisterRoutes(this global::Microsoft.AspNetCore.Builder.WebApplication app) { var aserviceclassGroup = app.MapGroup(string.Empty) - .WithGroupName("AServiceClass") + .WithTags("AServiceClass") .RequireCors("__DefaultCorsPolicy") .AddEndpointFilter() .AddEndpointFilter() .AddEndpointFilter(); aserviceclassGroup.MapGet("aroute", async (global::MediatR.IMediator mediator, [global::Microsoft.AspNetCore.Http.AsParameters] global::ANamespace.ARequest request) => - await mediator.Send(request, global::System.Threading.CancellationToken.None)); + await mediator.Send(request, global::System.Threading.CancellationToken.None)) + .WithOpenApi(op => + { + op.OperationId = "A"; + op.Description = "ARequest"; + op.Summary = "ARequest"; + return op; + }); } } @@ -712,7 +761,7 @@ public static class MinimalApiRegistration public static void RegisterRoutes(this global::Microsoft.AspNetCore.Builder.WebApplication app) { var aserviceclassGroup = app.MapGroup(string.Empty) - .WithGroupName("AServiceClass") + .WithTags("AServiceClass") .RequireCors("__DefaultCorsPolicy") .AddEndpointFilter() .AddEndpointFilter() @@ -722,7 +771,14 @@ public static void RegisterRoutes(this global::Microsoft.AspNetCore.Builder.WebA async (global::MediatR.IMediator mediator, [global::Microsoft.AspNetCore.Http.AsParameters] global::ANamespace.ARequest request) => await mediator.Send(request, global::System.Threading.CancellationToken.None)) .RequireAuthorization("Token") - .RequireCallerAuthorization("POLICY:{|Features|:{|Platform|:[|basic_features|]},|Roles|:{|Platform|:[|platform_standard|]}}POLICY:{|Features|:{|Platform|:[|paidtrial_features|]},|Roles|:{|Platform|:[|platform_operations|]}}"); + .RequireCallerAuthorization("POLICY:{|Features|:{|Platform|:[|basic_features|]},|Roles|:{|Platform|:[|platform_standard|]}}POLICY:{|Features|:{|Platform|:[|paidtrial_features|]},|Roles|:{|Platform|:[|platform_operations|]}}") + .WithOpenApi(op => + { + op.OperationId = "A"; + op.Description = "ARequest"; + op.Summary = "ARequest"; + return op; + }); #endif } @@ -796,7 +852,7 @@ public static class MinimalApiRegistration public static void RegisterRoutes(this global::Microsoft.AspNetCore.Builder.WebApplication app) { var aserviceclassGroup = app.MapGroup(string.Empty) - .WithGroupName("AServiceClass") + .WithTags("AServiceClass") .RequireCors("__DefaultCorsPolicy") .AddEndpointFilter() .AddEndpointFilter() @@ -806,7 +862,14 @@ public static void RegisterRoutes(this global::Microsoft.AspNetCore.Builder.WebA async (global::MediatR.IMediator mediator, [global::Microsoft.AspNetCore.Http.AsParameters] global::ANamespace.ARequest request) => await mediator.Send(request, global::System.Threading.CancellationToken.None)) .RequireAuthorization("Token") - .RequireCallerAuthorization("POLICY:{|Features|:{|Platform|:[|basic_features|]},|Roles|:{|Platform|:[|platform_standard|]}}"); + .RequireCallerAuthorization("POLICY:{|Features|:{|Platform|:[|basic_features|]},|Roles|:{|Platform|:[|platform_standard|]}}") + .WithOpenApi(op => + { + op.OperationId = "A"; + op.Description = "ARequest"; + op.Summary = "ARequest"; + return op; + }); #endif } @@ -881,7 +944,7 @@ public static class MinimalApiRegistration public static void RegisterRoutes(this global::Microsoft.AspNetCore.Builder.WebApplication app) { var aserviceclassGroup = app.MapGroup(string.Empty) - .WithGroupName("AServiceClass") + .WithTags("AServiceClass") .RequireCors("__DefaultCorsPolicy") .AddEndpointFilter() .AddEndpointFilter() @@ -891,7 +954,14 @@ public static void RegisterRoutes(this global::Microsoft.AspNetCore.Builder.WebA async (global::MediatR.IMediator mediator, [global::Microsoft.AspNetCore.Http.AsParameters] global::ANamespace.ARequest request) => await mediator.Send(request, global::System.Threading.CancellationToken.None)) .RequireAuthorization("Token") - .RequireCallerAuthorization("POLICY:{|Features|:{|Platform|:[|basic_features|]},|Roles|:{|Platform|:[|platform_standard|]}}"); + .RequireCallerAuthorization("POLICY:{|Features|:{|Platform|:[|basic_features|]},|Roles|:{|Platform|:[|platform_standard|]}}") + .WithOpenApi(op => + { + op.OperationId = "A"; + op.Description = "ARequest"; + op.Summary = "ARequest"; + return op; + }); } } @@ -962,7 +1032,7 @@ public static class MinimalApiRegistration public static void RegisterRoutes(this global::Microsoft.AspNetCore.Builder.WebApplication app) { var aserviceclassGroup = app.MapGroup(string.Empty) - .WithGroupName("AServiceClass") + .WithTags("AServiceClass") .RequireCors("__DefaultCorsPolicy") .AddEndpointFilter() .AddEndpointFilter() @@ -972,7 +1042,14 @@ public static void RegisterRoutes(this global::Microsoft.AspNetCore.Builder.WebA await mediator.Send(request, global::System.Threading.CancellationToken.None)) .RequireAuthorization("Token") .RequireCallerAuthorization("POLICY:{|Features|:{|Platform|:[|basic_features|]},|Roles|:{|Platform|:[|platform_standard|]}}") - .DisableAntiforgery(); + .DisableAntiforgery() + .WithOpenApi(op => + { + op.OperationId = "A"; + op.Description = "ARequest"; + op.Summary = "ARequest"; + return op; + }); } } @@ -1043,7 +1120,7 @@ public static class MinimalApiRegistration public static void RegisterRoutes(this global::Microsoft.AspNetCore.Builder.WebApplication app) { var aserviceclassGroup = app.MapGroup(string.Empty) - .WithGroupName("AServiceClass") + .WithTags("AServiceClass") .RequireCors("__DefaultCorsPolicy") .AddEndpointFilter() .AddEndpointFilter() @@ -1053,13 +1130,27 @@ public static void RegisterRoutes(this global::Microsoft.AspNetCore.Builder.WebA await mediator.Send(request, global::System.Threading.CancellationToken.None)) .RequireAuthorization("Token") .RequireCallerAuthorization("POLICY:{|Features|:{|Platform|:[|basic_features|]},|Roles|:{|Platform|:[|platform_standard|]}}") - .DisableAntiforgery(); + .DisableAntiforgery() + .WithOpenApi(op => + { + op.OperationId = "A (Put)"; + op.Description = "ARequest"; + op.Summary = "ARequest"; + return op; + }); aserviceclassGroup.MapPatch("aroute", async (global::MediatR.IMediator mediator, [global::Microsoft.AspNetCore.Mvc.FromForm] global::ANamespace.ARequest request) => await mediator.Send(request, global::System.Threading.CancellationToken.None)) .RequireAuthorization("Token") .RequireCallerAuthorization("POLICY:{|Features|:{|Platform|:[|basic_features|]},|Roles|:{|Platform|:[|platform_standard|]}}") - .DisableAntiforgery(); + .DisableAntiforgery() + .WithOpenApi(op => + { + op.OperationId = "A (Patch)"; + op.Description = "ARequest"; + op.Summary = "ARequest"; + return op; + }); } } @@ -1131,7 +1222,7 @@ public static class MinimalApiRegistration public static void RegisterRoutes(this global::Microsoft.AspNetCore.Builder.WebApplication app) { var aserviceclassGroup = app.MapGroup("aprefix") - .WithGroupName("AServiceClass") + .WithTags("AServiceClass") .RequireCors("__DefaultCorsPolicy") .AddEndpointFilter() .AddEndpointFilter() @@ -1141,7 +1232,14 @@ public static void RegisterRoutes(this global::Microsoft.AspNetCore.Builder.WebA async (global::MediatR.IMediator mediator, [global::Microsoft.AspNetCore.Http.AsParameters] global::ANamespace.ARequest request) => await mediator.Send(request, global::System.Threading.CancellationToken.None)) .RequireAuthorization("Token") - .RequireCallerAuthorization("POLICY:{|Features|:{|Platform|:[|basic_features|]},|Roles|:{|Platform|:[|platform_standard|]}}"); + .RequireCallerAuthorization("POLICY:{|Features|:{|Platform|:[|basic_features|]},|Roles|:{|Platform|:[|platform_standard|]}}") + .WithOpenApi(op => + { + op.OperationId = "A"; + op.Description = "ARequest"; + op.Summary = "ARequest"; + return op; + }); #endif } diff --git a/src/Tools.Generators.Web.Api/MinimalApiMediatRGenerator.cs b/src/Tools.Generators.Web.Api/MinimalApiMediatRGenerator.cs index 9ed9ab47..a52a37f0 100644 --- a/src/Tools.Generators.Web.Api/MinimalApiMediatRGenerator.cs +++ b/src/Tools.Generators.Web.Api/MinimalApiMediatRGenerator.cs @@ -5,6 +5,7 @@ using Infrastructure.Web.Hosting.Common; using Microsoft.CodeAnalysis; using Microsoft.CodeAnalysis.Text; +using Tools.Analyzers.Common; namespace Tools.Generators.Web.Api; @@ -106,7 +107,7 @@ private static void BuildEndpointRegistrations( var endpointFilters = BuildEndpointFilters(serviceRegistrations); endpointRegistrations.AppendLine($@" var {groupName} = app.MapGroup({prefix}) - .WithGroupName(""{serviceClassName}"") + .WithTags(""{serviceClassName}"") .RequireCors(""{WebHostingConstants.DefaultCORSPolicyName}""){endpointFilters};"); foreach (var registration in serviceRegistrations) @@ -116,11 +117,12 @@ private static void BuildEndpointRegistrations( endpointRegistrations.AppendLine($"#if {TestingOnlyDirective}"); } - var routeEndpointMethodNames = ToMinimalApiRegistrationMethodNames(registration.OperationMethod); - foreach (var routeEndpointMethod in routeEndpointMethodNames) + var routeEndpointMethods = ToHttpMethodNames(registration.OperationMethod); + foreach (var routeEndpointMethod in routeEndpointMethods) { + var endPointMethodName = $"Map{routeEndpointMethod}"; endpointRegistrations.AppendLine( - $" {groupName}.{routeEndpointMethod}(\"{registration.RoutePath}\","); + $" {groupName}.{endPointMethodName}(\"{registration.RoutePath}\","); if (registration.OperationMethod is OperationMethod.Get or OperationMethod.Search or OperationMethod.Delete) { @@ -175,7 +177,27 @@ private static void BuildEndpointRegistrations( @" .DisableAntiforgery()"); } - endpointRegistrations.AppendLine(";"); + var requestDtoName = registration.RequestDto; + var openApiName = GenerateOpenApiName(requestDtoName, routeEndpointMethod); + endpointRegistrations.AppendLine(); + // endpointRegistrations.AppendLine( + // @" .Produces((int)global::System.Net.HttpStatusCode.NotFound, null, global::Infrastructure.Web.Api.Common.HttpContentTypes.Json, global::Infrastructure.Web.Api.Common.HttpContentTypes.Xml)"); + // endpointRegistrations.AppendLine( + // @" .ProducesProblem((int)global::System.Net.HttpStatusCode.NotFound)"); + endpointRegistrations.AppendLine( + @" .WithOpenApi(op =>"); + endpointRegistrations.AppendLine( + @" {"); + endpointRegistrations.AppendLine( + $@" op.OperationId = ""{openApiName}"";"); + endpointRegistrations.AppendLine( + $@" op.Description = ""{requestDtoName.Name}"";"); + endpointRegistrations.AppendLine( + $@" op.Summary = ""{requestDtoName.Name}"";"); + endpointRegistrations.AppendLine( + @" return op;"); + endpointRegistrations.AppendLine( + @" });"); } if (registration.IsTestingOnly) @@ -183,6 +205,25 @@ private static void BuildEndpointRegistrations( endpointRegistrations.AppendLine("#endif"); } } + + return; + + string GenerateOpenApiName(WebApiAssemblyVisitor.TypeName requestDtoName, HttpMethod method) + { + var name = requestDtoName.Name; + + if (name.EndsWith(AnalyzerConstants.RequestTypeSuffix)) + { + name = name.Substring(0, name.Length - AnalyzerConstants.RequestTypeSuffix.Length); + } + + if (method is HttpMethod.Put or HttpMethod.Patch) + { + name = $"{name} ({method})"; + } + + return name; + } } private static string BuildEndpointFilters( @@ -354,16 +395,16 @@ private static string BuildInjectedParameters(List new[] { "MapGet" }, - OperationMethod.Search => new[] { "MapGet" }, - OperationMethod.Post => new[] { "MapPost" }, - OperationMethod.PutPatch => new[] { "MapPut", "MapPatch" }, - OperationMethod.Delete => new[] { "MapDelete" }, - _ => new[] { "MapGet" } + OperationMethod.Get => [HttpMethod.Get], + OperationMethod.Search => [HttpMethod.Get], + OperationMethod.Post => [HttpMethod.Post], + OperationMethod.PutPatch => [HttpMethod.Put, HttpMethod.Patch], + OperationMethod.Delete => [HttpMethod.Delete], + _ => [HttpMethod.Get] }; } } \ No newline at end of file diff --git a/src/Tools.Generators.Web.Api/Reference/System.Net.Http.HttpMethod.cs b/src/Tools.Generators.Web.Api/Reference/System.Net.Http.HttpMethod.cs new file mode 100644 index 00000000..2778b3f6 --- /dev/null +++ b/src/Tools.Generators.Web.Api/Reference/System.Net.Http.HttpMethod.cs @@ -0,0 +1,14 @@ +namespace System.Net.Http +{ + /// + /// HACK: We define this enum here as a workaround, since is not defined in netStandard20 + /// + public enum HttpMethod + { + Get, + Delete, + Post, + Put, + Patch + } +} diff --git a/src/Tools.Generators.Web.Api/Tools.Generators.Web.Api.csproj b/src/Tools.Generators.Web.Api/Tools.Generators.Web.Api.csproj index dbb0406f..f11a71a4 100644 --- a/src/Tools.Generators.Web.Api/Tools.Generators.Web.Api.csproj +++ b/src/Tools.Generators.Web.Api/Tools.Generators.Web.Api.csproj @@ -10,6 +10,10 @@ true + + + + diff --git a/src/UserProfilesInfrastructure.IntegrationTests/UserProfileApiSpec.cs b/src/UserProfilesInfrastructure.IntegrationTests/UserProfileApiSpec.cs index f148b8e1..719e9f50 100644 --- a/src/UserProfilesInfrastructure.IntegrationTests/UserProfileApiSpec.cs +++ b/src/UserProfilesInfrastructure.IntegrationTests/UserProfileApiSpec.cs @@ -3,8 +3,8 @@ using Common; using Domain.Interfaces; using FluentAssertions; -using Infrastructure.Web.Api.Common; using Infrastructure.Web.Api.Common.Extensions; +using Infrastructure.Web.Api.Interfaces; using Infrastructure.Web.Api.Operations.Shared.UserProfiles; using Infrastructure.Web.Interfaces.Clients; using IntegrationTesting.WebApi.Common; diff --git a/src/WebsiteHost/Controllers/CSRFController.cs b/src/WebsiteHost/Controllers/CSRFController.cs index cfa803c7..94e46a0c 100644 --- a/src/WebsiteHost/Controllers/CSRFController.cs +++ b/src/WebsiteHost/Controllers/CSRFController.cs @@ -1,6 +1,6 @@ using System.Text; using Common; -using Infrastructure.Web.Api.Common; +using Infrastructure.Web.Api.Interfaces; using Infrastructure.Web.Hosting.Common.Extensions; using Infrastructure.Web.Hosting.Common.Pipeline; using Microsoft.AspNetCore.Mvc; diff --git a/src/WebsiteHost/Controllers/Home/HomeController.cs b/src/WebsiteHost/Controllers/Home/HomeController.cs index 2b168c06..e724783a 100644 --- a/src/WebsiteHost/Controllers/Home/HomeController.cs +++ b/src/WebsiteHost/Controllers/Home/HomeController.cs @@ -18,7 +18,7 @@ public HomeController(IWebHostEnvironment hostEnvironment, CSRFMiddleware.ICSRFS EnsureWebsiteIsBuilt(); } - [Route("/error")] + [HttpGet("error")] public IActionResult Error() { return Problem();