Support user overrides of system-provided `ModelError` messages

- #2969
- add `ModelBindingMessages` for configuration and `IBindingMetadataProvider` overrides
  - use `interface` to avoid `new` oddities when adding a setter to an `abstract` property
- add `IModelBindingMessages` to `ModelMetadata` for use in rest of the product code
- plumb the various bits through the system
- add integration tests using a custom `IBindingMetadataProvider`s to override messages

nits:
- remove unused resources
- use `AttemptedValue` and not `model` in `SimpleTypeModelBinder`
This commit is contained in:
Doug Bunting 2015-09-25 16:48:21 -07:00
parent 0d6edf240a
commit 28aec3f5cc
25 changed files with 602 additions and 282 deletions

View File

@ -0,0 +1,34 @@
// 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;
namespace Microsoft.AspNet.Mvc.ModelBinding.Metadata
{
/// <summary>
/// Provider for error messages the model binding system detects.
/// </summary>
public interface IModelBindingMessageProvider
{
/// <summary>
/// Error message the model binding system adds when a property with an associated
/// <c>BindRequiredAttribute</c> is not bound.
/// </summary>
/// <value>Default <see cref="string"/> is "A value for the '{0}' property was not provided.".</value>
Func<string, string> MissingBindRequiredValueAccessor { get; }
/// <summary>
/// Error message the model binding system adds when either the key or the value of a
/// <see cref="System.Collections.Generic.KeyValuePair{TKey, TValue}"/> is bound but not both.
/// </summary>
/// <value>Default <see cref="string"/> is "A value is required.".</value>
Func<string> MissingKeyOrValueAccessor { get; }
/// <summary>
/// Error message the model binding system adds when a <c>null</c> value is bound to a
/// non-<see cref="Nullable"/> property.
/// </summary>
/// <value>Default <see cref="string"/> is "The value '{0}' is invalid.".</value>
Func<string, string> ValueMustNotBeNullAccessor { get; }
}
}

View File

@ -242,6 +242,11 @@ namespace Microsoft.AspNet.Mvc.ModelBinding
/// </remarks>
public abstract bool IsRequired { get; }
/// <summary>
/// Gets the <see cref="IModelBindingMessageProvider"/> instance.
/// </summary>
public abstract IModelBindingMessageProvider ModelBindingMessageProvider { get; }
/// <summary>
/// Gets a value indicating where the current metadata should be ordered relative to other properties
/// in its containing type.

View File

@ -3,6 +3,7 @@
using System;
using Microsoft.AspNet.Http;
using Microsoft.AspNet.Mvc.Core;
using Microsoft.AspNet.Mvc.Formatters;
using Microsoft.AspNet.Mvc.ModelBinding;
using Microsoft.AspNet.Mvc.ModelBinding.Metadata;
@ -23,6 +24,12 @@ namespace Microsoft.AspNet.Mvc.Internal
public static void ConfigureMvc(MvcOptions options)
{
// Set up default error messages
var messageProvider = options.ModelBindingMessageProvider;
messageProvider.MissingBindRequiredValueAccessor = Resources.FormatModelBinding_MissingBindRequiredMember;
messageProvider.MissingKeyOrValueAccessor = Resources.FormatKeyValuePair_BothKeyAndValueMustBePresent;
messageProvider.ValueMustNotBeNullAccessor = Resources.FormatModelBinding_NullValueNotValid;
// Set up ModelBinding
options.ModelBinders.Add(new BinderTypeBasedModelBinder());
options.ModelBinders.Add(new ServicesModelBinder());
@ -48,7 +55,7 @@ namespace Microsoft.AspNet.Mvc.Internal
options.ValueProviderFactories.Add(new JQueryFormValueProviderFactory());
// Set up metadata providers
options.ModelMetadataDetailsProviders.Add(new DefaultBindingMetadataProvider());
options.ModelMetadataDetailsProviders.Add(new DefaultBindingMetadataProvider(messageProvider));
options.ModelMetadataDetailsProviders.Add(new DefaultValidationMetadataProvider());
// Set up validators

View File

@ -1,5 +1,7 @@
// 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 Microsoft.AspNet.Mvc.Core;
using Microsoft.AspNet.Mvc.ModelBinding.Metadata;
namespace Microsoft.AspNet.Mvc.ModelBinding
@ -7,8 +9,33 @@ namespace Microsoft.AspNet.Mvc.ModelBinding
public class EmptyModelMetadataProvider : DefaultModelMetadataProvider
{
public EmptyModelMetadataProvider()
: base(new DefaultCompositeMetadataDetailsProvider(new IMetadataDetailsProvider[0]))
: base(new DefaultCompositeMetadataDetailsProvider(new IMetadataDetailsProvider[]
{
new MessageOnlyBindingProvider()
}))
{
}
private class MessageOnlyBindingProvider : IBindingMetadataProvider
{
private readonly ModelBindingMessageProvider _messageProvider = CreateMessageProvider();
public void GetBindingMetadata(BindingMetadataProviderContext context)
{
// Don't bother with ModelBindingMessageProvider copy constructor. No other provider can change the
// delegates.
context.BindingMetadata.ModelBindingMessageProvider = _messageProvider;
}
private static ModelBindingMessageProvider CreateMessageProvider()
{
return new ModelBindingMessageProvider
{
MissingBindRequiredValueAccessor = Resources.FormatModelBinding_MissingBindRequiredMember,
MissingKeyOrValueAccessor = Resources.FormatKeyValuePair_BothKeyAndValueMustBePresent,
ValueMustNotBeNullAccessor = Resources.FormatModelBinding_NullValueNotValid,
};
}
}
}
}

View File

