Skip to content

Commit 0900ced

Browse files
Copilotcaptainsafia
andcommitted
Made SerializerOptions property public and added tests for formatting complex keys
Co-authored-by: captainsafia <[email protected]>
1 parent f92b43e commit 0900ced

File tree

11 files changed

+396
-40
lines changed

11 files changed

+396
-40
lines changed

package-lock.json

Lines changed: 2 additions & 2 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

src/Http/Http.Abstractions/src/PublicAPI.Unshipped.txt

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,8 @@ Microsoft.AspNetCore.Http.Validation.ValidateContext.ValidationContext.get -> Sy
2828
Microsoft.AspNetCore.Http.Validation.ValidateContext.ValidationContext.set -> void
2929
Microsoft.AspNetCore.Http.Validation.ValidateContext.ValidationErrors.get -> System.Collections.Generic.Dictionary<string!, string![]!>?
3030
Microsoft.AspNetCore.Http.Validation.ValidateContext.ValidationErrors.set -> void
31+
Microsoft.AspNetCore.Http.Validation.ValidateContext.SerializerOptions.get -> System.Text.Json.JsonSerializerOptions?
32+
Microsoft.AspNetCore.Http.Validation.ValidateContext.SerializerOptions.set -> void
3133
Microsoft.AspNetCore.Http.Validation.ValidateContext.ValidationOptions.get -> Microsoft.AspNetCore.Http.Validation.ValidationOptions!
3234
Microsoft.AspNetCore.Http.Validation.ValidateContext.ValidationOptions.set -> void
3335
Microsoft.AspNetCore.Http.Validation.ValidationOptions

src/Http/Http.Abstractions/src/Validation/ValidateContext.cs

Lines changed: 1 addition & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -66,37 +66,7 @@ public sealed class ValidateContext
6666
/// When available, property names in validation errors will be formatted according to the
6767
/// PropertyNamingPolicy and JsonPropertyName attributes.
6868
/// </summary>
69-
internal JsonSerializerOptions? SerializerOptions
70-
{
71-
get
72-
{
73-
// If explicit options have been set, use those (primarily for testing)
74-
if (_serializerOptions is not null)
75-
{
76-
return _serializerOptions;
77-
}
78-
79-
// Otherwise try to get them from DI
80-
var jsonOptionsType = Type.GetType("Microsoft.AspNetCore.Http.Json.JsonOptions, Microsoft.AspNetCore.Http.Extensions");
81-
if (jsonOptionsType is null)
82-
{
83-
return null;
84-
}
85-
86-
var jsonOptionsService = ValidationContext.GetService(jsonOptionsType);
87-
if (jsonOptionsService is null)
88-
{
89-
return null;
90-
}
91-
92-
// Get the SerializerOptions property via reflection
93-
var serializerOptionsProperty = jsonOptionsType.GetProperty("SerializerOptions");
94-
return serializerOptionsProperty?.GetValue(jsonOptionsService) as JsonSerializerOptions;
95-
}
96-
set => _serializerOptions = value;
97-
}
98-
99-
private JsonSerializerOptions? _serializerOptions;
69+
public JsonSerializerOptions? SerializerOptions { get; set; }
10070

