TempData property attribute

Addresses #5600
This commit is contained in:
Jass Bagga 2017-03-16 16:34:35 -07:00 committed by GitHub
parent ce28117b8b
commit 1197657e5b
26 changed files with 950 additions and 240 deletions

View File

@ -8,6 +8,7 @@ using Microsoft.AspNetCore.Mvc.Razor;
using Microsoft.AspNetCore.Mvc.RazorPages;
using Microsoft.AspNetCore.Mvc.RazorPages.Infrastructure;
using Microsoft.AspNetCore.Mvc.RazorPages.Internal;
using Microsoft.AspNetCore.Mvc.ViewFeatures.Internal;
using Microsoft.Extensions.DependencyInjection.Extensions;
using Microsoft.Extensions.Options;

View File

@ -7,6 +7,7 @@ using Microsoft.AspNetCore.Mvc.Abstractions;
using Microsoft.AspNetCore.Mvc.ApplicationModels;
using Microsoft.AspNetCore.Mvc.Filters;
using Microsoft.AspNetCore.Mvc.Routing;
using Microsoft.AspNetCore.Mvc.ViewFeatures.Internal;
using Microsoft.AspNetCore.Razor.Evolution;
using Microsoft.Extensions.Options;
@ -75,7 +76,7 @@ namespace Microsoft.AspNetCore.Mvc.RazorPages.Infrastructure
var index = parentDirectoryPath.LastIndexOf('/');
if (index == -1)
{
parentDirectoryPath = string.Empty;
parentDirectoryPath = string.Empty;
}
else
{

View File

@ -357,7 +357,6 @@ namespace Microsoft.AspNetCore.Mvc.RazorPages.Internal
{
propertyFilter.OriginalValues = originalValues;
propertyFilter.Subject = _page;
propertyFilter.Prefix = TempDataPropertyProvider.Prefix;
}
IActionResult result = null;

View File

@ -15,6 +15,7 @@ using Microsoft.AspNetCore.Mvc.ModelBinding;
using Microsoft.AspNetCore.Mvc.Razor;
using Microsoft.AspNetCore.Mvc.RazorPages.Infrastructure;
using Microsoft.AspNetCore.Mvc.ViewFeatures;
using Microsoft.AspNetCore.Mvc.ViewFeatures.Internal;
using Microsoft.AspNetCore.Razor.Evolution;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;

View File

@ -18,7 +18,7 @@ namespace Microsoft.AspNetCore.Mvc.RazorPages.Internal
}
// Support for [TempData] on properties
options.ConfigureFilter(new SaveTempDataPropertyFilter());
options.ConfigureFilter(new SaveTempDataPropertyFilterFactory());
// Always require an antiforgery token on post
options.ConfigureFilter(new AutoValidateAntiforgeryTokenAttribute());
}

View File

@ -5,6 +5,7 @@ using System;
using System.Buffers;
using System.Linq;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.ApplicationModels;
using Microsoft.AspNetCore.Mvc.ApplicationParts;
using Microsoft.AspNetCore.Mvc.Formatters;
using Microsoft.AspNetCore.Mvc.Internal;
@ -197,6 +198,11 @@ namespace Microsoft.Extensions.DependencyInjection
//
// Temp Data
//
services.TryAddEnumerable(
ServiceDescriptor.Transient<IApplicationModelProvider, TempDataApplicationModelProvider>());
services.TryAddSingleton<SaveTempDataFilter>();
services.TryAddTransient<SaveTempDataPropertyFilter>();
// This does caching so it should stay singleton
services.TryAddSingleton<ITempDataProvider, CookieTempDataProvider>();
@ -208,8 +214,6 @@ namespace Microsoft.Extensions.DependencyInjection
// These are stateless so their lifetime isn't really important.
services.TryAddSingleton<ITempDataDictionaryFactory, TempDataDictionaryFactory>();
services.TryAddSingleton<SaveTempDataFilter>();
services.TryAddSingleton(ArrayPool<ViewBufferValue>.Shared);
services.TryAddScoped<IViewBufferScope, MemoryPoolViewBufferScope>();
}

View File

@ -2,8 +2,8 @@
// 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.Filters;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc.Filters;
using Microsoft.AspNetCore.Mvc.Internal;
namespace Microsoft.AspNetCore.Mvc.ViewFeatures.Internal

