Add support for top level validation to Razor Pages

This commit is contained in:
Pranav K 2018-02-13 22:34:27 -08:00
parent d40c60d15b
commit 06e40252a0
18 changed files with 663 additions and 136 deletions

View File

@ -9,6 +9,7 @@ using Microsoft.AspNetCore.Mvc.ModelBinding;
namespace Microsoft.AspNetCore.Mvc.Internal
{
// Note: changes made to binding behavior in type should also be made to PageBinderFactory.
public static class ControllerBinderDelegateProvider
{
public static ControllerBinderDelegate CreateBinderDelegate(
@ -100,7 +101,7 @@ namespace Microsoft.AspNetCore.Mvc.Internal
}
}
private static BindingInfo[] GetParameterBindingInfo(
private static BinderItem[] GetParameterBindingInfo(
IModelBinderFactory modelBinderFactory,
IModelMetadataProvider modelMetadataProvider,
ControllerActionDescriptor actionDescriptor)
@ -111,7 +112,7 @@ namespace Microsoft.AspNetCore.Mvc.Internal
return null;
}
var parameterBindingInfo = new BindingInfo[parameters.Count];
var parameterBindingInfo = new BinderItem[parameters.Count];
for (var i = 0; i < parameters.Count; i++)
{
var parameter = parameters[i];
@ -140,13 +141,13 @@ namespace Microsoft.AspNetCore.Mvc.Internal
CacheToken = parameter,
});
parameterBindingInfo[i] = new BindingInfo(binder, metadata);
parameterBindingInfo[i] = new BinderItem(binder, metadata);
}
return parameterBindingInfo;
}
private static BindingInfo[] GetPropertyBindingInfo(
private static BinderItem[] GetPropertyBindingInfo(
IModelBinderFactory modelBinderFactory,
IModelMetadataProvider modelMetadataProvider,
ControllerActionDescriptor actionDescriptor)
@ -157,7 +158,7 @@ namespace Microsoft.AspNetCore.Mvc.Internal
return null;
}
var propertyBindingInfo = new BindingInfo[properties.Count];
var propertyBindingInfo = new BinderItem[properties.Count];
var controllerType = actionDescriptor.ControllerTypeInfo.AsType();
for (var i = 0; i < properties.Count; i++)
{
@ -170,15 +171,15 @@ namespace Microsoft.AspNetCore.Mvc.Internal
CacheToken = property,
});
propertyBindingInfo[i] = new BindingInfo(binder, metadata);
propertyBindingInfo[i] = new BinderItem(binder, metadata);
}
return propertyBindingInfo;
}
private struct BindingInfo
private struct BinderItem
{
public BindingInfo(IModelBinder modelBinder, ModelMetadata modelMetadata)
public BinderItem(IModelBinder modelBinder, ModelMetadata modelMetadata)
{
ModelBinder = modelBinder;
ModelMetadata = modelMetadata;

View File

@ -6,7 +6,6 @@ using System.Collections.Generic;
using System.Linq;
using Microsoft.AspNetCore.Mvc.ApplicationModels;
using Microsoft.AspNetCore.Mvc.ApplicationParts;
using Microsoft.AspNetCore.Mvc.Razor;
using Microsoft.AspNetCore.Mvc.Razor.Compilation;
using Microsoft.AspNetCore.Mvc.Razor.Internal;
using Microsoft.AspNetCore.Mvc.RazorPages.Infrastructure;

View File

@ -13,7 +13,7 @@ namespace Microsoft.AspNetCore.Mvc.RazorPages.Internal
{
public static class ExecutorFactory
{
public static Func<object, object[], Task<IActionResult>> CreateExecutor(HandlerMethodDescriptor handlerDescriptor)
public static PageHandlerExecutorDelegate CreateExecutor(HandlerMethodDescriptor handlerDescriptor)
{
if (handlerDescriptor == null)
{

View File

@ -154,33 +154,26 @@ namespace Microsoft.AspNetCore.Mvc.RazorPages.Internal
private async Task BindArgumentsCoreAsync()
{
if (CacheEntry.PropertyBinder != null)
{
await CacheEntry.PropertyBinder(_pageContext, _instance);
}
await CacheEntry.PropertyBinder(_pageContext, _instance);
if (_handler == null)
{
return;
}
var valueProvider = await CompositeValueProvider.CreateAsync(_pageContext, _pageContext.ValueProviderFactories);
for (var i = 0; i < _handler.Parameters.Count; i++)
// We do two separate cache lookups, once for the binder and once for the executor.
// Reduding it to a single lookup requires a lot of code change with little value.
PageHandlerBinderDelegate handlerBinder = null;
for (var i = 0; i < _actionDescriptor.HandlerMethods.Count; i++)
{
var parameter = _handler.Parameters[i];
var result = await _parameterBinder.BindModelAsync(
_pageContext,
valueProvider,
parameter,
value: null);
if (result.IsModelSet)
if (object.ReferenceEquals(_handler, _actionDescriptor.HandlerMethods[i]))
{
_arguments[parameter.Name] = result.Model;
handlerBinder = CacheEntry.HandlerBinders[i];
break;
}
}
await handlerBinder(_pageContext, _arguments);
}
private static object[] PrepareArguments(
@ -223,16 +216,18 @@ namespace Microsoft.AspNetCore.Mvc.RazorPages.Internal
{
var arguments = PrepareArguments(_arguments, handler);
Func<object, object[], Task<IActionResult>> executor = null;
PageHandlerExecutorDelegate executor = null;
for (var i = 0; i < _actionDescriptor.HandlerMethods.Count; i++)
{
if (object.ReferenceEquals(handler, _actionDescriptor.HandlerMethods[i]))
{
executor = CacheEntry.Executors[i];
executor = CacheEntry.HandlerExecutors[i];
break;
}
}
Debug.Assert(executor != null, "We should always find a executor for a handler");
_diagnosticSource.BeforeHandlerMethod(_pageContext, handler, _arguments, _instance);
_logger.ExecutingHandlerMethod(_pageContext, handler, arguments);

View File

@ -22,7 +22,8 @@ namespace Microsoft.AspNetCore.Mvc.RazorPages.Internal
Func<PageContext, object> modelFactory,
Action<PageContext, object> releaseModel,
Func<PageContext, object, Task> propertyBinder,
Func<object, object[], Task<IActionResult>>[] executors,
PageHandlerExecutorDelegate[] handlerExecutors,
PageHandlerBinderDelegate[] handlerBinders,
IReadOnlyList<Func<IRazorPage>> viewStartFactories,
FilterItem[] cacheableFilters)
{
@ -33,7 +34,8 @@ namespace Microsoft.AspNetCore.Mvc.RazorPages.Internal
ModelFactory = modelFactory;
ReleaseModel = releaseModel;
PropertyBinder = propertyBinder;
Executors = executors;
HandlerExecutors = handlerExecutors;
HandlerBinders = handlerBinders;
ViewStartFactories = viewStartFactories;
CacheableFilters = cacheableFilters;
}
@ -60,7 +62,9 @@ namespace Microsoft.AspNetCore.Mvc.RazorPages.Internal
/// </summary>
public Func<PageContext, object, Task> PropertyBinder { get; }
public Func<object, object[], Task<IActionResult>>[] Executors { get; }
public PageHandlerExecutorDelegate[] HandlerExecutors { get; }
public PageHandlerBinderDelegate[] HandlerBinders { get; }
public Func<IModelMetadataProvider, ModelStateDictionary, ViewDataDictionary> ViewDataFactory { get; }
@ -70,5 +74,6 @@ namespace Microsoft.AspNetCore.Mvc.RazorPages.Internal
public IReadOnlyList<Func<IRazorPage>> ViewStartFactories { get; }
public FilterItem[] CacheableFilters { get; }
}
}

View File

@ -29,6 +29,7 @@ namespace Microsoft.AspNetCore.Mvc.RazorPages.Internal
private readonly IPageLoader _loader;
private readonly IPageFactoryProvider _pageFactoryProvider;
private readonly IPageModelFactoryProvider _modelFactoryProvider;
private readonly IModelBinderFactory _modelBinderFactory;
private readonly IRazorPageFactoryProvider _razorPageFactoryProvider;
private readonly IActionDescriptorCollectionProvider _collectionProvider;
private readonly IFilterProvider[] _filterProviders;
@ -52,6 +53,7 @@ namespace Microsoft.AspNetCore.Mvc.RazorPages.Internal
IEnumerable<IFilterProvider> filterProviders,
ParameterBinder parameterBinder,
IModelMetadataProvider modelMetadataProvider,
IModelBinderFactory modelBinderFactory,
ITempDataDictionaryFactory tempDataFactory,
IOptions<MvcOptions> mvcOptions,
IOptions<HtmlHelperOptions> htmlHelperOptions,
@ -63,6 +65,7 @@ namespace Microsoft.AspNetCore.Mvc.RazorPages.Internal
_loader = loader;
_pageFactoryProvider = pageFactoryProvider;
_modelFactoryProvider = modelFactoryProvider;
_modelBinderFactory = modelBinderFactory;
_razorPageFactoryProvider = razorPageFactoryProvider;
_collectionProvider = collectionProvider;
_filterProviders = filterProviders.ToArray();
@ -172,9 +175,10 @@ namespace Microsoft.AspNetCore.Mvc.RazorPages.Internal
var pageFactory = _pageFactoryProvider.CreatePageFactory(compiledActionDescriptor);
var pageDisposer = _pageFactoryProvider.CreatePageDisposer(compiledActionDescriptor);
var propertyBinder = PagePropertyBinderFactory.CreateBinder(
var propertyBinder = PageBinderFactory.CreatePropertyBinder(
_parameterBinder,
_modelMetadataProvider,
_modelBinderFactory,
compiledActionDescriptor);
Func<PageContext, object> modelFactory = null;
@ -187,7 +191,8 @@ namespace Microsoft.AspNetCore.Mvc.RazorPages.Internal
var viewStartFactories = GetViewStartFactories(compiledActionDescriptor);
var executors = GetExecutors(compiledActionDescriptor);
var handlerExecutors = GetHandlerExecutors(compiledActionDescriptor);
var handlerBinders = GetHandlerBinders(compiledActionDescriptor);
return new PageActionInvokerCacheEntry(
compiledActionDescriptor,
@ -197,7 +202,8 @@ namespace Microsoft.AspNetCore.Mvc.RazorPages.Internal
modelFactory,
modelReleaser,
propertyBinder,
executors,
handlerExecutors,
handlerBinders,
viewStartFactories,
cachedFilters);
}
@ -222,14 +228,14 @@ namespace Microsoft.AspNetCore.Mvc.RazorPages.Internal
return viewStartFactories;
}
private static Func<object, object[], Task<IActionResult>>[] GetExecutors(CompiledPageActionDescriptor actionDescriptor)
private static PageHandlerExecutorDelegate[] GetHandlerExecutors(CompiledPageActionDescriptor actionDescriptor)
{
if (actionDescriptor.HandlerMethods == null || actionDescriptor.HandlerMethods.Count == 0)
{
return Array.Empty<Func<object, object[], Task<IActionResult>>>();
return Array.Empty<PageHandlerExecutorDelegate>();
}
var results = new Func<object, object[], Task<IActionResult>>[actionDescriptor.HandlerMethods.Count];
var results = new PageHandlerExecutorDelegate[actionDescriptor.HandlerMethods.Count];
for (var i = 0; i < actionDescriptor.HandlerMethods.Count; i++)
{
@ -239,6 +245,28 @@ namespace Microsoft.AspNetCore.Mvc.RazorPages.Internal
return results;
}
private PageHandlerBinderDelegate[] GetHandlerBinders(CompiledPageActionDescriptor actionDescriptor)
{
if (actionDescriptor.HandlerMethods == null ||actionDescriptor.HandlerMethods.Count == 0)
{
return Array.Empty<PageHandlerBinderDelegate>();
}
var results = new PageHandlerBinderDelegate[actionDescriptor.HandlerMethods.Count];
for (var i = 0; i < actionDescriptor.HandlerMethods.Count; i++)
{
results[i] = PageBinderFactory.CreateHandlerBinder(
_parameterBinder,
_modelMetadataProvider,
_modelBinderFactory,
actionDescriptor,
actionDescriptor.HandlerMethods[i]);
}
return results;
}
internal class InnerCache
{
public InnerCache(int version)

View File

@ -0,0 +1,177 @@
// 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.Collections.Generic;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Mvc.Internal;
using Microsoft.AspNetCore.Mvc.ModelBinding;
using Microsoft.AspNetCore.Mvc.RazorPages.Infrastructure;
namespace Microsoft.AspNetCore.Mvc.RazorPages.Internal
{
internal static class PageBinderFactory
{
internal static readonly Func<PageContext, object, Task> NullPropertyBinder = (context, arguments) => Task.CompletedTask;
internal static readonly PageHandlerBinderDelegate NullHandlerBinder = (context, arguments) => Task.CompletedTask;
public static Func<PageContext, object, Task> CreatePropertyBinder(
ParameterBinder parameterBinder,
IModelMetadataProvider modelMetadataProvider,
IModelBinderFactory modelBinderFactory,
CompiledPageActionDescriptor actionDescriptor)
{
if (parameterBinder == null)
{
throw new ArgumentNullException(nameof(parameterBinder));
}
if (actionDescriptor == null)
{
throw new ArgumentNullException(nameof(actionDescriptor));
}
var properties = actionDescriptor.BoundProperties;
if (properties == null || properties.Count == 0)
{
return NullPropertyBinder;
}
var handlerType = actionDescriptor.HandlerTypeInfo.AsType();
var propertyBindingInfo = new BinderItem[properties.Count];
for (var i = 0; i < properties.Count; i++)
{
var property = properties[i];
var metadata = modelMetadataProvider.GetMetadataForProperty(handlerType, property.Name);
var binder = modelBinderFactory.CreateBinder(new ModelBinderFactoryContext
{
BindingInfo = property.BindingInfo,
Metadata = metadata,
CacheToken = property,
});
propertyBindingInfo[i] = new BinderItem(binder, metadata);
}
return Bind;
async Task Bind(PageContext pageContext, object instance)
{
var valueProvider = await CompositeValueProvider.CreateAsync(pageContext, pageContext.ValueProviderFactories);
for (var i = 0; i < properties.Count; i++)
{
var property = properties[i];
var bindingInfo = propertyBindingInfo[i];
var modelMetadata = bindingInfo.ModelMetadata;
if (!modelMetadata.IsBindingAllowed)
{
continue;
}
var result = await parameterBinder.BindModelAsync(
pageContext,
bindingInfo.ModelBinder,
valueProvider,
property,
modelMetadata,
value: null);
if (result.IsModelSet)
{
PropertyValueSetter.SetValue(bindingInfo.ModelMetadata, instance, result.Model);
}
}
}
}
public static PageHandlerBinderDelegate CreateHandlerBinder(
ParameterBinder parameterBinder,
IModelMetadataProvider modelMetadataProvider,
IModelBinderFactory modelBinderFactory,
CompiledPageActionDescriptor actionDescriptor,
HandlerMethodDescriptor handler)
{
if (handler.Parameters == null || handler.Parameters.Count == 0)
{
return NullHandlerBinder;
}
var handlerType = actionDescriptor.HandlerTypeInfo.AsType();
var parameterBindingInfo = new BinderItem[handler.Parameters.Count];
for (var i = 0; i < parameterBindingInfo.Length; i++)
{
var parameter = handler.Parameters[i];
ModelMetadata metadata;
if (modelMetadataProvider is ModelMetadataProvider modelMetadataProviderBase)
{
// The default model metadata provider derives from ModelMetadataProvider
// and can therefore supply information about attributes applied to parameters.
metadata = modelMetadataProviderBase.GetMetadataForParameter(parameter.ParameterInfo);
}
else
{
// For backward compatibility, if there's a custom model metadata provider that
// only implements the older IModelMetadataProvider interface, access the more
// limited metadata information it supplies. In this scenario, validation attributes
// are not supported on parameters.
metadata = modelMetadataProvider.GetMetadataForType(parameter.ParameterType);
}
var binder = modelBinderFactory.CreateBinder(new ModelBinderFactoryContext
{
BindingInfo = parameter.BindingInfo,
Metadata = metadata,
CacheToken = parameter,
});
parameterBindingInfo[i] = new BinderItem(binder, metadata);
}
return Bind;
async Task Bind(PageContext pageContext, IDictionary<string, object> arguments)
{
var valueProvider = await CompositeValueProvider.CreateAsync(pageContext, pageContext.ValueProviderFactories);
for (var i = 0; i < parameterBindingInfo.Length; i++)
{
var parameter = handler.Parameters[i];
var bindingInfo = parameterBindingInfo[i];
var modelMetadata = bindingInfo.ModelMetadata;
if (!modelMetadata.IsBindingAllowed)
{
continue;
}
var result = await parameterBinder.BindModelAsync(
pageContext,
bindingInfo.ModelBinder,
valueProvider,
parameter,
modelMetadata,
value: null);
if (result.IsModelSet)
{
arguments[parameter.Name] = result.Model;
}
}
}
}
private struct BinderItem
{
public BinderItem(IModelBinder modelBinder, ModelMetadata modelMetadata)
{
ModelMetadata = modelMetadata;
ModelBinder = modelBinder;
}
public ModelMetadata ModelMetadata { get; }
public IModelBinder ModelBinder { get; }
}
}
}

View File

@ -0,0 +1,10 @@
// 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;
namespace Microsoft.AspNetCore.Mvc.RazorPages.Internal
{
public delegate Task PageHandlerBinderDelegate(PageContext pageContext, IDictionary<string, object> arguments);
}

View File

@ -0,0 +1,9 @@
// 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.Threading.Tasks;
namespace Microsoft.AspNetCore.Mvc.RazorPages.Internal
{
public delegate Task<IActionResult> PageHandlerExecutorDelegate(object handler, object[] arguments);
}

View File

@ -1,69 +0,0 @@
// 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.Collections.Generic;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Mvc.Abstractions;
using Microsoft.AspNetCore.Mvc.Internal;
using Microsoft.AspNetCore.Mvc.ModelBinding;
namespace Microsoft.AspNetCore.Mvc.RazorPages.Internal
{
public static class PagePropertyBinderFactory
{
public static Func<PageContext, object, Task> CreateBinder(
ParameterBinder parameterBinder,
IModelMetadataProvider modelMetadataProvider,
CompiledPageActionDescriptor actionDescriptor)
{
if (parameterBinder == null)
{
throw new ArgumentNullException(nameof(parameterBinder));
}
if (actionDescriptor == null)
{
throw new ArgumentNullException(nameof(actionDescriptor));
}
var properties = actionDescriptor.BoundProperties;
if (properties == null || properties.Count == 0)
{
return null;
}
var type = actionDescriptor.HandlerTypeInfo.AsType();
var metadata = new ModelMetadata[properties.Count];
for (var i = 0; i < properties.Count; i++)
{
metadata[i] = modelMetadataProvider.GetMetadataForProperty(type, properties[i].Name);
}
return Bind;
Task Bind(PageContext pageContext, object instance)
{
return BindPropertiesAsync(parameterBinder, pageContext, instance, properties, metadata);
}
}
private static async Task BindPropertiesAsync(
ParameterBinder parameterBinder,
PageContext pageContext,
object instance,
IList<ParameterDescriptor> properties,
IList<ModelMetadata> metadata)
{
var valueProvider = await CompositeValueProvider.CreateAsync(pageContext, pageContext.ValueProviderFactories);
for (var i = 0; i < properties.Count; i++)
{
var result = await parameterBinder.BindModelAsync(pageContext, valueProvider, properties[i]);
if (result.IsModelSet)
{
PropertyValueSetter.SetValue(metadata[i], instance, result.Model);
}
}
}
}
}

View File

@ -463,5 +463,26 @@ Hello from /Pages/Shared/";
Assert.Equal(HttpStatusCode.Redirect, response.StatusCode);
Assert.Equal("/", response.Headers.Location.ToString());
}
[Fact]
public async Task ValidationAttributes_OnTopLevelProperties()
{
// Act
var response = await Client.GetStringAsync("/Validation/PageWithValidation?age=71");
// Assert
Assert.Contains("Name is required", response);
Assert.Contains("18 &#x2264; Age &#x2264; 60", response);
}
[Fact]
public async Task ValidationAttributes_OnHandlerParameters()
{
// Act
var response = await Client.GetStringAsync("/Validation/PageHandlerWithValidation");
// Assert
Assert.Contains("Name is required", response);
}
}
}

View File

@ -486,6 +486,7 @@ namespace Microsoft.AspNetCore.Mvc.RazorPages.Internal
}
var modelMetadataProvider = TestModelMetadataProvider.CreateDefaultProvider();
var modelBinderFactory = TestModelBinderFactory.CreateDefault();
var parameterBinder = new ParameterBinder(
modelMetadataProvider,
TestModelBinderFactory.CreateDefault(),
@ -501,6 +502,7 @@ namespace Microsoft.AspNetCore.Mvc.RazorPages.Internal
new IFilterProvider[0],
parameterBinder,
modelMetadataProvider,
modelBinderFactory,
tempDataFactory.Object,
Options.Create(new MvcOptions()),
Options.Create(new HtmlHelperOptions()),

View File

@ -1102,7 +1102,7 @@ namespace Microsoft.AspNetCore.Mvc.RazorPages.Internal
BoundProperties = new List<ParameterDescriptor>(),
};
var handlers = new List<Func<object, object[], Task<IActionResult>>>();
var handlers = new List<PageHandlerExecutorDelegate>();
if (result != null)
{
handlers.Add((obj, args) => Task.FromResult(result));
@ -1135,7 +1135,8 @@ namespace Microsoft.AspNetCore.Mvc.RazorPages.Internal
Func<PageContext, object> modelFactory = null,
ITempDataDictionaryFactory tempDataFactory = null,
IList<IValueProviderFactory> valueProviderFactories = null,
Func<object, object[], Task<IActionResult>>[] handlers = null,
PageHandlerExecutorDelegate[] handlers = null,
PageHandlerBinderDelegate[] handlerBinders = null,
RouteData routeData = null,
ILogger logger = null,
TestDiagnosticListener listener = null)
@ -1178,13 +1179,15 @@ namespace Microsoft.AspNetCore.Mvc.RazorPages.Internal
if (handlers == null)
{
handlers = new Func<object, object[], Task<IActionResult>>[actionDescriptor.HandlerMethods.Count];
handlers = new PageHandlerExecutorDelegate[actionDescriptor.HandlerMethods.Count];
for (var i = 0; i < handlers.Length; i++)
{
handlers[i] = (obj, args) => Task.FromResult<IActionResult>(new PageResult());
}
}
handlerBinders = handlerBinders ?? Array.Empty<PageHandlerBinderDelegate>();
if (modelFactory == null)
{
modelFactory = _ => Activator.CreateInstance(actionDescriptor.ModelTypeInfo.AsType());
@ -1199,6 +1202,7 @@ namespace Microsoft.AspNetCore.Mvc.RazorPages.Internal
(c, model) => { (model as IDisposable)?.Dispose(); },
null,
handlers,
handlerBinders,
null,
new FilterItem[0]);

View File

@ -3,6 +3,7 @@
using System;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using System.Reflection;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Http;
@ -18,7 +19,7 @@ using Xunit;
namespace Microsoft.AspNetCore.Mvc.RazorPages.Internal
{
public class PagePropertyBinderFactoryTest
public class PageBinderFactoryTest
{
[Fact]
public void GetModelBinderFactory_ReturnsNullIfPageHasNoBoundProperties()
@ -29,17 +30,19 @@ namespace Microsoft.AspNetCore.Mvc.RazorPages.Internal
PageTypeInfo = typeof(PageWithNoBoundProperties).GetTypeInfo(),
};
var modelMetadataProvider = TestModelMetadataProvider.CreateDefaultProvider();
var modelBinderFactory = TestModelBinderFactory.CreateDefault();
var binder = new ParameterBinder(
modelMetadataProvider,
TestModelBinderFactory.CreateDefault(),
modelBinderFactory,
Mock.Of<IModelValidatorProvider>(),
NullLoggerFactory.Instance);
// Act
var factory = PagePropertyBinderFactory.CreateBinder(binder, modelMetadataProvider, actionDescriptor);
var factory = PageBinderFactory.CreatePropertyBinder(binder, modelMetadataProvider, modelBinderFactory, actionDescriptor);
// Assert
Assert.Null(factory);
Assert.Same(PageBinderFactory.NullPropertyBinder, factory);
}
[Fact]
@ -52,18 +55,19 @@ namespace Microsoft.AspNetCore.Mvc.RazorPages.Internal
ModelTypeInfo = typeof(PageModelWithNoBoundProperties).GetTypeInfo(),
};
var modelMetadataProvider = TestModelMetadataProvider.CreateDefaultProvider();
var modelBinderFactory = TestModelBinderFactory.CreateDefault();
var binder = new ParameterBinder(
TestModelMetadataProvider.CreateDefaultProvider(),
TestModelBinderFactory.CreateDefault(),
modelMetadataProvider,
modelBinderFactory,
Mock.Of<IModelValidatorProvider>(),
NullLoggerFactory.Instance);
// Act
var factory = PagePropertyBinderFactory.CreateBinder(binder, modelMetadataProvider, actionDescriptor);
var factory = PageBinderFactory.CreatePropertyBinder(binder, modelMetadataProvider, modelBinderFactory, actionDescriptor);
// Assert
Assert.Null(factory);
Assert.Same(PageBinderFactory.NullPropertyBinder, factory);
}
[Fact]
@ -75,17 +79,19 @@ namespace Microsoft.AspNetCore.Mvc.RazorPages.Internal
PageTypeInfo = typeof(PageWithNoVisibleBoundProperties).GetTypeInfo(),
};
var modelMetadataProvider = TestModelMetadataProvider.CreateDefaultProvider();
var modelBinderFactory = TestModelBinderFactory.CreateDefault();
var binder = new ParameterBinder(
modelMetadataProvider,
TestModelBinderFactory.CreateDefault(),
modelBinderFactory,
Mock.Of<IModelValidatorProvider>(),
NullLoggerFactory.Instance);
// Act
var factory = PagePropertyBinderFactory.CreateBinder(binder, modelMetadataProvider, actionDescriptor);
var factory = PageBinderFactory.CreatePropertyBinder(binder, modelMetadataProvider, modelBinderFactory, actionDescriptor);
// Assert
Assert.Null(factory);
Assert.Same(PageBinderFactory.NullPropertyBinder, factory);
}
[Fact]
@ -98,17 +104,19 @@ namespace Microsoft.AspNetCore.Mvc.RazorPages.Internal
ModelTypeInfo = typeof(PageModelWithNoVisibleBoundProperties).GetTypeInfo(),
};
var modelMetadataProvider = TestModelMetadataProvider.CreateDefaultProvider();
var modelBinderFactory = TestModelBinderFactory.CreateDefault();
var binder = new ParameterBinder(
modelMetadataProvider,
TestModelBinderFactory.CreateDefault(),
modelBinderFactory,
Mock.Of<IModelValidatorProvider>(),
NullLoggerFactory.Instance);
// Act
var factory = PagePropertyBinderFactory.CreateBinder(binder, modelMetadataProvider, actionDescriptor);
var factory = PageBinderFactory.CreatePropertyBinder(binder, modelMetadataProvider, modelBinderFactory, actionDescriptor);
// Assert
Assert.Null(factory);
Assert.Same(PageBinderFactory.NullPropertyBinder, factory);
}
[Fact]
@ -120,17 +128,19 @@ namespace Microsoft.AspNetCore.Mvc.RazorPages.Internal
PageTypeInfo = typeof(PageWithReadOnlyProperties).GetTypeInfo(),
};
var modelMetadataProvider = TestModelMetadataProvider.CreateDefaultProvider();
var modelBinderFactory = TestModelBinderFactory.CreateDefault();
var binder = new ParameterBinder(
modelMetadataProvider,
TestModelBinderFactory.CreateDefault(),
modelBinderFactory,
Mock.Of<IModelValidatorProvider>(),
NullLoggerFactory.Instance);
// Act
var factory = PagePropertyBinderFactory.CreateBinder(binder, modelMetadataProvider, actionDescriptor);
var factory = PageBinderFactory.CreatePropertyBinder(binder, modelMetadataProvider, modelBinderFactory, actionDescriptor);
// Assert
Assert.Null(factory);
Assert.Same(PageBinderFactory.NullPropertyBinder, factory);
}
[Fact]
@ -143,17 +153,19 @@ namespace Microsoft.AspNetCore.Mvc.RazorPages.Internal
ModelTypeInfo = typeof(PageModelWithReadOnlyProperties).GetTypeInfo(),
};
var modelMetadataProvider = TestModelMetadataProvider.CreateDefaultProvider();
var modelBinderFactory = TestModelBinderFactory.CreateDefault();
var binder = new ParameterBinder(
modelMetadataProvider,
TestModelBinderFactory.CreateDefault(),
modelBinderFactory,
Mock.Of<IModelValidatorProvider>(),
NullLoggerFactory.Instance);
// Act
var factory = PagePropertyBinderFactory.CreateBinder(binder, modelMetadataProvider, actionDescriptor);
var factory = PageBinderFactory.CreatePropertyBinder(binder, modelMetadataProvider, modelBinderFactory, actionDescriptor);
// Assert
Assert.Null(factory);
Assert.Same(PageBinderFactory.NullPropertyBinder, factory);
}
[Fact]
@ -190,6 +202,7 @@ namespace Microsoft.AspNetCore.Mvc.RazorPages.Internal
};
var modelMetadataProvider = TestModelMetadataProvider.CreateDefaultProvider();
var modelBinderFactory = TestModelBinderFactory.CreateDefault();
var binder = new TestParameterBinder(new Dictionary<string, object>
{
@ -197,7 +210,7 @@ namespace Microsoft.AspNetCore.Mvc.RazorPages.Internal
{ nameof(PageWithProperty.RouteDifferentValue), "route-value" }
});
var factory = PagePropertyBinderFactory.CreateBinder(binder, modelMetadataProvider, actionDescriptor);
var factory = PageBinderFactory.CreatePropertyBinder(binder, modelMetadataProvider, modelBinderFactory, actionDescriptor);
var page = new PageWithProperty
{
@ -255,7 +268,9 @@ namespace Microsoft.AspNetCore.Mvc.RazorPages.Internal
});
var modelMetadataProvider = TestModelMetadataProvider.CreateDefaultProvider();
var factory = PagePropertyBinderFactory.CreateBinder(binder, modelMetadataProvider, actionDescriptor);
var modelBinderFactory = TestModelBinderFactory.CreateDefault();
var factory = PageBinderFactory.CreatePropertyBinder(binder, modelMetadataProvider, modelBinderFactory, actionDescriptor);
var page = new PageWithProperty
{
@ -303,7 +318,9 @@ namespace Microsoft.AspNetCore.Mvc.RazorPages.Internal
var binder = new TestParameterBinder(new Dictionary<string, object>());
var modelMetadataProvider = TestModelMetadataProvider.CreateDefaultProvider();
var factory = PagePropertyBinderFactory.CreateBinder(binder, modelMetadataProvider, actionDescriptor);
var modelBinderFactory = TestModelBinderFactory.CreateDefault();
var factory = PageBinderFactory.CreatePropertyBinder(binder, modelMetadataProvider, modelBinderFactory, actionDescriptor);
var page = new PageWithProperty
{
@ -364,7 +381,9 @@ namespace Microsoft.AspNetCore.Mvc.RazorPages.Internal
});
var modelMetadataProvider = TestModelMetadataProvider.CreateDefaultProvider();
var factory = PagePropertyBinderFactory.CreateBinder(binder, modelMetadataProvider, actionDescriptor);
var modelBinderFactory = TestModelBinderFactory.CreateDefault();
var factory = PageBinderFactory.CreatePropertyBinder(binder, modelMetadataProvider, modelBinderFactory, actionDescriptor);
var page = new PageWithProperty
{
@ -421,7 +440,9 @@ namespace Microsoft.AspNetCore.Mvc.RazorPages.Internal
});
var modelMetadataProvider = TestModelMetadataProvider.CreateDefaultProvider();
var factory = PagePropertyBinderFactory.CreateBinder(binder, modelMetadataProvider, actionDescriptor);
var modelBinderFactory = TestModelBinderFactory.CreateDefault();
var factory = PageBinderFactory.CreatePropertyBinder(binder, modelMetadataProvider, modelBinderFactory, actionDescriptor);
var page = new PageWithProperty
{
@ -440,6 +461,264 @@ namespace Microsoft.AspNetCore.Mvc.RazorPages.Internal
Assert.Equal("value", model.Default);
}
[Fact]
public async Task CreatePropertyBinder_SkipsBindingPropertiesWithBindNever()
{
// Arrange
var type = typeof(PageModelWithBindNeverProperty).GetTypeInfo();
var actionDescriptor = new CompiledPageActionDescriptor
{
BoundProperties = new[]
{
new PageBoundPropertyDescriptor()
{
Name = nameof(PageModelWithBindNeverProperty.BindNeverProperty),
ParameterType = typeof(string),
Property = type.GetProperty(nameof(PageModelWithBindNeverProperty.BindNeverProperty)),
},
},
HandlerTypeInfo = type,
PageTypeInfo = typeof(PageWithProperty).GetTypeInfo(),
ModelTypeInfo = type,
};
var binder = new TestParameterBinder(new Dictionary<string, object>
{
{ nameof(PageModelWithBindNeverProperty.BindNeverProperty), "value" },
});
var modelMetadataProvider = TestModelMetadataProvider.CreateDefaultProvider();
var modelBinderFactory = TestModelBinderFactory.CreateDefault();
var factory = PageBinderFactory.CreatePropertyBinder(binder, modelMetadataProvider, modelBinderFactory, actionDescriptor);
var page = new PageWithProperty
{
PageContext = GetPageContext()
};
var model = new PageModelWithBindNeverProperty();
// Act
await factory(page.PageContext, model);
// Assert
Assert.Null(model.BindNeverProperty);
}
[Fact]
public async Task CreatePropertyBinder_ValidatesTopLevelProperties()
{
// Arrange
var type = typeof(PageModelWithValidation).GetTypeInfo();
var actionDescriptor = new CompiledPageActionDescriptor
{
BoundProperties = new[]
{
new PageBoundPropertyDescriptor()
{
Name = nameof(PageModelWithValidation.Validated),
ParameterType = typeof(string),
Property = type.GetProperty(nameof(PageModelWithValidation.Validated)),
},
},
HandlerTypeInfo = type,
PageTypeInfo = typeof(PageWithProperty).GetTypeInfo(),
ModelTypeInfo = type,
};
var modelMetadataProvider = TestModelMetadataProvider.CreateDefaultProvider();
var modelBinderFactory = TestModelBinderFactory.CreateDefault();
var validatorProvider = TestModelValidatorProvider.CreateDefaultProvider();
var binder = new ParameterBinder(
modelMetadataProvider,
modelBinderFactory,
validatorProvider,
NullLoggerFactory.Instance);
var factory = PageBinderFactory.CreatePropertyBinder(binder, modelMetadataProvider, modelBinderFactory, actionDescriptor);
var page = new PageWithProperty
{
PageContext = GetPageContext()
};
var model = new PageModelWithValidation();
// Act
await factory(page.PageContext, model);
// Assert
var modelState = page.PageContext.ModelState;
Assert.False(modelState.IsValid);
Assert.Collection(
modelState,
kvp =>
{
Assert.Equal(nameof(PageModelWithValidation.Validated), kvp.Key);
});
}
[Fact]
public async Task CreateHandlerBinder_BindsHandlerParameters()
{
// Arrange
var type = typeof(PageModelWithExecutors);
var actionDescriptor = GetActionDescriptorWithHandlerMethod(type, nameof(PageModelWithExecutors.OnGet));
// Act
var parameterBinder = new TestParameterBinder(new Dictionary<string, object>()
{
{ "id", "value" },
});
var modelMetadataProvider = TestModelMetadataProvider.CreateDefaultProvider();
var modelBinderFactory = TestModelBinderFactory.CreateDefault();
var factory = PageBinderFactory.CreateHandlerBinder(
parameterBinder,
modelMetadataProvider,
modelBinderFactory,
actionDescriptor,
actionDescriptor.HandlerMethods[0]);
var page = new PageWithProperty
{
PageContext = GetPageContext()
};
var model = new PageModelWithExecutors();
var arguments = new Dictionary<string, object>();
// Act
await factory(page.PageContext, arguments);
// Assert
Assert.Collection(
arguments,
kvp =>
{
Assert.Equal("id", kvp.Key);
Assert.Equal("value", kvp.Value);
});
}
[Fact]
public async Task CreateHandlerBinder_SkipBindingParametersThatDisallowBinding()
{
// Arrange
var type = typeof(PageModelWithExecutors);
var actionDescriptor = GetActionDescriptorWithHandlerMethod(type, nameof(PageModelWithExecutors.OnGetWithBindNever));
// Act
var parameterBinder = new TestParameterBinder(new Dictionary<string, object>()
{
{ "id", "value" },
});
var modelMetadataProvider = TestModelMetadataProvider.CreateDefaultProvider();
var modelBinderFactory = TestModelBinderFactory.CreateDefault();
var factory = PageBinderFactory.CreateHandlerBinder(
parameterBinder,
modelMetadataProvider,
modelBinderFactory,
actionDescriptor,
actionDescriptor.HandlerMethods[0]);
var page = new PageWithProperty
{
PageContext = GetPageContext()
};
var model = new PageModelWithExecutors();
var arguments = new Dictionary<string, object>();
// Act
await factory(page.PageContext, arguments);
// Assert
Assert.Empty(arguments);
}
[Fact]
public async Task CreateHandlerBinder_ValidatesTopLevelParameters()
{
// Arrange
var type = typeof(PageModelWithExecutors);
var actionDescriptor = GetActionDescriptorWithHandlerMethod(type, nameof(PageModelWithExecutors.OnPostWithValidation));
// Act
var modelMetadataProvider = TestModelMetadataProvider.CreateDefaultProvider();
var modelBinderFactory = TestModelBinderFactory.CreateDefault();
var validatorProvider = TestModelValidatorProvider.CreateDefaultProvider();
var parameterBinder = new ParameterBinder(modelMetadataProvider, modelBinderFactory, validatorProvider, NullLoggerFactory.Instance);
var factory = PageBinderFactory.CreateHandlerBinder(
parameterBinder,
modelMetadataProvider,
modelBinderFactory,
actionDescriptor,
actionDescriptor.HandlerMethods[0]);
var page = new PageWithProperty
{
PageContext = GetPageContext()
};
var model = new PageModelWithExecutors();
var arguments = new Dictionary<string, object>();
// Act
await factory(page.PageContext, arguments);
// Assert
var modelState = page.PageContext.ModelState;
Assert.False(modelState.IsValid);
Assert.Collection(
page.PageContext.ModelState,
kvp =>
{
Assert.Equal("name", kvp.Key);
});
}
private static CompiledPageActionDescriptor GetActionDescriptorWithHandlerMethod(Type type, string method)
{
var handlerMethodInfo = type.GetMethod(method);
var parameterInfo = handlerMethodInfo.GetParameters()[0];
var actionDescriptor = new CompiledPageActionDescriptor
{
HandlerTypeInfo = type.GetTypeInfo(),
HandlerMethods = new[]
{
new HandlerMethodDescriptor
{
HttpMethod = "Post",
MethodInfo = handlerMethodInfo,
Parameters = new[]
{
new HandlerParameterDescriptor
{
ParameterInfo = parameterInfo,
ParameterType = parameterInfo.ParameterType,
Name = parameterInfo.Name
},
},
},
},
};
return actionDescriptor;
}
private PageContext GetPageContext(string httpMethod = null)
{
var services = new ServiceCollection();
@ -477,7 +756,13 @@ namespace Microsoft.AspNetCore.Mvc.RazorPages.Internal
public IList<ParameterDescriptor> Descriptors { get; } = new List<ParameterDescriptor>();
public override Task<ModelBindingResult> BindModelAsync(ActionContext actionContext, IValueProvider valueProvider, ParameterDescriptor parameter, object value)
public override Task<ModelBindingResult> BindModelAsync(
ActionContext actionContext,
IModelBinder modelBinder,
IValueProvider valueProvider,
ParameterDescriptor parameter,
ModelMetadata metadata,
object value)
{
Descriptors.Add(parameter);
@ -596,5 +881,34 @@ namespace Microsoft.AspNetCore.Mvc.RazorPages.Internal
public string Default { get; set; }
}
private class PageModelWithBindNeverProperty
{
[BindNever]
public string BindNeverProperty { get; set; }
}
private class PageModelWithValidation
{
[Required]
public string Validated { get; set; }
}
private class PageModelWithExecutors
{
public void OnGetWithBindNever([BindNever] string id)
{
}
public void OnGet(string id)
{
}
public void OnPostWithValidation([Required] string name)
{
}
}
}
}

View File

@ -0,0 +1,10 @@
@page
@using System.ComponentModel.DataAnnotations
@functions
{
public void OnGet([Required(ErrorMessage = "Name is required")] string name)
{
}
}
<div asp-validation-summary="All"></div>

View File

@ -0,0 +1,17 @@
using System.ComponentModel.DataAnnotations;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.RazorPages;
namespace RazorPagesWebSite
{
public class PageWithValidation : PageModel
{
[BindProperty(SupportsGet = true)]
[Required(ErrorMessage = "Name is required.")]
public string Name { get; set; }
[BindProperty(SupportsGet = true)]
[Range(18, 60, ErrorMessage = "18 ≤ Age ≤ 60")]
public int Age { get; set; }
}
}

View File

@ -0,0 +1,3 @@
@page
@model PageWithValidation
<div asp-validation-summary="All"></div>

View File

@ -0,0 +1 @@
@addTagHelper "*, Microsoft.AspNetCore.Mvc.TagHelpers"