@ -3,7 +3,6 @@
using System.Collections.Generic;
using System.Threading.Tasks;
using Microsoft.AspNet.Mvc.Core;
namespace Microsoft.AspNet.Mvc.ModelBinding
{
@ -30,7 +29,7 @@ namespace Microsoft.AspNet.Mvc.ModelBinding
{
bindingContext.ModelState.TryAddModelError(
keyResult.Key,
Resources.KeyValuePair_BothKeyAndValueMustBePresent);
bindingContext.ModelMetadata.ModelBindingMessageProvider.MissingKeyOrValueAccessor());
// Were able to get some data for this model.
// Always tell the model binding system to skip other model binders.
@ -40,7 +39,7 @@ namespace Microsoft.AspNet.Mvc.ModelBinding
{
bindingContext.ModelState.TryAddModelError(
valueResult.Key,
Resources.KeyValuePair_BothKeyAndValueMustBePresent);
bindingContext.ModelMetadata.ModelBindingMessageProvider.MissingKeyOrValueAccessor());
// Were able to get some data for this model.
// Always tell the model binding system to skip other model binders.

View File

@ -10,6 +10,8 @@ namespace Microsoft.AspNet.Mvc.ModelBinding.Metadata
/// </summary>
public class BindingMetadata
{
private ModelBindingMessageProvider _messageProvider;
/// <summary>
/// Gets or sets the <see cref="ModelBinding.BindingSource"/>.
/// See <see cref="ModelMetadata.BindingSource"/>.
@ -50,6 +52,27 @@ namespace Microsoft.AspNet.Mvc.ModelBinding.Metadata
/// </summary>
public bool? IsReadOnly { get; set; }
/// <summary>
/// Gets the <see cref="Metadata.ModelBindingMessageProvider"/> instance. See
/// <see cref="ModelMetadata.ModelBindingMessageProvider"/>.
/// </summary>
public ModelBindingMessageProvider ModelBindingMessageProvider
{
get
{
return _messageProvider;
}
set
{
if (value == null)
{
throw new ArgumentNullException(nameof(value));
}
_messageProvider = value;
}
}
/// <summary>
/// Gets or sets the <see cref="ModelBinding.IPropertyBindingPredicateProvider"/>.
/// See <see cref="ModelMetadata.PropertyBindingPredicateProvider"/>.

View File

@ -14,6 +14,18 @@ namespace Microsoft.AspNet.Mvc.ModelBinding.Metadata
/// </summary>
public class DefaultBindingMetadataProvider : IBindingMetadataProvider
{
private readonly ModelBindingMessageProvider _messageProvider;
public DefaultBindingMetadataProvider(ModelBindingMessageProvider messageProvider)
{
if (messageProvider == null)
{
throw new ArgumentNullException(nameof(messageProvider));
}
_messageProvider = messageProvider;
}
/// <inheritdoc />
public void GetBindingMetadata([NotNull] BindingMetadataProviderContext context)
{
@ -47,6 +59,10 @@ namespace Microsoft.AspNet.Mvc.ModelBinding.Metadata
}
}
// ModelBindingMessageProvider
// Provide a unique instance based on one passed to the constructor.
context.BindingMetadata.ModelBindingMessageProvider = new ModelBindingMessageProvider(_messageProvider);
// PropertyBindingPredicateProvider
var predicateProviders = context.Attributes.OfType<IPropertyBindingPredicateProvider>().ToArray();
if (predicateProviders.Length > 0)
@ -62,7 +78,7 @@ namespace Microsoft.AspNet.Mvc.ModelBinding.Metadata
var bindingBehavior = context.PropertyAttributes.OfType<BindingBehaviorAttribute>().FirstOrDefault();
if (bindingBehavior == null)
{
bindingBehavior =
bindingBehavior =
context.Key.ContainerType.GetTypeInfo()
.GetCustomAttributes(typeof(BindingBehaviorAttribute), inherit: true)
.OfType<BindingBehaviorAttribute>()

View File

@ -418,6 +418,15 @@ namespace Microsoft.AspNet.Mvc.ModelBinding.Metadata
}
}
/// <inheritdoc />
public override IModelBindingMessageProvider ModelBindingMessageProvider
{
get
{
return BindingMetadata.ModelBindingMessageProvider;
}
}
/// <inheritdoc />
public override string NullDisplayText
{

View File

@ -1,7 +1,6 @@
// 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.Linq;
using Microsoft.AspNet.Mvc.ModelBinding.Validation;
using Microsoft.Framework.Internal;
@ -19,8 +18,8 @@ namespace Microsoft.AspNet.Mvc.ModelBinding.Metadata
{
if (attribute is IModelValidator || attribute is IClientModelValidator)
{
// If another provider has already added this attribute, do not repeat it.
// This will prevent attributes like RemoteAttribute (which implement ValidationAttribute and
// If another provider has already added this attribute, do not repeat it.
// This will prevent attributes like RemoteAttribute (which implement ValidationAttribute and
// IClientModelValidator) to be added to the ValidationMetadata twice.
// This is to ensure we do not end up with duplication validation rules on the client side.
if (!context.ValidationMetadata.ValidatorMetadata.Contains(attribute))

View File

@ -0,0 +1,95 @@
// 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;
namespace Microsoft.AspNet.Mvc.ModelBinding.Metadata
{
/// <summary>
/// Read / write <see cref="IModelBindingMessageProvider"/> implementation.
/// </summary>
public class ModelBindingMessageProvider : IModelBindingMessageProvider
{
private Func<string, string> _missingBindRequiredValueAccessor;
private Func<string> _missingKeyOrValueAccessor;
private Func<string, string> _valueMustNotBeNullAccessor;
/// <summary>
/// Initializes a new instance of the <see cref="ModelBindingMessageProvider"/> class.
/// </summary>
public ModelBindingMessageProvider()
{
}
/// <summary>
/// Initializes a new instance of the <see cref="ModelBindingMessageProvider"/> class based on
/// <paramref name="originalProvider"/>.
/// </summary>
/// <param name="originalProvider">The <see cref="ModelBindingMessageProvider"/> to duplicate.</param>
public ModelBindingMessageProvider(ModelBindingMessageProvider originalProvider)
{
if (originalProvider == null)
{
throw new ArgumentNullException(nameof(originalProvider));
}
MissingBindRequiredValueAccessor = originalProvider.MissingBindRequiredValueAccessor;
MissingKeyOrValueAccessor = originalProvider.MissingKeyOrValueAccessor;
ValueMustNotBeNullAccessor = originalProvider.ValueMustNotBeNullAccessor;
}
/// <inheritdoc/>
public Func<string, string> MissingBindRequiredValueAccessor
{
get
{
return _missingBindRequiredValueAccessor;
}
set
{
if (value == null)
{
throw new ArgumentNullException(nameof(value));
}
_missingBindRequiredValueAccessor = value;
}
}
/// <inheritdoc/>
public Func<string> MissingKeyOrValueAccessor
{
get
{
return _missingKeyOrValueAccessor;
}
set
{
if (value == null)
{
throw new ArgumentNullException(nameof(value));
}
_missingKeyOrValueAccessor = value;
}
}
/// <inheritdoc/>
public Func<string, string> ValueMustNotBeNullAccessor
{
get
{
return _valueMustNotBeNullAccessor;
}
set
{
if (value == null)
{
throw new ArgumentNullException(nameof(value));
}
_valueMustNotBeNullAccessor = value;
}
}
}
}

View File

@ -6,8 +6,6 @@ using System.Collections.Generic;
using System.Linq;
using System.Reflection;
using System.Threading.Tasks;
using Microsoft.AspNet.Mvc.Core;
using Microsoft.AspNet.Mvc.ModelBinding.Validation;
using Microsoft.Framework.Internal;
namespace Microsoft.AspNet.Mvc.ModelBinding
@ -414,7 +412,8 @@ namespace Microsoft.AspNet.Mvc.ModelBinding
bindingContext.ModelState.TryAddModelError(
modelStateKey,
Resources.FormatModelBinding_MissingBindRequiredMember(propertyName));
bindingContext.ModelMetadata.ModelBindingMessageProvider.MissingBindRequiredValueAccessor(
propertyName));
}
// For each property that BindPropertiesAsync() attempted to bind, call the setter, recording

View File

@ -3,7 +3,6 @@
using System;
using System.Threading.Tasks;
using Microsoft.AspNet.Mvc.Core;
namespace Microsoft.AspNet.Mvc.ModelBinding
{
@ -51,7 +50,8 @@ namespace Microsoft.AspNet.Mvc.ModelBinding
{
bindingContext.ModelState.TryAddModelError(
bindingContext.ModelName,
Resources.FormatCommon_ValueNotValidForProperty(model));
bindingContext.ModelMetadata.ModelBindingMessageProvider.ValueMustNotBeNullAccessor(
valueProviderResult.ToString()));
return ModelBindingResult.FailedAsync(bindingContext.ModelName);
}

View File

@ -28,6 +28,7 @@ namespace Microsoft.AspNet.Mvc
InputFormatters = new FormatterCollection<IInputFormatter>();
OutputFormatters = new FormatterCollection<IOutputFormatter>();
ModelBinders = new List<IModelBinder>();
ModelBindingMessageProvider = new ModelBindingMessageProvider();
ModelMetadataDetailsProviders = new List<IMetadataDetailsProvider>();
ModelValidatorProviders = new List<IModelValidatorProvider>();
ValidationExcludeFilters = new ExcludeTypeValidationFilterCollection();
@ -87,7 +88,14 @@ namespace Microsoft.AspNet.Mvc
public IList<IModelBinder> ModelBinders { get; }
/// <summary>
/// Gets a list of <see cref="IMetadataDetailsProvider"/> instances that will be used to
/// Gets the default <see cref="IModelBindingMessageProvider"/>. Changes here are copied to the
/// <see cref="ModelMetadata.ModelBindingMessageProvider"/> property of all <see cref="ModelMetadata"/>
/// instances unless overridden in a custom <see cref="IBindingMetadataProvider"/>.
/// </summary>
public ModelBindingMessageProvider ModelBindingMessageProvider { get; }
/// <summary>
/// Gets a list of <see cref="IMetadataDetailsProvider"/> instances that will be used to
/// create <see cref="ModelMetadata"/> instances.
/// </summary>
/// <remarks>
@ -112,7 +120,7 @@ namespace Microsoft.AspNet.Mvc
public FormatterCollection<IOutputFormatter> OutputFormatters { get; }
/// <summary>
/// Gets or sets the flag which causes content negotiation to ignore Accept header
/// Gets or sets the flag which causes content negotiation to ignore Accept header
/// when it contains the media type */*. <see langword="false"/> by default.
/// </summary>
public bool RespectBrowserAcceptHeader { get; set; }

View File

@ -74,22 +74,6 @@ namespace Microsoft.AspNet.Mvc.Core
return string.Format(CultureInfo.CurrentCulture, GetString("ActionExecutor_UnexpectedTaskInstance"), p0, p1);
}
/// <summary>
/// Replacing the action context is not supported.
/// </summary>
internal static string ActionContextAccessor_SetValueNotSupported
{
get { return GetString("ActionContextAccessor_SetValueNotSupported"); }
}
/// <summary>
/// Replacing the action context is not supported.
/// </summary>
internal static string FormatActionContextAccessor_SetValueNotSupported()
{
return GetString("ActionContextAccessor_SetValueNotSupported");
}
/// <summary>
/// An action invoker could not be created for action '{0}'.
/// </summary>
@ -171,51 +155,19 @@ namespace Microsoft.AspNet.Mvc.Core
}
/// <summary>
/// The supplied route values are ambiguous and can select multiple sets of actions.
/// The value '{0}' is invalid.
/// </summary>
internal static string ActionSelector_GetCandidateActionsIsAmbiguous
internal static string ModelBinding_NullValueNotValid
{
get { return GetString("ActionSelector_GetCandidateActionsIsAmbiguous"); }
}
/// <summary>
/// The supplied route values are ambiguous and can select multiple sets of actions.
/// </summary>
internal static string FormatActionSelector_GetCandidateActionsIsAmbiguous()
{
return GetString("ActionSelector_GetCandidateActionsIsAmbiguous");
}
/// <summary>
/// Property '{0}' is of type '{1}', but this method requires a value of type '{2}'.
/// </summary>
internal static string ArgumentPropertyUnexpectedType
{
get { return GetString("ArgumentPropertyUnexpectedType"); }
}
/// <summary>
/// Property '{0}' is of type '{1}', but this method requires a value of type '{2}'.
/// </summary>
internal static string FormatArgumentPropertyUnexpectedType(object p0, object p1, object p2)
{
return string.Format(CultureInfo.CurrentCulture, GetString("ArgumentPropertyUnexpectedType"), p0, p1, p2);
get { return GetString("ModelBinding_NullValueNotValid"); }
}
/// <summary>
/// The value '{0}' is invalid.
/// </summary>
internal static string Common_ValueNotValidForProperty
internal static string FormatModelBinding_NullValueNotValid(object p0)
{
get { return GetString("Common_ValueNotValidForProperty"); }
}
/// <summary>
/// The value '{0}' is invalid.
/// </summary>
internal static string FormatCommon_ValueNotValidForProperty(object p0)
{
return string.Format(CultureInfo.CurrentCulture, GetString("Common_ValueNotValidForProperty"), p0);
return string.Format(CultureInfo.CurrentCulture, GetString("ModelBinding_NullValueNotValid"), p0);
}
/// <summary>
@ -282,38 +234,6 @@ namespace Microsoft.AspNet.Mvc.Core
return string.Format(CultureInfo.CurrentCulture, GetString("AsyncResultFilter_InvalidShortCircuit"), p0, p1, p2, p3);
}
/// <summary>
/// Unable to locate an implementation of IAuthorizationService.
/// </summary>
internal static string AuthorizeAttribute_AuthorizationServiceMustBeDefined
{
get { return GetString("AuthorizeAttribute_AuthorizationServiceMustBeDefined"); }
}
/// <summary>
/// Unable to locate an implementation of IAuthorizationService.
/// </summary>
internal static string FormatAuthorizeAttribute_AuthorizationServiceMustBeDefined()
{
return GetString("AuthorizeAttribute_AuthorizationServiceMustBeDefined");
}
/// <summary>
/// OnAuthorization is not implemented by this filter, use OnAuthorizationAsync instead.
/// </summary>
internal static string AuthorizeAttribute_OnAuthorizationNotImplemented
{
get { return GetString("AuthorizeAttribute_OnAuthorizationNotImplemented"); }
}
/// <summary>
/// OnAuthorization is not implemented by this filter, use OnAuthorizationAsync instead.
/// </summary>
internal static string FormatAuthorizeAttribute_OnAuthorizationNotImplemented()
{
return GetString("AuthorizeAttribute_OnAuthorizationNotImplemented");
}
/// <summary>
/// The type provided to '{0}' must implement '{1}'.
/// </summary>
@ -826,22 +746,6 @@ namespace Microsoft.AspNet.Mvc.Core
return string.Format(CultureInfo.CurrentCulture, GetString("Format_NotValid"), p0);
}
/// <summary>
/// The property '{0}' on controller '{1}' cannot be activated.
/// </summary>
internal static string ControllerFactory_PropertyCannotBeActivated
{
get { return GetString("ControllerFactory_PropertyCannotBeActivated"); }
}
/// <summary>
/// The property '{0}' on controller '{1}' cannot be activated.
/// </summary>
internal static string FormatControllerFactory_PropertyCannotBeActivated(object p0, object p1)
{
return string.Format(CultureInfo.CurrentCulture, GetString("ControllerFactory_PropertyCannotBeActivated"), p0, p1);
}
/// <summary>
/// The '{0}' cache profile is not defined.
/// </summary>
@ -938,22 +842,6 @@ namespace Microsoft.AspNet.Mvc.Core
return string.Format(CultureInfo.CurrentCulture, GetString("BindingSource_CannotBeGreedy"), p0, p1);
}
/// <summary>
/// The provided binding source '{0}' is not a request-based binding source. '{1}' requires that the source must represent data from an HTTP request.
/// </summary>
internal static string BindingSource_MustBeFromRequest
{
get { return GetString("BindingSource_MustBeFromRequest"); }
}
/// <summary>
/// The provided binding source '{0}' is not a request-based binding source. '{1}' requires that the source must represent data from an HTTP request.
/// </summary>
internal static string FormatBindingSource_MustBeFromRequest(object p0, object p1)
{
return string.Format(CultureInfo.CurrentCulture, GetString("BindingSource_MustBeFromRequest"), p0, p1);
}
/// <summary>
/// The property {0}.{1} could not be found.
/// </summary>
@ -1082,38 +970,6 @@ namespace Microsoft.AspNet.Mvc.Core
return string.Format(CultureInfo.CurrentCulture, GetString("ModelBinding_MissingBindRequiredMember"), p0);
}
/// <summary>
/// A value is required.
/// </summary>
internal static string ModelBinding_ValueRequired
{
get { return GetString("ModelBinding_ValueRequired"); }
}
/// <summary>
/// A value is required.
/// </summary>
internal static string FormatModelBinding_ValueRequired()
{
return GetString("ModelBinding_ValueRequired");
}
/// <summary>
/// More than one parameter and/or property is bound to the HTTP request's content.
/// </summary>
internal static string MultipleBodyParametersOrPropertiesAreNotAllowed
{
get { return GetString("MultipleBodyParametersOrPropertiesAreNotAllowed"); }
}
/// <summary>
/// More than one parameter and/or property is bound to the HTTP request's content.
/// </summary>
internal static string FormatMultipleBodyParametersOrPropertiesAreNotAllowed()
{
return GetString("MultipleBodyParametersOrPropertiesAreNotAllowed");
}
/// <summary>
/// The type '{0}' does not implement the interface '{1}'.
/// </summary>
@ -1130,38 +986,6 @@ namespace Microsoft.AspNet.Mvc.Core
return string.Format(CultureInfo.CurrentCulture, GetString("PropertyBindingPredicateProvider_WrongType"), p0, p1);
}
/// <summary>
/// The model object inside the metadata claimed to be compatible with '{0}', but was actually '{1}'.
/// </summary>
internal static string ValidatableObjectAdapter_IncompatibleType
{
get { return GetString("ValidatableObjectAdapter_IncompatibleType"); }
}
/// <summary>
/// The model object inside the metadata claimed to be compatible with '{0}', but was actually '{1}'.
/// </summary>
internal static string FormatValidatableObjectAdapter_IncompatibleType(object p0, object p1)
{
return string.Format(CultureInfo.CurrentCulture, GetString("ValidatableObjectAdapter_IncompatibleType"), p0, p1);
}
/// <summary>
/// Cannot convert value '{0}' to enum type '{1}'.
/// </summary>
internal static string ValueProviderResult_CannotConvertEnum
{
get { return GetString("ValueProviderResult_CannotConvertEnum"); }
}
/// <summary>
/// Cannot convert value '{0}' to enum type '{1}'.
/// </summary>
internal static string FormatValueProviderResult_CannotConvertEnum(object p0, object p1)
{
return string.Format(CultureInfo.CurrentCulture, GetString("ValueProviderResult_CannotConvertEnum"), p0, p1);
}
/// <summary>
/// The parameter conversion from type '{0}' to type '{1}' failed because no type converter can convert between these types.
/// </summary>

