From d7094fd32dd96eafcfce7eb9824b24a1329275ca Mon Sep 17 00:00:00 2001 From: Harsh Gupta Date: Mon, 10 Nov 2014 13:24:51 -0800 Subject: [PATCH] Adding Support for TryUpdateModel using include expressions and predicate. --- .../DefaultControllerActionArgumentBinder.cs | 15 +- ...DefaultPropertyBindingPredicateProvider.cs | 66 ++++++ .../ParameterBinding/ModelBindingHelper.cs | 13 +- .../BindAttribute.cs | 107 +++++++-- .../Binders/MutableObjectModelBinder.cs | 16 +- .../IPropertyBindingInfo.cs | 21 -- .../IPropertyBindingPredicateProvider.cs | 18 ++ ...CachedDataAnnotationsMetadataAttributes.cs | 10 +- .../CachedDataAnnotationsModelMetadata.cs | 92 ++++---- .../Metadata/CachedModelMetadata.cs | 76 ++----- .../Metadata/ModelMetadata.cs | 23 +- .../Properties/Resources.Designer.cs | 12 +- .../Resources.resx | 4 +- .../ControllerActionArgumentBinderTests.cs | 42 +--- .../ModelBindingTests.cs | 91 ++++++-- .../BindAttributeTest.cs | 159 ++++++++++++- .../Binders/MutableObjectModelBinderTest.cs | 120 +++++++++- ...edDataAnnotationsMetadataAttributesTest.cs | 26 ++- ...ataAnnotationsModelMetadataProviderTest.cs | 215 ++++++++++-------- .../CachedDataAnnotationsModelMetadataTest.cs | 3 +- .../Metadata/ModelMetadataTest.cs | 33 +-- .../Controllers/BindAttributeController.cs | 130 ++++++++--- .../ModelBindingWebSite/ITestService.cs} | 10 +- test/WebSites/ModelBindingWebSite/Startup.cs | 1 + .../ModelBindingWebSite/TestService.cs | 13 ++ 25 files changed, 903 insertions(+), 413 deletions(-) create mode 100644 src/Microsoft.AspNet.Mvc.Core/DefaultPropertyBindingPredicateProvider.cs delete mode 100644 src/Microsoft.AspNet.Mvc.ModelBinding/IPropertyBindingInfo.cs create mode 100644 src/Microsoft.AspNet.Mvc.ModelBinding/IPropertyBindingPredicateProvider.cs rename test/{Microsoft.AspNet.Mvc.ModelBinding.Test/Metadata/TestPropertyBindingInfo.cs => WebSites/ModelBindingWebSite/ITestService.cs} (50%) create mode 100644 test/WebSites/ModelBindingWebSite/TestService.cs diff --git a/src/Microsoft.AspNet.Mvc.Core/DefaultControllerActionArgumentBinder.cs b/src/Microsoft.AspNet.Mvc.Core/DefaultControllerActionArgumentBinder.cs index 9ba8c37656..e1b3fcb77e 100644 --- a/src/Microsoft.AspNet.Mvc.Core/DefaultControllerActionArgumentBinder.cs +++ b/src/Microsoft.AspNet.Mvc.Core/DefaultControllerActionArgumentBinder.cs @@ -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 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, diff --git a/src/Microsoft.AspNet.Mvc.Core/DefaultPropertyBindingPredicateProvider.cs b/src/Microsoft.AspNet.Mvc.Core/DefaultPropertyBindingPredicateProvider.cs new file mode 100644 index 0000000000..1ef3dd7b05 --- /dev/null +++ b/src/Microsoft.AspNet.Mvc.Core/DefaultPropertyBindingPredicateProvider.cs @@ -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 +{ + /// + /// Default implementation for . + /// Provides a expression based way to provide include properties. + /// + /// The target model Type. + public class DefaultPropertyBindingPredicateProvider : IPropertyBindingPredicateProvider + where TModel : class + { + private static readonly Func _defaultFilter = (context, propertyName) => true; + + /// + /// The prefix which is used while generating the property filter. + /// + public virtual string Prefix + { + get + { + return string.Empty; + } + } + + /// + /// Expressions which can be used to generate property filter which can filter model + /// properties. + /// + public virtual IEnumerable>> PropertyIncludeExpressions + { + get + { + return null; + } + } + + /// + public virtual Func PropertyFilter + { + get + { + if (PropertyIncludeExpressions == null) + { + return _defaultFilter; + } + + // We do not cache by default. + return GetPredicateFromExpression(PropertyIncludeExpressions); + } + } + + private Func GetPredicateFromExpression(IEnumerable>> includeExpressions) + { + var expression = ModelBindingHelper.GetIncludePredicateExpression(Prefix, includeExpressions.ToArray()); + return expression.Compile(); + } + } +} diff --git a/src/Microsoft.AspNet.Mvc.Core/ParameterBinding/ModelBindingHelper.cs b/src/Microsoft.AspNet.Mvc.Core/ParameterBinding/ModelBindingHelper.cs index cc26ad8d58..ae1965a9ca 100644 --- a/src/Microsoft.AspNet.Mvc.Core/ParameterBinding/ModelBindingHelper.cs +++ b/src/Microsoft.AspNet.Mvc.Core/ParameterBinding/ModelBindingHelper.cs @@ -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> GetIncludePredicateExpression - (string prefix, Expression>[] expressions) + /// + /// Creates an expression for a predicate to limit the set of properties used in model binding. + /// + /// The model type. + /// The model prefix. + /// Expressions identifying the properties to allow for binding. + /// An expression which can be used with . + public static Expression> GetIncludePredicateExpression( + string prefix, + Expression>[] expressions) { if (expressions.Length == 0) { diff --git a/src/Microsoft.AspNet.Mvc.ModelBinding/BindAttribute.cs b/src/Microsoft.AspNet.Mvc.ModelBinding/BindAttribute.cs index b8fccfa509..559d2a27a6 100644 --- a/src/Microsoft.AspNet.Mvc.ModelBinding/BindAttribute.cs +++ b/src/Microsoft.AspNet.Mvc.ModelBinding/BindAttribute.cs @@ -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. /// [AttributeUsage(AttributeTargets.Class | AttributeTargets.Parameter, AllowMultiple = false, Inherited = true)] - public sealed class BindAttribute : Attribute, IModelNameProvider, IPropertyBindingInfo + public class BindAttribute : Attribute, IModelNameProvider, IPropertyBindingPredicateProvider { - /// - /// Comma separated set of properties which are to be excluded during model binding. - /// - public string Exclude { get; set; } = string.Empty; + private static readonly Func _defaultFilter = (context, propertyName) => true; + + private Func _predicateFromInclude; /// - /// Comma separated set of properties which are to be included during model binding. + /// Creates a new instace of . /// - public string Include { get; set; } = string.Empty; + /// Names of parameters to include in binding. + public BindAttribute(params string[] include) + { + Include = include; + } + + /// + /// Creates a new instance of . + /// + /// The type which implements + /// . + /// + 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; + } + + /// + public Type PredicateProviderType { get; } + + /// + /// Gets the names of properties to include in model binding. + /// + public string[] Include { get; } - // This property is exposed for back compat reasons. /// /// Allows a user to specify a particular prefix to match during model binding. /// + // This property is exposed for back compat reasons. public string Prefix { get; set; } /// @@ -40,18 +71,56 @@ namespace Microsoft.AspNet.Mvc } } - public static bool IsPropertyAllowed(string propertyName, - IReadOnlyList includeProperties, - IReadOnlyList excludeProperties) + /// + public Func 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 CreatePredicateFromProviderType( + Type predicateProviderType) + { + // Holding state to avoid execessive creation of the provider. + var initialized = false; + Func predicate = null; + + return (ModelBindingContext context, string propertyName) => + { + if (!initialized) + { + var services = context.OperationBindingContext.HttpContext.RequestServices; + var activator = services.GetService(); + + var provider = (IPropertyBindingPredicateProvider)activator.CreateInstance( + services, + predicateProviderType); + + initialized = true; + predicate = provider.PropertyFilter ?? _defaultFilter; + } + + return predicate(context, propertyName); + }; } } } diff --git a/src/Microsoft.AspNet.Mvc.ModelBinding/Binders/MutableObjectModelBinder.cs b/src/Microsoft.AspNet.Mvc.ModelBinding/Binders/MutableObjectModelBinder.cs index af58f66544..359c50ff69 100644 --- a/src/Microsoft.AspNet.Mvc.ModelBinding/Binders/MutableObjectModelBinder.cs +++ b/src/Microsoft.AspNet.Mvc.ModelBinding/Binders/MutableObjectModelBinder.cs @@ -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 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 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(); diff --git a/src/Microsoft.AspNet.Mvc.ModelBinding/IPropertyBindingInfo.cs b/src/Microsoft.AspNet.Mvc.ModelBinding/IPropertyBindingInfo.cs deleted file mode 100644 index 875af16631..0000000000 --- a/src/Microsoft.AspNet.Mvc.ModelBinding/IPropertyBindingInfo.cs +++ /dev/null @@ -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 -{ - /// - /// Represents an entity which has binding information for a model. - /// - public interface IPropertyBindingInfo - { - /// - /// Comma separated set of properties which are to be excluded during model binding. - /// - string Exclude { get; } - - /// - /// Comma separated set of properties which are to be included during model binding. - /// - string Include { get; } - } -} diff --git a/src/Microsoft.AspNet.Mvc.ModelBinding/IPropertyBindingPredicateProvider.cs b/src/Microsoft.AspNet.Mvc.ModelBinding/IPropertyBindingPredicateProvider.cs new file mode 100644 index 0000000000..b39502120c --- /dev/null +++ b/src/Microsoft.AspNet.Mvc.ModelBinding/IPropertyBindingPredicateProvider.cs @@ -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 +{ + /// + /// Provides a predicate which can determines which model properties should be bound by model binding. + /// + public interface IPropertyBindingPredicateProvider + { + /// + /// Gets a predicate which can determines which model properties should be bound by model binding. + /// + Func PropertyFilter { get; } + } +} diff --git a/src/Microsoft.AspNet.Mvc.ModelBinding/Metadata/CachedDataAnnotationsMetadataAttributes.cs b/src/Microsoft.AspNet.Mvc.ModelBinding/Metadata/CachedDataAnnotationsMetadataAttributes.cs index 67e18709fd..cf7cdbc92f 100644 --- a/src/Microsoft.AspNet.Mvc.ModelBinding/Metadata/CachedDataAnnotationsMetadataAttributes.cs +++ b/src/Microsoft.AspNet.Mvc.ModelBinding/Metadata/CachedDataAnnotationsMetadataAttributes.cs @@ -21,7 +21,7 @@ namespace Microsoft.AspNet.Mvc.ModelBinding Required = attributes.OfType().FirstOrDefault(); ScaffoldColumn = attributes.OfType().FirstOrDefault(); BinderMetadata = attributes.OfType().FirstOrDefault(); - PropertyBindingInfo = attributes.OfType(); + PropertyBindingPredicateProviders = attributes.OfType(); BinderModelNameProvider = attributes.OfType().FirstOrDefault(); BinderTypeProviders = attributes.OfType(); @@ -82,11 +82,11 @@ namespace Microsoft.AspNet.Mvc.ModelBinding public HiddenInputAttribute HiddenInput { get; protected set; } /// - /// Gets (or sets in subclasses) found in collection - /// passed to the constructor, - /// if any. + /// Gets (or sets in subclasses) found in + /// collection passed to the + /// constructor, if any. /// - public IEnumerable PropertyBindingInfo { get; protected set; } + public IEnumerable PropertyBindingPredicateProviders { get; protected set; } public RequiredAttribute Required { get; protected set; } diff --git a/src/Microsoft.AspNet.Mvc.ModelBinding/Metadata/CachedDataAnnotationsModelMetadata.cs b/src/Microsoft.AspNet.Mvc.ModelBinding/Metadata/CachedDataAnnotationsModelMetadata.cs index ed0a28608d..770b4d5c28 100644 --- a/src/Microsoft.AspNet.Mvc.ModelBinding/Metadata/CachedDataAnnotationsModelMetadata.cs +++ b/src/Microsoft.AspNet.Mvc.ModelBinding/Metadata/CachedDataAnnotationsModelMetadata.cs @@ -70,56 +70,11 @@ namespace Microsoft.AspNet.Mvc.ModelBinding : base.ComputeBinderModelNamePrefix(); } - protected override IReadOnlyList 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 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 PropertyFilter + { + get + { + return CreatePredicate(); + } + } + + private Func 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; + }; + } + } } } diff --git a/src/Microsoft.AspNet.Mvc.ModelBinding/Metadata/CachedModelMetadata.cs b/src/Microsoft.AspNet.Mvc.ModelBinding/Metadata/CachedModelMetadata.cs index c53daaed5e..e56f722949 100644 --- a/src/Microsoft.AspNet.Mvc.ModelBinding/Metadata/CachedModelMetadata.cs +++ b/src/Microsoft.AspNet.Mvc.ModelBinding/Metadata/CachedModelMetadata.cs @@ -33,8 +33,7 @@ namespace Microsoft.AspNet.Mvc.ModelBinding private bool _showForEdit; private IBinderMetadata _binderMetadata; private string _binderModelName; - private IReadOnlyList _binderIncludeProperties; - private IReadOnlyList _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 prototype, Func modelAccessor) @@ -102,50 +100,8 @@ namespace Microsoft.AspNet.Mvc.ModelBinding _binderMetadata = value; _isBinderMetadataComputed = true; } - } - - /// - public sealed override IReadOnlyList BinderIncludeProperties - { - get - { - if (!_isBinderIncludePropertiesComputed) - { - _binderIncludeProperties = ComputeBinderIncludeProperties(); - _isBinderIncludePropertiesComputed = true; - } - - return _binderIncludeProperties; } - set - { - _binderIncludeProperties = value; - _isBinderIncludePropertiesComputed = true; - } - } - - /// - public sealed override IReadOnlyList BinderExcludeProperties - { - get - { - if (!_isBinderExcludePropertiesComputed) - { - _binderExcludeProperties = ComputeBinderExcludeProperties(); - _isBinderExcludePropertiesComputed = true; - } - - return _binderExcludeProperties; - } - - set - { - _binderExcludeProperties = value; - _isBinderExcludePropertiesComputed = true; - } - } - /// 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; + } + } + /// public sealed override bool ShowForDisplay { @@ -506,14 +481,9 @@ namespace Microsoft.AspNet.Mvc.ModelBinding return base.BinderMetadata; } - protected virtual IReadOnlyList ComputeBinderIncludeProperties() + protected virtual IPropertyBindingPredicateProvider ComputePropertyBindingPredicateProvider() { - return base.BinderIncludeProperties; - } - - protected virtual IReadOnlyList ComputeBinderExcludeProperties() - { - return base.BinderExcludeProperties; + return base.PropertyBindingPredicateProvider; } protected virtual string ComputeBinderModelNamePrefix() diff --git a/src/Microsoft.AspNet.Mvc.ModelBinding/Metadata/ModelMetadata.cs b/src/Microsoft.AspNet.Mvc.ModelBinding/Metadata/ModelMetadata.cs index af7d9ad835..53d129b3f2 100644 --- a/src/Microsoft.AspNet.Mvc.ModelBinding/Metadata/ModelMetadata.cs +++ b/src/Microsoft.AspNet.Mvc.ModelBinding/Metadata/ModelMetadata.cs @@ -48,23 +48,14 @@ namespace Microsoft.AspNet.Mvc.ModelBinding } /// - /// The name of the model if specified explicitly using . + /// Gets or sets the name of a model if specified explicitly using . /// public virtual string BinderModelName { get; set; } /// - /// Properties which are to be included while binding this model. - /// - public virtual IReadOnlyList BinderIncludeProperties { get; set; } - - /// - /// Properties which are to be excluded while binding this model. - /// - public virtual IReadOnlyList BinderExcludeProperties { get; set; } - - /// - /// The of an or an - /// of a model if specified explicitly using . + /// Gets or sets the of an or an + /// of a model if specified explicitly using + /// . /// public virtual Type BinderType { get; set; } @@ -211,6 +202,12 @@ namespace Microsoft.AspNet.Mvc.ModelBinding } } + /// + /// Gets or sets the , which can determine which properties + /// should be model bound. + /// + public virtual IPropertyBindingPredicateProvider PropertyBindingPredicateProvider { get; set; } + public string PropertyName { get { return _propertyName; } diff --git a/src/Microsoft.AspNet.Mvc.ModelBinding/Properties/Resources.Designer.cs b/src/Microsoft.AspNet.Mvc.ModelBinding/Properties/Resources.Designer.cs index a727041604..5ba29cced0 100644 --- a/src/Microsoft.AspNet.Mvc.ModelBinding/Properties/Resources.Designer.cs +++ b/src/Microsoft.AspNet.Mvc.ModelBinding/Properties/Resources.Designer.cs @@ -251,19 +251,19 @@ namespace Microsoft.AspNet.Mvc.ModelBinding } /// - /// The type '{0}' must derive from '{1}'. + /// The type '{0}' does not implement the interface '{1}'. /// - internal static string TypeMustDeriveFromType + internal static string PropertyBindingPredicateProvider_WrongType { - get { return GetString("TypeMustDeriveFromType"); } + get { return GetString("PropertyBindingPredicateProvider_WrongType"); } } /// - /// The type '{0}' must derive from '{1}'. + /// The type '{0}' does not implement the interface '{1}'. /// - 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); } /// diff --git a/src/Microsoft.AspNet.Mvc.ModelBinding/Resources.resx b/src/Microsoft.AspNet.Mvc.ModelBinding/Resources.resx index b90c721ed0..347d0fe052 100644 --- a/src/Microsoft.AspNet.Mvc.ModelBinding/Resources.resx +++ b/src/Microsoft.AspNet.Mvc.ModelBinding/Resources.resx @@ -162,8 +162,8 @@ The ModelMetadata property must be set before accessing this property. - - The type '{0}' must derive from '{1}'. + + The type '{0}' does not implement the interface '{1}'. The model object inside the metadata claimed to be compatible with '{0}', but was actually '{1}'. diff --git a/test/Microsoft.AspNet.Mvc.Core.Test/ParameterBinding/ControllerActionArgumentBinderTests.cs b/test/Microsoft.AspNet.Mvc.Core.Test/ParameterBinding/ControllerActionArgumentBinderTests.cs index 8c13fabdd0..44f17d7bd1 100644 --- a/test/Microsoft.AspNet.Mvc.Core.Test/ParameterBinding/ControllerActionArgumentBinderTests.cs +++ b/test/Microsoft.AspNet.Mvc.Core.Test/ParameterBinding/ControllerActionArgumentBinderTests.cs @@ -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()), - Mock.Of()); - - var metadataProvider = new DataAnnotationsModelMetadataProvider(); - var modelMetadata = metadataProvider.GetMetadataForType( - modelAccessor: null, modelType: typeof(TypeWithExcludedPropertiesUsingBindAttribute)); - - var actionBindingContext = new ActionBindingContext(actionContext, - Mock.Of(), - Mock.Of(), - Mock.Of(), - Mock.Of(), - Mock.Of()); - // Act - var context = DefaultControllerActionArgumentBinder.GetModelBindingContext( - modelMetadata, actionBindingContext, Mock.Of()); - - // 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()), @@ -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; } diff --git a/test/Microsoft.AspNet.Mvc.FunctionalTests/ModelBindingTests.cs b/test/Microsoft.AspNet.Mvc.FunctionalTests/ModelBindingTests.cs index a414aca54d..0fefaa5a9f 100644 --- a/test/Microsoft.AspNet.Mvc.FunctionalTests/ModelBindingTests.cs +++ b/test/Microsoft.AspNet.Mvc.FunctionalTests/ModelBindingTests.cs @@ -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(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(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¶m2.Id=123"); + + // Assert + var json = JsonConvert.DeserializeObject>(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¶m2.ExcludedExplicitlyAtTypeLevel=someValue"); + "UpdateUserId_BlackListingAtEitherLevelDoesNotBind" + + "?param1.LastName=someValue¶m2.Id=123"); // Assert var json = JsonConvert.DeserializeObject>(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¶m2.ExcludedExplicitlyAtTypeLevel=someValue"); + "UpdateFirstName_IncludingAtBothLevelBinds" + + "?param1.FirstName=someValue¶m2.Id=123"); // Assert var json = JsonConvert.DeserializeObject>(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¶m1.IncludedExplicitlyAtParameterLevel=someValue"); + "UpdateIsAdmin_IncludingAtOnlyOneLevelDoesNotBind" + + "?param1.FirstName=someValue¶m1.IsAdmin=true"); // Assert var json = JsonConvert.DeserializeObject>(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] diff --git a/test/Microsoft.AspNet.Mvc.ModelBinding.Test/BindAttributeTest.cs b/test/Microsoft.AspNet.Mvc.ModelBinding.Test/BindAttributeTest.cs index 674ef826eb..ef9b1204e2 100644 --- a/test/Microsoft.AspNet.Mvc.ModelBinding.Test/BindAttributeTest.cs +++ b/test/Microsoft.AspNet.Mvc.ModelBinding.Test/BindAttributeTest.cs @@ -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(() => 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(MockBehavior.Strict); + activator + .Setup(a => a.CreateInstance(It.IsAny(), typeof(TestProvider), It.IsAny())) + .Returns(new TestProvider()) + .Verifiable(); + + var services = new Mock(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(MockBehavior.Strict); + activator + .Setup(a => a.CreateInstance(It.IsAny(), typeof(TestProvider), It.IsAny())) + .Returns(new TestProvider()) + .Verifiable(); + + var services = new Mock(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(), typeof(TestProvider), It.IsAny()), + Times.Once()); + } +#endif + + private class TestProvider : IPropertyBindingPredicateProvider + { + public Func PropertyFilter + { + get + { + return (context, property) => string.Equals(property, "UserName", StringComparison.Ordinal); + } + } + } + + private class BaseProvider : IPropertyBindingPredicateProvider + { + public Func PropertyFilter + { + get + { + throw new NotImplementedException(); + } + } + } + + private class DerivedProvider : BaseProvider + { + } + + private class UnrelatedType + { } } } diff --git a/test/Microsoft.AspNet.Mvc.ModelBinding.Test/Binders/MutableObjectModelBinderTest.cs b/test/Microsoft.AspNet.Mvc.ModelBinding.Test/Binders/MutableObjectModelBinderTest.cs index 4c1e2a0d81..8ac8256f55 100644 --- a/test/Microsoft.AspNet.Mvc.ModelBinding.Test/Binders/MutableObjectModelBinderTest.cs +++ b/test/Microsoft.AspNet.Mvc.ModelBinding.Test/Binders/MutableObjectModelBinderTest.cs @@ -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(); @@ -247,12 +249,12 @@ namespace Microsoft.AspNet.Mvc.ModelBinding .Returns( 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(), 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(), + 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(), + 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 PropertyFilter + { + get + { + return (context, propertyName) => + !string.Equals("Excluded1", propertyName, StringComparison.OrdinalIgnoreCase) && + !string.Equals("Excluded2", propertyName, StringComparison.OrdinalIgnoreCase); + } + } + } + + private IServiceProvider CreateServices() + { + var services = new Mock(MockBehavior.Strict); + + var typeActivator = new Mock(MockBehavior.Strict); + typeActivator + .Setup(f => f.CreateInstance(It.IsAny(), 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) diff --git a/test/Microsoft.AspNet.Mvc.ModelBinding.Test/Metadata/CachedDataAnnotationsMetadataAttributesTest.cs b/test/Microsoft.AspNet.Mvc.ModelBinding.Test/Metadata/CachedDataAnnotationsMetadataAttributesTest.cs index 6671c1fe64..b26ac03f34 100644 --- a/test/Microsoft.AspNet.Mvc.ModelBinding.Test/Metadata/CachedDataAnnotationsMetadataAttributesTest.cs +++ b/test/Microsoft.AspNet.Mvc.ModelBinding.Test/Metadata/CachedDataAnnotationsMetadataAttributesTest.cs @@ -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> @@ -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 PropertyFilter + { + get + { + throw new NotImplementedException(); + } + } + } } } \ No newline at end of file diff --git a/test/Microsoft.AspNet.Mvc.ModelBinding.Test/Metadata/CachedDataAnnotationsModelMetadataProviderTest.cs b/test/Microsoft.AspNet.Mvc.ModelBinding.Test/Metadata/CachedDataAnnotationsModelMetadataProviderTest.cs index 259eee03b3..229271766c 100644 --- a/test/Microsoft.AspNet.Mvc.ModelBinding.Test/Metadata/CachedDataAnnotationsModelMetadataProviderTest.cs +++ b/test/Microsoft.AspNet.Mvc.ModelBinding.Test/Metadata/CachedDataAnnotationsModelMetadataProviderTest.cs @@ -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(); + foreach (var property in metadata.Properties) + { + if (predicate(context, property.PropertyName)) + { + matched.Add(property.PropertyName); + } + } + + Assert.Equal(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(); + foreach (var property in metadata.Properties) + { + if (predicate(context, property.PropertyName)) + { + matched.Add(property.PropertyName); + } + } + + Assert.Equal(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(); + foreach (var property in metadata.Properties) + { + if (predicate(context, property.PropertyName)) + { + matched.Add(property.PropertyName); + } + } + + Assert.Equal(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(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(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(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(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; } + } } } \ No newline at end of file diff --git a/test/Microsoft.AspNet.Mvc.ModelBinding.Test/Metadata/CachedDataAnnotationsModelMetadataTest.cs b/test/Microsoft.AspNet.Mvc.ModelBinding.Test/Metadata/CachedDataAnnotationsModelMetadataTest.cs index c48ef4936f..d7d71d730d 100644 --- a/test/Microsoft.AspNet.Mvc.ModelBinding.Test/Metadata/CachedDataAnnotationsModelMetadataTest.cs +++ b/test/Microsoft.AspNet.Mvc.ModelBinding.Test/Metadata/CachedDataAnnotationsModelMetadataTest.cs @@ -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> ExpectedAttributeDataStrings diff --git a/test/Microsoft.AspNet.Mvc.ModelBinding.Test/Metadata/ModelMetadataTest.cs b/test/Microsoft.AspNet.Mvc.ModelBinding.Test/Metadata/ModelMetadataTest.cs index 78a5ac233d..cbce75127a 100644 --- a/test/Microsoft.AspNet.Mvc.ModelBinding.Test/Metadata/ModelMetadataTest.cs +++ b/test/Microsoft.AspNet.Mvc.ModelBinding.Test/Metadata/ModelMetadataTest.cs @@ -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(); var nonEmptyPropertyList = new List() { "SomeProperty" }; return new TheoryData, Func, 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 PropertyFilter { get; set; } + } } } diff --git a/test/WebSites/ModelBindingWebSite/Controllers/BindAttributeController.cs b/test/WebSites/ModelBindingWebSite/Controllers/BindAttributeController.cs index 5399345f03..eed4d77ab5 100644 --- a/test/WebSites/ModelBindingWebSite/Controllers/BindAttributeController.cs +++ b/test/WebSites/ModelBindingWebSite/Controllers/BindAttributeController.cs @@ -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 - BindAtParamterLevelAndBindAtTypeLevelAreBothEvaluated_BlackListingAtEitherLevelDoesNotBind( - [Bind(Exclude = "IncludedExplicitlyAtTypeLevel")] TypeWithIncludedPropertyAtBindAttribute param1, - [Bind(Include = "ExcludedExplicitlyAtTypeLevel")] TypeWithExcludedPropertyAtBindAttribute param2) + public User EchoUser([Bind(typeof(ExcludeUserPropertiesAtParameter))] User user) { - return new Dictionary() - { - // 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 - BindAtParamterLevelAndBindAtTypeLevelAreBothEvaluated_WhiteListingAtBothLevelBinds( - [Bind(Include = "IncludedExplicitlyAtTypeLevel")] TypeWithIncludedPropertyAtBindAttribute param1) + UpdateUserId_BlackListingAtEitherLevelDoesNotBind( + [Bind(typeof(ExcludeLastName))] User2 param1, + [Bind("Id")] User2 param2) { return new Dictionary() { - // 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 - BindAtParamterLevelAndBindAtTypeLevelAreBothEvaluated_WhiteListingAtOnlyOneLevelDoesNotBind( - [Bind(Include = "IncludedExplicitlyAtParameterLevel")] - TypeWithIncludedPropertyAtParameterAndTypeUsingBindAttribute param1) + public Dictionary UpdateFirstName_IncludingAtBothLevelBinds( + [Bind("FirstName")] User2 param1) { return new Dictionary() { - // 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 UpdateIsAdmin_IncludingAtOnlyOneLevelDoesNotBind( + [Bind("IsAdmin" )] User2 param1) + { + return new Dictionary() + { + // 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 + { + public override string Prefix + { + get + { + return "user"; + } + } + + public override IEnumerable>> 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>> 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 PropertyFilter + { + get + { + return (context, propertyName) => + !string.Equals("LastName", propertyName, StringComparison.OrdinalIgnoreCase); + } + } } } \ No newline at end of file diff --git a/test/Microsoft.AspNet.Mvc.ModelBinding.Test/Metadata/TestPropertyBindingInfo.cs b/test/WebSites/ModelBindingWebSite/ITestService.cs similarity index 50% rename from test/Microsoft.AspNet.Mvc.ModelBinding.Test/Metadata/TestPropertyBindingInfo.cs rename to test/WebSites/ModelBindingWebSite/ITestService.cs index 29e6d2f4e7..8245aada13 100644 --- a/test/Microsoft.AspNet.Mvc.ModelBinding.Test/Metadata/TestPropertyBindingInfo.cs +++ b/test/WebSites/ModelBindingWebSite/ITestService.cs @@ -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(); } -} \ No newline at end of file +} diff --git a/test/WebSites/ModelBindingWebSite/Startup.cs b/test/WebSites/ModelBindingWebSite/Startup.cs index 0ea1b6182b..ead23b1274 100644 --- a/test/WebSites/ModelBindingWebSite/Startup.cs +++ b/test/WebSites/ModelBindingWebSite/Startup.cs @@ -27,6 +27,7 @@ namespace ModelBindingWebSite }); services.AddSingleton(); + services.AddSingleton(); }); // Add MVC to the request pipeline diff --git a/test/WebSites/ModelBindingWebSite/TestService.cs b/test/WebSites/ModelBindingWebSite/TestService.cs new file mode 100644 index 0000000000..c72c62c350 --- /dev/null +++ b/test/WebSites/ModelBindingWebSite/TestService.cs @@ -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; + } + } +}