Add file/non-file and generic fallback
Adds new constraints for checking if a route value is a file or not. Added a new set of builder methods that specify what it means to be a 'fallback'. This is really similar to what the older SPA fallback routes do, but this is lower in the stack and directly integrated with endpoints.
This commit is contained in:
parent
4c79e7fdc0
commit
f150e89125
|
|
@ -25,6 +25,11 @@ namespace Microsoft.AspNetCore.Builder
|
|||
public static Microsoft.AspNetCore.Builder.IApplicationBuilder UseEndpoint(this Microsoft.AspNetCore.Builder.IApplicationBuilder builder) { throw null; }
|
||||
public static Microsoft.AspNetCore.Builder.IApplicationBuilder UseRouting(this Microsoft.AspNetCore.Builder.IApplicationBuilder builder, System.Action<Microsoft.AspNetCore.Routing.IEndpointRouteBuilder> configure) { throw null; }
|
||||
}
|
||||
public static partial class FallbackEndpointRouteBuilderExtensions
|
||||
{
|
||||
public static Microsoft.AspNetCore.Builder.IEndpointConventionBuilder MapFallback(this Microsoft.AspNetCore.Routing.IEndpointRouteBuilder builder, Microsoft.AspNetCore.Http.RequestDelegate requestDelegate) { throw null; }
|
||||
public static Microsoft.AspNetCore.Builder.IEndpointConventionBuilder MapFallback(this Microsoft.AspNetCore.Routing.IEndpointRouteBuilder builder, string pattern, Microsoft.AspNetCore.Http.RequestDelegate requestDelegate) { throw null; }
|
||||
}
|
||||
public static partial class MapRouteRouteBuilderExtensions
|
||||
{
|
||||
public static Microsoft.AspNetCore.Routing.IRouteBuilder MapRoute(this Microsoft.AspNetCore.Routing.IRouteBuilder routeBuilder, string name, string template) { throw null; }
|
||||
|
|
@ -383,6 +388,11 @@ namespace Microsoft.AspNetCore.Routing.Constraints
|
|||
public DoubleRouteConstraint() { }
|
||||
public bool Match(Microsoft.AspNetCore.Http.HttpContext httpContext, Microsoft.AspNetCore.Routing.IRouter route, string routeKey, Microsoft.AspNetCore.Routing.RouteValueDictionary values, Microsoft.AspNetCore.Routing.RouteDirection routeDirection) { throw null; }
|
||||
}
|
||||
public partial class FileNameRouteConstraint : Microsoft.AspNetCore.Routing.IParameterPolicy, Microsoft.AspNetCore.Routing.IRouteConstraint
|
||||
{
|
||||
public FileNameRouteConstraint() { }
|
||||
public bool Match(Microsoft.AspNetCore.Http.HttpContext httpContext, Microsoft.AspNetCore.Routing.IRouter route, string routeKey, Microsoft.AspNetCore.Routing.RouteValueDictionary values, Microsoft.AspNetCore.Routing.RouteDirection routeDirection) { throw null; }
|
||||
}
|
||||
public partial class FloatRouteConstraint : Microsoft.AspNetCore.Routing.IParameterPolicy, Microsoft.AspNetCore.Routing.IRouteConstraint
|
||||
{
|
||||
public FloatRouteConstraint() { }
|
||||
|
|
@ -441,6 +451,11 @@ namespace Microsoft.AspNetCore.Routing.Constraints
|
|||
public long Min { [System.Runtime.CompilerServices.CompilerGeneratedAttribute]get { throw null; } }
|
||||
public bool Match(Microsoft.AspNetCore.Http.HttpContext httpContext, Microsoft.AspNetCore.Routing.IRouter route, string routeKey, Microsoft.AspNetCore.Routing.RouteValueDictionary values, Microsoft.AspNetCore.Routing.RouteDirection routeDirection) { throw null; }
|
||||
}
|
||||
public partial class NonFileNameRouteConstraint : Microsoft.AspNetCore.Routing.IParameterPolicy, Microsoft.AspNetCore.Routing.IRouteConstraint
|
||||
{
|
||||
public NonFileNameRouteConstraint() { }
|
||||
public bool Match(Microsoft.AspNetCore.Http.HttpContext httpContext, Microsoft.AspNetCore.Routing.IRouter route, string routeKey, Microsoft.AspNetCore.Routing.RouteValueDictionary values, Microsoft.AspNetCore.Routing.RouteDirection routeDirection) { throw null; }
|
||||
}
|
||||
public partial class OptionalRouteConstraint : Microsoft.AspNetCore.Routing.IParameterPolicy, Microsoft.AspNetCore.Routing.IRouteConstraint
|
||||
{
|
||||
public OptionalRouteConstraint(Microsoft.AspNetCore.Routing.IRouteConstraint innerConstraint) { }
|
||||
|
|
|
|||
|
|
@ -0,0 +1,96 @@
|
|||
// 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.Http;
|
||||
using Microsoft.AspNetCore.Routing;
|
||||
using Microsoft.AspNetCore.Routing.Patterns;
|
||||
|
||||
namespace Microsoft.AspNetCore.Builder
|
||||
{
|
||||
/// <summary>
|
||||
/// Contains extension methods for <see cref="IEndpointRouteBuilder"/>.
|
||||
/// </summary>
|
||||
public static class FallbackEndpointRouteBuilderExtensions
|
||||
{
|
||||
/// <summary>
|
||||
/// Adds a specialized <see cref="RouteEndpoint"/> to the <see cref="IEndpointRouteBuilder"/> that will match
|
||||
/// requests for non-file-names with the lowest possible priority.
|
||||
/// </summary>
|
||||
/// <param name="builder">The <see cref="IEndpointRouteBuilder"/> to add the route to.</param>
|
||||
/// <param name="requestDelegate">The delegate executed when the endpoint is matched.</param>
|
||||
/// <returns>A <see cref="IEndpointConventionBuilder"/> that can be used to further customize the endpoint.</returns>
|
||||
/// <remarks>
|
||||
/// <para>
|
||||
/// <see cref="MapFallback(IEndpointRouteBuilder, RequestDelegate)"/> is intended to handle cases where URL path of
|
||||
/// the request does not contain a file name, and no other endpoint has matched. This is convenient for routing
|
||||
/// requests for dynamic content to a SPA framework, while also allowing requests for non-existent files to
|
||||
/// result in an HTTP 404.
|
||||
/// </para>
|
||||
/// <para>
|
||||
/// <see cref="MapFallback(IEndpointRouteBuilder, RequestDelegate)"/> registers an endpoint using the pattern
|
||||
/// <c>{*path:nonfile}</c>. The order of the registered endpoint will be <c>int.MaxValue</c>.
|
||||
/// </para>
|
||||
/// </remarks>
|
||||
public static IEndpointConventionBuilder MapFallback(this IEndpointRouteBuilder builder, RequestDelegate requestDelegate)
|
||||
{
|
||||
if (builder == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(builder));
|
||||
}
|
||||
|
||||
if (requestDelegate == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(requestDelegate));
|
||||
}
|
||||
|
||||
return builder.MapFallback("{*path:nonfile}", requestDelegate);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Adds a specialized <see cref="RouteEndpoint"/> to the <see cref="IEndpointRouteBuilder"/> that will match
|
||||
/// the provided pattern with the lowest possible priority.
|
||||
/// </summary>
|
||||
/// <param name="builder">The <see cref="IEndpointRouteBuilder"/> to add the route to.</param>
|
||||
/// <param name="pattern">The route pattern.</param>
|
||||
/// <param name="requestDelegate">The delegate executed when the endpoint is matched.</param>
|
||||
/// <returns>A <see cref="IEndpointConventionBuilder"/> that can be used to further customize the endpoint.</returns>
|
||||
/// <remarks>
|
||||
/// <para>
|
||||
/// <see cref="MapFallback(IEndpointRouteBuilder, string, RequestDelegate)"/> is intended to handle cases where no
|
||||
/// other endpoint has matched. This is convenient for routing requests to a SPA framework.
|
||||
/// </para>
|
||||
/// <para>
|
||||
/// The order of the registered endpoint will be <c>int.MaxValue</c>.
|
||||
/// </para>
|
||||
/// <para>
|
||||
/// This overload will use the provided <paramref name="pattern"/> verbatim. Use the <c>:nonfile</c> route contraint
|
||||
/// to exclude requests for static files.
|
||||
/// </para>
|
||||
/// </remarks>
|
||||
public static IEndpointConventionBuilder MapFallback(
|
||||
this IEndpointRouteBuilder builder,
|
||||
string pattern,
|
||||
RequestDelegate requestDelegate)
|
||||
{
|
||||
if (builder == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(builder));
|
||||
}
|
||||
|
||||
if (pattern == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(pattern));
|
||||
}
|
||||
|
||||
if (requestDelegate == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(requestDelegate));
|
||||
}
|
||||
|
||||
var conventionBuilder = builder.Map(pattern, "Fallback " + pattern, requestDelegate);
|
||||
conventionBuilder.Add(b => ((RouteEndpointBuilder)b).Order = int.MaxValue);
|
||||
return conventionBuilder;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,148 @@
|
|||
// 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.Globalization;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
|
||||
namespace Microsoft.AspNetCore.Routing.Constraints
|
||||
{
|
||||
/// <summary>
|
||||
/// Constrains a route parameter to represent only file name values. Does not validate that
|
||||
/// the route value contains valid file system characters, or that the value represents
|
||||
/// an actual file on disk.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// <para>
|
||||
/// This constraint can be used to disambiguate requests for static files versus dynamic
|
||||
/// content served from the application.
|
||||
/// </para>
|
||||
/// <para>
|
||||
/// This constraint determines whether a route value represents a file name by examining
|
||||
/// the last URL Path segment of the value (delimited by <c>/</c>). The last segment
|
||||
/// must contain the dot (<c>.</c>) character followed by one or more non-(<c>.</c>) characters.
|
||||
/// </para>
|
||||
/// <para>
|
||||
/// If the route value does not contain a <c>/</c> then the entire value will be interpreted
|
||||
/// as the last segment.
|
||||
/// </para>
|
||||
/// <para>
|
||||
/// The <see cref="FileNameRouteConstraint"/> does not attempt to validate that the value contains
|
||||
/// a legal file name for the current operating system.
|
||||
/// </para>
|
||||
/// <para>
|
||||
/// The <see cref="FileNameRouteConstraint"/> does not attempt to validate that the value represents
|
||||
/// an actual file on disk.
|
||||
/// </para>
|
||||
/// <para>
|
||||
/// <list type="bullet">
|
||||
/// <listheader>
|
||||
/// <term>Examples of route values that will be matched as file names</term>
|
||||
/// <description>description</description>
|
||||
/// </listheader>
|
||||
/// <item>
|
||||
/// <term><c>/a/b/c.txt</c></term>
|
||||
/// <description>Final segment contains a <c>.</c> followed by other characters.</description>
|
||||
/// </item>
|
||||
/// <item>
|
||||
/// <term><c>/hello.world.txt</c></term>
|
||||
/// <description>Final segment contains a <c>.</c> followed by other characters.</description>
|
||||
/// </item>
|
||||
/// <item>
|
||||
/// <term><c>hello.world.txt</c></term>
|
||||
/// <description>Final segment contains a <c>.</c> followed by other characters.</description>
|
||||
/// </item>
|
||||
/// <item>
|
||||
/// <term><c>.gitignore</c></term>
|
||||
/// <description>Final segment contains a <c>.</c> followed by other characters.</description>
|
||||
/// </item>
|
||||
/// </list>
|
||||
/// <list type="bullet">
|
||||
/// <listheader>
|
||||
/// <term>Examples of route values that will be rejected as non-file-names</term>
|
||||
/// <description>description</description>
|
||||
/// </listheader>
|
||||
/// <item>
|
||||
/// <term><c>/a/b/c</c></term>
|
||||
/// <description>Final segment does not contain a <c>.</c>.</description>
|
||||
/// </item>
|
||||
/// <item>
|
||||
/// <term><c>/a/b.d/c</c></term>
|
||||
/// <description>Final segment does not contain a <c>.</c>.</description>
|
||||
/// </item>
|
||||
/// <item>
|
||||
/// <term><c>/a/b.d/c/</c></term>
|
||||
/// <description>Final segment is empty.</description>
|
||||
/// </item>
|
||||
/// <item>
|
||||
/// <term><c></c></term>
|
||||
/// <description>Value is empty</description>
|
||||
/// </item>
|
||||
/// </list>
|
||||
/// </para>
|
||||
/// </remarks>
|
||||
public class FileNameRouteConstraint : IRouteConstraint
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public bool Match(
|
||||
HttpContext httpContext,
|
||||
IRouter route,
|
||||
string routeKey,
|
||||
RouteValueDictionary values,
|
||||
RouteDirection routeDirection)
|
||||
{
|
||||
if (routeKey == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(routeKey));
|
||||
}
|
||||
|
||||
if (values == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(values));
|
||||
}
|
||||
|
||||
if (values.TryGetValue(routeKey, out var obj) && obj != null)
|
||||
{
|
||||
var value = Convert.ToString(obj, CultureInfo.InvariantCulture);
|
||||
return IsFileName(value);
|
||||
}
|
||||
|
||||
// No value or null value.
|
||||
return false;
|
||||
}
|
||||
|
||||
// This is used both here and in NonFileNameRouteConstraint
|
||||
// Any changes to this logic need to update the docs in those places.
|
||||
internal static bool IsFileName(ReadOnlySpan<char> value)
|
||||
{
|
||||
if (value.Length == 0)
|
||||
{
|
||||
// Not a file name because empty.
|
||||
return false;
|
||||
}
|
||||
|
||||
var lastSlashIndex = value.LastIndexOf('/');
|
||||
if (lastSlashIndex >= 0)
|
||||
{
|
||||
value = value.Slice(lastSlashIndex + 1);
|
||||
}
|
||||
|
||||
var dotIndex = value.IndexOf('.');
|
||||
if (dotIndex == -1)
|
||||
{
|
||||
// No dot.
|
||||
return false;
|
||||
}
|
||||
|
||||
for (var i = dotIndex + 1; i < value.Length; i++)
|
||||
{
|
||||
if (value[i] != '.')
|
||||
{
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,114 @@
|
|||
// 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.Globalization;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
|
||||
namespace Microsoft.AspNetCore.Routing.Constraints
|
||||
{
|
||||
/// <summary>
|
||||
/// Constrains a route parameter to represent only non-file-name values. Does not validate that
|
||||
/// the route value contains valid file system characters, or that the value represents
|
||||
/// an actual file on disk.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// <para>
|
||||
/// This constraint can be used to disambiguate requests for dynamic content versus
|
||||
/// static files served from the application.
|
||||
/// </para>
|
||||
/// <para>
|
||||
/// This constraint determines whether a route value represents a file name by examining
|
||||
/// the last URL Path segment of the value (delimited by <c>/</c>). The last segment
|
||||
/// must contain the dot (<c>.</c>) character followed by one or more non-(<c>.</c>) characters.
|
||||
/// </para>
|
||||
/// <para>
|
||||
/// If the route value does not contain a <c>/</c> then the entire value will be interpreted
|
||||
/// as a the last segment.
|
||||
/// </para>
|
||||
/// <para>
|
||||
/// The <see cref="NonFileNameRouteConstraint"/> does not attempt to validate that the value contains
|
||||
/// a legal file name for the current operating system.
|
||||
/// </para>
|
||||
/// <para>
|
||||
/// <list type="bullet">
|
||||
/// <listheader>
|
||||
/// <term>Examples of route values that will be matched as non-file-names</term>
|
||||
/// <description>description</description>
|
||||
/// </listheader>
|
||||
/// <item>
|
||||
/// <term><c>/a/b/c</c></term>
|
||||
/// <description>Final segment does not contain a <c>.</c>.</description>
|
||||
/// </item>
|
||||
/// <item>
|
||||
/// <term><c>/a/b.d/c</c></term>
|
||||
/// <description>Final segment does not contain a <c>.</c>.</description>
|
||||
/// </item>
|
||||
/// <item>
|
||||
/// <term><c>/a/b.d/c/</c></term>
|
||||
/// <description>Final segment is empty.</description>
|
||||
/// </item>
|
||||
/// <item>
|
||||
/// <term><c></c></term>
|
||||
/// <description>Value is empty</description>
|
||||
/// </item>
|
||||
/// </list>
|
||||
/// <list type="bullet">
|
||||
/// <listheader>
|
||||
/// <term>Examples of route values that will be rejected as file names</term>
|
||||
/// <description>description</description>
|
||||
/// </listheader>
|
||||
/// <item>
|
||||
/// <term><c>/a/b/c.txt</c></term>
|
||||
/// <description>Final segment contains a <c>.</c> followed by other characters.</description>
|
||||
/// </item>
|
||||
/// <item>
|
||||
/// <term><c>/hello.world.txt</c></term>
|
||||
/// <description>Final segment contains a <c>.</c> followed by other characters.</description>
|
||||
/// </item>
|
||||
/// <item>
|
||||
/// <term><c>hello.world.txt</c></term>
|
||||
/// <description>Final segment contains a <c>.</c> followed by other characters.</description>
|
||||
/// </item>
|
||||
/// <item>
|
||||
/// <term><c>.gitignore</c></term>
|
||||
/// <description>Final segment contains a <c>.</c> followed by other characters.</description>
|
||||
/// </item>
|
||||
/// </list>
|
||||
/// </para>
|
||||
/// </remarks>
|
||||
public class NonFileNameRouteConstraint : IRouteConstraint
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public bool Match(
|
||||
HttpContext httpContext,
|
||||
IRouter route,
|
||||
string routeKey,
|
||||
RouteValueDictionary values,
|
||||
RouteDirection routeDirection)
|
||||
{
|
||||
if (routeKey == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(routeKey));
|
||||
}
|
||||
|
||||
if (values == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(values));
|
||||
}
|
||||
|
||||
if (values.TryGetValue(routeKey, out var obj) && obj != null)
|
||||
{
|
||||
var value = Convert.ToString(obj, CultureInfo.InvariantCulture);
|
||||
return !FileNameRouteConstraint.IsFileName(value);
|
||||
}
|
||||
|
||||
// No value or null value.
|
||||
//
|
||||
// We want to return true here because the core use-case of the constraint is to *exclude*
|
||||
// things that look like file names. There's nothing here that looks like a file name, so
|
||||
// let it through.
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -76,6 +76,10 @@ namespace Microsoft.AspNetCore.Routing
|
|||
{ "regex", typeof(RegexInlineRouteConstraint) },
|
||||
|
||||
{"required", typeof(RequiredRouteConstraint) },
|
||||
|
||||
// Files
|
||||
{ "file", typeof(FileNameRouteConstraint) },
|
||||
{ "nonfile", typeof(NonFileNameRouteConstraint) },
|
||||
};
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,106 @@
|
|||
// 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.Net;
|
||||
using System.Net.Http;
|
||||
using System.Threading.Tasks;
|
||||
using RoutingWebSite;
|
||||
using Xunit;
|
||||
|
||||
namespace Microsoft.AspNetCore.Routing.FunctionalTests
|
||||
{
|
||||
public class MapFallbackTest : IClassFixture<RoutingTestFixture<MapFallbackStartup>>
|
||||
{
|
||||
private readonly RoutingTestFixture<MapFallbackStartup> _fixture;
|
||||
private readonly HttpClient _client;
|
||||
|
||||
public MapFallbackTest(RoutingTestFixture<MapFallbackStartup> fixture)
|
||||
{
|
||||
_fixture = fixture;
|
||||
_client = _fixture.CreateClient("http://localhost");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Get_HelloWorld()
|
||||
{
|
||||
// Arrange
|
||||
var request = new HttpRequestMessage(HttpMethod.Get, "helloworld");
|
||||
|
||||
// Act
|
||||
var response = await _client.SendAsync(request);
|
||||
var responseContent = await response.Content.ReadAsStringAsync();
|
||||
|
||||
// Assert
|
||||
Assert.Equal(HttpStatusCode.OK, response.StatusCode);
|
||||
Assert.Equal("Hello World", responseContent);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData("prefix/favicon.ico")]
|
||||
[InlineData("prefix/content/js/jquery.min.js")]
|
||||
public async Task Get_FallbackWithPattern_FileName(string path)
|
||||
{
|
||||
// Arrange
|
||||
var request = new HttpRequestMessage(HttpMethod.Get, path);
|
||||
|
||||
// Act
|
||||
var response = await _client.SendAsync(request);
|
||||
|
||||
// Assert
|
||||
Assert.Equal(HttpStatusCode.NotFound, response.StatusCode);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData("prefix")]
|
||||
[InlineData("prefix/")]
|
||||
[InlineData("prefix/store")]
|
||||
[InlineData("prefix/blog/read/18")]
|
||||
public async Task Get_FallbackWithPattern_NonFileName(string path)
|
||||
{
|
||||
// Arrange
|
||||
var request = new HttpRequestMessage(HttpMethod.Get, path);
|
||||
|
||||
// Act
|
||||
var response = await _client.SendAsync(request);
|
||||
var responseContent = await response.Content.ReadAsStringAsync();
|
||||
|
||||
// Assert
|
||||
Assert.Equal(HttpStatusCode.OK, response.StatusCode);
|
||||
Assert.Equal("FallbackCustomPattern", responseContent);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData("favicon.ico")]
|
||||
[InlineData("content/js/jquery.min.js")]
|
||||
public async Task Get_Fallback_FileName(string path)
|
||||
{
|
||||
// Arrange
|
||||
var request = new HttpRequestMessage(HttpMethod.Get, path);
|
||||
|
||||
// Act
|
||||
var response = await _client.SendAsync(request);
|
||||
|
||||
// Assert
|
||||
Assert.Equal(HttpStatusCode.NotFound, response.StatusCode);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData("")]
|
||||
[InlineData("/")]
|
||||
[InlineData("store")]
|
||||
[InlineData("blog/read/18")]
|
||||
public async Task Get_Fallback_NonFileName(string path)
|
||||
{
|
||||
// Arrange
|
||||
var request = new HttpRequestMessage(HttpMethod.Get, path);
|
||||
|
||||
// Act
|
||||
var response = await _client.SendAsync(request);
|
||||
var responseContent = await response.Content.ReadAsStringAsync();
|
||||
|
||||
// Assert
|
||||
Assert.Equal(HttpStatusCode.OK, response.StatusCode);
|
||||
Assert.Equal("FallbackDefaultPattern", responseContent);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,100 @@
|
|||
// 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 Xunit;
|
||||
|
||||
namespace Microsoft.AspNetCore.Routing.Constraints
|
||||
{
|
||||
public class FileNameRouteConstraintTest
|
||||
{
|
||||
public static TheoryData<object> FileNameData
|
||||
{
|
||||
get
|
||||
{
|
||||
return new TheoryData<object>()
|
||||
{
|
||||
"hello.txt",
|
||||
"hello.txt.jpg",
|
||||
"/hello.t",
|
||||
"/////hello.x",
|
||||
"a/b/c/d.e",
|
||||
"a/b./.c/d.e",
|
||||
".gitnore",
|
||||
".a",
|
||||
"/.......a"
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
[Theory]
|
||||
[MemberData(nameof(FileNameData))]
|
||||
public void Match_RouteValue_IsFileName(object value)
|
||||
{
|
||||
// Arrange
|
||||
var constraint = new FileNameRouteConstraint();
|
||||
|
||||
var values = new RouteValueDictionary();
|
||||
values.Add("path", value);
|
||||
|
||||
// Act
|
||||
var result = constraint.Match(httpContext: null, route: null, "path", values, RouteDirection.IncomingRequest);
|
||||
|
||||
// Assert
|
||||
Assert.True(result);
|
||||
}
|
||||
|
||||
public static TheoryData<object> NonFileNameData
|
||||
{
|
||||
get
|
||||
{
|
||||
return new TheoryData<object>()
|
||||
{
|
||||
null,
|
||||
string.Empty,
|
||||
"/",
|
||||
".",
|
||||
"..........",
|
||||
"hello.",
|
||||
"/hello",
|
||||
"//",
|
||||
"//b.c/",
|
||||
"/////hello.",
|
||||
"a/b./.c/d.",
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[MemberData(nameof(NonFileNameData))]
|
||||
public void Match_RouteValue_IsNotFileName(object value)
|
||||
{
|
||||
// Arrange
|
||||
var constraint = new FileNameRouteConstraint();
|
||||
|
||||
var values = new RouteValueDictionary();
|
||||
values.Add("path", value);
|
||||
|
||||
// Act
|
||||
var result = constraint.Match(httpContext: null, route: null, "path", values, RouteDirection.IncomingRequest);
|
||||
|
||||
// Assert
|
||||
Assert.False(result);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Match_MissingValue_IsNotFileName()
|
||||
{
|
||||
// Arrange
|
||||
var constraint = new FileNameRouteConstraint();
|
||||
|
||||
var values = new RouteValueDictionary();
|
||||
|
||||
// Act
|
||||
var result = constraint.Match(httpContext: null, route: null, "path", values, RouteDirection.IncomingRequest);
|
||||
|
||||
// Assert
|
||||
Assert.False(result);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,59 @@
|
|||
// 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 Xunit;
|
||||
|
||||
namespace Microsoft.AspNetCore.Routing.Constraints
|
||||
{
|
||||
public class NonFileNameRouteConstraintTest
|
||||
{
|
||||
[Theory]
|
||||
[MemberData(nameof(FileNameRouteConstraintTest.FileNameData), MemberType = typeof(FileNameRouteConstraintTest))]
|
||||
public void Match_RouteValue_IsNotNonFileName(object value)
|
||||
{
|
||||
// Arrange
|
||||
var constraint = new NonFileNameRouteConstraint();
|
||||
|
||||
var values = new RouteValueDictionary();
|
||||
values.Add("path", value);
|
||||
|
||||
// Act
|
||||
var result = constraint.Match(httpContext: null, route: null, "path", values, RouteDirection.IncomingRequest);
|
||||
|
||||
// Assert
|
||||
Assert.False(result);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[MemberData(nameof(FileNameRouteConstraintTest.NonFileNameData), MemberType = typeof(FileNameRouteConstraintTest))]
|
||||
public void Match_RouteValue_IsNonFileName(object value)
|
||||
{
|
||||
// Arrange
|
||||
var constraint = new NonFileNameRouteConstraint();
|
||||
|
||||
var values = new RouteValueDictionary();
|
||||
values.Add("path", value);
|
||||
|
||||
// Act
|
||||
var result = constraint.Match(httpContext: null, route: null, "path", values, RouteDirection.IncomingRequest);
|
||||
|
||||
// Assert
|
||||
Assert.True(result);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Match_MissingValue_IsNotFileName()
|
||||
{
|
||||
// Arrange
|
||||
var constraint = new NonFileNameRouteConstraint();
|
||||
|
||||
var values = new RouteValueDictionary();
|
||||
|
||||
// Act
|
||||
var result = constraint.Match(httpContext: null, route: null, "path", values, RouteDirection.IncomingRequest);
|
||||
|
||||
// Assert
|
||||
Assert.True(result);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,37 @@
|
|||
// 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.Builder;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
|
||||
namespace RoutingWebSite
|
||||
{
|
||||
public class MapFallbackStartup
|
||||
{
|
||||
public void ConfigureServices(IServiceCollection services)
|
||||
{
|
||||
services.AddRouting();
|
||||
}
|
||||
|
||||
public void Configure(IApplicationBuilder app)
|
||||
{
|
||||
app.UseRouting(routes =>
|
||||
{
|
||||
routes.MapFallback("/prefix/{*path:nonfile}", (context) =>
|
||||
{
|
||||
return context.Response.WriteAsync("FallbackCustomPattern");
|
||||
});
|
||||
|
||||
routes.MapFallback((context) =>
|
||||
{
|
||||
return context.Response.WriteAsync("FallbackDefaultPattern");
|
||||
});
|
||||
|
||||
routes.MapHello("/helloworld", "World");
|
||||
});
|
||||
|
||||
app.UseEndpoint();
|
||||
}
|
||||
}
|
||||
}
|
||||
Loading…
Reference in New Issue