From 1aac7f1e38eeb169095fb285a5a19c9d9899399b Mon Sep 17 00:00:00 2001 From: Ian Griffiths Date: Fri, 17 Dec 2021 18:38:39 +0000 Subject: [PATCH] Vnext (#258) * Enable ASP.NET integration without needing MVC (#205) * Update various NuGet refs * Drop netstandard2.0 support * Bump to V3 because of breaking changes * Fix all Messages in the Error List (#212) --- GitVersion.yml | 18 +- README.md | 2 +- Solutions/.editorconfig | 9 +- .../Menes.Abstractions.csproj | 4 +- ...sControlPolicyEvaluationFailedException.cs | 2 +- .../OpenApiLinkResolutionException.cs | 2 +- .../Menes/Internal/OpenApiConfiguration.cs | 11 +- .../Menes/Internal/OpenApiExceptionMapper.cs | 2 +- .../Links/HalDocumentLinkRemovalOptions.cs | 2 +- .../Menes.Abstractions/Menes/Links/WebLink.cs | 2 +- .../Menes/OpenApiServiceDefinitions.cs | 2 +- .../Menes.Hosting.AspNetCore.csproj | 17 +- .../AspNetCore/MenesCatchAllMiddleware.cs | 36 ++ ...enApiAspNetApplicationBuilderExtensions.cs | 24 ++ ...s => OpenApiHostActionResultExtensions.cs} | 12 +- .../OpenApiHostDirectPipelineExtensions.cs | 43 +++ .../Internal/HttpRequestParameterBuilder.cs | 12 +- .../Menes/Internal/IHttpResponseResult.cs | 31 ++ ...utBuilder.cs => IResponseOutputBuilder.cs} | 22 +- .../Menes/Internal/OpenApiActionResult.cs | 247 ++----------- .../Internal/OpenApiActionResultBuilder.cs | 41 +++ .../Internal/OpenApiHttpResponseResult.cs | 328 ++++++++++++++++++ .../OpenApiHttpResponseResultBuilder.cs | 40 +++ .../OpenApiResultActionResultOutputBuilder.cs | 56 +-- ...sultBuilder.cs => OpenApiResultBuilder.cs} | 43 ++- .../OpenApiResultHttpResponseOutputBuilder.cs | 53 +++ .../Internal/OpenApiResultOutputBuilder.cs | 100 ++++++ .../Internal/PocoActionResultOutputBuilder.cs | 53 +-- .../Internal/PocoHttpResponseOutputBuilder.cs | 56 +++ .../Menes/Internal/PocoOutputBuilder.cs | 99 ++++++ .../Internal/StatusCodeHttpResponseResult.cs | 34 ++ ...tCoreHostingServiceCollectionExtensions.cs | 108 ++++++ ...pRequestHostServiceCollectionExtensions.cs | 46 --- Solutions/Menes.Hosting/Menes.Hosting.csproj | 2 +- .../Menes/Internal/OpenApiOperationInvoker.cs | 2 +- ...e.Hosting.AspNetCore.DirectPipeline.csproj | 23 ++ .../Program.cs | 21 ++ .../Startup.cs | 45 +++ .../appsettings.Development.json | 9 + .../appsettings.json | 10 + .../.gitignore | 0 ...es.PetStore.Hosting.AzureFunctions.csproj} | 0 .../Menes/PetStore/Hosting/DemoOpenApiHost.cs | 0 .../Menes/PetStore/Hosting/Startup.cs | 26 ++ .../host.json | 0 .../packages.lock.json | 0 .../Bindings/SelfHostedApiBindings.cs | 52 ++- .../Features/CreatePet.feature.multi.cs | 28 ++ ...leUsingStubInMenesService.feature.multi.cs | 28 ++ .../Features/GetPetById.feature.multi.cs | 28 ++ .../Features/ListPets.feature.multi.cs | 28 ++ .../AspNetDirectPetStoreStartupTestWrapper.cs | 72 ++++ .../Internals/IMultiModeTest.cs | 18 + .../Internals/MultiTestHostBase.cs | 56 +++ .../Internals/TestHostTypes.cs | 20 ++ .../Menes.PetStore.Specs.csproj | 21 +- Solutions/Menes.PetStore.Specs/Steps/Steps.cs | 20 +- .../Stubs/StubPetStoreService.cs | 2 + .../Menes.PetStore/Menes.PetStore.csproj | 1 + .../PetStoreOpenApiHostConfiguration.cs | 28 ++ .../Menes/PetStore/PetStoreService.cs | 4 +- .../PetStoreInitializationExtensions.cs} | 57 ++- .../Bindings/MenesContainerBindings.cs | 2 +- .../Steps/HttpResultBuilderSteps.cs | 11 +- .../OpenApiDefaultParameterParsingSteps.cs | 7 +- ...HostingServiceCollectionExtensionsSteps.cs | 2 +- ...Menes.Testing.AspNetCoreSelfHosting.csproj | 2 +- .../OpenApiWebHostDirectPipelineStartup.cs | 27 ++ .../OpenApiWebHostManager.cs | 116 +++++-- Solutions/Menes.sln | 16 +- 70 files changed, 1827 insertions(+), 514 deletions(-) create mode 100644 Solutions/Menes.Hosting.AspNetCore/Menes/Hosting/AspNetCore/MenesCatchAllMiddleware.cs create mode 100644 Solutions/Menes.Hosting.AspNetCore/Menes/Hosting/AspNetCore/OpenApiAspNetApplicationBuilderExtensions.cs rename Solutions/Menes.Hosting.AspNetCore/Menes/Hosting/AspNetCore/{HttpRequestExtensions.cs => OpenApiHostActionResultExtensions.cs} (75%) create mode 100644 Solutions/Menes.Hosting.AspNetCore/Menes/Hosting/AspNetCore/OpenApiHostDirectPipelineExtensions.cs create mode 100644 Solutions/Menes.Hosting.AspNetCore/Menes/Internal/IHttpResponseResult.cs rename Solutions/Menes.Hosting.AspNetCore/Menes/Internal/{IActionResultOutputBuilder.cs => IResponseOutputBuilder.cs} (63%) create mode 100644 Solutions/Menes.Hosting.AspNetCore/Menes/Internal/OpenApiActionResultBuilder.cs create mode 100644 Solutions/Menes.Hosting.AspNetCore/Menes/Internal/OpenApiHttpResponseResult.cs create mode 100644 Solutions/Menes.Hosting.AspNetCore/Menes/Internal/OpenApiHttpResponseResultBuilder.cs rename Solutions/Menes.Hosting.AspNetCore/Menes/Internal/{HttpRequestResultBuilder.cs => OpenApiResultBuilder.cs} (58%) create mode 100644 Solutions/Menes.Hosting.AspNetCore/Menes/Internal/OpenApiResultHttpResponseOutputBuilder.cs create mode 100644 Solutions/Menes.Hosting.AspNetCore/Menes/Internal/OpenApiResultOutputBuilder.cs create mode 100644 Solutions/Menes.Hosting.AspNetCore/Menes/Internal/PocoHttpResponseOutputBuilder.cs create mode 100644 Solutions/Menes.Hosting.AspNetCore/Menes/Internal/PocoOutputBuilder.cs create mode 100644 Solutions/Menes.Hosting.AspNetCore/Menes/Internal/StatusCodeHttpResponseResult.cs create mode 100644 Solutions/Menes.Hosting.AspNetCore/Microsoft/Extensions/DependencyInjection/OpenApiHttpAspNetCoreHostingServiceCollectionExtensions.cs delete mode 100644 Solutions/Menes.Hosting.AspNetCore/Microsoft/Extensions/DependencyInjection/OpenApiHttpRequestHostServiceCollectionExtensions.cs create mode 100644 Solutions/Menes.PetStore.Hosting.AspNetCore.DirectPipeline/Menes.PetStore.Hosting.AspNetCore.DirectPipeline.csproj create mode 100644 Solutions/Menes.PetStore.Hosting.AspNetCore.DirectPipeline/Program.cs create mode 100644 Solutions/Menes.PetStore.Hosting.AspNetCore.DirectPipeline/Startup.cs create mode 100644 Solutions/Menes.PetStore.Hosting.AspNetCore.DirectPipeline/appsettings.Development.json create mode 100644 Solutions/Menes.PetStore.Hosting.AspNetCore.DirectPipeline/appsettings.json rename Solutions/{Menes.PetStore.Hosting => Menes.PetStore.Hosting.AzureFunctions}/.gitignore (100%) rename Solutions/{Menes.PetStore.Hosting/Menes.PetStore.Hosting.csproj => Menes.PetStore.Hosting.AzureFunctions/Menes.PetStore.Hosting.AzureFunctions.csproj} (100%) rename Solutions/{Menes.PetStore.Hosting => Menes.PetStore.Hosting.AzureFunctions}/Menes/PetStore/Hosting/DemoOpenApiHost.cs (100%) create mode 100644 Solutions/Menes.PetStore.Hosting.AzureFunctions/Menes/PetStore/Hosting/Startup.cs rename Solutions/{Menes.PetStore.Hosting => Menes.PetStore.Hosting.AzureFunctions}/host.json (100%) rename Solutions/{Menes.PetStore.Hosting => Menes.PetStore.Hosting.AzureFunctions}/packages.lock.json (100%) create mode 100644 Solutions/Menes.PetStore.Specs/Features/CreatePet.feature.multi.cs create mode 100644 Solutions/Menes.PetStore.Specs/Features/ExampleUsingStubInMenesService.feature.multi.cs create mode 100644 Solutions/Menes.PetStore.Specs/Features/GetPetById.feature.multi.cs create mode 100644 Solutions/Menes.PetStore.Specs/Features/ListPets.feature.multi.cs create mode 100644 Solutions/Menes.PetStore.Specs/Internals/AspNetDirectPetStoreStartupTestWrapper.cs create mode 100644 Solutions/Menes.PetStore.Specs/Internals/IMultiModeTest.cs create mode 100644 Solutions/Menes.PetStore.Specs/Internals/MultiTestHostBase.cs create mode 100644 Solutions/Menes.PetStore.Specs/Internals/TestHostTypes.cs create mode 100644 Solutions/Menes.PetStore/Menes/PetStore/PetStoreOpenApiHostConfiguration.cs rename Solutions/{Menes.PetStore.Hosting/Menes/PetStore/Hosting/Startup.cs => Menes.PetStore/Microsoft/Extensions/DependencyInjection/PetStoreInitializationExtensions.cs} (56%) create mode 100644 Solutions/Menes.Testing.AspNetCoreSelfHosting/Menes/Testing/AspNetCoreSelfHosting/Internal/OpenApiWebHostDirectPipelineStartup.cs diff --git a/GitVersion.yml b/GitVersion.yml index aa779092f..1b77473d7 100644 --- a/GitVersion.yml +++ b/GitVersion.yml @@ -1,8 +1,24 @@ +# DO NOT EDIT THIS FILE +# This file was generated by the pr-autoflow mechanism as a result of executing this action: +# https://github.com/endjin/.github/actions/workflows/deploy_pr_autoflow.yml +# This repository participates in this mechanism due to an entry in this file: +# https://github.com/endjin/.github/blob/b69ff1d66541ae049fb0457c65c719c6d7e9b862/repos/live/corvus-dotnet.yml + mode: ContinuousDeployment branches: master: regex: ^master|main$ tag: preview increment: patch -next-version: "2.1" + dependabot-pr: + regex: ^dependabot + tag: dependabot + source-branches: + - develop + - master + - release + - feature + - support + - hotfix +next-version: "3.0" diff --git a/README.md b/README.md index 53d765365..99b2436c8 100644 --- a/README.md +++ b/README.md @@ -5,7 +5,7 @@ This provides abstractions for the Menes framework. -It is built for netstandard2.0. +It is built for netstandard2.1. (netstandard2.0 support is available in v2.x releases.) ## Features diff --git a/Solutions/.editorconfig b/Solutions/.editorconfig index 7f5a19272..6534381e6 100644 --- a/Solutions/.editorconfig +++ b/Solutions/.editorconfig @@ -77,4 +77,11 @@ dotnet_style_qualification_for_property = true:suggestion dotnet_style_qualification_for_event = true:suggestion # Style - using -csharp_using_directive_placement = inside_namespace:warning \ No newline at end of file +csharp_using_directive_placement = inside_namespace:warning + +# IDE0090: Use 'new(...)' +# This is a bit aggressive. It suggests simplified new(...) even for: +# public static Thing MakeThing(string arg) => new(arg); +# That seems a step to far for me because you have to go searching for where +# the type name is. +dotnet_diagnostic.IDE0090.severity = silent diff --git a/Solutions/Menes.Abstractions/Menes.Abstractions.csproj b/Solutions/Menes.Abstractions/Menes.Abstractions.csproj index 5a609b999..95b963cca 100644 --- a/Solutions/Menes.Abstractions/Menes.Abstractions.csproj +++ b/Solutions/Menes.Abstractions/Menes.Abstractions.csproj @@ -2,7 +2,7 @@ - netstandard2.0;netstandard2.1 + netstandard2.1 enable Apache-2.0 @@ -11,7 +11,7 @@ - RCS1194 + $(NoWarn);RCS1194 diff --git a/Solutions/Menes.Abstractions/Menes/Exceptions/OpenApiAccessControlPolicyEvaluationFailedException.cs b/Solutions/Menes.Abstractions/Menes/Exceptions/OpenApiAccessControlPolicyEvaluationFailedException.cs index a2755f952..892f4a691 100644 --- a/Solutions/Menes.Abstractions/Menes/Exceptions/OpenApiAccessControlPolicyEvaluationFailedException.cs +++ b/Solutions/Menes.Abstractions/Menes/Exceptions/OpenApiAccessControlPolicyEvaluationFailedException.cs @@ -50,7 +50,7 @@ private void AddProblemDetails() if (!string.IsNullOrEmpty(this.PolicyName)) { - this.AddProblemDetailsExtension("Policy Name", this.PolicyName!); // ! required as netstandard2.0 lacks nullable attributes + this.AddProblemDetailsExtension("Policy Name", this.PolicyName); } if (this.Requests?.Length > 0) diff --git a/Solutions/Menes.Abstractions/Menes/Exceptions/OpenApiLinkResolutionException.cs b/Solutions/Menes.Abstractions/Menes/Exceptions/OpenApiLinkResolutionException.cs index c435fee44..27f20a1ec 100644 --- a/Solutions/Menes.Abstractions/Menes/Exceptions/OpenApiLinkResolutionException.cs +++ b/Solutions/Menes.Abstractions/Menes/Exceptions/OpenApiLinkResolutionException.cs @@ -43,7 +43,7 @@ private void AddProblemDetails() if (!string.IsNullOrEmpty(this.ContentType)) { - this.AddProblemDetailsExtension("Owner ContentType", this.ContentType!); // ! required as netstandard2.0 lacks nullable attributes + this.AddProblemDetailsExtension("Owner ContentType", this.ContentType); } this.AddProblemDetailsExtension("Owner CLR type", this.FullTypeName); diff --git a/Solutions/Menes.Abstractions/Menes/Internal/OpenApiConfiguration.cs b/Solutions/Menes.Abstractions/Menes/Internal/OpenApiConfiguration.cs index cfd24b831..0f7972680 100644 --- a/Solutions/Menes.Abstractions/Menes/Internal/OpenApiConfiguration.cs +++ b/Solutions/Menes.Abstractions/Menes/Internal/OpenApiConfiguration.cs @@ -40,15 +40,8 @@ public OpenApiConfiguration(IServiceProvider serviceProvider) /// public Dictionary DiscriminatedTypes { - get - { - return this.discriminators ?? (this.discriminators = new Dictionary()); - } - - set - { - this.discriminators = value; - } + get => this.discriminators ??= new Dictionary(); + set => this.discriminators = value; } /// diff --git a/Solutions/Menes.Abstractions/Menes/Internal/OpenApiExceptionMapper.cs b/Solutions/Menes.Abstractions/Menes/Internal/OpenApiExceptionMapper.cs index ca6916593..9e4a59221 100644 --- a/Solutions/Menes.Abstractions/Menes/Internal/OpenApiExceptionMapper.cs +++ b/Solutions/Menes.Abstractions/Menes/Internal/OpenApiExceptionMapper.cs @@ -117,7 +117,7 @@ private void ValidateStatusCode(int statusCode) { throw new ArgumentException( nameof(statusCode), - $"Do not explicitly map exceptions to the 500 status code."); + "Do not explicitly map exceptions to the 500 status code."); } } } diff --git a/Solutions/Menes.Abstractions/Menes/Links/HalDocumentLinkRemovalOptions.cs b/Solutions/Menes.Abstractions/Menes/Links/HalDocumentLinkRemovalOptions.cs index dffcc6e8d..951493ac2 100644 --- a/Solutions/Menes.Abstractions/Menes/Links/HalDocumentLinkRemovalOptions.cs +++ b/Solutions/Menes.Abstractions/Menes/Links/HalDocumentLinkRemovalOptions.cs @@ -42,7 +42,7 @@ public enum HalDocumentLinkRemovalOptions NonRecursive = 1, /// - /// Perform an unsafe check. With this options set, link checking will skip any self links + /// Perform an unsafe check. With this options set, link checking will skip any self links /// and will also not validate any links that have a corresponding document in the /// collection. /// diff --git a/Solutions/Menes.Abstractions/Menes/Links/WebLink.cs b/Solutions/Menes.Abstractions/Menes/Links/WebLink.cs index 0226fbf4b..36ac60429 100644 --- a/Solutions/Menes.Abstractions/Menes/Links/WebLink.cs +++ b/Solutions/Menes.Abstractions/Menes/Links/WebLink.cs @@ -149,7 +149,7 @@ public bool Equals(WebLink? other) /// public override int GetHashCode() { - return (this.Href, this.Name, this.IsTemplated, this.Hreflang, this.Title, this.Type, this.Profile).GetHashCode(); + return HashCode.Combine(this.Href, this.Name, this.IsTemplated, this.Hreflang, this.Title, this.Type, this.Profile); } } } diff --git a/Solutions/Menes.Abstractions/Menes/OpenApiServiceDefinitions.cs b/Solutions/Menes.Abstractions/Menes/OpenApiServiceDefinitions.cs index 5adf2304d..052d15c2b 100644 --- a/Solutions/Menes.Abstractions/Menes/OpenApiServiceDefinitions.cs +++ b/Solutions/Menes.Abstractions/Menes/OpenApiServiceDefinitions.cs @@ -58,7 +58,7 @@ public static OpenApiDocument GetOpenApiServiceFromEmbeddedDefinition( if (diagnostics.Errors.Count > 0) { - throw new OpenApiServiceMismatchException($"Errors reading the YAML file at resource name attribute.ResourceName") + throw new OpenApiServiceMismatchException($"Errors reading the YAML file at resource name {resourceName}") .AddProblemDetailsExtension("OpenApiErrors", diagnostics.Errors); } diff --git a/Solutions/Menes.Hosting.AspNetCore/Menes.Hosting.AspNetCore.csproj b/Solutions/Menes.Hosting.AspNetCore/Menes.Hosting.AspNetCore.csproj index 740724cab..c2eb729be 100644 --- a/Solutions/Menes.Hosting.AspNetCore/Menes.Hosting.AspNetCore.csproj +++ b/Solutions/Menes.Hosting.AspNetCore/Menes.Hosting.AspNetCore.csproj @@ -2,26 +2,29 @@ - netstandard2.0;netstandard2.1 + netcoreapp3.1 enable Apache-2.0 Menes is a framework for hosting Web APIs with OpenAPI-based service definitions. This library defines the ASP.NET-Core-specific aspects of hosting in Menes. + + + $(NoWarn);RCS1090 + + + + all runtime; build; native; contentfiles; analyzers; buildtransitive - - - - all - runtime; build; native; contentfiles; analyzers; buildtransitive - + + diff --git a/Solutions/Menes.Hosting.AspNetCore/Menes/Hosting/AspNetCore/MenesCatchAllMiddleware.cs b/Solutions/Menes.Hosting.AspNetCore/Menes/Hosting/AspNetCore/MenesCatchAllMiddleware.cs new file mode 100644 index 000000000..ba80739f0 --- /dev/null +++ b/Solutions/Menes.Hosting.AspNetCore/Menes/Hosting/AspNetCore/MenesCatchAllMiddleware.cs @@ -0,0 +1,36 @@ +// +// Copyright (c) Endjin Limited. All rights reserved. +// + +namespace Menes.Hosting.AspNetCore +{ + using System.Threading.Tasks; + + using Menes.Internal; + + using Microsoft.AspNetCore.Http; + + /// + /// Middleware that passes all requests on to Menes. This never forwards requests further down the pipeline. + /// + internal class MenesCatchAllMiddleware : IMiddleware + { + private static readonly object EmptyParameters = new (); + private readonly IOpenApiHost host; + + /// + /// Creates a . + /// + /// The Menes host. + public MenesCatchAllMiddleware(IOpenApiHost host) + { + this.host = host; + } + + /// + public async Task InvokeAsync(HttpContext context, RequestDelegate next) + { + await this.host.HandleRequestAsync(context, EmptyParameters); + } + } +} \ No newline at end of file diff --git a/Solutions/Menes.Hosting.AspNetCore/Menes/Hosting/AspNetCore/OpenApiAspNetApplicationBuilderExtensions.cs b/Solutions/Menes.Hosting.AspNetCore/Menes/Hosting/AspNetCore/OpenApiAspNetApplicationBuilderExtensions.cs new file mode 100644 index 000000000..d6e375137 --- /dev/null +++ b/Solutions/Menes.Hosting.AspNetCore/Menes/Hosting/AspNetCore/OpenApiAspNetApplicationBuilderExtensions.cs @@ -0,0 +1,24 @@ +// +// Copyright (c) Endjin Limited. All rights reserved. +// + +namespace Menes.Hosting.AspNetCore +{ + using Microsoft.AspNetCore.Builder; + + /// + /// Extension methods for adding Menes to an ASP.NET Core pipeline. + /// + public static class OpenApiAspNetApplicationBuilderExtensions + { + /// + /// Adds middleware that directs all requests to Menes. + /// + /// The pipeline builder. + /// The modified pipeline builder. + public static IApplicationBuilder UseMenesCatchAll(this IApplicationBuilder app) + { + return app.UseMiddleware(); + } + } +} \ No newline at end of file diff --git a/Solutions/Menes.Hosting.AspNetCore/Menes/Hosting/AspNetCore/HttpRequestExtensions.cs b/Solutions/Menes.Hosting.AspNetCore/Menes/Hosting/AspNetCore/OpenApiHostActionResultExtensions.cs similarity index 75% rename from Solutions/Menes.Hosting.AspNetCore/Menes/Hosting/AspNetCore/HttpRequestExtensions.cs rename to Solutions/Menes.Hosting.AspNetCore/Menes/Hosting/AspNetCore/OpenApiHostActionResultExtensions.cs index 6fc706cb7..842f1cee7 100644 --- a/Solutions/Menes.Hosting.AspNetCore/Menes/Hosting/AspNetCore/HttpRequestExtensions.cs +++ b/Solutions/Menes.Hosting.AspNetCore/Menes/Hosting/AspNetCore/OpenApiHostActionResultExtensions.cs @@ -1,18 +1,24 @@ -// +// // Copyright (c) Endjin Limited. All rights reserved. // namespace Menes.Hosting.AspNetCore { using System.Threading.Tasks; + using Menes; + using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc; /// /// Extension methods for HttpRequest. /// - public static class HttpRequestExtensions + /// + /// This is used in scenarios where we want to return an to the + /// hosting framework (e.g. in Azure Functions v3). + /// + public static class OpenApiHostActionResultExtensions { /// /// Uses the to handle the request. @@ -26,4 +32,4 @@ public static Task HandleRequestAsync(this IOpenApiHost +// Copyright (c) Endjin Limited. All rights reserved. +// + +namespace Menes.Hosting.AspNetCore +{ + using System.Threading.Tasks; + + using Menes.Internal; + + using Microsoft.AspNetCore.Http; + + /// + /// Extension methods for . + /// + /// + /// + /// Applications typically don't use this directly. Instead, they will normally use + /// + /// to Menes middleware that calls this to an ASP.NET Core pipeline. + /// + /// + /// This does not require a dependency on MVC. + /// + /// + public static class OpenApiHostDirectPipelineExtensions + { + /// + /// Uses the to handle the request. + /// + /// The host to handle the request. + /// The context for the request to handle. + /// Any dynamically constructed parameters sent to the request. + /// The result of the request. + public static async Task HandleRequestAsync( + this IOpenApiHost host, HttpContext httpContext, object parameters) + { + HttpRequest httpRequest = httpContext.Request; + IHttpResponseResult result = await host.HandleRequestAsync(httpRequest.Path, httpRequest.Method, httpRequest, parameters); + await result.ExecuteResultAsync(httpContext.Response); + } + } +} \ No newline at end of file diff --git a/Solutions/Menes.Hosting.AspNetCore/Menes/Internal/HttpRequestParameterBuilder.cs b/Solutions/Menes.Hosting.AspNetCore/Menes/Internal/HttpRequestParameterBuilder.cs index 20d197633..8cc4574ea 100644 --- a/Solutions/Menes.Hosting.AspNetCore/Menes/Internal/HttpRequestParameterBuilder.cs +++ b/Solutions/Menes.Hosting.AspNetCore/Menes/Internal/HttpRequestParameterBuilder.cs @@ -22,7 +22,7 @@ namespace Menes.Internal /// /// An implementation of an . /// - public class HttpRequestParameterBuilder : IOpenApiParameterBuilder + internal class HttpRequestParameterBuilder : IOpenApiParameterBuilder { private readonly IEnumerable converters; private readonly ILogger logger; @@ -276,7 +276,7 @@ private async Task TryAddBody(HttpRequest request, OpenApiOperationPathTemplate string? requestBaseContentType = this.GetBaseContentType(request.ContentType); - if (string.IsNullOrEmpty(requestBaseContentType) || !operationPathTemplate.Operation.RequestBody.Content.TryGetValue(requestBaseContentType, out OpenApiMediaType openApiMediaType)) + if (string.IsNullOrEmpty(requestBaseContentType) || !operationPathTemplate.Operation.RequestBody.Content.TryGetValue(requestBaseContentType, out OpenApiMediaType? openApiMediaType)) { if (operationPathTemplate.Operation.RequestBody.Required) { @@ -299,7 +299,7 @@ private async Task TryAddBody(HttpRequest request, OpenApiOperationPathTemplate request.Path, request.Method, request.ContentType, - string.Join(", ", operationPathTemplate?.Operation?.RequestBody?.Content?.Keys)); + string.Join(", ", operationPathTemplate?.Operation?.RequestBody?.Content?.Keys ?? Array.Empty())); } return; @@ -514,7 +514,7 @@ private bool TryGetParameterFromPath( return false; } - if (templateParameters.TryGetValue(parameter.Name, out object value)) + if (templateParameters.TryGetValue(parameter.Name, out object? value)) { if (this.logger.IsEnabled(LogLevel.Debug)) { @@ -523,7 +523,7 @@ private bool TryGetParameterFromPath( parameter.Name); } - result = this.ConvertValue(parameter.Schema, value.ToString()); + result = this.ConvertValue(parameter.Schema, value.ToString() !); return true; } @@ -674,4 +674,4 @@ private object ConvertValue(OpenApiSchema schema, string value) throw new OpenApiServiceMismatchException($"Unable to convert value to match [{schema.GetLoggingInformation()}]"); } } -} +} \ No newline at end of file diff --git a/Solutions/Menes.Hosting.AspNetCore/Menes/Internal/IHttpResponseResult.cs b/Solutions/Menes.Hosting.AspNetCore/Menes/Internal/IHttpResponseResult.cs new file mode 100644 index 000000000..3a1c8e1ba --- /dev/null +++ b/Solutions/Menes.Hosting.AspNetCore/Menes/Internal/IHttpResponseResult.cs @@ -0,0 +1,31 @@ +// +// Copyright (c) Endjin Limited. All rights reserved. +// + +namespace Menes.Internal +{ + using System.Threading.Tasks; + + using Menes.Hosting.AspNetCore; + + using Microsoft.AspNetCore.Http; + + /// + /// Populates an with the outcome (or failure) of an operation. + /// + /// + /// Used in direct pipeline mode (). + /// This enables Menes to build a description of how the HTTP response is to be generated, + /// which can then be applied to the supplied by ASP.NET once + /// Menes is done. + /// + public interface IHttpResponseResult + { + /// + /// Populates the response. + /// + /// The response to populate. + /// A task that completes when the response has been populated. + Task ExecuteResultAsync(HttpResponse httpResponse); + } +} \ No newline at end of file diff --git a/Solutions/Menes.Hosting.AspNetCore/Menes/Internal/IActionResultOutputBuilder.cs b/Solutions/Menes.Hosting.AspNetCore/Menes/Internal/IResponseOutputBuilder.cs similarity index 63% rename from Solutions/Menes.Hosting.AspNetCore/Menes/Internal/IActionResultOutputBuilder.cs rename to Solutions/Menes.Hosting.AspNetCore/Menes/Internal/IResponseOutputBuilder.cs index 221ad8b6c..f872a004c 100644 --- a/Solutions/Menes.Hosting.AspNetCore/Menes/Internal/IActionResultOutputBuilder.cs +++ b/Solutions/Menes.Hosting.AspNetCore/Menes/Internal/IResponseOutputBuilder.cs @@ -1,22 +1,20 @@ -// +// // Copyright (c) Endjin Limited. All rights reserved. // namespace Menes.Internal { - using Microsoft.AspNetCore.Mvc; using Microsoft.OpenApi.Models; /// - /// Interface implemented by types that are used to build based output. + /// Interface implemented by types that are used to build outputs to be used with the Menes + /// host. /// - /// - /// - /// This can be used by any host that implements the AspNetCore 2.x model, such as - /// Functions V2 and AspNetCore itself. - /// - /// - public interface IActionResultOutputBuilder + /// + /// The response type. Corresponds to the 2nd type argument in + /// . + /// + public interface IResponseOutputBuilder { /// /// Gets the priority of the output builder. @@ -41,7 +39,7 @@ public interface IActionResultOutputBuilder /// /// The result. /// The operation. - /// The constructed from the operation and result. - IActionResult BuildOutput(object result, OpenApiOperation operation); + /// The constructed from the operation and result. + TResponse BuildOutput(object result, OpenApiOperation operation); } } \ No newline at end of file diff --git a/Solutions/Menes.Hosting.AspNetCore/Menes/Internal/OpenApiActionResult.cs b/Solutions/Menes.Hosting.AspNetCore/Menes/Internal/OpenApiActionResult.cs index 3367d659b..d9486763b 100644 --- a/Solutions/Menes.Hosting.AspNetCore/Menes/Internal/OpenApiActionResult.cs +++ b/Solutions/Menes.Hosting.AspNetCore/Menes/Internal/OpenApiActionResult.cs @@ -4,14 +4,11 @@ namespace Menes.Internal { - using System; using System.Collections.Generic; - using System.IO; - using System.Linq; using System.Threading.Tasks; + using Menes.Converters; - using Menes.Exceptions; - using Microsoft.AspNetCore.Http; + using Microsoft.AspNetCore.Mvc; using Microsoft.Extensions.Logging; using Microsoft.OpenApi.Models; @@ -31,10 +28,12 @@ namespace Menes.Internal /// internal sealed class OpenApiActionResult : ActionResult { - private readonly OpenApiResult openApiResult; - private readonly OpenApiOperation operation; - private readonly IEnumerable converters; - private readonly ILogger logger; + /// + /// We need to do everything to the underlying HttpResponse that the lower-level + /// does, so we just delegate to that for the + /// bulk of the work. + /// + private readonly OpenApiHttpResponseResult httpResponseResult; /// /// Initializes a new instance of the class. @@ -49,10 +48,14 @@ private OpenApiActionResult( IEnumerable converters, ILogger logger) { - this.openApiResult = openApiResult; - this.operation = operation; - this.converters = converters; - this.logger = logger; + this.httpResponseResult = new OpenApiHttpResponseResult( + openApiResult, operation, converters, logger); + } + + /// + public override Task ExecuteResultAsync(ActionContext context) + { + return this.httpResponseResult.ExecuteResultAsync(context.HttpContext.Response); } /// @@ -63,7 +66,7 @@ private OpenApiActionResult( /// The OpenAPI converters to use. /// A logger for the operation. /// A new . - public static OpenApiActionResult FromOpenApiResult( + internal static OpenApiActionResult FromOpenApiResult( OpenApiResult openApiResult, OpenApiOperation operation, IEnumerable converters, @@ -78,7 +81,7 @@ public static OpenApiActionResult FromOpenApiResult( /// The OpenAPI converters to use. /// A logger for the operation. /// A new . - public static OpenApiActionResult FromPoco( + internal static OpenApiActionResult FromPoco( object result, OpenApiOperation operation, IEnumerable converters, @@ -92,7 +95,7 @@ public static OpenApiActionResult FromPoco( /// The OpenAPI operation definition. /// The . /// True if an action result can be constructed from this operation result. - public static bool CanConstructFrom(object poco, OpenApiOperation operation, ILogger logger) + internal static bool CanConstructFrom(object poco, OpenApiOperation operation, ILogger logger) { return CanConstructFrom(GetOpenApiResultForPoco(poco, operation, logger), operation); } @@ -103,205 +106,25 @@ public static bool CanConstructFrom(object poco, OpenApiOperation operation, ILo /// The . /// The OpenAPI operation definition. /// True if an action result can be constructed from this operation result. - public static bool CanConstructFrom(OpenApiResult openApiResult, OpenApiOperation operation) - { - if (!operation.Responses.TryGetResponseForStatusCode(openApiResult.StatusCode, out OpenApiResponse response)) - { - return false; - } - - // Validate the required headers - if (!response.Headers.Where(h => h.Value.Required).All(h => openApiResult.Results.ContainsKey(h.Key))) - { - return false; - } - - // Validate the response body - return response.Content == null || response.Content.Count == 0 || response.Content.Any(c => openApiResult.Results.ContainsKey(c.Key)); - } - - /// - public override Task ExecuteResultAsync(ActionContext context) + internal static bool CanConstructFrom(OpenApiResult openApiResult, OpenApiOperation operation) { - if (this.logger.IsEnabled(LogLevel.Debug)) - { - this.logger.LogDebug("Executing [{actionResult}]", this.openApiResult.GetLoggingInformation()); - } - - try - { - this.operation.Responses.TryGetResponseForStatusCode(this.openApiResult.StatusCode, out OpenApiResponse response); - - HttpResponse httpResponse = context.HttpContext.Response; - - httpResponse.StatusCode = this.openApiResult.StatusCode; - - this.BuildHeaders(httpResponse, response); - - Task task = this.WriteBodyAsync(httpResponse, response); - if (this.logger.IsEnabled(LogLevel.Debug)) - { - this.logger.LogDebug("Executed [{actionResult}]", this.openApiResult.GetLoggingInformation()); - } - - return task; - } - catch (Exception ex) - { - this.logger.LogError("Failed to execute OpenApiActionResult [{actionResult}], [{exMessage}]", this.openApiResult.GetLoggingInformation(), ex.Message); - throw; - } + return OpenApiHttpResponseResult.CanConstructFrom(openApiResult, operation); } - private static OpenApiResult GetOpenApiResultForPoco(object result, OpenApiOperation operation, ILogger logger) - { - logger.LogDebug( - "Attempting to get OpenAPI result for with POCO with [{operation}]", - operation.GetOperationId()); -#pragma warning disable IDE0007 // Use implicit type - see https://github.com/dotnet/roslyn/issues/30450 - List> successResponses = operation -#pragma warning restore IDE0007 // Use implicit type - .Responses - .Where(r => r.Key == "default" || (r.Key.Length == 3 && r.Key[0] == '2')) - .ToList(); - if (successResponses.Count != 1) - { - throw new OpenApiServiceMismatchException($"An OpenApi service can only return a plain old CLR object (POCO) for operations that define exactly one success response (including the default response, if present), but '{operation.OperationId}' defines '{successResponses.Count}'. Return an {nameof(OpenApiResult)} instead, or modify the service definition to define a single successful response."); - } - - KeyValuePair response = successResponses.Single(); - var openApiResult = new OpenApiResult - { - // Use the status code for the first response - StatusCode = int.Parse(response.Key), - }; - - foreach (KeyValuePair mediaType in response.Value.Content) - { - // Add all our candidate media types in - openApiResult.Results.Add(mediaType.Key, result); - } - - logger.LogDebug( - "Got openAPIResult [{apiResult}] for poco with [{operation}]", - openApiResult.Results.Keys, - operation.GetOperationId()); - return openApiResult; - } - - private Task WriteBodyAsync(HttpResponse httpResponse, OpenApiResponse response) - { - if (response.Content?.Count > 0) - { - // TODO: We should probably find the first one where we can also convert the value, rather than just the first one! - KeyValuePair responseContent = response.Content.First(c => this.openApiResult.Results.ContainsKey(c.Key)); - object responseValue = this.openApiResult.Results[responseContent.Key]; - - httpResponse.ContentType = responseContent.Key; - - if (responseValue is Stream responseAsStream) - { - return responseAsStream.CopyToAsync(httpResponse.Body); - } - else - { - string outputValue = this.ConvertValue(responseContent.Value.Schema, responseValue); - return httpResponse.WriteAsync(outputValue); - } - } - - return Task.CompletedTask; - } - - private string ConvertValue(OpenApiSchema schema, object value) - { - if (this.logger.IsEnabled(LogLevel.Debug)) - { - this.logger.LogDebug("Converting value to match [{schema}]", schema.GetLoggingInformation()); - } - - foreach (IOpenApiConverter converter in this.converters) - { - if (converter.CanConvert(schema)) - { - if (this.logger.IsEnabled(LogLevel.Debug)) - { - this.logger.LogDebug( - "Matched converter [{converter}] to the [{schema}]", - converter.GetType(), - schema.GetLoggingInformation()); - } - - return converter.ConvertTo(value, schema); - } - } - - if (this.logger.IsEnabled(LogLevel.Debug)) - { - this.logger.LogDebug( - "Failed to convert value with [{schema}], falling back to just type", - schema.GetLoggingInformation()); - } - - // We didn't hit anything directly, so let's fall back to just the type alone - foreach (IOpenApiConverter converter in this.converters) - { - if (converter.CanConvert(schema, true)) - { - if (this.logger.IsEnabled(LogLevel.Debug)) - { - this.logger.LogDebug( - "Matched converter [{converter}] to the [{schema}], ignoring format", - converter.GetType(), - schema.GetLoggingInformation()); - } - - return converter.ConvertTo(value, schema); - } - } - - this.logger.LogError( - "Failed to convert value with [{schema}]", - schema.GetLoggingInformation()); - - throw new OpenApiServiceMismatchException($"Failed to convert value to match [{schema.GetLoggingInformation()}]"); - } - - private void BuildHeaders(HttpResponse httpResponse, OpenApiResponse response) + /// + /// Builds an for a POCO. + /// + /// + /// The POCO result returned by the underlying operation implementation. + /// + /// + /// The OpenAPI operation that was invoked. + /// + /// A logger. + /// An . + internal static OpenApiResult GetOpenApiResultForPoco(object result, OpenApiOperation operation, ILogger logger) { - if (this.logger.IsEnabled(LogLevel.Debug)) - { - this.logger.LogDebug("Building headers for response [{response}]", response.Description); - } - - foreach (KeyValuePair header in response.Headers) - { - if (this.logger.IsEnabled(LogLevel.Debug)) - { - this.logger.LogDebug("Attempting to add header for response [{response}]", response.Description); - } - - if (this.openApiResult.Results.TryGetValue(header.Key, out object value)) - { - string? convertedValue = null; - - if (value is Func valueAsFuncOfString) - { - convertedValue = this.ConvertValue(header.Value.Schema, valueAsFuncOfString()); - } - else - { - convertedValue = this.ConvertValue(header.Value.Schema, value); - } - - httpResponse.Headers.Add(header.Key, new Microsoft.Extensions.Primitives.StringValues(convertedValue)); - - if (this.logger.IsEnabled(LogLevel.Debug)) - { - this.logger.LogDebug("Added header for response [{response}]", response.Description); - } - } - } + return OpenApiHttpResponseResult.GetOpenApiResultForPoco(result, operation, logger); } } -} +} \ No newline at end of file diff --git a/Solutions/Menes.Hosting.AspNetCore/Menes/Internal/OpenApiActionResultBuilder.cs b/Solutions/Menes.Hosting.AspNetCore/Menes/Internal/OpenApiActionResultBuilder.cs new file mode 100644 index 000000000..d85aaeb00 --- /dev/null +++ b/Solutions/Menes.Hosting.AspNetCore/Menes/Internal/OpenApiActionResultBuilder.cs @@ -0,0 +1,41 @@ +// +// Copyright (c) Endjin Limited. All rights reserved. +// + +namespace Menes.Internal +{ + using System.Collections.Generic; + + using Microsoft.AspNetCore.Mvc; + using Microsoft.Extensions.Logging; + + /// + /// Builds an IActionResult for an OpenAPI result and operation. + /// + internal class OpenApiActionResultBuilder : OpenApiResultBuilder + { + /// + /// Creates and instance of the . + /// + /// The output builders. + /// The logger. + public OpenApiActionResultBuilder( + IEnumerable> outputBuilders, + ILogger logger) + : base(outputBuilders, logger) + { + } + + /// + public override IActionResult BuildErrorResult(int statusCode) + { + return new StatusCodeResult(statusCode); + } + + /// + public override IActionResult BuildServiceOperationNotFoundResult() + { + return new NotFoundResult(); + } + } +} \ No newline at end of file diff --git a/Solutions/Menes.Hosting.AspNetCore/Menes/Internal/OpenApiHttpResponseResult.cs b/Solutions/Menes.Hosting.AspNetCore/Menes/Internal/OpenApiHttpResponseResult.cs new file mode 100644 index 000000000..92903f0e4 --- /dev/null +++ b/Solutions/Menes.Hosting.AspNetCore/Menes/Internal/OpenApiHttpResponseResult.cs @@ -0,0 +1,328 @@ +// +// Copyright (c) Endjin Limited. All rights reserved. +// + +namespace Menes.Internal +{ + using System; + using System.Collections.Generic; + using System.IO; + using System.Linq; + using System.Threading.Tasks; + + using Menes.Converters; + using Menes.Exceptions; + + using Microsoft.AspNetCore.Http; + using Microsoft.Extensions.Logging; + using Microsoft.OpenApi.Models; + + /// + /// Wraps an instance with the ability to apply the result to an + /// . + /// + /// + /// + /// Given an and an , this validates + /// that the result conforms to the requirements of the operation, and then writes the result to the + /// appropriate parts of the response (e.g. headers, response body). + /// + /// + /// CAVEAT: It does not currently support writing cookies. + /// + /// + internal class OpenApiHttpResponseResult : IHttpResponseResult + { + private readonly OpenApiResult openApiResult; + private readonly OpenApiOperation operation; + private readonly IEnumerable converters; + private readonly ILogger logger; + + /// + /// Creates an . + /// + /// + /// The result of the operation. + /// + /// + /// The OpenAPI operation that was invoked to produce this result. + /// + /// The OpenAPI converters to use. + /// A logger for the operation. + internal OpenApiHttpResponseResult( + OpenApiResult openApiResult, + OpenApiOperation operation, + IEnumerable converters, + ILogger logger) + { + this.openApiResult = openApiResult; + this.operation = operation; + this.converters = converters; + this.logger = logger; + } + + /// + /// Apply the actions described in the to an . + /// + /// The response to populate. + /// A task that completes when the work is finished. + public async Task ExecuteResultAsync(HttpResponse httpResponse) + { + if (this.logger.IsEnabled(LogLevel.Debug)) + { + this.logger.LogDebug("Executing [{actionResult}]", this.openApiResult.GetLoggingInformation()); + } + + try + { + this.operation.Responses.TryGetResponseForStatusCode(this.openApiResult.StatusCode, out OpenApiResponse response); + + httpResponse.StatusCode = this.openApiResult.StatusCode; + + this.BuildHeaders(httpResponse, response); + + await this.WriteBodyAsync(httpResponse, response); + if (this.logger.IsEnabled(LogLevel.Debug)) + { + this.logger.LogDebug("Executed [{actionResult}]", this.openApiResult.GetLoggingInformation()); + } + } + catch (Exception ex) + { + this.logger.LogError("Failed to execute OpenApiActionResult [{actionResult}], [{exMessage}]", this.openApiResult.GetLoggingInformation(), ex.Message); + throw; + } + } + + /// + /// Creates a new from an . + /// + /// The . + /// The OpenAPI operation definition. + /// The OpenAPI converters to use. + /// A logger for the operation. + /// A new . + internal static OpenApiHttpResponseResult FromOpenApiResult( + OpenApiResult openApiResult, + OpenApiOperation operation, + IEnumerable converters, + ILogger logger) + => new OpenApiHttpResponseResult(openApiResult, operation, converters, logger); + + /// + /// Creates a new from a plain object. + /// + /// The result object, or null. + /// The OpenAPI operation definition. + /// The OpenAPI converters to use. + /// A logger for the operation. + /// A new . + internal static OpenApiHttpResponseResult FromPoco( + object result, + OpenApiOperation operation, + IEnumerable converters, + ILogger logger) + => new OpenApiHttpResponseResult(OpenApiActionResult.GetOpenApiResultForPoco(result, operation, logger), operation, converters, logger); + + /// + /// Builds an for a POCO. + /// + /// + /// The POCO result returned by the underlying operation implementation. + /// + /// + /// The OpenAPI operation that was invoked. + /// + /// A logger. + /// An . + internal static OpenApiResult GetOpenApiResultForPoco(object result, OpenApiOperation operation, ILogger logger) + { + logger.LogDebug( + "Attempting to get OpenAPI result for with POCO with [{operation}]", + operation.GetOperationId()); + + // Due to https://github.com/dotnet/roslyn/issues/30450, VS wants us to use var here. + // And there seems to be a bug meaning that if we add a pragma to disable the IDE0007, + // VS reports that as an IDE0079 unnecessary suppression! So we're reduced either to + // having the (frankly non-obvious) type name remain obscure, or to put it in a comment. + // It's a List> + var successResponses = operation + .Responses + .Where(r => r.Key == "default" || (r.Key.Length == 3 && r.Key[0] == '2')) + .ToList(); + if (successResponses.Count != 1) + { + throw new OpenApiServiceMismatchException($"An OpenApi service can only return a plain old CLR object (POCO) for operations that define exactly one success response (including the default response, if present), but '{operation.OperationId}' defines '{successResponses.Count}'. Return an {nameof(OpenApiResult)} instead, or modify the service definition to define a single successful response."); + } + + KeyValuePair response = successResponses.Single(); + var openApiResult = new OpenApiResult + { + // Use the status code for the first response + StatusCode = int.Parse(response.Key), + }; + + foreach (KeyValuePair mediaType in response.Value.Content) + { + // Add all our candidate media types in + openApiResult.Results.Add(mediaType.Key, result); + } + + logger.LogDebug( + "Got openAPIResult [{apiResult}] for poco with [{operation}]", + openApiResult.Results.Keys, + operation.GetOperationId()); + return openApiResult; + } + + /// + /// Determines if the result can be constructed from the provided result and operation definition. + /// + /// The . + /// The OpenAPI operation definition. + /// True if an action result can be constructed from this operation result. + internal static bool CanConstructFrom(OpenApiResult openApiResult, OpenApiOperation operation) + { + if (!operation.Responses.TryGetResponseForStatusCode(openApiResult.StatusCode, out OpenApiResponse response)) + { + return false; + } + + // Validate the required headers + if (!response.Headers.Where(h => h.Value.Required).All(h => openApiResult.Results.ContainsKey(h.Key))) + { + return false; + } + + // Validate the response body + return response.Content == null || response.Content.Count == 0 || response.Content.Any(c => openApiResult.Results.ContainsKey(c.Key)); + } + + /// + /// Determines if the action result can be constructed from the provided result and operation definition. + /// + /// The poco from which to attempt to construct a result. + /// The OpenAPI operation definition. + /// The . + /// True if an action result can be constructed from this operation result. + internal static bool CanConstructFrom(object poco, OpenApiOperation operation, ILogger logger) + { + return CanConstructFrom(OpenApiActionResult.GetOpenApiResultForPoco(poco, operation, logger), operation); + } + + private Task WriteBodyAsync(HttpResponse httpResponse, OpenApiResponse response) + { + if (response.Content?.Count > 0) + { + // TODO: We should probably find the first one where we can also convert the value, rather than just the first one! + KeyValuePair responseContent = response.Content.First(c => this.openApiResult.Results.ContainsKey(c.Key)); + object responseValue = this.openApiResult.Results[responseContent.Key]; + + httpResponse.ContentType = responseContent.Key; + + if (responseValue is Stream responseAsStream) + { + return responseAsStream.CopyToAsync(httpResponse.Body); + } + else + { + string outputValue = this.ConvertValue(responseContent.Value.Schema, responseValue); + return httpResponse.WriteAsync(outputValue); + } + } + + return Task.CompletedTask; + } + + private string ConvertValue(OpenApiSchema schema, object value) + { + if (this.logger.IsEnabled(LogLevel.Debug)) + { + this.logger.LogDebug("Converting value to match [{schema}]", schema.GetLoggingInformation()); + } + + foreach (IOpenApiConverter converter in this.converters) + { + if (converter.CanConvert(schema)) + { + if (this.logger.IsEnabled(LogLevel.Debug)) + { + this.logger.LogDebug( + "Matched converter [{converter}] to the [{schema}]", + converter.GetType(), + schema.GetLoggingInformation()); + } + + return converter.ConvertTo(value, schema); + } + } + + if (this.logger.IsEnabled(LogLevel.Debug)) + { + this.logger.LogDebug( + "Failed to convert value with [{schema}], falling back to just type", + schema.GetLoggingInformation()); + } + + // We didn't hit anything directly, so let's fall back to just the type alone + foreach (IOpenApiConverter converter in this.converters) + { + if (converter.CanConvert(schema, true)) + { + if (this.logger.IsEnabled(LogLevel.Debug)) + { + this.logger.LogDebug( + "Matched converter [{converter}] to the [{schema}], ignoring format", + converter.GetType(), + schema.GetLoggingInformation()); + } + + return converter.ConvertTo(value, schema); + } + } + + this.logger.LogError( + "Failed to convert value with [{schema}]", + schema.GetLoggingInformation()); + + throw new OpenApiServiceMismatchException($"Failed to convert value to match [{schema.GetLoggingInformation()}]"); + } + + private void BuildHeaders(HttpResponse httpResponse, OpenApiResponse response) + { + if (this.logger.IsEnabled(LogLevel.Debug)) + { + this.logger.LogDebug("Building headers for response [{response}]", response.Description); + } + + foreach (KeyValuePair header in response.Headers) + { + if (this.logger.IsEnabled(LogLevel.Debug)) + { + this.logger.LogDebug("Attempting to add header for response [{response}]", response.Description); + } + + if (this.openApiResult.Results.TryGetValue(header.Key, out object? value)) + { + string? convertedValue = null; + + if (value is Func valueAsFuncOfString) + { + convertedValue = this.ConvertValue(header.Value.Schema, valueAsFuncOfString()); + } + else + { + convertedValue = this.ConvertValue(header.Value.Schema, value); + } + + httpResponse.Headers.Add(header.Key, new Microsoft.Extensions.Primitives.StringValues(convertedValue)); + + if (this.logger.IsEnabled(LogLevel.Debug)) + { + this.logger.LogDebug("Added header for response [{response}]", response.Description); + } + } + } + } + } +} \ No newline at end of file diff --git a/Solutions/Menes.Hosting.AspNetCore/Menes/Internal/OpenApiHttpResponseResultBuilder.cs b/Solutions/Menes.Hosting.AspNetCore/Menes/Internal/OpenApiHttpResponseResultBuilder.cs new file mode 100644 index 000000000..e390a137f --- /dev/null +++ b/Solutions/Menes.Hosting.AspNetCore/Menes/Internal/OpenApiHttpResponseResultBuilder.cs @@ -0,0 +1,40 @@ +// +// Copyright (c) Endjin Limited. All rights reserved. +// + +namespace Menes.Internal +{ + using System.Collections.Generic; + + using Microsoft.Extensions.Logging; + + /// + /// Builds an for an OpenAPI result and operation. + /// + internal class OpenApiHttpResponseResultBuilder : OpenApiResultBuilder + { + /// + /// Creates and instance of the . + /// + /// The output builders. + /// The logger. + public OpenApiHttpResponseResultBuilder( + IEnumerable> outputBuilders, + ILogger logger) + : base(outputBuilders, logger) + { + } + + /// + public override IHttpResponseResult BuildErrorResult(int statusCode) + { + return new StatusCodeHttpResponseResult(statusCode); + } + + /// + public override IHttpResponseResult BuildServiceOperationNotFoundResult() + { + return new StatusCodeHttpResponseResult(404); + } + } +} \ No newline at end of file diff --git a/Solutions/Menes.Hosting.AspNetCore/Menes/Internal/OpenApiResultActionResultOutputBuilder.cs b/Solutions/Menes.Hosting.AspNetCore/Menes/Internal/OpenApiResultActionResultOutputBuilder.cs index 967007ba1..bcb1e3f8a 100644 --- a/Solutions/Menes.Hosting.AspNetCore/Menes/Internal/OpenApiResultActionResultOutputBuilder.cs +++ b/Solutions/Menes.Hosting.AspNetCore/Menes/Internal/OpenApiResultActionResultOutputBuilder.cs @@ -5,73 +5,49 @@ namespace Menes.Internal { using System.Collections.Generic; + using Menes.Converters; + using Microsoft.AspNetCore.Mvc; using Microsoft.Extensions.Logging; using Microsoft.OpenApi.Models; /// - /// Builds an for an , with output validation aginst the definition. + /// Builds an for an , with output + /// validation against the definition. /// /// /// /// This uses the for the actual output formatting and validation. /// /// - public class OpenApiResultActionResultOutputBuilder : IActionResultOutputBuilder + internal class OpenApiResultActionResultOutputBuilder : OpenApiResultOutputBuilder { - private readonly IEnumerable converters; - private readonly ILogger logger; - /// /// Initializes a new instance of the class. /// /// The open API converters to use with the builder. /// The logger for the output builder. - public OpenApiResultActionResultOutputBuilder(IEnumerable converters, ILogger logger) + public OpenApiResultActionResultOutputBuilder( + IEnumerable converters, + ILogger logger) + : base(converters, logger) { - this.converters = converters; - this.logger = logger; } /// - public int Priority => 100; - - /// - public IActionResult BuildOutput(object result, OpenApiOperation operation) + protected override IActionResult FromOpenApiResult( + OpenApiResult openApiResult, + OpenApiOperation operation, + IEnumerable converters, + ILogger logger) { - if (this.logger.IsEnabled(LogLevel.Debug)) - { - this.logger.LogDebug( - "Building output for [{operation}]", - operation.GetOperationId()); - } - - // This must have been called after CanBuildOutput(), so we know these casts - // and lookups will succeed - var openApiResult = (OpenApiResult)result; - - var actionResult = OpenApiActionResult.FromOpenApiResult(openApiResult, operation, this.converters, this.logger); - - if (this.logger.IsEnabled(LogLevel.Debug)) - { - this.logger.LogDebug( - "Built output for [{operation}]", - operation.GetOperationId()); - } - - return actionResult; + return OpenApiActionResult.FromOpenApiResult(openApiResult, operation, converters, logger); } /// - public bool CanBuildOutput(object result, OpenApiOperation operation) + protected override bool CanConstructFrom(OpenApiResult openApiResult, OpenApiOperation operation) { - // Are we an OpenApi Result? - if (!(result is OpenApiResult openApiResult)) - { - return false; - } - return OpenApiActionResult.CanConstructFrom(openApiResult, operation); } } diff --git a/Solutions/Menes.Hosting.AspNetCore/Menes/Internal/HttpRequestResultBuilder.cs b/Solutions/Menes.Hosting.AspNetCore/Menes/Internal/OpenApiResultBuilder.cs similarity index 58% rename from Solutions/Menes.Hosting.AspNetCore/Menes/Internal/HttpRequestResultBuilder.cs rename to Solutions/Menes.Hosting.AspNetCore/Menes/Internal/OpenApiResultBuilder.cs index 916e91022..addf5c6ea 100644 --- a/Solutions/Menes.Hosting.AspNetCore/Menes/Internal/HttpRequestResultBuilder.cs +++ b/Solutions/Menes.Hosting.AspNetCore/Menes/Internal/OpenApiResultBuilder.cs @@ -1,4 +1,4 @@ -// +// // Copyright (c) Endjin Limited. All rights reserved. // @@ -6,43 +6,39 @@ namespace Menes.Internal { using System.Collections.Generic; using System.Linq; - using Microsoft.AspNetCore.Mvc; + using Microsoft.Extensions.Logging; using Microsoft.OpenApi.Models; /// - /// Builds an IActionResult for an OpenAPI result and operation. + /// Builds a response object for an OpenAPI result and operation. /// - public class HttpRequestResultBuilder : IOpenApiResultBuilder + /// + /// The response type. + /// + internal abstract class OpenApiResultBuilder : IOpenApiResultBuilder { - private readonly ILogger logger; - private readonly IEnumerable outputBuilders; + private readonly ILogger> logger; + private readonly IEnumerable> outputBuilders; /// - /// Creates and instance of the . + /// Creates and instance of the . /// /// The output builders. /// The logger. - public HttpRequestResultBuilder(IEnumerable outputBuilders, ILogger logger) + protected OpenApiResultBuilder( + IEnumerable> outputBuilders, + ILogger> logger) { this.logger = logger; this.outputBuilders = outputBuilders.OrderBy(b => b.Priority).ToList(); } /// - public IActionResult BuildErrorResult(int statusCode) - { - return new StatusCodeResult(statusCode); - } + public abstract TResponse BuildErrorResult(int statusCode); /// - public IActionResult BuildServiceOperationNotFoundResult() - { - return new NotFoundResult(); - } - - /// - public IActionResult BuildResult(object result, OpenApiOperation operation) + public TResponse BuildResult(object result, OpenApiOperation operation) { if (this.logger.IsEnabled(LogLevel.Debug)) { @@ -51,11 +47,11 @@ public IActionResult BuildResult(object result, OpenApiOperation operation) operation.OperationId); } - foreach (IActionResultOutputBuilder outputBuilder in this.outputBuilders) + foreach (IResponseOutputBuilder outputBuilder in this.outputBuilders) { if (outputBuilder.CanBuildOutput(result, operation)) { - IActionResult output = outputBuilder.BuildOutput(result, operation); + TResponse output = outputBuilder.BuildOutput(result, operation); if (this.logger.IsEnabled(LogLevel.Information)) { this.logger.LogInformation( @@ -72,5 +68,8 @@ public IActionResult BuildResult(object result, OpenApiOperation operation) operation.OperationId); throw new OutputBuilderNotFoundException(result, operation); } + + /// + public abstract TResponse BuildServiceOperationNotFoundResult(); } -} +} \ No newline at end of file diff --git a/Solutions/Menes.Hosting.AspNetCore/Menes/Internal/OpenApiResultHttpResponseOutputBuilder.cs b/Solutions/Menes.Hosting.AspNetCore/Menes/Internal/OpenApiResultHttpResponseOutputBuilder.cs new file mode 100644 index 000000000..2b69de33d --- /dev/null +++ b/Solutions/Menes.Hosting.AspNetCore/Menes/Internal/OpenApiResultHttpResponseOutputBuilder.cs @@ -0,0 +1,53 @@ +// +// Copyright (c) Endjin Limited. All rights reserved. +// + +namespace Menes.Internal +{ + using System.Collections.Generic; + + using Menes.Converters; + + using Microsoft.Extensions.Logging; + using Microsoft.OpenApi.Models; + + /// + /// Builds an for an , with + /// output validation against the definition. + /// + /// + /// + /// This uses the for the actual output formatting and validation. + /// + /// + internal class OpenApiResultHttpResponseOutputBuilder : OpenApiResultOutputBuilder + { + /// + /// Creates an . + /// + /// The open API converters to use with the builder. + /// The logger for the output builder. + public OpenApiResultHttpResponseOutputBuilder( + IEnumerable converters, + ILogger logger) + : base(converters, logger) + { + } + + /// + protected override IHttpResponseResult FromOpenApiResult( + OpenApiResult openApiResult, + OpenApiOperation operation, + IEnumerable converters, + ILogger logger) + { + return OpenApiHttpResponseResult.FromOpenApiResult(openApiResult, operation, converters, logger); + } + + /// + protected override bool CanConstructFrom(OpenApiResult openApiResult, OpenApiOperation operation) + { + return OpenApiHttpResponseResult.CanConstructFrom(openApiResult, operation); + } + } +} \ No newline at end of file diff --git a/Solutions/Menes.Hosting.AspNetCore/Menes/Internal/OpenApiResultOutputBuilder.cs b/Solutions/Menes.Hosting.AspNetCore/Menes/Internal/OpenApiResultOutputBuilder.cs new file mode 100644 index 000000000..6f9f2dc9b --- /dev/null +++ b/Solutions/Menes.Hosting.AspNetCore/Menes/Internal/OpenApiResultOutputBuilder.cs @@ -0,0 +1,100 @@ +// +// Copyright (c) Endjin Limited. All rights reserved. +// + +namespace Menes.Internal +{ + using System.Collections.Generic; + + using Menes.Converters; + + using Microsoft.Extensions.Logging; + using Microsoft.OpenApi.Models; + + /// + /// Common logic for building a response for an , with output + /// validation against the definition. + /// + /// The response type. + internal abstract class OpenApiResultOutputBuilder : IResponseOutputBuilder + { + private readonly IEnumerable converters; + private readonly ILogger logger; + + /// + /// Creates an . + /// + /// The open API converters to use with the builder. + /// The logger for the output builder. + protected OpenApiResultOutputBuilder( + IEnumerable converters, + ILogger logger) + { + this.converters = converters; + this.logger = logger; + } + + /// + public int Priority => 100; + + /// + public TResponse BuildOutput(object result, OpenApiOperation operation) + { + if (this.logger.IsEnabled(LogLevel.Debug)) + { + this.logger.LogDebug( + "Building output for [{operation}]", + operation.GetOperationId()); + } + + // This must have been called after CanBuildOutput(), so we know these casts + // and lookups will succeed + var openApiResult = (OpenApiResult)result; + + TResponse response = this.FromOpenApiResult(openApiResult, operation, this.converters, this.logger); + + if (this.logger.IsEnabled(LogLevel.Debug)) + { + this.logger.LogDebug( + "Built output for [{operation}]", + operation.GetOperationId()); + } + + return response; + } + + /// + public bool CanBuildOutput(object result, OpenApiOperation operation) + { + // Are we an OpenApi Result? + if (result is not OpenApiResult openApiResult) + { + return false; + } + + return this.CanConstructFrom(openApiResult, operation); + } + + /// + /// Creates a response object from an . + /// + /// The . + /// The OpenAPI operation definition. + /// The OpenAPI converters to use. + /// A logger for the operation. + /// A new . + protected abstract TResponse FromOpenApiResult( + OpenApiResult openApiResult, + OpenApiOperation operation, + IEnumerable converters, + ILogger logger); + + /// + /// Determines if the action result can be constructed from the provided result and operation definition. + /// + /// The . + /// The OpenAPI operation definition. + /// True if an action result can be constructed from this operation result. + protected abstract bool CanConstructFrom(OpenApiResult openApiResult, OpenApiOperation operation); + } +} \ No newline at end of file diff --git a/Solutions/Menes.Hosting.AspNetCore/Menes/Internal/PocoActionResultOutputBuilder.cs b/Solutions/Menes.Hosting.AspNetCore/Menes/Internal/PocoActionResultOutputBuilder.cs index 8127b41c9..a3e922309 100644 --- a/Solutions/Menes.Hosting.AspNetCore/Menes/Internal/PocoActionResultOutputBuilder.cs +++ b/Solutions/Menes.Hosting.AspNetCore/Menes/Internal/PocoActionResultOutputBuilder.cs @@ -5,69 +5,50 @@ namespace Menes.Internal { using System.Collections.Generic; + using Menes.Converters; + using Microsoft.AspNetCore.Mvc; using Microsoft.Extensions.Logging; using Microsoft.OpenApi.Models; /// - /// Builds an for a POCO, with output validation aginst the definition. + /// Builds an for a POCO, with output validation against the + /// definition. /// /// /// /// This uses the for the actual output formatting and validation. /// /// - public class PocoActionResultOutputBuilder : IActionResultOutputBuilder + internal class PocoActionResultOutputBuilder : PocoOutputBuilder { - private readonly IEnumerable converters; - private readonly ILogger logger; - /// /// Initializes a new instance of the class. /// /// The open API converters to use with the builder. /// The logger for the output builder. - public PocoActionResultOutputBuilder(IEnumerable converters, ILogger logger) + public PocoActionResultOutputBuilder( + IEnumerable converters, + ILogger logger) + : base(converters, logger) { - this.converters = converters; - this.logger = logger; } /// - public int Priority => 10000; - - /// - public IActionResult BuildOutput(object result, OpenApiOperation operation) + protected override IActionResult FromPoco( + object result, + OpenApiOperation operation, + IEnumerable converters, + ILogger logger) { - if (this.logger.IsEnabled(LogLevel.Debug)) - { - this.logger.LogDebug( - "Building output for [{operation}] using result [{@result}]", - operation.OperationId, - result); - } - - // This must have been called after CanBuildOutput(), so we know these casts - // and lookups will succeed - var actionResult = OpenApiActionResult.FromPoco(result, operation, this.converters, this.logger); - - if (this.logger.IsEnabled(LogLevel.Debug)) - { - this.logger.LogDebug( - "Built output [{actionResult}] for [{operation}] using result [{@result}]", - actionResult, - operation.OperationId, - result); - } - - return actionResult; + return OpenApiActionResult.FromPoco(result, operation, converters, logger); } /// - public bool CanBuildOutput(object result, OpenApiOperation operation) + protected override bool CanConstructFrom(OpenApiResult openApiResult, OpenApiOperation operation, ILogger logger) { - return !(result is OpenApiResult) && OpenApiActionResult.CanConstructFrom(result, operation, this.logger); + return OpenApiActionResult.CanConstructFrom(openApiResult, operation, logger); } } } \ No newline at end of file diff --git a/Solutions/Menes.Hosting.AspNetCore/Menes/Internal/PocoHttpResponseOutputBuilder.cs b/Solutions/Menes.Hosting.AspNetCore/Menes/Internal/PocoHttpResponseOutputBuilder.cs new file mode 100644 index 000000000..6fc5f07e9 --- /dev/null +++ b/Solutions/Menes.Hosting.AspNetCore/Menes/Internal/PocoHttpResponseOutputBuilder.cs @@ -0,0 +1,56 @@ +// +// Copyright (c) Endjin Limited. All rights reserved. +// + +namespace Menes.Internal +{ + using System.Collections.Generic; + + using Menes.Converters; + + using Microsoft.Extensions.Logging; + using Microsoft.OpenApi.Models; + + /// + /// Builds an for a POCO, with output validation + /// against the definition. + /// + /// + /// + /// This uses the for the actual output formatting and validation. + /// + /// + internal class PocoHttpResponseOutputBuilder : PocoOutputBuilder + { + /// + /// Initializes a new instance of the class. + /// + /// The open API converters to use with the builder. + /// The logger for the output builder. + public PocoHttpResponseOutputBuilder( + IEnumerable converters, + ILogger logger) + : base(converters, logger) + { + } + + /// + protected override IHttpResponseResult FromPoco( + object result, + OpenApiOperation operation, + IEnumerable converters, + ILogger logger) + { + return OpenApiHttpResponseResult.FromPoco(result, operation, converters, logger); + } + + /// + protected override bool CanConstructFrom( + OpenApiResult openApiResult, + OpenApiOperation operation, + ILogger logger) + { + return OpenApiHttpResponseResult.CanConstructFrom(openApiResult, operation, logger); + } + } +} diff --git a/Solutions/Menes.Hosting.AspNetCore/Menes/Internal/PocoOutputBuilder.cs b/Solutions/Menes.Hosting.AspNetCore/Menes/Internal/PocoOutputBuilder.cs new file mode 100644 index 000000000..9d2799bb4 --- /dev/null +++ b/Solutions/Menes.Hosting.AspNetCore/Menes/Internal/PocoOutputBuilder.cs @@ -0,0 +1,99 @@ +// +// Copyright (c) Endjin Limited. All rights reserved. +// + +namespace Menes.Internal +{ + using System.Collections.Generic; + + using Menes.Converters; + + using Microsoft.Extensions.Logging; + using Microsoft.OpenApi.Models; + + /// + /// Common logic for building a response for a POCO, with output validation against the + /// definition. + /// + /// The response type. + internal abstract class PocoOutputBuilder : IResponseOutputBuilder + { + private readonly IEnumerable converters; + private readonly ILogger logger; + + /// + /// Initializes a new instance of the class. + /// + /// The open API converters to use with the builder. + /// The logger for the output builder. + protected PocoOutputBuilder( + IEnumerable converters, + ILogger logger) + { + this.converters = converters; + this.logger = logger; + } + + /// + public int Priority => 10000; + + /// + public TResponse BuildOutput(object result, OpenApiOperation operation) + { + if (this.logger.IsEnabled(LogLevel.Debug)) + { + this.logger.LogDebug( + "Building output for [{operation}] using result [{@result}]", + operation.OperationId, + result); + } + + // This must have been called after CanBuildOutput(), so we know these casts + // and lookups will succeed + TResponse response = this.FromPoco(result, operation, this.converters, this.logger); + + if (this.logger.IsEnabled(LogLevel.Debug)) + { + this.logger.LogDebug( + "Built output [{actionResult}] for [{operation}] using result [{@result}]", + response, + operation.OperationId, + result); + } + + return response; + } + + /// + public bool CanBuildOutput(object result, OpenApiOperation operation) + { + return !(result is OpenApiResult) && OpenApiHttpResponseResult.CanConstructFrom(result, operation, this.logger); + } + + /// + /// Creates a response object from a plain object. + /// + /// The result object, or null. + /// The OpenAPI operation definition. + /// The OpenAPI converters to use. + /// A logger for the operation. + /// A new . + protected abstract TResponse FromPoco( + object result, + OpenApiOperation operation, + IEnumerable converters, + ILogger logger); + + /// + /// Determines if the action result can be constructed from the provided result and operation definition. + /// + /// The . + /// The OpenAPI operation definition. + /// A logger for the operation. + /// True if an action result can be constructed from this operation result. + protected abstract bool CanConstructFrom( + OpenApiResult openApiResult, + OpenApiOperation operation, + ILogger logger); + } +} \ No newline at end of file diff --git a/Solutions/Menes.Hosting.AspNetCore/Menes/Internal/StatusCodeHttpResponseResult.cs b/Solutions/Menes.Hosting.AspNetCore/Menes/Internal/StatusCodeHttpResponseResult.cs new file mode 100644 index 000000000..d83cfae6f --- /dev/null +++ b/Solutions/Menes.Hosting.AspNetCore/Menes/Internal/StatusCodeHttpResponseResult.cs @@ -0,0 +1,34 @@ +// +// Copyright (c) Endjin Limited. All rights reserved. +// + +namespace Menes.Internal +{ + using System.Threading.Tasks; + + using Microsoft.AspNetCore.Http; + + /// + /// An that sets an HTTP status code. + /// + internal class StatusCodeHttpResponseResult : IHttpResponseResult + { + private readonly int statusCode; + + /// + /// Creates a . + /// + /// The status code to set on the response. + public StatusCodeHttpResponseResult(int statusCode) + { + this.statusCode = statusCode; + } + + /// + public Task ExecuteResultAsync(HttpResponse httpResponse) + { + httpResponse.StatusCode = this.statusCode; + return Task.CompletedTask; + } + } +} diff --git a/Solutions/Menes.Hosting.AspNetCore/Microsoft/Extensions/DependencyInjection/OpenApiHttpAspNetCoreHostingServiceCollectionExtensions.cs b/Solutions/Menes.Hosting.AspNetCore/Microsoft/Extensions/DependencyInjection/OpenApiHttpAspNetCoreHostingServiceCollectionExtensions.cs new file mode 100644 index 000000000..efb904ce5 --- /dev/null +++ b/Solutions/Menes.Hosting.AspNetCore/Microsoft/Extensions/DependencyInjection/OpenApiHttpAspNetCoreHostingServiceCollectionExtensions.cs @@ -0,0 +1,108 @@ +// +// Copyright (c) Endjin Limited. All rights reserved. +// + +namespace Microsoft.Extensions.DependencyInjection +{ + using System; + using Menes; + using Menes.Hosting.AspNetCore; + using Menes.Internal; + using Microsoft.AspNetCore.Http; + using Microsoft.AspNetCore.Mvc; + + /// + /// Extensions to register open api request hosting for and . + /// + public static class OpenApiHttpAspNetCoreHostingServiceCollectionExtensions + { + /// + /// Adds / based hosting. + /// + /// The type of the OpenApi context. + /// The service collection to configure. + /// A function to configure the host. + /// A function to configure the environment. + /// The configured service collection. + [Obsolete("Use AddOpenApiActionResultHosting, or consider changing to AddOpenApiAspNetPipelineHosting")] + public static IServiceCollection AddOpenApiHttpRequestHosting( + this IServiceCollection services, + Action? configureHost, + Action? configureEnvironment = null) + where TContext : class, IOpenApiContext, new() + { + return services.AddOpenApiActionResultHosting(configureHost, configureEnvironment); + } + + /// + /// Adds / based hosting. + /// + /// The type of the OpenApi context. + /// The service collection to configure. + /// A function to configure the host. + /// A function to configure the environment. + /// The configured service collection. + public static IServiceCollection AddOpenApiActionResultHosting( + this IServiceCollection services, + Action? configureHost, + Action? configureEnvironment = null) + where TContext : class, IOpenApiContext, new() + { + services.AddSingleton, PocoActionResultOutputBuilder>(); + services.AddSingleton, OpenApiResultActionResultOutputBuilder>(); + services.AddSingleton, OpenApiActionResultBuilder>(); + + services.AddHttpRequestHosting(); + + services.AddOpenApiHosting( + configureHost, + configureEnvironment); + + return services; + } + + /// + /// Adds / middleware-based hosting. + /// + /// The type of the OpenApi context. + /// The service collection to configure. + /// A function to configure the host. + /// A function to configure the environment. + /// The configured service collection. + public static IServiceCollection AddOpenApiAspNetPipelineHosting( + this IServiceCollection services, + Action? configureHost, + Action? configureEnvironment = null) + where TContext : class, IOpenApiContext, new() + { + services.AddSingleton, PocoHttpResponseOutputBuilder>(); + services.AddSingleton, OpenApiResultHttpResponseOutputBuilder>(); + services.AddSingleton, OpenApiHttpResponseResultBuilder>(); + + services.AddHttpRequestHosting(); + + services.AddOpenApiHosting( + configureHost, + configureEnvironment); + + services.AddSingleton(); + + return services; + } + + /// + /// Common setup for hosting where the request is represented by . + /// + /// The type of the OpenApi context. + /// The service collection to configure. + /// The configured service collection. + private static IServiceCollection AddHttpRequestHosting(this IServiceCollection services) + where TContext : class, IOpenApiContext, new() + { + services.AddSingleton, OpenApiContextBuilder>(); + services.AddSingleton, HttpRequestParameterBuilder>(); + + return services; + } + } +} \ No newline at end of file diff --git a/Solutions/Menes.Hosting.AspNetCore/Microsoft/Extensions/DependencyInjection/OpenApiHttpRequestHostServiceCollectionExtensions.cs b/Solutions/Menes.Hosting.AspNetCore/Microsoft/Extensions/DependencyInjection/OpenApiHttpRequestHostServiceCollectionExtensions.cs deleted file mode 100644 index b082a416c..000000000 --- a/Solutions/Menes.Hosting.AspNetCore/Microsoft/Extensions/DependencyInjection/OpenApiHttpRequestHostServiceCollectionExtensions.cs +++ /dev/null @@ -1,46 +0,0 @@ -// -// Copyright (c) Endjin Limited. All rights reserved. -// - -namespace Microsoft.Extensions.DependencyInjection -{ - using System; - using Menes; - using Menes.Internal; - using Microsoft.AspNetCore.Http; - using Microsoft.AspNetCore.Mvc; - - /// - /// Extensions to register open api request hosting for and . - /// - public static class OpenApiHttpRequestHostServiceCollectionExtensions - { - /// - /// Adds / based hosting. - /// - /// The type of the OpenApi context. - /// The service collection to configure. - /// A function to configure the host. - /// A function to configure the environment. - /// The configured service collection. - public static IServiceCollection AddOpenApiHttpRequestHosting( - this IServiceCollection services, - Action? configureHost, - Action? configureEnvironment = null) - where TContext : class, IOpenApiContext, new() - { - services.AddSingleton(); - services.AddSingleton(); - - services.AddSingleton, OpenApiContextBuilder>(); - services.AddSingleton, HttpRequestParameterBuilder>(); - services.AddSingleton, HttpRequestResultBuilder>(); - - services.AddOpenApiHosting( - configureHost, - configureEnvironment); - - return services; - } - } -} \ No newline at end of file diff --git a/Solutions/Menes.Hosting/Menes.Hosting.csproj b/Solutions/Menes.Hosting/Menes.Hosting.csproj index a7d8da1f3..66bc8b2ad 100644 --- a/Solutions/Menes.Hosting/Menes.Hosting.csproj +++ b/Solutions/Menes.Hosting/Menes.Hosting.csproj @@ -2,7 +2,7 @@ - netstandard2.0;netstandard2.1 + netstandard2.1 enable Apache-2.0 diff --git a/Solutions/Menes.Hosting/Menes/Internal/OpenApiOperationInvoker.cs b/Solutions/Menes.Hosting/Menes/Internal/OpenApiOperationInvoker.cs index 2f2700d9e..9ae66b9af 100644 --- a/Solutions/Menes.Hosting/Menes/Internal/OpenApiOperationInvoker.cs +++ b/Solutions/Menes.Hosting/Menes/Internal/OpenApiOperationInvoker.cs @@ -205,7 +205,7 @@ private async Task CheckAccessPoliciesAsync( }; if (!string.IsNullOrWhiteSpace(result.Explanation)) { - x.AddProblemDetailsExplanation(result.Explanation!); // ! required as netstandard2.0 lacks nullable attributes + x.AddProblemDetailsExplanation(result.Explanation); } throw x; diff --git a/Solutions/Menes.PetStore.Hosting.AspNetCore.DirectPipeline/Menes.PetStore.Hosting.AspNetCore.DirectPipeline.csproj b/Solutions/Menes.PetStore.Hosting.AspNetCore.DirectPipeline/Menes.PetStore.Hosting.AspNetCore.DirectPipeline.csproj new file mode 100644 index 000000000..9147377e2 --- /dev/null +++ b/Solutions/Menes.PetStore.Hosting.AspNetCore.DirectPipeline/Menes.PetStore.Hosting.AspNetCore.DirectPipeline.csproj @@ -0,0 +1,23 @@ + + + + + net5.0 + enable + + false + + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + + + + + + + diff --git a/Solutions/Menes.PetStore.Hosting.AspNetCore.DirectPipeline/Program.cs b/Solutions/Menes.PetStore.Hosting.AspNetCore.DirectPipeline/Program.cs new file mode 100644 index 000000000..a1786b2e0 --- /dev/null +++ b/Solutions/Menes.PetStore.Hosting.AspNetCore.DirectPipeline/Program.cs @@ -0,0 +1,21 @@ +// +// Copyright (c) Endjin Limited. All rights reserved. +// + +namespace Menes.PetStore.Hosting.AspNetCore.DirectPipeline +{ + using Microsoft.AspNetCore.Hosting; + using Microsoft.Extensions.Hosting; + + internal static class Program + { + public static void Main(string[] args) + { + CreateHostBuilder(args).Build().Run(); + } + + public static IHostBuilder CreateHostBuilder(string[] args) => + Host.CreateDefaultBuilder(args) + .ConfigureWebHostDefaults(webBuilder => webBuilder.UseStartup()); + } +} \ No newline at end of file diff --git a/Solutions/Menes.PetStore.Hosting.AspNetCore.DirectPipeline/Startup.cs b/Solutions/Menes.PetStore.Hosting.AspNetCore.DirectPipeline/Startup.cs new file mode 100644 index 000000000..9cde8ae3c --- /dev/null +++ b/Solutions/Menes.PetStore.Hosting.AspNetCore.DirectPipeline/Startup.cs @@ -0,0 +1,45 @@ +// +// Copyright (c) Endjin Limited. All rights reserved. +// + +namespace Menes.PetStore.Hosting.AspNetCore.DirectPipeline +{ + using Menes.Hosting.AspNetCore; + + using Microsoft.AspNetCore.Builder; + using Microsoft.AspNetCore.Hosting; + using Microsoft.Extensions.DependencyInjection; + using Microsoft.Extensions.Hosting; + + /// + /// Startup code for the web host. + /// + public class Startup + { + /// + /// Called by ASP.NET Core during DI initialization. + /// + /// The DI service collection to initialize. + public void ConfigureServices(IServiceCollection services) + { + services.AddPetStore(); + + services.AddOpenApiAspNetPipelineHosting(PetStoreOpenApiHostConfiguration.LoadDocuments); + } + + /// + /// Called by ASP.NET Core to enable us to configure the HTTP request pipeline. + /// + /// Pipeline builder. + /// Host environment information. + public void Configure(IApplicationBuilder app, IWebHostEnvironment env) + { + if (env.IsDevelopment()) + { + app.UseDeveloperExceptionPage(); + } + + app.UseMenesCatchAll(); + } + } +} \ No newline at end of file diff --git a/Solutions/Menes.PetStore.Hosting.AspNetCore.DirectPipeline/appsettings.Development.json b/Solutions/Menes.PetStore.Hosting.AspNetCore.DirectPipeline/appsettings.Development.json new file mode 100644 index 000000000..8983e0fc1 --- /dev/null +++ b/Solutions/Menes.PetStore.Hosting.AspNetCore.DirectPipeline/appsettings.Development.json @@ -0,0 +1,9 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft": "Warning", + "Microsoft.Hosting.Lifetime": "Information" + } + } +} diff --git a/Solutions/Menes.PetStore.Hosting.AspNetCore.DirectPipeline/appsettings.json b/Solutions/Menes.PetStore.Hosting.AspNetCore.DirectPipeline/appsettings.json new file mode 100644 index 000000000..d9d9a9bff --- /dev/null +++ b/Solutions/Menes.PetStore.Hosting.AspNetCore.DirectPipeline/appsettings.json @@ -0,0 +1,10 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft": "Warning", + "Microsoft.Hosting.Lifetime": "Information" + } + }, + "AllowedHosts": "*" +} diff --git a/Solutions/Menes.PetStore.Hosting/.gitignore b/Solutions/Menes.PetStore.Hosting.AzureFunctions/.gitignore similarity index 100% rename from Solutions/Menes.PetStore.Hosting/.gitignore rename to Solutions/Menes.PetStore.Hosting.AzureFunctions/.gitignore diff --git a/Solutions/Menes.PetStore.Hosting/Menes.PetStore.Hosting.csproj b/Solutions/Menes.PetStore.Hosting.AzureFunctions/Menes.PetStore.Hosting.AzureFunctions.csproj similarity index 100% rename from Solutions/Menes.PetStore.Hosting/Menes.PetStore.Hosting.csproj rename to Solutions/Menes.PetStore.Hosting.AzureFunctions/Menes.PetStore.Hosting.AzureFunctions.csproj diff --git a/Solutions/Menes.PetStore.Hosting/Menes/PetStore/Hosting/DemoOpenApiHost.cs b/Solutions/Menes.PetStore.Hosting.AzureFunctions/Menes/PetStore/Hosting/DemoOpenApiHost.cs similarity index 100% rename from Solutions/Menes.PetStore.Hosting/Menes/PetStore/Hosting/DemoOpenApiHost.cs rename to Solutions/Menes.PetStore.Hosting.AzureFunctions/Menes/PetStore/Hosting/DemoOpenApiHost.cs diff --git a/Solutions/Menes.PetStore.Hosting.AzureFunctions/Menes/PetStore/Hosting/Startup.cs b/Solutions/Menes.PetStore.Hosting.AzureFunctions/Menes/PetStore/Hosting/Startup.cs new file mode 100644 index 000000000..ff10f1693 --- /dev/null +++ b/Solutions/Menes.PetStore.Hosting.AzureFunctions/Menes/PetStore/Hosting/Startup.cs @@ -0,0 +1,26 @@ +// +// Copyright (c) Endjin Limited. All rights reserved. +// + +[assembly: Microsoft.Azure.Functions.Extensions.DependencyInjection.FunctionsStartup(typeof(Menes.PetStore.Hosting.Startup))] + +namespace Menes.PetStore.Hosting +{ + using Microsoft.Azure.Functions.Extensions.DependencyInjection; + using Microsoft.Extensions.DependencyInjection; + + /// + /// Startup code for the Function. + /// + public class Startup : FunctionsStartup + { + /// + public override void Configure(IFunctionsHostBuilder builder) + { + IServiceCollection services = builder.Services; + + services.AddPetStore(); + services.AddOpenApiActionResultHosting(PetStoreOpenApiHostConfiguration.LoadDocuments); + } + } +} \ No newline at end of file diff --git a/Solutions/Menes.PetStore.Hosting/host.json b/Solutions/Menes.PetStore.Hosting.AzureFunctions/host.json similarity index 100% rename from Solutions/Menes.PetStore.Hosting/host.json rename to Solutions/Menes.PetStore.Hosting.AzureFunctions/host.json diff --git a/Solutions/Menes.PetStore.Hosting/packages.lock.json b/Solutions/Menes.PetStore.Hosting.AzureFunctions/packages.lock.json similarity index 100% rename from Solutions/Menes.PetStore.Hosting/packages.lock.json rename to Solutions/Menes.PetStore.Hosting.AzureFunctions/packages.lock.json diff --git a/Solutions/Menes.PetStore.Specs/Bindings/SelfHostedApiBindings.cs b/Solutions/Menes.PetStore.Specs/Bindings/SelfHostedApiBindings.cs index b71017e76..82e8c1ced 100644 --- a/Solutions/Menes.PetStore.Specs/Bindings/SelfHostedApiBindings.cs +++ b/Solutions/Menes.PetStore.Specs/Bindings/SelfHostedApiBindings.cs @@ -6,13 +6,18 @@ namespace Menes.PetStore.Specs.Bindings { using System.Linq; using System.Threading.Tasks; + using Corvus.Testing.SpecFlow; - using Menes.PetStore.Hosting; + + using Menes.PetStore.Specs.Internals; using Menes.PetStore.Specs.Stubs; using Menes.Testing.AspNetCoreSelfHosting; + using Microsoft.Extensions.DependencyInjection; - using Microsoft.Extensions.DependencyInjection.Extensions; using Microsoft.Extensions.Logging; + + using NUnit.Framework.Internal; + using TechTalk.SpecFlow; [Binding] @@ -21,23 +26,40 @@ public static class SelfHostedApiBindings [BeforeScenario("useSelfHostedApi", Order = ContainerBeforeScenarioOrder.ServiceProviderAvailable)] public static Task StartSelfHostedApi(ScenarioContext scenarioContext) { + bool emulateFunctionsHost = TestExecutionContext.CurrentContext.TestObject switch + { + IMultiModeTest multiModeTest => multiModeTest.TestType == TestHostTypes.EmulateFunctionWithActionResult, + _ => true + }; + var hostManager = new OpenApiWebHostManager(); scenarioContext.Set(hostManager); - return hostManager.StartHostAsync( - "http://localhost:7071", - services => + if (emulateFunctionsHost) + { + return hostManager.StartInProcessFunctionsHostAsync( + "http://localhost:7071", + ConfigureServices); + } + else + { + return hostManager.StartInProcessAspNetHostAsync( + "http://localhost:7071", + new AspNetDirectPetStoreStartupTestWrapper(ConfigureServices)); + } + + void ConfigureServices(IServiceCollection services) + { + // Ensure log level for the service is set to debug. + services.AddLogging(builder => builder.SetMinimumLevel(LogLevel.Debug)); + + if (scenarioContext.ScenarioInfo.Tags.Contains("useStubServiceImplementation")) { - // Ensure log level for the service is set to debug. - services.AddLogging(builder => builder.SetMinimumLevel(LogLevel.Debug)); - - if (scenarioContext.ScenarioInfo.Tags.Contains("useStubServiceImplementation")) - { - ServiceDescriptor petStoreServiceDescriptor = services.First(x => x.ImplementationType == typeof(PetStoreService)); - services.Remove(petStoreServiceDescriptor); - services.AddSingleton(); - } - }); + ServiceDescriptor petStoreServiceDescriptor = services.First(x => x.ImplementationType == typeof(PetStoreService)); + services.Remove(petStoreServiceDescriptor); + services.AddSingleton(); + } + } } [AfterScenario("useSelfHostedApi")] diff --git a/Solutions/Menes.PetStore.Specs/Features/CreatePet.feature.multi.cs b/Solutions/Menes.PetStore.Specs/Features/CreatePet.feature.multi.cs new file mode 100644 index 000000000..634c78830 --- /dev/null +++ b/Solutions/Menes.PetStore.Specs/Features/CreatePet.feature.multi.cs @@ -0,0 +1,28 @@ +// +// Copyright (c) Endjin Limited. All rights reserved. +// + +namespace Menes.PetStore.Specs.Features +{ + using Menes.PetStore.Specs.Internals; + + using NUnit.Framework; + + /// + /// Adds in multi-form execution. + /// + [TestFixtureSource(nameof(FixtureArgs))] + public partial class CreatePetFeature : MultiTestHostBase + { + /// + /// Creates a . + /// + /// + /// Hosting style to test for. + /// + public CreatePetFeature(TestHostTypes hostType) + : base(hostType) + { + } + } +} \ No newline at end of file diff --git a/Solutions/Menes.PetStore.Specs/Features/ExampleUsingStubInMenesService.feature.multi.cs b/Solutions/Menes.PetStore.Specs/Features/ExampleUsingStubInMenesService.feature.multi.cs new file mode 100644 index 000000000..1648576c6 --- /dev/null +++ b/Solutions/Menes.PetStore.Specs/Features/ExampleUsingStubInMenesService.feature.multi.cs @@ -0,0 +1,28 @@ +// +// Copyright (c) Endjin Limited. All rights reserved. +// + +namespace Menes.PetStore.Specs.Features +{ + using Menes.PetStore.Specs.Internals; + + using NUnit.Framework; + + /// + /// Adds in multi-form execution. + /// + [TestFixtureSource(nameof(FixtureArgs))] + public partial class ExampleUsingStubInMenesServiceFeature : MultiTestHostBase + { + /// + /// Creates a . + /// + /// + /// Hosting style to test for. + /// + public ExampleUsingStubInMenesServiceFeature(TestHostTypes hostType) + : base(hostType) + { + } + } +} \ No newline at end of file diff --git a/Solutions/Menes.PetStore.Specs/Features/GetPetById.feature.multi.cs b/Solutions/Menes.PetStore.Specs/Features/GetPetById.feature.multi.cs new file mode 100644 index 000000000..4aa4e5e8a --- /dev/null +++ b/Solutions/Menes.PetStore.Specs/Features/GetPetById.feature.multi.cs @@ -0,0 +1,28 @@ +// +// Copyright (c) Endjin Limited. All rights reserved. +// + +namespace Menes.PetStore.Specs.Features +{ + using Menes.PetStore.Specs.Internals; + + using NUnit.Framework; + + /// + /// Adds in multi-form execution. + /// + [TestFixtureSource(nameof(FixtureArgs))] + public partial class GetPetByIdFeature : MultiTestHostBase + { + /// + /// Creates a . + /// + /// + /// Hosting style to test for. + /// + public GetPetByIdFeature(TestHostTypes hostType) + : base(hostType) + { + } + } +} \ No newline at end of file diff --git a/Solutions/Menes.PetStore.Specs/Features/ListPets.feature.multi.cs b/Solutions/Menes.PetStore.Specs/Features/ListPets.feature.multi.cs new file mode 100644 index 000000000..88f5c74c3 --- /dev/null +++ b/Solutions/Menes.PetStore.Specs/Features/ListPets.feature.multi.cs @@ -0,0 +1,28 @@ +// +// Copyright (c) Endjin Limited. All rights reserved. +// + +namespace Menes.PetStore.Specs.Features +{ + using Menes.PetStore.Specs.Internals; + + using NUnit.Framework; + + /// + /// Adds in multi-form execution. + /// + [TestFixtureSource(nameof(FixtureArgs))] + public partial class ListPetsFeature : MultiTestHostBase + { + /// + /// Creates a . + /// + /// + /// Hosting style to test for. + /// + public ListPetsFeature(TestHostTypes hostType) + : base(hostType) + { + } + } +} \ No newline at end of file diff --git a/Solutions/Menes.PetStore.Specs/Internals/AspNetDirectPetStoreStartupTestWrapper.cs b/Solutions/Menes.PetStore.Specs/Internals/AspNetDirectPetStoreStartupTestWrapper.cs new file mode 100644 index 000000000..1461f7970 --- /dev/null +++ b/Solutions/Menes.PetStore.Specs/Internals/AspNetDirectPetStoreStartupTestWrapper.cs @@ -0,0 +1,72 @@ +// +// Copyright (c) Endjin Limited. All rights reserved. +// + +namespace Menes.PetStore.Specs.Bindings +{ + using System; + + using Menes.PetStore.Hosting.AspNetCore.DirectPipeline; + + using Microsoft.AspNetCore.Builder; + using Microsoft.AspNetCore.Hosting; + using Microsoft.Extensions.DependencyInjection; + + /// + /// Enables tests to meddle with service collections after the DirectPipeline host's Startup + /// class has finished configuring services. + /// + /// + /// + /// This supports the @useStubServiceImplementation tag. For that to work, we need to + /// adjust the service collection after the Startup class has run. This is slightly problematic + /// because the ASP.NET host startup code invokes configuration callbacks before it invokes the + /// Startup type's configuration method. + /// + /// + /// When tests are running in Functions mode, we actually invoke the Functions configuration + /// code ourselves as part of the fakery around reproducing aspects of the Functions host when + /// hosting code directly in the test process. So the fact that ASP.NET Core invokes + /// configuration callbacks before Startup configuration doesn't matter because ASP.NET Core + /// isn't the thing running the Startup type of the code under test. + /// + /// + /// In ASP.NET Core direct pipeline mode, we don't use the mechanisms we use in Functions mode, + /// so we need to adapt the existing startup type to be able to inject the code we need at the + /// right point. + /// + /// + public class AspNetDirectPetStoreStartupTestWrapper + { + private readonly Action postStartupConfigurationCallback; + private readonly Startup hostStartup; + + public AspNetDirectPetStoreStartupTestWrapper( + Action postStartupConfigurationCallback) + { + this.postStartupConfigurationCallback = postStartupConfigurationCallback; + this.hostStartup = new Startup(); + } + + /// + /// Called by ASP.NET Core during DI initialization. + /// + /// The DI service collection to initialize. + public void ConfigureServices(IServiceCollection services) + { + this.hostStartup.ConfigureServices(services); + + this.postStartupConfigurationCallback(services); + } + + /// + /// Called by ASP.NET Core to enable us to configure the HTTP request pipeline. + /// + /// Pipeline builder. + /// Host environment information. + public void Configure(IApplicationBuilder app, IWebHostEnvironment env) + { + this.hostStartup.Configure(app, env); + } + } +} \ No newline at end of file diff --git a/Solutions/Menes.PetStore.Specs/Internals/IMultiModeTest.cs b/Solutions/Menes.PetStore.Specs/Internals/IMultiModeTest.cs new file mode 100644 index 000000000..5602173a2 --- /dev/null +++ b/Solutions/Menes.PetStore.Specs/Internals/IMultiModeTest.cs @@ -0,0 +1,18 @@ +// +// Copyright (c) Endjin Limited. All rights reserved. +// + +namespace Menes.PetStore.Specs.Internals +{ + /// + /// Supports executing the same test fixture multiple times in different modes. + /// + /// The type used to indicate the mode. + internal interface IMultiModeTest + { + /// + /// Gets the mode in which the test is executing. + /// + T TestType { get; } + } +} \ No newline at end of file diff --git a/Solutions/Menes.PetStore.Specs/Internals/MultiTestHostBase.cs b/Solutions/Menes.PetStore.Specs/Internals/MultiTestHostBase.cs new file mode 100644 index 000000000..33893103f --- /dev/null +++ b/Solutions/Menes.PetStore.Specs/Internals/MultiTestHostBase.cs @@ -0,0 +1,56 @@ +// +// Copyright (c) Endjin Limited. All rights reserved. +// + +namespace Menes.PetStore.Specs.Internals +{ + /// + /// Base class for tests that need to run for both Functions emulation and direct ASP.NET + /// pipeline hosting. + /// + /// + /// + /// Tests can derive from this and set the following class-level attribute: + /// + /// + /// + /// This needs to be specified on each deriving class, because NUnit does not walk up the + /// inheritance chain when looking for test fixture sources. + /// + /// + /// Deriving classes should also define a constructor that has the same signature as this + /// class's constructor, forwarding the argument on. This, in conjunction with the test + /// fixture source attribute, will cause NUnit to run the fixture for this class multiple + /// times, once for each of the host types specified in . + /// + /// + /// When using SpecFlow, bindings can detect the mode by casting the reference in + /// TestExecutionContext.CurrentContext.TestObject to + /// and then inspecting the property. + /// + /// + public class MultiTestHostBase : IMultiModeTest + { + protected static readonly object[] FixtureArgs = + { + new object[] { TestHostTypes.EmulateFunctionWithActionResult }, + new object[] { TestHostTypes.AspNetDirectPipeline }, + }; + + /// + /// Creates a . + /// + /// + /// Hosting style to test for. + /// + private protected MultiTestHostBase(TestHostTypes testType) + { + this.TestType = testType; + } + + /// + public TestHostTypes TestType { get; } + } +} \ No newline at end of file diff --git a/Solutions/Menes.PetStore.Specs/Internals/TestHostTypes.cs b/Solutions/Menes.PetStore.Specs/Internals/TestHostTypes.cs new file mode 100644 index 000000000..a9307a16e --- /dev/null +++ b/Solutions/Menes.PetStore.Specs/Internals/TestHostTypes.cs @@ -0,0 +1,20 @@ +// +// Copyright (c) Endjin Limited. All rights reserved. +// + +namespace Menes.PetStore.Specs.Internals +{ + /// + /// Host types for which test suites may be executed. + /// + /// + /// Menes supports two modes of ASP.NET Core hosting, and we want to run certain sets of tests + /// against each of these. This enumeration type is used to determine which mode a suite is + /// being executed for. + /// + public enum TestHostTypes + { + EmulateFunctionWithActionResult, + AspNetDirectPipeline, + } +} \ No newline at end of file diff --git a/Solutions/Menes.PetStore.Specs/Menes.PetStore.Specs.csproj b/Solutions/Menes.PetStore.Specs/Menes.PetStore.Specs.csproj index ec9a23791..7b49ceb41 100644 --- a/Solutions/Menes.PetStore.Specs/Menes.PetStore.Specs.csproj +++ b/Solutions/Menes.PetStore.Specs/Menes.PetStore.Specs.csproj @@ -2,7 +2,7 @@ - netcoreapp3.1 + net5.0 enable false + + + %(RelativeDir)$([System.String]::Copy('%(Filename)').Replace(".multi", "")) + + + \ No newline at end of file diff --git a/Solutions/Menes.PetStore.Specs/Steps/Steps.cs b/Solutions/Menes.PetStore.Specs/Steps/Steps.cs index 7d0399a81..1060becde 100644 --- a/Solutions/Menes.PetStore.Specs/Steps/Steps.cs +++ b/Solutions/Menes.PetStore.Specs/Steps/Steps.cs @@ -54,17 +54,17 @@ public Task WhenIRequestThatANewPetBeCreated(Table table) // We want to be able to send invalid values for Id to test that the validation is working correctly. // But if the value is a valid integer, we need to make sure it's passed correctly, so... - string? idFieldValue = this.ParseStringValue(petRow["Id"]); + string? idFieldValue = ParseStringValue(petRow["Id"]); object? id = long.TryParse(idFieldValue, out long idAsLong) - ? (object?)idAsLong + ? idAsLong : idFieldValue; var pet = new { - id = id, - name = this.ParseStringValue(petRow["Name"]), - tag = this.ParseStringValue(petRow["Tag"]), - size = this.ParseStringValue(petRow["Size"]), + id, + name = ParseStringValue(petRow["Name"]), + tag = ParseStringValue(petRow["Tag"]), + size = ParseStringValue(petRow["Size"]), }; return this.SendPostRequest("/pets", pet); @@ -97,8 +97,8 @@ public void ThenTheResponseShouldContainTheHeader(string headerName) { HttpResponseMessage response = this.scenarioContext.Get(); - Assert.IsTrue(response.Headers.TryGetValues(headerName, out IEnumerable values)); - Assert.IsNotEmpty(values); + Assert.IsTrue(response.Headers.TryGetValues(headerName, out IEnumerable? values)); + Assert.IsNotEmpty(values!); } [Then("the response should not contain the '(.*)' header")] @@ -141,7 +141,7 @@ public void ThenTheResponseObjectShouldNotHaveAPropertyCalled(string propertyPat Assert.IsNull(token); } - [Then(@"the response object should have an array property called '(.*)' containing (.*) entries")] + [Then("the response object should have an array property called '(.*)' containing (.*) entries")] public void ThenTheResponseObjectShouldHaveAnArrayPropertyCalledContainingEntries(string propertyPath, int expectedEntryCount) { JToken actualToken = this.GetRequiredTokenFromResponseObject(propertyPath); @@ -192,7 +192,7 @@ private JToken GetRequiredTokenFromResponseObject(string propertyPath) return token; } - private string? ParseStringValue(string input, string? valueIfEmpty = null) + private static string? ParseStringValue(string input, string? valueIfEmpty = null) { if (string.IsNullOrEmpty(input)) { diff --git a/Solutions/Menes.PetStore.Specs/Stubs/StubPetStoreService.cs b/Solutions/Menes.PetStore.Specs/Stubs/StubPetStoreService.cs index 02ca3b5e9..b163f623c 100644 --- a/Solutions/Menes.PetStore.Specs/Stubs/StubPetStoreService.cs +++ b/Solutions/Menes.PetStore.Specs/Stubs/StubPetStoreService.cs @@ -10,6 +10,8 @@ namespace Menes.PetStore.Specs.Stubs using Menes.PetStore.Responses; using Menes.PetStore.Responses.Mappers; + [System.Diagnostics.CodeAnalysis.SuppressMessage("Style", "IDE0060:Remove unused parameter", Justification = "Some implementations deliberately empty, but parameters need to be present for Menes to match with the operation")] + [System.Diagnostics.CodeAnalysis.SuppressMessage("Redundancy", "RCS1163:Unused parameter.", Justification = "See IDE0060 justification")] public class StubPetStoreService : IOpenApiService { private readonly PetResourceMapper petResourceMapper; diff --git a/Solutions/Menes.PetStore/Menes.PetStore.csproj b/Solutions/Menes.PetStore/Menes.PetStore.csproj index 580956070..9a429055b 100644 --- a/Solutions/Menes.PetStore/Menes.PetStore.csproj +++ b/Solutions/Menes.PetStore/Menes.PetStore.csproj @@ -25,6 +25,7 @@ + diff --git a/Solutions/Menes.PetStore/Menes/PetStore/PetStoreOpenApiHostConfiguration.cs b/Solutions/Menes.PetStore/Menes/PetStore/PetStoreOpenApiHostConfiguration.cs new file mode 100644 index 000000000..bd26a4b08 --- /dev/null +++ b/Solutions/Menes.PetStore/Menes/PetStore/PetStoreOpenApiHostConfiguration.cs @@ -0,0 +1,28 @@ +// +// Copyright (c) Endjin Limited. All rights reserved. +// + +namespace Menes.PetStore +{ + using Microsoft.Extensions.DependencyInjection; + + /// + /// Common API host configuration. + /// + public static class PetStoreOpenApiHostConfiguration + { + /// + /// Callback that can be passed to DI registration methods that provide a + /// -based callback. + /// + /// The open API host configuration object. + public static void LoadDocuments(IOpenApiHostConfiguration hostConfig) + { + hostConfig.Documents.RegisterOpenApiServiceWithEmbeddedDefinition( + typeof(PetStoreService).Assembly, + "Menes.PetStore.PetStore.yaml"); + + hostConfig.Documents.AddSwaggerEndpoint(); + } + } +} \ No newline at end of file diff --git a/Solutions/Menes.PetStore/Menes/PetStore/PetStoreService.cs b/Solutions/Menes.PetStore/Menes/PetStore/PetStoreService.cs index a6a2b6e75..0fa1d8d29 100644 --- a/Solutions/Menes.PetStore/Menes/PetStore/PetStoreService.cs +++ b/Solutions/Menes.PetStore/Menes/PetStore/PetStoreService.cs @@ -212,7 +212,7 @@ public async Task CreatePets(PetResource body) HalDocument response = await this.petMapper.MapAsync(body).ConfigureAwait(false); WebLink location = response.GetLinksForRelation("self").First(); - return this.CreatedResult(location.Href, response, "application/hal+json").WithAuditData(("id", (object)body.Id)); + return this.CreatedResult(location.Href, response, "application/hal+json").WithAuditData(("id", body.Id)); } /// @@ -316,7 +316,7 @@ private async Task MapAndReturnPetAsync(PetResource result) return this .OkResult(response, "application/hal+json") - .WithAuditData(("id", (object)result.Id)); + .WithAuditData(("id", result.Id)); } } } \ No newline at end of file diff --git a/Solutions/Menes.PetStore.Hosting/Menes/PetStore/Hosting/Startup.cs b/Solutions/Menes.PetStore/Microsoft/Extensions/DependencyInjection/PetStoreInitializationExtensions.cs similarity index 56% rename from Solutions/Menes.PetStore.Hosting/Menes/PetStore/Hosting/Startup.cs rename to Solutions/Menes.PetStore/Microsoft/Extensions/DependencyInjection/PetStoreInitializationExtensions.cs index 1f2853ff8..166381105 100644 --- a/Solutions/Menes.PetStore.Hosting/Menes/PetStore/Hosting/Startup.cs +++ b/Solutions/Menes.PetStore/Microsoft/Extensions/DependencyInjection/PetStoreInitializationExtensions.cs @@ -1,45 +1,37 @@ -// +// // Copyright (c) Endjin Limited. All rights reserved. // -[assembly: Microsoft.Azure.Functions.Extensions.DependencyInjection.FunctionsStartup(typeof(Menes.PetStore.Hosting.Startup))] - -namespace Menes.PetStore.Hosting +namespace Microsoft.Extensions.DependencyInjection { + using Menes; + using Menes.PetStore; using Menes.PetStore.Responses; using Menes.PetStore.Responses.Mappers; - using Microsoft.Azure.Functions.Extensions.DependencyInjection; - using Microsoft.Extensions.DependencyInjection; + using Newtonsoft.Json; using Newtonsoft.Json.Converters; /// - /// Startup code for the Function. + /// Common DI initialization for PetStore. /// - public class Startup : FunctionsStartup + public static class PetStoreInitializationExtensions { - /// - public override void Configure(IFunctionsHostBuilder builder) + /// + /// Add services required by PetStore to DI. + /// + /// The service collection. + /// The modified service collection. + public static IServiceCollection AddPetStore(this IServiceCollection services) { - IServiceCollection services = builder.Services; - - services.AddLogging(); - services.AddHttpClient(); - - ConfigureMenes(services); - - // This converter used to be added by default as part of Menes v1.x initialisation, but this is no longer - // the case in v2.0 onwards. However, we have specified that our pets will have their Size value, which is - // represented by the Size enumeration, returned as a string. This conversion could be done manually in the - // PetResourceMapper, but we can achieve the same goal using the StringEnumConverter. - services.AddSingleton(new StringEnumConverter(true)); + return services + .AddLogging() + .AddHttpClient() // Needed because the PetStore grabs images from flickr + .AddPetStoreMenesServices(); } - private static void ConfigureMenes(IServiceCollection services) + private static IServiceCollection AddPetStoreMenesServices(this IServiceCollection services) { - // Main Menes setup call. - services.AddOpenApiHttpRequestHosting(LoadDocuments); - // We can add all the services here // We will only actually *provide* services that are in the YAML file(s) we load below // So you can register everything, and use the yaml files you deploy to decide what is responded to by this instance @@ -49,15 +41,14 @@ private static void ConfigureMenes(IServiceCollection services) // that their ConfigureLinkMap methods are added as part of initialisation. services.AddHalDocumentMapper(); services.AddHalDocumentMapper(); - } - private static void LoadDocuments(IOpenApiHostConfiguration hostConfig) - { - hostConfig.Documents.RegisterOpenApiServiceWithEmbeddedDefinition( - typeof(PetStoreService).Assembly, - "Menes.PetStore.PetStore.yaml"); + // This converter used to be added by default as part of Menes v1.x initialisation, but this is no longer + // the case in v2.0 onwards. However, we have specified that our pets will have their Size value, which is + // represented by the Size enumeration, returned as a string. This conversion could be done manually in the + // PetResourceMapper, but we can achieve the same goal using the StringEnumConverter. + services.AddSingleton(new StringEnumConverter(true)); - hostConfig.Documents.AddSwaggerEndpoint(); + return services; } } } \ No newline at end of file diff --git a/Solutions/Menes.Specs/Bindings/MenesContainerBindings.cs b/Solutions/Menes.Specs/Bindings/MenesContainerBindings.cs index 039c79f1f..a6723245f 100644 --- a/Solutions/Menes.Specs/Bindings/MenesContainerBindings.cs +++ b/Solutions/Menes.Specs/Bindings/MenesContainerBindings.cs @@ -30,7 +30,7 @@ public static void InitializeContainer(ScenarioContext scenarioContext) serviceCollection => { serviceCollection.AddLogging(); - serviceCollection.AddOpenApiHttpRequestHosting(null); + serviceCollection.AddOpenApiActionResultHosting(null); var instrumentationProvider = new FakeInstrumentationProvider(); serviceCollection.AddSingleton(instrumentationProvider); diff --git a/Solutions/Menes.Specs/Steps/HttpResultBuilderSteps.cs b/Solutions/Menes.Specs/Steps/HttpResultBuilderSteps.cs index 12cd9b89b..726ba174a 100644 --- a/Solutions/Menes.Specs/Steps/HttpResultBuilderSteps.cs +++ b/Solutions/Menes.Specs/Steps/HttpResultBuilderSteps.cs @@ -2,9 +2,6 @@ // Copyright (c) Endjin Limited. All rights reserved. // -#pragma warning disable SA1600 // Elements should be documented -#pragma warning disable CS1591 // Elements should be documented - namespace Menes.Specs.Steps { using System; @@ -41,7 +38,7 @@ public void GivenIHaveAnOpenApiOperation() new OpenApiResponse { Content = new Dictionary { { "application/hal+json", new OpenApiMediaType() } } }); } - [Given(@"I have an OpenApiResult with a (.*) response")] + [Given("I have an OpenApiResult with a (.*) response")] public void GivenIHaveAnOpenApiResultWithAResponse(int response) { this.result = new OpenApiResult { StatusCode = response }; @@ -61,14 +58,14 @@ public void WhenIPassTheOpenApiOperationAndOpenApiResultToHttpRequestResultBuild try { var resultBuilder = - new HttpRequestResultBuilder( + new OpenApiActionResultBuilder( new[] { new OpenApiResultActionResultOutputBuilder( Enumerable.Empty(), ContainerBindings.GetServiceProvider(this.scenarioContext).GetRequiredService>()), }, - ContainerBindings.GetServiceProvider(this.scenarioContext).GetRequiredService>()); + ContainerBindings.GetServiceProvider(this.scenarioContext).GetRequiredService>()); resultBuilder.BuildResult(this.result!, this.operation!); } catch (Exception x) @@ -83,7 +80,7 @@ public void ThenItShouldThrowAnOutputBuilderNotFoundException() Assert.IsInstanceOf(this.exception); } - [Then(@"it should not throw an OutputBuilderNotFoundException")] + [Then("it should not throw an OutputBuilderNotFoundException")] public void ThenItShouldNotThrowAnOutputBuilderNotFoundException() { Assert.IsNotInstanceOf(this.exception); diff --git a/Solutions/Menes.Specs/Steps/OpenApiDefaultParameterParsingSteps.cs b/Solutions/Menes.Specs/Steps/OpenApiDefaultParameterParsingSteps.cs index e96ad9081..c13b92f6a 100644 --- a/Solutions/Menes.Specs/Steps/OpenApiDefaultParameterParsingSteps.cs +++ b/Solutions/Menes.Specs/Steps/OpenApiDefaultParameterParsingSteps.cs @@ -2,9 +2,6 @@ // Copyright (c) Endjin Limited. All rights reserved. // -#pragma warning disable SA1600 // Elements should be documented -#pragma warning disable CS1591 // Elements should be documented - namespace Menes.Specs.Steps { using System; @@ -152,12 +149,12 @@ public void ThenAnShouldBeThrown(string exceptionType) { Assert.IsNotNull(this.exception); - Assert.AreEqual(exceptionType, this.exception!.GetType().Name.ToString()); + Assert.AreEqual(exceptionType, this.exception!.GetType().Name); } private void InitializeDocumentProviderAndPathMatcher(string openApiSpec) { - OpenApiDocument document = new OpenApiStringReader().Read(openApiSpec, out OpenApiDiagnostic diagnostic); + OpenApiDocument document = new OpenApiStringReader().Read(openApiSpec, out OpenApiDiagnostic _); var documentProvider = new OpenApiDocumentProvider(new LoggerFactory().CreateLogger()); documentProvider.Add(document); diff --git a/Solutions/Menes.Specs/Steps/OpenApiHostingServiceCollectionExtensionsSteps.cs b/Solutions/Menes.Specs/Steps/OpenApiHostingServiceCollectionExtensionsSteps.cs index b3644a0c9..7b4aaa34b 100644 --- a/Solutions/Menes.Specs/Steps/OpenApiHostingServiceCollectionExtensionsSteps.cs +++ b/Solutions/Menes.Specs/Steps/OpenApiHostingServiceCollectionExtensionsSteps.cs @@ -41,7 +41,7 @@ public void WhenIAddOpenApiHostingToTheServiceCollection() ServiceCollection collection = this.scenarioContext.Get(); collection.AddLogging(); - collection.AddOpenApiHttpRequestHosting(_ => { }, null); + collection.AddOpenApiActionResultHosting(_ => { }, null); } [Given("I have built the service provider from the service collection")] diff --git a/Solutions/Menes.Testing.AspNetCoreSelfHosting/Menes.Testing.AspNetCoreSelfHosting.csproj b/Solutions/Menes.Testing.AspNetCoreSelfHosting/Menes.Testing.AspNetCoreSelfHosting.csproj index 890aa7705..22f85d646 100644 --- a/Solutions/Menes.Testing.AspNetCoreSelfHosting/Menes.Testing.AspNetCoreSelfHosting.csproj +++ b/Solutions/Menes.Testing.AspNetCoreSelfHosting/Menes.Testing.AspNetCoreSelfHosting.csproj @@ -2,7 +2,7 @@ - netstandard2.0;netstandard2.1 + netcoreapp3.1 enable Apache-2.0 diff --git a/Solutions/Menes.Testing.AspNetCoreSelfHosting/Menes/Testing/AspNetCoreSelfHosting/Internal/OpenApiWebHostDirectPipelineStartup.cs b/Solutions/Menes.Testing.AspNetCoreSelfHosting/Menes/Testing/AspNetCoreSelfHosting/Internal/OpenApiWebHostDirectPipelineStartup.cs new file mode 100644 index 000000000..e75542e76 --- /dev/null +++ b/Solutions/Menes.Testing.AspNetCoreSelfHosting/Menes/Testing/AspNetCoreSelfHosting/Internal/OpenApiWebHostDirectPipelineStartup.cs @@ -0,0 +1,27 @@ +// +// Copyright (c) Endjin Limited. All rights reserved. +// + +namespace Menes.Testing.AspNetCoreSelfHosting.Internal +{ + using Menes.Hosting.AspNetCore; + + using Microsoft.AspNetCore.Builder; + using Microsoft.AspNetCore.Hosting; + + /// + /// Startup class used with to initialise a webhost for tests + /// the use ASP.NET direct pipeline hosting. + /// + internal class OpenApiWebHostDirectPipelineStartup + { + /// + /// Configures the function host, adding a catch-all route that then hands off to Menes to process the request. + /// + /// The to configure. + public void Configure(IApplicationBuilder app) + { + app.UseMenesCatchAll(); + } + } +} \ No newline at end of file diff --git a/Solutions/Menes.Testing.AspNetCoreSelfHosting/Menes/Testing/AspNetCoreSelfHosting/OpenApiWebHostManager.cs b/Solutions/Menes.Testing.AspNetCoreSelfHosting/Menes/Testing/AspNetCoreSelfHosting/OpenApiWebHostManager.cs index 482e459a7..7e4f10c0f 100644 --- a/Solutions/Menes.Testing.AspNetCoreSelfHosting/Menes/Testing/AspNetCoreSelfHosting/OpenApiWebHostManager.cs +++ b/Solutions/Menes.Testing.AspNetCoreSelfHosting/Menes/Testing/AspNetCoreSelfHosting/OpenApiWebHostManager.cs @@ -7,7 +7,9 @@ namespace Menes.Testing.AspNetCoreSelfHosting using System; using System.Collections.Generic; using System.Threading.Tasks; + using Menes.Testing.AspNetCoreSelfHosting.Internal; + using Microsoft.AspNetCore; using Microsoft.AspNetCore.Hosting; using Microsoft.Azure.WebJobs.Hosting; @@ -21,7 +23,7 @@ namespace Menes.Testing.AspNetCoreSelfHosting /// This class allows you to use the same Startup class that's used in an Azure function to run services in memory. /// To run the same services in memory as your function, obtain an instance of this class (if you wish to run multiple /// services, you can create a single instance of this class to manage all services). Then use the - /// method to add services, supplying the Startup class from your function and/or a + /// method to add services, supplying the Startup class from your function and/or a /// callback to configure the service collection and the base url (including port number where necessary) that the /// endpoints should be made available on. /// @@ -29,7 +31,7 @@ namespace Menes.Testing.AspNetCoreSelfHosting /// Note that this assumes you're using an instance method in your function host rather than the older static method /// approach. This means that your initialisation will have been moved into a Startup class that implements /// IWebJobsStartup or FunctionsStartup; this is the class that should be supplied to - /// . + /// . /// /// /// You will normally make this call in a method tagged with either BeforeScenario or BeforeFeature. In the @@ -44,7 +46,8 @@ public class OpenApiWebHostManager private readonly List webHosts = new List(); /// - /// Starts a new function host using the given Uri and startup class. + /// Starts a new in-process host using the given Uri and startup class, with basic emulation of the Azure Functions + /// host's request routing.. /// /// The type of the startup class. This should be the type of the class from the /// function host project that is used to initialise the OpenApi services and dependencies. @@ -54,21 +57,83 @@ public class OpenApiWebHostManager /// your startup class has executed. You can use this to swap out services for stubs or fakes. /// /// A representing the asynchronous operation. - public Task StartHostAsync( + public Task StartInProcessFunctionsHostAsync( string baseUrl, Action? additionalServiceConfigurationCallback = null) - where TFunctionStartup : IWebJobsStartup, new() + where TFunctionStartup : class, IWebJobsStartup, new() + => this.StartInProcessFunctionsHostAsync(new TFunctionStartup(), baseUrl, additionalServiceConfigurationCallback); + + /// + /// Starts a new in-process host using the given Uri and startup class, with basic emulation of the Azure Functions + /// host's request routing.. + /// + /// The type of the startup class. This should be the type of the class from the + /// function host project that is used to initialise the OpenApi services and dependencies. + /// The startup instance to use. + /// The url that the function will be exposed on. + /// + /// A callback that will allow you to make changes to the after the code in + /// your startup class has executed. You can use this to swap out services for stubs or fakes. + /// + /// A representing the asynchronous operation. + public Task StartInProcessFunctionsHostAsync( + TFunctionStartup startupInstance, + string baseUrl, + Action? additionalServiceConfigurationCallback = null) + where TFunctionStartup : class, IWebJobsStartup + => this.StartAspNetHostAsync( + baseUrl, + services => + { + // Shim to allow us to invoke the configuration method of the services startup class. + // (We pass a completely different class in for ASP.NET Core to use as the startup + // class, in order to fake the bits of the Functions host that we need to fake, + // which is why we need to invoke the real startup directly. This also has the + // benefit of enabling us to control the order: it's important that the additional + // configuration callback is invoked after Startup configuration, because that + // callback is used by some tests to replace certain services set up by Startup.) + var webJobBuilder = new WebJobBuilder(services); + startupInstance.Configure(webJobBuilder); + + // Invoke any extra container configuration. + additionalServiceConfigurationCallback?.Invoke(services); + }, + webHostBuilder => webHostBuilder.UseStartup()); + + /// + /// Starts a new in-process host using the given Uri and a pre-instantiated instance of the startup class. + /// + /// The type of the startup class. This should be the type of the class from the + /// ASP.NET host project that is used to initialise the OpenApi services and dependencies. + /// The url that the function will be exposed on. + /// The instantiated startup class to use. + /// A representing the asynchronous operation. + /// + /// If you need to run further service configuration that is guaranteed to execute after the Startup + /// DI configuration when testing with ASP.NET direct pipeline hosting, use a wrapper startup class + /// because that's the only way to ensure that your configuration will run after Startup. (The ASP.NET + /// web host startup prefers to run the Startup methods after all other configuration has occurred.) + /// + public Task StartInProcessAspNetHostAsync( + string baseUrl, + TStartup startupInstance) + where TStartup : class + => this.StartAspNetHostAsync( + baseUrl, + services => services.AddSingleton(startupInstance), + webHostBuilder => webHostBuilder.UseStartup()); + + /// + /// Stops all of the function hosts that were started via . + /// + /// A representing the asynchronous operation. + public async Task StopAllHostsAsync() { - return this.StartHostAsync(baseUrl, s => + foreach (IWebHost current in this.webHosts) { - // Shim to allow us to invoke the configuration method of the services startup class. - var webJobBuilder = new WebJobBuilder(s); - var targetStartup = new TFunctionStartup(); - targetStartup.Configure(webJobBuilder); - - // Invoke any extra container configuration. - additionalServiceConfigurationCallback?.Invoke(s); - }); + await current.StopAsync().ConfigureAwait(false); + current.Dispose(); + } } /// @@ -78,10 +143,14 @@ public Task StartHostAsync( /// /// A callback that will allow you to configure the for your service. /// + /// + /// A callback that will allow you to configure the for your service. + /// /// A representing the asynchronous operation. - public Task StartHostAsync( + private Task StartAspNetHostAsync( string baseUrl, - Action serviceConfigurationCallback) + Action serviceConfigurationCallback, + Action webHostBuilderCallback) { if (string.IsNullOrEmpty(baseUrl)) { @@ -95,7 +164,7 @@ public Task StartHostAsync( IWebHostBuilder builder = WebHost.CreateDefaultBuilder(); builder.UseUrls(baseUrl); - builder.UseStartup(); + webHostBuilderCallback(builder); builder.ConfigureServices(serviceConfigurationCallback); @@ -105,18 +174,5 @@ public Task StartHostAsync( return host.StartAsync(); } - - /// - /// Stops all of the function hosts that were started via . - /// - /// A representing the asynchronous operation. - public async Task StopAllHostsAsync() - { - foreach (IWebHost current in this.webHosts) - { - await current.StopAsync().ConfigureAwait(false); - current.Dispose(); - } - } } } diff --git a/Solutions/Menes.sln b/Solutions/Menes.sln index 6bc873efa..fe038afa9 100644 --- a/Solutions/Menes.sln +++ b/Solutions/Menes.sln @@ -13,11 +13,18 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Menes.PetStore", "Menes.Pet EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Demo", "Demo", "{9F5741F1-1FC8-42FA-A44A-CDD91AEC4758}" EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Menes.PetStore.Hosting", "Menes.PetStore.Hosting\Menes.PetStore.Hosting.csproj", "{C5D7A4FA-88FB-4E7F-BB49-2B1F19323DD0}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Menes.PetStore.Hosting.AzureFunctions", "Menes.PetStore.Hosting.AzureFunctions\Menes.PetStore.Hosting.AzureFunctions.csproj", "{C5D7A4FA-88FB-4E7F-BB49-2B1F19323DD0}" EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Menes.Specs", "Menes.Specs\Menes.Specs.csproj", "{D4255494-ACA0-4F19-8425-AE5B134A9C7F}" EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{C03F99CD-44E1-4FB5-A047-154B2F7BBC26}" + ProjectSection(SolutionItems) = preProject + .editorconfig = .editorconfig + ..\.gitignore = ..\.gitignore + ..\GitVersion.yml = ..\GitVersion.yml + stylecop.json = stylecop.json + StyleCop.ruleset = StyleCop.ruleset + EndProjectSection EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Menes.Testing.AspNetCoreSelfHosting", "Menes.Testing.AspNetCoreSelfHosting\Menes.Testing.AspNetCoreSelfHosting.csproj", "{32C5C52B-AF8C-4340-AFA3-AF1DBE032E61}" EndProject @@ -32,6 +39,8 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "DevOps", "DevOps", "{D2C950 ..\README.md = ..\README.md EndProjectSection EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Menes.PetStore.Hosting.AspNetCore.DirectPipeline", "Menes.PetStore.Hosting.AspNetCore.DirectPipeline\Menes.PetStore.Hosting.AspNetCore.DirectPipeline.csproj", "{3EE2BADC-7BDD-4D7A-ACD6-88EDAD23E425}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -70,6 +79,10 @@ Global {2C1674FB-F635-4B53-980E-ABB5C7FC6CA0}.Debug|Any CPU.Build.0 = Debug|Any CPU {2C1674FB-F635-4B53-980E-ABB5C7FC6CA0}.Release|Any CPU.ActiveCfg = Release|Any CPU {2C1674FB-F635-4B53-980E-ABB5C7FC6CA0}.Release|Any CPU.Build.0 = Release|Any CPU + {3EE2BADC-7BDD-4D7A-ACD6-88EDAD23E425}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {3EE2BADC-7BDD-4D7A-ACD6-88EDAD23E425}.Debug|Any CPU.Build.0 = Debug|Any CPU + {3EE2BADC-7BDD-4D7A-ACD6-88EDAD23E425}.Release|Any CPU.ActiveCfg = Release|Any CPU + {3EE2BADC-7BDD-4D7A-ACD6-88EDAD23E425}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -78,6 +91,7 @@ Global {587CC6F9-DAC0-42F2-8520-0B21FDCB013E} = {9F5741F1-1FC8-42FA-A44A-CDD91AEC4758} {C5D7A4FA-88FB-4E7F-BB49-2B1F19323DD0} = {9F5741F1-1FC8-42FA-A44A-CDD91AEC4758} {2C1674FB-F635-4B53-980E-ABB5C7FC6CA0} = {9F5741F1-1FC8-42FA-A44A-CDD91AEC4758} + {3EE2BADC-7BDD-4D7A-ACD6-88EDAD23E425} = {9F5741F1-1FC8-42FA-A44A-CDD91AEC4758} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {0B392B67-B21B-4AFD-BCC2-3EB2AF5E5D33}