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:
Ryan Nowak 2017-12-28 09:43:24 -08:00 committed by GitHub
parent 6981c29394
commit 747420e5aa
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
14 changed files with 739 additions and 4 deletions

View File

@ -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,
}
}

View File

@ -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;
}
}
}

View File

@ -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;
}
}
}

View File

@ -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(

View File

@ -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;
}
}
}

View File

@ -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);
}
}
}

View File

@ -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; }
}
}

View File

@ -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;
}
}

View File

@ -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;
}
}
}
}

View File

@ -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();
}
}

View File

@ -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[]

View File

@ -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);
}
}
}

View File

@ -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; }
}
}
}

View File

@ -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>();
}
}
}