From 6a0044ceea259dbd9aef77882350579bd33e9aa5 Mon Sep 17 00:00:00 2001 From: Haik Date: Mon, 10 Jun 2024 14:48:01 +0400 Subject: [PATCH 1/2] update --- src/ResponseCrafter/PandaExceptionHandler.cs | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/src/ResponseCrafter/PandaExceptionHandler.cs b/src/ResponseCrafter/PandaExceptionHandler.cs index a1d0d6b..66ccee1 100644 --- a/src/ResponseCrafter/PandaExceptionHandler.cs +++ b/src/ResponseCrafter/PandaExceptionHandler.cs @@ -1,5 +1,4 @@ -using System.Reflection; -using BaseConverter.Exceptions; +using BaseConverter.Exceptions; using EFCoreQueryMagic.Exceptions; using FluentImporter.Exceptions; using Microsoft.AspNetCore.Http; @@ -75,7 +74,7 @@ private async Task HandleBaseConverterExceptionAsync(HttpContext httpContext, case UnsupportedCharacterException _: var exceptionName = importException.GetType().Name; var formattedMessage = - $"{exceptionName} in Base Converter: {_namingConventionConverter(importException.Message)}"; + $"{exceptionName} in Base Converter: {importException.Message}"; var mappedException = new BadRequestException(formattedMessage); await HandleApiExceptionAsync(httpContext, mappedException, cancellationToken); break; @@ -183,7 +182,7 @@ private async Task HandleFilterExceptionAsync(HttpContext httpContext, FilterExc case ColumnNameMissingException _: var exceptionName = filterException.GetType().Name; var formattedMessage = - $"{exceptionName} in Filters: {_namingConventionConverter(filterException.Message)}"; + $"{exceptionName} in Filters: {filterException.Message}"; var mappedException = new BadRequestException(formattedMessage); await HandleApiExceptionAsync(httpContext, mappedException, cancellationToken); break; From d3b07e631e36746deb0d213a4f2444f96cb5fb96 Mon Sep 17 00:00:00 2001 From: Haik Date: Thu, 13 Jun 2024 16:26:30 +0400 Subject: [PATCH 2/2] Naming convention has been added --- Readme.md | 60 ++++++--- ResponseCrafter.sln.DotSettings | 3 +- src/ResponseCrafter/Enums/NamingConvention.cs | 8 +- .../Extensions/NamingConventionExtensions.cs | 47 ------- .../Extensions/StringExtensions.cs | 28 ++++ .../Extensions/WebApplicationExtensions.cs | 27 ++-- .../Options/NamingConventionOptions.cs | 8 ++ src/ResponseCrafter/PandaExceptionHandler.cs | 127 ++++++++++-------- src/ResponseCrafter/ResponseCrafter.csproj | 6 +- test/ResponseCrafter.Demo/Program.cs | 26 +++- 10 files changed, 194 insertions(+), 146 deletions(-) delete mode 100644 src/ResponseCrafter/Extensions/NamingConventionExtensions.cs create mode 100644 src/ResponseCrafter/Extensions/StringExtensions.cs create mode 100644 src/ResponseCrafter/Options/NamingConventionOptions.cs diff --git a/Readme.md b/Readme.md index 18b9d53..5016ac0 100644 --- a/Readme.md +++ b/Readme.md @@ -2,8 +2,8 @@ ## Introduction -ResponseCrafter is a comprehensive NuGet package for .NET 8+, specifically designed for enhanced exception handling and -logging in ASP.NET applications. This package simplifies the process of handling standard and custom exceptions by +**Pandatech.ResponseCrafter** is a comprehensive NuGet package for .NET 8+, specifically designed to enhance exception +handling and logging in ASP.NET Core applications. This package simplifies managing standard and custom exceptions by crafting detailed error responses suitable for both development and production environments. ## Features @@ -12,13 +12,14 @@ crafting detailed error responses suitable for both development and production e exceptions. * **Detailed Error Responses:** Generates verbose error messages, including stack traces for in-depth debugging in development environments. -* **Environment-Sensitive Logging:** Offers a class `PandaExceptionHandler` which can be configured for message verbosity. - In production environments, only the exception type and message are logged. In development environments, the entire - exception is logged, including the stack trace. -* **Frontend-Friendly Error Messages:** Encourages the use of snake_case in error messages, facilitating easier - integration with frontend localization systems. -* **Organized/Readable and standardized error responses:** Provides a standardized error response format for all - exceptions, making it easier for frontend applications to parse and display error messages. The error response format is shown below: +* **Environment-Sensitive Logging:** Provides flexible logging and response behavior based on visibility settings (`Public` or `Private`): + - **Private:** All exceptions are sent to the client as defined, and 4xx errors are logged as warnings while 5xx errors are logged as errors. + - **Public:** 4xx exceptions are sent to the client as defined, while 5xx errors are concealed with a generic message. Logging remains the same as in `Private`. +* **Frontend-Friendly Error Messages:** Supports converting error messages to your desired case convention, facilitating + easier integration with frontend localization systems. +* **Standardized Error Responses:** Provides a standardized error response format, making it easier for frontend + applications to parse and display error messages. The error response format is shown below: + ```json { "TraceId": "0HMVFE0A284AM:00000001", @@ -31,7 +32,6 @@ crafting detailed error responses suitable for both development and production e }, "Message": "the_request_was_invalid_or_cannot_be_otherwise_served." } - ```` ## Installation @@ -46,23 +46,51 @@ Install-Package ResponseCrafter ### 1. Setup Exception Handlers: -**Add** `AddResponseCrafter` in program.cs and in configuration set `"ResponseCrafterVisibility"` to `"Public"` or `"Private"`. +**Add** `AddResponseCrafter` in `program.cs` bby providing an optional naming convention, and +configure `ResponseCrafterVisibility` in your settings. ```csharp +var builder = WebApplication.CreateBuilder(args); + +// Basic setup builder.AddResponseCrafter(); + +// Setup with a specific naming convention +builder.AddResponseCrafter(NamingConvention.ToSnakeCase); + +var app = builder.Build(); +app.UseResponseCrafter(); +app.Run(); ``` + +Configure visibility in your `appsettings.json`: + ```json { "ResponseCrafterVisibility": "Public" } +``` +Supported naming conventions: + +```csharp +public enum NamingConvention +{ + Default = 0, + ToSnakeCase = 1, + ToPascalCase = 2, + ToCamelCase = 3, + ToKebabCase = 4, + ToTitleCase = 5, + ToHumanCase = 6 +} ``` ### 2. Define Custom Exceptions: -* Create a custom exception class that inherits from `ApiException` or use already created ones: - -* Utilize `ErrorDetails` records for specific error messages related to API requests. +Create custom exception classes that inherit from `ApiException` or use the predefined ones. Use `ErrorDetails` records +for +specific error messages related to API requests. ### 3. Configure Middleware: @@ -74,9 +102,9 @@ app.UseResponseCrafter(); ### 4. Logging and Error Responses: -* Automatically logs warnings or errors and provides crafted responses base on the exception type. +The package automatically logs warnings or errors and provides crafted responses based on the exception type. -## Custom Exception Already Created +## Custom HTTP Exception Already Created * `BadRequestException` * `UnauthorizedException` diff --git a/ResponseCrafter.sln.DotSettings b/ResponseCrafter.sln.DotSettings index 97c867b..fb5305b 100644 --- a/ResponseCrafter.sln.DotSettings +++ b/ResponseCrafter.sln.DotSettings @@ -1,2 +1,3 @@  - True \ No newline at end of file + True + True \ No newline at end of file diff --git a/src/ResponseCrafter/Enums/NamingConvention.cs b/src/ResponseCrafter/Enums/NamingConvention.cs index 6ed6df1..9ec53d3 100644 --- a/src/ResponseCrafter/Enums/NamingConvention.cs +++ b/src/ResponseCrafter/Enums/NamingConvention.cs @@ -3,6 +3,10 @@ namespace ResponseCrafter.Enums; public enum NamingConvention { Default = 0, - SnakeCaseLower = 1, - SnakeCaseUpper = 2 + ToSnakeCase = 1, + ToPascalCase = 2, + ToCamelCase = 3, + ToKebabCase = 4, + ToTitleCase = 5, + ToHumanCase = 6 } \ No newline at end of file diff --git a/src/ResponseCrafter/Extensions/NamingConventionExtensions.cs b/src/ResponseCrafter/Extensions/NamingConventionExtensions.cs deleted file mode 100644 index 24ce48c..0000000 --- a/src/ResponseCrafter/Extensions/NamingConventionExtensions.cs +++ /dev/null @@ -1,47 +0,0 @@ -using System.Text; - -namespace ResponseCrafter.Extensions; - -internal static class NamingConventionExtensions -{ - internal static string Default(this string message) - { - return message; - } - - internal static string ToSnakeCaseLowerNamingConvention(this string message) - { - var words = message.Split(' '); - - var newMessage = new StringBuilder(); - - for (var i = 0; i < words.Length; i++) - { - newMessage.Append(words[i].ToLower()); - if (i < words.Length - 1) - { - newMessage.Append('_'); - } - } - - return newMessage.ToString(); - } - - internal static string ToSnakeCaseUpperNamingConvention(this string message) - { - var words = message.Split(' '); - - var newMessage = new StringBuilder(); - - for (var i = 0; i < words.Length; i++) - { - newMessage.Append(words[i].ToUpper()); - if (i < words.Length - 1) - { - newMessage.Append('_'); - } - } - - return newMessage.ToString(); - } -} \ No newline at end of file diff --git a/src/ResponseCrafter/Extensions/StringExtensions.cs b/src/ResponseCrafter/Extensions/StringExtensions.cs new file mode 100644 index 0000000..ba788a4 --- /dev/null +++ b/src/ResponseCrafter/Extensions/StringExtensions.cs @@ -0,0 +1,28 @@ +using Humanizer; +using ResponseCrafter.Enums; +using ResponseCrafter.Options; + +namespace ResponseCrafter.Extensions; + +public static class StringExtensions +{ + public static string ConvertCase(this string message, NamingConvention namingConvention) + { + return namingConvention switch + { + NamingConvention.Default => message, + NamingConvention.ToSnakeCase => message.Underscore(), + NamingConvention.ToPascalCase => message.Underscore().Pascalize(), + NamingConvention.ToCamelCase => message.Underscore().Camelize(), + NamingConvention.ToKebabCase => message.Underscore().Kebaberize(), + NamingConvention.ToTitleCase => message.Underscore().Titleize(), + NamingConvention.ToHumanCase => message.Underscore().Humanize(), + _ => message + }; + } + + internal static string ConvertCase(this string message, NamingConventionOptions option) + { + return message.ConvertCase(option.NamingConvention); + } +} \ No newline at end of file diff --git a/src/ResponseCrafter/Extensions/WebApplicationExtensions.cs b/src/ResponseCrafter/Extensions/WebApplicationExtensions.cs index 2932660..e9bfbe4 100644 --- a/src/ResponseCrafter/Extensions/WebApplicationExtensions.cs +++ b/src/ResponseCrafter/Extensions/WebApplicationExtensions.cs @@ -1,36 +1,27 @@ using Microsoft.AspNetCore.Builder; using Microsoft.Extensions.DependencyInjection; using ResponseCrafter.Enums; +using ResponseCrafter.Options; namespace ResponseCrafter.Extensions; public static class WebApplicationExtensions { - public static WebApplicationBuilder AddResponseCrafter(this WebApplicationBuilder builder, NamingConvention? namingConvention = null) + public static WebApplicationBuilder AddResponseCrafter(this WebApplicationBuilder builder, + NamingConvention namingConvention = NamingConvention.Default) { + builder.Services.AddSingleton(new NamingConventionOptions { NamingConvention = namingConvention }); builder.Services.AddExceptionHandler(); - - switch (namingConvention) - { - case null: - case NamingConvention.Default: - builder.Services.AddSingleton>(NamingConventionExtensions.Default); - break; - case NamingConvention.SnakeCaseLower: - builder.Services.AddSingleton>(NamingConventionExtensions.ToSnakeCaseLowerNamingConvention); - break; - case NamingConvention.SnakeCaseUpper: - builder.Services.AddSingleton>(NamingConventionExtensions.ToSnakeCaseUpperNamingConvention); - break; - default: - throw new ArgumentOutOfRangeException(nameof(namingConvention), namingConvention, null); - } + return builder; } + public static WebApplication UseResponseCrafter(this WebApplication app) { - app.UseExceptionHandler(_ => { }); //the lambda parameter is not needed it is just .net 8 bug which might be fixed in the future + app.UseExceptionHandler(_ => + { + }); //the lambda parameter is not needed it is just .net 8 bug which might be fixed in the future return app; } diff --git a/src/ResponseCrafter/Options/NamingConventionOptions.cs b/src/ResponseCrafter/Options/NamingConventionOptions.cs new file mode 100644 index 0000000..a3841bf --- /dev/null +++ b/src/ResponseCrafter/Options/NamingConventionOptions.cs @@ -0,0 +1,8 @@ +using ResponseCrafter.Enums; + +namespace ResponseCrafter.Options; + +public class NamingConventionOptions +{ + public NamingConvention NamingConvention { get; set; } +} \ No newline at end of file diff --git a/src/ResponseCrafter/PandaExceptionHandler.cs b/src/ResponseCrafter/PandaExceptionHandler.cs index 66ccee1..656e1e3 100644 --- a/src/ResponseCrafter/PandaExceptionHandler.cs +++ b/src/ResponseCrafter/PandaExceptionHandler.cs @@ -1,13 +1,17 @@ using BaseConverter.Exceptions; using EFCoreQueryMagic.Exceptions; using FluentImporter.Exceptions; +using GridifyExtensions.Exceptions; using Microsoft.AspNetCore.Http; using Microsoft.Extensions.Logging; using ResponseCrafter.Dtos; using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.Configuration; using PandaTech.ServiceResponse; +using ResponseCrafter.Enums; +using ResponseCrafter.Extensions; using ResponseCrafter.HttpExceptions; +using ResponseCrafter.Options; using static ResponseCrafter.Helpers.ExceptionMessageBuilder; using IExceptionHandler = Microsoft.AspNetCore.Diagnostics.IExceptionHandler; @@ -16,17 +20,22 @@ namespace ResponseCrafter; public class PandaExceptionHandler : IExceptionHandler { private readonly ILogger _logger; + private readonly NamingConvention _convention; private readonly string _visibility; - private readonly Func _namingConventionConverter; + private const string DefaultMessage = "something_went_wrong_please_try_again_later_and_or_contact_it_support"; + + private const string ConcurrencyMessage = + "a_concurrency_conflict_occurred._please_reload_the_resource_and_try_you_update_again"; public PandaExceptionHandler(ILogger logger, IConfiguration configuration, - Func namingConventionConverter) + NamingConventionOptions convention) { _logger = logger; + _convention = convention.NamingConvention; _visibility = configuration["ResponseCrafterVisibility"]!; - _namingConventionConverter = namingConventionConverter; - if (string.IsNullOrEmpty(_visibility) || _visibility != "Private" && _visibility != "Public") + + if (string.IsNullOrWhiteSpace(_visibility) || _visibility != "Private" && _visibility != "Public") { _visibility = "Public"; _logger.LogWarning("Visibility configuration was not available. Defaulted to 'Public'."); @@ -38,23 +47,27 @@ public async ValueTask TryHandleAsync(HttpContext httpContext, Exception e { switch (exception) { - case ApiException apiException: - await HandleApiExceptionAsync(httpContext, apiException, cancellationToken); - break; case DbUpdateConcurrencyException: await HandleDbConcurrencyExceptionAsync(httpContext, cancellationToken); break; - case FilterException filterException: - await HandleFilterExceptionAsync(httpContext, filterException, cancellationToken); + case BaseConverterException targetInvocationException: + await HandleBaseConverterExceptionAsync(httpContext, targetInvocationException, cancellationToken); + break; + case ImportException targetInvocationException: + await HandleImportExceptionAsync(httpContext, targetInvocationException, cancellationToken); break; case ServiceException serviceException: await HandleServiceExceptionAsync(httpContext, serviceException, cancellationToken); break; - case ImportException targetInvocationException: - await HandleImportExceptionAsync(httpContext, targetInvocationException, cancellationToken); + case FilterException filterException: + await HandleFilterExceptionAsync(httpContext, filterException, cancellationToken); break; - case BaseConverterException targetInvocationException: - await HandleBaseConverterExceptionAsync(httpContext, targetInvocationException, cancellationToken); + case GridifyException gridifyException: + await HandleGridifyExceptionAsync(httpContext, gridifyException, cancellationToken); + break; + + case ApiException apiException: + await HandleApiExceptionAsync(httpContext, apiException, cancellationToken); break; default: await HandleGeneralExceptionAsync(httpContext, exception, cancellationToken); @@ -64,6 +77,14 @@ public async ValueTask TryHandleAsync(HttpContext httpContext, Exception e return true; } + + private async Task HandleDbConcurrencyExceptionAsync(HttpContext httpContext, CancellationToken cancellationToken) + { + var exception = + new ConflictException(ConcurrencyMessage.ConvertCase(_convention)); + await HandleApiExceptionAsync(httpContext, exception, cancellationToken); + } + private async Task HandleBaseConverterExceptionAsync(HttpContext httpContext, BaseConverterException importException, CancellationToken cancellationToken) @@ -72,10 +93,7 @@ private async Task HandleBaseConverterExceptionAsync(HttpContext httpContext, { case InputValidationException _: case UnsupportedCharacterException _: - var exceptionName = importException.GetType().Name; - var formattedMessage = - $"{exceptionName} in Base Converter: {importException.Message}"; - var mappedException = new BadRequestException(formattedMessage); + var mappedException = new BadRequestException(importException.Message.ConvertCase(_convention)); await HandleApiExceptionAsync(httpContext, mappedException, cancellationToken); break; default: @@ -93,9 +111,7 @@ private async Task HandleImportExceptionAsync(HttpContext httpContext, ImportExc case InvalidCellValueException _: case InvalidPropertyNameException _: case EmptyFileImportException _: - var exceptionName = importException.GetType().Name; - var formattedMessage = $"{exceptionName} in Import: {importException.Message}"; - var mappedException = new BadRequestException(formattedMessage); + var mappedException = new BadRequestException(importException.Message.ConvertCase(_convention)); await HandleApiExceptionAsync(httpContext, mappedException, cancellationToken); break; default: @@ -109,14 +125,14 @@ private async Task HandleServiceExceptionAsync(HttpContext httpContext, ServiceE { var response = new ServiceResponse { - Message = "a_concurrency_conflict_occurred._please_reload_the_resource_and_try_you_update_again", + Message = DefaultMessage.ConvertCase(_convention), ResponseStatus = serviceException.ResponseStatus, Success = false }; if (_visibility == "Private") { - response.Message = _namingConventionConverter(serviceException.Message); + response.Message = serviceException.Message.ConvertCase(_convention); } httpContext.Response.StatusCode = (int)serviceException.ResponseStatus; @@ -133,6 +149,35 @@ private async Task HandleServiceExceptionAsync(HttpContext httpContext, ServiceE await httpContext.Response.WriteAsJsonAsync(response, cancellationToken); } + + private async Task HandleFilterExceptionAsync(HttpContext httpContext, FilterException filterException, + CancellationToken cancellationToken) + { + switch (filterException) + { + case ComparisonNotSupportedException _: + case PaginationException _: + case PropertyNotFoundException _: + case UnsupportedFilterException _: + case UnsupportedValueException _: + case AggregateTypeMissingException _: + case ColumnNameMissingException _: + var mappedException = new BadRequestException(filterException.Message.ConvertCase(_convention)); + await HandleApiExceptionAsync(httpContext, mappedException, cancellationToken); + break; + default: + await HandleGeneralExceptionAsync(httpContext, filterException, cancellationToken); + break; + } + } + + private async Task HandleGridifyExceptionAsync(HttpContext httpContext, GridifyException gridifyException, + CancellationToken cancellationToken) + { + var exception = new BadRequestException(gridifyException.Message.ConvertCase(_convention)); + await HandleApiExceptionAsync(httpContext, exception, cancellationToken); + } + private async Task HandleApiExceptionAsync(HttpContext httpContext, ApiException exception, CancellationToken cancellationToken) { @@ -143,7 +188,7 @@ private async Task HandleApiExceptionAsync(HttpContext httpContext, ApiException StatusCode = exception.StatusCode, Type = exception.GetType().Name, Errors = exception.Errors, - Message = _namingConventionConverter(exception.Message) + Message = exception.Message.ConvertCase(_convention) }; httpContext.Response.StatusCode = exception.StatusCode; @@ -160,38 +205,6 @@ private async Task HandleApiExceptionAsync(HttpContext httpContext, ApiException } } - private async Task HandleDbConcurrencyExceptionAsync(HttpContext httpContext, CancellationToken cancellationToken) - { - var exception = - new ConflictException( - "a_concurrency_conflict_occurred._please_reload_the_resource_and_try_you_update_again"); - await HandleApiExceptionAsync(httpContext, exception, cancellationToken); - } - - private async Task HandleFilterExceptionAsync(HttpContext httpContext, FilterException filterException, - CancellationToken cancellationToken) - { - switch (filterException) - { - case ComparisonNotSupportedException _: - case PaginationException _: - case PropertyNotFoundException _: - case UnsupportedFilterException _: - case UnsupportedValueException _: - case AggregateTypeMissingException _: - case ColumnNameMissingException _: - var exceptionName = filterException.GetType().Name; - var formattedMessage = - $"{exceptionName} in Filters: {filterException.Message}"; - var mappedException = new BadRequestException(formattedMessage); - await HandleApiExceptionAsync(httpContext, mappedException, cancellationToken); - break; - default: - await HandleGeneralExceptionAsync(httpContext, filterException, cancellationToken); - break; - } - } - private async Task HandleGeneralExceptionAsync(HttpContext httpContext, Exception exception, CancellationToken cancellationToken) { @@ -203,13 +216,13 @@ private async Task HandleGeneralExceptionAsync(HttpContext httpContext, Exceptio Instance = CreateRequestPath(httpContext), StatusCode = 500, Type = "InternalServerError", - Message = "something_went_wrong_please_try_again_later_and_or_contact_it_support" + Message = DefaultMessage.ConvertCase(_convention) }; if (_visibility == "Private") { response.Type = exception.GetType().Name; - response.Message = _namingConventionConverter(verboseMessage); + response.Message = verboseMessage.ConvertCase(_convention); } httpContext.Response.StatusCode = response.StatusCode; diff --git a/src/ResponseCrafter/ResponseCrafter.csproj b/src/ResponseCrafter/ResponseCrafter.csproj index 767af40..88d87cf 100644 --- a/src/ResponseCrafter/ResponseCrafter.csproj +++ b/src/ResponseCrafter/ResponseCrafter.csproj @@ -8,13 +8,13 @@ MIT pandatech.png Readme.md - 1.6.3 + 1.7.0 Pandatech.ResponseCrafter Pandatech, library, exception handler, exception, middleware, Api response ResponseCrafter Handling exceptions, custom Dtos. https://github.com/PandaTechAM/be-lib-response-crafter - EfCore Magic exception handling update + Naming convention has been added @@ -23,9 +23,11 @@ + + diff --git a/test/ResponseCrafter.Demo/Program.cs b/test/ResponseCrafter.Demo/Program.cs index 1768d2a..0750378 100644 --- a/test/ResponseCrafter.Demo/Program.cs +++ b/test/ResponseCrafter.Demo/Program.cs @@ -1,5 +1,6 @@ using EFCoreQueryMagic.Exceptions; -using ResponseCrafter; +using Humanizer; +using Microsoft.AspNetCore.Mvc; using ResponseCrafter.Demo; using ResponseCrafter.Enums; using ResponseCrafter.Extensions; @@ -9,8 +10,7 @@ builder.Services.AddEndpointsApiExplorer(); builder.Services.AddSwaggerGen(); -// builder.AddResponseCrafter(); -builder.AddResponseCrafter(NamingConvention.SnakeCaseUpper); +builder.AddResponseCrafter(NamingConvention.ToSnakeCase); builder.Services.AddControllers(); var app = builder.Build(); @@ -41,6 +41,26 @@ // return httpContext.GetToken(); }); +app.MapPost("/humanizer", ([FromQuery] string input, [FromQuery] NamingConvention convention) => +{ + switch (convention) + { + case NamingConvention.ToSnakeCase: + return Results.Ok(input.Underscore()); + case NamingConvention.ToKebabCase: + return Results.Ok(input.Underscore().Kebaberize()); + case NamingConvention.ToCamelCase: + return Results.Ok(input.Underscore().Camelize()); + case NamingConvention.ToPascalCase: + return Results.Ok(input.Underscore().Pascalize()); + case NamingConvention.ToTitleCase: + return Results.Ok(input.Underscore().Titleize()); + case NamingConvention.ToHumanCase: + return Results.Ok(input.Underscore().Humanize()); + } + + return Results.Ok(input); +}); app.MapGet("/server-error", (Exception) => throw new Exception("some_unhandled_exception")); app.MapGet("/bad-request", () => { throw new BadRequestException(errors); });