Adding support for bind attribute.

This commit is contained in:
Harsh Gupta 2014-10-15 17:01:30 -07:00
parent c0d8ca8aed
commit 75405e3b76
16 changed files with 812 additions and 31 deletions

View File

@ -16,14 +16,11 @@ namespace Microsoft.AspNet.Mvc
/// </summary>
public class DefaultControllerActionArgumentBinder : IControllerActionArgumentBinder
{
private readonly IBodyModelValidator _modelValidator;
private readonly IActionBindingContextProvider _bindingContextProvider;
public DefaultControllerActionArgumentBinder(IActionBindingContextProvider bindingContextProvider,
IBodyModelValidator modelValidator)
public DefaultControllerActionArgumentBinder(IActionBindingContextProvider bindingContextProvider)
{
_bindingContextProvider = bindingContextProvider;
_modelValidator = modelValidator;
}
public async Task<IDictionary<string, object>> GetActionArgumentsAsync(ActionContext actionContext)
@ -49,8 +46,8 @@ namespace Microsoft.AspNet.Mvc
}
private async Task PopulateActionArgumentsAsync(IEnumerable<ModelMetadata> modelMetadatas,
ActionBindingContext actionBindingContext,
IDictionary<string, object> invocationInfo)
ActionBindingContext actionBindingContext,
IDictionary<string, object> invocationInfo)
{
var bodyBoundParameterCount = modelMetadatas.Count(
modelMetadata => modelMetadata.Marker is IBodyBinderMarker);
@ -61,19 +58,7 @@ namespace Microsoft.AspNet.Mvc
foreach (var modelMetadata in modelMetadatas)
{
var parameterType = modelMetadata.ModelType;
var modelBindingContext = new ModelBindingContext
{
ModelName = modelMetadata.PropertyName,
ModelMetadata = modelMetadata,
ModelState = actionBindingContext.ActionContext.ModelState,
ModelBinder = actionBindingContext.ModelBinder,
ValidatorProvider = actionBindingContext.ValidatorProvider,
MetadataProvider = actionBindingContext.MetadataProvider,
HttpContext = actionBindingContext.ActionContext.HttpContext,
FallbackToEmptyPrefix = true,
ValueProvider = actionBindingContext.ValueProvider,
};
var modelBindingContext = GetModelBindingContext(modelMetadata, actionBindingContext);
if (await actionBindingContext.ModelBinder.BindModelAsync(modelBindingContext))
{
@ -81,5 +66,25 @@ namespace Microsoft.AspNet.Mvc
}
}
}
internal static ModelBindingContext GetModelBindingContext(ModelMetadata modelMetadata, ActionBindingContext actionBindingContext)
{
var modelBindingContext = new ModelBindingContext
{
ModelName = modelMetadata.ModelName ?? modelMetadata.PropertyName,
ModelMetadata = modelMetadata,
ModelState = actionBindingContext.ActionContext.ModelState,
ModelBinder = actionBindingContext.ModelBinder,
ValidatorProvider = actionBindingContext.ValidatorProvider,
MetadataProvider = actionBindingContext.MetadataProvider,
HttpContext = actionBindingContext.ActionContext.HttpContext,
// Fallback only if there is no explicit model name set.
FallbackToEmptyPrefix = modelMetadata.ModelName == null,
ValueProvider = actionBindingContext.ValueProvider,
};
return modelBindingContext;
}
}
}

View File

@ -0,0 +1,41 @@
// 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
{
/// <summary>
/// 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, IModelPropertyBindingInfo
{
/// <summary>
/// Comma separated set of properties which are to be excluded during model binding.
/// </summary>
public string Exclude { get; set; } = string.Empty;
/// <summary>
/// Comma separated set of properties which are to be included during model binding.
/// </summary>
public string Include { get; set; } = string.Empty;
// This property is exposed for back compat reasons.
/// <summary>
/// Allows a user to specify a particular prefix to match during model binding.
/// </summary>
public string Prefix { get; set; }
/// <summary>
/// Represents the model name used during model binding.
/// </summary>
string IModelNameProvider.Name
{
get
{
return Prefix;
}
}
}
}

View File

