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

Commit 8d33fa5

Browse files
Avoid the need to pass urlPrefix/defaultPage to any middleware but one, enforcing a particular structure of middleware
1 parent e12c0ee commit 8d33fa5

File tree

6 files changed

+164
-111
lines changed

6 files changed

+164
-111
lines changed

src/Microsoft.AspNetCore.SpaServices/AngularCli/AngularCliMiddleware.cs

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@ internal class AngularCliMiddleware
2222
private readonly INodeServices _nodeServices;
2323
private readonly string _middlewareScriptPath;
2424

25-
public AngularCliMiddleware(IApplicationBuilder appBuilder, string sourcePath, string urlPrefix, string defaultPage)
25+
public AngularCliMiddleware(IApplicationBuilder appBuilder, string sourcePath, SpaDefaultPageMiddleware defaultPageMiddleware)
2626
{
2727
if (string.IsNullOrEmpty(sourcePath))
2828
{
@@ -39,7 +39,7 @@ public AngularCliMiddleware(IApplicationBuilder appBuilder, string sourcePath, s
3939
// Proxy the corresponding requests through ASP.NET and into the Node listener
4040
// Anything under /<publicpath> (e.g., /dist) is proxied as a normal HTTP request
4141
// with a typical timeout (100s is the default from HttpClient).
42-
UseProxyToLocalAngularCliMiddleware(appBuilder, urlPrefix, defaultPage,
42+
UseProxyToLocalAngularCliMiddleware(appBuilder, defaultPageMiddleware,
4343
angularCliServerInfoTask, TimeSpan.FromSeconds(100));
4444

4545
// Advertise the availability of this feature to other SPA middleware
@@ -105,7 +105,7 @@ private async Task<AngularCliServerInfo> StartAngularCliServerAsync()
105105
}
106106

107107
private static void UseProxyToLocalAngularCliMiddleware(
108-
IApplicationBuilder appBuilder, string urlPrefix, string defaultPage,
108+
IApplicationBuilder appBuilder, SpaDefaultPageMiddleware defaultPageMiddleware,
109109
Task<AngularCliServerInfo> serverInfoTask, TimeSpan requestTimeout)
110110
{
111111
// This is hardcoded to use http://localhost because:
@@ -119,8 +119,8 @@ private static void UseProxyToLocalAngularCliMiddleware(
119119

120120
// Requests outside /<urlPrefix> are proxied to the default page
121121
var hasRewrittenUrlMarker = new object();
122-
var defaultPageUrl = SpaDefaultPageExtensions.GetDefaultPageUrl(
123-
urlPrefix, defaultPage);
122+
var defaultPageUrl = defaultPageMiddleware.DefaultPageUrl;
123+
var urlPrefix = defaultPageMiddleware.UrlPrefix;
124124
var urlPrefixIsRoot = string.IsNullOrEmpty(urlPrefix) || urlPrefix == "/";
125125
appBuilder.Use((context, next) =>
126126
{

src/Microsoft.AspNetCore.SpaServices/AngularCli/AngularCliMiddlewareExtensions.cs

Lines changed: 9 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
33

44
using Microsoft.AspNetCore.Builder;
5+
using System;
56

67
namespace Microsoft.AspNetCore.SpaServices.AngularCli
78
{
@@ -19,16 +20,18 @@ public static class AngularCliMiddlewareExtensions
1920
/// sure not to enable the Angular CLI server.
2021
/// </summary>
2122
/// <param name="app">The <see cref="IApplicationBuilder"/>.</param>
22-
/// <param name="urlPrefix">The URL prefix from which your SPA's files are served. This needs to match the <c>--deploy-url</c> option passed to Angular CLI.</param>
2323
/// <param name="sourcePath">The disk path, relative to the current directory, of the directory containing the SPA source files. When Angular CLI executes, this will be its working directory.</param>
24-
/// <param name="defaultPage">Optional. Specifies the URL, relative to <paramref name="urlPrefix"/>, of the default HTML page that starts up your SPA. Defaults to <c>index.html</c>.</param>
2524
public static void UseAngularCliServer(
2625
this IApplicationBuilder app,
27-
string urlPrefix,
28-
string sourcePath,
29-
string defaultPage = null)
26+
string sourcePath)
3027
{
31-
new AngularCliMiddleware(app, sourcePath, urlPrefix, defaultPage);
28+
var defaultPageMiddleware = SpaDefaultPageMiddleware.FindInPipeline(app);
29+
if (defaultPageMiddleware == null)
30+
{
31+
throw new Exception($"{nameof(UseAngularCliServer)} should be called inside the 'configue' callback of a call to {nameof(SpaApplicationBuilderExtensions.UseSpa)}.");
32+
}
33+
34+
new AngularCliMiddleware(app, sourcePath, defaultPageMiddleware);
3235
}
3336
}
3437
}

src/Microsoft.AspNetCore.SpaServices/Prerendering/SpaPrerenderingExtensions.cs

Lines changed: 10 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
using Microsoft.AspNetCore.Http;
66
using Microsoft.AspNetCore.Http.Extensions;
77
using Microsoft.AspNetCore.NodeServices;
8+
using Microsoft.AspNetCore.SpaServices;
89
using Microsoft.AspNetCore.SpaServices.Prerendering;
910
using Microsoft.Extensions.DependencyInjection;
1011
using System;
@@ -22,21 +23,24 @@ public static class SpaPrerenderingExtensions
2223
/// </summary>
2324
/// <param name="appBuilder">The <see cref="IApplicationBuilder"/>.</param>
2425
/// <param name="entryPoint">The path, relative to your application root, of the JavaScript file containing prerendering logic.</param>
25-
/// <param name="urlPrefix">The URL prefix from which your SPA's files are served.</param>
2626
/// <param name="buildOnDemand">Optional. If specified, executes the supplied <see cref="ISpaPrerendererBuilder"/> before looking for the <paramref name="entryPoint"/> file. This is only intended to be used during development.</param>
27-
/// <param name="defaultPage">Optional. Specifies the URL, relative to <paramref name="urlPrefix"/>, of the default HTML page that starts up your SPA. Defaults to <c>index.html</c>.</param>
2827
public static void UseSpaPrerendering(
2928
this IApplicationBuilder appBuilder,
3029
string entryPoint,
31-
string urlPrefix,
32-
ISpaPrerendererBuilder buildOnDemand = null,
33-
string defaultPage = null)
30+
ISpaPrerendererBuilder buildOnDemand = null)
3431
{
3532
if (string.IsNullOrEmpty(entryPoint))
3633
{
3734
throw new ArgumentException("Cannot be null or empty", nameof(entryPoint));
3835
}
3936

37+
var defaultPageMiddleware = SpaDefaultPageMiddleware.FindInPipeline(appBuilder);
38+
if (defaultPageMiddleware == null)
39+
{
40+
throw new Exception($"{nameof(UseSpaPrerendering)} should be called inside the 'configure' callback of a call to {nameof(SpaApplicationBuilderExtensions.UseSpa)}.");
41+
}
42+
43+
var urlPrefix = defaultPageMiddleware.UrlPrefix;
4044
if (urlPrefix == null || urlPrefix.Length < 2)
4145
{
4246
throw new ArgumentException(
@@ -59,8 +63,6 @@ public static void UseSpaPrerendering(
5963
var applicationBasePath = serviceProvider.GetRequiredService<IHostingEnvironment>()
6064
.ContentRootPath;
6165
var moduleExport = new JavaScriptModuleExport(entryPoint);
62-
var defaultPageUrl = SpaDefaultPageExtensions.GetDefaultPageUrl(
63-
urlPrefix, defaultPage);
6466
var urlPrefixAsPathString = new PathString(urlPrefix);
6567

6668
// Add the actual middleware that intercepts requests for the SPA default file
@@ -87,7 +89,7 @@ public static void UseSpaPrerendering(
8789
// remove this.
8890
var customData = new
8991
{
90-
templateUrl = GetDefaultFileAbsoluteUrl(context, defaultPageUrl)
92+
templateUrl = GetDefaultFileAbsoluteUrl(context, defaultPageMiddleware.DefaultPageUrl)
9193
};
9294

9395
// TODO: Add an optional "supplyCustomData" callback param so people using
Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
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.SpaServices;
5+
using System;
6+
7+
namespace Microsoft.AspNetCore.Builder
8+
{
9+
/// <summary>
10+
/// Provides extension methods used for configuring an application to
11+
/// host a client-side Single Page Application (SPA).
12+
/// </summary>
13+
public static class SpaApplicationBuilderExtensions
14+
{
15+
/// <summary>
16+
/// Handles all requests from this point in the middleware chain by returning
17+
/// the default page for the Single Page Application (SPA).
18+
///
19+
/// This middleware should be placed late in the chain, so that other middleware
20+
/// for serving static files, MVC actions, etc., takes precedence.
21+
/// </summary>
22+
/// <param name="app">The <see cref="IApplicationBuilder"/>.</param>
23+
/// <param name="urlPrefix">
24+
/// The URL path, relative to your application's <c>PathBase</c>, from which the
25+
/// SPA files are served.
26+
///
27+
/// For example, if your SPA files are located in <c>wwwroot/dist</c>, then
28+
/// the value should usually be <c>"dist"</c>, because that is the URL prefix
29+
/// from which browsers can request those files.
30+
/// </param>
31+
/// <param name="defaultPage">
32+
/// Optional. If specified, configures the path (relative to <paramref name="urlPrefix"/>)
33+
/// of the default page that hosts your SPA user interface.
34+
/// If not specified, the default value is <c>"index.html"</c>.
35+
/// </param>
36+
/// <param name="configure">
37+
/// Optional. If specified, this callback will be invoked so that additional middleware
38+
/// can be registered within the context of this SPA.
39+
/// </param>
40+
public static void UseSpa(
41+
this IApplicationBuilder app,
42+
string urlPrefix,
43+
string defaultPage = null,
44+
Action configure = null)
45+
{
46+
new SpaDefaultPageMiddleware(app, urlPrefix, defaultPage, configure);
47+
}
48+
}
49+
}

src/Microsoft.AspNetCore.SpaServices/SpaDefaultPageExtensions.cs

Lines changed: 0 additions & 92 deletions
This file was deleted.
Lines changed: 91 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,91 @@
1+
using Microsoft.AspNetCore.Builder;
2+
using Microsoft.AspNetCore.Hosting;
3+
using Microsoft.AspNetCore.Http;
4+
using System;
5+
6+
namespace Microsoft.AspNetCore.SpaServices
7+
{
8+
internal class SpaDefaultPageMiddleware
9+
{
10+
private static readonly string _propertiesKey = Guid.NewGuid().ToString();
11+
12+
public static SpaDefaultPageMiddleware FindInPipeline(IApplicationBuilder app)
13+
{
14+
return app.Properties.TryGetValue(_propertiesKey, out var instance)
15+
? (SpaDefaultPageMiddleware)instance
16+
: null;
17+
}
18+
19+
public string UrlPrefix { get; }
20+
public string DefaultPageUrl { get; }
21+
22+
public SpaDefaultPageMiddleware(IApplicationBuilder app, string urlPrefix,
23+
string defaultPage, Action configure)
24+
{
25+
if (app == null)
26+
{
27+
throw new ArgumentNullException(nameof(app));
28+
}
29+
30+
UrlPrefix = urlPrefix ?? throw new ArgumentNullException(nameof(urlPrefix));
31+
DefaultPageUrl = ConstructDefaultPageUrl(urlPrefix, defaultPage);
32+
33+
// Attach to pipeline, but invoke 'configure' to give the developer a chance
34+
// to insert extra middleware before the 'default page' pipeline entries
35+
RegisterSoleInstanceInPipeline(app);
36+
configure?.Invoke();
37+
AttachMiddlewareToPipeline(app);
38+
}
39+
40+
private void RegisterSoleInstanceInPipeline(IApplicationBuilder app)
41+
{
42+
if (app.Properties.ContainsKey(_propertiesKey))
43+
{
44+
throw new Exception($"Only one usage of {nameof(SpaApplicationBuilderExtensions.UseSpa)} is allowed in any single branch of the middleware pipeline. This is because one instance would handle all requests.");
45+
}
46+
47+
app.Properties[_propertiesKey] = this;
48+
}
49+
50+
private void AttachMiddlewareToPipeline(IApplicationBuilder app)
51+
{
52+
// Rewrite all requests to the default page
53+
app.Use((context, next) =>
54+
{
55+
context.Request.Path = DefaultPageUrl;
56+
return next();
57+
});
58+
59+
// Serve it as file from disk
60+
app.UseStaticFiles();
61+
62+
// If the default file didn't get served as a static file (because it
63+
// was not present on disk), the SPA is definitely not going to work.
64+
app.Use((context, next) =>
65+
{
66+
var message = $"The SPA default page middleware could not return the default page '{DefaultPageUrl}' because it was not found on disk, and no other middleware handled the request.\n";
67+
68+
// Try to clarify the common scenario where someone runs an application in
69+
// Production environment without first publishing the whole application
70+
// or at least building the SPA.
71+
var hostEnvironment = (IHostingEnvironment)context.RequestServices.GetService(typeof(IHostingEnvironment));
72+
if (hostEnvironment != null && hostEnvironment.IsProduction())
73+
{
74+
message += "Your application is running in Production mode, so make sure it has been published, or that you have built your SPA manually. Alternatively you may wish to switch to the Development environment.\n";
75+
}
76+
77+
throw new Exception(message);
78+
});
79+
}
80+
81+
private static string ConstructDefaultPageUrl(string urlPrefix, string defaultPage)
82+
{
83+
if (string.IsNullOrEmpty(defaultPage))
84+
{
85+
defaultPage = "index.html";
86+
}
87+
88+
return new PathString(urlPrefix).Add(new PathString("/" + defaultPage));
89+
}
90+
}
91+
}

0 commit comments

Comments
 (0)