Skip to content
This repository was archived by the owner on Aug 30, 2025. It is now read-only.

Commit 38b827f

Browse files
committed
refactor: Improved property handler
BREAKING CHANGE: Removed dependecy from `PowerUtils.Text`
1 parent e5a01e4 commit 38b827f

File tree

10 files changed

+492
-416
lines changed

10 files changed

+492
-416
lines changed

README.md

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,6 @@
3939
## Dependencies <a name="dependencies"></a>
4040

4141
- Microsoft.AspNetCore.App [NuGet](https://www.nuget.org/packages/Microsoft.AspNetCore.App/)
42-
- PowerUtils.Text [NuGet](https://www.nuget.org/packages/PowerUtils.Text/)
4342

4443

4544

src/ApiProblemDetailsFactory.cs

Lines changed: 6 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -13,8 +13,8 @@ internal sealed class ApiProblemDetailsFactory : ProblemDetailsFactory, IProblem
1313
{
1414
private readonly IHttpContextAccessor _httpContextAccessor;
1515

16-
private readonly ApiBehaviorOptions _apiBehaviorOptions;
17-
private readonly ErrorHandlerOptions _errorHandlerOptions;
16+
private readonly IOptions<ApiBehaviorOptions> _apiBehaviorOptions;
17+
private readonly IOptions<ErrorHandlerOptions> _errorHandlerOptions;
1818

1919
public ApiProblemDetailsFactory(
2020
IHttpContextAccessor httpContextAccessor,
@@ -24,8 +24,8 @@ IOptions<ErrorHandlerOptions> errorHandlerOptions
2424
{
2525
_httpContextAccessor = httpContextAccessor;
2626

27-
_apiBehaviorOptions = apiBehaviorOptions?.Value ?? throw new ArgumentNullException(nameof(apiBehaviorOptions));
28-
_errorHandlerOptions = errorHandlerOptions?.Value ?? throw new ArgumentNullException(nameof(errorHandlerOptions));
27+
_apiBehaviorOptions = apiBehaviorOptions;
28+
_errorHandlerOptions = errorHandlerOptions;
2929
}
3030

3131

@@ -91,7 +91,7 @@ public ErrorProblemDetails Create(HttpContext httpContext, IEnumerable<KeyValueP
9191
{
9292
problemDetails.Errors
9393
.Add(
94-
_formatPropertyName(error.Key),
94+
_errorHandlerOptions.Value.PropertyHandler(error.Key),
9595
error.Value
9696
);
9797
}
@@ -186,7 +186,7 @@ private void _applyDefaults(HttpContext httpContext, ProblemDetails problemDetai
186186
{
187187
problemDetails.Status ??= httpContext.GetStatusCode();
188188

189-
if(_apiBehaviorOptions.ClientErrorMapping.TryGetValue(problemDetails.Status.Value, out var clientErrorData))
189+
if(_apiBehaviorOptions.Value.ClientErrorMapping.TryGetValue(problemDetails.Status.Value, out var clientErrorData))
190190
{
191191
problemDetails.Type ??= clientErrorData.Link;
192192
problemDetails.Title ??= clientErrorData.Title;
@@ -225,14 +225,5 @@ private static void _applyDetail(ProblemDetails problemDetails)
225225
_ => "An unexpected error has occurred."
226226
};
227227
}
228-
229-
// PropertyNamingPolicy.CamelCase is default value
230-
private string _formatPropertyName(string propertyName)
231-
=> _errorHandlerOptions.PropertyNamingPolicy switch
232-
{
233-
PropertyNamingPolicy.Original => propertyName,
234-
PropertyNamingPolicy.SnakeCase => propertyName.FormatToSnakeCase(),
235-
_ => propertyName.FormatToCamelCase(),
236-
};
237228
}
238229
}

src/ErrorHandlerOptions.cs

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,13 +24,24 @@ public class ExceptionMapper<TException> : IExceptionMapper
2424

2525
public class ErrorHandlerOptions
2626
{
27+
private PropertyNamingPolicy _propertyNamingPolicy = PropertyNamingPolicy.CamelCase;
2728
/// <summary>
2829
/// Default value: CamelCase
2930
/// </summary>
30-
public PropertyNamingPolicy PropertyNamingPolicy { get; set; } = PropertyNamingPolicy.CamelCase;
31+
public PropertyNamingPolicy PropertyNamingPolicy
32+
{
33+
get => _propertyNamingPolicy;
34+
set
35+
{
36+
_propertyNamingPolicy = value;
37+
PropertyHandler = Handlers.PropertyHandler.Create(_propertyNamingPolicy);
38+
}
39+
}
3140

3241
public IDictionary<Type, IExceptionMapper> ExceptionMappers { get; set; } = new Dictionary<Type, IExceptionMapper>();
3342

43+
internal Func<string, string> PropertyHandler { get; set; }
44+
3445
public ErrorHandlerOptions()
3546
{
3647
ExceptionMappers.Add(
@@ -48,6 +59,8 @@ public ErrorHandlerOptions()
4859
Handler = (_) => (StatusCodes.Status504GatewayTimeout, new Dictionary<string, ErrorDetails>())
4960
}
5061
);
62+
63+
PropertyHandler = Handlers.PropertyHandler.Create(_propertyNamingPolicy);
5164
}
5265
}
5366

src/Handlers/PropertyHandler.cs

Lines changed: 138 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,138 @@
1+
using System;
2+
using System.Globalization;
3+
using System.Text;
4+
5+
namespace PowerUtils.AspNetCore.ErrorHandler.Handlers
6+
{
7+
internal static class PropertyHandler
8+
{
9+
public static Func<string, string> Create(PropertyNamingPolicy propertyNamingPolicy)
10+
=> propertyNamingPolicy switch
11+
{
12+
PropertyNamingPolicy.Original => Original,
13+
PropertyNamingPolicy.SnakeCase => SnakeCase,
14+
_ => CamelCase,
15+
};
16+
17+
public static string Original(string property) => property;
18+
19+
public static string CamelCase(string property)
20+
{
21+
if(string.IsNullOrWhiteSpace(property))
22+
{
23+
return "";
24+
}
25+
26+
var propertyParts = property.Split('.');
27+
for(var count = 0; count < propertyParts.Length; count++)
28+
{
29+
// Prevent System.IndexOutOfRangeException: 'Index was outside the bounds of the array.' for cases "Hello."
30+
if(propertyParts[count].Length == 0)
31+
{
32+
continue;
33+
}
34+
35+
propertyParts[count] = char.ToLowerInvariant(propertyParts[count][0]) + propertyParts[count][1..];
36+
}
37+
38+
39+
return string.Join(".", propertyParts);
40+
}
41+
42+
public static string SnakeCase(string property)
43+
{
44+
if(string.IsNullOrWhiteSpace(property))
45+
{
46+
return "";
47+
}
48+
49+
var propertyParts = property.Split('.');
50+
51+
for(var count = 0; count < propertyParts.Length; count++)
52+
{
53+
propertyParts[count] = propertyParts[count].ToSnakeCase();
54+
}
55+
56+
57+
return string.Join(".", propertyParts);
58+
}
59+
60+
/// <summary>
61+
/// Convert a text to snake case format
62+
/// </summary>
63+
/// <param name="input">Text to be transformed</param>
64+
/// <returns>Text transformed</returns>
65+
public static string ToSnakeCase(this string input)
66+
{ // Reference1: https://stackoverflow.com/questions/63055621/how-to-convert-camel-case-to-snake-case-with-two-capitals-next-to-each-other
67+
// Reference1: https://github.com/efcore/EFCore.NamingConventions/blob/main/EFCore.NamingConventions/Internal/SnakeCaseNameRewriter.cs
68+
if(string.IsNullOrEmpty(input))
69+
{
70+
return input;
71+
}
72+
73+
var builder = new StringBuilder(input.Length + Math.Min(2, input.Length / 5));
74+
var previousCategory = default(UnicodeCategory?);
75+
76+
for(var currentIndex = 0; currentIndex < input.Length; currentIndex++)
77+
{
78+
var currentChar = input[currentIndex];
79+
if(currentChar == '_')
80+
{
81+
builder.Append('_');
82+
previousCategory = null;
83+
continue;
84+
}
85+
86+
var currentCategory = char.GetUnicodeCategory(currentChar);
87+
switch(currentCategory)
88+
{
89+
case UnicodeCategory.UppercaseLetter:
90+
case UnicodeCategory.TitlecaseLetter:
91+
_snakeCaseHandleUppercaseLetterAndTitlecaseLetter(
92+
input,
93+
currentIndex,
94+
previousCategory,
95+
builder
96+
);
97+
98+
currentChar = char.ToLower(currentChar, CultureInfo.InvariantCulture);
99+
break;
100+
101+
case UnicodeCategory.LowercaseLetter:
102+
case UnicodeCategory.DecimalDigitNumber:
103+
if(previousCategory == UnicodeCategory.SpaceSeparator)
104+
{
105+
builder.Append('_');
106+
}
107+
break;
108+
109+
default:
110+
if(previousCategory != null)
111+
{
112+
previousCategory = UnicodeCategory.SpaceSeparator;
113+
}
114+
continue;
115+
}
116+
117+
builder.Append(currentChar);
118+
previousCategory = currentCategory;
119+
}
120+
121+
return builder.ToString();
122+
}
123+
124+
private static void _snakeCaseHandleUppercaseLetterAndTitlecaseLetter(string input, int currentIndex, UnicodeCategory? previousCategory, StringBuilder builder)
125+
{
126+
if(previousCategory == UnicodeCategory.SpaceSeparator ||
127+
previousCategory == UnicodeCategory.LowercaseLetter ||
128+
(previousCategory != UnicodeCategory.DecimalDigitNumber &&
129+
previousCategory != null &&
130+
currentIndex > 0 &&
131+
currentIndex + 1 < input.Length &&
132+
char.IsLower(input[currentIndex + 1])))
133+
{
134+
builder.Append('_');
135+
}
136+
}
137+
}
138+
}

src/PowerUtils.AspNetCore.ErrorHandler.csproj

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -91,7 +91,6 @@
9191

9292
<ItemGroup>
9393
<PackageReference Include="Microsoft.AspNetCore.App" Version="2.2.8" />
94-
<PackageReference Include="PowerUtils.Text" Version="2.2.0" />
9594
</ItemGroup>
9695

9796

src/PropertiesNamingExtensions.cs

Lines changed: 0 additions & 48 deletions
This file was deleted.

0 commit comments

Comments
 (0)