Skip to content

Commit d22d23b

Browse files
committed
feat(notifications): add custom API conventions configuration for snake_case and kebab-case
1 parent 7250e22 commit d22d23b

3 files changed

Lines changed: 114 additions & 13 deletions

File tree

Lines changed: 102 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,102 @@
1+
using DotCruz.Notifications.Api.Filters;
2+
using Microsoft.AspNetCore.Mvc.ApplicationModels;
3+
using Microsoft.AspNetCore.Mvc.ModelBinding;
4+
using System.Globalization;
5+
using System.Text.Json;
6+
using System.Text.Json.Serialization;
7+
using System.Text.RegularExpressions;
8+
9+
namespace DotCruz.Notifications.Api.Configurations;
10+
11+
public static class ApiConventionsConfiguration
12+
{
13+
public static IMvcBuilder AddApiConventions(this IServiceCollection services)
14+
{
15+
services.ConfigureHttpJsonOptions(options =>
16+
{
17+
options.SerializerOptions.PropertyNamingPolicy = JsonNamingPolicy.SnakeCaseLower;
18+
options.SerializerOptions.DictionaryKeyPolicy = JsonNamingPolicy.SnakeCaseLower;
19+
options.SerializerOptions.Converters.Add(new JsonStringEnumConverter());
20+
});
21+
22+
return services
23+
.AddControllers(options =>
24+
{
25+
options.Filters.Add<ExceptionFilter>();
26+
27+
options.Conventions.Add(
28+
new RouteTokenTransformerConvention(new KebabCaseParameterTransformer()));
29+
30+
ReplaceQueryValueProvider(options.ValueProviderFactories);
31+
})
32+
.AddJsonOptions(options =>
33+
{
34+
options.JsonSerializerOptions.PropertyNamingPolicy = JsonNamingPolicy.SnakeCaseLower;
35+
options.JsonSerializerOptions.DictionaryKeyPolicy = JsonNamingPolicy.SnakeCaseLower;
36+
options.JsonSerializerOptions.Converters.Add(new JsonStringEnumConverter());
37+
});
38+
}
39+
40+
private static void ReplaceQueryValueProvider(IList<IValueProviderFactory> factories)
41+
{
42+
var defaultFactory = factories.OfType<QueryStringValueProviderFactory>().FirstOrDefault();
43+
44+
if (defaultFactory is null)
45+
{
46+
factories.Add(new SnakeCaseQueryValueProviderFactory());
47+
return;
48+
}
49+
50+
var index = factories.IndexOf(defaultFactory);
51+
factories[index] = new SnakeCaseQueryValueProviderFactory();
52+
}
53+
}
54+
55+
public sealed partial class KebabCaseParameterTransformer : IOutboundParameterTransformer
56+
{
57+
public string? TransformOutbound(object? value)
58+
{
59+
var input = value?.ToString();
60+
61+
return string.IsNullOrEmpty(input)
62+
? input
63+
: WordBoundaryRegex().Replace(input, "$1-$2").ToLowerInvariant();
64+
}
65+
66+
[GeneratedRegex("([a-z0-9])([A-Z])")]
67+
private static partial Regex WordBoundaryRegex();
68+
}
69+
70+
public sealed class SnakeCaseQueryValueProviderFactory : IValueProviderFactory
71+
{
72+
public Task CreateValueProviderAsync(ValueProviderFactoryContext context)
73+
{
74+
ArgumentNullException.ThrowIfNull(context);
75+
76+
var query = context.ActionContext.HttpContext.Request.Query;
77+
context.ValueProviders.Add(
78+
new SnakeCaseQueryValueProvider(BindingSource.Query, query, CultureInfo.InvariantCulture));
79+
80+
return Task.CompletedTask;
81+
}
82+
}
83+
84+
public sealed class SnakeCaseQueryValueProvider(
85+
BindingSource bindingSource,
86+
IQueryCollection values,
87+
CultureInfo culture) : QueryStringValueProvider(bindingSource, values, culture)
88+
{
89+
public override bool ContainsPrefix(string prefix) => base.ContainsPrefix(ToSnakeCase(prefix));
90+
91+
public override ValueProviderResult GetValue(string key) => base.GetValue(ToSnakeCase(key));
92+
93+
private static string ToSnakeCase(string key)
94+
{
95+
if (string.IsNullOrEmpty(key))
96+
{
97+
return key;
98+
}
99+
100+
return string.Join('.', key.Split('.').Select(JsonNamingPolicy.SnakeCaseLower.ConvertName));
101+
}
102+
}

src/DotCruz.Notifications.Api/Program.cs

Lines changed: 3 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
using System.Text.Json.Serialization;
1+
using DotCruz.Notifications.Api.Configurations;
22
using DotCruz.Notifications.Api.Filters;
33
using DotCruz.Notifications.Api.Middlewares;
44
using DotCruz.Notifications.Application;
@@ -12,15 +12,7 @@
1212

1313
// Add services to the container.
1414

15-
builder.Services.AddControllers().AddJsonOptions(options =>
16-
{
17-
options.JsonSerializerOptions.Converters.Add(new JsonStringEnumConverter());
18-
});
19-
20-
builder.Services.ConfigureHttpJsonOptions(options =>
21-
{
22-
options.SerializerOptions.Converters.Add(new JsonStringEnumConverter());
23-
});
15+
builder.Services.AddApiConventions();
2416

2517
// Learn more about configuring OpenAPI at https://aka.ms/aspnet/openapi
2618
builder.Services.AddOpenApi();
@@ -32,7 +24,7 @@
3224
builder.Services.AddApplication();
3325
builder.Services.AddInfrastructure(builder.Configuration);
3426

35-
builder.Services.AddMvc(options => options.Filters.Add<ExceptionFilter>());
27+
3628

3729
var app = builder.Build();
3830

tests/WebApi.Test/NotificationClassFixture.cs

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
using System.Net.Http.Headers;
22
using System.Net.Http.Json;
3+
using System.Text.Json;
34

45
namespace WebApi.Test;
56

@@ -8,6 +9,12 @@ public class NotificationClassFixture : IClassFixture<CustomWebApplicationFactor
89
private readonly HttpClient _httpClient;
910
private readonly CustomWebApplicationFactory _factory;
1011

12+
private static readonly JsonSerializerOptions JsonSerializerOptions = new()
13+
{
14+
PropertyNamingPolicy = JsonNamingPolicy.SnakeCaseLower,
15+
DictionaryKeyPolicy = JsonNamingPolicy.SnakeCaseLower
16+
};
17+
1118
public NotificationClassFixture(CustomWebApplicationFactory factory)
1219
{
1320
_httpClient = factory.CreateClient();
@@ -24,15 +31,15 @@ protected async Task<HttpResponseMessage> DoPost(
2431
ChangeRequestCulture(culture);
2532
AuthorizeRequest(token);
2633

27-
return await _httpClient.PostAsJsonAsync(endpoint, request);
34+
return await _httpClient.PostAsJsonAsync(endpoint, request, JsonSerializerOptions);
2835
}
2936

3037
protected async Task<HttpResponseMessage> DoPut(string method, object request, string token, string culture = "en")
3138
{
3239
ChangeRequestCulture(culture);
3340
AuthorizeRequest(token);
3441

35-
return await _httpClient.PutAsJsonAsync(method, request);
42+
return await _httpClient.PutAsJsonAsync(method, request, JsonSerializerOptions);
3643
}
3744

3845
protected async Task<HttpResponseMessage> DoDelete(string method, string token, string culture = "en")

0 commit comments

Comments
 (0)