Fix [TempData] for PageModel's

This commit is contained in:
Ryan Brandenburg 2017-04-04 17:04:17 -07:00
parent 1a8ac88da7
commit 59a3aade9b
25 changed files with 835 additions and 242 deletions

View File

@ -81,6 +81,8 @@ namespace Microsoft.Extensions.DependencyInjection
// Action executors
services.TryAddSingleton<PageResultExecutor>();
services.TryAddSingleton<RedirectToPageResultExecutor>();
services.TryAddTransient<PageSaveTempDataPropertyFilter>();
}
}
}

View File

@ -14,6 +14,28 @@ namespace Microsoft.Extensions.DependencyInjection
/// </summary>
public static class RazorPagesOptionsExtensions
{
/// <summary>
/// Configures the specified <paramref name="factory"/> to apply filters to all Razor Pages.
/// </summary>
/// <param name="options">The <see cref="RazorPagesOptions"/> to configure.</param>
/// <param name="factory">The factory to create filters.</param>
/// <returns></returns>
public static RazorPagesOptions ConfigureFilter(this RazorPagesOptions options, Func<PageApplicationModel, IFilterMetadata> factory)
{
if (options == null)
{
throw new ArgumentNullException(nameof(options));
}
if (factory == null)
{
throw new ArgumentNullException(nameof(factory));
}
options.Conventions.Add(new FolderConvention("/", model => model.Filters.Add(factory(model))));
return options;
}
/// <summary>
/// Configures the specified <paramref name="filter"/> to apply to all Razor Pages.
/// </summary>

View File

@ -346,10 +346,10 @@ namespace Microsoft.AspNetCore.Mvc.RazorPages.Internal
}
// This is a workaround for not yet having proper filter for Pages.
SaveTempDataPropertyFilter propertyFilter = null;
PageSaveTempDataPropertyFilter propertyFilter = null;
for (var i = 0; i < _filters.Length; i++)
{
propertyFilter = _filters[i] as SaveTempDataPropertyFilter;
propertyFilter = _filters[i] as PageSaveTempDataPropertyFilter;
if (propertyFilter != null)
{
break;
@ -358,7 +358,14 @@ namespace Microsoft.AspNetCore.Mvc.RazorPages.Internal
if (propertyFilter != null)
{
propertyFilter.Subject = _page;
object subject = _page;
if (_model != null)
{
subject = _model;
}
propertyFilter.Subject = subject;
propertyFilter.ApplyTempDataChanges(_pageContext.HttpContext);
}

View File

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

View File

@ -201,7 +201,9 @@ namespace Microsoft.Extensions.DependencyInjection
services.TryAddEnumerable(
ServiceDescriptor.Transient<IApplicationModelProvider, TempDataApplicationModelProvider>());
services.TryAddSingleton<SaveTempDataFilter>();
services.TryAddTransient<SaveTempDataPropertyFilter>();
services.TryAddTransient<ControllerSaveTempDataPropertyFilter>();
// This does caching so it should stay singleton
services.TryAddSingleton<ITempDataProvider, CookieTempDataProvider>();

View File

@ -1,114 +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.Collections.Generic;
using System.Reflection;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc.Filters;
using Microsoft.Extensions.Internal;
namespace Microsoft.AspNetCore.Mvc.ViewFeatures.Internal
{
public class SaveTempDataPropertyFilter : ISaveTempDataCallback, IActionFilter
{
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; }
public IDictionary<PropertyInfo, object> OriginalValues { get; set; }
/// <summary>
/// Puts the modified values of <see cref="Subject"/> into <paramref name="tempData"/>.
/// </summary>
/// <param name="tempData">The <see cref="ITempDataDictionary"/> to be updated.</param>
public void OnTempDataSaving(ITempDataDictionary tempData)
{
if (Subject != null && OriginalValues != null)
{
foreach (var kvp in OriginalValues)
{
var property = kvp.Key;
var originalValue = kvp.Value;
var newValue = property.GetValue(Subject);
if (newValue != null && !newValue.Equals(originalValue))
{
tempData[Prefix + property.Name] = newValue;
}
}
}
}
/// <summary>
/// Applies values from TempData from <paramref name="httpContext"/> to the <see cref="Subject"/>.
/// </summary>
/// <param name="httpContext">The <see cref="HttpContext"/> used to find TempData.</param>
public void ApplyTempDataChanges(HttpContext httpContext)
{
if (Subject == null)
{
throw new ArgumentNullException(nameof(Subject));
}
var tempData = _factory.GetTempData(httpContext);
if (OriginalValues == null)
{
OriginalValues = new Dictionary<PropertyInfo, object>();
}
SetPropertyVaules(tempData, Subject);
}
/// <inheritdoc />
public void OnActionExecuting(ActionExecutingContext context)
{
Subject = context.Controller;
var tempData = _factory.GetTempData(context.HttpContext);
OriginalValues = new Dictionary<PropertyInfo, object>();
SetPropertyVaules(tempData, Subject);
}
/// <inheritdoc />
public void OnActionExecuted(ActionExecutedContext context)
{
}
private void SetPropertyVaules(ITempDataDictionary tempData, object subject)
{
if (PropertyHelpers == null)
{
return;
}
for (var i = 0; i < PropertyHelpers.Count; i++)
{
var property = PropertyHelpers[i];
var value = tempData[Prefix + property.Name];
OriginalValues[property.Property] = value;
var propertyTypeInfo = property.Property.PropertyType.GetTypeInfo();
var isReferenceTypeOrNullable = !propertyTypeInfo.IsValueType || Nullable.GetUnderlyingType(property.GetType()) != null;
if (value != null || isReferenceTypeOrNullable)
{
property.SetValue(subject, value);
}
}
}
}
}

View File

@ -0,0 +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 Microsoft.AspNetCore.Mvc.Filters;
namespace Microsoft.AspNetCore.Mvc.ViewFeatures.Internal
{
public class ControllerSaveTempDataPropertyFilter : SaveTempDataPropertyFilterBase, IActionFilter
{
public ControllerSaveTempDataPropertyFilter(ITempDataDictionaryFactory factory)
: base(factory)
{
}
public void OnActionExecuted(ActionExecutedContext context)
{
}
/// <inheritdoc />
public void OnActionExecuting(ActionExecutingContext context)
{
Subject = context.Controller;
var tempData = _factory.GetTempData(context.HttpContext);
SetPropertyVaules(tempData, Subject);
}
}
}

View File

@ -5,14 +5,12 @@ 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
public class ControllerSaveTempDataPropertyFilterFactory : IFilterFactory
{
// Cannot be public as <c>PropertyHelper</c> is an internal shared source type
internal IList<PropertyHelper> TempDataProperties { get; set; }
public IList<TempDataProperty> TempDataProperties { get; set; }
public bool IsReusable => false;
@ -23,8 +21,8 @@ namespace Microsoft.AspNetCore.Mvc.ViewFeatures.Internal
throw new ArgumentNullException(nameof(serviceProvider));
}
var service = serviceProvider.GetRequiredService<SaveTempDataPropertyFilter>();
service.PropertyHelpers = TempDataProperties;
var service = serviceProvider.GetRequiredService<ControllerSaveTempDataPropertyFilter>();
service.TempDataProperties = TempDataProperties;
return service;
}
}

