From 06d3d7a731b66be9597fdc7886d857e8059e55b7 Mon Sep 17 00:00:00 2001 From: Bart Koelman Date: Tue, 14 Apr 2020 15:05:22 +0200 Subject: [PATCH] Fixed: Apply casing convention in error response resulting from invalid modal state --- .../Builders/ResourceGraphBuilder.cs | 3 +-- .../Configuration/IJsonApiOptions.cs | 3 +++ .../Controllers/BaseJsonApiController.cs | 11 +++++++++-- .../Exceptions/InvalidModelStateException.cs | 10 +++++----- src/JsonApiDotNetCore/Graph/ResourceNameFormatter.cs | 3 +-- .../Internal/DefaultRoutingConvention.cs | 3 +-- .../Acceptance/ModelStateValidationTests.cs | 4 ++-- 7 files changed, 22 insertions(+), 15 deletions(-) diff --git a/src/JsonApiDotNetCore/Builders/ResourceGraphBuilder.cs b/src/JsonApiDotNetCore/Builders/ResourceGraphBuilder.cs index f661caf03c..068533adce 100644 --- a/src/JsonApiDotNetCore/Builders/ResourceGraphBuilder.cs +++ b/src/JsonApiDotNetCore/Builders/ResourceGraphBuilder.cs @@ -227,8 +227,7 @@ private string FormatResourceName(Type resourceType) private string FormatPropertyName(PropertyInfo resourceProperty) { - var contractResolver = (DefaultContractResolver)_options.SerializerSettings.ContractResolver; - return contractResolver.NamingStrategy.GetPropertyName(resourceProperty.Name, false); + return _options.SerializerContractResolver.NamingStrategy.GetPropertyName(resourceProperty.Name, false); } } } diff --git a/src/JsonApiDotNetCore/Configuration/IJsonApiOptions.cs b/src/JsonApiDotNetCore/Configuration/IJsonApiOptions.cs index cf886b6730..9cae2b3518 100644 --- a/src/JsonApiDotNetCore/Configuration/IJsonApiOptions.cs +++ b/src/JsonApiDotNetCore/Configuration/IJsonApiOptions.cs @@ -1,6 +1,7 @@ using System; using JsonApiDotNetCore.Models.JsonApiDocuments; using Newtonsoft.Json; +using Newtonsoft.Json.Serialization; namespace JsonApiDotNetCore.Configuration { @@ -59,5 +60,7 @@ public interface IJsonApiOptions : ILinksConfiguration /// /// JsonSerializerSettings SerializerSettings { get; } + + internal DefaultContractResolver SerializerContractResolver => (DefaultContractResolver)SerializerSettings.ContractResolver; } } diff --git a/src/JsonApiDotNetCore/Controllers/BaseJsonApiController.cs b/src/JsonApiDotNetCore/Controllers/BaseJsonApiController.cs index f9d5e4e712..e9464fb269 100644 --- a/src/JsonApiDotNetCore/Controllers/BaseJsonApiController.cs +++ b/src/JsonApiDotNetCore/Controllers/BaseJsonApiController.cs @@ -6,6 +6,7 @@ using JsonApiDotNetCore.Services; using Microsoft.AspNetCore.Mvc; using Microsoft.Extensions.Logging; +using Newtonsoft.Json.Serialization; namespace JsonApiDotNetCore.Controllers { @@ -114,7 +115,10 @@ public virtual async Task PostAsync([FromBody] T entity) throw new ResourceIdInPostRequestNotAllowedException(); if (_jsonApiOptions.ValidateModelState && !ModelState.IsValid) - throw new InvalidModelStateException(ModelState, typeof(T), _jsonApiOptions.IncludeExceptionStackTraceInErrors); + { + var namingStrategy = _jsonApiOptions.SerializerContractResolver.NamingStrategy; + throw new InvalidModelStateException(ModelState, typeof(T), _jsonApiOptions.IncludeExceptionStackTraceInErrors, namingStrategy); + } entity = await _create.CreateAsync(entity); @@ -130,7 +134,10 @@ public virtual async Task PatchAsync(TId id, [FromBody] T entity) throw new InvalidRequestBodyException(null, null, null); if (_jsonApiOptions.ValidateModelState && !ModelState.IsValid) - throw new InvalidModelStateException(ModelState, typeof(T), _jsonApiOptions.IncludeExceptionStackTraceInErrors); + { + var namingStrategy = _jsonApiOptions.SerializerContractResolver.NamingStrategy; + throw new InvalidModelStateException(ModelState, typeof(T), _jsonApiOptions.IncludeExceptionStackTraceInErrors, namingStrategy); + } var updatedEntity = await _update.UpdateAsync(id, entity); return updatedEntity == null ? Ok(null) : Ok(updatedEntity); diff --git a/src/JsonApiDotNetCore/Exceptions/InvalidModelStateException.cs b/src/JsonApiDotNetCore/Exceptions/InvalidModelStateException.cs index cbb717786c..e227ab30ce 100644 --- a/src/JsonApiDotNetCore/Exceptions/InvalidModelStateException.cs +++ b/src/JsonApiDotNetCore/Exceptions/InvalidModelStateException.cs @@ -6,6 +6,7 @@ using JsonApiDotNetCore.Models; using JsonApiDotNetCore.Models.JsonApiDocuments; using Microsoft.AspNetCore.Mvc.ModelBinding; +using Newtonsoft.Json.Serialization; namespace JsonApiDotNetCore.Exceptions { @@ -17,13 +18,13 @@ public class InvalidModelStateException : Exception public IList Errors { get; } public InvalidModelStateException(ModelStateDictionary modelState, Type resourceType, - bool includeExceptionStackTraceInErrors) + bool includeExceptionStackTraceInErrors, NamingStrategy namingStrategy) { - Errors = FromModelState(modelState, resourceType, includeExceptionStackTraceInErrors); + Errors = FromModelState(modelState, resourceType, includeExceptionStackTraceInErrors, namingStrategy); } private static List FromModelState(ModelStateDictionary modelState, Type resourceType, - bool includeExceptionStackTraceInErrors) + bool includeExceptionStackTraceInErrors, NamingStrategy namingStrategy) { List errors = new List(); @@ -32,9 +33,8 @@ private static List FromModelState(ModelStateDictionary modelState, Type var propertyName = pair.Key; PropertyInfo property = resourceType.GetProperty(propertyName); - // TODO: Need access to ResourceContext here, in order to determine attribute name when not explicitly set. string attributeName = - property?.GetCustomAttribute().PublicAttributeName ?? property?.Name; + property.GetCustomAttribute().PublicAttributeName ?? namingStrategy.GetPropertyName(property.Name, false); foreach (var modelError in pair.Value.Errors) { diff --git a/src/JsonApiDotNetCore/Graph/ResourceNameFormatter.cs b/src/JsonApiDotNetCore/Graph/ResourceNameFormatter.cs index f81be8dae3..36cf2234e8 100644 --- a/src/JsonApiDotNetCore/Graph/ResourceNameFormatter.cs +++ b/src/JsonApiDotNetCore/Graph/ResourceNameFormatter.cs @@ -13,8 +13,7 @@ internal sealed class ResourceNameFormatter public ResourceNameFormatter(IJsonApiOptions options) { - var contractResolver = (DefaultContractResolver) options.SerializerSettings.ContractResolver; - _namingStrategy = contractResolver.NamingStrategy; + _namingStrategy = options.SerializerContractResolver.NamingStrategy; } /// diff --git a/src/JsonApiDotNetCore/Internal/DefaultRoutingConvention.cs b/src/JsonApiDotNetCore/Internal/DefaultRoutingConvention.cs index e8f796f3c2..176e46965f 100644 --- a/src/JsonApiDotNetCore/Internal/DefaultRoutingConvention.cs +++ b/src/JsonApiDotNetCore/Internal/DefaultRoutingConvention.cs @@ -107,8 +107,7 @@ private string TemplateFromResource(ControllerModel model) /// private string TemplateFromController(ControllerModel model) { - var contractResolver = (DefaultContractResolver) _options.SerializerSettings.ContractResolver; - string controllerName = contractResolver.NamingStrategy.GetPropertyName(model.ControllerName, false); + string controllerName = _options.SerializerContractResolver.NamingStrategy.GetPropertyName(model.ControllerName, false); var template = $"{_options.Namespace}/{controllerName}"; if (_registeredTemplates.Add(template)) diff --git a/test/JsonApiDotNetCoreExampleTests/Acceptance/ModelStateValidationTests.cs b/test/JsonApiDotNetCoreExampleTests/Acceptance/ModelStateValidationTests.cs index 5a13fa3e3d..9ae4d1c971 100644 --- a/test/JsonApiDotNetCoreExampleTests/Acceptance/ModelStateValidationTests.cs +++ b/test/JsonApiDotNetCoreExampleTests/Acceptance/ModelStateValidationTests.cs @@ -53,7 +53,7 @@ public async Task When_posting_tag_with_invalid_name_it_must_fail() Assert.Equal(HttpStatusCode.UnprocessableEntity, errorDocument.Errors[0].StatusCode); Assert.Equal("Input validation failed.", errorDocument.Errors[0].Title); Assert.Equal("The field Name must match the regular expression '^\\W$'.", errorDocument.Errors[0].Detail); - Assert.Equal("/data/attributes/Name", errorDocument.Errors[0].Source.Pointer); + Assert.Equal("/data/attributes/name", errorDocument.Errors[0].Source.Pointer); } [Fact] @@ -127,7 +127,7 @@ public async Task When_patching_tag_with_invalid_name_it_must_fail() Assert.Equal(HttpStatusCode.UnprocessableEntity, errorDocument.Errors[0].StatusCode); Assert.Equal("Input validation failed.", errorDocument.Errors[0].Title); Assert.Equal("The field Name must match the regular expression '^\\W$'.", errorDocument.Errors[0].Detail); - Assert.Equal("/data/attributes/Name", errorDocument.Errors[0].Source.Pointer); + Assert.Equal("/data/attributes/name", errorDocument.Errors[0].Source.Pointer); } [Fact]