/// <summary>
        /// Handles requests by passing them through to an instance of the Angular CLI server.
        /// This means you can always serve up-to-date CLI-built resources without having
        /// to run the Angular CLI server manually.
        ///
        /// This feature should only be used in development. For production deployments, be
        /// sure not to enable the Angular CLI server.
        /// </summary>
        /// <param name="app">The <see cref="IApplicationBuilder"/>.</param>
        /// <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>
        public static void UseAngularCliServer(
            this IApplicationBuilder app,
            string sourcePath)
        {
            var defaultPageMiddleware = SpaDefaultPageMiddleware.FindInPipeline(app);

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

            new AngularCliMiddleware(app, sourcePath, defaultPageMiddleware);
        }
Esempio n. 2
0
    /// <summary>
    /// Handles all requests from this point in the middleware chain by returning
    /// the default page for the Single Page Application (SPA).
    ///
    /// This middleware should be placed late in the chain, so that other middleware
    /// for serving static files, MVC actions, etc., takes precedence.
    /// </summary>
    /// <param name="app">The <see cref="IApplicationBuilder"/>.</param>
    /// <param name="configuration">
    /// This callback will be invoked so that additional middleware can be registered within
    /// the context of this SPA.
    /// </param>
    public static void UseSpa(this IApplicationBuilder app, Action <ISpaBuilder> configuration)
    {
        if (configuration == null)
        {
            throw new ArgumentNullException(nameof(configuration));
        }

        // Use the options configured in DI (or blank if none was configured). We have to clone it
        // otherwise if you have multiple UseSpa calls, their configurations would interfere with one another.
        var optionsProvider = app.ApplicationServices.GetService <IOptions <SpaOptions> >() !;
        var options         = new SpaOptions(optionsProvider.Value);

        var spaBuilder = new DefaultSpaBuilder(app, options);

        configuration.Invoke(spaBuilder);
        SpaDefaultPageMiddleware.Attach(spaBuilder);
    }
        /// <summary>
        /// Handles all requests from this point in the middleware chain by returning
        /// the default page for the Single Page Application (SPA).
        ///
        /// This middleware should be placed late in the chain, so that other middleware
        /// for serving static files, MVC actions, etc., takes precedence.
        /// </summary>
        /// <param name="app">The <see cref="IApplicationBuilder"/>.</param>
        /// <param name="urlPrefix">
        /// The URL path, relative to your application's <c>PathBase</c>, from which the
        /// SPA files are served.
        ///
        /// For example, if your SPA files are located in <c>wwwroot/dist</c>, then
        /// the value should usually be <c>"dist"</c>, because that is the URL prefix
        /// from which browsers can request those files.
        /// </param>
        /// <param name="sourcePath">
        /// Optional. If specified, configures the path (relative to the application working
        /// directory) of the directory that holds the SPA source files during development.
        /// The directory need not exist once the application is published.
        /// </param>
        /// <param name="defaultPage">
        /// Optional. If specified, configures the path (relative to <paramref name="urlPrefix"/>)
        /// of the default page that hosts your SPA user interface.
        /// If not specified, the default value is <c>"index.html"</c>.
        /// </param>
        /// <param name="configure">
        /// Optional. If specified, this callback will be invoked so that additional middleware
        /// can be registered within the context of this SPA.
        /// </param>
        public static void UseSpa(
            this IApplicationBuilder app,
            string urlPrefix,
            string sourcePath              = null,
            string defaultPage             = null,
            Action <ISpaOptions> configure = null)
        {
            var spaOptions = new DefaultSpaOptions(sourcePath, urlPrefix);

            spaOptions.RegisterSoleInstanceInPipeline(app);

            // Invoke 'configure' to give the developer a chance to insert extra
            // middleware before the 'default page' pipeline entries
            configure?.Invoke(spaOptions);

            SpaDefaultPageMiddleware.Attach(app, spaOptions);
        }
