Compatibility switches (#7142)
* [Design] Compatibility switches This introduces a pattern for versioning breaking behaviour changes in minor releases of MVC. The general plan is that application developers choose a release version (2.0, 2.1, Latest) as their baseline which determines the effective 'defaults' for some options. Anything the developer sets explicitly is an override and always wins. Then we add a version setting to the template to point to the current release. This allows us to be progressive with fixing issues and improving areas that don't work well, but offers the developer some choice about when to adopt new behaviours. In effect, we separate new behaviours from the libraries that develiver them. Apps can update the version, and then opt in to new behaviours as a separate change. * Be more american * improve docs, add example * Fix visibility * Fix broken test * Add test * Docs! * The rest of the tests * fix example * Adding docs * PR feedback
This commit is contained in:
parent
6981c29394
commit
747420e5aa
|
|
@ -0,0 +1,59 @@
|
|||
// Copyright (c) .NET Foundation. All rights reserved.
|
||||
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
|
||||
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
|
||||
namespace Microsoft.AspNetCore.Mvc
|
||||
{
|
||||
/// <summary>
|
||||
/// Specifies the version compatibility of runtime behaviors configured by <see cref="MvcOptions"/>.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// <para>
|
||||
/// The best way to set a compatibility version is by using
|
||||
/// <see cref="MvcCoreMvcBuilderExtensions.SetCompatibilityVersion"/> or
|
||||
/// <see cref="MvcCoreMvcCoreBuilderExtensions.SetCompatibilityVersion"/> in your application's
|
||||
/// <c>ConfigureServices</c> method.
|
||||
/// <example>
|
||||
/// Setting the compatibility version using <see cref="IMvcBuilder"/>:
|
||||
/// <code>
|
||||
/// public class Startup
|
||||
/// {
|
||||
/// ...
|
||||
///
|
||||
/// public void ConfigureServices(IServiceCollection services)
|
||||
/// {
|
||||
/// services.AddMvc().SetCompatibilityVersion(CompatibilityVersion.Version_2_1);
|
||||
/// }
|
||||
///
|
||||
/// ...
|
||||
/// }
|
||||
/// </code>
|
||||
/// </example>
|
||||
/// </para>
|
||||
/// <para>
|
||||
/// Setting compatiblity version to a specific version will change the default values of various
|
||||
/// settings to match a particular minor release of ASP.NET Core MVC.
|
||||
/// </para>
|
||||
/// </remarks>
|
||||
public enum CompatibilityVersion
|
||||
{
|
||||
/// <summary>
|
||||
/// Sets the default value of settings on <see cref="MvcOptions"/> to match the behavior of
|
||||
/// ASP.NET Core MVC 2.0.
|
||||
/// </summary>
|
||||
Version_2_0,
|
||||
|
||||
/// <summary>
|
||||
/// Sets the default value of settings on <see cref="MvcOptions"/> to match the behavior of
|
||||
/// ASP.NET Core MVC 2.1.
|
||||
/// </summary>
|
||||
Version_2_1,
|
||||
|
||||
/// <summary>
|
||||
/// Sets the default value of settings on <see cref="MvcOptions"/> to match the latest release. Use this
|
||||
/// value with care, upgrading minor versions will cause breaking changes when using <see cref="Latest"/>.
|
||||
/// </summary>
|
||||
Latest = int.MaxValue,
|
||||
}
|
||||
}
|
||||
|
|
@ -8,6 +8,7 @@ using Microsoft.AspNetCore.Mvc;
|
|||
using Microsoft.AspNetCore.Mvc.ApplicationParts;
|
||||
using Microsoft.AspNetCore.Mvc.Controllers;
|
||||
using Microsoft.AspNetCore.Mvc.Formatters;
|
||||
using Microsoft.AspNetCore.Mvc.Infrastructure;
|
||||
using Microsoft.Extensions.DependencyInjection.Extensions;
|
||||
|
||||
namespace Microsoft.Extensions.DependencyInjection
|
||||
|
|
@ -116,6 +117,11 @@ namespace Microsoft.Extensions.DependencyInjection
|
|||
/// <returns>The <see cref="IMvcBuilder"/>.</returns>
|
||||
public static IMvcBuilder AddControllersAsServices(this IMvcBuilder builder)
|
||||
{
|
||||
if (builder == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(builder));
|
||||
}
|
||||
|
||||
var feature = new ControllerFeature();
|
||||
builder.PartManager.PopulateFeature(feature);
|
||||
|
||||
|
|
@ -128,5 +134,22 @@ namespace Microsoft.Extensions.DependencyInjection
|
|||
|
||||
return builder;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Sets the <see cref="CompatibilityVersion"/> for ASP.NET Core MVC for the application.
|
||||
/// </summary>
|
||||
/// <param name="builder">The <see cref="IMvcBuilder"/>.</param>
|
||||
/// <param name="version">The <see cref="CompatibilityVersion"/> value to configure.</param>
|
||||
/// <returns>The <see cref="IMvcBuilder"/>.</returns>
|
||||
public static IMvcBuilder SetCompatibilityVersion(this IMvcBuilder builder, CompatibilityVersion version)
|
||||
{
|
||||
if (builder == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(builder));
|
||||
}
|
||||
|
||||
builder.Services.Configure<MvcCompatibilityOptions>(o => o.CompatibilityVersion = version);
|
||||
return builder;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -10,6 +10,7 @@ using Microsoft.AspNetCore.Mvc.ApplicationModels;
|
|||
using Microsoft.AspNetCore.Mvc.ApplicationParts;
|
||||
using Microsoft.AspNetCore.Mvc.Controllers;
|
||||
using Microsoft.AspNetCore.Mvc.Formatters;
|
||||
using Microsoft.AspNetCore.Mvc.Infrastructure;
|
||||
using Microsoft.AspNetCore.Mvc.Internal;
|
||||
using Microsoft.Extensions.DependencyInjection.Extensions;
|
||||
|
||||
|
|
@ -167,5 +168,22 @@ namespace Microsoft.Extensions.DependencyInjection
|
|||
|
||||
return builder;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Sets the <see cref="CompatibilityVersion"/> for ASP.NET Core MVC for the application.
|
||||
/// </summary>
|
||||
/// <param name="builder">The <see cref="IMvcCoreBuilder"/>.</param>
|
||||
/// <param name="version">The <see cref="CompatibilityVersion"/> value to configure.</param>
|
||||
/// <returns>The <see cref="IMvcCoreBuilder"/>.</returns>
|
||||
public static IMvcCoreBuilder SetCompatibilityVersion(this IMvcCoreBuilder builder, CompatibilityVersion version)
|
||||
{
|
||||
if (builder == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(builder));
|
||||
}
|
||||
|
||||
builder.Services.Configure<MvcCompatibilityOptions>(o => o.CompatibilityVersion = version);
|
||||
return builder;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -147,6 +147,8 @@ namespace Microsoft.Extensions.DependencyInjection
|
|||
//
|
||||
services.TryAddEnumerable(
|
||||
ServiceDescriptor.Transient<IConfigureOptions<MvcOptions>, MvcCoreMvcOptionsSetup>());
|
||||
services.TryAddEnumerable(
|
||||
ServiceDescriptor.Transient<IPostConfigureOptions<MvcOptions>, MvcOptionsConfigureCompatibilityOptions>());
|
||||
services.TryAddEnumerable(
|
||||
ServiceDescriptor.Transient<IConfigureOptions<ApiBehaviorOptions>, ApiBehaviorOptionsSetup>());
|
||||
services.TryAddEnumerable(
|
||||
|
|
|
|||
|
|
@ -0,0 +1,129 @@
|
|||
// 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.Extensions.Options;
|
||||
|
||||
namespace Microsoft.AspNetCore.Mvc.Infrastructure
|
||||
{
|
||||
// Guide to making breaking behavior changes in MVC:
|
||||
//
|
||||
// Hello, if you're reading this, you're probably trying to make a change in behavior in MVC in a minor
|
||||
// version. Every change in behavior is a breaking change to someone, even if a feature was buggy or
|
||||
// broken in some scenarios.
|
||||
//
|
||||
// To help make things easier for current users, we don't automatically opt users into breaking changes when
|
||||
// they upgrade applications to a new minor version of ASP.NET Core. It's a separate choice to opt in to new
|
||||
// behaviors in a minor release.
|
||||
//
|
||||
// To make things better for future users, we also want to provide an easy way for applications to get
|
||||
// access to the new behaviors. We make changes when they are improvments, and if we're changing something
|
||||
// we've already shipped, it must add value for all of our users (eventually). To this end, new applications
|
||||
// created using the template are always opted in to the 'current' version.
|
||||
//
|
||||
// This means that all changes in behavior should be opt-in.
|
||||
//
|
||||
// -----
|
||||
//
|
||||
// Moving on from general philosophy, here's how to implement a behavior change and corresponding
|
||||
// compatibility switch.
|
||||
//
|
||||
// Add a new property on options that uses a CompatibilitySwitch<T> as a backing field. Make sure the
|
||||
// new switch is exposed by implementing IEnumerable<ICompatibilitySwitch> on the options class. Pass the
|
||||
// property name to the CompatibilitySwitch constructor using nameof.
|
||||
//
|
||||
// Choose a boolean value or a new enum type as the 'value' of the property.
|
||||
//
|
||||
// If the new property has a boolean value, it should be named something like `SuppressFoo`
|
||||
// (if the new value deactivates some behavior) or like `AllowFoo` (if the new value enables some behavior).
|
||||
// Choose a name so that the old behavior equates to 'false'.
|
||||
//
|
||||
// If it's an enum, make sure you initialize the compatibility switch using the
|
||||
// CompatibilitySwitch(string, value) constructor to make it obvious the correct value is passed in. It's
|
||||
// a good idea to equate the original behavior with the default enum value as well.
|
||||
//
|
||||
// Then create (or modify) a subclass of ConfigureCompatibilityOptions appropriate for your options type.
|
||||
// Override the DefaultValues property and provide appropriate values based on the value of the Version
|
||||
// property. If you just added this class, register it as an IPostConfigureOptions<TOptions> in DI.
|
||||
//
|
||||
/// <summary>
|
||||
/// Infrastructure supporting the implementation of <see cref="CompatibilityVersion"/>. This is an
|
||||
/// implementation of <see cref="ICompatibilitySwitch"/> suitible for use with the <see cref="IOptions{T}"/>
|
||||
/// pattern. This is framework infrastructure and should not be used by application code.
|
||||
/// </summary>
|
||||
/// <typeparam name="TValue">The type of value assoicated with the compatibility switch.</typeparam>
|
||||
public class CompatibilitySwitch<TValue> : ICompatibilitySwitch where TValue : struct
|
||||
{
|
||||
private TValue _value;
|
||||
|
||||
/// <summary>
|
||||
/// Creates a new compatiblity switch with the provided name.
|
||||
/// </summary>
|
||||
/// <param name="name">
|
||||
/// The compatiblity switch name. The name must match a property name on an options type.
|
||||
/// </param>
|
||||
public CompatibilitySwitch(string name)
|
||||
: this(name, default)
|
||||
{
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates a new compatiblity switch with the provided name and initial value.
|
||||
/// </summary>
|
||||
/// <param name="name">
|
||||
/// The compatiblity switch name. The name must match a property name on an options type.
|
||||
/// </param>
|
||||
/// <param name="initialValue">
|
||||
/// The initial value to assign to the switch.
|
||||
/// </param>
|
||||
public CompatibilitySwitch(string name, TValue initialValue)
|
||||
{
|
||||
if (name == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(name));
|
||||
}
|
||||
|
||||
Name = name;
|
||||
_value = initialValue;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets a value indicating whether the <see cref="Value"/> property has been set.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// This is used by the compatibility infrastructure to determine whether the application developer
|
||||
/// has set explicitly set the value associated with this switch.
|
||||
/// </remarks>
|
||||
public bool IsValueSet { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the name of the compatibility switch.
|
||||
/// </summary>
|
||||
public string Name { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or set the value associated with the compatibility switch.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Setting the switch value using <see cref="Value"/> will set <see cref="IsValueSet"/> to <c>true</c>.
|
||||
/// As a consequence, the compatibility infrastructure will consider this switch explicitly configured by
|
||||
/// the application developer, and will not apply a default value based on the compatibility version.
|
||||
/// </remarks>
|
||||
public TValue Value
|
||||
{
|
||||
get => _value;
|
||||
set
|
||||
{
|
||||
IsValueSet = true;
|
||||
_value = value;
|
||||
}
|
||||
}
|
||||
|
||||
// Called by the compatibility infrastructure to set a default value when IsValueSet is false.
|
||||
object ICompatibilitySwitch.Value
|
||||
{
|
||||
get => Value;
|
||||
set => Value = (TValue)value;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,105 @@
|
|||
// 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.Logging;
|
||||
using Microsoft.Extensions.Options;
|
||||
|
||||
namespace Microsoft.AspNetCore.Mvc.Infrastructure
|
||||
{
|
||||
/// <summary>
|
||||
/// A base class for infrastructure that implements ASP.NET Core MVC's support for
|
||||
/// <see cref="CompatibilityVersion"/>. This is framework infrastructure and should not be used
|
||||
/// by application code.
|
||||
/// </summary>
|
||||
/// <typeparam name="TOptions"></typeparam>
|
||||
public abstract class ConfigureCompatibilityOptions<TOptions> : IPostConfigureOptions<TOptions>
|
||||
where TOptions : class, IEnumerable<ICompatibilitySwitch>
|
||||
{
|
||||
private readonly ILogger _logger;
|
||||
|
||||
/// <summary>
|
||||
/// Creates a new <see cref="ConfigureCompatibilityOptions{TOptions}"/>.
|
||||
/// </summary>
|
||||
/// <param name="loggerFactory">The <see cref="ILoggerFactory"/>.</param>
|
||||
/// <param name="compatibilityOptions">The <see cref="IOptions{MvcCompatibilityOptions}"/>.</param>
|
||||
protected ConfigureCompatibilityOptions(
|
||||
ILoggerFactory loggerFactory,
|
||||
IOptions<MvcCompatibilityOptions> compatibilityOptions)
|
||||
{
|
||||
if (loggerFactory == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(loggerFactory));
|
||||
}
|
||||
|
||||
Version = compatibilityOptions.Value.CompatibilityVersion;
|
||||
_logger = loggerFactory.CreateLogger<TOptions>();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the default values of compatibility switches associated with the applications configured
|
||||
/// <see cref="CompatibilityVersion"/>.
|
||||
/// </summary>
|
||||
protected abstract IReadOnlyDictionary<string, object> DefaultValues { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the <see cref="CompatibilityVersion"/> configured for the application.
|
||||
/// </summary>
|
||||
protected CompatibilityVersion Version { get; }
|
||||
|
||||
/// <inheritdoc />
|
||||
public virtual void PostConfigure(string name, TOptions options)
|
||||
{
|
||||
if (name == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(name));
|
||||
}
|
||||
|
||||
if (options == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(options));
|
||||
}
|
||||
|
||||
// Evaluate DefaultValues onces so subclasses don't have to cache.
|
||||
var defaultValues = DefaultValues;
|
||||
|
||||
foreach (var @switch in options)
|
||||
{
|
||||
ConfigureSwitch(@switch, defaultValues);
|
||||
}
|
||||
}
|
||||
|
||||
private void ConfigureSwitch(ICompatibilitySwitch @switch, IReadOnlyDictionary<string, object> defaultValues)
|
||||
{
|
||||
if (@switch.IsValueSet)
|
||||
{
|
||||
_logger.LogDebug(
|
||||
"Compatibility switch {SwitchName} in type {OptionsType} is using explicitly configured value {Value}",
|
||||
@switch.Name,
|
||||
typeof(TOptions).Name,
|
||||
@switch.Value);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!defaultValues.TryGetValue(@switch.Name, out var value))
|
||||
{
|
||||
_logger.LogDebug(
|
||||
"Compatibility switch {SwitchName} in type {OptionsType} is using default value {Value}",
|
||||
@switch.Name,
|
||||
typeof(TOptions).Name,
|
||||
@switch.Value,
|
||||
Version);
|
||||
return;
|
||||
}
|
||||
|
||||
@switch.Value = value;
|
||||
_logger.LogDebug(
|
||||
"Compatibility switch {SwitchName} in type {OptionsType} is using compatibility value {Value} for version {Version}",
|
||||
@switch.Name,
|
||||
typeof(TOptions).Name,
|
||||
@switch.Value,
|
||||
Version);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,36 @@
|
|||
// 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.
|
||||
|
||||
namespace Microsoft.AspNetCore.Mvc.Infrastructure
|
||||
{
|
||||
/// <summary>
|
||||
/// Defines a compatibility switch. This is framework infrastructure and should not be used
|
||||
/// by application code.
|
||||
/// </summary>
|
||||
public interface ICompatibilitySwitch
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets a value indicating whether the <see cref="Value"/> property has been set.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// This is used by the compatibility infrastructure to determine whether the application developer
|
||||
/// has set explicitly set the value associated with this switch.
|
||||
/// </remarks>
|
||||
bool IsValueSet { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the name of the compatibility switch.
|
||||
/// </summary>
|
||||
string Name { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or set the value associated with the compatibility switch.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Setting the switch value using <see cref="Value"/> will not set <see cref="IsValueSet"/> to <c>true</c>.
|
||||
/// This should be used by the compatibility infrastructure when <see cref="IsValueSet"/> is <c>false</c>
|
||||
/// to apply a compatibility value based on <see cref="CompatibilityVersion"/>.
|
||||
/// </remarks>
|
||||
object Value { get; set; }
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,23 @@
|
|||
// Copyright (c) .NET Foundation. All rights reserved.
|
||||
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
|
||||
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
|
||||
namespace Microsoft.AspNetCore.Mvc.Infrastructure
|
||||
{
|
||||
/// <summary>
|
||||
/// An options type for configuring the application <see cref="Mvc.CompatibilityVersion"/>.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// The primary way to configure the application's <see cref="Mvc.CompatibilityVersion"/> is by
|
||||
/// calling <see cref="MvcCoreMvcBuilderExtensions.SetCompatibilityVersion(IMvcBuilder, CompatibilityVersion)"/>
|
||||
/// or <see cref="MvcCoreMvcCoreBuilderExtensions.SetCompatibilityVersion(IMvcCoreBuilder, CompatibilityVersion)"/>.
|
||||
/// </remarks>
|
||||
public class MvcCompatibilityOptions
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets or sets the application's configured <see cref="Mvc.CompatibilityVersion"/>.
|
||||
/// </summary>
|
||||
public CompatibilityVersion CompatibilityVersion { get; set; } = CompatibilityVersion.Version_2_0;
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,35 @@
|
|||
// 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 Microsoft.AspNetCore.Mvc.Formatters;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Microsoft.Extensions.Options;
|
||||
|
||||
namespace Microsoft.AspNetCore.Mvc.Infrastructure
|
||||
{
|
||||
internal class MvcOptionsConfigureCompatibilityOptions : ConfigureCompatibilityOptions<MvcOptions>
|
||||
{
|
||||
public MvcOptionsConfigureCompatibilityOptions(
|
||||
ILoggerFactory loggerFactory,
|
||||
IOptions<MvcCompatibilityOptions> compatibilityOptions)
|
||||
: base(loggerFactory, compatibilityOptions)
|
||||
{
|
||||
}
|
||||
|
||||
protected override IReadOnlyDictionary<string, object> DefaultValues
|
||||
{
|
||||
get
|
||||
{
|
||||
var values = new Dictionary<string, object>();
|
||||
|
||||
if (Version >= CompatibilityVersion.Version_2_1)
|
||||
{
|
||||
values[nameof(MvcOptions.InputFormatterExceptionModelStatePolicy)] = InputFormatterExceptionModelStatePolicy.MalformedInputExceptions;
|
||||
}
|
||||
|
||||
return values;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -2,10 +2,12 @@
|
|||
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
|
||||
|
||||
using System;
|
||||
using System.Collections;
|
||||
using System.Collections.Generic;
|
||||
using Microsoft.AspNetCore.Mvc.ApplicationModels;
|
||||
using Microsoft.AspNetCore.Mvc.Filters;
|
||||
using Microsoft.AspNetCore.Mvc.Formatters;
|
||||
using Microsoft.AspNetCore.Mvc.Infrastructure;
|
||||
using Microsoft.AspNetCore.Mvc.ModelBinding;
|
||||
using Microsoft.AspNetCore.Mvc.ModelBinding.Metadata;
|
||||
using Microsoft.AspNetCore.Mvc.ModelBinding.Validation;
|
||||
|
|
@ -15,10 +17,16 @@ namespace Microsoft.AspNetCore.Mvc
|
|||
/// <summary>
|
||||
/// Provides programmatic configuration for the MVC framework.
|
||||
/// </summary>
|
||||
public class MvcOptions
|
||||
public class MvcOptions : IEnumerable<ICompatibilitySwitch>
|
||||
{
|
||||
private int _maxModelStateErrors = ModelStateDictionary.DefaultMaxAllowedErrors;
|
||||
|
||||
// See CompatibilitySwitch.cs for guide on how to implement these.
|
||||
private readonly CompatibilitySwitch<bool> _allowBindingUndefinedValueToEnumType;
|
||||
private readonly CompatibilitySwitch<InputFormatterExceptionModelStatePolicy> _inputFormatterExceptionModelStatePolicy;
|
||||
private readonly CompatibilitySwitch<bool> _suppressJsonDeserializationExceptionMessagesInModelState;
|
||||
private readonly ICompatibilitySwitch[] _switches;
|
||||
|
||||
public MvcOptions()
|
||||
{
|
||||
CacheProfiles = new Dictionary<string, CacheProfile>(StringComparer.OrdinalIgnoreCase);
|
||||
|
|
@ -32,6 +40,16 @@ namespace Microsoft.AspNetCore.Mvc
|
|||
ModelMetadataDetailsProviders = new List<IMetadataDetailsProvider>();
|
||||
ModelValidatorProviders = new List<IModelValidatorProvider>();
|
||||
ValueProviderFactories = new List<IValueProviderFactory>();
|
||||
|
||||
_allowBindingUndefinedValueToEnumType = new CompatibilitySwitch<bool>(nameof(AllowBindingUndefinedValueToEnumType));
|
||||
_inputFormatterExceptionModelStatePolicy = new CompatibilitySwitch<InputFormatterExceptionModelStatePolicy>(nameof(InputFormatterExceptionModelStatePolicy), InputFormatterExceptionModelStatePolicy.AllExceptions);
|
||||
_suppressJsonDeserializationExceptionMessagesInModelState = new CompatibilitySwitch<bool>(nameof(SuppressJsonDeserializationExceptionMessagesInModelState));
|
||||
_switches = new ICompatibilitySwitch[]
|
||||
{
|
||||
_allowBindingUndefinedValueToEnumType,
|
||||
_inputFormatterExceptionModelStatePolicy,
|
||||
_suppressJsonDeserializationExceptionMessagesInModelState,
|
||||
};
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
|
@ -167,7 +185,11 @@ namespace Microsoft.AspNetCore.Mvc
|
|||
/// Gets or sets an indication whether the model binding system will bind undefined values to enumeration types.
|
||||
/// <see langword="false"/> by default.
|
||||
/// </summary>
|
||||
public bool AllowBindingUndefinedValueToEnumType { get; set; }
|
||||
public bool AllowBindingUndefinedValueToEnumType
|
||||
{
|
||||
get => _allowBindingUndefinedValueToEnumType.Value;
|
||||
set => _allowBindingUndefinedValueToEnumType.Value = value;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the option to determine if model binding should convert all exceptions (including ones not related to bad input)
|
||||
|
|
@ -175,7 +197,11 @@ namespace Microsoft.AspNetCore.Mvc
|
|||
/// This option applies only to custom <see cref="IInputFormatter"/>s.
|
||||
/// Default is <see cref="InputFormatterExceptionModelStatePolicy.AllExceptions"/>.
|
||||
/// </summary>
|
||||
public InputFormatterExceptionModelStatePolicy InputFormatterExceptionModelStatePolicy { get; set; }
|
||||
public InputFormatterExceptionModelStatePolicy InputFormatterExceptionModelStatePolicy
|
||||
{
|
||||
get => _inputFormatterExceptionModelStatePolicy.Value;
|
||||
set => _inputFormatterExceptionModelStatePolicy.Value = value;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets a flag to determine whether, if an action receives invalid JSON in
|
||||
|
|
@ -184,6 +210,17 @@ namespace Microsoft.AspNetCore.Mvc
|
|||
/// <see langword="false"/> by default, meaning that clients may receive details about
|
||||
/// why the JSON they posted is considered invalid.
|
||||
/// </summary>
|
||||
public bool SuppressJsonDeserializationExceptionMessagesInModelState { get; set; } = false;
|
||||
public bool SuppressJsonDeserializationExceptionMessagesInModelState
|
||||
{
|
||||
get => _suppressJsonDeserializationExceptionMessagesInModelState.Value;
|
||||
set => _suppressJsonDeserializationExceptionMessagesInModelState.Value = value;
|
||||
}
|
||||
|
||||
IEnumerator<ICompatibilitySwitch> IEnumerable<ICompatibilitySwitch>.GetEnumerator()
|
||||
{
|
||||
return ((IEnumerable<ICompatibilitySwitch>)_switches).GetEnumerator();
|
||||
}
|
||||
|
||||
IEnumerator IEnumerable.GetEnumerator() => _switches.GetEnumerator();
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -12,6 +12,7 @@ using Microsoft.AspNetCore.Mvc.ApplicationModels;
|
|||
using Microsoft.AspNetCore.Mvc.ApplicationParts;
|
||||
using Microsoft.AspNetCore.Mvc.Controllers;
|
||||
using Microsoft.AspNetCore.Mvc.Filters;
|
||||
using Microsoft.AspNetCore.Mvc.Infrastructure;
|
||||
using Microsoft.AspNetCore.Mvc.Internal;
|
||||
using Microsoft.AspNetCore.Routing;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
|
|
@ -241,6 +242,13 @@ namespace Microsoft.AspNetCore.Mvc
|
|||
typeof(MvcCoreMvcOptionsSetup),
|
||||
}
|
||||
},
|
||||
{
|
||||
typeof(IPostConfigureOptions<MvcOptions>),
|
||||
new Type[]
|
||||
{
|
||||
typeof(MvcOptionsConfigureCompatibilityOptions),
|
||||
}
|
||||
},
|
||||
{
|
||||
typeof(IConfigureOptions<RouteOptions>),
|
||||
new Type[]
|
||||
|
|
|
|||
|
|
@ -0,0 +1,60 @@
|
|||
// 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 Xunit;
|
||||
|
||||
namespace Microsoft.AspNetCore.Mvc.Infrastructure
|
||||
{
|
||||
public class CompatibilitySwitchTest
|
||||
{
|
||||
[Fact]
|
||||
public void Constructor_WithName_IsValueSetIsFalse()
|
||||
{
|
||||
// Arrange & Act
|
||||
var @switch = new CompatibilitySwitch<bool>("TestProperty");
|
||||
|
||||
// Assert
|
||||
Assert.False(@switch.Value);
|
||||
Assert.False(@switch.IsValueSet);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Constructor_WithNameAndInitalValue_IsValueSetIsFalse()
|
||||
{
|
||||
// Arrange & Act
|
||||
var @switch = new CompatibilitySwitch<bool>("TestProperty", initialValue: true);
|
||||
|
||||
// Assert
|
||||
Assert.True(@switch.Value);
|
||||
Assert.False(@switch.IsValueSet);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ValueNonInterface_SettingValue_SetsIsValueSetToTrue()
|
||||
{
|
||||
// Arrange
|
||||
var @switch = new CompatibilitySwitch<bool>("TestProperty");
|
||||
|
||||
// Act
|
||||
@switch.Value = false; // You don't need to actually change the value, just caling the setting works
|
||||
|
||||
// Assert
|
||||
Assert.False(@switch.Value);
|
||||
Assert.True(@switch.IsValueSet);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ValueInterface_SettingValue_SetsIsValueSetToTrue()
|
||||
{
|
||||
// Arrange
|
||||
var @switch = new CompatibilitySwitch<bool>("TestProperty");
|
||||
|
||||
// Act
|
||||
((ICompatibilitySwitch)@switch).Value = true;
|
||||
|
||||
// Assert
|
||||
Assert.True(@switch.Value);
|
||||
Assert.True(@switch.IsValueSet);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,117 @@
|
|||
// 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;
|
||||
using System.Collections.Generic;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Microsoft.Extensions.Logging.Abstractions;
|
||||
using Microsoft.Extensions.Options;
|
||||
using Xunit;
|
||||
|
||||
namespace Microsoft.AspNetCore.Mvc.Infrastructure
|
||||
{
|
||||
public class ConfigureCompatibilityOptionsTest
|
||||
{
|
||||
[Fact]
|
||||
public void PostConfigure_NoValueForProperty_DoesNothing()
|
||||
{
|
||||
// Arrange
|
||||
var configure = Create(CompatibilityVersion.Version_2_0, new Dictionary<string, object>());
|
||||
|
||||
var options = new TestOptions();
|
||||
|
||||
// Act
|
||||
configure.PostConfigure(Options.DefaultName, options);
|
||||
|
||||
// Assert
|
||||
Assert.False(options.TestProperty);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void PostConfigure_ValueIsSet_DoesNothing()
|
||||
{
|
||||
// Arrange
|
||||
var configure = Create(CompatibilityVersion.Version_2_0, new Dictionary<string, object>()
|
||||
{
|
||||
{ nameof(TestOptions.TestProperty), true },
|
||||
});
|
||||
|
||||
var options = new TestOptions()
|
||||
{
|
||||
TestProperty = false,
|
||||
};
|
||||
|
||||
// Act
|
||||
configure.PostConfigure(Options.DefaultName, options);
|
||||
|
||||
// Assert
|
||||
Assert.False(options.TestProperty);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void PostConfigure_ValueNotSet_SetsValue()
|
||||
{
|
||||
// Arrange
|
||||
var configure = Create(CompatibilityVersion.Version_2_0, new Dictionary<string, object>()
|
||||
{
|
||||
{ nameof(TestOptions.TestProperty), true },
|
||||
});
|
||||
|
||||
var options = new TestOptions();
|
||||
|
||||
// Act
|
||||
configure.PostConfigure(Options.DefaultName, options);
|
||||
|
||||
// Assert
|
||||
Assert.True(options.TestProperty);
|
||||
}
|
||||
|
||||
private static ConfigureCompatibilityOptions<TestOptions> Create(
|
||||
CompatibilityVersion version,
|
||||
IReadOnlyDictionary<string, object> defaultValues)
|
||||
{
|
||||
var compatibilityOptions = Options.Create(new MvcCompatibilityOptions() { CompatibilityVersion = version });
|
||||
return new TestConfigure(NullLoggerFactory.Instance, compatibilityOptions, defaultValues);
|
||||
}
|
||||
|
||||
private class TestOptions : IEnumerable<ICompatibilitySwitch>
|
||||
{
|
||||
private readonly CompatibilitySwitch<bool> _testProperty;
|
||||
|
||||
private readonly ICompatibilitySwitch[] _switches;
|
||||
|
||||
public TestOptions()
|
||||
{
|
||||
_testProperty = new CompatibilitySwitch<bool>(nameof(TestProperty));
|
||||
_switches = new ICompatibilitySwitch[] { _testProperty };
|
||||
}
|
||||
|
||||
public bool TestProperty
|
||||
{
|
||||
get => _testProperty.Value;
|
||||
set => _testProperty.Value = value;
|
||||
}
|
||||
|
||||
public IEnumerator<ICompatibilitySwitch> GetEnumerator()
|
||||
{
|
||||
return ((IEnumerable<ICompatibilitySwitch>)_switches).GetEnumerator();
|
||||
}
|
||||
|
||||
IEnumerator IEnumerable.GetEnumerator() => _switches.GetEnumerator();
|
||||
}
|
||||
|
||||
private class TestConfigure : ConfigureCompatibilityOptions<TestOptions>
|
||||
{
|
||||
public TestConfigure(
|
||||
ILoggerFactory loggerFactory,
|
||||
IOptions<MvcCompatibilityOptions> compatibilityOptions,
|
||||
IReadOnlyDictionary<string, object> defaultValues)
|
||||
: base(loggerFactory, compatibilityOptions)
|
||||
{
|
||||
DefaultValues = defaultValues;
|
||||
}
|
||||
|
||||
protected override IReadOnlyDictionary<string, object> DefaultValues { get; }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,83 @@
|
|||
// Copyright (c) .NET Foundation. All rights reserved.
|
||||
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
|
||||
|
||||
using Microsoft.AspNetCore.Mvc.Formatters;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.Extensions.ObjectPool;
|
||||
using Microsoft.Extensions.Options;
|
||||
using Xunit;
|
||||
|
||||
namespace Microsoft.AspNetCore.Mvc.IntegrationTest
|
||||
{
|
||||
// Integration tests for compatibility switches. These tests verify which compatibility
|
||||
// values apply to each supported version.
|
||||
//
|
||||
// If you add a new compatibility switch, make sure to update ALL of these tests. Each test
|
||||
// here should include verification for all of the switches.
|
||||
public class CompatibilitySwitchIntegrationTest
|
||||
{
|
||||
[Fact(Skip = "#7157 - some settings have the wrong values, this test should pass once #7157 is fixed")]
|
||||
public void CompatibilitySwitches_Version_2_0()
|
||||
{
|
||||
// Arrange
|
||||
var serviceCollection = new ServiceCollection();
|
||||
AddHostingServices(serviceCollection);
|
||||
serviceCollection.AddMvc().SetCompatibilityVersion(CompatibilityVersion.Version_2_0);
|
||||
|
||||
var services = serviceCollection.BuildServiceProvider();
|
||||
|
||||
// Act
|
||||
var mvcOptions = services.GetRequiredService<IOptions<MvcOptions>>().Value;
|
||||
|
||||
// Assert
|
||||
Assert.False(mvcOptions.AllowBindingUndefinedValueToEnumType);
|
||||
Assert.Equal(InputFormatterExceptionModelStatePolicy.AllExceptions, mvcOptions.InputFormatterExceptionModelStatePolicy);
|
||||
Assert.False(mvcOptions.SuppressJsonDeserializationExceptionMessagesInModelState); // This name needs to be inverted in #7157
|
||||
}
|
||||
|
||||
[Fact(Skip = "#7157 - some settings have the wrong values, this test should pass once #7157 is fixed")]
|
||||
public void CompatibilitySwitches_Version_2_1()
|
||||
{
|
||||
// Arrange
|
||||
var serviceCollection = new ServiceCollection();
|
||||
AddHostingServices(serviceCollection);
|
||||
serviceCollection.AddMvc().SetCompatibilityVersion(CompatibilityVersion.Version_2_0);
|
||||
|
||||
var services = serviceCollection.BuildServiceProvider();
|
||||
|
||||
// Act
|
||||
var mvcOptions = services.GetRequiredService<IOptions<MvcOptions>>().Value;
|
||||
|
||||
// Assert
|
||||
Assert.True(mvcOptions.AllowBindingUndefinedValueToEnumType);
|
||||
Assert.Equal(InputFormatterExceptionModelStatePolicy.MalformedInputExceptions, mvcOptions.InputFormatterExceptionModelStatePolicy);
|
||||
Assert.True(mvcOptions.SuppressJsonDeserializationExceptionMessagesInModelState); // This name needs to be inverted in #7157
|
||||
}
|
||||
|
||||
[Fact(Skip = "#7157 - some settings have the wrong values, this test should pass once #7157 is fixed")]
|
||||
public void CompatibilitySwitches_Version_Latest()
|
||||
{
|
||||
// Arrange
|
||||
var serviceCollection = new ServiceCollection();
|
||||
AddHostingServices(serviceCollection);
|
||||
serviceCollection.AddMvc().SetCompatibilityVersion(CompatibilityVersion.Version_2_0);
|
||||
|
||||
var services = serviceCollection.BuildServiceProvider();
|
||||
|
||||
// Act
|
||||
var mvcOptions = services.GetRequiredService<IOptions<MvcOptions>>().Value;
|
||||
|
||||
// Assert
|
||||
Assert.True(mvcOptions.AllowBindingUndefinedValueToEnumType);
|
||||
Assert.Equal(InputFormatterExceptionModelStatePolicy.MalformedInputExceptions, mvcOptions.InputFormatterExceptionModelStatePolicy);
|
||||
Assert.True(mvcOptions.SuppressJsonDeserializationExceptionMessagesInModelState); // This name needs to be inverted in #7157
|
||||
}
|
||||
|
||||
// This just does the minimum needed to be able to resolve these options.
|
||||
private static void AddHostingServices(IServiceCollection serviceCollection)
|
||||
{
|
||||
serviceCollection.AddLogging();
|
||||
serviceCollection.AddSingleton<ObjectPoolProvider, DefaultObjectPoolProvider>();
|
||||
}
|
||||
}
|
||||
}
|
||||
Loading…
Reference in New Issue