Fix #2528 - Revert formatter behavior for [Required]

This change removes the support in the DCS formatter to issue an error
message when [Required] is used on a value type.
This commit is contained in:
Ryan Nowak 2015-05-11 19:02:44 -07:00
parent 13a3c0b931
commit 4939181075
4 changed files with 7 additions and 1062 deletions

View File

@ -1,212 +0,0 @@
// Copyright (c) .NET Foundation. 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.Concurrent;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using System.Reflection;
using System.Runtime.Serialization;
using Microsoft.AspNet.Mvc.ModelBinding;
using Microsoft.Framework.Internal;
namespace Microsoft.AspNet.Mvc.Xml
{
/// <summary>
/// Validates types having value type properties decorated with <see cref="RequiredAttribute"/>
/// but no <see cref="DataMemberAttribute"/>.
/// </summary>
/// <remarks>
/// <see cref="JsonInputFormatter"/> supports <see cref="RequiredAttribute"/> where as the xml formatters
/// do not. Since a user's aplication can have both Json and Xml formatters, a request could be validated
/// when posted as Json but not Xml. So to prevent end users from having a false sense of security when posting
/// as Xml, we add errors to model-state to at least let the users know that there is a problem with their models.
/// </remarks>
public class DataAnnotationRequiredAttributeValidation
{
// Since formatters are 'typically' registered as single instance, concurrent dictionary is used
// here to avoid duplicate errors being added for a type.
private ConcurrentDictionary<Type, Dictionary<Type, List<string>>> _cachedValidationErrors
= new ConcurrentDictionary<Type, Dictionary<Type, List<string>>>();
public void Validate([NotNull] Type modelType, [NotNull] ModelStateDictionary modelStateDictionary)
{
var visitedTypes = new HashSet<Type>();
// Every node maintains a dictionary of Type => Errors.
// It's a dictionary as we want to avoid adding duplicate error messages.
// Example:
// In the following case, from the perspective of type 'Store', we should not see duplicate
// errors related to type 'Address'
// public class Store
// {
// [Required]
// public int Id { get; set; }
// public Address Address { get; set; }
// }
// public class Employee
// {
// [Required]
// public int Id { get; set; }
// public Address Address { get; set; }
// }
// public class Address
// {
// [Required]
// public string Line1 { get; set; }
// [Required]
// public int Zipcode { get; set; }
// [Required]
// public string State { get; set; }
// }
var rootNodeValidationErrors = new Dictionary<Type, List<string>>();
Validate(modelType, visitedTypes, rootNodeValidationErrors);
foreach (var validationError in rootNodeValidationErrors)
{
foreach (var validationErrorMessage in validationError.Value)
{
// Add error message to model state as exception to avoid
// disclosing details to end user as SerializableError sanitizes the
// model state errors having exceptions with a generic message when sending
// it to the client.
modelStateDictionary.TryAddModelError(
validationError.Key.FullName,
new InvalidOperationException(validationErrorMessage));
}
}
}
private void Validate(
Type modelType,
HashSet<Type> visitedTypes,
Dictionary<Type, List<string>> errors)
{
// We don't need to code special handling for KeyValuePair (for example, when the model type
// is Dictionary<,> which implements IEnumerable<KeyValuePair<TKey, TValue>>) as the model
// type here would be KeyValuePair<TKey, TValue> where Key and Value are public properties
// which would also be probed for Required attribute validation.
if (modelType.GetTypeInfo().IsGenericType)
{
var enumerableOfT = ClosedGenericMatcher.ExtractGenericInterface(modelType, typeof(IEnumerable<>));
if (enumerableOfT != null)
{
modelType = enumerableOfT.GenericTypeArguments[0];
}
}
if (ExcludeTypeFromValidation(modelType))
{
return;
}
// Avoid infinite loop in case of self-referencing properties
if (!visitedTypes.Add(modelType))
{
return;
}
Dictionary<Type, List<string>> cachedErrors;
if (_cachedValidationErrors.TryGetValue(modelType, out cachedErrors))
{
foreach (var validationError in cachedErrors)
{
errors.Add(validationError.Key, validationError.Value);
}
return;
}
foreach (var propertyHelper in PropertyHelper.GetProperties(modelType))
{
var propertyInfo = propertyHelper.Property;
var propertyType = propertyInfo.PropertyType;
// Since DefaultObjectValidator can handle Required attribute validation for reference types,
// we only consider value types here.
if (propertyType.GetTypeInfo().IsValueType && !TypeHelper.IsNullableValueType(propertyType))
{
var validationError = GetValidationError(propertyInfo);
if (validationError != null)
{
List<string> errorMessages;
if (!errors.TryGetValue(validationError.ModelType, out errorMessages))
{
errorMessages = new List<string>();
errors.Add(validationError.ModelType, errorMessages);
}
errorMessages.Add(Resources.FormatRequiredProperty_MustHaveDataMemberRequired(
typeof(DataContractSerializer).FullName,
typeof(RequiredAttribute).FullName,
typeof(DataMemberAttribute).FullName,
nameof(DataMemberAttribute.IsRequired),
bool.TrueString,
validationError.PropertyName,
validationError.ModelType.FullName));
}
// if the type is not primitve, then it could be a struct in which case
// we need to probe its properties for validation
if (propertyType.GetTypeInfo().IsPrimitive)
{
continue;
}
}
var childNodeErrors = new Dictionary<Type, List<string>>();
Validate(propertyType, visitedTypes, childNodeErrors);
// Avoid adding duplicate errors at current node.
foreach (var modelTypeKey in childNodeErrors.Keys)
{
if (!errors.ContainsKey(modelTypeKey))
{
errors.Add(modelTypeKey, childNodeErrors[modelTypeKey]);
}
}
}
_cachedValidationErrors.TryAdd(modelType, errors);
visitedTypes.Remove(modelType);
}
private ValidationError GetValidationError(PropertyInfo propertyInfo)
{
var required = propertyInfo.GetCustomAttribute(typeof(RequiredAttribute), inherit: true);
if (required == null)
{
return null;
}
var dataMemberRequired = (DataMemberAttribute)propertyInfo.GetCustomAttribute(
typeof(DataMemberAttribute),
inherit: true);
if (dataMemberRequired != null && dataMemberRequired.IsRequired)
{
return null;
}
return new ValidationError()
{
ModelType = propertyInfo.DeclaringType,
PropertyName = propertyInfo.Name
};
}
private bool ExcludeTypeFromValidation(Type modelType)
{
return TypeHelper.IsSimpleType(modelType);
}
private class ValidationError
{
public Type ModelType { get; set; }
public string PropertyName { get; set; }
}
}
}

