Merge pull request #24258 from dotnet-maestro-bot/merge/release/5.0-preview8-to-master
[automated] Merge branch 'release/5.0-preview8' => 'master'
This commit is contained in:
commit
3ea1fc7aac
|
|
@ -110,21 +110,6 @@ namespace Microsoft.Extensions.DependencyInjection
|
|||
public static IServiceCollection ConfigureApplicationCookie(this IServiceCollection services, Action<CookieAuthenticationOptions> configure)
|
||||
=> services.Configure(IdentityConstants.ApplicationScheme, configure);
|
||||
|
||||
/// <summary>
|
||||
/// Configures the application cookie.
|
||||
/// </summary>
|
||||
/// <typeparam name="TService">TService: A service resolved from the IServiceProvider for use when configuring this authentication provider. If you need multiple services then specify IServiceProvider and resolve them directly.</typeparam>
|
||||
/// <param name="services">The services available in the application.</param>
|
||||
/// <param name="configure">An action to configure the <see cref="CookieAuthenticationOptions"/>.</param>
|
||||
/// <returns>The services.</returns>
|
||||
public static IServiceCollection ConfigureApplicationCookie<TService>(this IServiceCollection services, Action<CookieAuthenticationOptions, TService> configure) where TService : class
|
||||
{
|
||||
services.AddOptions<CookieAuthenticationOptions>(IdentityConstants.ApplicationScheme)
|
||||
.Configure(configure);
|
||||
|
||||
return services;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Configure the external cookie.
|
||||
/// </summary>
|
||||
|
|
@ -133,20 +118,5 @@ namespace Microsoft.Extensions.DependencyInjection
|
|||
/// <returns>The services.</returns>
|
||||
public static IServiceCollection ConfigureExternalCookie(this IServiceCollection services, Action<CookieAuthenticationOptions> configure)
|
||||
=> services.Configure(IdentityConstants.ExternalScheme, configure);
|
||||
|
||||
/// <summary>
|
||||
/// Configure the external cookie.
|
||||
/// </summary>
|
||||
/// <typeparam name="TService">TService: A service resolved from the IServiceProvider for use when configuring this authentication provider. If you need multiple services then specify IServiceProvider and resolve them directly.</typeparam>
|
||||
/// <param name="services">The services available in the application.</param>
|
||||
/// <param name="configure">An action to configure the <see cref="CookieAuthenticationOptions"/>.</param>
|
||||
/// <returns>The services.</returns>
|
||||
public static IServiceCollection ConfigureExternalCookie<TService>(this IServiceCollection services, Action<CookieAuthenticationOptions, TService> configure) where TService : class
|
||||
{
|
||||
services.AddOptions<CookieAuthenticationOptions>(IdentityConstants.ExternalScheme)
|
||||
.Configure(configure);
|
||||
|
||||
return services;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
@ -26,6 +26,7 @@
|
|||
},
|
||||
"BlazorServerWeb-CSharp": {
|
||||
"commandName": "Project",
|
||||
"dotnetRunMessages": "true",
|
||||
"launchBrowser": true,
|
||||
//#if(RequiresHttps)
|
||||
"applicationUrl": "https://localhost:5001;http://localhost:5000",
|
||||
|
|
|
|||
|
|
@ -27,6 +27,7 @@
|
|||
},
|
||||
"ComponentsWebAssembly-CSharp": {
|
||||
"commandName": "Project",
|
||||
"dotnetRunMessages": "true",
|
||||
"launchBrowser": true,
|
||||
"inspectUri": "{wsProtocol}://{url.hostname}:{url.port}/_framework/debug/ws-proxy?browser={browserInspectUri}",
|
||||
//#if(RequiresHttps)
|
||||
|
|
|
|||
|
|
@ -22,6 +22,7 @@
|
|||
},
|
||||
"ComponentsWebAssembly-CSharp.Server": {
|
||||
"commandName": "Project",
|
||||
"dotnetRunMessages": "true",
|
||||
"launchBrowser": true,
|
||||
"inspectUri": "{wsProtocol}://{url.hostname}:{url.port}/_framework/debug/ws-proxy?browser={browserInspectUri}",
|
||||
//#if(RequiresHttps)
|
||||
|
|
|
|||
|
|
@ -1,11 +1,11 @@
|
|||
{
|
||||
"iisSettings": {
|
||||
//#if (WindowsAuth)
|
||||
"windowsAuthentication": true,
|
||||
"anonymousAuthentication": false,
|
||||
//#else
|
||||
"windowsAuthentication": false,
|
||||
"anonymousAuthentication": true,
|
||||
//#if (WindowsAuth)
|
||||
"windowsAuthentication": true,
|
||||
"anonymousAuthentication": false,
|
||||
//#else
|
||||
"windowsAuthentication": false,
|
||||
"anonymousAuthentication": true,
|
||||
//#endif
|
||||
"iisExpress": {
|
||||
"applicationUrl": "http://localhost:8080",
|
||||
|
|
@ -26,6 +26,7 @@
|
|||
},
|
||||
"Company.WebApplication1": {
|
||||
"commandName": "Project",
|
||||
"dotnetRunMessages": "true",
|
||||
"launchBrowser": true,
|
||||
//#if(NoHttps)
|
||||
"applicationUrl": "http://localhost:5000",
|
||||
|
|
|
|||
|
|
@ -21,6 +21,7 @@
|
|||
},
|
||||
"Company.WebApplication1": {
|
||||
"commandName": "Project",
|
||||
"dotnetRunMessages": "true",
|
||||
"launchBrowser": true,
|
||||
//#if(NoHttps)
|
||||
"applicationUrl": "http://localhost:5000",
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@
|
|||
"profiles": {
|
||||
"GrpcService-CSharp": {
|
||||
"commandName": "Project",
|
||||
"dotnetRunMessages": "true",
|
||||
"launchBrowser": false,
|
||||
"applicationUrl": "http://localhost:5000;https://localhost:5001",
|
||||
"environmentVariables": {
|
||||
|
|
|
|||
|
|
@ -31,6 +31,7 @@
|
|||
},
|
||||
"Company.WebApplication1": {
|
||||
"commandName": "Project",
|
||||
"dotnetRunMessages": "true",
|
||||
"launchBrowser": true,
|
||||
//#if(RequiresHttps)
|
||||
"applicationUrl": "https://localhost:5001;http://localhost:5000",
|
||||
|
|
|
|||
|
|
@ -31,6 +31,7 @@
|
|||
},
|
||||
"Company.WebApplication1": {
|
||||
"commandName": "Project",
|
||||
"dotnetRunMessages": "true",
|
||||
"launchBrowser": true,
|
||||
//#if(RequiresHttps)
|
||||
"applicationUrl": "https://localhost:5001;http://localhost:5000",
|
||||
|
|
|
|||
|
|
@ -1,12 +1,12 @@
|
|||
{
|
||||
"iisSettings": {
|
||||
//#if (WindowsAuth)
|
||||
"windowsAuthentication": true,
|
||||
"anonymousAuthentication": false,
|
||||
//#else
|
||||
"windowsAuthentication": false,
|
||||
"anonymousAuthentication": true,
|
||||
//#endif
|
||||
//#if (WindowsAuth)
|
||||
"windowsAuthentication": true,
|
||||
"anonymousAuthentication": false,
|
||||
//#else
|
||||
"windowsAuthentication": false,
|
||||
"anonymousAuthentication": true,
|
||||
//#endif
|
||||
"iisExpress": {
|
||||
"applicationUrl": "http://localhost:8080",
|
||||
//#if(NoHttps)
|
||||
|
|
@ -26,6 +26,7 @@
|
|||
},
|
||||
"Company.WebApplication1": {
|
||||
"commandName": "Project",
|
||||
"dotnetRunMessages": "true",
|
||||
"launchBrowser": true,
|
||||
//#if(NoHttps)
|
||||
"applicationUrl": "http://localhost:5000",
|
||||
|
|
|
|||
|
|
@ -28,6 +28,7 @@
|
|||
},
|
||||
"Company.WebApplication1": {
|
||||
"commandName": "Project",
|
||||
"dotnetRunMessages": "true",
|
||||
"launchBrowser": true,
|
||||
"launchUrl": "weatherforecast",
|
||||
//#if(RequiresHttps)
|
||||
|
|
|
|||
|
|
@ -28,6 +28,7 @@
|
|||
},
|
||||
"Company.WebApplication1": {
|
||||
"commandName": "Project",
|
||||
"dotnetRunMessages": "true",
|
||||
"launchBrowser": true,
|
||||
"launchUrl": "weatherforecast",
|
||||
//#if(NoHttps)
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@
|
|||
"profiles": {
|
||||
"Company.Application1": {
|
||||
"commandName": "Project",
|
||||
"dotnetRunMessages": "true",
|
||||
"environmentVariables": {
|
||||
"DOTNET_ENVIRONMENT": "Development"
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@
|
|||
"profiles": {
|
||||
"Company.Application1": {
|
||||
"commandName": "Project",
|
||||
"dotnetRunMessages": "true",
|
||||
"environmentVariables": {
|
||||
"DOTNET_ENVIRONMENT": "Development"
|
||||
}
|
||||
|
|
|
|||
|
|
@ -28,7 +28,7 @@ namespace Microsoft.Extensions.DependencyInjection
|
|||
/// <param name="authenticationScheme"></param>
|
||||
/// <returns>The <see cref="AuthenticationBuilder"/>.</returns>
|
||||
public static AuthenticationBuilder AddCertificate(this AuthenticationBuilder builder, string authenticationScheme)
|
||||
=> builder.AddCertificate(authenticationScheme, configureOptions: (Action<CertificateAuthenticationOptions, IServiceProvider>)null);
|
||||
=> builder.AddCertificate(authenticationScheme, configureOptions: null);
|
||||
|
||||
/// <summary>
|
||||
/// Adds certificate authentication.
|
||||
|
|
@ -39,16 +39,6 @@ namespace Microsoft.Extensions.DependencyInjection
|
|||
public static AuthenticationBuilder AddCertificate(this AuthenticationBuilder builder, Action<CertificateAuthenticationOptions> configureOptions)
|
||||
=> builder.AddCertificate(CertificateAuthenticationDefaults.AuthenticationScheme, configureOptions);
|
||||
|
||||
/// <summary>
|
||||
/// Adds certificate authentication.
|
||||
/// </summary>
|
||||
/// <typeparam name="TService">TService: A service resolved from the IServiceProvider for use when configuring this authentication provider. If you need multiple services then specify IServiceProvider and resolve them directly.</typeparam>
|
||||
/// <param name="builder">The <see cref="AuthenticationBuilder"/>.</param>
|
||||
/// <param name="configureOptions"></param>
|
||||
/// <returns>The <see cref="AuthenticationBuilder"/>.</returns>
|
||||
public static AuthenticationBuilder AddCertificate<TService>(this AuthenticationBuilder builder, Action<CertificateAuthenticationOptions, TService> configureOptions) where TService : class
|
||||
=> builder.AddCertificate(CertificateAuthenticationDefaults.AuthenticationScheme, configureOptions);
|
||||
|
||||
/// <summary>
|
||||
/// Adds certificate authentication.
|
||||
/// </summary>
|
||||
|
|
@ -60,33 +50,7 @@ namespace Microsoft.Extensions.DependencyInjection
|
|||
this AuthenticationBuilder builder,
|
||||
string authenticationScheme,
|
||||
Action<CertificateAuthenticationOptions> configureOptions)
|
||||
{
|
||||
Action<CertificateAuthenticationOptions, IServiceProvider> configureOptionsWithServices;
|
||||
if (configureOptions == null)
|
||||
{
|
||||
configureOptionsWithServices = null;
|
||||
}
|
||||
else
|
||||
{
|
||||
configureOptionsWithServices = (options, _) => configureOptions(options);
|
||||
}
|
||||
|
||||
return builder.AddCertificate(authenticationScheme, configureOptionsWithServices);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Adds certificate authentication.
|
||||
/// </summary>
|
||||
/// <typeparam name="TService">TService: A service resolved from the IServiceProvider for use when configuring this authentication provider. If you need multiple services then specify IServiceProvider and resolve them directly.</typeparam>
|
||||
/// <param name="builder">The <see cref="AuthenticationBuilder"/>.</param>
|
||||
/// <param name="authenticationScheme"></param>
|
||||
/// <param name="configureOptions"></param>
|
||||
/// <returns>The <see cref="AuthenticationBuilder"/>.</returns>
|
||||
public static AuthenticationBuilder AddCertificate<TService>(
|
||||
this AuthenticationBuilder builder,
|
||||
string authenticationScheme,
|
||||
Action<CertificateAuthenticationOptions, TService> configureOptions) where TService : class
|
||||
=> builder.AddScheme<CertificateAuthenticationOptions, CertificateAuthenticationHandler, TService>(authenticationScheme, configureOptions);
|
||||
=> builder.AddScheme<CertificateAuthenticationOptions, CertificateAuthenticationHandler>(authenticationScheme, configureOptions);
|
||||
|
||||
/// <summary>
|
||||
/// Adds certificate authentication.
|
||||
|
|
|
|||
|
|
@ -14,14 +14,15 @@ namespace CookieSessionSample
|
|||
{
|
||||
public void ConfigureServices(IServiceCollection services)
|
||||
{
|
||||
services.AddSingleton<MemoryCacheTicketStore>();
|
||||
|
||||
// This can be removed after https://github.com/aspnet/IISIntegration/issues/371
|
||||
services.AddAuthentication(options =>
|
||||
{
|
||||
options.DefaultAuthenticateScheme = CookieAuthenticationDefaults.AuthenticationScheme;
|
||||
options.DefaultChallengeScheme = CookieAuthenticationDefaults.AuthenticationScheme;
|
||||
}).AddCookie<MemoryCacheTicketStore>((o, ticketStore) => o.SessionStore = ticketStore);
|
||||
}).AddCookie();
|
||||
|
||||
services.AddOptions<CookieAuthenticationOptions>(CookieAuthenticationDefaults.AuthenticationScheme)
|
||||
.Configure<MemoryCacheTicketStore>((o, ticketStore) => o.SessionStore = ticketStore);
|
||||
}
|
||||
|
||||
public void Configure(IApplicationBuilder app)
|
||||
|
|
|
|||
|
|
@ -15,40 +15,19 @@ namespace Microsoft.Extensions.DependencyInjection
|
|||
=> builder.AddCookie(CookieAuthenticationDefaults.AuthenticationScheme);
|
||||
|
||||
public static AuthenticationBuilder AddCookie(this AuthenticationBuilder builder, string authenticationScheme)
|
||||
=> builder.AddCookie(authenticationScheme, configureOptions: (Action<CookieAuthenticationOptions, IServiceProvider>)null);
|
||||
=> builder.AddCookie(authenticationScheme, configureOptions: null);
|
||||
|
||||
public static AuthenticationBuilder AddCookie(this AuthenticationBuilder builder, Action<CookieAuthenticationOptions> configureOptions)
|
||||
=> builder.AddCookie(CookieAuthenticationDefaults.AuthenticationScheme, configureOptions);
|
||||
|
||||
public static AuthenticationBuilder AddCookie<TService>(this AuthenticationBuilder builder, Action<CookieAuthenticationOptions, TService> configureOptions) where TService : class
|
||||
public static AuthenticationBuilder AddCookie(this AuthenticationBuilder builder, Action<CookieAuthenticationOptions> configureOptions)
|
||||
=> builder.AddCookie(CookieAuthenticationDefaults.AuthenticationScheme, configureOptions);
|
||||
|
||||
public static AuthenticationBuilder AddCookie(this AuthenticationBuilder builder, string authenticationScheme, Action<CookieAuthenticationOptions> configureOptions)
|
||||
=> builder.AddCookie(authenticationScheme, displayName: null, configureOptions: configureOptions);
|
||||
|
||||
public static AuthenticationBuilder AddCookie<TService>(this AuthenticationBuilder builder, string authenticationScheme, Action<CookieAuthenticationOptions, TService> configureOptions) where TService : class
|
||||
=> builder.AddCookie(authenticationScheme, displayName: null, configureOptions: configureOptions);
|
||||
|
||||
public static AuthenticationBuilder AddCookie(this AuthenticationBuilder builder, string authenticationScheme, string displayName, Action<CookieAuthenticationOptions> configureOptions)
|
||||
{
|
||||
Action<CookieAuthenticationOptions, IServiceProvider> configureOptionsWithServices;
|
||||
if (configureOptions == null)
|
||||
{
|
||||
configureOptionsWithServices = null;
|
||||
}
|
||||
else
|
||||
{
|
||||
configureOptionsWithServices = (options, _) => configureOptions(options);
|
||||
}
|
||||
|
||||
return builder.AddCookie(authenticationScheme, displayName, configureOptionsWithServices);
|
||||
}
|
||||
|
||||
public static AuthenticationBuilder AddCookie<TService>(this AuthenticationBuilder builder, string authenticationScheme, string displayName, Action<CookieAuthenticationOptions, TService> configureOptions) where TService : class
|
||||
{
|
||||
builder.Services.TryAddEnumerable(ServiceDescriptor.Singleton<IPostConfigureOptions<CookieAuthenticationOptions>, PostConfigureCookieAuthenticationOptions>());
|
||||
builder.Services.AddOptions<CookieAuthenticationOptions>(authenticationScheme).Validate(o => o.Cookie.Expiration == null, "Cookie.Expiration is ignored, use ExpireTimeSpan instead.");
|
||||
return builder.AddScheme<CookieAuthenticationOptions, CookieAuthenticationHandler, TService>(authenticationScheme, displayName, configureOptions);
|
||||
return builder.AddScheme<CookieAuthenticationOptions, CookieAuthenticationHandler>(authenticationScheme, displayName, configureOptions);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -25,31 +25,25 @@ namespace Microsoft.AspNetCore.Authentication
|
|||
/// </summary>
|
||||
public virtual IServiceCollection Services { get; }
|
||||
|
||||
private AuthenticationBuilder AddSchemeHelper<TOptions, THandler, TService>(string authenticationScheme, string displayName, Action<TOptions, TService> configureOptions) where TService : class
|
||||
private AuthenticationBuilder AddSchemeHelper<TOptions, THandler>(string authenticationScheme, string displayName, Action<TOptions> configureOptions)
|
||||
where TOptions : AuthenticationSchemeOptions, new()
|
||||
where THandler : class, IAuthenticationHandler
|
||||
{
|
||||
Services.Configure<AuthenticationOptions>(o =>
|
||||
{
|
||||
o.AddScheme(authenticationScheme, scheme =>
|
||||
{
|
||||
o.AddScheme(authenticationScheme, scheme => {
|
||||
scheme.HandlerType = typeof(THandler);
|
||||
scheme.DisplayName = displayName;
|
||||
});
|
||||
});
|
||||
|
||||
var optionsBuilder = Services.AddOptions<TOptions>(authenticationScheme)
|
||||
.Validate(o =>
|
||||
{
|
||||
o.Validate(authenticationScheme);
|
||||
return true;
|
||||
});
|
||||
|
||||
if (configureOptions != null)
|
||||
{
|
||||
optionsBuilder.Configure(configureOptions);
|
||||
Services.Configure(authenticationScheme, configureOptions);
|
||||
}
|
||||
|
||||
Services.AddOptions<TOptions>(authenticationScheme).Validate(o => {
|
||||
o.Validate(authenticationScheme);
|
||||
return true;
|
||||
});
|
||||
Services.AddTransient<THandler>();
|
||||
return this;
|
||||
}
|
||||
|
|
@ -66,22 +60,7 @@ namespace Microsoft.AspNetCore.Authentication
|
|||
public virtual AuthenticationBuilder AddScheme<TOptions, THandler>(string authenticationScheme, string displayName, Action<TOptions> configureOptions)
|
||||
where TOptions : AuthenticationSchemeOptions, new()
|
||||
where THandler : AuthenticationHandler<TOptions>
|
||||
=> AddSchemeHelper<TOptions, THandler, IServiceProvider>(authenticationScheme, displayName, MapConfiguration(configureOptions));
|
||||
|
||||
/// <summary>
|
||||
/// Adds a <see cref="AuthenticationScheme"/> which can be used by <see cref="IAuthenticationService"/>.
|
||||
/// </summary>
|
||||
/// <typeparam name="TOptions">The <see cref="AuthenticationSchemeOptions"/> type to configure the handler."/>.</typeparam>
|
||||
/// <typeparam name="THandler">The <see cref="AuthenticationHandler{TOptions}"/> used to handle this scheme.</typeparam>
|
||||
/// <typeparam name="TService">TService: A service resolved from the IServiceProvider for use when configuring this authentication provider. If you need multiple services then specify IServiceProvider and resolve them directly.</typeparam>
|
||||
/// <param name="authenticationScheme">The name of this scheme.</param>
|
||||
/// <param name="displayName">The display name of this scheme.</param>
|
||||
/// <param name="configureOptions">Used to configure the scheme options.</param>
|
||||
/// <returns>The builder.</returns>
|
||||
public virtual AuthenticationBuilder AddScheme<TOptions, THandler, TService>(string authenticationScheme, string displayName, Action<TOptions, TService> configureOptions) where TService : class
|
||||
where TOptions : AuthenticationSchemeOptions, new()
|
||||
where THandler : AuthenticationHandler<TOptions>
|
||||
=> AddSchemeHelper<TOptions, THandler, TService>(authenticationScheme, displayName, configureOptions);
|
||||
=> AddSchemeHelper<TOptions, THandler>(authenticationScheme, displayName, configureOptions);
|
||||
|
||||
/// <summary>
|
||||
/// Adds a <see cref="AuthenticationScheme"/> which can be used by <see cref="IAuthenticationService"/>.
|
||||
|
|
@ -96,20 +75,6 @@ namespace Microsoft.AspNetCore.Authentication
|
|||
where THandler : AuthenticationHandler<TOptions>
|
||||
=> AddScheme<TOptions, THandler>(authenticationScheme, displayName: null, configureOptions: configureOptions);
|
||||
|
||||
/// <summary>
|
||||
/// Adds a <see cref="AuthenticationScheme"/> which can be used by <see cref="IAuthenticationService"/>.
|
||||
/// </summary>
|
||||
/// <typeparam name="TOptions">The <see cref="AuthenticationSchemeOptions"/> type to configure the handler."/>.</typeparam>
|
||||
/// <typeparam name="THandler">The <see cref="AuthenticationHandler{TOptions}"/> used to handle this scheme.</typeparam>
|
||||
/// <typeparam name="TService">TService: A service resolved from the IServiceProvider for use when configuring this authentication provider. If you need multiple services then specify IServiceProvider and resolve them directly.</typeparam>
|
||||
/// <param name="authenticationScheme">The name of this scheme.</param>
|
||||
/// <param name="configureOptions">Used to configure the scheme options.</param>
|
||||
/// <returns>The builder.</returns>
|
||||
public virtual AuthenticationBuilder AddScheme<TOptions, THandler, TService>(string authenticationScheme, Action<TOptions, TService> configureOptions) where TService : class
|
||||
where TOptions : AuthenticationSchemeOptions, new()
|
||||
where THandler : AuthenticationHandler<TOptions>
|
||||
=> AddScheme<TOptions, THandler, TService>(authenticationScheme, displayName: null, configureOptions: configureOptions);
|
||||
|
||||
/// <summary>
|
||||
/// Adds a <see cref="RemoteAuthenticationHandler{TOptions}"/> based <see cref="AuthenticationScheme"/> that supports remote authentication
|
||||
/// which can be used by <see cref="IAuthenticationService"/>.
|
||||
|
|
@ -128,25 +93,6 @@ namespace Microsoft.AspNetCore.Authentication
|
|||
return AddScheme<TOptions, THandler>(authenticationScheme, displayName, configureOptions: configureOptions);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Adds a <see cref="RemoteAuthenticationHandler{TOptions}"/> based <see cref="AuthenticationScheme"/> that supports remote authentication
|
||||
/// which can be used by <see cref="IAuthenticationService"/>.
|
||||
/// </summary>
|
||||
/// <typeparam name="TOptions">The <see cref="RemoteAuthenticationOptions"/> type to configure the handler."/>.</typeparam>
|
||||
/// <typeparam name="THandler">The <see cref="RemoteAuthenticationHandler{TOptions}"/> used to handle this scheme.</typeparam>
|
||||
/// <typeparam name="TService">TService: A service resolved from the IServiceProvider for use when configuring this authentication provider. If you need multiple services then specify IServiceProvider and resolve them directly.</typeparam>
|
||||
/// <param name="authenticationScheme">The name of this scheme.</param>
|
||||
/// <param name="displayName">The display name of this scheme.</param>
|
||||
/// <param name="configureOptions">Used to configure the scheme options.</param>
|
||||
/// <returns>The builder.</returns>
|
||||
public virtual AuthenticationBuilder AddRemoteScheme<TOptions, THandler, TService>(string authenticationScheme, string displayName, Action<TOptions, TService> configureOptions) where TService : class
|
||||
where TOptions : RemoteAuthenticationOptions, new()
|
||||
where THandler : RemoteAuthenticationHandler<TOptions>
|
||||
{
|
||||
Services.TryAddEnumerable(ServiceDescriptor.Singleton<IPostConfigureOptions<TOptions>, EnsureSignInScheme<TOptions>>());
|
||||
return AddScheme<TOptions, THandler, TService>(authenticationScheme, displayName, configureOptions: configureOptions);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Adds a <see cref="PolicySchemeHandler"/> based authentication handler which can be used to
|
||||
/// redirect to other authentication schemes.
|
||||
|
|
@ -156,30 +102,7 @@ namespace Microsoft.AspNetCore.Authentication
|
|||
/// <param name="configureOptions">Used to configure the scheme options.</param>
|
||||
/// <returns>The builder.</returns>
|
||||
public virtual AuthenticationBuilder AddPolicyScheme(string authenticationScheme, string displayName, Action<PolicySchemeOptions> configureOptions)
|
||||
=> AddSchemeHelper<PolicySchemeOptions, PolicySchemeHandler, IServiceProvider>(authenticationScheme, displayName, MapConfiguration(configureOptions));
|
||||
|
||||
/// <summary>
|
||||
/// Adds a <see cref="PolicySchemeHandler"/> based authentication handler which can be used to
|
||||
/// redirect to other authentication schemes.
|
||||
/// </summary>
|
||||
/// <param name="authenticationScheme">The name of this scheme.</param>
|
||||
/// <param name="displayName">The display name of this scheme.</param>
|
||||
/// <param name="configureOptions">Used to configure the scheme options.</param>
|
||||
/// <returns>The builder.</returns>
|
||||
public virtual AuthenticationBuilder AddPolicyScheme<TService>(string authenticationScheme, string displayName, Action<PolicySchemeOptions, TService> configureOptions) where TService : class
|
||||
=> AddSchemeHelper<PolicySchemeOptions, PolicySchemeHandler, TService>(authenticationScheme, displayName, configureOptions);
|
||||
|
||||
private Action<TOptions, IServiceProvider> MapConfiguration<TOptions>(Action<TOptions> configureOptions)
|
||||
{
|
||||
if (configureOptions == null)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
else
|
||||
{
|
||||
return (options, _) => configureOptions(options);
|
||||
}
|
||||
}
|
||||
=> AddSchemeHelper<PolicySchemeOptions, PolicySchemeHandler>(authenticationScheme, displayName, configureOptions);
|
||||
|
||||
// Used to ensure that there's always a default sign in scheme that's not itself
|
||||
private class EnsureSignInScheme<TOptions> : IPostConfigureOptions<TOptions> where TOptions : RemoteAuthenticationOptions
|
||||
|
|
|
|||
|
|
@ -15,31 +15,10 @@ namespace Microsoft.Extensions.DependencyInjection
|
|||
public static AuthenticationBuilder AddFacebook(this AuthenticationBuilder builder, Action<FacebookOptions> configureOptions)
|
||||
=> builder.AddFacebook(FacebookDefaults.AuthenticationScheme, configureOptions);
|
||||
|
||||
public static AuthenticationBuilder AddFacebook<TService>(this AuthenticationBuilder builder, Action<FacebookOptions, TService> configureOptions) where TService : class
|
||||
=> builder.AddFacebook(FacebookDefaults.AuthenticationScheme, configureOptions);
|
||||
|
||||
public static AuthenticationBuilder AddFacebook(this AuthenticationBuilder builder, string authenticationScheme, Action<FacebookOptions> configureOptions)
|
||||
=> builder.AddFacebook(authenticationScheme, FacebookDefaults.DisplayName, configureOptions);
|
||||
|
||||
public static AuthenticationBuilder AddFacebook<TService>(this AuthenticationBuilder builder, string authenticationScheme, Action<FacebookOptions, TService> configureOptions) where TService : class
|
||||
=> builder.AddFacebook(authenticationScheme, FacebookDefaults.DisplayName, configureOptions);
|
||||
|
||||
public static AuthenticationBuilder AddFacebook(this AuthenticationBuilder builder, string authenticationScheme, string displayName, Action<FacebookOptions> configureOptions)
|
||||
{
|
||||
Action<FacebookOptions, IServiceProvider> configureOptionsWithServices;
|
||||
if (configureOptions == null)
|
||||
{
|
||||
configureOptionsWithServices = null;
|
||||
}
|
||||
else
|
||||
{
|
||||
configureOptionsWithServices = (options, _) => configureOptions(options);
|
||||
}
|
||||
|
||||
return builder.AddFacebook(authenticationScheme, displayName, configureOptionsWithServices);
|
||||
}
|
||||
|
||||
public static AuthenticationBuilder AddFacebook<TService>(this AuthenticationBuilder builder, string authenticationScheme, string displayName, Action<FacebookOptions, TService> configureOptions) where TService : class
|
||||
=> builder.AddOAuth<FacebookOptions, FacebookHandler, TService>(authenticationScheme, displayName, configureOptions);
|
||||
=> builder.AddOAuth<FacebookOptions, FacebookHandler>(authenticationScheme, displayName, configureOptions);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -15,31 +15,10 @@ namespace Microsoft.Extensions.DependencyInjection
|
|||
public static AuthenticationBuilder AddGoogle(this AuthenticationBuilder builder, Action<GoogleOptions> configureOptions)
|
||||
=> builder.AddGoogle(GoogleDefaults.AuthenticationScheme, configureOptions);
|
||||
|
||||
public static AuthenticationBuilder AddGoogle<TService>(this AuthenticationBuilder builder, Action<GoogleOptions, TService> configureOptions) where TService : class
|
||||
=> builder.AddGoogle(GoogleDefaults.AuthenticationScheme, configureOptions);
|
||||
|
||||
public static AuthenticationBuilder AddGoogle(this AuthenticationBuilder builder, string authenticationScheme, Action<GoogleOptions> configureOptions)
|
||||
=> builder.AddGoogle(authenticationScheme, GoogleDefaults.DisplayName, configureOptions);
|
||||
|
||||
public static AuthenticationBuilder AddGoogle<TService>(this AuthenticationBuilder builder, string authenticationScheme, Action<GoogleOptions, TService> configureOptions) where TService : class
|
||||
=> builder.AddGoogle(authenticationScheme, GoogleDefaults.DisplayName, configureOptions);
|
||||
|
||||
public static AuthenticationBuilder AddGoogle(this AuthenticationBuilder builder, string authenticationScheme, string displayName, Action<GoogleOptions> configureOptions)
|
||||
{
|
||||
Action<GoogleOptions, IServiceProvider> configureOptionsWithServices;
|
||||
if (configureOptions == null)
|
||||
{
|
||||
configureOptionsWithServices = null;
|
||||
}
|
||||
else
|
||||
{
|
||||
configureOptionsWithServices = (options, _) => configureOptions(options);
|
||||
}
|
||||
|
||||
return builder.AddGoogle(authenticationScheme, displayName, configureOptionsWithServices);
|
||||
}
|
||||
|
||||
public static AuthenticationBuilder AddGoogle<TService>(this AuthenticationBuilder builder, string authenticationScheme, string displayName, Action<GoogleOptions, TService> configureOptions) where TService : class
|
||||
=> builder.AddOAuth<GoogleOptions, GoogleHandler, TService>(authenticationScheme, displayName, configureOptions);
|
||||
=> builder.AddOAuth<GoogleOptions, GoogleHandler>(authenticationScheme, displayName, configureOptions);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -17,34 +17,13 @@ namespace Microsoft.Extensions.DependencyInjection
|
|||
public static AuthenticationBuilder AddJwtBearer(this AuthenticationBuilder builder, Action<JwtBearerOptions> configureOptions)
|
||||
=> builder.AddJwtBearer(JwtBearerDefaults.AuthenticationScheme, configureOptions);
|
||||
|
||||
public static AuthenticationBuilder AddJwtBearer<TService>(this AuthenticationBuilder builder, Action<JwtBearerOptions, TService> configureOptions) where TService : class
|
||||
=> builder.AddJwtBearer(JwtBearerDefaults.AuthenticationScheme, configureOptions);
|
||||
|
||||
public static AuthenticationBuilder AddJwtBearer(this AuthenticationBuilder builder, string authenticationScheme, Action<JwtBearerOptions> configureOptions)
|
||||
=> builder.AddJwtBearer(authenticationScheme, displayName: null, configureOptions: configureOptions);
|
||||
|
||||
public static AuthenticationBuilder AddJwtBearer<TService>(this AuthenticationBuilder builder, string authenticationScheme, Action<JwtBearerOptions, TService> configureOptions) where TService : class
|
||||
=> builder.AddJwtBearer(authenticationScheme, displayName: null, configureOptions: configureOptions);
|
||||
|
||||
public static AuthenticationBuilder AddJwtBearer(this AuthenticationBuilder builder, string authenticationScheme, string displayName, Action<JwtBearerOptions> configureOptions)
|
||||
{
|
||||
Action<JwtBearerOptions, IServiceProvider> configureOptionsWithServices;
|
||||
if (configureOptions == null)
|
||||
{
|
||||
configureOptionsWithServices = null;
|
||||
}
|
||||
else
|
||||
{
|
||||
configureOptionsWithServices = (options, _) => configureOptions(options);
|
||||
}
|
||||
|
||||
return builder.AddJwtBearer(authenticationScheme, displayName, configureOptionsWithServices);
|
||||
}
|
||||
|
||||
public static AuthenticationBuilder AddJwtBearer<TService>(this AuthenticationBuilder builder, string authenticationScheme, string displayName, Action<JwtBearerOptions, TService> configureOptions) where TService : class
|
||||
{
|
||||
builder.Services.TryAddEnumerable(ServiceDescriptor.Singleton<IPostConfigureOptions<JwtBearerOptions>, JwtBearerPostConfigureOptions>());
|
||||
return builder.AddScheme<JwtBearerOptions, JwtBearerHandler, TService>(authenticationScheme, displayName, configureOptions);
|
||||
return builder.AddScheme<JwtBearerOptions, JwtBearerHandler>(authenticationScheme, displayName, configureOptions);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -15,31 +15,10 @@ namespace Microsoft.Extensions.DependencyInjection
|
|||
public static AuthenticationBuilder AddMicrosoftAccount(this AuthenticationBuilder builder, Action<MicrosoftAccountOptions> configureOptions)
|
||||
=> builder.AddMicrosoftAccount(MicrosoftAccountDefaults.AuthenticationScheme, configureOptions);
|
||||
|
||||
public static AuthenticationBuilder AddMicrosoftAccount<TService>(this AuthenticationBuilder builder, Action<MicrosoftAccountOptions, TService> configureOptions) where TService : class
|
||||
=> builder.AddMicrosoftAccount(MicrosoftAccountDefaults.AuthenticationScheme, configureOptions);
|
||||
|
||||
public static AuthenticationBuilder AddMicrosoftAccount(this AuthenticationBuilder builder, string authenticationScheme, Action<MicrosoftAccountOptions> configureOptions)
|
||||
=> builder.AddMicrosoftAccount(authenticationScheme, MicrosoftAccountDefaults.DisplayName, configureOptions);
|
||||
|
||||
public static AuthenticationBuilder AddMicrosoftAccount<TService>(this AuthenticationBuilder builder, string authenticationScheme, Action<MicrosoftAccountOptions, TService> configureOptions) where TService : class
|
||||
=> builder.AddMicrosoftAccount(authenticationScheme, MicrosoftAccountDefaults.DisplayName, configureOptions);
|
||||
|
||||
public static AuthenticationBuilder AddMicrosoftAccount(this AuthenticationBuilder builder, string authenticationScheme, string displayName, Action<MicrosoftAccountOptions> configureOptions)
|
||||
{
|
||||
Action<MicrosoftAccountOptions, IServiceProvider> configureOptionsWithServices;
|
||||
if (configureOptions == null)
|
||||
{
|
||||
configureOptionsWithServices = null;
|
||||
}
|
||||
else
|
||||
{
|
||||
configureOptionsWithServices = (options, _) => configureOptions(options);
|
||||
}
|
||||
|
||||
return builder.AddMicrosoftAccount(authenticationScheme, displayName, configureOptionsWithServices);
|
||||
}
|
||||
|
||||
public static AuthenticationBuilder AddMicrosoftAccount<TService>(this AuthenticationBuilder builder, string authenticationScheme, string displayName, Action<MicrosoftAccountOptions, TService> configureOptions) where TService : class
|
||||
=> builder.AddOAuth<MicrosoftAccountOptions, MicrosoftAccountHandler, TService>(authenticationScheme, displayName, configureOptions);
|
||||
=> builder.AddOAuth<MicrosoftAccountOptions, MicrosoftAccountHandler>(authenticationScheme, displayName, configureOptions);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -31,16 +31,6 @@ namespace Microsoft.Extensions.DependencyInjection
|
|||
public static AuthenticationBuilder AddNegotiate(this AuthenticationBuilder builder, Action<NegotiateOptions> configureOptions)
|
||||
=> builder.AddNegotiate(NegotiateDefaults.AuthenticationScheme, configureOptions);
|
||||
|
||||
/// <summary>
|
||||
/// Adds and configures Negotiate authentication.
|
||||
/// </summary>
|
||||
/// <typeparam name="TService">TService: A service resolved from the IServiceProvider for use when configuring this authentication provider. If you need multiple services then specify IServiceProvider and resolve them directly.</typeparam>
|
||||
/// <param name="builder">The <see cref="AuthenticationBuilder"/>.</param>
|
||||
/// <param name="configureOptions">Allows for configuring the authentication handler.</param>
|
||||
/// <returns>The original builder.</returns>
|
||||
public static AuthenticationBuilder AddNegotiate<TService>(this AuthenticationBuilder builder, Action<NegotiateOptions, TService> configureOptions) where TService : class
|
||||
=> builder.AddNegotiate(NegotiateDefaults.AuthenticationScheme, configureOptions);
|
||||
|
||||
/// <summary>
|
||||
/// Adds and configures Negotiate authentication.
|
||||
/// </summary>
|
||||
|
|
@ -51,17 +41,6 @@ namespace Microsoft.Extensions.DependencyInjection
|
|||
public static AuthenticationBuilder AddNegotiate(this AuthenticationBuilder builder, string authenticationScheme, Action<NegotiateOptions> configureOptions)
|
||||
=> builder.AddNegotiate(authenticationScheme, displayName: null, configureOptions: configureOptions);
|
||||
|
||||
/// <summary>
|
||||
/// Adds and configures Negotiate authentication.
|
||||
/// </summary>
|
||||
/// <typeparam name="TService">TService: A service resolved from the IServiceProvider for use when configuring this authentication provider. If you need multiple services then specify IServiceProvider and resolve them directly.</typeparam>
|
||||
/// <param name="builder">The <see cref="AuthenticationBuilder"/>.</param>
|
||||
/// <param name="authenticationScheme">The scheme name used to identify the authentication handler internally.</param>
|
||||
/// <param name="configureOptions">Allows for configuring the authentication handler.</param>
|
||||
/// <returns>The original builder.</returns>
|
||||
public static AuthenticationBuilder AddNegotiate<TService>(this AuthenticationBuilder builder, string authenticationScheme, Action<NegotiateOptions, TService> configureOptions) where TService : class
|
||||
=> builder.AddNegotiate(authenticationScheme, displayName: null, configureOptions: configureOptions);
|
||||
|
||||
/// <summary>
|
||||
/// Adds and configures Negotiate authentication.
|
||||
/// </summary>
|
||||
|
|
@ -71,33 +50,9 @@ namespace Microsoft.Extensions.DependencyInjection
|
|||
/// <param name="configureOptions">Allows for configuring the authentication handler.</param>
|
||||
/// <returns>The original builder.</returns>
|
||||
public static AuthenticationBuilder AddNegotiate(this AuthenticationBuilder builder, string authenticationScheme, string displayName, Action<NegotiateOptions> configureOptions)
|
||||
{
|
||||
Action<NegotiateOptions, IServiceProvider> configureOptionsWithServices;
|
||||
if (configureOptions == null)
|
||||
{
|
||||
configureOptionsWithServices = null;
|
||||
}
|
||||
else
|
||||
{
|
||||
configureOptionsWithServices = (options, _) => configureOptions(options);
|
||||
}
|
||||
|
||||
return builder.AddNegotiate(authenticationScheme, displayName, configureOptionsWithServices);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Adds and configures Negotiate authentication.
|
||||
/// </summary>
|
||||
/// <typeparam name="TService">TService: A service resolved from the IServiceProvider for use when configuring this authentication provider. If you need multiple services then specify IServiceProvider and resolve them directly.</typeparam>
|
||||
/// <param name="builder">The <see cref="AuthenticationBuilder"/>.</param>
|
||||
/// <param name="authenticationScheme">The scheme name used to identify the authentication handler internally.</param>
|
||||
/// <param name="displayName">The name displayed to users when selecting an authentication handler. The default is null to prevent this from displaying.</param>
|
||||
/// <param name="configureOptions">Allows for configuring the authentication handler.</param>
|
||||
/// <returns>The original builder.</returns>
|
||||
public static AuthenticationBuilder AddNegotiate<TService>(this AuthenticationBuilder builder, string authenticationScheme, string displayName, Action<NegotiateOptions, TService> configureOptions) where TService : class
|
||||
{
|
||||
builder.Services.TryAddEnumerable(ServiceDescriptor.Singleton<IPostConfigureOptions<NegotiateOptions>, PostConfigureNegotiateOptions>());
|
||||
return builder.AddScheme<NegotiateOptions, NegotiateHandler, TService>(authenticationScheme, displayName, configureOptions);
|
||||
return builder.AddScheme<NegotiateOptions, NegotiateHandler>(authenticationScheme, displayName, configureOptions);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -14,50 +14,20 @@ namespace Microsoft.Extensions.DependencyInjection
|
|||
public static AuthenticationBuilder AddOAuth(this AuthenticationBuilder builder, string authenticationScheme, Action<OAuthOptions> configureOptions)
|
||||
=> builder.AddOAuth<OAuthOptions, OAuthHandler<OAuthOptions>>(authenticationScheme, configureOptions);
|
||||
|
||||
public static AuthenticationBuilder AddOAuth<TService>(this AuthenticationBuilder builder, string authenticationScheme, Action<OAuthOptions, TService> configureOptions) where TService : class
|
||||
=> builder.AddOAuth<OAuthOptions, OAuthHandler<OAuthOptions>, TService>(authenticationScheme, configureOptions);
|
||||
|
||||
public static AuthenticationBuilder AddOAuth(this AuthenticationBuilder builder, string authenticationScheme, string displayName, Action<OAuthOptions> configureOptions)
|
||||
=> builder.AddOAuth<OAuthOptions, OAuthHandler<OAuthOptions>>(authenticationScheme, displayName, configureOptions);
|
||||
|
||||
public static AuthenticationBuilder AddOAuth<TService>(this AuthenticationBuilder builder, string authenticationScheme, string displayName, Action<OAuthOptions, TService> configureOptions) where TService : class
|
||||
=> builder.AddOAuth<OAuthOptions, OAuthHandler<OAuthOptions>, TService>(authenticationScheme, displayName, configureOptions);
|
||||
|
||||
public static AuthenticationBuilder AddOAuth<TOptions, THandler>(this AuthenticationBuilder builder, string authenticationScheme, Action<TOptions> configureOptions)
|
||||
where TOptions : OAuthOptions, new()
|
||||
where THandler : OAuthHandler<TOptions>
|
||||
=> builder.AddOAuth<TOptions, THandler>(authenticationScheme, OAuthDefaults.DisplayName, configureOptions);
|
||||
|
||||
public static AuthenticationBuilder AddOAuth<TOptions, THandler, TService>(this AuthenticationBuilder builder, string authenticationScheme, Action<TOptions, TService> configureOptions)
|
||||
where TOptions : OAuthOptions, new()
|
||||
where THandler : OAuthHandler<TOptions>
|
||||
where TService : class
|
||||
=> builder.AddOAuth<TOptions, THandler, TService>(authenticationScheme, OAuthDefaults.DisplayName, configureOptions);
|
||||
|
||||
public static AuthenticationBuilder AddOAuth<TOptions, THandler>(this AuthenticationBuilder builder, string authenticationScheme, string displayName, Action<TOptions> configureOptions)
|
||||
where TOptions : OAuthOptions, new()
|
||||
where THandler : OAuthHandler<TOptions>
|
||||
{
|
||||
Action<TOptions, IServiceProvider> configureOptionsWithServices;
|
||||
if (configureOptions == null)
|
||||
{
|
||||
configureOptionsWithServices = null;
|
||||
}
|
||||
else
|
||||
{
|
||||
configureOptionsWithServices = (options, _) => configureOptions(options);
|
||||
}
|
||||
|
||||
return builder.AddOAuth<TOptions, THandler, IServiceProvider>(authenticationScheme, displayName, configureOptionsWithServices);
|
||||
}
|
||||
|
||||
public static AuthenticationBuilder AddOAuth<TOptions, THandler, TService>(this AuthenticationBuilder builder, string authenticationScheme, string displayName, Action<TOptions, TService> configureOptions)
|
||||
where TOptions : OAuthOptions, new()
|
||||
where THandler : OAuthHandler<TOptions>
|
||||
where TService : class
|
||||
{
|
||||
builder.Services.TryAddEnumerable(ServiceDescriptor.Singleton<IPostConfigureOptions<TOptions>, OAuthPostConfigureOptions<TOptions, THandler>>());
|
||||
return builder.AddRemoteScheme<TOptions, THandler, TService>(authenticationScheme, displayName, configureOptions);
|
||||
return builder.AddRemoteScheme<TOptions, THandler>(authenticationScheme, displayName, configureOptions);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -17,34 +17,13 @@ namespace Microsoft.Extensions.DependencyInjection
|
|||
public static AuthenticationBuilder AddOpenIdConnect(this AuthenticationBuilder builder, Action<OpenIdConnectOptions> configureOptions)
|
||||
=> builder.AddOpenIdConnect(OpenIdConnectDefaults.AuthenticationScheme, configureOptions);
|
||||
|
||||
public static AuthenticationBuilder AddOpenIdConnect<TService>(this AuthenticationBuilder builder, Action<OpenIdConnectOptions, TService> configureOptions) where TService : class
|
||||
=> builder.AddOpenIdConnect(OpenIdConnectDefaults.AuthenticationScheme, configureOptions);
|
||||
|
||||
public static AuthenticationBuilder AddOpenIdConnect(this AuthenticationBuilder builder, string authenticationScheme, Action<OpenIdConnectOptions> configureOptions)
|
||||
=> builder.AddOpenIdConnect(authenticationScheme, OpenIdConnectDefaults.DisplayName, configureOptions);
|
||||
|
||||
public static AuthenticationBuilder AddOpenIdConnect<TService>(this AuthenticationBuilder builder, string authenticationScheme, Action<OpenIdConnectOptions, TService> configureOptions) where TService : class
|
||||
=> builder.AddOpenIdConnect(authenticationScheme, OpenIdConnectDefaults.DisplayName, configureOptions);
|
||||
|
||||
public static AuthenticationBuilder AddOpenIdConnect(this AuthenticationBuilder builder, string authenticationScheme, string displayName, Action<OpenIdConnectOptions> configureOptions)
|
||||
{
|
||||
Action<OpenIdConnectOptions, IServiceProvider> configureOptionsWithServices;
|
||||
if (configureOptions == null)
|
||||
{
|
||||
configureOptionsWithServices = null;
|
||||
}
|
||||
else
|
||||
{
|
||||
configureOptionsWithServices = (options, _) => configureOptions(options);
|
||||
}
|
||||
|
||||
return builder.AddOpenIdConnect(authenticationScheme, displayName, configureOptionsWithServices);
|
||||
}
|
||||
|
||||
public static AuthenticationBuilder AddOpenIdConnect<TService>(this AuthenticationBuilder builder, string authenticationScheme, string displayName, Action<OpenIdConnectOptions, TService> configureOptions) where TService : class
|
||||
{
|
||||
builder.Services.TryAddEnumerable(ServiceDescriptor.Singleton<IPostConfigureOptions<OpenIdConnectOptions>, OpenIdConnectPostConfigureOptions>());
|
||||
return builder.AddRemoteScheme<OpenIdConnectOptions, OpenIdConnectHandler, TService>(authenticationScheme, displayName, configureOptions);
|
||||
return builder.AddRemoteScheme<OpenIdConnectOptions, OpenIdConnectHandler>(authenticationScheme, displayName, configureOptions);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -17,34 +17,13 @@ namespace Microsoft.Extensions.DependencyInjection
|
|||
public static AuthenticationBuilder AddTwitter(this AuthenticationBuilder builder, Action<TwitterOptions> configureOptions)
|
||||
=> builder.AddTwitter(TwitterDefaults.AuthenticationScheme, configureOptions);
|
||||
|
||||
public static AuthenticationBuilder AddTwitter<TService>(this AuthenticationBuilder builder, Action<TwitterOptions, TService> configureOptions) where TService : class
|
||||
=> builder.AddTwitter(TwitterDefaults.AuthenticationScheme, configureOptions);
|
||||
|
||||
public static AuthenticationBuilder AddTwitter(this AuthenticationBuilder builder, string authenticationScheme, Action<TwitterOptions> configureOptions)
|
||||
=> builder.AddTwitter(authenticationScheme, TwitterDefaults.DisplayName, configureOptions);
|
||||
|
||||
public static AuthenticationBuilder AddTwitter<TService>(this AuthenticationBuilder builder, string authenticationScheme, Action<TwitterOptions, TService> configureOptions) where TService : class
|
||||
=> builder.AddTwitter(authenticationScheme, TwitterDefaults.DisplayName, configureOptions);
|
||||
|
||||
public static AuthenticationBuilder AddTwitter(this AuthenticationBuilder builder, string authenticationScheme, string displayName, Action<TwitterOptions> configureOptions)
|
||||
{
|
||||
Action<TwitterOptions, IServiceProvider> configureOptionsWithServices;
|
||||
if (configureOptions == null)
|
||||
{
|
||||
configureOptionsWithServices = null;
|
||||
}
|
||||
else
|
||||
{
|
||||
configureOptionsWithServices = (options, _) => configureOptions(options);
|
||||
}
|
||||
|
||||
return builder.AddTwitter(authenticationScheme, displayName, configureOptionsWithServices);
|
||||
}
|
||||
|
||||
public static AuthenticationBuilder AddTwitter<TService>(this AuthenticationBuilder builder, string authenticationScheme, string displayName, Action<TwitterOptions, TService> configureOptions) where TService : class
|
||||
{
|
||||
builder.Services.TryAddEnumerable(ServiceDescriptor.Singleton<IPostConfigureOptions<TwitterOptions>, TwitterPostConfigureOptions>());
|
||||
return builder.AddRemoteScheme<TwitterOptions, TwitterHandler, TService>(authenticationScheme, displayName, configureOptions);
|
||||
return builder.AddRemoteScheme<TwitterOptions, TwitterHandler>(authenticationScheme, displayName, configureOptions);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -31,16 +31,6 @@ namespace Microsoft.Extensions.DependencyInjection
|
|||
public static AuthenticationBuilder AddWsFederation(this AuthenticationBuilder builder, Action<WsFederationOptions> configureOptions)
|
||||
=> builder.AddWsFederation(WsFederationDefaults.AuthenticationScheme, configureOptions);
|
||||
|
||||
/// <summary>
|
||||
/// Registers the <see cref="WsFederationHandler"/> using the default authentication scheme, display name, and the given options configuration.
|
||||
/// </summary>
|
||||
/// <typeparam name="TService">TService: A service resolved from the IServiceProvider for use when configuring this authentication provider. If you need multiple services then specify IServiceProvider and resolve them directly.</typeparam>
|
||||
/// <param name="builder"></param>
|
||||
/// <param name="configureOptions">A delegate that configures the <see cref="WsFederationOptions"/>.</param>
|
||||
/// <returns></returns>
|
||||
public static AuthenticationBuilder AddWsFederation<TService>(this AuthenticationBuilder builder, Action<WsFederationOptions, TService> configureOptions) where TService : class
|
||||
=> builder.AddWsFederation(WsFederationDefaults.AuthenticationScheme, configureOptions);
|
||||
|
||||
/// <summary>
|
||||
/// Registers the <see cref="WsFederationHandler"/> using the given authentication scheme, default display name, and the given options configuration.
|
||||
/// </summary>
|
||||
|
|
@ -51,17 +41,6 @@ namespace Microsoft.Extensions.DependencyInjection
|
|||
public static AuthenticationBuilder AddWsFederation(this AuthenticationBuilder builder, string authenticationScheme, Action<WsFederationOptions> configureOptions)
|
||||
=> builder.AddWsFederation(authenticationScheme, WsFederationDefaults.DisplayName, configureOptions);
|
||||
|
||||
/// <summary>
|
||||
/// Registers the <see cref="WsFederationHandler"/> using the given authentication scheme, default display name, and the given options configuration.
|
||||
/// </summary>
|
||||
/// <typeparam name="TService">TService: A service resolved from the IServiceProvider for use when configuring this authentication provider. If you need multiple services then specify IServiceProvider and resolve them directly.</typeparam>
|
||||
/// <param name="builder"></param>
|
||||
/// <param name="authenticationScheme"></param>
|
||||
/// <param name="configureOptions">A delegate that configures the <see cref="WsFederationOptions"/>.</param>
|
||||
/// <returns></returns>
|
||||
public static AuthenticationBuilder AddWsFederation<TService>(this AuthenticationBuilder builder, string authenticationScheme, Action<WsFederationOptions, TService> configureOptions) where TService : class
|
||||
=> builder.AddWsFederation(authenticationScheme, WsFederationDefaults.DisplayName, configureOptions);
|
||||
|
||||
/// <summary>
|
||||
/// Registers the <see cref="WsFederationHandler"/> using the given authentication scheme, display name, and options configuration.
|
||||
/// </summary>
|
||||
|
|
@ -71,33 +50,9 @@ namespace Microsoft.Extensions.DependencyInjection
|
|||
/// <param name="configureOptions">A delegate that configures the <see cref="WsFederationOptions"/>.</param>
|
||||
/// <returns></returns>
|
||||
public static AuthenticationBuilder AddWsFederation(this AuthenticationBuilder builder, string authenticationScheme, string displayName, Action<WsFederationOptions> configureOptions)
|
||||
{
|
||||
Action<WsFederationOptions, IServiceProvider> configureOptionsWithServices;
|
||||
if (configureOptions == null)
|
||||
{
|
||||
configureOptionsWithServices = null;
|
||||
}
|
||||
else
|
||||
{
|
||||
configureOptionsWithServices = (options, _) => configureOptions(options);
|
||||
}
|
||||
|
||||
return builder.AddWsFederation(authenticationScheme, displayName, configureOptionsWithServices);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Registers the <see cref="WsFederationHandler"/> using the given authentication scheme, display name, and options configuration.
|
||||
/// </summary>
|
||||
/// <typeparam name="TService">TService: A service resolved from the IServiceProvider for use when configuring this authentication provider. If you need multiple services then specify IServiceProvider and resolve them directly.</typeparam>
|
||||
/// <param name="builder"></param>
|
||||
/// <param name="authenticationScheme"></param>
|
||||
/// <param name="displayName"></param>
|
||||
/// <param name="configureOptions">A delegate that configures the <see cref="WsFederationOptions"/>.</param>
|
||||
/// <returns></returns>
|
||||
public static AuthenticationBuilder AddWsFederation<TService>(this AuthenticationBuilder builder, string authenticationScheme, string displayName, Action<WsFederationOptions, TService> configureOptions) where TService : class
|
||||
{
|
||||
builder.Services.TryAddEnumerable(ServiceDescriptor.Singleton<IPostConfigureOptions<WsFederationOptions>, WsFederationPostConfigureOptions>());
|
||||
return builder.AddRemoteScheme<WsFederationOptions, WsFederationHandler, TService>(authenticationScheme, displayName, configureOptions);
|
||||
return builder.AddRemoteScheme<WsFederationOptions, WsFederationHandler>(authenticationScheme, displayName, configureOptions);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,108 @@
|
|||
// 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.Linq;
|
||||
using System.Net.WebSockets;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.AspNetCore.Builder;
|
||||
using Microsoft.AspNetCore.Hosting;
|
||||
using Microsoft.AspNetCore.Hosting.Server;
|
||||
using Microsoft.AspNetCore.Hosting.Server.Features;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.Extensions.Hosting;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Microsoft.Extensions.Tools.Internal;
|
||||
|
||||
namespace Microsoft.DotNet.Watcher.Tools
|
||||
{
|
||||
public class BrowserRefreshServer : IAsyncDisposable
|
||||
{
|
||||
private readonly IReporter _reporter;
|
||||
private readonly TaskCompletionSource _taskCompletionSource;
|
||||
private IHost _refreshServer;
|
||||
private WebSocket _webSocket;
|
||||
|
||||
public BrowserRefreshServer(IReporter reporter)
|
||||
{
|
||||
_reporter = reporter;
|
||||
_taskCompletionSource = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously);
|
||||
}
|
||||
|
||||
public async ValueTask<string> StartAsync(CancellationToken cancellationToken)
|
||||
{
|
||||
_refreshServer = new HostBuilder()
|
||||
.ConfigureWebHost(builder =>
|
||||
{
|
||||
builder.UseKestrel();
|
||||
builder.UseUrls("http://127.0.0.1:0");
|
||||
|
||||
builder.Configure(app =>
|
||||
{
|
||||
app.UseWebSockets();
|
||||
app.Run(WebSocketRequest);
|
||||
});
|
||||
})
|
||||
.Build();
|
||||
|
||||
await _refreshServer.StartAsync(cancellationToken);
|
||||
|
||||
var serverUrl = _refreshServer.Services
|
||||
.GetRequiredService<IServer>()
|
||||
.Features
|
||||
.Get<IServerAddressesFeature>()
|
||||
.Addresses
|
||||
.First();
|
||||
|
||||
return serverUrl.Replace("http://", "ws://");
|
||||
}
|
||||
|
||||
private async Task WebSocketRequest(HttpContext context)
|
||||
{
|
||||
if (!context.WebSockets.IsWebSocketRequest)
|
||||
{
|
||||
context.Response.StatusCode = 400;
|
||||
return;
|
||||
}
|
||||
|
||||
_webSocket = await context.WebSockets.AcceptWebSocketAsync();
|
||||
await _taskCompletionSource.Task;
|
||||
}
|
||||
|
||||
public async Task SendMessage(byte[] messageBytes, CancellationToken cancellationToken = default)
|
||||
{
|
||||
if (_webSocket == null || _webSocket.CloseStatus.HasValue)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
await _webSocket.SendAsync(messageBytes, WebSocketMessageType.Text, endOfMessage: true, cancellationToken);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_reporter.Output($"Refresh server error: {ex}");
|
||||
}
|
||||
}
|
||||
|
||||
public async ValueTask DisposeAsync()
|
||||
{
|
||||
if (_webSocket != null)
|
||||
{
|
||||
await _webSocket.CloseOutputAsync(WebSocketCloseStatus.NormalClosure, null, default);
|
||||
_webSocket.Dispose();
|
||||
}
|
||||
|
||||
if (_refreshServer != null)
|
||||
{
|
||||
await _refreshServer.StopAsync();
|
||||
_refreshServer.Dispose();
|
||||
}
|
||||
|
||||
_taskCompletionSource.TrySetResult();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -14,7 +14,7 @@ using Microsoft.Extensions.Tools.Internal;
|
|||
|
||||
namespace Microsoft.DotNet.Watcher
|
||||
{
|
||||
public class DotNetWatcher
|
||||
public class DotNetWatcher : IAsyncDisposable
|
||||
{
|
||||
private readonly IReporter _reporter;
|
||||
private readonly ProcessRunner _processRunner;
|
||||
|
|
@ -31,6 +31,7 @@ namespace Microsoft.DotNet.Watcher
|
|||
{
|
||||
new MSBuildEvaluationFilter(fileSetFactory),
|
||||
new NoRestoreFilter(),
|
||||
new LaunchBrowserFilter(),
|
||||
};
|
||||
}
|
||||
|
||||
|
|
@ -140,5 +141,21 @@ namespace Microsoft.DotNet.Watcher
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
public async ValueTask DisposeAsync()
|
||||
{
|
||||
foreach (var filter in _filters)
|
||||
{
|
||||
if (filter is IAsyncDisposable asyncDisposable)
|
||||
{
|
||||
await asyncDisposable.DisposeAsync();
|
||||
}
|
||||
else if (filter is IDisposable diposable)
|
||||
{
|
||||
diposable.Dispose();
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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.Collections.Generic;
|
||||
|
|
@ -7,6 +7,8 @@ namespace Microsoft.DotNet.Watcher
|
|||
{
|
||||
public interface IFileSet : IEnumerable<string>
|
||||
{
|
||||
bool IsNetCoreApp31OrNewer { get; }
|
||||
|
||||
bool Contains(string filePath);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -11,8 +11,9 @@ namespace Microsoft.DotNet.Watcher.Internal
|
|||
{
|
||||
private readonly HashSet<string> _files;
|
||||
|
||||
public FileSet(IEnumerable<string> files)
|
||||
public FileSet(bool isNetCoreApp31OrNewer, IEnumerable<string> files)
|
||||
{
|
||||
IsNetCoreApp31OrNewer = isNetCoreApp31OrNewer;
|
||||
_files = new HashSet<string>(files, StringComparer.OrdinalIgnoreCase);
|
||||
}
|
||||
|
||||
|
|
@ -20,7 +21,9 @@ namespace Microsoft.DotNet.Watcher.Internal
|
|||
|
||||
public int Count => _files.Count;
|
||||
|
||||
public static IFileSet Empty = new FileSet(Array.Empty<string>());
|
||||
public bool IsNetCoreApp31OrNewer { get; }
|
||||
|
||||
public static IFileSet Empty = new FileSet(false, Array.Empty<string>());
|
||||
|
||||
public IEnumerator<string> GetEnumerator() => _files.GetEnumerator();
|
||||
IEnumerator IEnumerable.GetEnumerator() => _files.GetEnumerator();
|
||||
|
|
|
|||
|
|
@ -72,6 +72,7 @@ namespace Microsoft.DotNet.Watcher.Internal
|
|||
Arguments = new[]
|
||||
{
|
||||
"msbuild",
|
||||
"/nologo",
|
||||
_projectFile,
|
||||
$"/p:_DotNetWatchListFile={watchList}"
|
||||
}.Concat(_buildFlags),
|
||||
|
|
@ -84,8 +85,12 @@ namespace Microsoft.DotNet.Watcher.Internal
|
|||
|
||||
if (exitCode == 0 && File.Exists(watchList))
|
||||
{
|
||||
var lines = File.ReadAllLines(watchList);
|
||||
var isNetCoreApp31OrNewer = lines.FirstOrDefault() == "true";
|
||||
|
||||
var fileset = new FileSet(
|
||||
File.ReadAllLines(watchList)
|
||||
isNetCoreApp31OrNewer,
|
||||
lines.Skip(1)
|
||||
.Select(l => l?.Trim())
|
||||
.Where(l => !string.IsNullOrEmpty(l)));
|
||||
|
||||
|
|
@ -123,7 +128,7 @@ namespace Microsoft.DotNet.Watcher.Internal
|
|||
{
|
||||
_reporter.Warn("Fix the error to continue or press Ctrl+C to exit.");
|
||||
|
||||
var fileSet = new FileSet(new[] { _projectFile });
|
||||
var fileSet = new FileSet(false, new[] { _projectFile });
|
||||
|
||||
using (var watcher = new FileSetWatcher(fileSet, _reporter))
|
||||
{
|
||||
|
|
|
|||
|
|
@ -37,37 +37,49 @@ namespace Microsoft.DotNet.Watcher.Internal
|
|||
{
|
||||
cancellationToken.Register(() => processState.TryKill());
|
||||
|
||||
process.OutputDataReceived += (_, a) =>
|
||||
var readOutput = false;
|
||||
var readError = false;
|
||||
if (processSpec.IsOutputCaptured)
|
||||
{
|
||||
if (!string.IsNullOrEmpty(a.Data))
|
||||
readOutput = true;
|
||||
readError = true;
|
||||
process.OutputDataReceived += (_, a) =>
|
||||
{
|
||||
processSpec.OutputCapture.AddLine(a.Data);
|
||||
}
|
||||
};
|
||||
process.ErrorDataReceived += (_, a) =>
|
||||
if (!string.IsNullOrEmpty(a.Data))
|
||||
{
|
||||
processSpec.OutputCapture.AddLine(a.Data);
|
||||
}
|
||||
};
|
||||
process.ErrorDataReceived += (_, a) =>
|
||||
{
|
||||
if (!string.IsNullOrEmpty(a.Data))
|
||||
{
|
||||
processSpec.OutputCapture.AddLine(a.Data);
|
||||
}
|
||||
};
|
||||
}
|
||||
else if (processSpec.OnOutput != null)
|
||||
{
|
||||
if (!string.IsNullOrEmpty(a.Data))
|
||||
{
|
||||
processSpec.OutputCapture.AddLine(a.Data);
|
||||
}
|
||||
};
|
||||
readOutput = true;
|
||||
process.OutputDataReceived += processSpec.OnOutput;
|
||||
}
|
||||
|
||||
stopwatch.Start();
|
||||
process.Start();
|
||||
|
||||
_reporter.Verbose($"Started '{processSpec.Executable}' with process id {process.Id}");
|
||||
|
||||
if (processSpec.IsOutputCaptured)
|
||||
if (readOutput)
|
||||
{
|
||||
process.BeginOutputReadLine();
|
||||
}
|
||||
if (readError)
|
||||
{
|
||||
process.BeginErrorReadLine();
|
||||
process.BeginOutputReadLine();
|
||||
await processState.Task;
|
||||
}
|
||||
else
|
||||
{
|
||||
await processState.Task;
|
||||
}
|
||||
|
||||
await processState.Task;
|
||||
|
||||
exitCode = process.ExitCode;
|
||||
stopwatch.Stop();
|
||||
_reporter.Verbose($"Process id {process.Id} ran for {stopwatch.ElapsedMilliseconds}ms");
|
||||
|
|
@ -87,7 +99,7 @@ namespace Microsoft.DotNet.Watcher.Internal
|
|||
Arguments = ArgumentEscaper.EscapeAndConcatenate(processSpec.Arguments),
|
||||
UseShellExecute = false,
|
||||
WorkingDirectory = processSpec.WorkingDirectory,
|
||||
RedirectStandardOutput = processSpec.IsOutputCaptured,
|
||||
RedirectStandardOutput = processSpec.IsOutputCaptured || (processSpec.OnOutput != null),
|
||||
RedirectStandardError = processSpec.IsOutputCaptured,
|
||||
}
|
||||
};
|
||||
|
|
|
|||
|
|
@ -0,0 +1,219 @@
|
|||
// 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.Diagnostics;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Runtime.InteropServices;
|
||||
using System.Text;
|
||||
using System.Text.Json;
|
||||
using System.Text.RegularExpressions;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.Extensions.Tools.Internal;
|
||||
|
||||
namespace Microsoft.DotNet.Watcher.Tools
|
||||
{
|
||||
public sealed class LaunchBrowserFilter : IWatchFilter, IAsyncDisposable
|
||||
{
|
||||
private readonly byte[] ReloadMessage = Encoding.UTF8.GetBytes("Reload");
|
||||
private readonly byte[] WaitMessage = Encoding.UTF8.GetBytes("Wait");
|
||||
private static readonly Regex NowListeningRegex = new Regex(@"^\s*Now listening on: (?<url>.*)$", RegexOptions.None | RegexOptions.Compiled, TimeSpan.FromSeconds(10));
|
||||
|
||||
private readonly bool _runningInTest;
|
||||
private readonly bool _suppressLaunchBrowser;
|
||||
private readonly string _browserPath;
|
||||
private bool _canLaunchBrowser;
|
||||
private Process _browserProcess;
|
||||
private bool _browserLaunched;
|
||||
private BrowserRefreshServer _refreshServer;
|
||||
private IReporter _reporter;
|
||||
private string _launchPath;
|
||||
private CancellationToken _cancellationToken;
|
||||
|
||||
public LaunchBrowserFilter()
|
||||
{
|
||||
var suppressLaunchBrowser = Environment.GetEnvironmentVariable("DOTNET_WATCH_SUPPRESS_LAUNCH_BROWSER");
|
||||
_suppressLaunchBrowser = (suppressLaunchBrowser == "1" || suppressLaunchBrowser == "true");
|
||||
_runningInTest = Environment.GetEnvironmentVariable("__DOTNET_WATCH_RUNNING_AS_TEST") == "true";
|
||||
_browserPath = Environment.GetEnvironmentVariable("DOTNET_WATCH_BROWSER_PATH");
|
||||
}
|
||||
|
||||
public async ValueTask ProcessAsync(DotNetWatchContext context, CancellationToken cancellationToken)
|
||||
{
|
||||
if (_suppressLaunchBrowser)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (context.Iteration == 0)
|
||||
{
|
||||
_reporter = context.Reporter;
|
||||
|
||||
if (CanLaunchBrowser(context, out var launchPath))
|
||||
{
|
||||
context.Reporter.Verbose("dotnet-watch is configured to launch a browser on ASP.NET Core application startup.");
|
||||
_canLaunchBrowser = true;
|
||||
_launchPath = launchPath;
|
||||
_cancellationToken = cancellationToken;
|
||||
|
||||
_refreshServer = new BrowserRefreshServer(context.Reporter);
|
||||
var serverUrl = await _refreshServer.StartAsync(cancellationToken);
|
||||
|
||||
context.Reporter.Verbose($"Refresh server running at {serverUrl}.");
|
||||
context.ProcessSpec.EnvironmentVariables["DOTNET_WATCH_REFRESH_URL"] = serverUrl;
|
||||
|
||||
context.ProcessSpec.OnOutput += OnOutput;
|
||||
}
|
||||
}
|
||||
|
||||
if (_canLaunchBrowser)
|
||||
{
|
||||
if (context.Iteration > 0)
|
||||
{
|
||||
// We've detected a change. Notify the browser.
|
||||
await _refreshServer.SendMessage(WaitMessage, cancellationToken);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void OnOutput(object sender, DataReceivedEventArgs eventArgs)
|
||||
{
|
||||
// We've redirected the output, but want to ensure that it continues to appear in the user's console.
|
||||
Console.WriteLine(eventArgs.Data);
|
||||
|
||||
if (string.IsNullOrEmpty(eventArgs.Data))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var match = NowListeningRegex.Match(eventArgs.Data);
|
||||
if (match.Success)
|
||||
{
|
||||
var launchUrl = match.Groups["url"].Value;
|
||||
|
||||
var process = (Process)sender;
|
||||
process.OutputDataReceived -= OnOutput;
|
||||
process.CancelOutputRead();
|
||||
|
||||
if (!_browserLaunched)
|
||||
{
|
||||
_reporter.Verbose("Launching browser.");
|
||||
try
|
||||
{
|
||||
LaunchBrowser(launchUrl);
|
||||
_browserLaunched = true;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_reporter.Output($"Unable to launch browser: {ex}");
|
||||
_canLaunchBrowser = false;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
_reporter.Verbose("Reloading browser.");
|
||||
_ = _refreshServer.SendMessage(ReloadMessage, _cancellationToken);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void LaunchBrowser(string launchUrl)
|
||||
{
|
||||
var fileName = launchUrl + "/" + _launchPath;
|
||||
var args = string.Empty;
|
||||
if (!string.IsNullOrEmpty(_browserPath))
|
||||
{
|
||||
args = fileName;
|
||||
fileName = _browserPath;
|
||||
}
|
||||
|
||||
if (_runningInTest)
|
||||
{
|
||||
_reporter.Output($"Launching browser: {fileName} {args}");
|
||||
return;
|
||||
}
|
||||
|
||||
_browserProcess = Process.Start(new ProcessStartInfo
|
||||
{
|
||||
FileName = fileName,
|
||||
Arguments = args,
|
||||
UseShellExecute = true,
|
||||
});
|
||||
}
|
||||
|
||||
private static bool CanLaunchBrowser(DotNetWatchContext context, out string launchUrl)
|
||||
{
|
||||
launchUrl = null;
|
||||
var reporter = context.Reporter;
|
||||
|
||||
if (!context.FileSet.IsNetCoreApp31OrNewer)
|
||||
{
|
||||
// Browser refresh middleware supports 3.1 or newer
|
||||
reporter.Verbose("Browser refresh is only supported in .NET Core 3.1 or newer projects.");
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!RuntimeInformation.IsOSPlatform(OSPlatform.Windows) && !RuntimeInformation.IsOSPlatform(OSPlatform.OSX))
|
||||
{
|
||||
// Launching a browser requires file associations that are not available in all operating systems.
|
||||
reporter.Verbose("Browser refresh is only supported in Windows and MacOS.");
|
||||
return false;
|
||||
}
|
||||
|
||||
var dotnetCommand = context.ProcessSpec.Arguments.FirstOrDefault();
|
||||
if (!string.Equals(dotnetCommand, "run", StringComparison.Ordinal))
|
||||
{
|
||||
reporter.Verbose("Browser refresh is only supported for run commands.");
|
||||
return false;
|
||||
}
|
||||
|
||||
// We're executing the run-command. Determine if the launchSettings allows it
|
||||
var launchSettingsPath = Path.Combine(context.ProcessSpec.WorkingDirectory, "Properties", "launchSettings.json");
|
||||
if (!File.Exists(launchSettingsPath))
|
||||
{
|
||||
reporter.Verbose($"No launchSettings.json file found at {launchSettingsPath}. Unable to determine if browser refresh is allowed.");
|
||||
return false;
|
||||
}
|
||||
|
||||
LaunchSettingsJson launchSettings;
|
||||
try
|
||||
{
|
||||
launchSettings = JsonSerializer.Deserialize<LaunchSettingsJson>(
|
||||
File.ReadAllText(launchSettingsPath),
|
||||
new JsonSerializerOptions(JsonSerializerDefaults.Web));
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
reporter.Verbose($"Error reading launchSettings.json: {ex}.");
|
||||
return false;
|
||||
}
|
||||
|
||||
var defaultProfile = launchSettings.Profiles.FirstOrDefault(f => f.Value.CommandName == "Project").Value;
|
||||
if (defaultProfile is null)
|
||||
{
|
||||
reporter.Verbose("Unable to find default launchSettings profile.");
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!defaultProfile.LaunchBrowser)
|
||||
{
|
||||
reporter.Verbose("launchSettings does not allow launching browsers.");
|
||||
return false;
|
||||
}
|
||||
|
||||
launchUrl = defaultProfile.LaunchUrl;
|
||||
return true;
|
||||
}
|
||||
|
||||
public async ValueTask DisposeAsync()
|
||||
{
|
||||
_browserProcess?.Dispose();
|
||||
if (_refreshServer != null)
|
||||
{
|
||||
await _refreshServer.DisposeAsync();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,21 @@
|
|||
// 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;
|
||||
|
||||
namespace Microsoft.DotNet.Watcher.Tools
|
||||
{
|
||||
public class LaunchSettingsJson
|
||||
{
|
||||
public Dictionary<string, LaunchSettingsProfile> Profiles { get; set; }
|
||||
}
|
||||
|
||||
public class LaunchSettingsProfile
|
||||
{
|
||||
public string CommandName { get; set; }
|
||||
|
||||
public bool LaunchBrowser { get; set; }
|
||||
|
||||
public string LaunchUrl { get; set; }
|
||||
}
|
||||
}
|
||||
|
|
@ -1,8 +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.Diagnostics;
|
||||
using System.IO;
|
||||
using System.Threading;
|
||||
using Microsoft.DotNet.Watcher.Internal;
|
||||
|
||||
namespace Microsoft.DotNet.Watcher
|
||||
|
|
@ -19,5 +21,9 @@ namespace Microsoft.DotNet.Watcher
|
|||
=> Path.GetFileNameWithoutExtension(Executable);
|
||||
|
||||
public bool IsOutputCaptured => OutputCapture != null;
|
||||
|
||||
public DataReceivedEventHandler OnOutput { get; set; }
|
||||
|
||||
public CancellationToken CancelOutputCapture { get; set; }
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -162,8 +162,8 @@ namespace Microsoft.DotNet.Watcher
|
|||
_reporter.Output("Polling file watcher is enabled");
|
||||
}
|
||||
|
||||
await new DotNetWatcher(reporter, fileSetFactory)
|
||||
.WatchAsync(processInfo, cancellationToken);
|
||||
await using var watcher = new DotNetWatcher(reporter, fileSetFactory);
|
||||
await watcher.WatchAsync(processInfo, cancellationToken);
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -9,9 +9,22 @@ them to a file.
|
|||
-->
|
||||
<Target Name="GenerateWatchList"
|
||||
DependsOnTargets="_CollectWatchItems">
|
||||
|
||||
<PropertyGroup>
|
||||
<_IsMicrosoftNETCoreApp31OrNewer
|
||||
Condition="'$(TargetFrameworkIdentifier)' == '.NETCoreApp' And $([MSBuild]::VersionGreaterThanOrEquals($(TargetFrameworkVersion), '3.1'))">true</_IsMicrosoftNETCoreApp31OrNewer>
|
||||
|
||||
<_IsMicrosoftNETCoreApp31OrNewer Condition="'$(_IsMicrosoftNETCoreApp31OrNewer)' == ''">false</_IsMicrosoftNETCoreApp31OrNewer>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<_WatchListLine Include="$(_IsMicrosoftNETCoreApp31OrNewer)" />
|
||||
<_WatchListLine Include="%(Watch.FullPath)" />
|
||||
</ItemGroup>
|
||||
|
||||
<WriteLinesToFile Overwrite="true"
|
||||
File="$(_DotNetWatchListFile)"
|
||||
Lines="@(Watch -> '%(FullPath)')" />
|
||||
Lines="@(_WatchListLine)" />
|
||||
</Target>
|
||||
|
||||
<!--
|
||||
|
|
|
|||
|
|
@ -19,4 +19,24 @@
|
|||
<None Include="assets\**\*" CopyToOutputDirectory="PreserveNewest" CopyToPublishDirectory="PreserveNewest" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<Reference Include="Microsoft.AspNetCore" />
|
||||
<Reference Include="Microsoft.AspNetCore.WebSockets" />
|
||||
</ItemGroup>
|
||||
|
||||
<Target Name="_FixupRuntimeConfig" BeforeTargets="_GenerateRuntimeConfigurationFilesInputCache">
|
||||
<ItemGroup>
|
||||
<_RuntimeFramework Include="@(RuntimeFramework)" />
|
||||
<RuntimeFramework Remove="@(RuntimeFramework)" />
|
||||
<RuntimeFramework Include="Microsoft.AspNetCore.App" FrameworkName="Microsoft.AspNetCore.App" Version="5.0.0-preview" />
|
||||
</ItemGroup>
|
||||
</Target>
|
||||
|
||||
<Target Name="_UndoRuntimeConfigWorkarounds" AfterTargets="GenerateBuildRuntimeConfigurationFiles">
|
||||
<ItemGroup>
|
||||
<RuntimeFramework Remove="@(RuntimeFramework)" />
|
||||
<RuntimeFramework Include="@(_RuntimeFramework)" />
|
||||
</ItemGroup>
|
||||
</Target>
|
||||
|
||||
</Project>
|
||||
|
|
|
|||
|
|
@ -0,0 +1,3 @@
|
|||
{
|
||||
"rollForwardOnNoCandidateFx": 2
|
||||
}
|
||||
|
|
@ -0,0 +1,68 @@
|
|||
// 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.IO;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.AspNetCore.Testing;
|
||||
using Xunit.Abstractions;
|
||||
|
||||
namespace Microsoft.DotNet.Watcher.Tools.FunctionalTests
|
||||
{
|
||||
public class BrowserLaunchTests
|
||||
{
|
||||
private readonly WatchableApp _app;
|
||||
|
||||
public BrowserLaunchTests(ITestOutputHelper logger)
|
||||
{
|
||||
_app = new WatchableApp("AppWithLaunchSettings", logger);
|
||||
}
|
||||
|
||||
[ConditionalFact]
|
||||
[OSSkipCondition(OperatingSystems.Linux)]
|
||||
public async Task LaunchesBrowserOnStart()
|
||||
{
|
||||
var expected = "watch : Launching browser: https://localhost:5001/";
|
||||
_app.DotnetWatchArgs.Add("--verbose");
|
||||
|
||||
await _app.StartWatcherAsync();
|
||||
|
||||
// Verify we launched the browser.
|
||||
await _app.Process.GetOutputLineStartsWithAsync(expected, TimeSpan.FromMinutes(2));
|
||||
}
|
||||
|
||||
[ConditionalFact]
|
||||
[OSSkipCondition(OperatingSystems.Linux)]
|
||||
public async Task RefreshesBrowserOnChange()
|
||||
{
|
||||
var launchBrowserMessage = "watch : Launching browser: https://localhost:5001/";
|
||||
var refreshBrowserMessage = "watch : Reloading browser";
|
||||
_app.DotnetWatchArgs.Add("--verbose");
|
||||
var source = Path.Combine(_app.SourceDirectory, "Program.cs");
|
||||
|
||||
await _app.StartWatcherAsync();
|
||||
|
||||
// Verify we launched the browser.
|
||||
await _app.Process.GetOutputLineStartsWithAsync(launchBrowserMessage, TimeSpan.FromMinutes(2));
|
||||
|
||||
// Make a file change and verify we reloaded the browser.
|
||||
File.SetLastWriteTime(source, DateTime.Now);
|
||||
await _app.Process.GetOutputLineStartsWithAsync(refreshBrowserMessage, TimeSpan.FromMinutes(2));
|
||||
}
|
||||
|
||||
[ConditionalFact]
|
||||
[OSSkipCondition(OperatingSystems.Linux)]
|
||||
public async Task UsesBrowserSpecifiedInEnvironment()
|
||||
{
|
||||
var launchBrowserMessage = "watch : Launching browser: mycustombrowser.bat https://localhost:5001/";
|
||||
_app.EnvironmentVariables.Add("DOTNET_WATCH_BROWSER_PATH", "mycustombrowser.bat");
|
||||
|
||||
_app.DotnetWatchArgs.Add("--verbose");
|
||||
|
||||
await _app.StartWatcherAsync();
|
||||
|
||||
// Verify we launched the browser.
|
||||
await _app.Process.GetOutputLineStartsWithAsync(launchBrowserMessage, TimeSpan.FromMinutes(2));
|
||||
}
|
||||
}
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue