Skip to content

Commit 82f4247

Browse files
feat: Implement custom model binder for analytics query enums
1 parent d8a6abb commit 82f4247

2 files changed

Lines changed: 76 additions & 1 deletion

File tree

Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
1+
using Microsoft.AspNetCore.Mvc.ModelBinding;
2+
3+
using XtremeIdiots.Portal.Repository.Abstractions.Constants.V1.Analytics;
4+
5+
namespace XtremeIdiots.Portal.Repository.Api.V1.Analytics;
6+
7+
/// <summary>
8+
/// Model binder for the analytics query enums (<see cref="AnalyticsCompareMode"/>,
9+
/// <see cref="AnalyticsAlignMode"/>, <see cref="AnalyticsBucket"/>). The typed API client serialises
10+
/// these as snake_case wire values (for example <c>previous_period</c>) rather than the enum member
11+
/// name, which the default enum binder rejects. This binder accepts snake_case, the enum member name
12+
/// (case-insensitive) and numeric values.
13+
/// </summary>
14+
public sealed class AnalyticsEnumModelBinder : IModelBinder
15+
{
16+
public Task BindModelAsync(ModelBindingContext bindingContext)
17+
{
18+
ArgumentNullException.ThrowIfNull(bindingContext);
19+
20+
var modelName = bindingContext.ModelName;
21+
var valueProviderResult = bindingContext.ValueProvider.GetValue(modelName);
22+
23+
if (valueProviderResult == ValueProviderResult.None)
24+
{
25+
return Task.CompletedTask;
26+
}
27+
28+
var rawValue = valueProviderResult.FirstValue;
29+
if (string.IsNullOrWhiteSpace(rawValue))
30+
{
31+
return Task.CompletedTask;
32+
}
33+
34+
bindingContext.ModelState.SetModelValue(modelName, valueProviderResult);
35+
36+
var enumType = bindingContext.ModelMetadata.UnderlyingOrModelType;
37+
var normalized = rawValue.Replace("_", string.Empty, StringComparison.Ordinal);
38+
39+
if (Enum.TryParse(enumType, normalized, ignoreCase: true, out var parsed))
40+
{
41+
bindingContext.Result = ModelBindingResult.Success(parsed);
42+
}
43+
else
44+
{
45+
bindingContext.ModelState.TryAddModelError(modelName, $"The value '{rawValue}' is not valid.");
46+
}
47+
48+
return Task.CompletedTask;
49+
}
50+
}
51+
52+
/// <summary>
53+
/// Supplies <see cref="AnalyticsEnumModelBinder"/> for the analytics query enum types.
54+
/// </summary>
55+
public sealed class AnalyticsEnumModelBinderProvider : IModelBinderProvider
56+
{
57+
private static readonly HashSet<Type> SupportedTypes =
58+
[
59+
typeof(AnalyticsCompareMode),
60+
typeof(AnalyticsAlignMode),
61+
typeof(AnalyticsBucket)
62+
];
63+
64+
public IModelBinder? GetBinder(ModelBinderProviderContext context)
65+
{
66+
ArgumentNullException.ThrowIfNull(context);
67+
68+
return SupportedTypes.Contains(context.Metadata.UnderlyingOrModelType)
69+
? new AnalyticsEnumModelBinder()
70+
: null;
71+
}
72+
}

src/XtremeIdiots.Portal.Repository.Api.V1/Program.cs

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -88,7 +88,10 @@
8888
// Add services to the container.
8989
builder.Services.AddMicrosoftIdentityWebApiAuthentication(builder.Configuration);
9090

91-
builder.Services.AddControllers().AddNewtonsoftJson(options =>
91+
builder.Services.AddControllers(options =>
92+
{
93+
options.ModelBinderProviders.Insert(0, new XtremeIdiots.Portal.Repository.Api.V1.Analytics.AnalyticsEnumModelBinderProvider());
94+
}).AddNewtonsoftJson(options =>
9295
{
9396
options.SerializerSettings.Converters.Add(new UtcDateTimeJsonConverter());
9497
options.SerializerSettings.Converters.Add(new StringEnumConverter());

0 commit comments

Comments
 (0)