Making Pages Binding Consistent

This changeset reckonciles the binding work we did for pages with
controllers.

A quick summary:
- Moves [BindProperty] to the MVC namespace (#6401)
- Makes [FromRoute] and friends behave consistently (#6402)
- Makes [BindProperty] work with controllers (untracked)
This commit is contained in:
Ryan Nowak 2017-06-25 18:42:01 -07:00
parent cf09af52eb
commit 0ad9c7d4eb
12 changed files with 346 additions and 28 deletions

View File

@ -34,6 +34,7 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding
BinderModelName = other.BinderModelName; BinderModelName = other.BinderModelName;
BinderType = other.BinderType; BinderType = other.BinderType;
PropertyFilterProvider = other.PropertyFilterProvider; PropertyFilterProvider = other.PropertyFilterProvider;
RequestPredicate = other.RequestPredicate;
} }
/// <summary> /// <summary>
@ -56,6 +57,12 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding
/// </summary> /// </summary>
public IPropertyFilterProvider PropertyFilterProvider { get; set; } public IPropertyFilterProvider PropertyFilterProvider { get; set; }
/// <summary>
/// Gets or sets a predicate which determines whether or not the model should be bound based on state
/// from the current request.
/// </summary>
public Func<ActionContext, bool> RequestPredicate { get; set; }
/// <summary> /// <summary>
/// Constructs a new instance of <see cref="BindingInfo"/> from the given <paramref name="attributes"/>. /// Constructs a new instance of <see cref="BindingInfo"/> from the given <paramref name="attributes"/>.
/// </summary> /// </summary>
@ -113,6 +120,17 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding
bindingInfo.PropertyFilterProvider = new CompositePropertyFilterProvider(propertyFilterProviders); bindingInfo.PropertyFilterProvider = new CompositePropertyFilterProvider(propertyFilterProviders);
} }
// RequestPredicate
foreach (var requestPredicateProvider in attributes.OfType<IRequestPredicateProvider>())
{
isBindingInfoPresent = true;
if (requestPredicateProvider.RequestPredicate != null)
{
bindingInfo.RequestPredicate = requestPredicateProvider.RequestPredicate;
break;
}
}
return isBindingInfoPresent ? bindingInfo : null; return isBindingInfoPresent ? bindingInfo : null;
} }

View File

@ -0,0 +1,20 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System;
namespace Microsoft.AspNetCore.Mvc.ModelBinding
{
/// <summary>
/// An interface that allows a top-level model to be bound or not bound based on state associated
/// with the current request.
/// </summary>
public interface IRequestPredicateProvider
{
/// <summary>
/// Gets a function which determines whether or not the model object should be bound based
/// on the current request.
/// </summary>
Func<ActionContext, bool> RequestPredicate { get; }
}
}

View File

@ -4,11 +4,15 @@
using System; using System;
using Microsoft.AspNetCore.Mvc.ModelBinding; using Microsoft.AspNetCore.Mvc.ModelBinding;
namespace Microsoft.AspNetCore.Mvc.RazorPages namespace Microsoft.AspNetCore.Mvc
{ {
[AttributeUsage(AttributeTargets.Property | AttributeTargets.Class, AllowMultiple = false, Inherited = true)] [AttributeUsage(AttributeTargets.Property | AttributeTargets.Class, AllowMultiple = false, Inherited = true)]
public class BindPropertyAttribute : Attribute, IModelNameProvider, IBinderTypeProviderMetadata public class BindPropertyAttribute : Attribute, IModelNameProvider, IBinderTypeProviderMetadata, IRequestPredicateProvider
{ {
private static readonly Func<ActionContext, bool> _supportsAllRequests = (c) => true;
private static readonly Func<ActionContext, bool> _supportsNonGetRequests = IsNonGetRequest;
private BindingSource _bindingSource; private BindingSource _bindingSource;
public bool SupportsGet { get; set; } public bool SupportsGet { get; set; }
@ -35,5 +39,18 @@ namespace Microsoft.AspNetCore.Mvc.RazorPages
/// <inheritdoc /> /// <inheritdoc />
public string Name { get; set; } public string Name { get; set; }
Func<ActionContext, bool> IRequestPredicateProvider.RequestPredicate
{
get
{
return SupportsGet ? _supportsAllRequests : _supportsNonGetRequests;
}
}
private static bool IsNonGetRequest(ActionContext context)
{
return !string.Equals(context.HttpContext.Request.Method, "GET", StringComparison.OrdinalIgnoreCase);
}
} }
} }

