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
Original file line number Diff line number Diff line change
Expand Up @@ -16,11 +16,13 @@

<ItemGroup>
<PackageReference Include="Microsoft.Extensions.Caching.Abstractions" Version="8.0.0" />
<PackageReference Include="Microsoft.Extensions.Caching.Memory" Version="8.0.1" />
<PackageReference Include="Microsoft.Extensions.Configuration" Version="8.0.0" />
<PackageReference Include="Microsoft.Extensions.Configuration.Abstractions" Version="8.0.0" />
<PackageReference Include="Microsoft.Extensions.Logging.Abstractions" Version="8.0.1" />
<PackageReference Include="Microsoft.Extensions.Logging.Abstractions" Version="8.0.2" />
<PackageReference Include="Microsoft.Extensions.Options" Version="8.0.2" />
<PackageReference Include="Microsoft.Extensions.Options.ConfigurationExtensions" Version="8.0.0" />
<PackageReference Include="StackExchange.Redis" Version="2.9.32" />
</ItemGroup>

<ItemGroup>
Expand Down
46 changes: 46 additions & 0 deletions src/GovUK.Dfe.CoreLibs.Caching/Helpers/CacheKeyHelper.cs
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,52 @@ public static string GenerateHashedCacheKey(IEnumerable<string> inputs)

return GenerateHashedCacheKey(concatenatedInput);
}

