Fixes: #7611 - return convention builders for MVC

Adds support for returning convention builders from the various MVC
endpoint routing methods.

The fallback methods are excluded because the fallback endpoint doesn't
actually get executed.
This commit is contained in:
Ryan Nowak 2019-04-22 13:51:05 -07:00 committed by Ryan Nowak
parent 9ac2f52b22
commit 6a99743d33
14 changed files with 300 additions and 75 deletions

View File

@ -3,16 +3,21 @@
namespace Microsoft.AspNetCore.Builder
{
public sealed partial class ControllerActionEndpointConventionBuilder : Microsoft.AspNetCore.Builder.IEndpointConventionBuilder
{
internal ControllerActionEndpointConventionBuilder() { }
public void Add(System.Action<Microsoft.AspNetCore.Builder.EndpointBuilder> convention) { }
}
public static partial class ControllerEndpointRouteBuilderExtensions
{
public static void MapAreaControllerRoute(this Microsoft.AspNetCore.Routing.IEndpointRouteBuilder endpoints, string name, string areaName, string pattern, object defaults = null, object constraints = null, object dataTokens = null) { }
public static void MapControllerRoute(this Microsoft.AspNetCore.Routing.IEndpointRouteBuilder endpoints, string name, string pattern, object defaults = null, object constraints = null, object dataTokens = null) { }
public static Microsoft.AspNetCore.Builder.IEndpointConventionBuilder MapControllers(this Microsoft.AspNetCore.Routing.IEndpointRouteBuilder endpoints) { throw null; }
public static Microsoft.AspNetCore.Builder.IEndpointConventionBuilder MapDefaultControllerRoute(this Microsoft.AspNetCore.Routing.IEndpointRouteBuilder endpoints) { throw null; }
public static void MapFallbackToAreaController(this Microsoft.AspNetCore.Routing.IEndpointRouteBuilder endpoints, string action, string controller, string area) { }
public static void MapFallbackToAreaController(this Microsoft.AspNetCore.Routing.IEndpointRouteBuilder endpoints, string pattern, string action, string controller, string area) { }
public static void MapFallbackToController(this Microsoft.AspNetCore.Routing.IEndpointRouteBuilder endpoints, string action, string controller) { }
public static void MapFallbackToController(this Microsoft.AspNetCore.Routing.IEndpointRouteBuilder endpoints, string pattern, string action, string controller) { }
public static Microsoft.AspNetCore.Builder.ControllerActionEndpointConventionBuilder MapAreaControllerRoute(this Microsoft.AspNetCore.Routing.IEndpointRouteBuilder endpoints, string name, string areaName, string pattern, object defaults = null, object constraints = null, object dataTokens = null) { throw null; }
public static Microsoft.AspNetCore.Builder.ControllerActionEndpointConventionBuilder MapControllerRoute(this Microsoft.AspNetCore.Routing.IEndpointRouteBuilder endpoints, string name, string pattern, object defaults = null, object constraints = null, object dataTokens = null) { throw null; }
public static Microsoft.AspNetCore.Builder.ControllerActionEndpointConventionBuilder MapControllers(this Microsoft.AspNetCore.Routing.IEndpointRouteBuilder endpoints) { throw null; }
public static Microsoft.AspNetCore.Builder.ControllerActionEndpointConventionBuilder MapDefaultControllerRoute(this Microsoft.AspNetCore.Routing.IEndpointRouteBuilder endpoints) { throw null; }
public static Microsoft.AspNetCore.Builder.IEndpointConventionBuilder MapFallbackToAreaController(this Microsoft.AspNetCore.Routing.IEndpointRouteBuilder endpoints, string action, string controller, string area) { throw null; }
public static Microsoft.AspNetCore.Builder.IEndpointConventionBuilder MapFallbackToAreaController(this Microsoft.AspNetCore.Routing.IEndpointRouteBuilder endpoints, string pattern, string action, string controller, string area) { throw null; }
public static Microsoft.AspNetCore.Builder.IEndpointConventionBuilder MapFallbackToController(this Microsoft.AspNetCore.Routing.IEndpointRouteBuilder endpoints, string action, string controller) { throw null; }
public static Microsoft.AspNetCore.Builder.IEndpointConventionBuilder MapFallbackToController(this Microsoft.AspNetCore.Routing.IEndpointRouteBuilder endpoints, string pattern, string action, string controller) { throw null; }
}
public static partial class MvcApplicationBuilderExtensions
{

View File

@ -0,0 +1,46 @@
// 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;
namespace Microsoft.AspNetCore.Builder
{
/// <summary>
/// Builds conventions that will be used for customization of <see cref="EndpointBuilder"/> instances.
/// </summary>
/// <remarks>
/// This interface is used at application startup to customize endpoints for the application.
/// </remarks>
public sealed class ControllerActionEndpointConventionBuilder : IEndpointConventionBuilder
{
// The lock is shared with the data source.
private readonly object _lock;
private readonly List<Action<EndpointBuilder>> _conventions;
internal ControllerActionEndpointConventionBuilder(object @lock, List<Action<EndpointBuilder>> conventions)
{
_lock = @lock;
_conventions = conventions;
}
/// <summary>
/// Adds the specified convention to the builder. Conventions are used to customize <see cref="EndpointBuilder"/> instances.
/// </summary>
/// <param name="convention">The convention to add to the builder.</param>
public void Add(Action<EndpointBuilder> convention)
{
if (convention == null)
{
throw new ArgumentNullException(nameof(convention));
}
// The lock is shared with the data source. We want to lock here
// to avoid mutating this list while its read in the data source.
lock (_lock)
{
_conventions.Add(convention);
}
}
}
}

View File

@ -21,8 +21,8 @@ namespace Microsoft.AspNetCore.Builder
/// Adds endpoints for controller actions to the <see cref="IEndpointRouteBuilder"/> without specifying any routes.
/// </summary>
/// <param name="endpoints">The <see cref="IEndpointRouteBuilder"/>.</param>
/// <returns>An <see cref="IEndpointConventionBuilder"/> for endpoints associated with controller actions.</returns>
public static IEndpointConventionBuilder MapControllers(this IEndpointRouteBuilder endpoints)
/// <returns>An <see cref="ControllerActionEndpointConventionBuilder"/> for endpoints associated with controller actions.</returns>
public static ControllerActionEndpointConventionBuilder MapControllers(this IEndpointRouteBuilder endpoints)
{
if (endpoints == null)
{
@ -31,7 +31,7 @@ namespace Microsoft.AspNetCore.Builder
EnsureControllerServices(endpoints);
return GetOrCreateDataSource(endpoints);
return GetOrCreateDataSource(endpoints).DefaultBuilder;
}
/// <summary>
@ -39,8 +39,10 @@ namespace Microsoft.AspNetCore.Builder
/// <c>{controller=Home}/{action=Index}/{id?}</c>.
/// </summary>
/// <param name="endpoints">The <see cref="IEndpointRouteBuilder"/>.</param>
/// <returns>An <see cref="IEndpointConventionBuilder"/> for endpoints associated with controller actions.</returns>
public static IEndpointConventionBuilder MapDefaultControllerRoute(this IEndpointRouteBuilder endpoints)
/// <returns>
/// An <see cref="ControllerActionEndpointConventionBuilder"/> for endpoints associated with controller actions for this route.
/// </returns>
public static ControllerActionEndpointConventionBuilder MapDefaultControllerRoute(this IEndpointRouteBuilder endpoints)
{
if (endpoints == null)
{
@ -50,14 +52,12 @@ namespace Microsoft.AspNetCore.Builder
EnsureControllerServices(endpoints);
var dataSource = GetOrCreateDataSource(endpoints);
dataSource.AddRoute(
return dataSource.AddRoute(
"default",
"{controller=Home}/{action=Index}/{id?}",
defaults: null,
constraints: null,
dataTokens: null);
return dataSource;
}
/// <summary>
@ -80,7 +80,10 @@ namespace Microsoft.AspNetCore.Builder
/// An object that contains data tokens for the route. The object's properties represent the names and
/// values of the data tokens.
/// </param>
public static void MapControllerRoute(
/// <returns>
/// An <see cref="ControllerActionEndpointConventionBuilder"/> for endpoints associated with controller actions for this route.
/// </returns>
public static ControllerActionEndpointConventionBuilder MapControllerRoute(
this IEndpointRouteBuilder endpoints,
string name,
string pattern,
@ -96,7 +99,7 @@ namespace Microsoft.AspNetCore.Builder
EnsureControllerServices(endpoints);
var dataSource = GetOrCreateDataSource(endpoints);
dataSource.AddRoute(
return dataSource.AddRoute(
name,
pattern,
new RouteValueDictionary(defaults),
@ -125,7 +128,10 @@ namespace Microsoft.AspNetCore.Builder
/// An object that contains data tokens for the route. The object's properties represent the names and
/// values of the data tokens.
/// </param>
public static void MapAreaControllerRoute(
/// <returns>
/// An <see cref="ControllerActionEndpointConventionBuilder"/> for endpoints associated with controller actions for this route.
/// </returns>
public static ControllerActionEndpointConventionBuilder MapAreaControllerRoute(
this IEndpointRouteBuilder endpoints,
string name,
string areaName,
@ -150,7 +156,7 @@ namespace Microsoft.AspNetCore.Builder
var constraintsDictionary = new RouteValueDictionary(constraints);
constraintsDictionary["area"] = constraintsDictionary["area"] ?? new StringRouteConstraint(areaName);
endpoints.MapControllerRoute(name, pattern, defaultsDictionary, constraintsDictionary, dataTokens);
return endpoints.MapControllerRoute(name, pattern, defaultsDictionary, constraintsDictionary, dataTokens);
}
/// <summary>
@ -183,7 +189,7 @@ namespace Microsoft.AspNetCore.Builder
/// actions match these values, the result is implementation defined.
/// </para>
/// </remarks>
public static void MapFallbackToController(
public static IEndpointConventionBuilder MapFallbackToController(
this IEndpointRouteBuilder endpoints,
string action,
string controller)
@ -210,11 +216,13 @@ namespace Microsoft.AspNetCore.Builder
// Maps a fallback endpoint with an empty delegate. This is OK because
// we don't expect the delegate to run.
endpoints.MapFallback(context => Task.CompletedTask).Add(b =>
var builder = endpoints.MapFallback(context => Task.CompletedTask);
builder.Add(b =>
{
// MVC registers a policy that looks for this metadata.
b.Metadata.Add(CreateDynamicControllerMetadata(action, controller, area: null));
});
return builder;
}
/// <summary>
@ -251,7 +259,7 @@ namespace Microsoft.AspNetCore.Builder
/// actions match these values, the result is implementation defined.
/// </para>
/// </remarks>
public static void MapFallbackToController(
public static IEndpointConventionBuilder MapFallbackToController(
this IEndpointRouteBuilder endpoints,
string pattern,
string action,
@ -284,11 +292,13 @@ namespace Microsoft.AspNetCore.Builder
// Maps a fallback endpoint with an empty delegate. This is OK because
// we don't expect the delegate to run.
endpoints.MapFallback(pattern, context => Task.CompletedTask).Add(b =>
var builder = endpoints.MapFallback(pattern, context => Task.CompletedTask);
builder.Add(b =>
{
// MVC registers a policy that looks for this metadata.
b.Metadata.Add(CreateDynamicControllerMetadata(action, controller, area: null));
});
return builder;
}
/// <summary>
@ -322,7 +332,7 @@ namespace Microsoft.AspNetCore.Builder
/// actions match these values, the result is implementation defined.
/// </para>
/// </remarks>
public static void MapFallbackToAreaController(
public static IEndpointConventionBuilder MapFallbackToAreaController(
this IEndpointRouteBuilder endpoints,
string action,
string controller,
@ -350,11 +360,13 @@ namespace Microsoft.AspNetCore.Builder
// Maps a fallback endpoint with an empty delegate. This is OK because
// we don't expect the delegate to run.
endpoints.MapFallback(context => Task.CompletedTask).Add(b =>
var builder = endpoints.MapFallback(context => Task.CompletedTask);
builder.Add(b =>
{
// MVC registers a policy that looks for this metadata.
b.Metadata.Add(CreateDynamicControllerMetadata(action, controller, area));
});
return builder;
}
/// <summary>
@ -392,7 +404,7 @@ namespace Microsoft.AspNetCore.Builder
/// actions match these values, the result is implementation defined.
/// </para>
/// </remarks>
public static void MapFallbackToAreaController(
public static IEndpointConventionBuilder MapFallbackToAreaController(
this IEndpointRouteBuilder endpoints,
string pattern,
string action,
@ -426,11 +438,13 @@ namespace Microsoft.AspNetCore.Builder
// Maps a fallback endpoint with an empty delegate. This is OK because
// we don't expect the delegate to run.
endpoints.MapFallback(pattern, context => Task.CompletedTask).Add(b =>
var builder = endpoints.MapFallback(pattern, context => Task.CompletedTask);
builder.Add(b =>
{
// MVC registers a policy that looks for this metadata.
b.Metadata.Add(CreateDynamicControllerMetadata(action, controller, area));
});
return builder;
}
private static DynamicControllerMetadata CreateDynamicControllerMetadata(string action, string controller, string area)

View File

@ -14,7 +14,7 @@ using Microsoft.Extensions.Primitives;
namespace Microsoft.AspNetCore.Mvc.Routing
{
internal abstract class ActionEndpointDataSourceBase : EndpointDataSource, IEndpointConventionBuilder, IDisposable
internal abstract class ActionEndpointDataSourceBase : EndpointDataSource, IDisposable
{
private readonly IActionDescriptorCollectionProvider _actions;
@ -23,19 +23,20 @@ namespace Microsoft.AspNetCore.Mvc.Routing
// all of the threading behaviors.
protected readonly object Lock = new object();
// Protected for READS and WRITES.
protected readonly List<Action<EndpointBuilder>> Conventions;
private List<Endpoint> _endpoints;
private CancellationTokenSource _cancellationTokenSource;
private IChangeToken _changeToken;
private IDisposable _disposable;
// Protected for READS and WRITES.
private readonly List<Action<EndpointBuilder>> _conventions;
public ActionEndpointDataSourceBase(IActionDescriptorCollectionProvider actions)
{
_actions = actions;
_conventions = new List<Action<EndpointBuilder>>();
Conventions = new List<Action<EndpointBuilder>>();
}
public override IReadOnlyList<Endpoint> Endpoints
@ -67,19 +68,6 @@ namespace Microsoft.AspNetCore.Mvc.Routing
}
}
public void Add(Action<EndpointBuilder> convention)
{
if (convention == null)
{
throw new ArgumentNullException(nameof(convention));
}
lock (Lock)
{
_conventions.Add(convention);
}
}
public override IChangeToken GetChangeToken()
{
Initialize();
@ -113,7 +101,7 @@ namespace Microsoft.AspNetCore.Mvc.Routing
{
lock (Lock)
{
var endpoints = CreateEndpoints(_actions.ActionDescriptors.Items, _conventions);
var endpoints = CreateEndpoints(_actions.ActionDescriptors.Items, Conventions);
// See comments in DefaultActionDescriptorCollectionProvider. These steps are done
// in a specific order to ensure callers always see a consistent state.

View File

@ -83,7 +83,8 @@ namespace Microsoft.AspNetCore.Mvc.Routing
route.DataTokens,
suppressLinkGeneration: true,
suppressPathMatching: false,
conventions);
conventions,
route.Conventions);
endpoints.Add(builder);
}
}
@ -109,12 +110,17 @@ namespace Microsoft.AspNetCore.Mvc.Routing
dataTokens: null,
action.AttributeRouteInfo.SuppressLinkGeneration,
action.AttributeRouteInfo.SuppressPathMatching,
conventions);
conventions,
perRouteConventions: Array.Empty<Action<EndpointBuilder>>());
endpoints.Add(endpoint);
}
}
public void AddConventionalLinkGenerationRoute(List<Endpoint> endpoints, HashSet<string> keys, ConventionalRouteEntry route, IReadOnlyList<Action<EndpointBuilder>> conventions)
public void AddConventionalLinkGenerationRoute(
List<Endpoint> endpoints,
HashSet<string> keys,
ConventionalRouteEntry route,
IReadOnlyList<Action<EndpointBuilder>> conventions)
{
if (endpoints == null)
{
@ -177,6 +183,11 @@ namespace Microsoft.AspNetCore.Mvc.Routing
conventions[i](builder);
}
for (var i = 0; i < route.Conventions.Count; i++)
{
route.Conventions[i](builder);
}
endpoints.Add((RouteEndpoint)builder.Build());
}
@ -240,7 +251,8 @@ namespace Microsoft.AspNetCore.Mvc.Routing
RouteValueDictionary dataTokens,
bool suppressLinkGeneration,
bool suppressPathMatching,
IReadOnlyList<Action<EndpointBuilder>> conventions)
IReadOnlyList<Action<EndpointBuilder>> conventions,
IReadOnlyList<Action<EndpointBuilder>> perRouteConventions)
{
// We don't want to close over the retrieve the Invoker Factory in ActionEndpointFactory as
@ -340,6 +352,11 @@ namespace Microsoft.AspNetCore.Mvc.Routing
conventions[i](builder);
}
for (var i = 0; i < perRouteConventions.Count; i++)
{
perRouteConventions[i](builder);
}
return (RouteEndpoint)builder.Build();
}
}

View File

@ -27,7 +27,7 @@ namespace Microsoft.AspNetCore.Mvc.Routing
: base(actions)
{
_endpointFactory = endpointFactory;
_routes = new List<ConventionalRouteEntry>();
// In traditional conventional routing setup, the routes defined by a user have a order
@ -38,13 +38,16 @@ namespace Microsoft.AspNetCore.Mvc.Routing
// This is for scenarios dealing with migrating existing Router based code to Endpoint Routing world.
_order = 1;
DefaultBuilder = new ControllerActionEndpointConventionBuilder(Lock, Conventions);
// IMPORTANT: this needs to be the last thing we do in the constructor.
// Change notifications can happen immediately!
Subscribe();
}
public ControllerActionEndpointConventionBuilder DefaultBuilder { get; }
public void AddRoute(
public ControllerActionEndpointConventionBuilder AddRoute(
string routeName,
string pattern,
RouteValueDictionary defaults,
@ -53,7 +56,9 @@ namespace Microsoft.AspNetCore.Mvc.Routing
{
lock (Lock)
{
_routes.Add(new ConventionalRouteEntry(routeName, pattern, defaults, constraints, dataTokens, _order++));
var conventions = new List<Action<EndpointBuilder>>();
_routes.Add(new ConventionalRouteEntry(routeName, pattern, defaults, constraints, dataTokens, _order++, conventions));
return new ControllerActionEndpointConventionBuilder(Lock, conventions);
}
}

View File

@ -4,6 +4,7 @@
using System;
using System.Collections.Generic;
using System.Globalization;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Routing;
using Microsoft.AspNetCore.Routing.Patterns;
@ -15,6 +16,7 @@ namespace Microsoft.AspNetCore.Mvc.Routing
public readonly string RouteName;
public readonly RouteValueDictionary DataTokens;
public readonly int Order;
public readonly IReadOnlyList<Action<EndpointBuilder>> Conventions;
public ConventionalRouteEntry(
string routeName,
@ -22,11 +24,13 @@ namespace Microsoft.AspNetCore.Mvc.Routing
RouteValueDictionary defaults,
IDictionary<string, object> constraints,
RouteValueDictionary dataTokens,
int order)
int order,
List<Action<EndpointBuilder>> conventions)
{
RouteName = routeName;
DataTokens = dataTokens;
Order = order;
Conventions = conventions;
try
{

View File

@ -306,7 +306,7 @@ namespace Microsoft.AspNetCore.Mvc.Routing
private RouteEndpoint CreateConventionalRoutedEndpoint(ActionDescriptor action, string template)
{
return CreateConventionalRoutedEndpoint(action, new ConventionalRouteEntry(routeName: null, template, null, null, null, order: 0));
return CreateConventionalRoutedEndpoint(action, new ConventionalRouteEntry(routeName: null, template, null, null, null, order: 0, new List<Action<EndpointBuilder>>()));
}
private RouteEndpoint CreateConventionalRoutedEndpoint(ActionDescriptor action, ConventionalRouteEntry route)
@ -341,9 +341,11 @@ namespace Microsoft.AspNetCore.Mvc.Routing
RouteValueDictionary defaults = null,
IDictionary<string, object> constraints = null,
RouteValueDictionary dataTokens = null,
int order = 0)
int order = 0,
List<Action<EndpointBuilder>> conventions = null)
{
return new ConventionalRouteEntry(routeName, pattern, defaults, constraints, dataTokens, order);
conventions ??= new List<Action<EndpointBuilder>>();
return new ConventionalRouteEntry(routeName, pattern, defaults, constraints, dataTokens, order, conventions);
}
private ActionDescriptor CreateActionDescriptor(

View File

@ -155,7 +155,7 @@ namespace Microsoft.AspNetCore.Mvc.Routing
dataSource.AddRoute("1", "/1/{controller}/{action}/{id?}", null, null, null);
dataSource.AddRoute("2", "/2/{controller}/{action}/{id?}", null, null, null);
dataSource.Add((b) =>
dataSource.DefaultBuilder.Add((b) =>
{
b.Metadata.Add("Hi there");
});
@ -201,6 +201,87 @@ namespace Microsoft.AspNetCore.Mvc.Routing
});
}
[Fact]
public void Endpoints_AppliesConventions_RouteSpecificMetadata()
{
// Arrange
var actions = new List<ActionDescriptor>
{
new ControllerActionDescriptor
{
AttributeRouteInfo = new AttributeRouteInfo()
{
Template = "/test",
},
RouteValues = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase)
{
{ "action", "Test" },
{ "controller", "Test" },
},
},
new ControllerActionDescriptor
{
RouteValues = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase)
{
{ "action", "Index" },
{ "controller", "Home" },
},
}
};
var mockDescriptorProvider = new Mock<IActionDescriptorCollectionProvider>();
mockDescriptorProvider.Setup(m => m.ActionDescriptors).Returns(new ActionDescriptorCollection(actions, 0));
var dataSource = (ControllerActionEndpointDataSource)CreateDataSource(mockDescriptorProvider.Object);
dataSource.AddRoute("1", "/1/{controller}/{action}/{id?}", null, null, null).Add(b => b.Metadata.Add("A"));
dataSource.AddRoute("2", "/2/{controller}/{action}/{id?}", null, null, null).Add(b => b.Metadata.Add("B"));
dataSource.DefaultBuilder.Add((b) =>
{
b.Metadata.Add("Hi there");
});
// Act
var endpoints = dataSource.Endpoints;
// Assert
Assert.Collection(
endpoints.OfType<RouteEndpoint>().Where(e => !SupportsLinkGeneration(e)).OrderBy(e => e.RoutePattern.RawText),
e =>
{
Assert.Equal("/1/{controller}/{action}/{id?}", e.RoutePattern.RawText);
Assert.Same(actions[1], e.Metadata.GetMetadata<ActionDescriptor>());
Assert.Equal(new[] { "Hi there", "A" }, e.Metadata.GetOrderedMetadata<string>());
},
e =>
{
Assert.Equal("/2/{controller}/{action}/{id?}", e.RoutePattern.RawText);
Assert.Same(actions[1], e.Metadata.GetMetadata<ActionDescriptor>());
Assert.Equal(new[] { "Hi there", "B" }, e.Metadata.GetOrderedMetadata<string>());
});
Assert.Collection(
endpoints.OfType<RouteEndpoint>().Where(e => SupportsLinkGeneration(e)).OrderBy(e => e.RoutePattern.RawText),
e =>
{
Assert.Equal("/1/{controller}/{action}/{id?}", e.RoutePattern.RawText);
Assert.Null(e.Metadata.GetMetadata<ActionDescriptor>());
Assert.Equal(new[] { "Hi there", "A" }, e.Metadata.GetOrderedMetadata<string>());
},
e =>
{
Assert.Equal("/2/{controller}/{action}/{id?}", e.RoutePattern.RawText);
Assert.Null(e.Metadata.GetMetadata<ActionDescriptor>());
Assert.Equal(new[] { "Hi there", "B" }, e.Metadata.GetOrderedMetadata<string>());
},
e =>
{
Assert.Equal("/test", e.RoutePattern.RawText);
Assert.Same(actions[0], e.Metadata.GetMetadata<ActionDescriptor>());
Assert.Equal("Hi there", e.Metadata.GetMetadata<string>());
});
}
private static bool SupportsLinkGeneration(RouteEndpoint endpoint)
{
return !(endpoint.Metadata.GetMetadata<ISuppressLinkGenerationMetadata>()?.SuppressLinkGeneration == true);

View File

@ -3,13 +3,18 @@
namespace Microsoft.AspNetCore.Builder
{
public sealed partial class PageActionEndpointConventionBuilder : Microsoft.AspNetCore.Builder.IEndpointConventionBuilder
{
internal PageActionEndpointConventionBuilder() { }
public void Add(System.Action<Microsoft.AspNetCore.Builder.EndpointBuilder> convention) { }
}
public static partial class RazorPagesEndpointRouteBuilderExtensions
{
public static void MapFallbackToAreaPage(this Microsoft.AspNetCore.Routing.IEndpointRouteBuilder endpoints, string page, string area) { }
public static void MapFallbackToAreaPage(this Microsoft.AspNetCore.Routing.IEndpointRouteBuilder endpoints, string pattern, string page, string area) { }
public static void MapFallbackToPage(this Microsoft.AspNetCore.Routing.IEndpointRouteBuilder endpoints, string page) { }
public static void MapFallbackToPage(this Microsoft.AspNetCore.Routing.IEndpointRouteBuilder endpoints, string pattern, string page) { }
public static Microsoft.AspNetCore.Builder.IEndpointConventionBuilder MapRazorPages(this Microsoft.AspNetCore.Routing.IEndpointRouteBuilder endpoints) { throw null; }
public static Microsoft.AspNetCore.Builder.IEndpointConventionBuilder MapFallbackToAreaPage(this Microsoft.AspNetCore.Routing.IEndpointRouteBuilder endpoints, string page, string area) { throw null; }
public static Microsoft.AspNetCore.Builder.IEndpointConventionBuilder MapFallbackToAreaPage(this Microsoft.AspNetCore.Routing.IEndpointRouteBuilder endpoints, string pattern, string page, string area) { throw null; }
public static Microsoft.AspNetCore.Builder.IEndpointConventionBuilder MapFallbackToPage(this Microsoft.AspNetCore.Routing.IEndpointRouteBuilder endpoints, string page) { throw null; }
public static Microsoft.AspNetCore.Builder.IEndpointConventionBuilder MapFallbackToPage(this Microsoft.AspNetCore.Routing.IEndpointRouteBuilder endpoints, string pattern, string page) { throw null; }
public static Microsoft.AspNetCore.Builder.PageActionEndpointConventionBuilder MapRazorPages(this Microsoft.AspNetCore.Routing.IEndpointRouteBuilder endpoints) { throw null; }
}
}
namespace Microsoft.AspNetCore.Mvc.ApplicationModels

View File

@ -0,0 +1,46 @@
// 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;
namespace Microsoft.AspNetCore.Builder
{
/// <summary>
/// Builds conventions that will be used for customization of <see cref="EndpointBuilder"/> instances.
/// </summary>
/// <remarks>
/// This interface is used at application startup to customize endpoints for the application.
/// </remarks>
public sealed class PageActionEndpointConventionBuilder : IEndpointConventionBuilder
{
// The lock is shared with the data source.
private readonly object _lock;
private readonly List<Action<EndpointBuilder>> _conventions;
internal PageActionEndpointConventionBuilder(object @lock, List<Action<EndpointBuilder>> conventions)
{
_lock = @lock;
_conventions = conventions;
}
/// <summary>
/// Adds the specified convention to the builder. Conventions are used to customize <see cref="EndpointBuilder"/> instances.
/// </summary>
/// <param name="convention">The convention to add to the builder.</param>
public void Add(Action<EndpointBuilder> convention)
{
if (convention == null)
{
throw new ArgumentNullException(nameof(convention));
}
// The lock is shared with the data source. We want to lock here
// to avoid mutating this list while its read in the data source.
lock (_lock)
{
_conventions.Add(convention);
}
}
}
}

View File

@ -20,8 +20,8 @@ namespace Microsoft.AspNetCore.Builder
/// Adds endpoints for Razor Pages to the <see cref="IEndpointRouteBuilder"/>.
/// </summary>
/// <param name="endpoints">The <see cref="IEndpointRouteBuilder"/>.</param>
/// <returns>An <see cref="IEndpointConventionBuilder"/> for endpoints associated with Razor Pages.</returns>
public static IEndpointConventionBuilder MapRazorPages(this IEndpointRouteBuilder endpoints)
/// <returns>An <see cref="PageActionEndpointConventionBuilder"/> for endpoints associated with Razor Pages.</returns>
public static PageActionEndpointConventionBuilder MapRazorPages(this IEndpointRouteBuilder endpoints)
{
if (endpoints == null)
{
@ -30,7 +30,7 @@ namespace Microsoft.AspNetCore.Builder
EnsureRazorPagesServices(endpoints);
return GetOrCreateDataSource(endpoints);
return GetOrCreateDataSource(endpoints).DefaultBuilder;
}
/// <summary>
@ -57,7 +57,7 @@ namespace Microsoft.AspNetCore.Builder
/// will be available.
/// </para>
/// </remarks>
public static void MapFallbackToPage(this IEndpointRouteBuilder endpoints, string page)
public static IEndpointConventionBuilder MapFallbackToPage(this IEndpointRouteBuilder endpoints, string page)
{
if (endpoints == null)
{
@ -78,11 +78,13 @@ namespace Microsoft.AspNetCore.Builder
// Maps a fallback endpoint with an empty delegate. This is OK because
// we don't expect the delegate to run.
endpoints.MapFallback(context => Task.CompletedTask).Add(b =>
var builder = endpoints.MapFallback(context => Task.CompletedTask);
builder.Add(b =>
{
// MVC registers a policy that looks for this metadata.
b.Metadata.Add(CreateDynamicPageMetadata(page, area: null));
});
return builder;
}
/// <summary>
@ -113,7 +115,7 @@ namespace Microsoft.AspNetCore.Builder
/// <paramref name="pattern"/> will be available.
/// </para>
/// </remarks>
public static void MapFallbackToPage(
public static IEndpointConventionBuilder MapFallbackToPage(
this IEndpointRouteBuilder endpoints,
string pattern,
string page)
@ -142,11 +144,13 @@ namespace Microsoft.AspNetCore.Builder
// Maps a fallback endpoint with an empty delegate. This is OK because
// we don't expect the delegate to run.
endpoints.MapFallback(pattern, context => Task.CompletedTask).Add(b =>
var builder = endpoints.MapFallback(pattern, context => Task.CompletedTask);
builder.Add(b =>
{
// MVC registers a policy that looks for this metadata.
b.Metadata.Add(CreateDynamicPageMetadata(page, area: null));
});
return builder;
}
/// <summary>
@ -174,7 +178,7 @@ namespace Microsoft.AspNetCore.Builder
/// will be available.
/// </para>
/// </remarks>
public static void MapFallbackToAreaPage(
public static IEndpointConventionBuilder MapFallbackToAreaPage(
this IEndpointRouteBuilder endpoints,
string page,
string area)
@ -198,11 +202,13 @@ namespace Microsoft.AspNetCore.Builder
// Maps a fallback endpoint with an empty delegate. This is OK because
// we don't expect the delegate to run.
endpoints.MapFallback(context => Task.CompletedTask).Add(b =>
var builder = endpoints.MapFallback(context => Task.CompletedTask);
builder.Add(b =>
{
// MVC registers a policy that looks for this metadata.
b.Metadata.Add(CreateDynamicPageMetadata(page, area));
});
return builder;
}
/// <summary>
@ -234,7 +240,7 @@ namespace Microsoft.AspNetCore.Builder
/// <paramref name="pattern"/> will be available.
/// </para>
/// </remarks>
public static void MapFallbackToAreaPage(
public static IEndpointConventionBuilder MapFallbackToAreaPage(
this IEndpointRouteBuilder endpoints,
string pattern,
string page,
@ -264,11 +270,13 @@ namespace Microsoft.AspNetCore.Builder
// Maps a fallback endpoint with an empty delegate. This is OK because
// we don't expect the delegate to run.
endpoints.MapFallback(pattern, context => Task.CompletedTask).Add(b =>
var builder = endpoints.MapFallback(pattern, context => Task.CompletedTask);
builder.Add(b =>
{
// MVC registers a policy that looks for this metadata.
b.Metadata.Add(CreateDynamicPageMetadata(page, area));
});
return builder;
}
private static DynamicPageMetadata CreateDynamicPageMetadata(string page, string area)

View File

@ -20,11 +20,15 @@ namespace Microsoft.AspNetCore.Mvc.RazorPages.Infrastructure
{
_endpointFactory = endpointFactory;
DefaultBuilder = new PageActionEndpointConventionBuilder(Lock, Conventions);
// IMPORTANT: this needs to be the last thing we do in the constructor.
// Change notifications can happen immediately!
Subscribe();
}
public PageActionEndpointConventionBuilder DefaultBuilder { get; }
protected override List<Endpoint> CreateEndpoints(IReadOnlyList<ActionDescriptor> actions, IReadOnlyList<Action<EndpointBuilder>> conventions)
{
var endpoints = new List<Endpoint>();

View File

@ -71,7 +71,7 @@ namespace Microsoft.AspNetCore.Mvc.RazorPages.Infrastructure
var dataSource = (PageActionEndpointDataSource)CreateDataSource(mockDescriptorProvider.Object);
dataSource.Add((b) =>
dataSource.DefaultBuilder.Add((b) =>
{
b.Metadata.Add("Hi there");
});