View File

@ -152,6 +152,11 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding
throw new ArgumentNullException(nameof(metadata)); throw new ArgumentNullException(nameof(metadata));
} }
if (parameter.BindingInfo?.RequestPredicate?.Invoke(actionContext) == false)
{
return ModelBindingResult.Failed();
}
var modelBindingContext = DefaultModelBindingContext.CreateBindingContext( var modelBindingContext = DefaultModelBindingContext.CreateBindingContext(
actionContext, actionContext,
valueProvider, valueProvider,

View File

@ -9,7 +9,5 @@ namespace Microsoft.AspNetCore.Mvc.RazorPages.Infrastructure
public class PageBoundPropertyDescriptor : ParameterDescriptor public class PageBoundPropertyDescriptor : ParameterDescriptor
{ {
public PropertyInfo Property { get; set; } public PropertyInfo Property { get; set; }
public bool SupportsGet { get; set; }
} }
} }

View File

@ -248,26 +248,31 @@ namespace Microsoft.AspNetCore.Mvc.RazorPages.Internal
var property = properties[i]; var property = properties[i];
var bindingInfo = BindingInfo.GetBindingInfo(property.Property.GetCustomAttributes()); var bindingInfo = BindingInfo.GetBindingInfo(property.Property.GetCustomAttributes());
// If there's no binding info then that means there are no model binding attributes on the
// property. So we won't bind this property unless there's a [BindProperty] on the type.
if (bindingInfo == null && bindPropertyOnType == null) if (bindingInfo == null && bindPropertyOnType == null)
{ {
continue; continue;
} }
// If this property is declared as part of a pages base class, then it's likely infrastructure,
// so skip it.
if (property.Property.DeclaringType.GetTypeInfo().IsDefined(typeof(PagesBaseClassAttribute))) if (property.Property.DeclaringType.GetTypeInfo().IsDefined(typeof(PagesBaseClassAttribute)))
{ {
continue; continue;
} }
var bindPropertyOnProperty = property.Property.GetCustomAttribute<BindPropertyAttribute>(); bindingInfo = bindingInfo ?? new BindingInfo();
var supportsGet = bindPropertyOnProperty?.SupportsGet ?? bindPropertyOnType?.SupportsGet ?? false;
// Allow a predicate on the class to cascade if it wasn't set on the property.
bindingInfo.RequestPredicate = bindingInfo.RequestPredicate ?? ((IRequestPredicateProvider)bindPropertyOnType)?.RequestPredicate;
var descriptor = new PageBoundPropertyDescriptor() var descriptor = new PageBoundPropertyDescriptor()
{ {
BindingInfo = bindingInfo ?? new BindingInfo(), BindingInfo = bindingInfo,
Name = property.Name, Name = property.Name,
Property = property.Property, Property = property.Property,
ParameterType = property.Property.PropertyType, ParameterType = property.Property.PropertyType,
SupportsGet = supportsGet,
}; };
results.Add(descriptor); results.Add(descriptor);

View File