10171
internal void AddValidationError(string key, string[] error)
10272
{
Lines changed: 219 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,219 @@
1+
// Licensed to the .NET Foundation under one or more agreements.
2+
// The .NET Foundation licenses this file to you under the MIT license.
3+
4+
#pragma warning disable ASP0029 // Type is for evaluation purposes only and is subject to change or removal in future updates. Suppress this diagnostic to proceed.
5+
6+
using System.ComponentModel.DataAnnotations;
7+
using System.Globalization;
8+
using System.Text.Json;
9+
using System.Text.Json.Serialization;
10+
11+
namespace Microsoft.AspNetCore.Http.Validation;
12+
13+
public class ValidateContextTests
14+
{
15+
[Fact]
16+
public void AddValidationError_FormatsCamelCaseKeys_WithSerializerOptions()
17+
{
18+
// Arrange
19+
var context = CreateValidateContext();
20+
context.SerializerOptions = new JsonSerializerOptions
21+
{
22+
PropertyNamingPolicy = JsonNamingPolicy.CamelCase
23+
};
24+
25+
// Act
26+
context.AddValidationError("PropertyName", ["Error"]);
27+
28+
// Assert
29+
Assert.NotNull(context.ValidationErrors);
30+
Assert.True(context.ValidationErrors.ContainsKey("propertyName"));
31+
}
32+
33+
[Fact]
34+
public void AddValidationError_FormatsSimpleKeys_WithSerializerOptions()
35+
{
36+
// Arrange
37+
var context = CreateValidateContext();
38+
context.SerializerOptions = new JsonSerializerOptions
39+
{
40+
PropertyNamingPolicy = JsonNamingPolicy.CamelCase
41+
};
42+
43+
// Act
44+
context.AddValidationError("ThisIsAProperty", ["Error"]);
45+
46+
// Assert
47+
Assert.NotNull(context.ValidationErrors);
48+
Assert.True(context.ValidationErrors.ContainsKey("thisIsAProperty"));
49+
}
50+
51+
[Fact]
52+
public void FormatComplexKey_FormatsNestedProperties_WithDots()
53+
{
54+
// Arrange
55+
var context = CreateValidateContext();
56+
context.SerializerOptions = new JsonSerializerOptions
57+
{
58+
PropertyNamingPolicy = JsonNamingPolicy.CamelCase
59+
};
60+
61+
// Act
62+
context.AddValidationError("Customer.Address.Street", ["Error"]);
63+
64+
// Assert
65+
Assert.NotNull(context.ValidationErrors);
66+
Assert.True(context.ValidationErrors.ContainsKey("customer.address.street"));
67+
}
68+
69+
[Fact]
70+
public void FormatComplexKey_PreservesArrayIndices()
71+
{
72+
// Arrange
73+
var context = CreateValidateContext();
74+
context.SerializerOptions = new JsonSerializerOptions
75+
{
76+
PropertyNamingPolicy = JsonNamingPolicy.CamelCase
77+
};
78+
79+
// Act
80+
context.AddValidationError("Items[0].ProductName", ["Error"]);
81+
82+
// Assert
83+
Assert.NotNull(context.ValidationErrors);
84+
Assert.True(context.ValidationErrors.ContainsKey("items[0].productName"));
85+
Assert.False(context.ValidationErrors.ContainsKey("items[0].ProductName"));
86+
}
87+
88+
[Fact]
89+
public void FormatComplexKey_HandlesMultipleArrayIndices()
90+
{
91+
// Arrange
92+
var context = CreateValidateContext();
93+
context.SerializerOptions = new JsonSerializerOptions
94+
{
95+
PropertyNamingPolicy = JsonNamingPolicy.CamelCase
96+
};
97+
98+
// Act
99+
context.AddValidationError("Orders[0].Items[1].ProductName", ["Error"]);
100+
101+
// Assert
102+
Assert.NotNull(context.ValidationErrors);
103+
Assert.True(context.ValidationErrors.ContainsKey("orders[0].items[1].productName"));
104+
}
105+
106+
[Fact]
107+
public void FormatComplexKey_HandlesNestedArraysWithoutProperties()
108+
{
109+
// Arrange
110+
var context = CreateValidateContext();
111+
context.SerializerOptions = new JsonSerializerOptions
112+
{
113+
PropertyNamingPolicy = JsonNamingPolicy.CamelCase
114+
};
115+
116+
// Act
117+
context.AddValidationError("Matrix[0][1]", ["Error"]);
118+
119+
// Assert
120+
Assert.NotNull(context.ValidationErrors);
121+
Assert.True(context.ValidationErrors.ContainsKey("matrix[0][1]"));
122+
}
123+
124+
[Fact]
125+
public void FormatKey_ReturnsOriginalKey_WhenSerializerOptionsIsNull()
126+
{
127+
// Arrange
128+
var context = CreateValidateContext();
129+
context.SerializerOptions = null;
130+
131+
// Act
132+
context.AddValidationError("PropertyName", ["Error"]);
133+
134+
// Assert
135+
Assert.NotNull(context.ValidationErrors);
136+
Assert.True(context.ValidationErrors.ContainsKey("PropertyName"));
137+
}
138+
139+
[Fact]
140+
public void FormatKey_ReturnsOriginalKey_WhenPropertyNamingPolicyIsNull()
141+
{
142+
// Arrange
143+
var context = CreateValidateContext();
144+
context.SerializerOptions = new JsonSerializerOptions
145+
{
146+
PropertyNamingPolicy = null
147+
};
148+
149+
// Act
150+
context.AddValidationError("PropertyName", ["Error"]);
151+
152+
// Assert
153+
Assert.NotNull(context.ValidationErrors);
154+
Assert.True(context.ValidationErrors.ContainsKey("PropertyName"));
155+
}
156+
157+
[Fact]
158+
public void FormatKey_AppliesKebabCaseNamingPolicy()
159+
{
160+
// Arrange
161+
var context = CreateValidateContext();
162+
context.SerializerOptions = new JsonSerializerOptions
163+
{
164+
PropertyNamingPolicy = new KebabCaseNamingPolicy()
165+
};
166+
167+
// Act
168+
context.AddValidationError("ProductName", ["Error"]);
169+
context.AddValidationError("OrderItems[0].ProductName", ["Error"]);
170+
171+
// Assert
172+
Assert.NotNull(context.ValidationErrors);
173+
Assert.True(context.ValidationErrors.ContainsKey("product-name"));
174+
Assert.True(context.ValidationErrors.ContainsKey("order-items[0].product-name"));
175+
}
176+
177+
private static ValidateContext CreateValidateContext()
178+
{
179+
var serviceProvider = new EmptyServiceProvider();
180+
var options = new ValidationOptions();
181+
var validationContext = new ValidationContext(new object(), serviceProvider, null);
182+
183+
return new ValidateContext
184+
{
185+
ValidationContext = validationContext,
186+
ValidationOptions = options
187+
};
188+
}
189+
190+
private class KebabCaseNamingPolicy : JsonNamingPolicy
191+
{
192+
public override string ConvertName(string name)
193+
{
194+
if (string.IsNullOrEmpty(name))
195+
{
196+
return name;
197+
}
198+
199+
var result = string.Empty;
200+
201+
for (int i = 0; i < name.Length; i++)
202+
{
203+
if (i > 0 && char.IsUpper(name[i]))
204+
{
205+
result += "-";
206+
}
207+
208+
result += char.ToLower(name[i], CultureInfo.InvariantCulture);
209+
}
210+
211+
return result;
212+
}
213+
}
214+
215+
private class EmptyServiceProvider : IServiceProvider
216+
{
217+
public object? GetService(Type serviceType) => null;
218+
}
219+
}

src/Http/Routing/src/ValidationEndpointFilterFactory.cs

Lines changed: 15 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
using System.ComponentModel.DataAnnotations;
77
using System.Linq;
88
using System.Reflection;
9+
using System.Text.Json;
910
using Microsoft.AspNetCore.Http.Metadata;
1011
using Microsoft.Extensions.DependencyInjection;
1112
using Microsoft.Extensions.Options;
@@ -55,7 +56,18 @@ public static EndpointFilterDelegate Create(EndpointFilterFactoryContext context
5556
{
5657
ValidateContext? validateContext = null;
5758

58-
// JsonOptions will be retrieved directly by ValidateContext.SerializerOptions property
59+
// JsonOptions will be retrieved from DI to set the SerializerOptions
60+
var jsonOptionsType = Type.GetType("Microsoft.AspNetCore.Http.Json.JsonOptions, Microsoft.AspNetCore.Http.Extensions");
61+
JsonSerializerOptions? serializerOptions = null;
62+
if (jsonOptionsType is not null)
63+
{
64+
var jsonOptions = context.HttpContext.RequestServices.GetService(jsonOptionsType);
65+
if (jsonOptions is not null)
66+
{
67+
var serializerOptionsProperty = jsonOptionsType.GetProperty("SerializerOptions");
68+
serializerOptions = serializerOptionsProperty?.GetValue(jsonOptions) as JsonSerializerOptions;
69+
}
70+
}
5971

6072
for (var i = 0; i < context.Arguments.Count; i++)
6173
{
@@ -75,7 +87,8 @@ public static EndpointFilterDelegate Create(EndpointFilterFactoryContext context
7587
validateContext = new ValidateContext
7688
{
7789
ValidationOptions = options,
78-
ValidationContext = validationContext
90+
ValidationContext = validationContext,
91+
SerializerOptions = serializerOptions
7992
};
8093
}
8194
else

src/JSInterop/Microsoft.JSInterop.JS/src/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -44,4 +44,4 @@
4444
"rimraf": "^5.0.5",
4545
"typescript": "^5.3.3"
4646
}
47-
}
47+
}
Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
{
2+
"name": "@microsoft/dotnet-js-interop",
3+
"version": "10.0.0-dev",
4+
"description": "Provides abstractions and features for interop between .NET and JavaScript code.",
5+
"main": "dist/src/Microsoft.JSInterop.js",
6+
"types": "dist/src/Microsoft.JSInterop.d.ts",
7+
"type": "module",
8+
"scripts": {
9+
"clean": "rimraf ./dist",
10+
"test": "jest",
11+
"test:watch": "jest --watch",
12+
"test:debug": "node --nolazy --inspect-brk ./node_modules/jest/bin/jest.js --runInBand --colors --verbose",
13+
"build": "npm run clean && npm run build:esm",
14+
"build:lint": "eslint -c .eslintrc.json --ext .ts ./src",
15+
"build:esm": "tsc --project ./tsconfig.json",
16+
"get-version": "node -e \"const { name, version } = require('./package.json'); console.log(`${name};${version}`);\""
17+
},
18+
"repository": {
19+
"type": "git",
20+
"url": "git+https://github.com/dotnet/extensions.git"
21+
},
22+
"author": "Microsoft",
23+
"license": "MIT",
24+
"bugs": {
25+
"url": "https://github.com/dotnet/aspnetcore/issues"
26+
},
27+
"homepage": "https://github.com/dotnet/aspnetcore/tree/main/src/JSInterop",
28+
"files": [
29+
"dist/**"
30+
],
31+
"devDependencies": {
32+
"@babel/core": "^7.23.6",
33+
"@babel/preset-env": "^7.23.6",
34+
"@babel/preset-typescript": "^7.26.0",
35+
"@typescript-eslint/eslint-plugin": "^6.15.0",
36+
"@typescript-eslint/parser": "^6.15.0",
37+
"babel-jest": "^29.7.0",
38+
"eslint": "^8.56.0",
39+
"eslint-plugin-jsdoc": "^46.9.1",
40+
"eslint-plugin-prefer-arrow": "^1.2.3",
41+
"jest": "^29.7.0",
42+
"jest-environment-jsdom": "^29.7.0",
43+
"jest-junit": "^16.0.0",
44+
"rimraf": "^5.0.5",
45+
"typescript": "^5.3.3"
46+
}
47+
}

0 commit comments

Comments
 (0)