Add convenience extension methods for IPageApplicationModelConvention

This commit is contained in:
Pranav K 2017-02-27 15:49:12 -08:00
parent f6d25f7117
commit 7cadb58e12
16 changed files with 451 additions and 21 deletions

View File

@ -6,7 +6,7 @@ namespace Microsoft.AspNetCore.Mvc.ApplicationModels
/// <summary>
/// Allows customization of the of the <see cref="PageApplicationModel"/>.
/// </summary>
public interface IPageModelConvention
public interface IPageApplicationModelConvention
{
/// <summary>
/// Called to apply the convention to the <see cref="PageApplicationModel"/>.

View File

@ -0,0 +1,39 @@
// 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.RazorPages;
namespace Microsoft.Extensions.DependencyInjection
{
/// <summary>
/// Extensions methods for configuring Razor Pages via an <see cref="IMvcBuilder"/>.
/// </summary>
public static class MvcRazorPagesMvcBuilderExtensions
{
/// <summary>
/// Configures a set of <see cref="RazorViewEngineOptions"/> for the application.
/// </summary>
/// <param name="builder">The <see cref="IMvcBuilder"/>.</param>
/// <param name="setupAction">An action to configure the <see cref="RazorViewEngineOptions"/>.</param>
/// <returns>The <see cref="IMvcBuilder"/>.</returns>
public static IMvcBuilder AddRazorPagesOptions(
this IMvcBuilder builder,
Action<RazorPagesOptions> setupAction)
{
if (builder == null)
{
throw new ArgumentNullException(nameof(builder));
}
if (setupAction == null)
{
throw new ArgumentNullException(nameof(setupAction));
}
builder.Services.Configure(setupAction);
return builder;
}
}
}

View File

@ -9,6 +9,7 @@ using Microsoft.AspNetCore.Mvc.RazorPages;
using Microsoft.AspNetCore.Mvc.RazorPages.Infrastructure;
using Microsoft.AspNetCore.Mvc.RazorPages.Internal;
using Microsoft.Extensions.DependencyInjection.Extensions;
using Microsoft.Extensions.Options;
namespace Microsoft.Extensions.DependencyInjection
{
@ -51,6 +52,11 @@ namespace Microsoft.Extensions.DependencyInjection
// Internal for testing.
internal static void AddServices(IServiceCollection services)
{
services.TryAddEnumerable(
ServiceDescriptor.Transient<
IConfigureOptions<RazorPagesOptions>,
RazorPagesOptionsSetup>());
services.TryAddEnumerable(
ServiceDescriptor.Singleton<IActionDescriptorProvider, PageActionDescriptorProvider>());
@ -72,6 +78,7 @@ namespace Microsoft.Extensions.DependencyInjection
services.TryAddSingleton<IActionDescriptorChangeProvider, PageActionDescriptorChangeProvider>();
services.TryAddSingleton<TempDataPropertyProvider>();
}
}
}

View File