View File

@ -1,14 +1,26 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System;
using System.Collections.Generic;
using System.Reflection;
using Microsoft.AspNetCore.Mvc.Filters;
using Microsoft.Extensions.Internal;
namespace Microsoft.AspNetCore.Mvc.ViewFeatures.Internal
{
public class SaveTempDataPropertyFilter : ISaveTempDataCallback
public class SaveTempDataPropertyFilter : ISaveTempDataCallback, IActionFilter
{
public string Prefix { get; set; }
private const string Prefix = "TempDataProperty-";
private readonly ITempDataDictionaryFactory _factory;
public SaveTempDataPropertyFilter(ITempDataDictionaryFactory factory)
{
_factory = factory;
}
// Cannot be public as <c>PropertyHelper</c> is an internal shared source type
internal IList<PropertyHelper> PropertyHelpers { get; set; }
public object Subject { get; set; }
@ -24,12 +36,46 @@ namespace Microsoft.AspNetCore.Mvc.ViewFeatures.Internal
var originalValue = kvp.Value;
var newValue = property.GetValue(Subject);
if (newValue != null && newValue != originalValue)
if (newValue != null && !newValue.Equals(originalValue))
{
tempData[Prefix + property.Name] = newValue;
}
}
}
}
public void OnActionExecuting(ActionExecutingContext context)
{
if (PropertyHelpers == null)
{
throw new ArgumentNullException(nameof(PropertyHelpers));
}
Subject = context.Controller;
var tempData = _factory.GetTempData(context.HttpContext);
OriginalValues = new Dictionary<PropertyInfo, object>();
for (var i = 0; i < PropertyHelpers.Count; i++)
{
var property = PropertyHelpers[i].Property;
var value = tempData[Prefix + property.Name];
OriginalValues[property] = value;
var propertyTypeInfo = property.PropertyType.GetTypeInfo();
var isReferenceTypeOrNullable = !propertyTypeInfo.IsValueType || Nullable.GetUnderlyingType(property.GetType()) != null;
if (value != null || isReferenceTypeOrNullable)
{
property.SetValue(Subject, value);
}
}
}
public void OnActionExecuted(ActionExecutedContext context)
{
}
}
}

View File

@ -0,0 +1,31 @@
// 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.AspNetCore.Mvc.Filters;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Internal;
namespace Microsoft.AspNetCore.Mvc.ViewFeatures.Internal
{
public class SaveTempDataPropertyFilterFactory : IFilterFactory
{
// Cannot be public as <c>PropertyHelper</c> is an internal shared source type
internal IList<PropertyHelper> TempDataProperties { get; set; }
public bool IsReusable => false;
public IFilterMetadata CreateInstance(IServiceProvider serviceProvider)
{
if (serviceProvider == null)
{
throw new ArgumentNullException(nameof(serviceProvider));
}
var service = serviceProvider.GetRequiredService<SaveTempDataPropertyFilter>();
service.PropertyHelpers = TempDataProperties;
return service;
}
}
}

View File

