diff --git a/Solutions/Menes.Hosting.AspNetCore/Menes/Internal/PocoActionResultOutputBuilder.cs b/Solutions/Menes.Hosting.AspNetCore/Menes/Internal/PocoActionResultOutputBuilder.cs
index a3e92230..604e933d 100644
--- a/Solutions/Menes.Hosting.AspNetCore/Menes/Internal/PocoActionResultOutputBuilder.cs
+++ b/Solutions/Menes.Hosting.AspNetCore/Menes/Internal/PocoActionResultOutputBuilder.cs
@@ -46,9 +46,13 @@ protected override IActionResult FromPoco(
}
///
- protected override bool CanConstructFrom(OpenApiResult openApiResult, OpenApiOperation operation, ILogger logger)
+ protected override bool CanConstructFrom(
+ object result,
+ OpenApiOperation operation,
+ IEnumerable converters,
+ ILogger logger)
{
- return OpenApiActionResult.CanConstructFrom(openApiResult, operation, logger);
+ return OpenApiActionResult.CanConstructFrom(result, 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
index 17560894..4a4ddfce 100644
--- a/Solutions/Menes.Hosting.AspNetCore/Menes/Internal/PocoHttpResponseOutputBuilder.cs
+++ b/Solutions/Menes.Hosting.AspNetCore/Menes/Internal/PocoHttpResponseOutputBuilder.cs
@@ -46,11 +46,12 @@ protected override IHttpResponseResult FromPoco(
///
protected override bool CanConstructFrom(
- OpenApiResult openApiResult,
+ object result,
OpenApiOperation operation,
+ IEnumerable converters,
ILogger logger)
{
- return OpenApiHttpResponseResult.CanConstructFrom(openApiResult, operation, logger);
+ return OpenApiHttpResponseResult.CanConstructFrom(result, operation, logger);
}
}
}
\ No newline at end of file
diff --git a/Solutions/Menes.Hosting.AspNetCore/Microsoft/Extensions/DependencyInjection/OpenApiHttpAspNetCoreHostingServiceCollectionExtensions.cs b/Solutions/Menes.Hosting.AspNetCore/Microsoft/Extensions/DependencyInjection/OpenApiHttpAspNetCoreHostingServiceCollectionExtensions.cs
index efb904ce..5f4dddf4 100644
--- a/Solutions/Menes.Hosting.AspNetCore/Microsoft/Extensions/DependencyInjection/OpenApiHttpAspNetCoreHostingServiceCollectionExtensions.cs
+++ b/Solutions/Menes.Hosting.AspNetCore/Microsoft/Extensions/DependencyInjection/OpenApiHttpAspNetCoreHostingServiceCollectionExtensions.cs
@@ -62,7 +62,7 @@ public static IServiceCollection AddOpenApiActionResultHosting(
}
///
- /// Adds / middleware-based hosting.
+ /// Adds middleware-based hosting.
///
/// The type of the OpenApi context.
/// The service collection to configure.
diff --git a/Solutions/Menes.Hosting.FunctionsWorker/Menes/Hosting/AzureFunctionsWorker/HttpRequestDataParameterBuilder.cs b/Solutions/Menes.Hosting.FunctionsWorker/Menes/Hosting/AzureFunctionsWorker/HttpRequestDataParameterBuilder.cs
index 8a4d3a41..78d11d1f 100644
--- a/Solutions/Menes.Hosting.FunctionsWorker/Menes/Hosting/AzureFunctionsWorker/HttpRequestDataParameterBuilder.cs
+++ b/Solutions/Menes.Hosting.FunctionsWorker/Menes/Hosting/AzureFunctionsWorker/HttpRequestDataParameterBuilder.cs
@@ -5,8 +5,10 @@
namespace Menes.Hosting.AzureFunctionsWorker;
using System.Collections.Generic;
+using System.Collections.Specialized;
using System.Diagnostics.CodeAnalysis;
using System.IO;
+using System.Web;
using Menes.Converters;
@@ -33,30 +35,42 @@ public HttpRequestDataParameterBuilder(
///
protected override (Stream? Body, string? ContentType) GetBodyAndContentType(HttpRequestData request)
{
- throw new NotImplementedException();
+ string? contentType = request.Headers.TryGetValues("Content-Type", out IEnumerable? values)
+ ? values.FirstOrDefault() : null;
+ return (request.Body, contentType);
}
///
protected override (string Path, string Method) GetPathAndMethod(HttpRequestData request)
{
- throw new NotImplementedException();
+ return (request.Url.AbsolutePath, request.Method);
}
///
protected override bool TryGetCookieValue(HttpRequestData request, string key, [NotNullWhen(true)] out string? value)
{
- throw new NotImplementedException();
+ value = request.Cookies.FirstOrDefault(c => c.Name == key)?.Value;
+ return value is not null;
}
///
protected override bool TryGetHeaderValue(HttpRequestData request, string key, [NotNullWhen(true)] out string? value)
{
- throw new NotImplementedException();
+ if (request.Headers.TryGetValues(key, out IEnumerable? values))
+ {
+ value = values.First();
+ return true;
+ }
+
+ value = null;
+ return false;
}
///
protected override bool TryGetQueryValue(HttpRequestData request, string key, [NotNullWhen(true)] out string? value)
{
- throw new NotImplementedException();
+ NameValueCollection queryStringCollection = HttpUtility.ParseQueryString(request.Url.Query);
+ value = queryStringCollection[key];
+ return value is not null;
}
}
\ No newline at end of file
diff --git a/Solutions/Menes.Hosting.FunctionsWorker/Menes/Hosting/AzureFunctionsWorker/PocoHttpResponseDataOutputBuilder.cs b/Solutions/Menes.Hosting.FunctionsWorker/Menes/Hosting/AzureFunctionsWorker/PocoHttpResponseDataOutputBuilder.cs
index 50da54f9..f16baa4e 100644
--- a/Solutions/Menes.Hosting.FunctionsWorker/Menes/Hosting/AzureFunctionsWorker/PocoHttpResponseDataOutputBuilder.cs
+++ b/Solutions/Menes.Hosting.FunctionsWorker/Menes/Hosting/AzureFunctionsWorker/PocoHttpResponseDataOutputBuilder.cs
@@ -47,10 +47,11 @@ protected override IHttpResponseDataResult FromPoco(
///
protected override bool CanConstructFrom(
- OpenApiResult openApiResult,
+ object result,
OpenApiOperation operation,
+ IEnumerable converters,
ILogger logger)
{
- return OpenApiHttpResponseDataResult.CanConstructFrom(openApiResult, operation, logger);
+ return OpenApiHttpResponseDataResult.CanConstructFrom(result, operation, logger);
}
}
\ No newline at end of file
diff --git a/Solutions/Menes.Hosting.FunctionsWorker/Microsoft/Extensions/DependencyInjection/OpenApiAzureFunctionsWorkerHostingServiceCollectionExtensions.cs b/Solutions/Menes.Hosting.FunctionsWorker/Microsoft/Extensions/DependencyInjection/OpenApiAzureFunctionsWorkerHostingServiceCollectionExtensions.cs
new file mode 100644
index 00000000..1afe1759
--- /dev/null
+++ b/Solutions/Menes.Hosting.FunctionsWorker/Microsoft/Extensions/DependencyInjection/OpenApiAzureFunctionsWorkerHostingServiceCollectionExtensions.cs
@@ -0,0 +1,47 @@
+//
+// Copyright (c) Endjin Limited. All rights reserved.
+//
+
+namespace Microsoft.Extensions.DependencyInjection;
+
+using System;
+
+using Menes;
+using Menes.Hosting.AzureFunctionsWorker;
+using Menes.Internal;
+
+using Microsoft.Azure.Functions.Worker.Http;
+
+///
+/// Extensions to register open api request hosting for and .
+///
+public static class OpenApiAzureFunctionsWorkerHostingServiceCollectionExtensions
+{
+ ///
+ /// 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 AddOpenApiAzureFunctionsWorkerHosting(
+ this IServiceCollection services,
+ Action? configureHost,
+ Action? configureEnvironment = null)
+ where TContext : class, IOpenApiContext, new()
+ {
+ services.AddSingleton, PocoHttpResponseDataOutputBuilder>();
+ services.AddSingleton, OpenApiResultHttpResponseDataOutputBuilder>();
+ services.AddSingleton, OpenApiHttpResponseDataResultBuilder>();
+
+ services.AddSingleton, OpenApiContextBuilder>();
+ services.AddSingleton, HttpRequestDataParameterBuilder>();
+
+ services.AddOpenApiHosting(
+ configureHost,
+ configureEnvironment);
+
+ return services;
+ }
+}
\ No newline at end of file
diff --git a/Solutions/Menes.Hosting/Menes/Internal/PocoOutputBuilder.cs b/Solutions/Menes.Hosting/Menes/Internal/PocoOutputBuilder.cs
index 9e8ae22b..769e7f13 100644
--- a/Solutions/Menes.Hosting/Menes/Internal/PocoOutputBuilder.cs
+++ b/Solutions/Menes.Hosting/Menes/Internal/PocoOutputBuilder.cs
@@ -22,7 +22,7 @@ internal abstract class PocoOutputBuilder : IResponseOutputBuilder
- /// Initializes a new instance of the class.
+ /// Initializes a new instance of the class.
///
/// The open API converters to use with the builder.
/// The logger for the output builder.
@@ -67,7 +67,7 @@ public TResponse BuildOutput(object result, OpenApiOperation operation)
///
public bool CanBuildOutput(object result, OpenApiOperation operation)
{
- return result is not OpenApiResult && OpenApiHttpResponseResult.CanConstructFrom(result, operation, this.logger);
+ return result is not OpenApiResult && this.CanConstructFrom(result, operation, this.converters, this.logger);
}
///
@@ -85,15 +85,17 @@ protected abstract TResponse FromPoco(
ILogger logger);
///
- /// Determines if the action result can be constructed from the provided result and operation definition.
+ /// Determines if the resposne can be constructed from the provided result and operation definition.
///
- /// The .
+ /// The POCO result.
/// The OpenAPI operation definition.
+ /// The OpenAPI converters to use.
/// A logger for the operation.
/// True if an action result can be constructed from this operation result.
protected abstract bool CanConstructFrom(
- OpenApiResult openApiResult,
+ object result,
OpenApiOperation operation,
+ IEnumerable converters,
ILogger logger);
}
}
\ No newline at end of file
diff --git a/Solutions/Menes.Specs/Bindings/MenesContainerBindings.cs b/Solutions/Menes.Specs/Bindings/MenesContainerBindings.cs
index 6b8562a5..255d68b5 100644
--- a/Solutions/Menes.Specs/Bindings/MenesContainerBindings.cs
+++ b/Solutions/Menes.Specs/Bindings/MenesContainerBindings.cs
@@ -41,11 +41,17 @@ public static void InitializeContainer(ScenarioContext scenarioContext)
serviceCollection.AddLogging(configure => configure.SetMinimumLevel(LogLevel.Debug).AddProvider(new DummyLogger()));
serviceCollection.AddOpenApiAspNetPipelineHosting(
null,
- config =>
- {
- config.DiscriminatedTypes.Add("registeredDiscriminatedType1", typeof(RegisteredDiscriminatedType1));
- config.DiscriminatedTypes.Add("registeredDiscriminatedType2", typeof(RegisteredDiscriminatedType2));
- });
+ ConfigureOpenApi);
+ serviceCollection.AddOpenApiAzureFunctionsWorkerHosting(
+ null,
+ ConfigureOpenApi);
+
+ static void ConfigureOpenApi(IOpenApiConfiguration config)
+ {
+ config.DiscriminatedTypes.Add("registeredDiscriminatedType1", typeof(RegisteredDiscriminatedType1));
+ config.DiscriminatedTypes.Add("registeredDiscriminatedType2", typeof(RegisteredDiscriminatedType2));
+ }
+
serviceCollection.AddContent(cf =>
{
cf.RegisterTransientContent();
@@ -73,7 +79,9 @@ public void Dispose()
private class Logger : ILogger, IDisposable
{
- public IDisposable BeginScope(TState state) => this;
+ public IDisposable? BeginScope(TState state)
+ where TState : notnull
+ => this;
public void Dispose()
{
diff --git a/Solutions/Menes.Specs/Features/ParameterBuilders/HttpRequestDataParameterBuilder.feature b/Solutions/Menes.Specs/Features/ParameterBuilders/HttpRequestDataParameterBuilder.feature
new file mode 100644
index 00000000..33e43401
--- /dev/null
+++ b/Solutions/Menes.Specs/Features/ParameterBuilders/HttpRequestDataParameterBuilder.feature
@@ -0,0 +1,41 @@
+@perScenarioContainer
+
+Feature: Azure Functions HttpRequestData Parameter Builder
+ In order to implement a web API that can run in Azure Functions with the isolated worker model
+ As a developer
+ I want Menes to be able to extract inputs from requests wrapped as HttpRequestData
+
+Scenario: Body
+ Given I have constructed the OpenAPI specification with a request body of type array, containing items of type 'integer'
+ When I try to parse the value '[1,2,3,4,5]' as the HttpRequestData body
+ Then the parameter body should be [1,2,3,4,5] of type System.String
+
+Scenario Outline: Cookie
+ Given I have constructed the OpenAPI specification with a cookie parameter with name 'openApiBoolean', type 'boolean', and format ''
+ When I try to parse the value '' as the cookie 'openApiBoolean' in an HttpRequestData
+ Then the parameter openApiBoolean should be of type System.Boolean
+
+ Examples:
+ | Value | ExpectedValue |
+ | true | true |
+ | false | false |
+
+Scenario Outline: Header
+ Given I have constructed the OpenAPI specification with a header parameter with name 'openApiBoolean', type 'boolean', and format ''
+ When I try to parse the value '' as the header 'openApiBoolean' in an HttpRequestData
+ Then the parameter openApiBoolean should be of type System.Boolean
+
+ Examples:
+ | Value | ExpectedValue |
+ | true | true |
+ | false | false |
+
+Scenario Outline: Query
+ Given I have constructed the OpenAPI specification with a query parameter with name 'openApiBoolean', type 'boolean', and format ''
+ When I try to parse the value '' as the query parameter 'openApiBoolean' in an HttpRequestData
+ Then the parameter openApiBoolean should be of type System.Boolean
+
+ Examples:
+ | Value | ExpectedValue |
+ | true | true |
+ | false | false |
diff --git a/Solutions/Menes.Specs/Menes.Specs.csproj b/Solutions/Menes.Specs/Menes.Specs.csproj
index df087c8c..c807162b 100644
--- a/Solutions/Menes.Specs/Menes.Specs.csproj
+++ b/Solutions/Menes.Specs/Menes.Specs.csproj
@@ -2,7 +2,7 @@
- net6.0
+ net7.0enablefalseMenes.Specs
@@ -62,32 +62,23 @@
+
+ Always
-
-
- StringOutputParsing.feature
-
- PreserveNewest
-
-
- $(UsingMicrosoftNETSdk)
- %(RelativeDir)%(Filename).feature$(DefaultLanguageSourceExtension)
-
-
\ No newline at end of file
diff --git a/Solutions/Menes.Specs/Steps/HttpRequestDataSteps.cs b/Solutions/Menes.Specs/Steps/HttpRequestDataSteps.cs
new file mode 100644
index 00000000..66a074c7
--- /dev/null
+++ b/Solutions/Menes.Specs/Steps/HttpRequestDataSteps.cs
@@ -0,0 +1,155 @@
+//
+// Copyright (c) Endjin Limited. All rights reserved.
+//
+
+namespace Menes.Specs.Steps;
+
+using System;
+using System.Collections.Generic;
+using System.IO;
+using System.Security.Claims;
+using System.Text;
+using System.Threading.Tasks;
+
+using Corvus.Testing.SpecFlow;
+
+using Microsoft.Azure.Functions.Worker;
+using Microsoft.Azure.Functions.Worker.Http;
+using Microsoft.Extensions.DependencyInjection;
+
+using TechTalk.SpecFlow;
+
+[Binding]
+public class HttpRequestDataSteps
+{
+ private readonly IServiceProvider serviceProvider;
+ private readonly FakeFunctionContext ctx;
+ private readonly OpenApiParameterParsingSteps openApiSteps;
+ private FakeHttpRequestData request;
+
+ public HttpRequestDataSteps(
+ ScenarioContext scenarioContext,
+ OpenApiParameterParsingSteps openApiSteps)
+ {
+ this.serviceProvider = ContainerBindings.GetServiceProvider(scenarioContext);
+ this.ctx = new();
+ this.request = new FakeHttpRequestData(this.ctx, new Uri("https://example.com/pets"), "GET");
+ this.openApiSteps = openApiSteps;
+ }
+
+ [When("I try to parse the value '([^']*)' as the HttpRequestData body")]
+ public async Task WhenITryToParseTheValueAsTheHttpRequestDataBody(string body)
+ {
+ IOpenApiParameterBuilder builder = this.serviceProvider.GetRequiredService>();
+ this.openApiSteps.Matcher.FindOperationPathTemplate("/pets", "POST", out OpenApiOperationPathTemplate? operationPathTemplate);
+
+ this.request.Headers.Add("Content-Type", "application/json");
+ using (StreamWriter w = new(this.request.Body, Encoding.UTF8, leaveOpen: true))
+ {
+ w.Write(body);
+ }
+
+ this.request.Body.Position = 0;
+
+ this.openApiSteps.Parameters = await builder.BuildParametersAsync(this.request, operationPathTemplate!).ConfigureAwait(false);
+ }
+
+ [When("I try to parse the value '([^']*)' as the cookie '([^']*)' in an HttpRequestData")]
+ public async Task WhenITryToParseTheValueAsTheCookieInAnHttpRequestData(string cookieValue, string cookieName)
+ {
+ IOpenApiParameterBuilder builder = this.serviceProvider.GetRequiredService>();
+ this.openApiSteps.Matcher.FindOperationPathTemplate("/pets", "GET", out OpenApiOperationPathTemplate? operationPathTemplate);
+
+ this.request.WritableCookies.Add(new HttpCookie(cookieName, cookieValue));
+
+ this.openApiSteps.Parameters = await builder.BuildParametersAsync(this.request, operationPathTemplate!).ConfigureAwait(false);
+ }
+
+ [When("I try to parse the value '([^']*)' as the header '([^']*)' in an HttpRequestData")]
+ public async Task WhenITryToParseTheValueAsTheHeaderInAnHttpRequestData(string headerValue, string headerName)
+ {
+ IOpenApiParameterBuilder builder = this.serviceProvider.GetRequiredService>();
+ this.openApiSteps.Matcher.FindOperationPathTemplate("/pets", "GET", out OpenApiOperationPathTemplate? operationPathTemplate);
+
+ this.request.Headers.Add(headerName, headerValue);
+
+ this.openApiSteps.Parameters = await builder.BuildParametersAsync(this.request, operationPathTemplate!).ConfigureAwait(false);
+ }
+
+ [When("I try to parse the value '([^']*)' as the query parameter '([^']*)' in an HttpRequestData")]
+ public async Task WhenITryToParseTheValueAsTheQueryParameterInAnHttpRequestData(string headerValue, string headerName)
+ {
+ IOpenApiParameterBuilder builder = this.serviceProvider.GetRequiredService>();
+ this.openApiSteps.Matcher.FindOperationPathTemplate("/pets", "GET", out OpenApiOperationPathTemplate? operationPathTemplate);
+
+ this.request = new FakeHttpRequestData(
+ this.ctx,
+ new Uri($"https://example.com/pets?{headerName}={headerValue}"),
+ "GET");
+
+ this.openApiSteps.Parameters = await builder.BuildParametersAsync(this.request, operationPathTemplate!).ConfigureAwait(false);
+ }
+
+ private class FakeHttpRequestData : HttpRequestData
+ {
+ public FakeHttpRequestData(
+ FunctionContext functionContext,
+ Uri url,
+ string method)
+ : base(functionContext)
+ {
+ this.Url = url;
+ this.Method = method;
+ }
+
+ public override Stream Body { get; } = new MemoryStream();
+
+ public override HttpHeadersCollection Headers { get; } = new();
+
+ public override IReadOnlyCollection Cookies => this.WritableCookies;
+
+ public List WritableCookies { get; } = new List();
+
+ public override Uri Url { get; }
+
+ public override IEnumerable Identities { get; } = new List();
+
+ public override string Method { get; }
+
+ public override HttpResponseData CreateResponse()
+ {
+ throw new NotImplementedException();
+ }
+ }
+
+ // Even though the test doesn't really do anything with this, it has to exist for us
+ // to be able to construct our HttpRequestData implementation.
+ private class FakeFunctionContext : FunctionContext
+ {
+ public override string InvocationId => throw new NotImplementedException();
+
+ public override string FunctionId => throw new NotImplementedException();
+
+ public override TraceContext TraceContext => throw new NotImplementedException();
+
+ public override BindingContext BindingContext => throw new NotImplementedException();
+
+ public override RetryContext RetryContext => throw new NotImplementedException();
+
+ public override IServiceProvider InstanceServices
+ {
+ get => throw new NotImplementedException();
+ set => throw new NotImplementedException();
+ }
+
+ public override FunctionDefinition FunctionDefinition => throw new NotImplementedException();
+
+ public override IDictionary