66using Microsoft . AspNetCore . Http . Extensions ;
77using Microsoft . AspNetCore . Http . Features ;
88using Microsoft . AspNetCore . NodeServices ;
9- using Microsoft . AspNetCore . SpaServices ;
109using Microsoft . AspNetCore . SpaServices . Prerendering ;
1110using Microsoft . Extensions . DependencyInjection ;
11+ using Microsoft . Net . Http . Headers ;
1212using System ;
13+ using System . Collections . Generic ;
14+ using System . IO ;
15+ using System . Linq ;
16+ using System . Text ;
1317using System . Threading . Tasks ;
1418
1519namespace Microsoft . AspNetCore . Builder
@@ -25,33 +29,18 @@ public static class SpaPrerenderingExtensions
2529 /// <param name="appBuilder">The <see cref="IApplicationBuilder"/>.</param>
2630 /// <param name="entryPoint">The path, relative to your application root, of the JavaScript file containing prerendering logic.</param>
2731 /// <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>
32+ /// <param name="excludeUrls">Optional. If specified, requests within these URL paths will bypass the prerenderer.</param>
2833 public static void UseSpaPrerendering (
2934 this IApplicationBuilder appBuilder ,
3035 string entryPoint ,
31- ISpaPrerendererBuilder buildOnDemand = null )
36+ ISpaPrerendererBuilder buildOnDemand = null ,
37+ string [ ] excludeUrls = null )
3238 {
3339 if ( string . IsNullOrEmpty ( entryPoint ) )
3440 {
3541 throw new ArgumentException ( "Cannot be null or empty" , nameof ( entryPoint ) ) ;
3642 }
3743
38- var defaultPageMiddleware = SpaDefaultPageMiddleware . FindInPipeline ( appBuilder ) ;
39- if ( defaultPageMiddleware == null )
40- {
41- throw new Exception ( $ "{ nameof ( UseSpaPrerendering ) } should be called inside the 'configure' callback of a call to { nameof ( SpaApplicationBuilderExtensions . UseSpa ) } .") ;
42- }
43-
44- var urlPrefix = defaultPageMiddleware . UrlPrefix ;
45- if ( urlPrefix == null || urlPrefix . Length < 2 )
46- {
47- throw new ArgumentException (
48- "If you are using server-side prerendering, the SPA's public path must be " +
49- "set to a non-empty and non-root value. This makes it possible to identify " +
50- "requests for the SPA's internal static resources, so the prerenderer knows " +
51- "not to return prerendered HTML for those requests." ,
52- nameof ( urlPrefix ) ) ;
53- }
54-
5544 // We only want to start one build-on-demand task, but it can't commence until
5645 // a request comes in (because we need to wait for all middleware to be configured)
5746 var lazyBuildOnDemandTask = new Lazy < Task > ( ( ) => buildOnDemand ? . Build ( appBuilder ) ) ;
@@ -64,54 +53,89 @@ public static void UseSpaPrerendering(
6453 var applicationBasePath = serviceProvider . GetRequiredService < IHostingEnvironment > ( )
6554 . ContentRootPath ;
6655 var moduleExport = new JavaScriptModuleExport ( entryPoint ) ;
67- var urlPrefixAsPathString = new PathString ( urlPrefix ) ;
68-
69- // Add the actual middleware that intercepts requests for the SPA default file
70- // and invokes the prerendering code
56+ var excludePathStrings = ( excludeUrls ?? Array . Empty < string > ( ) )
57+ . Select ( url => new PathString ( url ) )
58+ . ToArray ( ) ;
59+
60+ // Capture the non-prerendered responses, which in production will typically only
61+ // be returning the default SPA index.html page (because other resources will be
62+ // served statically from disk). We will use this as a template in which to inject
63+ // the prerendered output.
7164 appBuilder . Use ( async ( context , next ) =>
7265 {
73- // Don't interfere with requests that are within the SPA's urlPrefix, because
74- // these requests are meant to serve its internal resources (.js, .css, etc.)
75- if ( context . Request . Path . StartsWithSegments ( urlPrefixAsPathString ) )
66+ // If this URL is excluded, skip prerendering
67+ foreach ( var excludePathString in excludePathStrings )
7668 {
77- await next ( ) ;
78- return ;
69+ if ( context . Request . Path . StartsWithSegments ( excludePathString ) )
70+ {
71+ await next ( ) ;
72+ return ;
73+ }
7974 }
8075
81- // If we're building on demand, do that first
82- var buildOnDemandTask = lazyBuildOnDemandTask . Value ;
83- if ( buildOnDemandTask != null && ! buildOnDemandTask . IsCompleted )
84- {
85- await buildOnDemandTask ;
86- }
76+ // It's no good if we try to return a 304. We need to capture the actual
77+ // HTML content so it can be passed as a template to the prerenderer.
78+ RemoveConditionalRequestHeaders ( context . Request ) ;
8779
88- // As a workaround for @angular/cli not emitting the index.html in 'server'
89- // builds, pass through a URL that can be used for obtaining it. Longer term,
90- // remove this.
91- var customData = new
80+ using ( var outputBuffer = new MemoryStream ( ) )
9281 {
93- templateUrl = GetDefaultFileAbsoluteUrl ( context , defaultPageMiddleware . DefaultPageUrl )
94- } ;
95-
96- // TODO: Add an optional "supplyCustomData" callback param so people using
97- // UsePrerendering() can, for example, pass through cookies into the .ts code
98-
99- var ( unencodedAbsoluteUrl , unencodedPathAndQuery ) = GetUnencodedUrlAndPathQuery ( context ) ;
100- var renderResult = await Prerenderer . RenderToString (
101- applicationBasePath ,
102- nodeServices ,
103- applicationStoppingToken ,
104- moduleExport ,
105- unencodedAbsoluteUrl ,
106- unencodedPathAndQuery ,
107- customDataParameter : customData ,
108- timeoutMilliseconds : 0 ,
109- requestPathBase : context . Request . PathBase . ToString ( ) ) ;
110-
111- await ApplyRenderResult ( context , renderResult ) ;
82+ var originalResponseStream = context . Response . Body ;
83+ context . Response . Body = outputBuffer ;
84+
85+ try
86+ {
87+ await next ( ) ;
88+ outputBuffer . Seek ( 0 , SeekOrigin . Begin ) ;
89+ }
90+ finally
91+ {
92+ context . Response . Body = originalResponseStream ;
93+ }
94+
95+ // If we're building on demand, do that first
96+ var buildOnDemandTask = lazyBuildOnDemandTask . Value ;
97+ if ( buildOnDemandTask != null && ! buildOnDemandTask . IsCompleted )
98+ {
99+ await buildOnDemandTask ;
100+ }
101+
102+ // Most prerendering logic will want to know about the original, unprerendered
103+ // HTML that the client would be getting otherwise. Typically this is used as
104+ // a template from which the fully prerendered page can be generated.
105+ var customData = new Dictionary < string , object >
106+ {
107+ { "originalHtml" , Encoding . UTF8 . GetString ( outputBuffer . GetBuffer ( ) ) }
108+ } ;
109+
110+ // TODO: Add an optional "supplyCustomData" callback param so people using
111+ // UsePrerendering() can, for example, pass through cookies into the .ts code
112+
113+ var ( unencodedAbsoluteUrl , unencodedPathAndQuery ) = GetUnencodedUrlAndPathQuery ( context ) ;
114+ var renderResult = await Prerenderer . RenderToString (
115+ applicationBasePath ,
116+ nodeServices ,
117+ applicationStoppingToken ,
118+ moduleExport ,
119+ unencodedAbsoluteUrl ,
120+ unencodedPathAndQuery ,
121+ customDataParameter : customData ,
122+ timeoutMilliseconds : 0 ,
123+ requestPathBase : context . Request . PathBase . ToString ( ) ) ;
124+
125+ await ApplyRenderResult ( context , renderResult ) ;
126+ }
112127 } ) ;
113128 }
114129
130+ private static void RemoveConditionalRequestHeaders ( HttpRequest request )
131+ {
132+ request . Headers . Remove ( HeaderNames . IfMatch ) ;
133+ request . Headers . Remove ( HeaderNames . IfModifiedSince ) ;
134+ request . Headers . Remove ( HeaderNames . IfNoneMatch ) ;
135+ request . Headers . Remove ( HeaderNames . IfUnmodifiedSince ) ;
136+ request . Headers . Remove ( HeaderNames . IfRange ) ;
137+ }
138+
115139 private static ( string , string ) GetUnencodedUrlAndPathQuery ( HttpContext httpContext )
116140 {
117141 // This is a duplicate of code from Prerenderer.cs in the SpaServices package.
@@ -128,6 +152,8 @@ private static (string, string) GetUnencodedUrlAndPathQuery(HttpContext httpCont
128152
129153 private static async Task ApplyRenderResult ( HttpContext context , RenderToStringResult renderResult )
130154 {
155+ context . Response . Clear ( ) ;
156+
131157 if ( ! string . IsNullOrEmpty ( renderResult . RedirectUrl ) )
132158 {
133159 context . Response . Redirect ( renderResult . RedirectUrl ) ;
0 commit comments