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 a1d0d6b..656e1e3 100644
--- a/src/ResponseCrafter/PandaExceptionHandler.cs
+++ b/src/ResponseCrafter/PandaExceptionHandler.cs
@@ -1,14 +1,17 @@
-using System.Reflection;
-using BaseConverter.Exceptions;
+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;
@@ -17,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'.");
@@ -39,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);
@@ -65,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)
@@ -73,10 +93,7 @@ private async Task HandleBaseConverterExceptionAsync(HttpContext httpContext,
{
case InputValidationException _:
case UnsupportedCharacterException _:
- var exceptionName = importException.GetType().Name;
- var formattedMessage =
- $"{exceptionName} in Base Converter: {_namingConventionConverter(importException.Message)}";
- var mappedException = new BadRequestException(formattedMessage);
+ var mappedException = new BadRequestException(importException.Message.ConvertCase(_convention));
await HandleApiExceptionAsync(httpContext, mappedException, cancellationToken);
break;
default:
@@ -94,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:
@@ -110,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;
@@ -134,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)
{
@@ -144,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;
@@ -161,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: {_namingConventionConverter(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)
{
@@ -204,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); });