Adding Support for TryUpdateModel using include expressions and predicate.

This commit is contained in:
Harsh Gupta 2014-11-10 13:24:51 -08:00 committed by Ryan Nowak
parent ca714c5149
commit d7094fd32d
25 changed files with 903 additions and 413 deletions

View File

@ -3,7 +3,6 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.AspNet.Mvc.Core;
using Microsoft.AspNet.Mvc.ModelBinding;
@ -96,21 +95,17 @@ namespace Microsoft.AspNet.Mvc
}
}
internal static ModelBindingContext GetModelBindingContext(ModelMetadata modelMetadata,
ActionBindingContext actionBindingContext,
OperationBindingContext operationBindingContext)
internal static ModelBindingContext GetModelBindingContext(
ModelMetadata modelMetadata,
ActionBindingContext actionBindingContext,
OperationBindingContext operationBindingContext)
{
Func<ModelBindingContext, string, bool> propertyFilter =
(context, propertyName) => BindAttribute.IsPropertyAllowed(propertyName,
modelMetadata.BinderIncludeProperties,
modelMetadata.BinderExcludeProperties);
var modelBindingContext = new ModelBindingContext
{
ModelName = modelMetadata.BinderModelName ?? modelMetadata.PropertyName,
ModelMetadata = modelMetadata,
ModelState = actionBindingContext.ActionContext.ModelState,
PropertyFilter = propertyFilter,
// Fallback only if there is no explicit model name set.
FallbackToEmptyPrefix = modelMetadata.BinderModelName == null,
ValueProvider = actionBindingContext.ValueProvider,

View File

@ -0,0 +1,66 @@
// 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.Linq;
using System.Linq.Expressions;
using Microsoft.AspNet.Mvc.ModelBinding;
namespace Microsoft.AspNet.Mvc
{
/// <summary>
/// Default implementation for <see cref="IPropertyBindingPredicateProvider"/>.
/// Provides a expression based way to provide include properties.
/// </summary>
/// <typeparam name="TModel">The target model Type.</typeparam>
public class DefaultPropertyBindingPredicateProvider<TModel> : IPropertyBindingPredicateProvider
where TModel : class
{
private static readonly Func<ModelBindingContext, string, bool> _defaultFilter = (context, propertyName) => true;
/// <summary>
/// The prefix which is used while generating the property filter.
/// </summary>
public virtual string Prefix
{
get
{
return string.Empty;
}
}
/// <summary>
/// Expressions which can be used to generate property filter which can filter model
/// properties.
/// </summary>
public virtual IEnumerable<Expression<Func<TModel, object>>> PropertyIncludeExpressions
{
get
{
return null;
}
}
/// <inheritdoc />
public virtual Func<ModelBindingContext, string, bool> PropertyFilter
{
get
{
if (PropertyIncludeExpressions == null)
{
return _defaultFilter;
}
// We do not cache by default.
return GetPredicateFromExpression(PropertyIncludeExpressions);
}
}
private Func<ModelBindingContext, string, bool> GetPredicateFromExpression(IEnumerable<Expression<Func<TModel, object>>> includeExpressions)
{
var expression = ModelBindingHelper.GetIncludePredicateExpression(Prefix, includeExpressions.ToArray());
return expression.Compile();
}
}
}

View File

@ -167,6 +167,7 @@ namespace Microsoft.AspNet.Mvc
return false;
}
// Internal for tests
internal static string GetPropertyName(Expression expression)
{
if (expression.NodeType == ExpressionType.Convert ||
@ -203,8 +204,16 @@ namespace Microsoft.AspNet.Mvc
}
}
private static Expression<Func<ModelBindingContext, string, bool>> GetIncludePredicateExpression<TModel>
(string prefix, Expression<Func<TModel, object>>[] expressions)
/// <summary>
/// Creates an expression for a predicate to limit the set of properties used in model binding.
/// </summary>
/// <typeparam name="TModel">The model type.</typeparam>
/// <param name="prefix">The model prefix.</param>
/// <param name="expressions">Expressions identifying the properties to allow for binding.</param>
/// <returns>An expression which can be used with <see cref="IPropertyBindingPredicateProvider"/>.</returns>
public static Expression<Func<ModelBindingContext, string, bool>> GetIncludePredicateExpression<TModel>(
string prefix,
Expression<Func<TModel, object>>[] expressions)
{
if (expressions.Length == 0)
{

View File

@ -2,8 +2,9 @@
// 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.Linq;
using Microsoft.AspNet.Mvc.ModelBinding;
using Microsoft.Framework.DependencyInjection;
namespace Microsoft.AspNet.Mvc
{
@ -11,22 +12,52 @@ namespace Microsoft.AspNet.Mvc
/// This attribute can be used on action parameters and types, to indicate model level metadata.
/// </summary>
[AttributeUsage(AttributeTargets.Class | AttributeTargets.Parameter, AllowMultiple = false, Inherited = true)]
public sealed class BindAttribute : Attribute, IModelNameProvider, IPropertyBindingInfo
public class BindAttribute : Attribute, IModelNameProvider, IPropertyBindingPredicateProvider
{
/// <summary>
/// Comma separated set of properties which are to be excluded during model binding.
/// </summary>
public string Exclude { get; set; } = string.Empty;
private static readonly Func<ModelBindingContext, string, bool> _defaultFilter = (context, propertyName) => true;
private Func<ModelBindingContext, string, bool> _predicateFromInclude;
/// <summary>
/// Comma separated set of properties which are to be included during model binding.
/// Creates a new instace of <see cref="BindAttribute"/>.
/// </summary>
public string Include { get; set; } = string.Empty;
/// <param name="include">Names of parameters to include in binding.</param>
public BindAttribute(params string[] include)
{
Include = include;
}
/// <summary>
/// Creates a new instance of <see cref="BindAttribute"/>.
/// </summary>
/// <param name="predicateProviderType">The type which implements
/// <see cref="IPropertyBindingPredicateProvider"/>.
/// </param>
public BindAttribute([NotNull] Type predicateProviderType)
{
if (!typeof(IPropertyBindingPredicateProvider).IsAssignableFrom(predicateProviderType))
{
var message = Resources.FormatPropertyBindingPredicateProvider_WrongType(
predicateProviderType.FullName,
typeof(IPropertyBindingPredicateProvider).FullName);
throw new ArgumentException(message, nameof(predicateProviderType));
}
PredicateProviderType = predicateProviderType;
}
/// <inheritdoc />
public Type PredicateProviderType { get; }
/// <summary>
/// Gets the names of properties to include in model binding.
/// </summary>
public string[] Include { get; }
// This property is exposed for back compat reasons.
/// <summary>
/// Allows a user to specify a particular prefix to match during model binding.
/// </summary>
// This property is exposed for back compat reasons.
public string Prefix { get; set; }
/// <summary>
@ -40,18 +71,56 @@ namespace Microsoft.AspNet.Mvc
}
}
public static bool IsPropertyAllowed(string propertyName,
IReadOnlyList<string> includeProperties,
IReadOnlyList<string> excludeProperties)
/// <inheritdoc />
public Func<ModelBindingContext, string, bool> PropertyFilter
{
// We allow a property to be bound if its both in the include list AND not in the exclude list.
// An empty exclude list implies no properties are disallowed.
var includeProperty = (includeProperties != null) &&
includeProperties.Contains(propertyName, StringComparer.OrdinalIgnoreCase);
var excludeProperty = (excludeProperties != null) &&
excludeProperties.Contains(propertyName, StringComparer.OrdinalIgnoreCase);
get
{
if (PredicateProviderType != null)
{
return CreatePredicateFromProviderType(PredicateProviderType);
}
else if (Include != null && Include.Length > 0)
{
if (_predicateFromInclude == null)
{
_predicateFromInclude =
(context, propertyName) => Include.Contains(propertyName, StringComparer.Ordinal);
}
return includeProperty && !excludeProperty;
return _predicateFromInclude;
}
else
{
return _defaultFilter;
}
}
}
private static Func<ModelBindingContext, string, bool> CreatePredicateFromProviderType(
Type predicateProviderType)
{
// Holding state to avoid execessive creation of the provider.
var initialized = false;
Func<ModelBindingContext, string, bool> predicate = null;
return (ModelBindingContext context, string propertyName) =>
{
if (!initialized)
{
var services = context.OperationBindingContext.HttpContext.RequestServices;
var activator = services.GetService<ITypeActivator>();
var provider = (IPropertyBindingPredicateProvider)activator.CreateInstance(
services,
predicateProviderType);
initialized = true;
predicate = provider.PropertyFilter ?? _defaultFilter;
}
return predicate(context, propertyName);
};
}
}
}

View File

@ -8,6 +8,7 @@ using System.Linq;
using System.Reflection;
using System.Threading.Tasks;
using Microsoft.AspNet.Mvc.ModelBinding.Internal;
using Microsoft.Framework.DependencyInjection;
namespace Microsoft.AspNet.Mvc.ModelBinding
{
@ -269,14 +270,27 @@ namespace Microsoft.AspNet.Mvc.ModelBinding
protected virtual IEnumerable<ModelMetadata> GetMetadataForProperties(ModelBindingContext bindingContext)
{
var validationInfo = GetPropertyValidationInfo(bindingContext);
var newPropertyFilter = GetPropertyFilter();
return bindingContext.ModelMetadata.Properties
.Where(propertyMetadata =>
bindingContext.PropertyFilter(bindingContext, propertyMetadata.PropertyName) &&
newPropertyFilter(bindingContext, propertyMetadata.PropertyName) &&
(validationInfo.RequiredProperties.Contains(propertyMetadata.PropertyName) ||
!validationInfo.SkipProperties.Contains(propertyMetadata.PropertyName)) &&
CanUpdateProperty(propertyMetadata));
}
private static Func<ModelBindingContext, string, bool> GetPropertyFilter()
{
return (ModelBindingContext context, string propertyName) =>
{
var modelMetadataPredicate = context.ModelMetadata.PropertyBindingPredicateProvider?.PropertyFilter;
return
context.PropertyFilter(context, propertyName) &&
(modelMetadataPredicate == null || modelMetadataPredicate(context, propertyName));
};
}
private static object GetPropertyDefaultValue(PropertyInfo propertyInfo)
{
var attr = propertyInfo.GetCustomAttribute<DefaultValueAttribute>();

View File

@ -1,21 +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.
namespace Microsoft.AspNet.Mvc
{
/// <summary>
/// Represents an entity which has binding information for a model.
/// </summary>
public interface IPropertyBindingInfo
{
/// <summary>
/// Comma separated set of properties which are to be excluded during model binding.
/// </summary>
string Exclude { get; }
/// <summary>
/// Comma separated set of properties which are to be included during model binding.
/// </summary>
string Include { get; }
}
}

View File

@ -0,0 +1,18 @@
// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System;
namespace Microsoft.AspNet.Mvc.ModelBinding
{
/// <summary>
/// Provides a predicate which can determines which model properties should be bound by model binding.
/// </summary>
public interface IPropertyBindingPredicateProvider
{
/// <summary>
/// Gets a predicate which can determines which model properties should be bound by model binding.
/// </summary>
Func<ModelBindingContext, string, bool> PropertyFilter { get; }
}
}

View File

@ -21,7 +21,7 @@ namespace Microsoft.AspNet.Mvc.ModelBinding
Required = attributes.OfType<RequiredAttribute>().FirstOrDefault();
ScaffoldColumn = attributes.OfType<ScaffoldColumnAttribute>().FirstOrDefault();
BinderMetadata = attributes.OfType<IBinderMetadata>().FirstOrDefault();
PropertyBindingInfo = attributes.OfType<IPropertyBindingInfo>();
PropertyBindingPredicateProviders = attributes.OfType<IPropertyBindingPredicateProvider>();
BinderModelNameProvider = attributes.OfType<IModelNameProvider>().FirstOrDefault();
BinderTypeProviders = attributes.OfType<IBinderTypeProviderMetadata>();
@ -82,11 +82,11 @@ namespace Microsoft.AspNet.Mvc.ModelBinding
public HiddenInputAttribute HiddenInput { get; protected set; }
/// <summary>
/// Gets (or sets in subclasses) <see cref="IEnumerable{IPropertyBindingInfo}"/> found in collection
/// passed to the <see cref="CachedDataAnnotationsMetadataAttributes(IEnumerable{object})"/> constructor,
/// if any.
/// Gets (or sets in subclasses) <see cref="IEnumerable{IPropertyBindingPredicateProvider}"/> found in
/// collection passed to the <see cref="CachedDataAnnotationsMetadataAttributes(IEnumerable{object})"/>
/// constructor, if any.
/// </summary>
public IEnumerable<IPropertyBindingInfo> PropertyBindingInfo { get; protected set; }
public IEnumerable<IPropertyBindingPredicateProvider> PropertyBindingPredicateProviders { get; protected set; }
public RequiredAttribute Required { get; protected set; }

View File

@ -70,56 +70,11 @@ namespace Microsoft.AspNet.Mvc.ModelBinding
: base.ComputeBinderModelNamePrefix();
}
protected override IReadOnlyList<string> ComputeBinderIncludeProperties()
protected override IPropertyBindingPredicateProvider ComputePropertyBindingPredicateProvider()
{
var propertyBindingInfo = PrototypeCache.PropertyBindingInfo?.ToList();
if (propertyBindingInfo != null && propertyBindingInfo.Count != 0)
{
if (string.IsNullOrEmpty(propertyBindingInfo[0].Include))
{
return Properties.Select(property => property.PropertyName).ToList();
}
var includeFirst = SplitString(propertyBindingInfo[0].Include).ToList();
if (propertyBindingInfo.Count != 2)
{
return includeFirst;
}
var includedAtType = SplitString(propertyBindingInfo[1].Include).ToList();
if (includeFirst.Count == 0 && includedAtType.Count == 0)
{
// Need to include everything by default.
return Properties.Select(property => property.PropertyName).ToList();
}
else
{
return includeFirst.Intersect(includedAtType).ToList();
}
}
// Need to include everything by default.
return Properties.Select(property => property.PropertyName).ToList();
}
protected override IReadOnlyList<string> ComputeBinderExcludeProperties()
{
var propertyBindingInfo = PrototypeCache.PropertyBindingInfo?.ToList();
if (propertyBindingInfo != null && propertyBindingInfo.Count != 0)
{
var excludeFirst = SplitString(propertyBindingInfo[0].Exclude).ToList();
if (propertyBindingInfo.Count != 2)
{
return excludeFirst;
}
var excludedAtType = SplitString(propertyBindingInfo[1].Exclude).ToList();
return excludeFirst.Union(excludedAtType).ToList();
}
return base.ComputeBinderExcludeProperties();
return PrototypeCache.PropertyBindingPredicateProviders.Any()
? new CompositePredicateProvider(PrototypeCache.PropertyBindingPredicateProviders.ToArray())
: null;
}
protected override bool ComputeConvertEmptyStringToNull()
@ -383,5 +338,44 @@ namespace Microsoft.AspNet.Mvc.ModelBinding
.Where(trimmed => !string.IsNullOrEmpty(trimmed));
return split;
}
private class CompositePredicateProvider : IPropertyBindingPredicateProvider
{
private readonly IPropertyBindingPredicateProvider[] _providers;
public CompositePredicateProvider(IPropertyBindingPredicateProvider[] providers)
{
_providers = providers;
}
public Func<ModelBindingContext, string, bool> PropertyFilter
{
get
{
return CreatePredicate();
}
}
private Func<ModelBindingContext, string, bool> CreatePredicate()
{
var predicates = _providers
.Select(p => p.PropertyFilter)
.Where(p => p != null)
.ToArray();
return (context, propertyName) =>
{
foreach (var predicate in predicates)
{
if (!predicate(context, propertyName))
{
return false;
}
}
return true;
};
}
}
}
}

