diff --git a/src/Microsoft.AspNetCore.Mvc.RazorPages/Internal/DefaultPageApplicationModelProvider.cs b/src/Microsoft.AspNetCore.Mvc.RazorPages/ApplicationModels/DefaultPageApplicationModelProvider.cs similarity index 92% rename from src/Microsoft.AspNetCore.Mvc.RazorPages/Internal/DefaultPageApplicationModelProvider.cs rename to src/Microsoft.AspNetCore.Mvc.RazorPages/ApplicationModels/DefaultPageApplicationModelProvider.cs index cc6a02af02..b486d40e8c 100644 --- a/src/Microsoft.AspNetCore.Mvc.RazorPages/Internal/DefaultPageApplicationModelProvider.cs +++ b/src/Microsoft.AspNetCore.Mvc.RazorPages/ApplicationModels/DefaultPageApplicationModelProvider.cs @@ -3,36 +3,43 @@ using System; using System.Reflection; -using Microsoft.AspNetCore.Mvc.ApplicationModels; +using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc.Filters; using Microsoft.AspNetCore.Mvc.ModelBinding; using Microsoft.AspNetCore.Mvc.Razor; +using Microsoft.AspNetCore.Mvc.RazorPages; using Microsoft.AspNetCore.Mvc.RazorPages.Infrastructure; +using Microsoft.AspNetCore.Mvc.RazorPages.Internal; using Microsoft.Extensions.Internal; using Microsoft.Extensions.Options; +using Resources = Microsoft.AspNetCore.Mvc.RazorPages.Resources; -namespace Microsoft.AspNetCore.Mvc.RazorPages.Internal +namespace Microsoft.AspNetCore.Mvc.ApplicationModels { - public class DefaultPageApplicationModelProvider : IPageApplicationModelProvider + internal class DefaultPageApplicationModelProvider : IPageApplicationModelProvider { private const string ModelPropertyName = "Model"; private readonly PageHandlerPageFilter _pageHandlerPageFilter = new PageHandlerPageFilter(); private readonly PageHandlerResultFilter _pageHandlerResultFilter = new PageHandlerResultFilter(); private readonly IModelMetadataProvider _modelMetadataProvider; - private readonly MvcOptions _options; + private readonly MvcOptions _mvcOptions; + private readonly RazorPagesOptions _razorPagesOptions; private readonly Func _supportsAllRequests; private readonly Func _supportsNonGetRequests; - + private readonly HandleOptionsRequestsPageFilter _handleOptionsRequestsFilter; public DefaultPageApplicationModelProvider( IModelMetadataProvider modelMetadataProvider, - IOptions options) + IOptions options, + IOptions razorPagesOptions) { _modelMetadataProvider = modelMetadataProvider; - _options = options.Value; + _mvcOptions = options.Value; + _razorPagesOptions = razorPagesOptions.Value; _supportsAllRequests = _ => true; - _supportsNonGetRequests = context => !string.Equals(context.HttpContext.Request.Method, "GET", StringComparison.OrdinalIgnoreCase); + _supportsNonGetRequests = context => !HttpMethods.IsGet(context.HttpContext.Request.Method); + _handleOptionsRequestsFilter = new HandleOptionsRequestsPageFilter(); } /// @@ -175,6 +182,11 @@ namespace Microsoft.AspNetCore.Mvc.RazorPages.Internal { pageModel.Filters.Add(_pageHandlerResultFilter); } + + if (_razorPagesOptions.AllowDefaultHandlingForOptionsRequests) + { + pageModel.Filters.Add(_handleOptionsRequestsFilter); + } } /// @@ -237,7 +249,7 @@ namespace Microsoft.AspNetCore.Mvc.RazorPages.Internal var attributes = parameter.GetCustomAttributes(inherit: true); BindingInfo bindingInfo; - if (_options.AllowValidatingTopLevelNodes && _modelMetadataProvider is ModelMetadataProvider modelMetadataProviderBase) + if (_mvcOptions.AllowValidatingTopLevelNodes && _modelMetadataProvider is ModelMetadataProvider modelMetadataProviderBase) { var modelMetadata = modelMetadataProviderBase.GetMetadataForParameter(parameter); bindingInfo = BindingInfo.GetBindingInfo(attributes, modelMetadata); diff --git a/src/Microsoft.AspNetCore.Mvc.RazorPages/Infrastructure/HandleOptionsRequestsPageFilter.cs b/src/Microsoft.AspNetCore.Mvc.RazorPages/Infrastructure/HandleOptionsRequestsPageFilter.cs new file mode 100644 index 0000000000..0c9221e507 --- /dev/null +++ b/src/Microsoft.AspNetCore.Mvc.RazorPages/Infrastructure/HandleOptionsRequestsPageFilter.cs @@ -0,0 +1,55 @@ +// 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.Http; +using Microsoft.AspNetCore.Mvc.Filters; + +namespace Microsoft.AspNetCore.Mvc.RazorPages.Infrastructure +{ + /// + /// A filter that handles OPTIONS requests page when no handler method is available. + /// + /// a) MVC treats no handler being selected no differently than a page having no handler, both execute the + /// page. + /// b) A common model for programming Razor Pages is to initialize content required by a page in the + /// OnGet handler. Executing a page without running the handler may result in runtime exceptions - + /// e.g. null ref or out of bounds exception if you expected a property or collection to be initialized. + /// + /// + /// Some web crawlers use OPTIONS request when probing servers. In the absence of an uncommon OnOptions + /// handler, executing the page will likely result in runtime errors as described in earlier. This filter + /// attempts to avoid this pit of failure by handling OPTIONS requests and returning a 200 if no handler is selected. + /// + /// + internal sealed class HandleOptionsRequestsPageFilter : IPageFilter, IOrderedFilter + { + /// + /// Ordered to run after filters with default order. + /// + public int Order => 1000; + + public void OnPageHandlerExecuted(PageHandlerExecutedContext context) + { + } + + public void OnPageHandlerExecuting(PageHandlerExecutingContext context) + { + if (context == null) + { + throw new ArgumentNullException(nameof(context)); + } + + if (context.HandlerMethod == null && + context.Result == null && + HttpMethods.IsOptions(context.HttpContext.Request.Method)) + { + context.Result = new OkResult(); + } + } + + public void OnPageHandlerSelected(PageHandlerSelectedContext context) + { + } + } +} diff --git a/src/Microsoft.AspNetCore.Mvc.RazorPages/RazorPagesOptions.cs b/src/Microsoft.AspNetCore.Mvc.RazorPages/RazorPagesOptions.cs index b3b0b15e11..38bd6f7988 100644 --- a/src/Microsoft.AspNetCore.Mvc.RazorPages/RazorPagesOptions.cs +++ b/src/Microsoft.AspNetCore.Mvc.RazorPages/RazorPagesOptions.cs @@ -16,6 +16,7 @@ namespace Microsoft.AspNetCore.Mvc.RazorPages { private readonly CompatibilitySwitch _allowAreas; private readonly CompatibilitySwitch _allowMappingHeadRequestsToGetHandler; + private readonly CompatibilitySwitch _allowsDefaultHandlingForOptionsRequests; private readonly ICompatibilitySwitch[] _switches; private string _root = "/Pages"; @@ -24,11 +25,13 @@ namespace Microsoft.AspNetCore.Mvc.RazorPages { _allowAreas = new CompatibilitySwitch(nameof(AllowAreas)); _allowMappingHeadRequestsToGetHandler = new CompatibilitySwitch(nameof(AllowMappingHeadRequestsToGetHandler)); + _allowsDefaultHandlingForOptionsRequests = new CompatibilitySwitch(nameof(AllowDefaultHandlingForOptionsRequests)); _switches = new ICompatibilitySwitch[] { _allowAreas, _allowMappingHeadRequestsToGetHandler, + _allowsDefaultHandlingForOptionsRequests, }; } @@ -134,6 +137,45 @@ namespace Microsoft.AspNetCore.Mvc.RazorPages set => _allowMappingHeadRequestsToGetHandler.Value = value; } + /// + /// Gets or sets a value that determines if HTTP requests with the OPTIONS method are handled by default, if + /// no handler is available. + /// + /// + /// The default value is if the version is + /// or later; otherwise. + /// + /// + /// + /// Razor Pages uses the current request's HTTP method to select a handler method. When no handler is available or selected, + /// the page is immediately executed. This may cause runtime errors if the page relies on the handler method to execute + /// and initialize some state. This setting attempts to avoid this class of error for HTTP OPTIONS requests by + /// returning a 200 OK response. + /// + /// + /// This property is associated with a compatibility switch and can provide a different behavior depending on + /// the configured compatibility version for the application. See for + /// guidance and examples of setting the application's compatibility version. + /// + /// + /// Configuring the desired of the value compatibility switch by calling this property's setter will take precedence + /// over the value implied by the application's . + /// + /// + /// If the application's compatibility version is set to then + /// this setting will have value true unless explicitly configured. + /// + /// + /// If the application's compatibility version is set to or + /// lower then this setting will have value true unless explicitly configured. + /// + /// + public bool AllowDefaultHandlingForOptionsRequests + { + get => _allowsDefaultHandlingForOptionsRequests.Value; + set => _allowsDefaultHandlingForOptionsRequests.Value = value; + } + IEnumerator IEnumerable.GetEnumerator() { return ((IEnumerable)_switches).GetEnumerator(); diff --git a/src/Microsoft.AspNetCore.Mvc.RazorPages/RazorPagesOptionsConfigureCompatibilityOptions.cs b/src/Microsoft.AspNetCore.Mvc.RazorPages/RazorPagesOptionsConfigureCompatibilityOptions.cs index 8f5d21d8bc..49596d55b2 100644 --- a/src/Microsoft.AspNetCore.Mvc.RazorPages/RazorPagesOptionsConfigureCompatibilityOptions.cs +++ b/src/Microsoft.AspNetCore.Mvc.RazorPages/RazorPagesOptionsConfigureCompatibilityOptions.cs @@ -29,6 +29,11 @@ namespace Microsoft.AspNetCore.Mvc.RazorPages values[nameof(RazorPagesOptions.AllowMappingHeadRequestsToGetHandler)] = true; } + if (Version >= CompatibilityVersion.Version_2_2) + { + values[nameof(RazorPagesOptions.AllowDefaultHandlingForOptionsRequests)] = true; + } + return values; } } diff --git a/test/Microsoft.AspNetCore.Mvc.FunctionalTests/RazorPagesTest.cs b/test/Microsoft.AspNetCore.Mvc.FunctionalTests/RazorPagesTest.cs index 8aca9a9bcf..ab9c81bfad 100644 --- a/test/Microsoft.AspNetCore.Mvc.FunctionalTests/RazorPagesTest.cs +++ b/test/Microsoft.AspNetCore.Mvc.FunctionalTests/RazorPagesTest.cs @@ -1431,6 +1431,54 @@ Microsoft.AspNetCore.Mvc.ViewFeatures.ViewDataDictionary`1[AspNetCore.InjectedPa Assert.Equal("ViewData: Bar", content); } + [Fact] + public async Task OptionsRequest_WithoutHandler_Returns200_WithoutExecutingPage() + { + // Arrange + var request = new HttpRequestMessage(HttpMethod.Options, "http://localhost/HelloWorld"); + + // Act + var response = await Client.SendAsync(request); + + // Assert + Assert.Equal(HttpStatusCode.OK, response.StatusCode); + + var content = await response.Content.ReadAsStringAsync(); + Assert.Empty(content.Trim()); + } + + [Fact] + public async Task PageWithOptionsHandler_ExecutesGetRequest() + { + // Arrange + var request = new HttpRequestMessage(HttpMethod.Get, "http://localhost/HelloWorldWithOptionsHandler"); + + // Act + var response = await Client.SendAsync(request); + + // Assert + Assert.Equal(HttpStatusCode.OK, response.StatusCode); + + var content = await response.Content.ReadAsStringAsync(); + Assert.Equal("Hello from OnGet!", content.Trim()); + } + + [Fact] + public async Task PageWithOptionsHandler_ExecutesOptionsRequest() + { + // Arrange + var request = new HttpRequestMessage(HttpMethod.Options, "http://localhost/HelloWorldWithOptionsHandler"); + + // Act + var response = await Client.SendAsync(request); + + // Assert + Assert.Equal(HttpStatusCode.OK, response.StatusCode); + + var content = await response.Content.ReadAsStringAsync(); + Assert.Equal("Hello from OnOptions!", content.Trim()); + } + private async Task AddAntiforgeryHeaders(HttpRequestMessage request) { var getResponse = await Client.GetAsync(request.RequestUri); diff --git a/test/Microsoft.AspNetCore.Mvc.RazorPages.Test/Internal/DefaultPageApplicationModelProviderTest.cs b/test/Microsoft.AspNetCore.Mvc.RazorPages.Test/ApplicationModels/DefaultPageApplicationModelProviderTest.cs similarity index 95% rename from test/Microsoft.AspNetCore.Mvc.RazorPages.Test/Internal/DefaultPageApplicationModelProviderTest.cs rename to test/Microsoft.AspNetCore.Mvc.RazorPages.Test/ApplicationModels/DefaultPageApplicationModelProviderTest.cs index 2ad90a08a8..7661f32707 100644 --- a/test/Microsoft.AspNetCore.Mvc.RazorPages.Test/Internal/DefaultPageApplicationModelProviderTest.cs +++ b/test/Microsoft.AspNetCore.Mvc.RazorPages.Test/ApplicationModels/DefaultPageApplicationModelProviderTest.cs @@ -6,15 +6,16 @@ using System.Linq; using System.Reflection; using System.Threading.Tasks; using Microsoft.AspNetCore.Http; -using Microsoft.AspNetCore.Mvc.ApplicationModels; using Microsoft.AspNetCore.Mvc.Filters; using Microsoft.AspNetCore.Mvc.ModelBinding; using Microsoft.AspNetCore.Mvc.Razor; +using Microsoft.AspNetCore.Mvc.RazorPages; using Microsoft.AspNetCore.Mvc.RazorPages.Infrastructure; +using Microsoft.AspNetCore.Mvc.RazorPages.Internal; using Microsoft.Extensions.Options; using Xunit; -namespace Microsoft.AspNetCore.Mvc.RazorPages.Internal +namespace Microsoft.AspNetCore.Mvc.ApplicationModels { public class DefaultPageApplicationModelProviderTest { @@ -887,7 +888,8 @@ namespace Microsoft.AspNetCore.Mvc.RazorPages.Internal // Arrange var provider = new DefaultPageApplicationModelProvider( TestModelMetadataProvider.CreateDefaultProvider(), - Options.Create(new MvcOptions { AllowValidatingTopLevelNodes = false })); + Options.Create(new MvcOptions { AllowValidatingTopLevelNodes = false }), + Options.Create(new RazorPagesOptions())); var typeInfo = typeof(PageWithHandlerParameters).GetTypeInfo(); var expected = typeInfo.GetMethod(nameof(PageWithHandlerParameters.OnPost)); var pageModel = new PageApplicationModel(new PageActionDescriptor(), typeInfo, new object[0]); @@ -921,11 +923,11 @@ namespace Microsoft.AspNetCore.Mvc.RazorPages.Internal public void OnPost(string name, [ModelBinder(Name = "personId")] int id) { } } - // We're using PropertyHelper from Common to find the properties here, which implements + // We're using PropertyHelper from Common to find the properties here, which implements // out standard set of semantics for properties that the framework interacts with. - // - // One of the desirable consequences of that is we only find 'visible' properties. We're not - // retesting all of the details of PropertyHelper here, just the visibility part as a quick check + // + // One of the desirable consequences of that is we only find 'visible' properties. We're not + // retesting all of the details of PropertyHelper here, just the visibility part as a quick check // that we're using PropertyHelper as expected. [Fact] public void PopulateHandlerProperties_UsesPropertyHelpers_ToFindProperties() @@ -1071,6 +1073,41 @@ namespace Microsoft.AspNetCore.Mvc.RazorPages.Internal public void OnGetUser() { } } + [Fact] + public void PopulateFilters_With21CompatBehavior_DoesNotAddDisallowOptionsRequestsPageFilter() + { + // Arrange + var provider = new DefaultPageApplicationModelProvider( + TestModelMetadataProvider.CreateDefaultProvider(), + Options.Create(new MvcOptions()), + Options.Create(new RazorPagesOptions())); + var typeInfo = typeof(object).GetTypeInfo(); + var pageModel = new PageApplicationModel(new PageActionDescriptor(), typeInfo, typeInfo.GetCustomAttributes(inherit: true)); + + // Act + provider.PopulateFilters(pageModel); + + // Assert + Assert.Empty(pageModel.Filters); + } + + [Fact] + public void PopulateFilters_AddsDisallowOptionsRequestsPageFilter() + { + // Arrange + var provider = CreateProvider(); + var typeInfo = typeof(object).GetTypeInfo(); + var pageModel = new PageApplicationModel(new PageActionDescriptor(), typeInfo, typeInfo.GetCustomAttributes(inherit: true)); + + // Act + provider.PopulateFilters(pageModel); + + // Assert + Assert.Collection( + pageModel.Filters, + filter => Assert.IsType(filter)); + } + [Fact] public void PopulateFilters_AddsIFilterMetadataAttributesToModel() { @@ -1085,7 +1122,8 @@ namespace Microsoft.AspNetCore.Mvc.RazorPages.Internal // Assert Assert.Collection( pageModel.Filters, - filter => Assert.IsType(filter)); + filter => Assert.IsType(filter), + filter => Assert.IsType(filter)); } [PageModel] @@ -1109,7 +1147,8 @@ namespace Microsoft.AspNetCore.Mvc.RazorPages.Internal // Assert Assert.Collection( pageModel.Filters, - filter => Assert.IsType(filter)); + filter => Assert.IsType(filter), + filter => Assert.IsType(filter)); } private class ModelImplementingAsyncPageFilter : IAsyncPageFilter @@ -1139,7 +1178,8 @@ namespace Microsoft.AspNetCore.Mvc.RazorPages.Internal // Assert Assert.Collection( pageModel.Filters, - filter => Assert.IsType(filter)); + filter => Assert.IsType(filter), + filter => Assert.IsType(filter)); } private class ModelImplementingPageFilter : IPageFilter @@ -1175,7 +1215,8 @@ namespace Microsoft.AspNetCore.Mvc.RazorPages.Internal Assert.Collection( pageModel.Filters, filter => Assert.IsType(filter), - filter => Assert.IsType(filter)); + filter => Assert.IsType(filter), + filter => Assert.IsType(filter)); } [ServiceFilter(typeof(IServiceProvider))] @@ -1185,7 +1226,8 @@ namespace Microsoft.AspNetCore.Mvc.RazorPages.Internal { return new DefaultPageApplicationModelProvider( TestModelMetadataProvider.CreateDefaultProvider(), - Options.Create(new MvcOptions { AllowValidatingTopLevelNodes = true })); + Options.Create(new MvcOptions { AllowValidatingTopLevelNodes = true }), + Options.Create(new RazorPagesOptions { AllowDefaultHandlingForOptionsRequests = true })); } } } diff --git a/test/Microsoft.AspNetCore.Mvc.RazorPages.Test/Infrastructure/DisallowOptionsRequestsPageFilterTest.cs b/test/Microsoft.AspNetCore.Mvc.RazorPages.Test/Infrastructure/DisallowOptionsRequestsPageFilterTest.cs new file mode 100644 index 0000000000..a2be81c8c1 --- /dev/null +++ b/test/Microsoft.AspNetCore.Mvc.RazorPages.Test/Infrastructure/DisallowOptionsRequestsPageFilterTest.cs @@ -0,0 +1,135 @@ +// 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.Http; +using Microsoft.AspNetCore.Mvc.Filters; +using Microsoft.AspNetCore.Routing; +using Xunit; + +namespace Microsoft.AspNetCore.Mvc.RazorPages.Infrastructure +{ + public class DisallowOptionsRequestsPageFilterTest + { + [Fact] + public void OnPageHandlerExecuting_DoesNothing_IfHandlerIsSelected() + { + // Arrange + var context = GetContext(new HandlerMethodDescriptor()); + var filter = new HandleOptionsRequestsPageFilter(); + + // Act + filter.OnPageHandlerExecuting(context); + + // Assert + Assert.Null(context.Result); + } + + [Fact] + public void OnPageHandlerExecuting_DoesNotOverwriteResult_IfHandlerIsSelected() + { + // Arrange + var expected = new PageResult(); + var context = GetContext(new HandlerMethodDescriptor()); + context.Result = expected; + var filter = new HandleOptionsRequestsPageFilter(); + + // Act + filter.OnPageHandlerExecuting(context); + + // Assert + Assert.Same(expected, context.Result); + } + + [Fact] + public void OnPageHandlerExecuting_DoesNothing_IfHandlerIsNotSelected_WhenRequestsIsNotOptions() + { + // Arrange + var context = GetContext(handlerMethodDescriptor: null); + context.HttpContext.Request.Method = "PUT"; + var filter = new HandleOptionsRequestsPageFilter(); + + // Act + filter.OnPageHandlerExecuting(context); + + // Assert + Assert.Null(context.Result); + } + + [Fact] + public void OnPageHandlerExecuting_DoesNotOverwriteResult_IfHandlerIsNotSelected_WhenRequestsIsNotOptions() + { + // Arrange + var expected = new PageResult(); + var context = GetContext(handlerMethodDescriptor: null); + context.HttpContext.Request.Method = "DELETE"; + context.Result = expected; + + var filter = new HandleOptionsRequestsPageFilter(); + + // Act + filter.OnPageHandlerExecuting(context); + + // Assert + Assert.Same(expected, context.Result); + } + + [Fact] + public void OnPageHandlerExecuting_DoesNothing_ForOptionsRequestWhenHandlerIsSelected() + { + // Arrange + var context = GetContext(new HandlerMethodDescriptor()); + context.HttpContext.Request.Method = "Options"; + + var filter = new HandleOptionsRequestsPageFilter(); + + // Act + filter.OnPageHandlerExecuting(context); + + // Assert + Assert.Null(context.Result); + } + + [Fact] + public void OnPageHandlerExecuting_DoesNotOverwriteResult_ForOptionsRequestWhenNoHandler() + { + // Arrange + var expected = new NotFoundResult(); + var context = GetContext(new HandlerMethodDescriptor()); + context.Result = expected; + context.HttpContext.Request.Method = "Options"; + + var filter = new HandleOptionsRequestsPageFilter(); + + // Act + filter.OnPageHandlerExecuting(context); + + // Assert + Assert.Same(expected, context.Result); + } + + [Fact] + public void OnPageHandlerExecuting_SetsResult_ForOptionsRequestWhenNoHandlerIsSelected() + { + // Arrange + var context = GetContext(handlerMethodDescriptor: null); + context.HttpContext.Request.Method = "Options"; + + var filter = new HandleOptionsRequestsPageFilter(); + + // Act + filter.OnPageHandlerExecuting(context); + + // Assert + Assert.IsType(context.Result); + } + + private static PageHandlerExecutingContext GetContext(HandlerMethodDescriptor handlerMethodDescriptor) + { + var actionContext = new ActionContext(new DefaultHttpContext(), new RouteData(), new PageActionDescriptor()); + var pageContext = new PageContext(actionContext); + return new PageHandlerExecutingContext(pageContext, Array.Empty(), handlerMethodDescriptor, new Dictionary(), new object()); + } + } +} diff --git a/test/Microsoft.AspNetCore.Mvc.RazorPages.Test/Internal/AuthorizationPageApplicationModelProviderTest.cs b/test/Microsoft.AspNetCore.Mvc.RazorPages.Test/Internal/AuthorizationPageApplicationModelProviderTest.cs index 008435da3c..559a653985 100644 --- a/test/Microsoft.AspNetCore.Mvc.RazorPages.Test/Internal/AuthorizationPageApplicationModelProviderTest.cs +++ b/test/Microsoft.AspNetCore.Mvc.RazorPages.Test/Internal/AuthorizationPageApplicationModelProviderTest.cs @@ -8,6 +8,7 @@ using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Mvc.ApplicationModels; using Microsoft.AspNetCore.Mvc.Authorization; using Microsoft.AspNetCore.Mvc.ModelBinding; +using Microsoft.AspNetCore.Mvc.RazorPages.Infrastructure; using Microsoft.Extensions.Options; using Xunit; @@ -30,7 +31,8 @@ namespace Microsoft.AspNetCore.Mvc.RazorPages.Internal // Assert Assert.Collection( context.PageApplicationModel.Filters, - f => Assert.IsType(f)); + f => Assert.IsType(f), + f => Assert.IsType(f)); } private class PageWithAuthorizeHandlers : Page @@ -63,6 +65,7 @@ namespace Microsoft.AspNetCore.Mvc.RazorPages.Internal Assert.Collection( context.PageApplicationModel.Filters, f => Assert.IsType(f), + f => Assert.IsType(f), f => Assert.IsType(f)); } @@ -102,6 +105,7 @@ namespace Microsoft.AspNetCore.Mvc.RazorPages.Internal Assert.Collection( context.PageApplicationModel.Filters, f => Assert.IsType(f), + f => Assert.IsType(f), f => authorizeFilter = Assert.IsType(f)); // Basic + Basic2 + Derived authorize @@ -143,6 +147,7 @@ namespace Microsoft.AspNetCore.Mvc.RazorPages.Internal Assert.Collection( context.PageApplicationModel.Filters, f => Assert.IsType(f), + f => Assert.IsType(f), f => Assert.IsType(f)); } @@ -163,7 +168,8 @@ namespace Microsoft.AspNetCore.Mvc.RazorPages.Internal { var defaultProvider = new DefaultPageApplicationModelProvider( TestModelMetadataProvider.CreateDefaultProvider(), - Options.Create(new MvcOptions { AllowValidatingTopLevelNodes = true })); + Options.Create(new MvcOptions { AllowValidatingTopLevelNodes = true }), + Options.Create(new RazorPagesOptions { AllowDefaultHandlingForOptionsRequests = true })); var context = new PageApplicationModelProviderContext(new PageActionDescriptor(), typeInfo); defaultProvider.OnProvidersExecuting(context); return context; diff --git a/test/Microsoft.AspNetCore.Mvc.RazorPages.Test/Internal/ResponseCacheFilterApplicationModelProviderTest.cs b/test/Microsoft.AspNetCore.Mvc.RazorPages.Test/Internal/ResponseCacheFilterApplicationModelProviderTest.cs index cf6acab03f..6b08977e2d 100644 --- a/test/Microsoft.AspNetCore.Mvc.RazorPages.Test/Internal/ResponseCacheFilterApplicationModelProviderTest.cs +++ b/test/Microsoft.AspNetCore.Mvc.RazorPages.Test/Internal/ResponseCacheFilterApplicationModelProviderTest.cs @@ -7,6 +7,7 @@ using System.Threading.Tasks; using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Mvc.ApplicationModels; using Microsoft.AspNetCore.Mvc.ModelBinding; +using Microsoft.AspNetCore.Mvc.RazorPages.Infrastructure; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; using Moq; @@ -31,7 +32,8 @@ namespace Microsoft.AspNetCore.Mvc.RazorPages.Internal // Assert Assert.Collection( context.PageApplicationModel.Filters, - f => Assert.IsType(f)); + f => Assert.IsType(f), + f => Assert.IsType(f)); } private class PageWithoutResponseCache : Page @@ -66,6 +68,7 @@ namespace Microsoft.AspNetCore.Mvc.RazorPages.Internal context.PageApplicationModel.Filters, f => { }, f => Assert.IsType(f), + f => Assert.IsType(f), f => { var filter = Assert.IsType(f); @@ -112,6 +115,7 @@ namespace Microsoft.AspNetCore.Mvc.RazorPages.Internal context.PageApplicationModel.Filters, f => { }, f => Assert.IsType(f), + f => Assert.IsType(f), f => { var filter = Assert.IsType(f); @@ -139,7 +143,8 @@ namespace Microsoft.AspNetCore.Mvc.RazorPages.Internal { var defaultProvider = new DefaultPageApplicationModelProvider( TestModelMetadataProvider.CreateDefaultProvider(), - Options.Create(new MvcOptions())); + Options.Create(new MvcOptions()), + Options.Create(new RazorPagesOptions { AllowDefaultHandlingForOptionsRequests = true })); var context = new PageApplicationModelProviderContext(new PageActionDescriptor(), typeInfo); defaultProvider.OnProvidersExecuting(context); return context; diff --git a/test/Microsoft.AspNetCore.Mvc.Test/IntegrationTest/CompatibilitySwitchIntegrationTest.cs b/test/Microsoft.AspNetCore.Mvc.Test/IntegrationTest/CompatibilitySwitchIntegrationTest.cs index 2281f4d602..409605d730 100644 --- a/test/Microsoft.AspNetCore.Mvc.Test/IntegrationTest/CompatibilitySwitchIntegrationTest.cs +++ b/test/Microsoft.AspNetCore.Mvc.Test/IntegrationTest/CompatibilitySwitchIntegrationTest.cs @@ -49,6 +49,7 @@ namespace Microsoft.AspNetCore.Mvc.IntegrationTest Assert.True(apiBehaviorOptions.SuppressUseValidationProblemDetailsForInvalidModelStateResponses); Assert.True(apiBehaviorOptions.SuppressMapClientErrors); Assert.True(razorViewEngineOptions.AllowRecompilingViewsOnFileChange); + Assert.False(razorPagesOptions.AllowDefaultHandlingForOptionsRequests); } [Fact] @@ -80,6 +81,7 @@ namespace Microsoft.AspNetCore.Mvc.IntegrationTest Assert.True(apiBehaviorOptions.SuppressUseValidationProblemDetailsForInvalidModelStateResponses); Assert.True(apiBehaviorOptions.SuppressMapClientErrors); Assert.True(razorViewEngineOptions.AllowRecompilingViewsOnFileChange); + Assert.False(razorPagesOptions.AllowDefaultHandlingForOptionsRequests); } [Fact] @@ -111,6 +113,7 @@ namespace Microsoft.AspNetCore.Mvc.IntegrationTest Assert.False(apiBehaviorOptions.SuppressUseValidationProblemDetailsForInvalidModelStateResponses); Assert.False(apiBehaviorOptions.SuppressMapClientErrors); Assert.False(razorViewEngineOptions.AllowRecompilingViewsOnFileChange); + Assert.True(razorPagesOptions.AllowDefaultHandlingForOptionsRequests); } [Fact] @@ -142,6 +145,7 @@ namespace Microsoft.AspNetCore.Mvc.IntegrationTest Assert.False(apiBehaviorOptions.SuppressUseValidationProblemDetailsForInvalidModelStateResponses); Assert.False(apiBehaviorOptions.SuppressMapClientErrors); Assert.False(razorViewEngineOptions.AllowRecompilingViewsOnFileChange); + Assert.True(razorPagesOptions.AllowDefaultHandlingForOptionsRequests); } // This just does the minimum needed to be able to resolve these options. diff --git a/test/WebSites/RazorPagesWebSite/HelloWorldWithOptionsHandler.cshtml b/test/WebSites/RazorPagesWebSite/HelloWorldWithOptionsHandler.cshtml new file mode 100644 index 0000000000..9ee557a98e --- /dev/null +++ b/test/WebSites/RazorPagesWebSite/HelloWorldWithOptionsHandler.cshtml @@ -0,0 +1,17 @@ +@page + +@functions { + public string Source { get; set; } + + public void OnGet() + { + Source = "OnGet"; + } + + public void OnOptions(string message) + { + Source = "OnOptions"; + } +} + +Hello from @Source! \ No newline at end of file