diff --git a/src/Codehard.Core.sln b/src/Codehard.Core.sln index 71d1b72..d99b78c 100644 --- a/src/Codehard.Core.sln +++ b/src/Codehard.Core.sln @@ -59,6 +59,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Codehard.Common.Tests", "Co EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Codehard.Functional.EntityFramework.Tests", "Codehard.Functional\Codehard.Functional.EntityFramework.Tests\Codehard.Functional.EntityFramework.Tests.csproj", "{317F58C6-2B02-468D-B3B3-78394A816D43}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Codehard.Functional.AspNetCore.Tests.Api", "Codehard.Functional\Codehard.Functional.AspNetCore.Tests.Api\Codehard.Functional.AspNetCore.Tests.Api.csproj", "{959DF6C9-9D68-46E8-8BB1-6ACC3DE13053}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -173,6 +175,10 @@ Global {317F58C6-2B02-468D-B3B3-78394A816D43}.Debug|Any CPU.Build.0 = Debug|Any CPU {317F58C6-2B02-468D-B3B3-78394A816D43}.Release|Any CPU.ActiveCfg = Release|Any CPU {317F58C6-2B02-468D-B3B3-78394A816D43}.Release|Any CPU.Build.0 = Release|Any CPU + {959DF6C9-9D68-46E8-8BB1-6ACC3DE13053}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {959DF6C9-9D68-46E8-8BB1-6ACC3DE13053}.Debug|Any CPU.Build.0 = Debug|Any CPU + {959DF6C9-9D68-46E8-8BB1-6ACC3DE13053}.Release|Any CPU.ActiveCfg = Release|Any CPU + {959DF6C9-9D68-46E8-8BB1-6ACC3DE13053}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -208,5 +214,6 @@ Global {C936B94D-D1D9-4A55-8CA1-748988282811} = {2F98EF48-527C-44C5-8FC3-65F25C808AC9} {06431584-7320-4AEE-AC93-A22B422F543A} = {2F98EF48-527C-44C5-8FC3-65F25C808AC9} {317F58C6-2B02-468D-B3B3-78394A816D43} = {0C257E94-AD98-4AFB-93B7-B6F64EB7D2BA} + {959DF6C9-9D68-46E8-8BB1-6ACC3DE13053} = {0C257E94-AD98-4AFB-93B7-B6F64EB7D2BA} EndGlobalSection EndGlobal diff --git a/src/Codehard.Functional/Codehard.Functional.AspNetCore.Tests.Api/Codehard.Functional.AspNetCore.Tests.Api.csproj b/src/Codehard.Functional/Codehard.Functional.AspNetCore.Tests.Api/Codehard.Functional.AspNetCore.Tests.Api.csproj index a281ed7..8974154 100644 --- a/src/Codehard.Functional/Codehard.Functional.AspNetCore.Tests.Api/Codehard.Functional.AspNetCore.Tests.Api.csproj +++ b/src/Codehard.Functional/Codehard.Functional.AspNetCore.Tests.Api/Codehard.Functional.AspNetCore.Tests.Api.csproj @@ -4,6 +4,7 @@ net6.0 enable enable + default diff --git a/src/Codehard.Functional/Codehard.Functional.AspNetCore.Tests.Api/Controllers/WeatherForecastController.cs b/src/Codehard.Functional/Codehard.Functional.AspNetCore.Tests.Api/Controllers/WeatherForecastController.cs index 04c79c4..b9615e6 100644 --- a/src/Codehard.Functional/Codehard.Functional.AspNetCore.Tests.Api/Controllers/WeatherForecastController.cs +++ b/src/Codehard.Functional/Codehard.Functional.AspNetCore.Tests.Api/Controllers/WeatherForecastController.cs @@ -13,11 +13,11 @@ public class WeatherForecastController : ControllerBase "Freezing", "Bracing", "Chilly", "Cool", "Mild", "Warm", "Balmy", "Hot", "Sweltering", "Scorching" }; - private readonly ILogger _logger; + private readonly ILogger logger; public WeatherForecastController(ILogger logger) { - _logger = logger; + this.logger = logger; } [HttpGet(Name = "GetWeatherForecast")] @@ -40,6 +40,12 @@ public IActionResult GetSuccessEff() return eff.RunToResult(); } + [HttpGet(template: "Get500WithThrowException")] + public IActionResult Get500WithThrowException() + { + throw new Exception("Error Msg"); + } + [HttpGet(template: "Get500FailWithMsgEff")] public IActionResult Get500FailWithMsgEff() { @@ -51,13 +57,24 @@ public IActionResult Get500FailWithMsgEff() return eff.RunToResult(); } + [HttpGet(template: "Get500FailWithExceptionEff")] + public IActionResult Get500FailWithExceptionEff() + { + var eff = + Eff>( + () => throw new Exception("Error Msg")); + + return eff.RunToResult(); + } + [HttpGet(template: "Get500FailWithMsgAndErrCodeEff")] public IActionResult Get500FailWithMsgAndErrCodeEff() { - var eff = FailEff>(HttpResultError.New( - HttpStatusCode.InternalServerError, - "Error Msg", - errorCode: "Err001")); + var eff = FailEff>( + HttpResultError.New( + HttpStatusCode.InternalServerError, + "Error Msg", + errorCode: "Err001")); return eff.RunToResult(); } diff --git a/src/Codehard.Functional/Codehard.Functional.AspNetCore.Tests.Api/Program.cs b/src/Codehard.Functional/Codehard.Functional.AspNetCore.Tests.Api/Program.cs index 8264bac..918ab90 100644 --- a/src/Codehard.Functional/Codehard.Functional.AspNetCore.Tests.Api/Program.cs +++ b/src/Codehard.Functional/Codehard.Functional.AspNetCore.Tests.Api/Program.cs @@ -1,8 +1,15 @@ +using Codehard.Functional.AspNetCore; + var builder = WebApplication.CreateBuilder(args); // Add services to the container. builder.Services.AddControllers(); +builder.Services.AddMvc(options => +{ + options.Filters.Add(); +}); + // Learn more about configuring Swagger/OpenAPI at https://aka.ms/aspnetcore/swashbuckle builder.Services.AddEndpointsApiExplorer(); builder.Services.AddSwaggerGen(); @@ -16,7 +23,7 @@ app.UseSwaggerUI(); } -app.UseHttpsRedirection(); +//app.UseHttpsRedirection(); app.UseAuthorization(); diff --git a/src/Codehard.Functional/Codehard.Functional.AspNetCore.Tests.Api/Properties/launchSettings.json b/src/Codehard.Functional/Codehard.Functional.AspNetCore.Tests.Api/Properties/launchSettings.json index e4f07c3..2c04686 100644 --- a/src/Codehard.Functional/Codehard.Functional.AspNetCore.Tests.Api/Properties/launchSettings.json +++ b/src/Codehard.Functional/Codehard.Functional.AspNetCore.Tests.Api/Properties/launchSettings.json @@ -14,7 +14,7 @@ "dotnetRunMessages": true, "launchBrowser": true, "launchUrl": "swagger", - "applicationUrl": "https://localhost:7269;http://localhost:5159", + "applicationUrl": "http://localhost:5159", "environmentVariables": { "ASPNETCORE_ENVIRONMENT": "Development" } diff --git a/src/Codehard.Functional/Codehard.Functional.AspNetCore/ControllerExtensions.cs b/src/Codehard.Functional/Codehard.Functional.AspNetCore/ControllerExtensions.cs index c5bb92c..44f4aa1 100644 --- a/src/Codehard.Functional/Codehard.Functional.AspNetCore/ControllerExtensions.cs +++ b/src/Codehard.Functional/Codehard.Functional.AspNetCore/ControllerExtensions.cs @@ -24,13 +24,13 @@ private static IActionResult MapErrorToActionResult(Error err) err switch { HttpResultError hre => new ErrorWrapperActionResult(hre), - _ => new ObjectResult(err.Message) - { - StatusCode = + _ => new ErrorWrapperActionResult( + HttpResultError.New( Enum.IsDefined(typeof(HttpStatusCode), err.Code) - ? err.Code - : (int)HttpStatusCode.InternalServerError, - } + ? (HttpStatusCode)err.Code + : HttpStatusCode.InternalServerError, + err.Message, + error: err)), }; } @@ -50,18 +50,7 @@ public static IActionResult MatchToResult( return fin .Match( res => MapToActionResult(successStatusCode, res), - err => - { - switch (err) - { - case HttpResultError hre: - logger?.Log(hre); - return MapErrorToActionResult(hre); - default: - logger?.Log(err); - return MapErrorToActionResult(err); - } - }); + MapErrorToActionResult); } /// diff --git a/src/Codehard.Functional/Codehard.Functional.AspNetCore/ErrorWrapperActionResult.cs b/src/Codehard.Functional/Codehard.Functional.AspNetCore/ErrorWrapperActionResult.cs index 6a15ff0..c733900 100644 --- a/src/Codehard.Functional/Codehard.Functional.AspNetCore/ErrorWrapperActionResult.cs +++ b/src/Codehard.Functional/Codehard.Functional.AspNetCore/ErrorWrapperActionResult.cs @@ -1,4 +1,5 @@ using System.Dynamic; +using Microsoft.AspNetCore.Http; namespace Codehard.Functional.AspNetCore; @@ -34,7 +35,7 @@ public async Task ExecuteResultAsync(ActionContext context) { this.Error.ErrorCode.IfSome(errCode => context.HttpContext.Response.Headers.Add("x-error-code", errCode)); - + context.HttpContext.Response.Headers.Add("x-trace-id", context.HttpContext.TraceIdentifier); await this.Error.Data @@ -62,13 +63,15 @@ await this.Error.Data }) .Match( ar => ar.ExecuteResultAsync(context), - None: () => + None: async () => { context.HttpContext.Response.StatusCode = (int)this.Error.StatusCode; - - return Task.CompletedTask; + context.HttpContext.Response.ContentType = "text/plain"; + await context.HttpContext.Response.WriteAsync(context.HttpContext.TraceIdentifier); }); + return; + ObjectResult AddErrorInfo(ObjectResult objectResult) { IDictionary expando = new ExpandoObject(); diff --git a/src/Codehard.Functional/Codehard.Functional.AspNetCore/ErrorWrapperActionResultLoggingFilter.cs b/src/Codehard.Functional/Codehard.Functional.AspNetCore/ErrorWrapperActionResultLoggingFilter.cs new file mode 100644 index 0000000..c1aae28 --- /dev/null +++ b/src/Codehard.Functional/Codehard.Functional.AspNetCore/ErrorWrapperActionResultLoggingFilter.cs @@ -0,0 +1,105 @@ +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Mvc.Filters; +using Microsoft.AspNetCore.Mvc.Infrastructure; + +namespace Codehard.Functional.AspNetCore; + +/// +/// An action filter that logs errors occurring during the execution of an action. +/// +public class ErrorWrapperActionResultLoggingFilter : IAsyncActionFilter +{ + private readonly ILogger logger; + + /// + /// Initializes a new instance of the class. + /// + /// The logger to log error information. + public ErrorWrapperActionResultLoggingFilter(ILogger logger) + { + this.logger = logger; + } + + /// + /// Executes the action and logs any errors that occur during the execution. + /// + /// The context in which the action is executed. + /// The delegate to execute the next action filter or action. + /// A representing the asynchronous operation. + public async Task OnActionExecutionAsync(ActionExecutingContext context, ActionExecutionDelegate next) + { + var actionExecutedContext = await next(); + + await OnActionExecutedAsync(actionExecutedContext); + } + + private Task OnActionExecutedAsync(ActionExecutedContext context) + { + var traceId = context.HttpContext.TraceIdentifier; + + switch (context.Result) + { + case ErrorWrapperActionResult ewar: + LogHttpResultError(ewar.Error); + break; + case IStatusCodeActionResult statusCodeResult: + { + if(statusCodeResult.StatusCode == StatusCodes.Status500InternalServerError) + { + LogError(context.Exception); + } + + break; + } + } + + return Task.CompletedTask; + + void LogHttpResultError(HttpResultError error) + { + this.logger.LogError( + message: "TraceId: {TraceId}, {Path}, {Query}, {Method}, {ResponseStatus}, {ErrorCode}", + traceId, + Sanitize(context.HttpContext.Request.Path), + Sanitize(context.HttpContext.Request.QueryString.Value), + Sanitize(context.HttpContext.Request.Method), + error.StatusCode, + error.ErrorCode.IfNoneUnsafe(default(string))); + + LogErrorOpt(error.Inner); + } + + void LogErrorOpt(Option errorOpt) + { + errorOpt.Iter( + Some: error => + { + LogError( + exception: error.Exception.IfNoneUnsafe(default(Exception))); + + LogErrorOpt(error.Inner); + }); + } + + void LogError(Exception? exception) + { + this.logger.LogError( + exception: exception, + message: "TraceId: {TraceId}, {Path}, {Query}, {Method}, {ResponseStatus}", + traceId, + Sanitize(context.HttpContext.Request.Path), + Sanitize(context.HttpContext.Request.QueryString.Value), + Sanitize(context.HttpContext.Request.Method), + HttpStatusCode.InternalServerError); + } + } + + private static string Sanitize(string? input) + { + return new string( + input + ?.Replace(Environment.NewLine, "") + .Replace("\n", "") + .Replace("\r", "")); + } +} \ No newline at end of file diff --git a/src/Codehard.Functional/Codehard.Functional.Logger/LoggerExtensions.cs b/src/Codehard.Functional/Codehard.Functional.Logger/LoggerExtensions.cs index aefda92..4ca4af8 100644 --- a/src/Codehard.Functional/Codehard.Functional.Logger/LoggerExtensions.cs +++ b/src/Codehard.Functional/Codehard.Functional.Logger/LoggerExtensions.cs @@ -17,7 +17,7 @@ private static Unit Log(this ILogger logger, Option errorOpt, LogLevel lo return error.Exception.Match( - Some: ex => logger.LogError(ex, error.Message), + Some: ex => logger.LogError(ex, "{Message}", error.Message), None: () => { if (string.IsNullOrWhiteSpace(error.Message))