Added support for validation of Required attributes in case of XmlDataContractSerializerInputFormatter

This commit is contained in:
Kiran Challa 2015-02-12 06:44:40 -08:00
parent eef8884d0f
commit c276ddaa39
23 changed files with 1330 additions and 57 deletions

View File

@ -15,6 +15,7 @@
"Microsoft.AspNet.Server.IIS": "1.0.0-*",
"Microsoft.AspNet.Server.WebListener": "1.0.0-*",
"Microsoft.AspNet.StaticFiles": "1.0.0-*",
"Microsoft.Framework.PropertyHelper.Internal": { "version": "1.0.0-*", "type": "build" },
"Microsoft.Framework.NotNullAttribute.Internal": { "type": "build", "version": "1.0.0-*" }
},
"frameworks": {

View File

@ -14,7 +14,9 @@
"Microsoft.AspNet.Mvc.TagHelpers": "6.0.0-*",
"Microsoft.AspNet.Server.IIS": "1.0.0-*",
"Microsoft.AspNet.Server.WebListener": "1.0.0-*",
"Microsoft.AspNet.StaticFiles": "1.0.0-*"
"Microsoft.AspNet.StaticFiles": "1.0.0-*",
"Microsoft.Framework.NotNullAttribute.Internal": { "type": "build", "version": "1.0.0-*" },
"Microsoft.Framework.PropertyHelper.Internal": { "version": "1.0.0-*", "type": "build" }
},
"frameworks": {
"aspnet50": { },

View File

@ -1,8 +1,10 @@
// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved.
// 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;
using System.Collections.Generic;
using System.ComponentModel;
using System.Reflection;
using System.Threading.Tasks;
using Microsoft.Framework.Internal;
@ -60,5 +62,35 @@ namespace Microsoft.AspNet.Mvc
return dictionary;
}
public static bool IsSimpleType(Type type)
{
return type.GetTypeInfo().IsPrimitive ||
type.Equals(typeof(decimal)) ||
type.Equals(typeof(string)) ||
type.Equals(typeof(DateTime)) ||
type.Equals(typeof(Guid)) ||
type.Equals(typeof(DateTimeOffset)) ||
type.Equals(typeof(TimeSpan)) ||
type.Equals(typeof(Uri));
}
public static bool HasStringConverter(Type type)
{
return TypeDescriptor.GetConverter(type).CanConvertFrom(typeof(string));
}
public static bool IsCollectionType(Type type)
{
if (type == typeof(string))
{
// Even though string implements IEnumerable, we don't really think of it
// as a collection for the purposes of model binding.
return false;
}
// We only need to look for IEnumerable, because IEnumerable<T> extends it.
return typeof(IEnumerable).IsAssignableFrom(type);
}
}
}

View File

@ -8,6 +8,7 @@
"aspnet50": { },
"aspnetcore50": {
"dependencies": {
"System.ComponentModel.TypeConverter": "4.0.0-beta-*",
"System.Reflection.Extensions": "4.0.0-beta-*",
"System.Text.Encoding.Extensions": "4.0.10-beta-*"
}

View File

@ -1,43 +0,0 @@
// 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;
using System.Collections.Generic;
using System.ComponentModel;
using System.Reflection;
namespace Microsoft.AspNet.Mvc.ModelBinding
{
internal class TypeHelper
{
internal static bool IsSimpleType(Type type)
{
return type.GetTypeInfo().IsPrimitive ||
type.Equals(typeof(decimal)) ||
type.Equals(typeof(string)) ||
type.Equals(typeof(DateTime)) ||
type.Equals(typeof(Guid)) ||
type.Equals(typeof(DateTimeOffset)) ||
type.Equals(typeof(TimeSpan));
}
internal static bool HasStringConverter(Type type)
{
return TypeDescriptor.GetConverter(type).CanConvertFrom(typeof(string));
}
internal static bool IsCollectionType(Type type)
{
if (type == typeof(string))
{
// Even though string implements IEnumerable, we don't really think of it
// as a collection for the purposes of model binding.
return false;
}
// We only need to look for IEnumerable, because IEnumerable<T> extends it.
return typeof(IEnumerable).IsAssignableFrom(type);
}
}
}

View File

@ -54,16 +54,7 @@ namespace Microsoft.AspNet.Mvc.ModelBinding
/// </summary>
protected virtual bool IsSimpleType(Type type)
{
var result = type.GetTypeInfo().IsPrimitive ||
type.Equals(typeof(decimal)) ||
type.Equals(typeof(string)) ||
type.Equals(typeof(DateTime)) ||
type.Equals(typeof(Guid)) ||
type.Equals(typeof(DateTimeOffset)) ||
type.Equals(typeof(TimeSpan)) ||
type.Equals(typeof(Uri));
return result;
return TypeHelper.IsSimpleType(type);
}
}
}

