Skip to content

Commit e4da829

Browse files
authoredMar 5, 2025
feat(templates): refactor http client pipeline in Boilerplate #10158 (#10172)
1 parent ee0e076 commit e4da829

File tree

7 files changed

+71
-44
lines changed

7 files changed

+71
-44
lines changed
 
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
using System.Reflection;
2+
using Boilerplate.Shared.Controllers;
3+
using Boilerplate.Client.Core.Services.HttpMessageHandlers;
4+
5+
namespace System.Net.Http;
6+
7+
public static class HttpRequestExtensions
8+
{
9+
/// <summary>
10+
/// <inheritdoc cref="AuthorizedApiAttribute"/>
11+
/// </summary>
12+
public static bool HasAuthorizedApiAttribute(this HttpRequestMessage request)
13+
{
14+
return request.HasApiAttribute<AuthorizedApiAttribute>();
15+
}
16+
17+
/// <summary>
18+
/// <see cref="NoRetryPolicyAttribute"/>
19+
/// </summary>
20+
public static bool HasNoRetryPolicyAttribute(this HttpRequestMessage request)
21+
{
22+
return request.HasApiAttribute<NoRetryPolicyAttribute>();
23+
}
24+
25+
/// <summary>
26+
/// <see cref="ExternalApiAttribute"/>
27+
/// </summary>
28+
public static bool HasExternalApiAttribute(this HttpRequestMessage request)
29+
{
30+
return request.HasApiAttribute<ExternalApiAttribute>();
31+
}
32+
33+
private static bool HasApiAttribute<TApiAttribute>(this HttpRequestMessage request)
34+
where TApiAttribute : Attribute
35+
{
36+
if (request.Options.TryGetValue(new(RequestOptionNames.IControllerType), out Type? controllerType) is false)
37+
return false;
38+
39+
var parameterTypes = ((Dictionary<string, Type>)request.Options.GetValueOrDefault(RequestOptionNames.ActionParametersInfo)!).Select(p => p.Value).ToArray();
40+
var method = controllerType!.GetMethod((string)request.Options.GetValueOrDefault(RequestOptionNames.ActionName)!, parameterTypes)!;
41+
42+
return controllerType.GetCustomAttribute<TApiAttribute>(inherit: true) is not null ||
43+
method.GetCustomAttribute<TApiAttribute>() is not null;
44+
}
45+
}

‎src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Core/Extensions/IClientCoreServiceCollectionExtensions.cs

