Merge pull request 'Implemented hybrid cache with redis support' (#14) from feat/HybridCache into v2.1
All checks were successful
Dev Publish: Nuget / Publish Dev Packages (push) Successful in 53s

Reviewed-on: #14
This commit was merged in pull request #14.
This commit is contained in:
2026-02-12 14:30:21 +00:00
19 changed files with 240 additions and 132 deletions

1
.gitignore vendored
View File

@@ -406,3 +406,4 @@ FodyWeavers.xsd
**/.env **/.env
**/appsettings.json **/appsettings.json
**/appsettings.Development.json **/appsettings.Development.json
**/storage

View File

@@ -2,5 +2,6 @@ namespace Moonlight.Api.Configuration;
public class ApiOptions 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);
} }

View File

@@ -0,0 +1,6 @@
namespace Moonlight.Api.Configuration;
public class CacheOptions
{
public bool EnableLayer2 { get; set; }
}

View File

@@ -0,0 +1,7 @@
namespace Moonlight.Api.Configuration;
public class RedisOptions
{
public bool Enable { get; set; }
public string ConnectionString { get; set; }
}

View File

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

View File

@@ -2,5 +2,6 @@
public class SettingsOptions 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);
} }

View File

@@ -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);
}

View File

@@ -1,8 +1,10 @@
using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc;
using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Caching.Hybrid;
using Moonlight.Api.Database; using Moonlight.Api.Database;
using Moonlight.Api.Database.Entities; using Moonlight.Api.Database.Entities;
using Moonlight.Api.Implementations.ApiKeyScheme;
using Moonlight.Api.Mappers; using Moonlight.Api.Mappers;
using Moonlight.Shared; using Moonlight.Shared;
using Moonlight.Shared.Http.Requests; using Moonlight.Shared.Http.Requests;
@@ -18,10 +20,12 @@ namespace Moonlight.Api.Http.Controllers.Admin;
public class ApiKeyController : Controller public class ApiKeyController : Controller
{ {
private readonly DatabaseRepository<ApiKey> KeyRepository; private readonly DatabaseRepository<ApiKey> KeyRepository;
private readonly HybridCache HybridCache;
public ApiKeyController(DatabaseRepository<ApiKey> keyRepository) public ApiKeyController(DatabaseRepository<ApiKey> keyRepository, HybridCache hybridCache)
{ {
KeyRepository = keyRepository; KeyRepository = keyRepository;
HybridCache = hybridCache;
} }
[HttpGet] [HttpGet]
@@ -114,6 +118,8 @@ public class ApiKeyController : Controller
ApiKeyMapper.Merge(apiKey, request); ApiKeyMapper.Merge(apiKey, request);
await KeyRepository.UpdateAsync(apiKey); await KeyRepository.UpdateAsync(apiKey);
await HybridCache.RemoveAsync(string.Format(ApiKeySchemeHandler.CacheKeyFormat, apiKey.Key));
return ApiKeyMapper.ToDto(apiKey); return ApiKeyMapper.ToDto(apiKey);
} }
@@ -129,6 +135,9 @@ public class ApiKeyController : Controller
return Problem("No API key with this id found", statusCode: 404); return Problem("No API key with this id found", statusCode: 404);
await KeyRepository.RemoveAsync(apiKey); await KeyRepository.RemoveAsync(apiKey);
await HybridCache.RemoveAsync(string.Format(ApiKeySchemeHandler.CacheKeyFormat, apiKey.Key));
return NoContent(); return NoContent();
} }
} }

View File