View File

@ -9,6 +9,7 @@
"Microsoft.AspNet.Mvc.Common": { "version": "6.0.0-*", "type": "build" },
"Microsoft.AspNet.Razor.Runtime": "4.0.0-*",
"Microsoft.Framework.Cache.Memory": "1.0.0-*",
"Microsoft.Framework.PropertyHelper.Internal": { "version": "1.0.0-*", "type": "build" },
"Microsoft.Framework.NotNullAttribute.Internal": { "version": "1.0.0-*", "type": "build" }
},
"frameworks": {

View File

@ -10,6 +10,7 @@
"Microsoft.Framework.Cache.Memory": "1.0.0-*",
"Microsoft.Framework.FileSystemGlobbing": "1.0.0-*",
"Microsoft.Framework.Logging.Interfaces": { "version": "1.0.0-*", "type": "build" },
"Microsoft.Framework.PropertyHelper.Internal": { "version": "1.0.0-*", "type": "build" },
"Microsoft.Framework.NotNullAttribute.Internal": { "version": "1.0.0-*", "type": "build" },
"System.Security.Cryptography.Hashing.Algorithms": "4.0.0-beta-*"
},

View File

@ -9,6 +9,7 @@
"Microsoft.AspNet.Mvc.Core": "6.0.0-*",
"Microsoft.AspNet.Mvc.ModelBinding": "6.0.0-*",
"Microsoft.AspNet.WebApi.Client": "5.2.2",
"Microsoft.Framework.PropertyHelper.Internal": { "version": "1.0.0-*", "type": "build" },
"Microsoft.Framework.NotNullAttribute.Internal": { "version": "1.0.0-*", "type": "build" }
},
"frameworks": {

View File

@ -26,6 +26,22 @@ namespace Microsoft.AspNet.Mvc.Xml
return string.Format(CultureInfo.CurrentCulture, GetString("EnumerableWrapperProvider_InvalidSourceEnumerableOfT"), p0);
}
/// <summary>
/// {0} does not recognize '{1}', so instead use '{2}' with '{3}' set to '{4}' for value type property '{5}' on type '{6}'.
/// </summary>
internal static string RequiredProperty_MustHaveDataMemberRequired
{
get { return GetString("RequiredProperty_MustHaveDataMemberRequired"); }
}
/// <summary>
/// {0} does not recognize '{1}', so instead use '{2}' with '{3}' set to '{4}' for value type property '{5}' on type '{6}'.
/// </summary>
internal static string FormatRequiredProperty_MustHaveDataMemberRequired(object p0, object p1, object p2, object p3, object p4, object p5, object p6)
{
return string.Format(CultureInfo.CurrentCulture, GetString("RequiredProperty_MustHaveDataMemberRequired"), p0, p1, p2, p3, p4, p5, p6);
}
/// <summary>
/// The object to be wrapped must be of type '{0}' but was of type '{1}'.
/// </summary>

View File