@ -7,7 +7,6 @@ using System.Threading.Tasks;
using Microsoft.AspNetCore.Mvc.Abstractions; using Microsoft.AspNetCore.Mvc.Abstractions;
using Microsoft.AspNetCore.Mvc.Internal; using Microsoft.AspNetCore.Mvc.Internal;
using Microsoft.AspNetCore.Mvc.ModelBinding; using Microsoft.AspNetCore.Mvc.ModelBinding;
using Microsoft.AspNetCore.Mvc.RazorPages.Infrastructure;
namespace Microsoft.AspNetCore.Mvc.RazorPages.Internal namespace Microsoft.AspNetCore.Mvc.RazorPages.Internal
{ {
@ -56,16 +55,9 @@ namespace Microsoft.AspNetCore.Mvc.RazorPages.Internal
IList<ParameterDescriptor> properties, IList<ParameterDescriptor> properties,
IList<ModelMetadata> metadata) IList<ModelMetadata> metadata)
{ {
var isGet = string.Equals("GET", pageContext.HttpContext.Request.Method, StringComparison.OrdinalIgnoreCase);
var valueProvider = await CompositeValueProvider.CreateAsync(pageContext, pageContext.ValueProviderFactories); var valueProvider = await CompositeValueProvider.CreateAsync(pageContext, pageContext.ValueProviderFactories);
for (var i = 0; i < properties.Count; i++) for (var i = 0; i < properties.Count; i++)
{ {
if (isGet && !((PageBoundPropertyDescriptor)properties[i]).SupportsGet)
{
continue;
}
var result = await parameterBinder.BindModelAsync(pageContext, valueProvider, properties[i]); var result = await parameterBinder.BindModelAsync(pageContext, valueProvider, properties[i]);
if (result.IsModelSet) if (result.IsModelSet)
{ {

View File

@ -13,6 +13,7 @@ using Microsoft.AspNetCore.Mvc.Formatters;
using Microsoft.AspNetCore.Mvc.ModelBinding; using Microsoft.AspNetCore.Mvc.ModelBinding;
using Microsoft.AspNetCore.Mvc.ModelBinding.Binders; using Microsoft.AspNetCore.Mvc.ModelBinding.Binders;
using Microsoft.AspNetCore.Mvc.ModelBinding.Validation; using Microsoft.AspNetCore.Mvc.ModelBinding.Validation;
using Microsoft.AspNetCore.Mvc.RazorPages;
using Microsoft.AspNetCore.Routing; using Microsoft.AspNetCore.Routing;
using Moq; using Moq;
using Xunit; using Xunit;
@ -490,6 +491,125 @@ namespace Microsoft.AspNetCore.Mvc.Internal
Assert.Null(controller.NullableProperty); Assert.Null(controller.NullableProperty);
} }
[Fact]
public async Task BindActionArgumentsAsync_SupportsRequestPredicate_ForPropertiesAndParameters_NotBound()
{
// Arrange
var actionDescriptor = GetActionDescriptor();
actionDescriptor.Parameters.Add(new ParameterDescriptor
{
Name = "test-parameter",
BindingInfo = new BindingInfo()
{
BindingSource = BindingSource.Custom,
// Simulates [BindProperty] on a parameter
RequestPredicate = ((IRequestPredicateProvider)new BindPropertyAttribute()).RequestPredicate,
},
ParameterType = typeof(string)
});
actionDescriptor.BoundProperties.Add(new ParameterDescriptor
{
Name = nameof(TestController.NullableProperty),
BindingInfo = new BindingInfo()
{
BindingSource = BindingSource.Custom,
// Simulates [BindProperty] on a property
RequestPredicate = ((IRequestPredicateProvider)new BindPropertyAttribute()).RequestPredicate,
},
ParameterType = typeof(string)
});
var controllerContext = GetControllerContext(actionDescriptor);
controllerContext.HttpContext.Request.Method = "GET";
var controller = new TestController();
var arguments = new Dictionary<string, object>(StringComparer.Ordinal);
var binder = new StubModelBinder(ModelBindingResult.Success(model: null));
var factory = GetModelBinderFactory(binder);
var parameterBinder = GetParameterBinder(factory);
// Some non default value.
controller.NullableProperty = -1;
// Act
var binderDelegate = ControllerBinderDelegateProvider.CreateBinderDelegate(
parameterBinder,
factory,
TestModelMetadataProvider.CreateDefaultProvider(),
actionDescriptor);
await binderDelegate(controllerContext, controller, arguments);
// Assert
Assert.Equal(-1, controller.NullableProperty);
Assert.DoesNotContain("test-parameter", arguments.Keys);
}
[Fact]
public async Task BindActionArgumentsAsync_SupportsRequestPredicate_ForPropertiesAndParameters_Bound()
{
// Arrange
var actionDescriptor = GetActionDescriptor();
actionDescriptor.Parameters.Add(new ParameterDescriptor
{
Name = "test-parameter",
BindingInfo = new BindingInfo()
{
BindingSource = BindingSource.Custom,
// Simulates [BindProperty] on a parameter
RequestPredicate = ((IRequestPredicateProvider)new BindPropertyAttribute()).RequestPredicate,
},
ParameterType = typeof(string)
});
actionDescriptor.BoundProperties.Add(new ParameterDescriptor
{
Name = nameof(TestController.NullableProperty),
BindingInfo = new BindingInfo()
{
BindingSource = BindingSource.Custom,
// Simulates [BindProperty] on a property
RequestPredicate = ((IRequestPredicateProvider)new BindPropertyAttribute()).RequestPredicate,
},
ParameterType = typeof(string)
});
var controllerContext = GetControllerContext(actionDescriptor);
controllerContext.HttpContext.Request.Method = "POST";
var controller = new TestController();
var arguments = new Dictionary<string, object>(StringComparer.Ordinal);
var binder = new StubModelBinder(ModelBindingResult.Success(model: null));
var factory = GetModelBinderFactory(binder);
var parameterBinder = GetParameterBinder(factory);
// Some non default value.
controller.NullableProperty = -1;
// Act
var binderDelegate = ControllerBinderDelegateProvider.CreateBinderDelegate(
parameterBinder,
factory,
TestModelMetadataProvider.CreateDefaultProvider(),
actionDescriptor);
await binderDelegate(controllerContext, controller, arguments);
// Assert
Assert.Null(controller.NullableProperty);
Assert.Contains("test-parameter", arguments.Keys);
Assert.Null(arguments["test-parameter"]);
}
// property name, property type, property accessor, input value, expected value // property name, property type, property accessor, input value, expected value
public static TheoryData<string, Type, Func<object, object>, object, object> SkippedPropertyData public static TheoryData<string, Type, Func<object, object>, object, object> SkippedPropertyData
{ {

View File

@ -728,7 +728,7 @@ namespace Microsoft.AspNetCore.Mvc.FunctionalTests
public async Task PagePropertiesAreNotBoundInGetRequests() public async Task PagePropertiesAreNotBoundInGetRequests()
{ {
// Arrange // Arrange
var expected = "Id = 11, Name = , Age ="; var expected = "Id = 11, Name = Test-Name, Age =";
var validationError = "The Name field is required."; var validationError = "The Name field is required.";
var request = new HttpRequestMessage(HttpMethod.Get, "Pages/PropertyBinding/PageWithPropertyAndArgumentBinding?id=11") var request = new HttpRequestMessage(HttpMethod.Get, "Pages/PropertyBinding/PageWithPropertyAndArgumentBinding?id=11")
{ {

View File

@ -0,0 +1,101 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System.Collections.Generic;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc.Abstractions;
using Microsoft.AspNetCore.Mvc.ModelBinding;
using Microsoft.Extensions.Primitives;
using Xunit;
namespace Microsoft.AspNetCore.Mvc.IntegrationTests
{
// Integration tests for binding top level models with [BindProperty]
public class BindPropertyIntegrationTest
{
private class Person
{
public string Name { get; set; }
}
[Fact]
public async Task BindModelAsync_WithBindProperty_BindsModel_WhenRequestIsNotGet()
{
// Arrange
var parameterBinder = ModelBindingTestHelper.GetParameterBinder();
var parameter = new ParameterDescriptor()
{
Name = "parameter",
ParameterType = typeof(Person),
BindingInfo = BindingInfo.GetBindingInfo(new[] { new BindPropertyAttribute() }),
};
var testContext = ModelBindingTestHelper.GetTestContext(request =>
{
request.Method = "POST";
request.QueryString = new QueryString("?parameter.Name=Joey");
});
// Act
var result = await parameterBinder.BindModelAsync(parameter, testContext);
// Assert
Assert.True(result.IsModelSet);
Assert.Equal("Joey", Assert.IsType<Person>(result.Model).Name);
}
[Fact]
public async Task BindModelAsync_WithBindProperty_SupportsGet_BindsModel_WhenRequestIsGet()
{
// Arrange
var parameterBinder = ModelBindingTestHelper.GetParameterBinder();
var parameter = new ParameterDescriptor()
{
Name = "parameter",
ParameterType = typeof(Person),
BindingInfo = BindingInfo.GetBindingInfo(new[] { new BindPropertyAttribute() { SupportsGet = true } }),
};
var testContext = ModelBindingTestHelper.GetTestContext(request =>
{
request.Method = "GET";
request.QueryString = new QueryString("?parameter.Name=Joey");
});
// Act
var result = await parameterBinder.BindModelAsync(parameter, testContext);
// Assert
Assert.True(result.IsModelSet);
Assert.Equal("Joey", Assert.IsType<Person>(result.Model).Name);
}
[Fact]
public async Task BindModelAsync_WithBindProperty_DoesNotBindModel_WhenRequestIsGet()
{
// Arrange
var parameterBinder = ModelBindingTestHelper.GetParameterBinder();
var parameter = new ParameterDescriptor()
{
Name = "parameter",
ParameterType = typeof(Person),
BindingInfo = BindingInfo.GetBindingInfo(new[] { new BindPropertyAttribute() }),
};
var testContext = ModelBindingTestHelper.GetTestContext(request =>
{
request.Method = "GET";
request.QueryString = new QueryString("?parameter.Name=Joey");
});
// Act
var result = await parameterBinder.BindModelAsync(parameter, testContext);
// Assert
Assert.False(result.IsModelSet);
}
}
}

View File

@ -6,6 +6,7 @@ using System.Collections.Generic;
using System.Linq; using System.Linq;
using System.Reflection; using System.Reflection;
using System.Threading.Tasks; using System.Threading.Tasks;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc.ActionConstraints; using Microsoft.AspNetCore.Mvc.ActionConstraints;
using Microsoft.AspNetCore.Mvc.Filters; using Microsoft.AspNetCore.Mvc.Filters;
using Microsoft.AspNetCore.Mvc.RazorPages.Infrastructure; using Microsoft.AspNetCore.Mvc.RazorPages.Infrastructure;
@ -576,7 +577,17 @@ namespace Microsoft.AspNetCore.Mvc.RazorPages.Internal
p => p =>
{ {
Assert.Equal("Property", p.Property.Name); Assert.Equal("Property", p.Property.Name);
Assert.True(p.SupportsGet); Assert.NotNull(p.BindingInfo.RequestPredicate);
Assert.True(p.BindingInfo.RequestPredicate(new ActionContext()
{
HttpContext = new DefaultHttpContext()
{
Request =
{
Method ="GET",
}
}
}));
}); });
} }
@ -603,7 +614,17 @@ namespace Microsoft.AspNetCore.Mvc.RazorPages.Internal
p => p =>
{ {
Assert.Equal("Property", p.Property.Name); Assert.Equal("Property", p.Property.Name);
Assert.True(p.SupportsGet); Assert.NotNull(p.BindingInfo.RequestPredicate);
Assert.True(p.BindingInfo.RequestPredicate(new ActionContext()
{
HttpContext = new DefaultHttpContext()
{
Request =
{
Method ="GET",
}
}
}));
}); });
} }
@ -628,7 +649,17 @@ namespace Microsoft.AspNetCore.Mvc.RazorPages.Internal
p => p =>
{ {
Assert.Equal("Property", p.Property.Name); Assert.Equal("Property", p.Property.Name);
Assert.False(p.SupportsGet); Assert.NotNull(p.BindingInfo.RequestPredicate);
Assert.False(p.BindingInfo.RequestPredicate(new ActionContext()
{
HttpContext = new DefaultHttpContext()
{
Request =
{
Method ="GET",
}
}
}));
}); });
} }