@ -0,0 +1,134 @@
// 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.Authorization;
using Microsoft.AspNetCore.Mvc.Filters;
using Microsoft.AspNetCore.Mvc.RazorPages;
namespace Microsoft.Extensions.DependencyInjection
{
/// <summary>
/// Extensions for <see cref="RazorPagesOptions"/>.
/// </summary>
public static class RazorPagesOptionsExtensions
{
/// <summary>
/// Configures the specified <paramref name="filter"/> to apply to all Razor Pages.
/// </summary>
/// <param name="options">The <see cref="RazorPagesOptions"/> to configure.</param>
/// <param name="filter">The <see cref="IFilterMetadata"/> to add.</param>
/// <returns>The <see cref="RazorPagesOptions"/>.</returns>
public static RazorPagesOptions ConfigureFilter(this RazorPagesOptions options, IFilterMetadata filter)
{
if (options == null)
{
throw new ArgumentNullException(nameof(options));
}
if (filter == null)
{
throw new ArgumentNullException(nameof(filter));
}
options.Conventions.Add(new FolderConvention("/", model => model.Filters.Add(filter)));
return options;
}
/// <summary>
/// Adds a <see cref="AuthorizeFilter"/> with the specified policy to the page with the specified path.
/// </summary>
/// <param name="options">The <see cref="RazorPagesOptions"/> to configure.</param>
/// <param name="path">The path of the Razor Page.</param>
/// <param name="policy">The authorization policy.</param>
/// <returns>The <see cref="RazorPagesOptions"/>.</returns>
public static RazorPagesOptions AuthorizePage(this RazorPagesOptions options, string path, string policy)
{
if (options == null)
{
throw new ArgumentNullException(nameof(options));
}
if (string.IsNullOrEmpty(path))
{
throw new ArgumentException(Resources.ArgumentCannotBeNullOrEmpty, nameof(path));
}
var authorizeFilter = new AuthorizeFilter(policy);
options.Conventions.Add(new PageConvention(path, model => model.Filters.Add(authorizeFilter)));
return options;
}
/// <summary>
/// Adds a <see cref="AuthorizeFilter"/> with the specified policy to all page under the specified path.
/// </summary>
/// <param name="options">The <see cref="RazorPagesOptions"/> to configure.</param>
/// <param name="folderPath">The folder path.</param>
/// <param name="policy">The authorization policy.</param>
/// <returns>The <see cref="RazorPagesOptions"/>.</returns>
public static RazorPagesOptions AuthorizeFolder(this RazorPagesOptions options, string folderPath, string policy)
{
if (options == null)
{
throw new ArgumentNullException(nameof(options));
}
if (string.IsNullOrEmpty(folderPath))
{
throw new ArgumentException(Resources.ArgumentCannotBeNullOrEmpty, nameof(folderPath));
}
var authorizeFilter = new AuthorizeFilter(policy);
options.Conventions.Add(new FolderConvention(folderPath, model => model.Filters.Add(authorizeFilter)));
return options;
}
private class PageConvention : IPageApplicationModelConvention
{
private readonly string _path;
private readonly Action<PageApplicationModel> _action;
public PageConvention(string path, Action<PageApplicationModel> action)
{
_path = path;
_action = action;
}
public void Apply(PageApplicationModel model)
{
if (string.Equals(model.ViewEnginePath, _path, StringComparison.OrdinalIgnoreCase))
{
_action(model);
}
}
}
private class FolderConvention : IPageApplicationModelConvention
{
private readonly string _folderPath;
private readonly Action<PageApplicationModel> _action;
public FolderConvention(string folderPath, Action<PageApplicationModel> action)
{
_folderPath = folderPath.TrimEnd('/');
_action = action;
}
public void Apply(PageApplicationModel model)
{
var viewEnginePath = model.ViewEnginePath;
var applyConvention = _folderPath == "/" ||
(viewEnginePath.Length > _folderPath.Length &&
viewEnginePath.StartsWith(_folderPath, StringComparison.OrdinalIgnoreCase) &&
viewEnginePath[_folderPath.Length] == '/');
if (applyConvention)
{
_action(model);
}
}
}
}
}

View File

