Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Cache API responses in AltinnRegisterService #612

Open
wants to merge 8 commits into
base: main
Choose a base branch
from
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
using Moq;
using Microsoft.Extensions.Caching.Distributed;
using Altinn.Correspondence.Common.Helpers;
using System.Text.Json;
using System.Text;

namespace Altinn.Correspondence.Tests.TestingUtility
{
public class CacheHelpersTests
{
[Fact]
public async Task StoreObjectInCacheAsync_ShouldStoreSerializedObjectInCache()
{
// Arrange
var key = "myKey";
Dictionary<string, object> value = new()
{
{ "Name", "John" },
{ "Age", 30 }
};
var cacheOptions = new DistributedCacheEntryOptions();
var cancellationToken = CancellationToken.None;
var serializedDataString = JsonSerializer.Serialize(value);
var mockCache = new Mock<IDistributedCache>();

mockCache.Setup(cache => cache.SetAsync(
key,
It.Is<byte[]>(bytes => bytes != null && bytes.Length > 0),
cacheOptions,
cancellationToken
)).Returns(Task.CompletedTask);

// Act
await CacheHelpers.StoreObjectInCacheAsync(key, value, mockCache.Object, cacheOptions, cancellationToken);

// Assert
mockCache.Verify(cache => cache.SetAsync(
key,
It.Is<byte[]>(bytes => bytes.SequenceEqual(Encoding.UTF8.GetBytes(serializedDataString))),
cacheOptions,
cancellationToken),
Times.Once);
}

[Fact]
public async Task GetObjectFromCacheAsync_ShouldReturnDeserializedObject_WhenDataIsFound()
{
// Arrange
var key = "myKey";
string storedValue = "John";
var serializedData = JsonSerializer.Serialize(storedValue);
var cancellationToken = CancellationToken.None;
var mockCache = new Mock<IDistributedCache>();

mockCache.Setup(cache => cache.GetAsync(key, cancellationToken))
.ReturnsAsync(Encoding.UTF8.GetBytes(serializedData));

// Act
var result = await CacheHelpers.GetObjectFromCacheAsync<string>(key, mockCache.Object, cancellationToken);

// Assert
Assert.NotNull(result);
Assert.Equal(storedValue, result);
}

[Fact]
public async Task GetObjectFromCacheAsync_ShouldReturnNull_WhenDataIsNotFound()
{
// Arrange
var key = "unknownKey";
var cancellationToken = CancellationToken.None;
var mockCache = new Mock<IDistributedCache>();

mockCache.Setup(cache => cache.GetAsync(key, cancellationToken))
.ReturnsAsync(Array.Empty<byte>());

// Act
var result = await CacheHelpers.GetObjectFromCacheAsync<object>(key, mockCache.Object, cancellationToken);

// Assert
Assert.Null(result);
}
}
}
24 changes: 24 additions & 0 deletions src/Altinn.Correspondence.Common/Helpers/CacheHelpers.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
using System.Text.Json;
using Microsoft.Extensions.Caching.Distributed;