View File

@ -33,8 +33,7 @@ namespace Microsoft.AspNet.Mvc.ModelBinding
private bool _showForEdit;
private IBinderMetadata _binderMetadata;
private string _binderModelName;
private IReadOnlyList<string> _binderIncludeProperties;
private IReadOnlyList<string> _binderExcludeProperties;
private IPropertyBindingPredicateProvider _propertyBindingPredicateProvider;
private Type _binderType;
private bool _convertEmptyStringToNullComputed;
@ -53,10 +52,9 @@ namespace Microsoft.AspNet.Mvc.ModelBinding
private bool _showForDisplayComputed;
private bool _showForEditComputed;
private bool _isBinderMetadataComputed;
private bool _isBinderIncludePropertiesComputed;
private bool _isBinderModelNameComputed;
private bool _isBinderExcludePropertiesComputed;
private bool _isBinderTypeComputed;
private bool _propertyBindingPredicateProviderComputed;
// Constructor for creating real instances of the metadata class based on a prototype
protected CachedModelMetadata(CachedModelMetadata<TPrototypeCache> prototype, Func<object> modelAccessor)
@ -102,50 +100,8 @@ namespace Microsoft.AspNet.Mvc.ModelBinding
_binderMetadata = value;
_isBinderMetadataComputed = true;
}
}
/// <inheritdoc />
public sealed override IReadOnlyList<string> BinderIncludeProperties
{
get
{
if (!_isBinderIncludePropertiesComputed)
{
_binderIncludeProperties = ComputeBinderIncludeProperties();
_isBinderIncludePropertiesComputed = true;
}
return _binderIncludeProperties;
}
set
{
_binderIncludeProperties = value;
_isBinderIncludePropertiesComputed = true;
}
}
/// <inheritdoc />
public sealed override IReadOnlyList<string> BinderExcludeProperties
{
get
{
if (!_isBinderExcludePropertiesComputed)
{
_binderExcludeProperties = ComputeBinderExcludeProperties();
_isBinderExcludePropertiesComputed = true;
}
return _binderExcludeProperties;
}
set
{
_binderExcludeProperties = value;
_isBinderExcludePropertiesComputed = true;
}
}
/// <inheritdoc />
public sealed override string BinderModelName
{
@ -422,6 +378,25 @@ namespace Microsoft.AspNet.Mvc.ModelBinding
}
}
public sealed override IPropertyBindingPredicateProvider PropertyBindingPredicateProvider
{
get
{
if (!_propertyBindingPredicateProviderComputed)
{
_propertyBindingPredicateProvider = ComputePropertyBindingPredicateProvider();
_propertyBindingPredicateProviderComputed = true;
}
return _propertyBindingPredicateProvider;
}
set
{
_propertyBindingPredicateProvider = value;
_propertyBindingPredicateProviderComputed = true;
}
}
/// <inheritdoc />
public sealed override bool ShowForDisplay
{
@ -506,14 +481,9 @@ namespace Microsoft.AspNet.Mvc.ModelBinding
return base.BinderMetadata;
}
protected virtual IReadOnlyList<string> ComputeBinderIncludeProperties()
protected virtual IPropertyBindingPredicateProvider ComputePropertyBindingPredicateProvider()
{
return base.BinderIncludeProperties;
}
protected virtual IReadOnlyList<string> ComputeBinderExcludeProperties()
{
return base.BinderExcludeProperties;
return base.PropertyBindingPredicateProvider;
}
protected virtual string ComputeBinderModelNamePrefix()

