From 2b7b1d4436a8cf019676a0376e069a1d964c10c0 Mon Sep 17 00:00:00 2001 From: Andrii Voznesenskyi Date: Sun, 6 Oct 2024 12:46:05 +0200 Subject: [PATCH] (#18) nuar: update requests processor [pack-all-force] --- src/Nuar/src/Nuar/Extensions.cs | 16 +- .../Nuar/Formatters/NetJsonInputFormatter.cs | 31 ++- .../Nuar/Formatters/NetJsonOutputFormatter.cs | 25 ++- .../src/Nuar/Handlers/DownstreamHandler.cs | 178 +++++++++--------- src/Nuar/src/Nuar/Nuar.csproj | 4 +- src/Nuar/src/Nuar/Requests/PayloadBuilder.cs | 29 +-- .../src/Nuar/Requests/RequestProcessor.cs | 17 +- .../src/Nuar/Routing/DownstreamBuilder.cs | 21 ++- .../obj/Debug/net9.0/Nuar.AssemblyInfo.cs | 2 +- .../net9.0/Nuar.AssemblyInfoInputs.cache | 2 +- ....GeneratedMSBuildEditorConfig.editorconfig | 2 +- 11 files changed, 189 insertions(+), 138 deletions(-) diff --git a/src/Nuar/src/Nuar/Extensions.cs b/src/Nuar/src/Nuar/Extensions.cs index 22226f3..7b9e35d 100644 --- a/src/Nuar/src/Nuar/Extensions.cs +++ b/src/Nuar/src/Nuar/Extensions.cs @@ -166,6 +166,7 @@ private static IServiceCollection AddNuarServices(this IServiceCollection servic services.AddSingleton(); services.AddSingleton(); services.AddSingleton(); + return services; } @@ -176,19 +177,24 @@ private static IServiceCollection AddExtensions(this IServiceCollection services var extensionProvider = new ExtensionProvider(options); services.AddSingleton(extensionProvider); + var logger = services.BuildServiceProvider().GetService>(); + foreach (var extension in extensionProvider.GetAll()) { - if (extension.Options.Enabled == false) + if (extension.Options.Enabled == false) { + logger.LogInformation($"Skipping disabled extension: {extension.Extension.Name}"); continue; } + logger.LogInformation($"Loading extension: {extension.Extension.Name}"); extension.Extension.Add(services, optionsProvider); } return services; } + public static IApplicationBuilder UseNuar(this IApplicationBuilder app) { var newLine = Environment.NewLine; @@ -199,7 +205,7 @@ public static IApplicationBuilder UseNuar(this IApplicationBuilder app) if (options.Auth?.Enabled == true) { logger.LogInformation("Authentication is enabled."); - app.UseAuthentication(); + app.UseAuthorization(); } else { @@ -305,7 +311,11 @@ private static void UseExtensions(this IApplicationBuilder app) } extension.Extension.Use(app, optionsProvider); - logger.LogInformation($"Enabled extension: '{extension.Extension.Name}'"); + var orderMessage = extension.Options.Order.HasValue + ? $" [order: {extension.Options.Order}]" + : string.Empty; + logger.LogInformation($"Enabled extension: '{extension.Extension.Name}' " + + $"({extension.Extension.Description}){orderMessage}"); } } diff --git a/src/Nuar/src/Nuar/Formatters/NetJsonInputFormatter.cs b/src/Nuar/src/Nuar/Formatters/NetJsonInputFormatter.cs index ff67841..d4a4ff8 100644 --- a/src/Nuar/src/Nuar/Formatters/NetJsonInputFormatter.cs +++ b/src/Nuar/src/Nuar/Formatters/NetJsonInputFormatter.cs @@ -1,4 +1,7 @@ +using System; +using System.IO; using System.Text; +using System.Threading.Tasks; using Microsoft.AspNetCore.Mvc.Formatters; namespace Nuar.Formatters @@ -19,12 +22,32 @@ protected override bool CanReadType(Type type) public override async Task ReadRequestBodyAsync(InputFormatterContext context, Encoding encoding) { var request = context.HttpContext.Request; + + if (request.Body == null) + { + return await InputFormatterResult.NoValueAsync(); + } + using (var reader = new StreamReader(request.Body, encoding)) { - var body = await reader.ReadToEndAsync(); - var result = NetJSON.NetJSON.Deserialize(context.ModelType, body); - return await InputFormatterResult.SuccessAsync(result); + try + { + var body = await reader.ReadToEndAsync(); + if (string.IsNullOrWhiteSpace(body)) + { + return await InputFormatterResult.NoValueAsync(); + } + + var result = NetJSON.NetJSON.Deserialize(context.ModelType, body); + return await InputFormatterResult.SuccessAsync(result); + } + catch + { + // Handle deserialization error by adding model state error and returning failure + context.ModelState.AddModelError("JSON", "Invalid JSON format."); + return await InputFormatterResult.FailureAsync(); + } } } } -} \ No newline at end of file +} diff --git a/src/Nuar/src/Nuar/Formatters/NetJsonOutputFormatter.cs b/src/Nuar/src/Nuar/Formatters/NetJsonOutputFormatter.cs index 0214901..dbdb794 100644 --- a/src/Nuar/src/Nuar/Formatters/NetJsonOutputFormatter.cs +++ b/src/Nuar/src/Nuar/Formatters/NetJsonOutputFormatter.cs @@ -1,4 +1,6 @@ +using System; using System.Text; +using System.Threading.Tasks; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc.Formatters; @@ -17,11 +19,26 @@ protected override bool CanWriteType(Type type) return type != null; } - public override Task WriteResponseBodyAsync(OutputFormatterWriteContext context, Encoding selectedEncoding) + public override async Task WriteResponseBodyAsync(OutputFormatterWriteContext context, Encoding selectedEncoding) { var response = context.HttpContext.Response; - var json = NetJSON.NetJSON.Serialize(context.Object); - return response.WriteAsync(json); + + // Set Content-Type to application/json + if (!response.Headers.ContainsKey("Content-Type")) + { + response.Headers.Add("Content-Type", "application/json"); + } + + try + { + var json = NetJSON.NetJSON.Serialize(context.Object); + await response.WriteAsync(json, selectedEncoding); + } + catch + { + // Handle serialization error by returning a failure response or rethrowing the exception + throw new InvalidOperationException("Failed to serialize the response to JSON."); + } } } -} \ No newline at end of file +} diff --git a/src/Nuar/src/Nuar/Handlers/DownstreamHandler.cs b/src/Nuar/src/Nuar/Handlers/DownstreamHandler.cs index 2f0c0a9..92c0922 100644 --- a/src/Nuar/src/Nuar/Handlers/DownstreamHandler.cs +++ b/src/Nuar/src/Nuar/Handlers/DownstreamHandler.cs @@ -99,106 +99,110 @@ public async Task HandleAsync(HttpContext context, RouteConfig config) } private async Task SendRequestAsync(ExecutionData executionData) +{ + var httpClient = _httpClientFactory.CreateClient("nuar"); + var method = (string.IsNullOrWhiteSpace(executionData.Route.DownstreamMethod) + ? executionData.Context.Request.Method + : executionData.Route.DownstreamMethod).ToLowerInvariant(); + + // Use the downstream URL as is (constructed with the query string in the DownstreamBuilder) + var downstreamUrl = executionData.Downstream; + + var request = new HttpRequestMessage + { + RequestUri = new Uri(downstreamUrl) + }; + + // Forward request headers if specified + if (executionData.Route.ForwardRequestHeaders == true || + (_options.ForwardRequestHeaders == true && executionData.Route.ForwardRequestHeaders != false)) + { + foreach (var (key, value) in executionData.Context.Request.Headers) { - var httpClient = _httpClientFactory.CreateClient("ntrada"); - var method = (string.IsNullOrWhiteSpace(executionData.Route.DownstreamMethod) - ? executionData.Context.Request.Method - : executionData.Route.DownstreamMethod).ToLowerInvariant(); + request.Headers.TryAddWithoutValidation(key, value.ToArray()); + } + } + + // Handle custom request headers + var requestHeaders = executionData.Route.RequestHeaders is null || !executionData.Route.RequestHeaders.Any() + ? _options.RequestHeaders + : executionData.Route.RequestHeaders; - var request = new HttpRequestMessage + if (requestHeaders != null) + { + foreach (var (key, value) in requestHeaders) + { + if (!string.IsNullOrWhiteSpace(value)) { - RequestUri = new Uri(executionData.Downstream) - }; + request.Headers.TryAddWithoutValidation(key, value); + continue; + } - if (executionData.Route.ForwardRequestHeaders == true || - _options.ForwardRequestHeaders == true && executionData.Route.ForwardRequestHeaders != false) + if (!executionData.Context.Request.Headers.TryGetValue(key, out var values)) { - foreach (var (key, value) in executionData.Context.Request.Headers) - { - request.Headers.TryAddWithoutValidation(key, value.ToArray()); - } + continue; } - var requestHeaders = executionData.Route.RequestHeaders is null || - !executionData.Route.RequestHeaders.Any() - ? _options.RequestHeaders - : executionData.Route.RequestHeaders; + request.Headers.TryAddWithoutValidation(key, values.ToArray()); + } + } - if (requestHeaders is {}) - { - foreach (var (key, value) in requestHeaders) - { - if (!string.IsNullOrWhiteSpace(value)) - { - request.Headers.TryAddWithoutValidation(key, value); - continue; - } + var includeBody = false; + switch (method) + { + case "get": + request.Method = HttpMethod.Get; + break; + case "post": + request.Method = HttpMethod.Post; + includeBody = true; + break; + case "put": + request.Method = HttpMethod.Put; + includeBody = true; + break; + case "patch": + request.Method = HttpMethod.Patch; + includeBody = true; + break; + case "delete": + request.Method = HttpMethod.Delete; + break; + case "head": + request.Method = HttpMethod.Head; + break; + case "options": + request.Method = HttpMethod.Options; + break; + case "trace": + request.Method = HttpMethod.Trace; + break; + default: + return null; + } - if (!executionData.Context.Request.Headers.TryGetValue(key, out var values)) - { - continue; - } + // Invoke any HTTP request hooks + if (_httpRequestHooks != null) + { + foreach (var hook in _httpRequestHooks) + { + if (hook == null) continue; + await hook.InvokeAsync(request, executionData); + } + } - request.Headers.TryAddWithoutValidation(key, values.ToArray()); - } - } + // Include body content if required (e.g., for POST, PUT, PATCH requests) + if (!includeBody) + { + return await httpClient.SendAsync(request); + } - var includeBody = false; - switch (method) - { - case "get": - request.Method = HttpMethod.Get; - break; - case "post": - request.Method = HttpMethod.Post; - includeBody = true; - break; - case "put": - request.Method = HttpMethod.Put; - includeBody = true; - break; - case "patch": - request.Method = HttpMethod.Patch; - includeBody = true; - break; - case "delete": - request.Method = HttpMethod.Delete; - break; - case "head": - request.Method = HttpMethod.Head; - break; - case "options": - request.Method = HttpMethod.Options; - break; - case "trace": - request.Method = HttpMethod.Trace; - break; - default: - return null; - } - - if (_httpRequestHooks is {}) - { - foreach (var hook in _httpRequestHooks) - { - if (hook is null) - { - continue; - } + using var content = GetHttpContent(executionData); + request.Content = content; + return await httpClient.SendAsync(request); +} - await hook.InvokeAsync(request, executionData); - } - } - - if (!includeBody) - { - return await httpClient.SendAsync(request); - } - using var content = GetHttpContent(executionData); - request.Content = content; - return await httpClient.SendAsync(request); - } private static HttpContent GetHttpContent(ExecutionData executionData) { diff --git a/src/Nuar/src/Nuar/Nuar.csproj b/src/Nuar/src/Nuar/Nuar.csproj index d00c8ff..26718ac 100644 --- a/src/Nuar/src/Nuar/Nuar.csproj +++ b/src/Nuar/src/Nuar/Nuar.csproj @@ -38,11 +38,9 @@ - - - + diff --git a/src/Nuar/src/Nuar/Requests/PayloadBuilder.cs b/src/Nuar/src/Nuar/Requests/PayloadBuilder.cs index d374507..000d7eb 100644 --- a/src/Nuar/src/Nuar/Requests/PayloadBuilder.cs +++ b/src/Nuar/src/Nuar/Requests/PayloadBuilder.cs @@ -9,28 +9,27 @@ internal sealed class PayloadBuilder : IPayloadBuilder { public PayloadBuilder() { + // NetJSON configuration NetJSON.NetJSON.DateFormat = NetJSON.NetJSONDateFormat.ISO; NetJSON.NetJSON.SkipDefaultValue = false; NetJSON.NetJSON.TimeZoneFormat = NetJSON.NetJSONTimeZoneFormat.Utc; + NetJSON.NetJSON.UseEnumString = true; // Ensures enums are serialized as strings if used } public async Task BuildRawAsync(HttpRequest request) { - var content = string.Empty; if (request.Body == null) { return string.Empty; } + // Reading the raw body of the request using (var reader = new StreamReader(request.Body)) { - content = await reader.ReadToEndAsync(); - + var content = await reader.ReadToEndAsync(); Console.WriteLine($"Incoming Payload: {content}"); - - + return content; } - return content; } public async Task BuildJsonAsync(HttpRequest request) where T : class, new() @@ -39,13 +38,21 @@ public async Task BuildRawAsync(HttpRequest request) if (string.IsNullOrWhiteSpace(payload)) { - return new T(); + return new T(); // Return empty instance if the payload is empty } - var deserializedPayload = NetJSON.NetJSON.Deserialize(payload); - Console.WriteLine($"Deserialized Payload: {NetJSON.NetJSON.Serialize(deserializedPayload)}"); - - return deserializedPayload; + // Handling payload deserialization and catching any errors + try + { + var deserializedPayload = NetJSON.NetJSON.Deserialize(payload); + Console.WriteLine($"Deserialized Payload: {NetJSON.NetJSON.Serialize(deserializedPayload)}"); + return deserializedPayload; + } + catch (NetJSON.NetJSONInvalidJSONException ex) + { + Console.WriteLine($"Error Deserializing Payload: {ex.Message}"); + throw; // Let the error propagate so you can handle it appropriately in the service + } } } } diff --git a/src/Nuar/src/Nuar/Requests/RequestProcessor.cs b/src/Nuar/src/Nuar/Requests/RequestProcessor.cs index a45f8db..722a4ae 100644 --- a/src/Nuar/src/Nuar/Requests/RequestProcessor.cs +++ b/src/Nuar/src/Nuar/Requests/RequestProcessor.cs @@ -44,11 +44,8 @@ public async Task ProcessAsync(RouteConfig routeConfig, HttpConte var route = routeConfig.Route; var skipPayload = route.Use == "downstream" && SkipPayloadMethods.Contains(route.DownstreamMethod); var routeData = context.GetRouteData(); - - var queryParams = GetQueryString(context.Request); - var hasTransformations = !skipPayload && _payloadTransformer.HasTransformations(resourceId, route); - var payload = hasTransformations && !string.IsNullOrEmpty(context.Request.Body.ToString()) + var payload = hasTransformations ? _payloadTransformer.Transform(await _payloadBuilder.BuildRawAsync(context.Request), resourceId, route, context.Request, routeData) : null; @@ -59,12 +56,13 @@ public async Task ProcessAsync(RouteConfig routeConfig, HttpConte ResourceId = resourceId, TraceId = traceId, UserId = context.Request.HttpContext.User?.Identity?.Name, - Claims = context.Request.HttpContext.User?.Claims?.ToDictionary(c => c.Type, c => c.Value) ?? EmptyClaims, + Claims = context.Request.HttpContext.User?.Claims?.ToDictionary(c => c.Type, c => c.Value) ?? + EmptyClaims, ContentType = contentTypeValue, Route = routeConfig.Route, Context = context, Data = routeData, - Downstream = _downstreamBuilder.GetDownstream(routeConfig, context.Request, routeData) + queryParams, + Downstream = _downstreamBuilder.GetDownstream(routeConfig, context.Request, routeData), Payload = payload?.Payload, HasPayload = hasTransformations }; @@ -106,12 +104,5 @@ public async Task ProcessAsync(RouteConfig routeConfig, HttpConte return (requestId, resourceId, traceId); } - private string GetQueryString(HttpRequest request) - { - if (!request.QueryString.HasValue) - return string.Empty; - - return request.QueryString.Value; - } } } diff --git a/src/Nuar/src/Nuar/Routing/DownstreamBuilder.cs b/src/Nuar/src/Nuar/Routing/DownstreamBuilder.cs index 00927b4..4abec2d 100644 --- a/src/Nuar/src/Nuar/Routing/DownstreamBuilder.cs +++ b/src/Nuar/src/Nuar/Routing/DownstreamBuilder.cs @@ -6,17 +6,17 @@ namespace Nuar.Routing { internal sealed class DownstreamBuilder : IDownstreamBuilder - { - private readonly NuarOptions _options; - private readonly IValueProvider _valueProvider; +{ + private readonly NuarOptions _options; + private readonly IValueProvider _valueProvider; - public DownstreamBuilder(NuarOptions options, IValueProvider valueProvider) - { - _options = options; - _valueProvider = valueProvider; - } + public DownstreamBuilder(NuarOptions options, IValueProvider valueProvider) + { + _options = options; + _valueProvider = valueProvider; + } - public string GetDownstream(RouteConfig routeConfig, HttpRequest request, RouteData data) + public string GetDownstream(RouteConfig routeConfig, HttpRequest request, RouteData data) { if (string.IsNullOrWhiteSpace(routeConfig.Downstream)) { @@ -47,7 +47,7 @@ public string GetDownstream(RouteConfig routeConfig, HttpRequest request, RouteD stringBuilder.Append($"/{value}"); continue; } - + stringBuilder.Replace($"{{{key}}}", value.ToString()); } @@ -67,4 +67,5 @@ public string GetDownstream(RouteConfig routeConfig, HttpRequest request, RouteD return stringBuilder.ToString(); } } + } diff --git a/src/Nuar/src/Nuar/obj/Debug/net9.0/Nuar.AssemblyInfo.cs b/src/Nuar/src/Nuar/obj/Debug/net9.0/Nuar.AssemblyInfo.cs index 67fbd59..4aa332a 100644 --- a/src/Nuar/src/Nuar/obj/Debug/net9.0/Nuar.AssemblyInfo.cs +++ b/src/Nuar/src/Nuar/obj/Debug/net9.0/Nuar.AssemblyInfo.cs @@ -15,7 +15,7 @@ [assembly: System.Reflection.AssemblyCopyrightAttribute("ITSharpPro © ")] [assembly: System.Reflection.AssemblyDescriptionAttribute("Nuar - A framework for building API gateways in a microservices architecture")] [assembly: System.Reflection.AssemblyFileVersionAttribute("1.0.0.0")] -[assembly: System.Reflection.AssemblyInformationalVersionAttribute("1.0.0+7f0c2e5c5b959c091d638e2a8d22a51fbb13b923")] +[assembly: System.Reflection.AssemblyInformationalVersionAttribute("1.0.0+b9b923154ddf6885e5905d545e5ca7c4e08b577f")] [assembly: System.Reflection.AssemblyProductAttribute("Nuar")] [assembly: System.Reflection.AssemblyTitleAttribute("Nuar")] [assembly: System.Reflection.AssemblyVersionAttribute("1.0.0.0")] diff --git a/src/Nuar/src/Nuar/obj/Debug/net9.0/Nuar.AssemblyInfoInputs.cache b/src/Nuar/src/Nuar/obj/Debug/net9.0/Nuar.AssemblyInfoInputs.cache index b46d484..073685a 100644 --- a/src/Nuar/src/Nuar/obj/Debug/net9.0/Nuar.AssemblyInfoInputs.cache +++ b/src/Nuar/src/Nuar/obj/Debug/net9.0/Nuar.AssemblyInfoInputs.cache @@ -1 +1 @@ -dbf2aeff69e744a666d1179eb97f73fc0a7de98dac9bec769e00f4a788b36f9a +16082bea86d2d2ab61c5609289d8bd8ccf3f69b60f60a8beafc8e6b4df51dd44 diff --git a/src/Nuar/src/Nuar/obj/Debug/net9.0/Nuar.GeneratedMSBuildEditorConfig.editorconfig b/src/Nuar/src/Nuar/obj/Debug/net9.0/Nuar.GeneratedMSBuildEditorConfig.editorconfig index 0c34fc0..0576368 100644 --- a/src/Nuar/src/Nuar/obj/Debug/net9.0/Nuar.GeneratedMSBuildEditorConfig.editorconfig +++ b/src/Nuar/src/Nuar/obj/Debug/net9.0/Nuar.GeneratedMSBuildEditorConfig.editorconfig @@ -8,7 +8,7 @@ build_property.PlatformNeutralAssembly = build_property.EnforceExtendedAnalyzerRules = build_property._SupportedPlatformList = Linux,macOS,Windows build_property.RootNamespace = Nuar -build_property.ProjectDir = /home/kaliuser/Documents/portfolio/commercial_apps/crmlxrms/nuar_framework/src/Nuar/src/Nuar/ +build_property.ProjectDir = /home/kaliuser/Documents/portfolio/commercial_apps/distributed_minispace/MiniSpace.APIGateway.Nuar/src/Nuar/src/Nuar/src/Nuar/ build_property.EnableComHosting = build_property.EnableGeneratedComInterfaceComImportInterop = build_property.EffectiveAnalysisLevelStyle = 9.0