Merge in 'release/5.0-preview8' changes

This commit is contained in:
dotnet-bot 2020-07-24 15:46:59 +00:00
commit 670f9523df
59 changed files with 14348 additions and 3889 deletions

View File

@ -6,12 +6,17 @@ using System;
namespace Microsoft.AspNetCore.Mvc.ModelBinding
{
/// <summary>
/// Provides a predicate which can determines which model properties should be bound by model binding.
/// Provides a predicate which can determines which model properties or parameters should be bound by model binding.
/// </summary>
public interface IPropertyFilterProvider
{
/// <summary>
/// <para>
/// Gets a predicate which can determines which model properties should be bound by model binding.
/// </para>
/// <para>
/// This predicate is also used to determine which parameters are bound when a model's constructor is bound.
/// </para>
/// </summary>
Func<ModelMetadata, bool> PropertyFilter { get; }
}

View File

@ -51,7 +51,7 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding.Metadata
/// <summary>
/// Error message the model binding system adds when <see cref="ModelError.Exception"/> is of type
/// <see cref="FormatException"/> or <see cref="OverflowException"/>, value is known, and error is associated
/// with a collection element or action parameter.
/// with a collection element or parameter.
/// </summary>
/// <value>Default <see cref="string"/> is "The value '{0}' is not valid.".</value>
public virtual Func<string, string> NonPropertyAttemptedValueIsInvalidAccessor { get; } = default!;
@ -67,7 +67,7 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding.Metadata
/// <summary>
/// Error message the model binding system adds when <see cref="ModelError.Exception"/> is of type
/// <see cref="FormatException"/> or <see cref="OverflowException"/>, value is unknown, and error is associated
/// with a collection element or action parameter.
/// with a collection element or parameter.
/// </summary>
/// <value>Default <see cref="string"/> is "The supplied value is invalid.".</value>
public virtual Func<string> NonPropertyUnknownValueIsInvalidAccessor { get; } = default!;

View File

@ -16,12 +16,14 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding.Metadata
Type modelType,
string? name = null,
Type? containerType = null,
object? fieldInfo = null)
object? fieldInfo = null,
ConstructorInfo? constructorInfo = null)
{
ModelType = modelType;
Name = name;
ContainerType = containerType;
FieldInfo = fieldInfo;
ConstructorInfo = constructorInfo;
}
/// <summary>
@ -130,6 +132,28 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding.Metadata
return new ModelMetadataIdentity(modelType, parameter.Name, fieldInfo: parameter);
}
/// <summary>
/// Creates a <see cref="ModelMetadataIdentity"/> for the provided parameter with the specified
/// model type.
/// </summary>
/// <param name="constructor">The <see cref="ConstructorInfo" />.</param>
/// <param name="modelType">The model type.</param>
/// <returns>A <see cref="ModelMetadataIdentity"/>.</returns>
public static ModelMetadataIdentity ForConstructor(ConstructorInfo constructor, Type modelType)
{
if (constructor == null)
{
throw new ArgumentNullException(nameof(constructor));
}
if (modelType == null)
{
throw new ArgumentNullException(nameof(modelType));
}
return new ModelMetadataIdentity(modelType, constructor.Name, constructorInfo: constructor);
}
/// <summary>
/// Gets the <see cref="Type"/> defining the model property represented by the current
/// instance, or <c>null</c> if the current instance does not represent a property.
@ -152,6 +176,10 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding.Metadata
{
return ModelMetadataKind.Parameter;
}
else if (ConstructorInfo != null)
{
return ModelMetadataKind.Constructor;
}
else if (ContainerType != null && Name != null)
{
return ModelMetadataKind.Property;
@ -183,6 +211,12 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding.Metadata
/// </summary>
public PropertyInfo? PropertyInfo => FieldInfo as PropertyInfo;
/// <summary>
/// Gets a descriptor for the constructor, or <c>null</c> if this instance
/// does not represent a constructor.
/// </summary>
public ConstructorInfo? ConstructorInfo { get; }
/// <inheritdoc />
public bool Equals(ModelMetadataIdentity other)
{
@ -191,7 +225,8 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding.Metadata
ModelType == other.ModelType &&
Name == other.Name &&
ParameterInfo == other.ParameterInfo &&
PropertyInfo == other.PropertyInfo;
PropertyInfo == other.PropertyInfo &&
ConstructorInfo == other.ConstructorInfo;
}
/// <inheritdoc />
@ -210,6 +245,7 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding.Metadata
hash.Add(Name, StringComparer.Ordinal);
hash.Add(ParameterInfo);
hash.Add(PropertyInfo);
hash.Add(ConstructorInfo);
return hash.ToHashCode();
}
}

View File

@ -22,5 +22,10 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding.Metadata
/// Used for <see cref="ModelMetadata"/> for a parameter.
/// </summary>
Parameter,
/// <summary>
/// <see cref="ModelMetadata"/> for a constructor.
/// </summary>
Constructor,
}
}
}

View File

@ -4,8 +4,10 @@
using System;
using System.Collections;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.ComponentModel;
using System.Diagnostics;
using System.Linq;
using System.Reflection;
using Microsoft.AspNetCore.Mvc.ModelBinding.Metadata;
using Microsoft.AspNetCore.Mvc.ModelBinding.Validation;
@ -24,7 +26,11 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding
/// </summary>
public static readonly int DefaultOrder = 10000;
private static readonly IReadOnlyDictionary<ModelMetadata, ModelMetadata> EmptyParameterMapping = new Dictionary<ModelMetadata, ModelMetadata>(0);
private int? _hashCode;
private IReadOnlyList<ModelMetadata>? _boundProperties;
private IReadOnlyDictionary<ModelMetadata, ModelMetadata>? _parameterMapping;
/// <summary>
/// Creates a new <see cref="ModelMetadata"/>.
@ -83,7 +89,7 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding
/// <summary>
/// Gets the key for the current instance.
/// </summary>
protected ModelMetadataIdentity Identity { get; }
protected internal ModelMetadataIdentity Identity { get; }
/// <summary>
/// Gets a collection of additional information about the model.
@ -95,6 +101,88 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding
/// </summary>
public abstract ModelPropertyCollection Properties { get; }
internal IReadOnlyList<ModelMetadata> BoundProperties
{
get
{
// In record types, each constructor parameter in the primary constructor is also a settable property with the same name.
// Executing model binding on these parameters twice may have detrimental effects, such as duplicate ModelState entries,
// or failures if a model expects to be bound exactly ones.
// Consequently when binding to a constructor, we only bind and validate the subset of properties whose names
// haven't appeared as parameters.
if (BoundConstructor is null)
{
return Properties;
}
if (_boundProperties is null)
{
var boundParameters = BoundConstructor.BoundConstructorParameters!;
var boundProperties = new List<ModelMetadata>();
foreach (var metadata in Properties)
{
if (!boundParameters.Any(p =>
string.Equals(p.ParameterName, metadata.PropertyName, StringComparison.Ordinal)
&& p.ModelType == metadata.ModelType))
{
boundProperties.Add(metadata);
}
}
_boundProperties = boundProperties;
}
return _boundProperties;
}
}
internal IReadOnlyDictionary<ModelMetadata, ModelMetadata> BoundConstructorParameterMapping
{
get
{
if (_parameterMapping != null)
{
return _parameterMapping;
}
if (BoundConstructor is null)
{
_parameterMapping = EmptyParameterMapping;
return _parameterMapping;
}
var boundParameters = BoundConstructor.BoundConstructorParameters!;
var parameterMapping = new Dictionary<ModelMetadata, ModelMetadata>();
foreach (var parameter in boundParameters)
{
var property = Properties.FirstOrDefault(p =>
string.Equals(p.Name, parameter.ParameterName, StringComparison.Ordinal) &&
p.ModelType == parameter.ModelType);
if (property != null)
{
parameterMapping[parameter] = property;
}
}
_parameterMapping = parameterMapping;
return _parameterMapping;
}
}
/// <summary>
/// Gets <see cref="ModelMetadata"/> instance for a constructor of a record type that is used during binding and validation.
/// </summary>
public virtual ModelMetadata? BoundConstructor { get; }
/// <summary>
/// Gets the collection of <see cref="ModelMetadata"/> instances for parameters on a <see cref="BoundConstructor"/>.
/// This is only available when <see cref="MetadataKind"/> is <see cref="ModelMetadataKind.Constructor"/>.
/// </summary>
public virtual IReadOnlyList<ModelMetadata>? BoundConstructorParameters { get; }
/// <summary>
/// Gets the name of a model if specified explicitly using <see cref="IModelNameProvider"/>.
/// </summary>
@ -401,6 +489,11 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding
/// </summary>
public abstract Action<object, object> PropertySetter { get; }
/// <summary>
/// Gets a delegate that invokes the bound constructor <see cref="BoundConstructor" /> if non-<see langword="null" />.
/// </summary>
public virtual Func<object[], object>? BoundConstructorInvoker => null;
/// <summary>
/// Gets a display name for the model.
/// </summary>
@ -500,6 +593,8 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding
return $"ModelMetadata (Property: '{ContainerType!.Name}.{PropertyName}' Type: '{ModelType.Name}')";
case ModelMetadataKind.Type:
return $"ModelMetadata (Type: '{ModelType.Name}')";
case ModelMetadataKind.Constructor:
return $"ModelMetadata (Constructor: '{ModelType.Name}')";
default:
return $"Unsupported MetadataKind '{MetadataKind}'.";
}

View File

@ -1,4 +1,4 @@
// Copyright (c) .NET Foundation. All rights reserved.
// 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;
@ -54,5 +54,16 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding
{
throw new NotSupportedException();
}
/// <summary>
/// Supplies metadata describing a constructor.
/// </summary>
/// <param name="constructor">The <see cref="ConstructorInfo"/>.</param>
/// <param name="modelType">The type declaring the constructor.</param>
/// <returns>A <see cref="ModelMetadata"/> instance describing the <paramref name="constructor"/>.</returns>
public virtual ModelMetadata GetMetadataForConstructor(ConstructorInfo constructor, Type modelType)
{
throw new NotSupportedException();
}
}
}

View File

@ -298,6 +298,7 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding
// "The value '' is not valid." (when no value was provided, not even an empty string) and
// "The supplied value is invalid for Int32." (when error is for an element or parameter).
var messageProvider = metadata.ModelBindingMessageProvider;
var name = metadata.DisplayName ?? metadata.PropertyName;
string errorMessage;
if (entry == null && name == null)

View File

@ -83,7 +83,7 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding
// Arrange
var attributes = new object[]
{
new ModelBinderAttribute { BinderType = typeof(ComplexTypeModelBinder), Name = "Test" },
new ModelBinderAttribute { BinderType = typeof(ComplexObjectModelBinder), Name = "Test" },
};
var modelType = typeof(Guid);
var provider = new TestModelMetadataProvider();
@ -100,7 +100,7 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding
// Assert
Assert.NotNull(bindingInfo);
Assert.Same(typeof(ComplexTypeModelBinder), bindingInfo.BinderType);
Assert.Same(typeof(ComplexObjectModelBinder), bindingInfo.BinderType);
Assert.Same("Test", bindingInfo.BinderModelName);
}
@ -110,7 +110,7 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding
// Arrange
var attributes = new object[]
{
new ModelBinderAttribute(typeof(ComplexTypeModelBinder)),
new ModelBinderAttribute(typeof(ComplexObjectModelBinder)),
new ControllerAttribute(),
new BindNeverAttribute(),
};
@ -129,7 +129,7 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding
// Assert
Assert.NotNull(bindingInfo);
Assert.Same(typeof(ComplexTypeModelBinder), bindingInfo.BinderType);
Assert.Same(typeof(ComplexObjectModelBinder), bindingInfo.BinderType);
Assert.Same("Different", bindingInfo.BinderModelName);
Assert.Same(BindingSource.Custom, bindingInfo.BindingSource);
}
@ -143,7 +143,7 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding
var provider = new TestModelMetadataProvider();
provider.ForType(modelType).BindingDetails(metadata =>
{
metadata.BinderType = typeof(ComplexTypeModelBinder);
metadata.BinderType = typeof(ComplexObjectModelBinder);
});
var modelMetadata = provider.GetMetadataForType(modelType);
@ -152,7 +152,7 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding
// Assert
Assert.NotNull(bindingInfo);
Assert.Same(typeof(ComplexTypeModelBinder), bindingInfo.BinderType);
Assert.Same(typeof(ComplexObjectModelBinder), bindingInfo.BinderType);
}
[Fact]
@ -187,7 +187,7 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding
// Arrange
var attributes = new object[]
{
new ModelBinderAttribute(typeof(ComplexTypeModelBinder)),
new ModelBinderAttribute(typeof(ComplexObjectModelBinder)),
new ControllerAttribute(),
new BindNeverAttribute(),
};

View File

@ -728,6 +728,12 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding
throw new NotImplementedException();
}
}
public override ModelMetadata BoundConstructor => throw new NotImplementedException();
public override Func<object[], object> BoundConstructorInvoker => throw new NotImplementedException();
public override IReadOnlyList<ModelMetadata> BoundConstructorParameters => throw new NotImplementedException();
}
private class CollectionImplementation : ICollection<string>

View File

