自訂 Query Strings 轉換格式
Query Strings 預設格式如 https://localhost:<port>/purchaseorders?purchaseordernumber=PO12345678
,此範例說明如何轉換為 SnakeCase
,即https://localhost:<port>/purchaseorders?purchase_order_number=PO12345678
。
範例
建立
ASP.NET Core Web API (.NET 8)
專案新增
DTO (Data Transfer Object)
封裝 Query Strings
1
2
3
4namespace Purchasing.UseCases.Dtos
{
public record PurchaseOrderDto(DateTime? StartDate, DateTime? EndDate);
}新增
API Controller
Response 的部分不影響這個範例,所以省略實作細節。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16using Microsoft.AspNetCore.Mvc;
using Purchasing.UseCases.Dtos;
namespace Purchasing.Api.Controllers
{
[ ]
[ ]
public class PurchaseOrdersController : ControllerBase
{
[ ]
public async Task<IActionResult> ListPurchaseOrdersAsync([FromQuery] PurchaseOrderDto dto)
{
// ...
}
}
}實作
SnakeCase
的轉換方法讀者可以自行實作,在這邊筆者直接參考 Newtonsoft.Json/Utilities/StringUtils.cs。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89using System.Text;
namespace Purchasing.Api.ModelBinding
{
internal static class SnakeCaseQueryStringValueExtensions
{
private enum SeparatedCaseState
{
Start,
Lower,
Upper,
NewWord
}
internal static string ToSnakeCase(this string s) => ToSeparatedCase(s, '_');
internal static string ToKebabCase(string s) => ToSeparatedCase(s, '-');
private static string ToSeparatedCase(string s, char separator)
{
if (string.IsNullOrEmpty(s))
{
return s;
}
StringBuilder sb = new StringBuilder();
SeparatedCaseState state = SeparatedCaseState.Start;
for (int i = 0; i < s.Length; i++)
{
if (s[i] == ' ')
{
if (state != SeparatedCaseState.Start)
{
state = SeparatedCaseState.NewWord;
}
}
else if (char.IsUpper(s[i]))
{
switch (state)
{
case SeparatedCaseState.Upper:
bool hasNext = i + 1 < s.Length;
if (i > 0 && hasNext)
{
char nextChar = s[i + 1];
if (!char.IsUpper(nextChar) && nextChar != separator)
{
sb.Append(separator);
}
}
break;
case SeparatedCaseState.Lower:
case SeparatedCaseState.NewWord:
sb.Append(separator);
break;
}
char c;
c = char.ToLower(s[i], CultureInfo.InvariantCulture);
c = char.ToLowerInvariant(s[i]);
sb.Append(c);
state = SeparatedCaseState.Upper;
}
else if (s[i] == separator)
{
sb.Append(separator);
state = SeparatedCaseState.Start;
}
else
{
if (state == SeparatedCaseState.NewWord)
{
sb.Append(separator);
}
sb.Append(s[i]);
state = SeparatedCaseState.Lower;
}
}
return sb.ToString();
}
}
}新增
SnakeCaseQueryStringValueProvider
繼承
QueryStringValueProvider
,覆寫GetValue(string key)
,把 key 轉換為SnakeCase
,其中 key 為 DTO 的 Property Name。1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21using Microsoft.AspNetCore.Mvc.ModelBinding;
using System.Globalization;
namespace Purchasing.Api.ModelBinding
{
internal class SnakeCaseQueryStringValueProvider : QueryStringValueProvider
{
public SnakeCaseQueryStringValueProvider(
BindingSource bindingSource,
IQueryCollection values,
CultureInfo? culture)
: base(bindingSource, values, culture)
{
}
public override ValueProviderResult GetValue(string key)
{
return base.GetValue(key.ToSnakeCase());
}
}
}新增
SnakeCaseQueryStringValueProviderFactory
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25using Microsoft.AspNetCore.Mvc.ModelBinding;
using System.Globalization;
namespace Goldtek.Services.Workflow.Api.ModelBinding
{
internal class SnakeCaseQueryStringValueProviderFactory : IValueProviderFactory
{
public Task CreateValueProviderAsync(ValueProviderFactoryContext context)
{
_ = context ?? throw new ArgumentNullException(nameof(context));
var query = context.ActionContext.HttpContext.Request.Query;
if (query?.Count > 0)
{
context.ValueProviders.Add(
new SnakeCaseQueryStringValueProvider(
BindingSource.Query,
query,
CultureInfo.CurrentCulture));
}
return Task.CompletedTask;
}
}
}在
Program.cs
配置SnakeCaseQueryStringValueProviderFactory
取代預設的
ValueProviderFactory
1
2
3
4
5
6
7
8
9builder.Services.AddControllers(options =>
{
var index = options.ValueProviderFactories.IndexOf(
options.ValueProviderFactories.OfType<QueryStringValueProviderFactory>()
.Single());
options.ValueProviderFactories[index] =
new SnakeCaseQueryStringValueProviderFactory();
});