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

View File

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

View File

@ -4,6 +4,7 @@
using System;
using System.Collections.Generic;
using System.Reflection;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc.Filters;
using Microsoft.Extensions.Internal;
@ -26,6 +27,10 @@ namespace Microsoft.AspNetCore.Mvc.ViewFeatures.Internal
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)
@ -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;
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].Property;
var property = PropertyHelpers[i];
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;
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<RazorPagesOptions>(razorPagesOptions ?? new RazorPagesOptions()),
Mock.Of<IPageHandlerMethodSelector>(),
new TempDataPropertyProvider(),
razorProject,
new DiagnosticListener("Microsoft.AspNetCore"),
NullLoggerFactory.Instance);

View File

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

View File

@ -25,24 +25,11 @@ namespace Microsoft.AspNetCore.Mvc.ViewFeatures.Internal
["TempDataProperty-Test"] = "FirstValue"
};
var factory = new Mock<ITempDataDictionaryFactory>();
factory.Setup(f => f.GetTempData(httpContext))
.Returns(tempData);
var filter = new SaveTempDataPropertyFilter(factory.Object);
var filter = CreateSaveTempDataPropertyFilter(httpContext, tempData);
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;
filter.PropertyHelpers = BuildPropertyHelpers<TestController>();
var context = new ActionExecutingContext(
new ActionContext
{
@ -75,23 +62,10 @@ namespace Microsoft.AspNetCore.Mvc.ViewFeatures.Internal
["TempDataProperty-Test"] = "FirstValue"
};
var factory = new Mock<ITempDataDictionaryFactory>();
factory.Setup(f => f.GetTempData(httpContext))
.Returns(tempData);
var filter = new SaveTempDataPropertyFilter(factory.Object);
var filter = CreateSaveTempDataPropertyFilter(httpContext, tempData: tempData);
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;
filter.PropertyHelpers = BuildPropertyHelpers<TestController>();
var context = new ActionExecutingContext(
new ActionContext
@ -113,6 +87,72 @@ 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(
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
{
[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)
{
}
}
}
}