diff --git a/src/Microsoft.AspNetCore.Mvc.Core/CompatibilityVersion.cs b/src/Microsoft.AspNetCore.Mvc.Core/CompatibilityVersion.cs new file mode 100644 index 0000000000..c49c27f454 --- /dev/null +++ b/src/Microsoft.AspNetCore.Mvc.Core/CompatibilityVersion.cs @@ -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 +{ + /// + /// Specifies the version compatibility of runtime behaviors configured by . + /// + /// + /// + /// The best way to set a compatibility version is by using + /// or + /// in your application's + /// ConfigureServices method. + /// + /// Setting the compatibility version using : + /// + /// public class Startup + /// { + /// ... + /// + /// public void ConfigureServices(IServiceCollection services) + /// { + /// services.AddMvc().SetCompatibilityVersion(CompatibilityVersion.Version_2_1); + /// } + /// + /// ... + /// } + /// + /// + /// + /// + /// 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. + /// + /// + public enum CompatibilityVersion + { + /// + /// Sets the default value of settings on to match the behavior of + /// ASP.NET Core MVC 2.0. + /// + Version_2_0, + + /// + /// Sets the default value of settings on to match the behavior of + /// ASP.NET Core MVC 2.1. + /// + Version_2_1, + + /// + /// Sets the default value of settings on to match the latest release. Use this + /// value with care, upgrading minor versions will cause breaking changes when using . + /// + Latest = int.MaxValue, + } +} diff --git a/src/Microsoft.AspNetCore.Mvc.Core/DependencyInjection/MvcCoreMvcBuilderExtensions.cs b/src/Microsoft.AspNetCore.Mvc.Core/DependencyInjection/MvcCoreMvcBuilderExtensions.cs index 7481cdcea1..6e1f9f9a1b 100644 --- a/src/Microsoft.AspNetCore.Mvc.Core/DependencyInjection/MvcCoreMvcBuilderExtensions.cs +++ b/src/Microsoft.AspNetCore.Mvc.Core/DependencyInjection/MvcCoreMvcBuilderExtensions.cs @@ -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 /// The . 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; } + + /// + /// Sets the for ASP.NET Core MVC for the application. + /// + /// The . + /// The value to configure. + /// The . + public static IMvcBuilder SetCompatibilityVersion(this IMvcBuilder builder, CompatibilityVersion version) + { + if (builder == null) + { + throw new ArgumentNullException(nameof(builder)); + } + + builder.Services.Configure(o => o.CompatibilityVersion = version); + return builder; + } } } diff --git a/src/Microsoft.AspNetCore.Mvc.Core/DependencyInjection/MvcCoreMvcCoreBuilderExtensions.cs b/src/Microsoft.AspNetCore.Mvc.Core/DependencyInjection/MvcCoreMvcCoreBuilderExtensions.cs index aabadff1ad..76ebe6c261 100644 --- a/src/Microsoft.AspNetCore.Mvc.Core/DependencyInjection/MvcCoreMvcCoreBuilderExtensions.cs +++ b/src/Microsoft.AspNetCore.Mvc.Core/DependencyInjection/MvcCoreMvcCoreBuilderExtensions.cs @@ -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; } + + /// + /// Sets the for ASP.NET Core MVC for the application. + /// + /// The . + /// The value to configure. + /// The . + public static IMvcCoreBuilder SetCompatibilityVersion(this IMvcCoreBuilder builder, CompatibilityVersion version) + { + if (builder == null) + { + throw new ArgumentNullException(nameof(builder)); + } + + builder.Services.Configure(o => o.CompatibilityVersion = version); + return builder; + } } } diff --git a/src/Microsoft.AspNetCore.Mvc.Core/DependencyInjection/MvcCoreServiceCollectionExtensions.cs b/src/Microsoft.AspNetCore.Mvc.Core/DependencyInjection/MvcCoreServiceCollectionExtensions.cs index 94c470e3a3..03ffe21330 100644 --- a/src/Microsoft.AspNetCore.Mvc.Core/DependencyInjection/MvcCoreServiceCollectionExtensions.cs +++ b/src/Microsoft.AspNetCore.Mvc.Core/DependencyInjection/MvcCoreServiceCollectionExtensions.cs @@ -147,6 +147,8 @@ namespace Microsoft.Extensions.DependencyInjection // services.TryAddEnumerable( ServiceDescriptor.Transient, MvcCoreMvcOptionsSetup>()); + services.TryAddEnumerable( + ServiceDescriptor.Transient, MvcOptionsConfigureCompatibilityOptions>()); services.TryAddEnumerable( ServiceDescriptor.Transient, ApiBehaviorOptionsSetup>()); services.TryAddEnumerable( diff --git a/src/Microsoft.AspNetCore.Mvc.Core/Infrastructure/CompatibilitySwitch.cs b/src/Microsoft.AspNetCore.Mvc.Core/Infrastructure/CompatibilitySwitch.cs new file mode 100644 index 0000000000..59f650038a --- /dev/null +++ b/src/Microsoft.AspNetCore.Mvc.Core/Infrastructure/CompatibilitySwitch.cs @@ -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 as a backing field. Make sure the + // new switch is exposed by implementing IEnumerable 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 in DI. + // + /// + /// Infrastructure supporting the implementation of . This is an + /// implementation of suitible for use with the + /// pattern. This is framework infrastructure and should not be used by application code. + /// + /// The type of value assoicated with the compatibility switch. + public class CompatibilitySwitch : ICompatibilitySwitch where TValue : struct + { + private TValue _value; + + /// + /// Creates a new compatiblity switch with the provided name. + /// + /// + /// The compatiblity switch name. The name must match a property name on an options type. + /// + public CompatibilitySwitch(string name) + : this(name, default) + { + } + + /// + /// Creates a new compatiblity switch with the provided name and initial value. + /// + /// + /// The compatiblity switch name. The name must match a property name on an options type. + /// + /// + /// The initial value to assign to the switch. + /// + public CompatibilitySwitch(string name, TValue initialValue) + { + if (name == null) + { + throw new ArgumentNullException(nameof(name)); + } + + Name = name; + _value = initialValue; + } + + /// + /// Gets a value indicating whether the property has been set. + /// + /// + /// This is used by the compatibility infrastructure to determine whether the application developer + /// has set explicitly set the value associated with this switch. + /// + public bool IsValueSet { get; private set; } + + /// + /// Gets the name of the compatibility switch. + /// + public string Name { get; } + + /// + /// Gets or set the value associated with the compatibility switch. + /// + /// + /// Setting the switch value using will set to true. + /// 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. + /// + 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; + } + } +} diff --git a/src/Microsoft.AspNetCore.Mvc.Core/Infrastructure/ConfigureCompatibilityOptions.cs b/src/Microsoft.AspNetCore.Mvc.Core/Infrastructure/ConfigureCompatibilityOptions.cs new file mode 100644 index 0000000000..c46a3529d0 --- /dev/null +++ b/src/Microsoft.AspNetCore.Mvc.Core/Infrastructure/ConfigureCompatibilityOptions.cs @@ -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 +{ + /// + /// A base class for infrastructure that implements ASP.NET Core MVC's support for + /// . This is framework infrastructure and should not be used + /// by application code. + /// + /// + public abstract class ConfigureCompatibilityOptions : IPostConfigureOptions + where TOptions : class, IEnumerable + { + private readonly ILogger _logger; + + /// + /// Creates a new . + /// + /// The . + /// The . + protected ConfigureCompatibilityOptions( + ILoggerFactory loggerFactory, + IOptions compatibilityOptions) + { + if (loggerFactory == null) + { + throw new ArgumentNullException(nameof(loggerFactory)); + } + + Version = compatibilityOptions.Value.CompatibilityVersion; + _logger = loggerFactory.CreateLogger(); + } + + /// + /// Gets the default values of compatibility switches associated with the applications configured + /// . + /// + protected abstract IReadOnlyDictionary DefaultValues { get; } + + /// + /// Gets the configured for the application. + /// + protected CompatibilityVersion Version { get; } + + /// + 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 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); + } + } +} diff --git a/src/Microsoft.AspNetCore.Mvc.Core/Infrastructure/ICompatibilitySwitch.cs b/src/Microsoft.AspNetCore.Mvc.Core/Infrastructure/ICompatibilitySwitch.cs new file mode 100644 index 0000000000..6f723db0e2 --- /dev/null +++ b/src/Microsoft.AspNetCore.Mvc.Core/Infrastructure/ICompatibilitySwitch.cs @@ -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 +{ + /// + /// Defines a compatibility switch. This is framework infrastructure and should not be used + /// by application code. + /// + public interface ICompatibilitySwitch + { + /// + /// Gets a value indicating whether the property has been set. + /// + /// + /// This is used by the compatibility infrastructure to determine whether the application developer + /// has set explicitly set the value associated with this switch. + /// + bool IsValueSet { get; } + + /// + /// Gets the name of the compatibility switch. + /// + string Name { get; } + + /// + /// Gets or set the value associated with the compatibility switch. + /// + /// + /// Setting the switch value using will not set to true. + /// This should be used by the compatibility infrastructure when is false + /// to apply a compatibility value based on . + /// + object Value { get; set; } + } +} diff --git a/src/Microsoft.AspNetCore.Mvc.Core/Infrastructure/MvcCompatibilityOptions.cs b/src/Microsoft.AspNetCore.Mvc.Core/Infrastructure/MvcCompatibilityOptions.cs new file mode 100644 index 0000000000..fb78662ed8 --- /dev/null +++ b/src/Microsoft.AspNetCore.Mvc.Core/Infrastructure/MvcCompatibilityOptions.cs @@ -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 +{ + /// + /// An options type for configuring the application . + /// + /// + /// The primary way to configure the application's is by + /// calling + /// or . + /// + public class MvcCompatibilityOptions + { + /// + /// Gets or sets the application's configured . + /// + public CompatibilityVersion CompatibilityVersion { get; set; } = CompatibilityVersion.Version_2_0; + } +} diff --git a/src/Microsoft.AspNetCore.Mvc.Core/Infrastructure/MvcOptionsConfigureCompatibilityOptions.cs b/src/Microsoft.AspNetCore.Mvc.Core/Infrastructure/MvcOptionsConfigureCompatibilityOptions.cs new file mode 100644 index 0000000000..e722e91935 --- /dev/null +++ b/src/Microsoft.AspNetCore.Mvc.Core/Infrastructure/MvcOptionsConfigureCompatibilityOptions.cs @@ -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 + { + public MvcOptionsConfigureCompatibilityOptions( + ILoggerFactory loggerFactory, + IOptions compatibilityOptions) + : base(loggerFactory, compatibilityOptions) + { + } + + protected override IReadOnlyDictionary DefaultValues + { + get + { + var values = new Dictionary(); + + if (Version >= CompatibilityVersion.Version_2_1) + { + values[nameof(MvcOptions.InputFormatterExceptionModelStatePolicy)] = InputFormatterExceptionModelStatePolicy.MalformedInputExceptions; + } + + return values; + } + } + } +} diff --git a/src/Microsoft.AspNetCore.Mvc.Core/MvcOptions.cs b/src/Microsoft.AspNetCore.Mvc.Core/MvcOptions.cs index 21c3f81627..b6dab32f73 100644 --- a/src/Microsoft.AspNetCore.Mvc.Core/MvcOptions.cs +++ b/src/Microsoft.AspNetCore.Mvc.Core/MvcOptions.cs @@ -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 /// /// Provides programmatic configuration for the MVC framework. /// - public class MvcOptions + public class MvcOptions : IEnumerable { private int _maxModelStateErrors = ModelStateDictionary.DefaultMaxAllowedErrors; + // See CompatibilitySwitch.cs for guide on how to implement these. + private readonly CompatibilitySwitch _allowBindingUndefinedValueToEnumType; + private readonly CompatibilitySwitch _inputFormatterExceptionModelStatePolicy; + private readonly CompatibilitySwitch _suppressJsonDeserializationExceptionMessagesInModelState; + private readonly ICompatibilitySwitch[] _switches; + public MvcOptions() { CacheProfiles = new Dictionary(StringComparer.OrdinalIgnoreCase); @@ -32,6 +40,16 @@ namespace Microsoft.AspNetCore.Mvc ModelMetadataDetailsProviders = new List(); ModelValidatorProviders = new List(); ValueProviderFactories = new List(); + + _allowBindingUndefinedValueToEnumType = new CompatibilitySwitch(nameof(AllowBindingUndefinedValueToEnumType)); + _inputFormatterExceptionModelStatePolicy = new CompatibilitySwitch(nameof(InputFormatterExceptionModelStatePolicy), InputFormatterExceptionModelStatePolicy.AllExceptions); + _suppressJsonDeserializationExceptionMessagesInModelState = new CompatibilitySwitch(nameof(SuppressJsonDeserializationExceptionMessagesInModelState)); + _switches = new ICompatibilitySwitch[] + { + _allowBindingUndefinedValueToEnumType, + _inputFormatterExceptionModelStatePolicy, + _suppressJsonDeserializationExceptionMessagesInModelState, + }; } /// @@ -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. /// by default. /// - public bool AllowBindingUndefinedValueToEnumType { get; set; } + public bool AllowBindingUndefinedValueToEnumType + { + get => _allowBindingUndefinedValueToEnumType.Value; + set => _allowBindingUndefinedValueToEnumType.Value = value; + } /// /// 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 s. /// Default is . /// - public InputFormatterExceptionModelStatePolicy InputFormatterExceptionModelStatePolicy { get; set; } + public InputFormatterExceptionModelStatePolicy InputFormatterExceptionModelStatePolicy + { + get => _inputFormatterExceptionModelStatePolicy.Value; + set => _inputFormatterExceptionModelStatePolicy.Value = value; + } /// /// Gets or sets a flag to determine whether, if an action receives invalid JSON in @@ -184,6 +210,17 @@ namespace Microsoft.AspNetCore.Mvc /// by default, meaning that clients may receive details about /// why the JSON they posted is considered invalid. /// - public bool SuppressJsonDeserializationExceptionMessagesInModelState { get; set; } = false; + public bool SuppressJsonDeserializationExceptionMessagesInModelState + { + get => _suppressJsonDeserializationExceptionMessagesInModelState.Value; + set => _suppressJsonDeserializationExceptionMessagesInModelState.Value = value; + } + + IEnumerator IEnumerable.GetEnumerator() + { + return ((IEnumerable)_switches).GetEnumerator(); + } + + IEnumerator IEnumerable.GetEnumerator() => _switches.GetEnumerator(); } } diff --git a/test/Microsoft.AspNetCore.Mvc.Core.Test/DependencyInjection/MvcCoreServiceCollectionExtensionsTest.cs b/test/Microsoft.AspNetCore.Mvc.Core.Test/DependencyInjection/MvcCoreServiceCollectionExtensionsTest.cs index 59ddee1789..237eb47046 100644 --- a/test/Microsoft.AspNetCore.Mvc.Core.Test/DependencyInjection/MvcCoreServiceCollectionExtensionsTest.cs +++ b/test/Microsoft.AspNetCore.Mvc.Core.Test/DependencyInjection/MvcCoreServiceCollectionExtensionsTest.cs @@ -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), + new Type[] + { + typeof(MvcOptionsConfigureCompatibilityOptions), + } + }, { typeof(IConfigureOptions), new Type[] diff --git a/test/Microsoft.AspNetCore.Mvc.Core.Test/Infrastructure/CompatibilitySwitchTest.cs b/test/Microsoft.AspNetCore.Mvc.Core.Test/Infrastructure/CompatibilitySwitchTest.cs new file mode 100644 index 0000000000..119113f668 --- /dev/null +++ b/test/Microsoft.AspNetCore.Mvc.Core.Test/Infrastructure/CompatibilitySwitchTest.cs @@ -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("TestProperty"); + + // Assert + Assert.False(@switch.Value); + Assert.False(@switch.IsValueSet); + } + + [Fact] + public void Constructor_WithNameAndInitalValue_IsValueSetIsFalse() + { + // Arrange & Act + var @switch = new CompatibilitySwitch("TestProperty", initialValue: true); + + // Assert + Assert.True(@switch.Value); + Assert.False(@switch.IsValueSet); + } + + [Fact] + public void ValueNonInterface_SettingValue_SetsIsValueSetToTrue() + { + // Arrange + var @switch = new CompatibilitySwitch("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("TestProperty"); + + // Act + ((ICompatibilitySwitch)@switch).Value = true; + + // Assert + Assert.True(@switch.Value); + Assert.True(@switch.IsValueSet); + } + } +} diff --git a/test/Microsoft.AspNetCore.Mvc.Core.Test/Infrastructure/ConfigureCompatibilityOptionsTest.cs b/test/Microsoft.AspNetCore.Mvc.Core.Test/Infrastructure/ConfigureCompatibilityOptionsTest.cs new file mode 100644 index 0000000000..a666303be8 --- /dev/null +++ b/test/Microsoft.AspNetCore.Mvc.Core.Test/Infrastructure/ConfigureCompatibilityOptionsTest.cs @@ -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()); + + 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() + { + { 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() + { + { nameof(TestOptions.TestProperty), true }, + }); + + var options = new TestOptions(); + + // Act + configure.PostConfigure(Options.DefaultName, options); + + // Assert + Assert.True(options.TestProperty); + } + + private static ConfigureCompatibilityOptions Create( + CompatibilityVersion version, + IReadOnlyDictionary defaultValues) + { + var compatibilityOptions = Options.Create(new MvcCompatibilityOptions() { CompatibilityVersion = version }); + return new TestConfigure(NullLoggerFactory.Instance, compatibilityOptions, defaultValues); + } + + private class TestOptions : IEnumerable + { + private readonly CompatibilitySwitch _testProperty; + + private readonly ICompatibilitySwitch[] _switches; + + public TestOptions() + { + _testProperty = new CompatibilitySwitch(nameof(TestProperty)); + _switches = new ICompatibilitySwitch[] { _testProperty }; + } + + public bool TestProperty + { + get => _testProperty.Value; + set => _testProperty.Value = value; + } + + public IEnumerator GetEnumerator() + { + return ((IEnumerable)_switches).GetEnumerator(); + } + + IEnumerator IEnumerable.GetEnumerator() => _switches.GetEnumerator(); + } + + private class TestConfigure : ConfigureCompatibilityOptions + { + public TestConfigure( + ILoggerFactory loggerFactory, + IOptions compatibilityOptions, + IReadOnlyDictionary defaultValues) + : base(loggerFactory, compatibilityOptions) + { + DefaultValues = defaultValues; + } + + protected override IReadOnlyDictionary DefaultValues { get; } + } + } +} diff --git a/test/Microsoft.AspNetCore.Mvc.Test/IntegrationTest/CompatibilitySwitchIntegrationTest.cs b/test/Microsoft.AspNetCore.Mvc.Test/IntegrationTest/CompatibilitySwitchIntegrationTest.cs new file mode 100644 index 0000000000..102fd5a316 --- /dev/null +++ b/test/Microsoft.AspNetCore.Mvc.Test/IntegrationTest/CompatibilitySwitchIntegrationTest.cs @@ -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>().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>().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>().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(); + } + } +}