using System; using System.Threading; using System.Threading.Tasks; using Microsoft.AspNetCore.NodeServices; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Http.Features; namespace Microsoft.AspNetCore.SpaServices.Prerendering { /// /// Performs server-side prerendering by invoking code in Node.js. /// public static class Prerenderer { private static readonly object CreateNodeScriptLock = new object(); private static StringAsTempFile NodeScript; internal static Task RenderToString( string applicationBasePath, INodeServices nodeServices, CancellationToken applicationStoppingToken, JavaScriptModuleExport bootModule, HttpContext httpContext, object customDataParameter, int timeoutMilliseconds) { // We want to pass the original, unencoded incoming URL data through to Node, so that // server-side code has the same view of the URL as client-side code (on the client, // location.pathname returns an unencoded string). // The following logic handles special characters in URL paths in the same way that // Node and client-side JS does. For example, the path "/a=b%20c" gets passed through // unchanged (whereas other .NET APIs do change it - Path.Value will return it as // "/a=b c" and Path.ToString() will return it as "/a%3db%20c") var requestFeature = httpContext.Features.Get(); var unencodedPathAndQuery = requestFeature.RawTarget; var request = httpContext.Request; var unencodedAbsoluteUrl = $"{request.Scheme}://{request.Host}{unencodedPathAndQuery}"; return RenderToString( applicationBasePath, nodeServices, applicationStoppingToken, bootModule, unencodedAbsoluteUrl, unencodedPathAndQuery, customDataParameter, timeoutMilliseconds, request.PathBase.ToString()); } /// /// Performs server-side prerendering by invoking code in Node.js. /// /// The root path to your application. This is used when resolving project-relative paths. /// The instance of that will be used to invoke JavaScript code. /// A token that indicates when the host application is stopping. /// The path to the JavaScript file containing the prerendering logic. /// The URL of the currently-executing HTTP request. This is supplied to the prerendering code. /// The path and query part of the URL of the currently-executing HTTP request. This is supplied to the prerendering code. /// An optional JSON-serializable parameter to be supplied to the prerendering code. /// The maximum duration to wait for prerendering to complete. /// The PathBase for the currently-executing HTTP request. /// public static Task RenderToString( string applicationBasePath, INodeServices nodeServices, CancellationToken applicationStoppingToken, JavaScriptModuleExport bootModule, string requestAbsoluteUrl, string requestPathAndQuery, object customDataParameter, int timeoutMilliseconds, string requestPathBase) { return nodeServices.InvokeExportAsync( GetNodeScriptFilename(applicationStoppingToken), "renderToString", applicationBasePath, bootModule, requestAbsoluteUrl, requestPathAndQuery, customDataParameter, timeoutMilliseconds, requestPathBase); } private static string GetNodeScriptFilename(CancellationToken applicationStoppingToken) { lock(CreateNodeScriptLock) { if (NodeScript == null) { var script = EmbeddedResourceReader.Read(typeof(Prerenderer), "/Content/Node/prerenderer.js"); NodeScript = new StringAsTempFile(script, applicationStoppingToken); // Will be cleaned up on process exit } } return NodeScript.FileName; } } }