< Summary

Information
Class: Orchestrator.Infrastructure.ServiceRegistrationExtensions
Assembly: Orchestrator
File(s): /home/runner/work/KicktippAi/KicktippAi/src/Orchestrator/Infrastructure/ServiceRegistrationExtensions.cs
Line coverage
92%
Covered lines: 224
Uncovered lines: 19
Coverable lines: 243
Total lines: 685
Line coverage: 92.1%
Branch coverage
76%
Covered branches: 70
Total branches: 92
Branch coverage: 76%
Method coverage

Feature is only available for sponsors

Upgrade to PRO version

Metrics

MethodBranch coverage Crap Score Cyclomatic complexity Line coverage
.cctor()100%11100%
AddOrchestratorInfrastructure(...)100%44100%
AddOpenAiHttpClientIfMissing(...)100%88100%
<AddOpenAiHttpClientIfMissing()62.5%9880%
AddLangfusePublicApiClient(...)100%44100%
<AddLangfusePublicApiClient()50%88100%
<AddLangfusePublicApiClient()50%22100%
ShouldRetryUnsafeLangfuseRateLimit(...)60%352066.67%
AddLangfuseTracing(...)90%1010100%
BuildLangfuseOtlpHeaders(...)100%11100%
AddListKpiCommandServices(...)100%11100%
AddUploadKpiCommandServices(...)100%22100%
AddCostCommandServices(...)100%11100%
AddMatchdayCommandServices(...)100%11100%
AddRandomMatchCommandServices(...)100%11100%
AddBonusCommandServices(...)100%11100%
AddVerifyMatchdayCommandServices(...)100%11100%
AddFifaRankingSourceServicesIfMissing(...)100%66100%
AddWm26LineupSourceServicesIfMissing(...)100%44100%
AddVerifyBonusCommandServices(...)100%11100%
AddCollectContextKicktippCommandServices(...)100%11100%
AddCollectContextFifaCommandServices(...)100%11100%
AddCollectContextLineupsCommandServices(...)100%11100%
AddCollectContextDevCommandServices(...)100%11100%
AddWm26RecentHistoryCommandServices(...)100%11100%
AddContextChangesCommandServices(...)100%11100%
AddUploadTransfersCommandServices(...)100%22100%
AddUploadContextCommandServices(...)100%11100%
AddCopyFirestoreContextCommandServices(...)100%11100%
AddAnalyzeMatchDetailedCommandServices(...)100%11100%
AddAnalyzeMatchComparisonCommandServices(...)100%11100%
AddAllCommandServices(...)100%11100%

File(s)

/home/runner/work/KicktippAi/KicktippAi/src/Orchestrator/Infrastructure/ServiceRegistrationExtensions.cs

