Add TempData property support for Pages

This commit is contained in:
Ryan Nowak 2017-02-07 14:24:12 -08:00
parent 690ef186a3
commit 314aa366e1
15 changed files with 263 additions and 13 deletions

View File

@ -1,12 +1,22 @@
@page Test @page Test
@model TestModel @model TestModel
@functions {
[TempData]
public string Message { get; set; }
}
<div class="row"> <div class="row">
<div class="col-md-3"> <div class="col-md-3">
<h2>RazorPages says Hello @Model.Name!</h2> <h2>RazorPages says Hello @Model.Name!</h2>
<ul> <ul>
<li>This file should give you a quick view of a Mvc Raor Page in action.</li> <li>This file should give you a quick view of a Mvc Razor Page in action.</li>
</ul> </ul>
<p>Message from TempData: @Message</p>
@{
Message = $"You visited this page at {DateTime.Now}.";
}
</div> </div>
<form method="post"> <form method="post">

View File

@ -16,7 +16,7 @@ namespace MvcSandbox
// This method gets called by the runtime. Use this method to add services to the container. // This method gets called by the runtime. Use this method to add services to the container.
public void ConfigureServices(IServiceCollection services) public void ConfigureServices(IServiceCollection services)
{ {
services.AddMvc(); services.AddMvc().AddCookieTempDataProvider();
services.Insert(0, ServiceDescriptor.Singleton( services.Insert(0, ServiceDescriptor.Singleton(
typeof(IConfigureOptions<AntiforgeryOptions>), typeof(IConfigureOptions<AntiforgeryOptions>),

View File

@ -66,8 +66,12 @@ namespace Microsoft.Extensions.DependencyInjection
services.TryAddSingleton<IPageLoader, DefaultPageLoader>(); services.TryAddSingleton<IPageLoader, DefaultPageLoader>();
services.TryAddSingleton<IPageHandlerMethodSelector, DefaultPageHandlerMethodSelector>(); services.TryAddSingleton<IPageHandlerMethodSelector, DefaultPageHandlerMethodSelector>();
services.TryAddSingleton<PageResultExecutor>(); services.TryAddSingleton<PageResultExecutor>();
services.TryAddSingleton<PageArgumentBinder, DefaultPageArgumentBinder>(); services.TryAddSingleton<PageArgumentBinder, DefaultPageArgumentBinder>();
services.TryAddSingleton<IActionDescriptorChangeProvider, PageActionDescriptorChangeProvider>(); services.TryAddSingleton<IActionDescriptorChangeProvider, PageActionDescriptorChangeProvider>();
services.TryAddSingleton<TempDataPropertyProvider>();
} }
} }
} }

View File