View File

@ -129,9 +129,6 @@
<data name="ActionExecutor_UnexpectedTaskInstance" xml:space="preserve">
<value>The method '{0}' on type '{1}' returned a Task instance even though it is not an asynchronous method.</value>
</data>
<data name="ActionContextAccessor_SetValueNotSupported" xml:space="preserve">
<value>Replacing the action context is not supported.</value>
</data>
<data name="ActionInvokerFactory_CouldNotCreateInvoker" xml:space="preserve">
<value>An action invoker could not be created for action '{0}'.</value>
</data>
@ -147,13 +144,7 @@
<data name="TypeMethodMustReturnNotNullValue" xml:space="preserve">
<value>The '{0}' method of type '{1}' cannot return a null value.</value>
</data>
<data name="ActionSelector_GetCandidateActionsIsAmbiguous" xml:space="preserve">
<value>The supplied route values are ambiguous and can select multiple sets of actions.</value>
</data>
<data name="ArgumentPropertyUnexpectedType" xml:space="preserve">
<value>Property '{0}' is of type '{1}', but this method requires a value of type '{2}'.</value>
</data>
<data name="Common_ValueNotValidForProperty" xml:space="preserve">
<data name="ModelBinding_NullValueNotValid" xml:space="preserve">
<value>The value '{0}' is invalid.</value>
</data>
<data name="Invalid_IncludePropertyExpression" xml:space="preserve">
@ -168,12 +159,6 @@
<data name="AsyncResultFilter_InvalidShortCircuit" xml:space="preserve">
<value>If an {0} cancels execution by setting the {1} property of {2} to 'true', then it cannot call the next filter by invoking {3}.</value>
</data>
<data name="AuthorizeAttribute_AuthorizationServiceMustBeDefined" xml:space="preserve">
<value>Unable to locate an implementation of IAuthorizationService.</value>
</data>
<data name="AuthorizeAttribute_OnAuthorizationNotImplemented" xml:space="preserve">
<value>OnAuthorization is not implemented by this filter, use OnAuthorizationAsync instead.</value>
</data>
<data name="FilterFactoryAttribute_TypeMustImplementIFilter" xml:space="preserve">
<value>The type provided to '{0}' must implement '{1}'.</value>
</data>
@ -279,9 +264,6 @@
<data name="Format_NotValid" xml:space="preserve">
<value>The format provided is invalid '{0}'. A format must be a non-empty file-extension, optionally prefixed with a '.' character.</value>
</data>
<data name="ControllerFactory_PropertyCannotBeActivated" xml:space="preserve">
<value>The property '{0}' on controller '{1}' cannot be activated.</value>
</data>
<data name="CacheProfileNotFound" xml:space="preserve">
<value>The '{0}' cache profile is not defined.</value>
</data>
@ -300,9 +282,6 @@
<data name="BindingSource_CannotBeGreedy" xml:space="preserve">
<value>The provided binding source '{0}' is a greedy data source. '{1}' does not support greedy data sources.</value>
</data>
<data name="BindingSource_MustBeFromRequest" xml:space="preserve">
<value>The provided binding source '{0}' is not a request-based binding source. '{1}' requires that the source must represent data from an HTTP request.</value>
</data>
<data name="Common_PropertyNotFound" xml:space="preserve">
<value>The property {0}.{1} could not be found.</value>
</data>
@ -327,21 +306,9 @@
<data name="ModelBinding_MissingBindRequiredMember" xml:space="preserve">
<value>A value for the '{0}' property was not provided.</value>
</data>
<data name="ModelBinding_ValueRequired" xml:space="preserve">
<value>A value is required.</value>
</data>
<data name="MultipleBodyParametersOrPropertiesAreNotAllowed" xml:space="preserve">
<value>More than one parameter and/or property is bound to the HTTP request's content.</value>
</data>
<data name="PropertyBindingPredicateProvider_WrongType" xml:space="preserve">
<value>The type '{0}' does not implement the interface '{1}'.</value>
</data>
<data name="ValidatableObjectAdapter_IncompatibleType" xml:space="preserve">
<value>The model object inside the metadata claimed to be compatible with '{0}', but was actually '{1}'.</value>
</data>
<data name="ValueProviderResult_CannotConvertEnum" xml:space="preserve">
<value>Cannot convert value '{0}' to enum type '{1}'.</value>
</data>
<data name="ValueProviderResult_NoConverterExists" xml:space="preserve">
<value>The parameter conversion from type '{0}' to type '{1}' failed because no type converter can convert between these types.</value>
</data>