View File

@ -324,7 +324,7 @@ namespace Microsoft.AspNetCore.Mvc.RazorPages.Internal
[InlineData("Get")] [InlineData("Get")]
[InlineData("GET")] [InlineData("GET")]
[InlineData("gET")] [InlineData("gET")]
public async Task ModelBinderFactory_IgnoresPropertyWithoutSupportsGet_WhenRequestIsGet(string method) public async Task ModelBinderFactory_BindsPropertyWithoutSupportsGet_WhenRequestIsGet(string method)
{ {
// Arrange // Arrange
var type = typeof(PageModelWithSupportsGetProperty).GetTypeInfo(); var type = typeof(PageModelWithSupportsGetProperty).GetTypeInfo();
@ -338,7 +338,11 @@ namespace Microsoft.AspNetCore.Mvc.RazorPages.Internal
Name = nameof(PageModelWithSupportsGetProperty.SupportsGet), Name = nameof(PageModelWithSupportsGetProperty.SupportsGet),
ParameterType = typeof(string), ParameterType = typeof(string),
Property = type.GetProperty(nameof(PageModelWithSupportsGetProperty.SupportsGet)), Property = type.GetProperty(nameof(PageModelWithSupportsGetProperty.SupportsGet)),
SupportsGet = true, BindingInfo = new BindingInfo()
{
// Simulates placing a [BindProperty] on the property
RequestPredicate = ((IRequestPredicateProvider)new BindPropertyAttribute() { SupportsGet = true }).RequestPredicate,
}
}, },
new PageBoundPropertyDescriptor() new PageBoundPropertyDescriptor()
{ {
@ -356,7 +360,7 @@ namespace Microsoft.AspNetCore.Mvc.RazorPages.Internal
var binder = new TestParameterBinder(new Dictionary<string, object>() var binder = new TestParameterBinder(new Dictionary<string, object>()
{ {
{ "SupportsGet", "value" }, { "SupportsGet", "value" },
{ "Default", "ignored" }, { "Default", "set" },
}); });
var modelMetadataProvider = TestModelMetadataProvider.CreateDefaultProvider(); var modelMetadataProvider = TestModelMetadataProvider.CreateDefaultProvider();
@ -366,12 +370,16 @@ namespace Microsoft.AspNetCore.Mvc.RazorPages.Internal
{ {
PageContext = new PageContext() PageContext = new PageContext()
{ {
HttpContext = new DefaultHttpContext(), HttpContext = new DefaultHttpContext()
{
Request=
{
Method = method,
}
}
} }
}; };
page.HttpContext.Request.Method = method;
var model = new PageModelWithSupportsGetProperty(); var model = new PageModelWithSupportsGetProperty();
// Act // Act
@ -379,7 +387,7 @@ namespace Microsoft.AspNetCore.Mvc.RazorPages.Internal
// Assert // Assert
Assert.Equal("value", model.SupportsGet); Assert.Equal("value", model.SupportsGet);
Assert.Null(model.Default); Assert.Equal("set", model.Default);
} }
[Fact] [Fact]
@ -397,7 +405,10 @@ namespace Microsoft.AspNetCore.Mvc.RazorPages.Internal
Name = nameof(PageModelWithSupportsGetProperty.SupportsGet), Name = nameof(PageModelWithSupportsGetProperty.SupportsGet),
ParameterType = typeof(string), ParameterType = typeof(string),
Property = type.GetProperty(nameof(PageModelWithSupportsGetProperty.SupportsGet)), Property = type.GetProperty(nameof(PageModelWithSupportsGetProperty.SupportsGet)),
SupportsGet = true, BindingInfo = new BindingInfo()
{
RequestPredicate = ((IRequestPredicateProvider)new BindPropertyAttribute() { SupportsGet = true }).RequestPredicate,
}
}, },
new PageBoundPropertyDescriptor() new PageBoundPropertyDescriptor()
{ {