@ -7,6 +7,7 @@ using Microsoft.AspNetCore.Mvc.Abstractions;
using Microsoft.AspNetCore.Mvc.ApplicationModels; using Microsoft.AspNetCore.Mvc.ApplicationModels;
using Microsoft.AspNetCore.Mvc.Filters; using Microsoft.AspNetCore.Mvc.Filters;
using Microsoft.AspNetCore.Mvc.Routing; using Microsoft.AspNetCore.Mvc.Routing;
using Microsoft.AspNetCore.Mvc.ViewFeatures.Internal;
using Microsoft.AspNetCore.Razor.Evolution; using Microsoft.AspNetCore.Razor.Evolution;
using Microsoft.Extensions.Options; using Microsoft.Extensions.Options;
@ -84,6 +85,9 @@ namespace Microsoft.AspNetCore.Mvc.RazorPages.Infrastructure
model.Selectors.Add(CreateSelectorModel(parentDirectoryPath, template)); model.Selectors.Add(CreateSelectorModel(parentDirectoryPath, template));
} }
model.Filters.Add(new SaveTempDataPropertyFilter()); // Support for [TempData] on properties
model.Filters.Add(new AutoValidateAntiforgeryTokenAttribute()); // Always require an antiforgery token on post
for (var i = 0; i < _pagesOptions.Conventions.Count; i++) for (var i = 0; i < _pagesOptions.Conventions.Count; i++)
{ {
_pagesOptions.Conventions[i].Apply(model); _pagesOptions.Conventions[i].Apply(model);

View File

@ -0,0 +1,63 @@
// 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;
using Microsoft.AspNetCore.Mvc.Rendering;
using Microsoft.AspNetCore.Mvc.ViewFeatures;
namespace Microsoft.AspNetCore.Mvc.RazorPages
{
public class TempDataPropertyProvider
{
public static readonly string Prefix = "TempDataProperty-";
private ConcurrentDictionary<Type, IEnumerable<PropertyInfo>> _subjectProperties =
new ConcurrentDictionary<Type, IEnumerable<PropertyInfo>>();
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

@ -13,6 +13,7 @@ using Microsoft.AspNetCore.Mvc.Internal;
using Microsoft.AspNetCore.Mvc.ModelBinding; using Microsoft.AspNetCore.Mvc.ModelBinding;
using Microsoft.AspNetCore.Mvc.Razor; using Microsoft.AspNetCore.Mvc.Razor;
using Microsoft.AspNetCore.Mvc.RazorPages.Infrastructure; using Microsoft.AspNetCore.Mvc.RazorPages.Infrastructure;
using Microsoft.AspNetCore.Mvc.ViewFeatures.Internal;
using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging;
namespace Microsoft.AspNetCore.Mvc.RazorPages.Internal namespace Microsoft.AspNetCore.Mvc.RazorPages.Internal
@ -21,6 +22,7 @@ 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;
@ -28,6 +30,7 @@ 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,
@ -42,6 +45,7 @@ namespace Microsoft.AspNetCore.Mvc.RazorPages.Internal
valueProviderFactories) valueProviderFactories)
{ {
_selector = handlerMethodSelector; _selector = handlerMethodSelector;
_propertyProvider = propertyProvider;
_pageContext = pageContext; _pageContext = pageContext;
CacheEntry = cacheEntry; CacheEntry = cacheEntry;
} }
@ -337,6 +341,25 @@ namespace Microsoft.AspNetCore.Mvc.RazorPages.Internal
_pageContext.ViewData.Model = _model; _pageContext.ViewData.Model = _model;
} }
// This is a workaround for not yet having proper filter for Pages.
SaveTempDataPropertyFilter propertyFilter = null;
for (var i = 0; i < _filters.Length; i++)
{
propertyFilter = _filters[i] as SaveTempDataPropertyFilter;
if (propertyFilter != null)
{
break;
}
}
var originalValues = _propertyProvider.LoadAndTrackChanges(_page, _pageContext.TempData);
if (propertyFilter != null)
{
propertyFilter.OriginalValues = originalValues;
propertyFilter.Subject = _page;
propertyFilter.Prefix = TempDataPropertyProvider.Prefix;
}
IActionResult result = null; IActionResult result = null;
var handler = _selector.Select(_pageContext); var handler = _selector.Select(_pageContext);

View File

