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
20 changes: 3 additions & 17 deletions .github/workflows/dotnet.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
34 changes: 17 additions & 17 deletions InMemoryCachingSample.Tests/CacheProviderTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -13,73 +13,73 @@ public void GetFromCache_WhenKeyExists_ReturnsValue()
// Arrange
var memoryCacheMock = new Mock<IMemoryCache>();
object? cachedValue = "cached-value";

memoryCacheMock
.Setup(m => m.TryGetValue(It.IsAny<object>(), out cachedValue))
.Returns(true);

var cacheProvider = new CacheProvider(memoryCacheMock.Object);

// Act
var result = cacheProvider.GetFromCache<string>("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<IMemoryCache>();
object? cachedValue = null;

memoryCacheMock
.Setup(m => m.TryGetValue(It.IsAny<object>(), out cachedValue))
.Returns(false);

var cacheProvider = new CacheProvider(memoryCacheMock.Object);

// Act
var result = cacheProvider.GetFromCache<string>("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<IMemoryCache>();
var cacheMockSetup = new Mock<ICacheEntry>();

memoryCacheMock
.Setup(m => m.CreateEntry(It.IsAny<object>()))
.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<IMemoryCache>();
var cacheProvider = new CacheProvider(memoryCacheMock.Object);

// Act
cacheProvider.ClearCache("test-key");

// Assert
memoryCacheMock.Verify(m => m.Remove("test-key"), Times.Once);
}
Expand Down
8 changes: 4 additions & 4 deletions InMemoryCachingSample.Tests/CacheServiceTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -17,11 +17,11 @@ public CacheServiceTests()
{
_cacheProviderMock = new Mock<ICacheProvider>();
_cacheService = new CacheService(_cacheProviderMock.Object);

_sampleUsers = new List<User>
{
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" }
};
}

Expand Down Expand Up @@ -50,4 +50,4 @@ public void ClearCache_RemovesUserCacheKey()
// Assert
_cacheProviderMock.Verify(c => c.ClearCache(CacheKeys.Users), Times.Once);
}
}
}
14 changes: 7 additions & 7 deletions InMemoryCachingSample.Tests/CachedUserServiceTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -20,11 +20,11 @@ public CachedUserServiceTests()
_usersServiceMock = new Mock<IUsersService>();
_cacheProviderMock = new Mock<ICacheProvider>();
_cachedUserService = new CachedUserService(_usersServiceMock.Object, _cacheProviderMock.Object);

_sampleUsers = new List<User>
{
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" }
};
}

Expand Down Expand Up @@ -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<IEnumerable<User>>(),
CacheKeys.Users,
It.IsAny<IEnumerable<User>>(),
It.IsAny<MemoryCacheEntryOptions>()
),
),
Times.Once
);
}
}
}
12 changes: 6 additions & 6 deletions InMemoryCachingSample.Tests/HttpClientTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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]
Expand Down Expand Up @@ -73,4 +73,4 @@ public async Task Get_WhenApiReturnsError_ThrowsHttpRequestException()
// Act & Assert
await Assert.ThrowsAsync<HttpRequestException>(() => client.Get());
}
}
}
Original file line number Diff line number Diff line change
@@ -1,15 +1,15 @@
<Project Sdk="Microsoft.NET.Sdk">

<PropertyGroup>
<TargetFrameworks>net8.0;net9.0;net10.0</TargetFrameworks>
<TargetFramework>net10.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>

<IsPackable>false</IsPackable>
</PropertyGroup>

<ItemGroup>
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="18.6.0" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="18.9.0" />
<PackageReference Include="Moq" Version="4.20.72" />
<PackageReference Include="xunit" Version="2.9.3" />
<PackageReference Include="xunit.runner.visualstudio" Version="3.1.5">
Expand Down
10 changes: 0 additions & 10 deletions InMemoryCachingSample.Tests/UnitTest1.cs

This file was deleted.

6 changes: 3 additions & 3 deletions InMemoryCachingSample/Controllers/HomeController.cs
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ public class HomeController(ILogger<HomeController> 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();
Expand All @@ -25,7 +25,7 @@ public async Task<IActionResult> CacheUser()

return RedirectToAction(nameof(Index));
}

public IActionResult ClearCache()
{
_cacheService.ClearCache();
Expand All @@ -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 });
}
}
}
2 changes: 1 addition & 1 deletion InMemoryCachingSample/InMemoryCachingSample.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
xsi:noNamespaceSchemaLocation="InMemoryCachingSample.xsd" Sdk="Microsoft.NET.Sdk.Web">

