diff --git a/src/Microsoft.AspNetCore.Mvc.Razor/RazorPage.cs b/src/Microsoft.AspNetCore.Mvc.Razor/RazorPage.cs index 4c220f1799..d8613d1e9d 100644 --- a/src/Microsoft.AspNetCore.Mvc.Razor/RazorPage.cs +++ b/src/Microsoft.AspNetCore.Mvc.Razor/RazorPage.cs @@ -7,6 +7,7 @@ using System.Diagnostics; using System.Linq; using System.Threading.Tasks; using Microsoft.AspNetCore.Html; +using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc.Rendering; using Microsoft.AspNetCore.Mvc.ViewFeatures; @@ -30,10 +31,9 @@ namespace Microsoft.AspNetCore.Mvc.Razor } /// - /// Gets the from the . + /// An representing the current request execution. /// - /// Returns null if is null. - public ITempDataDictionary TempData => ViewContext?.TempData; + public HttpContext Context => ViewContext?.HttpContext; /// /// In a Razor layout page, renders the portion of a content page that is not within a named section. diff --git a/src/Microsoft.AspNetCore.Mvc.Razor/RazorPageBase.cs b/src/Microsoft.AspNetCore.Mvc.Razor/RazorPageBase.cs index a58edccf8f..ea56d8302e 100644 --- a/src/Microsoft.AspNetCore.Mvc.Razor/RazorPageBase.cs +++ b/src/Microsoft.AspNetCore.Mvc.Razor/RazorPageBase.cs @@ -15,6 +15,7 @@ using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc.Razor.Internal; using Microsoft.AspNetCore.Mvc.Rendering; using Microsoft.AspNetCore.Mvc.Routing; +using Microsoft.AspNetCore.Mvc.ViewFeatures; using Microsoft.AspNetCore.Mvc.ViewFeatures.Internal; using Microsoft.AspNetCore.Razor.Runtime.TagHelpers; using Microsoft.AspNetCore.Razor.TagHelpers; @@ -35,15 +36,10 @@ namespace Microsoft.AspNetCore.Mvc.Razor private TagHelperAttributeInfo _tagHelperAttributeInfo; private IUrlHelper _urlHelper; - public ViewContext ViewContext { get; set; } + public virtual ViewContext ViewContext { get; set; } public string Layout { get; set; } - /// - /// An representing the current request execution. - /// - public HttpContext Context => ViewContext?.HttpContext; - /// /// Gets the that the page is writing output to. /// @@ -101,7 +97,13 @@ namespace Microsoft.AspNetCore.Mvc.Razor /// /// Gets the of the current logged in user. /// - public virtual ClaimsPrincipal User => Context?.User; + public virtual ClaimsPrincipal User => ViewContext?.HttpContext?.User; + + /// + /// Gets the from the . + /// + /// Returns null if is null. + public ITempDataDictionary TempData => ViewContext?.TempData; protected Stack TagHelperScopes { get; } = new Stack(); @@ -282,7 +284,7 @@ namespace Microsoft.AspNetCore.Mvc.Razor if (_urlHelper == null) { - var services = Context.RequestServices; + var services = ViewContext?.HttpContext.RequestServices; var factory = services.GetRequiredService(); _urlHelper = factory.GetUrlHelper(ViewContext); } @@ -707,7 +709,7 @@ namespace Microsoft.AspNetCore.Mvc.Razor } await Output.FlushAsync(); - await Context.Response.Body.FlushAsync(); + await ViewContext?.HttpContext.Response.Body.FlushAsync(); return HtmlString.Empty; } @@ -719,8 +721,8 @@ namespace Microsoft.AspNetCore.Mvc.Razor /// before flushes the headers. public virtual HtmlString SetAntiforgeryCookieAndHeader() { - var antiforgery = Context.RequestServices.GetRequiredService(); - antiforgery.SetCookieTokenAndHeader(Context); + var antiforgery = ViewContext?.HttpContext.RequestServices.GetRequiredService(); + antiforgery.SetCookieTokenAndHeader(ViewContext?.HttpContext); return HtmlString.Empty; } diff --git a/src/Microsoft.AspNetCore.Mvc.RazorPages/Page.cs b/src/Microsoft.AspNetCore.Mvc.RazorPages/Page.cs index 0007c2d3e5..32e73ad0b3 100644 --- a/src/Microsoft.AspNetCore.Mvc.RazorPages/Page.cs +++ b/src/Microsoft.AspNetCore.Mvc.RazorPages/Page.cs @@ -3,10 +3,11 @@ using System; using System.Diagnostics; +using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc.ModelBinding; using Microsoft.AspNetCore.Mvc.Razor; using Microsoft.AspNetCore.Mvc.RazorPages.Infrastructure; -using Microsoft.AspNetCore.Mvc.ViewFeatures; +using Microsoft.AspNetCore.Mvc.Rendering; using Microsoft.Extensions.DependencyInjection; namespace Microsoft.AspNetCore.Mvc.RazorPages @@ -23,6 +24,31 @@ namespace Microsoft.AspNetCore.Mvc.RazorPages /// public PageContext PageContext { get; set; } + /// + public override ViewContext ViewContext + { + get => PageContext; + set + { + PageContext = (PageContext)value; + } + } + + /// + /// Gets the . + /// + public HttpContext HttpContext => PageContext?.HttpContext; + + /// + /// Gets the . + /// + public HttpRequest Request => HttpContext?.Request; + + /// + /// Gets the . + /// + public HttpResponse Response => HttpContext?.Response; + /// /// Gets or sets the . /// @@ -54,12 +80,6 @@ namespace Microsoft.AspNetCore.Mvc.RazorPages /// public ModelStateDictionary ModelState => PageContext?.ModelState; - /// - /// Gets the from the . - /// - /// Returns null if is null. - public ITempDataDictionary TempData => PageContext?.TempData; - /// public override void EnsureRenderedBodyOrSections() { diff --git a/src/Microsoft.AspNetCore.Mvc.RazorPages/PageModel.cs b/src/Microsoft.AspNetCore.Mvc.RazorPages/PageModel.cs index b59bc66acd..292b1e4d0a 100644 --- a/src/Microsoft.AspNetCore.Mvc.RazorPages/PageModel.cs +++ b/src/Microsoft.AspNetCore.Mvc.RazorPages/PageModel.cs @@ -3,8 +3,11 @@ using System; using System.Threading.Tasks; +using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc.ModelBinding; using Microsoft.AspNetCore.Mvc.RazorPages.Infrastructure; +using Microsoft.AspNetCore.Mvc.Rendering; +using Microsoft.AspNetCore.Mvc.Routing; using Microsoft.AspNetCore.Mvc.ViewFeatures; using Microsoft.Extensions.DependencyInjection; @@ -13,14 +16,18 @@ namespace Microsoft.AspNetCore.Mvc.RazorPages public abstract class PageModel { private PageArgumentBinder _binder; + private IUrlHelper _urlHelper; + /// + /// Gets or sets the . + /// public PageArgumentBinder Binder { get { if (_binder == null) { - _binder = PageContext.HttpContext.RequestServices.GetRequiredService(); + _binder = HttpContext?.RequestServices?.GetRequiredService(); } return _binder; @@ -37,11 +44,66 @@ namespace Microsoft.AspNetCore.Mvc.RazorPages } } - public Page Page => PageContext.Page; + /// + /// Gets or sets the . + /// + public IUrlHelper Url + { + get + { + if (_urlHelper == null) + { + var factory = HttpContext?.RequestServices?.GetRequiredService(); + _urlHelper = factory?.GetUrlHelper(PageContext); + } + return _urlHelper; + } + set + { + if (value == null) + { + throw new ArgumentNullException(nameof(value)); + } + + _urlHelper = value; + } + } + + /// + /// Gets the instance this model belongs to. + /// + public Page Page => PageContext?.Page; + + /// + /// Gets the . + /// [PageContext] public PageContext PageContext { get; set; } + /// + /// Gets the . + /// + public ViewContext ViewContext => PageContext; + + /// + /// Gets the . + /// + public HttpContext HttpContext => PageContext?.HttpContext; + + /// + /// Gets the . + /// + public HttpRequest Request => HttpContext?.Request; + + /// + /// Gets the . + /// + public HttpResponse Response => HttpContext?.Response; + + /// + /// Gets the . + /// public ModelStateDictionary ModelState => PageContext.ModelState; /// @@ -50,34 +112,75 @@ namespace Microsoft.AspNetCore.Mvc.RazorPages /// Returns null if is null. public ITempDataDictionary TempData => PageContext?.TempData; + /// + /// Gets the . + /// public ViewDataDictionary ViewData => PageContext?.ViewData; - protected Task BindAsync(string name) + /// + /// Binds the model with the specified . + /// + /// The model type. + /// The model name. + /// A that on completion returns the bound model. + protected internal Task BindAsync(string name) { - return Binder.BindModelAsync(PageContext, name); + return Binder.BindModelAsync(PageContext, name); } - protected Task BindAsync(T @default, string name) + /// + /// Binds the model with the specified . + /// + /// The model type. + /// The model name. + /// The default model value. + /// A that on completion returns the bound model. + protected internal Task BindAsync(TModel @default, string name) { - return Binder.BindModelAsync(PageContext, @default, name); + return Binder.BindModelAsync(PageContext, @default, name); } - protected Task TryUpdateModelAsync(T value) + /// + /// Updates the specified instance using values from the controller's current + /// . + /// + /// The type of the model object. + /// The model instance to update. + /// A that on completion returns true if the update is successful. + protected internal Task TryUpdateModelAsync(TModel model) { - return Binder.TryUpdateModelAsync(PageContext, value); + return Binder.TryUpdateModelAsync(PageContext, model); } - protected Task TryUpdateModelAsync(T value, string name) + /// + /// Updates the specified instance using values from the controller's current + /// . + /// + /// The type of the model object. + /// The model instance to update. + /// The model name. + /// A that on completion returns true if the update is successful. + protected internal Task TryUpdateModelAsync(TModel model, string name) { - return Binder.TryUpdateModelAsync(PageContext, value, name); + return Binder.TryUpdateModelAsync(PageContext, model, name); } - protected IActionResult Redirect(string url) + /// + /// Creates a object that redirects () + /// to the specified . + /// + /// The URL to redirect to. + /// The created for the response. + protected internal RedirectResult Redirect(string url) { return new RedirectResult(url); } - protected IActionResult View() + /// + /// Creates a object that renders the page. + /// + /// The . + protected internal PageViewResult View() { return new PageViewResult(Page); } diff --git a/test/Microsoft.AspNetCore.Mvc.FunctionalTests/RazorPagesTest.cs b/test/Microsoft.AspNetCore.Mvc.FunctionalTests/RazorPagesTest.cs index 39675d6718..1046b880b7 100644 --- a/test/Microsoft.AspNetCore.Mvc.FunctionalTests/RazorPagesTest.cs +++ b/test/Microsoft.AspNetCore.Mvc.FunctionalTests/RazorPagesTest.cs @@ -590,6 +590,22 @@ namespace Microsoft.AspNetCore.Mvc.FunctionalTests Assert.DoesNotContain(validationError, content); } + [Fact] + public async Task PagePropertiesAreInjected() + { + // Arrange + var expected = +@"Microsoft.AspNetCore.Mvc.Routing.UrlHelper +Microsoft.AspNetCore.Mvc.ViewFeatures.HtmlHelper`1[AspNetCore._InjectedPageProperties_cshtml] +Microsoft.AspNetCore.Mvc.ViewFeatures.ViewDataDictionary`1[AspNetCore._InjectedPageProperties_cshtml]"; + + // Act + var response = await Client.GetStringAsync("InjectedPageProperties"); + + // Assert + Assert.Equal(expected, response.Trim()); + } + private async Task AddAntiforgeryHeaders(HttpRequestMessage request) { var getResponse = await Client.GetAsync(request.RequestUri); diff --git a/test/Microsoft.AspNetCore.Mvc.RazorPages.Test/PageModelTest.cs b/test/Microsoft.AspNetCore.Mvc.RazorPages.Test/PageModelTest.cs new file mode 100644 index 0000000000..2307219ef3 --- /dev/null +++ b/test/Microsoft.AspNetCore.Mvc.RazorPages.Test/PageModelTest.cs @@ -0,0 +1,165 @@ +// 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.Threading.Tasks; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Mvc.ModelBinding; +using Microsoft.AspNetCore.Mvc.RazorPages.Infrastructure; +using Microsoft.AspNetCore.Mvc.Routing; +using Microsoft.AspNetCore.Mvc.ViewFeatures; +using Microsoft.AspNetCore.Routing; +using Microsoft.Extensions.DependencyInjection; +using Moq; +using Xunit; + +namespace Microsoft.AspNetCore.Mvc.RazorPages +{ + public class PageModelTest + { + [Fact] + public void PageModelPropertiesArePopulatedFromContext() + { + // Arrange + var httpContext = new DefaultHttpContext(); + var modelState = new ModelStateDictionary(); + var actionContext = new ActionContext(httpContext, new RouteData(), new PageActionDescriptor(), modelState); + var modelMetadataProvider = new EmptyModelMetadataProvider(); + var viewDataDictionary = new ViewDataDictionary(modelMetadataProvider, modelState); + var tempData = Mock.Of(); + var pageContext = new PageContext(actionContext, viewDataDictionary, tempData, new HtmlHelperOptions()); + + var page = new TestPage + { + PageContext = pageContext, + }; + pageContext.Page = page; + + var pageModel = new TestPageModel + { + PageContext = pageContext, + }; + + // Act & Assert + Assert.Same(page, pageModel.Page); + Assert.Same(pageContext, pageModel.PageContext); + Assert.Same(pageContext, pageModel.ViewContext); + Assert.Same(httpContext, pageModel.HttpContext); + Assert.Same(httpContext.Request, pageModel.Request); + Assert.Same(httpContext.Response, pageModel.Response); + Assert.Same(modelState, pageModel.ModelState); + Assert.Same(viewDataDictionary, pageModel.ViewData); + Assert.Same(tempData, pageModel.TempData); + } + + [Fact] + public void UrlHelperIsSet() + { + // Arrange + var httpContext = new DefaultHttpContext(); + var urlHelper = Mock.Of(); + var urlHelperFactory = new Mock(); + urlHelperFactory.Setup(f => f.GetUrlHelper(It.IsAny())) + .Returns(urlHelper); + httpContext.RequestServices = new ServiceCollection() + .AddSingleton(urlHelperFactory.Object) + .BuildServiceProvider(); + var actionContext = new ActionContext + { + HttpContext = httpContext, + }; + var pageContext = new PageContext + { + HttpContext = httpContext, + }; + + var pageModel = new TestPageModel + { + PageContext = pageContext, + }; + + // Act & Assert + Assert.Same(urlHelper, pageModel.Url); + } + + [Fact] + public async Task BindModel_InvokesBindOnPageArgumentBinder() + { + // Arrange + var httpContext = new DefaultHttpContext(); + var binder = new TestPageArgumentBinder(); + httpContext.RequestServices = new ServiceCollection() + .AddSingleton(binder) + .BuildServiceProvider(); + var pageContext = new PageContext + { + HttpContext = httpContext, + }; + var pageModel = new TestPageModel + { + PageContext = pageContext, + }; + + // Act + var result = await pageModel.BindAsync("test-name"); + + // Assert + Assert.NotNull(result); + } + + [Fact] + public void Redirect_ReturnsARedirectResult() + { + // Arrange + var pageModel = new TestPageModel(); + + // Act + var result = pageModel.Redirect("test-url"); + + // Assert + var redirectResult = Assert.IsType(result); + Assert.Equal("test-url", redirectResult.Url); + } + + [Fact] + public void View_ReturnsPageViewResult() + { + // Arrange + var page = new TestPage(); + var pageModel = new TestPageModel + { + PageContext = new PageContext + { + Page = page, + } + }; + + // Act + var result = pageModel.View(); + + // Assert + var pageResult = Assert.IsType(result); + Assert.Same(page, pageResult.Page); + } + + private class TestPageModel : PageModel + { + } + + private class TestPage : Page + { + public override Task ExecuteAsync() + { + throw new NotImplementedException(); + } + } + + private class TestPageArgumentBinder : PageArgumentBinder + { + protected override Task BindAsync(PageContext context, object value, string name, Type type) + { + return Task.FromResult(ModelBindingResult.Success(Guid.NewGuid())); + } + } + } +} diff --git a/test/Microsoft.AspNetCore.Mvc.RazorPages.Test/PageTest.cs b/test/Microsoft.AspNetCore.Mvc.RazorPages.Test/PageTest.cs new file mode 100644 index 0000000000..1a8fde83b3 --- /dev/null +++ b/test/Microsoft.AspNetCore.Mvc.RazorPages.Test/PageTest.cs @@ -0,0 +1,53 @@ +// 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.Threading.Tasks; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Mvc.ModelBinding; +using Microsoft.AspNetCore.Mvc.RazorPages.Infrastructure; +using Microsoft.AspNetCore.Mvc.Routing; +using Microsoft.AspNetCore.Mvc.ViewFeatures; +using Microsoft.AspNetCore.Routing; +using Microsoft.Extensions.DependencyInjection; +using Moq; +using Xunit; +namespace Microsoft.AspNetCore.Mvc.RazorPages +{ + public class PageTest + { + [Fact] + public void PagePropertiesArePopulatedFromContext() + { + // Arrange + var httpContext = new DefaultHttpContext(); + var modelState = new ModelStateDictionary(); + var actionContext = new ActionContext(httpContext, new RouteData(), new PageActionDescriptor(), modelState); + var modelMetadataProvider = new EmptyModelMetadataProvider(); + var viewDataDictionary = new ViewDataDictionary(modelMetadataProvider, modelState); + var tempData = Mock.Of(); + var pageContext = new PageContext(actionContext, viewDataDictionary, tempData, new HtmlHelperOptions()); + + var page = new TestPage + { + PageContext = pageContext, + }; + + // Act & Assert + Assert.Same(pageContext, page.ViewContext); + Assert.Same(httpContext, page.HttpContext); + Assert.Same(httpContext.Request, page.Request); + Assert.Same(httpContext.Response, page.Response); + Assert.Same(modelState, page.ModelState); + Assert.Same(tempData, page.TempData); + } + + private class TestPage : Page + { + public override Task ExecuteAsync() + { + throw new NotImplementedException(); + } + } + } +} diff --git a/test/WebSites/RazorPagesWebSite/InjectedPageProperties.cshtml b/test/WebSites/RazorPagesWebSite/InjectedPageProperties.cshtml new file mode 100644 index 0000000000..ed07662bce --- /dev/null +++ b/test/WebSites/RazorPagesWebSite/InjectedPageProperties.cshtml @@ -0,0 +1,5 @@ +@page + +@Url.GetType() +@Html.GetType() +@ViewData.GetType()