View File

@ -457,6 +457,14 @@ namespace Microsoft.AspNet.Mvc.ModelBinding
}
}
public override IModelBindingMessageProvider ModelBindingMessageProvider
{
get
{
throw new NotImplementedException();
}
}
public override string NullDisplayText
{
get

View File

@ -4,7 +4,6 @@
#if DNX451
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.AspNet.Http.Internal;
using Microsoft.AspNet.Mvc.ModelBinding.Validation;
@ -54,7 +53,10 @@ namespace Microsoft.AspNet.Mvc.ModelBinding.Test
Assert.Null(result.Model);
Assert.False(bindingContext.ModelState.IsValid);
Assert.Equal("someName", bindingContext.ModelName);
Assert.Equal(bindingContext.ModelState["someName.Value"].Errors.First().ErrorMessage, "A value is required.");
var state = bindingContext.ModelState["someName.Value"];
Assert.NotNull(state);
var error = Assert.Single(state.Errors);
Assert.Equal("A value is required.", error.ErrorMessage);
}
[Fact]
@ -117,7 +119,7 @@ namespace Microsoft.AspNet.Mvc.ModelBinding.Test
{
innerResult = ModelBindingResult.Failed("somename.key");
}
var innerBinder = new Mock<IModelBinder>();
innerBinder
.Setup(o => o.BindModelAsync(It.IsAny<ModelBindingContext>()))
@ -161,7 +163,8 @@ namespace Microsoft.AspNet.Mvc.ModelBinding.Test
// Assert
Assert.NotEqual(ModelBindingResult.NoResult, result);
Assert.Equal(default(KeyValuePair<string, string>), Assert.IsType<KeyValuePair<string, string>>(result.Model));
var model = Assert.IsType<KeyValuePair<string, string>>(result.Model);
Assert.Equal(default(KeyValuePair<string, string>), model);
Assert.Equal("modelName", result.Key);
Assert.True(result.IsModelSet);
}

View File

