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
Original file line number Diff line number Diff line change
Expand Up @@ -77,35 +77,70 @@ public static IServiceCollection AddJwtBearerEnricher(
});
/// Adds the telemetry interceptor (order -50).
public static IServiceCollection AddTelemetryInterceptor(this IServiceCollection services)
{
services.TryAddSingleton(new ActivitySource("VisionaryCoder.Framework.Proxy"));
services.TryAddTransient<IOrderedProxyInterceptor, TelemetryInterceptor>();
return services;
}

/// Adds the correlation interceptor (order 0).
public static IServiceCollection AddCorrelationInterceptor(this IServiceCollection services)
{
services.TryAddSingleton<VisionaryCoder.Framework.Proxy.Abstractions.ICorrelationContext, DefaultCorrelationContext>();
services.TryAddSingleton<VisionaryCoder.Framework.Proxy.Abstractions.ICorrelationIdGenerator, GuidCorrelationIdGenerator>();
services.TryAddTransient<IOrderedProxyInterceptor, CorrelationInterceptor>();
return services;
}

/// Adds the logging interceptor (order 100).
public static IServiceCollection AddLoggingInterceptor(this IServiceCollection services)
{
services.TryAddTransient<IOrderedProxyInterceptor, LoggingInterceptor>();
return services;
}

/// Adds the caching interceptor (order 150).
public static IServiceCollection AddCachingInterceptor(this IServiceCollection services)
{
services.TryAddTransient<IOrderedProxyInterceptor, CachingInterceptor>();
return services;
}

/// Adds a proxy cache implementation.
public static IServiceCollection AddProxyCache<TCache>(this IServiceCollection services)
where TCache : class, IProxyCache
{
services.TryAddSingleton<IProxyCache, TCache>();
return services;
}

/// Adds the resilience interceptor (order 180).
public static IServiceCollection AddResilienceInterceptor(this IServiceCollection services)
{
services.TryAddTransient<IOrderedProxyInterceptor, ResilienceInterceptor>();
return services;
}

/// Adds the retry interceptor (order 200).
public static IServiceCollection AddRetryInterceptor(this IServiceCollection services)
{
services.TryAddTransient<IOrderedProxyInterceptor, RetryInterceptor>();
return services;
}

/// Adds the auditing interceptor (order 300).
public static IServiceCollection AddAuditingInterceptor(this IServiceCollection services)
{
services.TryAddTransient<IAuditSink, LoggingAuditSink>();
services.TryAddTransient<IOrderedProxyInterceptor, VisionaryCoder.Framework.Proxy.Interceptors.Auditing.AuditingInterceptor>();
return services;
}

/// Adds an audit sink.
public static IServiceCollection AddAuditSink<TSink>(this IServiceCollection services)
where TSink : class, IAuditSink
{
services.TryAddTransient<IAuditSink, TSink>();
return services;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -24,13 +24,16 @@ public ResilienceInterceptor(ILogger<ResilienceInterceptor> logger, ResiliencePi
this.logger = logger ?? throw new ArgumentNullException(nameof(logger));
this.resiliencePipeline = resiliencePipeline ?? CreateDefaultPipeline();
}
/// <summary>
/// Invokes the interceptor with resilience protection.
/// </summary>
/// <typeparam name="T">The type of the response data.</typeparam>
/// <param name="context">The proxy context.</param>
/// <param name="next">The next delegate in the pipeline.</param>
/// <param name="cancellationToken">The cancellation token to monitor for cancellation requests.</param>
/// <returns>A task representing the asynchronous operation with the response.</returns>
public async Task<Response<T>> InvokeAsync<T>(ProxyContext context, ProxyDelegate<T> next, CancellationToken cancellationToken = default)
{
var operationName = context.OperationName ?? "Unknown";
var correlationId = context.CorrelationId ?? "Undefined";
try
Expand All @@ -42,12 +45,19 @@ public async Task<Response<T>> InvokeAsync<T>(ProxyContext context, ProxyDelegat
return response;
}
catch (Exception ex)
{
context.Metadata["ResilienceException"] = ex.GetType().Name;
logger.LogError(ex, "Resilience pipeline failed for operation '{OperationName}'. Correlation ID: '{CorrelationId}'", operationName, correlationId);
throw;
}
}

/// <summary>
/// Creates a default resilience pipeline with retry and circuit breaker.
/// </summary>
/// <returns>A configured resilience pipeline.</returns>
private static ResiliencePipeline CreateDefaultPipeline()
{
return new ResiliencePipelineBuilder()
.AddRetry(new()
{
Expand All @@ -57,10 +67,13 @@ private static ResiliencePipeline CreateDefaultPipeline()
UseJitter = true
})
.AddCircuitBreaker(new()
{
FailureRatio = 0.5,
SamplingDuration = TimeSpan.FromSeconds(30),
MinimumThroughput = 5,
BreakDuration = TimeSpan.FromMinutes(1)
})
.AddTimeout(TimeSpan.FromSeconds(30))
.Build();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ public async Task<Response<T>> InvokeAsync<T>(
ProxyContext context,
ProxyDelegate<T> next,
CancellationToken cancellationToken = default)
{
using var _ = logger.BeginScope("SecurityInterceptor for {RequestType}", context.Request?.GetType().Name ?? "Unknown");

try
Expand All @@ -45,15 +46,20 @@ public async Task<Response<T>> InvokeAsync<T>(
}
// Check authorization policies
foreach (var policy in policies)
{
if (!await policy.IsAuthorizedAsync(context, cancellationToken))
{
logger.LogWarning("Authorization failed for policy {PolicyType}", policy.GetType().Name);
return Response<T>.Failure("Authorization failed");
}
}
logger.LogDebug("Security validation passed, proceeding to next interceptor");
return await next(context, cancellationToken);
}
catch (Exception ex) when (ex is not ProxyException)
{
logger.LogError(ex, "Unexpected error during security processing");
return Response<T>.Failure("Security processing failed");
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -26,17 +26,39 @@ public static IServiceCollection AddJwtBearerInterceptor(
});
return services;
}
/// <summary>
/// Adds the JWT Bearer interceptor that retrieves tokens from a secret provider.
/// </summary>
/// <param name="services">The service collection to add the interceptor to.</param>
/// <param name="secretName">The name of the secret containing the JWT token.</param>
/// <returns>The service collection for chaining.</returns>
public static IServiceCollection AddJwtBearerInterceptorFromSecret(
this IServiceCollection services,
string secretName)
{
services.AddSingleton<IProxyInterceptor>(provider =>
{
var secretProvider = provider.GetRequiredService<ISecretProvider>();
var logger = provider.GetRequiredService<ILogger<JwtBearerInterceptor>>();
Func<CancellationToken, Task<string?>> tokenProvider = async (cancellationToken) =>
{
return await secretProvider.GetAsync(secretName, cancellationToken);
};
return new JwtBearerInterceptor(logger, tokenProvider);
});
return services;
}

/// <summary>
/// Adds the JWT Bearer interceptor with a static token (useful for development).
/// </summary>
/// <param name="services">The service collection to add the interceptor to.</param>
/// <param name="staticToken">The static JWT token to use.</param>
/// <returns>The service collection for chaining.</returns>
public static IServiceCollection AddJwtBearerInterceptorWithStaticToken(
this IServiceCollection services,
string staticToken)
{
return services.AddJwtBearerInterceptor((cancellationToken) => Task.FromResult<string?>(staticToken));
}
}