/// <summary>
/// Computes the SHA-256 hash of the provided stream and returns it as an uppercase hexadecimal string.
/// </summary>
/// <param name="fileStream">
/// The stream to compute the hash for. The stream must be readable and positioned
/// at the beginning of the data to hash.
/// </param>
/// <returns>
/// A 64-character uppercase hexadecimal string representing the SHA-256 hash of the stream’s contents.
/// </returns>
/// <exception cref="ArgumentNullException">Thrown if <paramref name="fileStream"/> is <see langword="null"/>.</exception>
/// <exception cref="ArgumentException">Thrown if the stream cannot be read.</exception>
public static string ComputeSha256(Stream fileStream)
{
if (fileStream == null)
throw new ArgumentNullException(nameof(fileStream), "File stream cannot be null.");

if (!fileStream.CanRead)
throw new ArgumentException("File stream must be readable.", nameof(fileStream));

// Remember the original position if the stream supports seeking.
long originalPosition = 0;
if (fileStream.CanSeek)
{
originalPosition = fileStream.Position;
fileStream.Position = 0;
}

try
{
using var sha = SHA256.Create();
var hashBytes = sha.ComputeHash(fileStream);

// Convert the hash bytes to an uppercase hexadecimal string.
return Convert.ToHexString(hashBytes);
}
finally
{
// Restore the stream position so callers can continue using it if needed.
if (fileStream.CanSeek)
{
fileStream.Position = originalPosition;
}
}
}
}

}
3 changes: 2 additions & 1 deletion src/GovUK.Dfe.CoreLibs.Caching/Interfaces/ICacheService.cs
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
namespace GovUK.Dfe.CoreLibs.Caching.Interfaces
{
public interface ICacheService<TCacheType> where TCacheType : ICacheType

Check warning on line 3 in src/GovUK.Dfe.CoreLibs.Caching/Interfaces/ICacheService.cs

View workflow job for this annotation

GitHub Actions / build-and-test / build-and-test

'TCacheType' is not used in the interface. (https://rules.sonarsource.com/csharp/RSPEC-2326)

Check warning on line 3 in src/GovUK.Dfe.CoreLibs.Caching/Interfaces/ICacheService.cs

View workflow job for this annotation

GitHub Actions / build-and-test / build-and-test

'TCacheType' is not used in the interface. (https://rules.sonarsource.com/csharp/RSPEC-2326)
{
Task<T> GetOrAddAsync<T>(string cacheKey, Func<Task<T>> fetchFunction, string methodName);
Task<T> GetOrAddAsync<T>(string cacheKey, Func<Task<T>> fetchFunction, string methodName, CancellationToken cancellationToken = default);
Task<T?> GetAsync<T>(string cacheKey);
void Remove(string cacheKey);
Type CacheType { get; }
}
Expand Down
3 changes: 3 additions & 0 deletions src/GovUK.Dfe.CoreLibs.Caching/Interfaces/IRedisCacheType.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
namespace GovUK.Dfe.CoreLibs.Caching.Interfaces;

public interface IRedisCacheType : ICacheType;
62 changes: 61 additions & 1 deletion src/GovUK.Dfe.CoreLibs.Caching/ServiceCollectionExtensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2,18 +2,78 @@
using GovUK.Dfe.CoreLibs.Caching.Services;
using GovUK.Dfe.CoreLibs.Caching.Settings;
using Microsoft.Extensions.Configuration;
using StackExchange.Redis;

namespace Microsoft.Extensions.DependencyInjection
{
public static class ServiceCollectionExtensions
{
private static string GetRedisConnectionString(IConfiguration config)
{
// First, check ConnectionStrings:Redis
var connectionString = config.GetConnectionString("Redis");

if (!string.IsNullOrEmpty(connectionString))
{
return connectionString;
}

// Fall back to CacheSettings:Redis:ConnectionString
var cacheSettings = config.GetSection("CacheSettings:Redis").Get<RedisCacheSettings>();
if (!string.IsNullOrEmpty(cacheSettings?.ConnectionString))
{
return cacheSettings.ConnectionString;
}

throw new InvalidOperationException(
"Redis connection string is required but not configured. " +
"Please configure either 'ConnectionStrings:Redis' or 'CacheSettings:Redis:ConnectionString'");
}
public static IServiceCollection AddServiceCaching(
this IServiceCollection services, IConfiguration config)
{
services.Configure<CacheSettings>(config.GetSection("CacheSettings"));
services.AddMemoryCache();
services.AddSingleton<ICacheService<IMemoryCacheType>, MemoryCacheService>();

return services;
}

public static IServiceCollection AddRedisCaching(
this IServiceCollection services, IConfiguration config)
{
services.Configure<CacheSettings>(config.GetSection("CacheSettings"));

// Configure Redis connection
services.AddSingleton<IConnectionMultiplexer>(serviceProvider =>
{
var connectionString = GetRedisConnectionString(config);
return ConnectionMultiplexer.Connect(connectionString);
});

services.AddSingleton<ICacheService<IRedisCacheType>, RedisCacheService>();

return services;
}

public static IServiceCollection AddHybridCaching(
this IServiceCollection services, IConfiguration config)
{
services.Configure<CacheSettings>(config.GetSection("CacheSettings"));

// Add Memory Cache
services.AddMemoryCache();
services.AddSingleton<ICacheService<IMemoryCacheType>, MemoryCacheService>();

// Add Redis Cache
services.AddSingleton<IConnectionMultiplexer>(serviceProvider =>
{
var connectionString = GetRedisConnectionString(config);
return ConnectionMultiplexer.Connect(connectionString);
});
services.AddSingleton<ICacheService<IRedisCacheType>, RedisCacheService>();

return services;
}
}
}
}
18 changes: 17 additions & 1 deletion src/GovUK.Dfe.CoreLibs.Caching/Services/MemoryCacheService.cs
Original file line number Diff line number Diff line change
Expand Up @@ -15,14 +15,16 @@ public class MemoryCacheService(
private readonly MemoryCacheSettings _cacheSettings = cacheSettings.Value.Memory;
public Type CacheType => typeof(IMemoryCacheType);

public async Task<T> GetOrAddAsync<T>(string cacheKey, Func<Task<T>> fetchFunction, string methodName)
public async Task<T> GetOrAddAsync<T>(string cacheKey, Func<Task<T>> fetchFunction, string methodName, CancellationToken cancellationToken = default)
{
if (memoryCache.TryGetValue(cacheKey, out T? cachedValue))
{
logger.LogInformation("Cache hit for key: {CacheKey}", cacheKey);
return cachedValue!;
}

cancellationToken.ThrowIfCancellationRequested();

logger.LogInformation("Cache miss for key: {CacheKey}. Fetching from source...", cacheKey);
var result = await fetchFunction();

Expand All @@ -34,6 +36,20 @@ public async Task<T> GetOrAddAsync<T>(string cacheKey, Func<Task<T>> fetchFuncti
return result;
}

public async Task<T?> GetAsync<T>(string cacheKey)
{
await Task.CompletedTask; // Keep async signature for interface consistency

if (memoryCache.TryGetValue(cacheKey, out T? cachedValue))
{
logger.LogInformation("Cache hit for key: {CacheKey}", cacheKey);
return cachedValue;
}

logger.LogInformation("Cache miss for key: {CacheKey}", cacheKey);
return default(T);
}

public void Remove(string cacheKey)
{
memoryCache.Remove(cacheKey);
Expand Down
Loading