Introduce BindPropertiesAttribute
* Allow controller and Razor Page models to be annotated with BindPropertiesAttribute * Disallow BindPropertyAttribute from being declared on types. * Do not allow arbitrary binding attributes to be applied to Razor Page models. Fixes #7686
This commit is contained in:
parent
31f9432a39
commit
5d5222cdd8
|
|
@ -0,0 +1,23 @@
|
|||
// 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
|
||||
{
|
||||
/// <summary>
|
||||
/// An attribute that enables binding for all properties the decorated controller or Razor Page model defines.
|
||||
/// </summary>
|
||||
[AttributeUsage(AttributeTargets.Class, AllowMultiple = false, Inherited = true)]
|
||||
public class BindPropertiesAttribute : Attribute
|
||||
{
|
||||
/// <summary>
|
||||
/// When <c>true</c>, allows properties to be bound on GET requests. When <c>false</c>, properties
|
||||
/// do not get model bound or validated on GET requests.
|
||||
/// <para>
|
||||
/// Defaults to <c>false</c>.
|
||||
/// </para>
|
||||
/// </summary>
|
||||
public bool SupportsGet { get; set; }
|
||||
}
|
||||
}
|
||||
|
|
@ -6,7 +6,7 @@ using Microsoft.AspNetCore.Mvc.ModelBinding;
|
|||
|
||||
namespace Microsoft.AspNetCore.Mvc
|
||||
{
|
||||
[AttributeUsage(AttributeTargets.Class | AttributeTargets.Property, AllowMultiple = false, Inherited = true)]
|
||||
[AttributeUsage(AttributeTargets.Property, AllowMultiple = false, Inherited = true)]
|
||||
public class BindPropertyAttribute : Attribute, IModelNameProvider, IBinderTypeProviderMetadata, IRequestPredicateProvider
|
||||
{
|
||||
private static readonly Func<ActionContext, bool> _supportsAllRequests = (c) => true;
|
||||
|
|
|
|||
|
|
@ -20,6 +20,8 @@ namespace Microsoft.AspNetCore.Mvc.Internal
|
|||
{
|
||||
private readonly MvcOptions _mvcOptions;
|
||||
private readonly IModelMetadataProvider _modelMetadataProvider;
|
||||
private readonly Func<ActionContext, bool> _supportsAllRequests;
|
||||
private readonly Func<ActionContext, bool> _supportsNonGetRequests;
|
||||
|
||||
public DefaultApplicationModelProvider(
|
||||
IOptions<MvcOptions> mvcOptionsAccessor,
|
||||
|
|
@ -27,6 +29,9 @@ namespace Microsoft.AspNetCore.Mvc.Internal
|
|||
{
|
||||
_mvcOptions = mvcOptionsAccessor.Value;
|
||||
_modelMetadataProvider = modelMetadataProvider;
|
||||
|
||||
_supportsAllRequests = _ => true;
|
||||
_supportsNonGetRequests = context => !string.Equals(context.HttpContext.Request.Method, "GET", StringComparison.OrdinalIgnoreCase);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
|
|
@ -218,16 +223,24 @@ namespace Microsoft.AspNetCore.Mvc.Internal
|
|||
|
||||
var attributes = propertyInfo.GetCustomAttributes(inherit: true);
|
||||
|
||||
|
||||
// BindingInfo for properties can be either specified by decorating the property with binding specific attributes.
|
||||
// ModelMetadata also adds information from the property's type and any configured IBindingMetadataProvider.
|
||||
var modelMetadata = _modelMetadataProvider.GetMetadataForProperty(propertyInfo.DeclaringType, propertyInfo.Name);
|
||||
var bindingInfo = BindingInfo.GetBindingInfo(attributes, modelMetadata);
|
||||
|
||||
if (bindingInfo == null)
|
||||
{
|
||||
// Look for BindPropertiesAttribute on the handler type if no BindingInfo was inferred for the property.
|
||||
// This allows a user to enable model binding on properties by decorating the controller type with BindPropertiesAttribute.
|
||||
var declaringType = propertyInfo.DeclaringType;
|
||||
if (declaringType.IsDefined(typeof(BindPropertyAttribute), inherit: true))
|
||||
var bindPropertiesAttribute = declaringType.GetCustomAttribute<BindPropertiesAttribute>(inherit: true);
|
||||
if (bindPropertiesAttribute != null)
|
||||
{
|
||||
// Specify a BindingInfo so that the property is now considered for model binding.
|
||||
bindingInfo = new BindingInfo();
|
||||
var requestPredicate = bindPropertiesAttribute.SupportsGet ? _supportsAllRequests : _supportsNonGetRequests;
|
||||
bindingInfo = new BindingInfo
|
||||
{
|
||||
RequestPredicate = requestPredicate,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -20,6 +20,9 @@ namespace Microsoft.AspNetCore.Mvc.RazorPages.Internal
|
|||
private readonly PageHandlerResultFilter _pageHandlerResultFilter = new PageHandlerResultFilter();
|
||||
private readonly IModelMetadataProvider _modelMetadataProvider;
|
||||
private readonly MvcOptions _options;
|
||||
private readonly Func<ActionContext, bool> _supportsAllRequests;
|
||||
private readonly Func<ActionContext, bool> _supportsNonGetRequests;
|
||||
|
||||
|
||||
public DefaultPageApplicationModelProvider(
|
||||
IModelMetadataProvider modelMetadataProvider,
|
||||
|
|
@ -27,6 +30,9 @@ namespace Microsoft.AspNetCore.Mvc.RazorPages.Internal
|
|||
{
|
||||
_modelMetadataProvider = modelMetadataProvider;
|
||||
_options = options.Value;
|
||||
|
||||
_supportsAllRequests = _ => true;
|
||||
_supportsNonGetRequests = context => !string.Equals(context.HttpContext.Request.Method, "GET", StringComparison.OrdinalIgnoreCase);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
|
|
@ -262,17 +268,25 @@ namespace Microsoft.AspNetCore.Mvc.RazorPages.Internal
|
|||
|
||||
var propertyAttributes = property.GetCustomAttributes(inherit: true);
|
||||
|
||||
// BindingInfo for properties can be either specified by decorating the property with binding-specific attributes.
|
||||
// ModelMetadata also adds information from the property's type and any configured IBindingMetadataProvider.
|
||||
var propertyMetadata = _modelMetadataProvider.GetMetadataForProperty(property.DeclaringType, property.Name);
|
||||
var bindingInfo = BindingInfo.GetBindingInfo(propertyAttributes, propertyMetadata);
|
||||
|
||||
if (bindingInfo == null)
|
||||
{
|
||||
// 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 handlerType = property.DeclaringType;
|
||||
var handlerAttributes = handlerType.GetCustomAttributes(inherit: true);
|
||||
var handlerMetadata = _modelMetadataProvider.GetMetadataForType(property.DeclaringType);
|
||||
bindingInfo = BindingInfo.GetBindingInfo(handlerAttributes, handlerMetadata);
|
||||
// Look for BindPropertiesAttribute on the handler type if no BindingInfo was inferred for the property.
|
||||
// This allows a user to enable model binding on properties by decorating the controller type with BindPropertiesAttribute.
|
||||
var declaringType = property.DeclaringType;
|
||||
var bindPropertiesAttribute = declaringType.GetCustomAttribute<BindPropertiesAttribute>(inherit: true);
|
||||
if (bindPropertiesAttribute != null)
|
||||
{
|
||||
var requestPredicate = bindPropertiesAttribute.SupportsGet ? _supportsAllRequests : _supportsNonGetRequests;
|
||||
bindingInfo = new BindingInfo
|
||||
{
|
||||
RequestPredicate = requestPredicate,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
var model = new PagePropertyModel(property, propertyAttributes)
|
||||
|
|
|
|||
|
|
@ -1127,7 +1127,7 @@ namespace Microsoft.AspNetCore.Mvc.Internal
|
|||
Assert.Equal(typeInfo, action.ActionMethod.DeclaringType.GetTypeInfo());
|
||||
}
|
||||
|
||||
[BindProperty]
|
||||
[BindProperties]
|
||||
public class BindPropertyController
|
||||
{
|
||||
public string Property { get; set; }
|
||||
|
|
@ -1140,7 +1140,7 @@ namespace Microsoft.AspNetCore.Mvc.Internal
|
|||
}
|
||||
|
||||
[Fact]
|
||||
public void CreatePropertyModel_AddsBindingInfoToProperty_IfDeclaringTypeHasBindPropertyAttribute()
|
||||
public void CreatePropertyModel_AddsBindingInfoToProperty_IfDeclaringTypeHasBindPropertiesAttribute()
|
||||
{
|
||||
// Arrange
|
||||
var propertyInfo = typeof(BindPropertyController).GetProperty(nameof(BindPropertyController.Property));
|
||||
|
|
@ -1155,7 +1155,7 @@ namespace Microsoft.AspNetCore.Mvc.Internal
|
|||
Assert.Null(bindingInfo.BinderType);
|
||||
Assert.Null(bindingInfo.BindingSource);
|
||||
Assert.Null(bindingInfo.PropertyFilterProvider);
|
||||
Assert.Null(bindingInfo.RequestPredicate);
|
||||
Assert.NotNull(bindingInfo.RequestPredicate);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
|
|
@ -1206,7 +1206,7 @@ namespace Microsoft.AspNetCore.Mvc.Internal
|
|||
Assert.NotNull(property.BindingInfo);
|
||||
}
|
||||
|
||||
[BindProperty]
|
||||
[BindProperties]
|
||||
public class UserController : ControllerBase
|
||||
{
|
||||
public string DerivedProperty { get; set; }
|
||||
|
|
|
|||
|
|
@ -406,9 +406,9 @@ namespace Microsoft.AspNetCore.Mvc.FunctionalTests
|
|||
public async Task RedirectToAction_WithEmptyActionName_UsesAmbientValue()
|
||||
{
|
||||
// Arrange
|
||||
var product = new List<KeyValuePair<string, string>>
|
||||
var product = new Dictionary<string, string>
|
||||
{
|
||||
new KeyValuePair<string, string>("SampleInt", "20")
|
||||
{ "SampleInt", "20" }
|
||||
};
|
||||
|
||||
// Act
|
||||
|
|
@ -522,10 +522,17 @@ namespace Microsoft.AspNetCore.Mvc.FunctionalTests
|
|||
}
|
||||
|
||||
[Fact]
|
||||
public async Task BindPropertyCanBeAppliedToControllers()
|
||||
public async Task BindPropertiesAttribute_CanBeAppliedToControllers()
|
||||
{
|
||||
// Arrange
|
||||
var formContent = new Dictionary<string, string>
|
||||
{
|
||||
{ "Name", "TestName" },
|
||||
{ "Id", "10" },
|
||||
};
|
||||
|
||||
// Act
|
||||
var response = await Client.GetAsync("BindProperty/Action?Name=TestName&Id=10");
|
||||
var response = await Client.PostAsync("BindProperties/Action", new FormUrlEncodedContent(formContent));
|
||||
|
||||
// Assert
|
||||
await response.AssertStatusCodeAsync(HttpStatusCode.OK);
|
||||
|
|
@ -537,10 +544,18 @@ namespace Microsoft.AspNetCore.Mvc.FunctionalTests
|
|||
}
|
||||
|
||||
[Fact]
|
||||
public async Task BindProperty_DoesNotApplyToPropertiesWithBindingInfo()
|
||||
public async Task BindPropertiesAttribute_DoesNotApplyToPropertiesWithBindingInfo()
|
||||
{
|
||||
// Arrange
|
||||
var formContent = new Dictionary<string, string>
|
||||
{
|
||||
{ "Id", "10" },
|
||||
{ "FromRoute", "12" },
|
||||
{ "CustomBound", "Test" },
|
||||
};
|
||||
|
||||
// Act
|
||||
var response = await Client.GetAsync("BindProperty/Action?Id=10&IdFromRoute=12&CustomBound=Test");
|
||||
var response = await Client.PostAsync("BindProperties/Action", new FormUrlEncodedContent(formContent));
|
||||
|
||||
// Assert
|
||||
await response.AssertStatusCodeAsync(HttpStatusCode.OK);
|
||||
|
|
@ -552,6 +567,56 @@ namespace Microsoft.AspNetCore.Mvc.FunctionalTests
|
|||
Assert.Equal("CustomBoundValue", data.CustomBound);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task BindPropertiesAttribute_DoesNotCausePropertiesWithBindNeverAttributeToBeModelBound()
|
||||
{
|
||||
// Arrange
|
||||
var formContent = new Dictionary<string, string>
|
||||
{
|
||||
{ "BindNeverProperty", "Hello world" },
|
||||
};
|
||||
|
||||
// Act
|
||||
var response = await Client.PostAsync("BindProperties/Action", new FormUrlEncodedContent(formContent));
|
||||
|
||||
// Assert
|
||||
await response.AssertStatusCodeAsync(HttpStatusCode.OK);
|
||||
var content = await response.Content.ReadAsStringAsync();
|
||||
var data = JsonConvert.DeserializeObject<BindPropertyControllerData>(content);
|
||||
|
||||
Assert.Null(data.BindNeverProperty);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task BindPropertiesAttributeWithSupportsGet_BindsOnNonGet()
|
||||
{
|
||||
// Arrange
|
||||
var formContent = new Dictionary<string, string>
|
||||
{
|
||||
{ "Name", "TestName" },
|
||||
};
|
||||
|
||||
// Act
|
||||
var response = await Client.PostAsync("BindPropertiesSupportsGet/Action", new FormUrlEncodedContent(formContent));
|
||||
|
||||
// Assert
|
||||
await response.AssertStatusCodeAsync(HttpStatusCode.OK);
|
||||
var content = await response.Content.ReadAsStringAsync();
|
||||
Assert.Equal("TestName", content);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task BindPropertiesAttributeWithSupportsGet_BindsOnGet()
|
||||
{
|
||||
// Act
|
||||
var response = await Client.GetAsync("BindPropertiesSupportsGet/Action?Name=OnGetTestName");
|
||||
|
||||
// Assert
|
||||
await response.AssertStatusCodeAsync(HttpStatusCode.OK);
|
||||
var content = await response.Content.ReadAsStringAsync();
|
||||
Assert.Equal("OnGetTestName", content);
|
||||
}
|
||||
|
||||
public class BindPropertyControllerData
|
||||
{
|
||||
public string Name { get; set; }
|
||||
|
|
@ -561,6 +626,8 @@ namespace Microsoft.AspNetCore.Mvc.FunctionalTests
|
|||
public int? IdFromRoute { get; set; }
|
||||
|
||||
public string CustomBound { get; set; }
|
||||
|
||||
public string BindNeverProperty { get; set; }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1153,15 +1153,15 @@ Microsoft.AspNetCore.Mvc.ViewFeatures.ViewDataDictionary`1[AspNetCore.InjectedPa
|
|||
}
|
||||
|
||||
[Fact]
|
||||
public async Task BindPropertyAttribute_CanBeAppliedToModelType()
|
||||
public async Task BindPropertiesAttribute_CanBeAppliedToModelType()
|
||||
{
|
||||
// Arrange
|
||||
var expected = "Property1 = 123, Property2 = 25,";
|
||||
var request = new HttpRequestMessage(HttpMethod.Post, "/Pages/PropertyBinding/BindPropertyOnModel?Property1=123")
|
||||
var request = new HttpRequestMessage(HttpMethod.Post, "/Pages/PropertyBinding/BindPropertiesOnModel?Property1=123")
|
||||
{
|
||||
Content = new FormUrlEncodedContent(new[]
|
||||
Content = new FormUrlEncodedContent(new Dictionary<string, string>
|
||||
{
|
||||
new KeyValuePair<string, string>("Property2", "25"),
|
||||
{ "Property2", "25" },
|
||||
}),
|
||||
};
|
||||
await AddAntiforgeryHeaders(request);
|
||||
|
|
@ -1175,12 +1175,27 @@ Microsoft.AspNetCore.Mvc.ViewFeatures.ViewDataDictionary`1[AspNetCore.InjectedPa
|
|||
Assert.StartsWith(expected, responseContent.Trim());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task BindPropertiesAttribute_CanBeAppliedToModelType_AllowsBindingOnGet()
|
||||
{
|
||||
// Arrange
|
||||
var url = "/Pages/PropertyBinding/BindPropertiesWithSupportsGetOnModel?Property=Property-Value";
|
||||
|
||||
// Act
|
||||
var response = await Client.GetAsync(url);
|
||||
|
||||
// Assert
|
||||
await response.AssertStatusCodeAsync(HttpStatusCode.OK);
|
||||
var content = await response.Content.ReadAsStringAsync();
|
||||
Assert.Equal("Property-Value", content.Trim());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task BindingInfoOnPropertiesIsPreferredToBindingInfoOnType()
|
||||
{
|
||||
// Arrange
|
||||
var expected = "Property1 = 123, Property2 = 25,";
|
||||
var request = new HttpRequestMessage(HttpMethod.Post, "/Pages/PropertyBinding/BindPropertyOnModel?Property1=123")
|
||||
var request = new HttpRequestMessage(HttpMethod.Post, "/Pages/PropertyBinding/BindPropertiesOnModel?Property1=123")
|
||||
{
|
||||
Content = new FormUrlEncodedContent(new[]
|
||||
{
|
||||
|
|
|
|||
|
|
@ -273,7 +273,7 @@ namespace Microsoft.AspNetCore.Mvc.RazorPages.Internal
|
|||
public string Property1 { get; set; }
|
||||
}
|
||||
|
||||
[BindProperty]
|
||||
[BindProperties]
|
||||
private class ModelLevel2 : ModelLevel1
|
||||
{
|
||||
public string Property2 { get; set; }
|
||||
|
|
@ -397,7 +397,7 @@ namespace Microsoft.AspNetCore.Mvc.RazorPages.Internal
|
|||
public override Task ExecuteAsync() => null;
|
||||
}
|
||||
|
||||
[BindProperty]
|
||||
[BindProperties]
|
||||
[PageModel]
|
||||
private class ModelWithBindProperty
|
||||
{
|
||||
|
|
|
|||
|
|
@ -8,8 +8,8 @@ using Microsoft.AspNetCore.Mvc.ModelBinding;
|
|||
|
||||
namespace BasicWebSite
|
||||
{
|
||||
[BindProperty]
|
||||
public class BindPropertyController : Controller
|
||||
[BindProperties]
|
||||
public class BindPropertiesController : Controller
|
||||
{
|
||||
public string Name { get; set; }
|
||||
|
||||
|
|
@ -21,7 +21,10 @@ namespace BasicWebSite
|
|||
[ModelBinder(typeof(CustomBoundModelBinder))]
|
||||
public string CustomBound { get; set; }
|
||||
|
||||
public object Action() => new { Name, Id, IdFromRoute, CustomBound };
|
||||
[BindNever]
|
||||
public string BindNeverProperty { get; set; }
|
||||
|
||||
public object Action() => new { Name, Id, IdFromRoute, CustomBound, BindNeverProperty };
|
||||
|
||||
private class CustomBoundModelBinder : IModelBinder
|
||||
{
|
||||
|
|
@ -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 System;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.AspNetCore.Mvc.ModelBinding;
|
||||
|
||||
namespace BasicWebSite
|
||||
{
|
||||
[BindProperties(SupportsGet = true)]
|
||||
public class BindPropertiesSupportsGetController : Controller
|
||||
{
|
||||
public string Name { get; set; }
|
||||
|
||||
public IActionResult Action() => Content(Name);
|
||||
}
|
||||
}
|
||||
|
|
@ -7,8 +7,8 @@ using Microsoft.AspNetCore.Mvc.RazorPages;
|
|||
|
||||
namespace RazorPagesWebSite
|
||||
{
|
||||
[BindProperty]
|
||||
public class BindPropertyOnModel : PageModel
|
||||
[BindProperties]
|
||||
public class BindPropertiesOnModel : PageModel
|
||||
{
|
||||
[FromQuery]
|
||||
public string Property1 { get; set; }
|
||||
|
|
@ -1,5 +1,5 @@
|
|||
@page
|
||||
@model BindPropertyOnModel
|
||||
@model BindPropertiesOnModel
|
||||
|
||||
Property1 = @Model.Property1, Property2 = @Model.Property2,
|
||||
|
||||
|
|
@ -0,0 +1,15 @@
|
|||
// 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
|
||||
{
|
||||
[BindProperties(SupportsGet = true)]
|
||||
public class BindPropertiesWithSupportsGetOnModel : PageModel
|
||||
{
|
||||
public string Property { get; set; }
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,3 @@
|
|||
@page
|
||||
@model BindPropertiesWithSupportsGetOnModel
|
||||
@Model.Property
|
||||
Loading…
Reference in New Issue