Introduce ThemeService to manage themes and dark mode state, including initialization, persistence, and server sync. Add UserPreferencesService to handle API communication for retrieving and saving user preferences with in-memory caching to reduce network overhead. Register both services in the DI container in Program.cs. Enhance ThemeService with detailed documentation and introduce ThemeOption record for selectable themes. Improve server-side caching in UserPreferencesService for consistency and efficiency.
131 lines
5.1 KiB
C#
131 lines
5.1 KiB
C#
using System.Net.Http.Json;
|
|
using System.Text.Json;
|
|
using EnvelopeGenerator.Application.Common.Dto;
|
|
using Microsoft.Extensions.Logging;
|
|
|
|
namespace EnvelopeGenerator.Server.Client.Services;
|
|
|
|
/// <summary>
|
|
/// Retrieves and persists user UI preferences via the server API.
|
|
/// The server stores preferences in the distributed cache (dbo.TBDD_CACHE).
|
|
///
|
|
/// Client-side memory cache:
|
|
/// Preferences are cached in-memory for <see cref="CacheDuration"/> after the first fetch.
|
|
/// All reads (GetAsync, GetCachedAsync) are served from cache after the first load.
|
|
/// Writes (SaveAsync) update both the cache and the server atomically.
|
|
/// Cache is invalidated explicitly via InvalidateCache().
|
|
/// </summary>
|
|
public class UserPreferencesService(
|
|
IHttpClientFactory clientFactory,
|
|
ILogger<UserPreferencesService> logger)
|
|
{
|
|
private static readonly JsonSerializerOptions _jsonOptions = new(JsonSerializerDefaults.Web);
|
|
private const string Endpoint = "/api/UserPreferences";
|
|
|
|
// ── Client-side memory cache ──────────────────────────────────────────────
|
|
private static readonly TimeSpan CacheDuration = TimeSpan.FromHours(1);
|
|
private UserPreferencesDto? _cached;
|
|
private DateTimeOffset _cachedAt = DateTimeOffset.MinValue;
|
|
|
|
private bool IsCacheValid => _cached is not null
|
|
&& DateTimeOffset.UtcNow - _cachedAt < CacheDuration;
|
|
|
|
/// <summary>
|
|
/// Returns preferences from in-memory cache if valid; otherwise fetches from the server.
|
|
/// On first call after login this issues one HTTP GET — all subsequent calls within
|
|
/// <see cref="CacheDuration"/> are served from memory with zero network overhead.
|
|
/// </summary>
|
|
public async Task<UserPreferencesDto> GetAsync(CancellationToken ct = default)
|
|
{
|
|
if (IsCacheValid)
|
|
return _cached!;
|
|
|
|
try
|
|
{
|
|
var client = clientFactory.CreateClient("EnvelopeGenerator.Server");
|
|
var response = await client.GetAsync(Endpoint, ct);
|
|
|
|
// 404 means no preferences saved yet — return and cache defaults.
|
|
if (response.StatusCode == System.Net.HttpStatusCode.NotFound)
|
|
{
|
|
SetCache(new UserPreferencesDto());
|
|
return _cached!;
|
|
}
|
|
|
|
response.EnsureSuccessStatusCode();
|
|
|
|
var dto = await response.Content.ReadFromJsonAsync<UserPreferencesDto>(_jsonOptions, ct)
|
|
?? new UserPreferencesDto();
|
|
|
|
SetCache(dto);
|
|
return _cached!;
|
|
}
|
|
catch (Exception ex) when (ex is not TaskCanceledException)
|
|
{
|
|
logger.LogError(ex, "Failed to load user preferences from server.");
|
|
throw;
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Returns the cached preferences without hitting the network.
|
|
/// Returns null if the cache has not been populated yet (before first GetAsync call).
|
|
/// </summary>
|
|
public UserPreferencesDto? GetCached() => IsCacheValid ? _cached : null;
|
|
|
|
/// <summary>
|
|
/// Persists the given preferences on the server and updates the in-memory cache.
|
|
/// </summary>
|
|
public async Task<UserPreferencesDto> SaveAsync(UserPreferencesDto preferences, CancellationToken ct = default)
|
|
{
|
|
try
|
|
{
|
|
var client = clientFactory.CreateClient("EnvelopeGenerator.Server");
|
|
var response = await client.PutAsJsonAsync(Endpoint, preferences, _jsonOptions, ct);
|
|
response.EnsureSuccessStatusCode();
|
|
|
|
var saved = await response.Content.ReadFromJsonAsync<UserPreferencesDto>(_jsonOptions, ct)
|
|
?? preferences;
|
|
|
|
SetCache(saved);
|
|
return saved;
|
|
}
|
|
catch (Exception ex) when (ex is not TaskCanceledException)
|
|
{
|
|
logger.LogError(ex, "Failed to save user preferences to server.");
|
|
throw;
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Updates a single grid layout key without a network read.
|
|
/// Uses the in-memory cache for the current state, merges the new layout, then saves.
|
|
/// If the cache is empty (cold start), fetches from server first.
|
|
/// </summary>
|
|
/// <param name="gridKey">Stable grid identifier, e.g. "sender.active-envelopes".</param>
|
|
/// <param name="layoutJson">Opaque layout JSON produced by DxGrid.SaveLayoutAsync().</param>
|
|
public async Task SaveGridLayoutAsync(string gridKey, string layoutJson, CancellationToken ct = default)
|
|
{
|
|
// Use cached copy — avoids a GET before every grid layout save.
|
|
var prefs = GetCached() ?? await GetAsync(ct);
|
|
prefs.GridLayouts[gridKey] = layoutJson;
|
|
await SaveAsync(prefs, ct);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Invalidates the in-memory cache, forcing the next GetAsync to fetch from the server.
|
|
/// Call this after logout or when a forced refresh is needed.
|
|
/// </summary>
|
|
public void InvalidateCache()
|
|
{
|
|
_cached = null;
|
|
_cachedAt = DateTimeOffset.MinValue;
|
|
}
|
|
|
|
private void SetCache(UserPreferencesDto dto)
|
|
{
|
|
_cached = dto;
|
|
_cachedAt = DateTimeOffset.UtcNow;
|
|
}
|
|
}
|