@ -1,6 +1,7 @@
// 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 Microsoft.AspNet.Mvc.Core;
using Xunit;
namespace Microsoft.AspNet.Mvc.ModelBinding.Metadata
@ -21,7 +22,7 @@ namespace Microsoft.AspNet.Mvc.ModelBinding.Metadata
ModelMetadataIdentity.ForType(typeof(string)),
new ModelAttributes(attributes));
var provider = new DefaultBindingMetadataProvider();
var provider = new DefaultBindingMetadataProvider(CreateMessageProvider());
// Act
provider.GetBindingMetadata(context);
@ -45,7 +46,7 @@ namespace Microsoft.AspNet.Mvc.ModelBinding.Metadata
ModelMetadataIdentity.ForType(typeof(string)),
new ModelAttributes(attributes));
var provider = new DefaultBindingMetadataProvider();
var provider = new DefaultBindingMetadataProvider(CreateMessageProvider());
// Act
provider.GetBindingMetadata(context);
@ -68,7 +69,7 @@ namespace Microsoft.AspNet.Mvc.ModelBinding.Metadata
ModelMetadataIdentity.ForType(typeof(string)),
new ModelAttributes(attributes));
var provider = new DefaultBindingMetadataProvider();
var provider = new DefaultBindingMetadataProvider(CreateMessageProvider());
// Act
provider.GetBindingMetadata(context);
@ -92,7 +93,7 @@ namespace Microsoft.AspNet.Mvc.ModelBinding.Metadata
ModelMetadataIdentity.ForType(typeof(string)),
new ModelAttributes(attributes));
var provider = new DefaultBindingMetadataProvider();
var provider = new DefaultBindingMetadataProvider(CreateMessageProvider());
// Act
provider.GetBindingMetadata(context);
@ -115,7 +116,7 @@ namespace Microsoft.AspNet.Mvc.ModelBinding.Metadata
ModelMetadataIdentity.ForType(typeof(string)),
new ModelAttributes(attributes));
var provider = new DefaultBindingMetadataProvider();
var provider = new DefaultBindingMetadataProvider(CreateMessageProvider());
// Act
provider.GetBindingMetadata(context);
@ -139,7 +140,7 @@ namespace Microsoft.AspNet.Mvc.ModelBinding.Metadata
ModelMetadataIdentity.ForType(typeof(string)),
new ModelAttributes(attributes));
var provider = new DefaultBindingMetadataProvider();
var provider = new DefaultBindingMetadataProvider(CreateMessageProvider());
// Act
provider.GetBindingMetadata(context);
@ -161,7 +162,7 @@ namespace Microsoft.AspNet.Mvc.ModelBinding.Metadata
ModelMetadataIdentity.ForProperty(typeof(int), "Length", typeof(string)),
new ModelAttributes(propertyAttributes, typeAttributes: new object[0]));
var provider = new DefaultBindingMetadataProvider();
var provider = new DefaultBindingMetadataProvider(CreateMessageProvider());
// Act
provider.GetBindingMetadata(context);
@ -184,7 +185,7 @@ namespace Microsoft.AspNet.Mvc.ModelBinding.Metadata
ModelMetadataIdentity.ForProperty(typeof(int), "Length", typeof(string)),
new ModelAttributes(propertyAttributes, typeAttributes: new object[0]));
var provider = new DefaultBindingMetadataProvider();
var provider = new DefaultBindingMetadataProvider(CreateMessageProvider());
// Act
provider.GetBindingMetadata(context);
@ -207,7 +208,7 @@ namespace Microsoft.AspNet.Mvc.ModelBinding.Metadata
ModelMetadataIdentity.ForProperty(typeof(int), "Length", typeof(string)),
new ModelAttributes(propertyAttributes, typeAttributes: new object[0]));
var provider = new DefaultBindingMetadataProvider();
var provider = new DefaultBindingMetadataProvider(CreateMessageProvider());
// Act
provider.GetBindingMetadata(context);
@ -230,7 +231,7 @@ namespace Microsoft.AspNet.Mvc.ModelBinding.Metadata
ModelMetadataIdentity.ForProperty(typeof(int), "Length", typeof(string)),
new ModelAttributes(propertyAttributes, typeAttributes: new object[0]));
var provider = new DefaultBindingMetadataProvider();
var provider = new DefaultBindingMetadataProvider(CreateMessageProvider());
// Act
provider.GetBindingMetadata(context);
@ -253,7 +254,7 @@ namespace Microsoft.AspNet.Mvc.ModelBinding.Metadata
ModelMetadataIdentity.ForProperty(typeof(int), "Length", typeof(string)),
new ModelAttributes(propertyAttributes, typeAttributes: new object[0]));
var provider = new DefaultBindingMetadataProvider();
var provider = new DefaultBindingMetadataProvider(CreateMessageProvider());
// Act
provider.GetBindingMetadata(context);
@ -279,7 +280,7 @@ namespace Microsoft.AspNet.Mvc.ModelBinding.Metadata
ModelMetadataIdentity.ForProperty(typeof(int), "Length", typeof(string)),
new ModelAttributes(propertyAttributes, typeAttributes: new object[0]));
var provider = new DefaultBindingMetadataProvider();
var provider = new DefaultBindingMetadataProvider(CreateMessageProvider());
// Act
provider.GetBindingMetadata(context);
@ -297,7 +298,7 @@ namespace Microsoft.AspNet.Mvc.ModelBinding.Metadata
ModelMetadataIdentity.ForProperty(typeof(string), "Property", typeof(BindRequiredOnClass)),
new ModelAttributes(propertyAttributes: new object[0], typeAttributes: new object[0]));
var provider = new DefaultBindingMetadataProvider();
var provider = new DefaultBindingMetadataProvider(CreateMessageProvider());
// Act
provider.GetBindingMetadata(context);
@ -315,7 +316,7 @@ namespace Microsoft.AspNet.Mvc.ModelBinding.Metadata
ModelMetadataIdentity.ForProperty(typeof(string), "Property", typeof(BindNeverOnClass)),
new ModelAttributes(propertyAttributes: new object[0], typeAttributes: new object[0]));
var provider = new DefaultBindingMetadataProvider();
var provider = new DefaultBindingMetadataProvider(CreateMessageProvider());
// Act
provider.GetBindingMetadata(context);
@ -333,7 +334,7 @@ namespace Microsoft.AspNet.Mvc.ModelBinding.Metadata
ModelMetadataIdentity.ForProperty(typeof(string), "Property", typeof(InheritedBindNeverOnClass)),
new ModelAttributes(propertyAttributes: new object[0], typeAttributes: new object[0]));
var provider = new DefaultBindingMetadataProvider();
var provider = new DefaultBindingMetadataProvider(CreateMessageProvider());
// Act
provider.GetBindingMetadata(context);
@ -356,7 +357,7 @@ namespace Microsoft.AspNet.Mvc.ModelBinding.Metadata
ModelMetadataIdentity.ForProperty(typeof(string), "Property", typeof(BindNeverOnClass)),
new ModelAttributes(propertyAttributes, typeAttributes: new object[0]));
var provider = new DefaultBindingMetadataProvider();
var provider = new DefaultBindingMetadataProvider(CreateMessageProvider());
// Act
provider.GetBindingMetadata(context);
@ -379,7 +380,7 @@ namespace Microsoft.AspNet.Mvc.ModelBinding.Metadata
ModelMetadataIdentity.ForProperty(typeof(string), "Property", typeof(BindNeverOnClass)),
new ModelAttributes(propertyAttributes, typeAttributes: new object[0]));
var provider = new DefaultBindingMetadataProvider();
var provider = new DefaultBindingMetadataProvider(CreateMessageProvider());
// Act
provider.GetBindingMetadata(context);
@ -402,7 +403,7 @@ namespace Microsoft.AspNet.Mvc.ModelBinding.Metadata
ModelMetadataIdentity.ForProperty(typeof(string), "Property", typeof(InheritedBindNeverOnClass)),
new ModelAttributes(propertyAttributes, typeAttributes: new object[0]));
var provider = new DefaultBindingMetadataProvider();
var provider = new DefaultBindingMetadataProvider(CreateMessageProvider());
// Act
provider.GetBindingMetadata(context);
@ -425,7 +426,7 @@ namespace Microsoft.AspNet.Mvc.ModelBinding.Metadata
ModelMetadataIdentity.ForProperty(typeof(string), "Property", typeof(BindRequiredOnClass)),
new ModelAttributes(propertyAttributes, typeAttributes: new object[0]));
var provider = new DefaultBindingMetadataProvider();
var provider = new DefaultBindingMetadataProvider(CreateMessageProvider());
// Act
provider.GetBindingMetadata(context);
@ -444,7 +445,7 @@ namespace Microsoft.AspNet.Mvc.ModelBinding.Metadata
ModelMetadataIdentity.ForProperty(typeof(string), "Property", typeof(BindRequiredOverridesInheritedBindNever)),
new ModelAttributes(propertyAttributes: new object[0], typeAttributes: new object[0]));
var provider = new DefaultBindingMetadataProvider();
var provider = new DefaultBindingMetadataProvider(CreateMessageProvider());
// Act
provider.GetBindingMetadata(context);
@ -473,7 +474,7 @@ namespace Microsoft.AspNet.Mvc.ModelBinding.Metadata
context.BindingMetadata.IsBindingAllowed = initialValue;
context.BindingMetadata.IsBindingRequired = initialValue;
var provider = new DefaultBindingMetadataProvider();
var provider = new DefaultBindingMetadataProvider(CreateMessageProvider());
// Act
provider.GetBindingMetadata(context);
@ -504,7 +505,7 @@ namespace Microsoft.AspNet.Mvc.ModelBinding.Metadata
context.BindingMetadata.IsBindingAllowed = initialValue;
context.BindingMetadata.IsBindingRequired = initialValue;
var provider = new DefaultBindingMetadataProvider();
var provider = new DefaultBindingMetadataProvider(CreateMessageProvider());
// Act
provider.GetBindingMetadata(context);
@ -514,6 +515,16 @@ namespace Microsoft.AspNet.Mvc.ModelBinding.Metadata
Assert.Equal(initialValue, context.BindingMetadata.IsBindingRequired);
}
private static ModelBindingMessageProvider CreateMessageProvider()
{
return new ModelBindingMessageProvider
{
MissingBindRequiredValueAccessor = Resources.FormatModelBinding_MissingBindRequiredMember,
MissingKeyOrValueAccessor = Resources.FormatKeyValuePair_BothKeyAndValueMustBePresent,
ValueMustNotBeNullAccessor = Resources.FormatModelBinding_NullValueNotValid,
};
}
[BindNever]
private class BindNeverOnClass
{

View File

@ -787,7 +787,7 @@ namespace Microsoft.AspNet.Mvc.ModelBinding
property => ModelBindingResult.Failed(property.PropertyName));
var nameProperty = containerMetadata.Properties[nameof(model.Name)];
results[nameProperty] = ModelBindingResult.Success(string.Empty, "John Doe");
var testableBinder = new TestableMutableObjectModelBinder();
// Act
@ -1219,7 +1219,7 @@ namespace Microsoft.AspNet.Mvc.ModelBinding
results[lastNameProperty] = ModelBindingResult.Success(
nameof(model.LastName),
"Doe");
var testableBinder = new TestableMutableObjectModelBinder();
// Act

