Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 12 additions & 0 deletions Dan.Core/Middleware/DiagnosticsHeaderInjectionMiddleware.cs
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
using Dan.Core.Helpers;
using Dan.Core.Services.Interfaces;
using Microsoft.Azure.Functions.Worker;
using Microsoft.Azure.Functions.Worker.Middleware;
using Microsoft.Extensions.DependencyInjection;

namespace Dan.Core.Middleware;

Expand All @@ -22,5 +24,15 @@ public async Task Invoke(FunctionContext context, FunctionExecutionDelegate next
}

response.Headers.Add("x-version", _versionInfo);

// Check if we have some additional headers set via RequestContextService
var requestContextService = context.InstanceServices.GetService<IRequestContextService>();
if (requestContextService is not null)
{
foreach (var (header, value) in requestContextService.CustomResponseHeaders)
{
response.Headers.Add(header, value);
}
}
}
}
31 changes: 31 additions & 0 deletions Dan.Core/Middleware/FunctionContextAccessorMiddleware.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
using Dan.Core.Services.Interfaces;
using Microsoft.Azure.Functions.Worker.Middleware;
using Microsoft.Azure.Functions.Worker;

// Copied from https://gist.github.com/dolphinspired/796d26ebe1237b78ee04a3bff0620ea0
// See also https://github.com/Azure/azure-functions-dotnet-worker/issues/950

namespace Dan.Core.Middleware;
public class FunctionContextAccessorMiddleware : IFunctionsWorkerMiddleware
{
private IFunctionContextAccessor FunctionContextAccessor { get; }

public FunctionContextAccessorMiddleware(IFunctionContextAccessor accessor)
{
FunctionContextAccessor = accessor;
}

public Task Invoke(FunctionContext context, FunctionExecutionDelegate next)
{
if (FunctionContextAccessor.FunctionContext != null)
{
// This should never happen because the context should be localized to the current Task chain.
// But if it does happen (perhaps the implementation is bugged), then we need to know immediately so it can be fixed.
throw new InvalidOperationException($"Unable to initalize {nameof(IFunctionContextAccessor)}: context has already been initialized.");
}

FunctionContextAccessor.FunctionContext = context;

return next(context);
}
}
2 changes: 2 additions & 0 deletions Dan.Core/Program.cs
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,7 @@
.UseWhen<AuthenticationMiddleware>(context => !context.HasAttribute(typeof(NoAuthenticationAttribute)));

builder.UseMiddleware<DiagnosticsHeaderInjectionMiddleware>();
builder.UseMiddleware<FunctionContextAccessorMiddleware>();

