Optiize binder and api key retrieval caching

This commit is contained in:
honzapatCZ 2026-09-12 14:20:33 +02:00
parent c5a9db661b
commit bc4b04e378
2 changed files with 62 additions and 37 deletions

View File

@ -90,31 +90,27 @@ namespace NejCommon.Models
//get the binding parameter attributes //get the binding parameter attributes
// Fetch the route values from the route data // Fetch the route values from the route data
var capturedRouteData = new Dictionary<string, object>(); var values = new string[_routeValues.Length];
foreach (var routeValue in _routeValues) for (var i = 0; i < _routeValues.Length; i++)
{ {
var name = routeValue.Name; var routeValue = _routeValues[i];
if (routeValue.Primary && !string.IsNullOrWhiteSpace(bindingContext.ModelName))
{
name = bindingContext.ModelName;
}
/* var name =
Console.WriteLine("Route value: " + name); routeValue.Primary && !string.IsNullOrWhiteSpace(bindingContext.ModelName)
Console.WriteLine(); ? bindingContext.ModelName
Console.WriteLine(bindingContext.ActionContext.RouteData.Values[name]); : routeValue.Name;
*/
var value = bindingContext.ValueProvider.GetValue(name).FirstOrDefault();//bindingContext.ActionContext.RouteData.Values[name]; // If these really are route values, don't go through the entire
// MVC ValueProvider infrastructure.
if (value is null) if (!bindingContext.ActionContext.RouteData.Values.TryGetValue(name, out var rawValue) ||
rawValue is null)
{ {
bindingContext.ModelState.AddModelError(name, "Route value not found"); bindingContext.ModelState.AddModelError(name, "Route value not found");
bindingContext.Result = ModelBindingResult.Failed();
return;
} }
else
{ values[i] = rawValue.ToString()!;
capturedRouteData[routeValue.Name] = value;
}
} }
// Check if all the required route values are present // Check if all the required route values are present
@ -124,21 +120,39 @@ namespace NejCommon.Models
return; return;
} }
var entityParameter = Expression.Parameter(typeof(TEntity), "entity");
// Find the entity with the specified route values // Find the entity with the specified route values
var query = _db.Set<TEntity>().AsQueryable(); Expression? predicateBody = null;
foreach (var routeValue in _routeValues) for (var i = 0; i < _routeValues.Length; i++)
{ {
var value = capturedRouteData[routeValue.Name]; var routeValue = _routeValues[i];
var par = Expression.Parameter(typeof(TEntity)); // IMPORTANT:
var exp = Expression.Invoke(routeValue.Expression, par, Expression.Constant((string)value)); // captured variable -> EF parameter
var redExp = exp.Reduce(); // rather than Expression.Constant(value)
var compiledExp = Expression.Lambda<Func<TEntity, bool>>(redExp, par); var value = values[i];
query = query.Where(compiledExp); Expression<Func<string>> 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<Func<TEntity, bool>>(
predicateBody!,
entityParameter);
var model = await _db.Set<TEntity>().AsQueryable().FirstOrDefaultAsync(predicate, cancellationToken);
if (model == null) if (model == null)
{ {

View File

@ -1,17 +1,23 @@
using AspNetCore.Authentication.ApiKey; using AspNetCore.Authentication.ApiKey;
using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore;
using NejCommon.Models; using NejCommon.Models;
using NejCommon.Utils;
using ZiggyCreatures.Caching.Fusion;
namespace NejCommon.Services; namespace NejCommon.Services;
public partial class ApiKeyProvider : IApiKeyProvider, IScopedService public partial class ApiKeyProvider : IApiKeyProvider, IScopedService
{ {
private readonly ILogger<ApiKeyProvider> _logger; private readonly ILogger<ApiKeyProvider> _logger;
private readonly IFusionCache _cache;
public async Task<IApiKey> ProvideAsync(string key) public async Task<IApiKey?> 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 lookup = ApiKey.ComputeLookupHash(key);
var apiKey = await GetApiKeySet().FirstOrDefaultAsync(x => x.LookupHash.SequenceEqual(lookup)); var apiKey = await GetApiKeySet().FirstOrDefaultAsync(x => x.LookupHash.SequenceEqual(lookup));
@ -25,17 +31,22 @@ public partial class ApiKeyProvider : IApiKeyProvider, IScopedService
return null; return null;
} }
if (apiKey.Revoked)
{
return null;
}
apiKey.Key = key; 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 await _apiKeyRepository.GetApiKeyAsync(key);
return apiKey; return apiKey;
} }, new FusionCacheEntryOptions()
catch (System.Exception exception)
{ {
_logger.LogError(exception, exception.Message); Duration = TimeSpan.FromMinutes(1),
throw; IsFailSafeEnabled = true,
} FailSafeMaxDuration = TimeSpan.FromMinutes(5),
});
} }
public static string GenerateKey() => "nej-" + NanoidDotNet.Nanoid.Generate(size: 256); public static string GenerateKey() => "nej-" + NanoidDotNet.Nanoid.Generate(size: 256);