Implemented hybrid cache for user sessions, api keys and database provided settings. Cleaned up startup and adjusted caching option models for features
This commit is contained in:
@@ -1,6 +1,6 @@
|
||||
using System.Text.Json;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.Extensions.Caching.Memory;
|
||||
using Microsoft.Extensions.Caching.Hybrid;
|
||||
using Microsoft.Extensions.Options;
|
||||
using Moonlight.Api.Configuration;
|
||||
using Moonlight.Api.Database;
|
||||
@@ -12,18 +12,18 @@ public class SettingsService
|
||||
{
|
||||
private readonly DatabaseRepository<SettingsOption> Repository;
|
||||
private readonly IOptions<SettingsOptions> Options;
|
||||
private readonly IMemoryCache Cache;
|
||||
private readonly HybridCache HybridCache;
|
||||
|
||||
private const string CacheKey = "Moonlight.Api.SettingsService.{0}";
|
||||
|
||||
public SettingsService(
|
||||
DatabaseRepository<SettingsOption> repository,
|
||||
IOptions<SettingsOptions> options,
|
||||
IMemoryCache cache
|
||||
)
|
||||
HybridCache hybridCache
|
||||
)
|
||||
{
|
||||
Repository = repository;
|
||||
Cache = cache;
|
||||
HybridCache = hybridCache;
|
||||
Options = options;
|
||||
}
|
||||
|
||||
@@ -31,24 +31,26 @@ public class SettingsService
|
||||
{
|
||||
var cacheKey = string.Format(CacheKey, key);
|
||||
|
||||
if (Cache.TryGetValue<string>(cacheKey, out var value))
|
||||
return JsonSerializer.Deserialize<T>(value!);
|
||||
|
||||
value = await Repository
|
||||
.Query()
|
||||
.Where(x => x.Key == key)
|
||||
.Select(o => o.ValueJson)
|
||||
.FirstOrDefaultAsync();
|
||||
|
||||
if(string.IsNullOrEmpty(value))
|
||||
return default;
|
||||
|
||||
Cache.Set(
|
||||
var value = await HybridCache.GetOrCreateAsync<string?>(
|
||||
cacheKey,
|
||||
value,
|
||||
TimeSpan.FromMinutes(Options.Value.CacheMinutes)
|
||||
async ct =>
|
||||
{
|
||||
return await Repository
|
||||
.Query()
|
||||
.Where(x => x.Key == key)
|
||||
.Select(o => o.ValueJson)
|
||||
.FirstOrDefaultAsync(cancellationToken: ct);
|
||||
},
|
||||
new HybridCacheEntryOptions()
|
||||
{
|
||||
LocalCacheExpiration = Options.Value.LookupL1CacheTime,
|
||||
Expiration = Options.Value.LookupL2CacheTime
|
||||
}
|
||||
);
|
||||
|
||||
if (string.IsNullOrWhiteSpace(value))
|
||||
return default;
|
||||
|
||||
return JsonSerializer.Deserialize<T>(value);
|
||||
}
|
||||
|
||||
@@ -77,7 +79,7 @@ public class SettingsService
|
||||
|
||||
await Repository.AddAsync(option);
|
||||
}
|
||||
|
||||
Cache.Remove(cacheKey);
|
||||
|
||||
await HybridCache.RemoveAsync(cacheKey);
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
using System.Security.Claims;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.Extensions.Caching.Memory;
|
||||
using Microsoft.Extensions.Caching.Hybrid;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Microsoft.Extensions.Options;
|
||||
using Moonlight.Api.Configuration;
|
||||
@@ -14,10 +14,10 @@ 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 readonly IOptions<UserOptions> Options;
|
||||
private readonly IEnumerable<IUserAuthHook> Hooks;
|
||||
private readonly HybridCache HybridCache;
|
||||
|
||||
private const string UserIdClaim = "UserId";
|
||||
private const string IssuedAtClaim = "IssuedAt";
|
||||
@@ -27,15 +27,16 @@ public class UserAuthService
|
||||
public UserAuthService(
|
||||
DatabaseRepository<User> userRepository,
|
||||
ILogger<UserAuthService> logger,
|
||||
IMemoryCache cache, IOptions<SessionOptions> options,
|
||||
IEnumerable<IUserAuthHook> hooks
|
||||
IOptions<UserOptions> options,
|
||||
IEnumerable<IUserAuthHook> hooks,
|
||||
HybridCache hybridCache
|
||||
)
|
||||
{
|
||||
UserRepository = userRepository;
|
||||
Logger = logger;
|
||||
Cache = cache;
|
||||
Options = options;
|
||||
Hooks = hooks;
|
||||
HybridCache = hybridCache;
|
||||
}
|
||||
|
||||
public async Task<bool> SyncAsync(ClaimsPrincipal? principal)
|
||||
@@ -80,8 +81,8 @@ public class UserAuthService
|
||||
|
||||
foreach (var hook in Hooks)
|
||||
{
|
||||
// Run every hook and if any returns false we return false as well
|
||||
if(!await hook.SyncAsync(principal, user))
|
||||
// Run every hook, and if any returns false, we return false as well
|
||||
if (!await hook.SyncAsync(principal, user))
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -101,32 +102,29 @@ public class UserAuthService
|
||||
|
||||
var cacheKey = string.Format(CacheKeyPattern, userId);
|
||||
|
||||
if (!Cache.TryGetValue<UserSession>(cacheKey, out var user))
|
||||
{
|
||||
user = await UserRepository
|
||||
.Query()
|
||||
.AsNoTracking()
|
||||
.Where(u => u.Id == userId)
|
||||
.Select(u => new UserSession(
|
||||
u.InvalidateTimestamp,
|
||||
u.RoleMemberships.SelectMany(x => x.Role.Permissions).ToArray())
|
||||
)
|
||||
.FirstOrDefaultAsync();
|
||||
var user = await HybridCache.GetOrCreateAsync<UserSession?>(
|
||||
cacheKey,
|
||||
async ct =>
|
||||
{
|
||||
return await UserRepository
|
||||
.Query()
|
||||
.AsNoTracking()
|
||||
.Where(u => u.Id == userId)
|
||||
.Select(u => new UserSession(
|
||||
u.InvalidateTimestamp,
|
||||
u.RoleMemberships.SelectMany(x => x.Role.Permissions).ToArray())
|
||||
)
|
||||
.FirstOrDefaultAsync(cancellationToken: ct);
|
||||
},
|
||||
new HybridCacheEntryOptions()
|
||||
{
|
||||
LocalCacheExpiration = Options.Value.ValidationCacheL1Expiry,
|
||||
Expiration = Options.Value.ValidationCacheL2Expiry
|
||||
}
|
||||
);
|
||||
|
||||
if (user == null)
|
||||
return false;
|
||||
|
||||
Cache.Set(
|
||||
cacheKey,
|
||||
user,
|
||||
TimeSpan.FromMinutes(Options.Value.ValidationCacheMinutes)
|
||||
);
|
||||
}
|
||||
else
|
||||
{
|
||||
if (user == null)
|
||||
return false;
|
||||
}
|
||||
if (user == null)
|
||||
return false;
|
||||
|
||||
var issuedAtString = principal.FindFirstValue(IssuedAtClaim);
|
||||
|
||||
@@ -146,11 +144,11 @@ public class UserAuthService
|
||||
principal.Identities.First().AddClaims(
|
||||
user.Permissions.Select(x => new Claim(Permissions.ClaimType, x))
|
||||
);
|
||||
|
||||
|
||||
foreach (var hook in Hooks)
|
||||
{
|
||||
// Run every hook and if any returns false we return false as well
|
||||
if(!await hook.ValidateAsync(principal, userId))
|
||||
// Run every hook, and if any returns false we return false as well
|
||||
if (!await hook.ValidateAsync(principal, userId))
|
||||
return false;
|
||||
}
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.Extensions.Caching.Memory;
|
||||
using Microsoft.Extensions.Caching.Hybrid;
|
||||
using Moonlight.Api.Database;
|
||||
using Moonlight.Api.Database.Entities;
|
||||
using Moonlight.Api.Interfaces;
|
||||
@@ -10,13 +10,17 @@ public class UserDeletionService
|
||||
{
|
||||
private readonly DatabaseRepository<User> Repository;
|
||||
private readonly IEnumerable<IUserDeletionHook> Hooks;
|
||||
private readonly IMemoryCache Cache;
|
||||
private readonly HybridCache HybridCache;
|
||||
|
||||
public UserDeletionService(DatabaseRepository<User> repository, IEnumerable<IUserDeletionHook> hooks, IMemoryCache cache)
|
||||
public UserDeletionService(
|
||||
DatabaseRepository<User> repository,
|
||||
IEnumerable<IUserDeletionHook> hooks,
|
||||
HybridCache hybridCache
|
||||
)
|
||||
{
|
||||
Repository = repository;
|
||||
Hooks = hooks;
|
||||
Cache = cache;
|
||||
HybridCache = hybridCache;
|
||||
}
|
||||
|
||||
public async Task<UserDeletionValidationResult> ValidateAsync(int userId)
|
||||
@@ -54,7 +58,8 @@ public class UserDeletionService
|
||||
await hook.ExecuteAsync(user);
|
||||
|
||||
await Repository.RemoveAsync(user);
|
||||
Cache.Remove(string.Format(UserAuthService.CacheKeyPattern, userId));
|
||||
|
||||
await HybridCache.RemoveAsync(string.Format(UserAuthService.CacheKeyPattern, user.Id));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.Extensions.Caching.Memory;
|
||||
using Microsoft.Extensions.Caching.Hybrid;
|
||||
using Moonlight.Api.Database;
|
||||
using Moonlight.Api.Database.Entities;
|
||||
using Moonlight.Api.Interfaces;
|
||||
@@ -10,17 +10,17 @@ public class UserLogoutService
|
||||
{
|
||||
private readonly DatabaseRepository<User> Repository;
|
||||
private readonly IEnumerable<IUserLogoutHook> Hooks;
|
||||
private readonly IMemoryCache Cache;
|
||||
private readonly HybridCache HybridCache;
|
||||
|
||||
public UserLogoutService(
|
||||
DatabaseRepository<User> repository,
|
||||
IEnumerable<IUserLogoutHook> hooks,
|
||||
IMemoryCache cache
|
||||
HybridCache hybridCache
|
||||
)
|
||||
{
|
||||
Repository = repository;
|
||||
Hooks = hooks;
|
||||
Cache = cache;
|
||||
HybridCache = hybridCache;
|
||||
}
|
||||
|
||||
public async Task LogoutAsync(int userId)
|
||||
@@ -28,16 +28,16 @@ public class UserLogoutService
|
||||
var user = await Repository
|
||||
.Query()
|
||||
.FirstOrDefaultAsync(x => x.Id == userId);
|
||||
|
||||
if(user == null)
|
||||
|
||||
if (user == null)
|
||||
throw new AggregateException($"User with id {userId} not found");
|
||||
|
||||
foreach (var hook in Hooks)
|
||||
await hook.ExecuteAsync(user);
|
||||
|
||||
|
||||
user.InvalidateTimestamp = DateTimeOffset.UtcNow;
|
||||
await Repository.UpdateAsync(user);
|
||||
|
||||
Cache.Remove(string.Format(UserAuthService.CacheKeyPattern, userId));
|
||||
|
||||
await HybridCache.RemoveAsync(string.Format(UserAuthService.CacheKeyPattern, user.Id));
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user