if (!danHostingEnvironment.IsLocalDevelopment())
{
Expand Down Expand Up @@ -118,6 +119,7 @@
services.AddSingleton<IAccreditationRepository, CosmosDbAccreditationRepository>();
services.AddSingleton<IEntityRegistryService, EntityRegistryService>();
services.AddSingleton<IEntityRegistryApiClientService, CachingEntityRegistryApiClientService>();
services.AddSingleton<IFunctionContextAccessor, FunctionContextAccessor>();

services.AddScoped<IEvidenceStatusService, EvidenceStatusService>();
services.AddScoped<IEvidenceHarvesterService, EvidenceHarvesterService>();
Expand Down
36 changes: 33 additions & 3 deletions Dan.Core/Services/AvailableEvidenceCodesService.cs
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
using System.Text;
using Dan.Core.Helpers;
using Microsoft.Extensions.Caching.Distributed;
using Microsoft.Extensions.DependencyInjection;

namespace Dan.Core.Services;

Expand All @@ -26,19 +27,28 @@ public class AvailableEvidenceCodesService : IAvailableEvidenceCodesService
private DateTime _updateMemoryCache = DateTime.MinValue;
private readonly SemaphoreSlim _semaphoreForceRefresh = new(1, 1);
private readonly SemaphoreSlim _semaphore = new(1, 1);
private readonly IFunctionContextAccessor _functionContextAccessor;
private const int MemoryCacheTtlSeconds = 120;

private const string CachingPolicy = "EvidenceCodesCachePolicy";
private const string HttpClientName = "EvidenceCodesClient";
private const string CacheContextKey = "AvailableEvidenceCodes";

public AvailableEvidenceCodesService(ILoggerFactory loggerFactory, IHttpClientFactory httpClientFactory, IPolicyRegistry<string> policyRegistry, IDistributedCache distributedCache, IServiceContextService serviceContextService)
private const string CacheResponseHeader = "x-cache";

public AvailableEvidenceCodesService(
ILoggerFactory loggerFactory,
IHttpClientFactory httpClientFactory,
IPolicyRegistry<string> policyRegistry,
IDistributedCache distributedCache,
IServiceContextService serviceContextService,
IFunctionContextAccessor functionContextAccessor)
{
_logger = loggerFactory.CreateLogger<AvailableEvidenceCodesService>();
_httpClientFactory = httpClientFactory;
_policyRegistry = policyRegistry;
_distributedCache = distributedCache;
_serviceContextService = serviceContextService;
_functionContextAccessor = functionContextAccessor;
}

/// <summary>
Expand All @@ -52,6 +62,7 @@ public async Task<List<EvidenceCode>> GetAvailableEvidenceCodes(bool forceRefres
// Cache still valid
if (!forceRefresh && DateTime.UtcNow < _updateMemoryCache)
{
SetCacheDiagnosticsHeader("hit-local");
return FilterInactive(_memoryCache);
}

Expand Down Expand Up @@ -79,6 +90,7 @@ public async Task<List<EvidenceCode>> GetAvailableEvidenceCodes(bool forceRefres
// Recheck if another thread has updated the memory cache while we were waiting for the semaphore
if (DateTime.UtcNow < _updateMemoryCache)
{
SetCacheDiagnosticsHeader("hit-local-late");
return FilterInactive(_memoryCache);
}

Expand All @@ -95,13 +107,29 @@ public async Task<List<EvidenceCode>> GetAvailableEvidenceCodes(bool forceRefres
}
}

private void SetCacheDiagnosticsHeader(string value, bool overwrite = false)
{
var requestContextService = _functionContextAccessor.FunctionContext.InstanceServices.GetService<IRequestContextService>();
if (requestContextService == null) return;
if (overwrite)
{
requestContextService.CustomResponseHeaders[CacheResponseHeader] = value;
}
else
{
requestContextService.CustomResponseHeaders.TryAdd(CacheResponseHeader, value);
}

}

/// <summary>
/// This fetches evidence codes from the sources and updates the distributed and in-memory caches.
/// </summary>
/// <returns>Nothing</returns>
private async Task RefreshEvidenceCodesCache()
{
var evidenceCodes = await GetAvailableEvidenceCodesFromEvidenceSources();
SetCacheDiagnosticsHeader("force-evict");
if (evidenceCodes.Count == 0)
{
_logger.LogWarning("Failed to refresh evidence codes cache, received empty list");
Expand All @@ -111,7 +139,7 @@ private async Task RefreshEvidenceCodesCache()
// Add some metadata properties to make serialized output more parseable
foreach (var es in evidenceCodes)
{
es.AuthorizationRequirements?.ForEach(x => x.RequirementType = x.GetType().Name);
es.AuthorizationRequirements.ForEach(x => x.RequirementType = x.GetType().Name);
}

await _distributedCache.SetAsync(CacheContextKey, Encoding.UTF8.GetBytes(JsonConvert.SerializeObject(
Expand All @@ -131,13 +159,15 @@ await _distributedCache.SetAsync(CacheContextKey, Encoding.UTF8.GetBytes(JsonCon

private async Task<List<EvidenceCode>> GetAvailableEvidenceCodesFromDistributedCache()
{
SetCacheDiagnosticsHeader("hit-distributed");
var cachePolicy = _policyRegistry.Get<AsyncPolicy<List<EvidenceCode>>>(CachingPolicy);
return await cachePolicy.ExecuteAsync(
async _ => await GetAvailableEvidenceCodesFromEvidenceSources(), new Context(CacheContextKey));
}

private async Task<List<EvidenceCode>> GetAvailableEvidenceCodesFromEvidenceSources()
{
SetCacheDiagnosticsHeader("miss", overwrite: true);
using (var _ = _logger.Timer($"availableevidence-cache-refresh"))
{
var sources = GetEvidenceSources();
Expand Down
31 changes: 31 additions & 0 deletions Dan.Core/Services/FunctionContextAccessor.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
using Dan.Core.Services.Interfaces;
using Microsoft.Azure.Functions.Worker;

namespace Dan.Core.Services;
public class FunctionContextAccessor : IFunctionContextAccessor
{
private static readonly AsyncLocal<FunctionContextRedirect> CurrentContext = new();

public virtual FunctionContext FunctionContext
{
get => CurrentContext.Value!.HeldContext!;
set
{
var holder = CurrentContext.Value;
if (holder != null)
{
// Clear current context trapped in the AsyncLocals, as its done.
holder.HeldContext = null;
}

// Use an object indirection to hold the context in the AsyncLocal,
// so it can be cleared in all ExecutionContexts when its cleared.
CurrentContext.Value = new FunctionContextRedirect { HeldContext = value };
}
}

private class FunctionContextRedirect
{
public FunctionContext? HeldContext;
}
}
7 changes: 7 additions & 0 deletions Dan.Core/Services/Interfaces/IFunctionContextAccessor.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
using Microsoft.Azure.Functions.Worker;

namespace Dan.Core.Services.Interfaces;
public interface IFunctionContextAccessor
{
FunctionContext FunctionContext { get; set; }
}
4 changes: 3 additions & 1 deletion Dan.Core/Services/Interfaces/IRequestContextService.cs
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
using System.Collections.Concurrent;
using Dan.Common.Models;
using Microsoft.Azure.Functions.Worker.Http;

Expand All @@ -10,6 +11,7 @@ public interface IRequestContextService
List<string>? Scopes { get; set; }
ServiceContext ServiceContext { get; set; }
HttpRequestData? Request { get; set; }
Task BuildRequestContext(HttpRequestData request);
ConcurrentDictionary<string, string> CustomResponseHeaders { get; set; }
public Task BuildRequestContext(HttpRequestData request);
EvidenceHarvesterOptions GetEvidenceHarvesterOptionsFromRequest();
}
2 changes: 2 additions & 0 deletions Dan.Core/Services/RequestContextService.cs
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
using System.Collections.Concurrent;
using Dan.Common.Models;
using Dan.Core.Exceptions;
using Dan.Core.Extensions;
Expand All @@ -15,6 +16,7 @@ class RequestContextService : IRequestContextService
public List<string>? Scopes { get; set; }
public ServiceContext ServiceContext { get; set; } = new();
public HttpRequestData? Request { get; set; }
public ConcurrentDictionary<string, string> CustomResponseHeaders { get; set; } = new();

public const string ServicecontextHeader = "X-NADOBE-SERVICECONTEXT";
public const string QueryParamTokenOnBehalfOfOwner = "tokenOnBehalfOfOwner";
Expand Down