Skip to content
Open
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
90 changes: 89 additions & 1 deletion WebApiClientCore/Attributes/CacheAttributes/CacheAttribute.cs
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Net;
using System.Net.Http;
using System.Text;
using System.Threading.Tasks;
using WebApiClientCore.Exceptions;
Expand All @@ -14,6 +16,26 @@ namespace WebApiClientCore.Attributes
[AttributeUsage(AttributeTargets.Method, AllowMultiple = false, Inherited = true)]
public class CacheAttribute : ApiCacheAttribute
{
/// <summary>
/// 非200响应时的缓存毫秒数(默认3秒,设为0则不缓存)
/// </summary>
public double ErrorExpiration { get; set; }

/// <summary>
/// 是否允许对非 GET 请求进行缓存(默认为 false,需显式二次确认)
/// </summary>
public bool EnableNonGet { get; set; }

/// <summary>
/// 用于在运行时存储动态计算的过期时间
/// </summary>
private TimeSpan? _dynamicExpiration;

/// <summary>
/// 重写 Expiration 属性,使其能返回动态调整后的值
/// </summary>
public new TimeSpan Expiration => _dynamicExpiration ?? base.Expiration;

Comment on lines +29 to +38
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

🏁 Script executed:

# Find the ApiCacheAttribute base class definition and check Expiration property signature
find . -type f -name "*.cs" | head -20

Repository: dotnetcore/WebApiClient

Length of output: 770


🏁 Script executed:

# Locate ApiCacheAttribute class
rg -l "class ApiCacheAttribute" --type=cs

Repository: dotnetcore/WebApiClient

Length of output: 131


🏁 Script executed:

# Inspect the base ApiCacheAttribute class
rg -n "class ApiCacheAttribute|Expiration" --type=cs -A 3

Repository: dotnetcore/WebApiClient

Length of output: 5583


🏁 Script executed:

# Check CacheAttribute.cs for full context around lines 29-38 and 106-113
wc -l WebApiClientCore/Attributes/CacheAttributes/CacheAttribute.cs

Repository: dotnetcore/WebApiClient

Length of output: 132


🏁 Script executed:

# Read the full CacheAttribute.cs to see lines 90-120 context
sed -n '90,120p' WebApiClientCore/Attributes/CacheAttributes/CacheAttribute.cs

Repository: dotnetcore/WebApiClient

Length of output: 1009


🏁 Script executed:

# Check ApiRequestSender to see how attribute is passed
rg -n "SetAsync.*attribute\|provider.SetAsync" --type=cs -B 5 -A 2 WebApiClientCore/Implementations/ApiRequestSender.cs

Repository: dotnetcore/WebApiClient

Length of output: 49


🏁 Script executed:

# Find GetWritePolicy and GetReadPolicy definitions
rg -n "GetWritePolicy|GetReadPolicy" --type=cs -A 15 WebApiClientCore/Attributes/CacheAttributes/CacheAttribute.cs

Repository: dotnetcore/WebApiClient

Length of output: 1458


🏁 Script executed:

# Check IApiCacheAttribute interface definition
cat WebApiClientCore.Abstractions/Attributes/IApiCacheAttribute.cs

Repository: dotnetcore/WebApiClient

Length of output: 1263


🏁 Script executed:

# Check how attribute is passed/used in ApiRequestSender and related code
rg -n "attribute.*Expiration|IApiCacheAttribute.*Expiration|GetWritePolicy|GetReadPolicy" --type=cs -B 3 -A 3 WebApiClientCore/Implementations/ApiRequestSender.cs

Repository: dotnetcore/WebApiClient

Length of output: 741


🏁 Script executed:

# Find where SetAsync is called and what type attribute is
rg -n "SetAsync" --type=cs -B 10 WebApiClientCore/Implementations/ApiRequestSender.cs | head -40

Repository: dotnetcore/WebApiClient

Length of output: 515


🏁 Script executed:

# Check if ApiCacheAttribute.Expiration is read anywhere and from what type
rg -n "\.Expiration" --type=cs WebApiClientCore/Implementations/ | grep -v "ErrorExpiration"

Repository: dotnetcore/WebApiClient

Length of output: 226


🏁 Script executed:

# Check the type of 'attribute' in ApiRequestSender - look at method signature
rg -n "DoCachingAsync\|DoResponseCachingAsync" --type=cs -A 20 WebApiClientCore/Implementations/ApiRequestSender.cs | head -60

Repository: dotnetcore/WebApiClient

Length of output: 49


🏁 Script executed:

# Find where the attribute variable is declared or retrieved
rg -n "attribute.*=" --type=cs -B 5 WebApiClientCore/Implementations/ApiRequestSender.cs | grep -A 5 "attribute.*GetCustomAttribute\|attribute.*As\|IApiCacheAttribute"

Repository: dotnetcore/WebApiClient

Length of output: 49


🏁 Script executed:

# Look at full method context to see how attribute parameter is typed
rg -n "void.*Cach\|async.*Cach" --type=cs -B 2 -A 15 WebApiClientCore/Implementations/ApiRequestSender.cs | head -80

Repository: dotnetcore/WebApiClient

Length of output: 49


🏁 Script executed:

# Get the method containing SetAsync call
sed -n '140,180p' WebApiClientCore/Implementations/ApiRequestSender.cs

Repository: dotnetcore/WebApiClient

Length of output: 1158


🏁 Script executed:

# Look for method signatures around caching
sed -n '70,150p' WebApiClientCore/Implementations/ApiRequestSender.cs

Repository: dotnetcore/WebApiClient

Length of output: 2810


🏁 Script executed:

# Check the entire file size and structure
wc -l WebApiClientCore/Implementations/ApiRequestSender.cs
head -100 WebApiClientCore/Implementations/ApiRequestSender.cs

Repository: dotnetcore/WebApiClient

Length of output: 3730


🏁 Script executed:

# Find ActionDescriptor class to see what type CacheAttribute property is
rg -n "class ActionDescriptor\|CacheAttribute" --type=cs WebApiClientCore.Abstractions/ApiActionDescriptor.cs

Repository: dotnetcore/WebApiClient

Length of output: 49


🏁 Script executed:

# Get the full CacheAttribute property definition in ActionDescriptor
cat WebApiClientCore.Abstractions/ApiActionDescriptor.cs | head -100

Repository: dotnetcore/WebApiClient

Length of output: 1826


🏁 Script executed:

# Confirm CacheAttribute is the concrete type being stored as IApiCacheAttribute
rg -n "new CacheAttribute\|typeof.*CacheAttribute" --type=cs -B 2 -A 2

Repository: dotnetcore/WebApiClient

Length of output: 49


🏁 Script executed:

# Double-check the Expiration property access path through interface
sed -n '1,50p' WebApiClientCore.Abstractions/Attributes/IApiCacheAttribute.cs

Repository: dotnetcore/WebApiClient

Length of output: 1263


Expiration property won't be called due to interface access—dynamic TTL feature is broken

CacheAttribute.Expiration is stored and accessed as IApiCacheAttribute (the interface) in ApiRequestSender.SetCacheAsync (line 174). The interface property isn't virtual in the base class, so using new on the derived property doesn't enable polymorphism. The pipeline always calls ApiCacheAttribute.Expiration directly, never the overridden CacheAttribute.Expiration, making _dynamicExpiration unreachable dead code.

To fix this, make Expiration virtual in ApiCacheAttribute, then use override in CacheAttribute. Additionally:

