Skip to content

Commit dbc67d4

Browse files
author
Bart Koelman
committed
Use STJ naming convention on special-cased code paths
1 parent 0487b73 commit dbc67d4

File tree

12 files changed

+152
-49
lines changed

12 files changed

+152
-49
lines changed

src/JsonApiDotNetCore/Configuration/IJsonApiOptions.cs

Lines changed: 10 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,9 @@
11
using System;
22
using System.Data;
3+
using System.Text.Json;
34
using JsonApiDotNetCore.Resources.Annotations;
45
using JsonApiDotNetCore.Serialization.Objects;
56
using Newtonsoft.Json;
6-
using Newtonsoft.Json.Serialization;
77

88
namespace JsonApiDotNetCore.Configuration
99
{
@@ -12,15 +12,6 @@ namespace JsonApiDotNetCore.Configuration
1212
/// </summary>
1313
public interface IJsonApiOptions
1414
{
15-
internal NamingStrategy SerializerNamingStrategy
16-
{
17-
get
18-
{
19-
var contractResolver = SerializerSettings.ContractResolver as DefaultContractResolver;
20-
return contractResolver?.NamingStrategy ?? JsonApiOptions.DefaultNamingStrategy;
21-
}
22-
}
23-
2415
/// <summary>
2516
/// The URL prefix to use for exposed endpoints.
2617
/// </summary>
@@ -145,19 +136,19 @@ internal NamingStrategy SerializerNamingStrategy
145136
/// </summary>
146137
IsolationLevel? TransactionIsolationLevel { get; }
147138

139+
JsonSerializerSettings SerializerSettings { get; }
140+
148141
/// <summary>
149-
/// Specifies the settings that are used by the <see cref="JsonSerializer" />. Note that at some places a few settings are ignored, to ensure JSON:API
150-
/// spec compliance.
142+
/// Specifies the settings that are used by the <see cref="System.Text.Json.JsonSerializer" />. Note that at some places a few settings are ignored, to
143+
/// ensure JSON:API spec compliance.
144+
/// </summary>
151145
/// <example>
152-
/// The next example changes the naming convention to kebab casing.
146+
/// The next example sets the naming convention to camel casing.
153147
/// <code><![CDATA[
154-
/// options.SerializerSettings.ContractResolver = new DefaultContractResolver
155-
/// {
156-
/// NamingStrategy = new KebabCaseNamingStrategy()
157-
/// };
148+
/// options.SerializerOptions.PropertyNamingPolicy = JsonNamingPolicy.CamelCase;
149+
/// options.SerializerOptions.DictionaryKeyPolicy = JsonNamingPolicy.CamelCase;
158150
/// ]]></code>
159151
/// </example>
160-
/// </summary>
161-
JsonSerializerSettings SerializerSettings { get; }
152+
JsonSerializerOptions SerializerOptions { get; }
162153
}
163154
}

src/JsonApiDotNetCore/Configuration/JsonApiOptions.cs

Lines changed: 9 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
using System.Data;
2+
using System.Text.Json;
23
using JetBrains.Annotations;
34
using JsonApiDotNetCore.Resources.Annotations;
45
using Newtonsoft.Json;
@@ -10,8 +11,6 @@ namespace JsonApiDotNetCore.Configuration
1011
[PublicAPI]
1112
public sealed class JsonApiOptions : IJsonApiOptions
1213
{
13-
internal static readonly NamingStrategy DefaultNamingStrategy = new CamelCaseNamingStrategy();
14-
1514
// Workaround for https://github.com/dotnet/efcore/issues/21026
1615
internal bool DisableTopPagination { get; set; }
1716
internal bool DisableChildrenPagination { get; set; }
@@ -73,13 +72,19 @@ public sealed class JsonApiOptions : IJsonApiOptions
7372
/// <inheritdoc />
7473
public IsolationLevel? TransactionIsolationLevel { get; set; }
7574

76-
/// <inheritdoc />
7775
public JsonSerializerSettings SerializerSettings { get; } = new()
7876
{
7977
ContractResolver = new DefaultContractResolver
8078
{
81-
NamingStrategy = DefaultNamingStrategy
79+
NamingStrategy = new CamelCaseNamingStrategy()
8280
}
8381
};
82+
83+
/// <inheritdoc />
84+
public JsonSerializerOptions SerializerOptions { get; } = new()
85+
{
86+
PropertyNamingPolicy = JsonNamingPolicy.CamelCase,
87+
DictionaryKeyPolicy = JsonNamingPolicy.CamelCase
88+
};
8489
}
8590
}

src/JsonApiDotNetCore/Configuration/ResourceGraphBuilder.cs

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -243,13 +243,15 @@ private Type TypeOrElementType(Type type)
243243