View File

@ -0,0 +1,65 @@
// 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.Http;
namespace Microsoft.AspNetCore.Mvc.ViewFeatures.Internal
{
public class PageSaveTempDataPropertyFilter : SaveTempDataPropertyFilterBase
{
public PageSaveTempDataPropertyFilter(ITempDataDictionaryFactory factory)
: base(factory)
{
}
public PageSaveTempDataPropertyFilterFactory FilterFactory { get; set; }
public override object Subject {
get => base.Subject;
set
{
base.Subject = value;
SetTempDataProperties(value.GetType());
}
}
private void SetTempDataProperties(Type type)
{
if (type == null)
{
throw new ArgumentNullException(nameof(type));
}
if (FilterFactory == null)
{
throw new InvalidOperationException(
Resources.FormatPropertyOfTypeCannotBeNull(
nameof(FilterFactory),
typeof(PageSaveTempDataPropertyFilter).Name));
}
TempDataProperties = FilterFactory.GetTempDataProperties(type);
}
/// <summary>
/// Applies values from TempData from <paramref name="httpContext"/> to the
/// <see cref="SaveTempDataPropertyFilterBase.Subject"/>.
/// </summary>
/// <param name="httpContext">The <see cref="HttpContext"/> used to find TempData.</param>
public void ApplyTempDataChanges(HttpContext httpContext)
{
if (Subject == null)
{
throw new InvalidOperationException(
Resources.FormatPropertyOfTypeCannotBeNull(
nameof(Subject),
typeof(PageSaveTempDataPropertyFilter).Name));
}
var tempData = _factory.GetTempData(httpContext);
SetPropertyVaules(tempData, Subject);
}
}
}