@ -0,0 +1,80 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System;
using System.Collections.Generic;
using System.Reflection;
using Microsoft.AspNetCore.Mvc.ApplicationModels;
using Microsoft.AspNetCore.Mvc.Internal;
using Microsoft.Extensions.Internal;
namespace Microsoft.AspNetCore.Mvc.ViewFeatures.Internal
{
public class TempDataApplicationModelProvider : IApplicationModelProvider
{
/// <inheritdoc />
/// <remarks>This order ensures that <see cref="TempDataApplicationModelProvider"/> runs after the <see cref="DefaultApplicationModelProvider"/>.</remarks>
public int Order => -1000 + 10;
/// <inheritdoc />
public void OnProvidersExecuted(ApplicationModelProviderContext context)
{
}
/// <inheritdoc />
public void OnProvidersExecuting(ApplicationModelProviderContext context)
{
if (context == null)
{
throw new ArgumentNullException(nameof(context));
}
foreach (var controllerModel in context.Result.Controllers)
{
SaveTempDataPropertyFilterFactory factory = null;
var propertyHelpers = PropertyHelper.GetVisibleProperties(type: controllerModel.ControllerType.AsType());
for (var i = 0; i < propertyHelpers.Length; i++)
{
var propertyHelper = propertyHelpers[i];
if (propertyHelper.Property.IsDefined(typeof(TempDataAttribute)))
{
ValidateProperty(propertyHelper);
if (factory == null)
{
factory = new SaveTempDataPropertyFilterFactory()
{
TempDataProperties = new List<PropertyHelper>()
};
}
factory.TempDataProperties.Add(propertyHelper);
}
}
if (factory != null)
{
controllerModel.Filters.Add(factory);
}
}
}
private void ValidateProperty(PropertyHelper propertyHelper)
{
var property = propertyHelper.Property;
if (!(property.SetMethod != null &&
property.SetMethod.IsPublic &&
property.GetMethod != null &&
property.GetMethod.IsPublic))
{
throw new InvalidOperationException(
Resources.FormatTempDataProperties_PublicGetterSetter(property.DeclaringType.FullName, property.Name, nameof(TempDataAttribute)));
}
if (!(property.PropertyType.GetTypeInfo().IsPrimitive || property.PropertyType == typeof(string)))
{
throw new InvalidOperationException(
Resources.FormatTempDataProperties_PrimitiveTypeOrString(property.DeclaringType.FullName, property.Name, nameof(TempDataAttribute)));
}
}
}
}

View File