@ -4,7 +4,7 @@ namespace Microsoft.AspNetCore.Mvc.Analyzers.TopLevelParameterNameAnalyzerTestFi
{
public class IsProblematicParameter_ReturnsTrue_IfPropertyWithModelBindingAttributeHasSameNameAsParameter
{
[ModelBinder(typeof(ComplexTypeModelBinder), Name = "model")]
[ModelBinder(typeof(ComplexObjectModelBinder), Name = "model")]
public string Different { get; set; }
public void ActionMethod(

View File

@ -5,6 +5,7 @@ using System;
using System.Collections.Generic;
using System.Linq;
using Microsoft.AspNetCore.Mvc.ModelBinding;
using Microsoft.AspNetCore.Mvc.ModelBinding.Metadata;
namespace Microsoft.AspNetCore.Mvc
{
@ -56,17 +57,23 @@ namespace Microsoft.AspNetCore.Mvc
{
if (Include != null && Include.Length > 0)
{
if (_propertyFilter == null)
{
_propertyFilter = (m) => Include.Contains(m.PropertyName, StringComparer.Ordinal);
}
_propertyFilter ??= PropertyFilter;
return _propertyFilter;
}
else
{
return _default;
}
bool PropertyFilter(ModelMetadata modelMetadata)
{
if (modelMetadata.MetadataKind == ModelMetadataKind.Parameter)
{
return Include.Contains(modelMetadata.ParameterName, StringComparer.Ordinal);
}
return Include.Contains(modelMetadata.PropertyName, StringComparer.Ordinal);
}
}
}

View File

@ -72,7 +72,7 @@ namespace Microsoft.AspNetCore.Mvc
options.ModelBinderProviders.Add(new DictionaryModelBinderProvider());
options.ModelBinderProviders.Add(new ArrayModelBinderProvider());
options.ModelBinderProviders.Add(new CollectionModelBinderProvider());
options.ModelBinderProviders.Add(new ComplexTypeModelBinderProvider());
options.ModelBinderProviders.Add(new ComplexObjectModelBinderProvider());
// Set up filters
options.Filters.Add(new UnsupportedContentTypeFilter());

View File

@ -10,7 +10,7 @@ namespace Microsoft.AspNetCore.Mvc.Infrastructure
{
internal static class ParameterDefaultValues
{
public static object[] GetParameterDefaultValues(MethodInfo methodInfo)
public static object[] GetParameterDefaultValues(MethodBase methodInfo)
{
if (methodInfo == null)
{

View File

@ -0,0 +1,752 @@
// 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.Diagnostics;
using System.Linq.Expressions;
using System.Reflection;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Mvc.Core;
using Microsoft.AspNetCore.Mvc.ModelBinding.Metadata;
using Microsoft.Extensions.Logging;
namespace Microsoft.AspNetCore.Mvc.ModelBinding.Binders
{
/// <summary>
/// <see cref="IModelBinder"/> implementation for binding complex types.
/// </summary>
public sealed class ComplexObjectModelBinder : IModelBinder
{
// Don't want a new public enum because communication between the private and internal methods of this class
// should not be exposed. Can't use an internal enum because types of [TheoryData] values must be public.
// Model contains only properties that are expected to bind from value providers and no value provider has
// matching data.
internal const int NoDataAvailable = 0;
// If model contains properties that are expected to bind from value providers, no value provider has matching
// data. Remaining (greedy) properties might bind successfully.
internal const int GreedyPropertiesMayHaveData = 1;
// Model contains at least one property that is expected to bind from value providers and a value provider has
// matching data.
internal const int ValueProviderDataAvailable = 2;
private readonly IDictionary<ModelMetadata, IModelBinder> _propertyBinders;
private readonly IReadOnlyList<IModelBinder> _parameterBinders;
private readonly ILogger _logger;
private Func<object> _modelCreator;
internal ComplexObjectModelBinder(
IDictionary<ModelMetadata, IModelBinder> propertyBinders,
IReadOnlyList<IModelBinder> parameterBinders,
ILogger<ComplexObjectModelBinder> logger)
{
_propertyBinders = propertyBinders;
_parameterBinders = parameterBinders;
_logger = logger;
}
public Task BindModelAsync(ModelBindingContext bindingContext)
{
if (bindingContext == null)
{
throw new ArgumentNullException(nameof(bindingContext));
}
_logger.AttemptingToBindModel(bindingContext);
var parameterData = CanCreateModel(bindingContext);
if (parameterData == NoDataAvailable)
{
return Task.CompletedTask;
}
// Perf: separated to avoid allocating a state machine when we don't
// need to go async.
return BindModelCoreAsync(bindingContext, parameterData);
}
private async Task BindModelCoreAsync(ModelBindingContext bindingContext, int propertyData)
{
Debug.Assert(propertyData == GreedyPropertiesMayHaveData || propertyData == ValueProviderDataAvailable);
// Create model first (if necessary) to avoid reporting errors about properties when activation fails.
var attemptedBinding = false;
var bindingSucceeded = false;
var modelMetadata = bindingContext.ModelMetadata;
if (bindingContext.Model == null)
{
var boundConstructor = modelMetadata.BoundConstructor;
if (boundConstructor != null)
{
var values = new object[boundConstructor.BoundConstructorParameters.Count];
var (attemptedParameterBinding, parameterBindingSucceeded) = await BindParametersAsync(
bindingContext,
propertyData,
boundConstructor.BoundConstructorParameters,
values);
attemptedBinding |= attemptedParameterBinding;
bindingSucceeded |= parameterBindingSucceeded;
if (!CreateModel(bindingContext, boundConstructor, values))
{
return;
}
}
else
{
CreateModel(bindingContext);
}
}
var (attemptedPropertyBinding, propertyBindingSucceeded) = await BindPropertiesAsync(
bindingContext,
propertyData,
modelMetadata.BoundProperties);
attemptedBinding |= attemptedPropertyBinding;
bindingSucceeded |= propertyBindingSucceeded;
// Have we created a top-level model despite an inability to bind anything in said model and a lack of
// other IsBindingRequired errors? Does that violate [BindRequired] on the model? This case occurs when
// 1. The top-level model has no public settable properties.
// 2. All properties in a [BindRequired] model have [BindNever] or are otherwise excluded from binding.
// 3. No data exists for any property.
if (!attemptedBinding &&
bindingContext.IsTopLevelObject &&
modelMetadata.IsBindingRequired)
{
var messageProvider = modelMetadata.ModelBindingMessageProvider;
var message = messageProvider.MissingBindRequiredValueAccessor(bindingContext.FieldName);
bindingContext.ModelState.TryAddModelError(bindingContext.ModelName, message);
}
_logger.DoneAttemptingToBindModel(bindingContext);
// Have all binders failed because no data was available?
//
// If CanCreateModel determined a property has data, failures are likely due to conversion errors. For
// example, user may submit ?[0].id=twenty&[1].id=twenty-one&[2].id=22 for a collection of a complex type
// with an int id property. In that case, the bound model should be [ {}, {}, { id = 22 }] and
// ModelState should contain errors about both [0].id and [1].id. Do not inform higher-level binders of the
// failure in this and similar cases.
//
// If CanCreateModel could not find data for non-greedy properties, failures indicate greedy binders were
// unsuccessful. For example, user may submit file attachments [0].File and [1].File but not [2].File for
// a collection of a complex type containing an IFormFile property. In that case, we have exhausted the
// attached files and checking for [3].File is likely be pointless. (And, if it had a point, would we stop
// after 10 failures, 100, or more -- all adding redundant errors to ModelState?) Inform higher-level
// binders of the failure.
//
// Required properties do not change the logic below. Missed required properties cause ModelState errors
// but do not necessarily prevent further attempts to bind.
//
// This logic is intended to maximize correctness but does not avoid infinite loops or recursion when a
// greedy model binder succeeds unconditionally.
if (!bindingContext.IsTopLevelObject &&
!bindingSucceeded &&
propertyData == GreedyPropertiesMayHaveData)
{
bindingContext.Result = ModelBindingResult.Failed();
return;
}
bindingContext.Result = ModelBindingResult.Success(bindingContext.Model);
}
internal static bool CreateModel(ModelBindingContext bindingContext, ModelMetadata boundConstructor, object[] values)
{
try
{
bindingContext.Model = boundConstructor.BoundConstructorInvoker(values);
return true;
}
catch (Exception ex)
{
AddModelError(ex, bindingContext.ModelName, bindingContext);
bindingContext.Result = ModelBindingResult.Failed();
return false;
}
}
/// <summary>
/// Creates suitable <see cref="object"/> for given <paramref name="bindingContext"/>.
/// </summary>
/// <param name="bindingContext">The <see cref="ModelBindingContext"/>.</param>
/// <returns>An <see cref="object"/> compatible with <see cref="ModelBindingContext.ModelType"/>.</returns>
internal void CreateModel(ModelBindingContext bindingContext)
{
if (bindingContext == null)
{
throw new ArgumentNullException(nameof(bindingContext));
}
// If model creator throws an exception, we want to propagate it back up the call stack, since the
// application developer should know that this was an invalid type to try to bind to.
if (_modelCreator == null)
{
// The following check causes the ComplexTypeModelBinder to NOT participate in binding structs as
// reflection does not provide information about the implicit parameterless constructor for a struct.
// This binder would eventually fail to construct an instance of the struct as the Linq's NewExpression
// compile fails to construct it.
var modelTypeInfo = bindingContext.ModelType.GetTypeInfo();
if (modelTypeInfo.IsAbstract || modelTypeInfo.GetConstructor(Type.EmptyTypes) == null)
{
var metadata = bindingContext.ModelMetadata;
switch (metadata.MetadataKind)
{
case ModelMetadataKind.Parameter:
throw new InvalidOperationException(
Resources.FormatComplexObjectModelBinder_NoSuitableConstructor_ForParameter(
modelTypeInfo.FullName,
metadata.ParameterName));
case ModelMetadataKind.Property:
throw new InvalidOperationException(
Resources.FormatComplexObjectModelBinder_NoSuitableConstructor_ForProperty(
modelTypeInfo.FullName,
metadata.PropertyName,
bindingContext.ModelMetadata.ContainerType.FullName));
case ModelMetadataKind.Type:
throw new InvalidOperationException(
Resources.FormatComplexObjectModelBinder_NoSuitableConstructor_ForType(
modelTypeInfo.FullName));
}
}
_modelCreator = Expression
.Lambda<Func<object>>(Expression.New(bindingContext.ModelType))
.Compile();
}
bindingContext.Model = _modelCreator();
}
private async ValueTask<(bool attemptedBinding, bool bindingSucceeded)> BindParametersAsync(
ModelBindingContext bindingContext,
int propertyData,
IReadOnlyList<ModelMetadata> parameters,
object[] parameterValues)
{
var attemptedBinding = false;
var bindingSucceeded = false;
if (parameters.Count == 0)
{
return (attemptedBinding, bindingSucceeded);
}
var postponePlaceholderBinding = false;
for (var i = 0; i < parameters.Count; i++)
{
var parameter = parameters[i];
var fieldName = parameter.BinderModelName ?? parameter.ParameterName;
var modelName = ModelNames.CreatePropertyModelName(bindingContext.ModelName, fieldName);
if (!CanBindItem(bindingContext, parameter))
{
continue;
}
var parameterBinder = _parameterBinders[i];
if (parameterBinder is PlaceholderBinder)
{
if (postponePlaceholderBinding)
{
// Decided to postpone binding properties that complete a loop in the model types when handling
// an earlier loop-completing property. Postpone binding this property too.
continue;
}
else if (!bindingContext.IsTopLevelObject &&
!bindingSucceeded &&
propertyData == GreedyPropertiesMayHaveData)
{
// Have no confirmation of data for the current instance. Postpone completing the loop until
// we _know_ the current instance is useful. Recursion would otherwise occur prior to the
// block with a similar condition after the loop.
//
// Example cases include an Employee class containing
// 1. a Manager property of type Employee
// 2. an Employees property of type IList<Employee>
postponePlaceholderBinding = true;
continue;
}
}
var result = await BindParameterAsync(bindingContext, parameter, parameterBinder, fieldName, modelName);
if (result.IsModelSet)
{
attemptedBinding = true;
bindingSucceeded = true;
parameterValues[i] = result.Model;
}
else if (parameter.IsBindingRequired)
{
attemptedBinding = true;
}
}
if (postponePlaceholderBinding && bindingSucceeded)
{
// Have some data for this instance. Continue with the model type loop.
for (var i = 0; i < parameters.Count; i++)
{
var parameter = parameters[i];
if (!CanBindItem(bindingContext, parameter))
{
continue;
}
var parameterBinder = _parameterBinders[i];
if (parameterBinder is PlaceholderBinder)
{
var fieldName = parameter.BinderModelName ?? parameter.ParameterName;
var modelName = ModelNames.CreatePropertyModelName(bindingContext.ModelName, fieldName);
var result = await BindParameterAsync(bindingContext, parameter, parameterBinder, fieldName, modelName);
if (result.IsModelSet)
{
parameterValues[i] = result.Model;
}
}
}
}
return (attemptedBinding, bindingSucceeded);
}
private async ValueTask<(bool attemptedBinding, bool bindingSucceeded)> BindPropertiesAsync(
ModelBindingContext bindingContext,
int propertyData,
IReadOnlyList<ModelMetadata> boundProperties)
{
var attemptedBinding = false;
var bindingSucceeded = false;
if (boundProperties.Count == 0)
{
return (attemptedBinding, bindingSucceeded);
}
var postponePlaceholderBinding = false;
for (var i = 0; i < boundProperties.Count; i++)
{
var property = boundProperties[i];
if (!CanBindItem(bindingContext, property))
{
continue;
}
var propertyBinder = _propertyBinders[property];
if (propertyBinder is PlaceholderBinder)
{
if (postponePlaceholderBinding)
{
// Decided to postpone binding properties that complete a loop in the model types when handling
// an earlier loop-completing property. Postpone binding this property too.
continue;
}
else if (!bindingContext.IsTopLevelObject &&
!bindingSucceeded &&
propertyData == GreedyPropertiesMayHaveData)
{
// Have no confirmation of data for the current instance. Postpone completing the loop until
// we _know_ the current instance is useful. Recursion would otherwise occur prior to the
// block with a similar condition after the loop.
//
// Example cases include an Employee class containing
// 1. a Manager property of type Employee
// 2. an Employees property of type IList<Employee>
postponePlaceholderBinding = true;
continue;
}
}
var fieldName = property.BinderModelName ?? property.PropertyName;
var modelName = ModelNames.CreatePropertyModelName(bindingContext.ModelName, fieldName);
var result = await BindPropertyAsync(bindingContext, property, propertyBinder, fieldName, modelName);
if (result.IsModelSet)
{
attemptedBinding = true;
bindingSucceeded = true;
}
else if (property.IsBindingRequired)
{
attemptedBinding = true;
}
}
if (postponePlaceholderBinding && bindingSucceeded)
{
// Have some data for this instance. Continue with the model type loop.
for (var i = 0; i < boundProperties.Count; i++)
{
var property = boundProperties[i];
if (!CanBindItem(bindingContext, property))
{
continue;
}
var propertyBinder = _propertyBinders[property];
if (propertyBinder is PlaceholderBinder)
{
var fieldName = property.BinderModelName ?? property.PropertyName;
var modelName = ModelNames.CreatePropertyModelName(bindingContext.ModelName, fieldName);
await BindPropertyAsync(bindingContext, property, propertyBinder, fieldName, modelName);
}
}
}
return (attemptedBinding, bindingSucceeded);
}
internal bool CanBindItem(ModelBindingContext bindingContext, ModelMetadata propertyMetadata)
{
var metadataProviderFilter = bindingContext.ModelMetadata.PropertyFilterProvider?.PropertyFilter;
if (metadataProviderFilter?.Invoke(propertyMetadata) == false)
{
return false;
}
if (bindingContext.PropertyFilter?.Invoke(propertyMetadata) == false)
{
return false;
}
if (!propertyMetadata.IsBindingAllowed)
{
return false;
}
if (propertyMetadata.MetadataKind == ModelMetadataKind.Property && propertyMetadata.IsReadOnly)
{
// Determine if we can update a readonly property (such as a collection).
return CanUpdateReadOnlyProperty(propertyMetadata.ModelType);
}
return true;
}
private async ValueTask<ModelBindingResult> BindPropertyAsync(
ModelBindingContext bindingContext,
ModelMetadata property,
IModelBinder propertyBinder,
string fieldName,
string modelName)
{
Debug.Assert(property.MetadataKind == ModelMetadataKind.Property);
// Pass complex (including collection) values down so that binding system does not unnecessarily
// recreate instances or overwrite inner properties that are not bound. No need for this with simple
// values because they will be overwritten if binding succeeds. Arrays are never reused because they
// cannot be resized.
object propertyModel = null;
if (property.PropertyGetter != null &&
property.IsComplexType &&
!property.ModelType.IsArray)
{
propertyModel = property.PropertyGetter(bindingContext.Model);
}
ModelBindingResult result;
using (bindingContext.EnterNestedScope(
modelMetadata: property,
fieldName: fieldName,
modelName: modelName,
model: propertyModel))
{
await propertyBinder.BindModelAsync(bindingContext);
result = bindingContext.Result;
}
if (result.IsModelSet)
{
SetProperty(bindingContext, modelName, property, result);
}
else if (property.IsBindingRequired)
{
var message = property.ModelBindingMessageProvider.MissingBindRequiredValueAccessor(fieldName);
bindingContext.ModelState.TryAddModelError(modelName, message);
}
return result;
}
private async ValueTask<ModelBindingResult> BindParameterAsync(
ModelBindingContext bindingContext,
ModelMetadata parameter,
IModelBinder parameterBinder,
string fieldName,
string modelName)
{
Debug.Assert(parameter.MetadataKind == ModelMetadataKind.Parameter);
ModelBindingResult result;
using (bindingContext.EnterNestedScope(
modelMetadata: parameter,
fieldName: fieldName,
modelName: modelName,
model: null))
{
await parameterBinder.BindModelAsync(bindingContext);
result = bindingContext.Result;
}
if (!result.IsModelSet && parameter.IsBindingRequired)
{
var message = parameter.ModelBindingMessageProvider.MissingBindRequiredValueAccessor(fieldName);
bindingContext.ModelState.TryAddModelError(modelName, message);
}
return result;
}
internal int CanCreateModel(ModelBindingContext bindingContext)
{
var isTopLevelObject = bindingContext.IsTopLevelObject;
// If we get here the model is a complex object which was not directly bound by any previous model binder,
// so we want to decide if we want to continue binding. This is important to get right to avoid infinite
// recursion.
//
// First, we want to make sure this object is allowed to come from a value provider source as this binder
// will only include value provider data. For instance if the model is marked with [FromBody], then we
// can just skip it. A greedy source cannot be a value provider.
//
// If the model isn't marked with ANY binding source, then we assume it's OK also.
//
// We skip this check if it is a top level object because we want to always evaluate
// the creation of top level object (this is also required for ModelBinderAttribute to work.)
var bindingSource = bindingContext.BindingSource;
if (!isTopLevelObject && bindingSource != null && bindingSource.IsGreedy)
{
return NoDataAvailable;
}
// Create the object if:
// 1. It is a top level model.
if (isTopLevelObject)
{
return ValueProviderDataAvailable;
}
// 2. Any of the model properties can be bound.
return CanBindAnyModelItem(bindingContext);
}
private int CanBindAnyModelItem(ModelBindingContext bindingContext)
{
// If there are no properties on the model, and no constructor parameters, there is nothing to bind. We are here means this is not a top
// level object. So we return false.
var modelMetadata = bindingContext.ModelMetadata;
var performsConstructorBinding = bindingContext.Model == null && modelMetadata.BoundConstructor != null;
if (modelMetadata.Properties.Count == 0 &&
(!performsConstructorBinding || modelMetadata.BoundConstructor.BoundConstructorParameters.Count == 0))
{
Log.NoPublicSettableItems(_logger, bindingContext);
return NoDataAvailable;
}
// We want to check to see if any of the properties of the model can be bound using the value providers or
// a greedy binder.
//
// Because a property might specify a custom binding source ([FromForm]), it's not correct
// for us to just try bindingContext.ValueProvider.ContainsPrefixAsync(bindingContext.ModelName);
// that may include other value providers - that would lead us to mistakenly create the model
// when the data is coming from a source we should use (ex: value found in query string, but the
// model has [FromForm]).
//
// To do this we need to enumerate the properties, and see which of them provide a binding source
// through metadata, then we decide what to do.
//
// If a property has a binding source, and it's a greedy source, then it's always bound.
//
// If a property has a binding source, and it's a non-greedy source, then we'll filter the
// the value providers to just that source, and see if we can find a matching prefix
// (see CanBindValue).
//
// If a property does not have a binding source, then it's fair game for any value provider.
//
// Bottom line, if any property meets the above conditions and has a value from ValueProviders, then we'll
// create the model and try to bind it. Of, if ANY properties of the model have a greedy source,
// then we go ahead and create it.
var hasGreedyBinders = false;
for (var i = 0; i < bindingContext.ModelMetadata.Properties.Count; i++)
{
var propertyMetadata = bindingContext.ModelMetadata.Properties[i];
if (!CanBindItem(bindingContext, propertyMetadata))
{
continue;
}
// If any property can be bound from a greedy binding source, then success.
var bindingSource = propertyMetadata.BindingSource;
if (bindingSource != null && bindingSource.IsGreedy)
{
hasGreedyBinders = true;
continue;
}
// Otherwise, check whether the (perhaps filtered) value providers have a match.
var fieldName = propertyMetadata.BinderModelName ?? propertyMetadata.PropertyName;
var modelName = ModelNames.CreatePropertyModelName(bindingContext.ModelName, fieldName);
using (bindingContext.EnterNestedScope(
modelMetadata: propertyMetadata,
fieldName: fieldName,
modelName: modelName,
model: null))
{
// If any property can be bound from a value provider, then success.
if (bindingContext.ValueProvider.ContainsPrefix(bindingContext.ModelName))
{
return ValueProviderDataAvailable;
}
}
}
if (performsConstructorBinding)
{
var parameters = bindingContext.ModelMetadata.BoundConstructor.BoundConstructorParameters;
for (var i = 0; i < parameters.Count; i++)
{
var parameterMetadata = parameters[i];
if (!CanBindItem(bindingContext, parameterMetadata))
{
continue;
}
// If any parameter can be bound from a greedy binding source, then success.
var bindingSource = parameterMetadata.BindingSource;
if (bindingSource != null && bindingSource.IsGreedy)
{
hasGreedyBinders = true;
continue;
}
// Otherwise, check whether the (perhaps filtered) value providers have a match.
var fieldName = parameterMetadata.BinderModelName ?? parameterMetadata.ParameterName;
var modelName = ModelNames.CreatePropertyModelName(bindingContext.ModelName, fieldName);
using (bindingContext.EnterNestedScope(
modelMetadata: parameterMetadata,
fieldName: fieldName,
modelName: modelName,
model: null))
{
// If any parameter can be bound from a value provider, then success.
if (bindingContext.ValueProvider.ContainsPrefix(bindingContext.ModelName))
{
return ValueProviderDataAvailable;
}
}
}
}
if (hasGreedyBinders)
{
return GreedyPropertiesMayHaveData;
}
_logger.CannotBindToComplexType(bindingContext);
return NoDataAvailable;
}
internal static bool CanUpdateReadOnlyProperty(Type propertyType)
{
// Value types have copy-by-value semantics, which prevents us from updating
// properties that are marked readonly.
if (propertyType.GetTypeInfo().IsValueType)
{
return false;
}
// Arrays are strange beasts since their contents are mutable but their sizes aren't.
// Therefore we shouldn't even try to update these. Further reading:
// http://blogs.msdn.com/ericlippert/archive/2008/09/22/arrays-considered-somewhat-harmful.aspx
if (propertyType.IsArray)
{
return false;
}
// Special-case known immutable reference types
if (propertyType == typeof(string))
{
return false;
}
return true;
}
internal void SetProperty(
ModelBindingContext bindingContext,
string modelName,
ModelMetadata propertyMetadata,
ModelBindingResult result)
{
if (!result.IsModelSet)
{
// If we don't have a value, don't set it on the model and trounce a pre-initialized value.
return;
}
if (propertyMetadata.IsReadOnly)
{
// The property should have already been set when we called BindPropertyAsync, so there's
// nothing to do here.
return;
}
var value = result.Model;
try
{
propertyMetadata.PropertySetter(bindingContext.Model, value);
}
catch (Exception exception)
{
AddModelError(exception, modelName, bindingContext);
}
}
private static void AddModelError(
Exception exception,
string modelName,
ModelBindingContext bindingContext)
{
var targetInvocationException = exception as TargetInvocationException;
if (targetInvocationException?.InnerException != null)
{
exception = targetInvocationException.InnerException;
}
// Do not add an error message if a binding error has already occurred for this property.
var modelState = bindingContext.ModelState;
var validationState = modelState.GetFieldValidationState(modelName);
if (validationState == ModelValidationState.Unvalidated)
{
modelState.AddModelError(modelName, exception, bindingContext.ModelMetadata);
}
}
private static class Log
{
private static readonly Action<ILogger, string, Type, Exception> _noPublicSettableProperties = LoggerMessage.Define<string, Type>(
LogLevel.Debug,
new EventId(17, "NoPublicSettableItems"),
"Could not bind to model with name '{ModelName}' and type '{ModelType}' as the type has no public settable properties or constructor parameters.");
public static void NoPublicSettableItems(ILogger logger, ModelBindingContext bindingContext)
{
_noPublicSettableProperties(logger, bindingContext.ModelName, bindingContext.ModelType, null);
}
}
}
}

View File

@ -0,0 +1,64 @@
// 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 Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
namespace Microsoft.AspNetCore.Mvc.ModelBinding.Binders
{
/// <summary>
/// An <see cref="IModelBinderProvider"/> for complex types.
/// </summary>
public class ComplexObjectModelBinderProvider : IModelBinderProvider
{
/// <inheritdoc />
public IModelBinder GetBinder(ModelBinderProviderContext context)
{
if (context == null)
{
throw new ArgumentNullException(nameof(context));
}
var metadata = context.Metadata;
if (metadata.IsComplexType && !metadata.IsCollectionType)
{
var loggerFactory = context.Services.GetRequiredService<ILoggerFactory>();
var logger = loggerFactory.CreateLogger<ComplexObjectModelBinder>();
var parameterBinders = GetParameterBinders(context);
var propertyBinders = new Dictionary<ModelMetadata, IModelBinder>();
for (var i = 0; i < context.Metadata.Properties.Count; i++)
{
var property = context.Metadata.Properties[i];
propertyBinders.Add(property, context.CreateBinder(property));
}
return new ComplexObjectModelBinder(propertyBinders, parameterBinders, logger);
}
return null;
}
private static IReadOnlyList<IModelBinder> GetParameterBinders(ModelBinderProviderContext context)
{
var boundConstructor = context.Metadata.BoundConstructor;
if (boundConstructor is null)
{
return Array.Empty<IModelBinder>();
}
var parameterBinders = boundConstructor.BoundConstructorParameters.Count == 0 ?
Array.Empty<IModelBinder>() :
new IModelBinder[boundConstructor.BoundConstructorParameters.Count];
for (var i = 0; i < parameterBinders.Length; i++)
{
parameterBinders[i] = context.CreateBinder(boundConstructor.BoundConstructorParameters[i]);
}
return parameterBinders;
}
}
}

View File

@ -16,6 +16,7 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding.Binders
/// <summary>
/// <see cref="IModelBinder"/> implementation for binding complex types.
/// </summary>
[Obsolete("This type is obsolete and will be removed in a future version. Use ComplexObjectModelBinder instead.")]
public class ComplexTypeModelBinder : IModelBinder
{
// Don't want a new public enum because communication between the private and internal methods of this class

View File

@ -1,4 +1,4 @@
// Copyright (c) .NET Foundation. All rights reserved.
// 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;
@ -11,6 +11,7 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding.Binders
/// <summary>
/// An <see cref="IModelBinderProvider"/> for complex types.
/// </summary>
[Obsolete("This type is obsolete and will be removed in a future version. Use ComplexObjectModelBinderProvider instead.")]
public class ComplexTypeModelBinderProvider : IModelBinderProvider
{
/// <inheritdoc />

View File

@ -2,6 +2,7 @@
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System;
using System.Reflection;
using Microsoft.AspNetCore.Mvc.Core;
namespace Microsoft.AspNetCore.Mvc.ModelBinding.Metadata
@ -97,5 +98,10 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding.Metadata
/// See <see cref="ModelMetadata.PropertyFilterProvider"/>.
/// </summary>
public IPropertyFilterProvider PropertyFilterProvider { get; set; }
/// <summary>
/// Gets or sets the <see cref="ConstructorInfo"/> used to model bind and validate the model type.
/// </summary>
public ConstructorInfo BoundConstructor { get; set; }
}
}

View File

@ -5,6 +5,7 @@ using System;
using System.Collections.Generic;
using System.Linq;
using System.Reflection;
using Microsoft.Extensions.Internal;
namespace Microsoft.AspNetCore.Mvc.ModelBinding.Metadata
{
@ -72,6 +73,79 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding.Metadata
context.BindingMetadata.IsBindingAllowed = bindingBehavior.Behavior != BindingBehavior.Never;
context.BindingMetadata.IsBindingRequired = bindingBehavior.Behavior == BindingBehavior.Required;
}
if (GetBoundConstructor(context.Key.ModelType) is ConstructorInfo constructorInfo)
{
context.BindingMetadata.BoundConstructor = constructorInfo;
}
}
internal static ConstructorInfo GetBoundConstructor(Type type)
{
if (type.IsAbstract || type.IsValueType || type.IsInterface)
{
return null;
}
var constructors = type.GetConstructors();
if (constructors.Length == 0)
{
return null;
}
return GetRecordTypeConstructor(type, constructors);
}
private static ConstructorInfo GetRecordTypeConstructor(Type type, ConstructorInfo[] constructors)
{
if (!IsRecordType(type))
{
return null;
}
// For record types, we will support binding and validating the primary constructor.
// There isn't metadata to identify a primary constructor. Our heuristic is:
// We require exactly one constructor to be defined on the type, and that every parameter on
// that constructor is mapped to a property with the same name and type.
if (constructors.Length > 1)
{
return null;
}
var constructor = constructors[0];
var parameters = constructor.GetParameters();
if (parameters.Length == 0)
{
// We do not need to do special handling for parameterless constructors.
return null;
}
var properties = PropertyHelper.GetVisibleProperties(type);
for (var i = 0; i < parameters.Length; i++)
{
var parameter = parameters[i];
var mappedProperty = properties.FirstOrDefault(property =>
string.Equals(property.Name, parameter.Name, StringComparison.Ordinal) &&
property.Property.PropertyType == parameter.ParameterType);
if (mappedProperty is null)
{
// No property found, this is not a primary constructor.
return null;
}
}
return constructor;
static bool IsRecordType(Type type)
{
// Based on the state of the art as described in https://github.com/dotnet/roslyn/issues/45777
var cloneMethod = type.GetMethod("<>Clone", BindingFlags.Public | BindingFlags.Instance);
return cloneMethod != null && cloneMethod.ReturnType == type;
}
}
private static BindingBehaviorAttribute FindBindingBehavior(BindingMetadataProviderContext context)

View File

@ -54,6 +54,11 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding.Metadata
/// </summary>
public ModelMetadata[] Properties { get; set; }
/// <summary>
/// Gets or sets the <see cref="ModelMetadata"/> entries for constructor parameters.
/// </summary>
public ModelMetadata[] BoundConstructorParameters { get; set; }
/// <summary>
/// Gets or sets a property getter delegate to get the property value from a model object.
/// </summary>
@ -64,6 +69,11 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding.Metadata
/// </summary>
public Action<object, object> PropertySetter { get; set; }
/// <summary>
/// Gets or sets a delegate used to invoke the bound constructor for record types.
/// </summary>
public Func<object[], object> BoundConstructorInvoker { get; set; }
/// <summary>
/// Gets or sets the <see cref="Metadata.ValidationMetadata"/>
/// </summary>
@ -74,4 +84,4 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding.Metadata
/// </summary>
public ModelMetadata ContainerMetadata { get; set; }
}
}
}

View File

@ -25,6 +25,7 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding.Metadata
private ReadOnlyDictionary<object, object> _additionalValues;
private ModelMetadata _elementMetadata;
private ModelMetadata _constructorMetadata;
private bool? _isBindingRequired;
private bool? _isReadOnly;
private bool? _isRequired;
@ -386,6 +387,28 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding.Metadata
}
}
/// <inheritdoc />
public override ModelMetadata BoundConstructor
{
get
{
if (BindingMetadata.BoundConstructor == null)
{
return null;
}
if (_constructorMetadata == null)
{
var modelMetadataProvider = (ModelMetadataProvider)_provider;
_constructorMetadata = modelMetadataProvider.GetMetadataForConstructor(BindingMetadata.BoundConstructor, ModelType);
}
return _constructorMetadata;
}
}
public override IReadOnlyList<ModelMetadata> BoundConstructorParameters => _details.BoundConstructorParameters;
/// <inheritdoc />
public override IPropertyFilterProvider PropertyFilterProvider => BindingMetadata.PropertyFilterProvider;
@ -494,7 +517,16 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding.Metadata
}
else if (defaultModelMetadata.IsComplexType)
{
foreach (var property in defaultModelMetadata.Properties)
var parameters = defaultModelMetadata.BoundConstructor?.BoundConstructorParameters ?? Array.Empty<ModelMetadata>();
foreach (var parameter in parameters)
{
if (CalculateHasValidators(visited, parameter))
{
return true;
}
}
foreach (var property in defaultModelMetadata.BoundProperties)
{
if (CalculateHasValidators(visited, property))
{
@ -527,6 +559,10 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding.Metadata
/// <inheritdoc />
public override Action<object, object> PropertySetter => _details.PropertySetter;
public override Func<object[], object> BoundConstructorInvoker => _details.BoundConstructorInvoker;
internal DefaultMetadataDetails Details => _details;
/// <inheritdoc />
public override ModelMetadata GetMetadataForType(Type modelType)
{

View File

@ -5,6 +5,7 @@ using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq.Expressions;
using System.Reflection;
using Microsoft.Extensions.Internal;
using Microsoft.Extensions.Options;
@ -16,7 +17,7 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding.Metadata
/// </summary>
public class DefaultModelMetadataProvider : ModelMetadataProvider
{
private readonly TypeCache _typeCache = new TypeCache();
private readonly ModelMetadataCache _modelMetadataCache = new ModelMetadataCache();
private readonly Func<ModelMetadataIdentity, ModelMetadataCacheEntry> _cacheEntryFactory;
private readonly ModelMetadataCacheEntry _metadataCacheEntryForObjectType;
@ -150,6 +151,18 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding.Metadata
return cacheEntry.Metadata;
}
/// <inheritdoc />
public override ModelMetadata GetMetadataForConstructor(ConstructorInfo constructorInfo, Type modelType)
{
if (constructorInfo is null)
{
throw new ArgumentNullException(nameof(constructorInfo));
}
var cacheEntry = GetCacheEntry(constructorInfo, modelType);
return cacheEntry.Metadata;
}
private static DefaultModelBindingMessageProvider GetMessageProvider(IOptions<MvcOptions> optionsAccessor)
{
@ -174,7 +187,7 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding.Metadata
{
var key = ModelMetadataIdentity.ForType(modelType);
cacheEntry = _typeCache.GetOrAdd(key, _cacheEntryFactory);
cacheEntry = _modelMetadataCache.GetOrAdd(key, _cacheEntryFactory);
}
return cacheEntry;
@ -182,22 +195,34 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding.Metadata
private ModelMetadataCacheEntry GetCacheEntry(ParameterInfo parameter, Type modelType)
{
return _typeCache.GetOrAdd(
return _modelMetadataCache.GetOrAdd(
ModelMetadataIdentity.ForParameter(parameter, modelType),
_cacheEntryFactory);
}
private ModelMetadataCacheEntry GetCacheEntry(PropertyInfo property, Type modelType)
{
return _typeCache.GetOrAdd(
return _modelMetadataCache.GetOrAdd(
ModelMetadataIdentity.ForProperty(property, modelType, property.DeclaringType),
_cacheEntryFactory);
}
private ModelMetadataCacheEntry GetCacheEntry(ConstructorInfo constructor, Type modelType)
{
return _modelMetadataCache.GetOrAdd(
ModelMetadataIdentity.ForConstructor(constructor, modelType),
_cacheEntryFactory);
}
private ModelMetadataCacheEntry CreateCacheEntry(ModelMetadataIdentity key)
{
DefaultMetadataDetails details;
if (key.MetadataKind == ModelMetadataKind.Parameter)
if (key.MetadataKind == ModelMetadataKind.Constructor)
{
details = CreateConstructorDetails(key);
}
else if (key.MetadataKind == ModelMetadataKind.Parameter)
{
details = CreateParameterDetails(key);
}
@ -230,6 +255,73 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding.Metadata
return null;
}
private DefaultMetadataDetails CreateConstructorDetails(ModelMetadataIdentity constructorKey)
{
var constructor = constructorKey.ConstructorInfo;
var parameters = constructor.GetParameters();
var parameterMetadata = new ModelMetadata[parameters.Length];
var parameterTypes = new Type[parameters.Length];
for (var i = 0; i < parameters.Length; i++)
{
var parameter = parameters[i];
var parameterDetails = CreateParameterDetails(ModelMetadataIdentity.ForParameter(parameter));
parameterMetadata[i] = CreateModelMetadata(parameterDetails);
parameterTypes[i] = parameter.ParameterType;
}
var constructorDetails = new DefaultMetadataDetails(constructorKey, ModelAttributes.Empty);
constructorDetails.BoundConstructorParameters = parameterMetadata;
constructorDetails.BoundConstructorInvoker = CreateObjectFactory(constructor);
return constructorDetails;
static Func<object[], object> CreateObjectFactory(ConstructorInfo constructor)
{
var args = Expression.Parameter(typeof(object[]), "args");
var factoryExpressionBody = BuildFactoryExpression(constructor, args);
var factoryLamda = Expression.Lambda<Func<object[], object>>(factoryExpressionBody, args);
return factoryLamda.Compile();
}
}
private static Expression BuildFactoryExpression(
ConstructorInfo constructor,
Expression factoryArgumentArray)
{
var constructorParameters = constructor.GetParameters();
var constructorArguments = new Expression[constructorParameters.Length];
for (var i = 0; i < constructorParameters.Length; i++)
{
var constructorParameter = constructorParameters[i];
var parameterType = constructorParameter.ParameterType;
constructorArguments[i] = Expression.ArrayAccess(factoryArgumentArray, Expression.Constant(i));
if (ParameterDefaultValue.TryGetDefaultValue(constructorParameter, out var defaultValue))
{
// We have a default value;
}
else if (parameterType.IsValueType)
{
defaultValue = Activator.CreateInstance(parameterType);
}
if (defaultValue != null)
{
var defaultValueExpression = Expression.Constant(defaultValue);
constructorArguments[i] = Expression.Coalesce(constructorArguments[i], defaultValueExpression);
}
constructorArguments[i] = Expression.Convert(constructorArguments[i], parameterType);
}
return Expression.New(constructor, constructorArguments);
}
private ModelMetadataCacheEntry GetMetadataCacheEntryForObjectType()
{
var key = ModelMetadataIdentity.ForType(typeof(object));
@ -341,7 +433,7 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding.Metadata
ModelAttributes.GetAttributesForParameter(key.ParameterInfo, key.ModelType));
}
private class TypeCache : ConcurrentDictionary<ModelMetadataIdentity, ModelMetadataCacheEntry>
private class ModelMetadataCache : ConcurrentDictionary<ModelMetadataIdentity, ModelMetadataCacheEntry>
{
}
@ -358,4 +450,4 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding.Metadata
public DefaultMetadataDetails Details { get; }
}
}
}
}

View File

@ -53,6 +53,11 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding.Metadata
context.ValidationMetadata.PropertyValidationFilter = validationFilter;
}
else if (context.Key.MetadataKind == ModelMetadataKind.Parameter)
{
var validationFilter = context.ParameterAttributes.OfType<IPropertyValidationFilter>().FirstOrDefault();
context.ValidationMetadata.PropertyValidationFilter = validationFilter;
}
}
}
}