@ -17,8 +17,27 @@ namespace Microsoft.AspNet.Mvc.ModelBinding
{
ModelBindingHelper.ValidateBindingContext(bindingContext);
if (!CanBindType(bindingContext.ModelType) ||
!await bindingContext.ValueProvider.ContainsPrefixAsync(bindingContext.ModelName))
if (!CanBindType(bindingContext.ModelType))
{
return false;
}
var topLevelObject = bindingContext.ModelMetadata.ContainerType == null;
var isThereAnExplicitAlias = bindingContext.ModelMetadata.ModelName != null;
// The first check is necessary because if we fallback to empty prefix, we do not want to depend
// on a value provider to provide a value for empty prefix.
var containsPrefix = (bindingContext.ModelName == string.Empty && topLevelObject) ||
await bindingContext.ValueProvider.ContainsPrefixAsync(bindingContext.ModelName);
// Always create the model if
// 1. It is a top level object and the model name is empty.
// 2. There is a value provider which can provide value for the model name.
// 3. There is an explicit alias provided by the user and it is a top level object.
// The reson we depend on explicit alias is that otherwise we want the FallToEmptyPrefix codepath
// to kick in so that empty prefix values could be bound.
if (!containsPrefix && !(isThereAnExplicitAlias && topLevelObject))
{
return false;
}
@ -149,6 +168,9 @@ namespace Microsoft.AspNet.Mvc.ModelBinding
var validationInfo = GetPropertyValidationInfo(bindingContext);
return bindingContext.ModelMetadata.Properties
.Where(propertyMetadata =>
IsPropertyAllowed(propertyMetadata.PropertyName,
bindingContext.ModelMetadata.IncludedProperties,
bindingContext.ModelMetadata.ExcludedProperties) &&
(validationInfo.RequiredProperties.Contains(propertyMetadata.PropertyName) ||
!validationInfo.SkipProperties.Contains(propertyMetadata.PropertyName)) &&
CanUpdateProperty(propertyMetadata));
@ -349,6 +371,21 @@ namespace Microsoft.AspNet.Mvc.ModelBinding
return addedError;
}
private static bool IsPropertyAllowed(string propertyName,
IReadOnlyList<string> includeProperties,
IReadOnlyList<string> excludeProperties)
{
// 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);
return includeProperty && !excludeProperty;
}
internal sealed class PropertyValidationInfo
{
public PropertyValidationInfo()

View File

@ -0,0 +1,16 @@
// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
namespace Microsoft.AspNet.Mvc
{
/// <summary>
/// Represents an entity which can provide model name as metadata.
/// </summary>
public interface IModelNameProvider
{
/// <summary>
/// Model name.
/// </summary>
string Name { get; }
}
}

View File

@ -0,0 +1,21 @@
// 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 IModelPropertyBindingInfo
{
/// <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

@ -85,7 +85,6 @@ namespace Microsoft.AspNet.Mvc.ModelBinding
// Override for applying the prototype + modelAccess to yield the final metadata
protected abstract TModelMetadata CreateMetadataFromPrototype(TModelMetadata prototype,
Func<object> modelAccessor);
private ModelMetadata GetMetadataForParameterCore(Func<object> modelAccessor,
string parameterName,
ParameterInfo parameter)
@ -94,10 +93,61 @@ namespace Microsoft.AspNet.Mvc.ModelBinding
CreateParameterInfo(parameter.ParameterType,
parameter.GetCustomAttributes(),
parameterName);
var typePrototype = GetTypeInformation(parameter.ParameterType).Prototype;
var typeInfo = GetTypeInformation(parameter.ParameterType);
UpdateMetadataWithTypeInfo(parameterInfo.Prototype, typeInfo);
return CreateMetadataFromPrototype(parameterInfo.Prototype, modelAccessor);
}
private void UpdateMetadataWithTypeInfo(ModelMetadata parameterPrototype, TypeInformation typeInfo)
{
// If both are empty
// Include everything.
// If none are empty
// Include common.
// If nothing common
// Dont include anything.
if (typeInfo.Prototype.IncludedProperties == null || typeInfo.Prototype.IncludedProperties.Count == 0)
{
if (parameterPrototype.IncludedProperties == null || parameterPrototype.IncludedProperties.Count == 0)
{
parameterPrototype.IncludedProperties = typeInfo.Properties
.Select(property => property.Key)
.ToList();
}
}
else
{
if (parameterPrototype.IncludedProperties == null || parameterPrototype.IncludedProperties.Count == 0)
{
parameterPrototype.IncludedProperties = typeInfo.Prototype.IncludedProperties;
}
else
{
parameterPrototype.IncludedProperties = parameterPrototype.IncludedProperties
.Intersect(typeInfo.Prototype.IncludedProperties,
StringComparer.OrdinalIgnoreCase).ToList();
}
}
if (typeInfo.Prototype.ExcludedProperties != null)
{
if (parameterPrototype.ExcludedProperties == null || parameterPrototype.ExcludedProperties.Count == 0)
{
parameterPrototype.ExcludedProperties = typeInfo.Prototype.ExcludedProperties;
}
else
{
parameterPrototype.ExcludedProperties = parameterPrototype.ExcludedProperties
.Union(typeInfo.Prototype.ExcludedProperties,
StringComparer.OrdinalIgnoreCase).ToList();
}
}
// Ignore the ModelName specified at Type level. (This is to be compatible with MVC).
}
private IEnumerable<ModelMetadata> GetMetadataForPropertiesCore(object container, Type containerType)
{
var typeInfo = GetTypeInformation(containerType);
@ -162,8 +212,21 @@ namespace Microsoft.AspNet.Mvc.ModelBinding
properties.Add(propertyHelper.Name, CreatePropertyInformation(type, propertyHelper));
}
}
info.Properties = properties;
if (info.Prototype != null)
{
// Update the included properties so that the properties are not ignored while binding.
if (info.Prototype.IncludedProperties == null ||
info.Prototype.IncludedProperties.Count == 0)
{
// Mark all properties as included.
info.Prototype.IncludedProperties =
info.Properties.Select(property => property.Key).ToList();
}
}
return info;
}

