From 01bdeaac4011ac5d872f600a19db817121ad5e5c Mon Sep 17 00:00:00 2001 From: Sahan Serasinghe Date: Sat, 15 Aug 2026 11:30:59 +0930 Subject: [PATCH] Upgrade distributed cache sample to .NET 10 --- .github/workflows/codeql.yml | 8 +- .github/workflows/dotnet.yml | 10 +-- DistributedCache.Tests/CacheProviderTests.cs | 55 +++++++++++++ .../CachedUserServiceTests.cs | 48 +++++++++++ .../DistributedCache.Tests.csproj | 24 ++++++ DistributedCache.Tests/HttpClientTests.cs | 53 ++++++++++++ DistributedCache.Tests/Usings.cs | 1 + .../Controllers/HomeController.cs | 71 +++++++--------- DistributedCache/DistributedCache.csproj | 16 ++-- .../Infrastructure/CacheProvider.cs | 60 +++++++------- DistributedCache/Infrastructure/HttpClient.cs | 62 ++++---------- DistributedCache/Models/ErrorViewModel.cs | 19 ++--- DistributedCache/Models/User.cs | 19 +++-- DistributedCache/Program.cs | 22 +++-- DistributedCache/Services/CacheUserService.cs | 51 +++++------- .../Services/CachedUserService.cs | 80 +++++++++---------- DistributedCache/Services/UserService.cs | 33 +++----- DistributedCache/appsettings.Development.json | 4 +- DistributedCache/appsettings.json | 5 +- DistributedCachingNetCoreTutorial.sln | 22 ++++- README.md | 25 +++++- docker-compose.yaml | 16 ++-- global.json | 6 ++ 23 files changed, 422 insertions(+), 288 deletions(-) create mode 100644 DistributedCache.Tests/CacheProviderTests.cs create mode 100644 DistributedCache.Tests/CachedUserServiceTests.cs create mode 100644 DistributedCache.Tests/DistributedCache.Tests.csproj create mode 100644 DistributedCache.Tests/HttpClientTests.cs create mode 100644 DistributedCache.Tests/Usings.cs create mode 100644 global.json diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml index ce2fd79..aff0417 100644 --- a/.github/workflows/codeql.yml +++ b/.github/workflows/codeql.yml @@ -38,11 +38,11 @@ jobs: steps: - name: Checkout repository - uses: actions/checkout@v3 + uses: actions/checkout@v4 # Initializes the CodeQL tools for scanning. - name: Initialize CodeQL - uses: github/codeql-action/init@v2 + uses: github/codeql-action/init@v3 with: languages: ${{ matrix.language }} # If you wish to specify custom queries, you can do so here or in a config file. @@ -56,7 +56,7 @@ jobs: # Autobuild attempts to build any compiled languages (C/C++, C#, or Java). # If this step fails, then you should remove it and run the build manually (see below) - name: Autobuild - uses: github/codeql-action/autobuild@v2 + uses: github/codeql-action/autobuild@v3 # ℹ️ Command-line programs to run using the OS shell. # 📚 See https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#jobsjob_idstepsrun @@ -69,6 +69,6 @@ jobs: # ./location_of_script_within_repo/buildscript.sh - name: Perform CodeQL Analysis - uses: github/codeql-action/analyze@v2 + uses: github/codeql-action/analyze@v3 with: category: "/language:${{matrix.language}}" diff --git a/.github/workflows/dotnet.yml b/.github/workflows/dotnet.yml index 2d29079..4181378 100644 --- a/.github/workflows/dotnet.yml +++ b/.github/workflows/dotnet.yml @@ -12,14 +12,14 @@ jobs: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 - name: Setup .NET - uses: actions/setup-dotnet@v2 + uses: actions/setup-dotnet@v4 with: - dotnet-version: 6.0.x + dotnet-version: 10.0.x - name: Restore dependencies run: dotnet restore - name: Build - run: dotnet build --no-restore + run: dotnet build --no-restore --configuration Release - name: Test - run: dotnet test --no-build --verbosity normal + run: dotnet test --no-build --configuration Release --verbosity normal diff --git a/DistributedCache.Tests/CacheProviderTests.cs b/DistributedCache.Tests/CacheProviderTests.cs new file mode 100644 index 0000000..a36e2c8 --- /dev/null +++ b/DistributedCache.Tests/CacheProviderTests.cs @@ -0,0 +1,55 @@ +using System.Text; +using System.Text.Json; +using DistributedCache.Infrastructure; +using DistributedCache.Models; +using Microsoft.Extensions.Caching.Distributed; +using Moq; + +namespace DistributedCache.Tests; + +public class CacheProviderTests +{ + [Fact] + public async Task GetFromCacheAsync_WhenKeyExists_ReturnsDeserializedValue() + { + var users = new[] { new User { Id = 1, Email = "leanne@example.com" } }; + var serializedUsers = Encoding.UTF8.GetBytes(JsonSerializer.Serialize(users)); + var cache = new Mock(); + cache.Setup(x => x.GetAsync("users", It.IsAny())) + .ReturnsAsync(serializedUsers); + var provider = new CacheProvider(cache.Object); + + var result = await provider.GetFromCacheAsync>("users"); + + Assert.NotNull(result); + Assert.Single(result); + Assert.Equal("leanne@example.com", result[0].Email); + } + + [Fact] + public async Task SetCacheAsync_SerializesAndStoresValue() + { + var cache = new Mock(); + var provider = new CacheProvider(cache.Object); + var users = new[] { new User { Id = 1, Email = "leanne@example.com" } }; + + await provider.SetCacheAsync("users", users, new DistributedCacheEntryOptions()); + + cache.Verify(x => x.SetAsync( + "users", + It.Is(value => Encoding.UTF8.GetString(value).Contains("leanne@example.com")), + It.IsAny(), + It.IsAny()), Times.Once); + } + + [Fact] + public async Task ClearCacheAsync_RemovesKey() + { + var cache = new Mock(); + var provider = new CacheProvider(cache.Object); + + await provider.ClearCacheAsync("users"); + + cache.Verify(x => x.RemoveAsync("users", It.IsAny()), Times.Once); + } +} diff --git a/DistributedCache.Tests/CachedUserServiceTests.cs b/DistributedCache.Tests/CachedUserServiceTests.cs new file mode 100644 index 0000000..886bedf --- /dev/null +++ b/DistributedCache.Tests/CachedUserServiceTests.cs @@ -0,0 +1,48 @@ +using DistributedCache.Infrastructure; +using DistributedCache.Models; +using DistributedCache.Services; +using Microsoft.Extensions.Caching.Distributed; +using Moq; + +namespace DistributedCache.Tests; + +public class CachedUserServiceTests +{ + private static readonly IReadOnlyList Users = + [new User { Id = 1, Email = "leanne@example.com" }]; + + [Fact] + public async Task GetUsersAsync_WhenCacheExists_DoesNotCallUsersApi() + { + var usersApi = new Mock(); + var cache = new Mock(); + cache.Setup(x => x.GetFromCacheAsync>(CacheKeys.Users)) + .ReturnsAsync(Users); + var service = new CachedUserService(new UserService(usersApi.Object), cache.Object); + + var result = await service.GetUsersAsync(); + + Assert.Same(Users, result); + usersApi.Verify(x => x.GetUsersAsync(), Times.Never); + } + + [Fact] + public async Task GetUsersAsync_WhenCacheIsEmpty_LoadsAndCachesUsers() + { + var usersApi = new Mock(); + usersApi.Setup(x => x.GetUsersAsync()).ReturnsAsync(Users); + var cache = new Mock(); + cache.Setup(x => x.GetFromCacheAsync>(CacheKeys.Users)) + .ReturnsAsync((IReadOnlyList?)null); + var service = new CachedUserService(new UserService(usersApi.Object), cache.Object); + + var result = await service.GetUsersAsync(); + + Assert.Same(Users, result); + usersApi.Verify(x => x.GetUsersAsync(), Times.Once); + cache.Verify(x => x.SetCacheAsync( + CacheKeys.Users, + Users, + It.IsAny()), Times.Once); + } +} diff --git a/DistributedCache.Tests/DistributedCache.Tests.csproj b/DistributedCache.Tests/DistributedCache.Tests.csproj new file mode 100644 index 0000000..69a00c9 --- /dev/null +++ b/DistributedCache.Tests/DistributedCache.Tests.csproj @@ -0,0 +1,24 @@ + + + net10.0 + enable + enable + false + + + + + + + runtime; build; native; contentfiles; analyzers; buildtransitive + all + + + runtime; build; native; contentfiles; analyzers; buildtransitive + all + + + + + + diff --git a/DistributedCache.Tests/HttpClientTests.cs b/DistributedCache.Tests/HttpClientTests.cs new file mode 100644 index 0000000..759f98d --- /dev/null +++ b/DistributedCache.Tests/HttpClientTests.cs @@ -0,0 +1,53 @@ +using System.Net; +using DistributedCache.Infrastructure; +using Moq; +using Moq.Protected; + +namespace DistributedCache.Tests; + +public class HttpClientTests +{ + [Fact] + public async Task GetUsersAsync_ReturnsUsersFromApi() + { + var handler = new Mock(); + handler.Protected() + .Setup>( + "SendAsync", + ItExpr.IsAny(), + ItExpr.IsAny()) + .ReturnsAsync(new HttpResponseMessage + { + StatusCode = HttpStatusCode.OK, + Content = new StringContent( + """[{"id":1,"email":"leanne@example.com"}]""") + }); + var factory = new Mock(); + factory.Setup(x => x.CreateClient(It.IsAny())) + .Returns(new System.Net.Http.HttpClient(handler.Object)); + var client = new UsersApiClient(factory.Object); + + var result = await client.GetUsersAsync(); + + Assert.Single(result); + Assert.Equal("leanne@example.com", result[0].Email); + } + + [Fact] + public async Task GetUsersAsync_WhenApiReturnsError_ThrowsHttpRequestException() + { + var handler = new Mock(); + handler.Protected() + .Setup>( + "SendAsync", + ItExpr.IsAny(), + ItExpr.IsAny()) + .ReturnsAsync(new HttpResponseMessage(HttpStatusCode.InternalServerError)); + var factory = new Mock(); + factory.Setup(x => x.CreateClient(It.IsAny())) + .Returns(new System.Net.Http.HttpClient(handler.Object)); + var client = new UsersApiClient(factory.Object); + + await Assert.ThrowsAsync(() => client.GetUsersAsync()); + } +} diff --git a/DistributedCache.Tests/Usings.cs b/DistributedCache.Tests/Usings.cs new file mode 100644 index 0000000..c802f44 --- /dev/null +++ b/DistributedCache.Tests/Usings.cs @@ -0,0 +1 @@ +global using Xunit; diff --git a/DistributedCache/Controllers/HomeController.cs b/DistributedCache/Controllers/HomeController.cs index 9f1f9f8..7c1c78c 100644 --- a/DistributedCache/Controllers/HomeController.cs +++ b/DistributedCache/Controllers/HomeController.cs @@ -1,41 +1,30 @@ -using System.Linq; -using System.Threading.Tasks; -using Microsoft.AspNetCore.Mvc; -using Microsoft.Extensions.Logging; -using DistributedCache.Services; - -namespace DistributedCache.Controllers -{ - public class HomeController : Controller - { - private readonly ILogger _logger; - private readonly IUserService _userService; - private readonly ICacheUserService _cacheUserService; - - public HomeController(ILogger logger, IUserService userService, ICacheUserService cacheUserService) - { - _logger = logger; - _userService = userService; - _cacheUserService = cacheUserService; - } - - public async Task Index() - { - var users = (await _cacheUserService.GetCachedUser())?.FirstOrDefault(); - return View(users); - } - - public async Task CacheUserAsync() - { - var users = await _userService.GetUsersAsync(); - var cachedEntry = users.First(); - return View(nameof(Index), cachedEntry); - } - - public IActionResult CacheRemoveAsync() - { - _cacheUserService.ClearCache(); - return RedirectToAction(nameof(Index)); - } - } -} +using Microsoft.AspNetCore.Mvc; +using DistributedCache.Services; + +namespace DistributedCache.Controllers; + +public class HomeController( + IUserService userService, + ICacheUserService cacheUserService) : Controller +{ + private readonly IUserService _userService = userService; + private readonly ICacheUserService _cacheUserService = cacheUserService; + + public async Task Index() + { + var user = (await _cacheUserService.GetCachedUserAsync()).FirstOrDefault(); + return View(user); + } + + public async Task CacheUserAsync() + { + var users = await _userService.GetUsersAsync(); + return View(nameof(Index), users.FirstOrDefault()); + } + + public async Task CacheRemoveAsync() + { + await _cacheUserService.ClearCacheAsync(); + return RedirectToAction(nameof(Index)); + } +} diff --git a/DistributedCache/DistributedCache.csproj b/DistributedCache/DistributedCache.csproj index 0a2f0bf..b1a1887 100644 --- a/DistributedCache/DistributedCache.csproj +++ b/DistributedCache/DistributedCache.csproj @@ -1,11 +1,13 @@ - - net6.0 - - - - - + + net10.0 + enable + enable + + + + + diff --git a/DistributedCache/Infrastructure/CacheProvider.cs b/DistributedCache/Infrastructure/CacheProvider.cs index 729efb1..be2eb6b 100644 --- a/DistributedCache/Infrastructure/CacheProvider.cs +++ b/DistributedCache/Infrastructure/CacheProvider.cs @@ -1,41 +1,37 @@ using System.Text.Json; -using System.Threading.Tasks; using Microsoft.Extensions.Caching.Distributed; -namespace DistributedCache.Infrastructure +namespace DistributedCache.Infrastructure; + +public interface ICacheProvider +{ + Task GetFromCacheAsync(string key) where T : class; + Task SetCacheAsync(string key, T value, DistributedCacheEntryOptions options) + where T : class; + Task ClearCacheAsync(string key); +} + +public class CacheProvider(IDistributedCache cache) : ICacheProvider { - - public interface ICacheProvider + private readonly IDistributedCache _cache = cache; + + public async Task GetFromCacheAsync(string key) where T : class { - Task GetFromCache(string key) where T : class; - Task SetCache(string key, T value, DistributedCacheEntryOptions options) where T : class; - Task ClearCache(string key); + var cachedValue = await _cache.GetStringAsync(key); + return cachedValue is null ? null : JsonSerializer.Deserialize(cachedValue); } - - public class CacheProvider : ICacheProvider - { - private readonly IDistributedCache _cache; - public CacheProvider(IDistributedCache cache) - { - _cache = cache; - } - - public async Task GetFromCache(string key) where T : class - { - var cachedUsers = await _cache.GetStringAsync(key); - return cachedUsers == null ? null : JsonSerializer.Deserialize(cachedUsers); - } - - public async Task SetCache(string key, T value, DistributedCacheEntryOptions options) where T : class - { - var users = JsonSerializer.Serialize(value); - await _cache.SetStringAsync(key, users , options); - } + public Task SetCacheAsync( + string key, + T value, + DistributedCacheEntryOptions options) where T : class + { + var serializedValue = JsonSerializer.Serialize(value); + return _cache.SetStringAsync(key, serializedValue, options); + } - public async Task ClearCache(string key) - { - await _cache.RemoveAsync(key); - } + public Task ClearCacheAsync(string key) + { + return _cache.RemoveAsync(key); } -} \ No newline at end of file +} diff --git a/DistributedCache/Infrastructure/HttpClient.cs b/DistributedCache/Infrastructure/HttpClient.cs index d2347a0..604bb20 100644 --- a/DistributedCache/Infrastructure/HttpClient.cs +++ b/DistributedCache/Infrastructure/HttpClient.cs @@ -1,53 +1,21 @@ -using System; -using System.Collections.Generic; -using System.Net.Http; -using System.Text.Json; -using System.Text.Json.Serialization; -using System.Threading.Tasks; +using System.Net.Http.Json; using DistributedCache.Models; -namespace DistributedCache.Infrastructure -{ - - public class UserResponse - { - [JsonPropertyName("data")] - public User[] Data { get; set; } - } - - public interface IHttpClient - { - Task> Get(); - } - - public class HttpClient : IHttpClient - { - private const string UsersEndpoint = "https://reqres.in/api/users"; - private readonly IHttpClientFactory _clientFactory; - - public HttpClient(IHttpClientFactory clientFactory) - { - _clientFactory = clientFactory; - } +namespace DistributedCache.Infrastructure; - public async Task> Get() - { - var request = new HttpRequestMessage(HttpMethod.Get, UsersEndpoint); - var client = _clientFactory.CreateClient(); +public interface IUsersApiClient +{ + Task> GetUsersAsync(); +} - var response = await client.SendAsync(request); +public class UsersApiClient(IHttpClientFactory clientFactory) : IUsersApiClient +{ + private const string UsersEndpoint = "https://jsonplaceholder.typicode.com/users"; + private readonly IHttpClientFactory _clientFactory = clientFactory; - if (response.IsSuccessStatusCode) - { - await using var responseStream = await response.Content.ReadAsStreamAsync(); - var usersResponse = await JsonSerializer.DeserializeAsync(responseStream); - var users = usersResponse?.Data; - return users; - } - else - { - throw new Exception(); - } - } + public async Task> GetUsersAsync() + { + var client = _clientFactory.CreateClient(); + return await client.GetFromJsonAsync(UsersEndpoint) ?? []; } -} \ No newline at end of file +} diff --git a/DistributedCache/Models/ErrorViewModel.cs b/DistributedCache/Models/ErrorViewModel.cs index 8d801b0..390bb61 100644 --- a/DistributedCache/Models/ErrorViewModel.cs +++ b/DistributedCache/Models/ErrorViewModel.cs @@ -1,11 +1,8 @@ -using System; - -namespace DistributedCache.Models -{ - public class ErrorViewModel - { - public string RequestId { get; set; } - - public bool ShowRequestId => !string.IsNullOrEmpty(RequestId); - } -} +namespace DistributedCache.Models; + +public class ErrorViewModel +{ + public string RequestId { get; set; } = string.Empty; + + public bool ShowRequestId => !string.IsNullOrEmpty(RequestId); +} diff --git a/DistributedCache/Models/User.cs b/DistributedCache/Models/User.cs index 5564573..64819cb 100644 --- a/DistributedCache/Models/User.cs +++ b/DistributedCache/Models/User.cs @@ -1,13 +1,12 @@ using System.Text.Json.Serialization; -namespace DistributedCache.Models +namespace DistributedCache.Models; + +public class User { - public class User - { - [JsonPropertyName("id")] - public int Id { get; set; } - - [JsonPropertyName("email")] - public string Email { get; set; } - } -} \ No newline at end of file + [JsonPropertyName("id")] + public int Id { get; set; } + + [JsonPropertyName("email")] + public string Email { get; set; } = string.Empty; +} diff --git a/DistributedCache/Program.cs b/DistributedCache/Program.cs index 4d4061e..92f3cc4 100644 --- a/DistributedCache/Program.cs +++ b/DistributedCache/Program.cs @@ -1,25 +1,23 @@ using DistributedCache.Infrastructure; using DistributedCache.Services; -using Microsoft.AspNetCore.Builder; -using Microsoft.Extensions.DependencyInjection; -using Microsoft.Extensions.Hosting; - -var builder = WebApplication.CreateBuilder(args); +var builder = WebApplication.CreateBuilder(args); builder.Services.AddControllersWithViews(); -builder.Services.AddHttpClient(); +builder.Services.AddHttpClient(); -builder.Services.AddStackExchangeRedisCache(options => -{ - options.Configuration = builder.Configuration.GetSection("Redis")["ConnectionString"]; -}); +builder.Services.AddStackExchangeRedisCache(options => +{ + options.Configuration = builder.Configuration.GetConnectionString("Redis") + ?? throw new InvalidOperationException("Connection string 'Redis' is not configured."); + options.InstanceName = "DistributedCacheSample:"; +}); builder.Services.AddScoped(); builder.Services.AddScoped(); builder.Services.AddScoped(); builder.Services.AddScoped(); -builder.Services.AddScoped(); +builder.Services.AddScoped(); var app = builder.Build(); @@ -40,4 +38,4 @@ name: "default", pattern: "{controller=Home}/{action=Index}/{id?}"); -app.Run(); \ No newline at end of file +app.Run(); diff --git a/DistributedCache/Services/CacheUserService.cs b/DistributedCache/Services/CacheUserService.cs index 54226be..809d7df 100644 --- a/DistributedCache/Services/CacheUserService.cs +++ b/DistributedCache/Services/CacheUserService.cs @@ -1,39 +1,30 @@ -using System.Collections.Generic; -using System.Threading.Tasks; using DistributedCache.Infrastructure; using DistributedCache.Models; -namespace DistributedCache.Services +namespace DistributedCache.Services; + +public interface ICacheUserService { - - public interface ICacheUserService - { - Task> GetCachedUser(); - Task ClearCache(); - } + Task> GetCachedUserAsync(); + Task ClearCacheAsync(); +} - public static class CacheKeys - { - public static string Users => "_Users"; - } - - public class CacheUserService : ICacheUserService - { - private readonly ICacheProvider _cacheProvider; +public static class CacheKeys +{ + public const string Users = "users"; +} - public CacheUserService(ICacheProvider cacheProvider) - { - _cacheProvider = cacheProvider; - } +public class CacheUserService(ICacheProvider cacheProvider) : ICacheUserService +{ + private readonly ICacheProvider _cacheProvider = cacheProvider; - public async Task> GetCachedUser() - { - return await _cacheProvider.GetFromCache>(CacheKeys.Users); - } + public async Task> GetCachedUserAsync() + { + return await _cacheProvider.GetFromCacheAsync>(CacheKeys.Users) ?? []; + } - public async Task ClearCache() - { - await _cacheProvider.ClearCache(CacheKeys.Users); - } + public Task ClearCacheAsync() + { + return _cacheProvider.ClearCacheAsync(CacheKeys.Users); } -} \ No newline at end of file +} diff --git a/DistributedCache/Services/CachedUserService.cs b/DistributedCache/Services/CachedUserService.cs index df1d2c7..3a2b71b 100644 --- a/DistributedCache/Services/CachedUserService.cs +++ b/DistributedCache/Services/CachedUserService.cs @@ -1,58 +1,50 @@ -using System; -using System.Collections.Generic; -using System.Threading; -using System.Threading.Tasks; using DistributedCache.Infrastructure; using DistributedCache.Models; using Microsoft.Extensions.Caching.Distributed; -namespace DistributedCache.Services +namespace DistributedCache.Services; + +public class CachedUserService( + UserService userService, + ICacheProvider cacheProvider) : IUserService { - public class CachedUserService : IUserService + private const int CacheTimeToLiveInSeconds = 120; + private readonly UserService _userService = userService; + private readonly ICacheProvider _cacheProvider = cacheProvider; + private static readonly SemaphoreSlim GetUsersSemaphore = new(1, 1); + + public Task> GetUsersAsync() { - private const int CacheTimeToLive = 120; - private readonly UserService _userService; - private readonly ICacheProvider _cacheProvider; + return GetCachedResponseAsync( + CacheKeys.Users, + GetUsersSemaphore, + _userService.GetUsersAsync); + } - private static readonly SemaphoreSlim GetUsersSemaphore = new(1, 1); - - public CachedUserService(UserService userService, ICacheProvider cacheProvider) - { - _userService = userService; - _cacheProvider = cacheProvider; - } - - public async Task> GetUsersAsync() - { - return await GetCachedResponse(CacheKeys.Users, GetUsersSemaphore, () => _userService.GetUsersAsync()); - } + private async Task> GetCachedResponseAsync( + string cacheKey, + SemaphoreSlim semaphore, + Func>> valueFactory) + { + var users = await _cacheProvider.GetFromCacheAsync>(cacheKey); + if (users is not null) return users; - private async Task> GetCachedResponse(string cacheKey, SemaphoreSlim semaphore, Func>> func) + await semaphore.WaitAsync(); + try { - var users = await _cacheProvider.GetFromCache>(cacheKey); + users = await _cacheProvider.GetFromCacheAsync>(cacheKey); + if (users is not null) return users; - if (users != null) return users; - try - { - await semaphore.WaitAsync(); - - // Recheck to make sure it didn't populate before entering semaphore - users = await _cacheProvider.GetFromCache>(cacheKey); - if (users != null) return users; - - users = await func(); - - var cacheEntryOptions = new DistributedCacheEntryOptions() - .SetSlidingExpiration(TimeSpan.FromSeconds(CacheTimeToLive)); - - await _cacheProvider.SetCache(cacheKey, users, cacheEntryOptions); - } - finally - { - semaphore.Release(); - } + users = await valueFactory(); + var cacheEntryOptions = new DistributedCacheEntryOptions() + .SetSlidingExpiration(TimeSpan.FromSeconds(CacheTimeToLiveInSeconds)); + await _cacheProvider.SetCacheAsync(cacheKey, users, cacheEntryOptions); return users; } + finally + { + semaphore.Release(); + } } -} \ No newline at end of file +} diff --git a/DistributedCache/Services/UserService.cs b/DistributedCache/Services/UserService.cs index 295fde2..eb37e7f 100644 --- a/DistributedCache/Services/UserService.cs +++ b/DistributedCache/Services/UserService.cs @@ -1,28 +1,19 @@ -using System.Collections.Generic; -using System.Threading.Tasks; using DistributedCache.Infrastructure; using DistributedCache.Models; -namespace DistributedCache.Services +namespace DistributedCache.Services; + +public interface IUserService { - - public interface IUserService - { - Task>GetUsersAsync(); - } - - public class UserService : IUserService - { - private readonly IHttpClient _httpClient; + Task> GetUsersAsync(); +} - public UserService(IHttpClient httpClient) - { - _httpClient = httpClient; - } +public class UserService(IUsersApiClient usersApiClient) : IUserService +{ + private readonly IUsersApiClient _usersApiClient = usersApiClient; - public Task> GetUsersAsync() - { - return _httpClient.Get(); - } + public Task> GetUsersAsync() + { + return _usersApiClient.GetUsersAsync(); } -} \ No newline at end of file +} diff --git a/DistributedCache/appsettings.Development.json b/DistributedCache/appsettings.Development.json index e18b0d4..c5bfba9 100644 --- a/DistributedCache/appsettings.Development.json +++ b/DistributedCache/appsettings.Development.json @@ -6,7 +6,7 @@ "Microsoft.Hosting.Lifetime": "Information" } }, - "Redis": { - "ConnectionString": "localhost:5002" + "ConnectionStrings": { + "Redis": "localhost:5002" } } diff --git a/DistributedCache/appsettings.json b/DistributedCache/appsettings.json index 53a1967..d1a5304 100644 --- a/DistributedCache/appsettings.json +++ b/DistributedCache/appsettings.json @@ -6,8 +6,5 @@ "Microsoft.Hosting.Lifetime": "Information" } }, - "Redis": { - "ConnectionString": "" - }, - "AllowedHosts": "*" + "AllowedHosts": "*" } diff --git a/DistributedCachingNetCoreTutorial.sln b/DistributedCachingNetCoreTutorial.sln index a2f5acd..63c9965 100644 --- a/DistributedCachingNetCoreTutorial.sln +++ b/DistributedCachingNetCoreTutorial.sln @@ -3,8 +3,10 @@ Microsoft Visual Studio Solution File, Format Version 12.00 # Visual Studio 15 VisualStudioVersion = 15.0.26124.0 MinimumVisualStudioVersion = 15.0.26124.0 -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "DistributedCache", "DistributedCache\DistributedCache.csproj", "{FEC715D7-5A5D-4021-B887-90DC276622BC}" -EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "DistributedCache", "DistributedCache\DistributedCache.csproj", "{FEC715D7-5A5D-4021-B887-90DC276622BC}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "DistributedCache.Tests", "DistributedCache.Tests\DistributedCache.Tests.csproj", "{2E3BC7C6-00B4-4DC9-A7DA-4A3C11F0A10D}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -28,7 +30,19 @@ Global {FEC715D7-5A5D-4021-B887-90DC276622BC}.Release|Any CPU.Build.0 = Release|Any CPU {FEC715D7-5A5D-4021-B887-90DC276622BC}.Release|x64.ActiveCfg = Release|Any CPU {FEC715D7-5A5D-4021-B887-90DC276622BC}.Release|x64.Build.0 = Release|Any CPU - {FEC715D7-5A5D-4021-B887-90DC276622BC}.Release|x86.ActiveCfg = Release|Any CPU - {FEC715D7-5A5D-4021-B887-90DC276622BC}.Release|x86.Build.0 = Release|Any CPU + {FEC715D7-5A5D-4021-B887-90DC276622BC}.Release|x86.ActiveCfg = Release|Any CPU + {FEC715D7-5A5D-4021-B887-90DC276622BC}.Release|x86.Build.0 = Release|Any CPU + {2E3BC7C6-00B4-4DC9-A7DA-4A3C11F0A10D}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {2E3BC7C6-00B4-4DC9-A7DA-4A3C11F0A10D}.Debug|Any CPU.Build.0 = Debug|Any CPU + {2E3BC7C6-00B4-4DC9-A7DA-4A3C11F0A10D}.Debug|x64.ActiveCfg = Debug|Any CPU + {2E3BC7C6-00B4-4DC9-A7DA-4A3C11F0A10D}.Debug|x64.Build.0 = Debug|Any CPU + {2E3BC7C6-00B4-4DC9-A7DA-4A3C11F0A10D}.Debug|x86.ActiveCfg = Debug|Any CPU + {2E3BC7C6-00B4-4DC9-A7DA-4A3C11F0A10D}.Debug|x86.Build.0 = Debug|Any CPU + {2E3BC7C6-00B4-4DC9-A7DA-4A3C11F0A10D}.Release|Any CPU.ActiveCfg = Release|Any CPU + {2E3BC7C6-00B4-4DC9-A7DA-4A3C11F0A10D}.Release|Any CPU.Build.0 = Release|Any CPU + {2E3BC7C6-00B4-4DC9-A7DA-4A3C11F0A10D}.Release|x64.ActiveCfg = Release|Any CPU + {2E3BC7C6-00B4-4DC9-A7DA-4A3C11F0A10D}.Release|x64.Build.0 = Release|Any CPU + {2E3BC7C6-00B4-4DC9-A7DA-4A3C11F0A10D}.Release|x86.ActiveCfg = Release|Any CPU + {2E3BC7C6-00B4-4DC9-A7DA-4A3C11F0A10D}.Release|x86.Build.0 = Release|Any CPU EndGlobalSection EndGlobal diff --git a/README.md b/README.md index 897ce57..ce06765 100644 --- a/README.md +++ b/README.md @@ -6,11 +6,11 @@ ## Intro 👋 -This project leverages the IDistributedCache that's shipped as part of .NET/.NET Core SDKs to achieve distributed caching in a microservices environment. If you are looking for a monolithing caching approach, then, my [other project](https://github.com/sahansera/InMemoryCacheNetCore) would be more suitable for you. +This project uses ASP.NET Core's `IDistributedCache` abstraction with Redis to share cached data between application instances. If you only need a cache inside one application process, my [in-memory caching project](https://github.com/sahansera/InMemoryCacheNetCore) is a better starting point. I've also [blogged](https://sahansera.dev/distributed-caching-aspnet-core-redis/) this with a full explanation of how this is achieved. -> Note: I have recently migrated this project to .NET 6 and also a docker-compose.yaml for better dev experience 🎉 You can still access the old version from the [.NET 5 branch](https://github.com/sahansera/DistributedCacheAspNetCoreRedis/tree/dotnet5) in this repo. +`main` targets .NET 10 LTS. The unsupported .NET 5 version remains available on the [`dotnet5` branch](https://github.com/sahansera/DistributedCacheAspNetCoreRedis/tree/dotnet5) for historical reference. ## Architecture 🏗 @@ -26,10 +26,27 @@ I've also [blogged](https://sahansera.dev/distributed-caching-aspnet-core-redis/ ## Usage 🚀 -Open up a terminal and run the following: +Requirements: + +- .NET 10 SDK +- Docker with Docker Compose + +Start Redis: + +```sh +docker compose up -d +``` + +Run the application: + +```sh +dotnet run --project DistributedCache/DistributedCache.csproj +``` + +Build and test the solution: ```sh -docker-compose up and dotnet run +dotnet test ``` ## Questions? Bugs? Suggestions for Improvement? ❓ diff --git a/docker-compose.yaml b/docker-compose.yaml index bfca2e5..cb727c3 100644 --- a/docker-compose.yaml +++ b/docker-compose.yaml @@ -1,14 +1,10 @@ -version: "3.2" services: - - redis: - image: "redis:latest" - + redis: + image: redis:8.8.1-alpine ports: - - "5002:6379" - + - "5002:6379" volumes: - - $PWD/redis-data:/var/lib/redis + - redis-data:/data - environment: - - REDIS_REPLICATION_MODE=master +volumes: + redis-data: diff --git a/global.json b/global.json new file mode 100644 index 0000000..512142d --- /dev/null +++ b/global.json @@ -0,0 +1,6 @@ +{ + "sdk": { + "version": "10.0.100", + "rollForward": "latestFeature" + } +}