View File

@ -13,6 +13,16 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding
/// </summary>
public class ModelAttributes
{
internal static readonly ModelAttributes Empty = new ModelAttributes(Array.Empty<object>());
/// <summary>
/// Creates a new <see cref="ModelAttributes"/>.
/// </summary>
internal ModelAttributes(IReadOnlyList<object> attributes)
{
Attributes = attributes;
}
/// <summary>
/// Creates a new <see cref="ModelAttributes"/>.
/// </summary>

View File

@ -1,4 +1,4 @@
// Copyright (c) .NET Foundation. All rights reserved.
// 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;

View File

@ -4,7 +4,7 @@
using System;
using System.Collections;
using System.Collections.Generic;
using System.Reflection;
using Microsoft.AspNetCore.Mvc.Core;
namespace Microsoft.AspNetCore.Mvc.ModelBinding.Validation
{
@ -13,8 +13,6 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding.Validation
/// </summary>
internal class DefaultComplexObjectValidationStrategy : IValidationStrategy
{
private static readonly bool IsMono = Type.GetType("Mono.Runtime") != null;
/// <summary>
/// Gets an instance of <see cref="DefaultComplexObjectValidationStrategy"/>.
/// </summary>
@ -30,27 +28,42 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding.Validation
string key,
object model)
{
return new Enumerator(metadata.Properties, key, model);
return new Enumerator(metadata, key, model);
}
private class Enumerator : IEnumerator<ValidationEntry>
{
private readonly string _key;
private readonly object _model;
private readonly ModelPropertyCollection _properties;
private readonly int _count;
private readonly ModelMetadata _modelMetadata;
private readonly IReadOnlyList<ModelMetadata> _parameters;
private readonly IReadOnlyList<ModelMetadata> _properties;
private ValidationEntry _entry;
private int _index;
public Enumerator(
ModelPropertyCollection properties,
ModelMetadata modelMetadata,
string key,
object model)
{
_properties = properties;
_modelMetadata = modelMetadata;
_key = key;
_model = model;
if (_modelMetadata.BoundConstructor == null)
{
_parameters = Array.Empty<ModelMetadata>();
}
else
{
_parameters = _modelMetadata.BoundConstructor.BoundConstructorParameters;
}
_properties = _modelMetadata.BoundProperties;
_count = _properties.Count + _parameters.Count;
_index = -1;
}
@ -61,27 +74,48 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding.Validation
public bool MoveNext()
{
_index++;
if (_index >= _properties.Count)
if (_index >= _count)
{
return false;
}
var property = _properties[_index];
var propertyName = property.BinderModelName ?? property.PropertyName;
var key = ModelNames.CreatePropertyModelName(_key, propertyName);
if (_index < _parameters.Count)
{
var parameter = _parameters[_index];
var parameterName = parameter.BinderModelName ?? parameter.ParameterName;
var key = ModelNames.CreatePropertyModelName(_key, parameterName);
if (_model == null)
{
// Performance: Never create a delegate when container is null.
_entry = new ValidationEntry(property, key, model: null);
}
else if (IsMono)
{
_entry = new ValidationEntry(property, key, () => GetModelOnMono(_model, property.PropertyName));
if (_model is null)
{
_entry = new ValidationEntry(parameter, key, model: null);
}
else
{
if (!_modelMetadata.BoundConstructorParameterMapping.TryGetValue(parameter, out var property))
{
throw new InvalidOperationException(
Resources.FormatValidationStrategy_MappedPropertyNotFound(parameter, _modelMetadata.ModelType));
}
_entry = new ValidationEntry(parameter, key, () => GetModel(_model, property));
}
}
else
{
_entry = new ValidationEntry(property, key, () => GetModel(_model, property));
var property = _properties[_index - _parameters.Count];
var propertyName = property.BinderModelName ?? property.PropertyName;
var key = ModelNames.CreatePropertyModelName(_key, propertyName);
if (_model == null)
{
// Performance: Never create a delegate when container is null.
_entry = new ValidationEntry(property, key, model: null);
}
else
{
_entry = new ValidationEntry(property, key, () => GetModel(_model, property));
}
}
return true;
@ -100,21 +134,6 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding.Validation
{
return property.PropertyGetter(container);
}
// Our property accessors don't work on Mono 4.0.4 - see https://github.com/aspnet/External/issues/44
// This is a workaround for what the PropertyGetter does in the background.
private static object GetModelOnMono(object container, string propertyName)
{
var propertyInfo = container.GetType().GetRuntimeProperty(propertyName);
try
{
return propertyInfo.GetValue(container);
}
catch (TargetInvocationException ex)
{
throw ex.InnerException;
}
}
}
}
}

View File

@ -1,4 +1,4 @@
// Copyright (c) .NET Foundation. All rights reserved.
// 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;
@ -6,11 +6,12 @@ using System;
namespace Microsoft.AspNetCore.Mvc.ModelBinding.Validation
{
/// <summary>
/// <see cref="IPropertyValidationFilter"/> implementation that unconditionally indicates a property should be
/// excluded from validation. When applied to a property, the validation system excludes that property. When
/// applied to a type, the validation system excludes all properties within that type.
/// Indicates that a property or parameter should be excluded from validation.
/// When applied to a property, the validation system excludes that property.
/// When applied to a parameter, the validation system excludes that parameter.
/// When applied to a type, the validation system excludes all properties within that type.
/// </summary>
[AttributeUsage(AttributeTargets.Class | AttributeTargets.Property, AllowMultiple = false, Inherited = true)]
[AttributeUsage(AttributeTargets.Class | AttributeTargets.Property | AttributeTargets.Parameter, AllowMultiple = false, Inherited = true)]
public sealed class ValidateNeverAttribute : Attribute, IPropertyValidationFilter
{
/// <inheritdoc />

View File

@ -522,4 +522,16 @@
<data name="StateShouldBeNullForRouteValueTransformers" xml:space="preserve">
<value>Transformer '{0}' was retrieved from dependency injection with a state value. State can only be specified when the dynamic route is mapped using MapDynamicControllerRoute's state argument together with transient lifetime transformer. Ensure that '{0}' doesn't set its own state and that the transformer is registered with a transient lifetime in dependency injection.</value>
</data>
<data name="ComplexObjectModelBinder_NoSuitableConstructor_ForParameter" xml:space="preserve">
<value>Could not create an instance of type '{0}'. Model bound complex types must not be abstract or value types and must have a parameterless constructor. Record types must have a single primary constructor. Alternatively, give the '{1}' parameter a non-null default value.</value>
</data>
<data name="ComplexObjectModelBinder_NoSuitableConstructor_ForProperty" xml:space="preserve">
<value>Could not create an instance of type '{0}'. Model bound complex types must not be abstract or value types and must have a parameterless constructor. Record types must have a single primary constructor. Alternatively, set the '{1}' property to a non-null value in the '{2}' constructor.</value>
</data>
<data name="ComplexObjectModelBinder_NoSuitableConstructor_ForType" xml:space="preserve">
<value>Could not create an instance of type '{0}'. Model bound complex types must not be abstract or value types and must have a parameterless constructor. Record types must have a single primary constructor.</value>
</data>
<data name="ValidationStrategy_MappedPropertyNotFound" xml:space="preserve">
<value>No property found that maps to constructor parameter '{0}' for type '{1}'. Validation requires that each bound parameter of a record type's primary constructor must have a property to read the value.</value>
</data>
</root>

View File

@ -1270,7 +1270,7 @@ namespace Microsoft.AspNetCore.Mvc.ApplicationModels
{
public string Property { get; set; }
[ModelBinder(typeof(ComplexTypeModelBinder))]
[ModelBinder(typeof(ComplexObjectModelBinder))]
public string BinderType { get; set; }
[FromRoute]
@ -1307,7 +1307,7 @@ namespace Microsoft.AspNetCore.Mvc.ApplicationModels
// Assert
var bindingInfo = property.BindingInfo;
Assert.Same(typeof(ComplexTypeModelBinder), bindingInfo.BinderType);
Assert.Same(typeof(ComplexObjectModelBinder), bindingInfo.BinderType);
}
[Fact]

View File

@ -998,7 +998,7 @@ Environment.NewLine + "int b";
private class ParameterWithBindingInfo
{
[HttpGet("test")]
public IActionResult Action([ModelBinder(typeof(ComplexTypeModelBinder))] Car car) => null;
public IActionResult Action([ModelBinder(typeof(ComplexObjectModelBinder))] Car car) => null;
}
}
}

View File

@ -1,8 +1,9 @@
<Project Sdk="Microsoft.NET.Sdk">
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>$(DefaultNetCoreTargetFramework)</TargetFramework>
<RootNamespace>Microsoft.AspNetCore.Mvc</RootNamespace>
<LangVersion>9.0</LangVersion>
</PropertyGroup>
<ItemGroup>

View File

@ -0,0 +1,92 @@
// 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 Moq;
using Xunit;
namespace Microsoft.AspNetCore.Mvc.ModelBinding.Binders
{
public class ComplexObjectModelBinderProviderTest
{
[Theory]
[InlineData(typeof(string))]
[InlineData(typeof(int))]
[InlineData(typeof(List<int>))]
public void Create_ForNonComplexType_ReturnsNull(Type modelType)
{
// Arrange
var provider = new ComplexObjectModelBinderProvider();
var context = new TestModelBinderProviderContext(modelType);
// Act
var result = provider.GetBinder(context);
// Assert
Assert.Null(result);
}
[Fact]
public void Create_ForSupportedTypes_ReturnsBinder()
{
// Arrange
var provider = new ComplexObjectModelBinderProvider();
var context = new TestModelBinderProviderContext(typeof(Person));
context.OnCreatingBinder(m =>
{
if (m.ModelType == typeof(int) || m.ModelType == typeof(string))
{
return Mock.Of<IModelBinder>();
}
else
{
Assert.False(true, "Not the right model type");
return null;
}
});
// Act
var result = provider.GetBinder(context);
// Assert
Assert.IsType<ComplexObjectModelBinder>(result);
}
[Fact]
public void Create_ForSupportedType_ReturnsBinder()
{
// Arrange
var provider = new ComplexObjectModelBinderProvider();
var context = new TestModelBinderProviderContext(typeof(Person));
context.OnCreatingBinder(m =>
{
if (m.ModelType == typeof(int) || m.ModelType == typeof(string))
{
return Mock.Of<IModelBinder>();
}
else
{
Assert.False(true, "Not the right model type");
return null;
}
});
// Act
var result = provider.GetBinder(context);
// Assert
Assert.IsType<ComplexObjectModelBinder>(result);
}
private class Person
{
public string Name { get; set; }
public int Age { get; set; }
}
}
}

File diff suppressed because it is too large Load Diff

View File

@ -1,4 +1,4 @@
// Copyright (c) .NET Foundation. All rights reserved.
// 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;
@ -8,6 +8,7 @@ using Xunit;
namespace Microsoft.AspNetCore.Mvc.ModelBinding.Binders
{
#pragma warning disable CS0618 // Type or member is obsolete
public class ComplexTypeModelBinderProviderTest
{
[Theory]
@ -89,4 +90,5 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding.Binders
public int Age { get; set; }
}
}
#pragma warning restore CS0618 // Type or member is obsolete
}

View File

@ -19,6 +19,7 @@ using Xunit;
namespace Microsoft.AspNetCore.Mvc.ModelBinding.Binders
{
#pragma warning disable CS0618 // Type or member is obsolete
public class ComplexTypeModelBinderTest
{
private static readonly IModelMetadataProvider _metadataProvider = TestModelMetadataProvider.CreateDefaultProvider();
@ -1229,8 +1230,7 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding.Binders
setup.Configure(options.Value);
var lastIndex = options.Value.ModelBinderProviders.Count - 1;
Assert.IsType<ComplexTypeModelBinderProvider>(options.Value.ModelBinderProviders[lastIndex]);
options.Value.ModelBinderProviders.RemoveAt(lastIndex);
options.Value.ModelBinderProviders.RemoveType<ComplexObjectModelBinderProvider>();
options.Value.ModelBinderProviders.Add(new TestableComplexTypeModelBinderProvider());
var factory = TestModelBinderFactory.Create(options.Value.ModelBinderProviders.ToArray());
@ -1662,4 +1662,5 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding.Binders
}
}
}
#pragma warning restore CS0618 // Type or member is obsolete
}