#LineLine coverage
 1using System.Net;
 2using System.Net.Http.Headers;
 3using System.Text;
 4using EHonda.KicktippAi.Core;
 5using Microsoft.Extensions.DependencyInjection;
 6using Microsoft.Extensions.DependencyInjection.Extensions;
 7using Microsoft.Extensions.FileProviders;
 8using Microsoft.Extensions.Http.Resilience;
 9using Microsoft.Extensions.Logging;
 10using OpenTelemetry.Exporter;
 11using OpenTelemetry.Resources;
 12using OpenTelemetry.Trace;
 13using OpenAiIntegration;
 14using Orchestrator.Commands.Operations.CollectContext;
 15using Orchestrator.Infrastructure.Factories;
 16using Orchestrator.Infrastructure.Langfuse;
 17using Orchestrator.Services;
 18
 19namespace Orchestrator.Infrastructure;
 20
 21/// <summary>
 22/// Extension methods for registering Orchestrator services in dependency injection.
 23/// </summary>
 24public static class ServiceRegistrationExtensions
 25{
 26    public const string OpenAiHttpClientName = "openai";
 27
 28    private const string LangfuseIngestionVersionHeaderName = "x-langfuse-ingestion-version";
 29    private const string LangfuseIngestionVersionHeaderValue = "4";
 130    private static readonly Uri FifaApiBaseAddress = new("https://api.fifa.com/api/v3/");
 31
 32    /// <summary>
 33    /// Registers all shared infrastructure services (factories, logging).
 34    /// </summary>
 35    /// <remarks>
 36    /// This method is idempotent - calling it multiple times has no additional effect.
 37    /// </remarks>
 38    public static IServiceCollection AddOrchestratorInfrastructure(
 39        this IServiceCollection services,
 40        LogLevel minimumLogLevel = LogLevel.Information)
 41    {
 42        // Add logging (idempotent via TryAdd internally)
 143        services.AddLogging(builder =>
 144        {
 145            builder.AddSimpleConsole(options =>
 146            {
 147                options.SingleLine = true;
 148                options.IncludeScopes = false;
 149                options.TimestampFormat = null;
 150                options.ColorBehavior = Microsoft.Extensions.Logging.Console.LoggerColorBehavior.Enabled;
 151            });
 152            builder.SetMinimumLevel(minimumLogLevel);
 153        });
 54
 55        // Add memory cache for Kicktipp client
 156        services.AddMemoryCache();
 57
 58        // Add HTTP client factory for Kicktipp
 159        services.AddHttpClient();
 160        services.AddOpenAiHttpClientIfMissing();
 61
 162        if (!services.Any(descriptor => descriptor.ServiceType == typeof(ILangfusePublicApiClient)))
 63        {
 164            services.AddLangfusePublicApiClient();
 65        }
 66
 67        // Register factories (idempotent)
 168        services.TryAddSingleton<IFirebaseServiceFactory, FirebaseServiceFactory>();
 169        services.TryAddSingleton<IKicktippClientFactory, KicktippClientFactory>();
 170        services.TryAddSingleton<IOpenAiServiceFactory, OpenAiServiceFactory>();
 171        services.TryAddSingleton<IContextProviderFactory, ContextProviderFactory>();
 172        services.TryAddSingleton<TimeProvider>(TimeProvider.System);
 173        services.TryAddTransient<MatchOutcomeCollectionService>();
 74
 75        // Register Langfuse/OTel tracing (no-op if credentials are absent)
 176        services.AddLangfuseTracing();
 77
 178        return services;
 79    }
 80
 81    private static IServiceCollection AddOpenAiHttpClientIfMissing(this IServiceCollection services)
 82    {
 183        if (services.Any(descriptor => descriptor.ServiceType == typeof(OpenAiHttpClientRegistrationMarker)))
 84        {
 185            return services;
 86        }
 87
 188        services.TryAddSingleton<OpenAiHttpClientRegistrationMarker>();
 89
 190        var clientBuilder = services.AddHttpClient(OpenAiHttpClientName, client =>
 191        {
 192            // OpenAI timeout ownership stays in ResponsesClientOptions.NetworkTimeout and
 193            // the .NET HTTP resilience pipeline. Keeping HttpClient.Timeout infinite
 194            // avoids a third timeout source racing those mechanisms.
 195            client.Timeout = Timeout.InfiniteTimeSpan;
 196        });
 97
 198        clientBuilder.AddStandardResilienceHandler().Configure(options =>
 199        {
 1100            var defaultCircuitBreakerShouldHandle = options.CircuitBreaker.ShouldHandle;
 1101
 1102            options.Retry.DisableForUnsafeHttpMethods();
 1103            options.CircuitBreaker.ShouldHandle = async args =>
 1104            {
 1105                if (!await defaultCircuitBreakerShouldHandle(args).ConfigureAwait(false))
 1106                {
 0107                    return false;
 1108                }
 1109
 1110                return args.Outcome.Result?.StatusCode is not HttpStatusCode.RequestTimeout
 1111                    and not HttpStatusCode.TooManyRequests;
 1112            };
 1113            options.AttemptTimeout.Timeout = TimeSpan.FromMinutes(15);
 1114            options.CircuitBreaker.SamplingDuration = TimeSpan.FromMinutes(30);
 1115            options.TotalRequestTimeout.Timeout = TimeSpan.FromMinutes(15);
 1116        });
 117
 1118        return services;
 119    }
 120
 121    internal static IHttpClientBuilder AddLangfusePublicApiClient(this IServiceCollection services)
 122    {
 1123        services.TryAddTransient<LangfuseRetryLoggingHandler>();
 124
 1125        var clientBuilder = services.AddHttpClient<ILangfusePublicApiClient, LangfusePublicApiClient>((_, client) =>
 1126        {
 1127            var baseUrl = (Environment.GetEnvironmentVariable("LANGFUSE_BASE_URL") ?? "https://cloud.langfuse.com").Trim
 1128            client.BaseAddress = new Uri($"{baseUrl}/api/public/");
 1129            client.Timeout = Timeout.InfiniteTimeSpan;
 1130            client.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json"));
 1131
 1132            var publicKey = Environment.GetEnvironmentVariable("LANGFUSE_PUBLIC_KEY");
 1133            var secretKey = Environment.GetEnvironmentVariable("LANGFUSE_SECRET_KEY");
 1134            if (!string.IsNullOrWhiteSpace(publicKey) && !string.IsNullOrWhiteSpace(secretKey))
 1135            {
 0136                var authorization = Convert.ToBase64String(Encoding.UTF8.GetBytes($"{publicKey}:{secretKey}"));
 0137                client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Basic", authorization);
 1138            }
 1139        });
 140
 1141        clientBuilder.AddStandardResilienceHandler().Configure(options =>
 1142        {
 1143            var defaultShouldHandle = options.Retry.ShouldHandle;
 1144            var defaultCircuitBreakerShouldHandle = options.CircuitBreaker.ShouldHandle;
 1145            options.Retry.DisableForUnsafeHttpMethods();
 1146            var safeMethodShouldHandle = options.Retry.ShouldHandle;
 1147
 1148            options.Retry.ShouldHandle = async args =>
 1149            {
 1150                if (await safeMethodShouldHandle(args).ConfigureAwait(false))
 1151                {
 1152                    return true;
 1153                }
 1154
 1155                if (!await defaultShouldHandle(args).ConfigureAwait(false))
 1156                {
 1157                    return false;
 1158                }
 1159
 1160                var response = args.Outcome.Result;
 1161                var request = response?.RequestMessage;
 1162                return response?.StatusCode == HttpStatusCode.TooManyRequests
 1163                    && request is not null
 1164                    && ShouldRetryUnsafeLangfuseRateLimit(request);
 1165            };
 1166
 1167            options.Retry.DelayGenerator = static args =>
 1168            {
 1169                var response = args.Outcome.Result;
 1170                if (response is null)
 1171                {
 0172                    return new ValueTask<TimeSpan?>((TimeSpan?)null);
 1173                }
 1174
 1175                var retryMetadata = LangfuseRetryAfterUtility.GetRetryAfterMetadata(response.Headers);
 1176                return new ValueTask<TimeSpan?>(retryMetadata.RetryAfterDelay);
 1177            };
 1178            options.CircuitBreaker.ShouldHandle = async args =>
 1179            {
 1180                if (!await defaultCircuitBreakerShouldHandle(args).ConfigureAwait(false))
 1181                {
 1182                    return false;
 1183                }
 1184
 1185                return args.Outcome.Result?.StatusCode != HttpStatusCode.TooManyRequests;
 1186            };
 1187            options.AttemptTimeout.Timeout = TimeSpan.FromSeconds(45);
 1188            options.CircuitBreaker.SamplingDuration = TimeSpan.FromMinutes(2);
 1189            options.TotalRequestTimeout.Timeout = TimeSpan.FromMinutes(4);
 1190        });
 191
 192        // Keep the retry logging handler inside the resilience pipeline so every attempted request is visible.
 1193        clientBuilder.AddHttpMessageHandler<LangfuseRetryLoggingHandler>();
 194
 1195        return clientBuilder;
 196    }
 197
 198    private static bool ShouldRetryUnsafeLangfuseRateLimit(HttpRequestMessage request)
 199    {
 1200        var absolutePath = request.RequestUri?.AbsolutePath ?? string.Empty;
 201
 1202        if (string.Equals(request.Method.Method, HttpMethod.Post.Method, StringComparison.OrdinalIgnoreCase))
 203        {
 1204            return absolutePath.EndsWith("/api/public/scores", StringComparison.OrdinalIgnoreCase)
 1205                || absolutePath.EndsWith("/scores", StringComparison.OrdinalIgnoreCase)
 1206                || absolutePath.EndsWith("/api/public/dataset-items", StringComparison.OrdinalIgnoreCase)
 1207                || absolutePath.EndsWith("/dataset-items", StringComparison.OrdinalIgnoreCase)
 1208                || absolutePath.EndsWith("/api/public/dataset-run-items", StringComparison.OrdinalIgnoreCase)
 1209                || absolutePath.EndsWith("/dataset-run-items", StringComparison.OrdinalIgnoreCase);
 210        }
 211
 0212        if (string.Equals(request.Method.Method, HttpMethod.Delete.Method, StringComparison.OrdinalIgnoreCase))
 213        {
 0214            return absolutePath.Contains("/api/public/datasets/", StringComparison.OrdinalIgnoreCase)
 0215                && absolutePath.Contains("/runs/", StringComparison.OrdinalIgnoreCase);
 216        }
 217
 0218        return false;
 219    }
 220
 221    /// <summary>
 222    /// Registers the OpenTelemetry tracing pipeline with the Langfuse OTLP endpoint.
 223    /// If <c>LANGFUSE_PUBLIC_KEY</c> or <c>LANGFUSE_SECRET_KEY</c> are not set,
 224    /// no pipeline is registered and all <see cref="System.Diagnostics.ActivitySource.StartActivity(string)"/>
 225    /// calls return <c>null</c> (graceful degradation).
 226    /// </summary>
 227    /// <remarks>
 228    /// <para>
 229    /// Uses the standard <c>AddOpenTelemetry()</c> API from <c>OpenTelemetry.Extensions.Hosting</c>,
 230    /// which registers the <see cref="TracerProvider"/> as a DI-managed singleton and an
 231    /// <see cref="Microsoft.Extensions.Hosting.IHostedService"/> that triggers provider construction.
 232    /// Since this app uses Spectre.Console.Cli (no <c>IHost</c>), the <see cref="TypeRegistrar"/>
 233    /// manually starts hosted services after building the <c>ServiceProvider</c>.
 234    /// </para>
 235    /// <para>
 236    /// This method is idempotent â€” multiple calls have no additional effect.
 237    /// </para>
 238    /// </remarks>
 239    public static IServiceCollection AddLangfuseTracing(this IServiceCollection services)
 240    {
 241        // Idempotency: skip if tracing has already been registered.
 1242        if (_langfuseTracingRegistered)
 1243            return services;
 244
 1245        var publicKey = Environment.GetEnvironmentVariable("LANGFUSE_PUBLIC_KEY");
 1246        var secretKey = Environment.GetEnvironmentVariable("LANGFUSE_SECRET_KEY");
 247
 1248        if (string.IsNullOrEmpty(publicKey) || string.IsNullOrEmpty(secretKey))
 249        {
 250            // No credentials â€” skip OTel registration entirely
 1251            return services;
 252        }
 253
 1254        _langfuseTracingRegistered = true;
 255
 1256        var baseUrl = Environment.GetEnvironmentVariable("LANGFUSE_BASE_URL") ?? "https://cloud.langfuse.com";
 1257        var headers = BuildLangfuseOtlpHeaders(publicKey, secretKey);
 258
 259        // NOTE: Setting options.Endpoint programmatically sets AppendSignalPathToEndpoint = false,
 260        // so the full URL including /v1/traces must be provided.
 1261        services.AddOpenTelemetry()
 0262            .ConfigureResource(r => r.AddService("KicktippAi"))
 1263            .WithTracing(tracing => tracing
 1264                .AddSource(Telemetry.Source.Name)
 1265                .AddProcessor(new LangfuseBaggageSpanProcessor())
 1266                .AddOtlpExporter(options =>
 1267                {
 0268                    options.Endpoint = new Uri($"{baseUrl}/api/public/otel/v1/traces");
 0269                    options.Protocol = OtlpExportProtocol.HttpProtobuf;
 0270                    options.Headers = headers;
 0271                }));
 272
 1273        return services;
 274    }
 275
 276    internal static string BuildLangfuseOtlpHeaders(string publicKey, string secretKey)
 277    {
 1278        var authorization = Convert.ToBase64String(Encoding.UTF8.GetBytes($"{publicKey}:{secretKey}"));
 1279        return string.Join(",",
 1280            $"Authorization=Basic {authorization}",
 1281            $"{LangfuseIngestionVersionHeaderName}={LangfuseIngestionVersionHeaderValue}");
 282    }
 283
 284    private static bool _langfuseTracingRegistered;
 285
 286    private sealed class OpenAiHttpClientRegistrationMarker
 287    {
 288    }
 289
 290    /// <summary>
 291    /// Registers services specific to the ListKpiCommand.
 292    /// </summary>
 293    /// <remarks>
 294    /// This method is idempotent and ensures infrastructure is registered.
 295    /// </remarks>
 296    public static IServiceCollection AddListKpiCommandServices(
 297        this IServiceCollection services,
 298        LogLevel minimumLogLevel = LogLevel.Information)
 299    {
 1300        services.AddOrchestratorInfrastructure(minimumLogLevel);
 301
 302        // ListKpiCommand only needs Firebase factory (uses IKpiRepository)
 303        // No command-specific keyed services needed - factory pattern handles runtime config
 304
 1305        return services;
 306    }
 307
 308    /// <summary>
 309    /// Service key for the KPI documents file provider.
 310    /// </summary>
 311    public const string KpiDocumentsFileProviderKey = "kpi-documents";
 312
 313    /// <summary>
 314    /// Service key for the transfers documents file provider.
 315    /// </summary>
 316    public const string TransfersDocumentsFileProviderKey = "transfers-documents";
 317
 318    /// <summary>
 319    /// Registers services specific to the UploadKpiCommand.
 320    /// </summary>
 321    /// <remarks>
 322    /// This method is idempotent and ensures infrastructure is registered.
 323    /// </remarks>
 324    public static IServiceCollection AddUploadKpiCommandServices(
 325        this IServiceCollection services,
 326        LogLevel minimumLogLevel = LogLevel.Information)
 327    {
 1328        services.AddOrchestratorInfrastructure(minimumLogLevel);
 329
 330        // UploadKpiCommand only needs Firebase factory (uses IKpiRepository)
 331        // Register keyed file provider for KPI documents directory
 1332        services.TryAddKeyedSingleton<IFileProvider>(
 1333            KpiDocumentsFileProviderKey,
 0334            (_, _) => SolutionRelativeFileProvider.Create("kpi-documents"));
 335
 1336        return services;
 337    }
 338
 339    /// <summary>
 340    /// Registers services specific to the CostCommand.
 341    /// </summary>
 342    /// <remarks>
 343    /// This method is idempotent and ensures infrastructure is registered.
 344    /// </remarks>
 345    public static IServiceCollection AddCostCommandServices(
 346        this IServiceCollection services,
 347        LogLevel minimumLogLevel = LogLevel.Information)
 348    {
 1349        services.AddOrchestratorInfrastructure(minimumLogLevel);
 350
 351        // CostCommand needs Firebase factory (uses IPredictionRepository, FirestoreDb)
 352        // No command-specific keyed services needed - factory pattern handles runtime config
 353
 1354        return services;
 355    }
 356
 357    /// <summary>
 358    /// Registers services specific to the MatchdayCommand.
 359    /// </summary>
 360    /// <remarks>
 361    /// This method is idempotent and ensures infrastructure is registered.
 362    /// </remarks>
 363    public static IServiceCollection AddMatchdayCommandServices(
 364        this IServiceCollection services,
 365        LogLevel minimumLogLevel = LogLevel.Information)
 366    {
 1367        services.AddOrchestratorInfrastructure(minimumLogLevel);
 368
 369        // MatchdayCommand needs all factories:
 370        // - Firebase (IPredictionRepository, IContextRepository)
 371        // - Kicktipp (IKicktippClient)
 372        // - OpenAI (IPredictionService, ITokenUsageTracker)
 373        // Factory pattern handles runtime config based on settings
 374
 1375        return services;
 376    }
 377
 378    /// <summary>
 379    /// Registers services specific to the RandomMatchCommand.
 380    /// </summary>
 381    /// <remarks>
 382    /// This method is idempotent and ensures infrastructure is registered.
 383    /// </remarks>
 384    public static IServiceCollection AddRandomMatchCommandServices(
 385        this IServiceCollection services,
 386        LogLevel minimumLogLevel = LogLevel.Information)
 387    {
 1388        services.AddOrchestratorInfrastructure(minimumLogLevel);
 389
 390        // RandomMatchCommand needs the same factories as MatchdayCommand:
 391        // - Firebase (IPredictionRepository, IContextRepository)
 392        // - Kicktipp (IKicktippClient)
 393        // - OpenAI (IPredictionService, ITokenUsageTracker)
 394
 1395        return services;
 396    }
 397
 398    /// <summary>
 399    /// Registers services specific to the BonusCommand.
 400    /// </summary>
 401    /// <remarks>
 402    /// This method is idempotent and ensures infrastructure is registered.
 403    /// </remarks>
 404    public static IServiceCollection AddBonusCommandServices(
 405        this IServiceCollection services,
 406        LogLevel minimumLogLevel = LogLevel.Information)
 407    {
 1408        services.AddOrchestratorInfrastructure(minimumLogLevel);
 409
 410        // BonusCommand needs all factories:
 411        // - Firebase (IPredictionRepository, IKpiRepository)
 412        // - Kicktipp (IKicktippClient)
 413        // - OpenAI (IPredictionService, ITokenUsageTracker)
 414
 1415        return services;
 416    }
 417
 418    /// <summary>
 419    /// Registers services specific to the VerifyMatchdayCommand.
 420    /// </summary>
 421    public static IServiceCollection AddVerifyMatchdayCommandServices(
 422        this IServiceCollection services,
 423        LogLevel minimumLogLevel = LogLevel.Information)
 424    {
 1425        services.AddOrchestratorInfrastructure(minimumLogLevel);
 426
 427        // VerifyMatchdayCommand needs:
 428        // - Firebase (IPredictionRepository, IContextRepository)
 429        // - Kicktipp (IKicktippClient)
 430
 1431        return services;
 432    }
 433
 434    private static IServiceCollection AddFifaRankingSourceServicesIfMissing(this IServiceCollection services)
 435    {
 1436        services.TryAddTransient<IFifaRankingSource, FifaRankingSource>();
 437
 1438        if (services.Any(descriptor => descriptor.ServiceType == typeof(IFifaRankingApiClient)))
 439        {
 1440            return services;
 441        }
 442
 1443        services.AddHttpClient<IFifaRankingApiClient, FifaRankingApiClient>(client =>
 1444        {
 0445            client.BaseAddress = FifaApiBaseAddress;
 0446            client.Timeout = Timeout.InfiniteTimeSpan;
 0447            client.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json"));
 0448        });
 449
 1450        return services;
 451    }
 452
 453    private static IServiceCollection AddWm26LineupSourceServicesIfMissing(this IServiceCollection services)
 454    {
 1455        services.TryAddTransient<IWm26LineupSource, Wm26LineupSource>();
 456
 1457        if (!services.Any(descriptor => descriptor.ServiceType == typeof(IWm26TransfermarktDuckDbProvider)))
 458        {
 1459            services.AddHttpClient<IWm26TransfermarktDuckDbProvider, Wm26TransfermarktDuckDbProvider>();
 460        }
 461
 1462        return services;
 463    }
 464
 465    /// <summary>
 466    /// Registers services specific to the VerifyBonusCommand.
 467    /// </summary>
 468    public static IServiceCollection AddVerifyBonusCommandServices(
 469        this IServiceCollection services,
 470        LogLevel minimumLogLevel = LogLevel.Information)
 471    {
 1472        services.AddOrchestratorInfrastructure(minimumLogLevel);
 473
 474        // VerifyBonusCommand needs:
 475        // - Firebase (IPredictionRepository, IKpiRepository)
 476        // - Kicktipp (IKicktippClient)
 477
 1478        return services;
 479    }
 480
 481    /// <summary>
 482    /// Registers services specific to the CollectContextKicktippCommand.
 483    /// </summary>
 484    public static IServiceCollection AddCollectContextKicktippCommandServices(
 485        this IServiceCollection services,
 486        LogLevel minimumLogLevel = LogLevel.Information)
 487    {
 1488        services.AddOrchestratorInfrastructure(minimumLogLevel);
 489
 490        // CollectContextKicktippCommand needs:
 491        // - Firebase (IContextRepository, IMatchOutcomeRepository)
 492        // - Kicktipp (IKicktippClient)
 493
 1494        return services;
 495    }
 496
 497    /// <summary>
 498    /// Registers services specific to the CollectContextFifaCommand.
 499    /// </summary>
 500    public static IServiceCollection AddCollectContextFifaCommandServices(
 501        this IServiceCollection services,
 502        LogLevel minimumLogLevel = LogLevel.Information)
 503    {
 1504        services.AddOrchestratorInfrastructure(minimumLogLevel);
 505
 1506        services.AddFifaRankingSourceServicesIfMissing();
 507
 508        // CollectContextFifaCommand needs Firebase (IContextRepository, IKpiRepository)
 509        // and the public FIFA rankings API.
 510
 1511        return services;
 512    }
 513
 514    /// <summary>
 515    /// Registers services specific to the CollectContextLineupsCommand.
 516    /// </summary>
 517    public static IServiceCollection AddCollectContextLineupsCommandServices(
 518        this IServiceCollection services,
 519        LogLevel minimumLogLevel = LogLevel.Information)
 520    {
 1521        services.AddOrchestratorInfrastructure(minimumLogLevel);
 1522        services.AddWm26LineupSourceServicesIfMissing();
 523
 524        // CollectContextLineupsCommand needs Firebase (IContextRepository, IKpiRepository)
 525        // and the Transfermarkt DuckDB snapshot.
 526
 1527        return services;
 528    }
 529
 530    /// <summary>
 531    /// Registers services specific to the CollectContextDevCommand.
 532    /// </summary>
 533    public static IServiceCollection AddCollectContextDevCommandServices(
 534        this IServiceCollection services,
 535        LogLevel minimumLogLevel = LogLevel.Information)
 536    {
 1537        services.AddOrchestratorInfrastructure(minimumLogLevel);
 1538        services.AddFifaRankingSourceServicesIfMissing();
 1539        services.AddWm26LineupSourceServicesIfMissing();
 540
 541        // CollectContextDevCommand composes the Kicktipp, FIFA, and lineup collection paths.
 542
 1543        return services;
 544    }
 545
 546    /// <summary>
 547    /// Registers services specific to WM26 recent-history date-map commands.
 548    /// </summary>
 549    public static IServiceCollection AddWm26RecentHistoryCommandServices(
 550        this IServiceCollection services,
 551        LogLevel minimumLogLevel = LogLevel.Information)
 552    {
 1553        services.AddOrchestratorInfrastructure(minimumLogLevel);
 554
 555        // WM26 recent-history commands need Firebase (IContextRepository).
 556
 1557        return services;
 558    }
 559
 560    /// <summary>
 561    /// Registers services specific to the ContextChangesCommand.
 562    /// </summary>
 563    public static IServiceCollection AddContextChangesCommandServices(
 564        this IServiceCollection services,
 565        LogLevel minimumLogLevel = LogLevel.Information)
 566    {
 1567        services.AddOrchestratorInfrastructure(minimumLogLevel);
 568
 569        // ContextChangesCommand only needs Firebase (IContextRepository)
 570
 1571        return services;
 572    }
 573
 574    /// <summary>
 575    /// Registers services specific to the UploadTransfersCommand.
 576    /// </summary>
 577    public static IServiceCollection AddUploadTransfersCommandServices(
 578        this IServiceCollection services,
 579        LogLevel minimumLogLevel = LogLevel.Information)
 580    {
 1581        services.AddOrchestratorInfrastructure(minimumLogLevel);
 582
 583        // UploadTransfersCommand needs Firebase (IContextRepository)
 584        // Register keyed file provider for transfers documents directory
 1585        services.TryAddKeyedSingleton<IFileProvider>(
 1586            TransfersDocumentsFileProviderKey,
 0587            (_, _) => SolutionRelativeFileProvider.Create("transfers-documents"));
 588
 1589        return services;
 590    }
 591
 592    /// <summary>
 593    /// Registers services specific to the UploadContextCommand.
 594    /// </summary>
 595    public static IServiceCollection AddUploadContextCommandServices(
 596        this IServiceCollection services,
 597        LogLevel minimumLogLevel = LogLevel.Information)
 598    {
 1599        services.AddOrchestratorInfrastructure(minimumLogLevel);
 600
 601        // UploadContextCommand needs Firebase (IContextRepository).
 602
 1603        return services;
 604    }
 605
 606    /// <summary>
 607    /// Registers services specific to the CopyFirestoreContextCommand.
 608    /// </summary>
 609    public static IServiceCollection AddCopyFirestoreContextCommandServices(
 610        this IServiceCollection services,
 611        LogLevel minimumLogLevel = LogLevel.Information)
 612    {
 1613        services.AddOrchestratorInfrastructure(minimumLogLevel);
 614
 615        // CopyFirestoreContextCommand needs Firebase (IContextRepository, IKpiRepository).
 616
 1617        return services;
 618    }
 619
 620    /// <summary>
 621    /// Registers services specific to the AnalyzeMatchDetailedCommand.
 622    /// </summary>
 623    public static IServiceCollection AddAnalyzeMatchDetailedCommandServices(
 624        this IServiceCollection services,
 625        LogLevel minimumLogLevel = LogLevel.Information)
 626    {
 1627        services.AddOrchestratorInfrastructure(minimumLogLevel);
 628
 629        // AnalyzeMatchDetailedCommand needs:
 630        // - Firebase (IContextRepository)
 631        // - OpenAI (IPredictionService, ITokenUsageTracker)
 632
 1633        return services;
 634    }
 635
 636    /// <summary>
 637    /// Registers services specific to the AnalyzeMatchComparisonCommand.
 638    /// </summary>
 639    public static IServiceCollection AddAnalyzeMatchComparisonCommandServices(
 640        this IServiceCollection services,
 641        LogLevel minimumLogLevel = LogLevel.Information)
 642    {
 1643        services.AddOrchestratorInfrastructure(minimumLogLevel);
 644
 645        // AnalyzeMatchComparisonCommand needs:
 646        // - Firebase (IContextRepository)
 647        // - OpenAI (IPredictionService, ITokenUsageTracker)
 648
 1649        return services;
 650    }
 651
 652    /// <summary>
 653    /// Registers all command services. Useful for production setup.
 654    /// </summary>
 655    public static IServiceCollection AddAllCommandServices(
 656        this IServiceCollection services,
 657        LogLevel minimumLogLevel = LogLevel.Information)
 658    {
 659        // Infrastructure is added by each command method, but we call it first for clarity
 1660        services.AddOrchestratorInfrastructure(minimumLogLevel);
 661
 662        // Register all command-specific services
 1663        services.AddListKpiCommandServices(minimumLogLevel);
 1664        services.AddUploadKpiCommandServices(minimumLogLevel);
 1665        services.AddCostCommandServices(minimumLogLevel);
 1666        services.AddMatchdayCommandServices(minimumLogLevel);
 1667        services.AddRandomMatchCommandServices(minimumLogLevel);
 1668        services.AddBonusCommandServices(minimumLogLevel);
 1669        services.AddVerifyMatchdayCommandServices(minimumLogLevel);
 1670        services.AddVerifyBonusCommandServices(minimumLogLevel);
 1671        services.AddCollectContextKicktippCommandServices(minimumLogLevel);
 1672        services.AddCollectContextFifaCommandServices(minimumLogLevel);
 1673        services.AddCollectContextLineupsCommandServices(minimumLogLevel);
 1674        services.AddCollectContextDevCommandServices(minimumLogLevel);
 1675        services.AddWm26RecentHistoryCommandServices(minimumLogLevel);
 1676        services.AddContextChangesCommandServices(minimumLogLevel);
 1677        services.AddUploadTransfersCommandServices(minimumLogLevel);
 1678        services.AddUploadContextCommandServices(minimumLogLevel);
 1679        services.AddCopyFirestoreContextCommandServices(minimumLogLevel);
 1680        services.AddAnalyzeMatchDetailedCommandServices(minimumLogLevel);
 1681        services.AddAnalyzeMatchComparisonCommandServices(minimumLogLevel);
 682
 1683        return services;
 684    }
 685}

