This changes the recipe for client-side blazor to use similar primitives to server side applications. --- I ignored auto-rebuild support because it's currently dead code until we have that in VS. If we add auto-rebuild to ASP.NET Core - we'd probably want to make that a separate gesture inside `IsDevelopement()` like other dev-time features anyway. --- The static files hookup is a special thing because creating the file server for a client-side Blazor app involves some non-trivial work. We plan to make this better in the future. What's nice about this pattern is that the implementation is pretty simple and literal, and it scales fine if you have multiple Blazor client-side apps. I didn't provide a lot of options here, it's pretty much the same as UseBlazor. --- I feel pretty good about the wireup with routing to use the `index.html` from the client app. I think it's pretty to-the-point.
This commit is contained in:
parent
8fd86c38db
commit
d3400f7cb2
|
|
@ -20,6 +20,7 @@ namespace Microsoft.AspNetCore.Blazor.DevServer.Server
|
|||
public void ConfigureServices(IServiceCollection services)
|
||||
{
|
||||
services.AddRouting();
|
||||
|
||||
services.AddResponseCompression(options =>
|
||||
{
|
||||
options.MimeTypes = ResponseCompressionDefaults.MimeTypes.Concat(new[]
|
||||
|
|
@ -30,15 +31,22 @@ namespace Microsoft.AspNetCore.Blazor.DevServer.Server
|
|||
});
|
||||
}
|
||||
|
||||
public void Configure(IApplicationBuilder app, IConfiguration configuration)
|
||||
public void Configure(IApplicationBuilder app, IWebHostEnvironment environment, IConfiguration configuration)
|
||||
{
|
||||
app.UseDeveloperExceptionPage();
|
||||
app.UseResponseCompression();
|
||||
EnableConfiguredPathbase(app, configuration);
|
||||
|
||||
var clientAssemblyPath = FindClientAssemblyPath(app);
|
||||
app.UseBlazor(new BlazorOptions { ClientAssemblyPath = clientAssemblyPath });
|
||||
app.UseBlazorDebugging();
|
||||
|
||||
app.UseClientSideBlazorFiles(FindClientAssemblyPath(environment));
|
||||
|
||||
app.UseRouting();
|
||||
|
||||
app.UseEndpoints(endpoints =>
|
||||
{
|
||||
endpoints.MapFallbackToClientSideBlazor(FindClientAssemblyPath(environment), "index.html");
|
||||
});
|
||||
}
|
||||
|
||||
private static void EnableConfiguredPathbase(IApplicationBuilder app, IConfiguration configuration)
|
||||
|
|
@ -66,10 +74,9 @@ namespace Microsoft.AspNetCore.Blazor.DevServer.Server
|
|||
}
|
||||
}
|
||||
|
||||
private static string FindClientAssemblyPath(IApplicationBuilder app)
|
||||
private static string FindClientAssemblyPath(IWebHostEnvironment environment)
|
||||
{
|
||||
var env = app.ApplicationServices.GetRequiredService<IWebHostEnvironment>();
|
||||
var contentRoot = env.ContentRootPath;
|
||||
var contentRoot = environment.ContentRootPath;
|
||||
var binDir = FindClientBinDir(contentRoot);
|
||||
var appName = Path.GetFileName(contentRoot); // TODO: Allow for the possibility that the assembly name has been overridden
|
||||
var assemblyPath = Path.Combine(binDir, $"{appName}.dll");
|
||||
|
|
|
|||
|
|
@ -3,18 +3,20 @@
|
|||
|
||||
namespace Microsoft.AspNetCore.Builder
|
||||
{
|
||||
public static partial class BlazorApplicationBuilderExtensions
|
||||
public static partial class BlazorHostingApplicationBuilderExtensions
|
||||
{
|
||||
public static Microsoft.AspNetCore.Builder.IApplicationBuilder UseBlazor(this Microsoft.AspNetCore.Builder.IApplicationBuilder app, Microsoft.AspNetCore.Builder.BlazorOptions options) { throw null; }
|
||||
public static Microsoft.AspNetCore.Builder.IApplicationBuilder UseBlazor<TProgram>(this Microsoft.AspNetCore.Builder.IApplicationBuilder app) { throw null; }
|
||||
public static Microsoft.AspNetCore.Builder.IApplicationBuilder UseClientSideBlazorFiles(this Microsoft.AspNetCore.Builder.IApplicationBuilder app, string clientAssemblyFilePath) { throw null; }
|
||||
public static Microsoft.AspNetCore.Builder.IApplicationBuilder UseClientSideBlazorFiles<TClientApp>(this Microsoft.AspNetCore.Builder.IApplicationBuilder app) { throw null; }
|
||||
}
|
||||
public static partial class BlazorHostingEndpointRouteBuilderExtensions
|
||||
{
|
||||
public static Microsoft.AspNetCore.Builder.IEndpointConventionBuilder MapFallbackToClientSideBlazor(this Microsoft.AspNetCore.Routing.IEndpointRouteBuilder endpoints, string clientAssemblyFilePath, string filePath) { throw null; }
|
||||
public static Microsoft.AspNetCore.Builder.IEndpointConventionBuilder MapFallbackToClientSideBlazor(this Microsoft.AspNetCore.Routing.IEndpointRouteBuilder endpoints, string clientAssemblyFilePath, string pattern, string filePath) { throw null; }
|
||||
public static Microsoft.AspNetCore.Builder.IEndpointConventionBuilder MapFallbackToClientSideBlazor<TClientApp>(this Microsoft.AspNetCore.Routing.IEndpointRouteBuilder endpoints, string filePath) { throw null; }
|
||||
public static Microsoft.AspNetCore.Builder.IEndpointConventionBuilder MapFallbackToClientSideBlazor<TClientApp>(this Microsoft.AspNetCore.Routing.IEndpointRouteBuilder endpoints, string pattern, string filePath) { throw null; }
|
||||
}
|
||||
public static partial class BlazorMonoDebugProxyAppBuilderExtensions
|
||||
{
|
||||
public static void UseBlazorDebugging(this Microsoft.AspNetCore.Builder.IApplicationBuilder app) { }
|
||||
}
|
||||
public partial class BlazorOptions
|
||||
{
|
||||
public BlazorOptions() { }
|
||||
public string ClientAssemblyPath { [System.Runtime.CompilerServices.CompilerGeneratedAttribute]get { throw null; } [System.Runtime.CompilerServices.CompilerGeneratedAttribute]set { } }
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -47,5 +47,32 @@ namespace Microsoft.AspNetCore.Blazor.Server
|
|||
EnableAutoRebuilding = configLines.Contains("autorebuild:true", StringComparer.Ordinal);
|
||||
EnableDebugging = configLines.Contains("debug:true", StringComparer.Ordinal);
|
||||
}
|
||||
|
||||
public string FindIndexHtmlFile()
|
||||
{
|
||||
// Before publishing, the client project may have a wwwroot directory.
|
||||
// If so, and if it contains index.html, use that.
|
||||
if (!string.IsNullOrEmpty(WebRootPath))
|
||||
{
|
||||
var wwwrootIndexHtmlPath = Path.Combine(WebRootPath, "index.html");
|
||||
if (File.Exists(wwwrootIndexHtmlPath))
|
||||
{
|
||||
return wwwrootIndexHtmlPath;
|
||||
}
|
||||
}
|
||||
|
||||
// After publishing, the client project won't have a wwwroot directory.
|
||||
// The contents from that dir will have been copied to "dist" during publish.
|
||||
// So if "dist/index.html" now exists, use that.
|
||||
var distIndexHtmlPath = Path.Combine(DistPath, "index.html");
|
||||
if (File.Exists(distIndexHtmlPath))
|
||||
{
|
||||
return distIndexHtmlPath;
|
||||
}
|
||||
|
||||
// Since there's no index.html, we'll use the default DefaultPageStaticFileOptions,
|
||||
// hence we'll look for index.html in the host server app's wwwroot.
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,162 +0,0 @@
|
|||
// 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.Net.Mime;
|
||||
using Microsoft.AspNetCore.Blazor.Server;
|
||||
using Microsoft.Extensions.FileProviders;
|
||||
using Microsoft.AspNetCore.Hosting;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.AspNetCore.StaticFiles;
|
||||
using Microsoft.Extensions.Hosting;
|
||||
|
||||
namespace Microsoft.AspNetCore.Builder
|
||||
{
|
||||
/// <summary>
|
||||
/// Provides extension methods that add Blazor-related middleware to the ASP.NET pipeline.
|
||||
/// </summary>
|
||||
public static class BlazorApplicationBuilderExtensions
|
||||
{
|
||||
const string DevServerApplicationName = "blazor-devserver";
|
||||
|
||||
/// <summary>
|
||||
/// Configures the middleware pipeline to work with Blazor.
|
||||
/// </summary>
|
||||
/// <typeparam name="TProgram">Any type from the client app project. This is used to identify the client app assembly.</typeparam>
|
||||
/// <param name="app">The <see cref="IApplicationBuilder"/>.</param>
|
||||
/// <returns>The <see cref="IApplicationBuilder"/>.</returns>
|
||||
public static IApplicationBuilder UseBlazor<TProgram>(
|
||||
this IApplicationBuilder app)
|
||||
{
|
||||
var clientAssemblyInServerBinDir = typeof(TProgram).Assembly;
|
||||
return app.UseBlazor(new BlazorOptions
|
||||
{
|
||||
ClientAssemblyPath = clientAssemblyInServerBinDir.Location,
|
||||
});
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Configures the middleware pipeline to work with Blazor.
|
||||
/// </summary>
|
||||
/// <param name="app">The <see cref="IApplicationBuilder"/>.</param>
|
||||
/// <param name="options">Options to configure the middleware.</param>
|
||||
/// <returns>The <see cref="IApplicationBuilder"/>.</returns>
|
||||
public static IApplicationBuilder UseBlazor(
|
||||
this IApplicationBuilder app,
|
||||
BlazorOptions options)
|
||||
{
|
||||
// TODO: Make the .blazor.config file contents sane
|
||||
// Currently the items in it are bizarre and don't relate to their purpose,
|
||||
// hence all the path manipulation here. We shouldn't be hardcoding 'dist' here either.
|
||||
var env = (IWebHostEnvironment)app.ApplicationServices.GetService(typeof(IWebHostEnvironment));
|
||||
var config = BlazorConfig.Read(options.ClientAssemblyPath);
|
||||
|
||||
if (env.IsDevelopment() && config.EnableAutoRebuilding)
|
||||
{
|
||||
if (env.ApplicationName.Equals(DevServerApplicationName, StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
app.UseDevServerAutoRebuild(config);
|
||||
}
|
||||
else
|
||||
{
|
||||
app.UseHostedAutoRebuild(config, env.ContentRootPath);
|
||||
}
|
||||
}
|
||||
|
||||
// First, match the request against files in the client app dist directory
|
||||
app.UseStaticFiles(new StaticFileOptions
|
||||
{
|
||||
FileProvider = new PhysicalFileProvider(config.DistPath),
|
||||
ContentTypeProvider = CreateContentTypeProvider(config.EnableDebugging),
|
||||
OnPrepareResponse = CacheHeaderSettings.SetCacheHeaders,
|
||||
});
|
||||
|
||||
// * Before publishing, we serve the wwwroot files directly from source
|
||||
// (and don't require them to be copied into dist).
|
||||
// In this case, WebRootPath will be nonempty if that directory exists.
|
||||
// * After publishing, the wwwroot files are already copied to 'dist' and
|
||||
// will be served by the above middleware, so we do nothing here.
|
||||
// In this case, WebRootPath will be empty (the publish process sets this).
|
||||
if (!string.IsNullOrEmpty(config.WebRootPath))
|
||||
{
|
||||
app.UseStaticFiles(new StaticFileOptions
|
||||
{
|
||||
FileProvider = new PhysicalFileProvider(config.WebRootPath),
|
||||
OnPrepareResponse = CacheHeaderSettings.SetCacheHeaders,
|
||||
});
|
||||
}
|
||||
|
||||
// Finally, use SPA fallback routing (serve default page for anything else,
|
||||
// excluding /_framework/*)
|
||||
app.MapWhen(IsNotFrameworkDir, childAppBuilder =>
|
||||
{
|
||||
var indexHtmlPath = FindIndexHtmlFile(config);
|
||||
var indexHtmlStaticFileOptions = string.IsNullOrEmpty(indexHtmlPath)
|
||||
? null : new StaticFileOptions
|
||||
{
|
||||
FileProvider = new PhysicalFileProvider(Path.GetDirectoryName(indexHtmlPath)),
|
||||
OnPrepareResponse = CacheHeaderSettings.SetCacheHeaders,
|
||||
};
|
||||
|
||||
childAppBuilder.UseSpa(spa =>
|
||||
{
|
||||
spa.Options.DefaultPageStaticFileOptions = indexHtmlStaticFileOptions;
|
||||
});
|
||||
});
|
||||
|
||||
return app;
|
||||
}
|
||||
|
||||
private static string FindIndexHtmlFile(BlazorConfig config)
|
||||
{
|
||||
// Before publishing, the client project may have a wwwroot directory.
|
||||
// If so, and if it contains index.html, use that.
|
||||
if (!string.IsNullOrEmpty(config.WebRootPath))
|
||||
{
|
||||
var wwwrootIndexHtmlPath = Path.Combine(config.WebRootPath, "index.html");
|
||||
if (File.Exists(wwwrootIndexHtmlPath))
|
||||
{
|
||||
return wwwrootIndexHtmlPath;
|
||||
}
|
||||
}
|
||||
|
||||
// After publishing, the client project won't have a wwwroot directory.
|
||||
// The contents from that dir will have been copied to "dist" during publish.
|
||||
// So if "dist/index.html" now exists, use that.
|
||||
var distIndexHtmlPath = Path.Combine(config.DistPath, "index.html");
|
||||
if (File.Exists(distIndexHtmlPath))
|
||||
{
|
||||
return distIndexHtmlPath;
|
||||
}
|
||||
|
||||
// Since there's no index.html, we'll use the default DefaultPageStaticFileOptions,
|
||||
// hence we'll look for index.html in the host server app's wwwroot.
|
||||
return null;
|
||||
}
|
||||
|
||||
private static bool IsNotFrameworkDir(HttpContext context)
|
||||
=> !context.Request.Path.StartsWithSegments("/_framework");
|
||||
|
||||
private static IContentTypeProvider CreateContentTypeProvider(bool enableDebugging)
|
||||
{
|
||||
var result = new FileExtensionContentTypeProvider();
|
||||
AddMapping(result, ".dll", MediaTypeNames.Application.Octet);
|
||||
|
||||
if (enableDebugging)
|
||||
{
|
||||
AddMapping(result, ".pdb", MediaTypeNames.Application.Octet);
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
private static void AddMapping(FileExtensionContentTypeProvider provider, string name, string mimeType)
|
||||
{
|
||||
if (!provider.Mappings.ContainsKey(name))
|
||||
{
|
||||
provider.Mappings.Add(name, mimeType);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,98 @@
|
|||
// 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.Collections.Generic;
|
||||
using System.Net.Mime;
|
||||
using Microsoft.AspNetCore.Blazor.Server;
|
||||
using Microsoft.AspNetCore.StaticFiles;
|
||||
using Microsoft.Extensions.FileProviders;
|
||||
|
||||
namespace Microsoft.AspNetCore.Builder
|
||||
{
|
||||
/// <summary>
|
||||
/// Provides extension methods for hosting client-side Blazor applications in ASP.NET Core.
|
||||
/// </summary>
|
||||
public static class BlazorHostingApplicationBuilderExtensions
|
||||
{
|
||||
/// <summary>
|
||||
/// Adds a <see cref="StaticFileMiddleware"/> that will serve static files from the client-side Blazor application
|
||||
/// specified by <typeparamref name="TClientApp"/>.
|
||||
/// </summary>
|
||||
/// <typeparam name="TClientApp">A type in the client-side application.</typeparam>
|
||||
/// <param name="app">The <see cref="IApplicationBuilder"/>.</param>
|
||||
/// <returns>The <see cref="IApplicationBuilder"/>.</returns>
|
||||
public static IApplicationBuilder UseClientSideBlazorFiles<TClientApp>(this IApplicationBuilder app)
|
||||
{
|
||||
if (app == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(app));
|
||||
}
|
||||
|
||||
UseClientSideBlazorFiles(app, typeof(TClientApp).Assembly.Location);
|
||||
return app;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Adds a <see cref="StaticFileMiddleware"/> that will serve static files from the client-side Blazor application
|
||||
/// specified by <paramref name="clientAssemblyFilePath"/>.
|
||||
/// </summary>
|
||||
/// <param name="clientAssemblyFilePath">The file path of the client-side Blazor application assembly.</param>
|
||||
/// <param name="app">The <see cref="IApplicationBuilder"/>.</param>
|
||||
/// <returns>The <see cref="IApplicationBuilder"/>.</returns>
|
||||
public static IApplicationBuilder UseClientSideBlazorFiles(this IApplicationBuilder app, string clientAssemblyFilePath)
|
||||
{
|
||||
if (clientAssemblyFilePath == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(clientAssemblyFilePath));
|
||||
}
|
||||
|
||||
var fileProviders = new List<IFileProvider>();
|
||||
|
||||
// TODO: Make the .blazor.config file contents sane
|
||||
// Currently the items in it are bizarre and don't relate to their purpose,
|
||||
// hence all the path manipulation here. We shouldn't be hardcoding 'dist' here either.
|
||||
var config = BlazorConfig.Read(clientAssemblyFilePath);
|
||||
|
||||
// First, match the request against files in the client app dist directory
|
||||
fileProviders.Add(new PhysicalFileProvider(config.DistPath));
|
||||
|
||||
// * Before publishing, we serve the wwwroot files directly from source
|
||||
// (and don't require them to be copied into dist).
|
||||
// In this case, WebRootPath will be nonempty if that directory exists.
|
||||
// * After publishing, the wwwroot files are already copied to 'dist' and
|
||||
// will be served by the above middleware, so we do nothing here.
|
||||
// In this case, WebRootPath will be empty (the publish process sets this).
|
||||
if (!string.IsNullOrEmpty(config.WebRootPath))
|
||||
{
|
||||
fileProviders.Add(new PhysicalFileProvider(config.WebRootPath));
|
||||
}
|
||||
|
||||
// We can't modify an IFileContentTypeProvider, so we have to decorate.
|
||||
var contentTypeProvider = new FileExtensionContentTypeProvider();
|
||||
AddMapping(contentTypeProvider, ".dll", MediaTypeNames.Application.Octet);
|
||||
if (config.EnableDebugging)
|
||||
{
|
||||
AddMapping(contentTypeProvider, ".pdb", MediaTypeNames.Application.Octet);
|
||||
}
|
||||
|
||||
var options = new StaticFileOptions()
|
||||
{
|
||||
ContentTypeProvider = contentTypeProvider,
|
||||
FileProvider = new CompositeFileProvider(fileProviders),
|
||||
OnPrepareResponse = CacheHeaderSettings.SetCacheHeaders,
|
||||
};
|
||||
|
||||
app.UseStaticFiles(options);
|
||||
return app;
|
||||
|
||||
static void AddMapping(FileExtensionContentTypeProvider provider, string name, string mimeType)
|
||||
{
|
||||
if (!provider.Mappings.ContainsKey(name))
|
||||
{
|
||||
provider.Mappings.Add(name, mimeType);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,193 @@
|
|||
// 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 Microsoft.AspNetCore.Blazor.Server;
|
||||
using Microsoft.AspNetCore.Hosting;
|
||||
using Microsoft.AspNetCore.Routing;
|
||||
using Microsoft.AspNetCore.StaticFiles;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.Extensions.FileProviders;
|
||||
|
||||
namespace Microsoft.AspNetCore.Builder
|
||||
{
|
||||
/// <summary>
|
||||
/// Provides extension methods for hosting client-side Blazor applications in ASP.NET Core.
|
||||
/// </summary>
|
||||
public static class BlazorHostingEndpointRouteBuilderExtensions
|
||||
{
|
||||
/// <summary>
|
||||
/// Adds a low-priority endpoint that will serve the the file specified by <paramref name="filePath"/> from the client-side
|
||||
/// Blazor application specified by <typeparamref name="TClientApp"/>.
|
||||
/// </summary>
|
||||
/// <typeparam name="TClientApp">A type in the client-side application.</typeparam>
|
||||
/// <param name="endpoints">The <see cref="IEndpointRouteBuilder"/>.</param>
|
||||
/// <param name="filePath">
|
||||
/// The relative path to the entry point of the client-side application. The path is relative to the
|
||||
/// <see cref="IWebHostEnvironment.WebRootPath"/>, commonly <c>wwwroot</c>.
|
||||
/// </param>
|
||||
/// <returns>The <see cref="IApplicationBuilder"/>.</returns>
|
||||
/// <remarks>
|
||||
/// <para>
|
||||
/// This method 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 the client-side blazor
|
||||
/// application, while also allowing requests for non-existent files to result in an HTTP 404.
|
||||
/// </para>
|
||||
/// </remarks>
|
||||
public static IEndpointConventionBuilder MapFallbackToClientSideBlazor<TClientApp>(this IEndpointRouteBuilder endpoints, string filePath)
|
||||
{
|
||||
if (endpoints == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(endpoints));
|
||||
}
|
||||
|
||||
if (filePath == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(filePath));
|
||||
}
|
||||
|
||||
return MapFallbackToClientSideBlazor(endpoints, typeof(TClientApp).Assembly.Location, FallbackEndpointRouteBuilderExtensions.DefaultPattern, filePath);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Adds a low-priority endpoint that will serve the the file specified by <paramref name="filePath"/> from the client-side
|
||||
/// Blazor application specified by <paramref name="clientAssemblyFilePath"/>.
|
||||
/// </summary>
|
||||
/// <param name="endpoints">The <see cref="IEndpointRouteBuilder"/>.</param>
|
||||
/// <param name="clientAssemblyFilePath">The file path of the client-side Blazor application assembly.</param>
|
||||
/// <param name="filePath">
|
||||
/// The relative path to the entry point of the client-side application. The path is relative to the
|
||||
/// <see cref="IWebHostEnvironment.WebRootPath"/>, commonly <c>wwwroot</c>.
|
||||
/// </param>
|
||||
/// <returns>The <see cref="IApplicationBuilder"/>.</returns>
|
||||
/// <remarks>
|
||||
/// <para>
|
||||
/// This method 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 the client-side blazor
|
||||
/// application, while also allowing requests for non-existent files to result in an HTTP 404.
|
||||
/// </para>
|
||||
/// </remarks>
|
||||
public static IEndpointConventionBuilder MapFallbackToClientSideBlazor(this IEndpointRouteBuilder endpoints, string clientAssemblyFilePath, string filePath)
|
||||
{
|
||||
if (endpoints == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(endpoints));
|
||||
}
|
||||
|
||||
if (clientAssemblyFilePath == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(clientAssemblyFilePath));
|
||||
}
|
||||
|
||||
if (filePath == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(filePath));
|
||||
}
|
||||
|
||||
return MapFallbackToClientSideBlazor(endpoints, clientAssemblyFilePath, FallbackEndpointRouteBuilderExtensions.DefaultPattern, filePath);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Adds a low-priority endpoint that will serve the the file specified by <paramref name="filePath"/> from the client-side
|
||||
/// Blazor application specified by <typeparamref name="TClientApp"/>.
|
||||
/// </summary>
|
||||
/// <typeparam name="TClientApp">A type in the client-side application.</typeparam>
|
||||
/// <param name="endpoints">The <see cref="IEndpointRouteBuilder"/>.</param>
|
||||
/// <param name="pattern">The route pattern to match.</param>
|
||||
/// <param name="filePath">
|
||||
/// The relative path to the entry point of the client-side application. The path is relative to the
|
||||
/// <see cref="IWebHostEnvironment.WebRootPath"/>, commonly <c>wwwroot</c>.
|
||||
/// </param>
|
||||
/// <returns>The <see cref="IApplicationBuilder"/>.</returns>
|
||||
/// <remarks>
|
||||
/// <para>
|
||||
/// This method 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 the client-side blazor
|
||||
/// application, while also allowing requests for non-existent files to result in an HTTP 404.
|
||||
/// </para>
|
||||
/// </remarks>
|
||||
public static IEndpointConventionBuilder MapFallbackToClientSideBlazor<TClientApp>(this IEndpointRouteBuilder endpoints, string pattern, string filePath)
|
||||
{
|
||||
if (endpoints == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(endpoints));
|
||||
}
|
||||
|
||||
if (pattern == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(pattern));
|
||||
}
|
||||
|
||||
if (filePath == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(filePath));
|
||||
}
|
||||
|
||||
return MapFallbackToClientSideBlazor(endpoints, typeof(TClientApp).Assembly.Location, pattern, filePath);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Adds a low-priority endpoint that will serve the the file specified by <paramref name="filePath"/> from the client-side
|
||||
/// Blazor application specified by <paramref name="clientAssemblyFilePath"/>.
|
||||
/// </summary>
|
||||
/// <param name="clientAssemblyFilePath">The file path of the client-side Blazor application assembly.</param>
|
||||
/// <param name="endpoints">The <see cref="IEndpointRouteBuilder"/>.</param>
|
||||
/// <param name="pattern">The route pattern to match.</param>
|
||||
/// <param name="filePath">
|
||||
/// The relative path to the entry point of the client-side application. The path is relative to the
|
||||
/// <see cref="IWebHostEnvironment.WebRootPath"/>, commonly <c>wwwroot</c>.
|
||||
/// </param>
|
||||
/// <returns>The <see cref="IApplicationBuilder"/>.</returns>
|
||||
/// <remarks>
|
||||
/// <para>
|
||||
/// This method 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 the client-side blazor
|
||||
/// application, while also allowing requests for non-existent files to result in an HTTP 404.
|
||||
/// </para>
|
||||
/// </remarks>
|
||||
public static IEndpointConventionBuilder MapFallbackToClientSideBlazor(this IEndpointRouteBuilder endpoints, string clientAssemblyFilePath, string pattern, string filePath)
|
||||
{
|
||||
if (endpoints == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(endpoints));
|
||||
}
|
||||
|
||||
if (clientAssemblyFilePath == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(clientAssemblyFilePath));
|
||||
}
|
||||
|
||||
if (pattern == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(pattern));
|
||||
}
|
||||
|
||||
if (filePath == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(filePath));
|
||||
}
|
||||
|
||||
var config = BlazorConfig.Read(clientAssemblyFilePath);
|
||||
|
||||
// We want to serve "index.html" from whichever directory contains it in this priority order:
|
||||
// 1. Client app "dist" directory
|
||||
// 2. Client app "wwwroot" directory
|
||||
// 3. Server app "wwwroot" directory
|
||||
var directory = endpoints.ServiceProvider.GetRequiredService<IWebHostEnvironment>().WebRootPath;
|
||||
var indexHtml = config.FindIndexHtmlFile();
|
||||
if (indexHtml != null)
|
||||
{
|
||||
directory = Path.GetDirectoryName(indexHtml);
|
||||
}
|
||||
|
||||
var options = new StaticFileOptions()
|
||||
{
|
||||
FileProvider = new PhysicalFileProvider(directory),
|
||||
OnPrepareResponse = CacheHeaderSettings.SetCacheHeaders,
|
||||
};
|
||||
|
||||
return endpoints.MapFallbackToFile(pattern, filePath, options);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,17 +0,0 @@
|
|||
// 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.
|
||||
|
||||
namespace Microsoft.AspNetCore.Builder
|
||||
{
|
||||
/// <summary>
|
||||
/// Provides configuration options to the <see cref="BlazorApplicationBuilderExtensions.UseBlazor(IApplicationBuilder, BlazorOptions)"/>
|
||||
/// middleware.
|
||||
/// </summary>
|
||||
public class BlazorOptions
|
||||
{
|
||||
/// <summary>
|
||||
/// Full path to the client assembly.
|
||||
/// </summary>
|
||||
public string ClientAssemblyPath { get; set; }
|
||||
}
|
||||
}
|
||||
|
|
@ -33,14 +33,15 @@ namespace BlazorHosted_CSharp.Server
|
|||
app.UseBlazorDebugging();
|
||||
}
|
||||
|
||||
app.UseBlazorClientSideFiles<Client.Startup>();
|
||||
|
||||
app.UseRouting();
|
||||
|
||||
app.UseEndpoints(endpoints =>
|
||||
{
|
||||
endpoints.MapDefaultControllerRoute();
|
||||
endpoints.MapFallbackToClientSideBlazor<Client.Startup>("index.html");
|
||||
});
|
||||
|
||||
app.UseBlazor<Client.Startup>();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -25,7 +25,15 @@ namespace HostedInAspNet.Server
|
|||
app.UseBlazorDebugging();
|
||||
}
|
||||
|
||||
app.UseBlazor<Client.Program>();
|
||||
app.UseStaticFiles();
|
||||
app.UseClientSideBlazorFiles<Client.Program>();
|
||||
|
||||
app.UseRouting();
|
||||
|
||||
app.UseEndpoints(endpoints =>
|
||||
{
|
||||
endpoints.MapFallbackToClientSideBlazor<Client.Program>("index.html");
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,17 +2,27 @@
|
|||
// 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.Hosting;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
|
||||
namespace MonoSanity
|
||||
{
|
||||
public class Startup
|
||||
{
|
||||
public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
|
||||
public void ConfigureServices(IServiceCollection services)
|
||||
{
|
||||
}
|
||||
|
||||
public void Configure(IApplicationBuilder app)
|
||||
{
|
||||
app.UseDeveloperExceptionPage();
|
||||
app.UseFileServer(new FileServerOptions { EnableDefaultFiles = true });
|
||||
app.UseBlazor<MonoSanityClient.Program>();
|
||||
app.UseFileServer(new FileServerOptions() { EnableDefaultFiles = true, });
|
||||
app.UseStaticFiles();
|
||||
app.UseClientSideBlazorFiles<MonoSanityClient.Program>();
|
||||
app.UseRouting();
|
||||
app.UseEndpoints(endpoints =>
|
||||
{
|
||||
endpoints.MapFallbackToClientSideBlazor<MonoSanityClient.Program>("index.html");
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@
|
|||
|
||||
using System;
|
||||
using System.IO;
|
||||
using System.Reflection;
|
||||
using Microsoft.AspNetCore.Hosting;
|
||||
|
||||
namespace Microsoft.AspNetCore.Components.E2ETest.Infrastructure.ServerFixtures
|
||||
|
|
@ -11,6 +12,8 @@ namespace Microsoft.AspNetCore.Components.E2ETest.Infrastructure.ServerFixtures
|
|||
{
|
||||
public delegate IWebHost BuildWebHost(string[] args);
|
||||
|
||||
public Assembly ApplicationAssembly { get; set; }
|
||||
|
||||
public BuildWebHost BuildWebHostMethod { get; set; }
|
||||
|
||||
public AspNetEnvironment Environment { get; set; } = AspNetEnvironment.Production;
|
||||
|
|
@ -23,8 +26,8 @@ namespace Microsoft.AspNetCore.Components.E2ETest.Infrastructure.ServerFixtures
|
|||
$"No value was provided for {nameof(BuildWebHostMethod)}");
|
||||
}
|
||||
|
||||
var sampleSitePath = FindSampleOrTestSitePath(
|
||||
BuildWebHostMethod.Method.DeclaringType.Assembly.FullName);
|
||||
var assembly = ApplicationAssembly ?? BuildWebHostMethod.Method.DeclaringType.Assembly;
|
||||
var sampleSitePath = FindSampleOrTestSitePath(assembly.FullName);
|
||||
|
||||
return BuildWebHostMethod(new[]
|
||||
{
|
||||
|
|
|
|||
|
|
@ -0,0 +1,77 @@
|
|||
// 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 BasicTestApp;
|
||||
using Microsoft.AspNetCore.Components.E2ETest.Infrastructure;
|
||||
using Microsoft.AspNetCore.Components.E2ETest.Infrastructure.ServerFixtures;
|
||||
using Microsoft.AspNetCore.E2ETesting;
|
||||
using Microsoft.AspNetCore.Hosting;
|
||||
using OpenQA.Selenium;
|
||||
using OpenQA.Selenium.Support.UI;
|
||||
using Xunit;
|
||||
using Xunit.Abstractions;
|
||||
|
||||
namespace Microsoft.AspNetCore.Components.E2ETest.Tests
|
||||
{
|
||||
/// <summary>
|
||||
/// Tests for various MapFallbackToClientSideBlazor overloads. We're just verifying that things render correctly.
|
||||
/// That means that routing and file serving is working for the startup pattern under test.
|
||||
/// </summary>
|
||||
public class ClientSideHostingTest : ServerTestBase<AspNetSiteServerFixture>
|
||||
{
|
||||
public ClientSideHostingTest(
|
||||
BrowserFixture browserFixture,
|
||||
AspNetSiteServerFixture serverFixture,
|
||||
ITestOutputHelper output)
|
||||
: base(browserFixture, serverFixture, output)
|
||||
{
|
||||
serverFixture.ApplicationAssembly = typeof(TestServer.Program).Assembly;
|
||||
serverFixture.BuildWebHostMethod = BuildWebHost;
|
||||
}
|
||||
|
||||
private IWebHost BuildWebHost(string[] args)
|
||||
{
|
||||
return TestServer.Program.BuildWebHost<TestServer.StartupWithMapFallbackToClientSideBlazor>(args);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void MapFallbackToClientSideBlazor_FilePath()
|
||||
{
|
||||
Navigate("/filepath");
|
||||
WaitUntilLoaded();
|
||||
Assert.NotNull(Browser.FindElement(By.Id("test-selector")));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void MapFallbackToClientSideBlazor_Pattern_FilePath()
|
||||
{
|
||||
Navigate("/pattern_filepath/test");
|
||||
WaitUntilLoaded();
|
||||
Assert.NotNull(Browser.FindElement(By.Id("test-selector")));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void MapFallbackToClientSideBlazor_AssemblyPath_FilePath()
|
||||
{
|
||||
Navigate("/assemblypath_filepath");
|
||||
WaitUntilLoaded();
|
||||
Assert.NotNull(Browser.FindElement(By.Id("test-selector")));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void MapFallbackToClientSideBlazor_AssemblyPath_Pattern_FilePath()
|
||||
{
|
||||
Navigate("/assemblypath_pattern_filepath/test");
|
||||
WaitUntilLoaded();
|
||||
Assert.NotNull(Browser.FindElement(By.Id("test-selector")));
|
||||
}
|
||||
|
||||
|
||||
private void WaitUntilLoaded()
|
||||
{
|
||||
new WebDriverWait(Browser, TimeSpan.FromSeconds(30)).Until(
|
||||
driver => driver.FindElement(By.TagName("app")).Text != "Loading...");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,4 +1,4 @@
|
|||
using Microsoft.AspNetCore;
|
||||
using Microsoft.AspNetCore;
|
||||
using Microsoft.AspNetCore.Hosting;
|
||||
using Microsoft.Extensions.Configuration;
|
||||
|
||||
|
|
@ -11,12 +11,14 @@ namespace TestServer
|
|||
BuildWebHost(args).Run();
|
||||
}
|
||||
|
||||
public static IWebHost BuildWebHost(string[] args) =>
|
||||
public static IWebHost BuildWebHost(string[] args) => BuildWebHost<Startup>(args);
|
||||
|
||||
public static IWebHost BuildWebHost<TStartup>(string[] args) where TStartup : class =>
|
||||
WebHost.CreateDefaultBuilder(args)
|
||||
.UseConfiguration(new ConfigurationBuilder()
|
||||
.AddCommandLine(args)
|
||||
.Build())
|
||||
.UseStartup<Startup>()
|
||||
.UseStartup<TStartup>()
|
||||
.Build();
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -49,11 +49,12 @@ namespace TestServer
|
|||
.AllowCredentials();
|
||||
});
|
||||
|
||||
app.UseRouting();
|
||||
|
||||
// Mount the server-side Blazor app on /subdir
|
||||
app.Map("/subdir", subdirApp =>
|
||||
{
|
||||
subdirApp.UseClientSideBlazorFiles<BasicTestApp.Startup>();
|
||||
|
||||
// The following two lines are equivalent to:
|
||||
// endpoints.MapComponentsHub<Index>();
|
||||
//
|
||||
|
|
@ -66,13 +67,12 @@ namespace TestServer
|
|||
subdirApp.UseEndpoints(endpoints =>
|
||||
{
|
||||
endpoints.MapHub<ComponentHub>(ComponentHub.DefaultPath).AddComponent(typeof(Index), selector: "root");
|
||||
endpoints.MapFallbackToClientSideBlazor<BasicTestApp.Startup>("index.html");
|
||||
});
|
||||
|
||||
subdirApp.MapWhen(
|
||||
ctx => ctx.Features.Get<IEndpointFeature>()?.Endpoint == null,
|
||||
blazorBuilder => blazorBuilder.UseBlazor<BasicTestApp.Startup>());
|
||||
});
|
||||
|
||||
app.UseRouting();
|
||||
|
||||
app.UseEndpoints(endpoints =>
|
||||
{
|
||||
endpoints.MapControllers();
|
||||
|
|
|
|||
|
|
@ -0,0 +1,84 @@
|
|||
// 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 BasicTestApp;
|
||||
using Microsoft.AspNetCore.Builder;
|
||||
using Microsoft.AspNetCore.Components.Server;
|
||||
using Microsoft.AspNetCore.Hosting;
|
||||
using Microsoft.AspNetCore.Http.Features;
|
||||
using Microsoft.Extensions.Configuration;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.Extensions.Hosting;
|
||||
|
||||
namespace TestServer
|
||||
{
|
||||
// Used for E2E tests that verify different overloads of MapFallbackToClientSideBlazor.
|
||||
public class StartupWithMapFallbackToClientSideBlazor
|
||||
{
|
||||
public StartupWithMapFallbackToClientSideBlazor(IConfiguration configuration)
|
||||
{
|
||||
Configuration = configuration;
|
||||
}
|
||||
|
||||
public IConfiguration Configuration { get; }
|
||||
|
||||
public void ConfigureServices(IServiceCollection services)
|
||||
{
|
||||
}
|
||||
|
||||
public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
|
||||
{
|
||||
if (env.IsDevelopment())
|
||||
{
|
||||
app.UseDeveloperExceptionPage();
|
||||
}
|
||||
|
||||
// The client-side files middleware needs to be here because the base href in hardcoded to /subdir/
|
||||
app.Map("/subdir", app =>
|
||||
{
|
||||
app.UseClientSideBlazorFiles<BasicTestApp.Startup>();
|
||||
});
|
||||
|
||||
// The calls to `Map` allow us to test each of these overloads, while keeping them isolated.
|
||||
app.Map("/filepath", app =>
|
||||
{
|
||||
app.UseRouting();
|
||||
|
||||
app.UseEndpoints(endpoints =>
|
||||
{
|
||||
endpoints.MapFallbackToClientSideBlazor<BasicTestApp.Startup>("index.html");
|
||||
});
|
||||
});
|
||||
|
||||
app.Map("/pattern_filepath", app =>
|
||||
{
|
||||
app.UseRouting();
|
||||
|
||||
app.UseEndpoints(endpoints =>
|
||||
{
|
||||
endpoints.MapFallbackToClientSideBlazor<BasicTestApp.Startup>("test/{*path:nonfile}", "index.html");
|
||||
});
|
||||
});
|
||||
|
||||
app.Map("/assemblypath_filepath", app =>
|
||||
{
|
||||
app.UseRouting();
|
||||
|
||||
app.UseEndpoints(endpoints =>
|
||||
{
|
||||
endpoints.MapFallbackToClientSideBlazor(typeof(BasicTestApp.Startup).Assembly.Location, "index.html");
|
||||
});
|
||||
});
|
||||
|
||||
app.Map("/assemblypath_pattern_filepath", app =>
|
||||
{
|
||||
app.UseRouting();
|
||||
|
||||
app.UseEndpoints(endpoints =>
|
||||
{
|
||||
endpoints.MapFallbackToClientSideBlazor(typeof(BasicTestApp.Startup).Assembly.Location, "test/{*path:nonfile}", "index.html");
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -21,6 +21,7 @@ namespace Microsoft.AspNetCore.Builder
|
|||
}
|
||||
public static partial class FallbackEndpointRouteBuilderExtensions
|
||||
{
|
||||
public static readonly string DefaultPattern;
|
||||
public static Microsoft.AspNetCore.Builder.IEndpointConventionBuilder MapFallback(this Microsoft.AspNetCore.Routing.IEndpointRouteBuilder endpoints, Microsoft.AspNetCore.Http.RequestDelegate requestDelegate) { throw null; }
|
||||
public static Microsoft.AspNetCore.Builder.IEndpointConventionBuilder MapFallback(this Microsoft.AspNetCore.Routing.IEndpointRouteBuilder endpoints, string pattern, Microsoft.AspNetCore.Http.RequestDelegate requestDelegate) { throw null; }
|
||||
}
|
||||
|
|
|
|||
|
|
@ -12,6 +12,11 @@ namespace Microsoft.AspNetCore.Builder
|
|||
/// </summary>
|
||||
public static class FallbackEndpointRouteBuilderExtensions
|
||||
{
|
||||
/// <summary>
|
||||
/// The default route pattern used by fallback routing. <c>{*path:nonfile}</c>
|
||||
/// </summary>
|
||||
public static readonly string DefaultPattern = "{*path:nonfile}";
|
||||
|
||||
/// <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.
|
||||
|
|
|
|||
Loading…
Reference in New Issue