View File

@ -278,12 +278,13 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding.Binders
var binder = new DictionaryModelBinder<int, ModelWithProperties>(
new SimpleTypeModelBinder(typeof(int), NullLoggerFactory.Instance),
new ComplexTypeModelBinder(new Dictionary<ModelMetadata, IModelBinder>()
new ComplexObjectModelBinder(new Dictionary<ModelMetadata, IModelBinder>()
{
{ valueMetadata.Properties["Id"], new SimpleTypeModelBinder(typeof(int), NullLoggerFactory.Instance) },
{ valueMetadata.Properties["Name"], new SimpleTypeModelBinder(typeof(string), NullLoggerFactory.Instance) },
},
NullLoggerFactory.Instance),
Array.Empty<IModelBinder>(),
NullLogger<ComplexObjectModelBinder>.Instance),
NullLoggerFactory.Instance);
// Act

View File

@ -658,6 +658,198 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding.Metadata
Assert.Equal(initialValue, context.BindingMetadata.IsBindingRequired);
}
private class DefaultConstructorType { }
[Fact]
public void GetBoundConstructor_DefaultConstructor_ReturnsNull()
{
// Arrange
var type = typeof(DefaultConstructorType);
// Act
var result = DefaultBindingMetadataProvider.GetBoundConstructor(type);
// Assert
Assert.Null(result);
}
private class ParameterlessConstructorType
{
public ParameterlessConstructorType() { }
}
[Fact]
public void GetBoundConstructor_ParameterlessConstructor_ReturnsNull()
{
// Arrange
var type = typeof(ParameterlessConstructorType);
// Act
var result = DefaultBindingMetadataProvider.GetBoundConstructor(type);
// Assert
Assert.Null(result);
}
private class NonPublicParameterlessConstructorType
{
protected NonPublicParameterlessConstructorType() { }
}
[Fact]
public void GetBoundConstructor_DoesNotReturnsNonPublicParameterlessConstructor()
{
// Arrange
var type = typeof(NonPublicParameterlessConstructorType);
// Act
var result = DefaultBindingMetadataProvider.GetBoundConstructor(type);
// Assert
Assert.Null(result);
}
private class MultipleConstructorType
{
public MultipleConstructorType() { }
public MultipleConstructorType(string prop) { }
}
[Fact]
public void GetBoundConstructor_ReturnsParameterlessConstructor_ForTypeWithMultipleConstructors()
{
// Arrange
var type = typeof(NonPublicParameterlessConstructorType);
// Act
var result = DefaultBindingMetadataProvider.GetBoundConstructor(type);
// Assert
Assert.Null(result);
}
private record RecordTypeWithPrimaryConstructor(string name)
{
}
[Fact]
public void GetBoundConstructor_ReturnsPrimaryConstructor_ForRecordType()
{
// Arrange
var type = typeof(RecordTypeWithPrimaryConstructor);
// Act
var result = DefaultBindingMetadataProvider.GetBoundConstructor(type);
// Assert
Assert.NotNull(result);
Assert.Collection(
result.GetParameters(),
p => Assert.Equal("name", p.Name));
}
private record RecordTypeWithDefaultConstructor
{
public string Name { get; init; }
public int Age { get; init; }
}
private record RecordTypeWithParameterlessConstructor
{
public RecordTypeWithParameterlessConstructor() { }
public string Name { get; init; }
public int Age { get; init; }
}
[Theory]
[InlineData(typeof(RecordTypeWithDefaultConstructor))]
[InlineData(typeof(RecordTypeWithParameterlessConstructor))]
public void GetBoundConstructor_ReturnsNull_ForRecordTypeWithParameterlessConstructor(Type type)
{
// Act
var result = DefaultBindingMetadataProvider.GetBoundConstructor(type);
// Assert
Assert.Null(result);
}
private record RecordTypeWithMultipleConstructors(string Name)
{
public RecordTypeWithMultipleConstructors(string Name, int age) : this(Name) => Age = age;
public RecordTypeWithMultipleConstructors(int age) : this(string.Empty, age) { }
public int Age { get; set; }
}
[Fact]
public void GetBoundConstructor_ReturnsNull_ForRecordTypeWithMultipleConstructors()
{
// Arrange
var type = typeof(RecordTypeWithMultipleConstructors);
// Act
var result = DefaultBindingMetadataProvider.GetBoundConstructor(type);
// Assert
Assert.Null(result);
}
private record RecordTypeWithConformingSynthesizedConstructor
{
public RecordTypeWithConformingSynthesizedConstructor(string Name, int Age)
{
}
public string Name { get; set; }
public int Age { get; set; }
}
[Fact]
public void GetBoundConstructor_ReturnsConformingSynthesizedConstructor()
{
// Arrange
var type = typeof(RecordTypeWithConformingSynthesizedConstructor);
// Act
var result = DefaultBindingMetadataProvider.GetBoundConstructor(type);
// Assert
Assert.NotNull(result);
Assert.Collection(
result.GetParameters(),
p => Assert.Equal("Name", p.Name),
p => Assert.Equal("Age", p.Name));
}
private record RecordTypeWithNonConformingSynthesizedConstructor
{
public RecordTypeWithNonConformingSynthesizedConstructor(string name, string age)
{
}
public string Name { get; set; }
public int Age { get; set; }
}
[Fact]
public void GetBoundConstructor_ReturnsNull_IfSynthesizedConstructorIsNonConforming()
{
// Arrange
var type = typeof(RecordTypeWithNonConformingSynthesizedConstructor);
// Act
var result = DefaultBindingMetadataProvider.GetBoundConstructor(type);
// Assert
Assert.Null(result);
}
[BindNever]
private class BindNeverOnClass
{
@ -704,4 +896,4 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding.Metadata
public string Identifier { get; set; }
}
}
}
}

