diff --git a/src/Microsoft.AspNetCore.Mvc.Razor/Internal/RazorPagePropertyActivator.cs b/src/Microsoft.AspNetCore.Mvc.Razor/Internal/RazorPagePropertyActivator.cs index 8453d1e006..368738c448 100644 --- a/src/Microsoft.AspNetCore.Mvc.Razor/Internal/RazorPagePropertyActivator.cs +++ b/src/Microsoft.AspNetCore.Mvc.Razor/Internal/RazorPagePropertyActivator.cs @@ -30,9 +30,13 @@ namespace Microsoft.AspNetCore.Mvc.Razor.Internal { _metadataProvider = metadataProvider; - _viewDataDictionaryType = typeof(ViewDataDictionary<>).MakeGenericType(modelType); - _rootFactory = ViewDataDictionaryFactory.CreateFactory(modelType.GetTypeInfo()); - _nestedFactory = ViewDataDictionaryFactory.CreateNestedFactory(modelType.GetTypeInfo()); + + if (modelType != null) + { + _viewDataDictionaryType = typeof(ViewDataDictionary<>).MakeGenericType(modelType); + _rootFactory = ViewDataDictionaryFactory.CreateFactory(modelType.GetTypeInfo()); + _nestedFactory = ViewDataDictionaryFactory.CreateNestedFactory(modelType.GetTypeInfo()); + } _propertyActivators = PropertyActivator.GetPropertiesToActivate( pageType, @@ -48,7 +52,10 @@ namespace Microsoft.AspNetCore.Mvc.Razor.Internal throw new ArgumentNullException(nameof(context)); } - context.ViewData = CreateViewDataDictionary(context); + if (_viewDataDictionaryType != null) + { + context.ViewData = CreateViewDataDictionary(context); + } for (var i = 0; i < _propertyActivators.Length; i++) { diff --git a/src/Microsoft.AspNetCore.Mvc.Razor/Properties/Resources.Designer.cs b/src/Microsoft.AspNetCore.Mvc.Razor/Properties/Resources.Designer.cs index 9600750d6e..3790719784 100644 --- a/src/Microsoft.AspNetCore.Mvc.Razor/Properties/Resources.Designer.cs +++ b/src/Microsoft.AspNetCore.Mvc.Razor/Properties/Resources.Designer.cs @@ -80,48 +80,6 @@ namespace Microsoft.AspNetCore.Mvc.Razor internal static string FormatLayoutCannotBeRendered(object p0, object p1) => string.Format(CultureInfo.CurrentCulture, GetString("LayoutCannotBeRendered"), p0, p1); - /// - /// The 'inherits' keyword is not allowed when a '{0}' keyword is used. - /// - internal static string MvcRazorCodeParser_CannotHaveModelAndInheritsKeyword - { - get => GetString("MvcRazorCodeParser_CannotHaveModelAndInheritsKeyword"); - } - - /// - /// The 'inherits' keyword is not allowed when a '{0}' keyword is used. - /// - internal static string FormatMvcRazorCodeParser_CannotHaveModelAndInheritsKeyword(object p0) - => string.Format(CultureInfo.CurrentCulture, GetString("MvcRazorCodeParser_CannotHaveModelAndInheritsKeyword"), p0); - - /// - /// The '{0}' keyword must be followed by a type name on the same line. - /// - internal static string MvcRazorCodeParser_ModelKeywordMustBeFollowedByTypeName - { - get => GetString("MvcRazorCodeParser_ModelKeywordMustBeFollowedByTypeName"); - } - - /// - /// The '{0}' keyword must be followed by a type name on the same line. - /// - internal static string FormatMvcRazorCodeParser_ModelKeywordMustBeFollowedByTypeName(object p0) - => string.Format(CultureInfo.CurrentCulture, GetString("MvcRazorCodeParser_ModelKeywordMustBeFollowedByTypeName"), p0); - - /// - /// Only one '{0}' statement is allowed in a file. - /// - internal static string MvcRazorCodeParser_OnlyOneModelStatementIsAllowed - { - get => GetString("MvcRazorCodeParser_OnlyOneModelStatementIsAllowed"); - } - - /// - /// Only one '{0}' statement is allowed in a file. - /// - internal static string FormatMvcRazorCodeParser_OnlyOneModelStatementIsAllowed(object p0) - => string.Format(CultureInfo.CurrentCulture, GetString("MvcRazorCodeParser_OnlyOneModelStatementIsAllowed"), p0); - /// /// There is no active writing scope to end. /// @@ -234,20 +192,6 @@ namespace Microsoft.AspNetCore.Mvc.Razor internal static string FormatSectionsNotRendered(object p0, object p1, object p2) => string.Format(CultureInfo.CurrentCulture, GetString("SectionsNotRendered"), p0, p1, p2); - /// - /// View of type '{0}' cannot be activated by '{1}'. - /// - internal static string ViewCannotBeActivated - { - get => GetString("ViewCannotBeActivated"); - } - - /// - /// View of type '{0}' cannot be activated by '{1}'. - /// - internal static string FormatViewCannotBeActivated(object p0, object p1) - => string.Format(CultureInfo.CurrentCulture, GetString("ViewCannotBeActivated"), p0, p1); - /// /// '{0} must be set to access '{1}'. /// diff --git a/src/Microsoft.AspNetCore.Mvc.Razor/RazorPageActivator.cs b/src/Microsoft.AspNetCore.Mvc.Razor/RazorPageActivator.cs index f84278b49d..758255f494 100644 --- a/src/Microsoft.AspNetCore.Mvc.Razor/RazorPageActivator.cs +++ b/src/Microsoft.AspNetCore.Mvc.Razor/RazorPageActivator.cs @@ -67,15 +67,11 @@ namespace Microsoft.AspNetCore.Mvc.Razor if (!_activationInfo.TryGetValue(pageType, out propertyActivator)) { // Look for a property named "Model". If it is non-null, we'll assume this is - // the equivalent of TModel Model property on RazorPage - var modelProperty = pageType.GetRuntimeProperty(ModelPropertyName); - if (modelProperty == null) - { - var message = Resources.FormatViewCannotBeActivated(pageType.FullName, GetType().FullName); - throw new InvalidOperationException(message); - } - - var modelType = modelProperty.PropertyType; + // the equivalent of TModel Model property on RazorPage. + // + // Otherwise if we don't have a model property the activator will just skip setting + // the view data. + var modelType = pageType.GetRuntimeProperty(ModelPropertyName)?.PropertyType; propertyActivator = new RazorPagePropertyActivator( pageType, modelType, diff --git a/src/Microsoft.AspNetCore.Mvc.Razor/RazorPageBase.cs b/src/Microsoft.AspNetCore.Mvc.Razor/RazorPageBase.cs index 4fa1772250..96ef957f60 100644 --- a/src/Microsoft.AspNetCore.Mvc.Razor/RazorPageBase.cs +++ b/src/Microsoft.AspNetCore.Mvc.Razor/RazorPageBase.cs @@ -106,7 +106,7 @@ namespace Microsoft.AspNetCore.Mvc.Razor /// Returns null if is null. public ITempDataDictionary TempData => ViewContext?.TempData; - protected Stack TagHelperScopes { get; } = new Stack(); + private Stack TagHelperScopes { get; } = new Stack(); private ITagHelperFactory TagHelperFactory { @@ -755,7 +755,7 @@ namespace Microsoft.AspNetCore.Mvc.Razor public bool Suppressed { get; set; } } - protected struct TagHelperScopeInfo + private struct TagHelperScopeInfo { public TagHelperScopeInfo(ViewBuffer buffer, HtmlEncoder encoder, TextWriter writer) { diff --git a/src/Microsoft.AspNetCore.Mvc.Razor/Resources.resx b/src/Microsoft.AspNetCore.Mvc.Razor/Resources.resx index edcc139e30..13325e77e3 100644 --- a/src/Microsoft.AspNetCore.Mvc.Razor/Resources.resx +++ b/src/Microsoft.AspNetCore.Mvc.Razor/Resources.resx @@ -132,15 +132,6 @@ Layout page '{0}' cannot be rendered after '{1}' has been invoked. - - The 'inherits' keyword is not allowed when a '{0}' keyword is used. - - - The '{0}' keyword must be followed by a type name on the same line. - - - Only one '{0}' statement is allowed in a file. - There is no active writing scope to end. @@ -165,9 +156,6 @@ The following sections have been defined but have not been rendered by the page at '{0}': '{1}'. To ignore an unrendered section call {2}("sectionName"). - - View of type '{0}' cannot be activated by '{1}'. - '{0} must be set to access '{1}'. diff --git a/src/Microsoft.AspNetCore.Mvc.RazorPages/DependencyInjection/MvcRazorPagesMvcCoreBuilderExtensions.cs b/src/Microsoft.AspNetCore.Mvc.RazorPages/DependencyInjection/MvcRazorPagesMvcCoreBuilderExtensions.cs index 7cf4d35c01..93b97cefc9 100644 --- a/src/Microsoft.AspNetCore.Mvc.RazorPages/DependencyInjection/MvcRazorPagesMvcCoreBuilderExtensions.cs +++ b/src/Microsoft.AspNetCore.Mvc.RazorPages/DependencyInjection/MvcRazorPagesMvcCoreBuilderExtensions.cs @@ -114,7 +114,7 @@ namespace Microsoft.Extensions.DependencyInjection services.TryAddSingleton(); services.TryAddSingleton(); - services.TryAddSingleton(); + services.TryAddSingleton(); services.TryAddSingleton(); services.TryAddSingleton(); diff --git a/src/Microsoft.AspNetCore.Mvc.RazorPages/Infrastructure/DefaultPageFactory.cs b/src/Microsoft.AspNetCore.Mvc.RazorPages/Infrastructure/DefaultPageFactoryProvider.cs similarity index 96% rename from src/Microsoft.AspNetCore.Mvc.RazorPages/Infrastructure/DefaultPageFactory.cs rename to src/Microsoft.AspNetCore.Mvc.RazorPages/Infrastructure/DefaultPageFactoryProvider.cs index e4b21d0938..876541f9a1 100644 --- a/src/Microsoft.AspNetCore.Mvc.RazorPages/Infrastructure/DefaultPageFactory.cs +++ b/src/Microsoft.AspNetCore.Mvc.RazorPages/Infrastructure/DefaultPageFactoryProvider.cs @@ -13,13 +13,13 @@ using Microsoft.AspNetCore.Mvc.ViewFeatures; namespace Microsoft.AspNetCore.Mvc.RazorPages.Infrastructure { - public class DefaultPageFactory : IPageFactoryProvider + public class DefaultPageFactoryProvider : IPageFactoryProvider { private readonly IPageActivatorProvider _pageActivator; private readonly IModelMetadataProvider _modelMetadataProvider; private readonly RazorPagePropertyActivator.PropertyValueAccessors _propertyAccessors; - public DefaultPageFactory( + public DefaultPageFactoryProvider( IPageActivatorProvider pageActivator, IModelMetadataProvider metadataProvider, IUrlHelperFactory urlHelperFactory, diff --git a/src/Microsoft.AspNetCore.Mvc.RazorPages/Infrastructure/PageResultExecutor.cs b/src/Microsoft.AspNetCore.Mvc.RazorPages/Infrastructure/PageResultExecutor.cs index 93e4ec574d..d545a04a9a 100644 --- a/src/Microsoft.AspNetCore.Mvc.RazorPages/Infrastructure/PageResultExecutor.cs +++ b/src/Microsoft.AspNetCore.Mvc.RazorPages/Infrastructure/PageResultExecutor.cs @@ -43,7 +43,7 @@ namespace Microsoft.AspNetCore.Mvc.RazorPages.Infrastructure { _razorViewEngine = razorViewEngine; _htmlEncoder = htmlEncoder; - _razorPageActivator = new PassThruRazorPageActivator(razorPageActivator); + _razorPageActivator = razorPageActivator; } /// @@ -77,7 +77,7 @@ namespace Microsoft.AspNetCore.Mvc.RazorPages.Infrastructure _razorViewEngine, _razorPageActivator, viewStarts, - result.Page, + new RazorPageAdapter(result.Page), _htmlEncoder); return ExecuteAsync(viewContext, result.ContentType, result.StatusCode); diff --git a/src/Microsoft.AspNetCore.Mvc.RazorPages/Infrastructure/RazorPageAdapter.cs b/src/Microsoft.AspNetCore.Mvc.RazorPages/Infrastructure/RazorPageAdapter.cs new file mode 100644 index 0000000000..e98769267d --- /dev/null +++ b/src/Microsoft.AspNetCore.Mvc.RazorPages/Infrastructure/RazorPageAdapter.cs @@ -0,0 +1,80 @@ +// 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.Text; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Html; +using Microsoft.AspNetCore.Mvc.Razor; +using Microsoft.AspNetCore.Mvc.Rendering; + +namespace Microsoft.AspNetCore.Mvc.RazorPages.Infrastructure +{ + // Implements IRazorPage so that RazorPageBase-derived classes don't get activated twice. + // + // The page gets activated before handler methods run, but the RazorView will also activate + // each page. + public class RazorPageAdapter : IRazorPage + { + private readonly RazorPageBase _page; + + public RazorPageAdapter(RazorPageBase page) + { + if (page == null) + { + throw new ArgumentNullException(nameof(page)); + } + + _page = page; + } + + public ViewContext ViewContext + { + get { return _page.ViewContext; } + set { _page.ViewContext = value; } + } + + public IHtmlContent BodyContent + { + get { return _page.BodyContent; } + set { _page.BodyContent = value; } + } + + public bool IsLayoutBeingRendered + { + get { return _page.IsLayoutBeingRendered; } + set { _page.IsLayoutBeingRendered = value; } + } + + public string Path + { + get { return _page.Path; } + set { _page.Path = value; } + } + + public string Layout + { + get { return _page.Layout; } + set { _page.Layout = value; } + } + + public IDictionary PreviousSectionWriters + { + get { return _page.PreviousSectionWriters; } + set { _page.PreviousSectionWriters = value; } + } + + public IDictionary SectionWriters => _page.SectionWriters; + + public void EnsureRenderedBodyOrSections() + { + _page.EnsureRenderedBodyOrSections(); + } + + public Task ExecuteAsync() + { + return _page.ExecuteAsync(); + } + } +} diff --git a/src/Microsoft.AspNetCore.Mvc.RazorPages/Internal/PassThruRazorPageActivator.cs b/src/Microsoft.AspNetCore.Mvc.RazorPages/Internal/PassThruRazorPageActivator.cs deleted file mode 100644 index 432367a6e4..0000000000 --- a/src/Microsoft.AspNetCore.Mvc.RazorPages/Internal/PassThruRazorPageActivator.cs +++ /dev/null @@ -1,39 +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 Microsoft.AspNetCore.Mvc.Razor; -using Microsoft.AspNetCore.Mvc.Rendering; -using Microsoft.AspNetCore.Mvc.ViewFeatures; - -namespace Microsoft.AspNetCore.Mvc.RazorPages.Internal -{ - public class PassThruRazorPageActivator : IRazorPageActivator - { - private readonly IRazorPageActivator _pageActivator; - - public PassThruRazorPageActivator(IRazorPageActivator pageActivator) - { - _pageActivator = pageActivator; - } - - public void Activate(IRazorPage page, ViewContext context) - { - var razorView = (RazorView)context.View; - if (ReferenceEquals(page, razorView.RazorPage)) - { - var actionDescriptor = (CompiledPageActionDescriptor)context.ActionDescriptor; - var vddType = typeof(ViewDataDictionary<>); - - var modelTypeInfo = actionDescriptor.ModelTypeInfo ?? actionDescriptor.PageTypeInfo; - vddType = vddType.MakeGenericType(modelTypeInfo.AsType()); - - context.ViewData = (ViewDataDictionary)Activator.CreateInstance(vddType, context.ViewData); - } - else - { - _pageActivator.Activate(page, context); - } - } - } -} diff --git a/src/Microsoft.AspNetCore.Mvc.RazorPages/PageBase.cs b/src/Microsoft.AspNetCore.Mvc.RazorPages/PageBase.cs index 9a7ac319b3..ca5fa86731 100644 --- a/src/Microsoft.AspNetCore.Mvc.RazorPages/PageBase.cs +++ b/src/Microsoft.AspNetCore.Mvc.RazorPages/PageBase.cs @@ -25,7 +25,7 @@ namespace Microsoft.AspNetCore.Mvc.RazorPages /// A base class for a Razor page. /// [PagesBaseClass] - public abstract class PageBase : RazorPageBase, IRazorPage + public abstract class PageBase : RazorPageBase { private IObjectModelValidator _objectValidator; private IModelMetadataProvider _metadataProvider; @@ -112,6 +112,7 @@ namespace Microsoft.AspNetCore.Mvc.RazorPages /// public override void EnsureRenderedBodyOrSections() { + // This will never be called by MVC. MVC only calls this method on layout pages, and a Page can never be a layout page. throw new NotSupportedException(); } diff --git a/test/Microsoft.AspNetCore.Mvc.Razor.Test/RazorPageActivatorTest.cs b/test/Microsoft.AspNetCore.Mvc.Razor.Test/RazorPageActivatorTest.cs index 81e1a231d9..17b0999b58 100644 --- a/test/Microsoft.AspNetCore.Mvc.Razor.Test/RazorPageActivatorTest.cs +++ b/test/Microsoft.AspNetCore.Mvc.Razor.Test/RazorPageActivatorTest.cs @@ -2,13 +2,12 @@ // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. using System; -using System.Buffers; using System.Diagnostics; using System.IO; +using System.Text.Encodings.Web; using System.Threading.Tasks; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc.Abstractions; -using Microsoft.AspNetCore.Mvc.Formatters; using Microsoft.AspNetCore.Mvc.ModelBinding; using Microsoft.AspNetCore.Mvc.Razor.Internal; using Microsoft.AspNetCore.Mvc.Rendering; @@ -20,339 +19,185 @@ using Microsoft.AspNetCore.Routing; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.WebEncoders.Testing; using Moq; -using Newtonsoft.Json; using Xunit; namespace Microsoft.AspNetCore.Mvc.Razor { public class RazorPageActivatorTest { + public RazorPageActivatorTest() + { + DiagnosticSource = new DiagnosticListener("Microsoft.AspNetCore"); + HtmlEncoder = new HtmlTestEncoder(); + JsonHelper = Mock.Of(); + MetadataProvider = new EmptyModelMetadataProvider(); + ModelExpressionProvider = new ModelExpressionProvider(MetadataProvider, new ExpressionTextCache()); + UrlHelperFactory = new UrlHelperFactory(); + } + + private DiagnosticSource DiagnosticSource { get; } + + private HtmlEncoder HtmlEncoder { get; } + + private IJsonHelper JsonHelper { get; } + + private IModelMetadataProvider MetadataProvider { get; } + + private IModelExpressionProvider ModelExpressionProvider { get; } + + private IUrlHelperFactory UrlHelperFactory { get; } + [Fact] - public void Activate_ActivatesAndContextualizesPropertiesOnViews() + public void Activate_ContextualizesServices_AndSetsProperties_OnPage() { // Arrange - var modelMetadataProvider = new EmptyModelMetadataProvider(); - var modelExpressionProvider = new ModelExpressionProvider(modelMetadataProvider, new ExpressionTextCache()); - var urlHelperFactory = new UrlHelperFactory(); - var jsonHelper = new JsonHelper( - new JsonOutputFormatter(new JsonSerializerSettings(), ArrayPool.Shared), - ArrayPool.Shared); - var htmlEncoder = new HtmlTestEncoder(); - var diagnosticSource = new DiagnosticListener("Microsoft.AspNetCore"); - var activator = new RazorPageActivator( - new EmptyModelMetadataProvider(), - urlHelperFactory, - jsonHelper, - diagnosticSource, - htmlEncoder, - modelExpressionProvider); + var activator = CreateActivator(); var instance = new TestRazorPage(); + var viewData = new ViewDataDictionary(MetadataProvider, new ModelStateDictionary()); + var viewContext = CreateViewContext(); - var myService = new MyService(); - var helper = Mock.Of>(); - - var serviceProvider = new ServiceCollection() - .AddSingleton(myService) - .AddSingleton(helper) - .AddSingleton(new ExpressionTextCache()) - .BuildServiceProvider(); - var httpContext = new DefaultHttpContext - { - RequestServices = serviceProvider - }; - - var actionContext = new ActionContext(httpContext, new RouteData(), new ActionDescriptor()); - var viewContext = new ViewContext( - actionContext, - Mock.Of(), - new ViewDataDictionary(new EmptyModelMetadataProvider(), new ModelStateDictionary()), - Mock.Of(), - TextWriter.Null, - new HtmlHelperOptions()); - - var urlHelper = urlHelperFactory.GetUrlHelper(viewContext); + var urlHelper = UrlHelperFactory.GetUrlHelper(viewContext); // Act activator.Activate(instance, viewContext); // Assert - Assert.Same(helper, instance.Html); - Assert.Same(myService, instance.MyService); - Assert.Same(viewContext, myService.ViewContext); - Assert.Same(diagnosticSource, instance.DiagnosticSource); - Assert.Same(htmlEncoder, instance.HtmlEncoder); - Assert.Same(jsonHelper, instance.Json); + Assert.Same(DiagnosticSource, instance.DiagnosticSource); + Assert.Same(HtmlEncoder, instance.HtmlEncoder); + Assert.Same(JsonHelper, instance.Json); Assert.Same(urlHelper, instance.Url); + Assert.Same(viewContext.ViewData, instance.ViewData); + + // Has no [RazorInject] so it shouldn't get injected Assert.Null(instance.MyService2); + + // We're not testing the IViewContextualizable implementation here because it's a mock. + Assert.NotNull(instance.Html); + Assert.IsAssignableFrom>(instance.Html); + + var service = instance.MyService; + Assert.NotNull(service); + Assert.Same(viewContext, service.ViewContext); } [Fact] - public void Activate_ThrowsIfTheViewDoesNotDeriveFromRazorViewOfT() + public void Activate_ContextualizesServices_AndSetsProperties_OnPageWithoutModel() { // Arrange - var modelMetadataProvider = new EmptyModelMetadataProvider(); - var modelExpressionProvider = new ModelExpressionProvider(modelMetadataProvider, new ExpressionTextCache()); - var activator = new RazorPageActivator( - modelMetadataProvider, - new UrlHelperFactory(), - new JsonHelper( - new JsonOutputFormatter(new JsonSerializerSettings(), ArrayPool.Shared), - ArrayPool.Shared), - new DiagnosticListener("Microsoft.AspNetCore"), - new HtmlTestEncoder(), - modelExpressionProvider); + var activator = CreateActivator(); - var instance = new DoesNotDeriveFromRazorPageOfT(); + var viewData = new ViewDataDictionary(MetadataProvider, new ModelStateDictionary()); + var viewContext = CreateViewContext(viewData); - var myService = new MyService(); - var helper = Mock.Of>(); - var serviceProvider = new Mock(); - var httpContext = new DefaultHttpContext - { - RequestServices = new ServiceCollection().BuildServiceProvider() - }; + var urlHelper = UrlHelperFactory.GetUrlHelper(viewContext); - var actionContext = new ActionContext(httpContext, new RouteData(), new ActionDescriptor()); - var viewContext = new ViewContext( - actionContext, - Mock.Of(), - new ViewDataDictionary(new EmptyModelMetadataProvider(), new ModelStateDictionary()), - Mock.Of(), - TextWriter.Null, - new HtmlHelperOptions()); + var instance = new NoModelPropertyPage(); - // Act and Assert - var ex = Assert.Throws(() => activator.Activate(instance, viewContext)); - var message = $"View of type '{instance.GetType()}' cannot be activated by '{typeof(RazorPageActivator)}'."; - Assert.Equal(message, ex.Message); + // Act + activator.Activate(instance, viewContext); + + // Assert + Assert.Same(DiagnosticSource, instance.DiagnosticSource); + Assert.Same(HtmlEncoder, instance.HtmlEncoder); + + // When we don't have a model property, the activator will just leave viewdata alone. + Assert.NotNull(viewContext.ViewData); } [Fact] public void Activate_InstantiatesNewViewDataDictionaryType_IfTheTypeDoesNotMatch() { // Arrange - var modelMetadataProvider = new EmptyModelMetadataProvider(); - var modelExpressionProvider = new ModelExpressionProvider(modelMetadataProvider, new ExpressionTextCache()); - var activator = new RazorPageActivator( - modelMetadataProvider, - new UrlHelperFactory(), - new JsonHelper( - new JsonOutputFormatter(new JsonSerializerSettings(), ArrayPool.Shared), - ArrayPool.Shared), - new DiagnosticListener("Microsoft.AspNetCore.Mvc"), - new HtmlTestEncoder(), - modelExpressionProvider); + var activator = CreateActivator(); + + var viewData = new ViewDataDictionary(MetadataProvider, new ModelStateDictionary()) + { + { "key", "value" }, + }; + var viewContext = CreateViewContext(viewData); + + var urlHelper = UrlHelperFactory.GetUrlHelper(viewContext); var instance = new TestRazorPage(); - var myService = new MyService(); - var helper = Mock.Of>(); - var serviceProvider = new ServiceCollection() - .AddSingleton(myService) - .AddSingleton(helper) - .AddSingleton(new ExpressionTextCache()) - .BuildServiceProvider(); - var httpContext = new DefaultHttpContext - { - RequestServices = serviceProvider - }; - - var actionContext = new ActionContext(httpContext, new RouteData(), new ActionDescriptor()); - var viewData = new ViewDataDictionary(new EmptyModelMetadataProvider(), new ModelStateDictionary()) - { - Model = new MyModel() - }; - var viewContext = new ViewContext( - actionContext, - Mock.Of(), - viewData, - Mock.Of(), - TextWriter.Null, - new HtmlHelperOptions()); - // Act activator.Activate(instance, viewContext); // Assert - Assert.IsType>(viewContext.ViewData); - } + Assert.Same(DiagnosticSource, instance.DiagnosticSource); + Assert.Same(HtmlEncoder, instance.HtmlEncoder); + Assert.Same(JsonHelper, instance.Json); + Assert.Same(urlHelper, instance.Url); + Assert.Same(viewContext.ViewData, instance.ViewData); - [Fact] - public void Activate_UsesPassedInViewDataDictionaryInstance_IfPassedInTypeMatches() - { - // Arrange - var modelMetadataProvider = new EmptyModelMetadataProvider(); - var modelExpressionProvider = new ModelExpressionProvider(modelMetadataProvider, new ExpressionTextCache()); - var activator = new RazorPageActivator( - modelMetadataProvider, - new UrlHelperFactory(), - new JsonHelper( - new JsonOutputFormatter(new JsonSerializerSettings(), ArrayPool.Shared), - ArrayPool.Shared), - new DiagnosticListener("Microsoft.AspNetCore.Mvc"), - new HtmlTestEncoder(), - modelExpressionProvider); + // The original ViewDataDictionary was replaced. + Assert.NotSame(viewData, viewContext.ViewData); + Assert.NotSame(viewData, instance.ViewData); - var instance = new TestRazorPage(); - var myService = new MyService(); - var helper = Mock.Of>(); - var serviceProvider = new ServiceCollection() - .AddSingleton(myService) - .AddSingleton(helper) - .AddSingleton(new ExpressionTextCache()) - .BuildServiceProvider(); - var httpContext = new DefaultHttpContext - { - RequestServices = serviceProvider - }; + // But this value is copied + Assert.Equal("value", viewData["key"]); - var actionContext = new ActionContext(httpContext, new RouteData(), new ActionDescriptor()); - var viewData = new ViewDataDictionary(new EmptyModelMetadataProvider(), new ModelStateDictionary()) - { - Model = new MyModel() - }; - var viewContext = new ViewContext( - actionContext, - Mock.Of(), - viewData, - Mock.Of(), - TextWriter.Null, - new HtmlHelperOptions()); + // Has no [RazorInject] so it shouldn't get injected + Assert.Null(instance.MyService2); - // Act - activator.Activate(instance, viewContext); + // We're not testing the IViewContextualizable implementation here because it's a mock. + Assert.NotNull(instance.Html); + Assert.IsAssignableFrom>(instance.Html); - // Assert - Assert.Same(viewData, viewContext.ViewData); - } - - [Fact] - public void Activate_DeterminesModelTypeFromProperty() - { - // Arrange - var modelMetadataProvider = new EmptyModelMetadataProvider(); - var modelExpressionProvider = new ModelExpressionProvider(modelMetadataProvider, new ExpressionTextCache()); - var activator = new RazorPageActivator( - modelMetadataProvider, - new UrlHelperFactory(), - new JsonHelper( - new JsonOutputFormatter(new JsonSerializerSettings(), ArrayPool.Shared), - ArrayPool.Shared), - new DiagnosticListener("Microsoft.AspNetCore.Mvc"), - new HtmlTestEncoder(), - modelExpressionProvider); - - var instance = new DoesNotDeriveFromRazorPageOfTButHasModelProperty(); - var myService = new MyService(); - var helper = Mock.Of>(); - var serviceProvider = new ServiceCollection() - .AddSingleton(myService) - .AddSingleton(helper) - .AddSingleton(new ExpressionTextCache()) - .BuildServiceProvider(); - var httpContext = new DefaultHttpContext - { - RequestServices = serviceProvider - }; - - var actionContext = new ActionContext(httpContext, new RouteData(), new ActionDescriptor()); - var viewData = new ViewDataDictionary(new EmptyModelMetadataProvider(), new ModelStateDictionary()); - var viewContext = new ViewContext( - actionContext, - Mock.Of(), - viewData, - Mock.Of(), - TextWriter.Null, - new HtmlHelperOptions()); - - // Act - activator.Activate(instance, viewContext); - - // Assert - Assert.IsType>(viewContext.ViewData); + var service = instance.MyService; + Assert.NotNull(service); + Assert.Same(viewContext, service.ViewContext); } [Fact] public void Activate_Throws_WhenViewDataPropertyHasIncorrectType() { // Arrange - var modelMetadataProvider = new EmptyModelMetadataProvider(); - var modelExpressionProvider = new ModelExpressionProvider(modelMetadataProvider, new ExpressionTextCache()); - var activator = new RazorPageActivator( - modelMetadataProvider, - new UrlHelperFactory(), - new JsonHelper( - new JsonOutputFormatter(new JsonSerializerSettings(), ArrayPool.Shared), - ArrayPool.Shared), - new DiagnosticListener("Microsoft.AspNetCore.Mvc"), - new HtmlTestEncoder(), - modelExpressionProvider); + var activator = CreateActivator(); + + var viewData = new ViewDataDictionary(MetadataProvider, new ModelStateDictionary()); + var viewContext = CreateViewContext(viewData); var instance = new HasIncorrectViewDataPropertyType(); - var collection = new ServiceCollection(); - collection.AddSingleton(new ExpressionTextCache()); - var httpContext = new DefaultHttpContext - { - RequestServices = collection.BuildServiceProvider(), - }; - - var actionContext = new ActionContext(httpContext, new RouteData(), new ActionDescriptor()); - var viewContext = new ViewContext( - actionContext, - Mock.Of(), - new ViewDataDictionary(new EmptyModelMetadataProvider(), new ModelStateDictionary()), - Mock.Of(), - TextWriter.Null, - new HtmlHelperOptions()); - // Act & Assert Assert.Throws(() => activator.Activate(instance, viewContext)); } - [Fact] - public void Activate_CanGetUrlHelperFromDependencyInjection() + private RazorPageActivator CreateActivator() { - // Arrange - var modelMetadataProvider = new EmptyModelMetadataProvider(); - var modelExpressionProvider = new ModelExpressionProvider(modelMetadataProvider, new ExpressionTextCache()); - var activator = new RazorPageActivator( - modelMetadataProvider, - new UrlHelperFactory(), - new JsonHelper( - new JsonOutputFormatter(new JsonSerializerSettings(), ArrayPool.Shared), - ArrayPool.Shared), - new DiagnosticListener("Microsoft.AspNetCore.Mvc"), - new HtmlTestEncoder(), - modelExpressionProvider); + return new RazorPageActivator(MetadataProvider, UrlHelperFactory, JsonHelper, DiagnosticSource, HtmlEncoder, ModelExpressionProvider); + } - var instance = new HasUnusualIUrlHelperProperty(); + private ViewContext CreateViewContext(ViewDataDictionary viewData = null) + { + if (viewData == null) + { + viewData = new ViewDataDictionary(MetadataProvider, new ModelStateDictionary()); + } - // IUrlHelperFactory should not be used. But set it up to match a real configuration. - var collection = new ServiceCollection(); - collection + var myService = new MyService(); + var htmlHelper = Mock.Of>(); + + var serviceProvider = new ServiceCollection() + .AddSingleton(myService) + .AddSingleton(htmlHelper) .AddSingleton(new ExpressionTextCache()) - .AddSingleton(); + .BuildServiceProvider(); + var httpContext = new DefaultHttpContext { - RequestServices = collection.BuildServiceProvider(), + RequestServices = serviceProvider }; - var actionContext = new ActionContext(httpContext, new RouteData(), new ActionDescriptor()); - var viewContext = new ViewContext( + return new ViewContext( actionContext, Mock.Of(), - new ViewDataDictionary(new EmptyModelMetadataProvider(), new ModelStateDictionary()), + viewData, Mock.Of(), TextWriter.Null, new HtmlHelperOptions()); - - // Act - activator.Activate(instance, viewContext); - - // Assert - Assert.NotNull(instance.UrlHelper); } private abstract class TestPageBase : RazorPage @@ -380,11 +225,13 @@ namespace Microsoft.AspNetCore.Mvc.Razor } } - private abstract class DoesNotDeriveFromRazorPageOfTBase : RazorPage + private abstract class NoModelPropertyBase : RazorPage { + [RazorInject] + public ViewDataDictionary ViewData { get; set; } } - private class DoesNotDeriveFromRazorPageOfT : DoesNotDeriveFromRazorPageOfTBase + private class NoModelPropertyPage : NoModelPropertyBase { public override Task ExecuteAsync() { @@ -392,16 +239,6 @@ namespace Microsoft.AspNetCore.Mvc.Razor } } - private class DoesNotDeriveFromRazorPageOfTButHasModelProperty : DoesNotDeriveFromRazorPageOfTBase - { - public string Model { get; set; } - - public override Task ExecuteAsync() - { - throw new NotImplementedException(); - } - } - private class HasIncorrectViewDataPropertyType : RazorPage { [RazorInject] @@ -413,57 +250,6 @@ namespace Microsoft.AspNetCore.Mvc.Razor } } - private class HasUnusualIUrlHelperProperty : RazorPage - { - [RazorInject] - public IUrlHelperWrapper UrlHelper { get; set; } - - public override Task ExecuteAsync() - { - throw new NotImplementedException(); - } - } - - private class UrlHelperWrapper : IUrlHelperWrapper - { - public ActionContext ActionContext - { - get - { - throw new NotImplementedException(); - } - } - - public string Action(UrlActionContext actionContext) - { - throw new NotImplementedException(); - } - - public string Content(string contentPath) - { - throw new NotImplementedException(); - } - - public bool IsLocalUrl(string url) - { - throw new NotImplementedException(); - } - - public string Link(string routeName, object values) - { - throw new NotImplementedException(); - } - - public string RouteUrl(UrlRouteContext routeContext) - { - throw new NotImplementedException(); - } - } - - private interface IUrlHelperWrapper : IUrlHelper - { - } - private class MyService : IViewContextAware { public ViewContext ViewContext { get; private set; } diff --git a/test/Microsoft.AspNetCore.Mvc.RazorPages.Test/Infrastructure/DefaultPageFactoryProviderTest.cs b/test/Microsoft.AspNetCore.Mvc.RazorPages.Test/Infrastructure/DefaultPageFactoryProviderTest.cs index f0ccc4602d..e248494282 100644 --- a/test/Microsoft.AspNetCore.Mvc.RazorPages.Test/Infrastructure/DefaultPageFactoryProviderTest.cs +++ b/test/Microsoft.AspNetCore.Mvc.RazorPages.Test/Infrastructure/DefaultPageFactoryProviderTest.cs @@ -260,7 +260,7 @@ namespace Microsoft.AspNetCore.Mvc.RazorPages.Infrastructure Assert.NotNull(testPage.ModelExpressionProviderWithInject); } - private static DefaultPageFactory CreatePageFactory( + private static DefaultPageFactoryProvider CreatePageFactory( IPageActivatorProvider pageActivator = null, IModelMetadataProvider provider = null, IUrlHelperFactory urlHelperFactory = null, @@ -269,7 +269,7 @@ namespace Microsoft.AspNetCore.Mvc.RazorPages.Infrastructure HtmlEncoder htmlEncoder = null, IModelExpressionProvider modelExpressionProvider = null) { - return new DefaultPageFactory( + return new DefaultPageFactoryProvider( pageActivator ?? CreateActivator(), provider ?? Mock.Of(), urlHelperFactory ?? Mock.Of(),