View File

@ -48,23 +48,14 @@ namespace Microsoft.AspNet.Mvc.ModelBinding
}
/// <summary>
/// The name of the model if specified explicitly using <see cref="IModelNameProvider"/>.
/// Gets or sets the name of a model if specified explicitly using <see cref="IModelNameProvider"/>.
/// </summary>
public virtual string BinderModelName { get; set; }
/// <summary>
/// Properties which are to be included while binding this model.
/// </summary>
public virtual IReadOnlyList<string> BinderIncludeProperties { get; set; }
/// <summary>
/// Properties which are to be excluded while binding this model.
/// </summary>
public virtual IReadOnlyList<string> BinderExcludeProperties { get; set; }
/// <summary>
/// The <see cref="Type"/> of an <see cref="IModelBinder"/> or an <see cref="IModelBinderProvider"/>
/// of a model if specified explicitly using <see cref="IBinderTypeProviderMetadata"/>.
/// Gets or sets the <see cref="Type"/> of an <see cref="IModelBinder"/> or an
/// <see cref="IModelBinderProvider"/> of a model if specified explicitly using
/// <see cref="IBinderTypeProviderMetadata"/>.
/// </summary>
public virtual Type BinderType { get; set; }
@ -211,6 +202,12 @@ namespace Microsoft.AspNet.Mvc.ModelBinding
}
}
/// <summary>
/// Gets or sets the <see cref="IPropertyBindingPredicateProvider"/>, which can determine which properties
/// should be model bound.
/// </summary>
public virtual IPropertyBindingPredicateProvider PropertyBindingPredicateProvider { get; set; }
public string PropertyName
{
get { return _propertyName; }

View File

@ -251,19 +251,19 @@ namespace Microsoft.AspNet.Mvc.ModelBinding
}
/// <summary>
/// The type '{0}' must derive from '{1}'.
/// The type '{0}' does not implement the interface '{1}'.
/// </summary>
internal static string TypeMustDeriveFromType
internal static string PropertyBindingPredicateProvider_WrongType
{
get { return GetString("TypeMustDeriveFromType"); }
get { return GetString("PropertyBindingPredicateProvider_WrongType"); }
}
/// <summary>
/// The type '{0}' must derive from '{1}'.
/// The type '{0}' does not implement the interface '{1}'.
/// </summary>
internal static string FormatTypeMustDeriveFromType(object p0, object p1)
internal static string FormatPropertyBindingPredicateProvider_WrongType(object p0, object p1)
{
return string.Format(CultureInfo.CurrentCulture, GetString("TypeMustDeriveFromType"), p0, p1);
return string.Format(CultureInfo.CurrentCulture, GetString("PropertyBindingPredicateProvider_WrongType"), p0, p1);
}
/// <summary>

View File

@ -162,8 +162,8 @@
<data name="ModelBindingContext_ModelMetadataMustBeSet" xml:space="preserve">
<value>The ModelMetadata property must be set before accessing this property.</value>
</data>
<data name="TypeMustDeriveFromType" xml:space="preserve">
<value>The type '{0}' must derive from '{1}'.</value>
<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>

View File