View File

@ -34,6 +34,12 @@ namespace Microsoft.AspNet.Mvc.ModelBinding
new CachedDataAnnotationsMetadataAttributes(attributes))
{
Marker = attributes.OfType<IBinderMarker>().FirstOrDefault();
var modelNameProvider = attributes.OfType<IModelNameProvider>().FirstOrDefault();
ModelName = modelNameProvider?.Name;
var bindAttribute = attributes.OfType<BindAttribute>().FirstOrDefault();
ReadSettingsFromBindAttribute(bindAttribute);
}
protected override bool ComputeConvertEmptyStringToNull()
@ -265,5 +271,29 @@ namespace Microsoft.AspNet.Mvc.ModelBinding
modelType.FullName, displayColumnAttribute.DisplayColumn));
}
}
private void ReadSettingsFromBindAttribute(BindAttribute bindAttribute)
{
if (bindAttribute == null)
{
return;
}
ExcludedProperties = SplitString(bindAttribute.Exclude).ToList();
IncludedProperties = SplitString(bindAttribute.Include).ToList();
}
private static IEnumerable<string> SplitString(string original)
{
if (string.IsNullOrEmpty(original))
{
return new string[0];
}
var split = original.Split(',')
.Select(piece => piece.Trim())
.Where(trimmed => !string.IsNullOrEmpty(trimmed));
return split;
}
}
}

View File

@ -56,6 +56,9 @@ namespace Microsoft.AspNet.Mvc.ModelBinding
CacheKey = prototype.CacheKey;
PrototypeCache = prototype.PrototypeCache;
Marker = prototype.Marker;
IncludedProperties = prototype.IncludedProperties;
ExcludedProperties = prototype.ExcludedProperties;
ModelName = prototype.ModelName;
_isComplexType = prototype.IsComplexType;
_isComplexTypeComputed = true;
}

View File

@ -43,11 +43,25 @@ namespace Microsoft.AspNet.Mvc.ModelBinding
_modelAccessor = modelAccessor;
_modelType = modelType;
_propertyName = propertyName;
_convertEmptyStringToNull = true;
_isRequired = !modelType.AllowsNullValue();
}
/// <summary>
/// Represents the name of a model if specified explicitly using <see cref="IModelNameProvider"/>.
/// </summary>
public string ModelName { get; set; }
/// <summary>
/// Properties which are marked as Included for this model.
/// </summary>
public IReadOnlyList<string> IncludedProperties { get; set; }
/// <summary>
/// Properties which are marked as Excluded for this model.
/// </summary>
public IReadOnlyList<string> ExcludedProperties { get; set; }
/// <summary>
/// Gets or sets a binder marker for this model.
/// </summary>

View File

