From ccb67931268f9396652a22044e3563a98b2a5c8f Mon Sep 17 00:00:00 2001 From: Pranav K Date: Wed, 21 Mar 2018 12:28:45 -0700 Subject: [PATCH] Introduce LifecycleProperty Update TempDataAttribute infrastructure to use LifecycleProperty --- .../MvcOptions.cs | 2 +- .../PageSaveTempDataPropertyFilter.cs | 27 +- .../PageSaveTempDataPropertyFilterFactory.cs | 28 +-- ...pDataFilterPageApplicationModelProvider.cs | 24 +- .../Properties/AssemblyInfo.cs | 1 + ...MvcViewFeaturesMvcCoreBuilderExtensions.cs | 2 + .../ControllerSaveTempDataPropertyFilter.cs | 10 +- ...rollerSaveTempDataPropertyFilterFactory.cs | 12 +- .../Internal/LifecycleProperty.cs | 39 +++ .../SaveTempDataPropertyFilterBase.cs | 119 ++++----- .../TempDataApplicationModelProvider.cs | 28 ++- .../Internal/TempDataProperty.cs | 37 --- .../MvcViewOptions.cs | 63 ++++- .../Properties/AssemblyInfo.cs | 1 + .../TempDataAttribute.cs | 8 +- ...iewOptionsConfigureCompatibilityOptions.cs | 35 +++ ...geSaveTempDataPropertyFilterFactoryTest.cs | 18 +- .../PageSaveTempDataPropertyFilterTest.cs | 237 +++++++----------- ...aFilterPageApplicationModelProviderTest.cs | 153 +++++++++-- ...erSaveTempDataPropertyFilterFactoryTest.cs | 17 +- ...ontrollerSaveTempDataPropertyFilterTest.cs | 119 ++++++++- .../Internal/LifecyclePropertyTest.cs | 94 +++++++ .../TempDataApplicationModelProviderTest.cs | 172 +++++-------- 23 files changed, 789 insertions(+), 457 deletions(-) create mode 100644 src/Microsoft.AspNetCore.Mvc.ViewFeatures/Internal/LifecycleProperty.cs delete mode 100644 src/Microsoft.AspNetCore.Mvc.ViewFeatures/Internal/TempDataProperty.cs create mode 100644 src/Microsoft.AspNetCore.Mvc.ViewFeatures/ViewFeatures/MvcViewOptionsConfigureCompatibilityOptions.cs create mode 100644 test/Microsoft.AspNetCore.Mvc.ViewFeatures.Test/Internal/LifecyclePropertyTest.cs diff --git a/src/Microsoft.AspNetCore.Mvc.Core/MvcOptions.cs b/src/Microsoft.AspNetCore.Mvc.Core/MvcOptions.cs index e7e55a8bbb..bad17f6849 100644 --- a/src/Microsoft.AspNetCore.Mvc.Core/MvcOptions.cs +++ b/src/Microsoft.AspNetCore.Mvc.Core/MvcOptions.cs @@ -205,7 +205,7 @@ namespace Microsoft.AspNetCore.Mvc /// /// Gets or sets an value indicating whether the model binding system will bind undefined values to - /// enum types. The default value of the property is false. + /// enum types. The default value of the property is false. /// /// /// diff --git a/src/Microsoft.AspNetCore.Mvc.RazorPages/Internal/PageSaveTempDataPropertyFilter.cs b/src/Microsoft.AspNetCore.Mvc.RazorPages/Internal/PageSaveTempDataPropertyFilter.cs index a7b45146df..73633bf59c 100644 --- a/src/Microsoft.AspNetCore.Mvc.RazorPages/Internal/PageSaveTempDataPropertyFilter.cs +++ b/src/Microsoft.AspNetCore.Mvc.RazorPages/Internal/PageSaveTempDataPropertyFilter.cs @@ -1,48 +1,29 @@ // Copyright (c) .NET Foundation. All rights reserved. // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. -using System; using Microsoft.AspNetCore.Mvc.Filters; using Microsoft.AspNetCore.Mvc.ViewFeatures; using Microsoft.AspNetCore.Mvc.ViewFeatures.Internal; -namespace Microsoft.AspNetCore.Mvc.RazorPages.Internal +namespace Microsoft.AspNetCore.Mvc.RazorPages { - public class PageSaveTempDataPropertyFilter : SaveTempDataPropertyFilterBase, IPageFilter + internal class PageSaveTempDataPropertyFilter : SaveTempDataPropertyFilterBase, IPageFilter { public PageSaveTempDataPropertyFilter(ITempDataDictionaryFactory factory) : base(factory) { } - public PageSaveTempDataPropertyFilterFactory FilterFactory { get; set; } - public void OnPageHandlerSelected(PageHandlerSelectedContext context) { } public void OnPageHandlerExecuting(PageHandlerExecutingContext context) { - if (context.HandlerInstance == null) - { - throw new InvalidOperationException(Resources.FormatPropertyOfTypeCannotBeNull( - nameof(PageHandlerExecutingContext.HandlerInstance), - typeof(PageHandlerExecutingContext).Name)); - } - - if (FilterFactory == null) - { - throw new InvalidOperationException(Resources.FormatPropertyOfTypeCannotBeNull( - nameof(FilterFactory), - typeof(PageSaveTempDataPropertyFilter).Name)); - } - - var tempData = _factory.GetTempData(context.HttpContext); - Subject = context.HandlerInstance; - Properties = FilterFactory.GetTempDataProperties(Subject.GetType()); + var tempData = _tempDataFactory.GetTempData(context.HttpContext); - SetPropertyVaules(tempData, Subject); + SetPropertyVaules(tempData); } public void OnPageHandlerExecuted(PageHandlerExecutedContext context) diff --git a/src/Microsoft.AspNetCore.Mvc.RazorPages/Internal/PageSaveTempDataPropertyFilterFactory.cs b/src/Microsoft.AspNetCore.Mvc.RazorPages/Internal/PageSaveTempDataPropertyFilterFactory.cs index 66618a722a..e33e9ff537 100644 --- a/src/Microsoft.AspNetCore.Mvc.RazorPages/Internal/PageSaveTempDataPropertyFilterFactory.cs +++ b/src/Microsoft.AspNetCore.Mvc.RazorPages/Internal/PageSaveTempDataPropertyFilterFactory.cs @@ -4,14 +4,21 @@ using System; using System.Collections.Generic; using Microsoft.AspNetCore.Mvc.Filters; +using Microsoft.AspNetCore.Mvc.RazorPages.Internal; using Microsoft.AspNetCore.Mvc.ViewFeatures.Internal; using Microsoft.Extensions.DependencyInjection; -namespace Microsoft.AspNetCore.Mvc.RazorPages.Internal +namespace Microsoft.AspNetCore.Mvc.RazorPages { - public class PageSaveTempDataPropertyFilterFactory : IFilterFactory + internal class PageSaveTempDataPropertyFilterFactory : IFilterFactory { - public IList Properties { get; set; } + + public PageSaveTempDataPropertyFilterFactory(IReadOnlyList properties) + { + Properties = properties; + } + + public IReadOnlyList Properties { get; } public bool IsReusable => false; @@ -23,22 +30,9 @@ namespace Microsoft.AspNetCore.Mvc.RazorPages.Internal } var service = serviceProvider.GetRequiredService(); - service.FilterFactory = this; + service.Properties = Properties; return service; } - - public IList GetTempDataProperties(Type modelType) - { - // TempDataProperties are stored here as a cache for the filter. But in pages by the time we know the type - // of our model we no longer have access to the factory, so we store the factory on the filter so it can - // call this method to populate its TempDataProperties. - if (Properties == null) - { - Properties = SaveTempDataPropertyFilterBase.GetTempDataProperties(modelType); - } - - return Properties; - } } } \ No newline at end of file diff --git a/src/Microsoft.AspNetCore.Mvc.RazorPages/Internal/TempDataFilterPageApplicationModelProvider.cs b/src/Microsoft.AspNetCore.Mvc.RazorPages/Internal/TempDataFilterPageApplicationModelProvider.cs index 7cfaaedfbd..411c13c608 100644 --- a/src/Microsoft.AspNetCore.Mvc.RazorPages/Internal/TempDataFilterPageApplicationModelProvider.cs +++ b/src/Microsoft.AspNetCore.Mvc.RazorPages/Internal/TempDataFilterPageApplicationModelProvider.cs @@ -3,11 +3,20 @@ using System; using Microsoft.AspNetCore.Mvc.ApplicationModels; +using Microsoft.AspNetCore.Mvc.ViewFeatures.Internal; +using Microsoft.Extensions.Options; -namespace Microsoft.AspNetCore.Mvc.RazorPages.Internal +namespace Microsoft.AspNetCore.Mvc.RazorPages { - public class TempDataFilterPageApplicationModelProvider : IPageApplicationModelProvider + internal class TempDataFilterPageApplicationModelProvider : IPageApplicationModelProvider { + private readonly MvcViewOptions _options; + + public TempDataFilterPageApplicationModelProvider(IOptions options) + { + _options = options.Value; + } + // The order is set to execute after the DefaultPageApplicationModelProvider. public int Order => -1000 + 10; @@ -23,9 +32,16 @@ namespace Microsoft.AspNetCore.Mvc.RazorPages.Internal } var pageApplicationModel = context.PageApplicationModel; + var handlerType = pageApplicationModel.HandlerType.AsType(); - // Support for [TempData] on properties - pageApplicationModel.Filters.Add(new PageSaveTempDataPropertyFilterFactory()); + var tempDataProperties = SaveTempDataPropertyFilterBase.GetTempDataProperties(handlerType, _options); + if (tempDataProperties == null) + { + return; + } + + var filter = new PageSaveTempDataPropertyFilterFactory(tempDataProperties); + pageApplicationModel.Filters.Add(filter); } } } diff --git a/src/Microsoft.AspNetCore.Mvc.RazorPages/Properties/AssemblyInfo.cs b/src/Microsoft.AspNetCore.Mvc.RazorPages/Properties/AssemblyInfo.cs index a5c333c08d..458502d802 100644 --- a/src/Microsoft.AspNetCore.Mvc.RazorPages/Properties/AssemblyInfo.cs +++ b/src/Microsoft.AspNetCore.Mvc.RazorPages/Properties/AssemblyInfo.cs @@ -3,4 +3,5 @@ using System.Runtime.CompilerServices; +[assembly: InternalsVisibleTo("Microsoft.AspNetCore.Mvc.Test, PublicKey=0024000004800000940000000602000000240000525341310004000001000100f33a29044fa9d740c9b3213a93e57c84b472c84e0b8a0e1ae48e67a9f8f6de9d5f7f3d52ac23e48ac51801f1dc950abe901da34d2a9e3baadb141a17c77ef3c565dd5ee5054b91cf63bb3c6ab83f72ab3aafe93d0fc3c2348b764fafb0b1c0733de51459aeab46580384bf9d74c4e28164b7cde247f891ba07891c9d872ad2bb")] [assembly: InternalsVisibleTo("Microsoft.AspNetCore.Mvc.RazorPages.Test, PublicKey=0024000004800000940000000602000000240000525341310004000001000100f33a29044fa9d740c9b3213a93e57c84b472c84e0b8a0e1ae48e67a9f8f6de9d5f7f3d52ac23e48ac51801f1dc950abe901da34d2a9e3baadb141a17c77ef3c565dd5ee5054b91cf63bb3c6ab83f72ab3aafe93d0fc3c2348b764fafb0b1c0733de51459aeab46580384bf9d74c4e28164b7cde247f891ba07891c9d872ad2bb")] diff --git a/src/Microsoft.AspNetCore.Mvc.ViewFeatures/DependencyInjection/MvcViewFeaturesMvcCoreBuilderExtensions.cs b/src/Microsoft.AspNetCore.Mvc.ViewFeatures/DependencyInjection/MvcViewFeaturesMvcCoreBuilderExtensions.cs index 7e7ce79fb2..8cbcd02974 100644 --- a/src/Microsoft.AspNetCore.Mvc.ViewFeatures/DependencyInjection/MvcViewFeaturesMvcCoreBuilderExtensions.cs +++ b/src/Microsoft.AspNetCore.Mvc.ViewFeatures/DependencyInjection/MvcViewFeaturesMvcCoreBuilderExtensions.cs @@ -142,6 +142,8 @@ namespace Microsoft.Extensions.DependencyInjection services.TryAddEnumerable( ServiceDescriptor.Transient, MvcViewOptionsSetup>()); + services.TryAddEnumerable( + ServiceDescriptor.Transient, MvcViewOptionsConfigureCompatibilityOptions>()); services.TryAddEnumerable( ServiceDescriptor.Transient, TempDataMvcOptionsSetup>()); diff --git a/src/Microsoft.AspNetCore.Mvc.ViewFeatures/Internal/ControllerSaveTempDataPropertyFilter.cs b/src/Microsoft.AspNetCore.Mvc.ViewFeatures/Internal/ControllerSaveTempDataPropertyFilter.cs index ff35fac0f4..96228d5b7b 100644 --- a/src/Microsoft.AspNetCore.Mvc.ViewFeatures/Internal/ControllerSaveTempDataPropertyFilter.cs +++ b/src/Microsoft.AspNetCore.Mvc.ViewFeatures/Internal/ControllerSaveTempDataPropertyFilter.cs @@ -2,10 +2,11 @@ // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. using Microsoft.AspNetCore.Mvc.Filters; +using Microsoft.AspNetCore.Mvc.ViewFeatures.Internal; -namespace Microsoft.AspNetCore.Mvc.ViewFeatures.Internal +namespace Microsoft.AspNetCore.Mvc.ViewFeatures { - public class ControllerSaveTempDataPropertyFilter : SaveTempDataPropertyFilterBase, IActionFilter + internal class ControllerSaveTempDataPropertyFilter : SaveTempDataPropertyFilterBase, IActionFilter { public ControllerSaveTempDataPropertyFilter(ITempDataDictionaryFactory factory) : base(factory) @@ -20,10 +21,9 @@ namespace Microsoft.AspNetCore.Mvc.ViewFeatures.Internal public void OnActionExecuting(ActionExecutingContext context) { Subject = context.Controller; - var tempData = _factory.GetTempData(context.HttpContext); + var tempData = _tempDataFactory.GetTempData(context.HttpContext); - SetPropertyVaules(tempData, Subject); + SetPropertyVaules(tempData); } } } - diff --git a/src/Microsoft.AspNetCore.Mvc.ViewFeatures/Internal/ControllerSaveTempDataPropertyFilterFactory.cs b/src/Microsoft.AspNetCore.Mvc.ViewFeatures/Internal/ControllerSaveTempDataPropertyFilterFactory.cs index 98e1272b21..22b7754f72 100644 --- a/src/Microsoft.AspNetCore.Mvc.ViewFeatures/Internal/ControllerSaveTempDataPropertyFilterFactory.cs +++ b/src/Microsoft.AspNetCore.Mvc.ViewFeatures/Internal/ControllerSaveTempDataPropertyFilterFactory.cs @@ -4,13 +4,19 @@ using System; using System.Collections.Generic; using Microsoft.AspNetCore.Mvc.Filters; +using Microsoft.AspNetCore.Mvc.ViewFeatures.Internal; using Microsoft.Extensions.DependencyInjection; -namespace Microsoft.AspNetCore.Mvc.ViewFeatures.Internal +namespace Microsoft.AspNetCore.Mvc.ViewFeatures { - public class ControllerSaveTempDataPropertyFilterFactory : IFilterFactory + internal class ControllerSaveTempDataPropertyFilterFactory : IFilterFactory { - public IList TempDataProperties { get; set; } + public ControllerSaveTempDataPropertyFilterFactory(IReadOnlyList properties) + { + TempDataProperties = properties; + } + + public IReadOnlyList TempDataProperties { get; } public bool IsReusable => false; diff --git a/src/Microsoft.AspNetCore.Mvc.ViewFeatures/Internal/LifecycleProperty.cs b/src/Microsoft.AspNetCore.Mvc.ViewFeatures/Internal/LifecycleProperty.cs new file mode 100644 index 0000000000..5eaf6e1491 --- /dev/null +++ b/src/Microsoft.AspNetCore.Mvc.ViewFeatures/Internal/LifecycleProperty.cs @@ -0,0 +1,39 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using System.Diagnostics; +using System.Reflection; +using Microsoft.Extensions.Internal; + +namespace Microsoft.AspNetCore.Mvc.ViewFeatures.Internal +{ + [DebuggerDisplay("{PropertyInfo, nq}")] + public readonly struct LifecycleProperty + { + private readonly PropertyHelper _propertyHelper; + private readonly bool _isReferenceTypeOrNullable; + + public LifecycleProperty(PropertyInfo propertyInfo, string key) + { + Key = key; + _propertyHelper = new PropertyHelper(propertyInfo); + var propertyType = propertyInfo.PropertyType; + _isReferenceTypeOrNullable = !propertyType.IsValueType || Nullable.GetUnderlyingType(propertyType) != null; + } + + public string Key { get; } + + public PropertyInfo PropertyInfo => _propertyHelper.Property; + + public object GetValue(object instance) => _propertyHelper.GetValue(instance); + + public void SetValue(object instance, object value) + { + if (value != null || _isReferenceTypeOrNullable) + { + _propertyHelper.SetValue(instance, value); + } + } + } +} diff --git a/src/Microsoft.AspNetCore.Mvc.ViewFeatures/Internal/SaveTempDataPropertyFilterBase.cs b/src/Microsoft.AspNetCore.Mvc.ViewFeatures/Internal/SaveTempDataPropertyFilterBase.cs index 85643a33e8..c560584798 100644 --- a/src/Microsoft.AspNetCore.Mvc.ViewFeatures/Internal/SaveTempDataPropertyFilterBase.cs +++ b/src/Microsoft.AspNetCore.Mvc.ViewFeatures/Internal/SaveTempDataPropertyFilterBase.cs @@ -3,6 +3,7 @@ using System; using System.Collections.Generic; +using System.Diagnostics; using System.Reflection; using Microsoft.Extensions.Internal; @@ -10,14 +11,17 @@ namespace Microsoft.AspNetCore.Mvc.ViewFeatures.Internal { public abstract class SaveTempDataPropertyFilterBase : ISaveTempDataCallback { - protected const string Prefix = "TempDataProperty-"; + protected readonly ITempDataDictionaryFactory _tempDataFactory; - protected readonly ITempDataDictionaryFactory _factory; + public SaveTempDataPropertyFilterBase(ITempDataDictionaryFactory tempDataFactory) + { + _tempDataFactory = tempDataFactory; + } /// /// Describes the temp data properties which exist on /// - public IList Properties { get; set; } + public IReadOnlyList Properties { get; set; } /// /// The which has the temp data properties. @@ -29,64 +33,90 @@ namespace Microsoft.AspNetCore.Mvc.ViewFeatures.Internal /// public IDictionary OriginalValues { get; } = new Dictionary(); - public SaveTempDataPropertyFilterBase(ITempDataDictionaryFactory factory) - { - _factory = factory; - } - /// /// Puts the modified values of into . /// /// The to be updated. public void OnTempDataSaving(ITempDataDictionary tempData) { - if (Subject != null && Properties != null) + if (Subject == null) { - for (var i = 0; i < Properties.Count; i++) - { - var property = Properties[i]; - OriginalValues.TryGetValue(property.PropertyInfo, out var originalValue); + return; + } - var newValue = property.GetValue(Subject); - if (newValue != null && !newValue.Equals(originalValue)) - { - tempData[property.TempDataKey] = newValue; - } + for (var i = 0; i < Properties.Count; i++) + { + var property = Properties[i]; + OriginalValues.TryGetValue(property.PropertyInfo, out var originalValue); + + var newValue = property.GetValue(Subject); + if (newValue != null && !newValue.Equals(originalValue)) + { + tempData[property.Key] = newValue; } } } - public static IList GetTempDataProperties(Type type) + /// + /// Sets the values of the properties of from . + /// + /// The . + protected void SetPropertyVaules(ITempDataDictionary tempData) { - List results = null; + if (Properties == null) + { + return; + } + + Debug.Assert(Subject != null, "Subject must be set before this method is invoked."); + + for (var i = 0; i < Properties.Count; i++) + { + var property = Properties[i]; + var value = tempData[property.Key]; + + OriginalValues[property.PropertyInfo] = value; + property.SetValue(Subject, value); + } + } + + public static IReadOnlyList GetTempDataProperties(Type type, MvcViewOptions viewOptions) + { + List results = null; var propertyHelpers = PropertyHelper.GetVisibleProperties(type: type); + var prefix = viewOptions.SuppressTempDataAttributePrefix ? + string.Empty : + "TempDataProperty-"; for (var i = 0; i < propertyHelpers.Length; i++) { var propertyHelper = propertyHelpers[i]; - if (propertyHelper.Property.IsDefined(typeof(TempDataAttribute))) + var property = propertyHelper.Property; + var tempDataAttribute = property.GetCustomAttribute(); + if (tempDataAttribute != null) { - ValidateProperty(propertyHelper); + ValidateProperty(propertyHelper.Property); if (results == null) { - results = new List(); + results = new List(); } - results.Add(new TempDataProperty( - Prefix + propertyHelper.Name, - propertyHelper.Property, - propertyHelper.GetValue, - propertyHelper.SetValue)); + var key = tempDataAttribute.Key; + if (string.IsNullOrEmpty(key)) + { + key = prefix + property.Name; + } + + results.Add(new LifecycleProperty(property, key)); } } return results; } - private static void ValidateProperty(PropertyHelper propertyHelper) + private static void ValidateProperty(PropertyInfo property) { - var property = propertyHelper.Property; if (!(property.SetMethod != null && property.SetMethod.IsPublic && property.GetMethod != null && @@ -108,34 +138,5 @@ namespace Microsoft.AspNetCore.Mvc.ViewFeatures.Internal throw new InvalidOperationException($"{messageWithPropertyInfo} {errorMessage}"); } } - - /// - /// Sets the values of the properties of from . - /// - /// The with the data to set on . - /// The which will have it's properties set. - protected void SetPropertyVaules(ITempDataDictionary tempData, object subject) - { - if (Properties == null) - { - return; - } - - for (var i = 0; i < Properties.Count; i++) - { - var property = Properties[i]; - var value = tempData[Prefix + property.PropertyInfo.Name]; - - OriginalValues[property.PropertyInfo] = value; - - var propertyTypeInfo = property.PropertyInfo.PropertyType.GetTypeInfo(); - - var isReferenceTypeOrNullable = !propertyTypeInfo.IsValueType || Nullable.GetUnderlyingType(property.GetType()) != null; - if (value != null || isReferenceTypeOrNullable) - { - property.SetValue(subject, value); - } - } - } } } diff --git a/src/Microsoft.AspNetCore.Mvc.ViewFeatures/Internal/TempDataApplicationModelProvider.cs b/src/Microsoft.AspNetCore.Mvc.ViewFeatures/Internal/TempDataApplicationModelProvider.cs index a1c4e4ec99..7a111b4919 100644 --- a/src/Microsoft.AspNetCore.Mvc.ViewFeatures/Internal/TempDataApplicationModelProvider.cs +++ b/src/Microsoft.AspNetCore.Mvc.ViewFeatures/Internal/TempDataApplicationModelProvider.cs @@ -4,11 +4,20 @@ using System; using Microsoft.AspNetCore.Mvc.ApplicationModels; using Microsoft.AspNetCore.Mvc.Internal; +using Microsoft.AspNetCore.Mvc.ViewFeatures.Internal; +using Microsoft.Extensions.Options; -namespace Microsoft.AspNetCore.Mvc.ViewFeatures.Internal +namespace Microsoft.AspNetCore.Mvc.ViewFeatures { - public class TempDataApplicationModelProvider : IApplicationModelProvider + internal class TempDataApplicationModelProvider : IApplicationModelProvider { + private readonly MvcViewOptions _options; + + public TempDataApplicationModelProvider(IOptions options) + { + _options = options.Value; + } + /// /// This order ensures that runs after the . public int Order => -1000 + 10; @@ -28,19 +37,16 @@ namespace Microsoft.AspNetCore.Mvc.ViewFeatures.Internal foreach (var controllerModel in context.Result.Controllers) { - var modelType = controllerModel.ControllerType.AsType(); - var tempDataProperties = SaveTempDataPropertyFilterBase.GetTempDataProperties(modelType); - if (tempDataProperties != null) + var tempDataProperties = SaveTempDataPropertyFilterBase.GetTempDataProperties(modelType, _options); + if (tempDataProperties == null) { - var factory = new ControllerSaveTempDataPropertyFilterFactory() - { - TempDataProperties = tempDataProperties - }; - - controllerModel.Filters.Add(factory); + continue; } + + var filter = new ControllerSaveTempDataPropertyFilterFactory(tempDataProperties); + controllerModel.Filters.Add(filter); } } } diff --git a/src/Microsoft.AspNetCore.Mvc.ViewFeatures/Internal/TempDataProperty.cs b/src/Microsoft.AspNetCore.Mvc.ViewFeatures/Internal/TempDataProperty.cs deleted file mode 100644 index 85a9027b7d..0000000000 --- a/src/Microsoft.AspNetCore.Mvc.ViewFeatures/Internal/TempDataProperty.cs +++ /dev/null @@ -1,37 +0,0 @@ -// 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.Reflection; - -namespace Microsoft.AspNetCore.Mvc.ViewFeatures.Internal -{ - public struct TempDataProperty - { - private readonly Func _getter; - - private readonly Action _setter; - - public TempDataProperty(string tempDataKey, PropertyInfo propertyInfo, Func getter, Action setter) - { - TempDataKey = tempDataKey; - PropertyInfo = propertyInfo; - _getter = getter; - _setter = setter; - } - - public string TempDataKey { get; } - - public PropertyInfo PropertyInfo { get; } - - public object GetValue(object obj) - { - return _getter(obj); - } - - public void SetValue(object obj, object value) - { - _setter(obj, value); - } - } -} diff --git a/src/Microsoft.AspNetCore.Mvc.ViewFeatures/MvcViewOptions.cs b/src/Microsoft.AspNetCore.Mvc.ViewFeatures/MvcViewOptions.cs index 9805c88aeb..9d0627da96 100644 --- a/src/Microsoft.AspNetCore.Mvc.ViewFeatures/MvcViewOptions.cs +++ b/src/Microsoft.AspNetCore.Mvc.ViewFeatures/MvcViewOptions.cs @@ -2,7 +2,9 @@ // 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.Infrastructure; using Microsoft.AspNetCore.Mvc.ModelBinding.Validation; using Microsoft.AspNetCore.Mvc.ViewEngines; using Microsoft.AspNetCore.Mvc.ViewFeatures; @@ -12,10 +14,21 @@ namespace Microsoft.AspNetCore.Mvc /// /// Provides programmatic configuration for views in the MVC framework. /// - public class MvcViewOptions + public class MvcViewOptions : IEnumerable { + private readonly CompatibilitySwitch _suppressTempDataAttributePrefix; + private readonly ICompatibilitySwitch[] _switches; private HtmlHelperOptions _htmlHelperOptions = new HtmlHelperOptions(); + public MvcViewOptions() + { + _suppressTempDataAttributePrefix = new CompatibilitySwitch(nameof(SuppressTempDataAttributePrefix)); + _switches = new[] + { + _suppressTempDataAttributePrefix, + }; + } + /// /// Gets or sets programmatic configuration for the HTML helpers and . /// @@ -33,6 +46,47 @@ namespace Microsoft.AspNetCore.Mvc } } + /// + /// + /// Gets or sets a value that determines if the keys for + /// properties annotated with include the prefix TempDataProperty-. + /// + /// + /// When is not specified, the lookup key for properties annotated + /// with is derived from the property name. In releases prior to ASP.NET Core 2.1, + /// the calculated key was the property name prefixed by the value TempDataProperty-. + /// e.g. TempDataProperty-SuccessMessage. When this option is true, the calculated key for the property is + /// the property name e.g. SuccessMessage. + /// + /// + /// Defaults to false. + /// + /// + /// + /// + /// This property is associated with a compatibility switch and can provide a different behavior depending on + /// the configured compatibility version for the application. See for + /// guidance and examples of setting the application's compatibility version. + /// + /// + /// Configuring the desired value of the compatibility switch by calling this property's setter will take precedence + /// over the value implied by the application's . + /// + /// + /// If the application's compatibility version is set to then + /// this setting will have the value false unless explicitly configured. + /// + /// + /// If the application's compatibility version is set to or + /// higher then this setting will have the value true unless explicitly configured. + /// + /// + public bool SuppressTempDataAttributePrefix + { + get => _suppressTempDataAttributePrefix.Value; + set => _suppressTempDataAttributePrefix.Value = value; + } + /// /// Gets a list s used by this application. /// @@ -43,5 +97,12 @@ namespace Microsoft.AspNetCore.Mvc /// public IList ClientModelValidatorProviders { get; } = new List(); + + IEnumerator IEnumerable.GetEnumerator() + { + return ((IEnumerable)_switches).GetEnumerator(); + } + + IEnumerator IEnumerable.GetEnumerator() => _switches.GetEnumerator(); } } \ No newline at end of file diff --git a/src/Microsoft.AspNetCore.Mvc.ViewFeatures/Properties/AssemblyInfo.cs b/src/Microsoft.AspNetCore.Mvc.ViewFeatures/Properties/AssemblyInfo.cs index 9314a88a06..c49b597b20 100644 --- a/src/Microsoft.AspNetCore.Mvc.ViewFeatures/Properties/AssemblyInfo.cs +++ b/src/Microsoft.AspNetCore.Mvc.ViewFeatures/Properties/AssemblyInfo.cs @@ -3,5 +3,6 @@ using System.Runtime.CompilerServices; +[assembly: InternalsVisibleTo("Microsoft.AspNetCore.Mvc.Test, PublicKey=0024000004800000940000000602000000240000525341310004000001000100f33a29044fa9d740c9b3213a93e57c84b472c84e0b8a0e1ae48e67a9f8f6de9d5f7f3d52ac23e48ac51801f1dc950abe901da34d2a9e3baadb141a17c77ef3c565dd5ee5054b91cf63bb3c6ab83f72ab3aafe93d0fc3c2348b764fafb0b1c0733de51459aeab46580384bf9d74c4e28164b7cde247f891ba07891c9d872ad2bb")] [assembly: InternalsVisibleTo("Microsoft.AspNetCore.Mvc.ViewFeatures.Test, PublicKey=0024000004800000940000000602000000240000525341310004000001000100f33a29044fa9d740c9b3213a93e57c84b472c84e0b8a0e1ae48e67a9f8f6de9d5f7f3d52ac23e48ac51801f1dc950abe901da34d2a9e3baadb141a17c77ef3c565dd5ee5054b91cf63bb3c6ab83f72ab3aafe93d0fc3c2348b764fafb0b1c0733de51459aeab46580384bf9d74c4e28164b7cde247f891ba07891c9d872ad2bb")] [assembly: InternalsVisibleTo("DynamicProxyGenAssembly2, PublicKey=0024000004800000940000000602000000240000525341310004000001000100c547cac37abd99c8db225ef2f6c8a3602f3b3606cc9891605d02baa56104f4cfc0734aa39b93bf7852f7d9266654753cc297e7d2edfe0bac1cdcf9f717241550e0a7b191195b7667bb4f64bcb8e2121380fd1d9d46ad2d92d2d15605093924cceaf74c4861eff62abf69b9291ed0a340e113be11e6a7d3113e92484cf7045cc7")] diff --git a/src/Microsoft.AspNetCore.Mvc.ViewFeatures/TempDataAttribute.cs b/src/Microsoft.AspNetCore.Mvc.ViewFeatures/TempDataAttribute.cs index bc3eeacc49..e4e6e5c747 100644 --- a/src/Microsoft.AspNetCore.Mvc.ViewFeatures/TempDataAttribute.cs +++ b/src/Microsoft.AspNetCore.Mvc.ViewFeatures/TempDataAttribute.cs @@ -2,16 +2,22 @@ // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. using System; +using Microsoft.AspNetCore.Mvc.ViewFeatures; namespace Microsoft.AspNetCore.Mvc { /// /// Properties decorated with will have their values stored in - /// and loaded from the . + /// and loaded from the . /// is supported on properties of Controllers, Razor Pages, and Razor Page Models. /// [AttributeUsage(AttributeTargets.Property, Inherited = true, AllowMultiple = false)] public sealed class TempDataAttribute : Attribute { + /// + /// Gets or sets the key used to get or add the property from value from . + /// When unspecified, the key is derived from the property name. + /// + public string Key { get; set; } } } diff --git a/src/Microsoft.AspNetCore.Mvc.ViewFeatures/ViewFeatures/MvcViewOptionsConfigureCompatibilityOptions.cs b/src/Microsoft.AspNetCore.Mvc.ViewFeatures/ViewFeatures/MvcViewOptionsConfigureCompatibilityOptions.cs new file mode 100644 index 0000000000..81de59fe12 --- /dev/null +++ b/src/Microsoft.AspNetCore.Mvc.ViewFeatures/ViewFeatures/MvcViewOptionsConfigureCompatibilityOptions.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.Infrastructure; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Options; + +namespace Microsoft.AspNetCore.Mvc.ViewFeatures +{ + internal class MvcViewOptionsConfigureCompatibilityOptions : ConfigureCompatibilityOptions + { + public MvcViewOptionsConfigureCompatibilityOptions( + ILoggerFactory loggerFactory, + IOptions compatibilityOptions) + : base(loggerFactory, compatibilityOptions) + { + } + + protected override IReadOnlyDictionary DefaultValues + { + get + { + var values = new Dictionary(); + + if (Version >= CompatibilityVersion.Version_2_1) + { + values[nameof(MvcViewOptions.SuppressTempDataAttributePrefix)] = true; + } + + return values; + } + } + } +} diff --git a/test/Microsoft.AspNetCore.Mvc.RazorPages.Test/Internal/PageSaveTempDataPropertyFilterFactoryTest.cs b/test/Microsoft.AspNetCore.Mvc.RazorPages.Test/Internal/PageSaveTempDataPropertyFilterFactoryTest.cs index 6d116a586d..92b7455009 100644 --- a/test/Microsoft.AspNetCore.Mvc.RazorPages.Test/Internal/PageSaveTempDataPropertyFilterFactoryTest.cs +++ b/test/Microsoft.AspNetCore.Mvc.RazorPages.Test/Internal/PageSaveTempDataPropertyFilterFactoryTest.cs @@ -1,12 +1,13 @@ // Copyright (c) .NET Foundation. All rights reserved. // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. -using Microsoft.Extensions.DependencyInjection; using Microsoft.AspNetCore.Mvc.ViewFeatures; +using Microsoft.AspNetCore.Mvc.ViewFeatures.Internal; +using Microsoft.Extensions.DependencyInjection; using Moq; using Xunit; -namespace Microsoft.AspNetCore.Mvc.RazorPages.Internal +namespace Microsoft.AspNetCore.Mvc.RazorPages { public class PageSaveTempDataPropertyFilterFactoryTest { @@ -14,8 +15,9 @@ namespace Microsoft.AspNetCore.Mvc.RazorPages.Internal public void CreatesInstanceWithProperties() { // Arrange - var factory = new PageSaveTempDataPropertyFilterFactory(); - + var property = typeof(TestPageModel).GetProperty(nameof(TestPageModel.Property1)); + var lifecycleProperties = new[] { new LifecycleProperty(property, "key") }; + var factory = new PageSaveTempDataPropertyFilterFactory(lifecycleProperties); var serviceProvider = CreateServiceProvider(); // Act @@ -23,7 +25,7 @@ namespace Microsoft.AspNetCore.Mvc.RazorPages.Internal // Assert var pageFilter = Assert.IsType(filter); - Assert.Same(factory, pageFilter.FilterFactory); + Assert.Same(lifecycleProperties, pageFilter.Properties); } private ServiceProvider CreateServiceProvider() @@ -36,5 +38,11 @@ namespace Microsoft.AspNetCore.Mvc.RazorPages.Internal return serviceCollection.BuildServiceProvider(); } + + private class TestPageModel : PageModel + { + [TempData] + public string Property1 { get; set; } + } } } diff --git a/test/Microsoft.AspNetCore.Mvc.RazorPages.Test/Internal/PageSaveTempDataPropertyFilterTest.cs b/test/Microsoft.AspNetCore.Mvc.RazorPages.Test/Internal/PageSaveTempDataPropertyFilterTest.cs index a0cd75d1a5..c6a1c27f2e 100644 --- a/test/Microsoft.AspNetCore.Mvc.RazorPages.Test/Internal/PageSaveTempDataPropertyFilterTest.cs +++ b/test/Microsoft.AspNetCore.Mvc.RazorPages.Test/Internal/PageSaveTempDataPropertyFilterTest.cs @@ -3,14 +3,9 @@ using System; using System.Collections.Generic; -using System.IO; using System.Linq; -using System.Threading.Tasks; using Microsoft.AspNetCore.Http; -using Microsoft.AspNetCore.Mvc.Abstractions; using Microsoft.AspNetCore.Mvc.Filters; -using Microsoft.AspNetCore.Mvc.ModelBinding; -using Microsoft.AspNetCore.Mvc.Rendering; using Microsoft.AspNetCore.Mvc.ViewFeatures; using Microsoft.AspNetCore.Mvc.ViewFeatures.Internal; using Microsoft.AspNetCore.Routing; @@ -27,71 +22,29 @@ namespace Microsoft.AspNetCore.Mvc.RazorPages.Internal // Arrange var httpContext = new DefaultHttpContext(); - var tempData = new TempDataDictionary(httpContext, Mock.Of()); - - var page = new TestPage() + var tempData = new TempDataDictionary(httpContext, Mock.Of()) + { + ["TempDataProperty-Test"] = "Old-Value", + }; + var pageModel = new TestPageModel() { Test = "TestString", Test2 = "Test2", }; - var filter = CreatePageSaveTempDataPropertyFilter(tempData); - filter.Subject = page; - - var pageType = page.GetType(); - - var testProperty = pageType.GetProperty(nameof(TestPage.Test)); - var test2Property = pageType.GetProperty(nameof(TestPage.Test2)); - - filter.OriginalValues[testProperty] = "SomeValue"; - filter.OriginalValues[test2Property] = "Test2"; - - filter.Properties = new List - { - new TempDataProperty("TempDataProperty-Test", testProperty, testProperty.GetValue, testProperty.SetValue), - new TempDataProperty("TempDataProperty-Test2", test2Property, test2Property.GetValue, test2Property.SetValue) - }; + var filter = CreatePageSaveTempDataPropertyFilter(tempData, "TempDataProperty-"); + filter.Subject = pageModel; // Act filter.OnTempDataSaving(tempData); // Assert - Assert.Equal("TestString", page.Test); Assert.Equal("TestString", tempData["TempDataProperty-Test"]); Assert.False(tempData.ContainsKey("TestDataProperty-Test2")); } [Fact] - public void OnPageExecuting_NullFilterFactory_Throws() - { - // Arrange - var httpContext = new DefaultHttpContext(); - var tempData = new TempDataDictionary(httpContext, Mock.Of()); - tempData.Save(); - - var page = new TestPage(); - - var filter = CreatePageSaveTempDataPropertyFilter(tempData, filterFactory: false); - - var context = new PageHandlerExecutingContext( - new PageContext() - { - ActionDescriptor = new CompiledPageActionDescriptor(), - HttpContext = httpContext, - RouteData = new RouteData(), - }, - Array.Empty(), - null, - new Dictionary(), - page); - - // Act & Assert - var ex = Assert.Throws(() => filter.OnPageHandlerExecuting(context)); - Assert.Contains("FilterFactory", ex.Message); - } - - [Fact] - public void OnPageExecuting_ToPageModel_SetsPropertyValue() + public void OnPageExecuting_SetsPropertyValue() { // Arrange var httpContext = new DefaultHttpContext(); @@ -100,17 +53,12 @@ namespace Microsoft.AspNetCore.Mvc.RazorPages.Internal { { "TempDataProperty-Test", "Value" } }; - tempData.Save(); var pageModel = new TestPageModel(); - var filter = CreatePageSaveTempDataPropertyFilter(tempData); + var filter = CreatePageSaveTempDataPropertyFilter(tempData, "TempDataProperty-"); filter.Subject = pageModel; - var pageType = typeof(TestPageModel); - var testProperty = pageType.GetProperty(nameof(TestPageModel.Test)); - var test2Property = pageType.GetProperty(nameof(TestPageModel.Test2)); - var context = new PageHandlerExecutingContext( new PageContext() { @@ -131,50 +79,6 @@ namespace Microsoft.AspNetCore.Mvc.RazorPages.Internal Assert.Null(pageModel.Test2); } - [Fact] - public void OnPageExecuting_ToPage_SetsPropertyValue() - { - // Arrange - var httpContext = new DefaultHttpContext(); - - var tempData = new TempDataDictionary(httpContext, Mock.Of()) - { - { "TempDataProperty-Test", "Value" } - }; - tempData.Save(); - - var page = new TestPage() - { - ViewContext = CreateViewContext(httpContext, tempData) - }; - - var filter = CreatePageSaveTempDataPropertyFilter(tempData); - filter.Subject = page; - - var pageType = page.GetType(); - var testProperty = pageType.GetProperty(nameof(TestPage.Test)); - var test2Property = pageType.GetProperty(nameof(TestPage.Test2)); - - var context = new PageHandlerExecutingContext( - new PageContext() - { - ActionDescriptor = new CompiledPageActionDescriptor(), - HttpContext = httpContext, - RouteData = new RouteData(), - }, - Array.Empty(), - null, - new Dictionary(), - page); - - // Act - filter.OnPageHandlerExecuting(context); - - // Assert - Assert.Equal("Value", page.Test); - Assert.Null(page.Test2); - } - [Fact] public void OnPageExecuting_InitializesAndSavesProperties() { @@ -189,21 +93,13 @@ namespace Microsoft.AspNetCore.Mvc.RazorPages.Internal var pageModel = new TestPageModel(); - var filter = CreatePageSaveTempDataPropertyFilter(tempData); + var filter = CreatePageSaveTempDataPropertyFilter(tempData, "TempDataProperty-"); filter.Subject = pageModel; - var factory = filter.FilterFactory; - var pageType = typeof(TestPageModel); var testProperty = pageType.GetProperty(nameof(TestPageModel.Test)); var test2Property = pageType.GetProperty(nameof(TestPageModel.Test2)); - filter.Properties = new List - { - new TempDataProperty("TempDataProperty-Test", testProperty, testProperty.GetValue, testProperty.SetValue), - new TempDataProperty("TempDataProperty-Test2", test2Property, test2Property.GetValue, test2Property.SetValue) - }; - var context = new PageHandlerExecutingContext( new PageContext() { @@ -224,57 +120,98 @@ namespace Microsoft.AspNetCore.Mvc.RazorPages.Internal filter.Properties.OrderBy(p => p.PropertyInfo.Name), p => Assert.Equal(testProperty, p.PropertyInfo), p => Assert.Equal(test2Property, p.PropertyInfo)); - - Assert.Same(filter.Properties, factory.Properties); } - private static ViewContext CreateViewContext(HttpContext httpContext, ITempDataDictionary tempData) + [Fact] + public void OnPageExecuting_ReadsTempDataPropertiesWithoutPrefix() { - var actionContext = new ActionContext(httpContext, new RouteData(), new ActionDescriptor()); - var metadataProvider = new EmptyModelMetadataProvider(); - var viewData = new ViewDataDictionary(metadataProvider, new ModelStateDictionary()); - var viewContext = new ViewContext( - actionContext, - NullView.Instance, - viewData, - tempData, - TextWriter.Null, - new HtmlHelperOptions()); + // Arrange + var httpContext = new DefaultHttpContext(); - return viewContext; + var tempData = new TempDataDictionary(httpContext, Mock.Of()) + { + { "TempDataProperty-Test", "Prefix-Value" }, + { "Test", "Value" } + }; + tempData.Save(); + + var model = new TestPageModel(); + + var filter = CreatePageSaveTempDataPropertyFilter(tempData, string.Empty); + filter.Subject = model; + + var context = new PageHandlerExecutingContext( + new PageContext() + { + ActionDescriptor = new CompiledPageActionDescriptor(), + HttpContext = httpContext, + RouteData = new RouteData(), + }, + Array.Empty(), + null, + new Dictionary(), + model); + + // Act + filter.OnPageHandlerExecuting(context); + + // Assert + Assert.Equal("Value", model.Test); + Assert.Null(model.Test2); } - private PageSaveTempDataPropertyFilter CreatePageSaveTempDataPropertyFilter( - TempDataDictionary tempData, - bool filterFactory = true) + [Fact] + public void OnTempDataSaving_WritesToTempData_WithoutPrefix() + { + // Arrange + var httpContext = new DefaultHttpContext(); + + var tempData = new TempDataDictionary(httpContext, Mock.Of()) + { + ["Test"] = "Old-Value", + }; + var pageModel = new TestPageModel + { + Test = "New-Value", + }; + + var filter = CreatePageSaveTempDataPropertyFilter(tempData, string.Empty); + filter.Subject = pageModel; + + // Act + filter.OnTempDataSaving(tempData); + + // Assert + Assert.Collection( + tempData, + item => + { + Assert.Equal("Test", item.Key); + Assert.Equal("New-Value", item.Value); + }); + } + + private PageSaveTempDataPropertyFilter CreatePageSaveTempDataPropertyFilter(TempDataDictionary tempData, string prefix) { var factory = new Mock(); factory .Setup(f => f.GetTempData(It.IsAny())) .Returns(tempData); - var propertyFilter = new PageSaveTempDataPropertyFilter(factory.Object); + var pageModelType = typeof(TestPageModel); + var property1 = pageModelType.GetProperty(nameof(TestPageModel.Test)); + var property2 = pageModelType.GetProperty(nameof(TestPageModel.Test2)); - if (filterFactory) + var filter = new PageSaveTempDataPropertyFilter(factory.Object) { - propertyFilter.FilterFactory = Mock.Of(); - } + Properties = new[] + { + new LifecycleProperty(property1, prefix + property1.Name), + new LifecycleProperty(property2, prefix + property2.Name), + } + }; - return propertyFilter; - } - - public class TestPage : Page - { - [TempData] - public string Test { get; set; } - - [TempData] - public string Test2 { get; set; } - - public override Task ExecuteAsync() - { - throw new NotImplementedException(); - } + return filter; } public class TestPageModel : PageModel diff --git a/test/Microsoft.AspNetCore.Mvc.RazorPages.Test/Internal/TempDataFilterPageApplicationModelProviderTest.cs b/test/Microsoft.AspNetCore.Mvc.RazorPages.Test/Internal/TempDataFilterPageApplicationModelProviderTest.cs index 2f84b6a797..cc2429ca16 100644 --- a/test/Microsoft.AspNetCore.Mvc.RazorPages.Test/Internal/TempDataFilterPageApplicationModelProviderTest.cs +++ b/test/Microsoft.AspNetCore.Mvc.RazorPages.Test/Internal/TempDataFilterPageApplicationModelProviderTest.cs @@ -1,8 +1,11 @@ // 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.Reflection; +using System.Threading.Tasks; using Microsoft.AspNetCore.Mvc.ApplicationModels; +using Microsoft.Extensions.Options; using Xunit; namespace Microsoft.AspNetCore.Mvc.RazorPages.Internal @@ -10,27 +13,149 @@ namespace Microsoft.AspNetCore.Mvc.RazorPages.Internal public class TempDataFilterPageApplicationModelProviderTest { [Fact] - public void OnProvidersExecuting_AddsFiltersToModel() + public void OnProvidersExecuting_DoesNotAddFilter_IfTypeHasNoTempDataProperties() { // Arrange - var actionDescriptor = new PageActionDescriptor(); - var applicationModel = new PageApplicationModel( - actionDescriptor, - typeof(object).GetTypeInfo(), - new object[0]); - var applicationModelProvider = new TempDataFilterPageApplicationModelProvider(); - var context = new PageApplicationModelProviderContext(new PageActionDescriptor(), typeof(object).GetTypeInfo()) - { - PageApplicationModel = applicationModel, - }; + var type = typeof(TestPageModel_NoTempDataProperties); + var options = Options.Create(new MvcViewOptions()); + var provider = new TempDataFilterPageApplicationModelProvider(options); + var context = CreateProviderContext(type); // Act - applicationModelProvider.OnProvidersExecuting(context); + provider.OnProvidersExecuting(context); // Assert + Assert.Empty(context.PageApplicationModel.Filters); + } + + [Fact] + public void OnProvidersExecuting_ValidatesTempDataProperties() + { + // Arrange + var type = typeof(TestPageModel_PrivateSet); + var expected = $"The '{type.FullName}.Test' property with TempDataAttribute is invalid. A property using TempDataAttribute must have a public getter and setter."; + + var options = Options.Create(new MvcViewOptions()); + var provider = new TempDataFilterPageApplicationModelProvider(options); + var context = CreateProviderContext(type); + + // Act & Assert + var ex = Assert.Throws(() => provider.OnProvidersExecuting(context)); + Assert.Equal(expected, ex.Message); + } + + [Fact] + public void AddsTempDataPropertyFilter_ForTempDataAttributeProperties() + { + // Arrange + var type = typeof(TestPageModel_OneTempDataProperty); + var options = Options.Create(new MvcViewOptions()); + var provider = new TempDataFilterPageApplicationModelProvider(options); + var context = CreateProviderContext(type); + + // Act + provider.OnProvidersExecuting(context); + + // Assert + var filter = Assert.Single(context.PageApplicationModel.Filters); + Assert.IsType(filter); + } + + [Fact] + public void InitializeFilterFactory_WithExpectedPropertyHelpers_ForTempDataAttributeProperties() + { + // Arrange + var type = typeof(TestPageModel_OneTempDataProperty); + var options = Options.Create(new MvcViewOptions()); + var provider = new TempDataFilterPageApplicationModelProvider(options); + var context = CreateProviderContext(type); + + // Act + provider.OnProvidersExecuting(context); + + // Assert + var filter = Assert.IsType(Assert.Single(context.PageApplicationModel.Filters)); Assert.Collection( - applicationModel.Filters, - filter => Assert.IsType(filter)); + filter.Properties, + property => + { + Assert.Equal("TempDataProperty-Test2", property.Key); + Assert.Equal(type.GetProperty(nameof(TestPageModel_OneTempDataProperty.Test2)), property.PropertyInfo); + }); + } + + [Fact] + public void OnProvidersExecuting_SetsKeyPrefixToEmptyString_IfCompatSwitchIsSet() + { + // Arrange + var type = typeof(TestPageModel_OneTempDataProperty); + var options = Options.Create(new MvcViewOptions { SuppressTempDataAttributePrefix = true }); + var provider = new TempDataFilterPageApplicationModelProvider(options); + var context = CreateProviderContext(type); + + // Act + provider.OnProvidersExecuting(context); + + // Assert + var filter = Assert.IsType(Assert.Single(context.PageApplicationModel.Filters)); + Assert.Collection( + filter.Properties, + property => + { + Assert.Equal("Test2", property.Key); + }); + } + + private static PageApplicationModelProviderContext CreateProviderContext(Type handlerType) + { + var descriptor = new CompiledPageActionDescriptor(); + var context = new PageApplicationModelProviderContext(descriptor, typeof(TestPage).GetTypeInfo()) + { + PageApplicationModel = new PageApplicationModel(descriptor, handlerType.GetTypeInfo(), Array.Empty()), + }; + + return context; + } + + private static CompiledPageActionDescriptor CreateDescriptor(Type type) + { + return new CompiledPageActionDescriptor(new PageActionDescriptor()) + { + PageTypeInfo = typeof(TestPage).GetTypeInfo(), + HandlerTypeInfo = type.GetTypeInfo(), + }; + } + + private class TestPage : Page + { + public object Model => null; + + public override Task ExecuteAsync() => null; + } + + public class TestPageModel_NoTempDataProperties + { + public DateTime? DateTime { get; set; } + } + + public class TestPageModel_NullableNonPrimitiveTempDataProperty + { + [TempData] + public DateTime? DateTime { get; set; } + } + + public class TestPageModel_OneTempDataProperty + { + public string Test { get; set; } + + [TempData] + public string Test2 { get; set; } + } + + public class TestPageModel_PrivateSet + { + [TempData] + public string Test { get; private set; } } } } diff --git a/test/Microsoft.AspNetCore.Mvc.ViewFeatures.Test/Internal/ControllerSaveTempDataPropertyFilterFactoryTest.cs b/test/Microsoft.AspNetCore.Mvc.ViewFeatures.Test/Internal/ControllerSaveTempDataPropertyFilterFactoryTest.cs index 958ea08dba..5383d49d1c 100644 --- a/test/Microsoft.AspNetCore.Mvc.ViewFeatures.Test/Internal/ControllerSaveTempDataPropertyFilterFactoryTest.cs +++ b/test/Microsoft.AspNetCore.Mvc.ViewFeatures.Test/Internal/ControllerSaveTempDataPropertyFilterFactoryTest.cs @@ -1,7 +1,6 @@ // 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.Extensions.DependencyInjection; using Moq; using Xunit; @@ -14,21 +13,16 @@ namespace Microsoft.AspNetCore.Mvc.ViewFeatures.Internal public void CreateInstance_CreatesFilter() { // Arrange - var factory = new ControllerSaveTempDataPropertyFilterFactory(); - var propertyInfo = typeof(StringController).GetProperty("StringProp"); - - factory.TempDataProperties = new List() - { - new TempDataProperty("TempDataProperty-StringProp", propertyInfo, null, null) - }; + var property = typeof(StringController).GetProperty(nameof(StringController.StringProp)); + var lifecycleProperties = new[] { new LifecycleProperty(property, "key") }; + var factory = new ControllerSaveTempDataPropertyFilterFactory(lifecycleProperties); // Act var filter = factory.CreateInstance(CreateServiceProvider()); // Assert - Assert.Collection( - Assert.IsType(filter).Properties, - property => Assert.Equal(propertyInfo, property.PropertyInfo)); + var tempDataFilter = Assert.IsType(filter); + Assert.Same(lifecycleProperties, tempDataFilter.Properties); } private ServiceProvider CreateServiceProvider() @@ -44,6 +38,7 @@ namespace Microsoft.AspNetCore.Mvc.ViewFeatures.Internal private class StringController { + [TempData] public string StringProp { get; set; } } } diff --git a/test/Microsoft.AspNetCore.Mvc.ViewFeatures.Test/Internal/ControllerSaveTempDataPropertyFilterTest.cs b/test/Microsoft.AspNetCore.Mvc.ViewFeatures.Test/Internal/ControllerSaveTempDataPropertyFilterTest.cs index 9c6d6a5aac..7826bbb2e3 100644 --- a/test/Microsoft.AspNetCore.Mvc.ViewFeatures.Test/Internal/ControllerSaveTempDataPropertyFilterTest.cs +++ b/test/Microsoft.AspNetCore.Mvc.ViewFeatures.Test/Internal/ControllerSaveTempDataPropertyFilterTest.cs @@ -2,6 +2,7 @@ // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. using System.Collections.Generic; +using System.Linq; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc.Abstractions; using Microsoft.AspNetCore.Mvc.Filters; @@ -28,13 +29,13 @@ namespace Microsoft.AspNetCore.Mvc.ViewFeatures.Internal var controller = new TestController(); var controllerType = controller.GetType(); - var testProperty = controllerType.GetProperty(nameof(TestController.Test)); - var test2Property = controllerType.GetProperty(nameof(TestController.Test2)); + var property1 = controllerType.GetProperty(nameof(TestController.Test)); + var property2 = controllerType.GetProperty(nameof(TestController.Test2)); - filter.Properties = new List + filter.Properties = new[] { - new TempDataProperty("TempDataProperty-Test", testProperty, testProperty.GetValue, testProperty.SetValue), - new TempDataProperty("TempDataProperty-Test2", test2Property, test2Property.GetValue, test2Property.SetValue) + new LifecycleProperty(property1, "TempDataProperty-Test"), + new LifecycleProperty(property1, "TempDataProperty-Test2"), }; var context = new ActionExecutingContext( @@ -73,13 +74,14 @@ namespace Microsoft.AspNetCore.Mvc.ViewFeatures.Internal var controller = new TestController(); var controllerType = controller.GetType(); - var testProperty = controllerType.GetProperty(nameof(TestController.Test)); - var test2Property = controllerType.GetProperty(nameof(TestController.Test2)); - filter.Properties = new List + var property1 = controllerType.GetProperty(nameof(TestController.Test)); + var property2 = controllerType.GetProperty(nameof(TestController.Test2)); + + filter.Properties = new[] { - new TempDataProperty("TempDataProperty-Test", testProperty, testProperty.GetValue, testProperty.SetValue), - new TempDataProperty("TempDataProperty-Test2", test2Property, test2Property.GetValue, test2Property.SetValue) + new LifecycleProperty(property1, "TempDataProperty-Test"), + new LifecycleProperty(property2, "TempDataProperty-Test2"), }; var context = new ActionExecutingContext( @@ -95,13 +97,108 @@ namespace Microsoft.AspNetCore.Mvc.ViewFeatures.Internal // Act filter.OnActionExecuting(context); - filter.OnTempDataSaving(tempData); // Assert Assert.Equal("FirstValue", controller.Test); Assert.Equal(0, controller.Test2); } + [Fact] + public void ReadsTempDataFromTempDataDictionary_WithoutKeyPrefix() + { + // Arrange + var httpContext = new DefaultHttpContext(); + var tempData = new TempDataDictionary(httpContext, Mock.Of()) + { + ["TempDataProperty-Test"] = "ValueWithPrefix", + ["Test"] = "Value" + }; + + var filter = CreateControllerSaveTempDataPropertyFilter(httpContext, tempData: tempData); + var controller = new TestController(); + + var controllerType = controller.GetType(); + var property1 = controllerType.GetProperty(nameof(TestController.Test)); + var property2 = controllerType.GetProperty(nameof(TestController.Test2)); + + filter.Properties = new[] + { + new LifecycleProperty(property1, "Test"), + new LifecycleProperty(property2, "Test2"), + }; + + var context = new ActionExecutingContext( + new ActionContext + { + HttpContext = httpContext, + RouteData = new RouteData(), + ActionDescriptor = new ActionDescriptor(), + }, + new List(), + new Dictionary(), + controller); + + // Act + filter.OnActionExecuting(context); + + // Assert + Assert.Equal("Value", controller.Test); + Assert.Equal(0, controller.Test2); + } + + [Fact] + public void WritesTempDataFromTempDataDictionary_WithoutKeyPrefix() + { + // Arrange + var httpContext = new DefaultHttpContext(); + var tempData = new TempDataDictionary(httpContext, Mock.Of()); + + var filter = CreateControllerSaveTempDataPropertyFilter(httpContext, tempData: tempData); + var controller = new TestController(); + + var controllerType = controller.GetType(); + var property1 = controllerType.GetProperty(nameof(TestController.Test)); + var property2 = controllerType.GetProperty(nameof(TestController.Test2)); + + filter.Properties = new[] + { + new LifecycleProperty(property1, "Test"), + new LifecycleProperty(property2, "Test2"), + }; + + var context = new ActionExecutingContext( + new ActionContext + { + HttpContext = httpContext, + RouteData = new RouteData(), + ActionDescriptor = new ActionDescriptor(), + }, + new List(), + new Dictionary(), + controller); + + // Act + filter.OnActionExecuting(context); + controller.Test = "New-Value"; + controller.Test2 = 42; + + filter.OnTempDataSaving(tempData); + + // Assert + Assert.Collection( + tempData.OrderBy(i => i.Key), + item => + { + Assert.Equal(nameof(TestController.Test), item.Key); + Assert.Equal("New-Value", item.Value); + }, + item => + { + Assert.Equal(nameof(TestController.Test2), item.Key); + Assert.Equal(42, item.Value); + }); + } + private ControllerSaveTempDataPropertyFilter CreateControllerSaveTempDataPropertyFilter( HttpContext httpContext, TempDataDictionary tempData) diff --git a/test/Microsoft.AspNetCore.Mvc.ViewFeatures.Test/Internal/LifecyclePropertyTest.cs b/test/Microsoft.AspNetCore.Mvc.ViewFeatures.Test/Internal/LifecyclePropertyTest.cs new file mode 100644 index 0000000000..87aa7dd5e7 --- /dev/null +++ b/test/Microsoft.AspNetCore.Mvc.ViewFeatures.Test/Internal/LifecyclePropertyTest.cs @@ -0,0 +1,94 @@ +// 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.ViewFeatures.Internal +{ + public class LifecyclePropertyTest + { + [Fact] + public void GetValue_GetsPropertyValue() + { + // Arrange + var propertyInfo = typeof(TestSubject).GetProperty(nameof(TestSubject.TestProperty)); + var lifecycleProperty = new LifecycleProperty(propertyInfo, "test-key"); + var subject = new TestSubject { TestProperty = "test-value" }; + + // Act + var value = lifecycleProperty.GetValue(subject); + + // Assert + Assert.Equal("test-value", value); + } + + [Fact] + public void SetValue_SetsPropertyValue() + { + // Arrange + var propertyInfo = typeof(TestSubject).GetProperty(nameof(TestSubject.TestProperty)); + var lifecycleProperty = new LifecycleProperty(propertyInfo, "test-key"); + var subject = new TestSubject { TestProperty = "test-value" }; + + // Act + lifecycleProperty.SetValue(subject, "new-value"); + + // Assert + Assert.Equal("new-value", subject.TestProperty); + } + + [Fact] + public void SetValue_SetsNullPropertyValue() + { + // Arrange + var propertyInfo = typeof(TestSubject).GetProperty(nameof(TestSubject.TestProperty)); + var lifecycleProperty = new LifecycleProperty(propertyInfo, "test-key"); + var subject = new TestSubject { TestProperty = "test-value" }; + + // Act + lifecycleProperty.SetValue(subject, null); + + // Assert + Assert.Null(subject.TestProperty); + } + + [Fact] + public void SetValue_NoopsIfNullIsBeingAssignedToValueType() + { + // Arrange + var propertyInfo = typeof(TestSubject).GetProperty(nameof(TestSubject.ValueTypeProperty)); + var lifecycleProperty = new LifecycleProperty(propertyInfo, "test-key"); + var subject = new TestSubject { ValueTypeProperty = 42 }; + + // Act + lifecycleProperty.SetValue(subject, null); + + // Assert + Assert.Equal(42, subject.ValueTypeProperty); + } + + [Fact] + public void SetValue_SetsNullValue_ForNullableProperties() + { + // Arrange + var propertyInfo = typeof(TestSubject).GetProperty(nameof(TestSubject.NullableProperty)); + var lifecycleProperty = new LifecycleProperty(propertyInfo, "test-key"); + var subject = new TestSubject { NullableProperty = 42 }; + + // Act + lifecycleProperty.SetValue(subject, null); + + // Assert + Assert.Null(subject.NullableProperty); + } + + public class TestSubject + { + public string TestProperty { get; set; } + + public int ValueTypeProperty { get; set; } + + public int? NullableProperty { get; set; } + } + } +} diff --git a/test/Microsoft.AspNetCore.Mvc.ViewFeatures.Test/Internal/TempDataApplicationModelProviderTest.cs b/test/Microsoft.AspNetCore.Mvc.ViewFeatures.Test/Internal/TempDataApplicationModelProviderTest.cs index 8faed68cf2..459c398b9d 100644 --- a/test/Microsoft.AspNetCore.Mvc.ViewFeatures.Test/Internal/TempDataApplicationModelProviderTest.cs +++ b/test/Microsoft.AspNetCore.Mvc.ViewFeatures.Test/Internal/TempDataApplicationModelProviderTest.cs @@ -2,7 +2,6 @@ // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. using System; -using System.Collections.Generic; using System.Linq; using System.Reflection; using Microsoft.AspNetCore.Mvc.ApplicationModels; @@ -14,16 +13,13 @@ namespace Microsoft.AspNetCore.Mvc.ViewFeatures.Internal { public class TempDataApplicationModelProviderTest { - [Theory] - [InlineData(typeof(TestController_OneTempDataProperty))] - [InlineData(typeof(TestController_ListOfString))] - [InlineData(typeof(TestController_OneNullableTempDataProperty))] - [InlineData(typeof(TestController_TwoTempDataProperties))] - [InlineData(typeof(TestController_NullableNonPrimitiveTempDataProperty))] - public void AddsTempDataPropertyFilter_ForTempDataAttributeProperties(Type type) + [Fact] + public void OnProvidersExecuting_DoesNotAddFilter_IfTypeHasNoTempDataProperties() { // Arrange - var provider = new TempDataApplicationModelProvider(); + var type = typeof(TestController_NoTempDataProperties); + var options = Options.Create(new MvcViewOptions()); + var provider = new TempDataApplicationModelProvider(options); var defaultProvider = new DefaultApplicationModelProvider(Options.Create(new MvcOptions())); var context = new ApplicationModelProviderContext(new[] { type.GetTypeInfo() }); @@ -34,14 +30,54 @@ namespace Microsoft.AspNetCore.Mvc.ViewFeatures.Internal // Assert var controller = Assert.Single(context.Result.Controllers); - Assert.Single(controller.Filters, f => f is ControllerSaveTempDataPropertyFilterFactory); + Assert.Empty(controller.Filters); + } + + [Fact] + public void OnProvidersExecuting_ValidatesTempDataProperties() + { + // Arrange + var type = typeof(TestController_PrivateSet); + var options = Options.Create(new MvcViewOptions()); + var provider = new TempDataApplicationModelProvider(options); + var expected = $"The '{type.FullName}.Test' property with TempDataAttribute is invalid. A property using TempDataAttribute must have a public getter and setter."; + var defaultProvider = new DefaultApplicationModelProvider(Options.Create(new MvcOptions())); + + var context = new ApplicationModelProviderContext(new[] { type.GetTypeInfo() }); + defaultProvider.OnProvidersExecuting(context); + + // Act & Assert + var ex = Assert.Throws(() => provider.OnProvidersExecuting(context)); + Assert.Equal(expected, ex.Message); + } + + [Fact] + public void AddsTempDataPropertyFilter_ForTempDataAttributeProperties() + { + // Arrange + var type = typeof(TestController_NullableNonPrimitiveTempDataProperty); + var options = Options.Create(new MvcViewOptions()); + var provider = new TempDataApplicationModelProvider(options); + var defaultProvider = new DefaultApplicationModelProvider(Options.Create(new MvcOptions())); + + var context = new ApplicationModelProviderContext(new[] { type.GetTypeInfo() }); + defaultProvider.OnProvidersExecuting(context); + + // Act + provider.OnProvidersExecuting(context); + + // Assert + var controller = Assert.Single(context.Result.Controllers); + Assert.IsType(Assert.Single(controller.Filters)); } [Fact] public void InitializeFilterFactory_WithExpectedPropertyHelpers_ForTempDataAttributeProperties() { // Arrange - var provider = new TempDataApplicationModelProvider(); + var expected = typeof(TestController_OneTempDataProperty).GetProperty(nameof(TestController_OneTempDataProperty.Test2)); + var options = Options.Create(new MvcViewOptions()); + var provider = new TempDataApplicationModelProvider(options); var defaultProvider = new DefaultApplicationModelProvider(Options.Create(new MvcOptions())); var context = new ApplicationModelProviderContext(new[] { typeof(TestController_OneTempDataProperty).GetTypeInfo() }); @@ -50,79 +86,42 @@ namespace Microsoft.AspNetCore.Mvc.ViewFeatures.Internal // Act provider.OnProvidersExecuting(context); var controller = context.Result.Controllers.SingleOrDefault(); - var filter = controller.Filters.OfType(); - var saveTempDataPropertyFilterFactory = filter.SingleOrDefault(); - var expected = typeof(TestController_OneTempDataProperty).GetProperty(nameof(TestController_OneTempDataProperty.Test2)); + var filter = Assert.IsType(Assert.Single(controller.Filters)); // Assert - Assert.NotNull(saveTempDataPropertyFilterFactory); - var tempDataPropertyHelper = Assert.Single(saveTempDataPropertyFilterFactory.TempDataProperties); - Assert.Same(expected, tempDataPropertyHelper.PropertyInfo); + Assert.NotNull(filter); + var property = Assert.Single(filter.TempDataProperties); + Assert.Same(expected, property.PropertyInfo); + Assert.Equal("TempDataProperty-Test2", property.Key); } [Fact] - public void ThrowsInvalidOperationException_PrivateSetter() + public void OnProvidersExecuting_SetsKeyPrefixToEmptyString_IfCompatSwitchIsSet() { // Arrange - var provider = new TempDataApplicationModelProvider(); + var expected = typeof(TestController_OneTempDataProperty).GetProperty(nameof(TestController_OneTempDataProperty.Test2)); + var options = Options.Create(new MvcViewOptions { SuppressTempDataAttributePrefix = true }); + var provider = new TempDataApplicationModelProvider(options); var defaultProvider = new DefaultApplicationModelProvider(Options.Create(new MvcOptions())); - var context = new ApplicationModelProviderContext(new[] { typeof(TestController_PrivateSet).GetTypeInfo() }); + var context = new ApplicationModelProviderContext(new[] { typeof(TestController_OneTempDataProperty).GetTypeInfo() }); defaultProvider.OnProvidersExecuting(context); - // Act & Assert - var exception = Assert.Throws(() => - provider.OnProvidersExecuting(context)); + // Act + provider.OnProvidersExecuting(context); + var controller = context.Result.Controllers.SingleOrDefault(); + var filter = Assert.IsType(Assert.Single(controller.Filters)); - Assert.Equal( - $"The '{typeof(TestController_PrivateSet).FullName}.{nameof(TestController_NonPrimitiveType.Test)}'" - + $" property with {nameof(TempDataAttribute)} is invalid. A property using {nameof(TempDataAttribute)}" - + " must have a public getter and setter.", - exception.Message); + // Assert + Assert.NotNull(filter); + var property = Assert.Single(filter.TempDataProperties); + Assert.Same(expected, property.PropertyInfo); + Assert.Equal("Test2", property.Key); } - [Fact] - public void ThrowsInvalidOperationException_NonPrimitiveType() + public class TestController_NoTempDataProperties { - // Arrange - var provider = new TempDataApplicationModelProvider(); - var defaultProvider = new DefaultApplicationModelProvider(Options.Create(new MvcOptions())); - - var context = new ApplicationModelProviderContext(new[] { typeof(TestController_NonPrimitiveType).GetTypeInfo() }); - defaultProvider.OnProvidersExecuting(context); - - // Act & Assert - var exception = Assert.Throws(() => - provider.OnProvidersExecuting(context)); - - Assert.Equal( - $"The '{typeof(TestController_NonPrimitiveType).FullName}.{nameof(TestController_NonPrimitiveType.Test)}'" - + $" property with {nameof(TempDataAttribute)} is invalid. The '{typeof(TempDataSerializer).FullName}'" - + $" cannot serialize an object of type '{typeof(Object).FullName}'.", - exception.Message); - } - - [Fact] - public void ThrowsInvalidOperationException_NonStringDictionaryKey() - { - // Arrange - var provider = new TempDataApplicationModelProvider(); - var defaultProvider = new DefaultApplicationModelProvider(Options.Create(new MvcOptions())); - - var context = new ApplicationModelProviderContext( - new[] { typeof(TestController_NonStringDictionaryKey).GetTypeInfo() }); - defaultProvider.OnProvidersExecuting(context); - - // Act & Assert - var exception = Assert.Throws(() => - provider.OnProvidersExecuting(context)); - - Assert.Equal( - $"The '{typeof(TestController_NonStringDictionaryKey).FullName}.{nameof(TestController_NonStringDictionaryKey.Test)}'" - + $" property with {nameof(TempDataAttribute)} is invalid. The '{typeof(TempDataSerializer).FullName}'" - + $" cannot serialize a dictionary with a key of type '{typeof(Object)}'. The key must be of type" - + $" '{typeof(string).FullName}'.", - exception.Message); + public DateTime? DateTime { get; set; } } public class TestController_NullableNonPrimitiveTempDataProperty @@ -139,45 +138,10 @@ namespace Microsoft.AspNetCore.Mvc.ViewFeatures.Internal public string Test2 { get; set; } } - public class TestController_TwoTempDataProperties - { - [TempData] - public string Test { get; set; } - - [TempData] - public int Test2 { get; set; } - } - - public class TestController_OneNullableTempDataProperty - { - public string Test { get; set; } - - [TempData] - public int? Test2 { get; set; } - } - - public class TestController_ListOfString - { - [TempData] - public IList Test { get; set; } - } - public class TestController_PrivateSet { [TempData] public string Test { get; private set; } } - - public class TestController_NonPrimitiveType - { - [TempData] - public object Test { get; set; } - } - - public class TestController_NonStringDictionaryKey - { - [TempData] - public IDictionary Test { get; set; } - } } }