Add TempData property support for Pages
This commit is contained in:
parent
690ef186a3
commit
314aa366e1
|
|
@ -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">
|
||||
|
|
|
|||
|
|
@ -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>),
|
||||
|
|
|
|||
|
|
@ -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>();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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
|
||||
{
|
||||
|
||||
}
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
@ -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; }
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -607,6 +607,7 @@ namespace Microsoft.AspNetCore.Mvc.RazorPages.Internal
|
|||
|
||||
var invoker = new PageActionInvoker(
|
||||
selector,
|
||||
new TempDataPropertyProvider(),
|
||||
diagnosticSource,
|
||||
logger,
|
||||
pageContext,
|
||||
|
|
|
|||
Loading…
Reference in New Issue