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

✨ Add caching service and cached leaderboard, and user rankings #1153

Merged
merged 6 commits into from
Jan 21, 2025
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
Original file line number Diff line number Diff line change
Expand Up @@ -12,13 +12,16 @@ public class ClaimAchievementForUserCommand : IRequest<ClaimAchievementResult>
public class ClaimAchievementForUserCommandHandler : IRequestHandler<ClaimAchievementForUserCommand, ClaimAchievementResult>
{
private readonly IApplicationDbContext _context;
private readonly ICacheService _cacheService;
private readonly ILogger<ClaimAchievementForUserCommand> _logger;

public ClaimAchievementForUserCommandHandler(
IApplicationDbContext context,
ICacheService cacheService,
ILogger<ClaimAchievementForUserCommand> logger)
{
_context = context;
_cacheService = cacheService;
_logger = logger;
}

Expand All @@ -31,7 +34,7 @@ public async Task<ClaimAchievementResult> Handle(ClaimAchievementForUserCommand

if (achievement == null)
{
_logger.LogError("Achievement was not found for code: {0}", request.Code);
_logger.LogError("Achievement was not found for code: {AchievementCode}", request.Code);
return new ClaimAchievementResult
{
status = ClaimAchievementStatus.NotFound
Expand All @@ -45,7 +48,7 @@ public async Task<ClaimAchievementResult> Handle(ClaimAchievementForUserCommand

if (user == null)
{
_logger.LogError("User was not found for id: {0}", request.UserId);
_logger.LogError("User was not found for id: {UserId}", request.UserId);
return new ClaimAchievementResult
{
status = ClaimAchievementStatus.Error
Expand All @@ -59,7 +62,7 @@ public async Task<ClaimAchievementResult> Handle(ClaimAchievementForUserCommand

if (achievementCheck != null)
{
_logger.LogError("User already has achievement: {0}", request.Code);
_logger.LogError("User already has achievement: {AchievementCode}", request.Code);
return new ClaimAchievementResult
{
status = ClaimAchievementStatus.Duplicate
Expand All @@ -68,19 +71,20 @@ public async Task<ClaimAchievementResult> Handle(ClaimAchievementForUserCommand

try
{
await _context
.UserAchievements
.AddAsync(new UserAchievement
_context.UserAchievements
.Add(new UserAchievement
{
UserId = user.Id,
AchievementId = achievement.Id
}, cancellationToken);
});

await _context.SaveChangesAsync(cancellationToken);

_cacheService.Remove(CacheTags.UpdatedRanking);
}
catch (Exception e)
{
_logger.LogError(e.Message, e);
_logger.LogError(e, "Unable to claim achievement {AchievementId} for User {UserId} with error {ErrorMessage}", achievement.Id, user.Id, e.Message);
return new ClaimAchievementResult
{
status = ClaimAchievementStatus.Error
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,12 +12,14 @@ public class ClaimFormCompletedAchievementCommand : IRequest
public class ClaimFormCompletedAchievementCommandHandler : IRequestHandler<ClaimFormCompletedAchievementCommand>
{
private readonly IEmailService _emailService;
private readonly ICacheService _cacheService;
private readonly IApplicationDbContext _dbContext;

public ClaimFormCompletedAchievementCommandHandler(IApplicationDbContext dbContext, IEmailService emailService)
public ClaimFormCompletedAchievementCommandHandler(IApplicationDbContext dbContext, IEmailService emailService, ICacheService cacheService)
{
_dbContext = dbContext;
_emailService = emailService;
_cacheService = cacheService;
}

public async Task Handle(ClaimFormCompletedAchievementCommand request, CancellationToken cancellationToken)
Expand Down Expand Up @@ -76,6 +78,8 @@ await _emailService.SendFormCompletionCreateAccountEmail(request.Email.ToLower()
}

await _dbContext.SaveChangesAsync(cancellationToken);

_cacheService.Remove(CacheTags.UpdatedRanking);
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -10,14 +10,16 @@ public sealed class ClaimSocialMediaAchievementForUserCommandHandler : IRequestH
private readonly IApplicationDbContext _context;
private readonly ICurrentUserService _currentUserService;
private readonly IUserService _userService;
private readonly ICacheService _cacheService;
private readonly IDateTime _dateTimeService;

public ClaimSocialMediaAchievementForUserCommandHandler(IApplicationDbContext context, ICurrentUserService currentUserService, IUserService userService, IDateTime dateTimeService)
public ClaimSocialMediaAchievementForUserCommandHandler(IApplicationDbContext context, ICurrentUserService currentUserService, IUserService userService, IDateTime dateTimeService, ICacheService cacheService = null)
{
_context = context;
_currentUserService = currentUserService;
_userService = userService;
_dateTimeService = dateTimeService;
_cacheService = cacheService;
}

public async Task<int> Handle(ClaimSocialMediaAchievementForUserCommand request, CancellationToken cancellationToken)
Expand All @@ -41,8 +43,8 @@ public async Task<int> Handle(ClaimSocialMediaAchievementForUserCommand request,
// in a strange bug. Switching to use the user and
// achievement entities resolved the problem. See:
// https://github.com/SSWConsulting/SSW.Rewards.API/issues/20
var user = await _context.Users.FindAsync(currentUserId);
var achievement = await _context.Achievements.FindAsync(achievementId);
var user = await _context.Users.FindAsync(currentUserId, cancellationToken);
var achievement = await _context.Achievements.FindAsync(achievementId, cancellationToken);

userAchievement = new UserAchievement
{
Expand All @@ -53,6 +55,8 @@ public async Task<int> Handle(ClaimSocialMediaAchievementForUserCommand request,
};
_context.UserAchievements.Add(userAchievement);
await _context.SaveChangesAsync(cancellationToken);

_cacheService.Remove(CacheTags.UpdatedRanking);
}

return userAchievement.Id;
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
using SSW.Rewards.Shared.DTOs.Achievements;
using SSW.Rewards.Application.Achievements.Queries.Common;
using SSW.Rewards.Application.System.Commands.Common;
using SSW.Rewards.Application.System.Commands.Common;
using SSW.Rewards.Shared.DTOs.Achievements;

namespace SSW.Rewards.Application.Achievements.Command.PostAchievement;

Expand All @@ -13,22 +12,25 @@ public class PostAchievementCommandHandler : IRequestHandler<PostAchievementComm
{
private readonly IUserService _userService;
private readonly IApplicationDbContext _context;
private readonly ICacheService _cacheService;
private readonly IMapper _mapper;

public PostAchievementCommandHandler(
IUserService UserService,
IApplicationDbContext context,
ICacheService cacheService,
IMapper mapper)
{
_userService = UserService;
_context = context;
_cacheService = cacheService;
_mapper = mapper;
}

public async Task<ClaimAchievementResult> Handle(PostAchievementCommand request, CancellationToken cancellationToken)
{
var requestedAchievement = await _context
.Achievements
var requestedAchievement = await _context.Achievements
.TagWithContext("GetRelevantAchievement")
.Where(a => a.Code == request.Code)
.FirstOrDefaultAsync(cancellationToken);

Expand All @@ -39,45 +41,51 @@ public async Task<ClaimAchievementResult> Handle(PostAchievementCommand request,
status = ClaimAchievementStatus.NotFound
};
}

var achievementModel = _mapper.Map<AchievementDto>(requestedAchievement);

var userId = await _userService.GetCurrentUserId(cancellationToken);

var userAchievements = await _context
.UserAchievements
var userAchievements = await _context.UserAchievements
.TagWithContext("GetAllUserAchievement")
.Include(x => x.Achievement)
.Where(ua => ua.UserId == userId)
.ToListAsync(cancellationToken);

// check for milestone achievements
if (requestedAchievement.Type == AchievementType.Scanned)
{
var scannedUser = await _context.Users
.TagWithContext("GetScannedUserByAchievementId")
.FirstOrDefaultAsync(u => u.AchievementId == requestedAchievement.Id, cancellationToken);

if (scannedUser == null)
{
var staffMember = await _context.StaffMembers
.TagWithContext("GetScannedStaffMember")
.Include(s => s.StaffAchievement)
.Where(s => s.StaffAchievement != null)
.FirstOrDefaultAsync(s => s.StaffAchievement!.Id == requestedAchievement.Id, cancellationToken);

if (staffMember != null)
{
var staffUser = await _context.Users
.TagWithContext("GetScannedUserByEmail")
.FirstOrDefaultAsync(u => u.Email == staffMember.Email, cancellationToken);

achievementModel.UserId = staffUser?.Id;
}
}
else
{
achievementModel.UserId = scannedUser.Id;
}

if (!userAchievements.Any(ua => ua.Achievement.Name == MilestoneAchievements.MeetSSW))
{
var meetAchievement = await _context.Achievements.FirstOrDefaultAsync(a => a.Name == MilestoneAchievements.MeetSSW, cancellationToken);
var meetAchievement = await _context.Achievements
.TagWithContext("GetMilestone-MeetSSW")
.FirstOrDefaultAsync(a => a.Name == MilestoneAchievements.MeetSSW, cancellationToken);

var userMeetAchievement = new UserAchievement
{
Expand Down Expand Up @@ -117,7 +125,9 @@ public async Task<ClaimAchievementResult> Handle(PostAchievementCommand request,

if (!userAchievements.Any(ua => ua.Achievement.Name == MilestoneAchievements.AttendUG))
{
var ugAchievement = await _context.Achievements.FirstOrDefaultAsync(a => a.Name == MilestoneAchievements.AttendUG, cancellationToken);
var ugAchievement = await _context.Achievements
.TagWithContext("GetMilestone-AttendUG")
.FirstOrDefaultAsync(a => a.Name == MilestoneAchievements.AttendUG, cancellationToken);
milestoneAchievement.Achievement = ugAchievement;
}
break;
Expand All @@ -126,7 +136,9 @@ public async Task<ClaimAchievementResult> Handle(PostAchievementCommand request,

if (!userAchievements.Any(ua => ua.Achievement.Name == MilestoneAchievements.AttendHackday))
{
var hdAchievement = await _context.Achievements.FirstOrDefaultAsync(a => a.Name == MilestoneAchievements.AttendHackday, cancellationToken);
var hdAchievement = await _context.Achievements
.TagWithContext("GetMilestone-AttendHackDay")
.FirstOrDefaultAsync(a => a.Name == MilestoneAchievements.AttendHackday, cancellationToken);
milestoneAchievement.Achievement = hdAchievement;
}
break;
Expand All @@ -135,7 +147,9 @@ public async Task<ClaimAchievementResult> Handle(PostAchievementCommand request,

if (!userAchievements.Any(ua => ua.Achievement.Name == MilestoneAchievements.AttendSuperpowers))
{
var spAchievement = await _context.Achievements.FirstOrDefaultAsync(a => a.Name == MilestoneAchievements.AttendSuperpowers, cancellationToken);
var spAchievement = await _context.Achievements
.TagWithContext("GetMilestone-AttendSuperpowers")
.FirstOrDefaultAsync(a => a.Name == MilestoneAchievements.AttendSuperpowers, cancellationToken);
milestoneAchievement.Achievement = spAchievement;
}
break;
Expand All @@ -144,7 +158,9 @@ public async Task<ClaimAchievementResult> Handle(PostAchievementCommand request,

if (!userAchievements.Any(ua => ua.Achievement.Name == MilestoneAchievements.AttendWorkshop))
{
var wsAchievement = await _context.Achievements.FirstOrDefaultAsync(a => a.Name == MilestoneAchievements.AttendWorkshop, cancellationToken);
var wsAchievement = await _context.Achievements
.TagWithContext("GetMilestone-AttendWorkshop")
.FirstOrDefaultAsync(a => a.Name == MilestoneAchievements.AttendWorkshop, cancellationToken);
milestoneAchievement.Achievement = wsAchievement;
}
break;
Expand All @@ -161,6 +177,8 @@ public async Task<ClaimAchievementResult> Handle(PostAchievementCommand request,

await _context.SaveChangesAsync(cancellationToken);

_cacheService.Remove(CacheTags.UpdatedRanking);

return new ClaimAchievementResult
{
viewModel = achievementModel,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,10 +13,12 @@ public class UpdateAchievementCommand : IRequest<Unit>
public sealed class UpdateAchievementCommandHandler : IRequestHandler<UpdateAchievementCommand, Unit>
{
private readonly IApplicationDbContext _context;
private readonly ICacheService _cacheService;

public UpdateAchievementCommandHandler(IApplicationDbContext context)
public UpdateAchievementCommandHandler(IApplicationDbContext context, ICacheService cacheService)
{
_context = context;
_cacheService = cacheService;
}

public async Task<Unit> Handle(UpdateAchievementCommand request, CancellationToken cancellationToken)
Expand All @@ -34,6 +36,9 @@ public async Task<Unit> Handle(UpdateAchievementCommand request, CancellationTok
achievement.IsMultiscanEnabled = request.IsMultiscanEnabled;

await _context.SaveChangesAsync(cancellationToken);

_cacheService.Remove(CacheTags.UpdatedRanking);

return Unit.Value;
}
}
24 changes: 24 additions & 0 deletions src/Application/Common/Constants/CacheKeys.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
namespace SSW.Rewards.Application.Common.Constants;

public static class CacheKeys
{
public const string Leaderboard = "LeaderboardAllTime";
public const string UserRanking = "UserRankingsAllTime";
}

/// <summary>
/// Used to manage and remove multiple caches relative to their usage.
/// For instance, claiming an achievement impacts leaderboard and rankings while claiming a reward impacts only leaderboard (due to AdminUI).
///
/// As number of cache keys grows, this helps developer to organise cache keys by impact so they can be removed all at once for certain use case.
/// </summary>
public static class CacheTags
{
public static readonly string[] UpdatedOnlyRewards = [CacheKeys.Leaderboard];
public static readonly string[] Rankings = [CacheKeys.UserRanking];

public static readonly string[] UpdatedRanking = [.. UpdatedOnlyRewards, .. Rankings];
public static readonly string[] NewlyUserCreated = [.. UpdatedRanking];

public static readonly string[] AllStatic = [.. UpdatedRanking];
}
9 changes: 9 additions & 0 deletions src/Application/Common/Interfaces/ICacheService.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@

namespace SSW.Rewards.Application.Common.Interfaces;

public interface ICacheService
{
Task<TItem> GetOrAddAsync<TItem>(string cacheKey, Func<Task<TItem>> factory);
void Remove(string cacheKey);
void Remove(params string[] cacheKeys);
}
8 changes: 8 additions & 0 deletions src/Application/Common/Options/CacheOptions.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
namespace SSW.Rewards.Application.Common.Options;

public class CacheOptions
{
public bool Enabled { get; set; }
public TimeSpan DefaultExpiration { get; set; }
public Dictionary<string, TimeSpan> OverrideExpiration { get; set; } = new();
}
1 change: 1 addition & 0 deletions src/Application/GlobalUsings.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
global using FluentValidation;
global using MediatR;
global using Microsoft.EntityFrameworkCore;
global using SSW.Rewards.Application.Common.Constants;
global using SSW.Rewards.Application.Common.Extensions;
global using SSW.Rewards.Application.Common.Interfaces;
global using SSW.Rewards.Domain.Entities;
Expand Down
Loading
Loading