Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
using Microsoft.Extensions.DependencyInjection;
Copy link

Copilot AI Dec 5, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Missing using Microsoft.Extensions.Options; import. The code calls builder.Services.Configure<OAuthTokenOptions> on lines 22 and 37, which is an extension method from the Microsoft.Extensions.Options namespace. Without this import, the code will not compile.

Suggested change
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Options;

Copilot uses AI. Check for mistakes.
using System;

namespace WebApiClientCore.Extensions.OAuths.DependencyInjection
{
/// <summary>
/// OAuthToken配置的依赖注入扩展
/// </summary>
public static class OAuthTokenOptionsExtensions
{
/// <summary>
/// 配置OAuth Token刷新选项
/// 使用独立的配置选项,支持从appsettings.json加载
/// </summary>
/// <param name="builder">HttpClient构建器</param>
/// <param name="configure">配置委托</param>
/// <returns>HttpClient构建器</returns>
public static IHttpClientBuilder ConfigureOAuthTokenOptions(
this IHttpClientBuilder builder,
Action<OAuthTokenOptions> configure)
{
builder.Services.Configure<OAuthTokenOptions>(builder.Name, configure);
return builder;
}

/// <summary>
/// 配置OAuth Token刷新选项(通过IConfiguration)
/// 支持从appsettings.json等配置源加载
/// </summary>
/// <param name="builder">HttpClient构建器</param>
/// <param name="configuration">配置节</param>
/// <returns>HttpClient构建器</returns>
public static IHttpClientBuilder ConfigureOAuthTokenOptions(
this IHttpClientBuilder builder,
Microsoft.Extensions.Configuration.IConfiguration configuration)
{
builder.Services.Configure<OAuthTokenOptions>(builder.Name, configuration);
return builder;
}
}
}
45 changes: 45 additions & 0 deletions WebApiClientCore.Extensions.OAuths/HttpApiOptionsExtensions.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
using System;

namespace WebApiClientCore.Extensions.OAuths
{
/// <summary>
/// HttpApiOptions的扩展方法
/// </summary>
public static class HttpApiOptionsExtensions
{
/// <summary>
/// OAuthTokenOptions在Properties字典中的键
/// </summary>
private const string TokenRefreshOptionsKey = "WebApiClientCore.OAuths.TokenRefreshOptions";

/// <summary>
/// 获取或设置OAuth Token刷新选项
/// 此方法使用HttpApiOptions的Properties字典存储配置,不污染核心配置类
/// </summary>
/// <param name="options">HttpApi选项</param>
/// <returns>OAuth Token刷新选项</returns>
public static OAuthTokenOptions GetOAuthTokenOptions(this HttpApiOptions options)
{
if (!options.Properties.TryGetValue(TokenRefreshOptionsKey, out var value))
{
value = new OAuthTokenOptions();
options.Properties[TokenRefreshOptionsKey] = value;
}
return (OAuthTokenOptions)value;
}

/// <summary>
/// 配置OAuth Token刷新选项
/// 此方法使用HttpApiOptions的Properties字典存储配置,不污染核心配置类
/// </summary>
/// <param name="options">HttpApi选项</param>
/// <param name="configure">配置委托</param>
/// <returns>HttpApi选项</returns>
public static HttpApiOptions ConfigureOAuthToken(this HttpApiOptions options, Action<OAuthTokenOptions> configure)
{
var tokenOptions = options.GetOAuthTokenOptions();
configure(tokenOptions);
return options;
}
}
}
34 changes: 34 additions & 0 deletions WebApiClientCore.Extensions.OAuths/OAuthTokenOptions.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
namespace WebApiClientCore.Extensions.OAuths
{
/// <summary>
/// 表示OAuth Token的刷新选项
/// </summary>
public class OAuthTokenOptions
{
/// <summary>
/// 获取或设置是否启用Token提前刷新窗口
/// 默认值为 true
/// </summary>
public bool UseTokenRefreshWindow { get; set; } = true;

/// <summary>
/// 获取或设置固定刷新窗口时长(秒)
/// 当剩余有效时间小于等于此值时,触发提前刷新
/// 默认值为 60 秒
/// </summary>
public int RefreshWindowSeconds { get; set; } = 60;

/// <summary>
/// 获取或设置刷新窗口百分比(0-1)
/// 当剩余有效时间小于等于总有效期的此百分比时,触发提前刷新
/// 默认值为 0.1 (10%)
/// </summary>
public double RefreshWindowPercentage { get; set; } = 0.1;

/// <summary>
/// 获取或设置刷新窗口计算策略
/// 默认值为 Auto (自动选择固定时长和百分比中的较小值)
/// </summary>
public RefreshWindowStrategy RefreshWindowStrategy { get; set; } = RefreshWindowStrategy.Auto;
}
}
23 changes: 23 additions & 0 deletions WebApiClientCore.Extensions.OAuths/RefreshWindowStrategy.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
namespace WebApiClientCore.Extensions.OAuths
{
/// <summary>
/// 表示Token刷新窗口计算策略
/// </summary>
public enum RefreshWindowStrategy
{
/// <summary>
/// 仅使用固定秒数
/// </summary>
FixedSeconds = 0,

/// <summary>
/// 仅使用百分比
/// </summary>
Percentage = 1,

/// <summary>
/// 自动选择:取固定秒数和百分比的较小值
/// </summary>
Auto = 2
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -75,7 +75,7 @@ public async Task<TokenResult> GetTokenAsync()
using var scope = this.services.CreateScope();
this.token = await this.RequestTokenAsync(scope.ServiceProvider).ConfigureAwait(false);
}
else if (this.token.IsExpired() == true)
else if (this.ShouldRefreshToken(this.token))
{
using var scope = this.services.CreateScope();

Expand All @@ -88,6 +88,96 @@ public async Task<TokenResult> GetTokenAsync()
}
}