View File

@ -197,7 +197,7 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding.Metadata
var detailsProvider = new EmptyCompositeMetadataDetailsProvider();
var key = ModelMetadataIdentity.ForProperty(
typeof(TypeWithProperties).GetProperty(nameof(TypeWithProperties.PublicGetPublicSetProperty)),
typeof(TypeWithProperties).GetProperty(nameof(TypeWithProperties.PublicGetPublicSetProperty)),
typeof(string),
typeof(TypeWithProperties));
@ -626,12 +626,12 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding.Metadata
var metadata = new DefaultModelMetadata(provider, detailsProvider, cache);
// Act
var firstPropertiesEvaluation = metadata.Properties;
var SinglePropertiesEvaluation = metadata.Properties;
var secondPropertiesEvaluation = metadata.Properties;
// Assert
// Same IEnumerable<ModelMetadata> object.
Assert.Same(firstPropertiesEvaluation, secondPropertiesEvaluation);
Assert.Same(SinglePropertiesEvaluation, secondPropertiesEvaluation);
}
[Fact]
@ -647,12 +647,12 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding.Metadata
var metadata = new DefaultModelMetadata(provider, detailsProvider, cache);
// Act
var firstPropertiesEvaluation = metadata.Properties.ToList();
var SinglePropertiesEvaluation = metadata.Properties.ToList();
var secondPropertiesEvaluation = metadata.Properties.ToList();
// Assert
// Identical ModelMetadata objects every time we run through the Properties collection.
Assert.Equal(firstPropertiesEvaluation, secondPropertiesEvaluation);
Assert.Equal(SinglePropertiesEvaluation, secondPropertiesEvaluation);
}
[Fact]
@ -924,7 +924,7 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding.Metadata
.GetMethod(nameof(CalculateHasValidators_ParameterMetadata_TypeHasNoValidatorsMethod), BindingFlags.Static | BindingFlags.NonPublic)
.GetParameters()[0];
var modelIdentity = ModelMetadataIdentity.ForParameter(parameter);
var modelMetadata = CreateModelMetadata(modelIdentity, Mock.Of<IModelMetadataProvider>(), false);
var modelMetadata = CreateModelMetadata(modelIdentity, Mock.Of<IModelMetadataProvider>(), hasValidators: false);
// Act
var result = DefaultModelMetadata.CalculateHasValidators(new HashSet<DefaultModelMetadata>(), modelMetadata);
@ -942,7 +942,7 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding.Metadata
var property = GetType()
.GetProperty(nameof(CalculateHasValidators_PropertyMetadata_TypeHasNoValidatorsProperty), BindingFlags.Static | BindingFlags.NonPublic);
var modelIdentity = ModelMetadataIdentity.ForProperty(property, property.PropertyType, GetType());
var modelMetadata = CreateModelMetadata(modelIdentity, Mock.Of<IModelMetadataProvider>(), false);
var modelMetadata = CreateModelMetadata(modelIdentity, Mock.Of<IModelMetadataProvider>(), hasValidators: false);
// Act
var result = DefaultModelMetadata.CalculateHasValidators(new HashSet<DefaultModelMetadata>(), modelMetadata);
@ -958,7 +958,7 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding.Metadata
{
// Arrange
var modelIdentity = ModelMetadataIdentity.ForType(typeof(string));
var modelMetadata = CreateModelMetadata(modelIdentity, Mock.Of<IModelMetadataProvider>(), false);
var modelMetadata = CreateModelMetadata(modelIdentity, Mock.Of<IModelMetadataProvider>(), hasValidators: false);
// Act
var result = DefaultModelMetadata.CalculateHasValidators(new HashSet<DefaultModelMetadata>(), modelMetadata);
@ -972,7 +972,7 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding.Metadata
{
// Arrange
var modelIdentity = ModelMetadataIdentity.ForType(typeof(string));
var modelMetadata = CreateModelMetadata(modelIdentity, Mock.Of<IModelMetadataProvider>(), true);
var modelMetadata = CreateModelMetadata(modelIdentity, Mock.Of<IModelMetadataProvider>(), hasValidators: true);
// Act
var result = DefaultModelMetadata.CalculateHasValidators(new HashSet<DefaultModelMetadata>(), modelMetadata);
@ -986,7 +986,7 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding.Metadata
{
// Arrange
var modelIdentity = ModelMetadataIdentity.ForType(typeof(string));
var modelMetadata = CreateModelMetadata(modelIdentity, Mock.Of<IModelMetadataProvider>(), null);
var modelMetadata = CreateModelMetadata(modelIdentity, Mock.Of<IModelMetadataProvider>(), hasValidators: null);
// Act
var result = DefaultModelMetadata.CalculateHasValidators(new HashSet<DefaultModelMetadata>(), modelMetadata);
@ -1002,7 +1002,7 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding.Metadata
var modelType = typeof(TypeWithProperties);
var modelIdentity = ModelMetadataIdentity.ForType(modelType);
var metadataProvider = new Mock<IModelMetadataProvider>();
var modelMetadata = CreateModelMetadata(modelIdentity, metadataProvider.Object, false);
var modelMetadata = CreateModelMetadata(modelIdentity, metadataProvider.Object, hasValidators: false);
var property = typeof(TypeWithProperties).GetProperty(nameof(TypeWithProperties.PublicGetPublicSetProperty));
var propertyIdentity = ModelMetadataIdentity.ForProperty(property, typeof(int), typeof(TypeWithProperties));
@ -1027,13 +1027,13 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding.Metadata
var modelType = typeof(TypeWithProperties);
var modelIdentity = ModelMetadataIdentity.ForType(modelType);
var metadataProvider = new Mock<IModelMetadataProvider>();
var modelMetadata = CreateModelMetadata(modelIdentity, metadataProvider.Object, false);
var modelMetadata = CreateModelMetadata(modelIdentity, metadataProvider.Object, hasValidators: false);
var property1Identity = ModelMetadataIdentity.ForProperty(modelType.GetProperty(nameof(TypeWithProperties.PublicGetPublicSetProperty)), typeof(int), modelType);
var property1Metadata = CreateModelMetadata(property1Identity, metadataProvider.Object, false);
var property1Metadata = CreateModelMetadata(property1Identity, metadataProvider.Object, hasValidators: false);
var property2Identity = ModelMetadataIdentity.ForProperty(modelType.GetProperty(nameof(TypeWithProperties.PublicGetProtectedSetProperty)), typeof(int), modelType);
var property2Metadata = CreateModelMetadata(property2Identity, metadataProvider.Object, true);
var property2Metadata = CreateModelMetadata(property2Identity, metadataProvider.Object, hasValidators: true);
metadataProvider
.Setup(mp => mp.GetMetadataForProperties(modelType))
@ -1054,10 +1054,10 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding.Metadata
var modelType = typeof(TypeWithProperties);
var modelIdentity = ModelMetadataIdentity.ForType(modelType);
var metadataProvider = new Mock<IModelMetadataProvider>();
var modelMetadata = CreateModelMetadata(modelIdentity, metadataProvider.Object, false);
var modelMetadata = CreateModelMetadata(modelIdentity, metadataProvider.Object, hasValidators: false);
var propertyIdentity = ModelMetadataIdentity.ForProperty(modelType.GetProperty(nameof(TypeWithProperties.PublicGetPublicSetProperty)), typeof(int), modelType);
var propertyMetadata = CreateModelMetadata(propertyIdentity, metadataProvider.Object, null);
var propertyMetadata = CreateModelMetadata(propertyIdentity, metadataProvider.Object, hasValidators: null);
metadataProvider
.Setup(mp => mp.GetMetadataForProperties(modelType))
@ -1078,13 +1078,13 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding.Metadata
var modelType = typeof(TypeWithProperties);
var modelIdentity = ModelMetadataIdentity.ForType(modelType);
var metadataProvider = new Mock<IModelMetadataProvider>();
var modelMetadata = CreateModelMetadata(modelIdentity, metadataProvider.Object, false);
var modelMetadata = CreateModelMetadata(modelIdentity, metadataProvider.Object, hasValidators: false);
var property1Identity = ModelMetadataIdentity.ForProperty(modelType.GetProperty(nameof(TypeWithProperties.PublicGetPublicSetProperty)), typeof(int), modelType);
var property1Metadata = CreateModelMetadata(property1Identity, metadataProvider.Object, false);
var property1Metadata = CreateModelMetadata(property1Identity, metadataProvider.Object, hasValidators: false);
var property2Identity = ModelMetadataIdentity.ForProperty(modelType.GetProperty(nameof(TypeWithProperties.PublicGetProtectedSetProperty)), typeof(int), modelType);
var property2Metadata = CreateModelMetadata(property2Identity, metadataProvider.Object, false);
var property2Metadata = CreateModelMetadata(property2Identity, metadataProvider.Object, hasValidators: false);
metadataProvider
.Setup(mp => mp.GetMetadataForProperties(modelType))
@ -1105,22 +1105,22 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding.Metadata
var modelType = typeof(Employee);
var modelIdentity = ModelMetadataIdentity.ForType(modelType);
var metadataProvider = new Mock<IModelMetadataProvider>();
var modelMetadata = CreateModelMetadata(modelIdentity, metadataProvider.Object, false);
var modelMetadata = CreateModelMetadata(modelIdentity, metadataProvider.Object, hasValidators: false);
var employeeId = ModelMetadataIdentity.ForProperty(modelType.GetProperty(nameof(Employee.Id)), typeof(int), modelType);
var employeeIdMetadata = CreateModelMetadata(modelIdentity, metadataProvider.Object, false);
var employeeIdMetadata = CreateModelMetadata(modelIdentity, metadataProvider.Object, hasValidators: false);
var employeeUnit = ModelMetadataIdentity.ForProperty(modelType.GetProperty(nameof(Employee.Unit)), typeof(BusinessUnit), modelType);
var employeeUnitMetadata = CreateModelMetadata(employeeUnit, metadataProvider.Object, false);
var employeeUnitMetadata = CreateModelMetadata(employeeUnit, metadataProvider.Object, hasValidators: false);
var employeeManager = ModelMetadataIdentity.ForProperty(modelType.GetProperty(nameof(Employee.Manager)), typeof(Employee), modelType);
var employeeManagerMetadata = CreateModelMetadata(employeeManager, metadataProvider.Object, false);
var employeeManagerMetadata = CreateModelMetadata(employeeManager, metadataProvider.Object, hasValidators: false);
var employeeEmployees = ModelMetadataIdentity.ForProperty(modelType.GetProperty(nameof(Employee.Employees)), typeof(List<Employee>), modelType);
var employeeEmployeesMetadata = CreateModelMetadata(employeeEmployees, metadataProvider.Object, false);
var employeeEmployeesMetadata = CreateModelMetadata(employeeEmployees, metadataProvider.Object, hasValidators: false);
var unitModel = typeof(BusinessUnit);
var unitHead = ModelMetadataIdentity.ForProperty(unitModel.GetProperty(nameof(BusinessUnit.Head)), typeof(Employee), unitModel);
var unitHeadMetadata = CreateModelMetadata(unitHead, metadataProvider.Object, false);
var unitHeadMetadata = CreateModelMetadata(unitHead, metadataProvider.Object, hasValidators: false);
var unitId = ModelMetadataIdentity.ForProperty(unitModel.GetProperty(nameof(BusinessUnit.Id)), typeof(int), unitModel);
var unitIdMetadata = CreateModelMetadata(unitId, metadataProvider.Object, true); // BusinessUnit.Id has validators.
var unitIdMetadata = CreateModelMetadata(unitId, metadataProvider.Object, hasValidators: true); // BusinessUnit.Id has validators.
metadataProvider
.Setup(mp => mp.GetMetadataForProperties(modelType))
@ -1146,22 +1146,22 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding.Metadata
var modelType = typeof(Employee);
var modelIdentity = ModelMetadataIdentity.ForType(modelType);
var metadataProvider = new Mock<IModelMetadataProvider>();
var modelMetadata = CreateModelMetadata(modelIdentity, metadataProvider.Object, false);
var modelMetadata = CreateModelMetadata(modelIdentity, metadataProvider.Object, hasValidators: false);
var employeeId = ModelMetadataIdentity.ForProperty(modelType.GetProperty(nameof(Employee.Id)), typeof(int), modelType);
var employeeIdMetadata = CreateModelMetadata(modelIdentity, metadataProvider.Object, false);
var employeeIdMetadata = CreateModelMetadata(modelIdentity, metadataProvider.Object, hasValidators: false);
var employeeUnit = ModelMetadataIdentity.ForProperty(modelType.GetProperty(nameof(Employee.Unit)), typeof(BusinessUnit), modelType);
var employeeUnitMetadata = CreateModelMetadata(employeeUnit, metadataProvider.Object, false);
var employeeUnitMetadata = CreateModelMetadata(employeeUnit, metadataProvider.Object, hasValidators: false);
var employeeManager = ModelMetadataIdentity.ForProperty(modelType.GetProperty(nameof(Employee.Manager)), typeof(Employee), modelType);
var employeeManagerMetadata = CreateModelMetadata(employeeManager, metadataProvider.Object, false);
var employeeManagerMetadata = CreateModelMetadata(employeeManager, metadataProvider.Object, hasValidators: false);
var employeeEmployees = ModelMetadataIdentity.ForProperty(modelType.GetProperty(nameof(Employee.Employees)), typeof(List<Employee>), modelType);
var employeeEmployeesMetadata = CreateModelMetadata(employeeEmployees, metadataProvider.Object, false);
var employeeEmployeesMetadata = CreateModelMetadata(employeeEmployees, metadataProvider.Object, hasValidators: false);
var unitModel = typeof(BusinessUnit);
var unitHead = ModelMetadataIdentity.ForProperty(unitModel.GetProperty(nameof(BusinessUnit.Head)), typeof(Employee), unitModel);
var unitHeadMetadata = CreateModelMetadata(unitHead, metadataProvider.Object, true); // BusinessUnit.Head has validators
var unitHeadMetadata = CreateModelMetadata(unitHead, metadataProvider.Object, hasValidators: true); // BusinessUnit.Head has validators
var unitId = ModelMetadataIdentity.ForProperty(unitModel.GetProperty(nameof(BusinessUnit.Id)), typeof(int), unitModel);
var unitIdMetadata = CreateModelMetadata(unitId, metadataProvider.Object, false);
var unitIdMetadata = CreateModelMetadata(unitId, metadataProvider.Object, hasValidators: false);
metadataProvider
.Setup(mp => mp.GetMetadataForProperties(modelType))
@ -1189,12 +1189,12 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding.Metadata
var modelType = typeof(Employee);
var modelIdentity = ModelMetadataIdentity.ForType(modelType);
var metadataProvider = new Mock<IModelMetadataProvider>();
var modelMetadata = CreateModelMetadata(modelIdentity, metadataProvider.Object, false);
var modelMetadata = CreateModelMetadata(modelIdentity, metadataProvider.Object, hasValidators: false);
var employeeId = ModelMetadataIdentity.ForProperty(modelType.GetProperty(nameof(Employee.Id)), typeof(int), modelType);
var employeeIdMetadata = CreateModelMetadata(modelIdentity, metadataProvider.Object, false);
var employeeEmployees = ModelMetadataIdentity.ForProperty(modelType.GetProperty(nameof(Employee.Employees)), typeof(List<Employee>), modelType);
var employeeEmployeesMetadata = CreateModelMetadata(employeeEmployees, metadataProvider.Object, false);
var employeeEmployeesMetadata = CreateModelMetadata(employeeEmployees, metadataProvider.Object, hasValidators: false);
metadataProvider
.Setup(mp => mp.GetMetadataForProperties(modelType))
@ -1202,7 +1202,7 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding.Metadata
metadataProvider
.Setup(mp => mp.GetMetadataForType(modelType))
.Returns(CreateModelMetadata(modelIdentity, metadataProvider.Object, true)); // Employees.Employee has validators
.Returns(CreateModelMetadata(modelIdentity, metadataProvider.Object, hasValidators: true)); // Employees.Employee has validators
// Act
var result = DefaultModelMetadata.CalculateHasValidators(new HashSet<DefaultModelMetadata>(), modelMetadata);
@ -1218,22 +1218,22 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding.Metadata
var modelType = typeof(Employee);
var modelIdentity = ModelMetadataIdentity.ForType(modelType);
var metadataProvider = new Mock<IModelMetadataProvider>();
var modelMetadata = CreateModelMetadata(modelIdentity, metadataProvider.Object, false);
var modelMetadata = CreateModelMetadata(modelIdentity, metadataProvider.Object, hasValidators: false);
var employeeId = ModelMetadataIdentity.ForProperty(modelType.GetProperty(nameof(Employee.Id)), typeof(int), modelType);
var employeeIdMetadata = CreateModelMetadata(modelIdentity, metadataProvider.Object, false);
var employeeIdMetadata = CreateModelMetadata(modelIdentity, metadataProvider.Object, hasValidators: false);
var employeeUnit = ModelMetadataIdentity.ForProperty(modelType.GetProperty(nameof(Employee.Unit)), typeof(BusinessUnit), modelType);
var employeeUnitMetadata = CreateModelMetadata(employeeUnit, metadataProvider.Object, false);
var employeeUnitMetadata = CreateModelMetadata(employeeUnit, metadataProvider.Object, hasValidators: false);
var employeeManager = ModelMetadataIdentity.ForProperty(modelType.GetProperty(nameof(Employee.Manager)), typeof(Employee), modelType);
var employeeManagerMetadata = CreateModelMetadata(employeeManager, metadataProvider.Object, false);
var employeeManagerMetadata = CreateModelMetadata(employeeManager, metadataProvider.Object, hasValidators: false);
var employeeEmployeesId = ModelMetadataIdentity.ForProperty(modelType.GetProperty(nameof(Employee.Employees)), typeof(List<Employee>), modelType);
var employeeEmployeesIdMetadata = CreateModelMetadata(employeeEmployeesId, metadataProvider.Object, false);
var employeeEmployeesIdMetadata = CreateModelMetadata(employeeEmployeesId, metadataProvider.Object, hasValidators: false);
var unitModel = typeof(BusinessUnit);
var unitHead = ModelMetadataIdentity.ForProperty(unitModel.GetProperty(nameof(BusinessUnit.Head)), typeof(Employee), unitModel);
var unitHeadMetadata = CreateModelMetadata(unitHead, metadataProvider.Object, false);
var unitHeadMetadata = CreateModelMetadata(unitHead, metadataProvider.Object, hasValidators: false);
var unitId = ModelMetadataIdentity.ForProperty(unitModel.GetProperty(nameof(BusinessUnit.Id)), typeof(int), unitModel);
var unitIdMetadata = CreateModelMetadata(unitId, metadataProvider.Object, false);
var unitIdMetadata = CreateModelMetadata(unitId, metadataProvider.Object, hasValidators: false);
metadataProvider
.Setup(mp => mp.GetMetadataForProperties(modelType))
@ -1254,8 +1254,277 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding.Metadata
Assert.False(result);
}
private record SimpleRecordType(int Property);
[Fact]
public void CalculateHasValidators_RecordType_ParametersWithNoValidators()
{
// Arrange
var modelType = typeof(SimpleRecordType);
var constructor = modelType.GetConstructors().Single();
var parameter = constructor.GetParameters().Single();
var modelIdentity = ModelMetadataIdentity.ForType(modelType);
var metadataProvider = new Mock<ModelMetadataProvider>();
var modelMetadata = CreateModelMetadata(modelIdentity, metadataProvider.Object, hasValidators: false);
modelMetadata.BindingMetadata.BoundConstructor = constructor;
var propertyId = ModelMetadataIdentity.ForProperty(modelType.GetProperty(nameof(SimpleRecordType.Property)), typeof(int), modelType);
var propertyMetadata = CreateModelMetadata(propertyId, metadataProvider.Object, hasValidators: false);
var parameterId = ModelMetadataIdentity.ForParameter(parameter);
// Parameter has no validation metadata.
var parameterMetadata = CreateModelMetadata(parameterId, metadataProvider.Object, hasValidators: false);
var constructorMetadata = CreateModelMetadata(
ModelMetadataIdentity.ForConstructor(constructor, modelType), metadataProvider.Object, hasValidators: null);
constructorMetadata.Details.BoundConstructorParameters = new[]
{
parameterMetadata,
};
metadataProvider
.Setup(mp => mp.GetMetadataForConstructor(constructor, modelType))
.Returns(constructorMetadata)
.Verifiable();
metadataProvider
.Setup(mp => mp.GetMetadataForProperties(modelType))
.Returns(new[] { propertyMetadata })
.Verifiable();
// Act
var result = DefaultModelMetadata.CalculateHasValidators(new HashSet<DefaultModelMetadata>(), modelMetadata);
// Assert
Assert.False(result);
metadataProvider.Verify();
}
[Fact]
public void CalculateHasValidators_RecordType_ParametersWithValidators()
{
// Arrange
var modelType = typeof(SimpleRecordType);
var constructor = modelType.GetConstructors().Single();
var parameter = constructor.GetParameters().Single();
var modelIdentity = ModelMetadataIdentity.ForType(modelType);
var metadataProvider = new Mock<ModelMetadataProvider>();
var modelMetadata = CreateModelMetadata(modelIdentity, metadataProvider.Object, hasValidators: false);
modelMetadata.BindingMetadata.BoundConstructor = constructor;
var propertyId = ModelMetadataIdentity.ForProperty(modelType.GetProperty(nameof(SimpleRecordType.Property)), typeof(int), modelType);
var propertyMetadata = CreateModelMetadata(propertyId, metadataProvider.Object, hasValidators: false);
var parameterId = ModelMetadataIdentity.ForParameter(parameter);
// Parameter has some validation metadata.
var parameterMetadata = CreateModelMetadata(parameterId, metadataProvider.Object, hasValidators: true);
var constructorMetadata = CreateModelMetadata(ModelMetadataIdentity.ForConstructor(constructor, modelType), metadataProvider.Object, hasValidators: null);
constructorMetadata.Details.BoundConstructorParameters = new[]
{
parameterMetadata,
};
metadataProvider
.Setup(mp => mp.GetMetadataForConstructor(constructor, modelType))
.Returns(constructorMetadata);
metadataProvider
.Setup(mp => mp.GetMetadataForProperties(modelType))
.Returns(new[] { propertyMetadata });
// Act
var result = DefaultModelMetadata.CalculateHasValidators(new HashSet<DefaultModelMetadata>(), modelMetadata);
// Assert
Assert.True(result);
}
private record SimpleRecordTypeWithProperty(int Property)
{
public int Property2 { get; set; }
}
[Fact]
public void CalculateHasValidators_RecordTypeWithProperty_NoValidators()
{
// Arrange
var modelType = typeof(SimpleRecordTypeWithProperty);
var constructor = modelType.GetConstructors().Single();
var parameter = constructor.GetParameters().Single();
var modelIdentity = ModelMetadataIdentity.ForType(modelType);
var metadataProvider = new Mock<ModelMetadataProvider>();
var modelMetadata = CreateModelMetadata(modelIdentity, metadataProvider.Object, hasValidators: false);
modelMetadata.BindingMetadata.BoundConstructor = constructor;
var propertyId = ModelMetadataIdentity.ForProperty(modelType.GetProperty(nameof(SimpleRecordTypeWithProperty.Property)), typeof(int), modelType);
var propertyMetadata = CreateModelMetadata(propertyId, metadataProvider.Object, hasValidators: false);
// Property2 has no validators
var property2Id = ModelMetadataIdentity.ForProperty(modelType.GetProperty(nameof(SimpleRecordTypeWithProperty.Property2)), typeof(int), modelType);
var property2Metadata = CreateModelMetadata(property2Id, metadataProvider.Object, hasValidators: false);
// Parameter named "Property" has no validators
var parameterId = ModelMetadataIdentity.ForParameter(parameter);
var parameterMetadata = CreateModelMetadata(parameterId, metadataProvider.Object, hasValidators: false);
var constructorMetadata = CreateModelMetadata(
ModelMetadataIdentity.ForConstructor(constructor, modelType), metadataProvider.Object, hasValidators: null);
constructorMetadata.Details.BoundConstructorParameters = new[]
{
parameterMetadata,
};
metadataProvider
.Setup(mp => mp.GetMetadataForConstructor(constructor, modelType))
.Returns(constructorMetadata);
metadataProvider
.Setup(mp => mp.GetMetadataForProperties(modelType))
.Returns(new[] { propertyMetadata, property2Metadata });
// Act
var result = DefaultModelMetadata.CalculateHasValidators(new HashSet<DefaultModelMetadata>(), modelMetadata);
// Assert
Assert.False(result);
}
[Fact]
public void CalculateHasValidators_RecordTypeWithProperty_ParameteryHasValidators()
{
// Arrange
var modelType = typeof(SimpleRecordTypeWithProperty);
var constructor = modelType.GetConstructors().Single();
var parameter = constructor.GetParameters().Single();
var modelIdentity = ModelMetadataIdentity.ForType(modelType);
var metadataProvider = new Mock<ModelMetadataProvider>();
var modelMetadata = CreateModelMetadata(modelIdentity, metadataProvider.Object, hasValidators: false);
modelMetadata.BindingMetadata.BoundConstructor = constructor;
var propertyId = ModelMetadataIdentity.ForProperty(modelType.GetProperty(nameof(SimpleRecordTypeWithProperty.Property)), typeof(int), modelType);
var propertyMetadata = CreateModelMetadata(propertyId, metadataProvider.Object, hasValidators: false);
// Property2 has no validators
var property2Id = ModelMetadataIdentity.ForProperty(modelType.GetProperty(nameof(SimpleRecordTypeWithProperty.Property2)), typeof(int), modelType);
var property2Metadata = CreateModelMetadata(property2Id, metadataProvider.Object, hasValidators: false);
// Parameter named "Property" has validators
var parameterId = ModelMetadataIdentity.ForParameter(parameter);
var parameterMetadata = CreateModelMetadata(parameterId, metadataProvider.Object, hasValidators: true);
var constructorMetadata = CreateModelMetadata(
ModelMetadataIdentity.ForConstructor(constructor, modelType), metadataProvider.Object, hasValidators: null);
constructorMetadata.Details.BoundConstructorParameters = new[]
{
parameterMetadata,
};
metadataProvider
.Setup(mp => mp.GetMetadataForConstructor(constructor, modelType))
.Returns(constructorMetadata);
metadataProvider
.Setup(mp => mp.GetMetadataForProperties(modelType))
.Returns(new[] { propertyMetadata, property2Metadata });
// Act
var result = DefaultModelMetadata.CalculateHasValidators(new HashSet<DefaultModelMetadata>(), modelMetadata);
// Assert
Assert.True(result);
}
[Fact]
public void CalculateHasValidators_RecordTypeWithProperty_PropertyHasValidators()
{
// Arrange
var modelType = typeof(SimpleRecordTypeWithProperty);
var constructor = modelType.GetConstructors().Single();
var parameter = constructor.GetParameters().Single();
var modelIdentity = ModelMetadataIdentity.ForType(modelType);
var metadataProvider = new Mock<ModelMetadataProvider>();
var modelMetadata = CreateModelMetadata(modelIdentity, metadataProvider.Object, hasValidators: false);
modelMetadata.BindingMetadata.BoundConstructor = constructor;
var propertyId = ModelMetadataIdentity.ForProperty(modelType.GetProperty(nameof(SimpleRecordTypeWithProperty.Property)), typeof(int), modelType);
var propertyMetadata = CreateModelMetadata(propertyId, metadataProvider.Object, hasValidators: false);
// Property2 has some validators
var property2Id = ModelMetadataIdentity.ForProperty(modelType.GetProperty(nameof(SimpleRecordTypeWithProperty.Property2)), typeof(int), modelType);
var property2Metadata = CreateModelMetadata(property2Id, metadataProvider.Object, hasValidators: true);
// Parameter named "Property" has no validators
var parameterId = ModelMetadataIdentity.ForParameter(parameter);
var parameterMetadata = CreateModelMetadata(parameterId, metadataProvider.Object, hasValidators: false);
var constructorMetadata = CreateModelMetadata(
ModelMetadataIdentity.ForConstructor(constructor, modelType), metadataProvider.Object, hasValidators: null);
constructorMetadata.Details.BoundConstructorParameters = new[]
{
parameterMetadata,
};
metadataProvider
.Setup(mp => mp.GetMetadataForConstructor(constructor, modelType))
.Returns(constructorMetadata);
metadataProvider
.Setup(mp => mp.GetMetadataForProperties(modelType))
.Returns(new[] { propertyMetadata, property2Metadata });
// Act
var result = DefaultModelMetadata.CalculateHasValidators(new HashSet<DefaultModelMetadata>(), modelMetadata);
// Assert
Assert.True(result);
}
[Fact]
public void CalculateHasValidators_RecordTypeWithProperty_MappedPropertyHasValidators_ValidatorsAreIgnored()
{
// Arrange
var modelType = typeof(SimpleRecordTypeWithProperty);
var constructor = modelType.GetConstructors().Single();
var parameter = constructor.GetParameters().Single();
var modelIdentity = ModelMetadataIdentity.ForType(modelType);
var metadataProvider = new Mock<ModelMetadataProvider>();
var modelMetadata = CreateModelMetadata(modelIdentity, metadataProvider.Object, hasValidators: false);
modelMetadata.BindingMetadata.BoundConstructor = constructor;
var propertyId = ModelMetadataIdentity.ForProperty(modelType.GetProperty(nameof(SimpleRecordTypeWithProperty.Property)), typeof(int), modelType);
var propertyMetadata = CreateModelMetadata(propertyId, metadataProvider.Object, hasValidators: true);
var property2Id = ModelMetadataIdentity.ForProperty(modelType.GetProperty(nameof(SimpleRecordTypeWithProperty.Property2)), typeof(int), modelType);
var property2Metadata = CreateModelMetadata(property2Id, metadataProvider.Object, hasValidators: false);
var parameterId = ModelMetadataIdentity.ForParameter(parameter);
var parameterMetadata = CreateModelMetadata(parameterId, metadataProvider.Object, hasValidators: false);
var constructorMetadata = CreateModelMetadata(
ModelMetadataIdentity.ForConstructor(constructor, modelType), metadataProvider.Object, hasValidators: null);
constructorMetadata.Details.BoundConstructorParameters = new[]
{
parameterMetadata,
};
metadataProvider
.Setup(mp => mp.GetMetadataForConstructor(constructor, modelType))
.Returns(constructorMetadata);
metadataProvider
.Setup(mp => mp.GetMetadataForProperties(modelType))
.Returns(new[] { propertyMetadata, property2Metadata });
// Act
var result = DefaultModelMetadata.CalculateHasValidators(new HashSet<DefaultModelMetadata>(), modelMetadata);
// Assert
Assert.False(result);
}
private static DefaultModelMetadata CreateModelMetadata(
ModelMetadataIdentity modelIdentity,
ModelMetadataIdentity modelIdentity,
IModelMetadataProvider metadataProvider,
bool? hasValidators)
{

View File

@ -54,7 +54,7 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding
var binderProviders = new IModelBinderProvider[]
{
new SimpleTypeModelBinderProvider(),
new ComplexTypeModelBinderProvider(),
new ComplexObjectModelBinderProvider(),
};
var validator = new DataAnnotationsModelValidatorProvider(
@ -96,7 +96,7 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding
var binderProviders = new IModelBinderProvider[]
{
new SimpleTypeModelBinderProvider(),
new ComplexTypeModelBinderProvider(),
new ComplexObjectModelBinderProvider(),
};
var validator = new DataAnnotationsModelValidatorProvider(
@ -162,7 +162,7 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding
var binderProviders = new IModelBinderProvider[]
{
new SimpleTypeModelBinderProvider(),
new ComplexTypeModelBinderProvider(),
new ComplexObjectModelBinderProvider(),
};
var validator = new DataAnnotationsModelValidatorProvider(
@ -242,7 +242,7 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding
var binderProviders = new IModelBinderProvider[]
{
new SimpleTypeModelBinderProvider(),
new ComplexTypeModelBinderProvider(),
new ComplexObjectModelBinderProvider(),
};
var validator = new DataAnnotationsModelValidatorProvider(
@ -293,7 +293,7 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding
var binderProviders = new IModelBinderProvider[]
{
new SimpleTypeModelBinderProvider(),
new ComplexTypeModelBinderProvider(),
new ComplexObjectModelBinderProvider(),
};
var validator = new DataAnnotationsModelValidatorProvider(
@ -490,7 +490,7 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding
var binderProviders = new IModelBinderProvider[]
{
new SimpleTypeModelBinderProvider(),
new ComplexTypeModelBinderProvider(),
new ComplexObjectModelBinderProvider(),
};
var validator = new DataAnnotationsModelValidatorProvider(
@ -570,7 +570,7 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding
var binderProviders = new IModelBinderProvider[]
{
new SimpleTypeModelBinderProvider(),
new ComplexTypeModelBinderProvider(),
new ComplexObjectModelBinderProvider(),
};
var validator = new DataAnnotationsModelValidatorProvider(

View File

@ -630,7 +630,7 @@ namespace Microsoft.AspNetCore.Mvc.DataAnnotations
var attributes = new[]
{
new TestBinderTypeProvider(),
new TestBinderTypeProvider() { BinderType = typeof(ComplexTypeModelBinder) }
new TestBinderTypeProvider() { BinderType = typeof(ComplexObjectModelBinder) }
};
var provider = CreateProvider(attributes);
@ -639,7 +639,7 @@ namespace Microsoft.AspNetCore.Mvc.DataAnnotations
var metadata = provider.GetMetadataForType(typeof(string));
// Assert
Assert.Same(typeof(ComplexTypeModelBinder), metadata.BinderType);
Assert.Same(typeof(ComplexObjectModelBinder), metadata.BinderType);
}
[Fact]
@ -648,7 +648,7 @@ namespace Microsoft.AspNetCore.Mvc.DataAnnotations
// Arrange
var attributes = new[]
{
new TestBinderTypeProvider() { BinderType = typeof(ComplexTypeModelBinder) },
new TestBinderTypeProvider() { BinderType = typeof(ComplexObjectModelBinder) },
new TestBinderTypeProvider() { BinderType = typeof(SimpleTypeModelBinder) }
};
@ -658,7 +658,7 @@ namespace Microsoft.AspNetCore.Mvc.DataAnnotations
var metadata = provider.GetMetadataForType(typeof(string));
// Assert
Assert.Same(typeof(ComplexTypeModelBinder), metadata.BinderType);
Assert.Same(typeof(ComplexObjectModelBinder), metadata.BinderType);
}
[Fact]

View File

@ -67,7 +67,7 @@ namespace Microsoft.AspNetCore.Mvc
binder => Assert.IsType<DictionaryModelBinderProvider>(binder),
binder => Assert.IsType<ArrayModelBinderProvider>(binder),
binder => Assert.IsType<CollectionModelBinderProvider>(binder),
binder => Assert.IsType<ComplexTypeModelBinderProvider>(binder));
binder => Assert.IsType<ComplexObjectModelBinderProvider>(binder));
}
[Fact]

View File

@ -299,6 +299,40 @@ namespace Microsoft.AspNetCore.Mvc.FunctionalTests
#endif
}
[Fact]
public async Task ValidationTagHelpers_UsingRecords()
{
// Arrange
var request = new HttpRequestMessage(HttpMethod.Post, "http://localhost/Customer/HtmlGeneration_Customer/CustomerWithRecords");
var nameValueCollection = new List<KeyValuePair<string, string>>
{
new KeyValuePair<string,string>("Number", string.Empty),
new KeyValuePair<string,string>("Name", string.Empty),
new KeyValuePair<string,string>("Email", string.Empty),
new KeyValuePair<string,string>("PhoneNumber", string.Empty),
new KeyValuePair<string,string>("Password", string.Empty)
};
request.Content = new FormUrlEncodedContent(nameValueCollection);
// Act
var response = await Client.SendAsync(request);
// Assert
var document = await response.GetHtmlDocumentAsync();
var validation = document.RequiredQuerySelector("span[data-valmsg-for=Number]");
Assert.Equal("The value '' is invalid.", validation.TextContent);
validation = document.QuerySelector("span[data-valmsg-for=Name]");
Assert.Null(validation);
validation = document.QuerySelector("span[data-valmsg-for=Email]");
Assert.Equal("field-validation-valid", validation.ClassName);
validation = document.QuerySelector("span[data-valmsg-for=Password]");
Assert.Equal("The Password field is required.", validation.TextContent);
}
[Fact]
public async Task CacheTagHelper_CanCachePortionsOfViewsPartialViewsAndViewComponents()
{

View File

@ -5,6 +5,7 @@ using System;
using System.Linq;
using System.Net;
using System.Net.Http;
using System.Net.Http.Json;
using System.Text;
using System.Text.Json.Serialization;
using System.Threading.Tasks;
@ -121,6 +122,71 @@ namespace Microsoft.AspNetCore.Mvc.FunctionalTests
Assert.Equal(expected.StreetName, actual.StreetName);
}
[Fact]
public virtual async Task JsonInputFormatter_RoundtripsRecordType()
{
// Arrange
var expected = new JsonFormatterController.SimpleRecordModel(18, "James", "JnK");
// Act
var response = await Client.PostAsJsonAsync("http://localhost/JsonFormatter/RoundtripRecordType/", expected);
var actual = await response.Content.ReadAsAsync<JsonFormatterController.SimpleRecordModel>();
// Assert
Assert.Equal(HttpStatusCode.OK, response.StatusCode);
Assert.Equal(expected.Id, actual.Id);
Assert.Equal(expected.Name, actual.Name);
Assert.Equal(expected.StreetName, actual.StreetName);
}
[Fact]
public virtual async Task JsonInputFormatter_ValidationWithRecordTypes_ValidationErrors()
{
// Arrange
var expected = new JsonFormatterController.SimpleModelWithValidation(123, "This is a very long name", StreetName: null);
// Act
var response = await Client.PostAsJsonAsync($"JsonFormatter/{nameof(JsonFormatterController.RoundtripModelWithValidation)}", expected);
// Assert
await response.AssertStatusCodeAsync(HttpStatusCode.BadRequest);
var problem = await response.Content.ReadFromJsonAsync<ValidationProblemDetails>();
Assert.Collection(
problem.Errors.OrderBy(e => e.Key),
kvp =>
{
Assert.Equal("Id", kvp.Key);
Assert.Equal("The field Id must be between 1 and 100.", Assert.Single(kvp.Value));
},
kvp =>
{
Assert.Equal("Name", kvp.Key);
Assert.Equal("The field Name must be a string with a minimum length of 2 and a maximum length of 8.", Assert.Single(kvp.Value));
},
kvp =>
{
Assert.Equal("StreetName", kvp.Key);
Assert.Equal("The StreetName field is required.", Assert.Single(kvp.Value));
});
}
[Fact]
public virtual async Task JsonInputFormatter_ValidationWithRecordTypes_NoValidationErrors()
{
// Arrange
var expected = new JsonFormatterController.SimpleModelWithValidation(99, "TestName", "Some address");
// Act
var response = await Client.PostAsJsonAsync($"JsonFormatter/{nameof(JsonFormatterController.RoundtripModelWithValidation)}", expected);
// Assert
await response.AssertStatusCodeAsync(HttpStatusCode.OK);
var actual = await response.Content.ReadFromJsonAsync<JsonFormatterController.SimpleModel>();
Assert.Equal(expected.Id, actual.Id);
Assert.Equal(expected.Name, actual.Name);
Assert.Equal(expected.StreetName, actual.StreetName);
}
[Fact]
public async Task JsonInputFormatter_Returns415UnsupportedMediaType_ForEmptyContentType()
{

View File

@ -13,13 +13,16 @@ namespace Microsoft.AspNetCore.Mvc.FunctionalTests
{
}
[Theory(Skip = "https://github.com/dotnet/corefx/issues/36025")]
[InlineData("\"I'm a JSON string!\"")]
[InlineData("true")]
[InlineData("\"\"")] // Empty string
public override Task JsonInputFormatter_ReturnsDefaultValue_ForValueTypes(string input)
{
return base.JsonInputFormatter_ReturnsDefaultValue_ForValueTypes(input);
}
[Fact(Skip = "https://github.com/dotnet/runtime/issues/38539")]
public override Task JsonInputFormatter_RoundtripsRecordType()
=> base.JsonInputFormatter_RoundtripsRecordType();
[Fact(Skip = "https://github.com/dotnet/runtime/issues/38539")]
public override Task JsonInputFormatter_ValidationWithRecordTypes_NoValidationErrors()
=> base.JsonInputFormatter_ValidationWithRecordTypes_NoValidationErrors();
[Fact(Skip = "https://github.com/dotnet/runtime/issues/38539")]
public override Task JsonInputFormatter_ValidationWithRecordTypes_ValidationErrors()
=> base.JsonInputFormatter_ValidationWithRecordTypes_ValidationErrors();
}
}

View File

@ -11,6 +11,7 @@ using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc.Abstractions;
using Microsoft.AspNetCore.Mvc.ModelBinding;
using Microsoft.AspNetCore.Mvc.ModelBinding.Binders;
using Microsoft.AspNetCore.Mvc.ModelBinding.Validation;
using Microsoft.Extensions.Logging.Abstractions;
using Xunit;
@ -403,6 +404,72 @@ namespace Microsoft.AspNetCore.Mvc.IntegrationTests
});
var modelState = testContext.ModelState;
// Act & Assert
var exception = await Assert.ThrowsAsync<InvalidOperationException>(() => parameterBinder.BindModelAsync(parameter, testContext));
Assert.Equal(
string.Format(
"Could not create an instance of type '{0}'. Model bound complex types must not be abstract or " +
"value types and must have a parameterless constructor. Record types must have a single primary constructor. " +
"Alternatively, set the '{1}' property to a non-null value in the '{2}' constructor.",
typeof(ClassWithNoDefaultConstructor).FullName,
nameof(Class1.Property1),
typeof(Class1).FullName),
exception.Message);
}
public record ActionParameter_DefaultValueConstructor(string Name = "test", int Age = 23);
[Fact]
public async Task ActionParameter_UsesDefaultConstructorParameters()
{
// Arrange
var parameterType = typeof(ActionParameter_DefaultValueConstructor);
var parameterBinder = ModelBindingTestHelper.GetParameterBinder();
var parameter = new ParameterDescriptor()
{
Name = "p",
ParameterType = parameterType
};
var testContext = ModelBindingTestHelper.GetTestContext(request =>
{
request.QueryString = QueryString.Create("Name", "James");
});
var modelState = testContext.ModelState;
// Act
var result = await parameterBinder.BindModelAsync(parameter, testContext);
// Assert
Assert.True(modelState.IsValid);
var model = Assert.IsType<ActionParameter_DefaultValueConstructor>(result.Model);
Assert.Equal("James", model.Name);
Assert.Equal(23, model.Age);
}
[Fact]
public async Task ActionParameter_UsingComplexTypeModelBinder_ModelPropertyTypeWithNoParameterlessConstructor_ThrowsException()
{
// Arrange
var parameterType = typeof(Class1);
var parameterBinder = ModelBindingTestHelper.GetParameterBinder();
var parameter = new ParameterDescriptor()
{
Name = "p",
ParameterType = parameterType
};
var testContext = ModelBindingTestHelper.GetTestContext(request =>
{
request.QueryString = QueryString.Create("Name", "James").Add("Property1.City", "Seattle");
}, updateOptions: options =>
{
options.ModelBinderProviders.RemoveType<ComplexObjectModelBinderProvider>();
#pragma warning disable CS0618 // Type or member is obsolete
options.ModelBinderProviders.Add(new ComplexTypeModelBinderProvider());
#pragma warning restore CS0618 // Type or member is obsolete
});
var modelState = testContext.ModelState;
// Act & Assert
var exception = await Assert.ThrowsAsync<InvalidOperationException>(() => parameterBinder.BindModelAsync(parameter, testContext));
Assert.Equal(
@ -434,21 +501,19 @@ namespace Microsoft.AspNetCore.Mvc.IntegrationTests
Assert.Equal(
string.Format(
"Could not create an instance of type '{0}'. Model bound complex types must not be abstract or " +
"value types and must have a parameterless constructor.",
"value types and must have a parameterless constructor. Record types must have a single primary constructor.",
typeof(PointStruct).FullName),
exception.Message);
}
[Theory]
[InlineData(typeof(ClassWithNoDefaultConstructor))]
[InlineData(typeof(AbstractClassWithNoDefaultConstructor))]
public async Task ActionParameter_BindingToTypeWithNoParameterlessConstructor_ThrowsException(Type parameterType)
[Fact]
public async Task ActionParameter_BindingToAbstractionType_ThrowsException()
{
// Arrange
var parameterBinder = ModelBindingTestHelper.GetParameterBinder();
var parameter = new ParameterDescriptor()
{
ParameterType = parameterType,
ParameterType = typeof(AbstractClassWithNoDefaultConstructor),
Name = "p"
};
var testContext = ModelBindingTestHelper.GetTestContext();
@ -458,8 +523,78 @@ namespace Microsoft.AspNetCore.Mvc.IntegrationTests
Assert.Equal(
string.Format(
"Could not create an instance of type '{0}'. Model bound complex types must not be abstract or " +
"value types and must have a parameterless constructor.",
parameterType.FullName),
"value types and must have a parameterless constructor. Record types must have a single primary constructor.",
typeof(AbstractClassWithNoDefaultConstructor).FullName),
exception.Message);
}
public class ActionParameter_MultipleConstructorsWithDefaultValues_NoParameterlessConstructorModel
{
public ActionParameter_MultipleConstructorsWithDefaultValues_NoParameterlessConstructorModel(string name = "default-name") => (Name) = (name);
public ActionParameter_MultipleConstructorsWithDefaultValues_NoParameterlessConstructorModel(string name, int age) => (Name, Age) = (name, age);
public string Name { get; init; }
public int Age { get; init; }
}
[Fact]
public async Task ActionParameter_MultipleConstructorsWithDefaultValues_NoParameterlessConstructor_Throws()
{
// Arrange
var parameterType = typeof(ActionParameter_MultipleConstructorsWithDefaultValues_NoParameterlessConstructorModel);
var parameterBinder = ModelBindingTestHelper.GetParameterBinder();
var parameter = new ParameterDescriptor()
{
Name = "p",
ParameterType = parameterType
};
var testContext = ModelBindingTestHelper.GetTestContext(request =>
{
request.QueryString = QueryString.Create("Name", "James");
});
var modelState = testContext.ModelState;
// Act & Assert
var exception = await Assert.ThrowsAsync<InvalidOperationException>(() => parameterBinder.BindModelAsync(parameter, testContext));
Assert.Equal(
string.Format(
"Could not create an instance of type '{0}'. Model bound complex types must not be abstract or " +
"value types and must have a parameterless constructor. Record types must have a single primary constructor.",
typeof(ActionParameter_MultipleConstructorsWithDefaultValues_NoParameterlessConstructorModel).FullName),
exception.Message);
}
public record ActionParameter_RecordTypeWithMultipleConstructors(string Name, int Age)
{
public ActionParameter_RecordTypeWithMultipleConstructors(string Name) : this(Name, 0) { }
}
[Fact]
public async Task ActionParameter_RecordTypeWithMultipleConstructors_Throws()
{
// Arrange
var parameterType = typeof(ActionParameter_RecordTypeWithMultipleConstructors);
var parameterBinder = ModelBindingTestHelper.GetParameterBinder();
var parameter = new ParameterDescriptor()
{
Name = "p",
ParameterType = parameterType
};
var testContext = ModelBindingTestHelper.GetTestContext(request =>
{
request.QueryString = QueryString.Create("Name", "James").Add("Age", "29");
});
var modelState = testContext.ModelState;
// Act & Assert
var exception = await Assert.ThrowsAsync<InvalidOperationException>(() => parameterBinder.BindModelAsync(parameter, testContext));
Assert.Equal(
string.Format(
"Could not create an instance of type '{0}'. Model bound complex types must not be abstract or " +
"value types and must have a parameterless constructor. Record types must have a single primary constructor.",
typeof(ActionParameter_RecordTypeWithMultipleConstructors).FullName),
exception.Message);
}
@ -527,6 +662,46 @@ namespace Microsoft.AspNetCore.Mvc.IntegrationTests
Assert.True(modelState.IsValid);
}
[Fact]
public async Task ActionParameter_WithValidateNever_DoesNotGetValidated()
{
// Arrange
var parameterBinder = ModelBindingTestHelper.GetParameterBinder();
var parameter = new ParameterDescriptor()
{
Name = ParameterWithValidateNever.ValidateNeverParameterInfo.Name,
ParameterType = typeof(ModelWithIValidatableObject)
};
var testContext = ModelBindingTestHelper.GetTestContext(request =>
{
request.QueryString = QueryString.Create(nameof(ModelWithIValidatableObject.FirstName), "TestName");
});
var modelState = testContext.ModelState;
var modelMetadataProvider = TestModelMetadataProvider.CreateDefaultProvider();
var modelMetadata = modelMetadataProvider
.GetMetadataForParameter(ParameterWithValidateNever.ValidateNeverParameterInfo);
// Act
var modelBindingResult = await parameterBinder.BindModelAsync(
parameter,
testContext,
modelMetadataProvider,
modelMetadata);
// Assert
Assert.True(modelBindingResult.IsModelSet);
var model = Assert.IsType<ModelWithIValidatableObject>(modelBindingResult.Model);
Assert.Equal("TestName", model.FirstName);
// No validation errors are expected.
// Assert.True(modelState.IsValid);
// Tracking bug to enable this scenario: https://github.com/dotnet/aspnetcore/issues/24241
Assert.False(modelState.IsValid);
}
[Theory]
[InlineData(123, true)]
[InlineData(null, false)]
@ -800,6 +975,29 @@ namespace Microsoft.AspNetCore.Mvc.IntegrationTests
}
}
private class ParameterWithValidateNever
{
public void MyAction([Required] string Name, [ValidateNever] ModelWithIValidatableObject validatableObject)
{
}
private static MethodInfo MyActionMethodInfo
=> typeof(ParameterWithValidateNever).GetMethod(nameof(MyAction));
public static ParameterInfo NameParamterInfo
=> MyActionMethodInfo.GetParameters()[0];
public static ParameterInfo ValidateNeverParameterInfo
=> MyActionMethodInfo.GetParameters()[1];
public static ParameterInfo GetParameterInfo(string parameterName)
{
return MyActionMethodInfo
.GetParameters()
.Single(p => p.Name.Equals(parameterName, StringComparison.Ordinal));
}
}
private class CustomReadOnlyCollection<T> : ICollection<T>
{
private ICollection<T> _original;
@ -865,7 +1063,9 @@ namespace Microsoft.AspNetCore.Mvc.IntegrationTests
// By default the ComplexTypeModelBinder fails to construct models for types with no parameterless constructor,
// but a developer could change this behavior by overriding CreateModel
#pragma warning disable CS0618 // Type or member is obsolete
private class CustomComplexTypeModelBinder : ComplexTypeModelBinder
#pragma warning restore CS0618 // Type or member is obsolete
{
public CustomComplexTypeModelBinder(IDictionary<ModelMetadata, IModelBinder> propertyBinders)
: base(propertyBinders, NullLoggerFactory.Instance)

View File

@ -0,0 +1,13 @@
// 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 Microsoft.AspNetCore.Mvc.ModelBinding.Binders;
namespace Microsoft.AspNetCore.Mvc.IntegrationTests
{
public class ComplexObjectIntegrationTest : ComplexTypeIntegrationTestBase
{
protected override Type ExpectedModelBinderType => typeof(ComplexObjectModelBinder);
}
}

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@ -3,6 +3,7 @@
<PropertyGroup>
<TargetFramework>$(DefaultNetCoreTargetFramework)</TargetFramework>
<UseSharedCompilation>false</UseSharedCompilation>
<LangVersion>9.0</LangVersion>
</PropertyGroup>
<ItemGroup>

View File

@ -1139,6 +1139,179 @@ namespace Microsoft.AspNetCore.Mvc.IntegrationTests
Assert.Equal(ModelValidationState.Valid, state.ValidationState);
}
private record AddressRecord(string Street, string City)
{
public string ZipCode { get; set; }
}
[Fact]
public async Task TryUpdateModel_RecordTypeModel_DoesNotOverwriteConstructorParameters()
{
// Arrange
var testContext = ModelBindingTestHelper.GetTestContext(request =>
{
request.QueryString = QueryString.Create("Street", "SomeStreet");
});
var modelState = testContext.ModelState;
var model = new AddressRecord("DefaultStreet", "Toronto")
{
ZipCode = "98001",
};
var oldModel = model;
// Act
var result = await TryUpdateModelAsync(model, string.Empty, testContext);
// Assert
Assert.True(result);
// Model
Assert.Same(oldModel, model);
Assert.Equal("DefaultStreet", model.Street);
Assert.Equal("Toronto", model.City);
Assert.Equal("98001", model.ZipCode);
// ModelState
Assert.True(modelState.IsValid);
Assert.Empty(modelState);
}
[Fact]
public async Task TryUpdateModel_RecordTypeModel_UpdatesProperties()
{
// Arrange
var testContext = ModelBindingTestHelper.GetTestContext(request =>
{
request.QueryString = QueryString.Create("ZipCode", "98007").Add("Street", "SomeStreet");
});
var modelState = testContext.ModelState;
var model = new AddressRecord("DefaultStreet", "Toronto")
{
ZipCode = "98001",
};
var oldModel = model;
// Act
var result = await TryUpdateModelAsync(model, string.Empty, testContext);
// Assert
Assert.True(result);
// Model
Assert.Same(oldModel, model);
Assert.Equal("DefaultStreet", model.Street);
Assert.Equal("Toronto", model.City);
Assert.Equal("98007", model.ZipCode);
// ModelState
Assert.True(modelState.IsValid);
var entry = Assert.Single(modelState);
Assert.Equal("ZipCode", entry.Key);
var state = entry.Value;
Assert.Equal("98007", state.AttemptedValue);
Assert.Equal("98007", state.RawValue);
Assert.Empty(state.Errors);
Assert.Equal(ModelValidationState.Valid, state.ValidationState);
}
private class ModelWithRecordTypeProperty
{
public AddressRecord Address { get; set; }
}
[Fact]
public async Task TryUpdateModel_RecordTypeProperty()
{
// Arrange
var testContext = ModelBindingTestHelper.GetTestContext(request =>
{
request.QueryString = QueryString.Create("Address.ZipCode", "98007").Add("Address.Street", "SomeStreet");
});
var modelState = testContext.ModelState;
var model = new ModelWithRecordTypeProperty();
var oldModel = model;
// Act
var result = await TryUpdateModelAsync(model, string.Empty, testContext);
// Assert
Assert.True(result);
// Model
Assert.Same(oldModel, model);
Assert.NotNull(model.Address);
var address = model.Address;
Assert.Equal("SomeStreet", address.Street);
Assert.Null(address.City);
Assert.Equal("98007", address.ZipCode);
// ModelState
Assert.True(modelState.IsValid);
Assert.Equal(2, modelState.Count);
var entry = Assert.Single(modelState, k => k.Key == "Address.ZipCode");
var state = entry.Value;
Assert.Equal("98007", state.AttemptedValue);
Assert.Equal("98007", state.RawValue);
Assert.Empty(state.Errors);
Assert.Equal(ModelValidationState.Valid, state.ValidationState);
entry = Assert.Single(modelState, k => k.Key == "Address.Street");
state = entry.Value;
Assert.Equal("SomeStreet", state.AttemptedValue);
Assert.Equal("SomeStreet", state.RawValue);
Assert.Empty(state.Errors);
Assert.Equal(ModelValidationState.Valid, state.ValidationState);
}
[Fact]
public async Task TryUpdateModel_RecordTypeProperty_InitializedDoesNotOverwriteConstructorParameters()
{
// Arrange
var testContext = ModelBindingTestHelper.GetTestContext(request =>
{
request.QueryString = QueryString.Create("Address.ZipCode", "98007").Add("Address.Street", "SomeStreet");
});
var modelState = testContext.ModelState;
var model = new ModelWithRecordTypeProperty
{
Address = new AddressRecord("DefaultStreet", "DefaultCity")
{
ZipCode = "98056",
},
};
var oldModel = model;
// Act
var result = await TryUpdateModelAsync(model, string.Empty, testContext);
// Assert
Assert.True(result);
// Model
Assert.Same(oldModel, model);
Assert.NotNull(model.Address);
var address = model.Address;
Assert.Equal("DefaultStreet", address.Street);
Assert.Equal("DefaultCity", address.City);
Assert.Equal("98007", address.ZipCode);
// ModelState
Assert.True(modelState.IsValid);
var entry = Assert.Single(modelState);
var state = entry.Value;
Assert.Equal("98007", state.AttemptedValue);
Assert.Equal("98007", state.RawValue);
Assert.Empty(state.Errors);
Assert.Equal(ModelValidationState.Valid, state.ValidationState);
}
private void UpdateRequest(HttpRequest request, string data, string name)
{
const string fileName = "text.txt";
@ -1237,4 +1410,4 @@ namespace Microsoft.AspNetCore.Mvc.IntegrationTests
ModelBindingTestHelper.GetObjectValidator(testContext.MetadataProvider));
}
}
}
}