@ -49,33 +49,7 @@ namespace Microsoft.AspNet.Mvc.Core.Test
}
[Fact]
public void GetModelBindingContext_DoesNotReturn_ExcludedProperties()
{
// Arrange
var actionContext = new ActionContext(new RouteContext(Mock.Of<HttpContext>()),
Mock.Of<ActionDescriptor>());
var metadataProvider = new DataAnnotationsModelMetadataProvider();
var modelMetadata = metadataProvider.GetMetadataForType(
modelAccessor: null, modelType: typeof(TypeWithExcludedPropertiesUsingBindAttribute));
var actionBindingContext = new ActionBindingContext(actionContext,
Mock.Of<IModelMetadataProvider>(),
Mock.Of<IModelBinder>(),
Mock.Of<IValueProvider>(),
Mock.Of<IInputFormatterSelector>(),
Mock.Of<IModelValidatorProvider>());
// Act
var context = DefaultControllerActionArgumentBinder.GetModelBindingContext(
modelMetadata, actionBindingContext, Mock.Of<OperationBindingContext>());
// Assert
Assert.False(context.PropertyFilter(context, "Excluded1"));
Assert.False(context.PropertyFilter(context, "Excluded2"));
}
[Fact]
public void GetModelBindingContext_ReturnsOnlyWhiteListedProperties_UsingBindAttributeInclude()
public void GetModelBindingContext_ReturnsOnlyIncludedProperties_UsingBindAttributeInclude()
{
// Arrange
var actionContext = new ActionContext(new RouteContext(Mock.Of<HttpContext>()),
@ -325,19 +299,7 @@ namespace Microsoft.AspNet.Mvc.Core.Test
{
}
[Bind(Exclude = nameof(Excluded1) + "," + nameof(Excluded2))]
private class TypeWithExcludedPropertiesUsingBindAttribute
{
public int Excluded1 { get; set; }
public int Excluded2 { get; set; }
public int IncludedByDefault1 { get; set; }
public int IncludedByDefault2 { get; set; }
}
[Bind(Include = nameof(IncludedExplicitly1) + "," + nameof(IncludedExplicitly2))]
[Bind(new string[] { nameof(IncludedExplicitly1), nameof(IncludedExplicitly2) })]
private class TypeWithIncludedPropertiesUsingBindAttribute
{
public int ExcludedByDefault1 { get; set; }

View File

@ -707,6 +707,71 @@ namespace Microsoft.AspNet.Mvc.FunctionalTests
Assert.Equal("The Field3 field is required.", json["model.Field3"]);
}
[Fact]
public async Task BindAttribute_Filters_UsingDefaultPropertyFilterProvider_WithExpressions()
{
// Arrange
var server = TestServer.Create(_services, _app);
var client = server.CreateClient();
// Act
var response = await client.GetStringAsync("http://localhost/BindAttribute/" +
"EchoUser" +
"?user.UserName=someValue&user.RegisterationMonth=March&user.Id=123");
// Assert
var json = JsonConvert.DeserializeObject<User>(response);
// Does not touch what is not in the included expression.
Assert.Equal(0, json.Id);
// Updates the included properties.
Assert.Equal("someValue", json.UserName);
Assert.Equal("March", json.RegisterationMonth);
}
[Fact]
public async Task BindAttribute_Filters_UsingPropertyFilterProvider_UsingServices()
{
// Arrange
var server = TestServer.Create(_services, _app);
var client = server.CreateClient();
// Act
var response = await client.GetStringAsync("http://localhost/BindAttribute/" +
"EchoUserUsingServices" +
"?user.UserName=someValue&user.RegisterationMonth=March&user.Id=123");
// Assert
var json = JsonConvert.DeserializeObject<User>(response);
// Does not touch what is not in the included expression.
Assert.Equal(0, json.Id);
// Updates the included properties.
Assert.Equal("someValue", json.UserName);
Assert.Equal("March", json.RegisterationMonth);
}
[Fact]
public async Task BindAttribute_Filters_UsingDefaultPropertyFilterProvider_WithPredicate()
{
// Arrange
var server = TestServer.Create(_services, _app);
var client = server.CreateClient();
// Act
var response = await client.GetStringAsync("http://localhost/BindAttribute/" +
"UpdateUserId_BlackListingAtEitherLevelDoesNotBind" +
"?param1.LastName=someValue&param2.Id=123");
// Assert
var json = JsonConvert.DeserializeObject<Dictionary<string, string>>(response);
Assert.Equal(2, json.Count);
Assert.Null(json["param1.LastName"]);
Assert.Equal("0", json["param2.Id"]);
}
[Fact]
public async Task BindAttribute_AppliesAtBothParameterAndTypeLevelTogether_BlacklistedAtEitherLevelIsNotBound()
{
@ -716,18 +781,18 @@ namespace Microsoft.AspNet.Mvc.FunctionalTests
// Act
var response = await client.GetStringAsync("http://localhost/BindAttribute/" +
"BindAtParamterLevelAndBindAtTypeLevelAreBothEvaluated_BlackListingAtEitherLevelDoesNotBind" +
"?param1.IncludedExplicitlyAtTypeLevel=someValue&param2.ExcludedExplicitlyAtTypeLevel=someValue");
"UpdateUserId_BlackListingAtEitherLevelDoesNotBind" +
"?param1.LastName=someValue&param2.Id=123");
// Assert
var json = JsonConvert.DeserializeObject<Dictionary<string, string>>(response);
Assert.Equal(2, json.Count);
Assert.Null(json["param1.IncludedExplicitlyAtTypeLevel"]);
Assert.Null(json["param2.ExcludedExplicitlyAtTypeLevel"]);
Assert.Null(json["param1.LastName"]);
Assert.Equal("0", json["param2.Id"]);
}
[Fact]
public async Task BindAttribute_AppliesAtBothParameterAndTypeLevelTogether_WhitelistedAtBothLevelsIsBound()
public async Task BindAttribute_AppliesAtBothParameterAndTypeLevelTogether_IncludedAtBothLevelsIsBound()
{
// Arrange
var server = TestServer.Create(_services, _app);
@ -735,17 +800,17 @@ namespace Microsoft.AspNet.Mvc.FunctionalTests
// Act
var response = await client.GetStringAsync("http://localhost/BindAttribute/" +
"BindAtParamterLevelAndBindAtTypeLevelAreBothEvaluated_WhiteListingAtBothLevelBinds" +
"?param1.IncludedExplicitlyAtTypeLevel=someValue&param2.ExcludedExplicitlyAtTypeLevel=someValue");
"UpdateFirstName_IncludingAtBothLevelBinds" +
"?param1.FirstName=someValue&param2.Id=123");
// Assert
var json = JsonConvert.DeserializeObject<Dictionary<string, string>>(response);
Assert.Equal(1, json.Count);
Assert.Equal("someValue", json["param1.IncludedExplicitlyAtTypeLevel"]);
Assert.Equal("someValue", json["param1.FirstName"]);
}
[Fact]
public async Task BindAttribute_AppliesAtBothParameterAndTypeLevelTogether_WhitelistingAtOneLevelIsNotBound()
public async Task BindAttribute_AppliesAtBothParameterAndTypeLevelTogether_IncludingAtOneLevelIsNotBound()
{
// Arrange
var server = TestServer.Create(_services, _app);
@ -753,14 +818,14 @@ namespace Microsoft.AspNet.Mvc.FunctionalTests
// Act
var response = await client.GetStringAsync("http://localhost/BindAttribute/" +
"BindAtParamterLevelAndBindAtTypeLevelAreBothEvaluated_WhiteListingAtOnlyOneLevelDoesNotBind" +
"?param1.IncludedExplicitlyAtTypeLevel=someValue&param1.IncludedExplicitlyAtParameterLevel=someValue");
"UpdateIsAdmin_IncludingAtOnlyOneLevelDoesNotBind" +
"?param1.FirstName=someValue&param1.IsAdmin=true");
// Assert
var json = JsonConvert.DeserializeObject<Dictionary<string, string>>(response);
Assert.Equal(2, json.Count);
Assert.Null(json["param1.IncludedExplicitlyAtParameterLevel"]);
Assert.Null(json["param1.IncludedExplicitlyAtTypeLevel"]);
Assert.Equal("False", json["param1.IsAdmin"]);
Assert.Null(json["param1.FirstName"]);
}
[Fact]

View File

