diff --git a/src/Templates/AdminPanel/Bit.AdminPanel/src/Client/App/MauiProgram.cs b/src/Templates/AdminPanel/Bit.AdminPanel/src/Client/App/MauiProgram.cs index 5b9f3c30ce..aa9dd6fd0e 100644 --- a/src/Templates/AdminPanel/Bit.AdminPanel/src/Client/App/MauiProgram.cs +++ b/src/Templates/AdminPanel/Bit.AdminPanel/src/Client/App/MauiProgram.cs @@ -1,6 +1,7 @@ //-:cnd:noEmit using System.Reflection; +using AdminPanel.Client.Core.Services.HttpMessageHandlers; using Microsoft.Extensions.FileProviders; namespace AdminPanel.Client.App; @@ -27,13 +28,15 @@ public static MauiApp CreateMauiApp() services.AddBlazorWebViewDeveloperTools(); #endif + Uri.TryCreate(builder.Configuration.GetApiServerAddress(), UriKind.Absolute, out var apiServerAddress); + services.AddScoped(sp => { - HttpClient httpClient = new(sp.GetRequiredService()) + var handler = sp.GetRequiredService(); + HttpClient httpClient = new(handler) { - BaseAddress = new Uri(sp.GetRequiredService().GetApiServerAddress()) + BaseAddress = apiServerAddress }; - return httpClient; }); diff --git a/src/Templates/AdminPanel/Bit.AdminPanel/src/Client/Core/AdminPanel.Client.Core.csproj b/src/Templates/AdminPanel/Bit.AdminPanel/src/Client/Core/AdminPanel.Client.Core.csproj index 1886c523fb..e2b82f7e72 100644 --- a/src/Templates/AdminPanel/Bit.AdminPanel/src/Client/Core/AdminPanel.Client.Core.csproj +++ b/src/Templates/AdminPanel/Bit.AdminPanel/src/Client/Core/AdminPanel.Client.Core.csproj @@ -34,6 +34,7 @@ + diff --git a/src/Templates/AdminPanel/Bit.AdminPanel/src/Client/Core/App.razor b/src/Templates/AdminPanel/Bit.AdminPanel/src/Client/Core/App.razor index cefb84ab97..ae06ef56ae 100644 --- a/src/Templates/AdminPanel/Bit.AdminPanel/src/Client/Core/App.razor +++ b/src/Templates/AdminPanel/Bit.AdminPanel/src/Client/Core/App.razor @@ -1,22 +1,24 @@ - - - - - - - - - - - - - - - - - - - - \ No newline at end of file + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/Templates/AdminPanel/Bit.AdminPanel/src/Client/Core/Extensions/IConfigurationBuilderExtensions.cs b/src/Templates/AdminPanel/Bit.AdminPanel/src/Client/Core/Extensions/IConfigurationBuilderExtensions.cs new file mode 100644 index 0000000000..9623fd7dc6 --- /dev/null +++ b/src/Templates/AdminPanel/Bit.AdminPanel/src/Client/Core/Extensions/IConfigurationBuilderExtensions.cs @@ -0,0 +1,12 @@ +using System.Reflection; + +namespace Microsoft.Extensions.Configuration; + +public static class IConfigurationBuilderExtensions +{ + public static void AddClientConfigurations(this IConfigurationBuilder builder) + { + var assembly = Assembly.Load("AdminPanel.Client.Core"); + builder.AddJsonStream(assembly.GetManifestResourceStream("AdminPanel.Client.Core.appsettings.json")!); + } +} diff --git a/src/Templates/AdminPanel/Bit.AdminPanel/src/Client/Core/Extensions/IConfigurationExtensions.cs b/src/Templates/AdminPanel/Bit.AdminPanel/src/Client/Core/Extensions/IConfigurationExtensions.cs index 4e96485f81..e88b29bed5 100644 --- a/src/Templates/AdminPanel/Bit.AdminPanel/src/Client/Core/Extensions/IConfigurationExtensions.cs +++ b/src/Templates/AdminPanel/Bit.AdminPanel/src/Client/Core/Extensions/IConfigurationExtensions.cs @@ -4,10 +4,8 @@ public static class IConfigurationExtensions { public static string GetApiServerAddress(this IConfiguration configuration) { -#if BlazorWebAssembly - return "api/"; -#else - return configuration.GetValue("ApiServerAddress") ?? throw new InvalidOperationException("Could not find ApiServerAddress config"); -#endif + var apiServerAddress = configuration.GetValue("ApiServerAddress", defaultValue: "api/")!; + + return Uri.TryCreate(apiServerAddress, UriKind.RelativeOrAbsolute, out _) ? apiServerAddress : throw new InvalidOperationException($"Api server address {apiServerAddress} is invalid"); } } diff --git a/src/Templates/AdminPanel/Bit.AdminPanel/src/Client/Core/Extensions/IServiceCollectionExtensions.cs b/src/Templates/AdminPanel/Bit.AdminPanel/src/Client/Core/Extensions/IServiceCollectionExtensions.cs index a077191ff6..7ba4027118 100644 --- a/src/Templates/AdminPanel/Bit.AdminPanel/src/Client/Core/Extensions/IServiceCollectionExtensions.cs +++ b/src/Templates/AdminPanel/Bit.AdminPanel/src/Client/Core/Extensions/IServiceCollectionExtensions.cs @@ -1,5 +1,7 @@ //-:cnd:noEmit +using AdminPanel.Client.Core.Services.HttpMessageHandlers; + namespace Microsoft.Extensions.DependencyInjection; public static class IServiceCollectionExtensions @@ -13,7 +15,11 @@ public static IServiceCollection AddClientSharedServices(this IServiceCollection services.AddScoped(); services.AddBitBlazorUIServices(); - services.AddTransient(); + services.AddTransient(); + services.AddTransient(); + services.AddTransient(); + services.AddTransient(); + services.AddTransient(); services.AddScoped(); services.AddScoped(); diff --git a/src/Templates/AdminPanel/Bit.AdminPanel/src/Client/Core/Services/HttpMessageHandlers/AuthDelegatingHandler.cs b/src/Templates/AdminPanel/Bit.AdminPanel/src/Client/Core/Services/HttpMessageHandlers/AuthDelegatingHandler.cs new file mode 100644 index 0000000000..c056509708 --- /dev/null +++ b/src/Templates/AdminPanel/Bit.AdminPanel/src/Client/Core/Services/HttpMessageHandlers/AuthDelegatingHandler.cs @@ -0,0 +1,47 @@ +using System.Net.Http.Headers; + +namespace AdminPanel.Client.Core.Services.HttpMessageHandlers; + +public class AuthDelegatingHandler + : DelegatingHandler +{ + private IAuthTokenProvider _tokenProvider = default!; + private IJSRuntime _jsRuntime = default!; + + public AuthDelegatingHandler(IAuthTokenProvider tokenProvider, IJSRuntime jsRuntime, RetryDelegatingHandler handler) + : base(handler) + { + _tokenProvider = tokenProvider; + _jsRuntime = jsRuntime; + } + + public AuthDelegatingHandler(IAuthTokenProvider tokenProvider, IJSRuntime jsRuntime) + : base() + { + _tokenProvider = tokenProvider; + _jsRuntime = jsRuntime; + } + + protected override async Task SendAsync(HttpRequestMessage request, CancellationToken cancellationToken) + { + if (request.Headers.Authorization is null) + { + var access_token = await _tokenProvider.GetAccessTokenAsync(); + if (access_token is not null) + { + request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", access_token); + } + } + + try + { + return await base.SendAsync(request, cancellationToken); + } + catch (UnauthorizedException) + { + // try to get refresh token, store access token and refresh token, + // then use the new access token to request's authorization header and call base.SendAsync again. + throw; + } + } +} diff --git a/src/Templates/BlazorWeb/Bit.BlazorWeb/src/BlazorWeb.Client/Services/AppHttpClientHandler.cs b/src/Templates/AdminPanel/Bit.AdminPanel/src/Client/Core/Services/HttpMessageHandlers/ExceptionDelegatingHandler.cs similarity index 73% rename from src/Templates/BlazorWeb/Bit.BlazorWeb/src/BlazorWeb.Client/Services/AppHttpClientHandler.cs rename to src/Templates/AdminPanel/Bit.AdminPanel/src/Client/Core/Services/HttpMessageHandlers/ExceptionDelegatingHandler.cs index def38ae59e..99bc37233c 100644 --- a/src/Templates/BlazorWeb/Bit.BlazorWeb/src/BlazorWeb.Client/Services/AppHttpClientHandler.cs +++ b/src/Templates/AdminPanel/Bit.AdminPanel/src/Client/Core/Services/HttpMessageHandlers/ExceptionDelegatingHandler.cs @@ -1,28 +1,23 @@ -//-:cnd:noEmit -using System.Net; -using System.Net.Http.Headers; +using System.Net; -namespace BlazorWeb.Client.Services; +namespace AdminPanel.Client.Core.Services.HttpMessageHandlers; -public partial class AppHttpClientHandler : HttpClientHandler +public class ExceptionDelegatingHandler + : DelegatingHandler { - [AutoInject] private IAuthTokenProvider _tokenProvider = default!; + public ExceptionDelegatingHandler(HttpClientHandler httpClientHandler) + : base(httpClientHandler) + { - protected override async Task SendAsync(HttpRequestMessage request, CancellationToken cancellationToken) + } + + public ExceptionDelegatingHandler() { - if (request.Headers.Authorization is null) - { - var access_token = await _tokenProvider.GetAccessTokenAsync(); - if (access_token is not null) - { - request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", access_token); - } - } -#if MultilingualEnabled - request.Headers.AcceptLanguage.Add(new StringWithQualityHeaderValue(CultureInfo.CurrentCulture.Name)); -#endif + } + protected override async Task SendAsync(HttpRequestMessage request, CancellationToken cancellationToken) + { bool serverCommunicationSuccess = false; try @@ -51,7 +46,7 @@ protected override async Task SendAsync(HttpRequestMessage args.Add(restError.Payload); } - Exception exp = (Exception)Activator.CreateInstance(exceptionType, [.. args])!; + Exception exp = (Exception)Activator.CreateInstance(exceptionType, args.ToArray())!; throw exp; } diff --git a/src/Templates/AdminPanel/Bit.AdminPanel/src/Client/Core/Services/HttpMessageHandlers/LocalizationDelegatingHandler.cs b/src/Templates/AdminPanel/Bit.AdminPanel/src/Client/Core/Services/HttpMessageHandlers/LocalizationDelegatingHandler.cs new file mode 100644 index 0000000000..935d35a42e --- /dev/null +++ b/src/Templates/AdminPanel/Bit.AdminPanel/src/Client/Core/Services/HttpMessageHandlers/LocalizationDelegatingHandler.cs @@ -0,0 +1,28 @@ +using System.Net.Http.Headers; + +namespace AdminPanel.Client.Core.Services.HttpMessageHandlers; + +public class LocalizationDelegatingHandler + : DelegatingHandler +{ + public LocalizationDelegatingHandler(AuthDelegatingHandler handler) + : base(handler) + { + + } + + public LocalizationDelegatingHandler() + { + + } + + protected override async Task SendAsync(HttpRequestMessage request, CancellationToken cancellationToken) + { +#if MultilingualEnabled + request.Headers.AcceptLanguage.Add(new StringWithQualityHeaderValue(CultureInfo.CurrentCulture.Name)); +#endif + + return await base.SendAsync(request, cancellationToken); + } +} + diff --git a/src/Templates/AdminPanel/Bit.AdminPanel/src/Client/Core/Services/HttpMessageHandlers/RetryDelegatingHandler.cs b/src/Templates/AdminPanel/Bit.AdminPanel/src/Client/Core/Services/HttpMessageHandlers/RetryDelegatingHandler.cs new file mode 100644 index 0000000000..75fb1bffd0 --- /dev/null +++ b/src/Templates/AdminPanel/Bit.AdminPanel/src/Client/Core/Services/HttpMessageHandlers/RetryDelegatingHandler.cs @@ -0,0 +1,55 @@ +namespace AdminPanel.Client.Core.Services.HttpMessageHandlers; + +public class RetryDelegatingHandler + : DelegatingHandler +{ + public RetryDelegatingHandler(ExceptionDelegatingHandler handler) + : base(handler) + { + + } + + public RetryDelegatingHandler() + { + + } + + protected override async Task SendAsync(HttpRequestMessage request, CancellationToken cancellationToken) + { + var delays = GetDelays(scaleFirstTry: TimeSpan.FromSeconds(3), maxRetries: 3).ToArray(); + + Exception? lastExp = null; + + foreach (var delay in delays) + { + try + { + return await base.SendAsync(request, cancellationToken); + } + catch (Exception exp) when (exp is not KnownException) + { + lastExp = exp; + await Task.Delay(delay, cancellationToken); + } + } + + throw lastExp!; + } + + private static IEnumerable GetDelays(TimeSpan scaleFirstTry, int maxRetries) + { + TimeSpan maxValue = TimeSpan.MaxValue; + var maxTimeSpanDouble = maxValue.Ticks - 1_000.0; + var i = 0; + var targetTicksFirstDelay = scaleFirstTry.Ticks; + var num = 0.0; + for (; i < maxRetries; i++) + { + var num2 = i + Random.Shared.NextDouble(); + var next = Math.Pow(2.0, num2) * Math.Tanh(Math.Sqrt(4.0 * num2)); + var num3 = next - num; + yield return TimeSpan.FromTicks((long)Math.Min(num3 * 0.7_142_857_142_857_143 * targetTicksFirstDelay, maxTimeSpanDouble)); + num = next; + } + } +} diff --git a/src/Templates/AdminPanel/Bit.AdminPanel/src/Client/Web/Extensions/HttpRequestExtensions.cs b/src/Templates/AdminPanel/Bit.AdminPanel/src/Client/Web/Extensions/HttpRequestExtensions.cs index 5d373f0d99..b8d735e228 100644 --- a/src/Templates/AdminPanel/Bit.AdminPanel/src/Client/Web/Extensions/HttpRequestExtensions.cs +++ b/src/Templates/AdminPanel/Bit.AdminPanel/src/Client/Web/Extensions/HttpRequestExtensions.cs @@ -4,6 +4,20 @@ namespace Microsoft.AspNetCore.Http; public static class HttpRequestExtensions { + /// + /// https://blog.elmah.io/how-to-get-base-url-in-asp-net-core/ + /// + public static string GetBaseUrl(this HttpRequest req) + { + var uriBuilder = new UriBuilder(req.Scheme, req.Host.Host, req.Host.Port ?? -1); + if (uriBuilder.Uri.IsDefaultPort) + { + uriBuilder.Port = -1; + } + + return uriBuilder.Uri.AbsoluteUri; + } + public static bool ShouldRenderStaticMode(this HttpRequest request) { var agent = GetLoweredUserAgent(request); diff --git a/src/Templates/AdminPanel/Bit.AdminPanel/src/Client/Web/Program.BlazorElectron.cs b/src/Templates/AdminPanel/Bit.AdminPanel/src/Client/Web/Program.BlazorElectron.cs index ebe0bc9d3a..10d4f0301d 100644 --- a/src/Templates/AdminPanel/Bit.AdminPanel/src/Client/Web/Program.BlazorElectron.cs +++ b/src/Templates/AdminPanel/Bit.AdminPanel/src/Client/Web/Program.BlazorElectron.cs @@ -13,7 +13,7 @@ public static WebApplication CreateHostBuilder(string[] args) { var builder = WebApplication.CreateBuilder(args); - builder.Configuration.AddJsonStream(typeof(MainLayout).Assembly.GetManifestResourceStream("AdminPanel.Client.Core.appsettings.json")!); + builder.Configuration.AddClientConfigurations(); builder.WebHost.UseElectron(args); builder.Services.AddElectron(); diff --git a/src/Templates/AdminPanel/Bit.AdminPanel/src/Client/Web/Program.BlazorServer.cs b/src/Templates/AdminPanel/Bit.AdminPanel/src/Client/Web/Program.BlazorServer.cs index 1aa2cb8650..debcee9a15 100644 --- a/src/Templates/AdminPanel/Bit.AdminPanel/src/Client/Web/Program.BlazorServer.cs +++ b/src/Templates/AdminPanel/Bit.AdminPanel/src/Client/Web/Program.BlazorServer.cs @@ -9,7 +9,7 @@ public static WebApplication CreateHostBuilder(string[] args) { var builder = WebApplication.CreateBuilder(args); - builder.Configuration.AddJsonStream(typeof(MainLayout).Assembly.GetManifestResourceStream("AdminPanel.Client.Core.appsettings.json")!); + builder.Configuration.AddClientConfigurations(); #if DEBUG if (OperatingSystem.IsWindows()) diff --git a/src/Templates/AdminPanel/Bit.AdminPanel/src/Client/Web/Program.BlazorWebAssembly.cs b/src/Templates/AdminPanel/Bit.AdminPanel/src/Client/Web/Program.BlazorWebAssembly.cs index 56c799bf7f..dc2c0cc172 100644 --- a/src/Templates/AdminPanel/Bit.AdminPanel/src/Client/Web/Program.BlazorWebAssembly.cs +++ b/src/Templates/AdminPanel/Bit.AdminPanel/src/Client/Web/Program.BlazorWebAssembly.cs @@ -1,5 +1,6 @@ //-:cnd:noEmit #if BlazorWebAssembly +using AdminPanel.Client.Core.Services.HttpMessageHandlers; using Microsoft.AspNetCore.Components.WebAssembly.Hosting; using Microsoft.AspNetCore.Components.WebAssembly.Services; #endif @@ -13,13 +14,24 @@ public static WebAssemblyHost CreateHostBuilder(string[] args) { var builder = WebAssemblyHostBuilder.CreateDefault(); - builder.Configuration.AddJsonStream(typeof(MainLayout).Assembly.GetManifestResourceStream("AdminPanel.Client.Core.appsettings.json")!); + builder.Configuration.AddClientConfigurations(); - var apiServerAddressConfig = builder.Configuration.GetApiServerAddress(); + Uri.TryCreate(builder.Configuration.GetApiServerAddress(), UriKind.RelativeOrAbsolute, out var apiServerAddress); - var apiServerAddress = new Uri($"{builder.HostEnvironment.BaseAddress}{apiServerAddressConfig}"); + if (apiServerAddress!.IsAbsoluteUri is false) + { + apiServerAddress = new Uri($"{builder.HostEnvironment.BaseAddress}{apiServerAddress}"); + } - builder.Services.AddSingleton(sp => new HttpClient(sp.GetRequiredService()) { BaseAddress = apiServerAddress }); + builder.Services.AddSingleton(sp => + { + var handler = sp.GetRequiredService(); + HttpClient httpClient = new(handler) + { + BaseAddress = apiServerAddress + }; + return httpClient; + }); builder.Services.AddScoped(); builder.Services.AddTransient(); diff --git a/src/Templates/AdminPanel/Bit.AdminPanel/src/Client/Web/Startup/Services.cs b/src/Templates/AdminPanel/Bit.AdminPanel/src/Client/Web/Startup/Services.cs index 2b00cd2726..b5bafcb54c 100644 --- a/src/Templates/AdminPanel/Bit.AdminPanel/src/Client/Web/Startup/Services.cs +++ b/src/Templates/AdminPanel/Bit.AdminPanel/src/Client/Web/Startup/Services.cs @@ -1,6 +1,7 @@ //-:cnd:noEmit #if BlazorServer using System.IO.Compression; +using AdminPanel.Client.Core.Services.HttpMessageHandlers; using AdminPanel.Client.Web.Services; using Microsoft.AspNetCore.ResponseCompression; @@ -12,9 +13,11 @@ public static void Add(IServiceCollection services, IConfiguration configuration { services.AddScoped(sp => { - HttpClient httpClient = new(sp.GetRequiredService()) + Uri.TryCreate(configuration.GetApiServerAddress(), UriKind.Absolute, out var apiServerAddress); + var handler = sp.GetRequiredService(); + HttpClient httpClient = new(handler) { - BaseAddress = new Uri(sp.GetRequiredService().GetApiServerAddress()) + BaseAddress = apiServerAddress }; return httpClient; diff --git a/src/Templates/AdminPanel/Bit.AdminPanel/src/Server/Api/Program.cs b/src/Templates/AdminPanel/Bit.AdminPanel/src/Server/Api/Program.cs index 53d73c2fb8..bb21b9f744 100644 --- a/src/Templates/AdminPanel/Bit.AdminPanel/src/Server/Api/Program.cs +++ b/src/Templates/AdminPanel/Bit.AdminPanel/src/Server/Api/Program.cs @@ -1,6 +1,10 @@ //-:cnd:noEmit var builder = WebApplication.CreateBuilder(args); +#if BlazorWebAssembly +builder.Configuration.AddClientConfigurations(); +#endif + #if DEBUG // The following line (using the * in the URL), allows the emulators and mobile devices to access the app using the host IP address. if (OperatingSystem.IsWindows()) diff --git a/src/Templates/AdminPanel/Bit.AdminPanel/src/Server/Api/Properties/launchSettings.json b/src/Templates/AdminPanel/Bit.AdminPanel/src/Server/Api/Properties/launchSettings.json index fabcff7665..8599c09be3 100644 --- a/src/Templates/AdminPanel/Bit.AdminPanel/src/Server/Api/Properties/launchSettings.json +++ b/src/Templates/AdminPanel/Bit.AdminPanel/src/Server/Api/Properties/launchSettings.json @@ -4,7 +4,7 @@ "commandName": "Project", "dotnetRunMessages": true, "launchBrowser": true, - "applicationUrl": "https://localhost:5031;http://localhost:5030", + "applicationUrl": "http://localhost:5030;https://localhost:5031", "launchUrl": "swagger", "environmentVariables": { "ASPNETCORE_ENVIRONMENT": "Development" @@ -16,7 +16,7 @@ "dotnetRunMessages": true, "launchBrowser": true, "inspectUri": "{wsProtocol}://{url.hostname}:{url.port}/_framework/debug/ws-proxy?browser={browserInspectUri}", - "applicationUrl": "https://localhost:5031;http://localhost:5030", + "applicationUrl": "http://localhost:5030;https://localhost:5031", "environmentVariables": { "ASPNETCORE_ENVIRONMENT": "Development" } diff --git a/src/Templates/AdminPanel/Bit.AdminPanel/src/Server/Api/Startup/Services.cs b/src/Templates/AdminPanel/Bit.AdminPanel/src/Server/Api/Startup/Services.cs index 25d15c1b32..ec7fd65793 100644 --- a/src/Templates/AdminPanel/Bit.AdminPanel/src/Server/Api/Startup/Services.cs +++ b/src/Templates/AdminPanel/Bit.AdminPanel/src/Server/Api/Startup/Services.cs @@ -8,9 +8,11 @@ using Microsoft.AspNetCore.OData; using Microsoft.AspNetCore.ResponseCompression; #if BlazorWebAssembly +using AdminPanel.Client.Core.Services.HttpMessageHandlers; using AdminPanel.Client.Web.Services; using AdminPanel.Client.Core.Services; using Microsoft.AspNetCore.Components; +using Microsoft.JSInterop; #endif namespace AdminPanel.Server.Api.Startup; @@ -35,13 +37,20 @@ public static void Add(IServiceCollection services, IWebHostEnvironment env, ICo // In the Pre-Rendering mode, the configured HttpClient will use the access_token provided by the cookie in the request, so the pre-rendered content would be fitting for the current user. services.AddHttpClient("WebAssemblyPreRenderingHttpClient") - .ConfigurePrimaryHttpMessageHandler() + .AddHttpMessageHandler(sp => new LocalizationDelegatingHandler()) + .AddHttpMessageHandler(sp => new AuthDelegatingHandler(sp.GetRequiredService(), sp.GetRequiredService())) + .AddHttpMessageHandler(sp => new RetryDelegatingHandler()) + .AddHttpMessageHandler(sp => new ExceptionDelegatingHandler()) + .ConfigurePrimaryHttpMessageHandler() .ConfigureHttpClient((sp, httpClient) => { - NavigationManager navManager = sp.GetRequiredService().HttpContext!.RequestServices.GetRequiredService(); - httpClient.BaseAddress = new Uri($"{navManager.BaseUri}api/"); + Uri.TryCreate(configuration.GetApiServerAddress(), UriKind.RelativeOrAbsolute, out var apiServerAddress); + if (apiServerAddress!.IsAbsoluteUri is false) + { + apiServerAddress = new Uri($"{sp.GetRequiredService().HttpContext!.Request.GetBaseUrl()}{apiServerAddress}"); + } + httpClient.BaseAddress = apiServerAddress; }); - services.AddScoped(); services.AddScoped(sp => { @@ -50,8 +59,9 @@ public static void Add(IServiceCollection services, IWebHostEnvironment env, ICo // this is for pre rendering of blazor client/wasm // for other usages of http client, for example calling 3rd party apis, either use services.AddHttpClient("NamedHttpClient") or services.AddHttpClient(); }); + + services.AddScoped(); services.AddRazorPages(); - services.AddMvcCore(); #endif //+:cnd:noEmit diff --git a/src/Templates/BlazorDual/Bit.BlazorDual/src/BlazorDual.Api/Program.cs b/src/Templates/BlazorDual/Bit.BlazorDual/src/BlazorDual.Api/Program.cs index 28cea426f1..8b0ecef3a9 100644 --- a/src/Templates/BlazorDual/Bit.BlazorDual/src/BlazorDual.Api/Program.cs +++ b/src/Templates/BlazorDual/Bit.BlazorDual/src/BlazorDual.Api/Program.cs @@ -1,6 +1,10 @@ //-:cnd:noEmit var builder = WebApplication.CreateBuilder(args); +#if BlazorWebAssembly +builder.Configuration.AddClientConfigurations(); +#endif + BlazorDual.Api.Startup.Services.Add(builder.Services, builder.Environment, builder.Configuration); var app = builder.Build(); diff --git a/src/Templates/BlazorDual/Bit.BlazorDual/src/BlazorDual.Api/Startup/Services.cs b/src/Templates/BlazorDual/Bit.BlazorDual/src/BlazorDual.Api/Startup/Services.cs index 62d49a9251..78ee0d991d 100644 --- a/src/Templates/BlazorDual/Bit.BlazorDual/src/BlazorDual.Api/Startup/Services.cs +++ b/src/Templates/BlazorDual/Bit.BlazorDual/src/BlazorDual.Api/Startup/Services.cs @@ -8,6 +8,9 @@ using Microsoft.AspNetCore.OData; using Microsoft.AspNetCore.ResponseCompression; #if BlazorWebAssembly +using System.Net.Http; +using BlazorDual.Web.Services.HttpMessageHandlers; +using Microsoft.JSInterop; using Microsoft.AspNetCore.Components; using BlazorDual.Web.Services; #endif @@ -33,11 +36,21 @@ public static void Add(IServiceCollection services, IWebHostEnvironment env, ICo // In the Pre-Rendering mode, the configured HttpClient will use the access_token provided by the cookie in the request, so the pre-rendered content would be fitting for the current user. services.AddHttpClient("WebAssemblyPreRenderingHttpClient") - .ConfigurePrimaryHttpMessageHandler() + .AddHttpMessageHandler(sp => new LocalizationDelegatingHandler()) + .AddHttpMessageHandler(sp => new AuthDelegatingHandler(sp.GetRequiredService(), sp.GetRequiredService())) + .AddHttpMessageHandler(sp => new RetryDelegatingHandler()) + .AddHttpMessageHandler(sp => new ExceptionDelegatingHandler()) + .ConfigurePrimaryHttpMessageHandler() .ConfigureHttpClient((sp, httpClient) => { - NavigationManager navManager = sp.GetRequiredService().HttpContext!.RequestServices.GetRequiredService(); - httpClient.BaseAddress = new Uri($"{navManager.BaseUri}api/"); + Uri.TryCreate(configuration.GetApiServerAddress(), UriKind.RelativeOrAbsolute, out var apiServerAddress); + + if (apiServerAddress!.IsAbsoluteUri is false) + { + apiServerAddress = new Uri($"{sp.GetRequiredService().HttpContext!.Request.GetBaseUrl()}{apiServerAddress}"); + } + + httpClient.BaseAddress = apiServerAddress; }); services.AddScoped(); @@ -49,7 +62,6 @@ public static void Add(IServiceCollection services, IWebHostEnvironment env, ICo // for other usages of httpclient, for example calling 3rd party apis, either use services.AddHttpClient("NamedHttpClient") or services.AddHttpClient(); }); services.AddRazorPages(); - services.AddMvcCore(); #endif //+:cnd:noEmit diff --git a/src/Templates/BlazorDual/Bit.BlazorDual/src/BlazorDual.Web/App.razor b/src/Templates/BlazorDual/Bit.BlazorDual/src/BlazorDual.Web/App.razor index 190424cf1d..24896079a3 100644 --- a/src/Templates/BlazorDual/Bit.BlazorDual/src/BlazorDual.Web/App.razor +++ b/src/Templates/BlazorDual/Bit.BlazorDual/src/BlazorDual.Web/App.razor @@ -1,21 +1,23 @@ - - - - - - - - - - - - - - - - - - - - \ No newline at end of file + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/Templates/BlazorDual/Bit.BlazorDual/src/BlazorDual.Web/Extensions/HttpRequestExtensions.cs b/src/Templates/BlazorDual/Bit.BlazorDual/src/BlazorDual.Web/Extensions/HttpRequestExtensions.cs index 5d373f0d99..b8d735e228 100644 --- a/src/Templates/BlazorDual/Bit.BlazorDual/src/BlazorDual.Web/Extensions/HttpRequestExtensions.cs +++ b/src/Templates/BlazorDual/Bit.BlazorDual/src/BlazorDual.Web/Extensions/HttpRequestExtensions.cs @@ -4,6 +4,20 @@ namespace Microsoft.AspNetCore.Http; public static class HttpRequestExtensions { + /// + /// https://blog.elmah.io/how-to-get-base-url-in-asp-net-core/ + /// + public static string GetBaseUrl(this HttpRequest req) + { + var uriBuilder = new UriBuilder(req.Scheme, req.Host.Host, req.Host.Port ?? -1); + if (uriBuilder.Uri.IsDefaultPort) + { + uriBuilder.Port = -1; + } + + return uriBuilder.Uri.AbsoluteUri; + } + public static bool ShouldRenderStaticMode(this HttpRequest request) { var agent = GetLoweredUserAgent(request); diff --git a/src/Templates/BlazorDual/Bit.BlazorDual/src/BlazorDual.Web/Extensions/IConfigurationBuilderExtensions.cs b/src/Templates/BlazorDual/Bit.BlazorDual/src/BlazorDual.Web/Extensions/IConfigurationBuilderExtensions.cs new file mode 100644 index 0000000000..94acab9d9c --- /dev/null +++ b/src/Templates/BlazorDual/Bit.BlazorDual/src/BlazorDual.Web/Extensions/IConfigurationBuilderExtensions.cs @@ -0,0 +1,12 @@ +using System.Reflection; + +namespace Microsoft.Extensions.Configuration; + +public static class IConfigurationBuilderExtensions +{ + public static void AddClientConfigurations(this IConfigurationBuilder builder) + { + var assembly = Assembly.Load("BlazorDual.Web"); + builder.AddJsonStream(assembly.GetManifestResourceStream("BlazorDual.Web.appsettings.json")!); + } +} diff --git a/src/Templates/BlazorDual/Bit.BlazorDual/src/BlazorDual.Web/Extensions/IConfigurationExtensions.cs b/src/Templates/BlazorDual/Bit.BlazorDual/src/BlazorDual.Web/Extensions/IConfigurationExtensions.cs index f3e0fe69bb..4bb315c1db 100644 --- a/src/Templates/BlazorDual/Bit.BlazorDual/src/BlazorDual.Web/Extensions/IConfigurationExtensions.cs +++ b/src/Templates/BlazorDual/Bit.BlazorDual/src/BlazorDual.Web/Extensions/IConfigurationExtensions.cs @@ -3,6 +3,8 @@ public static class IConfigurationExtensions { public static string GetApiServerAddress(this IConfiguration configuration) { - return configuration.GetValue("ApiServerAddress") ?? throw new InvalidOperationException("Could not find ApiServerAddress config"); + var apiServerAddress = configuration.GetValue("ApiServerAddress", defaultValue: "api/")!; + + return Uri.TryCreate(apiServerAddress, UriKind.RelativeOrAbsolute, out _) ? apiServerAddress : throw new InvalidOperationException($"Api server address {apiServerAddress} is invalid"); } } diff --git a/src/Templates/BlazorDual/Bit.BlazorDual/src/BlazorDual.Web/Extensions/IServiceCollectionExtensions.cs b/src/Templates/BlazorDual/Bit.BlazorDual/src/BlazorDual.Web/Extensions/IServiceCollectionExtensions.cs index 820e662924..1e4260b515 100644 --- a/src/Templates/BlazorDual/Bit.BlazorDual/src/BlazorDual.Web/Extensions/IServiceCollectionExtensions.cs +++ b/src/Templates/BlazorDual/Bit.BlazorDual/src/BlazorDual.Web/Extensions/IServiceCollectionExtensions.cs @@ -1,4 +1,6 @@ //-:cnd:noEmit +using BlazorDual.Web.Services.HttpMessageHandlers; + namespace Microsoft.Extensions.DependencyInjection; public static class IServiceCollectionExtensions @@ -11,7 +13,11 @@ public static IServiceCollection AddClientSharedServices(this IServiceCollection services.AddScoped(); services.AddBitBlazorUIServices(); - services.AddTransient(); + services.AddTransient(); + services.AddTransient(); + services.AddTransient(); + services.AddTransient(); + services.AddTransient(); services.AddScoped(); services.AddScoped(); diff --git a/src/Templates/BlazorDual/Bit.BlazorDual/src/BlazorDual.Web/Program.BlazorServer.cs b/src/Templates/BlazorDual/Bit.BlazorDual/src/BlazorDual.Web/Program.BlazorServer.cs index f522a01da5..ab60fbe98c 100644 --- a/src/Templates/BlazorDual/Bit.BlazorDual/src/BlazorDual.Web/Program.BlazorServer.cs +++ b/src/Templates/BlazorDual/Bit.BlazorDual/src/BlazorDual.Web/Program.BlazorServer.cs @@ -9,7 +9,7 @@ public static WebApplication CreateHostBuilder(string[] args) { var builder = WebApplication.CreateBuilder(args); - builder.Configuration.AddJsonStream(typeof(MainLayout).Assembly.GetManifestResourceStream("BlazorDual.Web.appsettings.json")!); + builder.Configuration.AddClientConfigurations(); Startup.Services.Add(builder.Services, builder.Configuration); diff --git a/src/Templates/BlazorDual/Bit.BlazorDual/src/BlazorDual.Web/Program.BlazorWebAssembly.cs b/src/Templates/BlazorDual/Bit.BlazorDual/src/BlazorDual.Web/Program.BlazorWebAssembly.cs index d708c960d1..56ba4a3095 100644 --- a/src/Templates/BlazorDual/Bit.BlazorDual/src/BlazorDual.Web/Program.BlazorWebAssembly.cs +++ b/src/Templates/BlazorDual/Bit.BlazorDual/src/BlazorDual.Web/Program.BlazorWebAssembly.cs @@ -1,5 +1,6 @@ //-:cnd:noEmit #if BlazorWebAssembly +using BlazorDual.Web.Services.HttpMessageHandlers; using Microsoft.AspNetCore.Components.WebAssembly.Hosting; #endif @@ -11,19 +12,28 @@ public partial class Program public static WebAssemblyHost CreateHostBuilder(string[] args) { var builder = WebAssemblyHostBuilder.CreateDefault(); - builder.Configuration.AddJsonStream(typeof(MainLayout).Assembly.GetManifestResourceStream("BlazorDual.Web.appsettings.json")!); + builder.Configuration.AddClientConfigurations(); - if (Uri.TryCreate(builder.Configuration.GetApiServerAddress(), UriKind.RelativeOrAbsolute, out var apiServerAddress) is false) - { - throw new InvalidOperationException("Api server address is invalid"); - } + Uri.TryCreate(builder.Configuration.GetApiServerAddress(), UriKind.RelativeOrAbsolute, out var apiServerAddress); - if (apiServerAddress.IsAbsoluteUri is false) + if (apiServerAddress!.IsAbsoluteUri is false) { apiServerAddress = new Uri($"{builder.HostEnvironment.BaseAddress}{apiServerAddress}"); } - builder.Services.AddSingleton(sp => new HttpClient(sp.GetRequiredService()) { BaseAddress = apiServerAddress }); + builder.Services.AddSingleton(sp => + { + var handler = sp.GetRequiredService(); + + HttpClient httpClient = new(handler) + { + BaseAddress = apiServerAddress + }; + + return httpClient; + }); + + builder.Services.AddScoped(); builder.Services.AddTransient(); diff --git a/src/Templates/BlazorDual/Bit.BlazorDual/src/BlazorDual.Web/Services/HttpMessageHandlers/AuthDelegatingHandler.cs b/src/Templates/BlazorDual/Bit.BlazorDual/src/BlazorDual.Web/Services/HttpMessageHandlers/AuthDelegatingHandler.cs new file mode 100644 index 0000000000..efd679cb28 --- /dev/null +++ b/src/Templates/BlazorDual/Bit.BlazorDual/src/BlazorDual.Web/Services/HttpMessageHandlers/AuthDelegatingHandler.cs @@ -0,0 +1,47 @@ +using System.Net.Http.Headers; + +namespace BlazorDual.Web.Services.HttpMessageHandlers; + +public class AuthDelegatingHandler + : DelegatingHandler +{ + private IAuthTokenProvider _tokenProvider = default!; + private IJSRuntime _jsRuntime = default!; + + public AuthDelegatingHandler(IAuthTokenProvider tokenProvider, IJSRuntime jsRuntime, RetryDelegatingHandler handler) + : base(handler) + { + _tokenProvider = tokenProvider; + _jsRuntime = jsRuntime; + } + + public AuthDelegatingHandler(IAuthTokenProvider tokenProvider, IJSRuntime jsRuntime) + : base() + { + _tokenProvider = tokenProvider; + _jsRuntime = jsRuntime; + } + + protected override async Task SendAsync(HttpRequestMessage request, CancellationToken cancellationToken) + { + if (request.Headers.Authorization is null) + { + var access_token = await _tokenProvider.GetAccessTokenAsync(); + if (access_token is not null) + { + request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", access_token); + } + } + + try + { + return await base.SendAsync(request, cancellationToken); + } + catch (UnauthorizedException) + { + // try to get refresh token, store access token and refresh token, + // then use the new access token to request's authorization header and call base.SendAsync again. + throw; + } + } +} diff --git a/src/Templates/BlazorDual/Bit.BlazorDual/src/BlazorDual.Web/Services/AppHttpClientHandler.cs b/src/Templates/BlazorDual/Bit.BlazorDual/src/BlazorDual.Web/Services/HttpMessageHandlers/ExceptionDelegatingHandler.cs similarity index 72% rename from src/Templates/BlazorDual/Bit.BlazorDual/src/BlazorDual.Web/Services/AppHttpClientHandler.cs rename to src/Templates/BlazorDual/Bit.BlazorDual/src/BlazorDual.Web/Services/HttpMessageHandlers/ExceptionDelegatingHandler.cs index 332b6c4461..5e5c86434b 100644 --- a/src/Templates/BlazorDual/Bit.BlazorDual/src/BlazorDual.Web/Services/AppHttpClientHandler.cs +++ b/src/Templates/BlazorDual/Bit.BlazorDual/src/BlazorDual.Web/Services/HttpMessageHandlers/ExceptionDelegatingHandler.cs @@ -1,29 +1,23 @@ -//-:cnd:noEmit -using System.Net; -using System.Net.Http.Headers; +using System.Net; -namespace BlazorDual.Web.Services; +namespace BlazorDual.Web.Services.HttpMessageHandlers; -public partial class AppHttpClientHandler : HttpClientHandler +public class ExceptionDelegatingHandler + : DelegatingHandler { - [AutoInject] private IAuthTokenProvider _tokenProvider = default!; + public ExceptionDelegatingHandler(HttpClientHandler httpClientHandler) + : base(httpClientHandler) + { - protected override async Task SendAsync(HttpRequestMessage request, CancellationToken cancellationToken) + } + + public ExceptionDelegatingHandler() { - if (request.Headers.Authorization is null) - { - var access_token = await _tokenProvider.GetAccessTokenAsync(); - if (access_token is not null) - { - request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", access_token); - } - } -#if MultilingualEnabled && BlazorServer - string cultureCookie = $"c={CultureInfo.CurrentCulture.Name}|uic={CultureInfo.CurrentCulture.Name}"; - request.Headers.Add("Cookie", $".AspNetCore.Culture={cultureCookie}"); -#endif + } + protected override async Task SendAsync(HttpRequestMessage request, CancellationToken cancellationToken) + { bool serverCommunicationSuccess = false; try diff --git a/src/Templates/BlazorDual/Bit.BlazorDual/src/BlazorDual.Web/Services/HttpMessageHandlers/LocalizationDelegatingHandler.cs b/src/Templates/BlazorDual/Bit.BlazorDual/src/BlazorDual.Web/Services/HttpMessageHandlers/LocalizationDelegatingHandler.cs new file mode 100644 index 0000000000..45bdcf12b3 --- /dev/null +++ b/src/Templates/BlazorDual/Bit.BlazorDual/src/BlazorDual.Web/Services/HttpMessageHandlers/LocalizationDelegatingHandler.cs @@ -0,0 +1,28 @@ +using System.Net.Http.Headers; + +namespace BlazorDual.Web.Services.HttpMessageHandlers; + +public class LocalizationDelegatingHandler + : DelegatingHandler +{ + public LocalizationDelegatingHandler(AuthDelegatingHandler handler) + : base(handler) + { + + } + + public LocalizationDelegatingHandler() + { + + } + + protected override async Task SendAsync(HttpRequestMessage request, CancellationToken cancellationToken) + { +#if MultilingualEnabled + request.Headers.AcceptLanguage.Add(new StringWithQualityHeaderValue(CultureInfo.CurrentCulture.Name)); +#endif + + return await base.SendAsync(request, cancellationToken); + } +} + diff --git a/src/Templates/BlazorDual/Bit.BlazorDual/src/BlazorDual.Web/Services/HttpMessageHandlers/RetryDelegatingHandler.cs b/src/Templates/BlazorDual/Bit.BlazorDual/src/BlazorDual.Web/Services/HttpMessageHandlers/RetryDelegatingHandler.cs new file mode 100644 index 0000000000..906920353d --- /dev/null +++ b/src/Templates/BlazorDual/Bit.BlazorDual/src/BlazorDual.Web/Services/HttpMessageHandlers/RetryDelegatingHandler.cs @@ -0,0 +1,55 @@ +namespace BlazorDual.Web.Services.HttpMessageHandlers; + +public class RetryDelegatingHandler + : DelegatingHandler +{ + public RetryDelegatingHandler(ExceptionDelegatingHandler handler) + : base(handler) + { + + } + + public RetryDelegatingHandler() + { + + } + + protected override async Task SendAsync(HttpRequestMessage request, CancellationToken cancellationToken) + { + var delays = GetDelays(scaleFirstTry: TimeSpan.FromSeconds(3), maxRetries: 3).ToArray(); + + Exception? lastExp = null; + + foreach (var delay in delays) + { + try + { + return await base.SendAsync(request, cancellationToken); + } + catch (Exception exp) when (exp is not KnownException) + { + lastExp = exp; + await Task.Delay(delay, cancellationToken); + } + } + + throw lastExp!; + } + + private static IEnumerable GetDelays(TimeSpan scaleFirstTry, int maxRetries) + { + TimeSpan maxValue = TimeSpan.MaxValue; + var maxTimeSpanDouble = maxValue.Ticks - 1_000.0; + var i = 0; + var targetTicksFirstDelay = scaleFirstTry.Ticks; + var num = 0.0; + for (; i < maxRetries; i++) + { + var num2 = i + Random.Shared.NextDouble(); + var next = Math.Pow(2.0, num2) * Math.Tanh(Math.Sqrt(4.0 * num2)); + var num3 = next - num; + yield return TimeSpan.FromTicks((long)Math.Min(num3 * 0.7_142_857_142_857_143 * targetTicksFirstDelay, maxTimeSpanDouble)); + num = next; + } + } +} diff --git a/src/Templates/BlazorDual/Bit.BlazorDual/src/BlazorDual.Web/Startup/Services.cs b/src/Templates/BlazorDual/Bit.BlazorDual/src/BlazorDual.Web/Startup/Services.cs index f1de2ed66b..5377f2a02c 100644 --- a/src/Templates/BlazorDual/Bit.BlazorDual/src/BlazorDual.Web/Startup/Services.cs +++ b/src/Templates/BlazorDual/Bit.BlazorDual/src/BlazorDual.Web/Startup/Services.cs @@ -1,6 +1,7 @@ //-:cnd:noEmit #if BlazorServer using System.IO.Compression; +using BlazorDual.Web.Services.HttpMessageHandlers; using Microsoft.AspNetCore.ResponseCompression; namespace BlazorDual.Web.Startup; @@ -11,9 +12,13 @@ public static void Add(IServiceCollection services, IConfiguration configuration { services.AddScoped(sp => { - HttpClient httpClient = new(sp.GetRequiredService()) + Uri.TryCreate(configuration.GetApiServerAddress(), UriKind.Absolute, out var apiServerAddress); + + var handler = sp.GetRequiredService(); + + HttpClient httpClient = new(handler) { - BaseAddress = new Uri(sp.GetRequiredService().GetApiServerAddress()) + BaseAddress = apiServerAddress }; return httpClient; diff --git a/src/Templates/BlazorWeb/Bit.BlazorWeb/src/BlazorWeb.Client/Extensions/IConfigurationBuilderExtensions.cs b/src/Templates/BlazorWeb/Bit.BlazorWeb/src/BlazorWeb.Client/Extensions/IConfigurationBuilderExtensions.cs index 85c0fee3dd..d3a7f57a74 100644 --- a/src/Templates/BlazorWeb/Bit.BlazorWeb/src/BlazorWeb.Client/Extensions/IConfigurationBuilderExtensions.cs +++ b/src/Templates/BlazorWeb/Bit.BlazorWeb/src/BlazorWeb.Client/Extensions/IConfigurationBuilderExtensions.cs @@ -4,7 +4,7 @@ namespace Microsoft.Extensions.Configuration; public static class IConfigurationBuilderExtensions { - public static void AddClientAppConfigurations(this IConfigurationBuilder builder) + public static void AddClientConfigurations(this IConfigurationBuilder builder) { var assembly = Assembly.Load("BlazorWeb.Client"); builder.AddJsonStream(assembly.GetManifestResourceStream("BlazorWeb.Client.appsettings.json")!); diff --git a/src/Templates/BlazorWeb/Bit.BlazorWeb/src/BlazorWeb.Client/Extensions/IServiceCollectionExtensions.cs b/src/Templates/BlazorWeb/Bit.BlazorWeb/src/BlazorWeb.Client/Extensions/IServiceCollectionExtensions.cs index 93d83e9347..084d53a286 100644 --- a/src/Templates/BlazorWeb/Bit.BlazorWeb/src/BlazorWeb.Client/Extensions/IServiceCollectionExtensions.cs +++ b/src/Templates/BlazorWeb/Bit.BlazorWeb/src/BlazorWeb.Client/Extensions/IServiceCollectionExtensions.cs @@ -1,4 +1,6 @@ //-:cnd:noEmit +using BlazorWeb.Web.Services.HttpMessageHandlers; + namespace Microsoft.Extensions.DependencyInjection; public static class IServiceCollectionExtensions @@ -13,7 +15,11 @@ public static IServiceCollection AddClientSharedServices(this IServiceCollection services.AddScoped(); services.AddBitBlazorUIServices(); - services.AddTransient(); + services.AddTransient(); + services.AddTransient(); + services.AddTransient(); + services.AddTransient(); + services.AddTransient(); services.AddScoped(); services.AddScoped(); diff --git a/src/Templates/BlazorWeb/Bit.BlazorWeb/src/BlazorWeb.Client/Pages/Todo/TodoPage.razor.cs b/src/Templates/BlazorWeb/Bit.BlazorWeb/src/BlazorWeb.Client/Pages/Todo/TodoPage.razor.cs index 19eafcf4c4..7a41703215 100644 --- a/src/Templates/BlazorWeb/Bit.BlazorWeb/src/BlazorWeb.Client/Pages/Todo/TodoPage.razor.cs +++ b/src/Templates/BlazorWeb/Bit.BlazorWeb/src/BlazorWeb.Client/Pages/Todo/TodoPage.razor.cs @@ -39,8 +39,6 @@ private async Task LoadTodoItems() try { - await Task.Delay(10_000); - _allTodoItems = await PrerenderStateService.GetValue($"{nameof(TodoPage)}-allTodoItems", async () => await HttpClient.GetFromJsonAsync("TodoItem/Get", AppJsonContext.Default.ListTodoItemDto)) ?? []; diff --git a/src/Templates/BlazorWeb/Bit.BlazorWeb/src/BlazorWeb.Client/Program.cs b/src/Templates/BlazorWeb/Bit.BlazorWeb/src/BlazorWeb.Client/Program.cs index 6fbd6f107c..6790849c6c 100644 --- a/src/Templates/BlazorWeb/Bit.BlazorWeb/src/BlazorWeb.Client/Program.cs +++ b/src/Templates/BlazorWeb/Bit.BlazorWeb/src/BlazorWeb.Client/Program.cs @@ -1,8 +1,9 @@ -using Microsoft.AspNetCore.Components.WebAssembly.Hosting; +using BlazorWeb.Web.Services.HttpMessageHandlers; +using Microsoft.AspNetCore.Components.WebAssembly.Hosting; var builder = WebAssemblyHostBuilder.CreateDefault(args); -builder.Configuration.AddClientAppConfigurations(); +builder.Configuration.AddClientConfigurations(); Uri.TryCreate(builder.Configuration.GetApiServerAddress(), UriKind.RelativeOrAbsolute, out var apiServerAddress); @@ -11,7 +12,7 @@ apiServerAddress = new Uri($"{builder.HostEnvironment.BaseAddress}{apiServerAddress}"); } -builder.Services.AddSingleton(sp => new HttpClient(sp.GetRequiredService()) { BaseAddress = apiServerAddress }); +builder.Services.AddSingleton(sp => new HttpClient(sp.GetRequiredService()) { BaseAddress = apiServerAddress }); builder.Services.AddScoped(); builder.Services.AddTransient(); diff --git a/src/Templates/BlazorWeb/Bit.BlazorWeb/src/BlazorWeb.Client/Routes.razor b/src/Templates/BlazorWeb/Bit.BlazorWeb/src/BlazorWeb.Client/Routes.razor index a67f1664d2..b417b9b99e 100644 --- a/src/Templates/BlazorWeb/Bit.BlazorWeb/src/BlazorWeb.Client/Routes.razor +++ b/src/Templates/BlazorWeb/Bit.BlazorWeb/src/BlazorWeb.Client/Routes.razor @@ -1,15 +1,17 @@ - - - - - - - - - - - - - - \ No newline at end of file + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/Templates/BlazorWeb/Bit.BlazorWeb/src/BlazorWeb.Client/Services/HttpMessageHandlers/AuthDelegatingHandler.cs b/src/Templates/BlazorWeb/Bit.BlazorWeb/src/BlazorWeb.Client/Services/HttpMessageHandlers/AuthDelegatingHandler.cs new file mode 100644 index 0000000000..9ea606a35c --- /dev/null +++ b/src/Templates/BlazorWeb/Bit.BlazorWeb/src/BlazorWeb.Client/Services/HttpMessageHandlers/AuthDelegatingHandler.cs @@ -0,0 +1,47 @@ +using System.Net.Http.Headers; + +namespace BlazorWeb.Web.Services.HttpMessageHandlers; + +public class AuthDelegatingHandler + : DelegatingHandler +{ + private IAuthTokenProvider _tokenProvider = default!; + private IJSRuntime _jsRuntime = default!; + + public AuthDelegatingHandler(IAuthTokenProvider tokenProvider, IJSRuntime jsRuntime, RetryDelegatingHandler handler) + : base(handler) + { + _tokenProvider = tokenProvider; + _jsRuntime = jsRuntime; + } + + public AuthDelegatingHandler(IAuthTokenProvider tokenProvider, IJSRuntime jsRuntime) + : base() + { + _tokenProvider = tokenProvider; + _jsRuntime = jsRuntime; + } + + protected override async Task SendAsync(HttpRequestMessage request, CancellationToken cancellationToken) + { + if (request.Headers.Authorization is null) + { + var access_token = await _tokenProvider.GetAccessTokenAsync(); + if (access_token is not null) + { + request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", access_token); + } + } + + try + { + return await base.SendAsync(request, cancellationToken); + } + catch (UnauthorizedException) + { + // try to get refresh token, store access token and refresh token, + // then use the new access token to request's authorization header and call base.SendAsync again. + throw; + } + } +} diff --git a/src/Templates/TodoTemplate/Bit.TodoTemplate/src/Client/Core/Services/AppHttpClientHandler.cs b/src/Templates/BlazorWeb/Bit.BlazorWeb/src/BlazorWeb.Client/Services/HttpMessageHandlers/ExceptionDelegatingHandler.cs similarity index 69% rename from src/Templates/TodoTemplate/Bit.TodoTemplate/src/Client/Core/Services/AppHttpClientHandler.cs rename to src/Templates/BlazorWeb/Bit.BlazorWeb/src/BlazorWeb.Client/Services/HttpMessageHandlers/ExceptionDelegatingHandler.cs index 38c9d316d3..d119f0a41c 100644 --- a/src/Templates/TodoTemplate/Bit.TodoTemplate/src/Client/Core/Services/AppHttpClientHandler.cs +++ b/src/Templates/BlazorWeb/Bit.BlazorWeb/src/BlazorWeb.Client/Services/HttpMessageHandlers/ExceptionDelegatingHandler.cs @@ -1,29 +1,23 @@ -//-:cnd:noEmit -using System.Net; -using System.Net.Http.Headers; +using System.Net; -namespace TodoTemplate.Client.Core.Services; +namespace BlazorWeb.Web.Services.HttpMessageHandlers; -public partial class AppHttpClientHandler : HttpClientHandler +public class ExceptionDelegatingHandler + : DelegatingHandler { - [AutoInject] private IAuthTokenProvider _tokenProvider = default!; + public ExceptionDelegatingHandler(HttpClientHandler httpClientHandler) + : base(httpClientHandler) + { - protected override async Task SendAsync(HttpRequestMessage request, CancellationToken cancellationToken) + } + + public ExceptionDelegatingHandler() { - if (request.Headers.Authorization is null) - { - var access_token = await _tokenProvider.GetAccessTokenAsync(); - if (access_token is not null) - { - request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", access_token); - } - } -#if MultilingualEnabled && (BlazorServer || BlazorHybrid) - string cultureCookie = $"c={CultureInfo.CurrentCulture.Name}|uic={CultureInfo.CurrentCulture.Name}"; - request.Headers.Add("Cookie", $".AspNetCore.Culture={cultureCookie}"); -#endif + } + protected override async Task SendAsync(HttpRequestMessage request, CancellationToken cancellationToken) + { bool serverCommunicationSuccess = false; try @@ -43,7 +37,7 @@ protected override async Task SendAsync(HttpRequestMessage { RestErrorInfo restError = (await response!.Content.ReadFromJsonAsync(AppJsonContext.Default.RestErrorInfo, cancellationToken))!; - Type exceptionType = typeof(RestErrorInfo).Assembly.GetType(restError!.ExceptionType!) ?? typeof(UnknownException); + Type exceptionType = typeof(RestErrorInfo).Assembly.GetType(restError.ExceptionType!) ?? typeof(UnknownException); var args = new List { typeof(KnownException).IsAssignableFrom(exceptionType) ? new LocalizedString(restError.Key!, restError.Message!) : restError.Message! }; diff --git a/src/Templates/BlazorWeb/Bit.BlazorWeb/src/BlazorWeb.Client/Services/HttpMessageHandlers/LocalizationDelegatingHandler.cs b/src/Templates/BlazorWeb/Bit.BlazorWeb/src/BlazorWeb.Client/Services/HttpMessageHandlers/LocalizationDelegatingHandler.cs new file mode 100644 index 0000000000..a53c844c73 --- /dev/null +++ b/src/Templates/BlazorWeb/Bit.BlazorWeb/src/BlazorWeb.Client/Services/HttpMessageHandlers/LocalizationDelegatingHandler.cs @@ -0,0 +1,28 @@ +using System.Net.Http.Headers; + +namespace BlazorWeb.Web.Services.HttpMessageHandlers; + +public class LocalizationDelegatingHandler + : DelegatingHandler +{ + public LocalizationDelegatingHandler(AuthDelegatingHandler handler) + : base(handler) + { + + } + + public LocalizationDelegatingHandler() + { + + } + + protected override async Task SendAsync(HttpRequestMessage request, CancellationToken cancellationToken) + { +#if MultilingualEnabled + request.Headers.AcceptLanguage.Add(new StringWithQualityHeaderValue(CultureInfo.CurrentCulture.Name)); +#endif + + return await base.SendAsync(request, cancellationToken); + } +} + diff --git a/src/Templates/BlazorWeb/Bit.BlazorWeb/src/BlazorWeb.Client/Services/HttpMessageHandlers/RetryDelegatingHandler.cs b/src/Templates/BlazorWeb/Bit.BlazorWeb/src/BlazorWeb.Client/Services/HttpMessageHandlers/RetryDelegatingHandler.cs new file mode 100644 index 0000000000..4c7a3c6d51 --- /dev/null +++ b/src/Templates/BlazorWeb/Bit.BlazorWeb/src/BlazorWeb.Client/Services/HttpMessageHandlers/RetryDelegatingHandler.cs @@ -0,0 +1,55 @@ +namespace BlazorWeb.Web.Services.HttpMessageHandlers; + +public class RetryDelegatingHandler + : DelegatingHandler +{ + public RetryDelegatingHandler(ExceptionDelegatingHandler handler) + : base(handler) + { + + } + + public RetryDelegatingHandler() + { + + } + + protected override async Task SendAsync(HttpRequestMessage request, CancellationToken cancellationToken) + { + var delays = GetDelays(scaleFirstTry: TimeSpan.FromSeconds(3), maxRetries: 3).ToArray(); + + Exception? lastExp = null; + + foreach (var delay in delays) + { + try + { + return await base.SendAsync(request, cancellationToken); + } + catch (Exception exp) when (exp is not KnownException) + { + lastExp = exp; + await Task.Delay(delay, cancellationToken); + } + } + + throw lastExp!; + } + + private static IEnumerable GetDelays(TimeSpan scaleFirstTry, int maxRetries) + { + TimeSpan maxValue = TimeSpan.MaxValue; + var maxTimeSpanDouble = maxValue.Ticks - 1_000.0; + var i = 0; + var targetTicksFirstDelay = scaleFirstTry.Ticks; + var num = 0.0; + for (; i < maxRetries; i++) + { + var num2 = i + Random.Shared.NextDouble(); + var next = Math.Pow(2.0, num2) * Math.Tanh(Math.Sqrt(4.0 * num2)); + var num3 = next - num; + yield return TimeSpan.FromTicks((long)Math.Min(num3 * 0.7_142_857_142_857_143 * targetTicksFirstDelay, maxTimeSpanDouble)); + num = next; + } + } +} diff --git a/src/Templates/BlazorWeb/Bit.BlazorWeb/src/BlazorWeb.Server/Extensions/HttpRequestExtensions.cs b/src/Templates/BlazorWeb/Bit.BlazorWeb/src/BlazorWeb.Server/Extensions/HttpRequestExtensions.cs index 81891299d6..b8d735e228 100644 --- a/src/Templates/BlazorWeb/Bit.BlazorWeb/src/BlazorWeb.Server/Extensions/HttpRequestExtensions.cs +++ b/src/Templates/BlazorWeb/Bit.BlazorWeb/src/BlazorWeb.Server/Extensions/HttpRequestExtensions.cs @@ -7,7 +7,7 @@ public static class HttpRequestExtensions /// /// https://blog.elmah.io/how-to-get-base-url-in-asp-net-core/ /// - public static string BaseUrl(this HttpRequest req) + public static string GetBaseUrl(this HttpRequest req) { var uriBuilder = new UriBuilder(req.Scheme, req.Host.Host, req.Host.Port ?? -1); if (uriBuilder.Uri.IsDefaultPort) diff --git a/src/Templates/BlazorWeb/Bit.BlazorWeb/src/BlazorWeb.Server/Extensions/IServiceCollectionExtensions.cs b/src/Templates/BlazorWeb/Bit.BlazorWeb/src/BlazorWeb.Server/Extensions/IServiceCollectionExtensions.cs index 2254c18cb6..e599457aa0 100644 --- a/src/Templates/BlazorWeb/Bit.BlazorWeb/src/BlazorWeb.Server/Extensions/IServiceCollectionExtensions.cs +++ b/src/Templates/BlazorWeb/Bit.BlazorWeb/src/BlazorWeb.Server/Extensions/IServiceCollectionExtensions.cs @@ -10,6 +10,8 @@ using BlazorWeb.Server.Models.Identity; using BlazorWeb.Server.Services; using BlazorWeb.Client.Services; +using Microsoft.JSInterop; +using BlazorWeb.Web.Services.HttpMessageHandlers; namespace Microsoft.Extensions.DependencyInjection; @@ -22,21 +24,25 @@ public static void AddBlazor(this IServiceCollection services, IConfiguration co services.AddScoped(sp => { IHttpClientFactory httpClientFactory = sp.GetRequiredService(); - return httpClientFactory.CreateClient("PreRenderingHttpClient"); - // This registers HttpClient for pre rendering purposes only, so to use http client to call 3rd party apis and other use cases, + return httpClientFactory.CreateClient("BlazorHttpClient"); + // This registers HttpClient for pre rendering & blazor server only, so to use http client to call 3rd party apis and other use cases, // either use services.AddHttpClient("NamedHttpClient") or services.AddHttpClient(); }); // In the Pre-Rendering mode, the configured HttpClient will use the access_token provided by the cookie in the request, so the pre-rendered content would be fitting for the current user. - services.AddHttpClient("PreRenderingHttpClient") - .ConfigurePrimaryHttpMessageHandler() + services.AddHttpClient("BlazorHttpClient") + .AddHttpMessageHandler(sp => new LocalizationDelegatingHandler()) + .AddHttpMessageHandler(sp => new AuthDelegatingHandler(sp.GetRequiredService(), sp.GetRequiredService())) + .AddHttpMessageHandler(sp => new RetryDelegatingHandler()) + .AddHttpMessageHandler(sp => new ExceptionDelegatingHandler()) + .ConfigurePrimaryHttpMessageHandler() .ConfigureHttpClient((sp, httpClient) => { Uri.TryCreate(configuration.GetApiServerAddress(), UriKind.RelativeOrAbsolute, out var apiServerAddress); if (apiServerAddress!.IsAbsoluteUri is false) { - apiServerAddress = new Uri($"{sp.GetRequiredService().HttpContext!.Request.BaseUrl()}{apiServerAddress}"); + apiServerAddress = new Uri($"{sp.GetRequiredService().HttpContext!.Request.GetBaseUrl()}{apiServerAddress}"); } httpClient.BaseAddress = apiServerAddress; diff --git a/src/Templates/BlazorWeb/Bit.BlazorWeb/src/BlazorWeb.Server/Program.cs b/src/Templates/BlazorWeb/Bit.BlazorWeb/src/BlazorWeb.Server/Program.cs index cb09ddd9f9..43b5df17de 100644 --- a/src/Templates/BlazorWeb/Bit.BlazorWeb/src/BlazorWeb.Server/Program.cs +++ b/src/Templates/BlazorWeb/Bit.BlazorWeb/src/BlazorWeb.Server/Program.cs @@ -2,7 +2,7 @@ var builder = WebApplication.CreateBuilder(args); // We need to load the client app configurations to prerender the app on server side. -builder.Configuration.AddClientAppConfigurations(); +builder.Configuration.AddClientConfigurations(); BlazorWeb.Server.Startup.Services.Add(builder.Services, builder.Environment, builder.Configuration); diff --git a/src/Templates/TodoTemplate/Bit.TodoTemplate/src/Client/App/MauiProgram.cs b/src/Templates/TodoTemplate/Bit.TodoTemplate/src/Client/App/MauiProgram.cs index c7d6cf7bce..d2579982c8 100644 --- a/src/Templates/TodoTemplate/Bit.TodoTemplate/src/Client/App/MauiProgram.cs +++ b/src/Templates/TodoTemplate/Bit.TodoTemplate/src/Client/App/MauiProgram.cs @@ -1,6 +1,7 @@ //-:cnd:noEmit using System.Reflection; using Microsoft.Extensions.FileProviders; +using TodoTemplate.Client.Core.Services.HttpMessageHandlers; namespace TodoTemplate.Client.App; @@ -26,13 +27,15 @@ public static MauiApp CreateMauiApp() services.AddBlazorWebViewDeveloperTools(); #endif + Uri.TryCreate(builder.Configuration.GetApiServerAddress(), UriKind.Absolute, out var apiServerAddress); + services.AddScoped(sp => { - HttpClient httpClient = new(sp.GetRequiredService()) + var handler = sp.GetRequiredService(); + HttpClient httpClient = new(handler) { - BaseAddress = new Uri(sp.GetRequiredService().GetApiServerAddress()) + BaseAddress = apiServerAddress }; - return httpClient; }); diff --git a/src/Templates/TodoTemplate/Bit.TodoTemplate/src/Client/Core/App.razor b/src/Templates/TodoTemplate/Bit.TodoTemplate/src/Client/Core/App.razor index e327935d65..484d0e0eba 100644 --- a/src/Templates/TodoTemplate/Bit.TodoTemplate/src/Client/Core/App.razor +++ b/src/Templates/TodoTemplate/Bit.TodoTemplate/src/Client/Core/App.razor @@ -1,22 +1,24 @@ - - - - - - - - - - - - - - - - - - - - \ No newline at end of file + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/Templates/TodoTemplate/Bit.TodoTemplate/src/Client/Core/Extensions/IConfigurationBuilderExtensions.cs b/src/Templates/TodoTemplate/Bit.TodoTemplate/src/Client/Core/Extensions/IConfigurationBuilderExtensions.cs new file mode 100644 index 0000000000..207e0ceb3a --- /dev/null +++ b/src/Templates/TodoTemplate/Bit.TodoTemplate/src/Client/Core/Extensions/IConfigurationBuilderExtensions.cs @@ -0,0 +1,12 @@ +using System.Reflection; + +namespace Microsoft.Extensions.Configuration; + +public static class IConfigurationBuilderExtensions +{ + public static void AddClientConfigurations(this IConfigurationBuilder builder) + { + var assembly = Assembly.Load("TodoTemplate.Client.Core"); + builder.AddJsonStream(assembly.GetManifestResourceStream("TodoTemplate.Client.Core.appsettings.json")!); + } +} diff --git a/src/Templates/TodoTemplate/Bit.TodoTemplate/src/Client/Core/Extensions/IConfigurationExtensions.cs b/src/Templates/TodoTemplate/Bit.TodoTemplate/src/Client/Core/Extensions/IConfigurationExtensions.cs index 4e96485f81..e88b29bed5 100644 --- a/src/Templates/TodoTemplate/Bit.TodoTemplate/src/Client/Core/Extensions/IConfigurationExtensions.cs +++ b/src/Templates/TodoTemplate/Bit.TodoTemplate/src/Client/Core/Extensions/IConfigurationExtensions.cs @@ -4,10 +4,8 @@ public static class IConfigurationExtensions { public static string GetApiServerAddress(this IConfiguration configuration) { -#if BlazorWebAssembly - return "api/"; -#else - return configuration.GetValue("ApiServerAddress") ?? throw new InvalidOperationException("Could not find ApiServerAddress config"); -#endif + var apiServerAddress = configuration.GetValue("ApiServerAddress", defaultValue: "api/")!; + + return Uri.TryCreate(apiServerAddress, UriKind.RelativeOrAbsolute, out _) ? apiServerAddress : throw new InvalidOperationException($"Api server address {apiServerAddress} is invalid"); } } diff --git a/src/Templates/TodoTemplate/Bit.TodoTemplate/src/Client/Core/Extensions/IServiceCollectionExtensions.cs b/src/Templates/TodoTemplate/Bit.TodoTemplate/src/Client/Core/Extensions/IServiceCollectionExtensions.cs index fc9fff0cf1..0616499d86 100644 --- a/src/Templates/TodoTemplate/Bit.TodoTemplate/src/Client/Core/Extensions/IServiceCollectionExtensions.cs +++ b/src/Templates/TodoTemplate/Bit.TodoTemplate/src/Client/Core/Extensions/IServiceCollectionExtensions.cs @@ -1,4 +1,6 @@ //-:cnd:noEmit +using TodoTemplate.Client.Core.Services.HttpMessageHandlers; + namespace Microsoft.Extensions.DependencyInjection; public static class IServiceCollectionExtensions @@ -12,7 +14,11 @@ public static IServiceCollection AddClientSharedServices(this IServiceCollection services.AddScoped(); services.AddBitBlazorUIServices(); - services.AddTransient(); + services.AddTransient(); + services.AddTransient(); + services.AddTransient(); + services.AddTransient(); + services.AddTransient(); services.AddScoped(); services.AddScoped(); diff --git a/src/Templates/TodoTemplate/Bit.TodoTemplate/src/Client/Core/Services/HttpMessageHandlers/AuthDelegatingHandler.cs b/src/Templates/TodoTemplate/Bit.TodoTemplate/src/Client/Core/Services/HttpMessageHandlers/AuthDelegatingHandler.cs new file mode 100644 index 0000000000..9eeb95a2f1 --- /dev/null +++ b/src/Templates/TodoTemplate/Bit.TodoTemplate/src/Client/Core/Services/HttpMessageHandlers/AuthDelegatingHandler.cs @@ -0,0 +1,47 @@ +using System.Net.Http.Headers; + +namespace TodoTemplate.Client.Core.Services.HttpMessageHandlers; + +public class AuthDelegatingHandler + : DelegatingHandler +{ + private IAuthTokenProvider _tokenProvider = default!; + private IJSRuntime _jsRuntime = default!; + + public AuthDelegatingHandler(IAuthTokenProvider tokenProvider, IJSRuntime jsRuntime, RetryDelegatingHandler handler) + : base(handler) + { + _tokenProvider = tokenProvider; + _jsRuntime = jsRuntime; + } + + public AuthDelegatingHandler(IAuthTokenProvider tokenProvider, IJSRuntime jsRuntime) + : base() + { + _tokenProvider = tokenProvider; + _jsRuntime = jsRuntime; + } + + protected override async Task SendAsync(HttpRequestMessage request, CancellationToken cancellationToken) + { + if (request.Headers.Authorization is null) + { + var access_token = await _tokenProvider.GetAccessTokenAsync(); + if (access_token is not null) + { + request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", access_token); + } + } + + try + { + return await base.SendAsync(request, cancellationToken); + } + catch (UnauthorizedException) + { + // try to get refresh token, store access token and refresh token, + // then use the new access token to request's authorization header and call base.SendAsync again. + throw; + } + } +} diff --git a/src/Templates/AdminPanel/Bit.AdminPanel/src/Client/Core/Services/AppHttpClientHandler.cs b/src/Templates/TodoTemplate/Bit.TodoTemplate/src/Client/Core/Services/HttpMessageHandlers/ExceptionDelegatingHandler.cs similarity index 55% rename from src/Templates/AdminPanel/Bit.AdminPanel/src/Client/Core/Services/AppHttpClientHandler.cs rename to src/Templates/TodoTemplate/Bit.TodoTemplate/src/Client/Core/Services/HttpMessageHandlers/ExceptionDelegatingHandler.cs index 90f898692f..7a3a3b9507 100644 --- a/src/Templates/AdminPanel/Bit.AdminPanel/src/Client/Core/Services/AppHttpClientHandler.cs +++ b/src/Templates/TodoTemplate/Bit.TodoTemplate/src/Client/Core/Services/HttpMessageHandlers/ExceptionDelegatingHandler.cs @@ -1,29 +1,23 @@ -//-:cnd:noEmit -using System.Net; -using System.Net.Http.Headers; +using System.Net; -namespace AdminPanel.Client.Core.Services; +namespace TodoTemplate.Client.Core.Services.HttpMessageHandlers; -public partial class AppHttpClientHandler : HttpClientHandler +public class ExceptionDelegatingHandler + : DelegatingHandler { - [AutoInject] private IAuthTokenProvider _tokenProvider = default!; + public ExceptionDelegatingHandler(HttpClientHandler httpClientHandler) + : base(httpClientHandler) + { - protected override async Task SendAsync(HttpRequestMessage request, CancellationToken cancellationToken) + } + + public ExceptionDelegatingHandler() { - if (request.Headers.Authorization is null) - { - var access_token = await _tokenProvider.GetAccessTokenAsync(); - if (access_token is not null) - { - request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", access_token); - } - } -#if MultilingualEnabled && (BlazorServer || BlazorHybrid) - string cultureCookie = $"c={CultureInfo.CurrentCulture.Name}|uic={CultureInfo.CurrentCulture.Name}"; - request.Headers.Add("Cookie", $".AspNetCore.Culture={cultureCookie}"); -#endif + } + protected override async Task SendAsync(HttpRequestMessage request, CancellationToken cancellationToken) + { bool serverCommunicationSuccess = false; try @@ -43,21 +37,18 @@ protected override async Task SendAsync(HttpRequestMessage { RestErrorInfo restError = (await response!.Content.ReadFromJsonAsync(AppJsonContext.Default.RestErrorInfo, cancellationToken))!; - Type exceptionType = typeof(RestErrorInfo).Assembly.GetType(restError.ExceptionType ?? string.Empty) ?? typeof(UnknownException); + Type exceptionType = typeof(RestErrorInfo).Assembly.GetType(restError.ExceptionType!) ?? typeof(UnknownException); - List args = new() - { - typeof(KnownException).IsAssignableFrom(exceptionType) - ? new LocalizedString(restError.Key ?? string.Empty, restError.Message ?? string.Empty) - : restError.Message ?? string.Empty - }; + var args = new List { typeof(KnownException).IsAssignableFrom(exceptionType) ? new LocalizedString(restError.Key!, restError.Message!) : restError.Message! }; if (exceptionType == typeof(ResourceValidationException)) { args.Add(restError.Payload); } - throw (Exception)(Activator.CreateInstance(exceptionType, args.ToArray()) ?? new Exception()); + Exception exp = (Exception)Activator.CreateInstance(exceptionType, args.ToArray())!; + + throw exp; } } diff --git a/src/Templates/TodoTemplate/Bit.TodoTemplate/src/Client/Core/Services/HttpMessageHandlers/LocalizationDelegatingHandler.cs b/src/Templates/TodoTemplate/Bit.TodoTemplate/src/Client/Core/Services/HttpMessageHandlers/LocalizationDelegatingHandler.cs new file mode 100644 index 0000000000..620b83af68 --- /dev/null +++ b/src/Templates/TodoTemplate/Bit.TodoTemplate/src/Client/Core/Services/HttpMessageHandlers/LocalizationDelegatingHandler.cs @@ -0,0 +1,28 @@ +using System.Net.Http.Headers; + +namespace TodoTemplate.Client.Core.Services.HttpMessageHandlers; + +public class LocalizationDelegatingHandler + : DelegatingHandler +{ + public LocalizationDelegatingHandler(AuthDelegatingHandler handler) + : base(handler) + { + + } + + public LocalizationDelegatingHandler() + { + + } + + protected override async Task SendAsync(HttpRequestMessage request, CancellationToken cancellationToken) + { +#if MultilingualEnabled + request.Headers.AcceptLanguage.Add(new StringWithQualityHeaderValue(CultureInfo.CurrentCulture.Name)); +#endif + + return await base.SendAsync(request, cancellationToken); + } +} + diff --git a/src/Templates/TodoTemplate/Bit.TodoTemplate/src/Client/Core/Services/HttpMessageHandlers/RetryDelegatingHandler.cs b/src/Templates/TodoTemplate/Bit.TodoTemplate/src/Client/Core/Services/HttpMessageHandlers/RetryDelegatingHandler.cs new file mode 100644 index 0000000000..77be0531bd --- /dev/null +++ b/src/Templates/TodoTemplate/Bit.TodoTemplate/src/Client/Core/Services/HttpMessageHandlers/RetryDelegatingHandler.cs @@ -0,0 +1,55 @@ +namespace TodoTemplate.Client.Core.Services.HttpMessageHandlers; + +public class RetryDelegatingHandler + : DelegatingHandler +{ + public RetryDelegatingHandler(ExceptionDelegatingHandler handler) + : base(handler) + { + + } + + public RetryDelegatingHandler() + { + + } + + protected override async Task SendAsync(HttpRequestMessage request, CancellationToken cancellationToken) + { + var delays = GetDelays(scaleFirstTry: TimeSpan.FromSeconds(3), maxRetries: 3).ToArray(); + + Exception? lastExp = null; + + foreach (var delay in delays) + { + try + { + return await base.SendAsync(request, cancellationToken); + } + catch (Exception exp) when (exp is not KnownException) + { + lastExp = exp; + await Task.Delay(delay, cancellationToken); + } + } + + throw lastExp!; + } + + private static IEnumerable GetDelays(TimeSpan scaleFirstTry, int maxRetries) + { + TimeSpan maxValue = TimeSpan.MaxValue; + var maxTimeSpanDouble = maxValue.Ticks - 1_000.0; + var i = 0; + var targetTicksFirstDelay = scaleFirstTry.Ticks; + var num = 0.0; + for (; i < maxRetries; i++) + { + var num2 = i + Random.Shared.NextDouble(); + var next = Math.Pow(2.0, num2) * Math.Tanh(Math.Sqrt(4.0 * num2)); + var num3 = next - num; + yield return TimeSpan.FromTicks((long)Math.Min(num3 * 0.7_142_857_142_857_143 * targetTicksFirstDelay, maxTimeSpanDouble)); + num = next; + } + } +} diff --git a/src/Templates/TodoTemplate/Bit.TodoTemplate/src/Client/Core/TodoTemplate.Client.Core.csproj b/src/Templates/TodoTemplate/Bit.TodoTemplate/src/Client/Core/TodoTemplate.Client.Core.csproj index 6b3bcd1ef8..d5e2c5170f 100644 --- a/src/Templates/TodoTemplate/Bit.TodoTemplate/src/Client/Core/TodoTemplate.Client.Core.csproj +++ b/src/Templates/TodoTemplate/Bit.TodoTemplate/src/Client/Core/TodoTemplate.Client.Core.csproj @@ -34,6 +34,7 @@ + diff --git a/src/Templates/TodoTemplate/Bit.TodoTemplate/src/Client/Web/Extensions/HttpRequestExtensions.cs b/src/Templates/TodoTemplate/Bit.TodoTemplate/src/Client/Web/Extensions/HttpRequestExtensions.cs index 5d373f0d99..b8d735e228 100644 --- a/src/Templates/TodoTemplate/Bit.TodoTemplate/src/Client/Web/Extensions/HttpRequestExtensions.cs +++ b/src/Templates/TodoTemplate/Bit.TodoTemplate/src/Client/Web/Extensions/HttpRequestExtensions.cs @@ -4,6 +4,20 @@ namespace Microsoft.AspNetCore.Http; public static class HttpRequestExtensions { + /// + /// https://blog.elmah.io/how-to-get-base-url-in-asp-net-core/ + /// + public static string GetBaseUrl(this HttpRequest req) + { + var uriBuilder = new UriBuilder(req.Scheme, req.Host.Host, req.Host.Port ?? -1); + if (uriBuilder.Uri.IsDefaultPort) + { + uriBuilder.Port = -1; + } + + return uriBuilder.Uri.AbsoluteUri; + } + public static bool ShouldRenderStaticMode(this HttpRequest request) { var agent = GetLoweredUserAgent(request); diff --git a/src/Templates/TodoTemplate/Bit.TodoTemplate/src/Client/Web/Program.BlazorElectron.cs b/src/Templates/TodoTemplate/Bit.TodoTemplate/src/Client/Web/Program.BlazorElectron.cs index 7cd2a00625..80e2a63748 100644 --- a/src/Templates/TodoTemplate/Bit.TodoTemplate/src/Client/Web/Program.BlazorElectron.cs +++ b/src/Templates/TodoTemplate/Bit.TodoTemplate/src/Client/Web/Program.BlazorElectron.cs @@ -12,7 +12,7 @@ public partial class Program public static WebApplication CreateHostBuilder(string[] args) { var builder = WebApplication.CreateBuilder(args); - builder.Configuration.AddJsonStream(typeof(MainLayout).Assembly.GetManifestResourceStream("TodoTemplate.Client.Core.appsettings.json")!); + builder.Configuration.AddClientConfigurations(); builder.WebHost.UseElectron(args); builder.Services.AddElectron(); diff --git a/src/Templates/TodoTemplate/Bit.TodoTemplate/src/Client/Web/Program.BlazorServer.cs b/src/Templates/TodoTemplate/Bit.TodoTemplate/src/Client/Web/Program.BlazorServer.cs index 6aeed07b84..306686c573 100644 --- a/src/Templates/TodoTemplate/Bit.TodoTemplate/src/Client/Web/Program.BlazorServer.cs +++ b/src/Templates/TodoTemplate/Bit.TodoTemplate/src/Client/Web/Program.BlazorServer.cs @@ -10,7 +10,7 @@ public partial class Program public static WebApplication CreateHostBuilder(string[] args) { var builder = WebApplication.CreateBuilder(args); - builder.Configuration.AddJsonStream(typeof(MainLayout).Assembly.GetManifestResourceStream("TodoTemplate.Client.Core.appsettings.json")!); + builder.Configuration.AddClientConfigurations(); #if DEBUG if (OperatingSystem.IsWindows()) diff --git a/src/Templates/TodoTemplate/Bit.TodoTemplate/src/Client/Web/Program.BlazorWebAssembly.cs b/src/Templates/TodoTemplate/Bit.TodoTemplate/src/Client/Web/Program.BlazorWebAssembly.cs index f939e99191..b44dcdd03b 100644 --- a/src/Templates/TodoTemplate/Bit.TodoTemplate/src/Client/Web/Program.BlazorWebAssembly.cs +++ b/src/Templates/TodoTemplate/Bit.TodoTemplate/src/Client/Web/Program.BlazorWebAssembly.cs @@ -1,5 +1,6 @@ //-:cnd:noEmit #if BlazorWebAssembly +using TodoTemplate.Client.Core.Services.HttpMessageHandlers; using Microsoft.AspNetCore.Components.WebAssembly.Hosting; #endif @@ -12,13 +13,25 @@ public static WebAssemblyHost CreateHostBuilder(string[] args) { var builder = WebAssemblyHostBuilder.CreateDefault(); - builder.Configuration.AddJsonStream(typeof(MainLayout).Assembly.GetManifestResourceStream("TodoTemplate.Client.Core.appsettings.json")!); + builder.Configuration.AddClientConfigurations(); - var apiServerAddressConfig = builder.Configuration.GetApiServerAddress(); + Uri.TryCreate(builder.Configuration.GetApiServerAddress(), UriKind.RelativeOrAbsolute, out var apiServerAddress); - var apiServerAddress = new Uri($"{builder.HostEnvironment.BaseAddress}{apiServerAddressConfig}"); + if (apiServerAddress!.IsAbsoluteUri is false) + { + apiServerAddress = new Uri($"{builder.HostEnvironment.BaseAddress}{apiServerAddress}"); + } + + builder.Services.AddSingleton(sp => + { + var handler = sp.GetRequiredService(); + HttpClient httpClient = new(handler) + { + BaseAddress = apiServerAddress + }; + return httpClient; + }); - builder.Services.AddSingleton(sp => new HttpClient(sp.GetRequiredService()) { BaseAddress = apiServerAddress }); builder.Services.AddScoped(); builder.Services.AddTransient(); diff --git a/src/Templates/TodoTemplate/Bit.TodoTemplate/src/Client/Web/Startup/Services.cs b/src/Templates/TodoTemplate/Bit.TodoTemplate/src/Client/Web/Startup/Services.cs index 2f150f6cd2..cb9a556ebb 100644 --- a/src/Templates/TodoTemplate/Bit.TodoTemplate/src/Client/Web/Startup/Services.cs +++ b/src/Templates/TodoTemplate/Bit.TodoTemplate/src/Client/Web/Startup/Services.cs @@ -2,6 +2,7 @@ #if BlazorServer using System.IO.Compression; using Microsoft.AspNetCore.ResponseCompression; +using TodoTemplate.Client.Core.Services.HttpMessageHandlers; using TodoTemplate.Client.Web.Services; namespace TodoTemplate.Client.Web.Startup; @@ -12,9 +13,11 @@ public static void Add(IServiceCollection services, IConfiguration configuration { services.AddScoped(sp => { - HttpClient httpClient = new(sp.GetRequiredService()) + Uri.TryCreate(configuration.GetApiServerAddress(), UriKind.Absolute, out var apiServerAddress); + var handler = sp.GetRequiredService(); + HttpClient httpClient = new(handler) { - BaseAddress = new Uri(sp.GetRequiredService().GetApiServerAddress()) + BaseAddress = apiServerAddress }; return httpClient; diff --git a/src/Templates/TodoTemplate/Bit.TodoTemplate/src/Server/Api/Program.cs b/src/Templates/TodoTemplate/Bit.TodoTemplate/src/Server/Api/Program.cs index fefa223af8..5cb3c8ea12 100644 --- a/src/Templates/TodoTemplate/Bit.TodoTemplate/src/Server/Api/Program.cs +++ b/src/Templates/TodoTemplate/Bit.TodoTemplate/src/Server/Api/Program.cs @@ -1,6 +1,10 @@ //-:cnd:noEmit var builder = WebApplication.CreateBuilder(args); +#if BlazorWebAssembly +builder.Configuration.AddClientConfigurations(); +#endif + #if DEBUG if (OperatingSystem.IsWindows()) { diff --git a/src/Templates/TodoTemplate/Bit.TodoTemplate/src/Server/Api/Properties/launchSettings.json b/src/Templates/TodoTemplate/Bit.TodoTemplate/src/Server/Api/Properties/launchSettings.json index 433960cb36..d4276f9602 100644 --- a/src/Templates/TodoTemplate/Bit.TodoTemplate/src/Server/Api/Properties/launchSettings.json +++ b/src/Templates/TodoTemplate/Bit.TodoTemplate/src/Server/Api/Properties/launchSettings.json @@ -4,7 +4,7 @@ "commandName": "Project", "dotnetRunMessages": true, "launchBrowser": true, - "applicationUrl": "https://localhost:5041;http://localhost:5040", + "applicationUrl": "http://localhost:5040;https://localhost:5041", "launchUrl": "swagger", "environmentVariables": { "ASPNETCORE_ENVIRONMENT": "Development" @@ -16,7 +16,7 @@ "dotnetRunMessages": true, "launchBrowser": true, "inspectUri": "{wsProtocol}://{url.hostname}:{url.port}/_framework/debug/ws-proxy?browser={browserInspectUri}", - "applicationUrl": "https://localhost:5041;http://localhost:5040", + "applicationUrl": "http://localhost:5040;https://localhost:5041", "environmentVariables": { "ASPNETCORE_ENVIRONMENT": "Development" } diff --git a/src/Templates/TodoTemplate/Bit.TodoTemplate/src/Server/Api/Startup/Services.cs b/src/Templates/TodoTemplate/Bit.TodoTemplate/src/Server/Api/Startup/Services.cs index 92dca6d71d..582dd80189 100644 --- a/src/Templates/TodoTemplate/Bit.TodoTemplate/src/Server/Api/Startup/Services.cs +++ b/src/Templates/TodoTemplate/Bit.TodoTemplate/src/Server/Api/Startup/Services.cs @@ -8,8 +8,11 @@ using Microsoft.AspNetCore.ResponseCompression; using TodoTemplate.Server.Api.Services; #if BlazorWebAssembly +using System.Net.Http; +using Microsoft.JSInterop; using TodoTemplate.Client.Web.Services; using TodoTemplate.Client.Core.Services; +using TodoTemplate.Client.Core.Services.HttpMessageHandlers; using Microsoft.AspNetCore.Components; #endif @@ -35,11 +38,19 @@ public static void Add(IServiceCollection services, IWebHostEnvironment env, ICo // In the Pre-Rendering mode, the configured HttpClient will use the access_token provided by the cookie in the request, so the pre-rendered content would be fitting for the current user. services.AddHttpClient("WebAssemblyPreRenderingHttpClient") - .ConfigurePrimaryHttpMessageHandler() + .AddHttpMessageHandler(sp => new LocalizationDelegatingHandler()) + .AddHttpMessageHandler(sp => new AuthDelegatingHandler(sp.GetRequiredService(), sp.GetRequiredService())) + .AddHttpMessageHandler(sp => new RetryDelegatingHandler()) + .AddHttpMessageHandler(sp => new ExceptionDelegatingHandler()) + .ConfigurePrimaryHttpMessageHandler() .ConfigureHttpClient((sp, httpClient) => { - NavigationManager navManager = sp.GetRequiredService().HttpContext!.RequestServices.GetRequiredService(); - httpClient.BaseAddress = new Uri($"{navManager.BaseUri}api/"); + Uri.TryCreate(configuration.GetApiServerAddress(), UriKind.RelativeOrAbsolute, out var apiServerAddress); + if (apiServerAddress!.IsAbsoluteUri is false) + { + apiServerAddress = new Uri($"{sp.GetRequiredService().HttpContext!.Request.GetBaseUrl()}{apiServerAddress}"); + } + httpClient.BaseAddress = apiServerAddress; }); services.AddScoped(); @@ -51,7 +62,6 @@ public static void Add(IServiceCollection services, IWebHostEnvironment env, ICo // for other usages of http client, for example calling 3rd party apis, either use services.AddHttpClient("NamedHttpClient") or services.AddHttpClient(); }); services.AddRazorPages(); - services.AddMvcCore(); #endif //+:cnd:noEmit