/// <summary>
/// 判断是否应该刷新Token
/// 混合方案:优先从独立配置读取,其次从 HttpApiOptions.Properties 读取,最后使用默认行为
/// </summary>
/// <param name="token">token结果</param>
/// <returns></returns>
private bool ShouldRefreshToken(TokenResult token)
Copy link

Copilot AI Dec 5, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Local scope variable 'token' shadows TokenProvider.token.

Copilot uses AI. Check for mistakes.
{
// 获取配置
var tokenOptions = this.GetTokenRefreshOptions();

if (!tokenOptions.UseTokenRefreshWindow)
{
// 如果禁用刷新窗口,使用原有行为
return token.IsExpired();
}

// 计算刷新窗口
var refreshWindow = this.CalculateRefreshWindow(token, tokenOptions);
return token.IsExpired(refreshWindow);
}

/// <summary>
/// 获取Token刷新配置
/// 优先级:独立配置(IOptionsMonitor&lt;OAuthTokenOptions&gt;) > HttpApiOptions.Properties > 默认配置
/// </summary>
/// <returns>Token刷新配置</returns>
private OAuthTokenOptions GetTokenRefreshOptions()
{
// 优先从独立配置读取 (方案2:独立配置类)
var oauthOptionsMonitor = this.services.GetService<IOptionsMonitor<OAuthTokenOptions>>();
if (oauthOptionsMonitor != null)
{
try
{
var options = oauthOptionsMonitor.Get(this.Name);
// 检查是否为默认配置,如果不是则使用
if (options != null)
{
return options;
}
Comment on lines +127 to +131
Copy link

Copilot AI Dec 5, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The null check on line 128 is redundant. The IOptionsMonitor<T>.Get(string name) method never returns null - it creates a new default instance if no configuration exists. This check should either be removed or the logic should verify if the options are actually configured (e.g., by checking specific property values differ from defaults).

Suggested change
// 检查是否为默认配置,如果不是则使用
if (options != null)
{
return options;
}
// 直接返回 options,无需检查 null(IOptionsMonitor.Get 永不返回 null)
return options;

Copilot uses AI. Check for mistakes.
}
catch
{
// 如果获取失败,继续尝试下一种方式
}
Comment on lines +133 to +136
Copy link

Copilot AI Dec 5, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Empty catch blocks on lines 133 and 151 silently suppress all exceptions, making debugging difficult. Consider at least logging the exception or catching specific exception types that are expected. If any exception is acceptable here, add a comment explaining why it's safe to ignore.

Copilot uses AI. Check for mistakes.
}

// 其次从 HttpApiOptions.Properties 读取 (方案1:Properties 字典)
var httpApiOptionsMonitor = this.services.GetService<IOptionsMonitor<HttpApiOptions>>();
if (httpApiOptionsMonitor != null)
{
try
{
var httpApiOptions = httpApiOptionsMonitor.Get(this.Name);
if (httpApiOptions != null)
{
return httpApiOptions.GetOAuthTokenOptions();
}
Comment on lines +146 to +149
Copy link

Copilot AI Dec 5, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The null check on line 146 is redundant. The IOptionsMonitor<T>.Get(string name) method never returns null - it creates a new default instance if no configuration exists. This check should be removed.

Suggested change
if (httpApiOptions != null)
{
return httpApiOptions.GetOAuthTokenOptions();
}
return httpApiOptions.GetOAuthTokenOptions();

Copilot uses AI. Check for mistakes.
}
catch
{
// 如果获取失败,使用默认配置
}
Comment on lines +151 to +154
Copy link

Copilot AI Dec 5, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Empty catch block silently suppresses all exceptions, making debugging difficult. Consider at least logging the exception or catching specific exception types that are expected. If any exception is acceptable here, add a comment explaining why it's safe to ignore.

Copilot uses AI. Check for mistakes.
}

// 最后使用默认配置
return new OAuthTokenOptions();
}

