diff --git a/.github/workflows/dotnet.yml b/.github/workflows/dotnet.yml index 27de4aa..e8a2fc2 100644 --- a/.github/workflows/dotnet.yml +++ b/.github/workflows/dotnet.yml @@ -8,31 +8,17 @@ on: jobs: build: - runs-on: ubuntu-latest - strategy: - fail-fast: false - matrix: - include: - - framework: net8.0 - - framework: net9.0 - - framework: net10.0 - - name: build (${{ matrix.framework }}) - steps: - uses: actions/checkout@v4 - name: Setup .NET uses: actions/setup-dotnet@v4 with: - dotnet-version: | - 8.0.x - 9.0.x - 10.0.x + dotnet-version: 10.0.x - name: Restore dependencies run: dotnet restore - name: Build - run: dotnet build --no-restore --framework ${{ matrix.framework }} + run: dotnet build --no-restore --configuration Release - name: Test - run: dotnet test --no-build --framework ${{ matrix.framework }} --verbosity normal + run: dotnet test --no-build --configuration Release --verbosity normal diff --git a/InMemoryCachingSample.Tests/CacheProviderTests.cs b/InMemoryCachingSample.Tests/CacheProviderTests.cs index 17d1d9d..cdfeeac 100644 --- a/InMemoryCachingSample.Tests/CacheProviderTests.cs +++ b/InMemoryCachingSample.Tests/CacheProviderTests.cs @@ -13,73 +13,73 @@ public void GetFromCache_WhenKeyExists_ReturnsValue() // Arrange var memoryCacheMock = new Mock(); object? cachedValue = "cached-value"; - + memoryCacheMock .Setup(m => m.TryGetValue(It.IsAny(), out cachedValue)) .Returns(true); - + var cacheProvider = new CacheProvider(memoryCacheMock.Object); - + // Act var result = cacheProvider.GetFromCache("test-key"); - + // Assert Assert.Equal("cached-value", result); memoryCacheMock.Verify(m => m.TryGetValue("test-key", out cachedValue), Times.Once); } - + [Fact] public void GetFromCache_WhenKeyDoesNotExist_ReturnsNull() { // Arrange var memoryCacheMock = new Mock(); object? cachedValue = null; - + memoryCacheMock .Setup(m => m.TryGetValue(It.IsAny(), out cachedValue)) .Returns(false); - + var cacheProvider = new CacheProvider(memoryCacheMock.Object); - + // Act var result = cacheProvider.GetFromCache("non-existent-key"); - + // Assert Assert.Null(result); memoryCacheMock.Verify(m => m.TryGetValue("non-existent-key", out cachedValue), Times.Once); } - + [Fact] public void SetCache_SetsValueInCache() { // Arrange var memoryCacheMock = new Mock(); var cacheMockSetup = new Mock(); - + memoryCacheMock .Setup(m => m.CreateEntry(It.IsAny())) .Returns(cacheMockSetup.Object); - + var cacheProvider = new CacheProvider(memoryCacheMock.Object); var options = new MemoryCacheEntryOptions(); - + // Act cacheProvider.SetCache("test-key", "test-value", options); - + // Assert memoryCacheMock.Verify(m => m.CreateEntry("test-key"), Times.Once); } - + [Fact] public void ClearCache_RemovesKeyFromCache() { // Arrange var memoryCacheMock = new Mock(); var cacheProvider = new CacheProvider(memoryCacheMock.Object); - + // Act cacheProvider.ClearCache("test-key"); - + // Assert memoryCacheMock.Verify(m => m.Remove("test-key"), Times.Once); } diff --git a/InMemoryCachingSample.Tests/CacheServiceTests.cs b/InMemoryCachingSample.Tests/CacheServiceTests.cs index 5f237e6..d73b2e7 100644 --- a/InMemoryCachingSample.Tests/CacheServiceTests.cs +++ b/InMemoryCachingSample.Tests/CacheServiceTests.cs @@ -17,11 +17,11 @@ public CacheServiceTests() { _cacheProviderMock = new Mock(); _cacheService = new CacheService(_cacheProviderMock.Object); - + _sampleUsers = new List { - new User { id = 1, email = "user1@example.com" }, - new User { id = 2, email = "user2@example.com" } + new User { Id = 1, Email = "user1@example.com" }, + new User { Id = 2, Email = "user2@example.com" } }; } @@ -50,4 +50,4 @@ public void ClearCache_RemovesUserCacheKey() // Assert _cacheProviderMock.Verify(c => c.ClearCache(CacheKeys.Users), Times.Once); } -} \ No newline at end of file +} diff --git a/InMemoryCachingSample.Tests/CachedUserServiceTests.cs b/InMemoryCachingSample.Tests/CachedUserServiceTests.cs index 0112147..d79f219 100644 --- a/InMemoryCachingSample.Tests/CachedUserServiceTests.cs +++ b/InMemoryCachingSample.Tests/CachedUserServiceTests.cs @@ -20,11 +20,11 @@ public CachedUserServiceTests() _usersServiceMock = new Mock(); _cacheProviderMock = new Mock(); _cachedUserService = new CachedUserService(_usersServiceMock.Object, _cacheProviderMock.Object); - + _sampleUsers = new List { - new User { id = 1, email = "user1@example.com" }, - new User { id = 2, email = "user2@example.com" } + new User { Id = 1, Email = "user1@example.com" }, + new User { Id = 2, Email = "user2@example.com" } }; } @@ -64,11 +64,11 @@ public async Task GetUsersAsync_WhenCacheDoesNotExist_FetchesAndCachesData() _usersServiceMock.Verify(s => s.GetUsersAsync(), Times.Once); _cacheProviderMock.Verify( c => c.SetCache( - CacheKeys.Users, - It.IsAny>(), + CacheKeys.Users, + It.IsAny>(), It.IsAny() - ), + ), Times.Once ); } -} \ No newline at end of file +} diff --git a/InMemoryCachingSample.Tests/HttpClientTests.cs b/InMemoryCachingSample.Tests/HttpClientTests.cs index 459cba3..d9a65ea 100644 --- a/InMemoryCachingSample.Tests/HttpClientTests.cs +++ b/InMemoryCachingSample.Tests/HttpClientTests.cs @@ -17,7 +17,7 @@ public async Task Get_ReturnsUsersFromApi() var response = new HttpResponseMessage { StatusCode = HttpStatusCode.OK, - Content = new StringContent(@"{""data"":[{""id"":1,""email"":""george.bluth@reqres.in""},{""id"":2,""email"":""janet.weaver@reqres.in""}]}") + Content = new StringContent(@"[{""id"":1,""email"":""leanne@example.com""},{""id"":2,""email"":""ervin@example.com""}]") }; handlerMock @@ -40,10 +40,10 @@ public async Task Get_ReturnsUsersFromApi() // Assert var userList = users.ToList(); Assert.Equal(2, userList.Count); - Assert.Equal(1, userList[0].id); - Assert.Equal("george.bluth@reqres.in", userList[0].email); - Assert.Equal(2, userList[1].id); - Assert.Equal("janet.weaver@reqres.in", userList[1].email); + Assert.Equal(1, userList[0].Id); + Assert.Equal("leanne@example.com", userList[0].Email); + Assert.Equal(2, userList[1].Id); + Assert.Equal("ervin@example.com", userList[1].Email); } [Fact] @@ -73,4 +73,4 @@ public async Task Get_WhenApiReturnsError_ThrowsHttpRequestException() // Act & Assert await Assert.ThrowsAsync(() => client.Get()); } -} \ No newline at end of file +} diff --git a/InMemoryCachingSample.Tests/InMemoryCachingSample.Tests.csproj b/InMemoryCachingSample.Tests/InMemoryCachingSample.Tests.csproj index 598277c..c916253 100644 --- a/InMemoryCachingSample.Tests/InMemoryCachingSample.Tests.csproj +++ b/InMemoryCachingSample.Tests/InMemoryCachingSample.Tests.csproj @@ -1,7 +1,7 @@ - net8.0;net9.0;net10.0 + net10.0 enable enable @@ -9,7 +9,7 @@ - + diff --git a/InMemoryCachingSample.Tests/UnitTest1.cs b/InMemoryCachingSample.Tests/UnitTest1.cs deleted file mode 100644 index 71c805f..0000000 --- a/InMemoryCachingSample.Tests/UnitTest1.cs +++ /dev/null @@ -1,10 +0,0 @@ -namespace InMemoryCachingSample.Tests; - -public class UnitTest1 -{ - [Fact] - public void Test1() - { - - } -} \ No newline at end of file diff --git a/InMemoryCachingSample/Controllers/HomeController.cs b/InMemoryCachingSample/Controllers/HomeController.cs index f94d307..b784141 100644 --- a/InMemoryCachingSample/Controllers/HomeController.cs +++ b/InMemoryCachingSample/Controllers/HomeController.cs @@ -9,7 +9,7 @@ public class HomeController(ILogger logger, IUsersService usersS private readonly IUsersService _usersService = usersService; private readonly ICacheService _cacheService = cacheService; - public IActionResult Index() + public IActionResult Index() { var users = _cacheService.GetCachedUser(); if (users == null) return View(); @@ -25,7 +25,7 @@ public async Task CacheUser() return RedirectToAction(nameof(Index)); } - + public IActionResult ClearCache() { _cacheService.ClearCache(); @@ -35,7 +35,7 @@ public IActionResult ClearCache() [ResponseCache(Duration = 0, Location = ResponseCacheLocation.None, NoStore = true)] public IActionResult Error() { - return View(new ErrorViewModel {RequestId = Activity.Current?.Id ?? HttpContext.TraceIdentifier}); + return View(new ErrorViewModel { RequestId = Activity.Current?.Id ?? HttpContext.TraceIdentifier }); } } } \ No newline at end of file diff --git a/InMemoryCachingSample/InMemoryCachingSample.csproj b/InMemoryCachingSample/InMemoryCachingSample.csproj index aaa2a11..5930cde 100644 --- a/InMemoryCachingSample/InMemoryCachingSample.csproj +++ b/InMemoryCachingSample/InMemoryCachingSample.csproj @@ -2,7 +2,7 @@ xsi:noNamespaceSchemaLocation="InMemoryCachingSample.xsd" Sdk="Microsoft.NET.Sdk.Web"> - net8.0;net9.0;net10.0 + net10.0 enable enable diff --git a/InMemoryCachingSample/Infrastructure/CacheProvider.cs b/InMemoryCachingSample/Infrastructure/CacheProvider.cs index 4e57bef..4acab60 100644 --- a/InMemoryCachingSample/Infrastructure/CacheProvider.cs +++ b/InMemoryCachingSample/Infrastructure/CacheProvider.cs @@ -8,10 +8,10 @@ public interface ICacheProvider } public class CacheProvider(IMemoryCache cache) : ICacheProvider -{ +{ private readonly IMemoryCache _cache = cache; - public T? GetFromCache(string key) where T : class + public T? GetFromCache(string key) where T : class { _cache.TryGetValue(key, out T? cachedResponse); return cachedResponse; diff --git a/InMemoryCachingSample/Infrastructure/HttpClient.cs b/InMemoryCachingSample/Infrastructure/HttpClient.cs index e54c47f..a26e3d6 100644 --- a/InMemoryCachingSample/Infrastructure/HttpClient.cs +++ b/InMemoryCachingSample/Infrastructure/HttpClient.cs @@ -2,11 +2,6 @@ namespace InMemoryCachingSample.Infrastructure; -public class UserResponse -{ - public User[] Data { get; set; } = []; -} - public interface IHttpClient { Task> Get(); @@ -14,23 +9,22 @@ public interface IHttpClient public class HttpClient(IHttpClientFactory clientFactory) : IHttpClient { - private const string API_URL = "https://reqres.in/api/users"; - + private const string UsersEndpoint = "https://jsonplaceholder.typicode.com/users"; + private readonly IHttpClientFactory _clientFactory = clientFactory; - public async Task> Get() + public async Task> Get() { var client = _clientFactory.CreateClient(); try { - var usersResponse = await client.GetFromJsonAsync(API_URL); - return usersResponse?.Data ?? []; + return await client.GetFromJsonAsync(UsersEndpoint) ?? []; } catch (Exception ex) { // In a real application, you would log this exception - throw new HttpRequestException($"Error fetching users from {API_URL}", ex); + throw new HttpRequestException($"Error fetching users from {UsersEndpoint}", ex); } } -} \ No newline at end of file +} diff --git a/InMemoryCachingSample/Models/User.cs b/InMemoryCachingSample/Models/User.cs index 6c92b74..fc46b33 100644 --- a/InMemoryCachingSample/Models/User.cs +++ b/InMemoryCachingSample/Models/User.cs @@ -1,7 +1,12 @@ +using System.Text.Json.Serialization; + namespace InMemoryCachingSample.Models; public class User { - public int id { get; set; } - public string email { get; set; } = string.Empty; -} \ 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/InMemoryCachingSample/Program.cs b/InMemoryCachingSample/Program.cs index b5d7de5..b8b8eb1 100644 --- a/InMemoryCachingSample/Program.cs +++ b/InMemoryCachingSample/Program.cs @@ -14,7 +14,7 @@ // Use fully qualified name to resolve ambiguity builder.Services.AddScoped(); // Register IUsersService implementation with decorator pattern -builder.Services.AddScoped(sp => +builder.Services.AddScoped(sp => { var usersService = sp.GetRequiredService(); var cacheProvider = sp.GetRequiredService(); diff --git a/InMemoryCachingSample/Services/CacheService.cs b/InMemoryCachingSample/Services/CacheService.cs index 3a93d38..f2239f9 100644 --- a/InMemoryCachingSample/Services/CacheService.cs +++ b/InMemoryCachingSample/Services/CacheService.cs @@ -14,7 +14,7 @@ public class CacheService(ICacheProvider cacheProvider) : ICacheService { private readonly ICacheProvider _cacheProvider = cacheProvider; - public IEnumerable? GetCachedUser() + public IEnumerable? GetCachedUser() { return _cacheProvider.GetFromCache>(CacheKeys.Users); } diff --git a/InMemoryCachingSample/Services/CachedUserService.cs b/InMemoryCachingSample/Services/CachedUserService.cs index 9b40de6..7318130 100644 --- a/InMemoryCachingSample/Services/CachedUserService.cs +++ b/InMemoryCachingSample/Services/CachedUserService.cs @@ -17,11 +17,11 @@ public class CachedUserService(IUsersService usersService, ICacheProvider cacheP private static readonly SemaphoreSlim GetUsersSemaphore = new(1, 1); - public async Task> GetUsersAsync() + public async Task> GetUsersAsync() { return await GetCachedResponse(CacheKeys.Users, GetUsersSemaphore, _usersService.GetUsersAsync); } - + private async Task> GetCachedResponse(string cacheKey, SemaphoreSlim semaphore, Func>> func) { var users = _cacheProvider.GetFromCache>(cacheKey); @@ -30,13 +30,13 @@ private async Task> GetCachedResponse(string cacheKey, Semapho try { await semaphore.WaitAsync(); - + // Recheck to make sure it didn't populate before entering semaphore users = _cacheProvider.GetFromCache>(cacheKey); if (users != null) return users; users = await func(); - + _cacheProvider.SetCache(cacheKey, users, _cacheEntryOptions); } finally diff --git a/InMemoryCachingSample/Services/UsersService.cs b/InMemoryCachingSample/Services/UsersService.cs index de93529..f5e8a2c 100644 --- a/InMemoryCachingSample/Services/UsersService.cs +++ b/InMemoryCachingSample/Services/UsersService.cs @@ -12,7 +12,7 @@ public class UsersService(IHttpClient httpClient) : IUsersService { private readonly IHttpClient _httpClient = httpClient; - public Task> GetUsersAsync() + public Task> GetUsersAsync() { return _httpClient.Get(); } diff --git a/InMemoryCachingSample/Views/Home/Index.cshtml b/InMemoryCachingSample/Views/Home/Index.cshtml index 8ca4d16..daa4344 100644 --- a/InMemoryCachingSample/Views/Home/Index.cshtml +++ b/InMemoryCachingSample/Views/Home/Index.cshtml @@ -4,7 +4,7 @@ }
-