@ -1,20 +1,169 @@
// 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 Microsoft.AspNet.PipelineCore;
using Microsoft.Framework.DependencyInjection;
#if ASPNET50
using Moq;
#endif
using Xunit;
namespace Microsoft.AspNet.Mvc.ModelBinding.Test
namespace Microsoft.AspNet.Mvc.ModelBinding
{
public class BindAttributeTest
{
[Fact]
public void PrefixPropertyDefaultsToNull()
public void Constructor_Throws_IfTypeDoesNotImplement_IPropertyBindingPredicateProvider()
{
// Arrange
BindAttribute attr = new BindAttribute();
var expected =
"The type 'Microsoft.AspNet.Mvc.ModelBinding.BindAttributeTest+UnrelatedType' " +
"does not implement the interface " +
"'Microsoft.AspNet.Mvc.ModelBinding.IPropertyBindingPredicateProvider'." +
Environment.NewLine +
"Parameter name: predicateProviderType";
// Act & assert
Assert.Null(attr.Prefix);
// Act & Assert
var exception = Assert.Throws<ArgumentException>(() => new BindAttribute(typeof(UnrelatedType)));
Assert.Equal(expected, exception.Message);
}
[Theory]
[InlineData(typeof(DerivedProvider))]
[InlineData(typeof(BaseProvider))]
public void Constructor_SetsThe_PropertyFilterProviderType_ForValidTypes(Type type)
{
// Arrange
var attribute = new BindAttribute(type);
// Act & Assert
Assert.Equal(type, attribute.PredicateProviderType);
}
[Theory]
[InlineData("UserName", true)]
[InlineData("Username", false)]
[InlineData("Password", false)]
public void BindAttribute_Include(string property, bool isIncluded)
{
// Arrange
var bind = new BindAttribute(new string[] { "UserName", "FirstName" });
var context = new ModelBindingContext();
// Act
var predicate = bind.PropertyFilter;
// Assert
Assert.Equal(isIncluded, predicate(context, property));
}
#if ASPNET50
[Theory]
[InlineData("UserName", true)]
[InlineData("Username", false)]
[InlineData("Password", false)]
public void BindAttribute_ProviderType(string property, bool isIncluded)
{
// Arrange
var bind = new BindAttribute(typeof(TestProvider));
var context = new ModelBindingContext();
context.OperationBindingContext = new OperationBindingContext()
{
HttpContext = new DefaultHttpContext(),
};
var activator = new Mock<ITypeActivator>(MockBehavior.Strict);
activator
.Setup(a => a.CreateInstance(It.IsAny<IServiceProvider>(), typeof(TestProvider), It.IsAny<object[]>()))
.Returns(new TestProvider())
.Verifiable();
var services = new Mock<IServiceProvider>(MockBehavior.Strict);
services
.Setup(s => s.GetService(typeof(ITypeActivator)))
.Returns(activator.Object);
context.OperationBindingContext.HttpContext.RequestServices = services.Object;
// Act
var predicate = bind.PropertyFilter;
// Assert
Assert.Equal(isIncluded, predicate(context, property));
}
// Each time .PropertyFilter is called, a since instance of the provider should
// be created and cached.
[Fact]
public void BindAttribute_ProviderType_Cached()
{
// Arrange
var bind = new BindAttribute(typeof(TestProvider));
var context = new ModelBindingContext();
context.OperationBindingContext = new OperationBindingContext()
{
HttpContext = new DefaultHttpContext(),
};
var activator = new Mock<ITypeActivator>(MockBehavior.Strict);
activator
.Setup(a => a.CreateInstance(It.IsAny<IServiceProvider>(), typeof(TestProvider), It.IsAny<object[]>()))
.Returns(new TestProvider())
.Verifiable();
var services = new Mock<IServiceProvider>(MockBehavior.Strict);
services
.Setup(s => s.GetService(typeof(ITypeActivator)))
.Returns(activator.Object);
context.OperationBindingContext.HttpContext.RequestServices = services.Object;
// Act
var predicate = bind.PropertyFilter;
// Assert
Assert.True(predicate(context, "UserName"));
Assert.True(predicate(context, "UserName"));
activator
.Verify(
a => a.CreateInstance(It.IsAny<IServiceProvider>(), typeof(TestProvider), It.IsAny<object[]>()),
Times.Once());
}
#endif
private class TestProvider : IPropertyBindingPredicateProvider
{
public Func<ModelBindingContext, string, bool> PropertyFilter
{
get
{
return (context, property) => string.Equals(property, "UserName", StringComparison.Ordinal);
}
}
}
private class BaseProvider : IPropertyBindingPredicateProvider
{
public Func<ModelBindingContext, string, bool> PropertyFilter
{
get
{
throw new NotImplementedException();
}
}
}
private class DerivedProvider : BaseProvider
{
}
private class UnrelatedType
{
}
}
}

View File

@ -9,7 +9,9 @@ using System.ComponentModel.DataAnnotations;
using System.Linq;
using System.Reflection;
using System.Threading.Tasks;
using Microsoft.AspNet.PipelineCore;
using Microsoft.AspNet.Testing;
using Microsoft.Framework.DependencyInjection;
using Moq;
using Xunit;
@ -22,7 +24,7 @@ namespace Microsoft.AspNet.Mvc.ModelBinding
[InlineData(typeof(Person), false)]
[InlineData(typeof(EmptyModel), true)]
[InlineData(typeof(EmptyModel), false)]
public async Task
public async Task
CanCreateModel_CreatesModel_ForTopLevelObjectIfThereIsExplicitPrefix(Type modelType, bool isPrefixProvided)
{
var mockValueProvider = new Mock<IValueProvider>();
@ -247,12 +249,12 @@ namespace Microsoft.AspNet.Mvc.ModelBinding
.Returns<IValueProviderMetadata>(
valueProviderMetadata =>
{
if(valueProviderMetadata is ValueBinderMetadataAttribute)
{
return mockOriginalValueProvider.Object;
}
if (valueProviderMetadata is ValueBinderMetadataAttribute)
{
return mockOriginalValueProvider.Object;
}
return null;
return null;
});
var bindingContext = new MutableObjectBinderContext
@ -590,6 +592,60 @@ namespace Microsoft.AspNet.Mvc.ModelBinding
{
ValidatorProvider = Mock.Of<IModelValidatorProvider>(),
MetadataProvider = new DataAnnotationsModelMetadataProvider()
},
};
var testableBinder = new TestableMutableObjectModelBinder();
// Act
var propertyMetadatas = testableBinder.GetMetadataForProperties(bindingContext);
var returnedPropertyNames = propertyMetadatas.Select(o => o.PropertyName).ToArray();
// Assert
Assert.Equal(expectedPropertyNames, returnedPropertyNames);
}
[Fact]
public void GetMetadataForProperties_DoesNotReturn_ExcludedProperties()
{
// Arrange
var expectedPropertyNames = new[] { "IncludedByDefault1", "IncludedByDefault2" };
var bindingContext = new ModelBindingContext
{
ModelMetadata = GetMetadataForType(typeof(TypeWithExcludedPropertiesUsingBindAttribute)),
OperationBindingContext = new OperationBindingContext
{
HttpContext = new DefaultHttpContext
{
RequestServices = CreateServices()
},
ValidatorProvider = Mock.Of<IModelValidatorProvider>(),
MetadataProvider = new DataAnnotationsModelMetadataProvider(),
}
};
var testableBinder = new TestableMutableObjectModelBinder();
// Act
var propertyMetadatas = testableBinder.GetMetadataForProperties(bindingContext);
var returnedPropertyNames = propertyMetadatas.Select(o => o.PropertyName).ToArray();
// Assert
Assert.Equal(expectedPropertyNames, returnedPropertyNames);
}
[Fact]
public void GetMetadataForProperties_ReturnsOnlyIncludedProperties_UsingBindAttributeInclude()
{
// Arrange
var expectedPropertyNames = new[] { "IncludedExplicitly1", "IncludedExplicitly2" };
var bindingContext = new ModelBindingContext
{
ModelMetadata = GetMetadataForType(typeof(TypeWithIncludedPropertiesUsingBindAttribute)),
OperationBindingContext = new OperationBindingContext
{
ValidatorProvider = Mock.Of<IModelValidatorProvider>(),
MetadataProvider = new DataAnnotationsModelMetadataProvider(),
}
};
@ -1337,6 +1393,29 @@ namespace Microsoft.AspNet.Mvc.ModelBinding
public string MarkedWithABinderMetadata { get; set; }
}
[Bind(new[] { nameof(IncludedExplicitly1), nameof(IncludedExplicitly2) })]
private class TypeWithIncludedPropertiesUsingBindAttribute
{
public int ExcludedByDefault1 { get; set; }
public int ExcludedByDefault2 { get; set; }
public int IncludedExplicitly1 { get; set; }
public int IncludedExplicitly2 { get; set; }
}
[Bind(typeof(ExcludedProvider))]
private class TypeWithExcludedPropertiesUsingBindAttribute
{
public int Excluded1 { get; set; }
public int Excluded2 { get; set; }
public int IncludedByDefault1 { get; set; }
public int IncludedByDefault2 { get; set; }
}
public class Document
{
[NonValueBinderMetadata]
@ -1354,6 +1433,35 @@ namespace Microsoft.AspNet.Mvc.ModelBinding
{
}
public class ExcludedProvider : IPropertyBindingPredicateProvider
{
public Func<ModelBindingContext, string, bool> PropertyFilter
{
get
{
return (context, propertyName) =>
!string.Equals("Excluded1", propertyName, StringComparison.OrdinalIgnoreCase) &&
!string.Equals("Excluded2", propertyName, StringComparison.OrdinalIgnoreCase);
}
}
}
private IServiceProvider CreateServices()
{
var services = new Mock<IServiceProvider>(MockBehavior.Strict);
var typeActivator = new Mock<ITypeActivator>(MockBehavior.Strict);
typeActivator
.Setup(f => f.CreateInstance(It.IsAny<IServiceProvider>(), typeof(ExcludedProvider)))
.Returns(new ExcludedProvider());
services
.Setup(s => s.GetService(typeof(ITypeActivator)))
.Returns(typeActivator.Object);
return services.Object;
}
public class TestableMutableObjectModelBinder : MutableObjectModelBinder
{
public virtual bool CanUpdatePropertyPublic(ModelMetadata propertyMetadata)

View File

@ -34,7 +34,7 @@ namespace Microsoft.AspNet.Mvc.ModelBinding
Assert.Null(cache.ScaffoldColumn);
Assert.Null(cache.BinderMetadata);
Assert.Null(cache.BinderModelNameProvider);
Assert.Empty(cache.PropertyBindingInfo);
Assert.Empty(cache.PropertyBindingPredicateProviders);
}
public static TheoryData<object, Func<CachedDataAnnotationsMetadataAttributes, object>>
@ -78,18 +78,17 @@ namespace Microsoft.AspNet.Mvc.ModelBinding
public void Constructor_FindsPropertyBindingInfo()
{
// Arrange
var propertyBindingInfos =
new[] { new TestPropertyBindingInfo(), new TestPropertyBindingInfo() };
var providers = new[] { new TestPredicateProvider(), new TestPredicateProvider() };
// Act
var cache = new CachedDataAnnotationsMetadataAttributes(propertyBindingInfos);
var result = cache.PropertyBindingInfo.ToArray();
var cache = new CachedDataAnnotationsMetadataAttributes(providers);
var result = cache.PropertyBindingPredicateProviders.ToArray();
// Assert
Assert.Equal(propertyBindingInfos.Length, result.Length);
for (var index = 0; index < propertyBindingInfos.Length; index++)
Assert.Equal(providers.Length, result.Length);
for (var index = 0; index < providers.Length; index++)
{
Assert.Same(propertyBindingInfos[index], result[index]);
Assert.Same(providers[index], result[index]);
}
}
@ -147,5 +146,16 @@ namespace Microsoft.AspNet.Mvc.ModelBinding
{
public Type BinderType { get; set; }
}
private class TestPredicateProvider : IPropertyBindingPredicateProvider
{
public Func<ModelBindingContext, string, bool> PropertyFilter
{
get
{
throw new NotImplementedException();
}
}
}
}
}

