Move TempDataPropertyProvider into filter

This commit is contained in:
Ryan Brandenburg 2017-03-31 10:48:47 -07:00
parent 3424e20c67
commit 9e8d4db7d8
9 changed files with 115 additions and 236 deletions

View File

@ -81,9 +81,6 @@ namespace Microsoft.Extensions.DependencyInjection
// Action executors // Action executors
services.TryAddSingleton<PageResultExecutor>(); services.TryAddSingleton<PageResultExecutor>();
services.TryAddSingleton<RedirectToPageResultExecutor>(); services.TryAddSingleton<RedirectToPageResultExecutor>();
// Random infrastructure
services.TryAddSingleton<TempDataPropertyProvider>();
} }
} }
} }

View File

@ -22,7 +22,6 @@ namespace Microsoft.AspNetCore.Mvc.RazorPages.Internal
{ {
private readonly IPageHandlerMethodSelector _selector; private readonly IPageHandlerMethodSelector _selector;
private readonly PageContext _pageContext; private readonly PageContext _pageContext;
private readonly TempDataPropertyProvider _propertyProvider;
private Page _page; private Page _page;
private object _model; private object _model;
@ -30,7 +29,6 @@ namespace Microsoft.AspNetCore.Mvc.RazorPages.Internal
public PageActionInvoker( public PageActionInvoker(
IPageHandlerMethodSelector handlerMethodSelector, IPageHandlerMethodSelector handlerMethodSelector,
TempDataPropertyProvider propertyProvider,
DiagnosticSource diagnosticSource, DiagnosticSource diagnosticSource,
ILogger logger, ILogger logger,
PageContext pageContext, PageContext pageContext,
@ -45,7 +43,6 @@ namespace Microsoft.AspNetCore.Mvc.RazorPages.Internal
valueProviderFactories) valueProviderFactories)
{ {
_selector = handlerMethodSelector; _selector = handlerMethodSelector;
_propertyProvider = propertyProvider;
_pageContext = pageContext; _pageContext = pageContext;
CacheEntry = cacheEntry; CacheEntry = cacheEntry;
} }
@ -359,11 +356,10 @@ namespace Microsoft.AspNetCore.Mvc.RazorPages.Internal
} }
} }
var originalValues = _propertyProvider.LoadAndTrackChanges(_page, _pageContext.TempData);
if (propertyFilter != null) if (propertyFilter != null)
{ {
propertyFilter.OriginalValues = originalValues;
propertyFilter.Subject = _page; propertyFilter.Subject = _page;
propertyFilter.ApplyTempDataChanges(_pageContext.HttpContext);
} }
IActionResult result = null; IActionResult result = null;

View File

