< Summary

Information
Class: Orchestrator.Infrastructure.ServiceRegistrationExtensions
Assembly: Orchestrator
File(s): /home/runner/work/KicktippAi/KicktippAi/src/Orchestrator/Infrastructure/ServiceRegistrationExtensions.cs
Line coverage
91%
Covered lines: 78
Uncovered lines: 7
Coverable lines: 85
Total lines: 361
Line coverage: 91.7%
Branch coverage
94%
Covered branches: 17
Total branches: 18
Branch coverage: 94.4%
Method coverage

Feature is only available for sponsors

Upgrade to PRO version

Metrics

File(s)

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

#LineLine coverage
 1using System.Text;
 2using EHonda.KicktippAi.Core;
 3using Microsoft.Extensions.DependencyInjection;
 4using Microsoft.Extensions.DependencyInjection.Extensions;
 5using Microsoft.Extensions.FileProviders;
 6using Microsoft.Extensions.Logging;
 7using OpenTelemetry.Exporter;
 8using OpenTelemetry.Resources;
 9using OpenTelemetry.Trace;
 10using OpenAiIntegration;
 11using Orchestrator.Infrastructure.Factories;
 12using Orchestrator.Services;
 13
 14namespace Orchestrator.Infrastructure;
 15
 16/// <summary>
 17/// Extension methods for registering Orchestrator services in dependency injection.
 18/// </summary>
 19public static class ServiceRegistrationExtensions
 20{
 21    private const string LangfuseIngestionVersionHeaderName = "x-langfuse-ingestion-version";
 22    private const string LangfuseIngestionVersionHeaderValue = "4";
 23
 24    /// <summary>
 25    /// Registers all shared infrastructure services (factories, logging).
 26    /// </summary>
 27    /// <remarks>
 28    /// This method is idempotent - calling it multiple times has no additional effect.
 29    /// </remarks>
 30    public static IServiceCollection AddOrchestratorInfrastructure(this IServiceCollection services)
 31    {
 32        // Add logging (idempotent via TryAdd internally)
 133        services.AddLogging(builder =>
 134        {
 135            builder.SetMinimumLevel(LogLevel.Information);
 136        });
 37
 38        // Add memory cache for Kicktipp client
 139        services.AddMemoryCache();
 40
 41        // Add HTTP client factory for Kicktipp
 142        services.AddHttpClient();
 43
 44        // Register factories (idempotent)
 145        services.TryAddSingleton<IFirebaseServiceFactory, FirebaseServiceFactory>();
 146        services.TryAddSingleton<IKicktippClientFactory, KicktippClientFactory>();
 147        services.TryAddSingleton<IOpenAiServiceFactory, OpenAiServiceFactory>();
 148        services.TryAddSingleton<IContextProviderFactory, ContextProviderFactory>();
 149        services.TryAddTransient<MatchOutcomeCollectionService>();
 50
 51        // Register Langfuse/OTel tracing (no-op if credentials are absent)
 152        services.AddLangfuseTracing();
 53
 154        return services;
 55    }
 56
 57    /// <summary>
 58    /// Registers the OpenTelemetry tracing pipeline with the Langfuse OTLP endpoint.
 59    /// If <c>LANGFUSE_PUBLIC_KEY</c> or <c>LANGFUSE_SECRET_KEY</c> are not set,
 60    /// no pipeline is registered and all <see cref="System.Diagnostics.ActivitySource.StartActivity(string)"/>
 61    /// calls return <c>null</c> (graceful degradation).
 62    /// </summary>
 63    /// <remarks>
 64    /// <para>
 65    /// Uses the standard <c>AddOpenTelemetry()</c> API from <c>OpenTelemetry.Extensions.Hosting</c>,
 66    /// which registers the <see cref="TracerProvider"/> as a DI-managed singleton and an
 67    /// <see cref="Microsoft.Extensions.Hosting.IHostedService"/> that triggers provider construction.
 68    /// Since this app uses Spectre.Console.Cli (no <c>IHost</c>), the <see cref="TypeRegistrar"/>
 69    /// manually starts hosted services after building the <c>ServiceProvider</c>.
 70    /// </para>
 71    /// <para>
 72    /// This method is idempotent — multiple calls have no additional effect.
 73    /// </para>
 74    /// </remarks>
 75    public static IServiceCollection AddLangfuseTracing(this IServiceCollection services)
 76    {
 77        // Idempotency: skip if tracing has already been registered.
 178        if (_langfuseTracingRegistered)
 179            return services;
 80
 181        var publicKey = Environment.GetEnvironmentVariable("LANGFUSE_PUBLIC_KEY");
 182        var secretKey = Environment.GetEnvironmentVariable("LANGFUSE_SECRET_KEY");
 83
 184        if (string.IsNullOrEmpty(publicKey) || string.IsNullOrEmpty(secretKey))
 85        {
 86            // No credentials — skip OTel registration entirely
 187            return services;
 88        }
 89
 190        _langfuseTracingRegistered = true;
 91
 192        var baseUrl = Environment.GetEnvironmentVariable("LANGFUSE_BASE_URL") ?? "https://cloud.langfuse.com";
 193        var headers = BuildLangfuseOtlpHeaders(publicKey, secretKey);
 94
 95        // NOTE: Setting options.Endpoint programmatically sets AppendSignalPathToEndpoint = false,
 96        // so the full URL including /v1/traces must be provided.
 197        services.AddOpenTelemetry()
 098            .ConfigureResource(r => r.AddService("KicktippAi"))
 199            .WithTracing(tracing => tracing
 1100                .AddSource(Telemetry.Source.Name)
 1101                .AddProcessor(new LangfuseBaggageSpanProcessor())
 1102                .AddOtlpExporter(options =>
 1103                {
 0104                    options.Endpoint = new Uri($"{baseUrl}/api/public/otel/v1/traces");
 0105                    options.Protocol = OtlpExportProtocol.HttpProtobuf;
 0106                    options.Headers = headers;
 0107                }));
 108
 1109        return services;
 110    }
 111
 112    internal static string BuildLangfuseOtlpHeaders(string publicKey, string secretKey)
 113    {
 1114        var authorization = Convert.ToBase64String(Encoding.UTF8.GetBytes($"{publicKey}:{secretKey}"));
 1115        return string.Join(",",
 1116            $"Authorization=Basic {authorization}",
 1117            $"{LangfuseIngestionVersionHeaderName}={LangfuseIngestionVersionHeaderValue}");
 118    }
 119
 120    private static bool _langfuseTracingRegistered;
 121
 122    /// <summary>
 123    /// Registers services specific to the ListKpiCommand.
 124    /// </summary>
 125    /// <remarks>
 126    /// This method is idempotent and ensures infrastructure is registered.
 127    /// </remarks>
 128    public static IServiceCollection AddListKpiCommandServices(this IServiceCollection services)
 129    {
 1130        services.AddOrchestratorInfrastructure();
 131
 132        // ListKpiCommand only needs Firebase factory (uses IKpiRepository)
 133        // No command-specific keyed services needed - factory pattern handles runtime config
 134
 1135        return services;
 136    }
 137
 138    /// <summary>
 139    /// Service key for the KPI documents file provider.
 140    /// </summary>
 141    public const string KpiDocumentsFileProviderKey = "kpi-documents";
 142
 143    /// <summary>
 144    /// Service key for the transfers documents file provider.
 145    /// </summary>
 146    public const string TransfersDocumentsFileProviderKey = "transfers-documents";
 147
 148    /// <summary>
 149    /// Registers services specific to the UploadKpiCommand.
 150    /// </summary>
 151    /// <remarks>
 152    /// This method is idempotent and ensures infrastructure is registered.
 153    /// </remarks>
 154    public static IServiceCollection AddUploadKpiCommandServices(this IServiceCollection services)
 155    {
 1156        services.AddOrchestratorInfrastructure();
 157
 158        // UploadKpiCommand only needs Firebase factory (uses IKpiRepository)
 159        // Register keyed file provider for KPI documents directory
 1160        services.TryAddKeyedSingleton<IFileProvider>(
 1161            KpiDocumentsFileProviderKey,
 0162            (_, _) => SolutionRelativeFileProvider.Create("kpi-documents"));
 163
 1164        return services;
 165    }
 166
 167    /// <summary>
 168    /// Registers services specific to the CostCommand.
 169    /// </summary>
 170    /// <remarks>
 171    /// This method is idempotent and ensures infrastructure is registered.
 172    /// </remarks>
 173    public static IServiceCollection AddCostCommandServices(this IServiceCollection services)
 174    {
 1175        services.AddOrchestratorInfrastructure();
 176
 177        // CostCommand needs Firebase factory (uses IPredictionRepository, FirestoreDb)
 178        // No command-specific keyed services needed - factory pattern handles runtime config
 179
 1180        return services;
 181    }
 182
 183    /// <summary>
 184    /// Registers services specific to the MatchdayCommand.
 185    /// </summary>
 186    /// <remarks>
 187    /// This method is idempotent and ensures infrastructure is registered.
 188    /// </remarks>
 189    public static IServiceCollection AddMatchdayCommandServices(this IServiceCollection services)
 190    {
 1191        services.AddOrchestratorInfrastructure();
 192
 193        // MatchdayCommand needs all factories:
 194        // - Firebase (IPredictionRepository, IContextRepository)
 195        // - Kicktipp (IKicktippClient)
 196        // - OpenAI (IPredictionService, ITokenUsageTracker)
 197        // Factory pattern handles runtime config based on settings
 198
 1199        return services;
 200    }
 201
 202    /// <summary>
 203    /// Registers services specific to the RandomMatchCommand.
 204    /// </summary>
 205    /// <remarks>
 206    /// This method is idempotent and ensures infrastructure is registered.
 207    /// </remarks>
 208    public static IServiceCollection AddRandomMatchCommandServices(this IServiceCollection services)
 209    {
 1210        services.AddOrchestratorInfrastructure();
 211
 212        // RandomMatchCommand needs the same factories as MatchdayCommand:
 213        // - Firebase (IPredictionRepository, IContextRepository)
 214        // - Kicktipp (IKicktippClient)
 215        // - OpenAI (IPredictionService, ITokenUsageTracker)
 216
 1217        return services;
 218    }
 219
 220    /// <summary>
 221    /// Registers services specific to the BonusCommand.
 222    /// </summary>
 223    /// <remarks>
 224    /// This method is idempotent and ensures infrastructure is registered.
 225    /// </remarks>
 226    public static IServiceCollection AddBonusCommandServices(this IServiceCollection services)
 227    {
 1228        services.AddOrchestratorInfrastructure();
 229
 230        // BonusCommand needs all factories:
 231        // - Firebase (IPredictionRepository, IKpiRepository)
 232        // - Kicktipp (IKicktippClient)
 233        // - OpenAI (IPredictionService, ITokenUsageTracker)
 234
 1235        return services;
 236    }
 237
 238    /// <summary>
 239    /// Registers services specific to the VerifyMatchdayCommand.
 240    /// </summary>
 241    public static IServiceCollection AddVerifyMatchdayCommandServices(this IServiceCollection services)
 242    {
 1243        services.AddOrchestratorInfrastructure();
 244
 245        // VerifyMatchdayCommand needs:
 246        // - Firebase (IPredictionRepository, IContextRepository)
 247        // - Kicktipp (IKicktippClient)
 248
 1249        return services;
 250    }
 251
 252    /// <summary>
 253    /// Registers services specific to the VerifyBonusCommand.
 254    /// </summary>
 255    public static IServiceCollection AddVerifyBonusCommandServices(this IServiceCollection services)
 256    {
 1257        services.AddOrchestratorInfrastructure();
 258
 259        // VerifyBonusCommand needs:
 260        // - Firebase (IPredictionRepository, IKpiRepository)
 261        // - Kicktipp (IKicktippClient)
 262
 1263        return services;
 264    }
 265
 266    /// <summary>
 267    /// Registers services specific to the CollectContextKicktippCommand.
 268    /// </summary>
 269    public static IServiceCollection AddCollectContextKicktippCommandServices(this IServiceCollection services)
 270    {
 1271        services.AddOrchestratorInfrastructure();
 272
 273        // CollectContextKicktippCommand needs:
 274        // - Firebase (IContextRepository, IMatchOutcomeRepository)
 275        // - Kicktipp (IKicktippClient)
 276
 1277        return services;
 278    }
 279
 280    /// <summary>
 281    /// Registers services specific to the ContextChangesCommand.
 282    /// </summary>
 283    public static IServiceCollection AddContextChangesCommandServices(this IServiceCollection services)
 284    {
 1285        services.AddOrchestratorInfrastructure();
 286
 287        // ContextChangesCommand only needs Firebase (IContextRepository)
 288
 1289        return services;
 290    }
 291
 292    /// <summary>
 293    /// Registers services specific to the UploadTransfersCommand.
 294    /// </summary>
 295    public static IServiceCollection AddUploadTransfersCommandServices(this IServiceCollection services)
 296    {
 1297        services.AddOrchestratorInfrastructure();
 298
 299        // UploadTransfersCommand needs Firebase (IContextRepository)
 300        // Register keyed file provider for transfers documents directory
 1301        services.TryAddKeyedSingleton<IFileProvider>(
 1302            TransfersDocumentsFileProviderKey,
 0303            (_, _) => SolutionRelativeFileProvider.Create("transfers-documents"));
 304
 1305        return services;
 306    }
 307
 308    /// <summary>
 309    /// Registers services specific to the AnalyzeMatchDetailedCommand.
 310    /// </summary>
 311    public static IServiceCollection AddAnalyzeMatchDetailedCommandServices(this IServiceCollection services)
 312    {
 1313        services.AddOrchestratorInfrastructure();
 314
 315        // AnalyzeMatchDetailedCommand needs:
 316        // - Firebase (IContextRepository)
 317        // - OpenAI (IPredictionService, ITokenUsageTracker)
 318
 1319        return services;
 320    }
 321
 322    /// <summary>
 323    /// Registers services specific to the AnalyzeMatchComparisonCommand.
 324    /// </summary>
 325    public static IServiceCollection AddAnalyzeMatchComparisonCommandServices(this IServiceCollection services)
 326    {
 1327        services.AddOrchestratorInfrastructure();
 328
 329        // AnalyzeMatchComparisonCommand needs:
 330        // - Firebase (IContextRepository)
 331        // - OpenAI (IPredictionService, ITokenUsageTracker)
 332
 1333        return services;
 334    }
 335
 336    /// <summary>
 337    /// Registers all command services. Useful for production setup.
 338    /// </summary>
 339    public static IServiceCollection AddAllCommandServices(this IServiceCollection services)
 340    {
 341        // Infrastructure is added by each command method, but we call it first for clarity
 1342        services.AddOrchestratorInfrastructure();
 343
 344        // Register all command-specific services
 1345        services.AddListKpiCommandServices();
 1346        services.AddUploadKpiCommandServices();
 1347        services.AddCostCommandServices();
 1348        services.AddMatchdayCommandServices();
 1349        services.AddRandomMatchCommandServices();
 1350        services.AddBonusCommandServices();
 1351        services.AddVerifyMatchdayCommandServices();
 1352        services.AddVerifyBonusCommandServices();
 1353        services.AddCollectContextKicktippCommandServices();
 1354        services.AddContextChangesCommandServices();
 1355        services.AddUploadTransfersCommandServices();
 1356        services.AddAnalyzeMatchDetailedCommandServices();
 1357        services.AddAnalyzeMatchComparisonCommandServices();
 358
 1359        return services;
 360    }
 361}