Esempio n. 4
0
        private static void UseProxyToLocalAngularCliMiddleware(
            IApplicationBuilder appBuilder, SpaDefaultPageMiddleware defaultPageMiddleware,
            Task <AngularCliServerInfo> serverInfoTask, TimeSpan requestTimeout)
        {
            // This is hardcoded to use http://localhost because:
            // - the requests are always from the local machine (we're not accepting remote
            //   requests that go directly to the Angular CLI middleware server)
            // - given that, there's no reason to use https, and we couldn't even if we
            //   wanted to, because in general the Angular CLI server has no certificate
            var proxyOptionsTask = serverInfoTask.ContinueWith(
                task => new ConditionalProxyMiddlewareTarget(
                    "http", "localhost", task.Result.Port.ToString()));

            // Requests outside /<urlPrefix> are proxied to the default page
            var hasRewrittenUrlMarker = new object();
            var defaultPageUrl        = defaultPageMiddleware.DefaultPageUrl;
            var urlPrefix             = defaultPageMiddleware.UrlPrefix;
            var urlPrefixIsRoot       = string.IsNullOrEmpty(urlPrefix) || urlPrefix == "/";

            appBuilder.Use((context, next) =>
            {
                if (!urlPrefixIsRoot && !context.Request.Path.StartsWithSegments(urlPrefix))
                {
                    context.Items[hasRewrittenUrlMarker] = context.Request.Path;
                    context.Request.Path = defaultPageUrl;
                }

                return(next());
            });

            appBuilder.UseMiddleware <ConditionalProxyMiddleware>(urlPrefix, requestTimeout, proxyOptionsTask);

            // If we rewrote the path, rewrite it back. Don't want to interfere with
            // any other middleware.
            appBuilder.Use((context, next) =>
            {
                if (context.Items.ContainsKey(hasRewrittenUrlMarker))
                {
                    context.Request.Path = (PathString)context.Items[hasRewrittenUrlMarker];
                    context.Items.Remove(hasRewrittenUrlMarker);
                }

                return(next());
            });
        }
        public AngularCliMiddleware(
            IApplicationBuilder appBuilder,
            string sourcePath,
            SpaDefaultPageMiddleware defaultPageMiddleware)
        {
            if (string.IsNullOrEmpty(sourcePath))
            {
                throw new ArgumentException("Cannot be null or empty", nameof(sourcePath));
            }

            // Prepare to make calls into Node
            _nodeServices         = CreateNodeServicesInstance(appBuilder, sourcePath);
            _middlewareScriptPath = GetAngularCliMiddlewareScriptPath(appBuilder);

            // Start Angular CLI and attach to middleware pipeline
            var angularCliServerInfoTask = StartAngularCliServerAsync();

            // Everything we proxy is hardcoded to target http://localhost because:
            // - the requests are always from the local machine (we're not accepting remote
            //   requests that go directly to the Angular CLI middleware server)
            // - given that, there's no reason to use https, and we couldn't even if we
            //   wanted to, because in general the Angular CLI server has no certificate
            var proxyOptionsTask = angularCliServerInfoTask.ContinueWith(
                task => new ConditionalProxyMiddlewareTarget(
                    "http", "localhost", task.Result.Port.ToString()));

            var applicationStoppingToken = GetStoppingToken(appBuilder);

            // Proxy all requests into the Angular CLI server
            appBuilder.Use(async(context, next) =>
            {
                var didProxyRequest = await ConditionalProxy.PerformProxyRequest(
                    context, _neverTimeOutHttpClient, proxyOptionsTask, applicationStoppingToken);

                // Since we are proxying everything, this is the end of the middleware pipeline.
                // We won't call next().
                if (!didProxyRequest)
                {
                    context.Response.StatusCode = 404;
                }
            });

            // Advertise the availability of this feature to other SPA middleware
            appBuilder.Properties.Add(AngularCliMiddlewareKey, this);
        }
Esempio n. 6
0
        public AngularCliMiddleware(IApplicationBuilder appBuilder, string sourcePath, SpaDefaultPageMiddleware defaultPageMiddleware)
        {
            if (string.IsNullOrEmpty(sourcePath))
            {
                throw new ArgumentException("Cannot be null or empty", nameof(sourcePath));
            }

            // Prepare to make calls into Node
            _nodeServices         = CreateNodeServicesInstance(appBuilder, sourcePath);
            _middlewareScriptPath = GetAngularCliMiddlewareScriptPath(appBuilder);

            // Start Angular CLI and attach to middleware pipeline
            var angularCliServerInfoTask = StartAngularCliServerAsync();

            // Proxy the corresponding requests through ASP.NET and into the Node listener
            // Anything under /<publicpath> (e.g., /dist) is proxied as a normal HTTP request
            // with a typical timeout (100s is the default from HttpClient).
            UseProxyToLocalAngularCliMiddleware(appBuilder, defaultPageMiddleware,
                                                angularCliServerInfoTask, TimeSpan.FromSeconds(100));

            // Advertise the availability of this feature to other SPA middleware
            appBuilder.Properties.Add(AngularCliMiddlewareKey, this);
        }
        /// <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);
            });
        }