Skip to content

Commit

Permalink
Added Caching
Browse files Browse the repository at this point in the history
  • Loading branch information
Farshad DASHTI authored and Farshad DASHTI committed Oct 1, 2024
1 parent f972885 commit a3d6efb
Show file tree
Hide file tree
Showing 9 changed files with 265 additions and 0 deletions.
19 changes: 19 additions & 0 deletions .github/workflows/build-test-caching.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
name: .NET Build and Test for DfE.CoreLibs.Caching

on:
push:
branches:
- main
paths:
- 'src/DfE.CoreLibs.Caching/**'

jobs:
build-and-test:
uses: ./.github/workflows/build-test-template.yml
with:
project_name: DfE.CoreLibs.Caching
project_path: src/DfE.CoreLibs.Caching
sonar_project_key: DFE-Digital_corelibs-caching
secrets:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }}
85 changes: 85 additions & 0 deletions .github/workflows/built-test-template.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
name: .NET Build and Test Template

on:
workflow_call:
inputs:
project_name:
required: true
type: string
description: "The name of the project"
project_path:
required: true
type: string
description: "The relative path to the project directory"
sonar_project_key:
required: true
type: string
description: "SonarCloud project key"
secrets:
GITHUB_TOKEN:
required: true
SONAR_TOKEN:
required: true

env:
DOTNET_VERSION: '8.0.x'
EF_VERSION: '6.0.5'
JAVA_VERSION: '17'
CONNECTION_STRING: 'Server=localhost,1433;Database=sip;TrustServerCertificate=True;User Id=sa;Password=StrongPassword905'

jobs:
build-and-test:
runs-on: ubuntu-latest
permissions:
packages: read
contents: read
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0

- name: Setup .NET
uses: actions/setup-dotnet@v4
with:
dotnet-version: ${{ env.DOTNET_VERSION }}

- name: Setup JDK
uses: actions/setup-java@v4
with:
distribution: 'microsoft'
java-version: ${{ env.JAVA_VERSION }}

- name: Cache SonarCloud packages
uses: actions/cache@v4
with:
path: ~/.sonar/cache
key: ${{ runner.os }}-sonar
restore-keys: ${{ runner.os }}-sonar

- name: Install SonarCloud scanners
run: dotnet tool install --global dotnet-sonarscanner

- name: Install EF for tests
run: dotnet tool install --global dotnet-ef --version ${{ env.EF_VERSION }}

- name: Install dotnet reportgenerator
run: dotnet tool install --global dotnet-reportgenerator-globaltool

- name: Add nuget package source
run: dotnet nuget add source --username USERNAME --password ${{ secrets.GITHUB_TOKEN }} --store-password-in-clear-text --name github "https://nuget.pkg.github.com/DFE-Digital/index.json"

- name: Restore dependencies
run: dotnet restore ${{ inputs.project_path }}

- name: Build, Test and Analyze
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }}
ConnectionStrings__DefaultConnection: ${{ env.CONNECTION_STRING }}
CI: true
run: |
dotnet-sonarscanner begin /k:"${{ inputs.sonar_project_key }}" /o:"dfe-digital" /d:sonar.token="${{ secrets.SONAR_TOKEN }}" /d:sonar.host.url="https://sonarcloud.io" /d:sonar.coverageReportPaths=CoverageReport/SonarQube.xml
dotnet build --no-restore -p:CI=${CI} ${{ inputs.project_path }}
dotnet test --no-build --verbosity normal --collect:"XPlat Code Coverage" ${{ inputs.project_path }}
reportgenerator -reports:./**/coverage.cobertura.xml -targetdir:./CoverageReport -reporttypes:SonarQube
dotnet-sonarscanner end /d:sonar.token="${{ secrets.SONAR_TOKEN }}"
12 changes: 12 additions & 0 deletions DfE.CoreLibs.sln
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,19 @@ Microsoft Visual Studio Solution File, Format Version 12.00
# Visual Studio Version 17
VisualStudioVersion = 17.10.35122.118
MinimumVisualStudioVersion = 10.0.40219.1
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "DfE.CoreLibs.Caching", "src\DfE.CoreLibs.Caching\DfE.CoreLibs.Caching.csproj", "{D88D58F0-18C4-4C3B-805B-8A483500E73E}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
Release|Any CPU = Release|Any CPU
EndGlobalSection
GlobalSection(ProjectConfigurationPlatforms) = postSolution
{D88D58F0-18C4-4C3B-805B-8A483500E73E}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{D88D58F0-18C4-4C3B-805B-8A483500E73E}.Debug|Any CPU.Build.0 = Debug|Any CPU
{D88D58F0-18C4-4C3B-805B-8A483500E73E}.Release|Any CPU.ActiveCfg = Release|Any CPU
{D88D58F0-18C4-4C3B-805B-8A483500E73E}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
EndGlobalSection
Expand Down
18 changes: 18 additions & 0 deletions src/DfE.CoreLibs.Caching/DfE.CoreLibs.Caching.csproj
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
<Project Sdk="Microsoft.NET.Sdk">

