Skip to content
This repository was archived by the owner on Apr 8, 2020. It is now read-only.

Commit 30333e2

Browse files
AddSpaStaticFiles/UseSpaStaticFiles APIs to clean up the React template (or other cases where SPA files are outside wwwroot)
1 parent 08002e9 commit 30333e2

7 files changed

+229
-5
lines changed

build/dependencies.props

+1
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515
<MicrosoftAspNetCoreStaticFilesPackageVersion>2.1.0-preview1-27478</MicrosoftAspNetCoreStaticFilesPackageVersion>
1616
<MicrosoftAspNetCoreWebSocketsPackageVersion>2.1.0-preview1-27478</MicrosoftAspNetCoreWebSocketsPackageVersion>
1717
<MicrosoftExtensionsDependencyInjectionPackageVersion>2.1.0-preview1-27478</MicrosoftExtensionsDependencyInjectionPackageVersion>
18+
<MicrosoftExtensionsFileProvidersPhysicalPackageVersion>2.1.0-preview1-27478</MicrosoftExtensionsFileProvidersPhysicalPackageVersion>
1819
<MicrosoftExtensionsLoggingConsolePackageVersion>2.1.0-preview1-27478</MicrosoftExtensionsLoggingConsolePackageVersion>
1920
<MicrosoftExtensionsLoggingDebugPackageVersion>2.1.0-preview1-27478</MicrosoftExtensionsLoggingDebugPackageVersion>
2021
<MicrosoftNETCoreApp20PackageVersion>2.0.0</MicrosoftNETCoreApp20PackageVersion>

src/Microsoft.AspNetCore.SpaServices.Extensions/Microsoft.AspNetCore.SpaServices.Extensions.csproj

+1
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212
<ItemGroup>
1313
<PackageReference Include="Microsoft.AspNetCore.StaticFiles" Version="$(MicrosoftAspNetCoreStaticFilesPackageVersion)" />
1414
<PackageReference Include="Microsoft.AspNetCore.WebSockets" Version="$(MicrosoftAspNetCoreWebSocketsPackageVersion)" />
15+
<PackageReference Include="Microsoft.Extensions.FileProviders.Physical" Version="$(MicrosoftExtensionsFileProvidersPhysicalPackageVersion)" />
1516
</ItemGroup>
1617

1718
</Project>

src/Microsoft.AspNetCore.SpaServices.Extensions/SpaDefaultPageMiddleware.cs

+7-5
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33

44
using Microsoft.AspNetCore.Builder;
55
using Microsoft.AspNetCore.Hosting;
6+
using Microsoft.Extensions.DependencyInjection;
67
using System;
78

89
namespace Microsoft.AspNetCore.SpaServices
@@ -26,11 +27,12 @@ public static void Attach(ISpaBuilder spaBuilder)
2627
return next();
2728
});
2829

29-
// Serve it as file from wwwroot (by default), or any other configured file provider
30-
app.UseStaticFiles(new StaticFileOptions
31-
{
32-
FileProvider = options.DefaultPageFileProvider
33-
});
30+
// Serve it as a static file
31+
// Developers who need to host more than one SPA with distinct default pages can
32+
// override the file provider
33+
app.UseSpaStaticFilesInternal(
34+
overrideFileProvider: options.DefaultPageFileProvider,
35+
allowFallbackOnServingWebRootFiles: true);
3436

