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
@model TestModel
@functions {
[TempData]
public string Message { get; set; }
}
<div class="row">
<div class="col-md-3">
<h2>RazorPages says Hello @Model.Name!</h2>
<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>
<p>Message from TempData: @Message</p>
@{
Message = $"You visited this page at {DateTime.Now}.";
}
</div>
<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.
public void ConfigureServices(IServiceCollection services)
{
services.AddMvc();
services.AddMvc().AddCookieTempDataProvider();
services.Insert(0, ServiceDescriptor.Singleton(
typeof(IConfigureOptions<AntiforgeryOptions>),

View File

@ -66,8 +66,12 @@ namespace Microsoft.Extensions.DependencyInjection
services.TryAddSingleton<IPageLoader, DefaultPageLoader>();
services.TryAddSingleton<IPageHandlerMethodSelector, DefaultPageHandlerMethodSelector>();
services.TryAddSingleton<PageResultExecutor>();
services.TryAddSingleton<PageArgumentBinder, DefaultPageArgumentBinder>();
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.Filters;
using Microsoft.AspNetCore.Mvc.Routing;
using Microsoft.AspNetCore.Mvc.ViewFeatures.Internal;
using Microsoft.AspNetCore.Razor.Evolution;
using Microsoft.Extensions.Options;
@ -84,6 +85,9 @@ namespace Microsoft.AspNetCore.Mvc.RazorPages.Infrastructure
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++)
{
_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.Razor;
using Microsoft.AspNetCore.Mvc.RazorPages.Infrastructure;
using Microsoft.AspNetCore.Mvc.ViewFeatures.Internal;
using Microsoft.Extensions.Logging;
namespace Microsoft.AspNetCore.Mvc.RazorPages.Internal
@ -21,6 +22,7 @@ namespace Microsoft.AspNetCore.Mvc.RazorPages.Internal
{
private readonly IPageHandlerMethodSelector _selector;
private readonly PageContext _pageContext;
private readonly TempDataPropertyProvider _propertyProvider;
private Page _page;
private object _model;
@ -28,6 +30,7 @@ namespace Microsoft.AspNetCore.Mvc.RazorPages.Internal
public PageActionInvoker(
IPageHandlerMethodSelector handlerMethodSelector,
TempDataPropertyProvider propertyProvider,
DiagnosticSource diagnosticSource,
ILogger logger,
PageContext pageContext,
@ -42,6 +45,7 @@ namespace Microsoft.AspNetCore.Mvc.RazorPages.Internal
valueProviderFactories)
{
_selector = handlerMethodSelector;
_propertyProvider = propertyProvider;
_pageContext = pageContext;
CacheEntry = cacheEntry;
}
@ -337,6 +341,25 @@ namespace Microsoft.AspNetCore.Mvc.RazorPages.Internal
_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;
var handler = _selector.Select(_pageContext);

View File

@ -36,6 +36,7 @@ namespace Microsoft.AspNetCore.Mvc.RazorPages.Internal
private readonly ITempDataDictionaryFactory _tempDataFactory;
private readonly HtmlHelperOptions _htmlHelperOptions;
private readonly IPageHandlerMethodSelector _selector;
private readonly TempDataPropertyProvider _propertyProvider;
private readonly RazorProject _razorProject;
private readonly DiagnosticSource _diagnosticSource;
private readonly ILogger<PageActionInvoker> _logger;
@ -53,6 +54,7 @@ namespace Microsoft.AspNetCore.Mvc.RazorPages.Internal
IOptions<MvcOptions> mvcOptions,
IOptions<HtmlHelperOptions> htmlHelperOptions,
IPageHandlerMethodSelector selector,
TempDataPropertyProvider propertyProvider,
RazorProject razorProject,
DiagnosticSource diagnosticSource,
ILoggerFactory loggerFactory)
@ -68,6 +70,7 @@ namespace Microsoft.AspNetCore.Mvc.RazorPages.Internal
_tempDataFactory = tempDataFactory;
_htmlHelperOptions = htmlHelperOptions.Value;
_selector = selector;
_propertyProvider = propertyProvider;
_razorProject = razorProject;
_diagnosticSource = diagnosticSource;
_logger = loggerFactory.CreateLogger<PageActionInvoker>();
@ -149,6 +152,7 @@ namespace Microsoft.AspNetCore.Mvc.RazorPages.Internal
return new PageActionInvoker(
_selector,
_propertyProvider,
_diagnosticSource,
_logger,
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.
// 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.Internal;
@ -49,15 +50,17 @@ namespace Microsoft.AspNetCore.Mvc.ViewFeatures.Internal
SaveTempData(
result: null,
factory: saveTempDataContext.TempDataDictionaryFactory,
filters: saveTempDataContext.Filters,
httpContext: saveTempDataContext.HttpContext);
return TaskCache.CompletedTask;
},
state: new SaveTempDataContext()
{
HttpContext = context.HttpContext,
TempDataDictionaryFactory = _factory
});
state: new SaveTempDataContext()
{
Filters = context.Filters,
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.
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 (!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)
{
factory.GetTempData(httpContext).Keep();
tempData.Keep();
}
factory.GetTempData(httpContext).Save();
tempData.Save();
}
private class SaveTempDataContext
{
public IList<IFilterMetadata> Filters { get; set; }
public HttpContext HttpContext { 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");
// 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.Razor;
using Microsoft.AspNetCore.Mvc.Razor.Internal;
using Microsoft.AspNetCore.Mvc.ViewFeatures.Internal;
using Microsoft.AspNetCore.Razor.Evolution;
using Microsoft.Extensions.Options;
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]
public void GetDescriptors_AddsGlobalFilters()
{
@ -220,7 +259,8 @@ namespace Microsoft.AspNetCore.Mvc.RazorPages.Infrastructure
// Assert
var result = Assert.Single(context.Results);
var descriptor = Assert.IsType<PageActionDescriptor>(result);
Assert.Collection(descriptor.FilterDescriptors,
Assert.Collection(
descriptor.FilterDescriptors,
filterDescriptor =>
{
Assert.Equal(FilterScope.Global, filterDescriptor.Scope);
@ -230,6 +270,16 @@ namespace Microsoft.AspNetCore.Mvc.RazorPages.Infrastructure
{
Assert.Equal(FilterScope.Global, filterDescriptor.Scope);
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);
},
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.Same(localFilter, filterDescriptor.Filter);

View File

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

View File

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