Skip to content

Commit fe7dc75

Browse files
committed
Added support for razor pages, added tests and functional tests to validate the implementation
1 parent f20a7a6 commit fe7dc75

File tree

8 files changed

+469
-41
lines changed

8 files changed

+469
-41
lines changed

src/Mvc/Mvc.Core/test/Routing/DynamicControllerEndpointMatcherPolicyTest.cs

Lines changed: 172 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -58,7 +58,7 @@ public DynamicControllerEndpointMatcherPolicyTest()
5858
_ => Task.CompletedTask,
5959
new EndpointMetadataCollection(new object[]
6060
{
61-
new DynamicControllerRouteValueTransformerMetadata(typeof(CustomTransformer), null),
61+
new DynamicControllerRouteValueTransformerMetadata(typeof(CustomTransformer), State),
6262
}),
6363
"dynamic");
6464

@@ -71,7 +71,8 @@ public DynamicControllerEndpointMatcherPolicyTest()
7171
services.AddScoped<CustomTransformer>(s =>
7272
{
7373
var transformer = new CustomTransformer();
74-
transformer.Transform = (c, values) => Transform(c, values);
74+
transformer.Transform = (c, values, state) => Transform(c, values, state);
75+
transformer.Filter = (c, values, state, candidates) => Filter(c, values, state, candidates);
7576
return transformer;
7677
});
7778
Services = services.BuildServiceProvider();
@@ -91,7 +92,11 @@ public DynamicControllerEndpointMatcherPolicyTest()
9192

9293
private IServiceProvider Services { get; }
9394

94-
private Func<HttpContext, RouteValueDictionary, ValueTask<RouteValueDictionary>> Transform { get; set; }
95+
private Func<HttpContext, RouteValueDictionary, object, ValueTask<RouteValueDictionary>> Transform { get; set; }
96+
97+
private Func<HttpContext, RouteValueDictionary, object, IReadOnlyList<Endpoint>, ValueTask<IReadOnlyList<Endpoint>>> Filter { get; set; } = (_, __, ___, e) => new ValueTask<IReadOnlyList<Endpoint>>(e);
98+
99+
private object State { get; } = new object();
95100

96101
[Fact]
97102
public async Task ApplyAsync_NoMatch()
@@ -106,7 +111,7 @@ public async Task ApplyAsync_NoMatch()
106111
var candidates = new CandidateSet(endpoints, values, scores);
107112
candidates.SetValidity(0, false);
108113

109-
Transform = (c, values) =>
114+
Transform = (c, values, state) =>
110115
{
111116
throw new InvalidOperationException();
112117
};
@@ -135,7 +140,7 @@ public async Task ApplyAsync_HasMatchNoEndpointFound()
135140

136141
var candidates = new CandidateSet(endpoints, values, scores);
137142

138-
Transform = (c, values) =>
143+
Transform = (c, values, state) =>
139144
{
140145
return new ValueTask<RouteValueDictionary>(new RouteValueDictionary());
141146
};
@@ -166,7 +171,7 @@ public async Task ApplyAsync_HasMatchFindsEndpoint_WithoutRouteValues()
166171

167172
var candidates = new CandidateSet(endpoints, values, scores);
168173