File diff suppressed because it is too large Load Diff

View File

@ -1,7 +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;
using System.Buffers;
using System.ComponentModel.DataAnnotations;
using System.Security.Cryptography.X509Certificates;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.Formatters;
@ -44,7 +47,7 @@ namespace FormatterWebSite.Controllers
}
[HttpPost]
public IActionResult ReturnInput([FromBody]DummyClass dummyObject)
public IActionResult ReturnInput([FromBody] DummyClass dummyObject)
{
if (!ModelState.IsValid)
{
@ -71,6 +74,9 @@ namespace FormatterWebSite.Controllers
return model;
}
[HttpPost]
public ActionResult<SimpleRecordModel> RoundtripRecordType([FromBody] SimpleRecordModel model) => model;
public class SimpleModel
{
public int Id { get; set; }
@ -79,5 +85,26 @@ namespace FormatterWebSite.Controllers
public string StreetName { get; set; }
}
public record SimpleRecordModel(int Id, string Name, string StreetName);
public record SimpleModelWithValidation(
[Range(1, 100)]
int Id,
[Required]
[StringLength(8, MinimumLength = 2)]
string Name,
[Required]
string StreetName);
[HttpPost]
public ActionResult<SimpleModelWithValidation> RoundtripModelWithValidation([FromBody] SimpleModelWithValidation model)
{
if (!ModelState.IsValid)
return ValidationProblem();
return model;
}
}
}

