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