| File: BlazorGateway.cs | Web Access |
| Project: src\aspnetcore\src\Components\Gateway\src\Microsoft.AspNetCore.Components.Gateway.csproj (blazor-gateway) |
// Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. using Microsoft.AspNetCore.Diagnostics.HealthChecks; using Microsoft.Extensions.Diagnostics.HealthChecks; using OpenTelemetry; using OpenTelemetry.Metrics; using OpenTelemetry.Trace; namespace Microsoft.AspNetCore.Components.Gateway; /// <summary> /// Intended for framework test use only. /// </summary> public static class BlazorGateway { /// <summary> /// Builds a <see cref="WebApplication"/> configured as a Blazor Gateway. /// Reads ClientApps config section for endpoint manifests and YARP reverse proxy configuration. /// </summary> public static WebApplication BuildWebHost(string[] args) => BuildWebHost(WebApplication.CreateSlimBuilder(args)); internal static WebApplication BuildWebHost(WebApplicationBuilder builder) { var options = new BlazorGatewayOptions(); builder.Configuration.GetSection(BlazorGatewayOptions.SectionName).Bind(options); if (options.Telemetry.Enabled) { builder.ConfigureOpenTelemetry(options.Telemetry); } if (options.HealthChecks.Enabled) { builder.Services.AddHealthChecks() .AddCheck<LivenessHealthCheck>("self", tags: [options.HealthChecks.LivenessTag]); } builder.Services.AddServiceDiscovery(); builder.WebHost.UseKestrelHttpsConfiguration(); builder.WebHost.UseStaticWebAssets(); var appConfigs = builder.Configuration.GetSection("ClientApps") .Get<Dictionary<string, ClientAppConfiguration>>() ?? []; var proxySection = builder.Configuration.GetSection("ReverseProxy"); var hasProxy = proxySection.Exists(); if (hasProxy) { builder.Services.AddReverseProxy() .LoadFromConfig(proxySection) .AddServiceDiscoveryDestinationResolver(); } var app = builder.Build(); // HSTS tells browsers to always use HTTPS for this host, preventing future HTTP requests. // Only enable in non-development to avoid interfering with dev certificates and localhost. // See https://aka.ms/aspnetcore-hsts if (!app.Environment.IsDevelopment() && options.Hsts.Enabled) { app.UseHsts(); } if (options.HttpsRedirection.Enabled) { // Only redirect top-level navigations (browser URL bar) from HTTP to HTTPS. // The Sec-Fetch-Dest header distinguishes navigations from subresource loads // and API fetches. This ensures the served document loads on HTTPS when // available, making subsequent fetch/XHR requests same-origin. // See https://developer.mozilla.org/en-US/docs/Web/HTTP/Reference/Headers/Sec-Fetch-Dest app.UseWhen( context => string.Equals( context.Request.Headers["Sec-Fetch-Dest"].ToString(), "document", StringComparison.OrdinalIgnoreCase), branch => branch.UseHttpsRedirection()); } if (!string.IsNullOrEmpty(options.PathBase)) { app.UsePathBase(options.PathBase); } if (app.Environment.IsDevelopment() && options.HealthChecks.Enabled) { app.MapHealthChecks(options.HealthChecks.Path); } if (options.HealthChecks.Enabled) { app.MapHealthChecks(options.HealthChecks.LivenessPath, new HealthCheckOptions { Predicate = r => r.Tags.Contains(options.HealthChecks.LivenessTag) }); } if (hasProxy) { app.MapReverseProxy(); } foreach (var appConfig in appConfigs.Values) { if (!string.IsNullOrEmpty(appConfig.ConfigEndpointPath) && !string.IsNullOrEmpty(appConfig.ConfigResponse)) { app.MapGet(appConfig.ConfigEndpointPath, () => Results.Content(appConfig.ConfigResponse, "application/json")) .WithMetadata(new ContentEncodingMetadata("identity", 1.0)); } if (!string.IsNullOrEmpty(appConfig.EndpointsManifest)) { app.MapGroup(appConfig.PathPrefix ?? "").MapStaticAssets(appConfig.EndpointsManifest); } } return app; } private static IHostApplicationBuilder ConfigureOpenTelemetry(this IHostApplicationBuilder builder, BlazorGatewayOptions.TelemetryOptions telemetry) { builder.Logging.AddOpenTelemetry(logging => { logging.IncludeFormattedMessage = true; logging.IncludeScopes = true; }); builder.Services.AddOpenTelemetry() .WithMetrics(metrics => { metrics.AddAspNetCoreInstrumentation() .AddHttpClientInstrumentation() .AddRuntimeInstrumentation(); }) .WithTracing(tracing => { tracing.AddSource(builder.Environment.ApplicationName) .AddAspNetCoreInstrumentation(options => options.Filter = context => TelemetryFilters.ShouldTraceInboundRequest(context.Request.Path, telemetry.ExcludePaths)) .AddHttpClientInstrumentation(options => // Filter out the gateway's own OTLP export calls to the dashboard // to prevent a feedback loop (exporting traces creates new traces). options.FilterHttpRequestMessage = request => TelemetryFilters.ShouldTraceOutboundRequest(request.RequestUri, telemetry.ExcludeOutboundPaths)); }); var useOtlpExporter = !string.IsNullOrWhiteSpace(builder.Configuration["OTEL_EXPORTER_OTLP_ENDPOINT"]); if (useOtlpExporter) { builder.Services.AddOpenTelemetry().UseOtlpExporter(); } return builder; } } sealed class ClientAppConfiguration { public string? PathPrefix { get; set; } public string? EndpointsManifest { get; set; } public string? ConfigEndpointPath { get; set; } public string? ConfigResponse { get; set; } } // Liveness check that flips to Unhealthy as soon as the host begins shutting down, // so orchestrators (Kubernetes, ACA) stop routing new requests during the // terminationGracePeriodSeconds drain window while in-flight requests complete. internal sealed class LivenessHealthCheck(IHostApplicationLifetime lifetime) : IHealthCheck { public Task<HealthCheckResult> CheckHealthAsync(HealthCheckContext context, CancellationToken cancellationToken) => Task.FromResult(lifetime.ApplicationStopping.IsCancellationRequested ? HealthCheckResult.Unhealthy("Application is shutting down.") : HealthCheckResult.Healthy()); }