View File

@ -1,7 +1,8 @@
<Project Sdk="Microsoft.NET.Sdk.Web">
<Project Sdk="Microsoft.NET.Sdk.Web">
<PropertyGroup>
<TargetFramework>$(DefaultNetCoreTargetFramework)</TargetFramework>
<LangVersion>9.0</LangVersion>
</PropertyGroup>
<ItemGroup>

View File

@ -1,9 +1,10 @@
// Copyright (c) .NET Foundation. All rights reserved.
// 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.ComponentModel.DataAnnotations;
using System.Linq;
using Microsoft.AspNetCore.Mvc.ModelBinding.Validation;
namespace FormatterWebSite
{
@ -15,6 +16,7 @@ namespace FormatterWebSite
Value = identifier;
}
[Required]
public string Value { get; }
public RecursiveIdentifier AccountIdentifier => new RecursiveIdentifier(Value);
@ -24,4 +26,4 @@ namespace FormatterWebSite
return Enumerable.Empty<ValidationResult>();
}
}
}
}

View File

@ -12,5 +12,10 @@ namespace HtmlGenerationWebSite.Areas.Customer.Controllers
{
return View("Customer");
}
public IActionResult CustomerWithRecords(Models.CustomerRecord customer)
{
return View("CustomerWithRecords");
}
}
}