View File

@ -24,7 +24,6 @@ namespace Microsoft.AspNet.Mvc.Xml
private DataContractSerializerSettings _serializerSettings;
private ConcurrentDictionary<Type, object> _serializerCache = new ConcurrentDictionary<Type, object>();
private readonly XmlDictionaryReaderQuotas _readerQuotas = FormattingUtilities.GetDefaultXmlReaderQuotas();
private readonly DataAnnotationRequiredAttributeValidation _dataAnnotationRequiredAttributeValidation;
/// <summary>
/// Initializes a new instance of DataContractSerializerInputFormatter
@ -41,8 +40,6 @@ namespace Microsoft.AspNet.Mvc.Xml
WrapperProviderFactories = new List<IWrapperProviderFactory>();
WrapperProviderFactories.Add(new SerializableErrorWrapperProviderFactory());
_dataAnnotationRequiredAttributeValidation = new DataAnnotationRequiredAttributeValidation();
}
/// <summary>
@ -103,11 +100,6 @@ namespace Microsoft.AspNet.Mvc.Xml
using (var xmlReader = CreateXmlReader(new NonDisposableStream(request.Body), effectiveEncoding))
{
var type = GetSerializableType(context.ModelType);
_dataAnnotationRequiredAttributeValidation.Validate(
type,
context.ModelState);
var serializer = GetCachedSerializer(type);
var deserializedObject = serializer.ReadObject(xmlReader);

View File

@ -54,10 +54,8 @@ namespace Microsoft.AspNet.Mvc.FunctionalTests
data);
}
// Verifies that even though all the required data is posted to an action, the model
// state has errors related to value types's Required attribute validation.
[Fact]
public async Task RequiredDataIsProvided_AndModelIsBound_AndHasRequiredAttributeValidationErrors()
public async Task RequiredDataIsProvided_AndModelIsBound_NoValidationErrors()
{
// Arrange
var server = TestHelper.CreateServer(_app, SiteName, _configureServices);
@ -67,13 +65,6 @@ namespace Microsoft.AspNet.Mvc.FunctionalTests
"xmlns:i=\"http://www.w3.org/2001/XMLSchema-instance\"><Address><State>WA</State><Zipcode>" +
"98052</Zipcode></Address><Id>10</Id></Store>";
var content = new StringContent(input, Encoding.UTF8, "application/xml-dcs");
var propertiesCollection = new List<KeyValuePair<string, string>>();
propertiesCollection.Add(new KeyValuePair<string, string>(nameof(Store.Id), typeof(Store).FullName));
propertiesCollection.Add(new KeyValuePair<string, string>(nameof(Address.Zipcode), typeof(Address).FullName));
var expectedErrorMessages = propertiesCollection.Select(kvp =>
{
return string.Format(errorMessageFormat, kvp.Key, kvp.Value);
});
// Act
var response = await client.PostAsync("http://localhost/Validation/CreateStore", content);
@ -88,20 +79,12 @@ namespace Microsoft.AspNet.Mvc.FunctionalTests
Assert.NotNull(modelBindingInfo.Store.Address);
Assert.Equal(98052, modelBindingInfo.Store.Address.Zipcode);
Assert.Equal("WA", modelBindingInfo.Store.Address.State);
Assert.NotNull(modelBindingInfo.ModelStateErrorMessages);
Assert.Equal(expectedErrorMessages.Count(), modelBindingInfo.ModelStateErrorMessages.Count);
foreach (var expectedErrorMessage in expectedErrorMessages)
{
Assert.Contains(
modelBindingInfo.ModelStateErrorMessages,
(actualErrorMessage) => actualErrorMessage.Equals(expectedErrorMessage));
}
Assert.Empty(modelBindingInfo.ModelStateErrorMessages);
}
// Verifies that the model state has errors related to body model validation(for reference types) and also for
// Required attribute validation (for value types).
// Verifies that the model state has errors related to body model validation.
[Fact]
public async Task DataMissingForReferneceTypeProperties_AndModelIsBound_AndHasMixedValidationErrors()
public async Task DataMissingForRefereneceTypeProperties_AndModelIsBound_AndHasMixedValidationErrors()
{
// Arrange
var server = TestHelper.CreateServer(_app, SiteName, _configureServices);
@ -111,13 +94,8 @@ namespace Microsoft.AspNet.Mvc.FunctionalTests
" xmlns:i=\"http://www.w3.org/2001/XMLSchema-instance\">" +
"<Address i:nil=\"true\"/><Id>10</Id></Store>";
var content = new StringContent(input, Encoding.UTF8, "application/xml-dcs");
var propertiesCollection = new List<KeyValuePair<string, string>>();
propertiesCollection.Add(new KeyValuePair<string, string>(nameof(Store.Id), typeof(Store).FullName));
propertiesCollection.Add(new KeyValuePair<string, string>(nameof(Address.Zipcode), typeof(Address).FullName));
var expectedErrorMessages = propertiesCollection.Select(kvp =>
{
return string.Format(errorMessageFormat, kvp.Key, kvp.Value);
}).ToList();
var expectedErrorMessages = new List<string>();
expectedErrorMessages.Add("Address:The Address field is required.");
// Act
@ -131,6 +109,7 @@ namespace Microsoft.AspNet.Mvc.FunctionalTests
Assert.NotNull(modelBindingInfo.Store);
Assert.Equal(10, modelBindingInfo.Store.Id);
Assert.NotNull(modelBindingInfo.ModelStateErrorMessages);
Assert.Equal(expectedErrorMessages.Count(), modelBindingInfo.ModelStateErrorMessages.Count);
foreach (var expectedErrorMessage in expectedErrorMessages)
{

View File

@ -53,15 +53,6 @@ namespace Microsoft.AspNet.Mvc.Xml
public TestLevelOne TestOne { get; set; }
}
private readonly string requiredErrorMessageFormat = string.Format(
"{0} does not recognize '{1}', so instead use '{2}' with '{3}' set to '{4}' for value type property " +
"'{{0}}' on type '{{1}}'.",
typeof(DataContractSerializer).FullName,
typeof(RequiredAttribute).FullName,
typeof(DataMemberAttribute).FullName,
nameof(DataMemberAttribute.IsRequired),
bool.TrueString);
[Theory]
[InlineData("application/xml", true)]
[InlineData("application/*", true)]
@ -513,637 +504,6 @@ namespace Microsoft.AspNet.Mvc.Xml
Assert.Equal(expectedInt, dummyModel.SampleInt);
Assert.Equal(expectedString, dummyModel.SampleString);
}
[Fact]
public async Task PostingListOfModels_HasRequiredAttributeValidationErrors()
{
// Arrange
var input = "<?xml version=\"1.0\" encoding=\"utf-8\"?><ArrayOfAddress " +
"xmlns=\"http://schemas.datacontract.org/2004/07/Microsoft.AspNet.Mvc.Xml\" " +
"xmlns:i=\"http://www.w3.org/2001/XMLSchema-instance\">" +
"<Address><IsResidential>true</IsResidential><Zipcode>98052" +
"</Zipcode></Address></ArrayOfAddress>";
var formatter = new XmlDataContractSerializerInputFormatter();
var contentBytes = Encodings.UTF8EncodingWithoutBOM.GetBytes(input);
var context = GetInputFormatterContext(contentBytes, typeof(List<Address>));
// Act
var model = await formatter.ReadAsync(context) as List<Address>;
// Assert
Assert.NotNull(model);
Assert.Equal(1, model.Count);
Assert.Equal(98052, model[0].Zipcode);
Assert.Equal(true, model[0].IsResidential);
Assert.Equal(1, context.ModelState.Keys.Count);
AssertModelStateErrorMessages(
typeof(Address).FullName,
context,
expectedErrorMessages: new[]
{
string.Format(requiredErrorMessageFormat, nameof(Address.Zipcode), typeof(Address).FullName),
string.Format(
requiredErrorMessageFormat,
nameof(Address.IsResidential),
typeof(Address).FullName)
});
}
[Fact]
public async Task PostingModel_HasRequiredAttributeValidationErrors()
{
// Arrange
var input = "<?xml version=\"1.0\" encoding=\"utf-8\"?>" +
"<Address xmlns=\"http://schemas.datacontract.org/2004/07/Microsoft.AspNet.Mvc.Xml\"" +
" xmlns:i=\"http://www.w3.org/2001/XMLSchema-instance\"><IsResidential>" +
"true</IsResidential><Zipcode>98052</Zipcode></Address>";
var formatter = new XmlDataContractSerializerInputFormatter();
var contentBytes = Encodings.UTF8EncodingWithoutBOM.GetBytes(input);
var context = GetInputFormatterContext(contentBytes, typeof(Address));
// Act
var model = await formatter.ReadAsync(context) as Address;
// Assert
Assert.NotNull(model);
Assert.Equal(98052, model.Zipcode);
Assert.Equal(true, model.IsResidential);
Assert.Equal(1, context.ModelState.Keys.Count);
AssertModelStateErrorMessages(
typeof(Address).FullName,
context,
expectedErrorMessages: new[]
{
string.Format(requiredErrorMessageFormat, nameof(Address.Zipcode), typeof(Address).FullName),
string.Format(
requiredErrorMessageFormat,
nameof(Address.IsResidential),
typeof(Address).FullName)
});
}
[Fact]
public async Task PostingModelWithProperty_HasRequiredAttributeValidationErrors()
{
// Arrange
var input = "<?xml version=\"1.0\" encoding=\"utf-8\"?>" +
"<ModelWithPropertyHavingRequiredAttributeValidationErrors " +
"xmlns=\"http://schemas.datacontract.org/2004/07/Microsoft.AspNet.Mvc.Xml\"" +
" xmlns:i=\"http://www.w3.org/2001/XMLSchema-instance\"><AddressProperty>" +
"<IsResidential>true</IsResidential><Zipcode>98052</Zipcode></AddressProperty>" +
"</ModelWithPropertyHavingRequiredAttributeValidationErrors>";
var formatter = new XmlDataContractSerializerInputFormatter();
var contentBytes = Encodings.UTF8EncodingWithoutBOM.GetBytes(input);
var context = GetInputFormatterContext(
contentBytes,
typeof(ModelWithPropertyHavingRequiredAttributeValidationErrors));
// Act
var model = await formatter.ReadAsync(context) as ModelWithPropertyHavingRequiredAttributeValidationErrors;
// Assert
Assert.NotNull(model);
Assert.NotNull(model.AddressProperty);
Assert.Equal(98052, model.AddressProperty.Zipcode);
Assert.Equal(true, model.AddressProperty.IsResidential);
Assert.Equal(1, context.ModelState.Keys.Count);
AssertModelStateErrorMessages(
typeof(Address).FullName,
context,
expectedErrorMessages: new[]
{
string.Format(requiredErrorMessageFormat, nameof(Address.Zipcode), typeof(Address).FullName),
string.Format(requiredErrorMessageFormat, nameof(Address.IsResidential), typeof(Address).FullName)
});
}
[Fact]
public async Task PostingModel_WithCollectionProperty_HasRequiredAttributeValidationErrors()
{
// Arrange
var input = "<?xml version=\"1.0\" encoding=\"utf-8\"?>" +
"<ModelWithCollectionPropertyHavingRequiredAttributeValidationErrors" +
" xmlns=\"http://schemas.datacontract.org/2004/07/Microsoft.AspNet.Mvc.Xml\"" +
" xmlns:i=\"http://www.w3.org/2001/XMLSchema-instance\"><Addresses><Address>" +
"<IsResidential>true</IsResidential><Zipcode>98052</Zipcode></Address></Addresses>" +
"</ModelWithCollectionPropertyHavingRequiredAttributeValidationErrors>";
var formatter = new XmlDataContractSerializerInputFormatter();
var contentBytes = Encodings.UTF8EncodingWithoutBOM.GetBytes(input);
var context = GetInputFormatterContext(
contentBytes,
typeof(ModelWithCollectionPropertyHavingRequiredAttributeValidationErrors));
// Act
var model = await formatter.ReadAsync(context)
as ModelWithCollectionPropertyHavingRequiredAttributeValidationErrors;
// Assert
Assert.NotNull(model);
Assert.NotNull(model.Addresses);
Assert.Equal(98052, model.Addresses[0].Zipcode);
Assert.Equal(true, model.Addresses[0].IsResidential);
Assert.Equal(1, context.ModelState.Keys.Count);
AssertModelStateErrorMessages(
typeof(Address).FullName,
context,
new[]
{
string.Format(requiredErrorMessageFormat, nameof(Address.Zipcode), typeof(Address).FullName),
string.Format(requiredErrorMessageFormat, nameof(Address.IsResidential), typeof(Address).FullName)
});
}
[Fact]
public async Task PostingModelInheritingType_HasRequiredAttributeValidationErrors()
{
// Arrange
var input = "<?xml version=\"1.0\" encoding=\"utf-8\"?>" +
"<ModelInheritingTypeHavingRequiredAttributeValidationErrors" +
" xmlns=\"http://schemas.datacontract.org/2004/07/Microsoft.AspNet.Mvc.Xml\"" +
" xmlns:i=\"http://www.w3.org/2001/XMLSchema-instance\">" +
"<IsResidential>true</IsResidential><Zipcode>98052</Zipcode>" +
"</ModelInheritingTypeHavingRequiredAttributeValidationErrors>";
var formatter = new XmlDataContractSerializerInputFormatter();
var contentBytes = Encodings.UTF8EncodingWithoutBOM.GetBytes(input);
var context = GetInputFormatterContext(
contentBytes,
typeof(ModelInheritingTypeHavingRequiredAttributeValidationErrors));
// Act
var model = await formatter.ReadAsync(context)
as ModelInheritingTypeHavingRequiredAttributeValidationErrors;
// Assert
Assert.NotNull(model);
Assert.Equal(98052, model.Zipcode);
Assert.Equal(true, model.IsResidential);
Assert.Equal(1, context.ModelState.Keys.Count);
AssertModelStateErrorMessages(
typeof(Address).FullName,
context,
expectedErrorMessages: new[]
{
string.Format(requiredErrorMessageFormat, nameof(Address.Zipcode), typeof(Address).FullName),
string.Format(requiredErrorMessageFormat, nameof(Address.IsResidential), typeof(Address).FullName)
});
}
[Fact]
public async Task PostingModelHavingNullableValueTypes_NoRequiredAttributeValidationErrors()
{
// Arrange
var input = "<?xml version=\"1.0\" encoding=\"utf-8\"?><CarInfo " +
"xmlns=\"http://schemas.datacontract.org/2004/07/Microsoft.AspNet.Mvc.Xml\"" +
" xmlns:i=\"http://www.w3.org/2001/XMLSchema-instance\">" +
"<ServicedYears xmlns:a=\"http://schemas.datacontract.org/2004/07/System\">" +
"<a:int>2006</a:int><a:int>2007</a:int></ServicedYears><Year>2005</Year></CarInfo>";
var formatter = new XmlDataContractSerializerInputFormatter();
var contentBytes = Encodings.UTF8EncodingWithoutBOM.GetBytes(input);
var context = GetInputFormatterContext(contentBytes, typeof(CarInfo));
var expectedModel = new CarInfo() { Year = 2005, ServicedYears = new List<int?>() };
expectedModel.ServicedYears.Add(2006);
expectedModel.ServicedYears.Add(2007);
// Act
var model = await formatter.ReadAsync(context) as CarInfo;
// Assert
Assert.NotNull(model);
Assert.Equal(expectedModel.Year, model.Year);
Assert.Equal(expectedModel.ServicedYears, model.ServicedYears);
Assert.Empty(context.ModelState);
}
[Fact]
public async Task PostingModel_WithPropertyHavingNullableValueTypes_NoRequiredAttributeValidationErrors()
{
// Arrange
var input = "<?xml version=\"1.0\" encoding=\"utf-8\"?>" +
"<ModelWithPropertyHavingTypeWithNullableProperties " +
"xmlns=\"http://schemas.datacontract.org/2004/07/Microsoft.AspNet.Mvc.Xml\" " +
"xmlns:i=\"http://www.w3.org/2001/XMLSchema-instance\"><CarInfoProperty>" +
"<ServicedYears xmlns:a=\"http://schemas.datacontract.org/2004/07/System\"><a:int>2006</a:int>" +
"<a:int>2007</a:int></ServicedYears><Year>2005</Year></CarInfoProperty>" +
"</ModelWithPropertyHavingTypeWithNullableProperties>";
var formatter = new XmlDataContractSerializerInputFormatter();
var contentBytes = Encodings.UTF8EncodingWithoutBOM.GetBytes(input);
var context = GetInputFormatterContext(
contentBytes,
typeof(ModelWithPropertyHavingTypeWithNullableProperties));
var expectedModel = new ModelWithPropertyHavingTypeWithNullableProperties()
{
CarInfoProperty = new CarInfo() { Year = 2005, ServicedYears = new List<int?>() }
};
expectedModel.CarInfoProperty.ServicedYears.Add(2006);
expectedModel.CarInfoProperty.ServicedYears.Add(2007);
// Act
var model = await formatter.ReadAsync(context) as ModelWithPropertyHavingTypeWithNullableProperties;
// Assert
Assert.NotNull(model);
Assert.NotNull(model.CarInfoProperty);
Assert.Equal(expectedModel.CarInfoProperty.Year, model.CarInfoProperty.Year);
Assert.Equal(expectedModel.CarInfoProperty.ServicedYears, model.CarInfoProperty.ServicedYears);
Assert.Empty(context.ModelState);
}
[Fact]
public async Task PostingModel_WithPropertySelfReferencingItself()
{
// Arrange
var input = "<Employee xmlns=\"http://schemas.datacontract.org/2004/07/Microsoft.AspNet.Mvc.Xml\"" +
" xmlns:i=\"http://www.w3.org/2001/XMLSchema-instance\"><Id>10</Id><Manager><Id>11</Id><Manager" +
" i:nil=\"true\"/><Name>Mike</Name></Manager><Name>John</Name></Employee>";
var formatter = new XmlDataContractSerializerInputFormatter();
var contentBytes = Encodings.UTF8EncodingWithoutBOM.GetBytes(input);
var context = GetInputFormatterContext(contentBytes, typeof(Employee));
var expectedModel = new Employee()
{
Id = 10,
Name = "John",
Manager = new Employee()
{
Id = 11,
Name = "Mike"
}
};
// Act
var model = await formatter.ReadAsync(context) as Employee;
// Assert
Assert.NotNull(model);
Assert.Equal(expectedModel.Id, model.Id);
Assert.Equal(expectedModel.Name, model.Name);
Assert.NotNull(model.Manager);
Assert.Equal(expectedModel.Manager.Id, model.Manager.Id);
Assert.Equal(expectedModel.Manager.Name, model.Manager.Name);
Assert.Null(model.Manager.Manager);
Assert.Equal(1, context.ModelState.Keys.Count);
AssertModelStateErrorMessages(
typeof(Employee).FullName,
context,
expectedErrorMessages: new[]
{
string.Format(requiredErrorMessageFormat, nameof(Employee.Id), typeof(Employee).FullName)
});
}
[Fact]
public async Task PostingModel_WithBothRequiredAndDataMemberRequired_NoValidationErrors()
{
// Arrange
var input = "<?xml version=\"1.0\" encoding=\"utf-8\"?>" +
"<Laptop xmlns=\"http://schemas.datacontract.org/2004/07/Microsoft.AspNet.Mvc.Xml\"" +
" xmlns:i=\"http://www.w3.org/2001/XMLSchema-instance\"><Id>" +
"10</Id><SupportsVirtualization>true</SupportsVirtualization></Laptop>";
var formatter = new XmlDataContractSerializerInputFormatter();
var contentBytes = Encodings.UTF8EncodingWithoutBOM.GetBytes(input);
var context = GetInputFormatterContext(contentBytes, typeof(Laptop));
// Act
var model = await formatter.ReadAsync(context) as Laptop;
// Assert
Assert.NotNull(model);
Assert.Equal(10, model.Id);
Assert.Equal(true, model.SupportsVirtualization);
Assert.Empty(context.ModelState);
}
[Fact]
public async Task PostingListofModels_WithBothRequiredAndDataMemberRequired_NoValidationErrors()
{
// Arrange
var input = "<?xml version=\"1.0\" encoding=\"utf-8\"?>" +
"<ArrayOfLaptop xmlns=\"http://schemas.datacontract.org/2004/07/Microsoft.AspNet.Mvc.Xml\"" +
" xmlns:i=\"http://www.w3.org/2001/XMLSchema-instance\"><Laptop><Id>" +
"10</Id><SupportsVirtualization>true</SupportsVirtualization></Laptop></ArrayOfLaptop>";
var formatter = new XmlDataContractSerializerInputFormatter();
var contentBytes = Encodings.UTF8EncodingWithoutBOM.GetBytes(input);
var context = GetInputFormatterContext(contentBytes, typeof(List<Laptop>));
// Act
var model = await formatter.ReadAsync(context) as List<Laptop>;
// Assert
Assert.NotNull(model);
Assert.Equal(1, model.Count);
Assert.Equal(10, model[0].Id);
Assert.Equal(true, model[0].SupportsVirtualization);
Assert.Empty(context.ModelState);
}
[Fact]
public async Task PostingModel_WithRequiredAndDataMemberNoRequired_HasValidationErrors()
{
// Arrange
var input = "<?xml version=\"1.0\" encoding=\"utf-8\"?>" +
"<Product xmlns=\"http://schemas.datacontract.org/2004/07/Microsoft.AspNet.Mvc.Xml\"" +
" xmlns:i=\"http://www.w3.org/2001/XMLSchema-instance\"><Id>" +
"10</Id><Name>Phone</Name></Product>";
var formatter = new XmlDataContractSerializerInputFormatter();
var contentBytes = Encodings.UTF8EncodingWithoutBOM.GetBytes(input);
var context = GetInputFormatterContext(contentBytes, typeof(Product));
// Act
var model = await formatter.ReadAsync(context) as Product;
// Assert
Assert.NotNull(model);
Assert.Equal(10, model.Id);
Assert.Equal(2, context.ModelState.Keys.Count);
AssertModelStateErrorMessages(
typeof(Product).FullName,
context,
expectedErrorMessages: new[]
{
string.Format(requiredErrorMessageFormat, nameof(Product.Id), typeof(Product).FullName)
});
AssertModelStateErrorMessages(
typeof(Address).FullName,
context,
expectedErrorMessages: new[]
{
string.Format(requiredErrorMessageFormat, nameof(Address.Zipcode), typeof(Address).FullName),
string.Format(requiredErrorMessageFormat, nameof(Address.IsResidential), typeof(Address).FullName)
});
}
[Fact]
public async Task PostingListOfModels_WithRequiredAndDataMemberNoRequired_HasValidationErrors()
{
// Arrange
var input = "<?xml version=\"1.0\" encoding=\"utf-8\"?>" +
"<ArrayOfProduct xmlns=\"http://schemas.datacontract.org/2004/07/Microsoft.AspNet.Mvc.Xml\"" +
" xmlns:i=\"http://www.w3.org/2001/XMLSchema-instance\"><Product><Id>" +
"10</Id><Name>Phone</Name></Product></ArrayOfProduct>";
var formatter = new XmlDataContractSerializerInputFormatter();
var contentBytes = Encodings.UTF8EncodingWithoutBOM.GetBytes(input);
var context = GetInputFormatterContext(contentBytes, typeof(List<Product>));
// Act
var model = await formatter.ReadAsync(context) as List<Product>;
// Assert
Assert.NotNull(model);
Assert.Equal(1, model.Count);
Assert.Equal(10, model[0].Id);
Assert.Equal("Phone", model[0].Name);
Assert.Equal(2, context.ModelState.Keys.Count);
AssertModelStateErrorMessages(
typeof(Product).FullName,
context,
expectedErrorMessages: new[]
{
string.Format(requiredErrorMessageFormat, nameof(Product.Id), typeof(Product).FullName)
});
AssertModelStateErrorMessages(
typeof(Address).FullName,
context,
expectedErrorMessages: new[]
{
string.Format(requiredErrorMessageFormat, nameof(Address.Zipcode), typeof(Address).FullName),
string.Format(requiredErrorMessageFormat, nameof(Address.IsResidential), typeof(Address).FullName)
});
}
[Fact]
public async Task PostingModel_WithDeeperHierarchy_HasValidationErrors()
{
// Arrange
var input = "<?xml version=\"1.0\" encoding=\"utf-8\"?>" +
"<Store xmlns=\"http://schemas.datacontract.org/2004/07/Microsoft.AspNet.Mvc.Xml\"" +
" xmlns:i=\"http://www.w3.org/2001/XMLSchema-instance\" i:nil=\"true\" />";
var formatter = new XmlDataContractSerializerInputFormatter();
var contentBytes = Encodings.UTF8EncodingWithoutBOM.GetBytes(input);
var context = GetInputFormatterContext(contentBytes, typeof(Store));
// Act
var model = await formatter.ReadAsync(context) as Store;
// Assert
Assert.Null(model);
Assert.Equal(3, context.ModelState.Keys.Count);
AssertModelStateErrorMessages(
typeof(Address).FullName,
context,
expectedErrorMessages: new[]
{
string.Format(requiredErrorMessageFormat, nameof(Address.IsResidential), typeof(Address).FullName),
string.Format(requiredErrorMessageFormat, nameof(Address.Zipcode), typeof(Address).FullName)
});
AssertModelStateErrorMessages(
typeof(Employee).FullName,
context,
expectedErrorMessages: new[]
{
string.Format(requiredErrorMessageFormat, nameof(Employee.Id), typeof(Employee).FullName)
});
AssertModelStateErrorMessages(
typeof(Product).FullName,
context,
expectedErrorMessages: new[]
{
string.Format(requiredErrorMessageFormat, nameof(Product.Id), typeof(Product).FullName)
});
}
[Fact]
public async Task PostingModelOfStructs_WithDeeperHierarchy_HasValidationErrors()
{
// Arrange
var input = "<School i:nil=\"true\" " +
"xmlns=\"http://schemas.datacontract.org/2004/07/Microsoft.AspNet.Mvc.Xml\" " +
"xmlns:i=\"http://www.w3.org/2001/XMLSchema-instance\"/>";
var formatter = new XmlDataContractSerializerInputFormatter();
var contentBytes = Encodings.UTF8EncodingWithoutBOM.GetBytes(input);
var context = GetInputFormatterContext(contentBytes, typeof(School));
// Act
var model = await formatter.ReadAsync(context);
// Assert
Assert.Null(model);
Assert.Equal(3, context.ModelState.Keys.Count);
AssertModelStateErrorMessages(
typeof(School).FullName,
context,
expectedErrorMessages: new[]
{
string.Format(requiredErrorMessageFormat, nameof(School.Id), typeof(School).FullName)
});
AssertModelStateErrorMessages(
typeof(Website).FullName,
context,
expectedErrorMessages: new[]
{
string.Format(requiredErrorMessageFormat, nameof(Website.Id), typeof(Website).FullName)
});
AssertModelStateErrorMessages(
typeof(Student).FullName,
context,
expectedErrorMessages: new[]
{
string.Format(requiredErrorMessageFormat, nameof(Student.Id), typeof(Student).FullName)
});
}
[Fact]
public async Task PostingModel_WithDictionaryProperty_HasValidationErrorsOnKeyAndValue()
{
// Arrange
var input = "<FavoriteLocations " +
"i:nil=\"true\" xmlns=\"http://schemas.datacontract.org/2004/07/Microsoft.AspNet.Mvc.Xml\"" +
" xmlns:i=\"http://www.w3.org/2001/XMLSchema-instance\"/>";
var formatter = new XmlDataContractSerializerInputFormatter();
var contentBytes = Encodings.UTF8EncodingWithoutBOM.GetBytes(input);
var context = GetInputFormatterContext(contentBytes, typeof(FavoriteLocations));
// Act
var model = await formatter.ReadAsync(context);
// Assert
Assert.Null(model);
Assert.Equal(2, context.ModelState.Keys.Count);
AssertModelStateErrorMessages(
typeof(Point).FullName,
context,
expectedErrorMessages: new[]
{
string.Format(requiredErrorMessageFormat, nameof(Point.X), typeof(Point).FullName),
string.Format(requiredErrorMessageFormat, nameof(Point.Y), typeof(Point).FullName)
});
AssertModelStateErrorMessages(
typeof(Address).FullName,
context,
expectedErrorMessages: new[]
{
string.Format(requiredErrorMessageFormat, nameof(Address.IsResidential), typeof(Address).FullName),
string.Format(requiredErrorMessageFormat, nameof(Address.Zipcode), typeof(Address).FullName)
});
}
[Fact]
public async Task PostingModel_WithDifferentValueTypeProperties_HasValidationErrors()
{
// Arrange
var input = "<ValueTypePropertiesModel i:nil=\"true\" " +
"xmlns=\"http://schemas.datacontract.org/2004/07/Microsoft.AspNet.Mvc.Xml\" " +
"xmlns:i=\"http://www.w3.org/2001/XMLSchema-instance\"/>";
var formatter = new XmlDataContractSerializerInputFormatter();
var contentBytes = Encodings.UTF8EncodingWithoutBOM.GetBytes(input);
var context = GetInputFormatterContext(contentBytes, typeof(ValueTypePropertiesModel));
// Act
var model = await formatter.ReadAsync(context);
// Assert
Assert.Null(model);
Assert.Equal(3, context.ModelState.Keys.Count);
AssertModelStateErrorMessages(
typeof(Point).FullName,
context,
expectedErrorMessages: new[]
{
string.Format(requiredErrorMessageFormat, nameof(Point.X), typeof(Point).FullName),
string.Format(requiredErrorMessageFormat, nameof(Point.X), typeof(Point).FullName)
});
AssertModelStateErrorMessages(
typeof(GpsCoordinate).FullName,
context,
expectedErrorMessages: new[]
{
string.Format(
requiredErrorMessageFormat,
nameof(GpsCoordinate.Latitude),
typeof(GpsCoordinate).FullName),
string.Format(
requiredErrorMessageFormat,
nameof(GpsCoordinate.Longitude),
typeof(GpsCoordinate).FullName)
});
AssertModelStateErrorMessages(
typeof(ValueTypePropertiesModel).FullName,
context,
expectedErrorMessages: new[]
{
string.Format(
requiredErrorMessageFormat,
nameof(ValueTypePropertiesModel.IntProperty),
typeof(ValueTypePropertiesModel).FullName),
string.Format(
requiredErrorMessageFormat,
nameof(ValueTypePropertiesModel.DateTimeProperty),
typeof(ValueTypePropertiesModel).FullName),
string.Format(
requiredErrorMessageFormat,
nameof(ValueTypePropertiesModel.PointProperty),
typeof(ValueTypePropertiesModel).FullName),
string.Format(
requiredErrorMessageFormat,
nameof(ValueTypePropertiesModel.GpsCoordinateProperty),
typeof(ValueTypePropertiesModel).FullName)
});
}
private void AssertModelStateErrorMessages(
string modelStateKey,
InputFormatterContext context,
IEnumerable<string> expectedErrorMessages)
{
ModelState modelState;
context.ModelState.TryGetValue(modelStateKey, out modelState);
Assert.NotNull(modelState);
Assert.NotEmpty(modelState.Errors);
var actualErrorMessages = modelState.Errors.Select(error =>
{
if (string.IsNullOrEmpty(error.ErrorMessage))
{
if (error.Exception != null)
{
return error.Exception.Message;
}
}
return error.ErrorMessage;
});
Assert.Equal(expectedErrorMessages.Count(), actualErrorMessages.Count());
if (expectedErrorMessages != null)
{
foreach (var expectedErrorMessage in expectedErrorMessages)
{
Assert.Contains(expectedErrorMessage, actualErrorMessages);
}
}
}
private InputFormatterContext GetInputFormatterContext(byte[] contentBytes, Type modelType)
{
var httpContext = GetHttpContext(contentBytes);
@ -1177,178 +537,4 @@ namespace Microsoft.AspNet.Mvc.Xml
}
}
}
public class Address
{
[Required]
public int Zipcode { get; set; }
[Required]
public bool IsResidential { get; set; }
}
public class CarInfo
{
[Required]
public int? Year { get; set; }
[Required]
public List<int?> ServicedYears { get; set; }
}
public class Employee
{
[Required]
public int Id { get; set; }
[Required]
public string Name { get; set; }
[Required]
public Employee Manager { get; set; }
}
[DataContract]
public class Laptop
{
[DataMember(IsRequired = true)]
[Required]
public int Id { get; set; }
[DataMember(IsRequired = true)]
[Required]
public bool SupportsVirtualization { get; set; }
}
[DataContract]
public class Product
{
// Here the property has DataMember but does not set the value 'IsRequired = true'
[DataMember(Name = "Id")]
[Required]
public int Id { get; set; }
[DataMember(Name = "Name")]
[Required]
public string Name { get; set; }
[DataMember(Name = "Manufacturer")]
[Required]
public Manufacturer Manufacturer { get; set; }
}
public class ModelWithPropertyHavingRequiredAttributeValidationErrors
{
public Address AddressProperty { get; set; }
}
public class ModelWithCollectionPropertyHavingRequiredAttributeValidationErrors
{
public List<Address> Addresses { get; set; }
}
public class ModelInheritingTypeHavingRequiredAttributeValidationErrors : Address
{
}
public class ModelWithPropertyHavingTypeWithNullableProperties
{
public CarInfo CarInfoProperty { get; set; }
}
public class Store
{
public StoreDetails StoreDetails { get; set; }
public List<Product> Products { get; set; }
}
public class StoreDetails
{
public List<Employee> Employees { get; set; }
public Address Address { get; set; }
}
public class Manufacturer
{
public Address Address { get; set; }
}
public struct School
{
[Required]
public int Id { get; set; }
public List<Student> Students { get; set; }
public Website Address { get; set; }
}
public struct Student
{
[Required]
public int Id { get; set; }
public Website Address { get; set; }
}
public struct Website
{
[Required]
public int Id { get; set; }
[Required]
public string Name { get; set; }
}
public struct ValueTypePropertiesModel
{
[Required]
public int IntProperty { get; set; }
[Required]
public int? NullableIntProperty { get; set; }
[Required]
public DateTime DateTimeProperty { get; set; }
[Required]
public DateTime? NullableDateTimeProperty { get; set; }
[Required]
public Point PointProperty { get; set; }
[Required]
public Point? NullablePointProperty { get; set; }
[Required]
public GpsCoordinate GpsCoordinateProperty { get; set; }
[Required]
public GpsCoordinate? NullableGpsCoordinateProperty { get; set; }
}
public struct GpsCoordinate
{
[Required]
public Point Latitude { get; set; }
[Required]
public Point Longitude { get; set; }
}
public struct Point
{
[Required]
public int X { get; set; }
[Required]
public int Y { get; set; }
}
public class FavoriteLocations
{
public Dictionary<Point, Address> Addresses { get; set; }
}
}