View File

@ -105,7 +105,7 @@ namespace Microsoft.AspNet.Mvc.ModelBinding.Test
Assert.Null(result.Model);
var error = Assert.Single(bindingContext.ModelState["theModelName"].Errors);
Assert.Equal(error.ErrorMessage, "The value '' is invalid.", StringComparer.Ordinal);
Assert.Equal("The value '' is invalid.", error.ErrorMessage, StringComparer.Ordinal);
Assert.Null(error.Exception);
}

View File

@ -1026,13 +1026,23 @@ namespace Microsoft.AspNet.Mvc.ModelBinding.Metadata
public AttributeInjectModelMetadataProvider(object[] attributes)
: base(new DefaultCompositeMetadataDetailsProvider(new IMetadataDetailsProvider[]
{
new DefaultBindingMetadataProvider(),
new DefaultBindingMetadataProvider(CreateMessageProvider()),
new DataAnnotationsMetadataProvider(),
}))
{
_attributes = attributes;
}
private static ModelBindingMessageProvider CreateMessageProvider()
{
return new ModelBindingMessageProvider
{
MissingBindRequiredValueAccessor = name => $"A value for the '{ name }' property was not provided.",
MissingKeyOrValueAccessor = () => $"A value is required.",
ValueMustNotBeNullAccessor = value => $"The value '{ value }' is invalid.",
};
}
protected override DefaultMetadataDetails CreateTypeDetails(ModelMetadataIdentity key)
{
var entry = base.CreateTypeDetails(key);

View File

@ -5,6 +5,7 @@ using System.Collections.Generic;
using System.Threading.Tasks;
using Microsoft.AspNet.Http;
using Microsoft.AspNet.Mvc.Abstractions;
using Microsoft.AspNet.Mvc.Controllers;
using Microsoft.AspNet.Mvc.ModelBinding;
using Xunit;
@ -46,13 +47,183 @@ namespace Microsoft.AspNet.Mvc.IntegrationTests
var entry = Assert.Single(modelState, kvp => kvp.Key == "parameter.Key").Value;
Assert.Equal("key0", entry.AttemptedValue);
Assert.Equal("key0", entry.RawValue);
Assert.Equal("key0", entry.RawValue);
entry = Assert.Single(modelState, kvp => kvp.Key == "parameter.Value").Value;
Assert.Equal("10", entry.AttemptedValue);
Assert.Equal("10", entry.RawValue);
}
[Fact]
public async Task KeyValuePairModelBinder_SimpleTypes_WithNoKey_AddsError()
{
// Arrange
var argumentBinder = ModelBindingTestHelper.GetArgumentBinder();
var parameter = new ParameterDescriptor
{
Name = "parameter",
ParameterType = typeof(KeyValuePair<string, int>)
};
var operationContext = ModelBindingTestHelper.GetOperationBindingContext(request =>
{
request.QueryString = new QueryString("?parameter.Value=10");
});
var modelState = new ModelStateDictionary();
// Act
var modelBindingResult = await argumentBinder.BindModelAsync(parameter, modelState, operationContext);
// Assert
Assert.False(modelBindingResult.IsModelSet);
Assert.Equal(2, modelState.Count);
Assert.False(modelState.IsValid);
Assert.Equal(1, modelState.ErrorCount);
var entry = Assert.Single(modelState, kvp => kvp.Key == "parameter.Key").Value;
var error = Assert.Single(entry.Errors);
Assert.Null(error.Exception);
Assert.Equal("A value is required.", error.ErrorMessage);
entry = Assert.Single(modelState, kvp => kvp.Key == "parameter.Value").Value;
Assert.Empty(entry.Errors);
Assert.Equal("10", entry.AttemptedValue);
Assert.Equal("10", entry.RawValue);
}
[Fact]
public async Task KeyValuePairModelBinder_SimpleTypes_WithNoKey_AndCustomizedMessage_AddsGivenMessage()
{
// Arrange
var metadataProvider = new TestModelMetadataProvider();
metadataProvider
.ForType(typeof(KeyValuePair<string, int>))
.BindingDetails((System.Action<ModelBinding.Metadata.BindingMetadata>)(binding =>
{
// A real details provider could customize message based on BindingMetadataProviderContext.
binding.ModelBindingMessageProvider.MissingKeyOrValueAccessor = () => $"Hurts when nothing is provided.";
}));
var argumentBinder = new DefaultControllerActionArgumentBinder(
metadataProvider,
ModelBindingTestHelper.GetObjectValidator());
var parameter = new ParameterDescriptor
{
Name = "parameter",
ParameterType = typeof(KeyValuePair<string, int>)
};
var operationContext = ModelBindingTestHelper.GetOperationBindingContext(request =>
{
request.QueryString = new QueryString("?parameter.Value=10");
});
var modelState = new ModelStateDictionary();
// Act
var modelBindingResult = await argumentBinder.BindModelAsync(parameter, modelState, operationContext);
// Assert
Assert.False(modelBindingResult.IsModelSet);
Assert.Equal(2, modelState.Count);
Assert.False(modelState.IsValid);
Assert.Equal(1, modelState.ErrorCount);
var entry = Assert.Single(modelState, kvp => kvp.Key == "parameter.Key").Value;
var error = Assert.Single(entry.Errors);
Assert.Null(error.Exception);
Assert.Equal("Hurts when nothing is provided.", error.ErrorMessage);
entry = Assert.Single(modelState, kvp => kvp.Key == "parameter.Value").Value;
Assert.Empty(entry.Errors);
Assert.Equal("10", entry.AttemptedValue);
Assert.Equal("10", entry.RawValue);
}
[Fact]
public async Task KeyValuePairModelBinder_SimpleTypes_WithNoValue_AddsError()
{
// Arrange
var argumentBinder = ModelBindingTestHelper.GetArgumentBinder();
var parameter = new ParameterDescriptor
{
Name = "parameter",
ParameterType = typeof(KeyValuePair<string, int>)
};
var operationContext = ModelBindingTestHelper.GetOperationBindingContext(request =>
{
request.QueryString = new QueryString("?parameter.Key=10");
});
var modelState = new ModelStateDictionary();
// Act
var modelBindingResult = await argumentBinder.BindModelAsync(parameter, modelState, operationContext);
// Assert
Assert.False(modelBindingResult.IsModelSet);
Assert.Equal(2, modelState.Count);
Assert.False(modelState.IsValid);
Assert.Equal(1, modelState.ErrorCount);
var entry = Assert.Single(modelState, kvp => kvp.Key == "parameter.Key").Value;
Assert.Empty(entry.Errors);
Assert.Equal("10", entry.AttemptedValue);
Assert.Equal("10", entry.RawValue);
entry = Assert.Single(modelState, kvp => kvp.Key == "parameter.Value").Value;
var error = Assert.Single(entry.Errors);
Assert.Null(error.Exception);
Assert.Equal("A value is required.", error.ErrorMessage);
}
[Fact]
public async Task KeyValuePairModelBinder_SimpleTypes_WithNoValue_AndCustomizedMessage_AddsGivenMessage()
{
// Arrange
var metadataProvider = new TestModelMetadataProvider();
metadataProvider
.ForType(typeof(KeyValuePair<string, int>))
.BindingDetails((System.Action<ModelBinding.Metadata.BindingMetadata>)(binding =>
{
// A real details provider could customize message based on BindingMetadataProviderContext.
binding.ModelBindingMessageProvider.MissingKeyOrValueAccessor = () => $"Hurts when nothing is provided.";
}));
var argumentBinder = new DefaultControllerActionArgumentBinder(
metadataProvider,
ModelBindingTestHelper.GetObjectValidator());
var parameter = new ParameterDescriptor
{
Name = "parameter",
ParameterType = typeof(KeyValuePair<string, int>)
};
var operationContext = ModelBindingTestHelper.GetOperationBindingContext(request =>
{
request.QueryString = new QueryString("?parameter.Key=10");
});
var modelState = new ModelStateDictionary();
// Act
var modelBindingResult = await argumentBinder.BindModelAsync(parameter, modelState, operationContext);
// Assert
Assert.False(modelBindingResult.IsModelSet);
Assert.Equal(2, modelState.Count);
Assert.False(modelState.IsValid);
Assert.Equal(1, modelState.ErrorCount);
var entry = Assert.Single(modelState, kvp => kvp.Key == "parameter.Key").Value;
Assert.Empty(entry.Errors);
Assert.Equal("10", entry.AttemptedValue);
Assert.Equal("10", entry.RawValue);
entry = Assert.Single(modelState, kvp => kvp.Key == "parameter.Value").Value;
var error = Assert.Single(entry.Errors);
Assert.Null(error.Exception);
Assert.Equal("Hurts when nothing is provided.", error.ErrorMessage);
}
[Fact]
public async Task KeyValuePairModelBinder_BindsKeyValuePairOfSimpleType_WithExplicitPrefix_Success()
{
@ -90,7 +261,7 @@ namespace Microsoft.AspNet.Mvc.IntegrationTests
var entry = Assert.Single(modelState, kvp => kvp.Key == "prefix.Key").Value;
Assert.Equal("key0", entry.AttemptedValue);
Assert.Equal("key0", entry.RawValue);
Assert.Equal("key0", entry.RawValue);
entry = Assert.Single(modelState, kvp => kvp.Key == "prefix.Value").Value;
Assert.Equal("10", entry.AttemptedValue);
@ -130,7 +301,7 @@ namespace Microsoft.AspNet.Mvc.IntegrationTests
var entry = Assert.Single(modelState, kvp => kvp.Key == "Key").Value;
Assert.Equal("key0", entry.AttemptedValue);
Assert.Equal("key0", entry.RawValue);
Assert.Equal("key0", entry.RawValue);
entry = Assert.Single(modelState, kvp => kvp.Key == "Value").Value;
Assert.Equal("10", entry.AttemptedValue);
@ -207,7 +378,7 @@ namespace Microsoft.AspNet.Mvc.IntegrationTests
var entry = Assert.Single(modelState, kvp => kvp.Key == "parameter.Key").Value;
Assert.Equal("key0", entry.AttemptedValue);
Assert.Equal("key0", entry.RawValue);
Assert.Equal("key0", entry.RawValue);
entry = Assert.Single(modelState, kvp => kvp.Key == "parameter.Value.Id").Value;
Assert.Equal("10", entry.AttemptedValue);
@ -252,7 +423,7 @@ namespace Microsoft.AspNet.Mvc.IntegrationTests
var entry = Assert.Single(modelState, kvp => kvp.Key == "prefix.Key").Value;
Assert.Equal("key0", entry.AttemptedValue);
Assert.Equal("key0", entry.RawValue);
Assert.Equal("key0", entry.RawValue);
entry = Assert.Single(modelState, kvp => kvp.Key == "prefix.Value.Id").Value;
Assert.Equal("10", entry.AttemptedValue);
@ -293,7 +464,7 @@ namespace Microsoft.AspNet.Mvc.IntegrationTests
var entry = Assert.Single(modelState, kvp => kvp.Key == "Key").Value;
Assert.Equal("key0", entry.AttemptedValue);
Assert.Equal("key0", entry.RawValue);
Assert.Equal("key0", entry.RawValue);
entry = Assert.Single(modelState, kvp => kvp.Key == "Value.Id").Value;
Assert.Equal("10", entry.AttemptedValue);

View File

@ -10,6 +10,7 @@ using Microsoft.AspNet.Http;
using Microsoft.AspNet.Http.Features.Internal;
using Microsoft.AspNet.Http.Internal;
using Microsoft.AspNet.Mvc.Abstractions;
using Microsoft.AspNet.Mvc.Controllers;
using Microsoft.AspNet.Mvc.Infrastructure;
using Microsoft.AspNet.Mvc.ModelBinding;
using Microsoft.Framework.Primitives;
@ -859,8 +860,7 @@ namespace Microsoft.AspNet.Mvc.IntegrationTests
// Need to have a key here so that the MutableObjectModelBinder will recurse to bind elements.
var operationContext = ModelBindingTestHelper.GetOperationBindingContext(request =>
{
request.QueryString =
new QueryString("?Name=bill&ProductIds[0]=10&ProductIds[1]=11");
request.QueryString = new QueryString("?Name=bill&ProductIds[0]=10&ProductIds[1]=11");
});
var modelState = new ModelStateDictionary();
@ -1032,8 +1032,7 @@ namespace Microsoft.AspNet.Mvc.IntegrationTests
// Need to have a key here so that the MutableObjectModelBinder will recurse to bind elements.
var operationContext = ModelBindingTestHelper.GetOperationBindingContext(request =>
{
request.QueryString =
new QueryString("?Name=bill&ProductIds[0]=10&ProductIds[1]=11");
request.QueryString = new QueryString("?Name=bill&ProductIds[0]=10&ProductIds[1]=11");
});
var modelState = new ModelStateDictionary();
@ -1205,8 +1204,7 @@ namespace Microsoft.AspNet.Mvc.IntegrationTests
// Need to have a key here so that the MutableObjectModelBinder will recurse to bind elements.
var operationContext = ModelBindingTestHelper.GetOperationBindingContext(request =>
{
request.QueryString =
new QueryString("?Name=bill&ProductIds[0].Key=key0&ProductIds[0].Value=10");
request.QueryString = new QueryString("?Name=bill&ProductIds[0].Key=key0&ProductIds[0].Value=10");
});
var modelState = new ModelStateDictionary();
@ -1378,8 +1376,7 @@ namespace Microsoft.AspNet.Mvc.IntegrationTests
// Need to have a key here so that the MutableObjectModelBinder will recurse to bind elements.
var operationContext = ModelBindingTestHelper.GetOperationBindingContext(request =>
{
request.QueryString =
new QueryString("?Name=bill&ProductId.Key=key0&ProductId.Value=10");
request.QueryString = new QueryString("?Name=bill&ProductId.Key=key0&ProductId.Value=10");
});
var modelState = new ModelStateDictionary();
@ -1563,9 +1560,7 @@ namespace Microsoft.AspNet.Mvc.IntegrationTests
};
// No Data
var operationContext = ModelBindingTestHelper.GetOperationBindingContext(request =>
{
});
var operationContext = ModelBindingTestHelper.GetOperationBindingContext();
var modelState = new ModelStateDictionary();
@ -1589,6 +1584,53 @@ namespace Microsoft.AspNet.Mvc.IntegrationTests
Assert.Equal("A value for the 'Customer' property was not provided.", error.ErrorMessage);
}
[Fact]
public async Task MutableObjectModelBinder_WithBindRequired_NoData_AndCustomizedMessage_AddsGivenMessage()
{
// Arrange
var metadataProvider = new TestModelMetadataProvider();
metadataProvider
.ForType(typeof(Order10))
.BindingDetails((Action<ModelBinding.Metadata.BindingMetadata>)(binding =>
{
// A real details provider could customize message based on BindingMetadataProviderContext.
binding.ModelBindingMessageProvider.MissingBindRequiredValueAccessor =
name => $"Hurts when '{ name }' is not provided.";
}));
var argumentBinder = new DefaultControllerActionArgumentBinder(
metadataProvider,
ModelBindingTestHelper.GetObjectValidator());
var parameter = new ParameterDescriptor()
{
Name = "parameter",
ParameterType = typeof(Order10)
};
// No Data
var operationContext = ModelBindingTestHelper.GetOperationBindingContext();
var modelState = new ModelStateDictionary();
// Act
var modelBindingResult = await argumentBinder.BindModelAsync(parameter, modelState, operationContext);
// Assert
Assert.True(modelBindingResult.IsModelSet);
var model = Assert.IsType<Order10>(modelBindingResult.Model);
Assert.Null(model.Customer);
Assert.Equal(1, modelState.Count);
Assert.Equal(1, modelState.ErrorCount);
Assert.False(modelState.IsValid);
var entry = Assert.Single(modelState, e => e.Key == "Customer").Value;
Assert.Null(entry.RawValue);
Assert.Null(entry.AttemptedValue);
var error = Assert.Single(modelState["Customer"].Errors);
Assert.Equal("Hurts when 'Customer' is not provided.", error.ErrorMessage);
}
private class Order11
{
public Person11 Customer { get; set; }

View File

@ -9,6 +9,7 @@ using System.Threading.Tasks;
using Microsoft.AspNet.Http;
using Microsoft.AspNet.Http.Internal;
using Microsoft.AspNet.Mvc.Abstractions;
using Microsoft.AspNet.Mvc.Controllers;
using Microsoft.AspNet.Mvc.ModelBinding;
using Microsoft.Framework.Primitives;
using Xunit;
@ -262,7 +263,7 @@ namespace Microsoft.AspNet.Mvc.IntegrationTests
};
var operationContext = ModelBindingTestHelper.GetOperationBindingContext(request =>
{
request.QueryString = QueryString.Create("Parameter1", string.Empty);
request.QueryString = QueryString.Create("Parameter1", " ");
});
var modelState = new ModelStateDictionary();
@ -277,6 +278,58 @@ namespace Microsoft.AspNet.Mvc.IntegrationTests
// Model
Assert.Null(modelBindingResult.Model);
// ModelState
Assert.False(modelState.IsValid);
var key = Assert.Single(modelState.Keys);
Assert.Equal("Parameter1", key);
Assert.Equal(" ", modelState[key].AttemptedValue);
Assert.Equal(" ", modelState[key].RawValue);
var error = Assert.Single(modelState[key].Errors);
Assert.Equal("The value ' ' is invalid.", error.ErrorMessage, StringComparer.Ordinal);
Assert.Null(error.Exception);
}
[Theory]
[InlineData(typeof(int))]
[InlineData(typeof(bool))]
public async Task BindParameter_WithEmptyData_AndPerTypeMessage_AddsGivenMessage(Type parameterType)
{
// Arrange
var metadataProvider = new TestModelMetadataProvider();
metadataProvider
.ForType(parameterType)
.BindingDetails((Action<ModelBinding.Metadata.BindingMetadata>)(binding =>
{
// A real details provider could customize message based on BindingMetadataProviderContext.
binding.ModelBindingMessageProvider.ValueMustNotBeNullAccessor =
value => $"Hurts when '{ value }' is provided.";
}));
var argumentBinder = new DefaultControllerActionArgumentBinder(
metadataProvider,
ModelBindingTestHelper.GetObjectValidator());
var parameter = new ParameterDescriptor
{
Name = "Parameter1",
BindingInfo = new BindingInfo(),
ParameterType = parameterType
};
var operationContext = ModelBindingTestHelper.GetOperationBindingContext(request =>
{
request.QueryString = QueryString.Create("Parameter1", string.Empty);
});
var modelState = new ModelStateDictionary();
// Act
var modelBindingResult = await argumentBinder.BindModelAsync(parameter, modelState, operationContext);
// Assert
// ModelBindingResult
Assert.False(modelBindingResult.IsModelSet);
// Model
Assert.Null(modelBindingResult.Model);
// ModelState
Assert.False(modelState.IsValid);
var key = Assert.Single(modelState.Keys);
@ -284,7 +337,7 @@ namespace Microsoft.AspNet.Mvc.IntegrationTests
Assert.Equal(string.Empty, modelState[key].AttemptedValue);
Assert.Equal(string.Empty, modelState[key].RawValue);
var error = Assert.Single(modelState[key].Errors);
Assert.Equal(error.ErrorMessage, "The value '' is invalid.", StringComparer.Ordinal);
Assert.Equal("Hurts when '' is provided.", error.ErrorMessage, StringComparer.Ordinal);
Assert.Null(error.Exception);
}