+9-1
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,15 @@ public static IServiceCollection AddClientCoreProjectServices(this IServiceColle
3232
services.AddScoped<LazyAssemblyLoader>();
3333
services.AddScoped<IAuthTokenProvider, ClientSideAuthTokenProvider>();
3434
services.AddScoped<IExternalNavigationService, DefaultExternalNavigationService>();
35-
services.AddScoped<AbsoluteServerAddressProvider>(sp => new() { GetAddress = () => sp.GetRequiredService<HttpClient>().BaseAddress! /* Read AbsoluteServerAddressProvider's comments for more info. */ });
35+
36+
if (Uri.TryCreate(configuration.GetServerAddress(), UriKind.Absolute, out var serverAddress))
37+
{
38+
services.AddScoped<AbsoluteServerAddressProvider>(sp => new() { GetAddress = () => serverAddress });
39+
}
40+
else
41+
{
42+
services.AddScoped<AbsoluteServerAddressProvider>(sp => new() { GetAddress = () => sp.GetRequiredService<HttpClient>().BaseAddress! /* Read AbsoluteServerAddressProvider's comments for more info. */ });
43+
}
3644

3745
// The following services must be unique to each app session.
3846
// Defining them as singletons would result in them being shared across all users in Blazor Server and during pre-rendering.

‎src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Core/Services/HttpMessageHandlers/AuthDelegatingHandler.cs

+3-20
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,4 @@
1-
using System.Reflection;
2-
using System.Net.Http.Headers;
3-
using Boilerplate.Shared.Controllers;
1+
using System.Net.Http.Headers;
42
using Boilerplate.Shared.Controllers.Identity;
53

64
namespace Boilerplate.Client.Core.Services.HttpMessageHandlers;
@@ -10,22 +8,21 @@ public partial class AuthDelegatingHandler(IJSRuntime jsRuntime,
108
IServiceProvider serviceProvider,
119
IAuthTokenProvider tokenProvider,
1210
IStringLocalizer<AppStrings> localizer,
13-
AbsoluteServerAddressProvider absoluteServerAddress,
1411
HttpMessageHandler handler) : DelegatingHandler(handler)
1512
{
1613

1714
protected override async Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)
1815
{
1916
var logScopeData = (Dictionary<string, object?>)request.Options.GetValueOrDefault(RequestOptionNames.LogScopeData)!;
20-
var isInternalRequest = request.RequestUri!.ToString().StartsWith(absoluteServerAddress, StringComparison.InvariantCultureIgnoreCase);
17+
var isInternalRequest = request.HasExternalApiAttribute() is false;
2118

2219
try
2320
{
2421
if (isInternalRequest && /* We will restrict sending the access token to our own server only. */
2522
request.Headers.Authorization is null)
2623
{
2724
var accessToken = await tokenProvider.GetAccessToken();
28-
if (string.IsNullOrEmpty(accessToken) is false && HasAuthorizedApiAttribute(request))
25+
if (string.IsNullOrEmpty(accessToken) is false && request.HasAuthorizedApiAttribute())
2926
{
3027
if (IAuthTokenProvider.ParseAccessToken(accessToken, validateExpiry: true).IsAuthenticated() is false)
3128
{
@@ -68,18 +65,4 @@ protected override async Task<HttpResponseMessage> SendAsync(HttpRequestMessage
6865
return await base.SendAsync(request, cancellationToken);
6966
}
7067
}
71-
72-
/// <summary>
73-
/// <inheritdoc cref="AuthorizedApiAttribute"/>
74-
/// </summary>
75-
private static bool HasAuthorizedApiAttribute(HttpRequestMessage request)
76-
{
77-
if (request.Options.TryGetValue(new(RequestOptionNames.IControllerType), out Type? controllerType) is false)
78-
return false;
79-
80-
var parameterTypes = ((Dictionary<string, Type>)request.Options.GetValueOrDefault(RequestOptionNames.ActionParametersInfo)!).Select(p => p.Value).ToArray();
81-
var method = controllerType!.GetMethod((string)request.Options.GetValueOrDefault(RequestOptionNames.ActionName)!, parameterTypes)!;
82-
return controllerType.GetCustomAttribute<AuthorizedApiAttribute>(inherit: true) is not null ||
83-
method.GetCustomAttribute<AuthorizedApiAttribute>() is not null;
84-
}
8568
}

‎src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Core/Services/HttpMessageHandlers/ExceptionDelegatingHandler.cs

+1-2
Original file line numberDiff line numberDiff line change
@@ -7,15 +7,14 @@ namespace Boilerplate.Client.Core.Services.HttpMessageHandlers;
77
public partial class ExceptionDelegatingHandler(PubSubService pubSubService,
88
IStringLocalizer<AppStrings> localizer,
99
JsonSerializerOptions jsonSerializerOptions,
10-
AbsoluteServerAddressProvider absoluteServerAddress,
1110
HttpMessageHandler handler) : DelegatingHandler(handler)
1211
{
1312
protected override async Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)
1413
{
1514
var logScopeData = (Dictionary<string, object?>)request.Options.GetValueOrDefault(RequestOptionNames.LogScopeData)!;
1615

1716
bool serverCommunicationSuccess = false;
18-
var isInternalRequest = request.RequestUri!.ToString().StartsWith(absoluteServerAddress, StringComparison.InvariantCultureIgnoreCase);
17+
var isInternalRequest = request.HasExternalApiAttribute() is false;
1918

2019
string? requestIdValue = null;
2120

‎src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Core/Services/HttpMessageHandlers/RetryDelegatingHandler.cs

+2-20
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,4 @@
1-
using System.Reflection;
2-
using Boilerplate.Shared.Controllers;
3-
4-
namespace Boilerplate.Client.Core.Services.HttpMessageHandlers;
1+
namespace Boilerplate.Client.Core.Services.HttpMessageHandlers;
52

63
public partial class RetryDelegatingHandler(HttpMessageHandler handler)
74
: DelegatingHandler(handler)
@@ -23,7 +20,7 @@ protected override async Task<HttpResponseMessage> SendAsync(HttpRequestMessage
2320
}
2421
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.
2522
{
26-
if (HasNoRetryPolicyAttribute(request) || AppEnvironment.IsDev())
23+
if (request.HasNoRetryPolicyAttribute() || AppEnvironment.IsDev())
2724
throw;
2825
retryCount++;
2926
logScopeData["RetryCount"] = retryCount;
@@ -35,21 +32,6 @@ protected override async Task<HttpResponseMessage> SendAsync(HttpRequestMessage
3532
throw lastExp!;
3633
}
3734

38-
/// <summary>
39-
/// <see cref="NoRetryPolicyAttribute"/>
40-
/// </summary>
41-
private static bool HasNoRetryPolicyAttribute(HttpRequestMessage request)
42-
{
43-
if (request.Options.TryGetValue(new(RequestOptionNames.IControllerType), out Type? controllerType) is false)
44-
return false;
45-
46-
var parameterTypes = ((Dictionary<string, Type>)request.Options.GetValueOrDefault(RequestOptionNames.ActionParametersInfo)!).Select(p => p.Value).ToArray();
47-
var method = controllerType!.GetMethod((string)request.Options.GetValueOrDefault(RequestOptionNames.ActionName)!, parameterTypes)!;
48-
49-
return controllerType.GetCustomAttribute<NoRetryPolicyAttribute>(inherit: true) is not null ||
50-
method.GetCustomAttribute<NoRetryPolicyAttribute>() is not null;
51-
}
52-
5335
private static IEnumerable<TimeSpan> GetDelaySequence(TimeSpan scaleFirstTry)
5436
{
5537
TimeSpan maxValue = TimeSpan.MaxValue;

‎src/Templates/Boilerplate/Bit.Boilerplate/src/Shared/Controllers/Attributes.cs

+10
Original file line numberDiff line numberDiff line change
@@ -58,3 +58,13 @@ public partial class AuthorizedApiAttribute : Attribute
5858
{
5959

6060
}
61+
62+
/// <summary>
63+
/// This attribute designates an API as an external API,
64+
/// allowing HTTP message handlers to modify their behavior accordingly.
65+
/// </summary>
66+
[AttributeUsage(AttributeTargets.Method | AttributeTargets.Interface)]
67+
public partial class ExternalApiAttribute : Attribute
68+
{
69+
70+
}

‎src/Templates/Boilerplate/Bit.Boilerplate/src/Shared/Controllers/Statistics/IStatisticsController.cs

+1-1
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,6 @@ public interface IStatisticsController : IAppController
88
[HttpGet("{packageId}")]
99
Task<NugetStatsDto> GetNugetStats(string packageId, CancellationToken cancellationToken);
1010

11-
[HttpGet, Route("https://api.github.com/repos/bitfoundation/bitplatform")]
11+
[HttpGet, Route("https://api.github.com/repos/bitfoundation/bitplatform"), ExternalApi]
1212
Task<GitHubStats> GetGitHubStats(CancellationToken cancellationToken) => default!;
1313
}

0 commit comments

Comments
 (0)
Please sign in to comment.