Skip to content
代码片段 群组 项目
未验证 提交 5207cad8 编辑于 作者: Stephen Halter's avatar Stephen Halter 提交者: GitHub
浏览文件

Add new "MapAction" overloads (#30556)

* Add new "MapAction" overloads

* Create RouteEndpointBuilder directly

* fix typo: my -> by

* is { } -> is not null
上级 5bf4272b
No related branches found
No related tags found
无相关合并请求
......@@ -13,6 +13,11 @@ namespace Microsoft.AspNetCore.Builder
{
private readonly List<IEndpointConventionBuilder> _endpointConventionBuilders;
internal MapActionEndpointConventionBuilder(IEndpointConventionBuilder endpointConventionBuilder)
{
_endpointConventionBuilders = new List<IEndpointConventionBuilder>() { endpointConventionBuilder };
}
internal MapActionEndpointConventionBuilder(List<IEndpointConventionBuilder> endpointConventionBuilders)
{
_endpointConventionBuilders = endpointConventionBuilders;
......
......@@ -7,6 +7,7 @@ using System.Linq;
using System.Reflection;
using Microsoft.AspNetCore.Routing;
using Microsoft.AspNetCore.Routing.Internal;
using Microsoft.AspNetCore.Routing.Patterns;
namespace Microsoft.AspNetCore.Builder
{
......@@ -15,6 +16,12 @@ namespace Microsoft.AspNetCore.Builder
/// </summary>
public static class MapActionEndpointRouteBuilderExtensions
{
// Avoid creating a new array every call
private static readonly string[] GetVerb = new[] { "GET" };
private static readonly string[] PostVerb = new[] { "POST" };
private static readonly string[] PutVerb = new[] { "PUT" };
private static readonly string[] DeleteVerb = new[] { "DELETE" };
/// <summary>
/// Adds a <see cref="RouteEndpoint"/> to the <see cref="IEndpointRouteBuilder"/> that matches the pattern specified via attributes.
/// </summary>
......@@ -76,5 +83,203 @@ namespace Microsoft.AspNetCore.Builder
return new MapActionEndpointConventionBuilder(conventionBuilders);
}
/// <summary>
/// Adds a <see cref="RouteEndpoint"/> to the <see cref="IEndpointRouteBuilder"/> that matches HTTP GET requests
/// for the specified pattern.
/// </summary>
/// <param name="endpoints">The <see cref="IEndpointRouteBuilder"/> to add the route to.</param>
/// <param name="pattern">The route pattern.</param>
/// <param name="action">The delegate executed when the endpoint is matched.</param>
/// <returns>A <see cref="IEndpointConventionBuilder"/> that can be used to further customize the endpoint.</returns>
public static MapActionEndpointConventionBuilder MapGet(
this IEndpointRouteBuilder endpoints,
string pattern,
Delegate action)
{
return MapMethods(endpoints, pattern, GetVerb, action);
}
/// <summary>
/// Adds a <see cref="RouteEndpoint"/> to the <see cref="IEndpointRouteBuilder"/> that matches HTTP POST requests
/// for the specified pattern.
/// </summary>
/// <param name="endpoints">The <see cref="IEndpointRouteBuilder"/> to add the route to.</param>
/// <param name="pattern">The route pattern.</param>
/// <param name="action">The delegate executed when the endpoint is matched.</param>
/// <returns>A <see cref="IEndpointConventionBuilder"/> that can be used to further customize the endpoint.</returns>
public static MapActionEndpointConventionBuilder MapPost(
this IEndpointRouteBuilder endpoints,
string pattern,
Delegate action)
{
return MapMethods(endpoints, pattern, PostVerb, action);
}
/// <summary>
/// Adds a <see cref="RouteEndpoint"/> to the <see cref="IEndpointRouteBuilder"/> that matches HTTP PUT requests
/// for the specified pattern.
/// </summary>
/// <param name="endpoints">The <see cref="IEndpointRouteBuilder"/> to add the route to.</param>
/// <param name="pattern">The route pattern.</param>
/// <param name="action">The delegate executed when the endpoint is matched.</param>
/// <returns>A <see cref="IEndpointConventionBuilder"/> that canaction be used to further customize the endpoint.</returns>
public static MapActionEndpointConventionBuilder MapPut(
this IEndpointRouteBuilder endpoints,
string pattern,
Delegate action)
{
return MapMethods(endpoints, pattern, PutVerb, action);
}
/// <summary>
/// Adds a <see cref="RouteEndpoint"/> to the <see cref="IEndpointRouteBuilder"/> that matches HTTP DELETE requests
/// for the specified pattern.
/// </summary>
/// <param name="endpoints">The <see cref="IEndpointRouteBuilder"/> to add the route to.</param>
/// <param name="pattern">The route pattern.</param>
/// <param name="action">The delegate executed when the endpoint is matched.</param>
/// <returns>A <see cref="IEndpointConventionBuilder"/> that can be used to further customize the endpoint.</returns>
public static MapActionEndpointConventionBuilder MapDelete(
this IEndpointRouteBuilder endpoints,
string pattern,
Delegate action)
{
return MapMethods(endpoints, pattern, DeleteVerb, action);
}
/// <summary>
/// Adds a <see cref="RouteEndpoint"/> to the <see cref="IEndpointRouteBuilder"/> that matches HTTP requests
/// for the specified HTTP methods and pattern.
/// </summary>
/// <param name="endpoints">The <see cref="IEndpointRouteBuilder"/> to add the route to.</param>
/// <param name="pattern">The route pattern.</param>
/// <param name="action">The delegate executed when the endpoint is matched.</param>
/// <param name="httpMethods">HTTP methods that the endpoint will match.</param>
/// <returns>A <see cref="IEndpointConventionBuilder"/> that can be used to further customize the endpoint.</returns>
public static MapActionEndpointConventionBuilder MapMethods(
this IEndpointRouteBuilder endpoints,
string pattern,
IEnumerable<string> httpMethods,
Delegate action)
{
if (httpMethods is null)
{
throw new ArgumentNullException(nameof(httpMethods));
}
var displayName = $"{pattern} HTTP: {string.Join(", ", httpMethods)}";
var builder = endpoints.Map(RoutePatternFactory.Parse(pattern), action, displayName);
builder.WithMetadata(new HttpMethodMetadata(httpMethods));
return builder;
}
/// <summary>
/// Adds a <see cref="RouteEndpoint"/> to the <see cref="IEndpointRouteBuilder"/> that matches HTTP requests
/// for the specified pattern.
/// </summary>
/// <param name="endpoints">The <see cref="IEndpointRouteBuilder"/> to add the route to.</param>
/// <param name="pattern">The route pattern.</param>
/// <param name="action">The delegate executed when the endpoint is matched.</param>
/// <returns>A <see cref="IEndpointConventionBuilder"/> that can be used to further customize the endpoint.</returns>
public static MapActionEndpointConventionBuilder Map(
this IEndpointRouteBuilder endpoints,
string pattern,
Delegate action)
{
return Map(endpoints, RoutePatternFactory.Parse(pattern), action);
}
/// <summary>
/// Adds a <see cref="RouteEndpoint"/> to the <see cref="IEndpointRouteBuilder"/> that matches HTTP requests
/// for the specified pattern.
/// </summary>
/// <param name="endpoints">The <see cref="IEndpointRouteBuilder"/> to add the route to.</param>
/// <param name="pattern">The route pattern.</param>
/// <param name="action">The delegate executed when the endpoint is matched.</param>
/// <returns>A <see cref="IEndpointConventionBuilder"/> that can be used to further customize the endpoint.</returns>
public static MapActionEndpointConventionBuilder Map(
this IEndpointRouteBuilder endpoints,
RoutePattern pattern,
Delegate action)
{
return Map(endpoints, pattern, action, displayName: null);
}
private static MapActionEndpointConventionBuilder Map(
this IEndpointRouteBuilder endpoints,
RoutePattern pattern,
Delegate action,
string? displayName)
{
if (endpoints is null)
{
throw new ArgumentNullException(nameof(endpoints));
}
if (pattern is null)
{
throw new ArgumentNullException(nameof(pattern));
}
if (action is null)
{
throw new ArgumentNullException(nameof(action));
}
const int defaultOrder = 0;
var builder = new RouteEndpointBuilder(
MapActionExpressionTreeBuilder.BuildRequestDelegate(action),
pattern,
defaultOrder)
{
DisplayName = pattern.RawText ?? pattern.DebuggerToString(),
};
// Add delegate attributes as metadata
var attributes = action.Method.GetCustomAttributes();
string? routeName = null;
int? routeOrder = null;
// This can be null if the delegate is a dynamic method or compiled from an expression tree
if (attributes is not null)
{
foreach (var attribute in attributes)
{
if (attribute is IRoutePatternMetadata patternMetadata && patternMetadata.RoutePattern is not null)
{
throw new InvalidOperationException($"'{attribute.GetType()}' implements {nameof(IRoutePatternMetadata)} which is not supported by this method.");
}
if (attribute is IHttpMethodMetadata methodMetadata && methodMetadata.HttpMethods.Any())
{
throw new InvalidOperationException($"'{attribute.GetType()}' implements {nameof(IHttpMethodMetadata)} which is not supported by this method.");
}
if (attribute is IRouteNameMetadata nameMetadata && nameMetadata.RouteName is string name)
{
routeName = name;
}
if (attribute is IRouteOrderMetadata orderMetadata && orderMetadata.RouteOrder is int order)
{
routeOrder = order;
}
builder.Metadata.Add(attribute);
}
}
builder.DisplayName = routeName ?? displayName ?? builder.DisplayName;
builder.Order = routeOrder ?? defaultOrder;
var dataSource = endpoints.DataSources.OfType<ModelEndpointDataSource>().FirstOrDefault();
if (dataSource is null)
{
dataSource = new ModelEndpointDataSource();
endpoints.DataSources.Add(dataSource);
}
return new MapActionEndpointConventionBuilder(dataSource.AddEndpointBuilder(builder));
}
}
}
......@@ -18,4 +18,11 @@ Microsoft.AspNetCore.Routing.IRoutePatternMetadata
Microsoft.AspNetCore.Routing.IRoutePatternMetadata.RoutePattern.get -> string?
Microsoft.AspNetCore.Routing.RouteNameMetadata.RouteName.get -> string?
Microsoft.AspNetCore.Routing.RouteNameMetadata.RouteNameMetadata(string? routeName) -> void
static Microsoft.AspNetCore.Builder.MapActionEndpointRouteBuilderExtensions.Map(this Microsoft.AspNetCore.Routing.IEndpointRouteBuilder! endpoints, Microsoft.AspNetCore.Routing.Patterns.RoutePattern! pattern, System.Delegate! action) -> Microsoft.AspNetCore.Builder.MapActionEndpointConventionBuilder!
static Microsoft.AspNetCore.Builder.MapActionEndpointRouteBuilderExtensions.Map(this Microsoft.AspNetCore.Routing.IEndpointRouteBuilder! endpoints, string! pattern, System.Delegate! action) -> Microsoft.AspNetCore.Builder.MapActionEndpointConventionBuilder!
static Microsoft.AspNetCore.Builder.MapActionEndpointRouteBuilderExtensions.MapAction(this Microsoft.AspNetCore.Routing.IEndpointRouteBuilder! endpoints, System.Delegate! action) -> Microsoft.AspNetCore.Builder.MapActionEndpointConventionBuilder!
static Microsoft.AspNetCore.Builder.MapActionEndpointRouteBuilderExtensions.MapDelete(this Microsoft.AspNetCore.Routing.IEndpointRouteBuilder! endpoints, string! pattern, System.Delegate! action) -> Microsoft.AspNetCore.Builder.MapActionEndpointConventionBuilder!
static Microsoft.AspNetCore.Builder.MapActionEndpointRouteBuilderExtensions.MapGet(this Microsoft.AspNetCore.Routing.IEndpointRouteBuilder! endpoints, string! pattern, System.Delegate! action) -> Microsoft.AspNetCore.Builder.MapActionEndpointConventionBuilder!
static Microsoft.AspNetCore.Builder.MapActionEndpointRouteBuilderExtensions.MapMethods(this Microsoft.AspNetCore.Routing.IEndpointRouteBuilder! endpoints, string! pattern, System.Collections.Generic.IEnumerable<string!>! httpMethods, System.Delegate! action) -> Microsoft.AspNetCore.Builder.MapActionEndpointConventionBuilder!
static Microsoft.AspNetCore.Builder.MapActionEndpointRouteBuilderExtensions.MapPost(this Microsoft.AspNetCore.Routing.IEndpointRouteBuilder! endpoints, string! pattern, System.Delegate! action) -> Microsoft.AspNetCore.Builder.MapActionEndpointConventionBuilder!
static Microsoft.AspNetCore.Builder.MapActionEndpointRouteBuilderExtensions.MapPut(this Microsoft.AspNetCore.Routing.IEndpointRouteBuilder! endpoints, string! pattern, System.Delegate! action) -> Microsoft.AspNetCore.Builder.MapActionEndpointConventionBuilder!
......@@ -61,6 +61,47 @@ namespace Microsoft.AspNetCore.Routing.FunctionalTests
Assert.Equal(42, echoedTodo?.Id);
}
[Fact]
public async Task MapPost_FromBodyWorksWithJsonPayload()
{
Todo EchoTodo([FromRoute] int id, [FromBody] Todo todo) => todo with { Id = id };
using var host = new HostBuilder()
.ConfigureWebHost(webHostBuilder =>
{
webHostBuilder
.Configure(app =>
{
app.UseRouting();
app.UseEndpoints(b => b.MapPost("/EchoTodo/{id}", (Func<int, Todo, Todo>)EchoTodo));
})
.UseTestServer();
})
.ConfigureServices(services =>
{
services.AddRouting();
})
.Build();
using var server = host.GetTestServer();
await host.StartAsync();
var client = server.CreateClient();
var todo = new Todo
{
Name = "Write tests!"
};
var response = await client.PostAsJsonAsync("/EchoTodo/42", todo);
response.EnsureSuccessStatusCode();
var echoedTodo = await response.Content.ReadFromJsonAsync<Todo>();
Assert.NotNull(echoedTodo);
Assert.Equal(todo.Name, echoedTodo?.Name);
Assert.Equal(42, echoedTodo?.Id);
}
private record Todo
{
public int Id { get; set; }
......
......@@ -4,8 +4,10 @@
#nullable enable
using System;
using System.Collections.Generic;
using System.Linq;
using Microsoft.AspNetCore.Routing;
using Microsoft.AspNetCore.Routing.Patterns;
using Microsoft.AspNetCore.Routing.TestObjects;
using Moq;
using Xunit;
......@@ -53,7 +55,8 @@ namespace Microsoft.AspNetCore.Builder
const string customName = "Custom Name";
const int customOrder = 1337;
[CustomRouteMetadata(Name = customName, Order = customOrder)]
// This is tested separately because MapAction requires a Pattern and the other overloads forbit it.
[CustomRouteMetadata(Pattern = "/", Name = customName, Order = customOrder)]
void TestAction() { };
var builder = new DefaultEndpointRouteBuilder(Mock.Of<IApplicationBuilder>());
......@@ -67,5 +70,154 @@ namespace Microsoft.AspNetCore.Builder
Assert.Equal(customName, routeEndpointBuilder.DisplayName);
Assert.Equal(customOrder, routeEndpointBuilder.Order);
}
[Theory]
[MemberData(nameof(MapActionMethods))]
public void MapOverloads_BuildsEndpointWithRouteNameAndOrder(Action<IEndpointRouteBuilder, Delegate> mapOverload)
{
const string customName = "Custom Name";
const int customOrder = 1337;
[CustomRouteMetadata(Name = customName, Order = customOrder)]
void TestAction() { };
var builder = new DefaultEndpointRouteBuilder(Mock.Of<IApplicationBuilder>());
mapOverload(builder, (Action)TestAction);
var dataSource = GetBuilderEndpointDataSource(builder);
// Trigger Endpoint build by calling getter.
var endpoint = Assert.Single(dataSource.Endpoints);
var routeEndpointBuilder = GetRouteEndpointBuilder(builder);
Assert.Equal(customName, routeEndpointBuilder.DisplayName);
Assert.Equal(customOrder, routeEndpointBuilder.Order);
}
[Fact]
public void MapGet_BuildsEndpointWithRouteNameAndOrder()
{
var builder = new DefaultEndpointRouteBuilder(Mock.Of<IApplicationBuilder>());
_ = builder.MapGet("/", (Action)(() => { }));
var dataSource = GetBuilderEndpointDataSource(builder);
// Trigger Endpoint build by calling getter.
var endpoint = Assert.Single(dataSource.Endpoints);
var methodMetadata = endpoint.Metadata.GetMetadata<IHttpMethodMetadata>();
Assert.NotNull(methodMetadata);
var method = Assert.Single(methodMetadata!.HttpMethods);
Assert.Equal("GET", method);
}
[Fact]
public void MapPost_BuildsEndpointWithRouteNameAndOrder()
{
var builder = new DefaultEndpointRouteBuilder(Mock.Of<IApplicationBuilder>());
_ = builder.MapPost("/", (Action)(() => { }));
var dataSource = GetBuilderEndpointDataSource(builder);
// Trigger Endpoint build by calling getter.
var endpoint = Assert.Single(dataSource.Endpoints);
var methodMetadata = endpoint.Metadata.GetMetadata<IHttpMethodMetadata>();
Assert.NotNull(methodMetadata);
var method = Assert.Single(methodMetadata!.HttpMethods);
Assert.Equal("POST", method);
}
[Fact]
public void MapPut_BuildsEndpointWithRouteNameAndOrder()
{
var builder = new DefaultEndpointRouteBuilder(Mock.Of<IApplicationBuilder>());
_ = builder.MapPut("/", (Action)(() => { }));
var dataSource = GetBuilderEndpointDataSource(builder);
// Trigger Endpoint build by calling getter.
var endpoint = Assert.Single(dataSource.Endpoints);
var methodMetadata = endpoint.Metadata.GetMetadata<IHttpMethodMetadata>();
Assert.NotNull(methodMetadata);
var method = Assert.Single(methodMetadata!.HttpMethods);
Assert.Equal("PUT", method);
}
[Fact]
public void MapDelete_BuildsEndpointWithRouteNameAndOrder()
{
var builder = new DefaultEndpointRouteBuilder(Mock.Of<IApplicationBuilder>());
_ = builder.MapDelete("/", (Action)(() => { }));
var dataSource = GetBuilderEndpointDataSource(builder);
// Trigger Endpoint build by calling getter.
var endpoint = Assert.Single(dataSource.Endpoints);
var methodMetadata = endpoint.Metadata.GetMetadata<IHttpMethodMetadata>();
Assert.NotNull(methodMetadata);
var method = Assert.Single(methodMetadata!.HttpMethods);
Assert.Equal("DELETE", method);
}
[Theory]
[MemberData(nameof(MapActionMethods))]
public void MapOverloads_RejectActionsWithPatternMetadata(Action<IEndpointRouteBuilder, Delegate> mapOverload)
{
[CustomRouteMetadata(Pattern = "/")]
void TestAction() { };
var builder = new DefaultEndpointRouteBuilder(Mock.Of<IApplicationBuilder>());
var ex = Assert.Throws<InvalidOperationException>(() => mapOverload(builder, (Action)TestAction));
Assert.Contains(nameof(IRoutePatternMetadata), ex.Message);
}
[Theory]
[MemberData(nameof(MapActionMethods))]
public void MapOverloads_RejectActionsWithMethodMetadata(Action<IEndpointRouteBuilder, Delegate> mapOverload)
{
[CustomRouteMetadata(Methods = new[] { "GET" })]
void TestAction() { };
var builder = new DefaultEndpointRouteBuilder(Mock.Of<IApplicationBuilder>());
var ex = Assert.Throws<InvalidOperationException>(() => mapOverload(builder, (Action)TestAction));
Assert.Contains(nameof(IHttpMethodMetadata), ex.Message);
}
public static IEnumerable<object[]> MapActionMethods => new object[][]
{
new object[]
{
(Action<IEndpointRouteBuilder, Delegate>)(
(builder, action) => builder.MapGet("/", action))
},
new object[]
{
(Action<IEndpointRouteBuilder, Delegate>)(
(builder, action) => builder.MapPost("/", action))
},
new object[]
{
(Action<IEndpointRouteBuilder, Delegate>)(
(builder, action) => builder.MapPut("/", action))
},
new object[]
{
(Action<IEndpointRouteBuilder, Delegate>)(
(builder, action) => builder.MapDelete("/", action))
},
new object[]
{
(Action<IEndpointRouteBuilder, Delegate>)(
(builder, action) => builder.MapMethods("/", Array.Empty<string>(), action))
},
new object[]
{
(Action<IEndpointRouteBuilder, Delegate>)(
(builder, action) => builder.Map("/", action))
},
new object[]
{
(Action<IEndpointRouteBuilder, Delegate>)(
(builder, action) => builder.Map(RoutePatternFactory.Parse("/"), action))
},
};
}
}
......@@ -24,7 +24,7 @@ namespace Microsoft.AspNetCore.Routing.Internal
{
public class MapActionExpressionTreeBuilderTest
{
public static IEnumerable<object[]> NoResult
public static IEnumerable<object[]> NoResult
{
get
{
......@@ -113,8 +113,6 @@ namespace Microsoft.AspNetCore.Routing.Internal
return ValueTask.CompletedTask;
}
return new List<object[]>
{
new object[] { (Action<HttpContext, int>)TestAction },
......
......@@ -10,13 +10,13 @@ namespace Microsoft.AspNetCore.Routing.TestObjects
{
internal class CustomRouteMetadataAttribute : Attribute, IRoutePatternMetadata, IHttpMethodMetadata, IRouteNameMetadata, IRouteOrderMetadata
{
public string Pattern { get; set; } = "/";
public string? Pattern { get; set; }
public string? Name { get; set; }
public int Order { get; set; } = 0;
public string[] Methods { get; set; } = new[] { "GET" };
public string[] Methods { get; set; } = Array.Empty<string>();
string? IRoutePatternMetadata.RoutePattern => Pattern;
......
0% 加载中 .
You are about to add 0 people to the discussion. Proceed with caution.
先完成此消息的编辑!
想要评论请 注册