@ -6,10 +6,8 @@ using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Linq;
using System.Reflection;
using Microsoft.AspNetCore.Mvc.Rendering;
using Microsoft.AspNetCore.Mvc.ViewFeatures;
namespace Microsoft.AspNetCore.Mvc.RazorPages
namespace Microsoft.AspNetCore.Mvc.ViewFeatures.Internal
{
public class TempDataPropertyProvider
{
@ -18,6 +16,12 @@ namespace Microsoft.AspNetCore.Mvc.RazorPages
private ConcurrentDictionary<Type, IEnumerable<PropertyInfo>> _subjectProperties =
new ConcurrentDictionary<Type, IEnumerable<PropertyInfo>>();
/// <summary>
/// Loads and tracks any changes to the properties of the <paramref name="subject"/>.
/// </summary>
/// <param name="subject">The properties of the subject are loaded and tracked. May be a <see cref="Controller"/>.</param>
/// <param name="tempData">The <see cref="ITempDataDictionary"/>.</param>
/// <returns></returns>
public IDictionary<PropertyInfo, object> LoadAndTrackChanges(object subject, ITempDataDictionary tempData)
{
var properties = GetSubjectProperties(subject);

File diff suppressed because it is too large Load Diff

View File

@ -286,4 +286,10 @@
<data name="ViewEnginesAreRequired" xml:space="preserve">
<value>'{0}.{1}' must not be empty. At least one '{2}' is required to locate a view for rendering.</value>
</data>
<data name="TempDataProperties_PrimitiveTypeOrString" xml:space="preserve">
<value>The '{0}.{1}' property with {2} is invalid. A property using {2} must be of primitive or string type.</value>
</data>
<data name="TempDataProperties_PublicGetterSetter" xml:space="preserve">
<value>The '{0}.{1}' property with {2} is invalid. A property using {2} must have a public getter and setter.</value>
</data>
</root>

View File

@ -3,8 +3,11 @@
using System;
namespace Microsoft.AspNetCore.Mvc.Rendering
namespace Microsoft.AspNetCore.Mvc.ViewFeatures
{
/// <summary>
/// Properties with the <see cref="TempDataAttribute"/> are stored in the <see cref="TempDataDictionary"/>.
/// </summary>
[AttributeUsage(AttributeTargets.Property, Inherited = false, AllowMultiple = false)]
public sealed class TempDataAttribute : Attribute
{

View File

@ -0,0 +1,169 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System;
using System.Collections.Generic;
using System.Linq;
using System.Net;
using System.Net.Http;
using System.Threading.Tasks;
using Microsoft.Net.Http.Headers;
using Xunit;
namespace Microsoft.AspNetCore.Mvc.FunctionalTests
{
public class TempDataPropertyTest : IClassFixture<MvcTestFixture<BasicWebSite.Startup>>
{
protected HttpClient Client { get; }
public TempDataPropertyTest(MvcTestFixture<BasicWebSite.Startup> fixture)
{
Client = fixture.Client;
}
[Fact]
public async Task TempDataPropertyAttribute_RetainsTempDataWithView()
{
// Arrange
var message = "Success (from Temp Data)";
var nameValueCollection = new List<KeyValuePair<string, string>>
{
new KeyValuePair<string, string>("FullName", "Bob"),
new KeyValuePair<string, string>("id", "1"),
};
var expected = $"{message} for person {nameValueCollection[0].Value} with id {nameValueCollection[1].Value}.";
var content = new FormUrlEncodedContent(nameValueCollection);
// Act 1
var redirectResponse = await Client.PostAsync("TempDataProperty/CreateForView", content);
// Assert 1
Assert.Equal(HttpStatusCode.Redirect, redirectResponse.StatusCode);
// Act 2
var response = await Client.SendAsync(GetRequest(redirectResponse.Headers.Location.ToString(), redirectResponse));
// Assert 2
Assert.Equal(HttpStatusCode.OK, response.StatusCode);
var body = await response.Content.ReadAsStringAsync();
Assert.Equal(expected, body.ToString().Trim());
}
[Fact]
public async Task TempDataPropertyAttribute_RetainsTempDataWithoutView()
{
// Arrange
var message = "Success (from Temp Data)";
var nameValueCollection = new List<KeyValuePair<string, string>>
{
new KeyValuePair<string, string>("FullName", "Bob"),
new KeyValuePair<string, string>("id", "1"),
};
var expected = $"{message} for person {nameValueCollection[0].Value} with id {nameValueCollection[1].Value}.";
var content = new FormUrlEncodedContent(nameValueCollection);
// Act 1
var redirectResponse = await Client.PostAsync("TempDataProperty/Create", content);
// Assert 1
Assert.Equal(HttpStatusCode.Redirect, redirectResponse.StatusCode);
// Act 2
var response = await Client.SendAsync(GetRequest(redirectResponse.Headers.Location.ToString(), redirectResponse));
// Assert 2
Assert.Equal(HttpStatusCode.OK, response.StatusCode);
var body = await response.Content.ReadAsStringAsync();
Assert.Equal(expected, body);
}
[Fact]
public async Task TempDataPropertyAttribute_TempDataKept()
{
// Arrange
var message = "Success (from Temp Data)";
var nameValueCollection = new List<KeyValuePair<string, string>>
{
new KeyValuePair<string, string>("FullName", "Bob"),
new KeyValuePair<string, string>("id", "1"),
};
var expected = $"{message} for person {nameValueCollection[0].Value} with id {nameValueCollection[1].Value}.";
var content = new FormUrlEncodedContent(nameValueCollection);
// Act 1
var response = await Client.PostAsync("TempDataProperty/CreateNoRedirect", content);
// Assert 1
Assert.Equal(HttpStatusCode.OK, response.StatusCode);
// Act 2
response = await Client.SendAsync(GetRequest("TempDataProperty/TempDataKept", response));
// Assert 2
Assert.Equal(HttpStatusCode.OK, response.StatusCode);
var body = await response.Content.ReadAsStringAsync();
Assert.Equal(message, body);
// Act 3
response = await Client.SendAsync(GetRequest("TempDataProperty/ReadTempData", response));
// Assert 3
Assert.Equal(HttpStatusCode.OK, response.StatusCode);
body = await response.Content.ReadAsStringAsync();
Assert.Equal(message, body);
}
[Fact]
public async Task TempDataPropertyAttribute_TempDataNotKept()
{
// Arrange
var message = "Success (from Temp Data)";
var nameValueCollection = new List<KeyValuePair<string, string>>
{
new KeyValuePair<string, string>("FullName", "Bob"),
new KeyValuePair<string, string>("id", "1"),
};
var expected = $"{message} for person {nameValueCollection[0].Value} with id {nameValueCollection[1].Value}.";
var content = new FormUrlEncodedContent(nameValueCollection);
// Act 1
var response = await Client.PostAsync("TempDataProperty/CreateNoRedirect", content);
// Assert 1
Assert.Equal(HttpStatusCode.OK, response.StatusCode);
// Act 2
response = await Client.SendAsync(GetRequest("TempDataProperty/ReadTempData", response));
// Assert 2
Assert.Equal(HttpStatusCode.OK, response.StatusCode);
var body = await response.Content.ReadAsStringAsync();
Assert.Equal(message, body);
// Act 3
response = await Client.SendAsync(GetRequest("TempDataProperty/ReadTempData", response));
// Assert 3
body = await response.Content.ReadAsStringAsync();
Assert.Empty(body);
}
private HttpRequestMessage GetRequest(string path, HttpResponseMessage response)
{
var request = new HttpRequestMessage(HttpMethod.Get, path);
if (response.Headers.TryGetValues("Set-Cookie", out var values))
{
foreach (var cookie in SetCookieHeaderValue.ParseList(values.ToList()))
{
if (cookie.Expires == null || cookie.Expires >= DateTimeOffset.UtcNow)
{
request.Headers.Add("Cookie", new CookieHeaderValue(cookie.Name, cookie.Value).ToString());
}
}
}
return request;
}
}
}

View File

@ -292,7 +292,7 @@ namespace Microsoft.AspNetCore.Mvc.RazorPages.Infrastructure
filterDescriptor =>
{
Assert.Equal(FilterScope.Action, filterDescriptor.Scope);
Assert.IsType<SaveTempDataPropertyFilter>(filterDescriptor.Filter);
Assert.IsType<SaveTempDataPropertyFilterFactory>(filterDescriptor.Filter);
},
filterDescriptor =>
{
@ -343,7 +343,7 @@ namespace Microsoft.AspNetCore.Mvc.RazorPages.Infrastructure
filterDescriptor =>
{
Assert.Equal(FilterScope.Action, filterDescriptor.Scope);
Assert.IsType<SaveTempDataPropertyFilter>(filterDescriptor.Filter);
Assert.IsType<SaveTempDataPropertyFilterFactory>(filterDescriptor.Filter);
},
filterDescriptor =>
{
@ -396,7 +396,7 @@ namespace Microsoft.AspNetCore.Mvc.RazorPages.Infrastructure
filterDescriptor =>
{
Assert.Equal(FilterScope.Action, filterDescriptor.Scope);
Assert.IsType<SaveTempDataPropertyFilter>(filterDescriptor.Filter);
Assert.IsType<SaveTempDataPropertyFilterFactory>(filterDescriptor.Filter);
},
filterDescriptor =>
{

View File

@ -14,6 +14,7 @@ using Microsoft.AspNetCore.Mvc.ModelBinding;
using Microsoft.AspNetCore.Mvc.Razor;
using Microsoft.AspNetCore.Mvc.RazorPages.Infrastructure;
using Microsoft.AspNetCore.Mvc.ViewFeatures;
using Microsoft.AspNetCore.Mvc.ViewFeatures.Internal;
using Microsoft.AspNetCore.Razor.Evolution;
using Microsoft.AspNetCore.Routing;
using Microsoft.Extensions.Logging.Abstractions;

View File

@ -15,6 +15,7 @@ using Microsoft.AspNetCore.Mvc.Razor;
using Microsoft.AspNetCore.Mvc.RazorPages.Infrastructure;
using Microsoft.AspNetCore.Mvc.ViewEngines;
using Microsoft.AspNetCore.Mvc.ViewFeatures;
using Microsoft.AspNetCore.Mvc.ViewFeatures.Internal;
using Microsoft.AspNetCore.Routing;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;

View File

@ -26,7 +26,7 @@ namespace Microsoft.AspNetCore.Mvc.RazorPages.Internal
// Assert
Assert.Collection(applicationModel.Filters,
filter => Assert.IsType<SaveTempDataPropertyFilter>(filter),
filter => Assert.IsType<SaveTempDataPropertyFilterFactory>(filter),
filter => Assert.IsType<AutoValidateAntiforgeryTokenAttribute>(filter));
}
}

View File

@ -409,6 +409,7 @@ namespace Microsoft.AspNetCore.Mvc
typeof(DefaultApplicationModelProvider),
typeof(CorsApplicationModelProvider),
typeof(AuthorizationApplicationModelProvider),
typeof(TempDataApplicationModelProvider),
}
},
{

View File

@ -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.Text;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Http.Features;

View File

@ -0,0 +1,125 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System.Collections.Generic;
using System.Reflection;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc.Abstractions;
using Microsoft.AspNetCore.Mvc.Filters;
using Microsoft.AspNetCore.Routing;
using Microsoft.Extensions.Internal;
using Moq;
using Xunit;
namespace Microsoft.AspNetCore.Mvc.ViewFeatures.Internal
{
public class SaveTempDataPropertyFilterTest
{
[Fact]
public void SaveTempDataPropertyFilter_PopulatesTempDataWithValuesFromControllerProperty()
{
// Arrange
var httpContext = new DefaultHttpContext();
var tempData = new TempDataDictionary(httpContext, Mock.Of<ITempDataProvider>())
{
["TempDataProperty-Test"] = "FirstValue"
};
var factory = new Mock<ITempDataDictionaryFactory>();
factory.Setup(f => f.GetTempData(httpContext))
.Returns(tempData);
var filter = new SaveTempDataPropertyFilter(factory.Object);
var controller = new TestController();
var controllerType = controller.GetType().GetTypeInfo();
var propertyHelper1 = new PropertyHelper(controllerType.GetProperty(nameof(TestController.Test)));
var propertyHelper2 = new PropertyHelper(controllerType.GetProperty(nameof(TestController.Test2)));
var propertyHelpers = new List<PropertyHelper>
{
propertyHelper1,
propertyHelper2,
};
filter.PropertyHelpers = propertyHelpers;
var context = new ActionExecutingContext(
new ActionContext
{
HttpContext = httpContext,
RouteData = new RouteData(),
ActionDescriptor = new ActionDescriptor(),
},
new List<IFilterMetadata>(),
new Dictionary<string, object>(),
controller);
// Act
filter.OnActionExecuting(context);
controller.Test = "SecondValue";
filter.OnTempDataSaving(tempData);
// Assert
Assert.Equal("SecondValue", controller.Test);
Assert.Equal("SecondValue", tempData["TempDataProperty-Test"]);
Assert.Equal(0, controller.Test2);
}
[Fact]
public void SaveTempDataPropertyFilter_ReadsTempDataFromTempDataDictionary()
{
// Arrange
var httpContext = new DefaultHttpContext();
var tempData = new TempDataDictionary(httpContext, Mock.Of<ITempDataProvider>())
{
["TempDataProperty-Test"] = "FirstValue"
};
var factory = new Mock<ITempDataDictionaryFactory>();
factory.Setup(f => f.GetTempData(httpContext))
.Returns(tempData);
var filter = new SaveTempDataPropertyFilter(factory.Object);
var controller = new TestController();
var controllerType = controller.GetType().GetTypeInfo();
var propertyHelper1 = new PropertyHelper(controllerType.GetProperty(nameof(TestController.Test)));
var propertyHelper2 = new PropertyHelper(controllerType.GetProperty(nameof(TestController.Test2)));
var propertyHelpers = new List<PropertyHelper>
{
propertyHelper1,
propertyHelper2,
};
filter.PropertyHelpers = propertyHelpers;
var context = new ActionExecutingContext(
new ActionContext
{
HttpContext = httpContext,
RouteData = new RouteData(),
ActionDescriptor = new ActionDescriptor(),
},
new List<IFilterMetadata>(),
new Dictionary<string, object>(),
controller);
// Act
filter.OnActionExecuting(context);
filter.OnTempDataSaving(tempData);
// Assert
Assert.Equal("FirstValue", controller.Test);
Assert.Equal(0, controller.Test2);
}
public class TestController : Controller
{
[TempData]
public string Test { get; set; }
[TempData]
public int Test2 { get; set; }
}
}
}

View File

@ -0,0 +1,148 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System;
using System.Collections.Generic;
using System.Linq;
using System.Reflection;
using Microsoft.AspNetCore.Mvc.ApplicationModels;
using Microsoft.AspNetCore.Mvc.Internal;
using Xunit;
namespace Microsoft.AspNetCore.Mvc.ViewFeatures.Internal
{
public class TempDataApplicationModelProviderTest
{
[Theory]
[InlineData(typeof(TestController_OneTempDataProperty))]
[InlineData(typeof(TestController_TwoTempDataProperties))]
public void AddsTempDataPropertyFilter_ForTempDataAttributeProperties(Type type)
{
// Arrange
var provider = new TempDataApplicationModelProvider();
var defaultProvider = new DefaultApplicationModelProvider(new TestOptionsManager<MvcOptions>());
var context = new ApplicationModelProviderContext(new[] { type.GetTypeInfo() });
defaultProvider.OnProvidersExecuting(context);
// Act
provider.OnProvidersExecuting(context);
// Assert
var controller = Assert.Single(context.Result.Controllers);
Assert.Single(controller.Filters, f => f is SaveTempDataPropertyFilterFactory);
}
[Fact]
public void InitializeFilterFactory_WithExpectedPropertyHelpers_ForTempDataAttributeProperties()
{
// Arrange
var provider = new TempDataApplicationModelProvider();
var defaultProvider = new DefaultApplicationModelProvider(new TestOptionsManager<MvcOptions>());
var context = new ApplicationModelProviderContext(new[] { typeof(TestController_OneTempDataProperty).GetTypeInfo() });
defaultProvider.OnProvidersExecuting(context);
// Act
provider.OnProvidersExecuting(context);
var controller = context.Result.Controllers.SingleOrDefault();
var filter = controller.Filters.OfType<SaveTempDataPropertyFilterFactory>();
var saveTempDataPropertyFilterFactory = filter.SingleOrDefault();
var expected = typeof(TestController_OneTempDataProperty).GetProperty(nameof(TestController_OneTempDataProperty.Test2));
// Assert
Assert.NotNull(saveTempDataPropertyFilterFactory);
var tempDataPropertyHelper = Assert.Single(saveTempDataPropertyFilterFactory.TempDataProperties);
Assert.Same(expected, tempDataPropertyHelper.Property);
}
[Fact]
public void DoesNotInitializeFilterFactory_ThrowsInvalidOperationException_NonPrimitiveType()
{
// Arrange
var provider = new TempDataApplicationModelProvider();
var defaultProvider = new DefaultApplicationModelProvider(new TestOptionsManager<MvcOptions>());
var context = new ApplicationModelProviderContext(new[] { typeof(TestController_OneValid_OneInvalidProperty).GetTypeInfo() });
defaultProvider.OnProvidersExecuting(context);
// Act
var exception = Assert.Throws<InvalidOperationException>(() =>
provider.OnProvidersExecuting(context));
Assert.Equal($"The '{typeof(TestController_OneValid_OneInvalidProperty).FullName}.{nameof(TestController_OneValid_OneInvalidProperty.Test2)}' property with {nameof(TempDataAttribute)} is invalid. A property using {nameof(TempDataAttribute)} must be of primitive or string type.", exception.Message);
}
[Fact]
public void ThrowsInvalidOperationException_PrivateSetter()
{
// Arrange
var provider = new TempDataApplicationModelProvider();
var defaultProvider = new DefaultApplicationModelProvider(new TestOptionsManager<MvcOptions>());
var context = new ApplicationModelProviderContext(new[] { typeof(TestController_PrivateSet).GetTypeInfo() });
defaultProvider.OnProvidersExecuting(context);
// Act & Assert
var exception = Assert.Throws<InvalidOperationException>(() =>
provider.OnProvidersExecuting(context));
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);
}
[Fact]
public void ThrowsInvalidOperationException_NonPrimitiveType()
{
// Arrange
var provider = new TempDataApplicationModelProvider();
var defaultProvider = new DefaultApplicationModelProvider(new TestOptionsManager<MvcOptions>());
var context = new ApplicationModelProviderContext(new[] { typeof(TestController_NonPrimitiveType).GetTypeInfo() });
defaultProvider.OnProvidersExecuting(context);
// Act & Assert
var exception = Assert.Throws<InvalidOperationException>(() =>
provider.OnProvidersExecuting(context));
Assert.Equal($"The '{typeof(TestController_NonPrimitiveType).FullName}.{nameof(TestController_NonPrimitiveType.Test)}' property with {nameof(TempDataAttribute)} is invalid. A property using {nameof(TempDataAttribute)} must be of primitive or string type.", exception.Message);
}
public class TestController_OneTempDataProperty
{
public string Test { get; set; }
[TempData]
public string Test2 { get; set; }
}
public class TestController_TwoTempDataProperties
{
[TempData]
public string Test { get; set; }
[TempData]
public int Test2 { get; set; }
}
public class TestController_OneValid_OneInvalidProperty
{
[TempData]
public int Test { get; set; }
[TempData]
public IList<string> Test2 { get; set; }
}
public class TestController_PrivateSet
{
[TempData]
public string Test { get; private set; }
}
public class TestController_NonPrimitiveType
{
[TempData]
public object Test { get; set; }
}
}
}

View File

@ -0,0 +1,114 @@
// 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.AspNetCore.Http;
using Moq;
using Xunit;
namespace Microsoft.AspNetCore.Mvc.ViewFeatures.Internal
{
public class TempDataPropertyProviderTest
{
[Fact]
public void LoadAndTrackChanges_SetsPropertyValue()
{
// Arrange
var provider = new TempDataPropertyProvider();
var tempData = new TempDataDictionary(new DefaultHttpContext(), new NullTempDataProvider());
tempData["TempDataProperty-TestString"] = "Value";
tempData.Save();
var controller = new TestController()
{
TempData = tempData,
};
// Act
provider.LoadAndTrackChanges(controller, controller.TempData);
// Assert
Assert.Equal("Value", controller.TestString);
Assert.Null(controller.TestString2);
}
[Fact]
public void LoadAndTrackChanges_ThrowsInvalidOperationException_PrivateSetter()
{
// Arrange
var provider = new TempDataPropertyProvider();
var tempData = new TempDataDictionary(new DefaultHttpContext(), new NullTempDataProvider());
tempData["TempDataProperty-Test"] = "Value";
tempData.Save();
var controller = new TestController_PrivateSet()
{
TempData = tempData,
};
// Act & Assert
var exception = Assert.Throws<InvalidOperationException>(() =>
provider.LoadAndTrackChanges(controller, controller.TempData));
Assert.Equal("TempData properties must have a public getter and setter.", exception.Message);
}
[Fact]
public void LoadAndTrackChanges_ThrowsInvalidOperationException_NonPrimitiveType()
{
// Arrange
var provider = new TempDataPropertyProvider();
var tempData = new TempDataDictionary(new DefaultHttpContext(), new NullTempDataProvider());
tempData["TempDataProperty-Test"] = new object();
tempData.Save();
var controller = new TestController_NonPrimitiveType()
{
TempData = tempData,
};
// Act & Assert
var exception = Assert.Throws<InvalidOperationException>(() =>
provider.LoadAndTrackChanges(controller, controller.TempData));
Assert.Equal("TempData properties must be declared as primitive types or string only.", exception.Message);
}
public class TestController : Controller
{
[TempData]
public string TestString { get; set; }
[TempData]
public string TestString2 { get; set; }
}
public class TestController_PrivateSet : Controller
{
[TempData]
public string Test { get; private set; }
}
public class TestController_NonPrimitiveType : Controller
{
[TempData]
public object Test { get; set; }
}
private class NullTempDataProvider : ITempDataProvider
{
public IDictionary<string, object> LoadTempData(HttpContext context)
{
return null;
}
public void SaveTempData(HttpContext context, IDictionary<string, object> values)
{
}
}
}
}

View File

@ -0,0 +1,58 @@
// 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 BasicWebSite.Models;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.ViewFeatures;
using System.Net;
namespace BasicWebSite.Controllers
{
public class TempDataPropertyController : Controller
{
[TempData]
public string Message { get; set; }
[HttpPost]
public IActionResult CreateForView(Person person)
{
Message = "Success (from Temp Data)";
return RedirectToAction("DetailsView", person);
}
[HttpPost]
public IActionResult Create(Person person)
{
Message = "Success (from Temp Data)";
return RedirectToAction("Details", person);
}
public IActionResult DetailsView(Person person)
{
ViewData["Message"] = Message;
return View(person);
}
public string Details(Person person)
{
return $"{Message} for person {person.FullName} with id {person.id}.";
}
public StatusCodeResult CreateNoRedirect(Person person)
{
Message = "Success (from Temp Data)";
return new OkResult();
}
public string TempDataKept()
{
TempData.Keep();
return Message;
}
public string ReadTempData()
{
return Message;
}
}
}

View File

@ -0,0 +1,2 @@
@model BasicWebSite.Models.Person
@ViewData["Message"] for person @Model.FullName with id @Model.id.