244244
private string FormatResourceName(Type resourceType)
245245
{
246-
var formatter = new ResourceNameFormatter(_options.SerializerNamingStrategy);
246+
var formatter = new ResourceNameFormatter(_options.SerializerOptions.PropertyNamingPolicy);
247247
return formatter.FormatResourceName(resourceType);
248248
}
249249

250250
private string FormatPropertyName(PropertyInfo resourceProperty)
251251
{
252-
return _options.SerializerNamingStrategy.GetPropertyName(resourceProperty.Name, false);
252+
return _options.SerializerOptions.PropertyNamingPolicy == null
253+
? resourceProperty.Name
254+
: _options.SerializerOptions.PropertyNamingPolicy.ConvertName(resourceProperty.Name);
253255
}
254256
}
255257
}
Lines changed: 11 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,28 +1,32 @@
11
using System;
22
using System.Reflection;
3+
using System.Text.Json;
34
using Humanizer;
45
using JsonApiDotNetCore.Resources.Annotations;
5-
using Newtonsoft.Json.Serialization;
66

77
namespace JsonApiDotNetCore.Configuration
88
{
99
internal sealed class ResourceNameFormatter
1010
{
11-
private readonly NamingStrategy _namingStrategy;
11+
private readonly JsonNamingPolicy _namingPolicy;
1212

13-
public ResourceNameFormatter(NamingStrategy namingStrategy)
13+
public ResourceNameFormatter(JsonNamingPolicy namingPolicy)
1414
{
15-
_namingStrategy = namingStrategy;
15+
_namingPolicy = namingPolicy;
1616
}
1717

1818
/// <summary>
1919
/// Gets the publicly visible resource name for the internal type name using the configured naming convention.
2020
/// </summary>
2121
public string FormatResourceName(Type resourceType)
2222
{
23-
return resourceType.GetCustomAttribute(typeof(ResourceAttribute)) is ResourceAttribute attribute
24-
? attribute.PublicName
25-
: _namingStrategy.GetPropertyName(resourceType.Name.Pluralize(), false);
23+
if (resourceType.GetCustomAttribute(typeof(ResourceAttribute)) is ResourceAttribute attribute)
24+
{
25+
return attribute.PublicName;
26+
}
27+
28+
string publicName = resourceType.Name.Pluralize();
29+
return _namingPolicy != null ? _namingPolicy.ConvertName(publicName) : publicName;
2630
}
2731
}
2832
}

src/JsonApiDotNetCore/Controllers/BaseJsonApiController.cs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -190,7 +190,7 @@ public virtual async Task<IActionResult> PostAsync([FromBody] TResource resource
190190
if (_options.ValidateModelState && !ModelState.IsValid)
191191
{
192192
throw new InvalidModelStateException(ModelState, typeof(TResource), _options.IncludeExceptionStackTraceInErrors,
193-
_options.SerializerNamingStrategy);
193+
_options.SerializerOptions.PropertyNamingPolicy);
194194
}
195195

196196
TResource newResource = await _create.CreateAsync(resource, cancellationToken);
@@ -267,7 +267,7 @@ public virtual async Task<IActionResult> PatchAsync(TId id, [FromBody] TResource
267267
if (_options.ValidateModelState && !ModelState.IsValid)
268268
{
269269
throw new InvalidModelStateException(ModelState, typeof(TResource), _options.IncludeExceptionStackTraceInErrors,
270-
_options.SerializerNamingStrategy);
270+
_options.SerializerOptions.PropertyNamingPolicy);
271271
}
272272

273273
TResource updated = await _update.UpdateAsync(id, resource, cancellationToken);

src/JsonApiDotNetCore/Controllers/BaseJsonApiOperationsController.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -174,7 +174,7 @@ protected virtual void ValidateModelState(IEnumerable<OperationContainer> operat
174174

175175
if (violations.Any())
176176
{
177-
throw new InvalidModelStateException(violations, _options.IncludeExceptionStackTraceInErrors, _options.SerializerNamingStrategy);
177+
throw new InvalidModelStateException(violations, _options.IncludeExceptionStackTraceInErrors, _options.SerializerOptions.PropertyNamingPolicy);
178178
}
179179
}
180180

src/JsonApiDotNetCore/Errors/InvalidModelStateException.cs

Lines changed: 17 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -4,12 +4,12 @@
44
using System.Linq;
55
using System.Net;
66
using System.Reflection;
7+
using System.Text.Json;
78
using JetBrains.Annotations;
89
using JsonApiDotNetCore.Controllers;
910
using JsonApiDotNetCore.Resources.Annotations;
1011
using JsonApiDotNetCore.Serialization.Objects;
1112
using Microsoft.AspNetCore.Mvc.ModelBinding;
12-
using Newtonsoft.Json.Serialization;
1313

