Add fallback routing to file
This is a new routing feature that integrates static files to serve a static file when routing doesn't match anything else. This is a scenario that's covered by SPA services today, but given the improvements to routing it makes much more sense to move lower in the stack.
This commit is contained in:
parent
7e63e2da43
commit
089f64c528
|
|
@ -7,6 +7,7 @@
|
|||
<Compile Include="Microsoft.AspNetCore.StaticFiles.netcoreapp3.0.cs" />
|
||||
<Reference Include="Microsoft.AspNetCore.Hosting.Abstractions" />
|
||||
<Reference Include="Microsoft.AspNetCore.Http.Extensions" />
|
||||
<Reference Include="Microsoft.AspNetCore.Routing" />
|
||||
<Reference Include="Microsoft.Extensions.FileProviders.Abstractions" />
|
||||
<Reference Include="Microsoft.Extensions.Logging.Abstractions" />
|
||||
<Reference Include="Microsoft.Extensions.WebEncoders" />
|
||||
|
|
|
|||
|
|
@ -58,6 +58,13 @@ namespace Microsoft.AspNetCore.Builder
|
|||
public System.Action<Microsoft.AspNetCore.StaticFiles.StaticFileResponseContext> OnPrepareResponse { [System.Runtime.CompilerServices.CompilerGeneratedAttribute]get { throw null; } [System.Runtime.CompilerServices.CompilerGeneratedAttribute]set { } }
|
||||
public bool ServeUnknownFileTypes { [System.Runtime.CompilerServices.CompilerGeneratedAttribute]get { throw null; } [System.Runtime.CompilerServices.CompilerGeneratedAttribute]set { } }
|
||||
}
|
||||
public static partial class StaticFilesEndpointRouteBuilderExtensions
|
||||
{
|
||||
public static Microsoft.AspNetCore.Builder.IEndpointConventionBuilder MapFallbackToFile(this Microsoft.AspNetCore.Routing.IEndpointRouteBuilder builder, string filePath) { throw null; }
|
||||
public static Microsoft.AspNetCore.Builder.IEndpointConventionBuilder MapFallbackToFile(this Microsoft.AspNetCore.Routing.IEndpointRouteBuilder builder, string filePath, Microsoft.AspNetCore.Builder.StaticFileOptions options) { throw null; }
|
||||
public static Microsoft.AspNetCore.Builder.IEndpointConventionBuilder MapFallbackToFile(this Microsoft.AspNetCore.Routing.IEndpointRouteBuilder builder, string pattern, string filePath) { throw null; }
|
||||
public static Microsoft.AspNetCore.Builder.IEndpointConventionBuilder MapFallbackToFile(this Microsoft.AspNetCore.Routing.IEndpointRouteBuilder builder, string pattern, string filePath, Microsoft.AspNetCore.Builder.StaticFileOptions options) { throw null; }
|
||||
}
|
||||
}
|
||||
namespace Microsoft.AspNetCore.StaticFiles
|
||||
{
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<Description>ASP.NET Core static files middleware. Includes middleware for serving static files, directory browsing, and default files.</Description>
|
||||
|
|
@ -16,6 +16,7 @@
|
|||
<ItemGroup>
|
||||
<Reference Include="Microsoft.AspNetCore.Hosting.Abstractions" />
|
||||
<Reference Include="Microsoft.AspNetCore.Http.Extensions" />
|
||||
<Reference Include="Microsoft.AspNetCore.Routing" />
|
||||
<Reference Include="Microsoft.Extensions.FileProviders.Abstractions" />
|
||||
<Reference Include="Microsoft.Extensions.Logging.Abstractions" />
|
||||
<Reference Include="Microsoft.Extensions.WebEncoders" />
|
||||
|
|
|
|||
|
|
@ -0,0 +1,223 @@
|
|||
// 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.Http.Endpoints;
|
||||
using Microsoft.AspNetCore.Routing;
|
||||
using Microsoft.AspNetCore.StaticFiles;
|
||||
|
||||
namespace Microsoft.AspNetCore.Builder
|
||||
{
|
||||
/// <summary>
|
||||
/// Contains extension methods for using static files with endpoint routing.
|
||||
/// </summary>
|
||||
public static class StaticFilesEndpointRouteBuilderExtensions
|
||||
{
|
||||
/// <summary>
|
||||
/// Adds a specialized <see cref="RouteEndpoint"/> to the <see cref="IEndpointRouteBuilder"/> that will match
|
||||
/// requests for non-filenames with the lowest possible priority. The request will be routed to a
|
||||
/// <see cref="StaticFileMiddleware"/> that attempts to serve the file specified by <paramref name="filePath"/>.
|
||||
/// </summary>
|
||||
/// <param name="builder">The <see cref="IEndpointRouteBuilder"/>.</param>
|
||||
/// <param name="filePath">The file path of the file to serve.</param>
|
||||
/// <returns>The <see cref="IEndpointRouteBuilder"/></returns>
|
||||
/// <remarks>
|
||||
/// <para>
|
||||
/// <see cref="MapFallbackToFile(IEndpointRouteBuilder, string)"/> is intended to handle cases where URL path of
|
||||
/// the request does not contain a filename, 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>
|
||||
/// The default <see cref="StaticFileOptions"/> for the <see cref="StaticFileMiddleware"/> will be used.
|
||||
/// </para>
|
||||
/// <para>
|
||||
/// <see cref="MapFallbackToFile(IEndpointRouteBuilder, string)"/> 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 MapFallbackToFile(
|
||||
this IEndpointRouteBuilder builder,
|
||||
string filePath)
|
||||
{
|
||||
if (builder == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(builder));
|
||||
}
|
||||
|
||||
if (filePath == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(filePath));
|
||||
}
|
||||
|
||||
return builder.MapFallback(CreateRequestDelegate(builder, filePath));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Adds a specialized <see cref="RouteEndpoint"/> to the <see cref="IEndpointRouteBuilder"/> that will match
|
||||
/// requests for non-filenames with the lowest possible priority. The request will be routed to a
|
||||
/// <see cref="StaticFileMiddleware"/> that attempts to serve the file specified by <paramref name="filePath"/>.
|
||||
/// </summary>
|
||||
/// <param name="builder">The <see cref="IEndpointRouteBuilder"/>.</param>
|
||||
/// <param name="filePath">The file path of the file to serve.</param>
|
||||
/// <param name="options"><see cref="StaticFileOptions"/> for the <see cref="StaticFileMiddleware"/>.</param>
|
||||
/// <returns>The <see cref="IEndpointRouteBuilder"/></returns>
|
||||
/// <remarks>
|
||||
/// <para>
|
||||
/// <see cref="MapFallbackToFile(IEndpointRouteBuilder, string, StaticFileOptions)"/> 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="MapFallbackToFile(IEndpointRouteBuilder, string, StaticFileOptions)"/> 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 MapFallbackToFile(
|
||||
this IEndpointRouteBuilder builder,
|
||||
string filePath,
|
||||
StaticFileOptions options)
|
||||
{
|
||||
if (builder == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(builder));
|
||||
}
|
||||
|
||||
if (filePath == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(filePath));
|
||||
}
|
||||
|
||||
return builder.MapFallback(CreateRequestDelegate(builder, filePath, options));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Adds a specialized <see cref="RouteEndpoint"/> to the <see cref="IEndpointRouteBuilder"/> that will match
|
||||
/// requests for non-filenames with the lowest possible priority. The request will be routed to a
|
||||
/// <see cref="StaticFileMiddleware"/> that attempts to serve the file specified by <paramref name="filePath"/>.
|
||||
/// </summary>
|
||||
/// <param name="builder">The <see cref="IEndpointRouteBuilder"/>.</param>
|
||||
/// <param name="pattern">The route pattern.</param>
|
||||
/// <param name="filePath">The file path of the file to serve.</param>
|
||||
/// <returns>The <see cref="IEndpointRouteBuilder"/></returns>
|
||||
/// <remarks>
|
||||
/// <para>
|
||||
/// <see cref="MapFallbackToFile(IEndpointRouteBuilder, string, string)"/> is intended to handle
|
||||
/// cases where URL path of the request does not contain a filename, 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>
|
||||
/// The default <see cref="StaticFileOptions"/> for the <see cref="StaticFileMiddleware"/> will be used.
|
||||
/// </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 MapFallbackToFile(
|
||||
this IEndpointRouteBuilder builder,
|
||||
string pattern,
|
||||
string filePath)
|
||||
{
|
||||
if (builder == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(builder));
|
||||
}
|
||||
|
||||
if (pattern == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(filePath));
|
||||
}
|
||||
|
||||
if (filePath == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(filePath));
|
||||
}
|
||||
|
||||
return builder.MapFallback(pattern, CreateRequestDelegate(builder, filePath));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Adds a specialized <see cref="RouteEndpoint"/> to the <see cref="IEndpointRouteBuilder"/> that will match
|
||||
/// requests for non-filenames with the lowest possible priority. The request will be routed to a
|
||||
/// <see cref="StaticFileMiddleware"/> that attempts to serve the file specified by <paramref name="filePath"/>.
|
||||
/// </summary>
|
||||
/// <param name="builder">The <see cref="IEndpointRouteBuilder"/>.</param>\
|
||||
/// <param name="pattern">The route pattern.</param>
|
||||
/// <param name="filePath">The file path of the file to serve.</param>
|
||||
/// <param name="options"><see cref="StaticFileOptions"/> for the <see cref="StaticFileMiddleware"/>.</param>
|
||||
/// <returns>The <see cref="IEndpointRouteBuilder"/></returns>
|
||||
/// <remarks>
|
||||
/// <para>
|
||||
/// <see cref="MapFallbackToFile(IEndpointRouteBuilder, string, string, StaticFileOptions)"/> is intended to handle
|
||||
/// cases where URL path of the request does not contain a filename, 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>
|
||||
/// 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 MapFallbackToFile(
|
||||
this IEndpointRouteBuilder builder,
|
||||
string pattern,
|
||||
string filePath,
|
||||
StaticFileOptions options)
|
||||
{
|
||||
if (builder == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(builder));
|
||||
}
|
||||
|
||||
if (pattern == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(filePath));
|
||||
}
|
||||
|
||||
if (filePath == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(filePath));
|
||||
}
|
||||
|
||||
return builder.MapFallback(pattern, CreateRequestDelegate(builder, filePath, options));
|
||||
}
|
||||
|
||||
private static RequestDelegate CreateRequestDelegate(
|
||||
IEndpointRouteBuilder builder,
|
||||
string filePath,
|
||||
StaticFileOptions options = null)
|
||||
{
|
||||
var app = builder.CreateApplicationBuilder();
|
||||
app.Use(next => context =>
|
||||
{
|
||||
context.Request.Path = "/" + filePath;
|
||||
|
||||
// Set endpoint to null so the static files middleware will handle the request.
|
||||
context.SetEndpoint(null);
|
||||
|
||||
return next(context);
|
||||
});
|
||||
|
||||
if (options == null)
|
||||
{
|
||||
app.UseStaticFiles();
|
||||
}
|
||||
else
|
||||
{
|
||||
app.UseStaticFiles(options);
|
||||
}
|
||||
|
||||
return app.Build();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,131 @@
|
|||
// 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.IO;
|
||||
using System.Linq;
|
||||
using System.Net;
|
||||
using System.Net.Http;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.AspNetCore.Builder;
|
||||
using Microsoft.AspNetCore.Hosting;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.AspNetCore.Server.IntegrationTesting;
|
||||
using Microsoft.AspNetCore.Server.IntegrationTesting.Common;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.Extensions.FileProviders;
|
||||
using Microsoft.Extensions.Logging.Testing;
|
||||
using Xunit;
|
||||
|
||||
namespace Microsoft.AspNetCore.StaticFiles
|
||||
{
|
||||
public class FallbackStaticFileTest : LoggedTest
|
||||
{
|
||||
[Fact]
|
||||
public async Task ReturnsFileForDefaultPattern()
|
||||
{
|
||||
var builder = new WebHostBuilder()
|
||||
.ConfigureServices(services =>
|
||||
{
|
||||
services.AddRouting();
|
||||
services.AddSingleton(LoggerFactory);
|
||||
})
|
||||
.UseKestrel()
|
||||
.UseWebRoot(AppContext.BaseDirectory)
|
||||
.Configure(app =>
|
||||
{
|
||||
var environment = app.ApplicationServices.GetRequiredService<IWebHostEnvironment>();
|
||||
|
||||
app.UseRouting(routes =>
|
||||
{
|
||||
routes.Map("/hello", context =>
|
||||
{
|
||||
return context.Response.WriteAsync("Hello, world!");
|
||||
});
|
||||
|
||||
routes.MapFallbackToFile("default.html", new StaticFileOptions()
|
||||
{
|
||||
FileProvider = new PhysicalFileProvider(Path.Combine(environment.WebRootPath, "SubFolder")),
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
using (var server = builder.Start(TestUrlHelper.GetTestUrl(ServerType.Kestrel)))
|
||||
{
|
||||
var environment = server.Services.GetRequiredService<IWebHostEnvironment>();
|
||||
using (var client = new HttpClient { BaseAddress = new Uri(server.GetAddress()) })
|
||||
{
|
||||
var response = await client.GetAsync("hello");
|
||||
var responseText = await response.Content.ReadAsStringAsync();
|
||||
|
||||
Assert.Equal(HttpStatusCode.OK, response.StatusCode);
|
||||
Assert.Equal("Hello, world!", responseText);
|
||||
|
||||
response = await client.GetAsync("/");
|
||||
var responseContent = await response.Content.ReadAsByteArrayAsync();
|
||||
|
||||
Assert.Equal(HttpStatusCode.OK, response.StatusCode);
|
||||
AssertFileEquals(environment, "SubFolder/default.html", responseContent);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task ReturnsFileForCustomPattern()
|
||||
{
|
||||
var builder = new WebHostBuilder()
|
||||
.ConfigureServices(services =>
|
||||
{
|
||||
services.AddRouting();
|
||||
services.AddSingleton(LoggerFactory);
|
||||
})
|
||||
.UseKestrel()
|
||||
.UseWebRoot(AppContext.BaseDirectory)
|
||||
.Configure(app =>
|
||||
{
|
||||
app.UseRouting(routes =>
|
||||
{
|
||||
routes.Map("/hello", context =>
|
||||
{
|
||||
return context.Response.WriteAsync("Hello, world!");
|
||||
});
|
||||
|
||||
routes.MapFallbackToFile("/prefix/{*path:nonfile}", "TestDocument.txt");
|
||||
});
|
||||
});
|
||||
|
||||
using (var server = builder.Start(TestUrlHelper.GetTestUrl(ServerType.Kestrel)))
|
||||
{
|
||||
var environment = server.Services.GetRequiredService<IWebHostEnvironment>();
|
||||
using (var client = new HttpClient { BaseAddress = new Uri(server.GetAddress()) })
|
||||
{
|
||||
var response = await client.GetAsync("hello");
|
||||
var responseText = await response.Content.ReadAsStringAsync();
|
||||
|
||||
Assert.Equal(HttpStatusCode.OK, response.StatusCode);
|
||||
Assert.Equal("Hello, world!", responseText);
|
||||
|
||||
response = await client.GetAsync("prefix/Some-Path");
|
||||
var responseContent = await response.Content.ReadAsByteArrayAsync();
|
||||
|
||||
Assert.Equal(HttpStatusCode.OK, response.StatusCode);
|
||||
AssertFileEquals(environment, "TestDocument.txt", responseContent);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static void AssertFileEquals(IWebHostEnvironment environment, string filePath, byte[] responseContent)
|
||||
{
|
||||
var fileInfo = environment.WebRootFileProvider.GetFileInfo(filePath);
|
||||
Assert.NotNull(fileInfo);
|
||||
Assert.True(fileInfo.Exists);
|
||||
|
||||
using (var stream = fileInfo.CreateReadStream())
|
||||
{
|
||||
var fileContents = new byte[stream.Length];
|
||||
stream.Read(fileContents, 0, (int)stream.Length);
|
||||
Assert.True(responseContent.SequenceEqual(fileContents));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Loading…
Reference in New Issue