From 741a60adc653739545c00bf6bc15315241a33a01 Mon Sep 17 00:00:00 2001 From: ChiaraBm Date: Thu, 12 Feb 2026 15:29:35 +0100 Subject: [PATCH] Implemented hybrid cache for user sessions, api keys and database provided settings. Cleaned up startup and adjusted caching option models for features --- .gitignore | 3 +- Moonlight.Api/Configuration/ApiOptions.cs | 3 +- Moonlight.Api/Configuration/CacheOptions.cs | 6 ++ Moonlight.Api/Configuration/RedisOptions.cs | 7 ++ Moonlight.Api/Configuration/SessionOptions.cs | 6 -- .../Configuration/SettingsOptions.cs | 3 +- Moonlight.Api/Configuration/UserOptions.cs | 7 ++ .../Controllers/Admin/ApiKeyController.cs | 11 ++- .../ApiKeyScheme/ApiKeySchemeHandler.cs | 49 +++++++------ .../ApiKeyScheme/ApiKeySchemeOptions.cs | 3 +- Moonlight.Api/Moonlight.Api.csproj | 2 + Moonlight.Api/Services/SettingsService.cs | 46 ++++++------ Moonlight.Api/Services/UserAuthService.cs | 70 +++++++++---------- Moonlight.Api/Services/UserDeletionService.cs | 15 ++-- Moonlight.Api/Services/UserLogoutService.cs | 18 ++--- Moonlight.Api/Startup/Startup.Auth.cs | 44 ++++++++---- Moonlight.Api/Startup/Startup.Base.cs | 44 ++++++++---- Moonlight.Api/Startup/Startup.Cache.cs | 34 +++++++++ Moonlight.Api/Startup/Startup.cs | 1 + 19 files changed, 240 insertions(+), 132 deletions(-) create mode 100644 Moonlight.Api/Configuration/CacheOptions.cs create mode 100644 Moonlight.Api/Configuration/RedisOptions.cs delete mode 100644 Moonlight.Api/Configuration/SessionOptions.cs create mode 100644 Moonlight.Api/Configuration/UserOptions.cs create mode 100644 Moonlight.Api/Startup/Startup.Cache.cs diff --git a/.gitignore b/.gitignore index ea86fbb6..8316c260 100644 --- a/.gitignore +++ b/.gitignore @@ -405,4 +405,5 @@ FodyWeavers.xsd # Secrets **/.env **/appsettings.json -**/appsettings.Development.json \ No newline at end of file +**/appsettings.Development.json +**/storage \ No newline at end of file diff --git a/Moonlight.Api/Configuration/ApiOptions.cs b/Moonlight.Api/Configuration/ApiOptions.cs index 134b1813..87959c7e 100644 --- a/Moonlight.Api/Configuration/ApiOptions.cs +++ b/Moonlight.Api/Configuration/ApiOptions.cs @@ -2,5 +2,6 @@ namespace Moonlight.Api.Configuration; public class ApiOptions { - public int LookupCacheMinutes { get; set; } = 3; + public TimeSpan LookupCacheL1Expiry { get; set; } = TimeSpan.FromSeconds(30); + public TimeSpan LookupCacheL2Expiry { get; set; } = TimeSpan.FromMinutes(3); } \ No newline at end of file diff --git a/Moonlight.Api/Configuration/CacheOptions.cs b/Moonlight.Api/Configuration/CacheOptions.cs new file mode 100644 index 00000000..1d166b4f --- /dev/null +++ b/Moonlight.Api/Configuration/CacheOptions.cs @@ -0,0 +1,6 @@ +namespace Moonlight.Api.Configuration; + +public class CacheOptions +{ + public bool EnableLayer2 { get; set; } +} \ No newline at end of file diff --git a/Moonlight.Api/Configuration/RedisOptions.cs b/Moonlight.Api/Configuration/RedisOptions.cs new file mode 100644 index 00000000..37935ff7 --- /dev/null +++ b/Moonlight.Api/Configuration/RedisOptions.cs @@ -0,0 +1,7 @@ +namespace Moonlight.Api.Configuration; + +public class RedisOptions +{ + public bool Enable { get; set; } + public string ConnectionString { get; set; } +} \ No newline at end of file diff --git a/Moonlight.Api/Configuration/SessionOptions.cs b/Moonlight.Api/Configuration/SessionOptions.cs deleted file mode 100644 index 9767009b..00000000 --- a/Moonlight.Api/Configuration/SessionOptions.cs +++ /dev/null @@ -1,6 +0,0 @@ -namespace Moonlight.Api.Configuration; - -public class SessionOptions -{ - public int ValidationCacheMinutes { get; set; } = 3; -} \ No newline at end of file diff --git a/Moonlight.Api/Configuration/SettingsOptions.cs b/Moonlight.Api/Configuration/SettingsOptions.cs index 6ba1021e..758e0374 100644 --- a/Moonlight.Api/Configuration/SettingsOptions.cs +++ b/Moonlight.Api/Configuration/SettingsOptions.cs @@ -2,5 +2,6 @@ public class SettingsOptions { - public int CacheMinutes { get; set; } = 3; + public TimeSpan LookupL1CacheTime { get; set; } = TimeSpan.FromMinutes(1); + public TimeSpan LookupL2CacheTime { get; set; } = TimeSpan.FromMinutes(5); } \ No newline at end of file diff --git a/Moonlight.Api/Configuration/UserOptions.cs b/Moonlight.Api/Configuration/UserOptions.cs new file mode 100644 index 00000000..84e8aac3 --- /dev/null +++ b/Moonlight.Api/Configuration/UserOptions.cs @@ -0,0 +1,7 @@ +namespace Moonlight.Api.Configuration; + +public class UserOptions +{ + public TimeSpan ValidationCacheL1Expiry { get; set; } = TimeSpan.FromSeconds(30); + public TimeSpan ValidationCacheL2Expiry { get; set; } = TimeSpan.FromMinutes(3); +} \ No newline at end of file diff --git a/Moonlight.Api/Http/Controllers/Admin/ApiKeyController.cs b/Moonlight.Api/Http/Controllers/Admin/ApiKeyController.cs index 23c47e95..2ff16560 100644 --- a/Moonlight.Api/Http/Controllers/Admin/ApiKeyController.cs +++ b/Moonlight.Api/Http/Controllers/Admin/ApiKeyController.cs @@ -1,8 +1,10 @@ using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Mvc; using Microsoft.EntityFrameworkCore; +using Microsoft.Extensions.Caching.Hybrid; using Moonlight.Api.Database; using Moonlight.Api.Database.Entities; +using Moonlight.Api.Implementations.ApiKeyScheme; using Moonlight.Api.Mappers; using Moonlight.Shared; using Moonlight.Shared.Http.Requests; @@ -18,10 +20,12 @@ namespace Moonlight.Api.Http.Controllers.Admin; public class ApiKeyController : Controller { private readonly DatabaseRepository KeyRepository; + private readonly HybridCache HybridCache; - public ApiKeyController(DatabaseRepository keyRepository) + public ApiKeyController(DatabaseRepository keyRepository, HybridCache hybridCache) { KeyRepository = keyRepository; + HybridCache = hybridCache; } [HttpGet] @@ -114,6 +118,8 @@ public class ApiKeyController : Controller ApiKeyMapper.Merge(apiKey, request); await KeyRepository.UpdateAsync(apiKey); + await HybridCache.RemoveAsync(string.Format(ApiKeySchemeHandler.CacheKeyFormat, apiKey.Key)); + return ApiKeyMapper.ToDto(apiKey); } @@ -129,6 +135,9 @@ public class ApiKeyController : Controller return Problem("No API key with this id found", statusCode: 404); await KeyRepository.RemoveAsync(apiKey); + + await HybridCache.RemoveAsync(string.Format(ApiKeySchemeHandler.CacheKeyFormat, apiKey.Key)); + return NoContent(); } } \ No newline at end of file diff --git a/Moonlight.Api/Implementations/ApiKeyScheme/ApiKeySchemeHandler.cs b/Moonlight.Api/Implementations/ApiKeyScheme/ApiKeySchemeHandler.cs index dcc9a483..a13bf79a 100644 --- a/Moonlight.Api/Implementations/ApiKeyScheme/ApiKeySchemeHandler.cs +++ b/Moonlight.Api/Implementations/ApiKeyScheme/ApiKeySchemeHandler.cs @@ -2,7 +2,7 @@ using System.Security.Claims; using System.Text.Encodings.Web; using Microsoft.AspNetCore.Authentication; using Microsoft.EntityFrameworkCore; -using Microsoft.Extensions.Caching.Memory; +using Microsoft.Extensions.Caching.Hybrid; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; using Moonlight.Api.Database; @@ -14,20 +14,20 @@ namespace Moonlight.Api.Implementations.ApiKeyScheme; public class ApiKeySchemeHandler : AuthenticationHandler { private readonly DatabaseRepository ApiKeyRepository; - private readonly IMemoryCache MemoryCache; + private readonly HybridCache HybridCache; - private const string CacheKeyFormat = $"Moonlight.Api.{nameof(ApiKeySchemeHandler)}.{{0}}"; + public const string CacheKeyFormat = $"Moonlight.Api.{nameof(ApiKeySchemeHandler)}.{{0}}"; public ApiKeySchemeHandler( IOptionsMonitor options, ILoggerFactory logger, UrlEncoder encoder, DatabaseRepository apiKeyRepository, - IMemoryCache memoryCache + HybridCache hybridCache ) : base(options, logger, encoder) { ApiKeyRepository = apiKeyRepository; - MemoryCache = memoryCache; + HybridCache = hybridCache; } protected override async Task HandleAuthenticateAsync() @@ -41,25 +41,30 @@ public class ApiKeySchemeHandler : AuthenticationHandler return AuthenticateResult.Fail("Invalid api key specified"); var cacheKey = string.Format(CacheKeyFormat, authHeaderValue); - - if (!MemoryCache.TryGetValue(cacheKey, out var apiKey)) - { - apiKey = await ApiKeyRepository - .Query() - .Where(x => x.Key == authHeaderValue) - .Select(x => new ApiKeySession(x.Permissions, x.ValidUntil)) - .FirstOrDefaultAsync(); - if (apiKey == null) - return AuthenticateResult.Fail("Invalid api key specified"); + var apiKey = await HybridCache.GetOrCreateAsync( + cacheKey, + async ct => + { + var x = await ApiKeyRepository + .Query() + .Where(x => x.Key == authHeaderValue) + .Select(x => new ApiKeySession(x.Permissions, x.ValidUntil)) + .FirstOrDefaultAsync(cancellationToken: ct); - MemoryCache.Set(cacheKey, apiKey, Options.LookupCacheTime); - } - else - { - if (apiKey == null) - return AuthenticateResult.Fail("Invalid api key specified"); - } + Console.WriteLine($"API: {x?.ValidUntil}"); + + return x; + }, + new HybridCacheEntryOptions() + { + LocalCacheExpiration = Options.LookupL1CacheTime, + Expiration = Options.LookupL2CacheTime + } + ); + + if (apiKey == null) + return AuthenticateResult.Fail("Invalid api key specified"); if (DateTimeOffset.UtcNow > apiKey.ValidUntil) return AuthenticateResult.Fail("Api key expired"); diff --git a/Moonlight.Api/Implementations/ApiKeyScheme/ApiKeySchemeOptions.cs b/Moonlight.Api/Implementations/ApiKeyScheme/ApiKeySchemeOptions.cs index 5041e344..0057f976 100644 --- a/Moonlight.Api/Implementations/ApiKeyScheme/ApiKeySchemeOptions.cs +++ b/Moonlight.Api/Implementations/ApiKeyScheme/ApiKeySchemeOptions.cs @@ -4,5 +4,6 @@ namespace Moonlight.Api.Implementations.ApiKeyScheme; public class ApiKeySchemeOptions : AuthenticationSchemeOptions { - public TimeSpan LookupCacheTime { get; set; } + public TimeSpan LookupL1CacheTime { get; set; } + public TimeSpan LookupL2CacheTime { get; set; } } \ No newline at end of file diff --git a/Moonlight.Api/Moonlight.Api.csproj b/Moonlight.Api/Moonlight.Api.csproj index 10a62a6c..a4ed8ba1 100644 --- a/Moonlight.Api/Moonlight.Api.csproj +++ b/Moonlight.Api/Moonlight.Api.csproj @@ -26,6 +26,8 @@ + + diff --git a/Moonlight.Api/Services/SettingsService.cs b/Moonlight.Api/Services/SettingsService.cs index f6361fbb..c30cce2b 100644 --- a/Moonlight.Api/Services/SettingsService.cs +++ b/Moonlight.Api/Services/SettingsService.cs @@ -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 Repository; private readonly IOptions Options; - private readonly IMemoryCache Cache; + private readonly HybridCache HybridCache; private const string CacheKey = "Moonlight.Api.SettingsService.{0}"; public SettingsService( DatabaseRepository repository, IOptions 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(cacheKey, out var value)) - return JsonSerializer.Deserialize(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( 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(value); } @@ -77,7 +79,7 @@ public class SettingsService await Repository.AddAsync(option); } - - Cache.Remove(cacheKey); + + await HybridCache.RemoveAsync(cacheKey); } } \ No newline at end of file diff --git a/Moonlight.Api/Services/UserAuthService.cs b/Moonlight.Api/Services/UserAuthService.cs index 8f8d4126..9a9a27c2 100644 --- a/Moonlight.Api/Services/UserAuthService.cs +++ b/Moonlight.Api/Services/UserAuthService.cs @@ -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 UserRepository; - private readonly IMemoryCache Cache; private readonly ILogger Logger; - private readonly IOptions Options; + private readonly IOptions Options; private readonly IEnumerable 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 userRepository, ILogger logger, - IMemoryCache cache, IOptions options, - IEnumerable hooks + IOptions options, + IEnumerable hooks, + HybridCache hybridCache ) { UserRepository = userRepository; Logger = logger; - Cache = cache; Options = options; Hooks = hooks; + HybridCache = hybridCache; } public async Task 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(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( + 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; } diff --git a/Moonlight.Api/Services/UserDeletionService.cs b/Moonlight.Api/Services/UserDeletionService.cs index 5cfb8e49..6253bbab 100644 --- a/Moonlight.Api/Services/UserDeletionService.cs +++ b/Moonlight.Api/Services/UserDeletionService.cs @@ -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 Repository; private readonly IEnumerable Hooks; - private readonly IMemoryCache Cache; + private readonly HybridCache HybridCache; - public UserDeletionService(DatabaseRepository repository, IEnumerable hooks, IMemoryCache cache) + public UserDeletionService( + DatabaseRepository repository, + IEnumerable hooks, + HybridCache hybridCache + ) { Repository = repository; Hooks = hooks; - Cache = cache; + HybridCache = hybridCache; } public async Task 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)); } } diff --git a/Moonlight.Api/Services/UserLogoutService.cs b/Moonlight.Api/Services/UserLogoutService.cs index c630f398..59b87257 100644 --- a/Moonlight.Api/Services/UserLogoutService.cs +++ b/Moonlight.Api/Services/UserLogoutService.cs @@ -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 Repository; private readonly IEnumerable Hooks; - private readonly IMemoryCache Cache; + private readonly HybridCache HybridCache; public UserLogoutService( DatabaseRepository repository, IEnumerable 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)); } } \ No newline at end of file diff --git a/Moonlight.Api/Startup/Startup.Auth.cs b/Moonlight.Api/Startup/Startup.Auth.cs index 2b42c4d9..7d404c56 100644 --- a/Moonlight.Api/Startup/Startup.Auth.cs +++ b/Moonlight.Api/Startup/Startup.Auth.cs @@ -17,20 +17,26 @@ public partial class Startup { private static void AddAuth(WebApplicationBuilder builder) { + // OIDC var oidcOptions = new OidcOptions(); builder.Configuration.GetSection("Moonlight:Oidc").Bind(oidcOptions); + // API Key var apiKeyOptions = new ApiOptions(); builder.Configuration.GetSection("Moonlight:Api").Bind(apiKeyOptions); builder.Services.AddOptions().BindConfiguration("Moonlight:Api"); + + // Session + builder.Services.AddOptions().BindConfiguration("Moonlight:User"); - builder.Services.AddScoped(); - + // Authentication builder.Services.AddAuthentication("Main") - .AddPolicyScheme("Main", null, options => - { - options.ForwardDefaultSelector += context => context.Request.Headers.Authorization.Count > 0 ? "ApiKey" : "Session"; - }) + .AddPolicyScheme("Main", null, + options => + { + options.ForwardDefaultSelector += context => + context.Request.Headers.Authorization.Count > 0 ? "ApiKey" : "Session"; + }) .AddCookie("Session", null, options => { options.Events.OnSigningIn += async context => @@ -83,7 +89,7 @@ public partial class Startup var scopes = oidcOptions.Scopes ?? ["openid", "email", "profile"]; options.Scope.Clear(); - + foreach (var scope in scopes) options.Scope.Add(scope); @@ -97,18 +103,26 @@ public partial class Startup options.GetClaimsFromUserInfoEndpoint = true; }) - .AddScheme("ApiKey", null, options => - { - options.LookupCacheTime = TimeSpan.FromMinutes(apiKeyOptions.LookupCacheMinutes); - }); - - builder.Logging.AddFilter("Moonlight.Api.Implementations.ApiKeyScheme.ApiKeySchemeHandler", LogLevel.Warning); + .AddScheme("ApiKey", null, + options => + { + options.LookupL1CacheTime = apiKeyOptions.LookupCacheL1Expiry; + options.LookupL2CacheTime = apiKeyOptions.LookupCacheL2Expiry; + }); + // Authorization + builder.Services.AddAuthorization(); + + // Reduce log noise + builder.Logging.AddFilter("Moonlight.Api.Implementations.ApiKeyScheme.ApiKeySchemeHandler", LogLevel.Warning); + + // Custom permission handling using named policies builder.Services.AddSingleton(); builder.Services.AddSingleton(); - builder.Services.AddOptions().BindConfiguration("Moonlight:Settings"); - builder.Services.AddScoped(); + builder.Services.AddScoped(); + builder.Services.AddScoped(); + builder.Services.AddScoped(); } private static void UseAuth(WebApplication application) diff --git a/Moonlight.Api/Startup/Startup.Base.cs b/Moonlight.Api/Startup/Startup.Base.cs index a71f0217..1c4d1e91 100644 --- a/Moonlight.Api/Startup/Startup.Base.cs +++ b/Moonlight.Api/Startup/Startup.Base.cs @@ -1,4 +1,6 @@ using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.DataProtection; +using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging.Console; @@ -9,7 +11,6 @@ using Moonlight.Api.Helpers; using Moonlight.Api.Implementations; using Moonlight.Api.Interfaces; using Moonlight.Api.Services; -using SessionOptions = Moonlight.Api.Configuration.SessionOptions; namespace Moonlight.Api.Startup; @@ -17,44 +18,63 @@ public partial class Startup { private static void AddBase(WebApplicationBuilder builder) { + // Create the base directory + Directory.CreateDirectory("storage"); + + // Hook up source-generated serialization builder.Services.AddControllers().AddJsonOptions(options => { options.JsonSerializerOptions.TypeInfoResolverChain.Add(SerializationContext.Default); }); + // Configure logging builder.Logging.ClearProviders(); builder.Logging.AddConsole(options => { options.FormatterName = nameof(AppConsoleFormatter); }); builder.Logging.AddConsoleFormatter(); + // Application service builder.Services.AddSingleton(); builder.Services.AddHostedService(sp => sp.GetRequiredService()); + // Diagnose builder.Services.AddSingleton(); - builder.Services.AddSingleton(); - - builder.Services.AddMemoryCache(); - builder.Services.AddOptions().BindConfiguration("Moonlight:Session"); + // Frontend builder.Services.AddOptions().BindConfiguration("Moonlight:Frontend"); builder.Services.AddScoped(); + // HTTP Client builder.Services.AddHttpClient(); - + + // Version fetching configuration builder.Services.AddOptions().BindConfiguration("Moonlight:Version"); builder.Services.AddSingleton(); + + // Container Helper Options + builder.Configuration.GetSection("Moonlight:ContainerHelper").Bind(builder.Configuration); builder.Services.AddOptions().BindConfiguration("Moonlight:ContainerHelper"); builder.Services.AddSingleton(); - + builder.Services.AddHttpClient("ContainerHelper", (provider, client) => { - var options = provider.GetRequiredService>(); - client.BaseAddress = new Uri(options.Value.IsEnabled ? options.Value.Url : "http://you-should-fail.invalid"); + var options = provider.GetRequiredService>(); + client.BaseAddress = + new Uri(options.Value.IsEnabled ? options.Value.Url : "http://you-should-fail.invalid"); }); - + + // User management services builder.Services.AddScoped(); builder.Services.AddScoped(); + + // Settings options + builder.Services.AddOptions().BindConfiguration("Moonlight:Settings"); + builder.Services.AddScoped(); + + // Setup key loading + var keysDirectory = new DirectoryInfo(Path.Combine("storage", "keys")); + builder.Services.AddDataProtection().PersistKeysToFileSystem(keysDirectory); } private static void UseBase(WebApplication application) @@ -67,8 +87,8 @@ public partial class Startup application.MapControllers(); var options = application.Services.GetRequiredService>(); - - if(options.Value.Enabled) + + if (options.Value.Enabled) application.MapFallbackToFile("index.html"); } } \ No newline at end of file diff --git a/Moonlight.Api/Startup/Startup.Cache.cs b/Moonlight.Api/Startup/Startup.Cache.cs new file mode 100644 index 00000000..9d642ff5 --- /dev/null +++ b/Moonlight.Api/Startup/Startup.Cache.cs @@ -0,0 +1,34 @@ +using Microsoft.AspNetCore.Builder; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; +using Moonlight.Api.Configuration; + +namespace Moonlight.Api.Startup; + +public partial class Startup +{ + private static void AddCache(WebApplicationBuilder builder) + { + // Load cache options + var cacheOptions = new CacheOptions(); + builder.Configuration.GetSection("Moonlight:Cache").Bind(cacheOptions); + + builder.Services.AddMemoryCache(); + builder.Services.AddHybridCache(); + + if (!cacheOptions.EnableLayer2) + return; + + var redisOptions = new RedisOptions(); + builder.Configuration.GetSection("Moonlight:Redis").Bind(redisOptions); + + if(!redisOptions.Enable) + return; + + builder.Services.AddStackExchangeRedisCache(options => + { + options.Configuration = redisOptions.ConnectionString; + options.InstanceName = "Moonlight:"; + }); + } +} \ No newline at end of file diff --git a/Moonlight.Api/Startup/Startup.cs b/Moonlight.Api/Startup/Startup.cs index e5066df1..7aebd692 100644 --- a/Moonlight.Api/Startup/Startup.cs +++ b/Moonlight.Api/Startup/Startup.cs @@ -9,6 +9,7 @@ public partial class Startup : IAppStartup AddBase(builder); AddAuth(builder); AddDatabase(builder); + AddCache(builder); } public void PostBuild(WebApplication application)