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; }
+ }
+}