@@ -2,7 +2,7 @@ using System.Security.Claims;
using System.Text.Encodings.Web; using System.Text.Encodings.Web;
using Microsoft.AspNetCore.Authentication; using Microsoft.AspNetCore.Authentication;
using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Caching.Memory; using Microsoft.Extensions.Caching.Hybrid;
using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options; using Microsoft.Extensions.Options;
using Moonlight.Api.Database; using Moonlight.Api.Database;
@@ -14,20 +14,20 @@ namespace Moonlight.Api.Implementations.ApiKeyScheme;
public class ApiKeySchemeHandler : AuthenticationHandler<ApiKeySchemeOptions> public class ApiKeySchemeHandler : AuthenticationHandler<ApiKeySchemeOptions>
{ {
private readonly DatabaseRepository<ApiKey> ApiKeyRepository; private readonly DatabaseRepository<ApiKey> 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( public ApiKeySchemeHandler(
IOptionsMonitor<ApiKeySchemeOptions> options, IOptionsMonitor<ApiKeySchemeOptions> options,
ILoggerFactory logger, ILoggerFactory logger,
UrlEncoder encoder, UrlEncoder encoder,
DatabaseRepository<ApiKey> apiKeyRepository, DatabaseRepository<ApiKey> apiKeyRepository,
IMemoryCache memoryCache HybridCache hybridCache
) : base(options, logger, encoder) ) : base(options, logger, encoder)
{ {
ApiKeyRepository = apiKeyRepository; ApiKeyRepository = apiKeyRepository;
MemoryCache = memoryCache; HybridCache = hybridCache;
} }
protected override async Task<AuthenticateResult> HandleAuthenticateAsync() protected override async Task<AuthenticateResult> HandleAuthenticateAsync()
@@ -42,24 +42,29 @@ public class ApiKeySchemeHandler : AuthenticationHandler<ApiKeySchemeOptions>
var cacheKey = string.Format(CacheKeyFormat, authHeaderValue); var cacheKey = string.Format(CacheKeyFormat, authHeaderValue);
if (!MemoryCache.TryGetValue<ApiKeySession>(cacheKey, out var apiKey)) var apiKey = await HybridCache.GetOrCreateAsync<ApiKeySession?>(
{ cacheKey,
apiKey = await ApiKeyRepository async ct =>
.Query() {
.Where(x => x.Key == authHeaderValue) var x = await ApiKeyRepository
.Select(x => new ApiKeySession(x.Permissions, x.ValidUntil)) .Query()
.FirstOrDefaultAsync(); .Where(x => x.Key == authHeaderValue)
.Select(x => new ApiKeySession(x.Permissions, x.ValidUntil))
.FirstOrDefaultAsync(cancellationToken: ct);
if (apiKey == null) Console.WriteLine($"API: {x?.ValidUntil}");
return AuthenticateResult.Fail("Invalid api key specified");
MemoryCache.Set(cacheKey, apiKey, Options.LookupCacheTime); return x;
} },
else new HybridCacheEntryOptions()
{ {
if (apiKey == null) LocalCacheExpiration = Options.LookupL1CacheTime,
return AuthenticateResult.Fail("Invalid api key specified"); Expiration = Options.LookupL2CacheTime
} }
);
if (apiKey == null)
return AuthenticateResult.Fail("Invalid api key specified");
if (DateTimeOffset.UtcNow > apiKey.ValidUntil) if (DateTimeOffset.UtcNow > apiKey.ValidUntil)
return AuthenticateResult.Fail("Api key expired"); return AuthenticateResult.Fail("Api key expired");

View File

@@ -4,5 +4,6 @@ namespace Moonlight.Api.Implementations.ApiKeyScheme;
public class ApiKeySchemeOptions : AuthenticationSchemeOptions public class ApiKeySchemeOptions : AuthenticationSchemeOptions
{ {
public TimeSpan LookupCacheTime { get; set; } public TimeSpan LookupL1CacheTime { get; set; }
public TimeSpan LookupL2CacheTime { get; set; }
} }

View File

@@ -26,6 +26,8 @@
<ItemGroup> <ItemGroup>
<PackageReference Include="Microsoft.AspNetCore.Authentication.OpenIdConnect" Version="10.0.1"/> <PackageReference Include="Microsoft.AspNetCore.Authentication.OpenIdConnect" Version="10.0.1"/>
<PackageReference Include="Microsoft.EntityFrameworkCore" Version="10.0.1"/> <PackageReference Include="Microsoft.EntityFrameworkCore" Version="10.0.1"/>
<PackageReference Include="Microsoft.Extensions.Caching.Hybrid" Version="10.3.0" />
<PackageReference Include="Microsoft.Extensions.Caching.StackExchangeRedis" Version="10.0.3" />
<PackageReference Include="Npgsql.EntityFrameworkCore.PostgreSQL" Version="10.0.0"/> <PackageReference Include="Npgsql.EntityFrameworkCore.PostgreSQL" Version="10.0.0"/>
<PackageReference Include="Riok.Mapperly" Version="4.3.1-next.0"/> <PackageReference Include="Riok.Mapperly" Version="4.3.1-next.0"/>
</ItemGroup> </ItemGroup>

View File

@@ -1,6 +1,6 @@
using System.Text.Json; using System.Text.Json;
using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Caching.Memory; using Microsoft.Extensions.Caching.Hybrid;
using Microsoft.Extensions.Options; using Microsoft.Extensions.Options;
using Moonlight.Api.Configuration; using Moonlight.Api.Configuration;
using Moonlight.Api.Database; using Moonlight.Api.Database;
@@ -12,18 +12,18 @@ public class SettingsService
{ {
private readonly DatabaseRepository<SettingsOption> Repository; private readonly DatabaseRepository<SettingsOption> Repository;
private readonly IOptions<SettingsOptions> Options; private readonly IOptions<SettingsOptions> Options;
private readonly IMemoryCache Cache; private readonly HybridCache HybridCache;
private const string CacheKey = "Moonlight.Api.SettingsService.{0}"; private const string CacheKey = "Moonlight.Api.SettingsService.{0}";
public SettingsService( public SettingsService(
DatabaseRepository<SettingsOption> repository, DatabaseRepository<SettingsOption> repository,
IOptions<SettingsOptions> options, IOptions<SettingsOptions> options,
IMemoryCache cache HybridCache hybridCache
) )
{ {
Repository = repository; Repository = repository;
Cache = cache; HybridCache = hybridCache;
Options = options; Options = options;
} }
@@ -31,24 +31,26 @@ public class SettingsService
{ {
var cacheKey = string.Format(CacheKey, key); var cacheKey = string.Format(CacheKey, key);
if (Cache.TryGetValue<string>(cacheKey, out var value)) var value = await HybridCache.GetOrCreateAsync<string?>(
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(
cacheKey, cacheKey,
value, async ct =>
TimeSpan.FromMinutes(Options.Value.CacheMinutes) {
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); return JsonSerializer.Deserialize<T>(value);
} }
@@ -78,6 +80,6 @@ public class SettingsService
await Repository.AddAsync(option); await Repository.AddAsync(option);
} }
Cache.Remove(cacheKey); await HybridCache.RemoveAsync(cacheKey);
} }
} }