@ -39,7 +39,6 @@ namespace Microsoft.AspNetCore.Mvc.RazorPages.Internal
private readonly HtmlHelperOptions _htmlHelperOptions; private readonly HtmlHelperOptions _htmlHelperOptions;
private readonly RazorPagesOptions _razorPagesOptions; private readonly RazorPagesOptions _razorPagesOptions;
private readonly IPageHandlerMethodSelector _selector; private readonly IPageHandlerMethodSelector _selector;
private readonly TempDataPropertyProvider _propertyProvider;
private readonly RazorProject _razorProject; private readonly RazorProject _razorProject;
private readonly DiagnosticSource _diagnosticSource; private readonly DiagnosticSource _diagnosticSource;
private readonly ILogger<PageActionInvoker> _logger; private readonly ILogger<PageActionInvoker> _logger;
@ -59,7 +58,6 @@ namespace Microsoft.AspNetCore.Mvc.RazorPages.Internal
IOptions<HtmlHelperOptions> htmlHelperOptions, IOptions<HtmlHelperOptions> htmlHelperOptions,
IOptions<RazorPagesOptions> razorPagesOptions, IOptions<RazorPagesOptions> razorPagesOptions,
IPageHandlerMethodSelector selector, IPageHandlerMethodSelector selector,
TempDataPropertyProvider propertyProvider,
RazorProject razorProject, RazorProject razorProject,
DiagnosticSource diagnosticSource, DiagnosticSource diagnosticSource,
ILoggerFactory loggerFactory) ILoggerFactory loggerFactory)
@ -77,7 +75,6 @@ namespace Microsoft.AspNetCore.Mvc.RazorPages.Internal
_htmlHelperOptions = htmlHelperOptions.Value; _htmlHelperOptions = htmlHelperOptions.Value;
_razorPagesOptions = razorPagesOptions.Value; _razorPagesOptions = razorPagesOptions.Value;
_selector = selector; _selector = selector;
_propertyProvider = propertyProvider;
_razorProject = razorProject; _razorProject = razorProject;
_diagnosticSource = diagnosticSource; _diagnosticSource = diagnosticSource;
_logger = loggerFactory.CreateLogger<PageActionInvoker>(); _logger = loggerFactory.CreateLogger<PageActionInvoker>();
@ -159,7 +156,6 @@ namespace Microsoft.AspNetCore.Mvc.RazorPages.Internal
return new PageActionInvoker( return new PageActionInvoker(
_selector, _selector,
_propertyProvider,
_diagnosticSource, _diagnosticSource,
_logger, _logger,
pageContext, pageContext,

View File

@ -4,6 +4,7 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Reflection; using System.Reflection;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc.Filters; using Microsoft.AspNetCore.Mvc.Filters;
using Microsoft.Extensions.Internal; using Microsoft.Extensions.Internal;
@ -26,6 +27,10 @@ namespace Microsoft.AspNetCore.Mvc.ViewFeatures.Internal
public IDictionary<PropertyInfo, object> OriginalValues { 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) public void OnTempDataSaving(ITempDataDictionary tempData)
{ {
if (Subject != null && OriginalValues != null) if (Subject != null && OriginalValues != null)
@ -44,38 +49,66 @@ namespace Microsoft.AspNetCore.Mvc.ViewFeatures.Internal
} }
} }
public void OnActionExecuting(ActionExecutingContext context) /// <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 (PropertyHelpers == null) if (Subject == null)
{ {
throw new ArgumentNullException(nameof(PropertyHelpers)); 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; Subject = context.Controller;
var tempData = _factory.GetTempData(context.HttpContext); var tempData = _factory.GetTempData(context.HttpContext);
OriginalValues = new Dictionary<PropertyInfo, object>(); 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++) for (var i = 0; i < PropertyHelpers.Count; i++)
{ {
var property = PropertyHelpers[i].Property; var property = PropertyHelpers[i];
var value = tempData[Prefix + property.Name]; var value = tempData[Prefix + property.Name];
OriginalValues[property] = value; OriginalValues[property.Property] = value;
var propertyTypeInfo = property.PropertyType.GetTypeInfo(); var propertyTypeInfo = property.Property.PropertyType.GetTypeInfo();
var isReferenceTypeOrNullable = !propertyTypeInfo.IsValueType || Nullable.GetUnderlyingType(property.GetType()) != null; var isReferenceTypeOrNullable = !propertyTypeInfo.IsValueType || Nullable.GetUnderlyingType(property.GetType()) != null;
if (value != null || isReferenceTypeOrNullable) if (value != null || isReferenceTypeOrNullable)
{ {
property.SetValue(Subject, value); property.SetValue(subject, value);
} }
} }
} }
public void OnActionExecuted(ActionExecutedContext context)
{
}
} }
} }

View File

@ -1,67 +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.Concurrent;
using System.Collections.Generic;
using System.Linq;
using System.Reflection;
namespace Microsoft.AspNetCore.Mvc.ViewFeatures.Internal
{
public class TempDataPropertyProvider
{
public static readonly string Prefix = "TempDataProperty-";
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);
var result = new Dictionary<PropertyInfo, object>();
foreach (var property in properties)
{
var value = tempData[Prefix + property.Name];
result[property] = value;
// TODO: Clarify what behavior should be for null values here
if (value != null && property.PropertyType.IsAssignableFrom(value.GetType()))
{
property.SetValue(subject, value);
}
}
return result;
}
private IEnumerable<PropertyInfo> GetSubjectProperties(object subject)
{
return _subjectProperties.GetOrAdd(subject.GetType(), subjectType =>
{
var properties = subjectType.GetRuntimeProperties()
.Where(pi => pi.GetCustomAttribute<TempDataAttribute>() != null);
if (properties.Any(pi => !(pi.SetMethod != null && pi.SetMethod.IsPublic && pi.GetMethod != null && pi.GetMethod.IsPublic)))
{
throw new InvalidOperationException("TempData properties must have a public getter and setter.");
}
if (properties.Any(pi => !(pi.PropertyType.GetTypeInfo().IsPrimitive || pi.PropertyType == typeof(string))))
{
throw new InvalidOperationException("TempData properties must be declared as primitive types or string only.");
}
return properties;
});
}
}
}