@ -7,7 +7,6 @@ using Microsoft.AspNetCore.Mvc.Abstractions;
using Microsoft.AspNetCore.Mvc.ApplicationModels;
using Microsoft.AspNetCore.Mvc.Filters;
using Microsoft.AspNetCore.Mvc.Routing;
using Microsoft.AspNetCore.Mvc.ViewFeatures.Internal;
using Microsoft.AspNetCore.Razor.Evolution;
using Microsoft.Extensions.Options;
@ -85,9 +84,6 @@ namespace Microsoft.AspNetCore.Mvc.RazorPages.Infrastructure
model.Selectors.Add(CreateSelectorModel(parentDirectoryPath, template));
}
model.Filters.Add(new SaveTempDataPropertyFilter()); // Support for [TempData] on properties
model.Filters.Add(new AutoValidateAntiforgeryTokenAttribute()); // Always require an antiforgery token on post
for (var i = 0; i < _pagesOptions.Conventions.Count; i++)
{
_pagesOptions.Conventions[i].Apply(model);

View File

@ -0,0 +1,26 @@
// 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 Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Options;
namespace Microsoft.AspNetCore.Mvc.RazorPages.Internal
{
public class RazorPagesOptionsSetup : IConfigureOptions<RazorPagesOptions>
{
public void Configure(RazorPagesOptions options)
{
if (options == null)
{
throw new ArgumentNullException(nameof(options));
}
// Support for [TempData] on properties
options.ConfigureFilter(new SaveTempDataPropertyFilter());
// Always require an antiforgery token on post
options.ConfigureFilter(new AutoValidateAntiforgeryTokenAttribute());
}
}
}

View File

@ -1,6 +1,7 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System;
using System.Collections.Generic;
using Microsoft.AspNetCore.Mvc.ApplicationModels;
@ -12,9 +13,9 @@ namespace Microsoft.AspNetCore.Mvc.RazorPages
public class RazorPagesOptions
{
/// <summary>
/// Gets a list of <see cref="IPageModelConvention"/> instances that will be applied to
/// Gets a list of <see cref="IPageApplicationModelConvention"/> instances that will be applied to
/// the <see cref="PageModel"/> when discovering Razor Pages.
/// </summary>
public IList<IPageModelConvention> Conventions { get; } = new List<IPageModelConvention>();
public IList<IPageApplicationModelConvention> Conventions { get; } = new List<IPageApplicationModelConvention>();
}
}

View File