@ -0,0 +1,212 @@
// 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.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.IsGenericType())
{
var enumerableOfT = modelType.ExtractGenericInterface(typeof(IEnumerable<>));
if (enumerableOfT != null)
{
modelType = enumerableOfT.GetGenericArguments()[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.IsValueType() && !propertyType.IsNullableValueType())
{
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

@ -120,6 +120,9 @@
<data name="EnumerableWrapperProvider_InvalidSourceEnumerableOfT" xml:space="preserve">
<value>The type must be an interface and must be or derive from '{0}'.</value>
</data>
<data name="RequiredProperty_MustHaveDataMemberRequired" xml:space="preserve">
<value>{0} does not recognize '{1}', so instead use '{2}' with '{3}' set to '{4}' for value type property '{5}' on type '{6}'.</value>
</data>
<data name="WrapperProvider_MismatchType" xml:space="preserve">
<value>The object to be wrapped must be of type '{0}' but was of type '{1}'.</value>
</data>

View File

@ -23,7 +23,8 @@ 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
/// </summary>
@ -39,6 +40,8 @@ namespace Microsoft.AspNet.Mvc.Xml
WrapperProviderFactories = new List<IWrapperProviderFactory>();
WrapperProviderFactories.Add(new SerializableErrorWrapperProviderFactory());
_dataAnnotationRequiredAttributeValidation = new DataAnnotationRequiredAttributeValidation();
}
/// <summary>
@ -96,6 +99,10 @@ namespace Microsoft.AspNet.Mvc.Xml
{
var type = GetSerializableType(context.ModelType);
_dataAnnotationRequiredAttributeValidation.Validate(
type,
context.ActionContext.ModelState);
var serializer = GetCachedSerializer(type);
var deserializedObject = serializer.ReadObject(xmlReader);

View File

@ -7,6 +7,7 @@
"dependencies": {
"Microsoft.AspNet.Mvc.Common": { "version": "6.0.0-*", "type": "build" },
"Microsoft.AspNet.Mvc.Core": "6.0.0-*",
"Microsoft.Framework.PropertyHelper.Internal": { "version": "1.0.0-*", "type": "build" },
"Microsoft.Framework.NotNullAttribute.Internal": { "version": "1.0.0-*", "type": "build" }
},
"frameworks": {

View File

@ -8,6 +8,7 @@
"Microsoft.AspNet.Mvc.Common": { "version": "6.0.0-*", "type": "build" },
"Microsoft.AspNet.Mvc.Razor": "6.0.0-*",
"Microsoft.Framework.Cache.Memory": "1.0.0-*",
"Microsoft.Framework.PropertyHelper.Internal": { "version": "1.0.0-*", "type": "build" },
"Microsoft.Framework.NotNullAttribute.Internal": { "version": "1.0.0-*", "type": "build" }
},
"frameworks": {

View File

@ -5,6 +5,7 @@
"dependencies": {
"Microsoft.AspNet.Mvc.Common": { "version": "6.0.0-*", "type": "build" },
"Microsoft.AspNet.Testing": "1.0.0-*",
"Microsoft.Framework.PropertyHelper.Internal": { "version": "1.0.0-*", "type": "build" },
"Microsoft.Framework.CopyOnWriteDictionary.Internal": { "version": "1.0.0-*", "type": "build" },
"Microsoft.Framework.NotNullAttribute.Internal": { "version": "1.0.0-*", "type": "build" },
"xunit.runner.kre": "1.0.0-*",

View File

@ -0,0 +1,119 @@
// 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;
using System.Linq;
using System.Net.Http;
using System.Net.Http.Headers;
using System.Runtime.Serialization;
using System.Text;
using System.Threading.Tasks;
using Microsoft.AspNet.Builder;
using Microsoft.AspNet.TestHost;
using XmlFormattersWebSite;
using Xunit;
namespace Microsoft.AspNet.Mvc.FunctionalTests
{
public class XmlDataContractSerializerInputFormatterTest
{
private readonly IServiceProvider _services = TestHelper.CreateServices(nameof(XmlFormattersWebSite));
private readonly Action<IApplicationBuilder> _app = new Startup().Configure;
private readonly string errorMessageFormat = string.Format(
"{{1}}:{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);
// 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()
{
// Arrange
var server = TestServer.Create(_services, _app);
var client = server.CreateClient();
client.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue("application/xml-dcs"));
var input = "<Store xmlns=\"http://schemas.datacontract.org/2004/07/XmlFormattersWebSite\" " +
"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);
//Assert
var dcsSerializer = new DataContractSerializer(typeof(ModelBindingInfo));
var responseStream = await response.Content.ReadAsStreamAsync();
var modelBindingInfo = dcsSerializer.ReadObject(responseStream) as ModelBindingInfo;
Assert.NotNull(modelBindingInfo);
Assert.NotNull(modelBindingInfo.Store);
Assert.Equal(10, modelBindingInfo.Store.Id);
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));
}
}
// Verifies that the model state has errors related to body model validation(for reference types) and also for
// Required attribute validation (for value types).
[Fact]
public async Task DataMissingForReferneceTypeProperties_AndModelIsBound_AndHasMixedValidationErrors()
{
// Arrange
var server = TestServer.Create(_services, _app);
var client = server.CreateClient();
client.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue("application/xml-dcs"));
var input = "<Store xmlns=\"http://schemas.datacontract.org/2004/07/XmlFormattersWebSite\"" +
" 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();
expectedErrorMessages.Add("store.Address:The Address field is required.");
// Act
var response = await client.PostAsync("http://localhost/Validation/CreateStore", content);
//Assert
var dcsSerializer = new DataContractSerializer(typeof(ModelBindingInfo));
var responseStream = await response.Content.ReadAsStreamAsync();
var modelBindingInfo = dcsSerializer.ReadObject(responseStream) as ModelBindingInfo;
Assert.NotNull(modelBindingInfo);
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)
{
Assert.Contains(
modelBindingInfo.ModelStateErrorMessages,
(actualErrorMessage) => actualErrorMessage.Equals(expectedErrorMessage));
}
}
}
}