169-
Transform = (c, values) =>
174+
Transform = (c, values, state) =>
170175
{
171176
return new ValueTask<RouteValueDictionary>(new RouteValueDictionary(new
172177
{
@@ -212,12 +217,13 @@ public async Task ApplyAsync_HasMatchFindsEndpoint_WithRouteValues()
212217

213218
var candidates = new CandidateSet(endpoints, values, scores);
214219

215-
Transform = (c, values) =>
220+
Transform = (c, values, state) =>
216221
{
217222
return new ValueTask<RouteValueDictionary>(new RouteValueDictionary(new
218223
{
219224
controller = "Home",
220225
action = "Index",
226+
state
221227
}));
222228
};
223229

@@ -242,13 +248,162 @@ public async Task ApplyAsync_HasMatchFindsEndpoint_WithRouteValues()
242248
{
243249
Assert.Equal("controller", kvp.Key);
244250
Assert.Equal("Home", kvp.Value);
245-
},
251+
},
252+
kvp =>
253+
{
254+
Assert.Equal("slug", kvp.Key);
255+
Assert.Equal("test", kvp.Value);
256+
},
257+
kvp =>
258+
{
259+
Assert.Equal("state", kvp.Key);
260+
Assert.Same(State, kvp.Value);
261+
});
262+
Assert.True(candidates.IsValidCandidate(0));
263+
}
264+
265+
[Fact]
266+
public async Task ApplyAsync_CanDiscardFoundEndpoints()
267+
{
268+
// Arrange
269+
var policy = new DynamicControllerEndpointMatcherPolicy(Selector, Comparer);
270+
271+
var endpoints = new[] { DynamicEndpoint, };
272+
var values = new RouteValueDictionary[] { new RouteValueDictionary(new { slug = "test", }), };
273+
var scores = new[] { 0, };
274+
275+
var candidates = new CandidateSet(endpoints, values, scores);
276+
277+
Transform = (c, values, state) =>
278+
{
279+
return new ValueTask<RouteValueDictionary>(new RouteValueDictionary(new
280+
{
281+
controller = "Home",
282+
action = "Index",
283+
state
284+
}));
285+
};
286+
287+
Filter = (c, values, state, endpoints) =>
288+
{
289+
return new ValueTask<IReadOnlyList<Endpoint>>(Array.Empty<Endpoint>());
290+
};
291+
292+
var httpContext = new DefaultHttpContext()
293+
{
294+
RequestServices = Services,
295+
};
296+
297+
// Act
298+
await policy.ApplyAsync(httpContext, candidates);
299+
300+
// Assert
301+
Assert.False(candidates.IsValidCandidate(0));
302+
}
303+
304+
[Fact]
305+
public async Task ApplyAsync_CanReplaceFoundEndpoints()
306+
{
307+
// Arrange
308+
var policy = new DynamicControllerEndpointMatcherPolicy(Selector, Comparer);
309+
310+
var endpoints = new[] { DynamicEndpoint, };
311+
var values = new RouteValueDictionary[] { new RouteValueDictionary(new { slug = "test", }), };
312+
var scores = new[] { 0, };
313+
314+
var candidates = new CandidateSet(endpoints, values, scores);
315+
316+
Transform = (c, values, state) =>
317+
{
318+
return new ValueTask<RouteValueDictionary>(new RouteValueDictionary(new
319+
{
320+
controller = "Home",
321+
action = "Index",
322+
state
323+
}));
324+
};
325+
326+
Filter = (c, values, state, endpoints) => new ValueTask<IReadOnlyList<Endpoint>>(new[]
327+
{
328+
new Endpoint((ctx) => Task.CompletedTask, new EndpointMetadataCollection(Array.Empty<object>()), "ReplacedEndpoint")
329+
});
330+
331+
var httpContext = new DefaultHttpContext()
332+
{
333+
RequestServices = Services,
334+
};
335+
336+
// Act
337+
await policy.ApplyAsync(httpContext, candidates);
338+
339+
// Assert
340+
Assert.Collection(
341+
candidates[0].Values.OrderBy(kvp => kvp.Key),
342+
kvp =>
343+
{
344+
Assert.Equal("action", kvp.Key);
345+
Assert.Equal("Index", kvp.Value);
346+
},
347+
kvp =>
348+
{
349+
Assert.Equal("controller", kvp.Key);
350+
Assert.Equal("Home", kvp.Value);
351+
},
246352
kvp =>
247353
{
248354
Assert.Equal("slug", kvp.Key);
249355
Assert.Equal("test", kvp.Value);
356+
},
357+
kvp =>
358+
{
359+
Assert.Equal("state", kvp.Key);
360+
Assert.Same(State, kvp.Value);
250361
});
362+
Assert.Equal("ReplacedEndpoint", candidates[0].Endpoint.DisplayName);
363+
Assert.True(candidates.IsValidCandidate(0));
364+
}
365+
366+
[Fact]
367+
public async Task ApplyAsync_CanExpandTheListOfFoundEndpoints()
368+
{
369+
// Arrange
370+
var policy = new DynamicControllerEndpointMatcherPolicy(Selector, Comparer);
371+
372+
var endpoints = new[] { DynamicEndpoint, };
373+
var values = new RouteValueDictionary[] { new RouteValueDictionary(new { slug = "test", }), };
374+
var scores = new[] { 0, };
375+
376+
var candidates = new CandidateSet(endpoints, values, scores);
377+
378+
Transform = (c, values, state) =>
379+
{
380+
return new ValueTask<RouteValueDictionary>(new RouteValueDictionary(new
381+
{
382+
controller = "Home",
383+
action = "Index",
384+
state
385+
}));
386+
};
387+
388+
Filter = (c, values, state, endpoints) => new ValueTask<IReadOnlyList<Endpoint>>(new[]
389+
{
390+
ControllerEndpoints[1], ControllerEndpoints[2]
391+
});
392+
393+
var httpContext = new DefaultHttpContext()
394+
{
395+
RequestServices = Services,
396+
};
397+
398+
// Act
399+
await policy.ApplyAsync(httpContext, candidates);
400+
401+
// Assert
402+
Assert.Equal(2, candidates.Count);
251403
Assert.True(candidates.IsValidCandidate(0));
404+
Assert.True(candidates.IsValidCandidate(1));
405+
Assert.Same(ControllerEndpoints[1], candidates[0].Endpoint);
406+
Assert.Same(ControllerEndpoints[2], candidates[1].Endpoint);
252407
}
253408

254409
private class TestDynamicControllerEndpointSelector : DynamicControllerEndpointSelector
@@ -261,11 +416,18 @@ public TestDynamicControllerEndpointSelector(EndpointDataSource dataSource)
261416

262417
private class CustomTransformer : DynamicRouteValueTransformer
263418
{
264-
public Func<HttpContext, RouteValueDictionary, ValueTask<RouteValueDictionary>> Transform { get; set; }
419+
public Func<HttpContext, RouteValueDictionary, object, ValueTask<RouteValueDictionary>> Transform { get; set; }
420+
421+
public Func<HttpContext, RouteValueDictionary, object, IReadOnlyList<Endpoint>, ValueTask<IReadOnlyList<Endpoint>>> Filter { get; set; }
265422

266423
public override ValueTask<RouteValueDictionary> TransformAsync(HttpContext httpContext, RouteValueDictionary values)
267424
{
268-
return Transform(httpContext, values);
425+
return Transform(httpContext, values, State);
426+
}
427+
428+
public override ValueTask<IReadOnlyList<Endpoint>> FilterAsync(HttpContext httpContext, RouteValueDictionary values, IReadOnlyList<Endpoint> endpoints)
429+
{
430+
return Filter(httpContext, values, State, endpoints);
269431
}
270432
}
271433
}

