/// <summary>
        /// Adds middleware for server-side prerendering of a Single Page Application.
        /// </summary>
        /// <param name="spaBuilder">The <see cref="ISpaBuilder"/>.</param>
        /// <param name="entryPoint">The path, relative to your application root, of the JavaScript file containing prerendering logic.</param>
        /// <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>
        public static void UsePrerendering(
            this ISpaBuilder spaBuilder,
            string entryPoint,
            ISpaPrerendererBuilder buildOnDemand = null)
        {
            if (string.IsNullOrEmpty(entryPoint))
            {
                throw new ArgumentException("Cannot be null or empty", nameof(entryPoint));
            }

            // We only want to start one build-on-demand task, but it can't commence until
            // a request comes in (because we need to wait for all middleware to be configured)
            var lazyBuildOnDemandTask = new Lazy <Task>(() => buildOnDemand?.Build(spaBuilder));

            var appBuilder  = spaBuilder.AppBuilder;
            var prerenderer = GetPrerenderer(appBuilder.ApplicationServices);

            // Add the actual middleware that intercepts requests for the SPA default file
            // and invokes the prerendering code
            appBuilder.Use(async(context, next) =>
            {
                // Don't interfere with requests that aren't meant to render the SPA default file
                if (!context.Items.ContainsKey(SpaExtensions.IsSpaFallbackRequestTag))
                {
                    await next();
                    return;
                }

                // If we're building on demand, do that first
                var buildOnDemandTask = lazyBuildOnDemandTask.Value;
                if (buildOnDemandTask != null && !buildOnDemandTask.IsCompleted)
                {
                    await buildOnDemandTask;
                }

                // If we're waiting for other SPA initialization tasks, do that first.
                await spaBuilder.StartupTasks;

                // As a workaround for @angular/cli not emitting the index.html in 'server'
                // builds, pass through a URL that can be used for obtaining it. Longer term,
                // remove this.
                var customData = new
                {
                    templateUrl = GetDefaultFileAbsoluteUrl(spaBuilder, context)
                };

                // TODO: Add an optional "supplyCustomData" callback param so people using
                //       UsePrerendering() can, for example, pass through cookies into the .ts code

                var renderResult = await prerenderer.RenderToString(
                    entryPoint,
                    customDataParameter: customData);
                await ApplyRenderResult(context, renderResult);
            });
        }
        /// <summary>
        /// Enables server-side prerendering middleware for a Single Page Application.
        /// </summary>
        /// <param name="appBuilder">The <see cref="IApplicationBuilder"/>.</param>
        /// <param name="entryPoint">The path, relative to your application root, of the JavaScript file containing prerendering logic.</param>
        /// <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>
        public static void UseSpaPrerendering(
            this IApplicationBuilder appBuilder,
            string entryPoint,
            ISpaPrerendererBuilder buildOnDemand = null)
        {
            if (string.IsNullOrEmpty(entryPoint))
            {
                throw new ArgumentException("Cannot be null or empty", nameof(entryPoint));
            }

            var defaultPageMiddleware = SpaDefaultPageMiddleware.FindInPipeline(appBuilder);

            if (defaultPageMiddleware == null)
            {
                throw new Exception($"{nameof(UseSpaPrerendering)} should be called inside the 'configure' callback of a call to {nameof(SpaApplicationBuilderExtensions.UseSpa)}.");
            }

            var urlPrefix = defaultPageMiddleware.UrlPrefix;

            if (urlPrefix == null || urlPrefix.Length < 2)
            {
                throw new ArgumentException(
                          "If you are using server-side prerendering, the SPA's public path must be " +
                          "set to a non-empty and non-root value. This makes it possible to identify " +
                          "requests for the SPA's internal static resources, so the prerenderer knows " +
                          "not to return prerendered HTML for those requests.",
                          nameof(urlPrefix));
            }

            // We only want to start one build-on-demand task, but it can't commence until
            // a request comes in (because we need to wait for all middleware to be configured)
            var lazyBuildOnDemandTask = new Lazy <Task>(() => buildOnDemand?.Build(appBuilder));

            // Get all the necessary context info that will be used for each prerendering call
            var serviceProvider          = appBuilder.ApplicationServices;
            var nodeServices             = GetNodeServices(serviceProvider);
            var applicationStoppingToken = serviceProvider.GetRequiredService <IApplicationLifetime>()
                                           .ApplicationStopping;
            var applicationBasePath = serviceProvider.GetRequiredService <IHostingEnvironment>()
                                      .ContentRootPath;
            var moduleExport          = new JavaScriptModuleExport(entryPoint);
            var urlPrefixAsPathString = new PathString(urlPrefix);

            // Add the actual middleware that intercepts requests for the SPA default file
            // and invokes the prerendering code
            appBuilder.Use(async(context, next) =>
            {
                // Don't interfere with requests that are within the SPA's urlPrefix, because
                // these requests are meant to serve its internal resources (.js, .css, etc.)
                if (context.Request.Path.StartsWithSegments(urlPrefixAsPathString))
                {
                    await next();
                    return;
                }

                // If we're building on demand, do that first
                var buildOnDemandTask = lazyBuildOnDemandTask.Value;
                if (buildOnDemandTask != null && !buildOnDemandTask.IsCompleted)
                {
                    await buildOnDemandTask;
                }

                // As a workaround for @angular/cli not emitting the index.html in 'server'
                // builds, pass through a URL that can be used for obtaining it. Longer term,
                // remove this.
                var customData = new
                {
                    templateUrl = GetDefaultFileAbsoluteUrl(context, defaultPageMiddleware.DefaultPageUrl)
                };

                // TODO: Add an optional "supplyCustomData" callback param so people using
                //       UsePrerendering() can, for example, pass through cookies into the .ts code

                var renderResult = await Prerenderer.RenderToString(
                    applicationBasePath,
                    nodeServices,
                    applicationStoppingToken,
                    moduleExport,
                    context,
                    customDataParameter: customData,
                    timeoutMilliseconds: 0);

                await ApplyRenderResult(context, renderResult);
            });
        }
        /// <summary>
        /// Enables server-side prerendering middleware for a Single Page Application.
        /// </summary>
        /// <param name="appBuilder">The <see cref="IApplicationBuilder"/>.</param>
        /// <param name="entryPoint">The path, relative to your application root, of the JavaScript file containing prerendering logic.</param>
        /// <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>
        /// <param name="excludeUrls">Optional. If specified, requests within these URL paths will bypass the prerenderer.</param>
        /// <param name="supplyData">Optional. If specified, this callback will be invoked during prerendering, allowing you to pass additional data to the prerendering entrypoint code.</param>
        public static void UseSpaPrerendering(
            this IApplicationBuilder appBuilder,
            string entryPoint,
            ISpaPrerendererBuilder buildOnDemand = null,
            string[] excludeUrls = null,
            Action <HttpContext, IDictionary <string, object> > supplyData = null)
        {
            if (string.IsNullOrEmpty(entryPoint))
            {
                throw new ArgumentException("Cannot be null or empty", nameof(entryPoint));
            }

            // If we're building on demand, start that process now
            var buildOnDemandTask = buildOnDemand?.Build(appBuilder);

            // Get all the necessary context info that will be used for each prerendering call
            var serviceProvider          = appBuilder.ApplicationServices;
            var nodeServices             = GetNodeServices(serviceProvider);
            var applicationStoppingToken = serviceProvider.GetRequiredService <IApplicationLifetime>()
                                           .ApplicationStopping;
            var applicationBasePath = serviceProvider.GetRequiredService <IHostingEnvironment>()
                                      .ContentRootPath;
            var moduleExport       = new JavaScriptModuleExport(entryPoint);
            var excludePathStrings = (excludeUrls ?? Array.Empty <string>())
                                     .Select(url => new PathString(url))
                                     .ToArray();

            // 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.
            appBuilder.Use(async(context, next) =>
            {
                // If this URL is excluded, skip prerendering
                foreach (var excludePathString in excludePathStrings)
                {
                    if (context.Request.Path.StartsWithSegments(excludePathString))
                    {
                        await next();
                        return;
                    }
                }

                // If we're building on demand, do that first
                if (buildOnDemandTask != null)
                {
                    await buildOnDemandTask;
                }

                // 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);

                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 it's not a success response, we're not going to have any template HTML
                    // to pass to the prerenderer.
                    if (context.Response.StatusCode < 200 || context.Response.StatusCode >= 300)
                    {
                        var message = $"Prerendering failed because no HTML template could be obtained. Check that your SPA is compiling without errors. The {nameof(SpaApplicationBuilderExtensions.UseSpa)}() middleware returned a response with status code {context.Response.StatusCode}";
                        if (outputBuffer.Length > 0)
                        {
                            message += " and the following content: " + Encoding.UTF8.GetString(outputBuffer.GetBuffer());
                        }

                        throw new InvalidOperationException(message);
                    }

                    // 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()) }
                    };

                    supplyData?.Invoke(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 ApplyRenderResult(context, renderResult);
                }
            });
        }