Merge in 'release/5.0-preview8' changes
This commit is contained in:
commit
670f9523df
|
|
@ -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; }
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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!;
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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}'.";
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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(),
|
||||
};
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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(
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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());
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
{
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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 />
|
||||
|
|
|
|||
|
|
@ -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; }
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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; }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
{
|
||||
|
|
|
|||
|
|
@ -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; }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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 />
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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]
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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
|
|
@ -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
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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; }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
{
|
||||
|
|
|
|||
|
|
@ -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(
|
||||
|
|
|
|||
|
|
@ -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]
|
||||
|
|
|
|||
|
|
@ -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]
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
{
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
{
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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
File diff suppressed because it is too large
Load Diff
|
|
@ -3,6 +3,7 @@
|
|||
<PropertyGroup>
|
||||
<TargetFramework>$(DefaultNetCoreTargetFramework)</TargetFramework>
|
||||
<UseSharedCompilation>false</UseSharedCompilation>
|
||||
<LangVersion>9.0</LangVersion>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
|
|
|
|||
|
|
@ -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
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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>();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -12,5 +12,10 @@ namespace HtmlGenerationWebSite.Areas.Customer.Controllers
|
|||
{
|
||||
return View("Customer");
|
||||
}
|
||||
|
||||
public IActionResult CustomerWithRecords(Models.CustomerRecord customer)
|
||||
{
|
||||
return View("CustomerWithRecords");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -2,6 +2,7 @@
|
|||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>$(DefaultNetCoreTargetFramework)</TargetFramework>
|
||||
<LangVersion>9.0</LangVersion>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
|
|
|
|||
|
|
@ -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
|
||||
);
|
||||
}
|
||||
|
|
@ -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>
|
||||
Loading…
Reference in New Issue