@ -36,6 +36,7 @@ namespace Microsoft.AspNetCore.Mvc.RazorPages.Internal
private readonly ITempDataDictionaryFactory _tempDataFactory; private readonly ITempDataDictionaryFactory _tempDataFactory;
private readonly HtmlHelperOptions _htmlHelperOptions; private readonly HtmlHelperOptions _htmlHelperOptions;
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;
@ -53,6 +54,7 @@ namespace Microsoft.AspNetCore.Mvc.RazorPages.Internal
IOptions<MvcOptions> mvcOptions, IOptions<MvcOptions> mvcOptions,
IOptions<HtmlHelperOptions> htmlHelperOptions, IOptions<HtmlHelperOptions> htmlHelperOptions,
IPageHandlerMethodSelector selector, IPageHandlerMethodSelector selector,
TempDataPropertyProvider propertyProvider,
RazorProject razorProject, RazorProject razorProject,
DiagnosticSource diagnosticSource, DiagnosticSource diagnosticSource,
ILoggerFactory loggerFactory) ILoggerFactory loggerFactory)
@ -68,6 +70,7 @@ namespace Microsoft.AspNetCore.Mvc.RazorPages.Internal
_tempDataFactory = tempDataFactory; _tempDataFactory = tempDataFactory;
_htmlHelperOptions = htmlHelperOptions.Value; _htmlHelperOptions = htmlHelperOptions.Value;
_selector = selector; _selector = selector;
_propertyProvider = propertyProvider;
_razorProject = razorProject; _razorProject = razorProject;
_diagnosticSource = diagnosticSource; _diagnosticSource = diagnosticSource;
_logger = loggerFactory.CreateLogger<PageActionInvoker>(); _logger = loggerFactory.CreateLogger<PageActionInvoker>();
@ -149,6 +152,7 @@ namespace Microsoft.AspNetCore.Mvc.RazorPages.Internal
return new PageActionInvoker( return new PageActionInvoker(
_selector, _selector,
_propertyProvider,
_diagnosticSource, _diagnosticSource,
_logger, _logger,
pageContext, pageContext,

View File

@ -0,0 +1,13 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System;
namespace Microsoft.AspNetCore.Mvc.Rendering
{
[AttributeUsage(AttributeTargets.Property, Inherited = false, AllowMultiple = false)]
public sealed class TempDataAttribute : Attribute
{
}
}

View File

@ -0,0 +1,12 @@
// 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 interface ISaveTempDataCallback : IFilterMetadata
{
void OnTempDataSaving(ITempDataDictionary tempData);
}
}

View File

@ -1,6 +1,7 @@
// Copyright (c) .NET Foundation. All rights reserved. // 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. // 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.Mvc.Filters;
using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc.Internal; using Microsoft.AspNetCore.Mvc.Internal;
@ -49,15 +50,17 @@ namespace Microsoft.AspNetCore.Mvc.ViewFeatures.Internal
SaveTempData( SaveTempData(
result: null, result: null,
factory: saveTempDataContext.TempDataDictionaryFactory, factory: saveTempDataContext.TempDataDictionaryFactory,
filters: saveTempDataContext.Filters,
httpContext: saveTempDataContext.HttpContext); httpContext: saveTempDataContext.HttpContext);
return TaskCache.CompletedTask; return TaskCache.CompletedTask;
}, },
state: new SaveTempDataContext() state: new SaveTempDataContext()
{ {
HttpContext = context.HttpContext, Filters = context.Filters,
TempDataDictionaryFactory = _factory HttpContext = context.HttpContext,
}); TempDataDictionaryFactory = _factory
});
} }
} }
@ -79,7 +82,7 @@ namespace Microsoft.AspNetCore.Mvc.ViewFeatures.Internal
// late in the pipeline at which point SessionFeature would not be available. // late in the pipeline at which point SessionFeature would not be available.
if (!context.HttpContext.Response.HasStarted) if (!context.HttpContext.Response.HasStarted)
{ {
SaveTempData(context.Result, _factory, context.HttpContext); SaveTempData(context.Result, _factory, context.Filters, context.HttpContext);
// If SaveTempDataFilter got added twice this might already be in there. // If SaveTempDataFilter got added twice this might already be in there.
if (!context.HttpContext.Items.ContainsKey(TempDataSavedKey)) if (!context.HttpContext.Items.ContainsKey(TempDataSavedKey))
{ {
@ -88,17 +91,34 @@ namespace Microsoft.AspNetCore.Mvc.ViewFeatures.Internal
} }
} }
private static void SaveTempData(IActionResult result, ITempDataDictionaryFactory factory, HttpContext httpContext) private static void SaveTempData(
IActionResult result,
ITempDataDictionaryFactory factory,
IList<IFilterMetadata> filters,
HttpContext httpContext)
{ {
var tempData = factory.GetTempData(httpContext);
for (var i = 0; i < filters.Count; i++)
{
var callback = filters[i] as ISaveTempDataCallback;
if (callback != null)
{
callback.OnTempDataSaving(tempData);
}
}
if (result is IKeepTempDataResult) if (result is IKeepTempDataResult)
{ {
factory.GetTempData(httpContext).Keep(); tempData.Keep();
} }
factory.GetTempData(httpContext).Save();
tempData.Save();
} }
private class SaveTempDataContext private class SaveTempDataContext
{ {
public IList<IFilterMetadata> Filters { get; set; }
public HttpContext HttpContext { get; set; } public HttpContext HttpContext { get; set; }
public ITempDataDictionaryFactory TempDataDictionaryFactory { get; set; } public ITempDataDictionaryFactory TempDataDictionaryFactory { get; set; }
} }

View File

@ -0,0 +1,35 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System.Collections.Generic;
using System.Reflection;
namespace Microsoft.AspNetCore.Mvc.ViewFeatures.Internal
{
public class SaveTempDataPropertyFilter : ISaveTempDataCallback
{
public string Prefix { get; set; }
public object Subject { get; set; }
public IDictionary<PropertyInfo, object> OriginalValues { get; set; }
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 != originalValue)
{
tempData[Prefix + property.Name] = newValue;
}
}
}
}
}
}

View File

@ -32,7 +32,7 @@ namespace Microsoft.AspNetCore.Mvc.FunctionalTests
var response = await Client.GetStringAsync("http://localhost/Pages/Test"); var response = await Client.GetStringAsync("http://localhost/Pages/Test");
// Assert // Assert
Assert.Contains("This file should give you a quick view of a Mvc Raor Page in action.", response); Assert.Contains("This file should give you a quick view of a Mvc Razor Page in action.", response);
} }
} }
} }

View File