Cached User: @(Model == null ? "No cached entry found" : Model.email)

+

Cached User: @(Model == null ? "No cached entry found" : Model.Email)

Cache It Clear It diff --git a/README.md b/README.md index 0142f84..5cfe03f 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ # In-Memory Caching in .NET 10 with IMemoryCache [![Documentation](https://img.shields.io/badge/documentation-yes-brightgreen.svg)](https://sahansera.dev) -[![.NET](https://github.com/sahansera/InMemoryCacheNetCore/actions/workflows/dotnet.yml/badge.svg?branch=master)](https://github.com/sahansera/InMemoryCacheNetCore/actions/workflows/dotnet.yml) +[![.NET](https://github.com/sahansera/InMemoryCacheNetCore/actions/workflows/dotnet.yml/badge.svg?branch=main)](https://github.com/sahansera/InMemoryCacheNetCore/actions/workflows/dotnet.yml) [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](#) [![Twitter: _SahanSera](https://img.shields.io/twitter/follow/_SahanSera.svg?style=social)](https://twitter.com/_SahanSera) @@ -18,32 +18,17 @@ This project has evolved through several .NET versions: - Started with .NET Core 3.1 - Updated to .NET 6 with minimal hosting model - Upgraded to .NET 9.0 with modern C# features -- Currently multi-targets every in-support .NET version (8 LTS, 9 STS, 10 LTS), validated by a CI matrix +- Upgraded to .NET 10 LTS ### Version history 🏷 -`main` multi-targets the .NET versions that are currently in support and is -validated against each of them in CI. As versions reach end-of-life they are -dropped from the target frameworks. For an older, out-of-support release, check -out the matching tag (e.g. `git checkout v9.0`). Browse all snapshots on the -[tags page](https://github.com/sahansera/InMemoryCacheNetCore/tags). - -| Tag | .NET version | Support status | -| ------- | ------------- | -------------- | -| `v10.0` | .NET 10 | LTS | -| `v9.0` | .NET 9.0 | STS (EOL Nov 2026) | -| `v6.0` | .NET 6 | EOL | -| `v3.1` | .NET Core 3.1 | EOL | - -> Note: tags are immutable snapshots, not maintained branches. Older, -> out-of-support versions are preserved for reference only. +`main` targets .NET 10 LTS and is validated in CI. Earlier implementations can +still be inspected in the repository's Git history, but they are not supported. ## Maintenance 🔧 -To stay current with low upkeep, `main` targets every **in-support** .NET -version and the CI matrix builds and tests against each one. When a version -reaches end-of-life it is removed from `` and the CI matrix, -and a `vX.0` tag is cut so version-pinned readers can self-serve. +To stay current with low upkeep, `main` follows the current .NET LTS release. +Older, out-of-support target frameworks are not kept on the default branch. ## Key Features in .NET 10 Version ✨ @@ -89,7 +74,7 @@ InMemoryCachingSample.Tests/ # Test project ## Usage 🚀 ### Requirements -- .NET 8, 9, and 10 SDKs to build all target frameworks (or build a single one with `dotnet build --framework net10.0`) +- .NET 10 SDK ### Running the Project ```bash @@ -103,7 +88,7 @@ cd InMemoryCacheNetCore dotnet build # Run the application -dotnet run --project InMemoryCachingSample/InMemoryCachingSample.csproj --framework net10.0 +dotnet run --project InMemoryCachingSample/InMemoryCachingSample.csproj # Run the tests dotnet test 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" + } +}