Skip to content

Commit

Permalink
feat(templates): disable request retry policy for SignUp page in Boil…
Browse files Browse the repository at this point in the history
…erplate project template #7864 (#7865)
  • Loading branch information
ysmoradi authored Jun 25, 2024
1 parent db49214 commit 8dd7d8e
Show file tree
Hide file tree
Showing 7 changed files with 87 additions and 9 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -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, string>
{{
{ 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()}\");");
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -35,3 +35,23 @@ internal class HttpPatchAttribute(string? template = null) : Attribute
{
public string? Template { get; } = template;
}

/// <summary>
/// Avoid retrying the request upon failure.
/// <see cref="Services.HttpMessageHandlers.RetryDelegatingHandler" />
/// </summary>
[AttributeUsage(AttributeTargets.Method)]
internal class NoRetryPolicyAttribute : Attribute
{

}

/// <summary>
/// Ensure the authorization header is not set for the action.
/// <see cref="Services.HttpMessageHandlers.AuthDelegatingHandler" />
/// </summary>
[AttributeUsage(AttributeTargets.Method)]
internal class NoAuthorizeHeaderPolicyAttribute : Attribute
{

}
Original file line number Diff line number Diff line change
@@ -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);

Expand All @@ -29,6 +27,12 @@ public interface IIdentityController : IAppController
[HttpPost]
Task<TokenResponseDto> 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<SignInResponseDto> SignIn(SignInRequestDto request, CancellationToken cancellationToken = default) => default!;

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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()
});
Expand Down
Original file line number Diff line number Diff line change
@@ -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;

Expand All @@ -7,7 +9,7 @@ public class AuthDelegatingHandler(IAuthTokenProvider tokenProvider, IServicePro
{
protected override async Task<HttpResponseMessage> 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)
Expand Down Expand Up @@ -45,4 +47,18 @@ protected override async Task<HttpResponseMessage> SendAsync(HttpRequestMessage
return await base.SendAsync(request, cancellationToken);
}
}

/// <summary>
/// <see cref="NoAuthorizeHeaderPolicyAttribute"/>
/// </summary>
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<string, string>)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<NoAuthorizeHeaderPolicyAttribute>() is not null;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
namespace Boilerplate.Client.Core.Services.HttpMessageHandlers;

/// <summary>
/// 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 <see cref="AuthDelegatingHandler"/>.
/// </summary>
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);
}
Original file line number Diff line number Diff line change
@@ -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)
Expand All @@ -16,8 +19,11 @@ protected override async Task<HttpResponseMessage> 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);
}
Expand All @@ -26,6 +32,20 @@ protected override async Task<HttpResponseMessage> SendAsync(HttpRequestMessage
throw lastExp!;
}

/// <summary>
/// <see cref="NoRetryPolicyAttribute"/>
/// </summary>
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<string, string>)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<NoRetryPolicyAttribute>() is not null;
}

private static IEnumerable<TimeSpan> GetDelays(TimeSpan scaleFirstTry, int maxRetries)
{
TimeSpan maxValue = TimeSpan.MaxValue;
Expand Down

0 comments on commit 8dd7d8e

Please sign in to comment.