optimize startup

This commit is contained in:
sbwalker
2025-08-26 15:27:35 -04:00
parent 6a0c47f7b1
commit ec06c1cdf1
4 changed files with 241 additions and 238 deletions

View File

@ -2,14 +2,105 @@ using System;
using System.Linq; using System.Linq;
using System.Reflection; using System.Reflection;
using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Cors.Infrastructure;
using Microsoft.AspNetCore.Hosting; using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using Microsoft.Net.Http.Headers;
using Oqtane.Components;
using Oqtane.Infrastructure; using Oqtane.Infrastructure;
using Oqtane.Shared;
using Oqtane.UI;
using OqtaneSSR.Extensions;
namespace Oqtane.Extensions namespace Oqtane.Extensions
{ {
public static class ApplicationBuilderExtensions 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<App>()
.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) public static IApplicationBuilder ConfigureOqtaneAssemblies(this IApplicationBuilder app, IWebHostEnvironment env)
{ {
var startUps = AppDomain.CurrentDomain var startUps = AppDomain.CurrentDomain

View File

@ -1,4 +1,5 @@
using System; using System;
using System.Collections.Generic;
using System.Diagnostics; using System.Diagnostics;
using System.IdentityModel.Tokens.Jwt; using System.IdentityModel.Tokens.Jwt;
using System.IO; using System.IO;
@ -11,12 +12,17 @@ using System.Threading.Tasks;
using Microsoft.AspNetCore.Authentication.OAuth; using Microsoft.AspNetCore.Authentication.OAuth;
using Microsoft.AspNetCore.Authentication.OpenIdConnect; using Microsoft.AspNetCore.Authentication.OpenIdConnect;
using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Components.Authorization;
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.HttpOverrides;
using Microsoft.AspNetCore.Identity; using Microsoft.AspNetCore.Identity;
using Microsoft.Extensions.Configuration; using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.Hosting; using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging;
using Microsoft.OpenApi.Models; using Microsoft.OpenApi.Models;
using Oqtane.Extensions;
using Oqtane.Infrastructure; using Oqtane.Infrastructure;
using Oqtane.Interfaces; using Oqtane.Interfaces;
using Oqtane.Managers; using Oqtane.Managers;
@ -31,10 +37,126 @@ namespace Microsoft.Extensions.DependencyInjection
{ {
public static class OqtaneServiceCollectionExtensions 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<ForwardedHeadersOptions>(options =>
{
options.ForwardedHeaders = ForwardedHeaders.XForwardedFor | ForwardedHeaders.XForwardedProto;
});
// register localization services
services.AddLocalization(options => options.ResourcesPath = "Resources");
services.AddOptions<List<Oqtane.Models.Database>>().Bind(configuration.GetSection(SettingKeys.AvailableDatabasesSection));
// register scoped core services
services.AddScoped<IAuthorizationHandler, PermissionHandler>()
.AddOqtaneServerScopedServices();
services.AddSingleton<IHttpContextAccessor, HttpContextAccessor>();
// 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<IdentityUser>(options => { })
.AddEntityFrameworkStores<TenantDBContext>()
.AddSignInManager()
.AddDefaultTokenProviders()
.AddClaimsPrincipalFactory<ClaimsPrincipalFactory<IdentityUser>>(); // role claims
services.ConfigureOqtaneIdentityOptions(configuration);
services.AddCascadingAuthenticationState();
services.AddScoped<AuthenticationStateProvider, IdentityRevalidatingAuthenticationStateProvider>();
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(); LoadAssemblies();
LoadSatelliteAssemblies(installedCultures); LoadSatelliteAssemblies();
services.AddOqtaneServices(); services.AddOqtaneServices();
return services; return services;
@ -53,7 +175,7 @@ namespace Microsoft.Extensions.DependencyInjection
return new OqtaneSiteOptionsBuilder(services); return new OqtaneSiteOptionsBuilder(services);
} }
internal static IServiceCollection AddOqtaneSingletonServices(this IServiceCollection services) public static IServiceCollection AddOqtaneSingletonServices(this IServiceCollection services)
{ {
services.AddSingleton<IInstallationManager, InstallationManager>(); services.AddSingleton<IInstallationManager, InstallationManager>();
services.AddSingleton<ISyncManager, SyncManager>(); services.AddSingleton<ISyncManager, SyncManager>();
@ -66,7 +188,7 @@ namespace Microsoft.Extensions.DependencyInjection
return services; return services;
} }
internal static IServiceCollection AddOqtaneServerScopedServices(this IServiceCollection services) public static IServiceCollection AddOqtaneServerScopedServices(this IServiceCollection services)
{ {
services.AddScoped<Oqtane.Shared.SiteState>(); services.AddScoped<Oqtane.Shared.SiteState>();
services.AddScoped<IInstallationService, InstallationService>(); services.AddScoped<IInstallationService, InstallationService>();
@ -112,7 +234,7 @@ namespace Microsoft.Extensions.DependencyInjection
return services; return services;
} }
internal static IServiceCollection AddOqtaneTransientServices(this IServiceCollection services) public static IServiceCollection AddOqtaneTransientServices(this IServiceCollection services)
{ {
// services // services
services.AddTransient<ISiteService, ServerSiteService>(); services.AddTransient<ISiteService, ServerSiteService>();
@ -242,7 +364,7 @@ namespace Microsoft.Extensions.DependencyInjection
return services; return services;
} }
internal static IServiceCollection AddHttpClients(this IServiceCollection services) public static IServiceCollection AddHttpClients(this IServiceCollection services)
{ {
if (!services.Any(x => x.ServiceType == typeof(HttpClient))) if (!services.Any(x => x.ServiceType == typeof(HttpClient)))
{ {
@ -285,9 +407,9 @@ namespace Microsoft.Extensions.DependencyInjection
return services; 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 => 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; AssemblyLoadContext.Default.Resolving += ResolveDependencies;
var installedCultures = LocalizationManager.GetSatelliteAssemblyCultures();
foreach (var file in Directory.EnumerateFiles(Path.GetDirectoryName(Assembly.GetEntryAssembly().Location), $"*{Constants.SatelliteAssemblyExtension}", SearchOption.AllDirectories)) foreach (var file in Directory.EnumerateFiles(Path.GetDirectoryName(Assembly.GetEntryAssembly().Location), $"*{Constants.SatelliteAssemblyExtension}", SearchOption.AllDirectories))
{ {
var code = Path.GetFileName(Path.GetDirectoryName(file)); var code = Path.GetFileName(Path.GetDirectoryName(file));

View File

@ -45,6 +45,12 @@ namespace Oqtane.Infrastructure
} }
public string[] GetInstalledCultures() public string[] GetInstalledCultures()
{
return GetSatelliteAssemblyCultures();
}
// method is static as it is called during startup
public static string[] GetSatelliteAssemblyCultures()
{ {
var cultures = new List<string>(); var cultures = new List<string>();
foreach (var file in Directory.EnumerateFiles(Path.GetDirectoryName(Assembly.GetEntryAssembly().Location), $"*{Constants.SatelliteAssemblyExtension}", SearchOption.AllDirectories)) foreach (var file in Directory.EnumerateFiles(Path.GetDirectoryName(Assembly.GetEntryAssembly().Location), $"*{Constants.SatelliteAssemblyExtension}", SearchOption.AllDirectories))

View File

@ -1,260 +1,43 @@
using System; using System;
using System.Collections.Generic;
using System.IO; using System.IO;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting; using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Identity;
using Microsoft.Extensions.Configuration; using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using Oqtane.Extensions; using Oqtane.Extensions;
using Oqtane.Infrastructure; using Oqtane.Infrastructure;
using Oqtane.Repository;
using Oqtane.Security;
using Oqtane.Shared; 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.AspNetCore.Cors.Infrastructure;
using Microsoft.Net.Http.Headers;
namespace Oqtane namespace Oqtane
{ {
public class Startup public class Startup
{ {
private readonly bool _useSwagger; private readonly IConfigurationRoot _configuration;
private readonly IWebHostEnvironment _env; private readonly IWebHostEnvironment _environment;
private readonly string[] _installedCultures;
private string _configureServicesErrors;
public IConfigurationRoot Configuration { get; } public Startup(IWebHostEnvironment environment)
public Startup(IWebHostEnvironment env, ILocalizationManager localizationManager)
{ {
AppDomain.CurrentDomain.SetData(Constants.DataDirectory, Path.Combine(environment.ContentRootPath, "Data"));
var builder = new ConfigurationBuilder() var builder = new ConfigurationBuilder()
.SetBasePath(env.ContentRootPath) .SetBasePath(environment.ContentRootPath)
.AddJsonFile("appsettings.json", false, true) .AddJsonFile("appsettings.json", false, true)
.AddJsonFile($"appsettings.{env.EnvironmentName}.json", true, true) .AddJsonFile($"appsettings.{environment.EnvironmentName}.json", true, true)
.AddEnvironmentVariables(); .AddEnvironmentVariables();
Configuration = builder.Build(); _configuration = builder.Build();
_environment = environment;
_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;
} }
// 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) public void ConfigureServices(IServiceCollection services)
{ {
// process forwarded headers on load balancers and proxy servers services.AddOqtane(_configuration, _environment);
services.Configure<ForwardedHeadersOptions>(options =>
{
options.ForwardedHeaders = ForwardedHeaders.XForwardedFor | ForwardedHeaders.XForwardedProto;
});
// register localization services
services.AddLocalization(options => options.ResourcesPath = "Resources");
services.AddOptions<List<Models.Database>>().Bind(Configuration.GetSection(SettingKeys.AvailableDatabasesSection));
// register scoped core services
services.AddScoped<IAuthorizationHandler, PermissionHandler>()
.AddOqtaneServerScopedServices();
services.AddSingleton<IHttpContextAccessor, HttpContextAccessor>();
// 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<IdentityUser>(options => { })
.AddEntityFrameworkStores<TenantDBContext>()
.AddSignInManager()
.AddDefaultTokenProviders()
.AddClaimsPrincipalFactory<ClaimsPrincipalFactory<IdentityUser>>(); // role claims
services.ConfigureOqtaneIdentityOptions(Configuration);
services.AddCascadingAuthenticationState();
services.AddScoped<AuthenticationStateProvider, IdentityRevalidatingAuthenticationStateProvider>();
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);
} }
// This method gets called by the runtime. Use this method to configure the HTTP request pipeline. public void Configure(IApplicationBuilder app, IConfigurationRoot configuration, IWebHostEnvironment environment, ICorsService corsService, ICorsPolicyProvider corsPolicyProvider, ISyncManager sync)
public void Configure(IApplicationBuilder app, IWebHostEnvironment env, ISyncManager sync, ICorsService corsService, ICorsPolicyProvider corsPolicyProvider, ILogger<Startup> logger)
{ {
if (!string.IsNullOrEmpty(_configureServicesErrors)) app.UseOqtane(configuration, environment, corsService, corsPolicyProvider, sync);
{
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<App>()
.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);
} }
} }
} }