View File

@@ -1,6 +1,6 @@
using System.Security.Claims; using System.Security.Claims;
using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Caching.Memory; using Microsoft.Extensions.Caching.Hybrid;
using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options; using Microsoft.Extensions.Options;
using Moonlight.Api.Configuration; using Moonlight.Api.Configuration;
@@ -14,10 +14,10 @@ namespace Moonlight.Api.Services;
public class UserAuthService public class UserAuthService
{ {
private readonly DatabaseRepository<User> UserRepository; private readonly DatabaseRepository<User> UserRepository;
private readonly IMemoryCache Cache;
private readonly ILogger<UserAuthService> Logger; private readonly ILogger<UserAuthService> Logger;
private readonly IOptions<SessionOptions> Options; private readonly IOptions<UserOptions> Options;
private readonly IEnumerable<IUserAuthHook> Hooks; private readonly IEnumerable<IUserAuthHook> Hooks;
private readonly HybridCache HybridCache;
private const string UserIdClaim = "UserId"; private const string UserIdClaim = "UserId";
private const string IssuedAtClaim = "IssuedAt"; private const string IssuedAtClaim = "IssuedAt";
@@ -27,15 +27,16 @@ public class UserAuthService
public UserAuthService( public UserAuthService(
DatabaseRepository<User> userRepository, DatabaseRepository<User> userRepository,
ILogger<UserAuthService> logger, ILogger<UserAuthService> logger,
IMemoryCache cache, IOptions<SessionOptions> options, IOptions<UserOptions> options,
IEnumerable<IUserAuthHook> hooks IEnumerable<IUserAuthHook> hooks,
HybridCache hybridCache
) )
{ {
UserRepository = userRepository; UserRepository = userRepository;
Logger = logger; Logger = logger;
Cache = cache;
Options = options; Options = options;
Hooks = hooks; Hooks = hooks;
HybridCache = hybridCache;
} }
public async Task<bool> SyncAsync(ClaimsPrincipal? principal) public async Task<bool> SyncAsync(ClaimsPrincipal? principal)
@@ -80,8 +81,8 @@ public class UserAuthService
foreach (var hook in Hooks) foreach (var hook in Hooks)
{ {
// Run every hook and if any returns false we return false as well // Run every hook, and if any returns false, we return false as well
if(!await hook.SyncAsync(principal, user)) if (!await hook.SyncAsync(principal, user))
return false; return false;
} }
@@ -101,32 +102,29 @@ public class UserAuthService
var cacheKey = string.Format(CacheKeyPattern, userId); var cacheKey = string.Format(CacheKeyPattern, userId);
if (!Cache.TryGetValue<UserSession>(cacheKey, out var user)) var user = await HybridCache.GetOrCreateAsync<UserSession?>(
{ cacheKey,
user = await UserRepository async ct =>
.Query() {
.AsNoTracking() return await UserRepository
.Where(u => u.Id == userId) .Query()
.Select(u => new UserSession( .AsNoTracking()
u.InvalidateTimestamp, .Where(u => u.Id == userId)
u.RoleMemberships.SelectMany(x => x.Role.Permissions).ToArray()) .Select(u => new UserSession(
) u.InvalidateTimestamp,
.FirstOrDefaultAsync(); u.RoleMemberships.SelectMany(x => x.Role.Permissions).ToArray())
)
.FirstOrDefaultAsync(cancellationToken: ct);
},
new HybridCacheEntryOptions()
{
LocalCacheExpiration = Options.Value.ValidationCacheL1Expiry,
Expiration = Options.Value.ValidationCacheL2Expiry
}
);
if (user == null) if (user == null)
return false; return false;
Cache.Set(
cacheKey,
user,
TimeSpan.FromMinutes(Options.Value.ValidationCacheMinutes)
);
}
else
{
if (user == null)
return false;
}
var issuedAtString = principal.FindFirstValue(IssuedAtClaim); var issuedAtString = principal.FindFirstValue(IssuedAtClaim);
@@ -149,8 +147,8 @@ public class UserAuthService
foreach (var hook in Hooks) foreach (var hook in Hooks)
{ {
// Run every hook and if any returns false we return false as well // Run every hook, and if any returns false we return false as well
if(!await hook.ValidateAsync(principal, userId)) if (!await hook.ValidateAsync(principal, userId))
return false; return false;
} }

View File

@@ -1,5 +1,5 @@
using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Caching.Memory; using Microsoft.Extensions.Caching.Hybrid;
using Moonlight.Api.Database; using Moonlight.Api.Database;
using Moonlight.Api.Database.Entities; using Moonlight.Api.Database.Entities;
using Moonlight.Api.Interfaces; using Moonlight.Api.Interfaces;
@@ -10,13 +10,17 @@ public class UserDeletionService
{ {
private readonly DatabaseRepository<User> Repository; private readonly DatabaseRepository<User> Repository;
private readonly IEnumerable<IUserDeletionHook> Hooks; 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; Repository = repository;
Hooks = hooks; Hooks = hooks;
Cache = cache; HybridCache = hybridCache;
} }
public async Task<UserDeletionValidationResult> ValidateAsync(int userId) public async Task<UserDeletionValidationResult> ValidateAsync(int userId)
@@ -54,7 +58,8 @@ public class UserDeletionService
await hook.ExecuteAsync(user); await hook.ExecuteAsync(user);
await Repository.RemoveAsync(user); await Repository.RemoveAsync(user);
Cache.Remove(string.Format(UserAuthService.CacheKeyPattern, userId));
await HybridCache.RemoveAsync(string.Format(UserAuthService.CacheKeyPattern, user.Id));
} }
} }

