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;
BinderType = other.BinderType;
PropertyFilterProvider = other.PropertyFilterProvider;
RequestPredicate = other.RequestPredicate;
}
/// <summary>
@ -56,6 +57,12 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding
/// </summary>
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>
/// Constructs a new instance of <see cref="BindingInfo"/> from the given <paramref name="attributes"/>.
/// </summary>
@ -113,6 +120,17 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding
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;
}

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 Microsoft.AspNetCore.Mvc.ModelBinding;
namespace Microsoft.AspNetCore.Mvc.RazorPages
namespace Microsoft.AspNetCore.Mvc
{
[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;
public bool SupportsGet { get; set; }
@ -35,5 +39,18 @@ namespace Microsoft.AspNetCore.Mvc.RazorPages
/// <inheritdoc />
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));
}
if (parameter.BindingInfo?.RequestPredicate?.Invoke(actionContext) == false)
{
return ModelBindingResult.Failed();
}
var modelBindingContext = DefaultModelBindingContext.CreateBindingContext(
actionContext,
valueProvider,

View File

@ -9,7 +9,5 @@ namespace Microsoft.AspNetCore.Mvc.RazorPages.Infrastructure
public class PageBoundPropertyDescriptor : ParameterDescriptor
{
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 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)
{
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)))
{
continue;
}
var bindPropertyOnProperty = property.Property.GetCustomAttribute<BindPropertyAttribute>();
var supportsGet = bindPropertyOnProperty?.SupportsGet ?? bindPropertyOnType?.SupportsGet ?? false;
bindingInfo = bindingInfo ?? new BindingInfo();
// 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()
{
BindingInfo = bindingInfo ?? new BindingInfo(),
BindingInfo = bindingInfo,
Name = property.Name,
Property = property.Property,
ParameterType = property.Property.PropertyType,
SupportsGet = supportsGet,
};
results.Add(descriptor);

View File

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

View File

@ -13,6 +13,7 @@ using Microsoft.AspNetCore.Mvc.Formatters;
using Microsoft.AspNetCore.Mvc.ModelBinding;
using Microsoft.AspNetCore.Mvc.ModelBinding.Binders;
using Microsoft.AspNetCore.Mvc.ModelBinding.Validation;
using Microsoft.AspNetCore.Mvc.RazorPages;
using Microsoft.AspNetCore.Routing;
using Moq;
using Xunit;
@ -490,6 +491,125 @@ namespace Microsoft.AspNetCore.Mvc.Internal
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
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()
{
// Arrange
var expected = "Id = 11, Name = , Age =";
var expected = "Id = 11, Name = Test-Name, Age =";
var validationError = "The Name field is required.";
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.Reflection;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc.ActionConstraints;
using Microsoft.AspNetCore.Mvc.Filters;
using Microsoft.AspNetCore.Mvc.RazorPages.Infrastructure;
@ -576,7 +577,17 @@ namespace Microsoft.AspNetCore.Mvc.RazorPages.Internal
p =>
{
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 =>
{
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 =>
{
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")]
public async Task ModelBinderFactory_IgnoresPropertyWithoutSupportsGet_WhenRequestIsGet(string method)
public async Task ModelBinderFactory_BindsPropertyWithoutSupportsGet_WhenRequestIsGet(string method)
{
// Arrange
var type = typeof(PageModelWithSupportsGetProperty).GetTypeInfo();
@ -338,7 +338,11 @@ namespace Microsoft.AspNetCore.Mvc.RazorPages.Internal
Name = nameof(PageModelWithSupportsGetProperty.SupportsGet),
ParameterType = typeof(string),
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()
{
@ -356,7 +360,7 @@ namespace Microsoft.AspNetCore.Mvc.RazorPages.Internal
var binder = new TestParameterBinder(new Dictionary<string, object>()
{
{ "SupportsGet", "value" },
{ "Default", "ignored" },
{ "Default", "set" },
});
var modelMetadataProvider = TestModelMetadataProvider.CreateDefaultProvider();
@ -366,12 +370,16 @@ namespace Microsoft.AspNetCore.Mvc.RazorPages.Internal
{
PageContext = new PageContext()
{
HttpContext = new DefaultHttpContext(),
HttpContext = new DefaultHttpContext()
{
Request=
{
Method = method,
}
}
}
};
page.HttpContext.Request.Method = method;
var model = new PageModelWithSupportsGetProperty();
// Act
@ -379,7 +387,7 @@ namespace Microsoft.AspNetCore.Mvc.RazorPages.Internal
// Assert
Assert.Equal("value", model.SupportsGet);
Assert.Null(model.Default);
Assert.Equal("set", model.Default);
}
[Fact]
@ -397,7 +405,10 @@ namespace Microsoft.AspNetCore.Mvc.RazorPages.Internal
Name = nameof(PageModelWithSupportsGetProperty.SupportsGet),
ParameterType = typeof(string),
Property = type.GetProperty(nameof(PageModelWithSupportsGetProperty.SupportsGet)),
SupportsGet = true,
BindingInfo = new BindingInfo()
{
RequestPredicate = ((IRequestPredicateProvider)new BindPropertyAttribute() { SupportsGet = true }).RequestPredicate,
}
},
new PageBoundPropertyDescriptor()
{