src/Mvc/Mvc.RazorPages/src/Builder/RazorPagesEndpointRouteBuilderExtensions.cs

Lines changed: 27 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
// Copyright (c) .NET Foundation. All rights reserved.
1+
// Copyright (c) .NET Foundation. All rights reserved.
22
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
33

44
using System;
@@ -299,6 +299,30 @@ public static IEndpointConventionBuilder MapFallbackToAreaPage(
299299
/// </remarks>
300300
public static void MapDynamicPageRoute<TTransformer>(this IEndpointRouteBuilder endpoints, string pattern)
301301
where TTransformer : DynamicRouteValueTransformer
302+
{
303+
MapDynamicPageRoute<TTransformer>(endpoints, pattern, state: null);
304+
}
305+
306+
/// <summary>
307+
/// Adds a specialized <see cref="RouteEndpoint"/> to the <see cref="IEndpointRouteBuilder"/> that will
308+
/// attempt to select a page using the route values produced by <typeparamref name="TTransformer"/>.
309+
/// </summary>
310+
/// <param name="endpoints">The <see cref="IEndpointRouteBuilder"/> to add the route to.</param>
311+
/// <param name="pattern">The URL pattern of the route.</param>
312+
/// <param name="state">A state object to provide to the <typeparamref name="TTransformer" /> instance.</param>
313+
/// <typeparam name="TTransformer">The type of a <see cref="DynamicRouteValueTransformer"/>.</typeparam>
314+
/// <remarks>
315+
/// <para>
316+
/// This method allows the registration of a <see cref="RouteEndpoint"/> and <see cref="DynamicRouteValueTransformer"/>
317+
/// that combine to dynamically select a page using custom logic.
318+
/// </para>
319+
/// <para>
320+
/// The instance of <typeparamref name="TTransformer"/> will be retrieved from the dependency injection container.
321+
/// Register <typeparamref name="TTransformer"/> with the desired service lifetime in <c>ConfigureServices</c>.
322+
/// </para>
323+
/// </remarks>
324+
public static void MapDynamicPageRoute<TTransformer>(this IEndpointRouteBuilder endpoints, string pattern, object state)
325+
where TTransformer : DynamicRouteValueTransformer
302326
{
303327
if (endpoints == null)
304328
{
@@ -316,14 +340,14 @@ public static void MapDynamicPageRoute<TTransformer>(this IEndpointRouteBuilder
316340
GetOrCreateDataSource(endpoints).CreateInertEndpoints = true;
317341

318342
endpoints.Map(
319-
pattern,
343+
pattern,
320344
context =>
321345
{
322346
throw new InvalidOperationException("This endpoint is not expected to be executed directly.");
323347
})
324348
.Add(b =>
325349
{
326-
b.Metadata.Add(new DynamicPageRouteValueTransformerMetadata(typeof(TTransformer)));
350+
b.Metadata.Add(new DynamicPageRouteValueTransformerMetadata(typeof(TTransformer), state));
327351
});
328352
}
329353

src/Mvc/Mvc.RazorPages/src/Infrastructure/DynamicPageEndpointMatcherPolicy.cs

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
// Copyright (c) .NET Foundation. All rights reserved.
1+
// Copyright (c) .NET Foundation. All rights reserved.
22
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
33

44
using System;
@@ -105,13 +105,15 @@ public async Task ApplyAsync(HttpContext httpContext, CandidateSet candidates)
105105
// no realistic way this could happen.
106106
var dynamicPageMetadata = endpoint.Metadata.GetMetadata<DynamicPageMetadata>();
107107
var transformerMetadata = endpoint.Metadata.GetMetadata<DynamicPageRouteValueTransformerMetadata>();
108+
DynamicRouteValueTransformer transformer = null;
108109
if (dynamicPageMetadata != null)
109110
{
110111
dynamicValues = dynamicPageMetadata.Values;
111112
}
112113
else if (transformerMetadata != null)
113114
{
114-
var transformer = (DynamicRouteValueTransformer)httpContext.RequestServices.GetRequiredService(transformerMetadata.SelectorType);
115+
transformer = (DynamicRouteValueTransformer)httpContext.RequestServices.GetRequiredService(transformerMetadata.SelectorType);
116+
transformer.State = transformerMetadata.State;
115117
dynamicValues = await transformer.TransformAsync(httpContext, originalValues);
116118
}
117119
else
@@ -154,6 +156,16 @@ public async Task ApplyAsync(HttpContext httpContext, CandidateSet candidates)
154156
}
155157
}
156158

159+
if (transformer != null)
160+
{
161+
endpoints = await transformer.FilterAsync(httpContext, values, endpoints);
162+
if (endpoints.Count == 0)
163+
{
164+
candidates.ReplaceEndpoint(i, null, null);
165+
continue;
166+
}
167+
}
168+
157169
// Update the route values
158170
candidates.ReplaceEndpoint(i, endpoint, values);
159171

src/Mvc/Mvc.RazorPages/src/Infrastructure/DynamicPageRouteValueTransformerMetadata.cs

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
// Copyright (c) .NET Foundation. All rights reserved.
1+
// Copyright (c) .NET Foundation. All rights reserved.
22
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
33

44
using System;
@@ -9,7 +9,7 @@ namespace Microsoft.AspNetCore.Mvc.RazorPages.Infrastructure
99
{
1010
internal class DynamicPageRouteValueTransformerMetadata : IDynamicEndpointMetadata
1111
{
12-
public DynamicPageRouteValueTransformerMetadata(Type selectorType)
12+
public DynamicPageRouteValueTransformerMetadata(Type selectorType, object state)
1313
{
1414
if (selectorType == null)
1515
{
@@ -24,10 +24,13 @@ public DynamicPageRouteValueTransformerMetadata(Type selectorType)
2424
}
2525

2626
SelectorType = selectorType;
27+
State = state;
2728
}
2829

2930
public bool IsDynamic => true;
3031

32+
public object State { get; }
33+
3134
public Type SelectorType { get; }
3235
}
3336
}

0 commit comments

Comments
 (0)