View File

@ -2,6 +2,7 @@
<PropertyGroup>
<TargetFramework>$(DefaultNetCoreTargetFramework)</TargetFramework>
<LangVersion>9.0</LangVersion>
</PropertyGroup>
<ItemGroup>

View File

@ -0,0 +1,29 @@
// 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.ComponentModel.DataAnnotations;
using Microsoft.AspNetCore.Mvc;
namespace HtmlGenerationWebSite.Models
{
public record CustomerRecord
(
[Range(1, 100)]
int Number,
string Name,
[Required]
string Password,
[EnumDataType(typeof(Gender))]
Gender Gender,
string PhoneNumber,
[DataType(DataType.EmailAddress)]
string Email,
string Key
);
}

View File

@ -0,0 +1,46 @@
@model HtmlGenerationWebSite.Models.CustomerRecord
@{
ViewBag.Title = "Customer Page";
}
@addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers
<html>
<body>
<form asp-route-area="Customer" asp-controller="HtmlGeneration_Customer" asp-action="Index">
<div id="Number">
<label asp-for="Number" class="order"></label>
<input asp-for="Number" type="number" class="form-control" />
<span asp-validation-for="Number"></span>
</div>
<div id="Name">
<label asp-for="Name" class="order"></label>
<input asp-for="Name" type="text" />
</div>
<div id="Email">
<label asp-for="Email" class="order"></label>
<input asp-for="Email" type="email" />
<span asp-validation-for="Email"></span>
</div>
<div id="PhoneNumber">
<label asp-for="PhoneNumber" class="order"></label>
<input asp-for="PhoneNumber" type="tel" />
</div>
<div id="Password">
<label asp-for="Password" class="order"></label>
<input asp-for="Password" type="password" class="form-control" />
<span asp-validation-for="Password"></span>
</div>
<div id="Gender">
<label asp-for="Gender" class="order"></label>
<input asp-for="Gender" type="radio" value="Male" /> Male
<input asp-for="Gender" type="radio" value="Female" /> Female
<span asp-validation-for="Gender"></span>
</div>
<div id="validation-summary-all" asp-validation-summary="All" class="order"></div>
<div id="validation-summary-model" asp-validation-summary="ModelOnly" class="order"></div>
<input type="submit"/>
</form>
</body>
</html>