View File

@ -2,6 +2,8 @@
// 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;
using System.IO;
using System.Linq;
using System.Runtime.Serialization;
@ -51,6 +53,15 @@ 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)]
@ -474,6 +485,636 @@ namespace Microsoft.AspNet.Mvc.Xml
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.ActionContext.ModelState.Keys.Count);
AssertModelStateErrorMessages(
typeof(Address).FullName,
context.ActionContext,
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.ActionContext.ModelState.Keys.Count);
AssertModelStateErrorMessages(
typeof(Address).FullName,
context.ActionContext,
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.ActionContext.ModelState.Keys.Count);
AssertModelStateErrorMessages(
typeof(Address).FullName,
context.ActionContext,
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.ActionContext.ModelState.Keys.Count);
AssertModelStateErrorMessages(
modelStateKey: typeof(Address).FullName,
actionContext: context.ActionContext,
expectedErrorMessages: 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.ActionContext.ModelState.Keys.Count);
AssertModelStateErrorMessages(
typeof(Address).FullName,
context.ActionContext,
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.ActionContext.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.ActionContext.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.ActionContext.ModelState.Keys.Count);
AssertModelStateErrorMessages(
typeof(Employee).FullName,
context.ActionContext,
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.ActionContext.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.ActionContext.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.ActionContext.ModelState.Keys.Count);
AssertModelStateErrorMessages(
typeof(Product).FullName,
context.ActionContext,
expectedErrorMessages: new[]
{
string.Format(requiredErrorMessageFormat, nameof(Product.Id), typeof(Product).FullName)
});
AssertModelStateErrorMessages(
typeof(Address).FullName,
context.ActionContext,
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.ActionContext.ModelState.Keys.Count);
AssertModelStateErrorMessages(
typeof(Product).FullName,
context.ActionContext,
expectedErrorMessages: new[]
{
string.Format(requiredErrorMessageFormat, nameof(Product.Id), typeof(Product).FullName)
});
AssertModelStateErrorMessages(
typeof(Address).FullName,
context.ActionContext,
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.ActionContext.ModelState.Keys.Count);
AssertModelStateErrorMessages(
typeof(Address).FullName,
context.ActionContext,
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.ActionContext,
expectedErrorMessages: new[]
{
string.Format(requiredErrorMessageFormat, nameof(Employee.Id), typeof(Employee).FullName)
});
AssertModelStateErrorMessages(
typeof(Product).FullName,
context.ActionContext,
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.ActionContext.ModelState.Keys.Count);
AssertModelStateErrorMessages(
typeof(School).FullName,
context.ActionContext,
expectedErrorMessages: new[]
{
string.Format(requiredErrorMessageFormat, nameof(School.Id), typeof(School).FullName)
});
AssertModelStateErrorMessages(
typeof(Website).FullName,
context.ActionContext,
expectedErrorMessages: new[]
{
string.Format(requiredErrorMessageFormat, nameof(Website.Id), typeof(Website).FullName)
});
AssertModelStateErrorMessages(
typeof(Student).FullName,
context.ActionContext,
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.ActionContext.ModelState.Keys.Count);
AssertModelStateErrorMessages(
typeof(Point).FullName,
context.ActionContext,
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.ActionContext,
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.ActionContext.ModelState.Keys.Count);
AssertModelStateErrorMessages(
typeof(Point).FullName,
context.ActionContext,
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.ActionContext,
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.ActionContext,
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,
ActionContext actionContext,
IEnumerable<string> expectedErrorMessages)
{
ModelState modelState;
actionContext.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 actionContext = GetActionContext(contentBytes);
@ -514,4 +1155,178 @@ 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; }
}
}