<PropertyGroup>
<TargetFrameworks>net8.0;net9.0;net10.0</TargetFrameworks>
<TargetFramework>net10.0</TargetFramework>
<Nullable>enable</Nullable>
<ImplicitUsings>enable</ImplicitUsings>
</PropertyGroup>
Expand Down
4 changes: 2 additions & 2 deletions InMemoryCachingSample/Infrastructure/CacheProvider.cs
Original file line number Diff line number Diff line change
Expand Up @@ -8,10 +8,10 @@ public interface ICacheProvider
}

public class CacheProvider(IMemoryCache cache) : ICacheProvider
{
{
private readonly IMemoryCache _cache = cache;

public T? GetFromCache<T>(string key) where T : class
public T? GetFromCache<T>(string key) where T : class
{
_cache.TryGetValue(key, out T? cachedResponse);
return cachedResponse;
Expand Down
18 changes: 6 additions & 12 deletions InMemoryCachingSample/Infrastructure/HttpClient.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2,35 +2,29 @@

namespace InMemoryCachingSample.Infrastructure;

public class UserResponse
{
public User[] Data { get; set; } = [];
}

public interface IHttpClient
{
Task<IEnumerable<User>> Get();
}

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<IEnumerable<User>> Get()
public async Task<IEnumerable<User>> Get()
{
var client = _clientFactory.CreateClient();

try
{
var usersResponse = await client.GetFromJsonAsync<UserResponse>(API_URL);
return usersResponse?.Data ?? [];
return await client.GetFromJsonAsync<User[]>(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);
}
}
}
}
11 changes: 8 additions & 3 deletions InMemoryCachingSample/Models/User.cs
Original file line number Diff line number Diff line change
@@ -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;
}
[JsonPropertyName("id")]
public int Id { get; set; }

[JsonPropertyName("email")]
public string Email { get; set; } = string.Empty;
}
2 changes: 1 addition & 1 deletion InMemoryCachingSample/Program.cs
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@
// Use fully qualified name to resolve ambiguity
builder.Services.AddScoped<IHttpClient, InMemoryCachingSample.Infrastructure.HttpClient>();
// Register IUsersService implementation with decorator pattern
builder.Services.AddScoped<IUsersService>(sp =>
builder.Services.AddScoped<IUsersService>(sp =>
{
var usersService = sp.GetRequiredService<UsersService>();
var cacheProvider = sp.GetRequiredService<ICacheProvider>();
Expand Down
2 changes: 1 addition & 1 deletion InMemoryCachingSample/Services/CacheService.cs
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ public class CacheService(ICacheProvider cacheProvider) : ICacheService
{
private readonly ICacheProvider _cacheProvider = cacheProvider;

public IEnumerable<User>? GetCachedUser()
public IEnumerable<User>? GetCachedUser()
{
return _cacheProvider.GetFromCache<IEnumerable<User>>(CacheKeys.Users);
}
Expand Down
8 changes: 4 additions & 4 deletions InMemoryCachingSample/Services/CachedUserService.cs
Original file line number Diff line number Diff line change
Expand Up @@ -17,11 +17,11 @@ public class CachedUserService(IUsersService usersService, ICacheProvider cacheP

private static readonly SemaphoreSlim GetUsersSemaphore = new(1, 1);

public async Task<IEnumerable<User>> GetUsersAsync()
public async Task<IEnumerable<User>> GetUsersAsync()
{
return await GetCachedResponse(CacheKeys.Users, GetUsersSemaphore, _usersService.GetUsersAsync);
}

private async Task<IEnumerable<User>> GetCachedResponse(string cacheKey, SemaphoreSlim semaphore, Func<Task<IEnumerable<User>>> func)
{
var users = _cacheProvider.GetFromCache<IEnumerable<User>>(cacheKey);
Expand All @@ -30,13 +30,13 @@ private async Task<IEnumerable<User>> GetCachedResponse(string cacheKey, Semapho
try
{
await semaphore.WaitAsync();

// Recheck to make sure it didn't populate before entering semaphore
users = _cacheProvider.GetFromCache<IEnumerable<User>>(cacheKey);
if (users != null) return users;

users = await func();

_cacheProvider.SetCache(cacheKey, users, _cacheEntryOptions);
}
finally
Expand Down
Loading
Loading