| File: Hosting\WebAssemblyHostBuilder.cs | Web Access |
| Project: src\aspnetcore\src\Components\WebAssembly\WebAssembly\src\Microsoft.AspNetCore.Components.WebAssembly.csproj (Microsoft.AspNetCore.Components.WebAssembly) |
// Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. using System.Diagnostics.CodeAnalysis; using System.Globalization; using System.Reflection; using Microsoft.AspNetCore.Components.Forms; using Microsoft.AspNetCore.Components.Infrastructure; using Microsoft.AspNetCore.Components.RenderTree; using Microsoft.AspNetCore.Components.Routing; using Microsoft.AspNetCore.Components.Web; using Microsoft.AspNetCore.Components.WebAssembly.Infrastructure; using Microsoft.AspNetCore.Components.WebAssembly.Rendering; using Microsoft.AspNetCore.Components.WebAssembly.Services; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.Configuration.Json; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Hosting; using Microsoft.Extensions.Logging; using Microsoft.JSInterop; using static Microsoft.AspNetCore.Internal.LinkerFlags; namespace Microsoft.AspNetCore.Components.WebAssembly.Hosting; /// <summary> /// A builder for configuring and creating a <see cref="WebAssemblyHost"/>. /// </summary> public sealed class WebAssemblyHostBuilder { private readonly IInternalJSImportMethods _jsMethods; private Func<IServiceProvider> _createServiceProvider; private RootTypeCache? _rootComponentCache; private string? _persistedState; private ServiceProviderOptions? _serviceProviderOptions; [FeatureSwitchDefinition("System.Diagnostics.Metrics.Meter.IsSupported")] internal static bool IsMeterSupported { get; } = AppContext.TryGetSwitch("System.Diagnostics.Metrics.Meter.IsSupported", out var isSupported) ? isSupported : true; internal static bool IsMeterEnabled { get; set; } = IsMeterSupported; /// <summary> /// Creates an instance of <see cref="WebAssemblyHostBuilder"/> using the most common /// conventions and settings. /// </summary> /// <param name="args">The argument passed to the application's main method.</param> /// <returns>A <see cref="WebAssemblyHostBuilder"/>.</returns> [DynamicDependency(nameof(JSInteropMethods.NotifyLocationChanged), typeof(JSInteropMethods))] [DynamicDependency(nameof(JSInteropMethods.NotifyLocationChangingAsync), typeof(JSInteropMethods))] [DynamicDependency(JsonSerialized, typeof(WebEventDescriptor))] // The following dependency prevents HeadOutlet from getting trimmed away in // WebAssembly prerendered apps. [DynamicDependency(Component, typeof(HeadOutlet))] public static WebAssemblyHostBuilder CreateDefault(string[]? args = default) { // We don't use the args for anything right now, but we want to accept them // here so that it shows up this way in the project templates. var builder = new WebAssemblyHostBuilder(InternalJSImportMethods.Instance); WebAssemblyCultureProvider.Initialize(); // Add environment variables to configuration by default. // This aligns WebAssembly behavior with server-side ASP.NET Core applications // where environment variables are automatically included in IConfiguration. builder.Configuration.AddEnvironmentVariables(); // Right now we don't have conventions or behaviors that are specific to this method // however, making this the default for the template allows us to add things like that // in the future, while giving `new WebAssemblyHostBuilder` as an opt-out of opinionated // settings. return builder; } /// <summary> /// Creates an instance of <see cref="WebAssemblyHostBuilder"/> with the minimal configuration. /// </summary> internal WebAssemblyHostBuilder(IInternalJSImportMethods jsMethods) { // Private right now because we don't have much reason to expose it. This can be exposed // in the future if we want to give people a choice between CreateDefault and something // less opinionated. _jsMethods = jsMethods; Configuration = new WebAssemblyHostConfiguration(); RootComponents = new RootComponentMappingCollection(); Services = new ServiceCollection(); Logging = new LoggingBuilder(Services); var assembly = Assembly.GetEntryAssembly(); if (assembly != null) { InitializeRoutingAppContextSwitch(assembly); } InitializeWebAssemblyRenderer(); // Retrieve required attributes from JSRuntimeInvoker InitializeNavigationManager(); InitializeRegisteredRootComponents(); InitializePersistedState(); InitializeDefaultServices(); var hostEnvironment = InitializeEnvironment(); HostEnvironment = hostEnvironment; _createServiceProvider = () => { var isDevelopment = WebAssemblyHostEnvironmentExtensions.IsDevelopment(hostEnvironment); // Use custom options if provided, otherwise use defaults var options = _serviceProviderOptions ?? new ServiceProviderOptions { ValidateScopes = isDevelopment, ValidateOnBuild = isDevelopment }; return Services.BuildServiceProvider(options); }; } private static void InitializeRoutingAppContextSwitch(Assembly assembly) { var assemblyMetadataAttributes = assembly.GetCustomAttributes<AssemblyMetadataAttribute>(); foreach (var ama in assemblyMetadataAttributes) { if (string.Equals(ama.Key, "Microsoft.AspNetCore.Components.Routing.RegexConstraintSupport", StringComparison.Ordinal)) { if (ama.Value != null && string.Equals((string?)ama.Value, "true", StringComparison.OrdinalIgnoreCase)) { AppContext.SetSwitch("Microsoft.AspNetCore.Components.Routing.RegexConstraintSupport", true); } return; } } } [UnconditionalSuppressMessage("Trimming", "IL2072", Justification = "Root components are expected to be defined in assemblies that do not get trimmed.")] private void InitializeRegisteredRootComponents() { var componentsCount = _jsMethods.RegisteredComponents_GetRegisteredComponentsCount(); if (componentsCount == 0) { return; } var registeredComponents = new ComponentMarker[componentsCount]; for (var i = 0; i < componentsCount; i++) { var assembly = _jsMethods.RegisteredComponents_GetAssembly(i); var typeName = _jsMethods.RegisteredComponents_GetTypeName(i); var serializedParameterDefinitions = _jsMethods.RegisteredComponents_GetParameterDefinitions(i); var serializedParameterValues = _jsMethods.RegisteredComponents_GetParameterValues(i); registeredComponents[i] = ComponentMarker.Create(ComponentMarker.WebAssemblyMarkerType, false, null); registeredComponents[i].WriteWebAssemblyData( assembly, typeName, serializedParameterDefinitions, serializedParameterValues); registeredComponents[i].PrerenderId = i.ToString(CultureInfo.InvariantCulture); } _rootComponentCache = new RootTypeCache(); var componentDeserializer = WebAssemblyComponentParameterDeserializer.Instance; foreach (var registeredComponent in registeredComponents) { var componentType = _rootComponentCache.GetRootType(registeredComponent.Assembly!, registeredComponent.TypeName!); if (componentType is null) { throw new InvalidOperationException( $"Root component type '{registeredComponent.TypeName}' could not be found in the assembly '{registeredComponent.Assembly}'. " + $"This is likely a result of trimming (tree shaking)."); } var definitions = WebAssemblyComponentParameterDeserializer.GetParameterDefinitions(registeredComponent.ParameterDefinitions!); var values = WebAssemblyComponentParameterDeserializer.GetParameterValues(registeredComponent.ParameterValues!); var parameters = componentDeserializer.DeserializeParameters(definitions, values); RootComponents.Add(componentType, registeredComponent.PrerenderId!, parameters); } } private void InitializePersistedState() { _persistedState = _jsMethods.GetPersistedState(); } private void InitializeNavigationManager() { var baseUri = _jsMethods.NavigationManager_GetBaseUri(); var uri = _jsMethods.NavigationManager_GetLocationHref(); WebAssemblyNavigationManager.Instance = new WebAssemblyNavigationManager(baseUri, uri); } private WebAssemblyHostEnvironment InitializeEnvironment() { var applicationEnvironment = _jsMethods.GetApplicationEnvironment(); var hostEnvironment = new WebAssemblyHostEnvironment(applicationEnvironment, WebAssemblyNavigationManager.Instance.BaseUri); Services.AddSingleton<IWebAssemblyHostEnvironment>(hostEnvironment); Services.AddSingleton<IHostEnvironment>(sp => new WebAssemblyHostEnvironmentAdapter(sp.GetRequiredService<IWebAssemblyHostEnvironment>())); var configFiles = new[] { "appsettings.json", $"appsettings.{applicationEnvironment}.json" }; foreach (var configFile in configFiles) { if (File.Exists(configFile)) { var appSettingsJson = File.ReadAllBytes(configFile); // Perf: Using this over AddJsonStream. This allows the linker to trim out the "File"-specific APIs and assemblies // for Configuration, of where there are several. Configuration.Add<JsonStreamConfigurationSource>(s => s.Stream = new MemoryStream(appSettingsJson)); } } return hostEnvironment; } private static void InitializeWebAssemblyRenderer() { // note that when this is running in single-threaded context or multi-threaded-CoreCLR unit tests, we don't want to install WebAssemblyDispatcher if (OperatingSystem.IsBrowser()) { var currentThread = Thread.CurrentThread; if (currentThread.IsThreadPoolThread || currentThread.IsBackground) { throw new InvalidOperationException("WebAssemblyHostBuilder needs to be instantiated in the UI thread."); } // capture the JSSynchronizationContext from the main thread, which runtime already installed. // if SynchronizationContext.Current is null, it means we are on the single-threaded runtime // if user somehow installed SynchronizationContext different from JSSynchronizationContext, they need to make sure the behavior is consistent with JSSynchronizationContext. if (WebAssemblyDispatcher._mainSynchronizationContext == null && SynchronizationContext.Current != null) { WebAssemblyDispatcher._mainSynchronizationContext = SynchronizationContext.Current; WebAssemblyDispatcher._mainManagedThreadId = currentThread.ManagedThreadId; } } } /// <summary> /// Gets an <see cref="WebAssemblyHostConfiguration"/> that can be used to customize the application's /// configuration sources and read configuration attributes. /// </summary> public WebAssemblyHostConfiguration Configuration { get; } /// <summary> /// Gets the collection of root component mappings configured for the application. /// </summary> public RootComponentMappingCollection RootComponents { get; } /// <summary> /// Gets the service collection. /// </summary> public IServiceCollection Services { get; } /// <summary> /// Gets information about the app's host environment. /// </summary> public IWebAssemblyHostEnvironment HostEnvironment { get; } /// <summary> /// Gets the logging builder for configuring logging services. /// </summary> public ILoggingBuilder Logging { get; } /// <summary> /// Registers a <see cref="IServiceProviderFactory{TBuilder}" /> instance to be used to create the <see cref="IServiceProvider" />. /// </summary> /// <param name="factory">The <see cref="IServiceProviderFactory{TBuilder}" />.</param> /// <param name="configure"> /// A delegate used to configure the <typeparamref T="TBuilder" />. This can be used to configure services using /// APIS specific to the <see cref="IServiceProviderFactory{TBuilder}" /> implementation. /// </param> /// <typeparam name="TBuilder">The type of builder provided by the <see cref="IServiceProviderFactory{TBuilder}" />.</typeparam> /// <remarks> /// <para> /// <see cref="ConfigureContainer{TBuilder}(IServiceProviderFactory{TBuilder}, Action{TBuilder})"/> is called by <see cref="Build"/> /// and so the delegate provided by <paramref name="configure"/> will run after all other services have been registered. /// </para> /// <para> /// Multiple calls to <see cref="ConfigureContainer{TBuilder}(IServiceProviderFactory{TBuilder}, Action{TBuilder})"/> will replace /// the previously stored <paramref name="factory"/> and <paramref name="configure"/> delegate. /// </para> /// </remarks> public void ConfigureContainer<TBuilder>(IServiceProviderFactory<TBuilder> factory, Action<TBuilder>? configure = null) where TBuilder : notnull { ArgumentNullException.ThrowIfNull(factory); _createServiceProvider = () => { var container = factory.CreateBuilder(Services); configure?.Invoke(container); return factory.CreateServiceProvider(container); }; } // In WebAssemblyHostBuilder class: /// <summary> /// Configures the service provider options for this host builder. /// </summary> /// <param name="options">The service provider options to use.</param> public WebAssemblyHostBuilder UseServiceProviderOptions(ServiceProviderOptions options) { _serviceProviderOptions = options ?? throw new ArgumentNullException(nameof(options)); return this; } /// <summary> /// Builds a <see cref="WebAssemblyHost"/> instance based on the configuration of this builder. /// </summary> /// <returns>A <see cref="WebAssemblyHost"/> object.</returns> public WebAssemblyHost Build() { // Intentionally overwrite configuration with the one we're creating. Services.AddSingleton<IConfiguration>(Configuration); // A Blazor application always runs in a scope. Since we want to make it possible for the user // to configure services inside *that scope* inside their startup code, we create *both* the // service provider and the scope here. var services = _createServiceProvider(); var scope = services.GetRequiredService<IServiceScopeFactory>().CreateAsyncScope(); return new WebAssemblyHost(this, services, scope, _persistedState); } [DynamicDependency(JsonSerialized, typeof(DefaultAntiforgeryStateProvider))] [DynamicDependency(JsonSerialized, typeof(AntiforgeryRequestToken))] internal void InitializeDefaultServices() { Services.AddSingleton<IJSRuntime>(DefaultWebAssemblyJSRuntime.Instance); Services.AddSingleton<NavigationManager>(WebAssemblyNavigationManager.Instance); Services.AddSingleton<INavigationInterception>(WebAssemblyNavigationInterception.Instance); Services.AddSingleton<IScrollToLocationHash>(WebAssemblyScrollToLocationHash.Instance); Services.AddSingleton(_jsMethods); Services.AddSingleton(new LazyAssemblyLoader(DefaultWebAssemblyJSRuntime.Instance)); Services.AddSingleton(_ => _rootComponentCache ?? new()); Services.AddSingleton<ComponentStatePersistenceManager>(); Services.AddSingleton(sp => sp.GetRequiredService<ComponentStatePersistenceManager>().State); Services.AddSupplyValueFromPersistentComponentStateProvider(); Services.AddSingleton<IErrorBoundaryLogger, WebAssemblyErrorBoundaryLogger>(); Services.AddSingleton<ResourceCollectionProvider>(); RegisterPersistentComponentStateServiceCollectionExtensions.AddPersistentServiceRegistration<ResourceCollectionProvider>(Services, RenderMode.InteractiveWebAssembly); Services.AddLogging(builder => { builder.AddProvider(new WebAssemblyConsoleLoggerProvider(DefaultWebAssemblyJSRuntime.Instance)); }); Services.AddSingleton<AntiforgeryStateProvider, DefaultAntiforgeryStateProvider>(); RegisterPersistentComponentStateServiceCollectionExtensions.AddPersistentServiceRegistration<AntiforgeryStateProvider>(Services, RenderMode.InteractiveWebAssembly); Services.AddSingleton<CultureStateProvider>(); RegisterPersistentComponentStateServiceCollectionExtensions.AddPersistentServiceRegistration<CultureStateProvider>(Services, RenderMode.InteractiveWebAssembly); Services.AddSupplyValueFromQueryProvider(); // Register metrics and tracing when supported and not disabled by the feature switch. if (IsMeterSupported && IsMeterEnabled) { ComponentsMetricsServiceCollectionExtensions.AddComponentsMetrics(Services); ComponentsMetricsServiceCollectionExtensions.AddComponentsTracing(Services); } Services.AddSingleton<HostedServiceExecutor>(); } }