1414
namespace JsonApiDotNetCore.Errors
1515
{
@@ -20,13 +20,13 @@ namespace JsonApiDotNetCore.Errors
2020
public sealed class InvalidModelStateException : JsonApiException
2121
{
2222
public InvalidModelStateException(ModelStateDictionary modelState, Type resourceType, bool includeExceptionStackTraceInErrors,
23-
NamingStrategy namingStrategy)
24-
: this(FromModelStateDictionary(modelState, resourceType), includeExceptionStackTraceInErrors, namingStrategy)
23+
JsonNamingPolicy namingPolicy)
24+
: this(FromModelStateDictionary(modelState, resourceType), includeExceptionStackTraceInErrors, namingPolicy)
2525
{
2626
}
2727

28-
public InvalidModelStateException(IEnumerable<ModelStateViolation> violations, bool includeExceptionStackTraceInErrors, NamingStrategy namingStrategy)
29-
: base(FromModelStateViolations(violations, includeExceptionStackTraceInErrors, namingStrategy))
28+
public InvalidModelStateException(IEnumerable<ModelStateViolation> violations, bool includeExceptionStackTraceInErrors, JsonNamingPolicy namingPolicy)
29+
: base(FromModelStateViolations(violations, includeExceptionStackTraceInErrors, namingPolicy))
3030
{
3131
}
3232

@@ -55,16 +55,15 @@ private static void AddValidationErrors(ModelStateEntry entry, string propertyNa
5555
}
5656

5757
private static IEnumerable<Error> FromModelStateViolations(IEnumerable<ModelStateViolation> violations, bool includeExceptionStackTraceInErrors,
58-
NamingStrategy namingStrategy)
58+
JsonNamingPolicy namingPolicy)
5959
{
6060
ArgumentGuard.NotNull(violations, nameof(violations));
61-
ArgumentGuard.NotNull(namingStrategy, nameof(namingStrategy));
6261

63-
return violations.SelectMany(violation => FromModelStateViolation(violation, includeExceptionStackTraceInErrors, namingStrategy));
62+
return violations.SelectMany(violation => FromModelStateViolation(violation, includeExceptionStackTraceInErrors, namingPolicy));
6463
}
6564