/// <summary>
/// 根据配置策略计算刷新窗口
/// </summary>
/// <param name="token">Token结果</param>
/// <param name="options">配置选项</param>
/// <returns>刷新窗口时间</returns>
private TimeSpan CalculateRefreshWindow(TokenResult token, OAuthTokenOptions options)
{
var fixedWindow = TimeSpan.FromSeconds(options.RefreshWindowSeconds);
var percentageWindow = TimeSpan.FromSeconds(token.Expires_in * options.RefreshWindowPercentage);
Comment on lines +167 to +170
Copy link

Copilot AI Dec 5, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Local scope variable 'token' shadows TokenProvider.token.

Suggested change
private TimeSpan CalculateRefreshWindow(TokenResult token, OAuthTokenOptions options)
{
var fixedWindow = TimeSpan.FromSeconds(options.RefreshWindowSeconds);
var percentageWindow = TimeSpan.FromSeconds(token.Expires_in * options.RefreshWindowPercentage);
private TimeSpan CalculateRefreshWindow(TokenResult tokenResult, OAuthTokenOptions options)
{
var fixedWindow = TimeSpan.FromSeconds(options.RefreshWindowSeconds);
var percentageWindow = TimeSpan.FromSeconds(tokenResult.Expires_in * options.RefreshWindowPercentage);

Copilot uses AI. Check for mistakes.

return options.RefreshWindowStrategy switch
{
RefreshWindowStrategy.FixedSeconds => fixedWindow,
RefreshWindowStrategy.Percentage => percentageWindow,
RefreshWindowStrategy.Auto => fixedWindow < percentageWindow ? fixedWindow : percentageWindow,
_ => fixedWindow < percentageWindow ? fixedWindow : percentageWindow // 默认使用Auto
};
}
Comment on lines +91 to +179
Copy link

Copilot AI Dec 5, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The new token refresh window feature lacks test coverage for key scenarios:

  • Tests for ShouldRefreshToken method
  • Tests for GetTokenRefreshOptions method's configuration priority logic
  • Tests for CalculateRefreshWindow method with different strategies
  • Integration tests verifying the configuration from both HttpApiOptions and IOptionsMonitor<OAuthTokenOptions>

Consider adding tests that verify the configuration fallback chain and the refresh window calculation strategies work correctly.

Copilot uses AI. Check for mistakes.


/// <summary>
/// 请求获取 token
Expand Down
14 changes: 13 additions & 1 deletion WebApiClientCore.Extensions.OAuths/TokenResult.cs
Original file line number Diff line number Diff line change
Expand Up @@ -75,7 +75,19 @@ public virtual bool IsSuccess()
/// <returns></returns>
public virtual bool IsExpired()
{
return DateTime.Now.Subtract(this.createTime) > TimeSpan.FromSeconds(this.Expires_in);
return IsExpired(TimeSpan.Zero);
}

/// <summary>
/// 返回是否已过期或在刷新窗口内
/// </summary>
/// <param name="refreshWindow">提前刷新时间窗口</param>
/// <returns>如果已过期或剩余有效时间小于等于刷新窗口,返回true</returns>
public virtual bool IsExpired(TimeSpan refreshWindow)
{
var elapsed = DateTime.Now.Subtract(this.createTime);
var remaining = TimeSpan.FromSeconds(this.Expires_in).Subtract(elapsed);
return remaining <= refreshWindow;
}

/// <summary>
Expand Down
Loading