View File

@ -2,6 +2,7 @@
// 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.Reflection;
@ -11,57 +12,50 @@ namespace Microsoft.AspNet.Mvc.ModelBinding
{
public class CachedDataAnnotationsModelMetadataProviderTest
{
[Bind(Include = nameof(IncludedAndExcludedExplicitly1) + "," + nameof(IncludedExplicitly1),
Exclude = nameof(IncludedAndExcludedExplicitly1) + "," + nameof(ExcludedExplicitly1),
Prefix = "TypePrefix")]
private class TypeWithExludedAndIncludedPropertiesUsingBindAttribute
{
public int ExcludedExplicitly1 { get; set; }
public int IncludedAndExcludedExplicitly1 { get; set; }
public int IncludedExplicitly1 { get; set; }
public int NotIncludedOrExcluded { get; set; }
public void ActionWithBindAttribute(
[Bind(Include = "Property1, Property2,IncludedAndExcludedExplicitly1",
Exclude ="Property3, Property4, IncludedAndExcludedExplicitly1",
Prefix = "ParameterPrefix")]
TypeWithExludedAndIncludedPropertiesUsingBindAttribute param)
{
}
}
[Fact]
public void DataAnnotationsModelMetadataProvider_ReadsIncludedAndExcludedProperties_ForTypes()
public void DataAnnotationsModelMetadataProvider_UsesPredicateOnType()
{
// Arrange
var type = typeof(TypeWithExludedAndIncludedPropertiesUsingBindAttribute);
var type = typeof(User);
var provider = new DataAnnotationsModelMetadataProvider();
var expectedIncludedPropertyNames = new[] { "IncludedAndExcludedExplicitly1", "IncludedExplicitly1" };
var expectedExcludedPropertyNames = new[] { "IncludedAndExcludedExplicitly1", "ExcludedExplicitly1" };
var context = new ModelBindingContext();
var expected = new[] { "IsAdmin", "UserName" };
// Act
var metadata = provider.GetMetadataForType(null, type);
// Assert
Assert.Equal(expectedIncludedPropertyNames.ToList(), metadata.BinderIncludeProperties);
Assert.Equal(expectedExcludedPropertyNames.ToList(), metadata.BinderExcludeProperties);
var predicate = metadata.PropertyBindingPredicateProvider.PropertyFilter;
var matched = new HashSet<string>();
foreach (var property in metadata.Properties)
{
if (predicate(context, property.PropertyName))
{
matched.Add(property.PropertyName);
}
}
Assert.Equal<string>(expected, matched);
}
[Fact]
public void ModelMetadataProvider_ReadsIncludedAndExcludedProperties_AtParameterAndType_ForParameters()
public void DataAnnotationsModelMetadataProvider_UsesPredicateOnParameter()
{
// Arrange
var type = typeof(TypeWithExludedAndIncludedPropertiesUsingBindAttribute);
var methodInfo = type.GetMethod("ActionWithBindAttribute");
var provider = new DataAnnotationsModelMetadataProvider();
var type = GetType();
var methodInfo = type.GetMethod(
"ActionWithoutBindAttribute",
BindingFlags.Instance | BindingFlags.NonPublic);
// Note it does an intersection for included and a union for excluded.
var expectedIncludedPropertyNames = new[] { "IncludedAndExcludedExplicitly1" };
var expectedExcludedPropertyNames = new[] {
"Property3", "Property4", "IncludedAndExcludedExplicitly1", "ExcludedExplicitly1" };
var provider = new DataAnnotationsModelMetadataProvider();
var context = new ModelBindingContext();
// Note it does an intersection for included -- only properties that
// pass both predicates will be bound.
var expected = new[] { "IsAdmin", "UserName" };
// Act
var metadata = provider.GetMetadataForParameter(
@ -70,16 +64,68 @@ namespace Microsoft.AspNet.Mvc.ModelBinding
parameterName: "param");
// Assert
Assert.Equal(expectedIncludedPropertyNames.ToList(), metadata.BinderIncludeProperties);
Assert.Equal(expectedExcludedPropertyNames.ToList(), metadata.BinderExcludeProperties);
var predicate = metadata.PropertyBindingPredicateProvider.PropertyFilter;
Assert.NotNull(predicate);
var matched = new HashSet<string>();
foreach (var property in metadata.Properties)
{
if (predicate(context, property.PropertyName))
{
matched.Add(property.PropertyName);
}
}
Assert.Equal<string>(expected, matched);
}
[Fact]
public void ModelMetadataProvider_ReadsPrefixProperty_OnlyAtParameterLevel_ForParameters()
public void DataAnnotationsModelMetadataProvider_UsesPredicateOnParameter_Merge()
{
// Arrange
var type = typeof(TypeWithExludedAndIncludedPropertiesUsingBindAttribute);
var methodInfo = type.GetMethod("ActionWithBindAttribute");
var type = GetType();
var methodInfo = type.GetMethod(
"ActionWithBindAttribute",
BindingFlags.Instance | BindingFlags.NonPublic);
var provider = new DataAnnotationsModelMetadataProvider();
var context = new ModelBindingContext();
// Note it does an intersection for included -- only properties that
// pass both predicates will be bound.
var expected = new[] { "IsAdmin" };
// Act
var metadata = provider.GetMetadataForParameter(
modelAccessor: null,
methodInfo: methodInfo,
parameterName: "param");
// Assert
var predicate = metadata.PropertyBindingPredicateProvider.PropertyFilter;
Assert.NotNull(predicate);
var matched = new HashSet<string>();
foreach (var property in metadata.Properties)
{
if (predicate(context, property.PropertyName))
{
matched.Add(property.PropertyName);
}
}
Assert.Equal<string>(expected, matched);
}
[Fact]
public void DataAnnotationsModelMetadataProvider_ReadsModelNameProperty_ForParameters()
{
// Arrange
var type = GetType();
var methodInfo = type.GetMethod(
"ActionWithBindAttribute",
BindingFlags.Instance | BindingFlags.NonPublic);
var provider = new DataAnnotationsModelMetadataProvider();
// Act
@ -96,7 +142,7 @@ namespace Microsoft.AspNet.Mvc.ModelBinding
public void DataAnnotationsModelMetadataProvider_ReadsModelNameProperty_ForTypes()
{
// Arrange
var type = typeof(TypeWithExludedAndIncludedPropertiesUsingBindAttribute);
var type = typeof(User);
var provider = new DataAnnotationsModelMetadataProvider();
// Act
@ -106,23 +152,6 @@ namespace Microsoft.AspNet.Mvc.ModelBinding
Assert.Equal("TypePrefix", metadata.BinderModelName);
}
[Fact]
public void DataAnnotationsModelMetadataProvider_ReadsModelNameProperty_ForParameters()
{
// Arrange
var type = typeof(TypeWithExludedAndIncludedPropertiesUsingBindAttribute);
var methodInfo = type.GetMethod("ActionWithBindAttribute");
var provider = new DataAnnotationsModelMetadataProvider();
// Act
var metadata = provider.GetMetadataForParameter(
modelAccessor: null,
methodInfo: methodInfo,
parameterName: "param");
// Assert
Assert.Equal("ParameterPrefix", metadata.BinderModelName);
}
[Fact]
public void DataAnnotationsModelMetadataProvider_ReadsScaffoldColumnAttribute_ForShowForDisplay()
@ -193,8 +222,6 @@ namespace Microsoft.AspNet.Mvc.ModelBinding
Assert.NotNull(propertyMetadata.BinderMetadata);
var attribute = Assert.IsType<TypeBasedBinderAttribute>(propertyMetadata.BinderMetadata);
Assert.Equal("PersonType", propertyMetadata.BinderModelName);
Assert.Equal(new[] { "IncludeAtType" }, propertyMetadata.BinderIncludeProperties.ToArray());
Assert.Equal(new[] { "ExcludeAtType" }, propertyMetadata.BinderExcludeProperties.ToArray());
}
[Fact]
@ -210,12 +237,8 @@ namespace Microsoft.AspNet.Mvc.ModelBinding
Assert.NotNull(propertyMetadata.BinderMetadata);
var attribute = Assert.IsType<NonTypeBasedBinderAttribute>(propertyMetadata.BinderMetadata);
Assert.Equal("GrandParentProperty", propertyMetadata.BinderModelName);
Assert.Empty(propertyMetadata.BinderIncludeProperties);
Assert.Equal(new[] { "ExcludeAtProperty", "ExcludeAtType" },
propertyMetadata.BinderExcludeProperties.ToArray());
}
#if ASPNET50
[Fact]
public void GetMetadataForParameter_WithNoBinderMetadata_GetsItFromType()
{
@ -223,16 +246,15 @@ namespace Microsoft.AspNet.Mvc.ModelBinding
var provider = new DataAnnotationsModelMetadataProvider();
// Act
var parameterMetadata = provider.GetMetadataForParameter(null,
typeof(Person).GetMethod("Update"),
"person");
var parameterMetadata = provider.GetMetadataForParameter(
null,
typeof(Person).GetMethod("Update"),
"person");
// Assert
Assert.NotNull(parameterMetadata.BinderMetadata);
var attribute = Assert.IsType<TypeBasedBinderAttribute>(parameterMetadata.BinderMetadata);
Assert.Equal("PersonType", parameterMetadata.BinderModelName);
Assert.Equal(new[] { "IncludeAtType" }, parameterMetadata.BinderIncludeProperties.ToArray());
Assert.Equal(new[] { "ExcludeAtType" }, parameterMetadata.BinderExcludeProperties.ToArray());
}
[Fact]
@ -242,53 +264,48 @@ namespace Microsoft.AspNet.Mvc.ModelBinding
var provider = new DataAnnotationsModelMetadataProvider();
// Act
var parameterMetadata = provider.GetMetadataForParameter(null,
typeof(Person).GetMethod("Save"),
"person");
var parameterMetadata = provider.GetMetadataForParameter(
null,
typeof(Person).GetMethod("Save"),
"person");
// Assert
Assert.NotNull(parameterMetadata.BinderMetadata);
var attribute = Assert.IsType<NonTypeBasedBinderAttribute>(parameterMetadata.BinderMetadata);
Assert.Equal("PersonParameter", parameterMetadata.BinderModelName);
Assert.Empty(parameterMetadata.BinderIncludeProperties);
Assert.Equal(new[] { "ExcludeAtParameter", "ExcludeAtType" },
parameterMetadata.BinderExcludeProperties.ToArray());
}
#endif
public class TypeBasedBinderAttribute : Attribute,
IBinderMetadata, IModelNameProvider, IPropertyBindingInfo
private void ActionWithoutBindAttribute(User param)
{
}
private void ActionWithBindAttribute([Bind(new string[] { "IsAdmin" }, Prefix = "ParameterPrefix")] User param)
{
}
public class TypeBasedBinderAttribute : Attribute, IBinderMetadata, IModelNameProvider
{
public string Name { get; set; }
public string Exclude { get; set; }
public string Include { get; set; }
}
public class NonTypeBasedBinderAttribute : Attribute,
IBinderMetadata, IModelNameProvider, IPropertyBindingInfo
public class NonTypeBasedBinderAttribute : Attribute, IBinderMetadata, IModelNameProvider
{
public string Name { get; set; }
public string Exclude { get; set; }
public string Include { get; set; }
}
[TypeBasedBinder(Name = "PersonType", Include = "IncludeAtType", Exclude = "ExcludeAtType")]
[TypeBasedBinder(Name = "PersonType")]
public class Person
{
public Person Parent { get; set; }
[NonTypeBasedBinder(Name = "GrandParentProperty", Include = "IncludeAtProperty", Exclude = "ExcludeAtProperty")]
[NonTypeBasedBinder(Name = "GrandParentProperty")]
public Person GrandParent { get; set; }
public void Update(Person person)
{
}
public void Save([NonTypeBasedBinder(Name = "PersonParameter",
Include = "IncludeAtParameter", Exclude = "ExcludeAtParameter")] Person person)
public void Save([NonTypeBasedBinder(Name = "PersonParameter")] Person person)
{
}
}
@ -317,5 +334,17 @@ namespace Microsoft.AspNet.Mvc.ModelBinding
public HiddenClass OfHiddenType { get; set; }
}
[Bind(new[] { nameof(IsAdmin), nameof(UserName) }, Prefix = "TypePrefix")]
private class User
{
public int Id { get; set; }
public bool IsAdmin { get; set; }
public int UserName { get; set; }
public int NotIncludedOrExcluded { get; set; }
}
}
}