View File

@ -16,7 +16,7 @@ namespace Microsoft.AspNet.Mvc.ModelBinding
{
var detailsProviders = new IMetadataDetailsProvider[]
{
new DefaultBindingMetadataProvider(),
new DefaultBindingMetadataProvider(CreateMessageProvider()),
new DefaultValidationMetadataProvider(),
new DataAnnotationsMetadataProvider(),
new DataMemberRequiredBindingMetadataProvider(),
@ -36,7 +36,7 @@ namespace Microsoft.AspNet.Mvc.ModelBinding
private TestModelMetadataProvider(TestModelMetadataDetailsProvider detailsProvider)
: base(new DefaultCompositeMetadataDetailsProvider(new IMetadataDetailsProvider[]
{
new DefaultBindingMetadataProvider(),
new DefaultBindingMetadataProvider(CreateMessageProvider()),
new DefaultValidationMetadataProvider(),
new DataAnnotationsMetadataProvider(),
detailsProvider
@ -76,6 +76,16 @@ namespace Microsoft.AspNet.Mvc.ModelBinding
return ForProperty(typeof(TContainer), propertyName);
}
private static ModelBindingMessageProvider CreateMessageProvider()
{
return new ModelBindingMessageProvider
{
MissingBindRequiredValueAccessor = name => $"A value for the '{ name }' property was not provided.",
MissingKeyOrValueAccessor = () => $"A value is required.",
ValueMustNotBeNullAccessor = value => $"The value '{ value }' is invalid.",
};
}
private class TestModelMetadataDetailsProvider :
IBindingMetadataProvider,
IDisplayMetadataProvider,