@ -1402,8 +1402,7 @@ namespace Microsoft.AspNet.Mvc
actionDescriptor,
inputFormattersProvider.Object,
new DefaultControllerActionArgumentBinder(
actionBindingContextProvider.Object,
new DefaultBodyModelValidator()));
actionBindingContextProvider.Object));
// Act
await invoker.InvokeAsync();

View File

@ -14,6 +14,103 @@ namespace Microsoft.AspNet.Mvc.Core.Test
{
public class ControllerActionArgumentBinderTests
{
public class MySimpleModel
{
}
[Bind(Prefix = "TypePrefix")]
public class MySimpleModelWithTypeBasedBind
{
}
public void ParameterHasFieldPrefix([Bind(Prefix = "bar")] string foo)
{
}
public void ParameterHasEmptyFieldPrefix([Bind(Prefix = "")] MySimpleModel foo,
[Bind(Prefix = "")] MySimpleModelWithTypeBasedBind foo1)
{
}
public void ParameterHasPrefixAndComplexType([Bind(Prefix = "bar")] MySimpleModel foo,
[Bind(Prefix = "bar")] MySimpleModelWithTypeBasedBind foo1)
{
}
public void ParameterHasEmptyBindAttribute([Bind] MySimpleModel foo,
[Bind] MySimpleModelWithTypeBasedBind foo1)
{
}
[Theory]
[InlineData("ParameterHasFieldPrefix", false, "bar")]
[InlineData("ParameterHasEmptyFieldPrefix", false, "")]
[InlineData("ParameterHasPrefixAndComplexType", false, "bar")]
[InlineData("ParameterHasEmptyBindAttribute", true, "foo")]
public void GetModelBindingContext_ModelBindingContextIsSetWithModelName_ForParameters(
string actionMethodName, bool expectedFallToEmptyPrefix, string expectedModelName)
{
// Arrange
var type = typeof(ControllerActionArgumentBinderTests);
var methodInfo = type.GetMethod(actionMethodName);
var actionContext = new ActionContext(new RouteContext(Mock.Of<HttpContext>()),
Mock.Of<ActionDescriptor>());
var metadataProvider = new DataAnnotationsModelMetadataProvider();
var modelMetadata = metadataProvider.GetMetadataForParameter(modelAccessor: null,
methodInfo: methodInfo,
parameterName: "foo");
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);
// Assert
Assert.Equal(expectedFallToEmptyPrefix, context.FallbackToEmptyPrefix);
Assert.Equal(expectedModelName, context.ModelName);
}
[Theory]
[InlineData("ParameterHasEmptyFieldPrefix", false, "")]
[InlineData("ParameterHasPrefixAndComplexType", false, "bar")]
[InlineData("ParameterHasEmptyBindAttribute", true, "foo1")]
public void GetModelBindingContext_ModelBindingContextIsNotSet_ForTypes(
string actionMethodName, bool expectedFallToEmptyPrefix, string expectedModelName)
{
// Arrange
var type = typeof(ControllerActionArgumentBinderTests);
var methodInfo = type.GetMethod(actionMethodName);
var actionContext = new ActionContext(new RouteContext(Mock.Of<HttpContext>()),
Mock.Of<ActionDescriptor>());
var metadataProvider = new DataAnnotationsModelMetadataProvider();
var modelMetadata = metadataProvider.GetMetadataForParameter(modelAccessor: null,
methodInfo: methodInfo,
parameterName: "foo1");
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);
// Assert
Assert.Equal(expectedFallToEmptyPrefix, context.FallbackToEmptyPrefix);
Assert.Equal(expectedModelName, context.ModelName);
}
[Fact]
public async Task Parameters_WithMultipleFromBody_Throw()
{
@ -53,7 +150,7 @@ namespace Microsoft.AspNet.Mvc.Core.Test
.Returns(Task.FromResult(bindingContext));
var invoker = new DefaultControllerActionArgumentBinder(
actionBindingContextProvider.Object, Mock.Of<IBodyModelValidator>());
actionBindingContextProvider.Object);
// Act
var ex = await Assert.ThrowsAsync<InvalidOperationException>(
@ -101,7 +198,7 @@ namespace Microsoft.AspNet.Mvc.Core.Test
.Returns(Task.FromResult(bindingContext));
var invoker = new DefaultControllerActionArgumentBinder(
actionBindingContextProvider.Object, Mock.Of<IBodyModelValidator>());
actionBindingContextProvider.Object);
// Act
var result = await invoker.GetActionArgumentsAsync(actionContext);
@ -153,7 +250,7 @@ namespace Microsoft.AspNet.Mvc.Core.Test
.Returns(Task.FromResult(bindingContext));
var invoker = new DefaultControllerActionArgumentBinder(
actionBindingContextProvider.Object, Mock.Of<IBodyModelValidator>());
actionBindingContextProvider.Object);
// Act
var result = await invoker.GetActionArgumentsAsync(actionContext);