View File

@ -52,9 +52,8 @@ namespace Microsoft.AspNet.Mvc.ModelBinding
Assert.Null(metadata.BinderModelName);
Assert.Null(metadata.BinderMetadata);
Assert.Null(metadata.PropertyBindingPredicateProvider);
Assert.Null(metadata.BinderType);
Assert.Empty(metadata.BinderIncludeProperties);
Assert.Null(metadata.BinderExcludeProperties);
}
public static TheoryData<object, Func<ModelMetadata, string>> ExpectedAttributeDataStrings

View File

@ -22,6 +22,7 @@ namespace Microsoft.AspNet.Mvc.ModelBinding
var nonEmptycontainerModel = new DummyModelContainer { Model = contactModel };
var binderMetadata = new TestBinderMetadata();
var predicateProvider = new DummyPropertyBindingPredicateProvider();
var emptyPropertyList = new List<string>();
var nonEmptyPropertyList = new List<string>() { "SomeProperty" };
return new TheoryData<Action<ModelMetadata>, Func<ModelMetadata, object>, object>
@ -56,28 +57,8 @@ namespace Microsoft.AspNet.Mvc.ModelBinding
{ m => m.BinderModelName = string.Empty, m => m.BinderModelName, string.Empty },
{ m => m.BinderType = null, m => m.BinderType, null },
{ m => m.BinderType = typeof(string), m => m.BinderType, typeof(string) },
{ m => m.BinderIncludeProperties = null, m => m.BinderIncludeProperties, null },
{
m => m.BinderIncludeProperties = emptyPropertyList,
m => m.BinderIncludeProperties,
emptyPropertyList
},
{
m => m.BinderIncludeProperties = nonEmptyPropertyList,
m => m.BinderIncludeProperties,
nonEmptyPropertyList
},
{ m => m.BinderExcludeProperties = null, m => m.BinderExcludeProperties, null },
{
m => m.BinderExcludeProperties = emptyPropertyList,
m => m.BinderExcludeProperties,
emptyPropertyList
},
{
m => m.BinderExcludeProperties = nonEmptyPropertyList,
m => m.BinderExcludeProperties,
nonEmptyPropertyList
},
{ m => m.PropertyBindingPredicateProvider = null, m => m.PropertyBindingPredicateProvider, null },
{ m => m.PropertyBindingPredicateProvider = predicateProvider, m => m.PropertyBindingPredicateProvider, predicateProvider },
};
}
}
@ -128,8 +109,7 @@ namespace Microsoft.AspNet.Mvc.ModelBinding
Assert.Null(metadata.BinderModelName);
Assert.Null(metadata.BinderType);
Assert.Null(metadata.BinderMetadata);
Assert.Null(metadata.BinderIncludeProperties);
Assert.Null(metadata.BinderExcludeProperties);
Assert.Null(metadata.PropertyBindingPredicateProvider);
}
// IsComplexType
@ -514,5 +494,10 @@ namespace Microsoft.AspNet.Mvc.ModelBinding
{
public DummyContactModel Model { get; set; }
}
private class DummyPropertyBindingPredicateProvider : IPropertyBindingPredicateProvider
{
public Func<ModelBindingContext, string, bool> PropertyFilter { get; set; }
}
}
}