Methods/Properties

AddOrchestratorInfrastructure(Microsoft.Extensions.DependencyInjection.IServiceCollection)
AddLangfuseTracing(Microsoft.Extensions.DependencyInjection.IServiceCollection)
BuildLangfuseOtlpHeaders(string, string)
AddListKpiCommandServices(Microsoft.Extensions.DependencyInjection.IServiceCollection)
AddUploadKpiCommandServices(Microsoft.Extensions.DependencyInjection.IServiceCollection)
AddCostCommandServices(Microsoft.Extensions.DependencyInjection.IServiceCollection)
AddMatchdayCommandServices(Microsoft.Extensions.DependencyInjection.IServiceCollection)
AddRandomMatchCommandServices(Microsoft.Extensions.DependencyInjection.IServiceCollection)
AddBonusCommandServices(Microsoft.Extensions.DependencyInjection.IServiceCollection)
AddVerifyMatchdayCommandServices(Microsoft.Extensions.DependencyInjection.IServiceCollection)
AddVerifyBonusCommandServices(Microsoft.Extensions.DependencyInjection.IServiceCollection)
AddCollectContextKicktippCommandServices(Microsoft.Extensions.DependencyInjection.IServiceCollection)
AddContextChangesCommandServices(Microsoft.Extensions.DependencyInjection.IServiceCollection)
AddUploadTransfersCommandServices(Microsoft.Extensions.DependencyInjection.IServiceCollection)
AddAnalyzeMatchDetailedCommandServices(Microsoft.Extensions.DependencyInjection.IServiceCollection)
AddAnalyzeMatchComparisonCommandServices(Microsoft.Extensions.DependencyInjection.IServiceCollection)
AddAllCommandServices(Microsoft.Extensions.DependencyInjection.IServiceCollection)