View File

@ -2,23 +2,30 @@
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System.ComponentModel.DataAnnotations;
using System.Runtime.Serialization;
namespace FormatterWebSite
{
[DataContract]
public class User
{
[DataMember(IsRequired = true)]
[Required, Range(1, 2000)]
public int Id { get; set; }
[DataMember(IsRequired = true)]
[Required, MinLength(5)]
public string Name { get; set; }
[DataMember]
[StringLength(15, MinimumLength = 3)]
public string Alias { get; set; }
[DataMember]
[RegularExpression("[0-9a-zA-Z]*")]
public string Designation { get; set; }
[DataMember]
public string description { get; set; }
}
}

View File

@ -10,7 +10,9 @@
"Microsoft.AspNet.Mvc.TestConfiguration": "1.0.0",
"Microsoft.AspNet.Server.IIS": "1.0.0-*",
"Microsoft.AspNet.Server.WebListener": "1.0.0-*",
"Microsoft.AspNet.StaticFiles": "1.0.0-*"
"Microsoft.AspNet.StaticFiles": "1.0.0-*",
"Microsoft.Framework.NotNullAttribute.Internal": { "type": "build", "version": "1.0.0-*" },
"Microsoft.Framework.PropertyHelper.Internal": { "version": "1.0.0-*", "type": "build" }
},
"frameworks": {
"aspnet50": {

View File

@ -0,0 +1,69 @@
// 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 Microsoft.AspNet.Mvc;
using Microsoft.AspNet.Mvc.ModelBinding;
namespace XmlFormattersWebSite
{
public class ValidationController : Controller
{
public IActionResult CreateStore([FromBody] Store store)
{
// We want to verify that 'store' is model bound and also that the
// model state has the errors we are expecting.
return new ObjectResult(new ModelBindingInfo()
{
Store = store,
ModelStateErrorMessages = GetModelStateErrorMessages(ModelState)
});
}
// Cannot use 'SerializableError' here as 'RequiredAttribute' validation errors are added as exceptions
// into the model state dictionary and 'SerializableError' sanitizes exceptions with generic error message.
// Since the tests need to verify the messages, we are doing the following.
private List<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 ModelBindingInfo
{
public Store Store { get; set; }
public List<string> ModelStateErrorMessages { get; set; }
}
}

View File

@ -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 XmlFormattersWebSite
{
public class Address
{
[Required]
public string State { get; set; }
[Required]
public int Zipcode { get; set; }
}
}

View File

@ -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.Collections.Generic;
using System.ComponentModel.DataAnnotations;
namespace XmlFormattersWebSite
{
public class Store
{
[Required]
public int Id { get; set; }
[Required]
public Address Address { get; set; }
}
}