6665
private static IEnumerable<Error> FromModelStateViolation(ModelStateViolation violation, bool includeExceptionStackTraceInErrors,
67-
NamingStrategy namingStrategy)
66+
JsonNamingPolicy namingPolicy)
6867
{
6968
if (violation.Error.Exception is JsonApiException jsonApiException)
7069
{
@@ -75,21 +74,27 @@ private static IEnumerable<Error> FromModelStateViolation(ModelStateViolation vi
7574
}
7675
else
7776
{
78-
string attributeName = GetDisplayNameForProperty(violation.PropertyName, violation.ResourceType, namingStrategy);
77+
string attributeName = GetDisplayNameForProperty(violation.PropertyName, violation.ResourceType, namingPolicy);
7978
string attributePath = $"{violation.Prefix}{attributeName}";
8079

8180
yield return FromModelError(violation.Error, attributePath, includeExceptionStackTraceInErrors);
8281
}
8382
}
8483

85-
private static string GetDisplayNameForProperty(string propertyName, Type resourceType, NamingStrategy namingStrategy)
84+
private static string GetDisplayNameForProperty(string propertyName, Type resourceType, JsonNamingPolicy namingPolicy)
8685
{
8786
PropertyInfo property = resourceType.GetProperty(propertyName);
8887

8988
if (property != null)
9089
{
9190
var attrAttribute = property.GetCustomAttribute<AttrAttribute>();
92-
return attrAttribute?.PublicName ?? namingStrategy.GetPropertyName(property.Name, false);
91+
92+
if (attrAttribute?.PublicName != null)
93+
{
94+
return attrAttribute.PublicName;
95+
}
96+
97+
return namingPolicy != null ? namingPolicy.ConvertName(property.Name) : property.Name;
9398
}
9499

95100
return propertyName;

src/JsonApiDotNetCore/Middleware/JsonApiRoutingConvention.cs

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -146,7 +146,10 @@ private string TemplateFromResource(ControllerModel model)
146146
/// </summary>
147147
private string TemplateFromController(ControllerModel model)
148148
{
149-
string controllerName = _options.SerializerNamingStrategy.GetPropertyName(model.ControllerName, false);
149+
string controllerName = _options.SerializerOptions.PropertyNamingPolicy == null
150+
? model.ControllerName
151+
: _options.SerializerOptions.PropertyNamingPolicy.ConvertName(model.ControllerName);
152+
150153
return $"{_options.Namespace}/{controllerName}";
151154
}
152155

src/JsonApiDotNetCore/Serialization/Building/MetaBuilder.cs

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,11 @@ public IDictionary<string, object> Build()
4040
{
4141
if (_paginationContext.TotalResourceCount != null)
4242
{
43-
string key = _options.SerializerNamingStrategy.GetPropertyName("TotalResources", false);
43+
const string keyName = "TotalResources";
44+
45+
string key = _options.SerializerOptions.DictionaryKeyPolicy == null
46+
? keyName
47+
: _options.SerializerOptions.DictionaryKeyPolicy.ConvertName(keyName);
4448

4549
_meta.Add(key, _paginationContext.TotalResourceCount);
4650
}
Lines changed: 83 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,83 @@
1+
using System;
2+
using System.Text;
3+
using System.Text.Json;
4+
5+
namespace JsonApiDotNetCoreTests.IntegrationTests.NamingConventions
6+
{
7+
// Based on https://github.com/J0rgeSerran0/JsonNamingPolicy
8+
internal sealed class JsonKebabCaseNamingPolicy : JsonNamingPolicy
9+
{
10+
private const char Separator = '-';
11+
12+
public static readonly JsonKebabCaseNamingPolicy Instance = new();
13+
14+
public override string ConvertName(string name)
15+
{
16+
if (string.IsNullOrWhiteSpace(name))
17+
{
18+
return string.Empty;
19+
}
20+
21+
ReadOnlySpan<char> spanName = name.Trim();
22+
23+
var stringBuilder = new StringBuilder();
24+
bool addCharacter = true;
25+
26+
bool isNextLower = false;
27+
bool isNextUpper = false;
28+
bool isNextSpace = false;
29+
30+
for (int position = 0; position < spanName.Length; position++)
31+
{
32+
if (position != 0)
33+
{
34+
bool isCurrentSpace = spanName[position] == 32;
35+
bool isPreviousSpace = spanName[position - 1] == 32;
36+
bool isPreviousSeparator = spanName[position - 1] == 95;
37+
38+
if (position + 1 != spanName.Length)
39+
{
40+
isNextLower = spanName[position + 1] > 96 && spanName[position + 1] < 123;
41+
isNextUpper = spanName[position + 1] > 64 && spanName[position + 1] < 91;
42+
isNextSpace = spanName[position + 1] == 32;
43+
}
44+
45+
if (isCurrentSpace && (isPreviousSpace || isPreviousSeparator || isNextUpper || isNextSpace))
46+
{
47+
addCharacter = false;
48+
}
49+
else
50+
{
51+
bool isCurrentUpper = spanName[position] > 64 && spanName[position] < 91;
52+
bool isPreviousLower = spanName[position - 1] > 96 && spanName[position - 1] < 123;
53+
bool isPreviousNumber = spanName[position - 1] > 47 && spanName[position - 1] < 58;
54+
55+
if (isCurrentUpper && (isPreviousLower || isPreviousNumber || isNextLower || isNextSpace))
56+
{
57+
stringBuilder.Append(Separator);
58+
}
59+
else
60+
{
61+
if (isCurrentSpace)
62+
{
63+
stringBuilder.Append(Separator);
64+
addCharacter = false;
65+
}
66+
}
67+
}
68+
}
69+
70+
if (addCharacter)
71+
{
72+
stringBuilder.Append(spanName[position]);
73+
}
74+
else
75+
{
76+
addCharacter = true;
77+
}
78+
}
79+
80+
return stringBuilder.ToString().ToLower();
81+
}
82+
}
83+
}

test/JsonApiDotNetCoreTests/IntegrationTests/NamingConventions/KebabCasingConventionStartup.cs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,9 @@ protected override void SetJsonApiOptions(JsonApiOptions options)
2323
{
2424
NamingStrategy = new KebabCaseNamingStrategy()
2525
};
26+
27+
options.SerializerOptions.PropertyNamingPolicy = JsonKebabCaseNamingPolicy.Instance;
28+
options.SerializerOptions.DictionaryKeyPolicy = JsonKebabCaseNamingPolicy.Instance;
2629
}
2730
}
2831
}

test/JsonApiDotNetCoreTests/IntegrationTests/NamingConventions/PascalCasingConventionStartup.cs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,9 @@ protected override void SetJsonApiOptions(JsonApiOptions options)
2323
{
2424
NamingStrategy = new DefaultNamingStrategy()
2525
};
26+
27+
options.SerializerOptions.PropertyNamingPolicy = null;
28+
options.SerializerOptions.DictionaryKeyPolicy = null;
2629
}
2730
}
2831
}

0 commit comments

Comments
 (0)