Added JsonContractResolver to validate value type properties
This commit is contained in:
parent
c276ddaa39
commit
f737083c94
|
|
@ -0,0 +1,42 @@
|
|||
// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved.
|
||||
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
|
||||
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
using System.Reflection;
|
||||
using Newtonsoft.Json;
|
||||
using Newtonsoft.Json.Serialization;
|
||||
|
||||
namespace Microsoft.AspNet.Mvc
|
||||
{
|
||||
/// <summary>
|
||||
/// The default <see cref="IContractResolver"/> for <see cref="JsonInputFormatter"/>.
|
||||
/// It determines if a value type member has <see cref="RequiredAttribute"/> and sets the appropriate
|
||||
/// JsonProperty settings.
|
||||
/// </summary>
|
||||
public class JsonContractResolver : DefaultContractResolver
|
||||
{
|
||||
protected override JsonProperty CreateProperty(MemberInfo member, MemberSerialization memberSerialization)
|
||||
{
|
||||
var property = base.CreateProperty(member, memberSerialization);
|
||||
|
||||
var required = member.GetCustomAttribute(typeof(RequiredAttribute), inherit: true);
|
||||
if (required != null)
|
||||
{
|
||||
var propertyType = ((PropertyInfo)member).PropertyType;
|
||||
|
||||
// DefaultObjectValidator does required attribute validation on properties based on the property
|
||||
// value being null. Since this is not possible in case of value types, we depend on the formatters
|
||||
// to handle value type validation.
|
||||
// With the following settings here, if a value is not present on the wire for value types
|
||||
// like primitive, struct etc., Json.net's serializer would throw exception which we catch
|
||||
// and add it to model state.
|
||||
if (propertyType.IsValueType() && !propertyType.IsNullableValueType())
|
||||
{
|
||||
property.Required = Required.AllowNull;
|
||||
}
|
||||
}
|
||||
|
||||
return property;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -36,6 +36,8 @@ namespace Microsoft.AspNet.Mvc
|
|||
// Setting this to None prevents Json.NET from loading malicious, unsafe, or security-sensitive types
|
||||
TypeNameHandling = TypeNameHandling.None
|
||||
};
|
||||
|
||||
_jsonSerializerSettings.ContractResolver = new JsonContractResolver();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@
|
|||
#if ASPNET50
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
|
|
@ -234,6 +235,126 @@ namespace Microsoft.AspNet.Mvc
|
|||
Assert.Contains("Required property 'Password' not found in JSON", modelErrorMessage);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task ThrowsException_OnSupplyingNull_ForRequiredValueType()
|
||||
{
|
||||
// Arrange
|
||||
var contentBytes = Encoding.UTF8.GetBytes("{\"Id\":\"null\",\"Name\":\"Programming C#\"}");
|
||||
var jsonFormatter = new JsonInputFormatter() { CaptureDeserilizationErrors = true };
|
||||
var actionContext = GetActionContext(contentBytes, "application/json;charset=utf-8");
|
||||
var metadata = new EmptyModelMetadataProvider().GetMetadataForType(typeof(Book));
|
||||
var inputFormatterContext = new InputFormatterContext(actionContext, metadata.ModelType);
|
||||
|
||||
// Act
|
||||
var obj = await jsonFormatter.ReadAsync(inputFormatterContext);
|
||||
|
||||
// Assert
|
||||
var book = obj as Book;
|
||||
Assert.NotNull(book);
|
||||
Assert.Equal(0, book.Id);
|
||||
Assert.Equal("Programming C#", book.Name);
|
||||
Assert.False(actionContext.ModelState.IsValid);
|
||||
|
||||
Assert.Equal(1, actionContext.ModelState.Values.First().Errors.Count);
|
||||
var modelErrorMessage = actionContext.ModelState.Values.First().Errors[0].Exception.Message;
|
||||
Assert.Contains("Could not convert string to integer: null. Path 'Id'", modelErrorMessage);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData(typeof(Book))]
|
||||
[InlineData(typeof(EBook))]
|
||||
public async Task Validates_RequiredAttribute_OnRegularAndInheritedProperties(Type type)
|
||||
{
|
||||
// Arrange
|
||||
var contentBytes = Encoding.UTF8.GetBytes("{ \"Name\" : \"Programming C#\"}");
|
||||
var jsonFormatter = new JsonInputFormatter() { CaptureDeserilizationErrors = true };
|
||||
var actionContext = GetActionContext(contentBytes, "application/json;charset=utf-8");
|
||||
var metadata = new EmptyModelMetadataProvider().GetMetadataForType(type);
|
||||
var inputFormatterContext = new InputFormatterContext(actionContext, metadata.ModelType);
|
||||
|
||||
// Act
|
||||
var obj = await jsonFormatter.ReadAsync(inputFormatterContext);
|
||||
|
||||
// Assert
|
||||
Assert.False(actionContext.ModelState.IsValid);
|
||||
Assert.Equal(1, actionContext.ModelState.Count);
|
||||
|
||||
var modelErrorMessage = actionContext.ModelState.Values.First().Errors[0].Exception.Message;
|
||||
Assert.Contains("Required property 'Id' not found in JSON", modelErrorMessage);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Validates_RequiredAttributeOnStructTypes()
|
||||
{
|
||||
// Arrange
|
||||
var contentBytes = Encoding.UTF8.GetBytes("{\"Longitude\":{}}");
|
||||
var jsonFormatter = new JsonInputFormatter() { CaptureDeserilizationErrors = true };
|
||||
var actionContext = GetActionContext(contentBytes, "application/json;charset=utf-8");
|
||||
var metadata = new EmptyModelMetadataProvider().GetMetadataForType(typeof(GpsCoordinate));
|
||||
var inputFormatterContext = new InputFormatterContext(actionContext, metadata.ModelType);
|
||||
|
||||
// Act
|
||||
var obj = await jsonFormatter.ReadAsync(inputFormatterContext);
|
||||
|
||||
// Assert
|
||||
Assert.False(actionContext.ModelState.IsValid);
|
||||
Assert.Equal(2, actionContext.ModelState.Count);
|
||||
var errorMessages = GetModelStateErrorMessages(actionContext.ModelState);
|
||||
Assert.Equal(3, errorMessages.Count());
|
||||
Assert.Contains(
|
||||
errorMessages,
|
||||
(errorMessage) => errorMessage.Contains("Required property 'Latitude' not found in JSON"));
|
||||
Assert.Contains(
|
||||
errorMessages,
|
||||
(errorMessage) => errorMessage.Contains("Required property 'X' not found in JSON"));
|
||||
Assert.Contains(
|
||||
errorMessages,
|
||||
(errorMessage) => errorMessage.Contains("Required property 'Y' not found in JSON"));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Validation_DoesNotHappen_ForNonRequired_ValueTypeProperties()
|
||||
{
|
||||
// Arrange
|
||||
var contentBytes = Encoding.UTF8.GetBytes("{\"Name\":\"Seattle\"}");
|
||||
var jsonFormatter = new JsonInputFormatter() { CaptureDeserilizationErrors = true };
|
||||
var actionContext = GetActionContext(contentBytes, "application/json;charset=utf-8");
|
||||
var metadata = new EmptyModelMetadataProvider().GetMetadataForType(typeof(Location));
|
||||
var inputFormatterContext = new InputFormatterContext(actionContext, metadata.ModelType);
|
||||
|
||||
// Act
|
||||
var obj = await jsonFormatter.ReadAsync(inputFormatterContext);
|
||||
|
||||
// Assert
|
||||
Assert.True(actionContext.ModelState.IsValid);
|
||||
var location = obj as Location;
|
||||
Assert.NotNull(location);
|
||||
Assert.Equal(0, location.Id);
|
||||
Assert.Equal("Seattle", location.Name);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Validation_DoesNotHappen_OnNullableValueTypeProperties()
|
||||
{
|
||||
// Arrange
|
||||
var contentBytes = Encoding.UTF8.GetBytes("{}");
|
||||
var jsonFormatter = new JsonInputFormatter() { CaptureDeserilizationErrors = true };
|
||||
var actionContext = GetActionContext(contentBytes, "application/json;charset=utf-8");
|
||||
var metadata = new EmptyModelMetadataProvider().GetMetadataForType(typeof(Venue));
|
||||
var inputFormatterContext = new InputFormatterContext(actionContext, metadata.ModelType);
|
||||
|
||||
// Act
|
||||
var obj = await jsonFormatter.ReadAsync(inputFormatterContext);
|
||||
|
||||
// Assert
|
||||
Assert.True(actionContext.ModelState.IsValid);
|
||||
var venue = obj as Venue;
|
||||
Assert.NotNull(venue);
|
||||
Assert.Null(venue.Location);
|
||||
Assert.Null(venue.NearByLocations);
|
||||
Assert.Null(venue.Name);
|
||||
}
|
||||
|
||||
private static ActionContext GetActionContext(byte[] contentBytes,
|
||||
string contentType = "application/xml")
|
||||
{
|
||||
|
|
@ -257,6 +378,35 @@ namespace Microsoft.AspNet.Mvc
|
|||
return httpContext.Object;
|
||||
}
|
||||
|
||||
private IEnumerable<string> GetModelStateErrorMessages(ModelStateDictionary modelStateDictionary)
|
||||
{
|
||||
var allErrorMessages = new List<string>();
|
||||
foreach (var keyModelStatePair in modelStateDictionary)
|
||||
{
|
||||
var key = keyModelStatePair.Key;
|
||||
var errors = keyModelStatePair.Value.Errors;
|
||||
if (errors != null && errors.Count > 0)
|
||||
{
|
||||
foreach (var modelError in errors)
|
||||
{
|
||||
if (string.IsNullOrEmpty(modelError.ErrorMessage))
|
||||
{
|
||||
if (modelError.Exception != null)
|
||||
{
|
||||
allErrorMessages.Add(modelError.Exception.Message);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
allErrorMessages.Add(modelError.ErrorMessage);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return allErrorMessages;
|
||||
}
|
||||
|
||||
private sealed class User
|
||||
{
|
||||
public string Name { get; set; }
|
||||
|
|
@ -272,6 +422,56 @@ namespace Microsoft.AspNet.Mvc
|
|||
[JsonProperty(Required = Required.Always)]
|
||||
public string Password { get; set; }
|
||||
}
|
||||
|
||||
private class Book
|
||||
{
|
||||
[Required]
|
||||
public int Id { get; set; }
|
||||
|
||||
[Required]
|
||||
public string Name { get; set; }
|
||||
}
|
||||
|
||||
private class EBook : Book
|
||||
{
|
||||
}
|
||||
|
||||
private struct Point
|
||||
{
|
||||
[Required]
|
||||
public int X { get; set; }
|
||||
|
||||
[Required]
|
||||
public int Y { get; set; }
|
||||
}
|
||||
|
||||
private class GpsCoordinate
|
||||
{
|
||||
[Required]
|
||||
public Point Latitude { get; set; }
|
||||
|
||||
[Required]
|
||||
public Point Longitude { get; set; }
|
||||
}
|
||||
|
||||
private class Location
|
||||
{
|
||||
public int Id { get; set; }
|
||||
|
||||
public string Name { get; set; }
|
||||
}
|
||||
|
||||
private class Venue
|
||||
{
|
||||
[Required]
|
||||
public string Name { get; set; }
|
||||
|
||||
[Required]
|
||||
public Point? Location { get; set; }
|
||||
|
||||
[Required]
|
||||
public List<Point> NearByLocations { get; set; }
|
||||
}
|
||||
}
|
||||
}
|
||||
#endif
|
||||
|
|
|
|||
|
|
@ -175,8 +175,10 @@ namespace Microsoft.AspNet.Mvc.FunctionalTests
|
|||
// Arrange
|
||||
var server = TestServer.Create(_services, _app);
|
||||
var client = server.CreateClient();
|
||||
var content = new StringContent("{\"Alias\":\"xyz\"}", Encoding.UTF8, "application/json");
|
||||
|
||||
var kvps = new List<KeyValuePair<string, string>>();
|
||||
kvps.Add(new KeyValuePair<string, string>("Alias", "xyz"));
|
||||
var content = new FormUrlEncodedContent(kvps);
|
||||
|
||||
// Act
|
||||
var response = await client.PostAsync("http://localhost/Validation/GetDeveloperAlias", content);
|
||||
|
||||
|
|
|
|||
|
|
@ -1777,5 +1777,74 @@ namespace Microsoft.AspNet.Mvc.FunctionalTests
|
|||
// Should Update all included properties.
|
||||
Assert.Equal("March", user.RegisterationMonth);
|
||||
}
|
||||
|
||||
public static TheoryData<string, string[]> ModelStateHasErrorsForValueAndReferenceTypesData
|
||||
{
|
||||
get
|
||||
{
|
||||
return new TheoryData<string, string[]>()
|
||||
{
|
||||
{
|
||||
"{}",
|
||||
new[]
|
||||
{
|
||||
":Required property 'Id' not found in JSON",
|
||||
"rectangle.Lines:The Lines field is required."
|
||||
}
|
||||
},
|
||||
{
|
||||
"{\"Id\":10}",
|
||||
new[]
|
||||
{
|
||||
"rectangle.Lines:The Lines field is required."
|
||||
}
|
||||
},
|
||||
{
|
||||
"{\"Id\":10,\"Lines\":[{}]}",
|
||||
new []
|
||||
{
|
||||
"Lines[0]:Required property 'Start' not found in JSON",
|
||||
"Lines[0]:Required property 'End' not found in JSON"
|
||||
}
|
||||
},
|
||||
{
|
||||
"{\"Id\":10,\"Lines\":[{\"Start\":{\"X\":10,\"Y\":10},\"End\":{\"X\":10}}]}",
|
||||
new []
|
||||
{
|
||||
"Lines[0].End:Required property 'Y' not found in JSON"
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[MemberData(nameof(ModelStateHasErrorsForValueAndReferenceTypesData))]
|
||||
public async Task ModelState_HasErrors_ForValueAndReferenceTypes(
|
||||
string input,
|
||||
IEnumerable<string> expectedModelStateErrorMessages)
|
||||
{
|
||||
// Arrange
|
||||
var server = TestServer.Create(_services, _app);
|
||||
var client = server.CreateClient();
|
||||
var content = new StringContent(input, Encoding.UTF8, "text/json");
|
||||
|
||||
// Act
|
||||
var response = await client.PostAsync(
|
||||
"http://localhost/Validation/CreateRectangle",
|
||||
content);
|
||||
|
||||
// Assert
|
||||
var data = await response.Content.ReadAsStringAsync();
|
||||
var actualModelStateErrorMessages = JsonConvert.DeserializeObject<IEnumerable<string>>(data);
|
||||
Assert.NotNull(actualModelStateErrorMessages);
|
||||
Assert.Equal(expectedModelStateErrorMessages.Count(), actualModelStateErrorMessages.Count());
|
||||
foreach (var expectedErrorMessage in expectedModelStateErrorMessages)
|
||||
{
|
||||
Assert.Contains(
|
||||
actualModelStateErrorMessages,
|
||||
(actualErrorMessage) => actualErrorMessage.StartsWith(expectedErrorMessage));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -6,6 +6,7 @@ using System.Collections.Generic;
|
|||
using System.ComponentModel.DataAnnotations;
|
||||
using System.Linq;
|
||||
using Microsoft.AspNet.Mvc;
|
||||
using Microsoft.AspNet.Mvc.ModelBinding;
|
||||
|
||||
namespace ModelBindingWebSite.Controllers
|
||||
{
|
||||
|
|
@ -21,6 +22,51 @@ namespace ModelBindingWebSite.Controllers
|
|||
{
|
||||
return ModelState.IsValid;
|
||||
}
|
||||
|
||||
public IActionResult CreateRectangle([FromBody] Rectangle rectangle)
|
||||
{
|
||||
if (!ModelState.IsValid)
|
||||
{
|
||||
return new ObjectResult(GetModelStateErrorMessages(ModelState)) { StatusCode = 400 };
|
||||
}
|
||||
|
||||
return new ObjectResult(rectangle);
|
||||
}
|
||||
|
||||
private IEnumerable<string> GetModelStateErrorMessages(ModelStateDictionary modelStateDictionary)
|
||||
{
|
||||
var allErrorMessages = new List<string>();
|
||||
foreach (var keyModelStatePair in modelStateDictionary)
|
||||
{
|
||||
var key = keyModelStatePair.Key;
|
||||
var errors = keyModelStatePair.Value.Errors;
|
||||
if (errors != null && errors.Count > 0)
|
||||
{
|
||||
string errorMessage = null;
|
||||
foreach (var modelError in errors)
|
||||
{
|
||||
if (string.IsNullOrEmpty(modelError.ErrorMessage))
|
||||
{
|
||||
if (modelError.Exception != null)
|
||||
{
|
||||
errorMessage = modelError.Exception.Message;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
errorMessage = modelError.ErrorMessage;
|
||||
}
|
||||
|
||||
if (errorMessage != null)
|
||||
{
|
||||
allErrorMessages.Add(string.Format("{0}:{1}", key, errorMessage));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return allErrorMessages;
|
||||
}
|
||||
}
|
||||
|
||||
public class SelfishPerson
|
||||
|
|
|
|||
|
|
@ -0,0 +1,18 @@
|
|||
// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved.
|
||||
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
|
||||
namespace ModelBindingWebSite
|
||||
{
|
||||
public class Drawing
|
||||
{
|
||||
[Required]
|
||||
public int Id { get; set; }
|
||||
|
||||
[Required]
|
||||
public List<Line> Lines { get; set; }
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,17 @@
|
|||
// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved.
|
||||
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
|
||||
|
||||
using System;
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
|
||||
namespace ModelBindingWebSite
|
||||
{
|
||||
public struct Line
|
||||
{
|
||||
[Required]
|
||||
public Point Start { get; set; }
|
||||
|
||||
[Required]
|
||||
public Point End { get; set; }
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,16 @@
|
|||
// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved.
|
||||
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
|
||||
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
|
||||
namespace ModelBindingWebSite
|
||||
{
|
||||
public struct Point
|
||||
{
|
||||
[Required]
|
||||
public int X { get; set; }
|
||||
|
||||
[Required]
|
||||
public int Y { get; set; }
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,12 @@
|
|||
// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved.
|
||||
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
|
||||
|
||||
using System;
|
||||
|
||||
namespace ModelBindingWebSite
|
||||
{
|
||||
public class Rectangle : Drawing
|
||||
{
|
||||
|
||||
}
|
||||
}
|
||||
|
|
@ -29,6 +29,8 @@ namespace ModelBindingWebSite
|
|||
m.MaxModelValidationErrors = 8;
|
||||
m.ModelBinders.Insert(0, typeof(TestBindingSourceModelBinder));
|
||||
|
||||
m.InputFormatters.InstanceOf<JsonInputFormatter>().CaptureDeserilizationErrors = true;
|
||||
|
||||
m.AddXmlDataContractSerializerFormatter();
|
||||
m.ValidationExcludeFilters.Add(typeof(Address));
|
||||
});
|
||||
|
|
|
|||
Loading…
Reference in New Issue