Skip to content
Merged
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
21 changes: 12 additions & 9 deletions src/HttpClient.Cache/CacheDataExtensions.cs
Original file line number Diff line number Diff line change
@@ -1,30 +1,33 @@
using System.Text.Json;
using System.Diagnostics;
using Newtonsoft.Json;

namespace HttpClient.Cache;

public static class CacheDataExtensions
{
public static byte[] Serialize(this CacheData cacheData)
public static byte[] Pack(this CacheData cacheData)
{
string json = JsonSerializer.Serialize(cacheData);
byte[] bytes = new byte[json.Length * sizeof(char)];
var json = JsonConvert.SerializeObject(cacheData);
var bytes = new byte[json.Length * sizeof(char)];

Buffer.BlockCopy(json.ToCharArray(), 0, bytes, 0, bytes.Length);
return bytes;
}

public static CacheData? Deserialize(this byte[] cacheData)
public static CacheData? Unpack(this byte[] cacheData)
{
try
{
char[] chars = new char[cacheData.Length / sizeof(char)];
var chars = new char[cacheData.Length / sizeof(char)];
Buffer.BlockCopy(cacheData, 0, chars, 0, cacheData.Length);
string json = new string(chars);
CacheData? data = JsonSerializer.Deserialize<CacheData>(json);
var json = new string(chars);

var data = JsonConvert.DeserializeObject<CacheData>(json);
return data;
}
catch
catch (Exception ex)
{
Debug.WriteLine($"{ex}");
return null;
}
}
Expand Down
15 changes: 15 additions & 0 deletions src/HttpClient.Cache/CacheEntryExtensions.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
namespace HttpClient.Cache;

public static class CacheEntryExtensions
{
public static ICacheEntry AddExpirationToken(this ICacheEntry entry, IChangeToken token)
{
if (token == null)
{
throw new ArgumentNullException(nameof(token));
}

entry.ExpirationTokens.Add(token);
return entry;
}
}
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
namespace HttpClient.Cache;

public enum CacheItemPriority
public enum CacheEntryPriority
{
Low,
Normal,
Expand Down
14 changes: 14 additions & 0 deletions src/HttpClient.Cache/DefaultCacheKeysProvider.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
namespace HttpClient.Cache;

public class DefaultCacheKeysProvider : ICacheKeysProvider
{
public string GetKey(HttpRequestMessage? request)
{
if (request is null)
{
throw new ArgumentNullException(nameof(request));
}

return $"MET_{request.Method};URI_{request.RequestUri}";
}
}
11 changes: 11 additions & 0 deletions src/HttpClient.Cache/EvictionReason.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
namespace HttpClient.Cache;

public enum EvictionReason
{
None,
Removed,
Replaced,
Expired,
TokenExpired,
Capacity
}
4 changes: 4 additions & 0 deletions src/HttpClient.Cache/HttpClient.Cache.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -6,4 +6,8 @@
<Nullable>enable</Nullable>
</PropertyGroup>

<ItemGroup>
<PackageReference Include="Newtonsoft.Json" Version="13.0.3" />
</ItemGroup>

</Project>
2 changes: 1 addition & 1 deletion src/HttpClient.Cache/ICacheEntry.cs
Original file line number Diff line number Diff line change
Expand Up @@ -14,5 +14,5 @@ public interface ICacheEntry: IDisposable

IList<PostEvictionCallbackRegistration> PostEvictionCallbacks { get; }

CacheItemPriority Priority { get; set; }
CacheEntryPriority Priority { get; set; }
}
6 changes: 6 additions & 0 deletions src/HttpClient.Cache/ICacheKeysProvider.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
namespace HttpClient.Cache;

public interface ICacheKeysProvider
{
string GetKey(HttpRequestMessage request);
}
262 changes: 262 additions & 0 deletions src/HttpClient.Cache/InMemory/CacheEntry.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,262 @@
using System.Diagnostics;

namespace HttpClient.Cache.InMemory;

internal class CacheEntry : ICacheEntry
{
private readonly object _lock = new();
private static readonly Action<object> ExpirationCallback = ExpirationTokensExpired;

private readonly Action<CacheEntry> _notifyCacheEntryDisposed;
private readonly Action<CacheEntry> _notifyCacheOfExpiration;
private DateTimeOffset? _absoluteExpiration;
private TimeSpan? _absoluteExpirationRelativeToNow;
private TimeSpan? _slidingExpiration;

private IList<IDisposable>? _expirationTokenRegistrations;
private IList<IChangeToken>? _expirationTokens;
private IList<PostEvictionCallbackRegistration>? _postEvictionCallbacks;

private bool _isDisposed;
private bool _isExpired;

internal CacheEntry(
object key,
Action<CacheEntry> notifyCacheEntryDisposed,
Action<CacheEntry> notifyCacheOfExpiration)
{
Key = key ?? throw new ArgumentNullException(nameof(key));

_notifyCacheEntryDisposed = notifyCacheEntryDisposed ??
throw new ArgumentNullException(nameof(notifyCacheEntryDisposed));
_notifyCacheOfExpiration =
notifyCacheOfExpiration ?? throw new ArgumentNullException(nameof(notifyCacheOfExpiration));
}

public object Key { get; }
public object Value { get; set; }

public CacheEntryPriority Priority { get; set; } = CacheEntryPriority.Normal;

public DateTimeOffset? AbsoluteExpiration
{
get => _absoluteExpiration;
set => _absoluteExpiration = value;
}

public TimeSpan? AbsoluteExpirationRelativeToNow
{
get => _absoluteExpirationRelativeToNow;
set
{
if ((value.HasValue ? value.GetValueOrDefault() <= TimeSpan.Zero ? 1 : 0 : 0) != 0)
{
throw new ArgumentOutOfRangeException(nameof(AbsoluteExpirationRelativeToNow), value,
"The relative expiration must be positive");
}

_absoluteExpirationRelativeToNow = value;
}
}

public TimeSpan? SlidingExpiration
{
get => _slidingExpiration;
set
{
if ((value.HasValue ? value.GetValueOrDefault() <= TimeSpan.Zero ? 1 : 0 : 0) != 0)
{
throw new ArgumentOutOfRangeException(nameof(SlidingExpiration), value,
"The sliding expiration must be positive");
}

_slidingExpiration = value;
}
}

public IList<IChangeToken> ExpirationTokens
{
get
{
return _expirationTokens ??= new List<IChangeToken>();
}
}

public IList<PostEvictionCallbackRegistration> PostEvictionCallbacks =>
_postEvictionCallbacks ?? new List<PostEvictionCallbackRegistration>();

internal DateTimeOffset LastAccessed { get; set; }

internal EvictionReason EvictionReason { get; private set; }

public void Dispose()
{
if (_isDisposed)
{
return;
}

_isDisposed = true;
_notifyCacheEntryDisposed(this);
}

internal bool IsExpired(DateTimeOffset currentTime)
{
if (!_isExpired && !IsExpiredNow(currentTime))
{
return IsAnyExpirationTokenExpired();
}
return true;
}

private bool IsExpiredNow(DateTimeOffset currentTime)
{
if (_absoluteExpiration.HasValue && _absoluteExpiration.Value <= currentTime)
{
ExpireEntryByReason(EvictionReason.Expired);
return true;
}

if (!_slidingExpiration.HasValue)
{
return false;
}

var accessedOffset = currentTime - LastAccessed;
var expiration = _slidingExpiration;
if ((expiration.HasValue ? (accessedOffset >= expiration.GetValueOrDefault() ? 1 : 0) : 0) != 0)
{
ExpireEntryByReason(EvictionReason.Expired);
return true;
}

return false;
}

private bool IsAnyExpirationTokenExpired()
{
if (_expirationTokens == null)
{
return false;
}

foreach (var token in _expirationTokens)
{
if (!token.HasChanged)
{
continue;
}

ExpireEntryByReason(EvictionReason.TokenExpired);
return true;
}

return false;
}

private static void ExpirationTokensExpired(object entry)
{
Task.Factory.StartNew(state =>
{
CacheEntry? cacheEntry = state as CacheEntry;
cacheEntry?.ExpireEntryByReason(EvictionReason.TokenExpired);
cacheEntry?._notifyCacheOfExpiration(cacheEntry);
}, entry, CancellationToken.None, TaskCreationOptions.DenyChildAttach, TaskScheduler.Default);
}

internal void ExpireEntryByReason(EvictionReason reason)
{
if (EvictionReason == EvictionReason.None)
{
EvictionReason = reason;
}

_isExpired = true;
DetachTokens();
}

internal void AttachTokens()
{
if (_expirationTokens == null)
{
return;
}

lock (_lock)
{
foreach (var token in _expirationTokens)
{
if (!token.ActiveChangeCallbacks)
{
continue;
}

_expirationTokenRegistrations ??= new List<IDisposable>();
_expirationTokenRegistrations.Add(token.RegisterChangeCallback(ExpirationCallback, this));
}
}
}

internal void DetachTokens()
{
lock (_lock)
{
var tokenRegistrations = _expirationTokenRegistrations;
if (tokenRegistrations == null)
{
return;
}

_expirationTokenRegistrations = null;
foreach (IDisposable registration in tokenRegistrations)
{
registration.Dispose();
}
}
}

internal void InvokeEvictionCallbacks()
{
if (_postEvictionCallbacks == null)
{
return;
}

Task.Factory.StartNew(state => InvokeCallbacks((CacheEntry)state), this, CancellationToken.None,
TaskCreationOptions.DenyChildAttach, TaskScheduler.Default);
}

private static void InvokeCallbacks(CacheEntry entry)
{
var evictionCallbacks =
Interlocked.Exchange(ref entry._postEvictionCallbacks, null);

if (evictionCallbacks == null)
{
return;
}

foreach (var callback in evictionCallbacks)
{
try
{
var evictionCallback = callback.EvictionCallback;
if (evictionCallback == null)
{
continue;
}

object key = entry.Key;
object? value = entry.Value;
EvictionReason reason = entry.EvictionReason;
object? state = callback.State;

evictionCallback.Invoke(key, value, reason.ToString(), state);
}
catch (Exception ex)
{
Debug.WriteLine($"{ex}");
}
}
}
}
6 changes: 6 additions & 0 deletions src/HttpClient.Cache/InMemory/Clock/ISystemClock.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
namespace HttpClient.Cache.InMemory.Clock;

public interface ISystemClock
{
DateTimeOffset UtcNow { get; }
}
Loading