View File

@@ -1,5 +1,5 @@
using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Caching.Memory; using Microsoft.Extensions.Caching.Hybrid;
using Moonlight.Api.Database; using Moonlight.Api.Database;
using Moonlight.Api.Database.Entities; using Moonlight.Api.Database.Entities;
using Moonlight.Api.Interfaces; using Moonlight.Api.Interfaces;
@@ -10,17 +10,17 @@ public class UserLogoutService
{ {
private readonly DatabaseRepository<User> Repository; private readonly DatabaseRepository<User> Repository;
private readonly IEnumerable<IUserLogoutHook> Hooks; private readonly IEnumerable<IUserLogoutHook> Hooks;
private readonly IMemoryCache Cache; private readonly HybridCache HybridCache;
public UserLogoutService( public UserLogoutService(
DatabaseRepository<User> repository, DatabaseRepository<User> repository,
IEnumerable<IUserLogoutHook> hooks, IEnumerable<IUserLogoutHook> hooks,
IMemoryCache cache HybridCache hybridCache
) )
{ {
Repository = repository; Repository = repository;
Hooks = hooks; Hooks = hooks;
Cache = cache; HybridCache = hybridCache;
} }
public async Task LogoutAsync(int userId) public async Task LogoutAsync(int userId)
@@ -29,7 +29,7 @@ public class UserLogoutService
.Query() .Query()
.FirstOrDefaultAsync(x => x.Id == userId); .FirstOrDefaultAsync(x => x.Id == userId);
if(user == null) if (user == null)
throw new AggregateException($"User with id {userId} not found"); throw new AggregateException($"User with id {userId} not found");
foreach (var hook in Hooks) foreach (var hook in Hooks)
@@ -38,6 +38,6 @@ public class UserLogoutService
user.InvalidateTimestamp = DateTimeOffset.UtcNow; user.InvalidateTimestamp = DateTimeOffset.UtcNow;
await Repository.UpdateAsync(user); await Repository.UpdateAsync(user);
Cache.Remove(string.Format(UserAuthService.CacheKeyPattern, userId)); await HybridCache.RemoveAsync(string.Format(UserAuthService.CacheKeyPattern, user.Id));
} }
} }

