Skip to content

Commit 5207cad

Browse files
authored
Add new "MapAction" overloads (#30556)
* Add new "MapAction" overloads * Create RouteEndpointBuilder directly * fix typo: my -> by * is { } -> is not null
1 parent 5bf4272 commit 5207cad

7 files changed

+414
-6
lines changed

src/Http/Routing/src/Builder/MapActionEndpointConventionBuilder.cs

+5
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,11 @@ public sealed class MapActionEndpointConventionBuilder : IEndpointConventionBuil
1313
{
1414
private readonly List<IEndpointConventionBuilder> _endpointConventionBuilders;
1515

16+
internal MapActionEndpointConventionBuilder(IEndpointConventionBuilder endpointConventionBuilder)
17+
{
18+
_endpointConventionBuilders = new List<IEndpointConventionBuilder>() { endpointConventionBuilder };
19+
}
20+
1621
internal MapActionEndpointConventionBuilder(List<IEndpointConventionBuilder> endpointConventionBuilders)
1722
{
1823
_endpointConventionBuilders = endpointConventionBuilders;

src/Http/Routing/src/Builder/MapActionEndpointRouteBuilderExtensions.cs

+205
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
using System.Reflection;
88
using Microsoft.AspNetCore.Routing;
99
using Microsoft.AspNetCore.Routing.Internal;
10+
using Microsoft.AspNetCore.Routing.Patterns;
1011

1112
namespace Microsoft.AspNetCore.Builder
1213
{
@@ -15,6 +16,12 @@ namespace Microsoft.AspNetCore.Builder
1516
/// </summary>
1617
public static class MapActionEndpointRouteBuilderExtensions
1718
{
19+
// Avoid creating a new array every call
20+
private static readonly string[] GetVerb = new[] { "GET" };
21+
private static readonly string[] PostVerb = new[] { "POST" };
22+
private static readonly string[] PutVerb = new[] { "PUT" };
23+
private static readonly string[] DeleteVerb = new[] { "DELETE" };
24+
1825
/// <summary>
1926
/// Adds a <see cref="RouteEndpoint"/> to the <see cref="IEndpointRouteBuilder"/> that matches the pattern specified via attributes.
2027
/// </summary>
@@ -76,5 +83,203 @@ public static MapActionEndpointConventionBuilder MapAction(
7683

7784
return new MapActionEndpointConventionBuilder(conventionBuilders);
7885
}
86+
87+
/// <summary>
88+
/// Adds a <see cref="RouteEndpoint"/> to the <see cref="IEndpointRouteBuilder"/> that matches HTTP GET requests
89+
/// for the specified pattern.
90+
/// </summary>
91+
/// <param name="endpoints">The <see cref="IEndpointRouteBuilder"/> to add the route to.</param>
92+
/// <param name="pattern">The route pattern.</param>
93+
/// <param name="action">The delegate executed when the endpoint is matched.</param>
94+
/// <returns>A <see cref="IEndpointConventionBuilder"/> that can be used to further customize the endpoint.</returns>
95+
public static MapActionEndpointConventionBuilder MapGet(
96+
this IEndpointRouteBuilder endpoints,
97+
string pattern,
98+
Delegate action)
99+
{
100+
return MapMethods(endpoints, pattern, GetVerb, action);
101+
}
102+
103+
/// <summary>
104+
/// Adds a <see cref="RouteEndpoint"/> to the <see cref="IEndpointRouteBuilder"/> that matches HTTP POST requests
105+
/// for the specified pattern.
106+
/// </summary>
107+
/// <param name="endpoints">The <see cref="IEndpointRouteBuilder"/> to add the route to.</param>
108+
/// <param name="pattern">The route pattern.</param>
109+
/// <param name="action">The delegate executed when the endpoint is matched.</param>
110+
/// <returns>A <see cref="IEndpointConventionBuilder"/> that can be used to further customize the endpoint.</returns>
111+
public static MapActionEndpointConventionBuilder MapPost(
112+
this IEndpointRouteBuilder endpoints,
113+
string pattern,
114+
Delegate action)
115+
{
116+
return MapMethods(endpoints, pattern, PostVerb, action);
117+
}
118+
119+
/// <summary>
120+
/// Adds a <see cref="RouteEndpoint"/> to the <see cref="IEndpointRouteBuilder"/> that matches HTTP PUT requests
121+
/// for the specified pattern.
122+
/// </summary>
123+
/// <param name="endpoints">The <see cref="IEndpointRouteBuilder"/> to add the route to.</param>
124+
/// <param name="pattern">The route pattern.</param>
125+
/// <param name="action">The delegate executed when the endpoint is matched.</param>
126+
/// <returns>A <see cref="IEndpointConventionBuilder"/> that canaction be used to further customize the endpoint.</returns>
127+
public static MapActionEndpointConventionBuilder MapPut(
128+
this IEndpointRouteBuilder endpoints,
129+
string pattern,
130+
Delegate action)
131+
{
132+
return MapMethods(endpoints, pattern, PutVerb, action);
133+
}
134+
135+
/// <summary>
136+
/// Adds a <see cref="RouteEndpoint"/> to the <see cref="IEndpointRouteBuilder"/> that matches HTTP DELETE requests
137+
/// for the specified pattern.
138+
/// </summary>
139+
/// <param name="endpoints">The <see cref="IEndpointRouteBuilder"/> to add the route to.</param>
140+
/// <param name="pattern">The route pattern.</param>
141+
/// <param name="action">The delegate executed when the endpoint is matched.</param>
142+
/// <returns>A <see cref="IEndpointConventionBuilder"/> that can be used to further customize the endpoint.</returns>
143+
public static MapActionEndpointConventionBuilder MapDelete(
144+
this IEndpointRouteBuilder endpoints,
145+
string pattern,
146+
Delegate action)
147+
{
148+
return MapMethods(endpoints, pattern, DeleteVerb, action);
149+
}
150+
151+
/// <summary>
152+
/// Adds a <see cref="RouteEndpoint"/> to the <see cref="IEndpointRouteBuilder"/> that matches HTTP requests
153+
/// for the specified HTTP methods and pattern.
154+
/// </summary>
155+
/// <param name="endpoints">The <see cref="IEndpointRouteBuilder"/> to add the route to.</param>
156+
/// <param name="pattern">The route pattern.</param>
157+
/// <param name="action">The delegate executed when the endpoint is matched.</param>
158+
/// <param name="httpMethods">HTTP methods that the endpoint will match.</param>
159+
/// <returns>A <see cref="IEndpointConventionBuilder"/> that can be used to further customize the endpoint.</returns>
160+
public static MapActionEndpointConventionBuilder MapMethods(
161+
this IEndpointRouteBuilder endpoints,
162+
string pattern,
163+
IEnumerable<string> httpMethods,
164+
Delegate action)
165+
{
166+
if (httpMethods is null)
167+
{
168+
throw new ArgumentNullException(nameof(httpMethods));
169+
}
170+
171+
var displayName = $"{pattern} HTTP: {string.Join(", ", httpMethods)}";
172+
var builder = endpoints.Map(RoutePatternFactory.Parse(pattern), action, displayName);
173+
builder.WithMetadata(new HttpMethodMetadata(httpMethods));
174+
return builder;
175+
}
176+
177+
/// <summary>
178+
/// Adds a <see cref="RouteEndpoint"/> to the <see cref="IEndpointRouteBuilder"/> that matches HTTP requests
179+
/// for the specified pattern.
180+
/// </summary>
181+
/// <param name="endpoints">The <see cref="IEndpointRouteBuilder"/> to add the route to.</param>
182+
/// <param name="pattern">The route pattern.</param>
183+
/// <param name="action">The delegate executed when the endpoint is matched.</param>
184+
/// <returns>A <see cref="IEndpointConventionBuilder"/> that can be used to further customize the endpoint.</returns>
185+
public static MapActionEndpointConventionBuilder Map(
186+
this IEndpointRouteBuilder endpoints,
187+
string pattern,
188+
Delegate action)
189+
{
190+
return Map(endpoints, RoutePatternFactory.Parse(pattern), action);
191+
}
192+
193+
/// <summary>
194+
/// Adds a <see cref="RouteEndpoint"/> to the <see cref="IEndpointRouteBuilder"/> that matches HTTP requests
195+
/// for the specified pattern.
196+
/// </summary>
197+
/// <param name="endpoints">The <see cref="IEndpointRouteBuilder"/> to add the route to.</param>
198+
/// <param name="pattern">The route pattern.</param>
199+
/// <param name="action">The delegate executed when the endpoint is matched.</param>
200+
/// <returns>A <see cref="IEndpointConventionBuilder"/> that can be used to further customize the endpoint.</returns>
201+
public static MapActionEndpointConventionBuilder Map(
202+
this IEndpointRouteBuilder endpoints,
203+
RoutePattern pattern,
204+
Delegate action)
205+
{
206+
return Map(endpoints, pattern, action, displayName: null);
207+
}
208+
209+
private static MapActionEndpointConventionBuilder Map(
210+
this IEndpointRouteBuilder endpoints,
211+
RoutePattern pattern,
212+
Delegate action,
213+
string? displayName)
214+
{
215+
if (endpoints is null)
216+
{
217+
throw new ArgumentNullException(nameof(endpoints));
218+
}
219+
220+
if (pattern is null)
221+
{
222+
throw new ArgumentNullException(nameof(pattern));
223+
}
224+
225+
if (action is null)
226+
{
227+
throw new ArgumentNullException(nameof(action));
228+
}
229+
230+
const int defaultOrder = 0;
231+
232+
var builder = new RouteEndpointBuilder(
233+
MapActionExpressionTreeBuilder.BuildRequestDelegate(action),
234+
pattern,
235+
defaultOrder)
236+
{
237+
DisplayName = pattern.RawText ?? pattern.DebuggerToString(),
238+
};
239+
240+
// Add delegate attributes as metadata
241+
var attributes = action.Method.GetCustomAttributes();
242+
string? routeName = null;
243+
int? routeOrder = null;
244+
245+
// This can be null if the delegate is a dynamic method or compiled from an expression tree
246+
if (attributes is not null)
247+
{
248+
foreach (var attribute in attributes)
249+
{
250+
if (attribute is IRoutePatternMetadata patternMetadata && patternMetadata.RoutePattern is not null)
251+
{
252+
throw new InvalidOperationException($"'{attribute.GetType()}' implements {nameof(IRoutePatternMetadata)} which is not supported by this method.");
253+
}
254+
if (attribute is IHttpMethodMetadata methodMetadata && methodMetadata.HttpMethods.Any())
255+
{
256+
throw new InvalidOperationException($"'{attribute.GetType()}' implements {nameof(IHttpMethodMetadata)} which is not supported by this method.");
257+
}
258+
259+
if (attribute is IRouteNameMetadata nameMetadata && nameMetadata.RouteName is string name)
260+
{
261+
routeName = name;
262+
}
263+
if (attribute is IRouteOrderMetadata orderMetadata && orderMetadata.RouteOrder is int order)
264+
{
265+
routeOrder = order;
266+
}
267+
268+
builder.Metadata.Add(attribute);
269+
}
270+
}
271+
272+
builder.DisplayName = routeName ?? displayName ?? builder.DisplayName;
273+
builder.Order = routeOrder ?? defaultOrder;
274+
275+
var dataSource = endpoints.DataSources.OfType<ModelEndpointDataSource>().FirstOrDefault();
276+
if (dataSource is null)
277+
{
278+
dataSource = new ModelEndpointDataSource();
279+
endpoints.DataSources.Add(dataSource);
280+
}
281+
282+
return new MapActionEndpointConventionBuilder(dataSource.AddEndpointBuilder(builder));
283+
}
79284
}
80285
}

src/Http/Routing/src/PublicAPI.Unshipped.txt

+7
Original file line numberDiff line numberDiff line change
@@ -18,4 +18,11 @@ Microsoft.AspNetCore.Routing.IRoutePatternMetadata
1818
Microsoft.AspNetCore.Routing.IRoutePatternMetadata.RoutePattern.get -> string?
1919
Microsoft.AspNetCore.Routing.RouteNameMetadata.RouteName.get -> string?
2020
Microsoft.AspNetCore.Routing.RouteNameMetadata.RouteNameMetadata(string? routeName) -> void
21+
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!
22+
static Microsoft.AspNetCore.Builder.MapActionEndpointRouteBuilderExtensions.Map(this Microsoft.AspNetCore.Routing.IEndpointRouteBuilder! endpoints, string! pattern, System.Delegate! action) -> Microsoft.AspNetCore.Builder.MapActionEndpointConventionBuilder!
2123
static Microsoft.AspNetCore.Builder.MapActionEndpointRouteBuilderExtensions.MapAction(this Microsoft.AspNetCore.Routing.IEndpointRouteBuilder! endpoints, System.Delegate! action) -> Microsoft.AspNetCore.Builder.MapActionEndpointConventionBuilder!
24+
static Microsoft.AspNetCore.Builder.MapActionEndpointRouteBuilderExtensions.MapDelete(this Microsoft.AspNetCore.Routing.IEndpointRouteBuilder! endpoints, string! pattern, System.Delegate! action) -> Microsoft.AspNetCore.Builder.MapActionEndpointConventionBuilder!
25+
static Microsoft.AspNetCore.Builder.MapActionEndpointRouteBuilderExtensions.MapGet(this Microsoft.AspNetCore.Routing.IEndpointRouteBuilder! endpoints, string! pattern, System.Delegate! action) -> Microsoft.AspNetCore.Builder.MapActionEndpointConventionBuilder!
26+
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!
27+
static Microsoft.AspNetCore.Builder.MapActionEndpointRouteBuilderExtensions.MapPost(this Microsoft.AspNetCore.Routing.IEndpointRouteBuilder! endpoints, string! pattern, System.Delegate! action) -> Microsoft.AspNetCore.Builder.MapActionEndpointConventionBuilder!
28+
static Microsoft.AspNetCore.Builder.MapActionEndpointRouteBuilderExtensions.MapPut(this Microsoft.AspNetCore.Routing.IEndpointRouteBuilder! endpoints, string! pattern, System.Delegate! action) -> Microsoft.AspNetCore.Builder.MapActionEndpointConventionBuilder!

src/Http/Routing/test/FunctionalTests/MapActionTest.cs

+41
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,47 @@ public async Task MapAction_FromBodyWorksWithJsonPayload()
6161
Assert.Equal(42, echoedTodo?.Id);
6262
}
6363

64+
[Fact]
65+
public async Task MapPost_FromBodyWorksWithJsonPayload()
66+
{
67+
Todo EchoTodo([FromRoute] int id, [FromBody] Todo todo) => todo with { Id = id };
68+
69+
using var host = new HostBuilder()
70+
.ConfigureWebHost(webHostBuilder =>
71+
{
72+
webHostBuilder
73+
.Configure(app =>
74+
{
75+
app.UseRouting();
76+
app.UseEndpoints(b => b.MapPost("/EchoTodo/{id}", (Func<int, Todo, Todo>)EchoTodo));
77+
})
78+
.UseTestServer();
79+
})
80+
.ConfigureServices(services =>
81+
{
82+
services.AddRouting();
83+
})
84+
.Build();
85+
86+
using var server = host.GetTestServer();
87+
await host.StartAsync();
88+
var client = server.CreateClient();
89+
90+
var todo = new Todo
91+
{
92+
Name = "Write tests!"
93+
};
94+
95+
var response = await client.PostAsJsonAsync("/EchoTodo/42", todo);
96+
response.EnsureSuccessStatusCode();
97+
98+
var echoedTodo = await response.Content.ReadFromJsonAsync<Todo>();
99+
100+
Assert.NotNull(echoedTodo);
101+
Assert.Equal(todo.Name, echoedTodo?.Name);
102+
Assert.Equal(42, echoedTodo?.Id);
103+
}
104+
64105
private record Todo
65106
{
66107
public int Id { get; set; }

0 commit comments

Comments
 (0)