View File

@ -0,0 +1,43 @@
// 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;
namespace Microsoft.AspNetCore.Mvc.ViewFeatures.Internal
{
public class PageSaveTempDataPropertyFilterFactory : IFilterFactory
{
public IList<TempDataProperty> TempDataProperties { get; set; }
public bool IsReusable => false;
public IFilterMetadata CreateInstance(IServiceProvider serviceProvider)
{
if (serviceProvider == null)
{
throw new ArgumentNullException(nameof(serviceProvider));
}
var service = serviceProvider.GetRequiredService<PageSaveTempDataPropertyFilter>();
service.FilterFactory = this;
return service;
}
public IList<TempDataProperty> 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 (TempDataProperties == null)
{
TempDataProperties = SaveTempDataPropertyFilterBase.GetTempDataProperties(modelType);
}
return TempDataProperties;
}
}
}

View File

@ -0,0 +1,134 @@
// 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.Extensions.Internal;
namespace Microsoft.AspNetCore.Mvc.ViewFeatures.Internal
{
public abstract class SaveTempDataPropertyFilterBase : ISaveTempDataCallback
{
protected const string Prefix = "TempDataProperty-";
protected readonly ITempDataDictionaryFactory _factory;
/// <summary>
/// Describes the temp data properties which exist on <see cref="Subject"/>
/// </summary>
public IList<TempDataProperty> TempDataProperties { get; set; }
/// <summary>
/// The <see cref="object"/> which has the temp data properties.
/// </summary>
public virtual object Subject { get; set; }
/// <summary>
/// Tracks the values which originally existed in temp data.
/// </summary>
public IDictionary<PropertyInfo, object> OriginalValues { get; } = new Dictionary<PropertyInfo, object>();
public SaveTempDataPropertyFilterBase(ITempDataDictionaryFactory factory)
{
_factory = factory;
}
/// <summary>
/// Puts the modified values of <see cref="Subject"/> into <paramref name="tempData"/>.
/// </summary>
/// <param name="tempData">The <see cref="ITempDataDictionary"/> to be updated.</param>
public void OnTempDataSaving(ITempDataDictionary tempData)
{
if (Subject != null && OriginalValues != null)
{
foreach (var kvp in OriginalValues)
{
var property = kvp.Key;
var originalValue = kvp.Value;
var newValue = property.GetValue(Subject);
if (newValue != null && !newValue.Equals(originalValue))
{
tempData[Prefix + property.Name] = newValue;
}
}
}
}
public static IList<TempDataProperty> GetTempDataProperties(Type type)
{
List<TempDataProperty> results = null;
var propertyHelpers = PropertyHelper.GetVisibleProperties(type: type);
for (var i = 0; i < propertyHelpers.Length; i++)
{
var propertyHelper = propertyHelpers[i];
if (propertyHelper.Property.IsDefined(typeof(TempDataAttribute)))
{
ValidateProperty(propertyHelper);
if (results == null)
{
results = new List<TempDataProperty>();
}
results.Add(new TempDataProperty(
propertyHelper.Property,
propertyHelper.GetValue,
propertyHelper.SetValue));
}
}
return results;
}
private static 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)));
}
}
/// <summary>
/// Sets the values of the properties of <paramref name="subject"/> from <paramref name="tempData"/>.
/// </summary>
/// <param name="tempData">The <see cref="ITempDataDictionary"/> with the data to set on <paramref name="subject"/>.</param>
/// <param name="subject">The <see cref="object"/> which will have it's properties set.</param>
protected void SetPropertyVaules(ITempDataDictionary tempData, object subject)
{
if (TempDataProperties == null)
{
return;
}
for (var i = 0; i < TempDataProperties.Count; i++)
{
var property = TempDataProperties[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);
}
}
}
}
}