<PropertyGroup>
<TargetFramework>net8.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
</PropertyGroup>

<ItemGroup>
<PackageReference Include="Microsoft.Extensions.Caching.Abstractions" Version="8.0.0" />
<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.Options" Version="8.0.2" />
<PackageReference Include="Microsoft.Extensions.Options.ConfigurationExtensions" Version="8.0.0" />
</ItemGroup>

</Project>
43 changes: 43 additions & 0 deletions src/DfE.CoreLibs.Caching/Helpers/CacheKeyHelper.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
using System.Security.Cryptography;
using System.Text;

namespace DfE.CoreLibs.Caching.Helpers
{
public static class CacheKeyHelper
{
/// <summary>
/// Generates a hashed cache key for any given input string.
/// </summary>
/// <param name="input">The input string to be hashed.</param>
/// <returns>A hashed string that can be used as a cache key.</returns>
public static string GenerateHashedCacheKey(string input)
{
if (string.IsNullOrWhiteSpace(input))
{
throw new ArgumentException("Input cannot be null or empty", nameof(input));
}

var bytes = SHA256.HashData(Encoding.UTF8.GetBytes(input));

return BitConverter.ToString(bytes).Replace("-", "").ToLower();
}

/// <summary>
/// Generates a hashed cache key for a collection of strings by concatenating them.
/// </summary>
/// <param name="inputs">A collection of strings to be concatenated and hashed.</param>
/// <returns>A hashed string that can be used as a cache key.</returns>
public static string GenerateHashedCacheKey(IEnumerable<string> inputs)
{
if (inputs == null || !inputs.Any())
{
throw new ArgumentException("Input collection cannot be null or empty", nameof(inputs));
}

var concatenatedInput = string.Join(",", inputs);

return GenerateHashedCacheKey(concatenatedInput);
}
}

}
8 changes: 8 additions & 0 deletions src/DfE.CoreLibs.Caching/Interfaces/ICacheService.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
namespace DfE.CoreLibs.Caching.Interfaces
{
public interface ICacheService
{
Task<T> GetOrAddAsync<T>(string cacheKey, Func<Task<T>> fetchFunction, string methodName);
void Remove(string cacheKey);
}
}
19 changes: 19 additions & 0 deletions src/DfE.CoreLibs.Caching/ServiceCollectionExtensions.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
using DfE.CoreLibs.Caching.Interfaces;
using DfE.CoreLibs.Caching.Services;
using DfE.CoreLibs.Caching.Settings;
using Microsoft.Extensions.Configuration;

namespace Microsoft.Extensions.DependencyInjection
{
public static class ServiceCollectionExtensions
{
public static IServiceCollection AddServiceCaching(
this IServiceCollection services, IConfiguration config)
{
services.Configure<CacheSettings>(config.GetSection("CacheSettings"));
services.AddSingleton<ICacheService, MemoryCacheService>();

return services;
}
}
}
53 changes: 53 additions & 0 deletions src/DfE.CoreLibs.Caching/Services/MemoryCacheService.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
using System.Diagnostics.CodeAnalysis;
using DfE.CoreLibs.Caching.Interfaces;
using DfE.CoreLibs.Caching.Settings;
using Microsoft.Extensions.Caching.Memory;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;

namespace DfE.CoreLibs.Caching.Services
{
[ExcludeFromCodeCoverage]
public class MemoryCacheService(
IMemoryCache memoryCache,
ILogger<MemoryCacheService> logger,
IOptions<CacheSettings> cacheSettings)
: ICacheService
{
private readonly CacheSettings _cacheSettings = cacheSettings.Value;

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

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

if (Equals(result, default(T))) return result;
var cacheDuration = GetCacheDurationForMethod(methodName);
memoryCache.Set(cacheKey, result, cacheDuration);
logger.LogInformation("Cached result for key: {CacheKey} for duration: {CacheDuration}", cacheKey, cacheDuration);

return result;
}

public void Remove(string cacheKey)
{
memoryCache.Remove(cacheKey);
logger.LogInformation("Cache removed for key: {CacheKey}", cacheKey);
}

private TimeSpan GetCacheDurationForMethod(string methodName)
{
if (_cacheSettings.Durations.TryGetValue(methodName, out int durationInSeconds))
{
return TimeSpan.FromSeconds(durationInSeconds);
}
return TimeSpan.FromSeconds(_cacheSettings.DefaultDurationInSeconds);
}
}
}
8 changes: 8 additions & 0 deletions src/DfE.CoreLibs.Caching/Settings/CacheSettings.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
namespace DfE.CoreLibs.Caching.Settings
{
public class CacheSettings
{
public int DefaultDurationInSeconds { get; set; } = 5;
public Dictionary<string, int> Durations { get; set; } = new();
}
}

0 comments on commit a3d6efb

Please sign in to comment.