Allow BindPropertyAttribute to be applied on PageModel

Fixes #6653
This commit is contained in:
Pranav K 2017-09-11 16:05:53 -07:00
parent 63397653fa
commit de38922601
7 changed files with 188 additions and 16 deletions

View File

@ -6,7 +6,7 @@ using Microsoft.AspNetCore.Mvc.ModelBinding;
namespace Microsoft.AspNetCore.Mvc
{
[AttributeUsage(AttributeTargets.Property, AllowMultiple = false, Inherited = true)]
[AttributeUsage(AttributeTargets.Class | AttributeTargets.Property, AllowMultiple = false, Inherited = true)]
public class BindPropertyAttribute : Attribute, IModelNameProvider, IBinderTypeProviderMetadata, IRequestPredicateProvider
{
private static readonly Func<ActionContext, bool> _supportsAllRequests = (c) => true;
@ -31,22 +31,14 @@ namespace Microsoft.AspNetCore.Mvc
return _bindingSource;
}
protected set
{
_bindingSource = value;
}
protected set => _bindingSource = value;
}
/// <inheritdoc />
public string Name { get; set; }
Func<ActionContext, bool> IRequestPredicateProvider.RequestPredicate
{
get
{
return SupportsGet ? _supportsAllRequests : _supportsNonGetRequests;
}
}
=> SupportsGet ? _supportsAllRequests : _supportsNonGetRequests;
private static bool IsNonGetRequest(ActionContext context)
{

View File

@ -224,7 +224,6 @@ namespace Microsoft.AspNetCore.Mvc.Internal
return propertyModel;
}
/// <summary>
/// Creates the <see cref="ActionModel"/> instance for the given action <see cref="MethodInfo"/>.
/// </summary>

View File