View File

@ -0,0 +1,34 @@
// 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 Func<object, object> _getter;
private Action<object, object> _setter;
public TempDataProperty(PropertyInfo propertyInfo, Func<object, object> getter, Action<object, object> setter)
{
PropertyInfo = propertyInfo;
_getter = getter;
_setter = setter;
}
public PropertyInfo PropertyInfo { get; }
public object GetValue(object obj)
{
return _getter(obj);
}
public void SetValue(object obj, object value)
{
_setter(obj, value);
}
}
}

View File

@ -2,11 +2,8 @@
// 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
{
@ -31,50 +28,20 @@ namespace Microsoft.AspNetCore.Mvc.ViewFeatures.Internal
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 modelType = controllerModel.ControllerType.AsType();
var tempDataProperties = SaveTempDataPropertyFilterBase.GetTempDataProperties(modelType);
if (tempDataProperties != null)
{
var propertyHelper = propertyHelpers[i];
if (propertyHelper.Property.IsDefined(typeof(TempDataAttribute)))
var factory = new ControllerSaveTempDataPropertyFilterFactory()
{
ValidateProperty(propertyHelper);
if (factory == null)
{
factory = new SaveTempDataPropertyFilterFactory()
{
TempDataProperties = new List<PropertyHelper>()
};
}
TempDataProperties = tempDataProperties
};
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

@ -171,7 +171,7 @@ namespace Microsoft.AspNetCore.Mvc.FunctionalTests
{
// Arrange
var routeRequest = new HttpRequestMessage(HttpMethod.Get, "http://localhost/RouteData/pizza");
// Act
var routeResponse = await Client.SendAsync(routeRequest);
@ -413,6 +413,69 @@ namespace Microsoft.AspNetCore.Mvc.FunctionalTests
Assert.Equal("Hi2", content.Trim());
}
[Fact]
public async Task TempData_TempDataPropertyOnPageModel_IsPopulatedFromTempData()
{
// Arrange 1
var url = "http://localhost/TempData/SetMessageAndRedirect";
var request = new HttpRequestMessage(HttpMethod.Get, url);
// Act 1
var response = await Client.SendAsync(request);
// Assert 1
Assert.Equal(HttpStatusCode.Redirect, response.StatusCode);
// Act 2
request = new HttpRequestMessage(HttpMethod.Get, response.Headers.Location);
request.Headers.Add("Cookie", GetCookie(response));
response = await Client.SendAsync(request);
// Assert 2
Assert.Equal(HttpStatusCode.OK, response.StatusCode);
var content = await response.Content.ReadAsStringAsync();
Assert.StartsWith("Message: Secret Message", content.Trim());
Assert.EndsWith("TempData: Secret Message", content.Trim());
}
[Fact]
public async Task TempData_TempDataPropertyOnPageModel_PopulatesTempData()
{
// Arrange 1
var getRequest = new HttpRequestMessage(HttpMethod.Get, "http://localhost/TempData/TempDataPageModelProperty");
var getResponse = await Client.SendAsync(getRequest);
var getResponseBody = await getResponse.Content.ReadAsStringAsync();
var formToken = AntiforgeryTestHelper.RetrieveAntiforgeryToken(getResponseBody, "/TempData/TempDataPageModelProperty");
var cookie = AntiforgeryTestHelper.RetrieveAntiforgeryCookie(getResponse);
var url = "http://localhost/TempData/TempDataPageModelProperty";
var request = new HttpRequestMessage(HttpMethod.Post, url);
request.Headers.Add("Cookie", cookie.Key + "=" + cookie.Value);
request.Headers.Add("RequestVerificationToken", formToken);
// Act 1
var response = await Client.SendAsync(request);
// Assert 1
Assert.Equal(HttpStatusCode.OK, response.StatusCode);
var content = await response.Content.ReadAsStringAsync();
Assert.StartsWith("Message: Secret post", content.Trim());
Assert.EndsWith("TempData:", content.Trim());
// Arrange 2
request = new HttpRequestMessage(HttpMethod.Get, "http://localhost/TempData/TempDataPageModelProperty");
request.Headers.Add("Cookie", GetCookie(response));
// Act 2
response = await Client.SendAsync(request);
// Assert 2
Assert.Equal(HttpStatusCode.OK, response.StatusCode);
content = await response.Content.ReadAsStringAsync();
Assert.StartsWith("Message: Secret post", content.Trim());
Assert.EndsWith("TempData: Secret post", content.Trim());
}
[Fact]
public async Task AuthorizePage_AddsAuthorizationForSpecificPages()
{
@ -432,7 +495,7 @@ namespace Microsoft.AspNetCore.Mvc.FunctionalTests
{
// Arrange
var url = "/Pages/Admin/Login";
// Act
var response = await Client.GetAsync(url);
@ -677,7 +740,7 @@ Microsoft.AspNetCore.Mvc.ViewFeatures.ViewDataDictionary`1[AspNetCore._InjectedP
Assert.Equal(expected, response.Headers.Location.ToString());
}
[Fact]
public async Task RedirectToSelfWorks()
{

View File

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

View File

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

View File

@ -0,0 +1,50 @@
// 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;
namespace Microsoft.AspNetCore.Mvc.ViewFeatures.Internal
{
public class ControllerSaveTempDataPropertyFilterFactoryTest
{
[Fact]
public void CreateInstance_CreatesFilter()
{
// Arrange
var factory = new ControllerSaveTempDataPropertyFilterFactory();
var propertyInfo = typeof(StringController).GetProperty("StringProp");
factory.TempDataProperties = new List<TempDataProperty>()
{
new TempDataProperty(propertyInfo, null, null)
};
// Act
var filter = factory.CreateInstance(CreateServiceProvider());
// Assert
var controllerFilter = Assert.IsType<ControllerSaveTempDataPropertyFilter>(filter);
Assert.Collection(controllerFilter.TempDataProperties,
property => Assert.Equal("StringProp", property.PropertyInfo.Name));
}
private ServiceProvider CreateServiceProvider()
{
var serviceCollection = new ServiceCollection();
serviceCollection.AddSingleton(Mock.Of<ITempDataProvider>());
serviceCollection.AddSingleton<ITempDataDictionaryFactory, TempDataDictionaryFactory>();
serviceCollection.AddTransient<ControllerSaveTempDataPropertyFilter>();
return serviceCollection.BuildServiceProvider();
}
private class StringController
{
public string StringProp { get; set; }
}
}
}

View File

@ -2,21 +2,19 @@
// 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
public class ControllerSaveTempDataPropertyFilterTest
{
[Fact]
public void SaveTempDataPropertyFilter_PopulatesTempDataWithValuesFromControllerProperty()
public void PopulatesTempDataWithValuesFromControllerProperty()
{
// Arrange
var httpContext = new DefaultHttpContext();
@ -25,11 +23,20 @@ namespace Microsoft.AspNetCore.Mvc.ViewFeatures.Internal
["TempDataProperty-Test"] = "FirstValue"
};
var filter = CreateSaveTempDataPropertyFilter(httpContext, tempData);
var filter = CreateControllerSaveTempDataPropertyFilter(httpContext, tempData);
var controller = new TestController();
filter.PropertyHelpers = BuildPropertyHelpers<TestController>();
var controllerType = controller.GetType();
var testProp = controllerType.GetProperty(nameof(TestController.Test));
var test2Prop = controllerType.GetProperty(nameof(TestController.Test2));
filter.TempDataProperties = new List<TempDataProperty>
{
new TempDataProperty(testProp, testProp.GetValue, testProp.SetValue),
new TempDataProperty(test2Prop, test2Prop.GetValue, test2Prop.SetValue)
};
var context = new ActionExecutingContext(
new ActionContext
{
@ -53,7 +60,7 @@ namespace Microsoft.AspNetCore.Mvc.ViewFeatures.Internal
}
[Fact]
public void SaveTempDataPropertyFilter_ReadsTempDataFromTempDataDictionary()
public void ReadsTempDataFromTempDataDictionary()
{
// Arrange
var httpContext = new DefaultHttpContext();
@ -62,10 +69,18 @@ namespace Microsoft.AspNetCore.Mvc.ViewFeatures.Internal
["TempDataProperty-Test"] = "FirstValue"
};
var filter = CreateSaveTempDataPropertyFilter(httpContext, tempData: tempData);
var filter = CreateControllerSaveTempDataPropertyFilter(httpContext, tempData: tempData);
var controller = new TestController();
filter.PropertyHelpers = BuildPropertyHelpers<TestController>();
var controllerType = controller.GetType();
var testProp = controllerType.GetProperty(nameof(TestController.Test));
var test2Prop = controllerType.GetProperty(nameof(TestController.Test2));
filter.TempDataProperties = new List<TempDataProperty>
{
new TempDataProperty(testProp, testProp.GetValue, testProp.SetValue),
new TempDataProperty(test2Prop, test2Prop.GetValue, test2Prop.SetValue)
};
var context = new ActionExecutingContext(
new ActionContext
@ -87,53 +102,7 @@ namespace Microsoft.AspNetCore.Mvc.ViewFeatures.Internal
Assert.Equal(0, controller.Test2);
}
[Fact]
public void ApplyTempDataChanges_SetsPropertyValue()
{
// Arrange
var httpContext = new DefaultHttpContext();
var tempData = new TempDataDictionary(httpContext, Mock.Of<ITempDataProvider>())
{
{ "TempDataProperty-Test", "Value" }
};
tempData.Save();
var controller = new TestControllerStrings()
{
TempData = tempData,
};
var provider = CreateSaveTempDataPropertyFilter(httpContext, tempData: tempData);
provider.Subject = controller;
provider.PropertyHelpers = BuildPropertyHelpers<TestControllerStrings>();
// Act
provider.ApplyTempDataChanges(httpContext);
// Assert
Assert.Equal("Value", controller.Test);
Assert.Null(controller.Test2);
}
private IList<PropertyHelper> BuildPropertyHelpers<TSubject>()
{
var subjectType = typeof(TSubject);
var properties = subjectType.GetProperties(
BindingFlags.Public | BindingFlags.Instance | BindingFlags.DeclaredOnly);
var result = new List<PropertyHelper>();
foreach (var property in properties)
{
result.Add(new PropertyHelper(property));
}
return result;
}
private SaveTempDataPropertyFilter CreateSaveTempDataPropertyFilter(
private ControllerSaveTempDataPropertyFilter CreateControllerSaveTempDataPropertyFilter(
HttpContext httpContext,
TempDataDictionary tempData)
{
@ -141,16 +110,7 @@ namespace Microsoft.AspNetCore.Mvc.ViewFeatures.Internal
factory.Setup(f => f.GetTempData(httpContext))
.Returns(tempData);
return new SaveTempDataPropertyFilter(factory.Object);
}
public class TestControllerStrings : Controller
{
[TempData]
public string Test { get; set; }
[TempData]
public string Test2 { get; set; }
return new ControllerSaveTempDataPropertyFilter(factory.Object);
}
public class TestController : Controller

View File

@ -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 Microsoft.Extensions.DependencyInjection;
using Moq;
using Xunit;
namespace Microsoft.AspNetCore.Mvc.ViewFeatures.Internal
{
public class PageSaveTempDataPropertyFilterFactoryTest
{
[Fact]
public void CreatesInstanceWithProperties()
{
// Arrange
var factory = new PageSaveTempDataPropertyFilterFactory();
var serviceProvider = CreateServiceProvider();
// Act
var filter = factory.CreateInstance(serviceProvider);
// Assert
var pageFilter = Assert.IsType<PageSaveTempDataPropertyFilter>(filter);
Assert.Same(factory, pageFilter.FilterFactory);
}
private ServiceProvider CreateServiceProvider()
{
var serviceCollection = new ServiceCollection();
serviceCollection.AddSingleton(Mock.Of<ITempDataProvider>());
serviceCollection.AddSingleton<ITempDataDictionaryFactory, TempDataDictionaryFactory>();
serviceCollection.AddTransient<PageSaveTempDataPropertyFilter>();
return serviceCollection.BuildServiceProvider();
}
}
}

View File

@ -0,0 +1,246 @@
// 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.Threading.Tasks;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc.Abstractions;
using Microsoft.AspNetCore.Mvc.ModelBinding;
using Microsoft.AspNetCore.Mvc.RazorPages;
using Microsoft.AspNetCore.Routing;
using Moq;
using Xunit;
namespace Microsoft.AspNetCore.Mvc.ViewFeatures.Internal
{
public class PageSaveTempDataPropertyFilterTest
{
[Fact]
public void OnTempDataSaving_PopulatesTempDataWithValuesFromPageProperty()
{
// Arrange
var httpContext = new DefaultHttpContext();
var tempData = new TempDataDictionary(httpContext, Mock.Of<ITempDataProvider>())
{
{ "TempDataProperty-Test", "TestString" }
};
tempData.Save();
var page = new TestPageString()
{
Test = "TestString",
ViewContext = CreateViewContext(httpContext, tempData)
};
var provider = CreatePageSaveTempDataPropertyFilter(httpContext, tempData: tempData);
provider.Subject = page;
var pageType = page.GetType();
var testProp = pageType.GetProperty(nameof(TestPageString.Test));
var test2Prop = pageType.GetProperty(nameof(TestPageString.Test2));
provider.TempDataProperties = new List<TempDataProperty>{
new TempDataProperty(testProp, testProp.GetValue, testProp.SetValue),
new TempDataProperty(test2Prop, test2Prop.GetValue, test2Prop.SetValue)
};
// Act
provider.OnTempDataSaving(tempData);
// Assert
Assert.Equal("TestString", page.Test);
Assert.Equal("TestString", page.TempData["TempDataProperty-Test"]);
}
[Fact]
public void SetSubject_NullFilterFactory_Throws()
{
// Arrange
var httpContext = new DefaultHttpContext();
var tempData = new TempDataDictionary(httpContext, Mock.Of<ITempDataProvider>());
tempData.Save();
var page = new TestPageString()
{
ViewContext = CreateViewContext(httpContext, tempData)
};
var provider = CreatePageSaveTempDataPropertyFilter(httpContext, tempData: tempData, filterFactory: false);
// Act & Assert
var ex = Assert.Throws<InvalidOperationException>(() => provider.Subject = page);
Assert.Contains("FilterFactory", ex.Message);
}
[Fact]
public void SetSubject_ModifiesFactoryAndFilter()
{
// Arrange
var httpContext = new DefaultHttpContext();
var tempData = new TempDataDictionary(httpContext, Mock.Of<ITempDataProvider>());
tempData.Save();
var page = new TestPageString()
{
Test = "TestString",
ViewContext = CreateViewContext(httpContext, tempData)
};
var provider = CreatePageSaveTempDataPropertyFilter(httpContext, tempData: tempData);
provider.FilterFactory = new PageSaveTempDataPropertyFilterFactory();
// Act
provider.Subject = page;
// Assert
Assert.Collection(provider.TempDataProperties,
property => Assert.Equal("Test", property.PropertyInfo.Name),
property => Assert.Equal("Test2", property.PropertyInfo.Name));
}
[Fact]
public void ApplyTempDataChanges_ToPageModel_SetsPropertyValue()
{
// Arrange
var httpContext = new DefaultHttpContext();
var tempData = new TempDataDictionary(httpContext, Mock.Of<ITempDataProvider>())
{
{ "TempDataProperty-Test", "Value" }
};
tempData.Save();
var page = new TestPageString()
{
ViewContext = CreateViewContext(httpContext, tempData)
};
var provider = CreatePageSaveTempDataPropertyFilter(httpContext, tempData: tempData);
provider.Subject = page;
var pageType = typeof(TestPageString);
var testProp = pageType.GetProperty("Test");
var test2Prop = pageType.GetProperty("Test2");
provider.TempDataProperties = new List<TempDataProperty> {
new TempDataProperty(testProp, testProp.GetValue, testProp.SetValue),
new TempDataProperty(test2Prop, test2Prop.GetValue, test2Prop.SetValue)
};
// Act
provider.ApplyTempDataChanges(httpContext);
// Assert
Assert.Equal("Value", page.Test);
Assert.Null(page.Test2);
}
[Fact]
public void ApplyTempDataChanges_ToPage_SetsPropertyValue()
{
// Arrange
var httpContext = new DefaultHttpContext();
var tempData = new TempDataDictionary(httpContext, Mock.Of<ITempDataProvider>())
{
{ "TempDataProperty-Test", "Value" }
};
tempData.Save();
var page = new TestPageString()
{
ViewContext = CreateViewContext(httpContext, tempData)
};
var provider = CreatePageSaveTempDataPropertyFilter(httpContext, tempData: tempData);
provider.Subject = page;
var pageType = page.GetType();
var testProp = pageType.GetProperty(nameof(TestPageString.Test));
var test2Prop = pageType.GetProperty(nameof(TestPageString.Test2));
provider.TempDataProperties = new List<TempDataProperty> {
new TempDataProperty(testProp, testProp.GetValue, testProp.SetValue),
new TempDataProperty(test2Prop, test2Prop.GetValue, test2Prop.SetValue)
};
// Act
provider.ApplyTempDataChanges(httpContext);
// Assert
Assert.Equal("Value", page.Test);
Assert.Null(page.Test2);
}
private static PageContext CreateViewContext(HttpContext httpContext, ITempDataDictionary tempData)
{
var actionContext = new ActionContext(httpContext, new RouteData(), new ActionDescriptor());
var metadataProvider = new EmptyModelMetadataProvider();
var viewData = new ViewDataDictionary(metadataProvider, new ModelStateDictionary());
var viewContext = new PageContext(
actionContext,
viewData,
tempData,
new HtmlHelperOptions());
return viewContext;
}
private PageSaveTempDataPropertyFilter CreatePageSaveTempDataPropertyFilter(
HttpContext httpContext,
TempDataDictionary tempData,
bool filterFactory = true)
{
var factory = new Mock<ITempDataDictionaryFactory>();
factory.Setup(f => f.GetTempData(httpContext))
.Returns(tempData);
var propertyFilter = new PageSaveTempDataPropertyFilter(factory.Object);
if (filterFactory)
{
propertyFilter.FilterFactory = new Mock<PageSaveTempDataPropertyFilterFactory>().Object;
}
return propertyFilter;
}
public class TestPageString : Page
{
[TempData]
public string Test { get; set; }
[TempData]
public string Test2 { get; set; }
public override Task ExecuteAsync()
{
throw new NotImplementedException();
}
}
public class TestPageStringWithModel : Page
{
public PageModel TestPageModelWithString { get; set; }
public override Task ExecuteAsync()
{
throw new NotImplementedException();
}
}
public class TestPageModelWithString : PageModel
{
[TempData]
public string Test { get; set; }
[TempData]
public string Test2 { get; set; }
}
}
}

View File

@ -30,7 +30,7 @@ namespace Microsoft.AspNetCore.Mvc.ViewFeatures.Internal
// Assert
var controller = Assert.Single(context.Result.Controllers);
Assert.Single(controller.Filters, f => f is SaveTempDataPropertyFilterFactory);
Assert.Single(controller.Filters, f => f is ControllerSaveTempDataPropertyFilterFactory);
}
[Fact]
@ -46,14 +46,14 @@ namespace Microsoft.AspNetCore.Mvc.ViewFeatures.Internal
// Act
provider.OnProvidersExecuting(context);
var controller = context.Result.Controllers.SingleOrDefault();
var filter = controller.Filters.OfType<SaveTempDataPropertyFilterFactory>();
var filter = controller.Filters.OfType<ControllerSaveTempDataPropertyFilterFactory>();
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);
Assert.Same(expected, tempDataPropertyHelper.PropertyInfo);
}
[Fact]

View File

@ -0,0 +1,10 @@
@page
@functions
{
public IActionResult OnGet()
{
TempData["TempDataProperty-Message"] = "Secret Message";
return Redirect("~/TempData/TempDataPageModelProperty");
}
}

View File

@ -0,0 +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 Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.RazorPages;
using Microsoft.AspNetCore.Mvc.ViewFeatures;
namespace RazorPagesWebSite.TempData
{
public class TempDataPageModel : PageModel
{
[TempData]
public string Message { get; set; }
public IActionResult OnGet()
{
return View();
}
public IActionResult OnPost()
{
Message = "Secret post";
return View();
}
}
}

View File

@ -0,0 +1,9 @@
@page
@model RazorPagesWebSite.TempData.TempDataPageModel
Message: @Model.Message
@using (Html.BeginForm())
{
@Html.AntiForgeryToken()
}
TempData: @TempData["TempDataProperty-Message"]