View File

@ -235,5 +235,126 @@ namespace Microsoft.AspNet.Mvc.FunctionalTests
Assert.Equal("The Field2 field is required.", json["model.Field2"]);
Assert.Equal("The Field3 field is required.", json["model.Field3"]);
}
[Fact]
public async Task BindAttribute_AppliesAtBothParameterAndTypeLevelTogether_BlacklistedAtEitherLevelIsNotBound()
{
// Arrange
var server = TestServer.Create(_services, _app);
var client = server.CreateClient();
// Act
var response = await client.GetStringAsync("http://localhost/BindAttribute/" +
"BindAtParamterLevelAndBindAtTypeLevelAreBothEvaluated_BlackListingAtEitherLevelDoesNotBind" +
"?param1.IncludedExplicitlyAtTypeLevel=someValue&param2.ExcludedExplicitlyAtTypeLevel=someValue");
// Assert
var json = JsonConvert.DeserializeObject<Dictionary<string, string>>(response);
Assert.Equal(2, json.Count);
Assert.Null(json["param1.IncludedExplicitlyAtTypeLevel"]);
Assert.Null(json["param2.ExcludedExplicitlyAtTypeLevel"]);
}
[Fact]
public async Task BindAttribute_AppliesAtBothParameterAndTypeLevelTogether_WhitelistedAtBothLevelsIsBound()
{
// Arrange
var server = TestServer.Create(_services, _app);
var client = server.CreateClient();
// Act
var response = await client.GetStringAsync("http://localhost/BindAttribute/" +
"BindAtParamterLevelAndBindAtTypeLevelAreBothEvaluated_WhiteListingAtBothLevelBinds" +
"?param1.IncludedExplicitlyAtTypeLevel=someValue&param2.ExcludedExplicitlyAtTypeLevel=someValue");
// Assert
var json = JsonConvert.DeserializeObject<Dictionary<string, string>>(response);
Assert.Equal(1, json.Count);
Assert.Equal("someValue", json["param1.IncludedExplicitlyAtTypeLevel"]);
}
[Fact]
public async Task BindAttribute_AppliesAtBothParameterAndTypeLevelTogether_WhitelistingAtOneLevelIsNotBound()
{
// Arrange
var server = TestServer.Create(_services, _app);
var client = server.CreateClient();
// Act
var response = await client.GetStringAsync("http://localhost/BindAttribute/" +
"BindAtParamterLevelAndBindAtTypeLevelAreBothEvaluated_WhiteListingAtOnlyOneLevelDoesNotBind" +
"?param1.IncludedExplicitlyAtTypeLevel=someValue&param1.IncludedExplicitlyAtParameterLevel=someValue");
// Assert
var json = JsonConvert.DeserializeObject<Dictionary<string, string>>(response);
Assert.Equal(2, json.Count);
Assert.Null(json["param1.IncludedExplicitlyAtParameterLevel"]);
Assert.Null(json["param1.IncludedExplicitlyAtTypeLevel"]);
}
[Fact]
public async Task BindAttribute_BindsUsingParameterPrefix()
{
// Arrange
var server = TestServer.Create(_services, _app);
var client = server.CreateClient();
// Act
var response = await client.GetStringAsync("http://localhost/BindAttribute/" +
"BindParameterUsingParameterPrefix" +
"?randomPrefix.Value=someValue");
// Assert
Assert.Equal("someValue", response);
}
[Fact]
public async Task BindAttribute_DoesNotUseTypePrefix()
{
// Arrange
var server = TestServer.Create(_services, _app);
var client = server.CreateClient();
// Act
var response = await client.GetStringAsync("http://localhost/BindAttribute/" +
"TypePrefixIsNeverUsed" +
"?param.Value=someValue");
// Assert
Assert.Equal("someValue", response);
}
[Fact]
public async Task BindAttribute_FallsBackOnEmptyPrefixIfNoParameterPrefixIsProvided()
{
// Arrange
var server = TestServer.Create(_services, _app);
var client = server.CreateClient();
// Act
var response = await client.GetStringAsync("http://localhost/BindAttribute/" +
"TypePrefixIsNeverUsed" +
"?Value=someValue");
// Assert
Assert.Equal("someValue", response);
}
[Fact]
public async Task BindAttribute_DoesNotFallBackOnEmptyPrefixIfParameterPrefixIsProvided()
{
// Arrange
var server = TestServer.Create(_services, _app);
var client = server.CreateClient();
// Act
var response = await client.GetAsync("http://localhost/BindAttribute/" +
"BindParameterUsingParameterPrefix" +
"?Value=someValue");
// Assert
Assert.Equal(HttpStatusCode.NoContent, response.StatusCode);
Assert.Equal(string.Empty, await response.Content.ReadAsStringAsync());
}
}
}