@ -131,6 +131,20 @@ namespace Microsoft.AspNetCore.Mvc.FunctionalTests
Assert.Equal("Hi2", content.Trim());
}
[Fact]
public async Task AuthorizePage_AddsAuthorizationForSpecificPages()
{
// Arrange
var url = "/HelloWorldWithAuth";
// Act
var response = await Client.GetAsync(url);
// Assert
Assert.Equal(HttpStatusCode.Redirect, response.StatusCode);
Assert.Equal("/Login?ReturnUrl=%2FHelloWorldWithAuth", response.Headers.Location.PathAndQuery);
}
private static string GetCookie(HttpResponseMessage response)
{
var setCookie = response.Headers.GetValues("Set-Cookie").ToArray();

View File

@ -0,0 +1,38 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using Microsoft.AspNetCore.Mvc.ApplicationModels;
using Microsoft.AspNetCore.Mvc.ApplicationParts;
using Microsoft.AspNetCore.Mvc.Internal;
using Microsoft.AspNetCore.Mvc.RazorPages;
using Microsoft.Extensions.Options;
using Moq;
using Xunit;
namespace Microsoft.Extensions.DependencyInjection
{
public class MvcRazorPagesMvcBuilderExtensionsTest
{
[Fact]
public void AddRazorPagesOptions_AddsApplicationModelConventions()
{
// Arrange
var services = new ServiceCollection().AddOptions();
var expected = Mock.Of<IPageApplicationModelConvention>();
var builder = new MvcBuilder(services, new ApplicationPartManager());
builder.AddRazorPagesOptions(options =>
{
options.Conventions.Add(expected);
});
var serviceProvider = services.BuildServiceProvider();
var accessor = serviceProvider.GetRequiredService<IOptions<RazorPagesOptions>>();
// Act
var conventions = accessor.Value.Conventions;
// Assert
Assert.Collection(conventions,
convention => Assert.Same(expected, convention));
}
}
}

View File

@ -0,0 +1,117 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc.ApplicationModels;
using Microsoft.AspNetCore.Mvc.Authorization;
using Microsoft.AspNetCore.Mvc.Filters;
using Microsoft.AspNetCore.Mvc.RazorPages;
using Moq;
using Xunit;
namespace Microsoft.Extensions.DependencyInjection
{
public class RazorPagesOptionsExtensionsTest
{
[Fact]
public void AddFilter_AddsFiltersToAllPages()
{
// Arrange
var filter = Mock.Of<IFilterMetadata>();
var options = new RazorPagesOptions();
var models = new[]
{
new PageApplicationModel("/Pages/Index.cshtml", "/Index.cshtml"),
new PageApplicationModel("/Pages/Users/Account.cshtml", "/Users/Account.cshtml"),
new PageApplicationModel("/Pages/Users/Contact.cshtml", "/Users/Contact.cshtml"),
};
// Act
options.ConfigureFilter(filter);
ApplyConventions(options, models);
// Assert
Assert.Collection(models,
model => Assert.Same(filter, Assert.Single(model.Filters)),
model => Assert.Same(filter, Assert.Single(model.Filters)),
model => Assert.Same(filter, Assert.Single(model.Filters)));
}
[Fact]
public void AuthorizePage_AddsAuthorizeFilterToSpecificPage()
{
// Arrange
var options = new RazorPagesOptions();
var models = new[]
{
new PageApplicationModel("/Pages/Index.cshtml", "/Index.cshtml"),
new PageApplicationModel("/Pages/Users/Account.cshtml", "/Users/Account.cshtml"),
new PageApplicationModel("/Pages/Users/Contact.cshtml", "/Users/Contact.cshtml"),
};
// Act
options.AuthorizePage("/Users/Account.cshtml", "Manage-Accounts");
ApplyConventions(options, models);
// Assert
Assert.Collection(models,
model => Assert.Empty(model.Filters),
model =>
{
Assert.Equal("/Users/Account.cshtml", model.ViewEnginePath);
var authorizeFilter = Assert.IsType<AuthorizeFilter>(Assert.Single(model.Filters));
var authorizeData = Assert.IsType<AuthorizeAttribute>(Assert.Single(authorizeFilter.AuthorizeData));
Assert.Equal("Manage-Accounts", authorizeData.Policy);
},
model => Assert.Empty(model.Filters));
}
[Theory]
[InlineData("/Users")]
[InlineData("/Users/")]
public void AuthorizePage_AddsAuthorizeFilterToPagesUnderFolder(string folderName)
{
// Arrange
var options = new RazorPagesOptions();
var models = new[]
{
new PageApplicationModel("/Pages/Index.cshtml", "/Index.cshtml"),
new PageApplicationModel("/Pages/Users/Account.cshtml", "/Users/Account.cshtml"),
new PageApplicationModel("/Pages/Users/Contact.cshtml", "/Users/Contact.cshtml"),
};
// Act
options.AuthorizeFolder(folderName, "Manage-Accounts");
ApplyConventions(options, models);
// Assert
Assert.Collection(models,
model => Assert.Empty(model.Filters),
model =>
{
Assert.Equal("/Users/Account.cshtml", model.ViewEnginePath);
var authorizeFilter = Assert.IsType<AuthorizeFilter>(Assert.Single(model.Filters));
var authorizeData = Assert.IsType<AuthorizeAttribute>(Assert.Single(authorizeFilter.AuthorizeData));
Assert.Equal("Manage-Accounts", authorizeData.Policy);
},
model =>
{
Assert.Equal("/Users/Contact.cshtml", model.ViewEnginePath);
var authorizeFilter = Assert.IsType<AuthorizeFilter>(Assert.Single(model.Filters));
var authorizeData = Assert.IsType<AuthorizeAttribute>(Assert.Single(authorizeFilter.AuthorizeData));
Assert.Equal("Manage-Accounts", authorizeData.Policy);
});
}
private static void ApplyConventions(RazorPagesOptions options, PageApplicationModel[] models)
{
foreach (var convention in options.Conventions)
{
foreach (var model in models)
{
convention.Apply(model);
}
}
}
}
}

View File

@ -7,6 +7,7 @@ using Microsoft.AspNetCore.Mvc.ApplicationModels;
using Microsoft.AspNetCore.Mvc.Filters;
using Microsoft.AspNetCore.Mvc.Razor;
using Microsoft.AspNetCore.Mvc.Razor.Internal;
using Microsoft.AspNetCore.Mvc.RazorPages.Internal;
using Microsoft.AspNetCore.Mvc.ViewFeatures.Internal;
using Microsoft.AspNetCore.Razor.Evolution;
using Microsoft.Extensions.Options;
@ -30,7 +31,7 @@ namespace Microsoft.AspNetCore.Mvc.RazorPages.Infrastructure
var provider = new PageActionDescriptorProvider(
razorProject.Object,
GetAccessor<MvcOptions>(),
GetAccessor<RazorPagesOptions>());
GetRazorPagesOptions());
var context = new ActionDescriptorProviderContext();
// Act
@ -53,7 +54,7 @@ namespace Microsoft.AspNetCore.Mvc.RazorPages.Infrastructure
var provider = new PageActionDescriptorProvider(
razorProject.Object,
GetAccessor<MvcOptions>(),
GetAccessor<RazorPagesOptions>());
GetRazorPagesOptions());
var context = new ActionDescriptorProviderContext();
// Act
@ -80,7 +81,7 @@ namespace Microsoft.AspNetCore.Mvc.RazorPages.Infrastructure
var provider = new PageActionDescriptorProvider(
razorProject.Object,
GetAccessor<MvcOptions>(),
GetAccessor<RazorPagesOptions>());
GetRazorPagesOptions());
var context = new ActionDescriptorProviderContext();
// Act
@ -109,7 +110,7 @@ namespace Microsoft.AspNetCore.Mvc.RazorPages.Infrastructure
var provider = new PageActionDescriptorProvider(
razorProject.Object,
GetAccessor<MvcOptions>(),
GetAccessor<RazorPagesOptions>());
GetRazorPagesOptions());
var context = new ActionDescriptorProviderContext();
// Act and Assert
@ -133,7 +134,7 @@ namespace Microsoft.AspNetCore.Mvc.RazorPages.Infrastructure
var provider = new PageActionDescriptorProvider(
razorProject.Object,
GetAccessor<MvcOptions>(),
GetAccessor<RazorPagesOptions>());
GetRazorPagesOptions());
var context = new ActionDescriptorProviderContext();
// Act
@ -170,7 +171,7 @@ namespace Microsoft.AspNetCore.Mvc.RazorPages.Infrastructure
var provider = new PageActionDescriptorProvider(
razorProject.Object,
GetAccessor<MvcOptions>(),
GetAccessor<RazorPagesOptions>());
GetRazorPagesOptions());
var context = new ActionDescriptorProviderContext();
// Act
@ -208,7 +209,7 @@ namespace Microsoft.AspNetCore.Mvc.RazorPages.Infrastructure
var provider = new PageActionDescriptorProvider(
razorProject.Object,
GetAccessor(options),
GetAccessor<RazorPagesOptions>());
GetRazorPagesOptions());
var context = new ActionDescriptorProviderContext();
// Act
@ -250,7 +251,7 @@ namespace Microsoft.AspNetCore.Mvc.RazorPages.Infrastructure
var provider = new PageActionDescriptorProvider(
razorProject.Object,
GetAccessor(options),
GetAccessor<RazorPagesOptions>());
GetRazorPagesOptions());
var context = new ActionDescriptorProviderContext();
// Act
@ -291,14 +292,14 @@ namespace Microsoft.AspNetCore.Mvc.RazorPages.Infrastructure
var localFilter = Mock.Of<IFilterMetadata>();
var options = new MvcOptions();
options.Filters.Add(globalFilter);
var convention = new Mock<IPageModelConvention>();
var convention = new Mock<IPageApplicationModelConvention>();
convention.Setup(c => c.Apply(It.IsAny<PageApplicationModel>()))
.Callback((PageApplicationModel model) =>
{
model.Filters.Add(localFilter);
});
var razorOptions = new RazorPagesOptions();
razorOptions.Conventions.Add(convention.Object);
var razorOptions = GetRazorPagesOptions();
razorOptions.Value.Conventions.Add(convention.Object);
var razorProject = new Mock<RazorProject>();
razorProject.Setup(p => p.EnumerateItems("/"))
@ -309,7 +310,7 @@ namespace Microsoft.AspNetCore.Mvc.RazorPages.Infrastructure
var provider = new PageActionDescriptorProvider(
razorProject.Object,
GetAccessor(options),
GetAccessor(razorOptions));
razorOptions);
var context = new ActionDescriptorProviderContext();
// Act
@ -349,6 +350,11 @@ namespace Microsoft.AspNetCore.Mvc.RazorPages.Infrastructure
return accessor.Object;
}
private static IOptions<RazorPagesOptions> GetRazorPagesOptions()
{
return new OptionsManager<RazorPagesOptions>(new[] { new RazorPagesOptionsSetup() });
}
private static RazorProjectItem GetProjectItem(string basePath, string path, string content)
{
var testFileInfo = new TestFileInfo

View File

@ -0,0 +1,33 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using Microsoft.AspNetCore.Mvc.ApplicationModels;
using Microsoft.AspNetCore.Mvc.ViewFeatures.Internal;
using Xunit;
namespace Microsoft.AspNetCore.Mvc.RazorPages.Internal
{
public class RazorPagesOptionsSetupTest
{
[Fact]
public void Configure_AddsGlobalFilters()
{
// Arrange
var options = new RazorPagesOptions();
var setup = new RazorPagesOptionsSetup();
var applicationModel = new PageApplicationModel("/Home.cshtml", "/Home.cshtml");
// Act
setup.Configure(options);
foreach (var convention in options.Conventions)
{
convention.Apply(applicationModel);
}
// Assert
Assert.Collection(applicationModel.Filters,
filter => Assert.IsType<SaveTempDataPropertyFilter>(filter),
filter => Assert.IsType<AutoValidateAntiforgeryTokenAttribute>(filter));
}
}
}

View File

@ -0,0 +1,3 @@
@page
Can't see me

View File

@ -0,0 +1,2 @@
@page
@Context.Request.Query["ReturnUrl"]

View File

@ -10,6 +10,7 @@
<ProjectReference Include="..\..\..\src\Microsoft.AspNetCore.Mvc\Microsoft.AspNetCore.Mvc.csproj" />
<ProjectReference Include="..\Microsoft.AspNetCore.Mvc.TestConfiguration\Microsoft.AspNetCore.Mvc.TestConfiguration.csproj" />
<PackageReference Include="Microsoft.AspNetCore.Authentication.Cookies" Version="1.2.0-*" />
<PackageReference Include="Microsoft.AspNetCore.Server.IISIntegration" Version="1.2.0-*" />
<PackageReference Include="Microsoft.AspNetCore.Server.Kestrel" Version="1.2.0-*" />
<PackageReference Include="Microsoft.AspNetCore.StaticFiles" Version="1.2.0-*" />

View File

@ -10,15 +10,28 @@ namespace RazorPagesWebSite
{
public void ConfigureServices(IServiceCollection services)
{
services.AddMvc().AddCookieTempDataProvider();
services
.AddMvc()
.AddCookieTempDataProvider()
.AddRazorPagesOptions(options =>
{
options.AuthorizePage("/HelloWorldWithAuth", string.Empty);
});
}
public void Configure(IApplicationBuilder app)
{
app.UseCultureReplacer();
app.UseCookieAuthentication(new CookieAuthenticationOptions
{
LoginPath = "/Login",
AutomaticAuthenticate = true,
AutomaticChallenge = true
});
app.UseStaticFiles();
app.UseMvc();
}
}