diff --git a/src/SourceGenerators/Bit.SourceGenerators/HttpClientProxy/HttpClientProxySourceGenerator.cs b/src/SourceGenerators/Bit.SourceGenerators/HttpClientProxy/HttpClientProxySourceGenerator.cs index fb713a5a19..07e69d324c 100644 --- a/src/SourceGenerators/Bit.SourceGenerators/HttpClientProxy/HttpClientProxySourceGenerator.cs +++ b/src/SourceGenerators/Bit.SourceGenerators/HttpClientProxy/HttpClientProxySourceGenerator.cs @@ -47,6 +47,10 @@ public void Execute(GeneratorExecutionContext context) var requestOptions = new StringBuilder(); requestOptions.AppendLine($"__request.Options.TryAdd(\"IControllerTypeName\", \"{iController.Symbol.GetAssemblyQualifiedName()}\");"); requestOptions.AppendLine($"__request.Options.TryAdd(\"ActionName\", \"{action.Method.Name}\");"); + requestOptions.AppendLine($@"__request.Options.TryAdd(""ActionParametersInfo"", new Dictionary + {{ + { string.Join(", ", action.Parameters.Select(p => $"{{ \"{p.Name}\", \"{p.Type.GetAssemblyQualifiedName()}\" }}")) } + }});"); if (action.BodyParameter is not null) { requestOptions.AppendLine($"__request.Options.TryAdd(\"RequestTypeName\", \"{action.BodyParameter.Type.GetAssemblyQualifiedName()}\");"); diff --git a/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Core/Controllers/Attributes.cs b/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Core/Controllers/Attributes.cs index f91c250104..05a417a760 100644 --- a/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Core/Controllers/Attributes.cs +++ b/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Core/Controllers/Attributes.cs @@ -35,3 +35,23 @@ internal class HttpPatchAttribute(string? template = null) : Attribute { public string? Template { get; } = template; } + +/// +/// Avoid retrying the request upon failure. +/// +/// +[AttributeUsage(AttributeTargets.Method)] +internal class NoRetryPolicyAttribute : Attribute +{ + +} + +/// +/// Ensure the authorization header is not set for the action. +/// +/// +[AttributeUsage(AttributeTargets.Method)] +internal class NoAuthorizeHeaderPolicyAttribute : Attribute +{ + +} diff --git a/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Core/Controllers/Identity/IIdentityController.cs b/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Core/Controllers/Identity/IIdentityController.cs index b6087bc15c..52f5bbfcc6 100644 --- a/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Core/Controllers/Identity/IIdentityController.cs +++ b/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Core/Controllers/Identity/IIdentityController.cs @@ -1,13 +1,11 @@ -using Boilerplate.Shared.Dtos.Identity; +//+:cnd:noEmit +using Boilerplate.Shared.Dtos.Identity; namespace Boilerplate.Client.Core.Controllers.Identity; [Route("api/[controller]/[action]/")] public interface IIdentityController : IAppController { - [HttpPost] - Task SignUp(SignUpRequestDto request, CancellationToken cancellationToken = default); - [HttpPost] Task SendConfirmEmailToken(SendEmailTokenRequestDto request, CancellationToken cancellationToken = default); @@ -29,6 +27,12 @@ public interface IIdentityController : IAppController [HttpPost] Task Refresh(RefreshRequestDto request, CancellationToken cancellationToken = default) => default!; + [HttpPost] + //#if (captcha == "reCaptcha") + [NoRetryPolicy] // Please note that retrying requests with Google reCaptcha will not work, as the Google verification mechanism only accepts a captcha response once. + //#endif + Task SignUp(SignUpRequestDto request, CancellationToken cancellationToken = default); + [HttpPost] Task SignIn(SignInRequestDto request, CancellationToken cancellationToken = default) => default!; diff --git a/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Core/Services/AuthenticationManager.cs b/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Core/Services/AuthenticationManager.cs index e974c1961f..17236687f2 100644 --- a/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Core/Services/AuthenticationManager.cs +++ b/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Core/Services/AuthenticationManager.cs @@ -112,7 +112,7 @@ await cookie.Set(new() { Name = "access_token", Value = response.AccessToken, - MaxAge = response.ExpiresIn, + MaxAge = rememberMe is true ? response.ExpiresIn : null, // to create a session cookie SameSite = SameSite.Strict, Secure = BuildConfiguration.IsRelease() }); diff --git a/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Core/Services/HttpMessageHandlers/AuthDelegatingHandler.cs b/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Core/Services/HttpMessageHandlers/AuthDelegatingHandler.cs index 7b5044b1e9..0baf6f0930 100644 --- a/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Core/Services/HttpMessageHandlers/AuthDelegatingHandler.cs +++ b/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Core/Services/HttpMessageHandlers/AuthDelegatingHandler.cs @@ -1,4 +1,6 @@ -using System.Net.Http.Headers; +using System.Reflection; +using System.Net.Http.Headers; +using Boilerplate.Client.Core.Controllers; namespace Boilerplate.Client.Core.Services.HttpMessageHandlers; @@ -7,7 +9,7 @@ public class AuthDelegatingHandler(IAuthTokenProvider tokenProvider, IServicePro { protected override async Task SendAsync(HttpRequestMessage request, CancellationToken cancellationToken) { - if (request.Headers.Authorization is null) + if (request.Headers.Authorization is null && HasNoAuthHeaderPolicy(request) is false) { var access_token = await tokenProvider.GetAccessTokenAsync(); if (access_token is not null) @@ -45,4 +47,18 @@ protected override async Task SendAsync(HttpRequestMessage return await base.SendAsync(request, cancellationToken); } } + + /// + /// + /// + private static bool HasNoAuthHeaderPolicy(HttpRequestMessage request) + { + if (request.Options.TryGetValue(new(RequestOptionNames.IControllerTypeName), out string? controllerTypeName) is false) + return false; + + var controllerType = Type.GetType(controllerTypeName!)!; + var parameterTypes = ((Dictionary)request.Options.GetValueOrDefault(RequestOptionNames.ActionParametersInfo)!).Select(p => Type.GetType(p.Value)!).ToArray(); + var method = controllerType.GetMethod((string)request.Options.GetValueOrDefault(RequestOptionNames.ActionName)!, parameterTypes)!; + return method.GetCustomAttribute() is not null; + } } diff --git a/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Core/Services/HttpMessageHandlers/RequestOptionNames.cs b/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Core/Services/HttpMessageHandlers/RequestOptionNames.cs new file mode 100644 index 0000000000..76ce24cef6 --- /dev/null +++ b/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Core/Services/HttpMessageHandlers/RequestOptionNames.cs @@ -0,0 +1,14 @@ +namespace Boilerplate.Client.Core.Services.HttpMessageHandlers; + +/// +/// The generated HTTP client proxy by Bit.SourceGenerators will automatically include these request options in the constructed HttpRequestMessage. +/// You can access these values within HTTP message handlers, such as . +/// +public class RequestOptionNames +{ + public const string IControllerTypeName = nameof(IControllerTypeName); + public const string ActionName = nameof(ActionName); + public const string ActionParametersInfo = nameof(ActionParametersInfo); + public const string RequestTypeName = nameof(RequestTypeName); + public const string ResponseTypeName = nameof(ResponseTypeName); +} diff --git a/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Core/Services/HttpMessageHandlers/RetryDelegatingHandler.cs b/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Core/Services/HttpMessageHandlers/RetryDelegatingHandler.cs index 519c2e467f..e36ec00962 100644 --- a/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Core/Services/HttpMessageHandlers/RetryDelegatingHandler.cs +++ b/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Core/Services/HttpMessageHandlers/RetryDelegatingHandler.cs @@ -1,4 +1,7 @@ -namespace Boilerplate.Client.Core.Services.HttpMessageHandlers; +using System.Reflection; +using Boilerplate.Client.Core.Controllers; + +namespace Boilerplate.Client.Core.Services.HttpMessageHandlers; public class RetryDelegatingHandler(ExceptionDelegatingHandler handler) : DelegatingHandler(handler) @@ -16,8 +19,11 @@ protected override async Task SendAsync(HttpRequestMessage { return await base.SendAsync(request, cancellationToken); } - catch (Exception exp) when (exp is not KnownException || exp is ServerConnectionException) + catch (Exception exp) when (exp is not KnownException || exp is ServerConnectionException) // If the exception is either unknown or a server connection issue, let's retry once more. { + if (HasNoRetryPolicy(request)) + throw; + lastExp = exp; await Task.Delay(delay, cancellationToken); } @@ -26,6 +32,20 @@ protected override async Task SendAsync(HttpRequestMessage throw lastExp!; } + /// + /// + /// + private static bool HasNoRetryPolicy(HttpRequestMessage request) + { + if (request.Options.TryGetValue(new(RequestOptionNames.IControllerTypeName), out string? controllerTypeName) is false) + return false; + + var controllerType = Type.GetType(controllerTypeName!)!; + var parameterTypes = ((Dictionary)request.Options.GetValueOrDefault(RequestOptionNames.ActionParametersInfo)!).Select(p => Type.GetType(p.Value)!).ToArray(); + var method = controllerType.GetMethod((string)request.Options.GetValueOrDefault(RequestOptionNames.ActionName)!, parameterTypes)!; + return method.GetCustomAttribute() is not null; + } + private static IEnumerable GetDelays(TimeSpan scaleFirstTry, int maxRetries) { TimeSpan maxValue = TimeSpan.MaxValue;