View File

@ -820,7 +820,6 @@ namespace Microsoft.AspNetCore.Mvc.RazorPages.Internal
new TestOptionsManager<HtmlHelperOptions>(), new TestOptionsManager<HtmlHelperOptions>(),
new TestOptionsManager<RazorPagesOptions>(razorPagesOptions ?? new RazorPagesOptions()), new TestOptionsManager<RazorPagesOptions>(razorPagesOptions ?? new RazorPagesOptions()),
Mock.Of<IPageHandlerMethodSelector>(), Mock.Of<IPageHandlerMethodSelector>(),
new TempDataPropertyProvider(),
razorProject, razorProject,
new DiagnosticListener("Microsoft.AspNetCore"), new DiagnosticListener("Microsoft.AspNetCore"),
NullLoggerFactory.Instance); NullLoggerFactory.Instance);

View File

@ -611,7 +611,6 @@ namespace Microsoft.AspNetCore.Mvc.RazorPages.Internal
var invoker = new PageActionInvoker( var invoker = new PageActionInvoker(
selector, selector,
new TempDataPropertyProvider(),
diagnosticSource, diagnosticSource,
logger, logger,
pageContext, pageContext,

View File

@ -25,24 +25,11 @@ namespace Microsoft.AspNetCore.Mvc.ViewFeatures.Internal
["TempDataProperty-Test"] = "FirstValue" ["TempDataProperty-Test"] = "FirstValue"
}; };
var factory = new Mock<ITempDataDictionaryFactory>(); var filter = CreateSaveTempDataPropertyFilter(httpContext, tempData);
factory.Setup(f => f.GetTempData(httpContext))
.Returns(tempData);
var filter = new SaveTempDataPropertyFilter(factory.Object);
var controller = new TestController(); var controller = new TestController();
var controllerType = controller.GetType().GetTypeInfo();
var propertyHelper1 = new PropertyHelper(controllerType.GetProperty(nameof(TestController.Test))); filter.PropertyHelpers = BuildPropertyHelpers<TestController>();
var propertyHelper2 = new PropertyHelper(controllerType.GetProperty(nameof(TestController.Test2)));
var propertyHelpers = new List<PropertyHelper>
{
propertyHelper1,
propertyHelper2,
};
filter.PropertyHelpers = propertyHelpers;
var context = new ActionExecutingContext( var context = new ActionExecutingContext(
new ActionContext new ActionContext
{ {
@ -75,23 +62,10 @@ namespace Microsoft.AspNetCore.Mvc.ViewFeatures.Internal
["TempDataProperty-Test"] = "FirstValue" ["TempDataProperty-Test"] = "FirstValue"
}; };
var factory = new Mock<ITempDataDictionaryFactory>(); var filter = CreateSaveTempDataPropertyFilter(httpContext, tempData: tempData);
factory.Setup(f => f.GetTempData(httpContext))
.Returns(tempData);
var filter = new SaveTempDataPropertyFilter(factory.Object);
var controller = new TestController(); var controller = new TestController();
var controllerType = controller.GetType().GetTypeInfo();
var propertyHelper1 = new PropertyHelper(controllerType.GetProperty(nameof(TestController.Test))); filter.PropertyHelpers = BuildPropertyHelpers<TestController>();
var propertyHelper2 = new PropertyHelper(controllerType.GetProperty(nameof(TestController.Test2)));
var propertyHelpers = new List<PropertyHelper>
{
propertyHelper1,
propertyHelper2,
};
filter.PropertyHelpers = propertyHelpers;
var context = new ActionExecutingContext( var context = new ActionExecutingContext(
new ActionContext new ActionContext
@ -113,6 +87,72 @@ namespace Microsoft.AspNetCore.Mvc.ViewFeatures.Internal
Assert.Equal(0, controller.Test2); 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(
HttpContext httpContext,
TempDataDictionary tempData)
{
var factory = new Mock<ITempDataDictionaryFactory>();
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; }
}
public class TestController : Controller public class TestController : Controller
{ {
[TempData] [TempData]

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 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)
{
}
}
}
}