diff --git a/src/Components/Blazor/DevServer/src/Server/Startup.cs b/src/Components/Blazor/DevServer/src/Server/Startup.cs index d5de91abb8..257d6327c6 100644 --- a/src/Components/Blazor/DevServer/src/Server/Startup.cs +++ b/src/Components/Blazor/DevServer/src/Server/Startup.cs @@ -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(); - 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"); diff --git a/src/Components/Blazor/Server/ref/Microsoft.AspNetCore.Blazor.Server.netcoreapp3.0.cs b/src/Components/Blazor/Server/ref/Microsoft.AspNetCore.Blazor.Server.netcoreapp3.0.cs index deb4cfda2d..74cf8a6a8b 100644 --- a/src/Components/Blazor/Server/ref/Microsoft.AspNetCore.Blazor.Server.netcoreapp3.0.cs +++ b/src/Components/Blazor/Server/ref/Microsoft.AspNetCore.Blazor.Server.netcoreapp3.0.cs @@ -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(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(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(this Microsoft.AspNetCore.Routing.IEndpointRouteBuilder endpoints, string filePath) { throw null; } + public static Microsoft.AspNetCore.Builder.IEndpointConventionBuilder MapFallbackToClientSideBlazor(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 { } } - } } diff --git a/src/Components/Blazor/Server/src/BlazorConfig.cs b/src/Components/Blazor/Server/src/BlazorConfig.cs index 79bf6ac6bb..5438a7e36a 100644 --- a/src/Components/Blazor/Server/src/BlazorConfig.cs +++ b/src/Components/Blazor/Server/src/BlazorConfig.cs @@ -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; + } } } diff --git a/src/Components/Blazor/Server/src/Builder/BlazorApplicationBuilderExtensions.cs b/src/Components/Blazor/Server/src/Builder/BlazorApplicationBuilderExtensions.cs deleted file mode 100644 index 42e0ee2bb8..0000000000 --- a/src/Components/Blazor/Server/src/Builder/BlazorApplicationBuilderExtensions.cs +++ /dev/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 -{ - /// - /// Provides extension methods that add Blazor-related middleware to the ASP.NET pipeline. - /// - public static class BlazorApplicationBuilderExtensions - { - const string DevServerApplicationName = "blazor-devserver"; - - /// - /// Configures the middleware pipeline to work with Blazor. - /// - /// Any type from the client app project. This is used to identify the client app assembly. - /// The . - /// The . - public static IApplicationBuilder UseBlazor( - this IApplicationBuilder app) - { - var clientAssemblyInServerBinDir = typeof(TProgram).Assembly; - return app.UseBlazor(new BlazorOptions - { - ClientAssemblyPath = clientAssemblyInServerBinDir.Location, - }); - } - - /// - /// Configures the middleware pipeline to work with Blazor. - /// - /// The . - /// Options to configure the middleware. - /// The . - 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); - } - } - } -} diff --git a/src/Components/Blazor/Server/src/Builder/BlazorHostingApplicationBuilderExtensions.cs b/src/Components/Blazor/Server/src/Builder/BlazorHostingApplicationBuilderExtensions.cs new file mode 100644 index 0000000000..74cb3c7b21 --- /dev/null +++ b/src/Components/Blazor/Server/src/Builder/BlazorHostingApplicationBuilderExtensions.cs @@ -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 +{ + /// + /// Provides extension methods for hosting client-side Blazor applications in ASP.NET Core. + /// + public static class BlazorHostingApplicationBuilderExtensions + { + /// + /// Adds a that will serve static files from the client-side Blazor application + /// specified by . + /// + /// A type in the client-side application. + /// The . + /// The . + public static IApplicationBuilder UseClientSideBlazorFiles(this IApplicationBuilder app) + { + if (app == null) + { + throw new ArgumentNullException(nameof(app)); + } + + UseClientSideBlazorFiles(app, typeof(TClientApp).Assembly.Location); + return app; + } + + /// + /// Adds a that will serve static files from the client-side Blazor application + /// specified by . + /// + /// The file path of the client-side Blazor application assembly. + /// The . + /// The . + public static IApplicationBuilder UseClientSideBlazorFiles(this IApplicationBuilder app, string clientAssemblyFilePath) + { + if (clientAssemblyFilePath == null) + { + throw new ArgumentNullException(nameof(clientAssemblyFilePath)); + } + + var fileProviders = new List(); + + // 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); + } + } + } + } +} diff --git a/src/Components/Blazor/Server/src/Builder/BlazorHostingEndpointRouteBuilderExtensions.cs b/src/Components/Blazor/Server/src/Builder/BlazorHostingEndpointRouteBuilderExtensions.cs new file mode 100644 index 0000000000..f899f2576f --- /dev/null +++ b/src/Components/Blazor/Server/src/Builder/BlazorHostingEndpointRouteBuilderExtensions.cs @@ -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 +{ + /// + /// Provides extension methods for hosting client-side Blazor applications in ASP.NET Core. + /// + public static class BlazorHostingEndpointRouteBuilderExtensions + { + /// + /// Adds a low-priority endpoint that will serve the the file specified by from the client-side + /// Blazor application specified by . + /// + /// A type in the client-side application. + /// The . + /// + /// The relative path to the entry point of the client-side application. The path is relative to the + /// , commonly wwwroot. + /// + /// The . + /// + /// + /// 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. + /// + /// + public static IEndpointConventionBuilder MapFallbackToClientSideBlazor(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); + } + + /// + /// Adds a low-priority endpoint that will serve the the file specified by from the client-side + /// Blazor application specified by . + /// + /// The . + /// The file path of the client-side Blazor application assembly. + /// + /// The relative path to the entry point of the client-side application. The path is relative to the + /// , commonly wwwroot. + /// + /// The . + /// + /// + /// 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. + /// + /// + 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); + } + + /// + /// Adds a low-priority endpoint that will serve the the file specified by from the client-side + /// Blazor application specified by . + /// + /// A type in the client-side application. + /// The . + /// The route pattern to match. + /// + /// The relative path to the entry point of the client-side application. The path is relative to the + /// , commonly wwwroot. + /// + /// The . + /// + /// + /// 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. + /// + /// + public static IEndpointConventionBuilder MapFallbackToClientSideBlazor(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); + } + + /// + /// Adds a low-priority endpoint that will serve the the file specified by from the client-side + /// Blazor application specified by . + /// + /// The file path of the client-side Blazor application assembly. + /// The . + /// The route pattern to match. + /// + /// The relative path to the entry point of the client-side application. The path is relative to the + /// , commonly wwwroot. + /// + /// The . + /// + /// + /// 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. + /// + /// + 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().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); + } + } +} diff --git a/src/Components/Blazor/Server/src/Builder/BlazorOptions.cs b/src/Components/Blazor/Server/src/Builder/BlazorOptions.cs deleted file mode 100644 index 2b60b1a0de..0000000000 --- a/src/Components/Blazor/Server/src/Builder/BlazorOptions.cs +++ /dev/null @@ -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 -{ - /// - /// Provides configuration options to the - /// middleware. - /// - public class BlazorOptions - { - /// - /// Full path to the client assembly. - /// - public string ClientAssemblyPath { get; set; } - } -} diff --git a/src/Components/Blazor/Templates/src/content/BlazorHosted-CSharp/BlazorHosted-CSharp.Server/Startup.cs b/src/Components/Blazor/Templates/src/content/BlazorHosted-CSharp/BlazorHosted-CSharp.Server/Startup.cs index 6dfe8c1bb2..acb05b6294 100644 --- a/src/Components/Blazor/Templates/src/content/BlazorHosted-CSharp/BlazorHosted-CSharp.Server/Startup.cs +++ b/src/Components/Blazor/Templates/src/content/BlazorHosted-CSharp/BlazorHosted-CSharp.Server/Startup.cs @@ -33,14 +33,15 @@ namespace BlazorHosted_CSharp.Server app.UseBlazorDebugging(); } + app.UseBlazorClientSideFiles(); + app.UseRouting(); app.UseEndpoints(endpoints => { endpoints.MapDefaultControllerRoute(); + endpoints.MapFallbackToClientSideBlazor("index.html"); }); - - app.UseBlazor(); } } } diff --git a/src/Components/Blazor/testassets/HostedInAspNet.Server/Startup.cs b/src/Components/Blazor/testassets/HostedInAspNet.Server/Startup.cs index 87b023f95b..24e99f9691 100644 --- a/src/Components/Blazor/testassets/HostedInAspNet.Server/Startup.cs +++ b/src/Components/Blazor/testassets/HostedInAspNet.Server/Startup.cs @@ -25,7 +25,15 @@ namespace HostedInAspNet.Server app.UseBlazorDebugging(); } - app.UseBlazor(); + app.UseStaticFiles(); + app.UseClientSideBlazorFiles(); + + app.UseRouting(); + + app.UseEndpoints(endpoints => + { + endpoints.MapFallbackToClientSideBlazor("index.html"); + }); } } } diff --git a/src/Components/Blazor/testassets/MonoSanity/Startup.cs b/src/Components/Blazor/testassets/MonoSanity/Startup.cs index 6457a5fce2..84614a890a 100644 --- a/src/Components/Blazor/testassets/MonoSanity/Startup.cs +++ b/src/Components/Blazor/testassets/MonoSanity/Startup.cs @@ -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(); + app.UseFileServer(new FileServerOptions() { EnableDefaultFiles = true, }); + app.UseStaticFiles(); + app.UseClientSideBlazorFiles(); + app.UseRouting(); + app.UseEndpoints(endpoints => + { + endpoints.MapFallbackToClientSideBlazor("index.html"); + }); } } } diff --git a/src/Components/test/E2ETest/Infrastructure/ServerFixtures/AspNetSiteServerFixture.cs b/src/Components/test/E2ETest/Infrastructure/ServerFixtures/AspNetSiteServerFixture.cs index 7d3e3c3ad8..dfadf38985 100644 --- a/src/Components/test/E2ETest/Infrastructure/ServerFixtures/AspNetSiteServerFixture.cs +++ b/src/Components/test/E2ETest/Infrastructure/ServerFixtures/AspNetSiteServerFixture.cs @@ -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[] { diff --git a/src/Components/test/E2ETest/Tests/ClientSideHostingTest.cs b/src/Components/test/E2ETest/Tests/ClientSideHostingTest.cs new file mode 100644 index 0000000000..3097d06dca --- /dev/null +++ b/src/Components/test/E2ETest/Tests/ClientSideHostingTest.cs @@ -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 +{ + /// + /// 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. + /// + public class ClientSideHostingTest : ServerTestBase + { + 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(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..."); + } + } +} diff --git a/src/Components/test/testassets/TestServer/Program.cs b/src/Components/test/testassets/TestServer/Program.cs index aaad4cef42..fc2a4d4d2e 100644 --- a/src/Components/test/testassets/TestServer/Program.cs +++ b/src/Components/test/testassets/TestServer/Program.cs @@ -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(args); + + public static IWebHost BuildWebHost(string[] args) where TStartup : class => WebHost.CreateDefaultBuilder(args) .UseConfiguration(new ConfigurationBuilder() .AddCommandLine(args) .Build()) - .UseStartup() + .UseStartup() .Build(); } } diff --git a/src/Components/test/testassets/TestServer/Startup.cs b/src/Components/test/testassets/TestServer/Startup.cs index 1e06fad17e..18acb716a6 100644 --- a/src/Components/test/testassets/TestServer/Startup.cs +++ b/src/Components/test/testassets/TestServer/Startup.cs @@ -49,11 +49,12 @@ namespace TestServer .AllowCredentials(); }); - app.UseRouting(); // Mount the server-side Blazor app on /subdir app.Map("/subdir", subdirApp => { + subdirApp.UseClientSideBlazorFiles(); + // The following two lines are equivalent to: // endpoints.MapComponentsHub(); // @@ -66,13 +67,12 @@ namespace TestServer subdirApp.UseEndpoints(endpoints => { endpoints.MapHub(ComponentHub.DefaultPath).AddComponent(typeof(Index), selector: "root"); + endpoints.MapFallbackToClientSideBlazor("index.html"); }); - - subdirApp.MapWhen( - ctx => ctx.Features.Get()?.Endpoint == null, - blazorBuilder => blazorBuilder.UseBlazor()); }); + app.UseRouting(); + app.UseEndpoints(endpoints => { endpoints.MapControllers(); diff --git a/src/Components/test/testassets/TestServer/StartupWithMapFallbackToClientSideBlazor.cs b/src/Components/test/testassets/TestServer/StartupWithMapFallbackToClientSideBlazor.cs new file mode 100644 index 0000000000..483dd00d78 --- /dev/null +++ b/src/Components/test/testassets/TestServer/StartupWithMapFallbackToClientSideBlazor.cs @@ -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(); + }); + + // 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("index.html"); + }); + }); + + app.Map("/pattern_filepath", app => + { + app.UseRouting(); + + app.UseEndpoints(endpoints => + { + endpoints.MapFallbackToClientSideBlazor("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"); + }); + }); + } + } +} diff --git a/src/Http/Routing/ref/Microsoft.AspNetCore.Routing.netcoreapp3.0.cs b/src/Http/Routing/ref/Microsoft.AspNetCore.Routing.netcoreapp3.0.cs index 5dae735d85..fc0a07386f 100644 --- a/src/Http/Routing/ref/Microsoft.AspNetCore.Routing.netcoreapp3.0.cs +++ b/src/Http/Routing/ref/Microsoft.AspNetCore.Routing.netcoreapp3.0.cs @@ -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; } } diff --git a/src/Http/Routing/src/Builder/FallbackEndpointRouteBuilderExtensions.cs b/src/Http/Routing/src/Builder/FallbackEndpointRouteBuilderExtensions.cs index 354d0e400e..ac907a1f5b 100644 --- a/src/Http/Routing/src/Builder/FallbackEndpointRouteBuilderExtensions.cs +++ b/src/Http/Routing/src/Builder/FallbackEndpointRouteBuilderExtensions.cs @@ -12,6 +12,11 @@ namespace Microsoft.AspNetCore.Builder /// public static class FallbackEndpointRouteBuilderExtensions { + /// + /// The default route pattern used by fallback routing. {*path:nonfile} + /// + public static readonly string DefaultPattern = "{*path:nonfile}"; + /// /// Adds a specialized to the that will match /// requests for non-file-names with the lowest possible priority.