View File

@ -0,0 +1,20 @@
// 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 Xunit;
namespace Microsoft.AspNet.Mvc.ModelBinding.Test
{
public class BindAttributeTest
{
[Fact]
public void PrefixPropertyDefaultsToNull()
{
// Arrange
BindAttribute attr = new BindAttribute();
// Act & assert
Assert.Null(attr.Prefix);
}
}
}

View File

@ -7,6 +7,7 @@ using System.Collections.Generic;
using System.ComponentModel;
using System.ComponentModel.DataAnnotations;
using System.Linq;
using System.Reflection;
using System.Threading.Tasks;
using Microsoft.AspNet.Testing;
using Moq;
@ -58,6 +59,48 @@ namespace Microsoft.AspNet.Mvc.ModelBinding
testableBinder.Verify();
}
[Fact]
public async Task BindModel_InitsInstance_ForEmptyModelName()
{
// Arrange
var mockValueProvider = new Mock<IValueProvider>();
mockValueProvider.Setup(o => o.ContainsPrefixAsync(It.IsAny<string>()))
.Returns(Task.FromResult(false));
var mockDtoBinder = new Mock<IModelBinder>();
var bindingContext = new ModelBindingContext
{
ModelMetadata = GetMetadataForObject(new Person()),
ModelName = "",
ValueProvider = mockValueProvider.Object,
ModelBinder = mockDtoBinder.Object,
MetadataProvider = new DataAnnotationsModelMetadataProvider(),
ValidatorProvider = Mock.Of<IModelValidatorProvider>()
};
mockDtoBinder
.Setup(o => o.BindModelAsync(It.IsAny<ModelBindingContext>()))
.Returns((ModelBindingContext mbc) =>
{
// just return the DTO unchanged
return Task.FromResult(true);
});
var testableBinder = new Mock<TestableMutableObjectModelBinder> { CallBase = true };
testableBinder.Setup(o => o.EnsureModelPublic(bindingContext)).Verifiable();
testableBinder.Setup(o => o.GetMetadataForPropertiesPublic(bindingContext))
.Returns(new ModelMetadata[0]).Verifiable();
// Act
var retValue = await testableBinder.Object.BindModelAsync(bindingContext);
// Assert
Assert.True(retValue);
Assert.IsType<Person>(bindingContext.Model);
Assert.True(bindingContext.ValidationNode.ValidateAllProperties);
testableBinder.Verify();
}
[Fact]
public void CanUpdateProperty_HasPublicSetter_ReturnsTrue()
{
@ -194,7 +237,8 @@ namespace Microsoft.AspNet.Mvc.ModelBinding
var bindingContext = new ModelBindingContext
{
ModelMetadata = GetMetadataForType(typeof(PersonWithBindExclusion)),
ValidatorProvider = Mock.Of<IModelValidatorProvider>()
ValidatorProvider = Mock.Of<IModelValidatorProvider>(),
MetadataProvider = new DataAnnotationsModelMetadataProvider()
};
var testableBinder = new TestableMutableObjectModelBinder();
@ -215,7 +259,75 @@ namespace Microsoft.AspNet.Mvc.ModelBinding
var bindingContext = new ModelBindingContext
{
ModelMetadata = GetMetadataForType(typeof(Person)),
ValidatorProvider = Mock.Of<IModelValidatorProvider>()
ValidatorProvider = Mock.Of<IModelValidatorProvider>(),
MetadataProvider = new DataAnnotationsModelMetadataProvider()
};
var testableBinder = new TestableMutableObjectModelBinder();
// Act
var propertyMetadatas = testableBinder.GetMetadataForPropertiesPublic(bindingContext);
var returnedPropertyNames = propertyMetadatas.Select(o => o.PropertyName).ToArray();
// Assert
Assert.Equal(expectedPropertyNames, returnedPropertyNames);
}
[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; }
}
[Fact]
public void GetMetadataForProperties_DoesNotReturn_ExcludedProperties()
{
// Arrange
var expectedPropertyNames = new[] { "IncludedByDefault1", "IncludedByDefault2" };
var bindingContext = new ModelBindingContext
{
ModelMetadata = GetMetadataForType(typeof(TypeWithExcludedPropertiesUsingBindAttribute)),
ValidatorProvider = Mock.Of<IModelValidatorProvider>(),
MetadataProvider = new DataAnnotationsModelMetadataProvider()
};
var testableBinder = new TestableMutableObjectModelBinder();
// Act
var propertyMetadatas = testableBinder.GetMetadataForPropertiesPublic(bindingContext);
var returnedPropertyNames = propertyMetadatas.Select(o => o.PropertyName).ToArray();
// Assert
Assert.Equal(expectedPropertyNames, returnedPropertyNames);
}
[Bind(Include = 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; }
}
[Fact]
public void GetMetadataForProperties_ReturnsOnlyWhiteListedProperties_UsingBindAttributeInclude()
{
// Arrange
var expectedPropertyNames = new[] { "IncludedExplicitly1", "IncludedExplicitly2" };
var bindingContext = new ModelBindingContext
{
ModelMetadata = GetMetadataForType(typeof(TypeWithIncludedPropertiesUsingBindAttribute)),
ValidatorProvider = Mock.Of<IModelValidatorProvider>(),
MetadataProvider = new DataAnnotationsModelMetadataProvider()
};
var testableBinder = new TestableMutableObjectModelBinder();
@ -812,6 +924,12 @@ namespace Microsoft.AspNet.Mvc.ModelBinding
return metadataProvider.GetMetadataForType(null, t);
}
private static ModelMetadata GetMetadataForParameter(MethodInfo methodInfo, string parameterName)
{
var metadataProvider = new DataAnnotationsModelMetadataProvider();
return metadataProvider.GetMetadataForParameter(null, methodInfo, parameterName);
}
private class Person
{
private DateTime? _dateOfDeath;

View File

@ -3,12 +3,117 @@
using System.ComponentModel.DataAnnotations;
using System.Linq;
using System.Reflection;
using Xunit;
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()
{
// Arrange
var type = typeof(TypeWithExludedAndIncludedPropertiesUsingBindAttribute);
var provider = new DataAnnotationsModelMetadataProvider();
var expectedIncludedPropertyNames = new[] { "IncludedAndExcludedExplicitly1", "IncludedExplicitly1" };
var expectedExcludedPropertyNames = new[] { "IncludedAndExcludedExplicitly1", "ExcludedExplicitly1" };
// Act
var metadata = provider.GetMetadataForType(null, type);
// Assert
Assert.Equal(expectedIncludedPropertyNames.ToList(), metadata.IncludedProperties);
Assert.Equal(expectedExcludedPropertyNames.ToList(), metadata.ExcludedProperties);
}
[Fact]
public void ModelMetadataProvider_ReadsIncludedAndExcludedProperties_BothAtParameterAndTypeLevel_ForParameters()
{
// Arrange
var type = typeof(TypeWithExludedAndIncludedPropertiesUsingBindAttribute);
var methodInfo = type.GetMethod("ActionWithBindAttribute");
var provider = new DataAnnotationsModelMetadataProvider();
// Note it does an intersection for included and a union for excluded.
var expectedIncludedPropertyNames = new[] { "IncludedAndExcludedExplicitly1" };
var expectedExcludedPropertyNames = new[] {
"Property3", "Property4", "IncludedAndExcludedExplicitly1", "ExcludedExplicitly1" };
// Act
var metadata = provider.GetMetadataForParameter(null, methodInfo, "param");
// Assert
Assert.Equal(expectedIncludedPropertyNames.ToList(), metadata.IncludedProperties);
Assert.Equal(expectedExcludedPropertyNames.ToList(), metadata.ExcludedProperties);
}
[Fact]
public void ModelMetadataProvider_ReadsPrefixProperty_OnlyAtParameterLevel_ForParameters()
{
// Arrange
var type = typeof(TypeWithExludedAndIncludedPropertiesUsingBindAttribute);
var methodInfo = type.GetMethod("ActionWithBindAttribute");
var provider = new DataAnnotationsModelMetadataProvider();
// Act
var metadata = provider.GetMetadataForParameter(null, methodInfo, "param");
// Assert
Assert.Equal("ParameterPrefix", metadata.ModelName);
}
[Fact]
public void DataAnnotationsModelMetadataProvider_ReadsModelNameProperty_ForTypes()
{
// Arrange
var type = typeof(TypeWithExludedAndIncludedPropertiesUsingBindAttribute);
var provider = new DataAnnotationsModelMetadataProvider();
// Act
var metadata = provider.GetMetadataForType(null, type);
// Assert
Assert.Equal("TypePrefix", metadata.ModelName);
}
[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(null, methodInfo, "param");
// Assert
Assert.Equal("ParameterPrefix", metadata.ModelName);
}
[Fact]
public void DataAnnotationsModelMetadataProvider_ReadsScaffoldColumnAttribute_ForShowForDisplay()
{

View File

@ -0,0 +1,91 @@
// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System.Collections.Generic;
using Microsoft.AspNet.Mvc;
namespace ModelBindingWebSite.Controllers
{
public class BindAttributeController : Controller
{
public Dictionary<string, string>
BindAtParamterLevelAndBindAtTypeLevelAreBothEvaluated_BlackListingAtEitherLevelDoesNotBind(
[Bind(Exclude = "IncludedExplicitlyAtTypeLevel")] TypeWithIncludedPropertyAtBindAttribute param1,
[Bind(Include = "ExcludedExplicitlyAtTypeLevel")] TypeWithExcludedPropertyAtBindAttribute param2)
{
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 },
// The second one should not be included because the type level bind attribute filters it out.
{ "param2.ExcludedExplicitlyAtTypeLevel", param2.ExcludedExplicitlyAtTypeLevel },
};
}
public Dictionary<string, string>
BindAtParamterLevelAndBindAtTypeLevelAreBothEvaluated_WhiteListingAtBothLevelBinds(
[Bind(Include = "IncludedExplicitlyAtTypeLevel")] TypeWithIncludedPropertyAtBindAttribute param1)
{
return new Dictionary<string, string>()
{
// The since this is included at both level it is bound.
{ "param1.IncludedExplicitlyAtTypeLevel", param1.IncludedExplicitlyAtTypeLevel },
};
}
public Dictionary<string, string>
BindAtParamterLevelAndBindAtTypeLevelAreBothEvaluated_WhiteListingAtOnlyOneLevelDoesNotBind(
[Bind(Include = "IncludedExplicitlyAtParameterLevel")]
TypeWithIncludedPropertyAtParameterAndTypeUsingBindAttribute 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 },
};
}
public string BindParameterUsingParameterPrefix([Bind(Prefix = "randomPrefix")] ParameterPrefix param)
{
return param.Value;
}
// This will use param to try to bind and not the value specified at TypePrefix.
public string TypePrefixIsNeverUsed([Bind] TypePrefix param)
{
return param.Value;
}
}
[Bind(Prefix = "TypePrefix")]
public class TypePrefix
{
public string Value { get; set; }
}
public class ParameterPrefix
{
public string Value { get; set; }
}
[Bind(Include = nameof(IncludedExplicitlyAtTypeLevel))]
public class TypeWithIncludedPropertyAtParameterAndTypeUsingBindAttribute
{
public string IncludedExplicitlyAtTypeLevel { get; set; }
public string IncludedExplicitlyAtParameterLevel { get; set; }
}
[Bind(Include = nameof(IncludedExplicitlyAtTypeLevel))]
public class TypeWithIncludedPropertyAtBindAttribute
{
public string IncludedExplicitlyAtTypeLevel { get; set; }
}
[Bind(Exclude = nameof(ExcludedExplicitlyAtTypeLevel))]
public class TypeWithExcludedPropertyAtBindAttribute
{
public string ExcludedExplicitlyAtTypeLevel { get; set; }
}
}