using System;
using System.Net;
using System.Net.Http;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.DependencyInjection.Extensions;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
using Polly;
using Polly.Extensions.Http;
namespace StellaOps.Auth.Client;
///
/// DI helpers for the StellaOps auth client.
///
public static class ServiceCollectionExtensions
{
///
/// Registers the StellaOps auth client with the provided configuration.
///
public static IServiceCollection AddStellaOpsAuthClient(this IServiceCollection services, Action configure)
{
ArgumentNullException.ThrowIfNull(services);
ArgumentNullException.ThrowIfNull(configure);
services.AddOptions()
.Configure(configure)
.PostConfigure(static options => options.Validate());
services.TryAddSingleton();
services.AddHttpClient((provider, client) =>
{
var options = provider.GetRequiredService>().CurrentValue;
client.Timeout = options.HttpTimeout;
}).AddPolicyHandler(static (provider, _) => CreateRetryPolicy(provider));
services.AddHttpClient((provider, client) =>
{
var options = provider.GetRequiredService>().CurrentValue;
client.Timeout = options.HttpTimeout;
}).AddPolicyHandler(static (provider, _) => CreateRetryPolicy(provider));
services.AddHttpClient((provider, client) =>
{
var options = provider.GetRequiredService>().CurrentValue;
client.Timeout = options.HttpTimeout;
}).AddPolicyHandler(static (provider, _) => CreateRetryPolicy(provider));
return services;
}
///
/// Registers a file-backed token cache implementation.
///
public static IServiceCollection AddStellaOpsFileTokenCache(this IServiceCollection services, string cacheDirectory)
{
ArgumentNullException.ThrowIfNull(services);
ArgumentException.ThrowIfNullOrWhiteSpace(cacheDirectory);
services.Replace(ServiceDescriptor.Singleton(provider =>
{
var logger = provider.GetService>();
var options = provider.GetRequiredService>().CurrentValue;
return new FileTokenCache(cacheDirectory, TimeProvider.System, options.ExpirationSkew, logger);
}));
return services;
}
private static IAsyncPolicy CreateRetryPolicy(IServiceProvider provider)
{
var options = provider.GetRequiredService>().CurrentValue;
var delays = options.NormalizedRetryDelays;
if (delays.Count == 0)
{
return Policy.NoOpAsync();
}
var logger = provider.GetService()?.CreateLogger("StellaOps.Auth.Client.HttpRetry");
return HttpPolicyExtensions
.HandleTransientHttpError()
.OrResult(static response => response.StatusCode == HttpStatusCode.TooManyRequests)
.WaitAndRetryAsync(
delays.Count,
attempt => delays[attempt - 1],
(outcome, delay, attempt, _) =>
{
if (logger is null)
{
return;
}
if (outcome.Exception is not null)
{
logger.LogWarning(
outcome.Exception,
"Retrying Authority HTTP call ({Attempt}/{TotalAttempts}) after exception; waiting {Delay}.",
attempt,
delays.Count,
delay);
}
else
{
logger.LogWarning(
"Retrying Authority HTTP call ({Attempt}/{TotalAttempts}) due to status {StatusCode}; waiting {Delay}.",
attempt,
delays.Count,
outcome.Result!.StatusCode,
delay);
}
});
}
}