namespace Altinn.Correspondence.Common.Helpers
{
public static class CacheHelpers
{
public static async Task StoreObjectInCacheAsync<T>(string key, T value, IDistributedCache cache, DistributedCacheEntryOptions cacheOptions, CancellationToken cancellationToken = default)
{
string serializedDataString = JsonSerializer.Serialize(value);
await cache.SetStringAsync(key, serializedDataString, cacheOptions, cancellationToken);
}

public static async Task<T?> GetObjectFromCacheAsync<T>(string key, IDistributedCache cache, CancellationToken cancellationToken = default)
{
string? cachedDataString = await cache.GetStringAsync(key, cancellationToken);
if (!string.IsNullOrEmpty(cachedDataString))
{
return JsonSerializer.Deserialize<T?>(cachedDataString);
}
return default;
axely123 marked this conversation as resolved.
Show resolved Hide resolved
}
}
}
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
using Altinn.Correspondence.Common.Constants;
using Altinn.Correspondence.Common.Helpers;
using Altinn.Correspondence.Core.Models.Entities;
using Altinn.Correspondence.Core.Models.Enums;
using Altinn.Correspondence.Core.Options;
Expand Down Expand Up @@ -41,15 +42,15 @@ public async Task<List<PartyWithSubUnits>> GetAuthorizedParties(Party partyToReq
{
string cacheKey = $"AuthorizedParties_{partyToRequestFor.PartyId}";
try {
string? cachedDataString = await _cache.GetStringAsync(cacheKey, cancellationToken);
if (!string.IsNullOrEmpty(cachedDataString))
var cachedParties = await CacheHelpers.GetObjectFromCacheAsync<List<PartyWithSubUnits>>(cacheKey, _cache, cancellationToken);
if (cachedParties != null)
{
return JsonSerializer.Deserialize<List<PartyWithSubUnits>>(cachedDataString) ?? new List<PartyWithSubUnits>();
return cachedParties;
}
}
catch (Exception ex)
{
_logger.LogWarning(ex, "Error retrieving authorized parties from cache. Proceeding with API call.");
_logger.LogWarning(ex, "Error retrieving authorized parties from cache in Access Management Service.");
}

AuthorizedPartiesRequest request = new(partyToRequestFor);
Expand Down Expand Up @@ -90,12 +91,11 @@ public async Task<List<PartyWithSubUnits>> GetAuthorizedParties(Party partyToReq
}

try {
string serializedDataString = JsonSerializer.Serialize(parties);
await _cache.SetStringAsync(cacheKey, serializedDataString, _cacheOptions, cancellationToken);
await CacheHelpers.StoreObjectInCacheAsync(cacheKey, parties, _cache, _cacheOptions, cancellationToken);
}
catch (Exception ex)
{
_logger.LogWarning(ex, "Error saving response content from Authorization GetAuthorizedParties to cache.");
_logger.LogWarning(ex, "Error storing response content to cache when looking up authorized parties in Access Management Service.");
}

return parties;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
using Altinn.Correspondence.Core.Options;
using Altinn.Correspondence.Core.Services;
using Altinn.Platform.Register.Models;
using Microsoft.Extensions.Caching.Distributed;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
using Party = Altinn.Correspondence.Core.Models.Entities.Party;
Expand All @@ -13,12 +14,19 @@ public class AltinnRegisterService : IAltinnRegisterService
{
private readonly HttpClient _httpClient;
private readonly ILogger<AltinnRegisterService> _logger;
private readonly IDistributedCache _cache;
private readonly DistributedCacheEntryOptions _cacheOptions;

public AltinnRegisterService(HttpClient httpClient, IOptions<AltinnOptions> altinnOptions, ILogger<AltinnRegisterService> logger)
public AltinnRegisterService(HttpClient httpClient, IOptions<AltinnOptions> altinnOptions, ILogger<AltinnRegisterService> logger, IDistributedCache cache)
{
httpClient.DefaultRequestHeaders.Add("Ocp-Apim-Subscription-Key", altinnOptions.Value.PlatformSubscriptionKey);
_httpClient = httpClient;
_logger = logger;
_cache = cache;
_cacheOptions = new DistributedCacheEntryOptions
{
AbsoluteExpirationRelativeToNow = TimeSpan.FromHours(24)
};
}

public async Task<int?> LookUpPartyId(string identificationId, CancellationToken cancellationToken = default)
Expand All @@ -35,6 +43,20 @@ public AltinnRegisterService(HttpClient httpClient, IOptions<AltinnOptions> alti

public async Task<Party?> LookUpPartyByPartyId(int partyId, CancellationToken cancellationToken = default)
{
string cacheKey = $"PartyByPartyId_{partyId}";
try
{
var cachedParty = await CacheHelpers.GetObjectFromCacheAsync<Party>(cacheKey, _cache, cancellationToken);
if (cachedParty != null)
{
return cachedParty;
}
}
catch (Exception ex)
{
_logger.LogWarning(ex, "Error retrieving Party from cache when looking up Party in Altinn Register Service.");
}

if (partyId <= 0)
{
_logger.LogError("partyId is not a valid number.");
Expand All @@ -54,10 +76,34 @@ public AltinnRegisterService(HttpClient httpClient, IOptions<AltinnOptions> alti
return null;
}

try
{
await CacheHelpers.StoreObjectInCacheAsync(cacheKey, party, _cache, _cacheOptions, cancellationToken);
}
catch (Exception ex)
{
_logger.LogWarning(ex, "Error storing response content to cache when looking up Party in Altinn Register Service.");
}

return party;
}

public async Task<Party?> LookUpPartyById(string identificationId, CancellationToken cancellationToken = default)
{
string cacheKey = $"PartyById_{identificationId}";
try
{
var cachedParty = await CacheHelpers.GetObjectFromCacheAsync<Party>(cacheKey, _cache, cancellationToken);
if (cachedParty != null)
{
return cachedParty;
}
}
catch (Exception ex)
{
_logger.LogWarning(ex, "Error retrieving organization from cache when looking up organization in Altinn Register Service.");
}

identificationId = identificationId.WithoutPrefix();

var partyLookup = new PartyLookup()
Expand All @@ -78,10 +124,35 @@ public AltinnRegisterService(HttpClient httpClient, IOptions<AltinnOptions> alti
_logger.LogError("Unexpected json response when looking up organization in Altinn Register");
return null;
}

try
{
await CacheHelpers.StoreObjectInCacheAsync(cacheKey, party, _cache, _cacheOptions, cancellationToken);
}
catch (Exception ex)
{
_logger.LogWarning(ex, "Error storing response content to cache when looking up organization in Altinn Register Service.");
}

return party;
}

public async Task<List<Party>?> LookUpPartiesByIds(List<string> identificationIds, CancellationToken cancellationToken = default)
{
string cacheKey = $"PartiesByIds_{string.Join("_", identificationIds).GetHashCode()}";
try
{
var cachedParty = await CacheHelpers.GetObjectFromCacheAsync<List<Party>>(cacheKey, _cache, cancellationToken);
if (cachedParty != null)
{
return cachedParty;
}
}
catch (Exception ex)
{
_logger.LogWarning(ex, "Error retrieving party names from cache when looking up party names in Altinn Register Service.");
}

var organizations = identificationIds.Where(x => x.IsOrganizationNumber()).Select(x => new PartyLookup() { OrgNo = x }).ToList();
var socialSecurityNumbers = identificationIds.Where(x => x.IsSocialSecurityNumber()).Select(x => new PartyLookup() { Ssn = x }).ToList();
var partyLookup = new PartyNamesLookup()
Expand All @@ -101,11 +172,22 @@ public AltinnRegisterService(HttpClient httpClient, IOptions<AltinnOptions> alti
return null;
}

return parties.PartyNames.Select(x => new Party
List<Party> partyNames = parties.PartyNames.Select(x => new Party
{
OrgNumber = x.OrgNo,
SSN = x.Ssn,
Name = CultureInfo.CurrentCulture.TextInfo.ToTitleCase(x.Name.ToLower())
}).ToList();

try
{
await CacheHelpers.StoreObjectInCacheAsync(cacheKey, partyNames, _cache, _cacheOptions, cancellationToken);
}
catch (Exception ex)
{
_logger.LogWarning(ex, "Error storing response content to cache when looking up party names in Altinn Register Service.");
}

return partyNames;
}
}
Loading