View File

@ -1,49 +1,61 @@
// 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.Linq.Expressions;
using Microsoft.AspNet.Mvc;
using Microsoft.AspNet.Mvc.ModelBinding;
namespace ModelBindingWebSite.Controllers
{
public class BindAttributeController : Controller
{
public Dictionary<string, string>
BindAtParamterLevelAndBindAtTypeLevelAreBothEvaluated_BlackListingAtEitherLevelDoesNotBind(
[Bind(Exclude = "IncludedExplicitlyAtTypeLevel")] TypeWithIncludedPropertyAtBindAttribute param1,
[Bind(Include = "ExcludedExplicitlyAtTypeLevel")] TypeWithExcludedPropertyAtBindAttribute param2)
public User EchoUser([Bind(typeof(ExcludeUserPropertiesAtParameter))] User user)
{
return new Dictionary<string, string>()
{
// The first one should not be included because the parameter level bind attribute filters it out.
{ "param1.IncludedExplicitlyAtTypeLevel", param1.IncludedExplicitlyAtTypeLevel },
return user;
}
// The second one should not be included because the type level bind attribute filters it out.
{ "param2.ExcludedExplicitlyAtTypeLevel", param2.ExcludedExplicitlyAtTypeLevel },
};
public User EchoUserUsingServices([Bind(typeof(ExcludeUserPropertiesUsingService))] User user)
{
return user;
}
public Dictionary<string, string>
BindAtParamterLevelAndBindAtTypeLevelAreBothEvaluated_WhiteListingAtBothLevelBinds(
[Bind(Include = "IncludedExplicitlyAtTypeLevel")] TypeWithIncludedPropertyAtBindAttribute param1)
UpdateUserId_BlackListingAtEitherLevelDoesNotBind(
[Bind(typeof(ExcludeLastName))] User2 param1,
[Bind("Id")] User2 param2)
{
return new Dictionary<string, string>()
{
// The since this is included at both level it is bound.
{ "param1.IncludedExplicitlyAtTypeLevel", param1.IncludedExplicitlyAtTypeLevel },
// LastName is excluded at parameter level.
{ "param1.LastName", param1.LastName },
// Id is excluded because it is not explicitly included by the bind attribute at type level.
{ "param2.Id", param2.Id.ToString() },
};
}
public Dictionary<string, string>
BindAtParamterLevelAndBindAtTypeLevelAreBothEvaluated_WhiteListingAtOnlyOneLevelDoesNotBind(
[Bind(Include = "IncludedExplicitlyAtParameterLevel")]
TypeWithIncludedPropertyAtParameterAndTypeUsingBindAttribute param1)
public Dictionary<string, string> UpdateFirstName_IncludingAtBothLevelBinds(
[Bind("FirstName")] User2 param1)
{
return new Dictionary<string, string>()
{
// The since this is included at only type level it is not bound.
{ "param1.IncludedExplicitlyAtParameterLevel", param1.IncludedExplicitlyAtParameterLevel },
{ "param1.IncludedExplicitlyAtTypeLevel", param1.IncludedExplicitlyAtTypeLevel },
// The since FirstName is included at both level it is bound.
{ "param1.FirstName", param1.FirstName },
};
}
public Dictionary<string, string> UpdateIsAdmin_IncludingAtOnlyOneLevelDoesNotBind(
[Bind("IsAdmin" )] User2 param1)
{
return new Dictionary<string, string>()
{
// IsAdmin is not included because it is not explicitly included at type level.
{ "param1.IsAdmin", param1.IsAdmin.ToString() },
// FirstName is not included because it is not explicitly included at parameter level.
{ "param1.FirstName", param1.FirstName },
};
}
@ -57,6 +69,49 @@ namespace ModelBindingWebSite.Controllers
{
return param.Value;
}
private class ExcludeUserPropertiesAtParameter : DefaultPropertyBindingPredicateProvider<User>
{
public override string Prefix
{
get
{
return "user";
}
}
public override IEnumerable<Expression<Func<User, object>>> PropertyIncludeExpressions
{
get
{
yield return m => m.RegisterationMonth;
yield return m => m.UserName;
}
}
}
private class ExcludeUserPropertiesUsingService : ExcludeUserPropertiesAtParameter
{
private ITestService _testService;
public ExcludeUserPropertiesUsingService(ITestService testService)
{
_testService = testService;
}
public override IEnumerable<Expression<Func<User, object>>> PropertyIncludeExpressions
{
get
{
if (_testService.Test())
{
return base.PropertyIncludeExpressions;
}
return null;
}
}
}
}
[Bind(Prefix = "TypePrefix")]
@ -70,22 +125,27 @@ namespace ModelBindingWebSite.Controllers
public string Value { get; set; }
}
[Bind(Include = nameof(IncludedExplicitlyAtTypeLevel))]
public class TypeWithIncludedPropertyAtParameterAndTypeUsingBindAttribute
[Bind(nameof(FirstName), nameof(LastName))]
public class User2
{
public string IncludedExplicitlyAtTypeLevel { get; set; }
public string IncludedExplicitlyAtParameterLevel { get; set; }
public int Id { get; set; }
public string FirstName { get; set; }
public string LastName { get; set; }
public bool IsAdmin { get; set; }
}
[Bind(Include = nameof(IncludedExplicitlyAtTypeLevel))]
public class TypeWithIncludedPropertyAtBindAttribute
public class ExcludeLastName : IPropertyBindingPredicateProvider
{
public string IncludedExplicitlyAtTypeLevel { get; set; }
}
[Bind(Exclude = nameof(ExcludedExplicitlyAtTypeLevel))]
public class TypeWithExcludedPropertyAtBindAttribute
{
public string ExcludedExplicitlyAtTypeLevel { get; set; }
public Func<ModelBindingContext, string, bool> PropertyFilter
{
get
{
return (context, propertyName) =>
!string.Equals("LastName", propertyName, StringComparison.OrdinalIgnoreCase);
}
}
}
}

View File

@ -1,12 +1,10 @@
// 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.
namespace Microsoft.AspNet.Mvc.ModelBinding
namespace ModelBindingWebSite
{
public class TestPropertyBindingInfo : IPropertyBindingInfo
public interface ITestService
{
public string Exclude { get; set; }
public string Include { get; set; }
bool Test();
}
}
}

View File

@ -27,6 +27,7 @@ namespace ModelBindingWebSite
});
services.AddSingleton<ICalculator, DefaultCalculator>();
services.AddSingleton<ITestService, TestService>();
});
// Add MVC to the request pipeline

View File

@ -0,0 +1,13 @@
// 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.
namespace ModelBindingWebSite
{
public class TestService : ITestService
{
public bool Test()
{
return true;
}
}
}