From 30333e250abbfa8ce45cdcdb5b575f0397403a05 Mon Sep 17 00:00:00 2001 From: Steve Sanderson Date: Thu, 9 Nov 2017 14:09:45 -0800 Subject: [PATCH] AddSpaStaticFiles/UseSpaStaticFiles APIs to clean up the React template (or other cases where SPA files are outside wwwroot) --- build/dependencies.props | 1 + ...t.AspNetCore.SpaServices.Extensions.csproj | 1 + .../SpaDefaultPageMiddleware.cs | 12 +- .../DefaultSpaStaticFileProvider.cs | 52 ++++++++ .../StaticFiles/ISpaStaticFileProvider.cs | 20 +++ .../StaticFiles/SpaStaticFilesExtensions.cs | 125 ++++++++++++++++++ .../StaticFiles/SpaStaticFilesOptions.cs | 23 ++++ 7 files changed, 229 insertions(+), 5 deletions(-) create mode 100644 src/Microsoft.AspNetCore.SpaServices.Extensions/StaticFiles/DefaultSpaStaticFileProvider.cs create mode 100644 src/Microsoft.AspNetCore.SpaServices.Extensions/StaticFiles/ISpaStaticFileProvider.cs create mode 100644 src/Microsoft.AspNetCore.SpaServices.Extensions/StaticFiles/SpaStaticFilesExtensions.cs create mode 100644 src/Microsoft.AspNetCore.SpaServices.Extensions/StaticFiles/SpaStaticFilesOptions.cs diff --git a/build/dependencies.props b/build/dependencies.props index 8a5dab0..cb00888 100644 --- a/build/dependencies.props +++ b/build/dependencies.props @@ -15,6 +15,7 @@ 2.1.0-preview1-27478 2.1.0-preview1-27478 2.1.0-preview1-27478 + 2.1.0-preview1-27478 2.1.0-preview1-27478 2.1.0-preview1-27478 2.0.0 diff --git a/src/Microsoft.AspNetCore.SpaServices.Extensions/Microsoft.AspNetCore.SpaServices.Extensions.csproj b/src/Microsoft.AspNetCore.SpaServices.Extensions/Microsoft.AspNetCore.SpaServices.Extensions.csproj index 0388b51..4472217 100644 --- a/src/Microsoft.AspNetCore.SpaServices.Extensions/Microsoft.AspNetCore.SpaServices.Extensions.csproj +++ b/src/Microsoft.AspNetCore.SpaServices.Extensions/Microsoft.AspNetCore.SpaServices.Extensions.csproj @@ -12,6 +12,7 @@ + diff --git a/src/Microsoft.AspNetCore.SpaServices.Extensions/SpaDefaultPageMiddleware.cs b/src/Microsoft.AspNetCore.SpaServices.Extensions/SpaDefaultPageMiddleware.cs index ed2ecb6..d39c49f 100644 --- a/src/Microsoft.AspNetCore.SpaServices.Extensions/SpaDefaultPageMiddleware.cs +++ b/src/Microsoft.AspNetCore.SpaServices.Extensions/SpaDefaultPageMiddleware.cs @@ -3,6 +3,7 @@ using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Hosting; +using Microsoft.Extensions.DependencyInjection; using System; namespace Microsoft.AspNetCore.SpaServices @@ -26,11 +27,12 @@ namespace Microsoft.AspNetCore.SpaServices return next(); }); - // Serve it as file from wwwroot (by default), or any other configured file provider - app.UseStaticFiles(new StaticFileOptions - { - FileProvider = options.DefaultPageFileProvider - }); + // Serve it as a static file + // Developers who need to host more than one SPA with distinct default pages can + // override the file provider + app.UseSpaStaticFilesInternal( + overrideFileProvider: options.DefaultPageFileProvider, + allowFallbackOnServingWebRootFiles: true); // If the default file didn't get served as a static file (usually because it was not // present on disk), the SPA is definitely not going to work. diff --git a/src/Microsoft.AspNetCore.SpaServices.Extensions/StaticFiles/DefaultSpaStaticFileProvider.cs b/src/Microsoft.AspNetCore.SpaServices.Extensions/StaticFiles/DefaultSpaStaticFileProvider.cs new file mode 100644 index 0000000..0c2348f --- /dev/null +++ b/src/Microsoft.AspNetCore.SpaServices.Extensions/StaticFiles/DefaultSpaStaticFileProvider.cs @@ -0,0 +1,52 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using Microsoft.AspNetCore.Hosting; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.FileProviders; +using System; +using System.IO; + +namespace Microsoft.AspNetCore.SpaServices.StaticFiles +{ + /// + /// Provides an implementation of that supplies + /// physical files at a location configured using . + /// + internal class DefaultSpaStaticFileProvider : ISpaStaticFileProvider + { + private IFileProvider _fileProvider; + + public DefaultSpaStaticFileProvider( + IServiceProvider serviceProvider, + SpaStaticFilesOptions options) + { + if (options == null) + { + throw new ArgumentNullException(nameof(options)); + } + + if (string.IsNullOrEmpty(options.RootPath)) + { + throw new ArgumentException($"The {nameof(options.RootPath)} property " + + $"of {nameof(options)} cannot be null or empty."); + } + + var env = serviceProvider.GetRequiredService(); + var absoluteRootPath = Path.Combine( + env.ContentRootPath, + options.RootPath); + + // PhysicalFileProvider will throw if you pass a non-existent path, + // but we don't want that scenario to be an error because for SPA + // scenarios, it's better if non-existing directory just means we + // don't serve any static files. + if (Directory.Exists(absoluteRootPath)) + { + _fileProvider = new PhysicalFileProvider(absoluteRootPath); + } + } + + public IFileProvider FileProvider => _fileProvider; + } +} diff --git a/src/Microsoft.AspNetCore.SpaServices.Extensions/StaticFiles/ISpaStaticFileProvider.cs b/src/Microsoft.AspNetCore.SpaServices.Extensions/StaticFiles/ISpaStaticFileProvider.cs new file mode 100644 index 0000000..c0312d9 --- /dev/null +++ b/src/Microsoft.AspNetCore.SpaServices.Extensions/StaticFiles/ISpaStaticFileProvider.cs @@ -0,0 +1,20 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using Microsoft.Extensions.FileProviders; + +namespace Microsoft.AspNetCore.SpaServices.StaticFiles +{ + /// + /// Represents a service that can provide static files to be served for a Single Page + /// Application (SPA). + /// + public interface ISpaStaticFileProvider + { + /// + /// Gets the file provider, if available, that supplies the static files for the SPA. + /// The value is null if no file provider is available. + /// + IFileProvider FileProvider { get; } + } +} diff --git a/src/Microsoft.AspNetCore.SpaServices.Extensions/StaticFiles/SpaStaticFilesExtensions.cs b/src/Microsoft.AspNetCore.SpaServices.Extensions/StaticFiles/SpaStaticFilesExtensions.cs new file mode 100644 index 0000000..767d997 --- /dev/null +++ b/src/Microsoft.AspNetCore.SpaServices.Extensions/StaticFiles/SpaStaticFilesExtensions.cs @@ -0,0 +1,125 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.SpaServices.StaticFiles; +using Microsoft.Extensions.FileProviders; +using Microsoft.Extensions.Options; +using System; + +namespace Microsoft.Extensions.DependencyInjection +{ + /// + /// Extension methods for configuring an application to serve static files for a + /// Single Page Application (SPA). + /// + public static class SpaStaticFilesExtensions + { + /// + /// Registers an service that can provide static + /// files to be served for a Single Page Application (SPA). + /// + /// The . + /// If specified, this callback will be invoked to set additional configuration options. + public static void AddSpaStaticFiles( + this IServiceCollection services, + Action configuration = null) + { + services.AddSingleton(serviceProvider => + { + // Use the options configured in DI (or blank if none was configured) + var optionsProvider = serviceProvider.GetService>(); + var options = optionsProvider.Value; + + // Allow the developer to perform further configuration + configuration?.Invoke(options); + + if (string.IsNullOrEmpty(options.RootPath)) + { + throw new InvalidOperationException($"No {nameof(SpaStaticFilesOptions.RootPath)} " + + $"was set on the {nameof(SpaStaticFilesOptions)}."); + } + + return new DefaultSpaStaticFileProvider(serviceProvider, options); + }); + } + + /// + /// Configures the application to serve static files for a Single Page Application (SPA). + /// The files will be located using the registered service. + /// + /// The . + public static void UseSpaStaticFiles(this IApplicationBuilder applicationBuilder) + { + if (applicationBuilder == null) + { + throw new ArgumentNullException(nameof(applicationBuilder)); + } + + UseSpaStaticFilesInternal(applicationBuilder, + overrideFileProvider: null, + allowFallbackOnServingWebRootFiles: false); + } + + internal static void UseSpaStaticFilesInternal( + this IApplicationBuilder app, + IFileProvider overrideFileProvider, + bool allowFallbackOnServingWebRootFiles) + { + var shouldServeStaticFiles = ShouldServeStaticFiles( + app, + overrideFileProvider, + allowFallbackOnServingWebRootFiles, + out var fileProviderOrDefault); + + if (shouldServeStaticFiles) + { + app.UseStaticFiles(new StaticFileOptions + { + FileProvider = fileProviderOrDefault + }); + } + } + + private static bool ShouldServeStaticFiles( + IApplicationBuilder app, + IFileProvider overrideFileProvider, + bool allowFallbackOnServingWebRootFiles, + out IFileProvider fileProviderOrDefault) + { + if (overrideFileProvider != null) + { + // If the file provider was explicitly supplied, that takes precedence over any other + // configured file provider. This is most useful if the application hosts multiple SPAs + // (via multiple calls to UseSpa()), so each needs to serve its own separate static files + // instead of using AddSpaStaticFiles/UseSpaStaticFiles. + fileProviderOrDefault = overrideFileProvider; + return true; + } + + var spaStaticFilesService = app.ApplicationServices.GetService(); + if (spaStaticFilesService != null) + { + // If an ISpaStaticFileProvider was configured but it says no IFileProvider is available + // (i.e., it supplies 'null'), this implies we should not serve any static files. This + // is typically the case in development when SPA static files are being served from a + // SPA development server (e.g., Angular CLI or create-react-app), in which case no + // directory of prebuilt files will exist on disk. + fileProviderOrDefault = spaStaticFilesService.FileProvider; + return fileProviderOrDefault != null; + } + else if (!allowFallbackOnServingWebRootFiles) + { + throw new InvalidOperationException($"To use {nameof(UseSpaStaticFiles)}, you must " + + $"first register an {nameof(ISpaStaticFileProvider)} in the service provider, typically " + + $"by calling services.{nameof(AddSpaStaticFiles)}."); + } + else + { + // Fall back on serving wwwroot + fileProviderOrDefault = null; + return true; + } + } + } +} diff --git a/src/Microsoft.AspNetCore.SpaServices.Extensions/StaticFiles/SpaStaticFilesOptions.cs b/src/Microsoft.AspNetCore.SpaServices.Extensions/StaticFiles/SpaStaticFilesOptions.cs new file mode 100644 index 0000000..82bde12 --- /dev/null +++ b/src/Microsoft.AspNetCore.SpaServices.Extensions/StaticFiles/SpaStaticFilesOptions.cs @@ -0,0 +1,23 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using Microsoft.Extensions.DependencyInjection; + +namespace Microsoft.AspNetCore.SpaServices.StaticFiles +{ + /// + /// Represents options for serving static files for a Single Page Application (SPA). + /// + public class SpaStaticFilesOptions + { + /// + /// Gets or sets the path, relative to the application root, of the directory in which + /// the physical files are located. + /// + /// If the specified directory does not exist, then the + /// + /// middleware will not serve any static files. + /// + public string RootPath { get; set; } + } +}