From bc4b04e3782ebad699a6bfe9820114c82665abf2 Mon Sep 17 00:00:00 2001 From: honzapatCZ Date: Sat, 12 Sep 2026 14:20:33 +0200 Subject: [PATCH] Optiize binder and api key retrieval caching --- Models/Api/ModelBinder.cs | 72 +++++++++++++++++++++++--------------- Services/ApiKeyProvider.cs | 27 +++++++++----- 2 files changed, 62 insertions(+), 37 deletions(-) diff --git a/Models/Api/ModelBinder.cs b/Models/Api/ModelBinder.cs index 03efa01..0bab211 100644 --- a/Models/Api/ModelBinder.cs +++ b/Models/Api/ModelBinder.cs @@ -83,38 +83,34 @@ namespace NejCommon.Models throw new ArgumentNullException(nameof(bindingContext)); } using var activity = Telemetry.Start(); - + var cancellationToken = bindingContext.HttpContext.RequestAborted; cancellationToken.ThrowIfCancellationRequested(); //get the binding parameter attributes // Fetch the route values from the route data - var capturedRouteData = new Dictionary(); - foreach (var routeValue in _routeValues) + var values = new string[_routeValues.Length]; + for (var i = 0; i < _routeValues.Length; i++) { - var name = routeValue.Name; - if (routeValue.Primary && !string.IsNullOrWhiteSpace(bindingContext.ModelName)) - { - name = bindingContext.ModelName; - } + var routeValue = _routeValues[i]; - /* - Console.WriteLine("Route value: " + name); - Console.WriteLine(); - Console.WriteLine(bindingContext.ActionContext.RouteData.Values[name]); - */ + var name = + routeValue.Primary && !string.IsNullOrWhiteSpace(bindingContext.ModelName) + ? bindingContext.ModelName + : routeValue.Name; - var value = bindingContext.ValueProvider.GetValue(name).FirstOrDefault();//bindingContext.ActionContext.RouteData.Values[name]; - - if (value is null) + // If these really are route values, don't go through the entire + // MVC ValueProvider infrastructure. + if (!bindingContext.ActionContext.RouteData.Values.TryGetValue(name, out var rawValue) || + rawValue is null) { bindingContext.ModelState.AddModelError(name, "Route value not found"); + bindingContext.Result = ModelBindingResult.Failed(); + return; } - else - { - capturedRouteData[routeValue.Name] = value; - } + + values[i] = rawValue.ToString()!; } // Check if all the required route values are present @@ -124,21 +120,39 @@ namespace NejCommon.Models return; } + var entityParameter = Expression.Parameter(typeof(TEntity), "entity"); + // Find the entity with the specified route values - var query = _db.Set().AsQueryable(); - foreach (var routeValue in _routeValues) + Expression? predicateBody = null; + for (var i = 0; i < _routeValues.Length; i++) { - var value = capturedRouteData[routeValue.Name]; + var routeValue = _routeValues[i]; - var par = Expression.Parameter(typeof(TEntity)); - var exp = Expression.Invoke(routeValue.Expression, par, Expression.Constant((string)value)); - var redExp = exp.Reduce(); - var compiledExp = Expression.Lambda>(redExp, par); + // IMPORTANT: + // captured variable -> EF parameter + // rather than Expression.Constant(value) + var value = values[i]; - query = query.Where(compiledExp); + Expression> valueExpression = () => value; + + var expression = Expression.Invoke( + routeValue.Expression, + entityParameter, + valueExpression.Body); + + var body = expression.Reduce(); + + predicateBody = predicateBody is null + ? body + : Expression.AndAlso(predicateBody, body); } - var model = await query.FirstOrDefaultAsync(cancellationToken); + var predicate = + Expression.Lambda>( + predicateBody!, + entityParameter); + + var model = await _db.Set().AsQueryable().FirstOrDefaultAsync(predicate, cancellationToken); if (model == null) { diff --git a/Services/ApiKeyProvider.cs b/Services/ApiKeyProvider.cs index aaf24f3..99356ca 100644 --- a/Services/ApiKeyProvider.cs +++ b/Services/ApiKeyProvider.cs @@ -1,17 +1,23 @@ using AspNetCore.Authentication.ApiKey; using Microsoft.EntityFrameworkCore; using NejCommon.Models; +using NejCommon.Utils; +using ZiggyCreatures.Caching.Fusion; namespace NejCommon.Services; public partial class ApiKeyProvider : IApiKeyProvider, IScopedService { private readonly ILogger _logger; + private readonly IFusionCache _cache; - public async Task ProvideAsync(string key) + public async Task ProvideAsync(string key) { - try + using var activity = Telemetry.Start("ApiKeyProvider.ProvideAsync"); + + return await _cache.GetOrSetAsync($"ApiKeyProvider.ProvideAsync.{key}", async entry => { + // write your validation implementation here and return an instance of a valid ApiKey or retun null for an invalid key. var lookup = ApiKey.ComputeLookupHash(key); var apiKey = await GetApiKeySet().FirstOrDefaultAsync(x => x.LookupHash.SequenceEqual(lookup)); @@ -25,17 +31,22 @@ public partial class ApiKeyProvider : IApiKeyProvider, IScopedService return null; } + if (apiKey.Revoked) + { + return null; + } + apiKey.Key = key; - // write your validation implementation here and return an instance of a valid ApiKey or retun null for an invalid key. // return await _apiKeyRepository.GetApiKeyAsync(key); return apiKey; - } - catch (System.Exception exception) + }, new FusionCacheEntryOptions() { - _logger.LogError(exception, exception.Message); - throw; - } + Duration = TimeSpan.FromMinutes(1), + IsFailSafeEnabled = true, + FailSafeMaxDuration = TimeSpan.FromMinutes(5), + }); + } public static string GenerateKey() => "nej-" + NanoidDotNet.Nanoid.Generate(size: 256);