Added session caching for user validation to reduce db calls and introduced configurable session options.

This commit is contained in:
2026-01-16 09:19:15 +01:00
parent 10cd0f0b09
commit bee381702b
3 changed files with 50 additions and 9 deletions

View File

@@ -0,0 +1,6 @@
namespace Moonlight.Api.Configuration;
public class SessionOptions
{
public int ValidationCacheMinutes { get; set; } = 3;
}

View File

@@ -1,6 +1,9 @@
using System.Security.Claims;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Caching.Memory;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
using Moonlight.Api.Configuration;
using Moonlight.Api.Database;
using Moonlight.Api.Database.Entities;
@@ -9,15 +12,22 @@ namespace Moonlight.Api.Services;
public class UserAuthService
{
private readonly DatabaseRepository<User> UserRepository;
private readonly IMemoryCache Cache;
private readonly ILogger<UserAuthService> Logger;
private readonly IOptions<SessionOptions> Options;
private const string UserIdClaim = "UserId";
private const string IssuedAtClaim = "IssuedAt";
public UserAuthService(DatabaseRepository<User> userRepository, ILogger<UserAuthService> logger)
public UserAuthService(
DatabaseRepository<User> userRepository,
ILogger<UserAuthService> logger,
IMemoryCache cache, IOptions<SessionOptions> options)
{
UserRepository = userRepository;
Logger = logger;
Cache = cache;
Options = options;
}
public async Task<bool> SyncAsync(ClaimsPrincipal? principal)
@@ -75,13 +85,31 @@ public class UserAuthService
if (!int.TryParse(userIdString, out var userId))
return false;
var user = await UserRepository
.Query()
.AsNoTracking()
.FirstOrDefaultAsync(user => user.Id == userId);
var cacheKey = $"Moonlight.{nameof(UserAuthService)}.{nameof(ValidateAsync)}-{userId}";
if (user == null)
return false;
if (!Cache.TryGetValue<UserSession>(cacheKey, out var user))
{
user = await UserRepository
.Query()
.AsNoTracking()
.Where(u => u.Id == userId)
.Select(u => new UserSession(u.InvalidateTimestamp))
.FirstOrDefaultAsync();
if (user == null)
return false;
Cache.Set(
cacheKey,
user,
TimeSpan.FromMinutes(Options.Value.ValidationCacheMinutes)
);
}
else
{
if (user == null)
return false;
}
var issuedAtString = principal.FindFirstValue(IssuedAtClaim);
@@ -90,10 +118,14 @@ public class UserAuthService
var issuedAt = DateTimeOffset.FromUnixTimeSeconds(issuedAtUnix).ToUniversalTime();
// If the issued at timestamp is greater than the token validation timestamp
// everything is fine. If not it means that the token should be invalidated
// If the issued at timestamp is greater than the token validation timestamp,
// everything is fine. If not, it means that the token should be invalidated
// as it is too old
return issuedAt > user.InvalidateTimestamp;
}
// A small model which contains data queried per session validation after the defined cache time.
// Used for projection
private record UserSession(DateTimeOffset InvalidateTimestamp);
}

View File

@@ -29,6 +29,9 @@ public partial class Startup
builder.Services.AddSingleton<DiagnoseService>();
builder.Services.AddSingleton<IDiagnoseProvider, UpdateDiagnoseProvider>();
builder.Services.AddMemoryCache();
builder.Services.AddOptions<SessionOptions>().BindConfiguration("Moonlight:Session");
}
private static void UseBase(WebApplication application)