internal static Task <RenderToStringResult> 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 <IHttpRequestFeature>();
            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()));
        }
 public static Task <RenderToStringResult> RenderToString(
     string applicationBasePath,
     INodeServices nodeServices,
     CancellationToken applicationStoppingToken,
     JavaScriptModuleExport bootModule,
     string requestAbsoluteUrl,
     string requestPathAndQuery,
     object customDataParameter,
     int timeoutMilliseconds,
     string requestPathBase)
 {
     return(nodeServices.InvokeExportAsync <RenderToStringResult>(
                GetNodeScriptFilename(applicationStoppingToken),
                "renderToString",
                applicationBasePath,
                bootModule,
                requestAbsoluteUrl,
                requestPathAndQuery,
                customDataParameter,
                timeoutMilliseconds,
                requestPathBase));
 }
Exemple #3
0
        /// <summary>
        /// Enables server-side prerendering middleware for a Single Page Application.
        /// </summary>
        /// <param name="spaBuilder">The <see cref="ISpaBuilder"/>.</param>
        /// <param name="configuration">Supplies configuration for the prerendering middleware.</param>
        public static IApplicationBuilder UseSpaPrerendering(
            this ISpaBuilder spaBuilder,
            Action <MintPlayer.AspNetCore.Builder.SpaPrerenderingOptions> configuration)
        {
            if (spaBuilder == null)
            {
                throw new ArgumentNullException(nameof(spaBuilder));
            }

            if (configuration == null)
            {
                throw new ArgumentNullException(nameof(configuration));
            }

            var options = new Builder.SpaPrerenderingOptions();

            configuration.Invoke(options);

            var capturedBootModulePath = options.BootModulePath;

            if (string.IsNullOrEmpty(capturedBootModulePath))
            {
                throw new InvalidOperationException($"To use {nameof(UseSpaPrerendering)}, you " +
                                                    $"must set a nonempty value on the ${nameof(SpaPrerenderingOptions.BootModulePath)} " +
                                                    $"property on the ${nameof(SpaPrerenderingOptions)}.");
            }

            // If we're building on demand, start that process in the background now
            var buildOnDemandTask = options.BootModuleBuilder?.Build(spaBuilder);

            // Get all the necessary context info that will be used for each prerendering call
            var applicationBuilder       = spaBuilder.ApplicationBuilder;
            var serviceProvider          = applicationBuilder.ApplicationServices;
            var nodeServices             = GetNodeServices(serviceProvider);
            var applicationStoppingToken = serviceProvider.GetRequiredService <IHostApplicationLifetime>()
                                           .ApplicationStopping;
            var applicationBasePath = serviceProvider.GetRequiredService <IWebHostEnvironment>()
                                      .ContentRootPath;
            var moduleExport       = new JavaScriptModuleExport(capturedBootModulePath);
            var excludePathStrings = (options.ExcludeUrls ?? Array.Empty <string>())
                                     .Select(url => new PathString(url))
                                     .ToArray();
            var buildTimeout = spaBuilder.Options.StartupTimeout;

            applicationBuilder.Use(async(context, next) =>
            {
                context.Response.OnStarting(async() =>
                {
                    if (options.OnPrepareResponse != null)
                    {
                        await options.OnPrepareResponse(context);
                    }
                });

                // If this URL is excluded, skip prerendering.
                // This is typically used to ensure that static client-side resources
                // (e.g., /dist/*.css) are served normally or through SPA development
                // middleware, and don't return the prerendered index.html page.
                foreach (var excludePathString in excludePathStrings)
                {
                    if (context.Request.Path.StartsWithSegments(excludePathString))
                    {
                        await next();
                        return;
                    }
                }

                // If we're building on demand, wait for that to finish, or raise any build errors
                if (buildOnDemandTask != null && !buildOnDemandTask.IsCompleted)
                {
                    // For better debuggability, create a per-request timeout that makes it clear if the
                    // prerendering builder took too long for this request, but without aborting the
                    // underlying build task so that subsequent requests could still work.
                    await buildOnDemandTask.WithTimeout(buildTimeout,
                                                        $"The prerendering build process did not complete within the " +
                                                        $"timeout period of {buildTimeout.Seconds} seconds. " +
                                                        $"Check the log output for error information.");
                }

                // It's no good if we try to return a 304. We need to capture the actual
                // HTML content so it can be passed as a template to the prerenderer.
                RemoveConditionalRequestHeaders(context.Request);

                // Make sure we're not capturing compressed content, because then we'd have
                // to decompress it. Since this sub-request isn't leaving the machine, there's
                // little to no benefit in having compression on it.
                var originalAcceptEncodingValue = GetAndRemoveAcceptEncodingHeader(context.Request);

                // Capture the non-prerendered responses, which in production will typically only
                // be returning the default SPA index.html page (because other resources will be
                // served statically from disk). We will use this as a template in which to inject
                // the prerendered output.
                using (var outputBuffer = new MemoryStream())
                {
                    var originalResponseStream = context.Response.Body;
                    context.Response.Body      = outputBuffer;

                    try
                    {
                        await next();
                        outputBuffer.Seek(0, SeekOrigin.Begin);
                    }
                    finally
                    {
                        context.Response.Body = originalResponseStream;

                        if (!string.IsNullOrEmpty(originalAcceptEncodingValue))
                        {
                            context.Request.Headers[HeaderNames.AcceptEncoding] = originalAcceptEncodingValue;
                        }
                    }

                    // If it isn't an HTML page that we can use as the template for prerendering,
                    //  - ... because it's not text/html
                    //  - ... or because it's an error
                    // then prerendering doesn't apply to this request, so just pass through the
                    // response as-is. Note that the non-text/html case is not an error: this is
                    // typically how the SPA dev server responses for static content are returned
                    // in development mode.
                    var canPrerender = IsSuccessStatusCode(context.Response.StatusCode) &&
                                       IsHtmlContentType(context.Response.ContentType);
                    if (!canPrerender)
                    {
                        await outputBuffer.CopyToAsync(context.Response.Body);
                        return;
                    }

                    // Most prerendering logic will want to know about the original, unprerendered
                    // HTML that the client would be getting otherwise. Typically this is used as
                    // a template from which the fully prerendered page can be generated.
                    var customData = new Dictionary <string, object>
                    {
                        { "originalHtml", Encoding.UTF8.GetString(outputBuffer.GetBuffer()) }
                    };

                    // If the developer wants to use custom logic to pass arbitrary data to the
                    // prerendering JS code (e.g., to pass through cookie data), now's their chance
                    var spaPrerenderingService = context.RequestServices.GetService <Services.ISpaPrerenderingService>();
                    if (spaPrerenderingService != null)
                    {
                        await spaPrerenderingService.OnSupplyData(context, customData);
                    }

                    var(unencodedAbsoluteUrl, unencodedPathAndQuery)
                        = GetUnencodedUrlAndPathQuery(context);
                    var renderResult = await Prerenderer.RenderToString(
                        applicationBasePath,
                        nodeServices,
                        applicationStoppingToken,
                        moduleExport,
                        unencodedAbsoluteUrl,
                        unencodedPathAndQuery,
                        customDataParameter: customData,
                        timeoutMilliseconds: 0,
                        requestPathBase: context.Request.PathBase.ToString());

                    await ServePrerenderResult(context, renderResult);
                }
            });
            return(applicationBuilder);
        }