From 46787ed01b15d2db5552a3564d4373a442746d42 Mon Sep 17 00:00:00 2001 From: stijnmoreels <9039753+stijnmoreels@users.noreply.github.com> Date: Fri, 3 Dec 2021 10:22:15 +0100 Subject: [PATCH 1/7] chore: move az func http trigger to system.text.json --- .../HttpBasedAzureFunction.cs | 400 +++++++++--------- .../Model/Order.cs | 40 +- .../OrderFunction.cs | 154 +++---- 3 files changed, 287 insertions(+), 307 deletions(-) diff --git a/src/Arcus.Templates.AzureFunctions.Http/HttpBasedAzureFunction.cs b/src/Arcus.Templates.AzureFunctions.Http/HttpBasedAzureFunction.cs index 8e3bcfbf..caa0c187 100644 --- a/src/Arcus.Templates.AzureFunctions.Http/HttpBasedAzureFunction.cs +++ b/src/Arcus.Templates.AzureFunctions.Http/HttpBasedAzureFunction.cs @@ -1,209 +1,191 @@ -using System; -using System.Collections.Generic; -using System.ComponentModel.DataAnnotations; -using System.IO; -using System.Linq; -using System.Text.Json; -using System.Text.Json.Serialization; -using System.Threading.Tasks; -using Arcus.WebApi.Logging.Correlation; -using GuardNet; -using Microsoft.AspNetCore.Http; -using Microsoft.AspNetCore.Mvc; -using Microsoft.AspNetCore.Mvc.Formatters; -using Microsoft.Extensions.Logging; -using Microsoft.Extensions.Logging.Abstractions; -using Microsoft.Net.Http.Headers; -using Newtonsoft.Json; -using Newtonsoft.Json.Converters; -using Newtonsoft.Json.Linq; -using JsonSerializer = Newtonsoft.Json.JsonSerializer; - -namespace Arcus.Templates.AzureFunctions.Http -{ - /// - /// Represents the base HTTP web API functionality for an Azure Function implementation. - /// - public abstract class HttpBasedAzureFunction - { - private readonly HttpCorrelation _httpCorrelation; - - /// - /// Initializes a new instance of the class. - /// - /// The correlation service to provide information of related requests. - /// The logger instance to write diagnostic messages throughout the execution of the HTTP trigger. - /// Thrown when the is null - protected HttpBasedAzureFunction(HttpCorrelation httpCorrelation, ILogger logger) - { - Guard.NotNull(httpCorrelation, nameof(httpCorrelation), "Requires an HTTP correlation instance"); - _httpCorrelation = httpCorrelation; - - Logger = logger ?? NullLogger.Instance; - - var jsonSettings = new JsonSerializerSettings - { - NullValueHandling = NullValueHandling.Ignore, - TypeNameHandling = TypeNameHandling.None - }; - jsonSettings.Converters.Add(new StringEnumConverter()); - JsonSerializer = JsonSerializer.Create(jsonSettings); - - var jsonOptions = new JsonSerializerOptions - { - IgnoreNullValues = true, - PropertyNamingPolicy = JsonNamingPolicy.CamelCase - }; - jsonOptions.Converters.Add(new JsonStringEnumConverter()); - OutputFormatters = new FormatterCollection {new SystemTextJsonOutputFormatter(jsonOptions)}; - } - - /// - /// Gets the serializer that's being used when incoming requests are being serialized or deserialized into JSON. - /// - protected JsonSerializer JsonSerializer { get; } - - /// - /// Gets the set of formatters that's being used when an outgoing response is being sent back to the sender. - /// - protected FormatterCollection OutputFormatters { get; } - - /// - /// Gets the logger instance used throughout this Azure Function. - /// - protected ILogger Logger { get; } - - /// - /// Correlate the current HTTP request according to the previously configured ; - /// returning an when the correlation failed. - /// - /// The failure message that describes why the correlation of the HTTP request wasn't successful. - /// - /// [true] when the HTTP request was successfully correlated and the HTTP response was altered accordingly; - /// [false] there was a problem with the correlation, describing the failure in the . - /// - protected bool TryHttpCorrelate(out string errorMessage) - { - return _httpCorrelation.TryHttpCorrelate(out errorMessage); - } - - /// - /// Reads the body of the incoming request as JSON to deserialize it into a given type. - /// - /// The type of the request's body that's being deserialized. - /// The incoming request which body will be deserialized. - /// The deserialized request body into the model. - /// Thrown when the is null - /// Thrown when the deserialization of the request body to a JSON representation fails. - /// Thrown when the deserialized request body didn't correspond to the required validation rules. - protected async Task GetJsonBodyAsync(HttpRequest request) - { - using (var streamReader = new StreamReader(request.Body)) - using (var jsonReader = new JsonTextReader(streamReader)) - { - JToken parsedOrder = await JToken.LoadAsync(jsonReader); - var model = parsedOrder.ToObject(JsonSerializer); - Validator.ValidateObject(model, new ValidationContext(model), validateAllProperties: true); - - return model; - } - } - - /// - /// Determines whether the incoming 's body can be considered as JSON or not. - /// - /// The incoming HTTP request to verify. - /// - /// [true] if the 's body is considered JSON; [false] otherwise. - /// - /// Thrown when the is null. - protected bool IsJson(HttpRequest request) - { - Guard.NotNull(request, nameof(request), "Requires a HTTP request to verify if the request's body can be considered as JSON"); - return request.ContentType == "application/json"; - } - - /// - /// Determines if the incoming accepts a JSON response. - /// - /// The incoming HTTP request to verify. - /// - /// [true] if the incoming accepts a JSON response; [false] otherwise. - /// - /// Thrown when the is null. - protected bool AcceptsJson(HttpRequest request) - { - Guard.NotNull(request, nameof(request), "Requires a HTTP request to verify if the request accepts a JSON response"); - return GetAcceptMediaTypes(request).Any(mediaType => mediaType == "application/json"); - } - - /// - /// Gets all the media types in the incoming 's 'Accept' header. - /// - /// The incoming request to extract the media types from.. - /// Thrown when the is null. - protected IEnumerable GetAcceptMediaTypes(HttpRequest request) - { - Guard.NotNull(request, nameof(request), "Requires a HTTP request to retrieve the media types from the Accept header"); - IList acceptHeaders = request.GetTypedHeaders().Accept ?? new List(); - return acceptHeaders.Select(header => header.MediaType.ToString()); - } - - /// - /// Creates an instance with a JSON response . - /// - /// The response JSON body. - /// The HTTP status code of the response; default 200 OK. - /// Thrown when the is null. - protected IActionResult Json(object body, int statusCode = StatusCodes.Status200OK) - { - Guard.NotNull(body, nameof(body), "Requires a JSON body for the response body"); - return new ObjectResult(body) - { - StatusCode = statusCode, - Formatters = OutputFormatters, - ContentTypes = { "application/json" } - }; - } - - /// - /// Creates an instance for a '415 Unsupported Media Type' failure with an accompanied . - /// - /// The error message to describe the failure. - /// Throw when the is blank. - protected IActionResult UnsupportedMediaType(string errorMessage) - { - Guard.NotNullOrWhitespace(errorMessage, nameof(errorMessage), "Requires a non-blank error message to accompany the 415 Unsupported Media Type failure"); - return new ObjectResult(errorMessage) - { - StatusCode = StatusCodes.Status415UnsupportedMediaType - }; - } - - /// - /// Creates an instance for a '400 Bad Request' failure with an accompanied model to describe the failure. - /// - /// The error model to describe the failure. - /// Thrown when the is null. - protected IActionResult BadRequest(object error) - { - Guard.NotNull(error, nameof(error), "Requires an error model to describe the 400 Bad Request failure"); - return new BadRequestObjectResult(error); - } - - /// - /// Creates an instance for a '500 Internal Server Error' failure with an accompanied . - /// - /// The error message to describe the failure. - /// Thrown when the is blank. - protected IActionResult InternalServerError(string errorMessage) - { - Guard.NotNullOrWhitespace(errorMessage, nameof(errorMessage), "Requires an non-blank error message to accompany the 500 Internal Server Error failure"); - return new ObjectResult(errorMessage) - { - StatusCode = StatusCodes.Status500InternalServerError, - ContentTypes = {"text/plain"} - }; - } - } -} +using System; +using System.Collections.Generic; +using System.ComponentModel.DataAnnotations; +using System.IO; +using System.Linq; +using System.Text.Json; +using System.Text.Json.Serialization; +using System.Threading.Tasks; +using Arcus.WebApi.Logging.Correlation; +using GuardNet; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Mvc.Formatters; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Logging.Abstractions; +using Microsoft.Net.Http.Headers; + +namespace Arcus.Templates.AzureFunctions.Http +{ + /// + /// Represents the base HTTP web API functionality for an Azure Function implementation. + /// + public abstract class HttpBasedAzureFunction + { + private readonly HttpCorrelation _httpCorrelation; + + /// + /// Initializes a new instance of the class. + /// + /// The correlation service to provide information of related requests. + /// The logger instance to write diagnostic messages throughout the execution of the HTTP trigger. + /// Thrown when the is null + protected HttpBasedAzureFunction(HttpCorrelation httpCorrelation, ILogger logger) + { + Guard.NotNull(httpCorrelation, nameof(httpCorrelation), "Requires an HTTP correlation instance"); + _httpCorrelation = httpCorrelation; + + Logger = logger ?? NullLogger.Instance; + + var options = new JsonSerializerOptions(); + options.IgnoreNullValues = true; + options.PropertyNamingPolicy = JsonNamingPolicy.CamelCase; + options.Converters.Add(new JsonStringEnumConverter()); + JsonOptions = options; + OutputFormatters = new FormatterCollection { new SystemTextJsonOutputFormatter(JsonOptions) }; + } + + /// + /// Gets the serializer that's being used when incoming requests are being serialized or deserialized into JSON. + /// + protected JsonSerializerOptions JsonOptions { get; } + + /// + /// Gets the set of formatters that's being used when an outgoing response is being sent back to the sender. + /// + protected FormatterCollection OutputFormatters { get; } + + /// + /// Gets the logger instance used throughout this Azure Function. + /// + protected ILogger Logger { get; } + + /// + /// Correlate the current HTTP request according to the previously configured ; + /// returning an when the correlation failed. + /// + /// The failure message that describes why the correlation of the HTTP request wasn't successful. + /// + /// [true] when the HTTP request was successfully correlated and the HTTP response was altered accordingly; + /// [false] there was a problem with the correlation, describing the failure in the . + /// + protected bool TryHttpCorrelate(out string errorMessage) + { + return _httpCorrelation.TryHttpCorrelate(out errorMessage); + } + + /// + /// Reads the body of the incoming request as JSON to deserialize it into a given type. + /// + /// The type of the request's body that's being deserialized. + /// The incoming request which body will be deserialized. + /// The deserialized request body into the model. + /// Thrown when the is null + /// Thrown when the deserialization of the request body to a JSON representation fails. + /// Thrown when the deserialized request body didn't correspond to the required validation rules. + protected async Task GetJsonBodyAsync(HttpRequest request) + { + TMessage message = await JsonSerializer.DeserializeAsync(request.Body, JsonOptions); + Validator.ValidateObject(message, new ValidationContext(message), validateAllProperties: true); + + return message; + } + + /// + /// Determines whether the incoming 's body can be considered as JSON or not. + /// + /// The incoming HTTP request to verify. + /// + /// [true] if the 's body is considered JSON; [false] otherwise. + /// + /// Thrown when the is null. + protected bool IsJson(HttpRequest request) + { + Guard.NotNull(request, nameof(request), "Requires a HTTP request to verify if the request's body can be considered as JSON"); + return request.ContentType == "application/json"; + } + + /// + /// Determines if the incoming accepts a JSON response. + /// + /// The incoming HTTP request to verify. + /// + /// [true] if the incoming accepts a JSON response; [false] otherwise. + /// + /// Thrown when the is null. + protected bool AcceptsJson(HttpRequest request) + { + Guard.NotNull(request, nameof(request), "Requires a HTTP request to verify if the request accepts a JSON response"); + return GetAcceptMediaTypes(request).Any(mediaType => mediaType == "application/json"); + } + + /// + /// Gets all the media types in the incoming 's 'Accept' header. + /// + /// The incoming request to extract the media types from.. + /// Thrown when the is null. + protected IEnumerable GetAcceptMediaTypes(HttpRequest request) + { + Guard.NotNull(request, nameof(request), "Requires a HTTP request to retrieve the media types from the Accept header"); + IList acceptHeaders = request.GetTypedHeaders().Accept ?? new List(); + return acceptHeaders.Select(header => header.MediaType.ToString()); + } + + /// + /// Creates an instance with a JSON response . + /// + /// The response JSON body. + /// The HTTP status code of the response; default 200 OK. + /// Thrown when the is null. + protected IActionResult Json(object body, int statusCode = StatusCodes.Status200OK) + { + Guard.NotNull(body, nameof(body), "Requires a JSON body for the response body"); + return new ObjectResult(body) + { + StatusCode = statusCode, + Formatters = OutputFormatters, + ContentTypes = { "application/json" } + }; + } + + /// + /// Creates an instance for a '415 Unsupported Media Type' failure with an accompanied . + /// + /// The error message to describe the failure. + /// Throw when the is blank. + protected IActionResult UnsupportedMediaType(string errorMessage) + { + Guard.NotNullOrWhitespace(errorMessage, nameof(errorMessage), "Requires a non-blank error message to accompany the 415 Unsupported Media Type failure"); + return new ObjectResult(errorMessage) + { + StatusCode = StatusCodes.Status415UnsupportedMediaType + }; + } + + /// + /// Creates an instance for a '400 Bad Request' failure with an accompanied model to describe the failure. + /// + /// The error model to describe the failure. + /// Thrown when the is null. + protected IActionResult BadRequest(object error) + { + Guard.NotNull(error, nameof(error), "Requires an error model to describe the 400 Bad Request failure"); + return new BadRequestObjectResult(error); + } + + /// + /// Creates an instance for a '500 Internal Server Error' failure with an accompanied . + /// + /// The error message to describe the failure. + /// Thrown when the is blank. + protected IActionResult InternalServerError(string errorMessage) + { + Guard.NotNullOrWhitespace(errorMessage, nameof(errorMessage), "Requires an non-blank error message to accompany the 500 Internal Server Error failure"); + return new ObjectResult(errorMessage) + { + StatusCode = StatusCodes.Status500InternalServerError, + ContentTypes = { "text/plain" } + }; + } + } +} diff --git a/src/Arcus.Templates.AzureFunctions.Http/Model/Order.cs b/src/Arcus.Templates.AzureFunctions.Http/Model/Order.cs index a5c38797..da297042 100644 --- a/src/Arcus.Templates.AzureFunctions.Http/Model/Order.cs +++ b/src/Arcus.Templates.AzureFunctions.Http/Model/Order.cs @@ -1,21 +1,19 @@ -using System; -using System.Collections.Generic; -using System.ComponentModel.DataAnnotations; -using System.Text; - -namespace Arcus.Templates.AzureFunctions.Http.Model -{ - public class Order - { - [Required] - public string Id { get; set; } - - [Required] - [MaxLength(100)] - public string ArticleNumber { get; set; } - - [Required] - [DataType(DataType.DateTime)] - public DateTimeOffset Scheduled { get; set; } - } -} +using System; +using System.ComponentModel.DataAnnotations; + +namespace Arcus.Templates.AzureFunctions.Http.Model +{ + public class Order + { + [Required] + public string Id { get; set; } + + [Required] + [MaxLength(100)] + public string ArticleNumber { get; set; } + + [Required] + [DataType(DataType.DateTime)] + public DateTimeOffset Scheduled { get; set; } + } +} diff --git a/src/Arcus.Templates.AzureFunctions.Http/OrderFunction.cs b/src/Arcus.Templates.AzureFunctions.Http/OrderFunction.cs index b70874bc..31fc8684 100644 --- a/src/Arcus.Templates.AzureFunctions.Http/OrderFunction.cs +++ b/src/Arcus.Templates.AzureFunctions.Http/OrderFunction.cs @@ -1,77 +1,77 @@ -using System; -using System.ComponentModel.DataAnnotations; -using System.Threading.Tasks; -using Arcus.Security.Core; -using Arcus.WebApi.Logging.Correlation; -using GuardNet; -using Microsoft.AspNetCore.Mvc; -using Microsoft.Azure.WebJobs; -using Microsoft.Azure.WebJobs.Extensions.Http; -using Microsoft.AspNetCore.Http; -using Microsoft.Extensions.Logging; -using Newtonsoft.Json; -using Arcus.Templates.AzureFunctions.Http.Model; - -namespace Arcus.Templates.AzureFunctions.Http -{ - /// - /// Represents the root endpoint of the Azure Function. - /// - public class OrderFunction : HttpBasedAzureFunction - { - private readonly ISecretProvider _secretProvider; - - /// - /// Initializes a new instance of the class. - /// - /// The instance that provides secrets to the HTTP trigger. - /// The instance to handle the HTTP request correlation. - /// The logger instance to write diagnostic trace messages while handling the HTTP request. - /// Thrown when the or is null. - public OrderFunction(ISecretProvider secretProvider, HttpCorrelation httpCorrelation, ILogger logger) : base(httpCorrelation, logger) - { - Guard.NotNull(secretProvider, nameof(secretProvider), "Requires a secret provider instance"); - _secretProvider = secretProvider; - } - - [FunctionName("order")] - public async Task Run( - [HttpTrigger(AuthorizationLevel.Function, "post", Route = "v1/order")] HttpRequest request) - { - try - { - Logger.LogInformation("C# HTTP trigger 'order' function processed a request"); - if (IsJson(request) == false || AcceptsJson(request) == false) - { - string accept = String.Join(", ", GetAcceptMediaTypes(request)); - Logger.LogError("Could not process current request because the request body is not JSON and/or could not accept JSON as response (Content-Type: {ContentType}, Accept: {Accept})", request.ContentType, accept); - - return UnsupportedMediaType("Could not process current request because the request body is not JSON and/or could not accept JSON as response"); - } - - if (TryHttpCorrelate(out string errorMessage) == false) - { - return BadRequest(errorMessage); - } - - var order = await GetJsonBodyAsync(request); - return Json(order); - } - catch (JsonException exception) - { - Logger.LogError(exception, exception.Message); - return BadRequest("Could not process the current request due to an JSON deserialization failure"); - } - catch (ValidationException exception) - { - Logger.LogError(exception, exception.Message); - return BadRequest(exception.ValidationResult); - } - catch (Exception exception) - { - Logger.LogCritical(exception, exception.Message); - return InternalServerError("Could not process the current request due to an unexpected exception"); - } - } - } -} +using System; +using System.ComponentModel.DataAnnotations; +using System.Threading.Tasks; +using Arcus.Security.Core; +using Arcus.WebApi.Logging.Correlation; +using GuardNet; +using Microsoft.AspNetCore.Mvc; +using Microsoft.Azure.WebJobs; +using Microsoft.Azure.WebJobs.Extensions.Http; +using Microsoft.AspNetCore.Http; +using Microsoft.Extensions.Logging; +using Arcus.Templates.AzureFunctions.Http.Model; +using System.Text.Json; + +namespace Arcus.Templates.AzureFunctions.Http +{ + /// + /// Represents the root endpoint of the Azure Function. + /// + public class OrderFunction : HttpBasedAzureFunction + { + private readonly ISecretProvider _secretProvider; + + /// + /// Initializes a new instance of the class. + /// + /// The instance that provides secrets to the HTTP trigger. + /// The instance to handle the HTTP request correlation. + /// The logger instance to write diagnostic trace messages while handling the HTTP request. + /// Thrown when the or is null. + public OrderFunction(ISecretProvider secretProvider, HttpCorrelation httpCorrelation, ILogger logger) : base(httpCorrelation, logger) + { + Guard.NotNull(secretProvider, nameof(secretProvider), "Requires a secret provider instance"); + _secretProvider = secretProvider; + } + + [FunctionName("order")] + public async Task Run( + [HttpTrigger(AuthorizationLevel.Function, "post", Route = "v1/order")] HttpRequest request) + { + try + { + Logger.LogInformation("C# HTTP trigger 'order' function processed a request"); + if (IsJson(request) == false || AcceptsJson(request) == false) + { + string accept = String.Join(", ", GetAcceptMediaTypes(request)); + Logger.LogError("Could not process current request because the request body is not JSON and/or could not accept JSON as response (Content-Type: {ContentType}, Accept: {Accept})", request.ContentType, accept); + + return UnsupportedMediaType("Could not process current request because the request body is not JSON and/or could not accept JSON as response"); + } + + if (TryHttpCorrelate(out string errorMessage) == false) + { + return BadRequest(errorMessage); + } + + var order = await GetJsonBodyAsync(request); + return Json(order); + } + catch (JsonException exception) + { + Logger.LogError(exception, exception.Message); + return BadRequest("Could not process the current request due to an JSON deserialization failure"); + } + catch (ValidationException exception) + { + Logger.LogError(exception, exception.Message); + return BadRequest(exception.ValidationResult); + } + catch (Exception exception) + { + Logger.LogCritical(exception, exception.Message); + return InternalServerError("Could not process the current request due to an unexpected exception"); + } + } + } +} From 0625545cd02a4cafc22c3fadea0aea0d3faa0280 Mon Sep 17 00:00:00 2001 From: stijnmoreels <9039753+stijnmoreels@users.noreply.github.com> Date: Fri, 3 Dec 2021 10:23:04 +0100 Subject: [PATCH 2/7] pr-sug: update with more correct xml comments --- .../HttpBasedAzureFunction.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Arcus.Templates.AzureFunctions.Http/HttpBasedAzureFunction.cs b/src/Arcus.Templates.AzureFunctions.Http/HttpBasedAzureFunction.cs index caa0c187..a8e6694a 100644 --- a/src/Arcus.Templates.AzureFunctions.Http/HttpBasedAzureFunction.cs +++ b/src/Arcus.Templates.AzureFunctions.Http/HttpBasedAzureFunction.cs @@ -46,7 +46,7 @@ protected HttpBasedAzureFunction(HttpCorrelation httpCorrelation, ILogger logger } /// - /// Gets the serializer that's being used when incoming requests are being serialized or deserialized into JSON. + /// Gets the serializer options that's being used when incoming requests are being serialized or deserialized into JSON. /// protected JsonSerializerOptions JsonOptions { get; } From 809471117a76cfc6d6ca81c7d3ae07ef66b30bfc Mon Sep 17 00:00:00 2001 From: stijnmoreels <9039753+stijnmoreels@users.noreply.github.com> Date: Fri, 3 Dec 2021 10:27:14 +0100 Subject: [PATCH 3/7] pr-fix: complete merge --- .../OrderFunction.cs | 180 +++++++++--------- 1 file changed, 90 insertions(+), 90 deletions(-) diff --git a/src/Arcus.Templates.AzureFunctions.Http/OrderFunction.cs b/src/Arcus.Templates.AzureFunctions.Http/OrderFunction.cs index 355f26b6..ce9dd193 100644 --- a/src/Arcus.Templates.AzureFunctions.Http/OrderFunction.cs +++ b/src/Arcus.Templates.AzureFunctions.Http/OrderFunction.cs @@ -1,91 +1,91 @@ -using System; -using System.ComponentModel.DataAnnotations; -using System.Net; -using System.Text.Json; -using System.Threading.Tasks; -using Arcus.Security.Core; -using Arcus.WebApi.Logging.Correlation; -using Arcus.Templates.AzureFunctions.Http.Model; -using GuardNet; -using Microsoft.AspNetCore.Mvc; -using Microsoft.Azure.WebJobs; -using Microsoft.Azure.WebJobs.Extensions.Http; -using Microsoft.AspNetCore.Http; +using System; +using System.ComponentModel.DataAnnotations; +using System.Net; +using System.Text.Json; +using System.Threading.Tasks; +using Arcus.Security.Core; +using Arcus.WebApi.Logging.Correlation; +using GuardNet; +using Microsoft.AspNetCore.Mvc; +using Microsoft.Azure.WebJobs; +using Microsoft.Azure.WebJobs.Extensions.Http; +using Microsoft.AspNetCore.Http; using Microsoft.Extensions.Logging; -using Microsoft.Azure.WebJobs.Extensions.OpenApi.Core.Attributes; -using Microsoft.Azure.WebJobs.Extensions.OpenApi.Core.Enums; -using Microsoft.OpenApi.Models; - -namespace Arcus.Templates.AzureFunctions.Http -{ - /// - /// Represents the root endpoint of the Azure Function. - /// - public class OrderFunction : HttpBasedAzureFunction - { - private readonly ISecretProvider _secretProvider; - - /// - /// Initializes a new instance of the class. - /// - /// The instance that provides secrets to the HTTP trigger. - /// The instance to handle the HTTP request correlation. - /// The logger instance to write diagnostic trace messages while handling the HTTP request. - /// Thrown when the or is null. - public OrderFunction(ISecretProvider secretProvider, HttpCorrelation httpCorrelation, ILogger logger) : base(httpCorrelation, logger) - { - Guard.NotNull(secretProvider, nameof(secretProvider), "Requires a secret provider instance"); - _secretProvider = secretProvider; - } - - [FunctionName("order")] -#if (ExcludeOpenApi == false) - [OpenApiOperation("Order_Get", tags: new[] { "order" }, Summary = "Gets the order", Description = "Gets the order from the request", Visibility = OpenApiVisibilityType.Important)] - [OpenApiSecurity("function_key", SecuritySchemeType.ApiKey, Name = "code", In = OpenApiSecurityLocationType.Header)] - [OpenApiRequestBody("application/json", typeof(Order), Description = "The to-be-processed order")] - [OpenApiParameter("X-Transaction-Id", In = ParameterLocation.Header, Type = typeof(string), Required = false, Summary = "The correlation transaction ID", Description = "The correlation transaction ID is used to correlate multiple operation calls")] - [OpenApiResponseWithBody(HttpStatusCode.OK, "application/json", typeof(Order), Summary = "The processed order", Description = "The processed order result", CustomHeaderType = typeof(HttpCorrelationOpenApiResponseHeaders))] - [OpenApiResponseWithBody(HttpStatusCode.UnsupportedMediaType, "text/plain", typeof(string), Summary = "The faulted response for non-JSON requests", Description = "The faulted response (415) when the request doesn't accept JSON")] - [OpenApiResponseWithBody(HttpStatusCode.BadRequest, "text/plain", typeof(string), Summary = "The faulted response for invalid correlation requests", Description = "The faulted response (400) when the request doesn't correlate correctly")] - [OpenApiResponseWithBody(HttpStatusCode.InternalServerError, "text/plain", typeof(string), Summary = "The faulted response for general failures", Description = "The faulted response (500) for any general and unexpected server-related failure")] -#endif - public async Task Run( - [HttpTrigger(AuthorizationLevel.Function, "post", Route = "v1/order")] HttpRequest request) - { - try - { - Logger.LogInformation("C# HTTP trigger 'order' function processed a request"); - if (IsJson(request) == false || AcceptsJson(request) == false) - { - string accept = String.Join(", ", GetAcceptMediaTypes(request)); - Logger.LogError("Could not process current request because the request body is not JSON and/or could not accept JSON as response (Content-Type: {ContentType}, Accept: {Accept})", request.ContentType, accept); - - return UnsupportedMediaType("Could not process current request because the request body is not JSON and/or could not accept JSON as response"); - } - - if (TryHttpCorrelate(out string errorMessage) == false) - { - return BadRequest(errorMessage); - } - - var order = await GetJsonBodyAsync(request); - return Json(order); - } - catch (JsonException exception) - { - Logger.LogError(exception, exception.Message); - return BadRequest("Could not process the current request due to an JSON deserialization failure"); - } - catch (ValidationException exception) - { - Logger.LogError(exception, exception.Message); - return BadRequest(exception.ValidationResult); - } - catch (Exception exception) - { - Logger.LogCritical(exception, exception.Message); - return InternalServerError("Could not process the current request due to an unexpected exception"); - } - } - } -} +using Arcus.Templates.AzureFunctions.Http.Model; +using Microsoft.Azure.WebJobs.Extensions.OpenApi.Core.Attributes; +using Microsoft.Azure.WebJobs.Extensions.OpenApi.Core.Enums; +using Microsoft.OpenApi.Models; + +namespace Arcus.Templates.AzureFunctions.Http +{ + /// + /// Represents the root endpoint of the Azure Function. + /// + public class OrderFunction : HttpBasedAzureFunction + { + private readonly ISecretProvider _secretProvider; + + /// + /// Initializes a new instance of the class. + /// + /// The instance that provides secrets to the HTTP trigger. + /// The instance to handle the HTTP request correlation. + /// The logger instance to write diagnostic trace messages while handling the HTTP request. + /// Thrown when the or is null. + public OrderFunction(ISecretProvider secretProvider, HttpCorrelation httpCorrelation, ILogger logger) : base(httpCorrelation, logger) + { + Guard.NotNull(secretProvider, nameof(secretProvider), "Requires a secret provider instance"); + _secretProvider = secretProvider; + } + + [FunctionName("order")] +#if (ExcludeOpenApi == false) + [OpenApiOperation("Order_Get", tags: new[] { "order" }, Summary = "Gets the order", Description = "Gets the order from the request", Visibility = OpenApiVisibilityType.Important)] + [OpenApiSecurity("function_key", SecuritySchemeType.ApiKey, Name = "code", In = OpenApiSecurityLocationType.Header)] + [OpenApiRequestBody("application/json", typeof(Order), Description = "The to-be-processed order")] + [OpenApiParameter("X-Transaction-Id", In = ParameterLocation.Header, Type = typeof(string), Required = false, Summary = "The correlation transaction ID", Description = "The correlation transaction ID is used to correlate multiple operation calls")] + [OpenApiResponseWithBody(HttpStatusCode.OK, "application/json", typeof(Order), Summary = "The processed order", Description = "The processed order result", CustomHeaderType = typeof(HttpCorrelationOpenApiResponseHeaders))] + [OpenApiResponseWithBody(HttpStatusCode.UnsupportedMediaType, "text/plain", typeof(string), Summary = "The faulted response for non-JSON requests", Description = "The faulted response (415) when the request doesn't accept JSON")] + [OpenApiResponseWithBody(HttpStatusCode.BadRequest, "text/plain", typeof(string), Summary = "The faulted response for invalid correlation requests", Description = "The faulted response (400) when the request doesn't correlate correctly")] + [OpenApiResponseWithBody(HttpStatusCode.InternalServerError, "text/plain", typeof(string), Summary = "The faulted response for general failures", Description = "The faulted response (500) for any general and unexpected server-related failure")] +#endif + public async Task Run( + [HttpTrigger(AuthorizationLevel.Function, "post", Route = "v1/order")] HttpRequest request) + { + try + { + Logger.LogInformation("C# HTTP trigger 'order' function processed a request"); + if (IsJson(request) == false || AcceptsJson(request) == false) + { + string accept = String.Join(", ", GetAcceptMediaTypes(request)); + Logger.LogError("Could not process current request because the request body is not JSON and/or could not accept JSON as response (Content-Type: {ContentType}, Accept: {Accept})", request.ContentType, accept); + + return UnsupportedMediaType("Could not process current request because the request body is not JSON and/or could not accept JSON as response"); + } + + if (TryHttpCorrelate(out string errorMessage) == false) + { + return BadRequest(errorMessage); + } + + var order = await GetJsonBodyAsync(request); + return Json(order); + } + catch (JsonException exception) + { + Logger.LogError(exception, exception.Message); + return BadRequest("Could not process the current request due to an JSON deserialization failure"); + } + catch (ValidationException exception) + { + Logger.LogError(exception, exception.Message); + return BadRequest(exception.ValidationResult); + } + catch (Exception exception) + { + Logger.LogCritical(exception, exception.Message); + return InternalServerError("Could not process the current request due to an unexpected exception"); + } + } + } +} \ No newline at end of file From 7434ceffe6611c3588d49e3a4d6dacd5a0c388af Mon Sep 17 00:00:00 2001 From: stijnmoreels <9039753+stijnmoreels@users.noreply.github.com> Date: Fri, 3 Dec 2021 10:33:03 +0100 Subject: [PATCH 4/7] pr-fix: update with system.text.json in tests --- .../Http/Api/HttpTriggerOrderTests.cs | 124 ++++++------ .../AzureFunctions/Http/Api/OrderService.cs | 189 +++++++++--------- 2 files changed, 159 insertions(+), 154 deletions(-) diff --git a/src/Arcus.Templates.Tests.Integration/AzureFunctions/Http/Api/HttpTriggerOrderTests.cs b/src/Arcus.Templates.Tests.Integration/AzureFunctions/Http/Api/HttpTriggerOrderTests.cs index 63a03502..04341a55 100644 --- a/src/Arcus.Templates.Tests.Integration/AzureFunctions/Http/Api/HttpTriggerOrderTests.cs +++ b/src/Arcus.Templates.Tests.Integration/AzureFunctions/Http/Api/HttpTriggerOrderTests.cs @@ -1,62 +1,62 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Net; -using System.Net.Http; -using System.Threading.Tasks; -using Arcus.Templates.AzureFunctions.Http.Model; -using Arcus.Templates.Tests.Integration.Fixture; -using Bogus; -using Newtonsoft.Json; -using Xunit; -using Xunit.Abstractions; - -namespace Arcus.Templates.Tests.Integration.AzureFunctions.Http.Api -{ - [Collection(TestCollections.Integration)] - [Trait("Category", TestTraits.Integration)] - public class HttpTriggerOrderTests - { - private readonly TestConfig _config; - private readonly ITestOutputHelper _outputWriter; - - private static readonly Faker BogusGenerator = new Faker(); - - /// - /// Initializes a new instance of the class. - /// - public HttpTriggerOrderTests(ITestOutputHelper outputWriter) - { - _outputWriter = outputWriter; - _config = TestConfig.Create(); - } - - [Fact] - public async Task AzureFunctionsHttpProject_WithoutOptions_ResponseToCorrectOrder() - { - // Arrange - var order = new Order - { - Id = Guid.NewGuid().ToString(), - ArticleNumber = BogusGenerator.Random.String(1, 100), - Scheduled = BogusGenerator.Date.RecentOffset() - }; - - using (var project = await AzureFunctionsHttpProject.StartNewAsync(_config, _outputWriter)) - { - // Act - using (HttpResponseMessage response = await project.Order.PostAsync(order)) - { - // Assert - string responseContent = await response.Content.ReadAsStringAsync(); - Assert.True(HttpStatusCode.OK == response.StatusCode, responseContent); - Assert.NotNull(JsonConvert.DeserializeObject(responseContent)); - - IEnumerable responseHeaderNames = response.Headers.Select(header => header.Key).ToArray(); - Assert.Contains("X-Transaction-ID", responseHeaderNames); - Assert.Contains("RequestId", responseHeaderNames); - } - } - } - } -} +using System; +using System.Collections.Generic; +using System.Linq; +using System.Net; +using System.Net.Http; +using System.Text.Json; +using System.Threading.Tasks; +using Arcus.Templates.AzureFunctions.Http.Model; +using Arcus.Templates.Tests.Integration.Fixture; +using Bogus; +using Xunit; +using Xunit.Abstractions; + +namespace Arcus.Templates.Tests.Integration.AzureFunctions.Http.Api +{ + [Collection(TestCollections.Integration)] + [Trait("Category", TestTraits.Integration)] + public class HttpTriggerOrderTests + { + private readonly TestConfig _config; + private readonly ITestOutputHelper _outputWriter; + + private static readonly Faker BogusGenerator = new Faker(); + + /// + /// Initializes a new instance of the class. + /// + public HttpTriggerOrderTests(ITestOutputHelper outputWriter) + { + _outputWriter = outputWriter; + _config = TestConfig.Create(); + } + + [Fact] + public async Task AzureFunctionsHttpProject_WithoutOptions_ResponseToCorrectOrder() + { + // Arrange + var order = new Order + { + Id = Guid.NewGuid().ToString(), + ArticleNumber = BogusGenerator.Random.String(1, 100), + Scheduled = BogusGenerator.Date.RecentOffset() + }; + + using (var project = await AzureFunctionsHttpProject.StartNewAsync(_config, _outputWriter)) + { + // Act + using (HttpResponseMessage response = await project.Order.PostAsync(order)) + { + // Assert + string responseContent = await response.Content.ReadAsStringAsync(); + Assert.True(HttpStatusCode.OK == response.StatusCode, responseContent); + Assert.NotNull(JsonSerializer.Deserialize(responseContent)); + + IEnumerable responseHeaderNames = response.Headers.Select(header => header.Key).ToArray(); + Assert.Contains("X-Transaction-ID", responseHeaderNames); + Assert.Contains("RequestId", responseHeaderNames); + } + } + } + } +} diff --git a/src/Arcus.Templates.Tests.Integration/AzureFunctions/Http/Api/OrderService.cs b/src/Arcus.Templates.Tests.Integration/AzureFunctions/Http/Api/OrderService.cs index 7e02f172..7c20f247 100644 --- a/src/Arcus.Templates.Tests.Integration/AzureFunctions/Http/Api/OrderService.cs +++ b/src/Arcus.Templates.Tests.Integration/AzureFunctions/Http/Api/OrderService.cs @@ -1,92 +1,97 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Net.Http; -using System.Net.Http.Headers; -using System.Threading.Tasks; -using Arcus.Templates.AzureFunctions.Http.Model; -using Arcus.Templates.Tests.Integration.WebApi.Fixture; -using GuardNet; -using Microsoft.Rest; -using Newtonsoft.Json; -using Xunit.Abstractions; - -namespace Arcus.Templates.Tests.Integration.AzureFunctions.Http.Api -{ - /// - /// HTTP endpoint service to contact the 'Order' HTTP trigger of the Azure Functions test project. - /// - public class OrderService : EndpointService - { - private readonly Uri _orderEndpoint; - - /// - /// Initializes a new instance of the class. - /// - /// The HTTP endpoint where the HTTP trigger for the 'Order' system is located. - /// The logger instance to write diagnostic trace messages during the interaction with the 'Order' HTTP trigger of the Azure Functions test project. - /// Thrown when the or is null. - public OrderService(Uri orderEndpoint, ITestOutputHelper outputWriter) : base(outputWriter) - { - Guard.NotNull(orderEndpoint, nameof(orderEndpoint), "Requires an HTTP endpoint to locate the 'Order' HTTP trigger in the Azure Functions test project"); - Guard.NotNull(outputWriter, nameof(outputWriter), "Requires an logger instance to write diagnostic trace messages while interacting with the 'Order' HTTP trigger of the Azure Functions test project"); - _orderEndpoint = orderEndpoint; - } - - /// - /// HTTP POST an to the 'Order' HTTP trigger of the Azure Functions test project. - /// - /// The order to send to the HTTP trigger. - /// - /// The HTTP response of the HTTP trigger. - /// - /// Thrown when the is null. - public async Task PostAsync(Order order) - { - Guard.NotNull(order, nameof(order), $"Requires an '{nameof(Order)}' model to post to the Azure Functions HTTP trigger"); - - string json = JsonConvert.SerializeObject(order); - HttpResponseMessage response = await PostAsync(json); - - return response; - } - - /// - /// HTTP POST an to the 'Order' HTTP trigger of the Azure Functions test project. - /// - /// The order to send to the HTTP trigger. - /// - /// The HTTP response of the HTTP trigger. - /// - /// Thrown when the is null. - public async Task PostAsync(string json) - { - Guard.NotNull(json, nameof(json), $"Requires a JSON content representation of an '{nameof(Order)}' model to post to the Azure Function HTTP trigger"); - var content = new StringContent(json); - - using (var request = new HttpRequestMessage(HttpMethod.Post, _orderEndpoint)) - { - request.Content = content; - request.Content.Headers.ContentType = new MediaTypeHeaderValue("application/json"); - request.Headers.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json")); - - Logger.WriteLine("POST {{{0}}} -> {1}", FormatHeaders(request.GetContentHeaders().Concat(request.Headers)), _orderEndpoint); - HttpResponseMessage response = await HttpClient.SendAsync(request); - Logger.WriteLine("{0} {{{1}}} <- {2}", response.StatusCode, FormatHeaders(response.GetContentHeaders().Concat(response.Headers)), _orderEndpoint); - - return response; - } - } - - private static string FormatHeaders(IEnumerable>> headers) - { - IEnumerable formattedHeaders = headers.Select(header => - { - string values = String.Join(", ", header.Value); - return $"[{header.Key}] = {values}"; - }); - - return $"{{{String.Join(", ", formattedHeaders)}}}"; - } - } -} +using System; +using System.Collections.Generic; +using System.Linq; +using System.Net.Http; +using System.Net.Http.Headers; +using System.Text.Json; +using System.Threading.Tasks; +using Arcus.Templates.AzureFunctions.Http.Model; +using Arcus.Templates.Tests.Integration.WebApi.Fixture; +using GuardNet; +using Microsoft.Rest; +using Xunit.Abstractions; + +namespace Arcus.Templates.Tests.Integration.AzureFunctions.Http.Api +{ + /// + /// HTTP endpoint service to contact the 'Order' HTTP trigger of the Azure Functions test project. + /// + public class OrderService : EndpointService + { + private readonly Uri _orderEndpoint; + + private static readonly JsonSerializerOptions JsonOptions = new JsonSerializerOptions + { + PropertyNamingPolicy = JsonNamingPolicy.CamelCase + }; + + /// + /// Initializes a new instance of the class. + /// + /// The HTTP endpoint where the HTTP trigger for the 'Order' system is located. + /// The logger instance to write diagnostic trace messages during the interaction with the 'Order' HTTP trigger of the Azure Functions test project. + /// Thrown when the or is null. + public OrderService(Uri orderEndpoint, ITestOutputHelper outputWriter) : base(outputWriter) + { + Guard.NotNull(orderEndpoint, nameof(orderEndpoint), "Requires an HTTP endpoint to locate the 'Order' HTTP trigger in the Azure Functions test project"); + Guard.NotNull(outputWriter, nameof(outputWriter), "Requires an logger instance to write diagnostic trace messages while interacting with the 'Order' HTTP trigger of the Azure Functions test project"); + _orderEndpoint = orderEndpoint; + } + + /// + /// HTTP POST an to the 'Order' HTTP trigger of the Azure Functions test project. + /// + /// The order to send to the HTTP trigger. + /// + /// The HTTP response of the HTTP trigger. + /// + /// Thrown when the is null. + public async Task PostAsync(Order order) + { + Guard.NotNull(order, nameof(order), $"Requires an '{nameof(Order)}' model to post to the Azure Functions HTTP trigger"); + + string json = JsonSerializer.Serialize(order, JsonOptions); + HttpResponseMessage response = await PostAsync(json); + + return response; + } + + /// + /// HTTP POST an to the 'Order' HTTP trigger of the Azure Functions test project. + /// + /// The order to send to the HTTP trigger. + /// + /// The HTTP response of the HTTP trigger. + /// + /// Thrown when the is null. + public async Task PostAsync(string json) + { + Guard.NotNull(json, nameof(json), $"Requires a JSON content representation of an '{nameof(Order)}' model to post to the Azure Function HTTP trigger"); + var content = new StringContent(json); + + using (var request = new HttpRequestMessage(HttpMethod.Post, _orderEndpoint)) + { + request.Content = content; + request.Content.Headers.ContentType = new MediaTypeHeaderValue("application/json"); + request.Headers.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json")); + + Logger.WriteLine("POST {{{0}}} -> {1}", FormatHeaders(request.GetContentHeaders().Concat(request.Headers)), _orderEndpoint); + HttpResponseMessage response = await HttpClient.SendAsync(request); + Logger.WriteLine("{0} {{{1}}} <- {2}", response.StatusCode, FormatHeaders(response.GetContentHeaders().Concat(response.Headers)), _orderEndpoint); + + return response; + } + } + + private static string FormatHeaders(IEnumerable>> headers) + { + IEnumerable formattedHeaders = headers.Select(header => + { + string values = String.Join(", ", header.Value); + return $"[{header.Key}] = {values}"; + }); + + return $"{{{String.Join(", ", formattedHeaders)}}}"; + } + } +} From cc3c73780aeba7785159a657c959d607a87ce787 Mon Sep 17 00:00:00 2001 From: stijnmoreels <9039753+stijnmoreels@users.noreply.github.com> Date: Fri, 3 Dec 2021 11:04:24 +0100 Subject: [PATCH 5/7] pr-fix: remove 'strange' logging arguments --- .../AzureFunctions/Http/Api/OrderService.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Arcus.Templates.Tests.Integration/AzureFunctions/Http/Api/OrderService.cs b/src/Arcus.Templates.Tests.Integration/AzureFunctions/Http/Api/OrderService.cs index 7c20f247..e92d4c2e 100644 --- a/src/Arcus.Templates.Tests.Integration/AzureFunctions/Http/Api/OrderService.cs +++ b/src/Arcus.Templates.Tests.Integration/AzureFunctions/Http/Api/OrderService.cs @@ -75,9 +75,9 @@ public async Task PostAsync(string json) request.Content.Headers.ContentType = new MediaTypeHeaderValue("application/json"); request.Headers.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json")); - Logger.WriteLine("POST {{{0}}} -> {1}", FormatHeaders(request.GetContentHeaders().Concat(request.Headers)), _orderEndpoint); + Logger.WriteLine("POST {0} -> {1}", FormatHeaders(request.GetContentHeaders().Concat(request.Headers)), _orderEndpoint); HttpResponseMessage response = await HttpClient.SendAsync(request); - Logger.WriteLine("{0} {{{1}}} <- {2}", response.StatusCode, FormatHeaders(response.GetContentHeaders().Concat(response.Headers)), _orderEndpoint); + Logger.WriteLine("{0} {1} <- {2}", response.StatusCode, FormatHeaders(response.GetContentHeaders().Concat(response.Headers)), _orderEndpoint); return response; } From 52b1c767a3cef98e92a8621a2a0bc8af48e3a14f Mon Sep 17 00:00:00 2001 From: stijnmoreels <9039753+stijnmoreels@users.noreply.github.com> Date: Tue, 7 Dec 2021 07:54:38 +0100 Subject: [PATCH 6/7] Update OrderService.cs --- .../AzureFunctions/Http/Api/OrderService.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Arcus.Templates.Tests.Integration/AzureFunctions/Http/Api/OrderService.cs b/src/Arcus.Templates.Tests.Integration/AzureFunctions/Http/Api/OrderService.cs index e92d4c2e..13a169c9 100644 --- a/src/Arcus.Templates.Tests.Integration/AzureFunctions/Http/Api/OrderService.cs +++ b/src/Arcus.Templates.Tests.Integration/AzureFunctions/Http/Api/OrderService.cs @@ -91,7 +91,7 @@ private static string FormatHeaders(IEnumerable Date: Tue, 7 Dec 2021 09:09:47 +0100 Subject: [PATCH 7/7] pr-fix: revert wrong change --- .../AzureFunctions/Http/Api/OrderService.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Arcus.Templates.Tests.Integration/AzureFunctions/Http/Api/OrderService.cs b/src/Arcus.Templates.Tests.Integration/AzureFunctions/Http/Api/OrderService.cs index 13a169c9..e92d4c2e 100644 --- a/src/Arcus.Templates.Tests.Integration/AzureFunctions/Http/Api/OrderService.cs +++ b/src/Arcus.Templates.Tests.Integration/AzureFunctions/Http/Api/OrderService.cs @@ -91,7 +91,7 @@ private static string FormatHeaders(IEnumerable