Methods/Properties

.cctor()
AddOrchestratorInfrastructure(Microsoft.Extensions.DependencyInjection.IServiceCollection, Microsoft.Extensions.Logging.LogLevel)
AddOpenAiHttpClientIfMissing(Microsoft.Extensions.DependencyInjection.IServiceCollection)
<AddOpenAiHttpClientIfMissing()
AddLangfusePublicApiClient(Microsoft.Extensions.DependencyInjection.IServiceCollection)
<AddLangfusePublicApiClient()
<AddLangfusePublicApiClient()
ShouldRetryUnsafeLangfuseRateLimit(System.Net.Http.HttpRequestMessage)
AddLangfuseTracing(Microsoft.Extensions.DependencyInjection.IServiceCollection)
BuildLangfuseOtlpHeaders(string, string)
AddListKpiCommandServices(Microsoft.Extensions.DependencyInjection.IServiceCollection, Microsoft.Extensions.Logging.LogLevel)
AddUploadKpiCommandServices(Microsoft.Extensions.DependencyInjection.IServiceCollection, Microsoft.Extensions.Logging.LogLevel)
AddCostCommandServices(Microsoft.Extensions.DependencyInjection.IServiceCollection, Microsoft.Extensions.Logging.LogLevel)
AddMatchdayCommandServices(Microsoft.Extensions.DependencyInjection.IServiceCollection, Microsoft.Extensions.Logging.LogLevel)
AddRandomMatchCommandServices(Microsoft.Extensions.DependencyInjection.IServiceCollection, Microsoft.Extensions.Logging.LogLevel)
AddBonusCommandServices(Microsoft.Extensions.DependencyInjection.IServiceCollection, Microsoft.Extensions.Logging.LogLevel)
AddVerifyMatchdayCommandServices(Microsoft.Extensions.DependencyInjection.IServiceCollection, Microsoft.Extensions.Logging.LogLevel)
AddFifaRankingSourceServicesIfMissing(Microsoft.Extensions.DependencyInjection.IServiceCollection)
AddWm26LineupSourceServicesIfMissing(Microsoft.Extensions.DependencyInjection.IServiceCollection)
AddVerifyBonusCommandServices(Microsoft.Extensions.DependencyInjection.IServiceCollection, Microsoft.Extensions.Logging.LogLevel)
AddCollectContextKicktippCommandServices(Microsoft.Extensions.DependencyInjection.IServiceCollection, Microsoft.Extensions.Logging.LogLevel)
AddCollectContextFifaCommandServices(Microsoft.Extensions.DependencyInjection.IServiceCollection, Microsoft.Extensions.Logging.LogLevel)
AddCollectContextLineupsCommandServices(Microsoft.Extensions.DependencyInjection.IServiceCollection, Microsoft.Extensions.Logging.LogLevel)
AddCollectContextDevCommandServices(Microsoft.Extensions.DependencyInjection.IServiceCollection, Microsoft.Extensions.Logging.LogLevel)
AddWm26RecentHistoryCommandServices(Microsoft.Extensions.DependencyInjection.IServiceCollection, Microsoft.Extensions.Logging.LogLevel)
AddContextChangesCommandServices(Microsoft.Extensions.DependencyInjection.IServiceCollection, Microsoft.Extensions.Logging.LogLevel)
AddUploadTransfersCommandServices(Microsoft.Extensions.DependencyInjection.IServiceCollection, Microsoft.Extensions.Logging.LogLevel)
AddUploadContextCommandServices(Microsoft.Extensions.DependencyInjection.IServiceCollection, Microsoft.Extensions.Logging.LogLevel)
AddCopyFirestoreContextCommandServices(Microsoft.Extensions.DependencyInjection.IServiceCollection, Microsoft.Extensions.Logging.LogLevel)
AddAnalyzeMatchDetailedCommandServices(Microsoft.Extensions.DependencyInjection.IServiceCollection, Microsoft.Extensions.Logging.LogLevel)
AddAnalyzeMatchComparisonCommandServices(Microsoft.Extensions.DependencyInjection.IServiceCollection, Microsoft.Extensions.Logging.LogLevel)
AddAllCommandServices(Microsoft.Extensions.DependencyInjection.IServiceCollection, Microsoft.Extensions.Logging.LogLevel)