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 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 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 partial class MapRouteRouteBuilderExtensions
|
||||||
{
|
{
|
||||||
public static Microsoft.AspNetCore.Routing.IRouteBuilder MapRoute(this Microsoft.AspNetCore.Routing.IRouteBuilder routeBuilder, string name, string template) { throw null; }
|
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 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 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 partial class FloatRouteConstraint : Microsoft.AspNetCore.Routing.IParameterPolicy, Microsoft.AspNetCore.Routing.IRouteConstraint
|
||||||
{
|
{
|
||||||
public FloatRouteConstraint() { }
|
public FloatRouteConstraint() { }
|
||||||
|
|
@ -441,6 +451,11 @@ namespace Microsoft.AspNetCore.Routing.Constraints
|
||||||
public long Min { [System.Runtime.CompilerServices.CompilerGeneratedAttribute]get { throw null; } }
|
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 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 partial class OptionalRouteConstraint : Microsoft.AspNetCore.Routing.IParameterPolicy, Microsoft.AspNetCore.Routing.IRouteConstraint
|
||||||
{
|
{
|
||||||
public OptionalRouteConstraint(Microsoft.AspNetCore.Routing.IRouteConstraint innerConstraint) { }
|
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) },
|
{ "regex", typeof(RegexInlineRouteConstraint) },
|
||||||
|
|
||||||
{"required", typeof(RequiredRouteConstraint) },
|
{"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