From ec06c1cdf1bf2f903c3607d95762c095ccb45328 Mon Sep 17 00:00:00 2001 From: sbwalker Date: Tue, 26 Aug 2025 15:27:35 -0400 Subject: [PATCH] optimize startup --- .../ApplicationBuilderExtensions.cs | 91 +++++++ .../OqtaneServiceCollectionExtensions.cs | 141 +++++++++- .../Infrastructure/LocalizationManager.cs | 6 + Oqtane.Server/Startup.cs | 241 +----------------- 4 files changed, 241 insertions(+), 238 deletions(-) diff --git a/Oqtane.Server/Extensions/ApplicationBuilderExtensions.cs b/Oqtane.Server/Extensions/ApplicationBuilderExtensions.cs index 3e31ac60..fa3835ea 100644 --- a/Oqtane.Server/Extensions/ApplicationBuilderExtensions.cs +++ b/Oqtane.Server/Extensions/ApplicationBuilderExtensions.cs @@ -2,14 +2,105 @@ using System; using System.Linq; using System.Reflection; using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Cors.Infrastructure; using Microsoft.AspNetCore.Hosting; +using Microsoft.AspNetCore.Http; +using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Hosting; +using Microsoft.Net.Http.Headers; +using Oqtane.Components; using Oqtane.Infrastructure; +using Oqtane.Shared; +using Oqtane.UI; +using OqtaneSSR.Extensions; namespace Oqtane.Extensions { public static class ApplicationBuilderExtensions { + public static IApplicationBuilder UseOqtane(this IApplicationBuilder app, IConfigurationRoot configuration, IWebHostEnvironment environment, ICorsService corsService, ICorsPolicyProvider corsPolicyProvider, ISyncManager sync) + { + ServiceActivator.Configure(app.ApplicationServices); + + if (environment.IsDevelopment()) + { + app.UseWebAssemblyDebugging(); + app.UseForwardedHeaders(); + } + else + { + app.UseForwardedHeaders(); + app.UseExceptionHandler("/Error", createScopeForErrors: true); + // The default HSTS value is 30 days. You may want to change this for production scenarios, see https://aka.ms/aspnetcore-hsts. + app.UseHsts(); + } + + // allow oqtane localization middleware + app.UseOqtaneLocalization(); + + app.UseHttpsRedirection(); + app.UseStaticFiles(new StaticFileOptions + { + OnPrepareResponse = (ctx) => + { + // static asset caching + var cachecontrol = configuration.GetSection("CacheControl"); + if (!string.IsNullOrEmpty(cachecontrol.Value)) + { + ctx.Context.Response.Headers.Append(HeaderNames.CacheControl, cachecontrol.Value); + } + // CORS headers for .NET MAUI clients + var policy = corsPolicyProvider.GetPolicyAsync(ctx.Context, Constants.MauiCorsPolicy) + .ConfigureAwait(false).GetAwaiter().GetResult(); + corsService.ApplyResult(corsService.EvaluatePolicy(ctx.Context, policy), ctx.Context.Response); + } + }); + app.UseExceptionMiddleWare(); + app.UseTenantResolution(); + app.UseJwtAuthorization(); + app.UseRouting(); + app.UseCors(); + app.UseOutputCache(); + app.UseAuthentication(); + app.UseAuthorization(); + app.UseAntiforgery(); + + // execute any IServerStartup logic + app.ConfigureOqtaneAssemblies(environment); + + if (configuration.GetSection("UseSwagger").Value != "false") + { + app.UseSwagger(); + app.UseSwaggerUI(c => { c.SwaggerEndpoint("/swagger/" + Constants.Version + "/swagger.json", Constants.PackageId + " " + Constants.Version); }); + } + + app.UseEndpoints(endpoints => + { + endpoints.MapControllers(); + endpoints.MapRazorPages(); + }); + + app.UseEndpoints(endpoints => + { + endpoints.MapRazorComponents() + .AddInteractiveServerRenderMode() + .AddInteractiveWebAssemblyRenderMode() + .AddAdditionalAssemblies(typeof(SiteRouter).Assembly); + }); + + // simulate the fallback routing approach of traditional Blazor - allowing the custom SiteRouter to handle all routing concerns + app.UseEndpoints(endpoints => + { + endpoints.MapFallback(); + }); + + // create a global sync event to identify server application startup + sync.AddSyncEvent(-1, -1, EntityNames.Host, -1, SyncEventActions.Reload); + + return app; + } + public static IApplicationBuilder ConfigureOqtaneAssemblies(this IApplicationBuilder app, IWebHostEnvironment env) { var startUps = AppDomain.CurrentDomain diff --git a/Oqtane.Server/Extensions/OqtaneServiceCollectionExtensions.cs b/Oqtane.Server/Extensions/OqtaneServiceCollectionExtensions.cs index 20a11ced..ccd9f3a3 100644 --- a/Oqtane.Server/Extensions/OqtaneServiceCollectionExtensions.cs +++ b/Oqtane.Server/Extensions/OqtaneServiceCollectionExtensions.cs @@ -1,4 +1,5 @@ using System; +using System.Collections.Generic; using System.Diagnostics; using System.IdentityModel.Tokens.Jwt; using System.IO; @@ -11,12 +12,17 @@ using System.Threading.Tasks; using Microsoft.AspNetCore.Authentication.OAuth; using Microsoft.AspNetCore.Authentication.OpenIdConnect; using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Components.Authorization; +using Microsoft.AspNetCore.Hosting; using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.HttpOverrides; using Microsoft.AspNetCore.Identity; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.Hosting; using Microsoft.Extensions.Logging; using Microsoft.OpenApi.Models; +using Oqtane.Extensions; using Oqtane.Infrastructure; using Oqtane.Interfaces; using Oqtane.Managers; @@ -31,10 +37,126 @@ namespace Microsoft.Extensions.DependencyInjection { public static class OqtaneServiceCollectionExtensions { - public static IServiceCollection AddOqtane(this IServiceCollection services, string[] installedCultures) + public static IServiceCollection AddOqtane(this IServiceCollection services, IConfigurationRoot configuration, IWebHostEnvironment environment) + { + // process forwarded headers on load balancers and proxy servers + services.Configure(options => + { + options.ForwardedHeaders = ForwardedHeaders.XForwardedFor | ForwardedHeaders.XForwardedProto; + }); + + // register localization services + services.AddLocalization(options => options.ResourcesPath = "Resources"); + + services.AddOptions>().Bind(configuration.GetSection(SettingKeys.AvailableDatabasesSection)); + + // register scoped core services + services.AddScoped() + .AddOqtaneServerScopedServices(); + + services.AddSingleton(); + + // setup HttpClient for server side in a client side compatible fashion ( with auth cookie ) + services.AddHttpClients(); + + // register singleton scoped core services + services.AddSingleton(configuration) + .AddOqtaneSingletonServices(); + + // install any modules or themes ( this needs to occur BEFORE the assemblies are loaded into the app domain ) + InstallationManager.InstallPackages(environment.WebRootPath, environment.ContentRootPath); + + // register transient scoped core services + services.AddOqtaneTransientServices(); + + // load the external assemblies into the app domain, install services + services.AddOqtaneAssemblies(); + services.AddOqtaneDbContext(); + + services.AddAntiforgery(options => + { + options.HeaderName = Constants.AntiForgeryTokenHeaderName; + options.Cookie.Name = Constants.AntiForgeryTokenCookieName; + options.Cookie.SameSite = Microsoft.AspNetCore.Http.SameSiteMode.Strict; + options.Cookie.SecurePolicy = CookieSecurePolicy.SameAsRequest; + options.Cookie.HttpOnly = true; + }); + + services.AddIdentityCore(options => { }) + .AddEntityFrameworkStores() + .AddSignInManager() + .AddDefaultTokenProviders() + .AddClaimsPrincipalFactory>(); // role claims + + services.ConfigureOqtaneIdentityOptions(configuration); + + services.AddCascadingAuthenticationState(); + services.AddScoped(); + services.AddAuthorization(); + + services.AddAuthentication(options => + { + options.DefaultScheme = Constants.AuthenticationScheme; + }) + .AddCookie(Constants.AuthenticationScheme) + .AddOpenIdConnect(AuthenticationProviderTypes.OpenIDConnect, options => { }) + .AddOAuth(AuthenticationProviderTypes.OAuth2, options => { }); + + services.ConfigureOqtaneCookieOptions(); + services.ConfigureOqtaneAuthenticationOptions(configuration); + + services.AddOqtaneSiteOptions() + .WithSiteIdentity() + .WithSiteAuthentication(); + + services.AddCors(options => + { + options.AddPolicy(Constants.MauiCorsPolicy, + policy => + { + // allow .NET MAUI client cross origin calls + policy.WithOrigins("https://0.0.0.1", "http://0.0.0.1", "app://0.0.0.1") + .AllowAnyHeader().AllowAnyMethod().AllowCredentials(); + }); + }); + + services.AddOutputCache(); + + services.AddMvc(options => + { + options.Filters.Add(new AutoValidateAntiforgeryTokenAttribute()); + }) + .AddOqtaneApplicationParts() // register any Controllers from custom modules + .ConfigureOqtaneMvc(); // any additional configuration from IStartup classes + + services.AddRazorPages(); + + services.AddRazorComponents() + .AddInteractiveServerComponents(options => + { + if (environment.IsDevelopment()) + { + options.DetailedErrors = true; + } + }).AddHubOptions(options => + { + options.MaximumReceiveMessageSize = null; // no limit (for large amounts of data ie. textarea components) + }) + .AddInteractiveWebAssemblyComponents(); + + services.AddSwaggerGen(options => + { + options.CustomSchemaIds(type => type.ToString()); // Handle SchemaId already used for different type + }); + services.TryAddSwagger(configuration); + + return services; + } + + public static IServiceCollection AddOqtaneAssemblies(this IServiceCollection services) { LoadAssemblies(); - LoadSatelliteAssemblies(installedCultures); + LoadSatelliteAssemblies(); services.AddOqtaneServices(); return services; @@ -53,7 +175,7 @@ namespace Microsoft.Extensions.DependencyInjection return new OqtaneSiteOptionsBuilder(services); } - internal static IServiceCollection AddOqtaneSingletonServices(this IServiceCollection services) + public static IServiceCollection AddOqtaneSingletonServices(this IServiceCollection services) { services.AddSingleton(); services.AddSingleton(); @@ -66,7 +188,7 @@ namespace Microsoft.Extensions.DependencyInjection return services; } - internal static IServiceCollection AddOqtaneServerScopedServices(this IServiceCollection services) + public static IServiceCollection AddOqtaneServerScopedServices(this IServiceCollection services) { services.AddScoped(); services.AddScoped(); @@ -112,7 +234,7 @@ namespace Microsoft.Extensions.DependencyInjection return services; } - internal static IServiceCollection AddOqtaneTransientServices(this IServiceCollection services) + public static IServiceCollection AddOqtaneTransientServices(this IServiceCollection services) { // services services.AddTransient(); @@ -242,7 +364,7 @@ namespace Microsoft.Extensions.DependencyInjection return services; } - internal static IServiceCollection AddHttpClients(this IServiceCollection services) + public static IServiceCollection AddHttpClients(this IServiceCollection services) { if (!services.Any(x => x.ServiceType == typeof(HttpClient))) { @@ -285,9 +407,9 @@ namespace Microsoft.Extensions.DependencyInjection return services; } - internal static IServiceCollection TryAddSwagger(this IServiceCollection services, bool useSwagger) + public static IServiceCollection TryAddSwagger(this IServiceCollection services, IConfigurationRoot configuration) { - if (useSwagger) + if (configuration.GetSection("UseSwagger").Value != "false") { services.AddSwaggerGen(c => { @@ -386,10 +508,11 @@ namespace Microsoft.Extensions.DependencyInjection } } - private static void LoadSatelliteAssemblies(string[] installedCultures) + private static void LoadSatelliteAssemblies() { AssemblyLoadContext.Default.Resolving += ResolveDependencies; + var installedCultures = LocalizationManager.GetSatelliteAssemblyCultures(); foreach (var file in Directory.EnumerateFiles(Path.GetDirectoryName(Assembly.GetEntryAssembly().Location), $"*{Constants.SatelliteAssemblyExtension}", SearchOption.AllDirectories)) { var code = Path.GetFileName(Path.GetDirectoryName(file)); diff --git a/Oqtane.Server/Infrastructure/LocalizationManager.cs b/Oqtane.Server/Infrastructure/LocalizationManager.cs index 0d083c4f..3dfc0b54 100644 --- a/Oqtane.Server/Infrastructure/LocalizationManager.cs +++ b/Oqtane.Server/Infrastructure/LocalizationManager.cs @@ -45,6 +45,12 @@ namespace Oqtane.Infrastructure } public string[] GetInstalledCultures() + { + return GetSatelliteAssemblyCultures(); + } + + // method is static as it is called during startup + public static string[] GetSatelliteAssemblyCultures() { var cultures = new List(); foreach (var file in Directory.EnumerateFiles(Path.GetDirectoryName(Assembly.GetEntryAssembly().Location), $"*{Constants.SatelliteAssemblyExtension}", SearchOption.AllDirectories)) diff --git a/Oqtane.Server/Startup.cs b/Oqtane.Server/Startup.cs index 3642addd..979c57e4 100644 --- a/Oqtane.Server/Startup.cs +++ b/Oqtane.Server/Startup.cs @@ -1,260 +1,43 @@ using System; -using System.Collections.Generic; using System.IO; -using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Hosting; -using Microsoft.AspNetCore.Http; -using Microsoft.AspNetCore.Identity; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; -using Microsoft.Extensions.Hosting; using Oqtane.Extensions; using Oqtane.Infrastructure; -using Oqtane.Repository; -using Oqtane.Security; using Oqtane.Shared; -using Microsoft.AspNetCore.HttpOverrides; -using Microsoft.Extensions.Logging; -using Oqtane.Components; -using Oqtane.UI; -using OqtaneSSR.Extensions; -using Microsoft.AspNetCore.Components.Authorization; -using Oqtane.Providers; using Microsoft.AspNetCore.Cors.Infrastructure; -using Microsoft.Net.Http.Headers; namespace Oqtane { public class Startup { - private readonly bool _useSwagger; - private readonly IWebHostEnvironment _env; - private readonly string[] _installedCultures; - private string _configureServicesErrors; + private readonly IConfigurationRoot _configuration; + private readonly IWebHostEnvironment _environment; - public IConfigurationRoot Configuration { get; } - - public Startup(IWebHostEnvironment env, ILocalizationManager localizationManager) + public Startup(IWebHostEnvironment environment) { + AppDomain.CurrentDomain.SetData(Constants.DataDirectory, Path.Combine(environment.ContentRootPath, "Data")); + var builder = new ConfigurationBuilder() - .SetBasePath(env.ContentRootPath) + .SetBasePath(environment.ContentRootPath) .AddJsonFile("appsettings.json", false, true) - .AddJsonFile($"appsettings.{env.EnvironmentName}.json", true, true) + .AddJsonFile($"appsettings.{environment.EnvironmentName}.json", true, true) .AddEnvironmentVariables(); - Configuration = builder.Build(); - - _installedCultures = localizationManager.GetInstalledCultures(); - - //add possibility to switch off swagger on production. - _useSwagger = Configuration.GetSection("UseSwagger").Value != "false"; - - AppDomain.CurrentDomain.SetData(Constants.DataDirectory, Path.Combine(env.ContentRootPath, "Data")); - - _env = env; + _configuration = builder.Build(); + _environment = environment; } - // This method gets called by the runtime. Use this method to add services to the container. - // For more information on how to configure your application, visit https://go.microsoft.com/fwlink/?LinkID=398940 public void ConfigureServices(IServiceCollection services) { - // process forwarded headers on load balancers and proxy servers - services.Configure(options => - { - options.ForwardedHeaders = ForwardedHeaders.XForwardedFor | ForwardedHeaders.XForwardedProto; - }); - - // register localization services - services.AddLocalization(options => options.ResourcesPath = "Resources"); - - services.AddOptions>().Bind(Configuration.GetSection(SettingKeys.AvailableDatabasesSection)); - - // register scoped core services - services.AddScoped() - .AddOqtaneServerScopedServices(); - - services.AddSingleton(); - - // setup HttpClient for server side in a client side compatible fashion ( with auth cookie ) - services.AddHttpClients(); - - // register singleton scoped core services - services.AddSingleton(Configuration) - .AddOqtaneSingletonServices(); - - // install any modules or themes ( this needs to occur BEFORE the assemblies are loaded into the app domain ) - _configureServicesErrors += InstallationManager.InstallPackages(_env.WebRootPath, _env.ContentRootPath); - - // register transient scoped core services - services.AddOqtaneTransientServices(); - - // load the external assemblies into the app domain, install services - services.AddOqtane(_installedCultures); - services.AddOqtaneDbContext(); - - services.AddAntiforgery(options => - { - options.HeaderName = Constants.AntiForgeryTokenHeaderName; - options.Cookie.Name = Constants.AntiForgeryTokenCookieName; - options.Cookie.SameSite = Microsoft.AspNetCore.Http.SameSiteMode.Strict; - options.Cookie.SecurePolicy = CookieSecurePolicy.SameAsRequest; - options.Cookie.HttpOnly = true; - }); - - services.AddIdentityCore(options => { }) - .AddEntityFrameworkStores() - .AddSignInManager() - .AddDefaultTokenProviders() - .AddClaimsPrincipalFactory>(); // role claims - - services.ConfigureOqtaneIdentityOptions(Configuration); - - services.AddCascadingAuthenticationState(); - services.AddScoped(); - services.AddAuthorization(); - - services.AddAuthentication(options => - { - options.DefaultScheme = Constants.AuthenticationScheme; - }) - .AddCookie(Constants.AuthenticationScheme) - .AddOpenIdConnect(AuthenticationProviderTypes.OpenIDConnect, options => { }) - .AddOAuth(AuthenticationProviderTypes.OAuth2, options => { }); - - services.ConfigureOqtaneCookieOptions(); - services.ConfigureOqtaneAuthenticationOptions(Configuration); - - services.AddOqtaneSiteOptions() - .WithSiteIdentity() - .WithSiteAuthentication(); - - services.AddCors(options => - { - options.AddPolicy(Constants.MauiCorsPolicy, - policy => - { - // allow .NET MAUI client cross origin calls - policy.WithOrigins("https://0.0.0.1", "http://0.0.0.1", "app://0.0.0.1") - .AllowAnyHeader().AllowAnyMethod().AllowCredentials(); - }); - }); - - services.AddOutputCache(); - - services.AddMvc(options => - { - options.Filters.Add(new AutoValidateAntiforgeryTokenAttribute()); - }) - .AddOqtaneApplicationParts() // register any Controllers from custom modules - .ConfigureOqtaneMvc(); // any additional configuration from IStartup classes - - services.AddRazorPages(); - - services.AddRazorComponents() - .AddInteractiveServerComponents(options => - { - if (_env.IsDevelopment()) - { - options.DetailedErrors = true; - } - }).AddHubOptions(options => - { - options.MaximumReceiveMessageSize = null; // no limit (for large amounts of data ie. textarea components) - }) - .AddInteractiveWebAssemblyComponents(); - - services.AddSwaggerGen(options => - { - options.CustomSchemaIds(type => type.ToString()); // Handle SchemaId already used for different type - }); - services.TryAddSwagger(_useSwagger); + services.AddOqtane(_configuration, _environment); } - // This method gets called by the runtime. Use this method to configure the HTTP request pipeline. - public void Configure(IApplicationBuilder app, IWebHostEnvironment env, ISyncManager sync, ICorsService corsService, ICorsPolicyProvider corsPolicyProvider, ILogger logger) + public void Configure(IApplicationBuilder app, IConfigurationRoot configuration, IWebHostEnvironment environment, ICorsService corsService, ICorsPolicyProvider corsPolicyProvider, ISyncManager sync) { - if (!string.IsNullOrEmpty(_configureServicesErrors)) - { - logger.LogError(_configureServicesErrors); - } - - ServiceActivator.Configure(app.ApplicationServices); - - if (env.IsDevelopment()) - { - app.UseWebAssemblyDebugging(); - app.UseForwardedHeaders(); - } - else - { - app.UseForwardedHeaders(); - app.UseExceptionHandler("/Error", createScopeForErrors: true); - // The default HSTS value is 30 days. You may want to change this for production scenarios, see https://aka.ms/aspnetcore-hsts. - app.UseHsts(); - } - - // allow oqtane localization middleware - app.UseOqtaneLocalization(); - - app.UseHttpsRedirection(); - app.UseStaticFiles(new StaticFileOptions - { - OnPrepareResponse = (ctx) => - { - // static asset caching - var cachecontrol = Configuration.GetSection("CacheControl"); - if (!string.IsNullOrEmpty(cachecontrol.Value)) - { - ctx.Context.Response.Headers.Append(HeaderNames.CacheControl, cachecontrol.Value); - } - // CORS headers for .NET MAUI clients - var policy = corsPolicyProvider.GetPolicyAsync(ctx.Context, Constants.MauiCorsPolicy) - .ConfigureAwait(false).GetAwaiter().GetResult(); - corsService.ApplyResult(corsService.EvaluatePolicy(ctx.Context, policy), ctx.Context.Response); - } - }); - app.UseExceptionMiddleWare(); - app.UseTenantResolution(); - app.UseJwtAuthorization(); - app.UseRouting(); - app.UseCors(); - app.UseOutputCache(); - app.UseAuthentication(); - app.UseAuthorization(); - app.UseAntiforgery(); - - // execute any IServerStartup logic - app.ConfigureOqtaneAssemblies(env); - - if (_useSwagger) - { - app.UseSwagger(); - app.UseSwaggerUI(c => { c.SwaggerEndpoint("/swagger/" + Constants.Version + "/swagger.json", Constants.PackageId + " " + Constants.Version); }); - } - - app.UseEndpoints(endpoints => - { - endpoints.MapControllers(); - endpoints.MapRazorPages(); - }); - - app.UseEndpoints(endpoints => - { - endpoints.MapRazorComponents() - .AddInteractiveServerRenderMode() - .AddInteractiveWebAssemblyRenderMode() - .AddAdditionalAssemblies(typeof(SiteRouter).Assembly); - }); - - // simulate the fallback routing approach of traditional Blazor - allowing the custom SiteRouter to handle all routing concerns - app.UseEndpoints(endpoints => - { - endpoints.MapFallback(); - }); - - // create a global sync event to identify server application startup - sync.AddSyncEvent(-1, -1, EntityNames.Host, -1, SyncEventActions.Reload); + app.UseOqtane(configuration, environment, corsService, corsPolicyProvider, sync); } } }