@ -220,10 +220,15 @@ namespace Microsoft.AspNetCore.Mvc.RazorPages.Internal
throw new ArgumentNullException(nameof(property));
}
var attributes = property.GetCustomAttributes(inherit: true);
var bindingInfo = BindingInfo.GetBindingInfo(attributes);
var propertyAttributes = property.GetCustomAttributes(inherit: true);
var handlerAttributes = property.DeclaringType.GetCustomAttributes(inherit: true);
var model = new PagePropertyModel(property, property.GetCustomAttributes(inherit: true))
// Look for binding info on the handler if nothing is specified on the property.
// This allows BindProperty attributes on handlers to apply to properties.
var bindingInfo = BindingInfo.GetBindingInfo(propertyAttributes) ??
BindingInfo.GetBindingInfo(handlerAttributes);
var model = new PagePropertyModel(property, propertyAttributes)
{
PropertyName = property.Name,
BindingInfo = bindingInfo,

View File

@ -1136,9 +1136,59 @@ Microsoft.AspNetCore.Mvc.ViewFeatures.ViewDataDictionary`1[AspNetCore._InjectedP
// Act
var response = await Client.GetStringAsync("/Pages/Localized/PageWithModel?culture=fr-FR");
// Assert
Assert.Equal(expected, response.Trim());
}
[Fact]
public async Task BindPropertyAttribute_CanBeAppliedToModelType()
{
// Arrange
var expected = "Property1 = 123, Property2 = 25,";
var request = new HttpRequestMessage(HttpMethod.Post, "/Pages/PropertyBinding/BindPropertyOnModel?Property1=123")
{
Content = new FormUrlEncodedContent(new[]
{
new KeyValuePair<string, string>("Property2", "25"),
}),
};
await AddAntiforgeryHeaders(request);
// Act
var response = await Client.SendAsync(request);
// Assert
response.EnsureSuccessStatusCode();
var responseContent = await response.Content.ReadAsStringAsync();
Assert.StartsWith(expected, responseContent.Trim());
}
[Fact]
public async Task BindingInfoOnPropertiesIsPreferredToBindingInfoOnType()
{
// Arrange
var expected = "Property1 = 123, Property2 = 25,";
var request = new HttpRequestMessage(HttpMethod.Post, "/Pages/PropertyBinding/BindPropertyOnModel?Property1=123")
{
Content = new FormUrlEncodedContent(new[]
{
// FormValueProvider appears before QueryStringValueProvider. However, the FromQuery explicitly listed
// on the property should cause it to use the latter.
new KeyValuePair<string, string>("Property1", "345"),
new KeyValuePair<string, string>("Property2", "25"),
}),
};
await AddAntiforgeryHeaders(request);
// Act
var response = await Client.SendAsync(request);
// Assert
response.EnsureSuccessStatusCode();
var responseContent = await response.Content.ReadAsStringAsync();
Assert.StartsWith(expected, responseContent.Trim());
}
private async Task AddAntiforgeryHeaders(HttpRequestMessage request)
{
var getResponse = await Client.GetAsync(request.RequestUri);

View File

@ -10,7 +10,6 @@ using Microsoft.AspNetCore.Mvc.ApplicationModels;
using Microsoft.AspNetCore.Mvc.ModelBinding;
using Microsoft.AspNetCore.Mvc.Razor;
using Microsoft.AspNetCore.Mvc.RazorPages.Infrastructure;
using Microsoft.Extensions.Options;
using Xunit;
namespace Microsoft.AspNetCore.Mvc.RazorPages.Internal
@ -229,6 +228,60 @@ namespace Microsoft.AspNetCore.Mvc.RazorPages.Internal
public string Property { get; set; }
}
[Fact]
public void OnProvidersExecuting_DiscoversProperties_FromAllSubTypesThatDeclaresBindProperty()
{
// Arrange
var provider = new DefaultPageApplicationModelProvider();
var typeInfo = typeof(BindPropertyAttributeOnBaseModelPage).GetTypeInfo();
var descriptor = new PageActionDescriptor();
var context = new PageApplicationModelProviderContext(descriptor, typeInfo);
// Act
provider.OnProvidersExecuting(context);
// Assert
Assert.NotNull(context.PageApplicationModel);
Assert.Collection(
context.PageApplicationModel.HandlerProperties.OrderBy(p => p.PropertyName).Where(p => p.BindingInfo != null),
property =>
{
var name = nameof(ModelLevel3.Property2);
Assert.Equal(typeof(ModelLevel3).GetProperty(name), property.PropertyInfo);
Assert.Equal(name, property.PropertyName);
Assert.NotNull(property.BindingInfo);
},
property =>
{
var name = nameof(ModelLevel3.Property3);
Assert.Equal(typeof(ModelLevel3).GetProperty(name), property.PropertyInfo);
Assert.Equal(name, property.PropertyName);
Assert.NotNull(property.BindingInfo);
});
}
private class BindPropertyAttributeOnBaseModelPage : Page
{
public ModelLevel3 Model => null;
public override Task ExecuteAsync() => throw new NotImplementedException();
}
private class ModelLevel1 : PageModel
{
public string Property1 { get; set; }
}
[BindProperty]
private class ModelLevel2 : ModelLevel1
{
public string Property2 { get; set; }
}
private class ModelLevel3 : ModelLevel2
{
public string Property3 { get; set; }
}
[Fact]
public void OnProvidersExecuting_DiscoversHandlersFromPage()
{
@ -305,6 +358,53 @@ namespace Microsoft.AspNetCore.Mvc.RazorPages.Internal
});
}
[Fact]
public void OnProvidersExecuting_DiscoversBindingInfoFromHandler()
{
// Arrange
var provider = new DefaultPageApplicationModelProvider();
var typeInfo = typeof(PageWithBindPropertyModel).GetTypeInfo();
var modelType = typeof(ModelWithBindProperty);
var descriptor = new PageActionDescriptor();
var context = new PageApplicationModelProviderContext(descriptor, typeInfo);
// Act
provider.OnProvidersExecuting(context);
// Assert
Assert.NotNull(context.PageApplicationModel);
Assert.Collection(
context.PageApplicationModel.HandlerProperties.OrderBy(p => p.PropertyName),
property =>
{
Assert.Equal(nameof(ModelWithBindProperty.Property1), property.PropertyName);
Assert.NotNull(property.BindingInfo);
},
property =>
{
Assert.Equal(nameof(ModelWithBindProperty.Property2), property.PropertyName);
Assert.NotNull(property.BindingInfo);
Assert.Equal(BindingSource.Path, property.BindingInfo.BindingSource);
});
}
private class PageWithBindPropertyModel : PageBase
{
public ModelWithBindProperty Model => null;
public override Task ExecuteAsync() => null;
}
[BindProperty]
[PageModel]
private class ModelWithBindProperty
{
public string Property1 { get; set; }
[FromRoute]
public string Property2 { get; set; }
}
[Fact]
public void OnProvidersExecuting_DiscoversHandlersFromModel()
{

View File

@ -0,0 +1,18 @@
// 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 Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.ModelBinding;
using Microsoft.AspNetCore.Mvc.RazorPages;
namespace RazorPagesWebSite
{
[BindProperty]
public class BindPropertyOnModel : PageModel
{
[FromQuery]
public string Property1 { get; set; }
public string Property2 { get; set; }
}
}

View File

@ -0,0 +1,8 @@
@page
@model BindPropertyOnModel
Property1 = @Model.Property1, Property2 = @Model.Property2,
<form action="">
@Html.AntiForgeryToken()
</form>