3537
// If the default file didn't get served as a static file (usually because it was not
3638
// present on disk), the SPA is definitely not going to work.
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
// Copyright (c) .NET Foundation. All rights reserved.
2+
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
3+
4+
using Microsoft.AspNetCore.Hosting;
5+
using Microsoft.Extensions.DependencyInjection;
6+
using Microsoft.Extensions.FileProviders;
7+
using System;
8+
using System.IO;
9+
10+
namespace Microsoft.AspNetCore.SpaServices.StaticFiles
11+
{
12+
/// <summary>
13+
/// Provides an implementation of <see cref="ISpaStaticFileProvider"/> that supplies
14+
/// physical files at a location configured using <see cref="SpaStaticFilesOptions"/>.
15+
/// </summary>
16+
internal class DefaultSpaStaticFileProvider : ISpaStaticFileProvider
17+
{
18+
private IFileProvider _fileProvider;
19+
20+
public DefaultSpaStaticFileProvider(
21+
IServiceProvider serviceProvider,
22+
SpaStaticFilesOptions options)
23+
{
24+
if (options == null)
25+
{
26+
throw new ArgumentNullException(nameof(options));
27+
}
28+
29+
if (string.IsNullOrEmpty(options.RootPath))
30+
{
31+
throw new ArgumentException($"The {nameof(options.RootPath)} property " +
32+
$"of {nameof(options)} cannot be null or empty.");
33+
}
34+
35+
var env = serviceProvider.GetRequiredService<IHostingEnvironment>();
36+
var absoluteRootPath = Path.Combine(
37+
env.ContentRootPath,
38+
options.RootPath);
39+
40+
// PhysicalFileProvider will throw if you pass a non-existent path,
41+
// but we don't want that scenario to be an error because for SPA
42+
// scenarios, it's better if non-existing directory just means we
43+
// don't serve any static files.
44+
if (Directory.Exists(absoluteRootPath))
45+
{
46+
_fileProvider = new PhysicalFileProvider(absoluteRootPath);
47+
}
48+
}
49+
50+
public IFileProvider FileProvider => _fileProvider;
51+
}
52+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
// Copyright (c) .NET Foundation. All rights reserved.
2+
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
3+
4+
using Microsoft.Extensions.FileProviders;
5+
6+
namespace Microsoft.AspNetCore.SpaServices.StaticFiles
7+
{
8+
/// <summary>
9+
/// Represents a service that can provide static files to be served for a Single Page
10+
/// Application (SPA).
11+
/// </summary>
12+
public interface ISpaStaticFileProvider
13+
{
14+
/// <summary>
15+
/// Gets the file provider, if available, that supplies the static files for the SPA.
16+
/// The value is <c>null</c> if no file provider is available.
17+
/// </summary>
18+
IFileProvider FileProvider { get; }
19+
}
20+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,125 @@
1+
// Copyright (c) .NET Foundation. All rights reserved.
2+
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
3+
4+
using Microsoft.AspNetCore.Builder;
5+
using Microsoft.AspNetCore.SpaServices.StaticFiles;
6+
using Microsoft.Extensions.FileProviders;
7+
using Microsoft.Extensions.Options;
8+
using System;
9+
10+
namespace Microsoft.Extensions.DependencyInjection
11+
{
12+
/// <summary>
13+
/// Extension methods for configuring an application to serve static files for a
14+
/// Single Page Application (SPA).
15+
/// </summary>
16+
public static class SpaStaticFilesExtensions
17+
{
18+
/// <summary>
19+
/// Registers an <see cref="ISpaStaticFileProvider"/> service that can provide static
20+
/// files to be served for a Single Page Application (SPA).
21+
/// </summary>
22+
/// <param name="services">The <see cref="IServiceCollection"/>.</param>
23+
/// <param name="configuration">If specified, this callback will be invoked to set additional configuration options.</param>
24+
public static void AddSpaStaticFiles(
25+
this IServiceCollection services,
26+
Action<SpaStaticFilesOptions> configuration = null)
27+
{
28+
services.AddSingleton<ISpaStaticFileProvider>(serviceProvider =>
29+
{
30+
// Use the options configured in DI (or blank if none was configured)
31+
var optionsProvider = serviceProvider.GetService<IOptions<SpaStaticFilesOptions>>();
32+
var options = optionsProvider.Value;
33+
34+
// Allow the developer to perform further configuration
35+
configuration?.Invoke(options);
36+
37+
if (string.IsNullOrEmpty(options.RootPath))
38+
{
39+
throw new InvalidOperationException($"No {nameof(SpaStaticFilesOptions.RootPath)} " +
40+
$"was set on the {nameof(SpaStaticFilesOptions)}.");
41+
}
42+
43+
return new DefaultSpaStaticFileProvider(serviceProvider, options);
44+
});
45+
}
46+
47+
/// <summary>
48+
/// Configures the application to serve static files for a Single Page Application (SPA).
49+
/// The files will be located using the registered <see cref="ISpaStaticFileProvider"/> service.
50+
/// </summary>
51+
/// <param name="applicationBuilder">The <see cref="IApplicationBuilder"/>.</param>
52+
public static void UseSpaStaticFiles(this IApplicationBuilder applicationBuilder)
53+
{
54+
if (applicationBuilder == null)
55+
{
56+
throw new ArgumentNullException(nameof(applicationBuilder));
57+
}
58+
59+
UseSpaStaticFilesInternal(applicationBuilder,
60+
overrideFileProvider: null,
61+
allowFallbackOnServingWebRootFiles: false);
62+
}
63+
64+
internal static void UseSpaStaticFilesInternal(
65+
this IApplicationBuilder app,
66+
IFileProvider overrideFileProvider,
67+
bool allowFallbackOnServingWebRootFiles)
68+
{
69+
var shouldServeStaticFiles = ShouldServeStaticFiles(
70+
app,
71+
overrideFileProvider,
72+
allowFallbackOnServingWebRootFiles,
73+
out var fileProviderOrDefault);
74+
75+
if (shouldServeStaticFiles)
76+
{
77+
app.UseStaticFiles(new StaticFileOptions
78+
{
79+
FileProvider = fileProviderOrDefault
80+
});
81+
}
82+
}
83+
84+
private static bool ShouldServeStaticFiles(
85+
IApplicationBuilder app,
86+
IFileProvider overrideFileProvider,
87+
bool allowFallbackOnServingWebRootFiles,
88+
out IFileProvider fileProviderOrDefault)
89+
{
90+
if (overrideFileProvider != null)
91+
{
92+
// If the file provider was explicitly supplied, that takes precedence over any other
93+
// configured file provider. This is most useful if the application hosts multiple SPAs
94+
// (via multiple calls to UseSpa()), so each needs to serve its own separate static files
95+
// instead of using AddSpaStaticFiles/UseSpaStaticFiles.
96+
fileProviderOrDefault = overrideFileProvider;
97+
return true;
98+
}
99+
100+
var spaStaticFilesService = app.ApplicationServices.GetService<ISpaStaticFileProvider>();
101+
if (spaStaticFilesService != null)
102+
{
103+
// If an ISpaStaticFileProvider was configured but it says no IFileProvider is available
104+
// (i.e., it supplies 'null'), this implies we should not serve any static files. This
105+
// is typically the case in development when SPA static files are being served from a
106+
// SPA development server (e.g., Angular CLI or create-react-app), in which case no
107+
// directory of prebuilt files will exist on disk.
108+
fileProviderOrDefault = spaStaticFilesService.FileProvider;
109+
return fileProviderOrDefault != null;
110+
}
111+
else if (!allowFallbackOnServingWebRootFiles)
112+
{
113+
throw new InvalidOperationException($"To use {nameof(UseSpaStaticFiles)}, you must " +
114+
$"first register an {nameof(ISpaStaticFileProvider)} in the service provider, typically " +
115+
$"by calling services.{nameof(AddSpaStaticFiles)}.");
116+
}
117+
else
118+
{
119+
// Fall back on serving wwwroot
120+
fileProviderOrDefault = null;
121+
return true;
122+
}
123+
}
124+
}
125+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
// Copyright (c) .NET Foundation. All rights reserved.
2+
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
3+
4+
using Microsoft.Extensions.DependencyInjection;
5+
6+
namespace Microsoft.AspNetCore.SpaServices.StaticFiles
7+
{
8+
/// <summary>
9+
/// Represents options for serving static files for a Single Page Application (SPA).
10+
/// </summary>
11+
public class SpaStaticFilesOptions
12+
{
13+
/// <summary>
14+
/// Gets or sets the path, relative to the application root, of the directory in which
15+
/// the physical files are located.
16+
///
17+
/// If the specified directory does not exist, then the
18+
/// <see cref="SpaStaticFilesExtensions.UseSpaStaticFiles(Builder.IApplicationBuilder)"/>
19+
/// middleware will not serve any static files.
20+
/// </summary>
21+
public string RootPath { get; set; }
22+
}
23+
}

0 commit comments

Comments
 (0)