@ -7,6 +7,7 @@ using Microsoft.AspNetCore.Mvc.ApplicationModels;
using Microsoft.AspNetCore.Mvc.Filters; using Microsoft.AspNetCore.Mvc.Filters;
using Microsoft.AspNetCore.Mvc.Razor; using Microsoft.AspNetCore.Mvc.Razor;
using Microsoft.AspNetCore.Mvc.Razor.Internal; using Microsoft.AspNetCore.Mvc.Razor.Internal;
using Microsoft.AspNetCore.Mvc.ViewFeatures.Internal;
using Microsoft.AspNetCore.Razor.Evolution; using Microsoft.AspNetCore.Razor.Evolution;
using Microsoft.Extensions.Options; using Microsoft.Extensions.Options;
using Moq; using Moq;
@ -193,6 +194,44 @@ namespace Microsoft.AspNetCore.Mvc.RazorPages.Infrastructure
}); });
} }
[Fact]
public void GetDescriptors_ImplicitFilters()
{
// Arrange
var options = new MvcOptions();
var razorProject = new Mock<RazorProject>();
razorProject.Setup(p => p.EnumerateItems("/"))
.Returns(new[]
{
GetProjectItem("/", "/Home.cshtml", $"@page {Environment.NewLine}"),
});
var provider = new PageActionDescriptorProvider(
razorProject.Object,
GetAccessor(options),
GetAccessor<RazorPagesOptions>());
var context = new ActionDescriptorProviderContext();
// Act
provider.OnProvidersExecuting(context);
// Assert
var result = Assert.Single(context.Results);
var descriptor = Assert.IsType<PageActionDescriptor>(result);
Assert.Collection(
descriptor.FilterDescriptors,
filterDescriptor =>
{
Assert.Equal(FilterScope.Action, filterDescriptor.Scope);
Assert.IsType<SaveTempDataPropertyFilter>(filterDescriptor.Filter);
},
filterDescriptor =>
{
Assert.Equal(FilterScope.Action, filterDescriptor.Scope);
Assert.IsType<AutoValidateAntiforgeryTokenAttribute>(filterDescriptor.Filter);
});
}
[Fact] [Fact]
public void GetDescriptors_AddsGlobalFilters() public void GetDescriptors_AddsGlobalFilters()
{ {
@ -220,7 +259,8 @@ namespace Microsoft.AspNetCore.Mvc.RazorPages.Infrastructure
// Assert // Assert
var result = Assert.Single(context.Results); var result = Assert.Single(context.Results);
var descriptor = Assert.IsType<PageActionDescriptor>(result); var descriptor = Assert.IsType<PageActionDescriptor>(result);
Assert.Collection(descriptor.FilterDescriptors, Assert.Collection(
descriptor.FilterDescriptors,
filterDescriptor => filterDescriptor =>
{ {
Assert.Equal(FilterScope.Global, filterDescriptor.Scope); Assert.Equal(FilterScope.Global, filterDescriptor.Scope);
@ -230,6 +270,16 @@ namespace Microsoft.AspNetCore.Mvc.RazorPages.Infrastructure
{ {
Assert.Equal(FilterScope.Global, filterDescriptor.Scope); Assert.Equal(FilterScope.Global, filterDescriptor.Scope);
Assert.Same(filter2, filterDescriptor.Filter); Assert.Same(filter2, filterDescriptor.Filter);
},
filterDescriptor =>
{
Assert.Equal(FilterScope.Action, filterDescriptor.Scope);
Assert.IsType<SaveTempDataPropertyFilter>(filterDescriptor.Filter);
},
filterDescriptor =>
{
Assert.Equal(FilterScope.Action, filterDescriptor.Scope);
Assert.IsType<AutoValidateAntiforgeryTokenAttribute>(filterDescriptor.Filter);
}); });
} }
@ -275,6 +325,16 @@ namespace Microsoft.AspNetCore.Mvc.RazorPages.Infrastructure
Assert.Same(globalFilter, filterDescriptor.Filter); Assert.Same(globalFilter, filterDescriptor.Filter);
}, },
filterDescriptor => filterDescriptor =>
{
Assert.Equal(FilterScope.Action, filterDescriptor.Scope);
Assert.IsType<SaveTempDataPropertyFilter>(filterDescriptor.Filter);
},
filterDescriptor =>
{
Assert.Equal(FilterScope.Action, filterDescriptor.Scope);
Assert.IsType<AutoValidateAntiforgeryTokenAttribute>(filterDescriptor.Filter);
},
filterDescriptor =>
{ {
Assert.Equal(FilterScope.Action, filterDescriptor.Scope); Assert.Equal(FilterScope.Action, filterDescriptor.Scope);
Assert.Same(localFilter, filterDescriptor.Filter); Assert.Same(localFilter, filterDescriptor.Filter);

View File

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

View File

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