  • _dynamicExpiration is mutable instance state on a reused attribute, introducing thread safety concerns.
  • Console.WriteLine at lines 88, 101, and 128 logs RequestUri (may contain sensitive data) and is noisy for library consumers. Use ILogger instead.
  • Line 100's StatusCode != HttpStatusCode.OK treats all non-2xx responses as errors. Confirm this is intended, as 201/204/3xx responses may not warrant short caching.
🤖 Prompt for AI Agents
In `@WebApiClientCore/Attributes/CacheAttributes/CacheAttribute.cs` around lines
29 - 38, The Expiration property on CacheAttribute is hidden with new so calls
through the ApiCacheAttribute/IApiCacheAttribute path never hit the dynamic TTL;
change the base ApiCacheAttribute.Expiration to virtual and change
CacheAttribute.Expiration to override so polymorphism works (referencing
ApiCacheAttribute.Expiration and CacheAttribute.Expiration), and replace the
mutable per-attribute field _dynamicExpiration with a request-scoped/functional
approach (e.g., a Func<HttpResponseMessage,TimeSpan?> or a
ComputeExpiration(HttpResponseMessage) method) to avoid thread-safety issues;
also remove Console.WriteLine calls and instead accept/use an ILogger (inject or
pass ILogger to the cache logic) in place of Console.WriteLine occurrences, and
adjust the StatusCode check (currently using StatusCode != HttpStatusCode.OK) to
treat 2xx responses as successful or make the success status range configurable.

/// <summary>
/// 缓存键的请求头名称
/// </summary>
Expand All @@ -38,9 +60,75 @@ public string? IncludeHeaders
/// 使用缓存的特性
/// </summary>
/// <param name="expiration">缓存毫秒数</param>
public CacheAttribute(double expiration)
/// <param name="errorExpiration">接口异常时 缓存毫秒数,默认:3秒</param>
/// <param name="enableNonGet">是否允许对非 GET 请求进行缓存</param>
public CacheAttribute(double expiration, double errorExpiration = 3000, bool enableNonGet = false)
: base(expiration)
{
this.ErrorExpiration = errorExpiration;
this.EnableNonGet = enableNonGet;
}

/// <summary>
/// <inheritdoc/>
/// </summary>
/// <param name="context"></param>
/// <returns></returns>
public override CachePolicy GetWritePolicy(ApiRequestContext context)
{
var request = context.HttpContext.RequestMessage;
var response = context.HttpContext.ResponseMessage;

// 判断是否为非 GET 方法
if (request.Method != HttpMethod.Get)
{
if (!this.EnableNonGet)
{
// 输出请求路径和方法,提醒开发者该非GET请求已被跳过缓存
Console.WriteLine($"[WARN] Cache Ignored: Method {request.Method} on {request.RequestUri} requires 'EnableNonGet=true'.");
return CachePolicy.Ignore;
}
else
{
// 表示这是特殊处理的非GET缓存
Console.WriteLine($"[INFO] Cache Enabled for Non-GET: {request.RequestUri}");
}
}

// 接口升级/异常状态(非 200)
if (response != null && response.StatusCode != HttpStatusCode.OK)
{
if (this.ErrorExpiration <= 0)
{
return CachePolicy.Ignore;
}

// 动态记录短缓存时间
_dynamicExpiration = TimeSpan.FromMilliseconds(this.ErrorExpiration);
return CachePolicy.Include;
}

// 200 OK 正常情况
_dynamicExpiration = null; // 恢复使用 base.Expiration
return CachePolicy.Include;
}

/// <summary>
/// <inheritdoc/>
/// </summary>
/// <param name="context"></param>
/// <returns></returns>
public override CachePolicy GetReadPolicy(ApiRequestContext context)
{
var request = context.HttpContext.RequestMessage;
// 如果不是 GET 且没有明确开启非GET支持
if (request.Method != HttpMethod.Get && !this.EnableNonGet)
{
// 跳过之前的缓存
Console.WriteLine($"[WARN] Cache Ignored: Method {request.Method} on {request.RequestUri} requires 'EnableNonGet=true'.");
return CachePolicy.Ignore;
}
return CachePolicy.Include;
}

/// <summary>
Expand Down