View File

@@ -17,20 +17,26 @@ public partial class Startup
{ {
private static void AddAuth(WebApplicationBuilder builder) private static void AddAuth(WebApplicationBuilder builder)
{ {
// OIDC
var oidcOptions = new OidcOptions(); var oidcOptions = new OidcOptions();
builder.Configuration.GetSection("Moonlight:Oidc").Bind(oidcOptions); builder.Configuration.GetSection("Moonlight:Oidc").Bind(oidcOptions);
// API Key
var apiKeyOptions = new ApiOptions(); var apiKeyOptions = new ApiOptions();
builder.Configuration.GetSection("Moonlight:Api").Bind(apiKeyOptions); builder.Configuration.GetSection("Moonlight:Api").Bind(apiKeyOptions);
builder.Services.AddOptions<ApiOptions>().BindConfiguration("Moonlight:Api"); builder.Services.AddOptions<ApiOptions>().BindConfiguration("Moonlight:Api");
builder.Services.AddScoped<UserAuthService>(); // Session
builder.Services.AddOptions<UserOptions>().BindConfiguration("Moonlight:User");
// Authentication
builder.Services.AddAuthentication("Main") builder.Services.AddAuthentication("Main")
.AddPolicyScheme("Main", null, options => .AddPolicyScheme("Main", null,
{ options =>
options.ForwardDefaultSelector += context => context.Request.Headers.Authorization.Count > 0 ? "ApiKey" : "Session"; {
}) options.ForwardDefaultSelector += context =>
context.Request.Headers.Authorization.Count > 0 ? "ApiKey" : "Session";
})
.AddCookie("Session", null, options => .AddCookie("Session", null, options =>
{ {
options.Events.OnSigningIn += async context => options.Events.OnSigningIn += async context =>
@@ -97,18 +103,26 @@ public partial class Startup
options.GetClaimsFromUserInfoEndpoint = true; options.GetClaimsFromUserInfoEndpoint = true;
}) })
.AddScheme<ApiKeySchemeOptions, ApiKeySchemeHandler>("ApiKey", null, options => .AddScheme<ApiKeySchemeOptions, ApiKeySchemeHandler>("ApiKey", null,
{ options =>
options.LookupCacheTime = TimeSpan.FromMinutes(apiKeyOptions.LookupCacheMinutes); {
}); 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); builder.Logging.AddFilter("Moonlight.Api.Implementations.ApiKeyScheme.ApiKeySchemeHandler", LogLevel.Warning);
// Custom permission handling using named policies
builder.Services.AddSingleton<IAuthorizationHandler, PermissionAuthorizationHandler>(); builder.Services.AddSingleton<IAuthorizationHandler, PermissionAuthorizationHandler>();
builder.Services.AddSingleton<IAuthorizationPolicyProvider, PermissionPolicyProvider>(); builder.Services.AddSingleton<IAuthorizationPolicyProvider, PermissionPolicyProvider>();
builder.Services.AddOptions<SettingsOptions>().BindConfiguration("Moonlight:Settings"); builder.Services.AddScoped<UserDeletionService>();
builder.Services.AddScoped<SettingsService>(); builder.Services.AddScoped<UserLogoutService>();
builder.Services.AddScoped<UserAuthService>();
} }
private static void UseAuth(WebApplication application) private static void UseAuth(WebApplication application)

View File

@@ -1,4 +1,6 @@
using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.DataProtection;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Logging.Console; using Microsoft.Extensions.Logging.Console;
@@ -9,7 +11,6 @@ using Moonlight.Api.Helpers;
using Moonlight.Api.Implementations; using Moonlight.Api.Implementations;
using Moonlight.Api.Interfaces; using Moonlight.Api.Interfaces;
using Moonlight.Api.Services; using Moonlight.Api.Services;
using SessionOptions = Moonlight.Api.Configuration.SessionOptions;
namespace Moonlight.Api.Startup; namespace Moonlight.Api.Startup;
@@ -17,44 +18,63 @@ public partial class Startup
{ {
private static void AddBase(WebApplicationBuilder builder) private static void AddBase(WebApplicationBuilder builder)
{ {
// Create the base directory
Directory.CreateDirectory("storage");
// Hook up source-generated serialization
builder.Services.AddControllers().AddJsonOptions(options => builder.Services.AddControllers().AddJsonOptions(options =>
{ {
options.JsonSerializerOptions.TypeInfoResolverChain.Add(SerializationContext.Default); options.JsonSerializerOptions.TypeInfoResolverChain.Add(SerializationContext.Default);
}); });
// Configure logging
builder.Logging.ClearProviders(); builder.Logging.ClearProviders();
builder.Logging.AddConsole(options => { options.FormatterName = nameof(AppConsoleFormatter); }); builder.Logging.AddConsole(options => { options.FormatterName = nameof(AppConsoleFormatter); });
builder.Logging.AddConsoleFormatter<AppConsoleFormatter, ConsoleFormatterOptions>(); builder.Logging.AddConsoleFormatter<AppConsoleFormatter, ConsoleFormatterOptions>();
// Application service
builder.Services.AddSingleton<ApplicationService>(); builder.Services.AddSingleton<ApplicationService>();
builder.Services.AddHostedService(sp => sp.GetRequiredService<ApplicationService>()); builder.Services.AddHostedService(sp => sp.GetRequiredService<ApplicationService>());
// Diagnose
builder.Services.AddSingleton<DiagnoseService>(); builder.Services.AddSingleton<DiagnoseService>();
builder.Services.AddSingleton<IDiagnoseProvider, UpdateDiagnoseProvider>(); builder.Services.AddSingleton<IDiagnoseProvider, UpdateDiagnoseProvider>();
builder.Services.AddMemoryCache(); // Frontend
builder.Services.AddOptions<SessionOptions>().BindConfiguration("Moonlight:Session");
builder.Services.AddOptions<FrontendOptions>().BindConfiguration("Moonlight:Frontend"); builder.Services.AddOptions<FrontendOptions>().BindConfiguration("Moonlight:Frontend");
builder.Services.AddScoped<FrontendService>(); builder.Services.AddScoped<FrontendService>();
// HTTP Client
builder.Services.AddHttpClient(); builder.Services.AddHttpClient();
// Version fetching configuration
builder.Services.AddOptions<VersionOptions>().BindConfiguration("Moonlight:Version"); builder.Services.AddOptions<VersionOptions>().BindConfiguration("Moonlight:Version");
builder.Services.AddSingleton<VersionService>(); builder.Services.AddSingleton<VersionService>();
// Container Helper Options
builder.Configuration.GetSection("Moonlight:ContainerHelper").Bind(builder.Configuration);
builder.Services.AddOptions<ContainerHelperOptions>().BindConfiguration("Moonlight:ContainerHelper"); builder.Services.AddOptions<ContainerHelperOptions>().BindConfiguration("Moonlight:ContainerHelper");
builder.Services.AddSingleton<ContainerHelperService>(); builder.Services.AddSingleton<ContainerHelperService>();
builder.Services.AddHttpClient("ContainerHelper", (provider, client) => builder.Services.AddHttpClient("ContainerHelper", (provider, client) =>
{ {
var options = provider.GetRequiredService<IOptions<ContainerHelperOptions>>(); var options = provider.GetRequiredService<IOptions<ContainerHelperOptions>>();
client.BaseAddress = new Uri(options.Value.IsEnabled ? options.Value.Url : "http://you-should-fail.invalid"); client.BaseAddress =
new Uri(options.Value.IsEnabled ? options.Value.Url : "http://you-should-fail.invalid");
}); });
// User management services
builder.Services.AddScoped<UserDeletionService>(); builder.Services.AddScoped<UserDeletionService>();
builder.Services.AddScoped<UserLogoutService>(); builder.Services.AddScoped<UserLogoutService>();
// Settings options
builder.Services.AddOptions<SettingsOptions>().BindConfiguration("Moonlight:Settings");
builder.Services.AddScoped<SettingsService>();
// Setup key loading
var keysDirectory = new DirectoryInfo(Path.Combine("storage", "keys"));
builder.Services.AddDataProtection().PersistKeysToFileSystem(keysDirectory);
} }
private static void UseBase(WebApplication application) private static void UseBase(WebApplication application)
@@ -68,7 +88,7 @@ public partial class Startup
var options = application.Services.GetRequiredService<IOptions<FrontendOptions>>(); var options = application.Services.GetRequiredService<IOptions<FrontendOptions>>();
if(options.Value.Enabled) if (options.Value.Enabled)
application.MapFallbackToFile("index.html"); application.MapFallbackToFile("index.html");
} }
} }

View File

@@ -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:";
});
}
}

View File

@@ -9,6 +9,7 @@ public partial class Startup : IAppStartup
AddBase(builder); AddBase(builder);
AddAuth(builder); AddAuth(builder);
AddDatabase(builder); AddDatabase(builder);
AddCache(builder);
} }
public void PostBuild(WebApplication application) public void PostBuild(WebApplication application)