diff --git a/build/dependencies.props b/build/dependencies.props
index 8bb74f83e6..e65587c712 100644
--- a/build/dependencies.props
+++ b/build/dependencies.props
@@ -3,6 +3,7 @@
$(MSBuildAllProjects);$(MSBuildThisFileFullPath)
+ 0.9.9
0.10.13
2.1.0-preview2-15749
2.1.0-preview2-30478
diff --git a/src/Microsoft.AspNetCore.Mvc.RazorPages/DependencyInjection/MvcRazorPagesMvcCoreBuilderExtensions.cs b/src/Microsoft.AspNetCore.Mvc.RazorPages/DependencyInjection/MvcRazorPagesMvcCoreBuilderExtensions.cs
index 8d6e48bb0e..35a6e75a45 100644
--- a/src/Microsoft.AspNetCore.Mvc.RazorPages/DependencyInjection/MvcRazorPagesMvcCoreBuilderExtensions.cs
+++ b/src/Microsoft.AspNetCore.Mvc.RazorPages/DependencyInjection/MvcRazorPagesMvcCoreBuilderExtensions.cs
@@ -101,6 +101,8 @@ namespace Microsoft.Extensions.DependencyInjection
ServiceDescriptor.Singleton());
services.TryAddEnumerable(
ServiceDescriptor.Singleton());
+ services.TryAddEnumerable(
+ ServiceDescriptor.Singleton());
services.TryAddEnumerable(
ServiceDescriptor.Singleton());
diff --git a/src/Microsoft.AspNetCore.Mvc.RazorPages/Infrastructure/PageResultExecutor.cs b/src/Microsoft.AspNetCore.Mvc.RazorPages/Infrastructure/PageResultExecutor.cs
index 4ebf07eab2..10539c0b2a 100644
--- a/src/Microsoft.AspNetCore.Mvc.RazorPages/Infrastructure/PageResultExecutor.cs
+++ b/src/Microsoft.AspNetCore.Mvc.RazorPages/Infrastructure/PageResultExecutor.cs
@@ -9,6 +9,7 @@ using Microsoft.AspNetCore.Mvc.Infrastructure;
using Microsoft.AspNetCore.Mvc.Razor;
using Microsoft.AspNetCore.Mvc.ViewEngines;
using Microsoft.AspNetCore.Mvc.ViewFeatures;
+using Microsoft.AspNetCore.Mvc.ViewFeatures.Internal;
namespace Microsoft.AspNetCore.Mvc.RazorPages.Infrastructure
{
@@ -66,6 +67,8 @@ namespace Microsoft.AspNetCore.Mvc.RazorPages.Infrastructure
pageContext.ViewData.Model = result.Model;
}
+ OnExecuting(pageContext);
+
var viewStarts = new IRazorPage[pageContext.ViewStartFactories.Count];
for (var i = 0; i < pageContext.ViewStartFactories.Count; i++)
{
@@ -83,5 +86,14 @@ namespace Microsoft.AspNetCore.Mvc.RazorPages.Infrastructure
return ExecuteAsync(viewContext, result.ContentType, result.StatusCode);
}
+
+ private void OnExecuting(PageContext pageContext)
+ {
+ var viewDataValuesProvider = pageContext.HttpContext.Features.Get();
+ if (viewDataValuesProvider != null)
+ {
+ viewDataValuesProvider.ProvideViewDataValues(pageContext.ViewData);
+ }
+ }
}
}
diff --git a/src/Microsoft.AspNetCore.Mvc.RazorPages/Internal/PageSaveTempDataPropertyFilterFactory.cs b/src/Microsoft.AspNetCore.Mvc.RazorPages/Internal/PageSaveTempDataPropertyFilterFactory.cs
index e33e9ff537..b50ce6b2a9 100644
--- a/src/Microsoft.AspNetCore.Mvc.RazorPages/Internal/PageSaveTempDataPropertyFilterFactory.cs
+++ b/src/Microsoft.AspNetCore.Mvc.RazorPages/Internal/PageSaveTempDataPropertyFilterFactory.cs
@@ -4,7 +4,6 @@
using System;
using System.Collections.Generic;
using Microsoft.AspNetCore.Mvc.Filters;
-using Microsoft.AspNetCore.Mvc.RazorPages.Internal;
using Microsoft.AspNetCore.Mvc.ViewFeatures.Internal;
using Microsoft.Extensions.DependencyInjection;
diff --git a/src/Microsoft.AspNetCore.Mvc.RazorPages/Internal/PageViewDataAttributeFilter.cs b/src/Microsoft.AspNetCore.Mvc.RazorPages/Internal/PageViewDataAttributeFilter.cs
new file mode 100644
index 0000000000..1199f1a91e
--- /dev/null
+++ b/src/Microsoft.AspNetCore.Mvc.RazorPages/Internal/PageViewDataAttributeFilter.cs
@@ -0,0 +1,50 @@
+// 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.Mvc.ViewFeatures;
+using Microsoft.AspNetCore.Mvc.ViewFeatures.Internal;
+
+namespace Microsoft.AspNetCore.Mvc.RazorPages
+{
+ internal class PageViewDataAttributeFilter : IPageFilter, IViewDataValuesProviderFeature
+ {
+ public PageViewDataAttributeFilter(IReadOnlyList properties)
+ {
+ Properties = properties;
+ }
+
+ public IReadOnlyList Properties { get; }
+
+ public object Subject { get; set; }
+
+ public void OnPageHandlerExecuted(PageHandlerExecutedContext context)
+ {
+ }
+
+ public void OnPageHandlerExecuting(PageHandlerExecutingContext context)
+ {
+ Subject = context.HandlerInstance;
+ context.HttpContext.Features.Set(this);
+ }
+
+ public void OnPageHandlerSelected(PageHandlerSelectedContext context)
+ {
+ }
+
+ public void ProvideViewDataValues(ViewDataDictionary viewData)
+ {
+ for (var i = 0; i < Properties.Count; i++)
+ {
+ var property = Properties[i];
+ var value = property.GetValue(Subject);
+
+ if (value != null)
+ {
+ viewData[property.Key] = value;
+ }
+ }
+ }
+ }
+}
diff --git a/src/Microsoft.AspNetCore.Mvc.RazorPages/Internal/PageViewDataAttributeFilterFactory.cs b/src/Microsoft.AspNetCore.Mvc.RazorPages/Internal/PageViewDataAttributeFilterFactory.cs
new file mode 100644
index 0000000000..e75f518aec
--- /dev/null
+++ b/src/Microsoft.AspNetCore.Mvc.RazorPages/Internal/PageViewDataAttributeFilterFactory.cs
@@ -0,0 +1,28 @@
+// 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.Mvc.Filters;
+using Microsoft.AspNetCore.Mvc.ViewFeatures.Internal;
+
+namespace Microsoft.AspNetCore.Mvc.RazorPages
+{
+ internal class PageViewDataAttributeFilterFactory : IFilterFactory
+ {
+ public PageViewDataAttributeFilterFactory(IReadOnlyList properties)
+ {
+ Properties = properties;
+ }
+
+ public IReadOnlyList Properties { get; }
+
+ // PageViewDataAttributeFilter is stateful and cannot be reused.
+ public bool IsReusable => false;
+
+ public IFilterMetadata CreateInstance(IServiceProvider serviceProvider)
+ {
+ return new PageViewDataAttributeFilter(Properties);
+ }
+ }
+}
diff --git a/src/Microsoft.AspNetCore.Mvc.RazorPages/Internal/ViewDataAttributePageApplicationModelProvider.cs b/src/Microsoft.AspNetCore.Mvc.RazorPages/Internal/ViewDataAttributePageApplicationModelProvider.cs
new file mode 100644
index 0000000000..cdfbe11f0c
--- /dev/null
+++ b/src/Microsoft.AspNetCore.Mvc.RazorPages/Internal/ViewDataAttributePageApplicationModelProvider.cs
@@ -0,0 +1,42 @@
+// 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 Microsoft.AspNetCore.Mvc.ApplicationModels;
+using Microsoft.AspNetCore.Mvc.RazorPages.Internal;
+using Microsoft.AspNetCore.Mvc.ViewFeatures.Internal;
+
+namespace Microsoft.AspNetCore.Mvc.RazorPages
+{
+ internal class ViewDataAttributePageApplicationModelProvider : IPageApplicationModelProvider
+ {
+ ///
+ /// This order ensures that runs after the .
+ public int Order => -1000 + 10;
+
+ ///
+ public void OnProvidersExecuted(PageApplicationModelProviderContext context)
+ {
+ }
+
+ ///
+ public void OnProvidersExecuting(PageApplicationModelProviderContext context)
+ {
+ if (context == null)
+ {
+ throw new ArgumentNullException(nameof(context));
+ }
+
+ var handlerType = context.PageApplicationModel.HandlerType.AsType();
+
+ var viewDataProperties = ViewDataAttributePropertyProvider.GetViewDataProperties(handlerType);
+ if (viewDataProperties == null)
+ {
+ return;
+ }
+
+ var filter = new PageViewDataAttributeFilterFactory(viewDataProperties);
+ context.PageApplicationModel.Filters.Add(filter);
+ }
+ }
+}
diff --git a/src/Microsoft.AspNetCore.Mvc.ViewFeatures/DependencyInjection/MvcViewFeaturesMvcCoreBuilderExtensions.cs b/src/Microsoft.AspNetCore.Mvc.ViewFeatures/DependencyInjection/MvcViewFeaturesMvcCoreBuilderExtensions.cs
index 8cbcd02974..ebedcbc56c 100644
--- a/src/Microsoft.AspNetCore.Mvc.ViewFeatures/DependencyInjection/MvcViewFeaturesMvcCoreBuilderExtensions.cs
+++ b/src/Microsoft.AspNetCore.Mvc.ViewFeatures/DependencyInjection/MvcViewFeaturesMvcCoreBuilderExtensions.cs
@@ -203,6 +203,8 @@ namespace Microsoft.Extensions.DependencyInjection
//
services.TryAddEnumerable(
ServiceDescriptor.Transient());
+ services.TryAddEnumerable(
+ ServiceDescriptor.Transient());
services.TryAddSingleton();
diff --git a/src/Microsoft.AspNetCore.Mvc.ViewFeatures/Internal/ControllerViewDataAttributeFilter.cs b/src/Microsoft.AspNetCore.Mvc.ViewFeatures/Internal/ControllerViewDataAttributeFilter.cs
new file mode 100644
index 0000000000..f08508736d
--- /dev/null
+++ b/src/Microsoft.AspNetCore.Mvc.ViewFeatures/Internal/ControllerViewDataAttributeFilter.cs
@@ -0,0 +1,45 @@
+// 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.Mvc.ViewFeatures.Internal;
+
+namespace Microsoft.AspNetCore.Mvc.ViewFeatures
+{
+ internal class ControllerViewDataAttributeFilter : IActionFilter, IViewDataValuesProviderFeature
+ {
+ public ControllerViewDataAttributeFilter(IReadOnlyList properties)
+ {
+ Properties = properties;
+ }
+
+ public object Subject { get; set; }
+
+ public IReadOnlyList Properties { get; }
+
+ public void OnActionExecuted(ActionExecutedContext context)
+ {
+ }
+
+ public void OnActionExecuting(ActionExecutingContext context)
+ {
+ Subject = context.Controller;
+ context.HttpContext.Features.Set(this);
+ }
+
+ public void ProvideViewDataValues(ViewDataDictionary viewData)
+ {
+ for (var i = 0; i < Properties.Count; i++)
+ {
+ var property = Properties[i];
+ var value = property.GetValue(Subject);
+
+ if (value != null)
+ {
+ viewData[property.Key] = value;
+ }
+ }
+ }
+ }
+}
diff --git a/src/Microsoft.AspNetCore.Mvc.ViewFeatures/Internal/ControllerViewDataAttributeFilterFactory.cs b/src/Microsoft.AspNetCore.Mvc.ViewFeatures/Internal/ControllerViewDataAttributeFilterFactory.cs
new file mode 100644
index 0000000000..61e9e172df
--- /dev/null
+++ b/src/Microsoft.AspNetCore.Mvc.ViewFeatures/Internal/ControllerViewDataAttributeFilterFactory.cs
@@ -0,0 +1,28 @@
+// 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.Mvc.Filters;
+using Microsoft.AspNetCore.Mvc.ViewFeatures.Internal;
+
+namespace Microsoft.AspNetCore.Mvc.ViewFeatures
+{
+ internal class ControllerViewDataAttributeFilterFactory : IFilterFactory
+ {
+ public ControllerViewDataAttributeFilterFactory(IReadOnlyList properties)
+ {
+ Properties = properties;
+ }
+
+ public IReadOnlyList Properties { get; }
+
+ // ControllerViewDataAttributeFilter is stateful and cannot be reused.
+ public bool IsReusable => false;
+
+ public IFilterMetadata CreateInstance(IServiceProvider serviceProvider)
+ {
+ return new ControllerViewDataAttributeFilter(Properties);
+ }
+ }
+}
diff --git a/src/Microsoft.AspNetCore.Mvc.ViewFeatures/Internal/IViewDataValuesProviderFeature.cs b/src/Microsoft.AspNetCore.Mvc.ViewFeatures/Internal/IViewDataValuesProviderFeature.cs
new file mode 100644
index 0000000000..50648d6ecf
--- /dev/null
+++ b/src/Microsoft.AspNetCore.Mvc.ViewFeatures/Internal/IViewDataValuesProviderFeature.cs
@@ -0,0 +1,10 @@
+// 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.
+
+namespace Microsoft.AspNetCore.Mvc.ViewFeatures.Internal
+{
+ public interface IViewDataValuesProviderFeature
+ {
+ void ProvideViewDataValues(ViewDataDictionary viewData);
+ }
+}
diff --git a/src/Microsoft.AspNetCore.Mvc.ViewFeatures/Internal/ViewDataAttributeApplicationModelProvider.cs b/src/Microsoft.AspNetCore.Mvc.ViewFeatures/Internal/ViewDataAttributeApplicationModelProvider.cs
new file mode 100644
index 0000000000..3237558537
--- /dev/null
+++ b/src/Microsoft.AspNetCore.Mvc.ViewFeatures/Internal/ViewDataAttributeApplicationModelProvider.cs
@@ -0,0 +1,45 @@
+// 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 Microsoft.AspNetCore.Mvc.ApplicationModels;
+using Microsoft.AspNetCore.Mvc.Internal;
+using Microsoft.AspNetCore.Mvc.ViewFeatures.Internal;
+
+namespace Microsoft.AspNetCore.Mvc.ViewFeatures
+{
+ internal class ViewDataAttributeApplicationModelProvider : IApplicationModelProvider
+ {
+ ///
+ /// This order ensures that runs after the .
+ public int Order => -1000 + 10;
+
+ ///
+ public void OnProvidersExecuted(ApplicationModelProviderContext context)
+ {
+ }
+
+ ///
+ public void OnProvidersExecuting(ApplicationModelProviderContext context)
+ {
+ if (context == null)
+ {
+ throw new ArgumentNullException(nameof(context));
+ }
+
+ foreach (var controllerModel in context.Result.Controllers)
+ {
+ var controllerType = controllerModel.ControllerType.AsType();
+
+ var viewDataProperties = ViewDataAttributePropertyProvider.GetViewDataProperties(controllerType);
+ if (viewDataProperties == null)
+ {
+ continue;
+ }
+
+ var filter = new ControllerViewDataAttributeFilterFactory(viewDataProperties);
+ controllerModel.Filters.Add(filter);
+ }
+ }
+ }
+}
diff --git a/src/Microsoft.AspNetCore.Mvc.ViewFeatures/Internal/ViewDataAttributePropertyProvider.cs b/src/Microsoft.AspNetCore.Mvc.ViewFeatures/Internal/ViewDataAttributePropertyProvider.cs
new file mode 100644
index 0000000000..494a4b6c2d
--- /dev/null
+++ b/src/Microsoft.AspNetCore.Mvc.ViewFeatures/Internal/ViewDataAttributePropertyProvider.cs
@@ -0,0 +1,44 @@
+// 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 System.Reflection;
+using Microsoft.Extensions.Internal;
+
+namespace Microsoft.AspNetCore.Mvc.ViewFeatures.Internal
+{
+ public static class ViewDataAttributePropertyProvider
+ {
+ public static IReadOnlyList GetViewDataProperties(Type type)
+ {
+ List results = null;
+
+ var propertyHelpers = PropertyHelper.GetVisibleProperties(type: type);
+
+ for (var i = 0; i < propertyHelpers.Length; i++)
+ {
+ var propertyHelper = propertyHelpers[i];
+ var property = propertyHelper.Property;
+ var tempDataAttribute = property.GetCustomAttribute();
+ if (tempDataAttribute != null)
+ {
+ if (results == null)
+ {
+ results = new List();
+ }
+
+ var key = tempDataAttribute.Key;
+ if (string.IsNullOrEmpty(key))
+ {
+ key = property.Name;
+ }
+
+ results.Add(new LifecycleProperty(property, key));
+ }
+ }
+
+ return results;
+ }
+ }
+}
diff --git a/src/Microsoft.AspNetCore.Mvc.ViewFeatures/ViewDataAttribute.cs b/src/Microsoft.AspNetCore.Mvc.ViewFeatures/ViewDataAttribute.cs
new file mode 100644
index 0000000000..ca0cee6ea0
--- /dev/null
+++ b/src/Microsoft.AspNetCore.Mvc.ViewFeatures/ViewDataAttribute.cs
@@ -0,0 +1,25 @@
+// 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.Mvc.ViewFeatures;
+using Microsoft.AspNetCore.Mvc.ViewFeatures.Internal;
+
+namespace Microsoft.AspNetCore.Mvc
+{
+ ///
+ /// Properties decorated with will have their values stored in
+ /// and loaded from the .
+ /// is supported on properties of Controllers, and Razor Page handlers.
+ ///
+ [AttributeUsage(AttributeTargets.Property, Inherited = true, AllowMultiple = false)]
+ public sealed class ViewDataAttribute : Attribute
+ {
+ ///
+ /// Gets or sets the key used to get or add the property from value from .
+ /// When unspecified, the key is the property name.
+ ///
+ public string Key { get; set; }
+ }
+}
diff --git a/src/Microsoft.AspNetCore.Mvc.ViewFeatures/ViewFeatures/ViewComponentResultExecutor.cs b/src/Microsoft.AspNetCore.Mvc.ViewFeatures/ViewFeatures/ViewComponentResultExecutor.cs
index 1ae46c44b0..164de35ba1 100644
--- a/src/Microsoft.AspNetCore.Mvc.ViewFeatures/ViewFeatures/ViewComponentResultExecutor.cs
+++ b/src/Microsoft.AspNetCore.Mvc.ViewFeatures/ViewFeatures/ViewComponentResultExecutor.cs
@@ -115,6 +115,8 @@ namespace Microsoft.AspNetCore.Mvc.ViewFeatures
writer,
_htmlHelperOptions);
+ OnExecuting(viewContext);
+
// IViewComponentHelper is stateful, we want to make sure to retrieve it every time we need it.
var viewComponentHelper = context.HttpContext.RequestServices.GetRequiredService();
(viewComponentHelper as IViewContextAware)?.Contextualize(viewContext);
@@ -124,6 +126,15 @@ namespace Microsoft.AspNetCore.Mvc.ViewFeatures
}
}
+ private void OnExecuting(ViewContext viewContext)
+ {
+ var viewDataValuesProvider = viewContext.HttpContext.Features.Get();
+ if (viewDataValuesProvider != null)
+ {
+ viewDataValuesProvider.ProvideViewDataValues(viewContext.ViewData);
+ }
+ }
+
private Task GetViewComponentResult(IViewComponentHelper viewComponentHelper, ILogger logger, ViewComponentResult result)
{
if (result.ViewComponentType == null && result.ViewComponentName == null)
diff --git a/src/Microsoft.AspNetCore.Mvc.ViewFeatures/ViewFeatures/ViewExecutor.cs b/src/Microsoft.AspNetCore.Mvc.ViewFeatures/ViewFeatures/ViewExecutor.cs
index cae30b038e..748a9c94b7 100644
--- a/src/Microsoft.AspNetCore.Mvc.ViewFeatures/ViewFeatures/ViewExecutor.cs
+++ b/src/Microsoft.AspNetCore.Mvc.ViewFeatures/ViewFeatures/ViewExecutor.cs
@@ -231,6 +231,8 @@ namespace Microsoft.AspNetCore.Mvc.ViewFeatures
response.StatusCode = statusCode.Value;
}
+ OnExecuting(viewContext);
+
using (var writer = WriterFactory.CreateWriter(response.Body, resolvedContentTypeEncoding))
{
var view = viewContext.View;
@@ -257,5 +259,14 @@ namespace Microsoft.AspNetCore.Mvc.ViewFeatures
await writer.FlushAsync();
}
}
+
+ private void OnExecuting(ViewContext viewContext)
+ {
+ var viewDataValuesProvider = viewContext.HttpContext.Features.Get();
+ if (viewDataValuesProvider != null)
+ {
+ viewDataValuesProvider.ProvideViewDataValues(viewContext.ViewData);
+ }
+ }
}
}
diff --git a/src/Microsoft.AspNetCore.Mvc.ViewFeatures/ViewFeatures/ViewResultExecutor.cs b/src/Microsoft.AspNetCore.Mvc.ViewFeatures/ViewFeatures/ViewResultExecutor.cs
index 7d6b1ebc7c..af284f0908 100644
--- a/src/Microsoft.AspNetCore.Mvc.ViewFeatures/ViewFeatures/ViewResultExecutor.cs
+++ b/src/Microsoft.AspNetCore.Mvc.ViewFeatures/ViewFeatures/ViewResultExecutor.cs
@@ -167,7 +167,6 @@ namespace Microsoft.AspNetCore.Mvc.ViewFeatures
var view = viewEngineResult.View;
using (view as IDisposable)
{
-
await ExecuteAsync(
context,
view,
diff --git a/test/Microsoft.AspNetCore.Mvc.FunctionalTests/BasicTests.cs b/test/Microsoft.AspNetCore.Mvc.FunctionalTests/BasicTests.cs
index 6df040c055..488cb6633a 100644
--- a/test/Microsoft.AspNetCore.Mvc.FunctionalTests/BasicTests.cs
+++ b/test/Microsoft.AspNetCore.Mvc.FunctionalTests/BasicTests.cs
@@ -489,5 +489,36 @@ namespace Microsoft.AspNetCore.Mvc.FunctionalTests
// Assert
Assert.Equal(expected, assemblyParts);
}
+
+ [Fact]
+ public async Task ViewDataProperties_AreTransferredToViews()
+ {
+ // Act
+ var document = await Client.GetHtmlDocumentAsync("ViewDataProperty/ViewDataPropertyToView");
+
+ // Assert
+ var message = document.QuerySelector("#message").TextContent;
+ Assert.Equal("Message set in action", message);
+
+ var filterMessage = document.QuerySelector("#filter-message").TextContent;
+ Assert.Equal("Value set in OnActionExecuting", filterMessage);
+
+ var title = document.QuerySelector("title").TextContent;
+ Assert.Equal("View Data Property Sample", title);
+ }
+
+ [Fact]
+ public async Task ViewDataProperties_AreTransferredToViewComponents()
+ {
+ // Act
+ var document = await Client.GetHtmlDocumentAsync("ViewDataProperty/ViewDataPropertyToViewComponent");
+
+ // Assert
+ var message = document.QuerySelector("#message").TextContent;
+ Assert.Equal("Message set in action", message);
+
+ var title = document.QuerySelector("title").TextContent;
+ Assert.Equal("View Data Property Sample", title);
+ }
}
}
diff --git a/test/Microsoft.AspNetCore.Mvc.FunctionalTests/Infrastructure/HttpClientExtensions.cs b/test/Microsoft.AspNetCore.Mvc.FunctionalTests/Infrastructure/HttpClientExtensions.cs
new file mode 100644
index 0000000000..590e1d02a9
--- /dev/null
+++ b/test/Microsoft.AspNetCore.Mvc.FunctionalTests/Infrastructure/HttpClientExtensions.cs
@@ -0,0 +1,75 @@
+// 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.Net;
+using System.Net.Http;
+using System.Threading.Tasks;
+using AngleSharp.Dom.Html;
+using AngleSharp.Parser.Html;
+using Xunit;
+using Xunit.Sdk;
+
+namespace Microsoft.AspNetCore.Mvc.FunctionalTests
+{
+ public static class HttpClientExtensions
+ {
+ public static async Task GetHtmlDocumentAsync(this HttpClient client, string requestUri)
+ {
+ var response = await client.GetAsync(requestUri);
+ await AssertStatusCodeAsync(response, HttpStatusCode.OK);
+
+ var content = await response.Content.ReadAsStringAsync();
+ var parser = new HtmlParser();
+ var document = parser.Parse(content);
+ if (document == null)
+ {
+ throw new InvalidOperationException("Response content could not be parsed as HTML: " + Environment.NewLine + content);
+ }
+
+ return document;
+ }
+
+ public static async Task AssertStatusCodeAsync(this HttpResponseMessage response, HttpStatusCode expectedStatusCode)
+ {
+ if (response.StatusCode == HttpStatusCode.OK)
+ {
+ return response;
+ }
+
+ string responseContent = null;
+ try
+ {
+ responseContent = await response.Content.ReadAsStringAsync();
+ }
+ catch
+ {
+ // No-op
+ }
+
+ throw new StatusCodeMismatchException
+ {
+ ExpectedStatusCode = HttpStatusCode.OK,
+ ActualStatusCode = response.StatusCode,
+ ResponseContent = responseContent,
+ };
+ }
+
+ private class StatusCodeMismatchException : XunitException
+ {
+ public HttpStatusCode ExpectedStatusCode { get; set; }
+
+ public HttpStatusCode ActualStatusCode { get; set; }
+
+ public string ResponseContent { get; set; }
+
+ public override string Message
+ {
+ get
+ {
+ return $"Excepted status code 200. Actual {ActualStatusCode}. Response Content:" + Environment.NewLine + ResponseContent;
+ }
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/test/Microsoft.AspNetCore.Mvc.FunctionalTests/Microsoft.AspNetCore.Mvc.FunctionalTests.csproj b/test/Microsoft.AspNetCore.Mvc.FunctionalTests/Microsoft.AspNetCore.Mvc.FunctionalTests.csproj
index 54e7694ffa..954ef92727 100644
--- a/test/Microsoft.AspNetCore.Mvc.FunctionalTests/Microsoft.AspNetCore.Mvc.FunctionalTests.csproj
+++ b/test/Microsoft.AspNetCore.Mvc.FunctionalTests/Microsoft.AspNetCore.Mvc.FunctionalTests.csproj
@@ -18,20 +18,10 @@
-
+
-
+
@@ -59,6 +49,7 @@
+
diff --git a/test/Microsoft.AspNetCore.Mvc.FunctionalTests/RazorPagesTest.cs b/test/Microsoft.AspNetCore.Mvc.FunctionalTests/RazorPagesTest.cs
index 18ba7a5df6..0f896568d5 100644
--- a/test/Microsoft.AspNetCore.Mvc.FunctionalTests/RazorPagesTest.cs
+++ b/test/Microsoft.AspNetCore.Mvc.FunctionalTests/RazorPagesTest.cs
@@ -845,7 +845,6 @@ Microsoft.AspNetCore.Mvc.ViewFeatures.ViewDataDictionary`1[AspNetCore.InjectedPa
Assert.Equal(expected, response.Headers.Location.ToString());
}
-
[Fact]
public async Task RedirectToSelfWorks()
{
diff --git a/test/Microsoft.AspNetCore.Mvc.FunctionalTests/RazorPagesWithBasePathTest.cs b/test/Microsoft.AspNetCore.Mvc.FunctionalTests/RazorPagesWithBasePathTest.cs
index d465d58b02..680af29004 100644
--- a/test/Microsoft.AspNetCore.Mvc.FunctionalTests/RazorPagesWithBasePathTest.cs
+++ b/test/Microsoft.AspNetCore.Mvc.FunctionalTests/RazorPagesWithBasePathTest.cs
@@ -504,5 +504,53 @@ Hello from /Pages/Shared/";
// Assert
Assert.Contains("This page is overriden by RazorPagesWebSite", response);
}
+
+ [Fact]
+ public async Task ViewDataAttributes_SetInPageModel_AreTransferedToLayout()
+ {
+ // Arrange
+ var document = await Client.GetHtmlDocumentAsync("/ViewData/ViewDataInPage");
+
+ // Assert
+ var description = document.QuerySelector("meta[name='description']").Attributes["content"];
+ Assert.Equal("Description set in handler", description.Value);
+
+ var keywords = document.QuerySelector("meta[name='keywords']").Attributes["content"];
+ Assert.Equal("Value set in filter", keywords.Value);
+
+ var author = document.QuerySelector("meta[name='author']").Attributes["content"];
+ Assert.Equal("Property with key", author.Value);
+
+ var title = document.QuerySelector("title").TextContent;
+ Assert.Equal("Title with default value", title);
+ }
+
+ [Fact]
+ public async Task ViewDataAttributes_SetInPageWithoutModel_AreTransferedToLayout()
+ {
+ // Arrange
+ var document = await Client.GetHtmlDocumentAsync("/ViewData/ViewDataInPageWithoutModel");
+
+ // Assert
+ var description = document.QuerySelector("meta[name='description']").Attributes["content"];
+ Assert.Equal("Description set in page handler", description.Value);
+
+ var title = document.QuerySelector("title").TextContent;
+ Assert.Equal("Default value", title);
+ }
+
+ [Fact]
+ public async Task ViewDataProperties_SetInPageModel_AreTransferredToViewComponents()
+ {
+ // Act
+ var document = await Client.GetHtmlDocumentAsync("ViewData/ViewDataToViewComponentPage");
+
+ // Assert
+ var message = document.QuerySelector("#message").TextContent;
+ Assert.Equal("Message set in handler", message);
+
+ var title = document.QuerySelector("title").TextContent;
+ Assert.Equal("View Data in Pages", title);
+ }
}
}
diff --git a/test/Microsoft.AspNetCore.Mvc.RazorPages.Test/Internal/PageViewDataAttributeFilterFactoryTest.cs b/test/Microsoft.AspNetCore.Mvc.RazorPages.Test/Internal/PageViewDataAttributeFilterFactoryTest.cs
new file mode 100644
index 0000000000..8b032e59f9
--- /dev/null
+++ b/test/Microsoft.AspNetCore.Mvc.RazorPages.Test/Internal/PageViewDataAttributeFilterFactoryTest.cs
@@ -0,0 +1,32 @@
+// 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 Microsoft.AspNetCore.Mvc.ViewFeatures.Internal;
+using Moq;
+using Xunit;
+
+namespace Microsoft.AspNetCore.Mvc.RazorPages
+{
+ public class PageViewDataAttributeFilterFactoryTest
+ {
+ [Fact]
+ public void CreateInstance_CreatesFilter()
+ {
+ // Arrange
+ var properties = new LifecycleProperty[]
+ {
+ new LifecycleProperty(),
+ new LifecycleProperty(),
+ };
+ var filterFactory = new PageViewDataAttributeFilterFactory(properties);
+
+ // Act
+ var result = filterFactory.CreateInstance(Mock.Of());
+
+ // Assert
+ var filter = Assert.IsType(result);
+ Assert.Same(properties, filter.Properties);
+ }
+ }
+}
diff --git a/test/Microsoft.AspNetCore.Mvc.RazorPages.Test/Internal/PageViewDataAttributeFilterTest.cs b/test/Microsoft.AspNetCore.Mvc.RazorPages.Test/Internal/PageViewDataAttributeFilterTest.cs
new file mode 100644
index 0000000000..1978424d5a
--- /dev/null
+++ b/test/Microsoft.AspNetCore.Mvc.RazorPages.Test/Internal/PageViewDataAttributeFilterTest.cs
@@ -0,0 +1,105 @@
+// 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 System.Linq;
+using Microsoft.AspNetCore.Http;
+using Microsoft.AspNetCore.Mvc.Abstractions;
+using Microsoft.AspNetCore.Mvc.Filters;
+using Microsoft.AspNetCore.Mvc.ModelBinding;
+using Microsoft.AspNetCore.Mvc.RazorPages.Infrastructure;
+using Microsoft.AspNetCore.Mvc.ViewFeatures;
+using Microsoft.AspNetCore.Mvc.ViewFeatures.Internal;
+using Microsoft.AspNetCore.Routing;
+using Xunit;
+
+namespace Microsoft.AspNetCore.Mvc.RazorPages
+{
+ public class PageViewDataAttributeFilterTest
+ {
+ [Fact]
+ public void OnPageHandlerExecuting_AddsFeature()
+ {
+ // Arrange
+ var filter = new PageViewDataAttributeFilter(Array.Empty());
+ var handler = new object();
+ var httpContext = new DefaultHttpContext();
+ var actionContext = new ActionContext(httpContext, new RouteData(), new ActionDescriptor());
+ var pageContext = new PageContext(actionContext);
+ var context = new PageHandlerExecutingContext(pageContext, new IFilterMetadata[0], new HandlerMethodDescriptor(), new Dictionary(), handler);
+
+ // Act
+ filter.OnPageHandlerExecuting(context);
+
+ // Assert
+ var feature = Assert.Single(httpContext.Features, f => f.Key == typeof(IViewDataValuesProviderFeature));
+ Assert.Same(filter, feature.Value);
+ }
+
+ [Fact]
+ public void OnPageHandlerExecuting_SetsSubject()
+ {
+ // Arrange
+ var filter = new PageViewDataAttributeFilter(Array.Empty());
+ var handler = new object();
+ var httpContext = new DefaultHttpContext();
+ var actionContext = new ActionContext(httpContext, new RouteData(), new ActionDescriptor());
+ var pageContext = new PageContext(actionContext);
+ var context = new PageHandlerExecutingContext(pageContext, new IFilterMetadata[0], new HandlerMethodDescriptor(), new Dictionary(), handler);
+
+ // Act
+ filter.OnPageHandlerExecuting(context);
+
+ // Assert
+ Assert.Same(handler, filter.Subject);
+ }
+
+ [Fact]
+ public void ProvideValues_AddsNonNullPropertyValuesToViewData()
+ {
+ // Arrange
+ var type = typeof(TestModel);
+ var properties = new[]
+ {
+ new LifecycleProperty(type.GetProperty(nameof(TestModel.Prop1)), "Prop1"),
+ new LifecycleProperty(type.GetProperty(nameof(TestModel.Prop2)), "Prop2"),
+ new LifecycleProperty(type.GetProperty(nameof(TestModel.Prop3)), "Prop3"),
+ };
+
+ var controller = new TestModel();
+ var filter = new PageViewDataAttributeFilter(properties)
+ {
+ Subject = controller,
+ };
+ var viewData = new ViewDataDictionary(new EmptyModelMetadataProvider(), new ModelStateDictionary());
+
+ // Act
+ controller.Prop1 = "New-Value";
+ filter.ProvideViewDataValues(viewData);
+
+ // Assert
+ Assert.Collection(
+ viewData.OrderBy(kvp => kvp.Key),
+ kvp =>
+ {
+ Assert.Equal("Prop1", kvp.Key);
+ Assert.Equal("New-Value", kvp.Value);
+ },
+ kvp =>
+ {
+ Assert.Equal("Prop2", kvp.Key);
+ Assert.Equal("Test", kvp.Value);
+ });
+ }
+
+ public class TestModel
+ {
+ public string Prop1 { get; set; }
+
+ public string Prop2 => "Test";
+
+ public string Prop3 { get; set; }
+ }
+ }
+}
diff --git a/test/Microsoft.AspNetCore.Mvc.RazorPages.Test/Internal/ViewDataAttributePageApplicationModelProviderTest.cs b/test/Microsoft.AspNetCore.Mvc.RazorPages.Test/Internal/ViewDataAttributePageApplicationModelProviderTest.cs
new file mode 100644
index 0000000000..7caf17e358
--- /dev/null
+++ b/test/Microsoft.AspNetCore.Mvc.RazorPages.Test/Internal/ViewDataAttributePageApplicationModelProviderTest.cs
@@ -0,0 +1,77 @@
+// 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.Reflection;
+using System.Threading.Tasks;
+using Microsoft.AspNetCore.Mvc.ApplicationModels;
+using Xunit;
+
+namespace Microsoft.AspNetCore.Mvc.RazorPages.Internal
+{
+ public class ViewDataAttributePageApplicationModelProviderTest
+ {
+ [Fact]
+ public void OnProvidersExecuting_DoesNotAddFilter_IfTypeHasNoViewDataProperties()
+ {
+ // Arrange
+ var type = typeof(TestPageModel_NoViewDataProperties);
+ var provider = new ViewDataAttributePageApplicationModelProvider();
+ var context = CreateProviderContext(type);
+
+ // Act
+ provider.OnProvidersExecuting(context);
+
+ // Assert
+ Assert.Empty(context.PageApplicationModel.Filters);
+ }
+
+ [Fact]
+ public void AddsViewDataPropertyFilter_ForViewDataAttributeProperties()
+ {
+ // Arrange
+ var type = typeof(TestPageModel_ViewDataProperties);
+ var provider = new ViewDataAttributePageApplicationModelProvider();
+ var context = CreateProviderContext(type);
+
+ // Act
+ provider.OnProvidersExecuting(context);
+
+ // Assert
+ var filter = Assert.Single(context.PageApplicationModel.Filters);
+ var viewDataFilter = Assert.IsType(filter);
+ Assert.Collection(
+ viewDataFilter.Properties,
+ property => Assert.Equal(nameof(TestPageModel_ViewDataProperties.DateTime), property.PropertyInfo.Name));
+ }
+
+ private static PageApplicationModelProviderContext CreateProviderContext(Type handlerType)
+ {
+ var descriptor = new CompiledPageActionDescriptor();
+ var context = new PageApplicationModelProviderContext(descriptor, typeof(TestPage).GetTypeInfo())
+ {
+ PageApplicationModel = new PageApplicationModel(descriptor, handlerType.GetTypeInfo(), Array.Empty