< Summary - AsyncResponse (Release / net8.0+net10.0 / unit+integration)

Information
Class: Microsoft.Extensions.DependencyInjection.AsyncResponseCoreServiceCollectionExtensions
Assembly: AsyncResponse.Core
File(s): /home/runner/work/AsyncResponse/AsyncResponse/src/AsyncResponse.Core/ServiceCollectionExtensions.cs
Line coverage
100%
Covered lines: 81
Uncovered lines: 0
Coverable lines: 81
Total lines: 232
Line coverage: 100%
Branch coverage
97%
Covered branches: 39
Total branches: 40
Branch coverage: 97.5%
Method coverage

Feature is only available for sponsors

Upgrade to PRO version

Metrics

File(s)

/home/runner/work/AsyncResponse/AsyncResponse/src/AsyncResponse.Core/ServiceCollectionExtensions.cs

#LineLine coverage
 1using AsyncResponse;
 2using Microsoft.Extensions.DependencyInjection.Extensions;
 3using System.Diagnostics.CodeAnalysis;
 4
 5namespace Microsoft.Extensions.DependencyInjection;
 6
 7/// <summary>
 8/// Core registrations for AsyncResponse. Everything is configured through the fluent builder
 9/// returned by <see cref="AddAsyncResponse"/>: chain exactly one channel and exactly one worker
 10/// transport, and exactly one durable-flow state store.
 11/// </summary>
 12public static class AsyncResponseCoreServiceCollectionExtensions
 13{
 14    /// <summary>
 15    /// Registers the channel-agnostic AsyncResponse engine (fluent waiter builder, transport-neutral
 16    /// ingress, worker-job executor, and the recovery watchdog) and returns a builder to configure
 17    /// the rest. It deliberately registers <em>no</em> response channel: chain exactly one
 18    /// (<see cref="WithInMemoryChannel"/> or the Redis channel package's <c>WithRedisChannel</c>) and
 19    /// exactly one worker transport (<see cref="WithInMemoryTransport"/> or a broker transport
 20    /// package such as <c>WithGooglePubSubTransport</c> / <c>WithRabbitMqTransport</c>), and exactly
 21    /// one durable-flow state store (<see cref="WithInMemoryDurableFlows"/> or a provider package).
 22    /// An app that starts without any of these choices fails fast at host startup.
 23    /// </summary>
 24    public static AsyncResponseRegistrationBuilder AddAsyncResponse(
 25        this IServiceCollection services,
 26        Action<AsyncResponseOptions>? configure = null)
 27    {
 328        services.AddOptions();
 329        if (configure is not null)
 30        {
 331            services.Configure(configure);
 32        }
 33
 34        // Channel-agnostic engine.
 335        services.TryAddSingleton<AsyncResponseContextPropagation>();
 336        services.TryAddSingleton<WorkerJobExecutor>();
 337        services.TryAddSingleton<IAsyncResponseIngress, AsyncResponseIngress>();
 238        services.TryAddSingleton<IAsyncResponseBuilder>(provider => new AsyncResponseBuilder(
 239            provider.GetRequiredService<IAsyncResponseSubscriber>(),
 240            provider.GetService<IWorkerTransport>(),
 241            provider.GetService<IAsyncResponseReplyTargetProvider>(),
 242            provider.GetRequiredService<AsyncResponseContextPropagation>()));
 43
 44        // Fail fast before background services do any real work if the required channel,
 45        // transport, and durable-flow store choices were not made explicitly. TryAddEnumerable
 46        // (keyed by implementation type) keeps a second AddAsyncResponse() call from registering a
 47        // second validator or watchdog instance.
 348        services.TryAddEnumerable(ServiceDescriptor.Singleton<Microsoft.Extensions.Hosting.IHostedService, AsyncResponse
 49
 50        // The recovery watchdog is part of the engine and runs by default for whatever channel is
 51        // registered (scanning + liveness go through IRecoveryStateScanner / IActiveSubscriberProbe).
 352        services.TryAddSingleton<AsyncResponseWatchdogState>();
 353        services.TryAddEnumerable(ServiceDescriptor.Singleton<Microsoft.Extensions.Hosting.IHostedService, AsyncResponse
 54
 355        return new AsyncResponseRegistrationBuilder(services);
 56    }
 57
 58    /// <summary>
 59    /// Uses an application-owned store for durable-flow state. The store must implement atomic
 60    /// creation, revision-checked updates, and execution leases. Use
 61    /// <see cref="WithInMemoryDurableFlows"/> explicitly for a process-local development or test
 62    /// store; <see cref="AddAsyncResponse"/> does not select one implicitly.
 63    /// </summary>
 64    public static AsyncResponseRegistrationBuilder WithDurableFlows<[DynamicallyAccessedMembers(DynamicallyAccessedMembe
 65        this AsyncResponseRegistrationBuilder builder,
 66        Action<DurableFlowOptions>? configure = null)
 67        where TFlowStateStore : class, IFlowStateStore
 368        => builder.WithDurableFlows<TFlowStateStore, DurableFlowOptions>(configure);
 69
 70    /// <summary>
 71    /// Registers an application or provider-owned durable-flow store whose options combine the
 72    /// common <see cref="DurableFlowOptions"/> settings with store-specific settings. Provider
 73    /// packages use this overload to expose one cohesive <c>With*DurableFlows(...)</c> callback.
 74    /// </summary>
 75    public static AsyncResponseRegistrationBuilder WithDurableFlows<[DynamicallyAccessedMembers(DynamicallyAccessedMembe
 76        this AsyncResponseRegistrationBuilder builder,
 77        Action<TOptions>? configure = null)
 78        where TFlowStateStore : class, IFlowStateStore
 79        where TOptions : DurableFlowOptions
 80    {
 381        builder.Services.AddOptions<TOptions>();
 382        if (configure is not null)
 383            builder.Services.Configure(configure);
 84
 85        // The engine consumes the same configured instance as the provider store, viewed through
 86        // the common base type. This keeps all durable-flow settings in one callback without a
 87        // second options object or per-execution adaptation/allocation.
 388        builder.Services.AddSingleton<DurableFlowOptions>(provider =>
 389            provider.GetRequiredService<Microsoft.Extensions.Options.IOptions<TOptions>>().Value);
 90
 91        // Forward the interface to the concrete registration so resolving either yields the same
 92        // instance within a scope. TryAdd lets callers (and the DurableFlows.* packages) pre-register
 93        // the concrete type with a different lifetime — e.g. singleton for stores whose dependencies
 94        // are all singletons — without this scoped default overriding it.
 395        builder.Services.TryAddScoped<TFlowStateStore>();
 396        builder.Services.AddScoped<IFlowStateStore>(provider => provider.GetRequiredService<TFlowStateStore>());
 397        builder.Services.AddSingleton(new AsyncResponseDurableFlowStoreMarker(typeof(TFlowStateStore)));
 398        AddDurableFlowEngine(builder.Services);
 399        return builder;
 100    }
 101
 102    /// <summary>
 103    /// Registers a durable flow class for execution: adds it to DI (scoped, unless the app
 104    /// pre-registered it with another lifetime) and records a statically-typed execution route the
 105    /// flow executor prefers over reflection-based type-name resolution. Registration is optional
 106    /// on JIT deployments (unregistered flows resolve reflectively as before) and required for
 107    /// flows executed in trimmed/Native AOT apps, where persisted type names cannot root code.
 108    /// </summary>
 109    /// <typeparam name="TFlow">The flow class; its full name is the persisted <see cref="FlowState.FlowTypeName"/>.</ty
 110    /// <typeparam name="TInput">The flow input type, persisted as JSON with the flow state.</typeparam>
 111    public static AsyncResponseRegistrationBuilder WithDurableFlow<[DynamicallyAccessedMembers(DynamicallyAccessedMember
 112        this AsyncResponseRegistrationBuilder builder)
 113        where TFlow : class, IDurableFlow<TInput>
 114    {
 3115        builder.Services.TryAddScoped<TFlow>();
 3116        builder.Services.AddSingleton(new DurableFlowRegistration
 3117        {
 3118            FlowTypeFullName = typeof(TFlow).FullName
 3119                ?? throw new InvalidOperationException("Durable flow classes must have a FullName."),
 3120            FlowType = typeof(TFlow),
 3121            DeserializeInput = static json => JsonSafety.SafeDeserialize<TInput>(json),
 3122            ExecuteAsync = static (flow, context, input) =>
 3123                ((TFlow)flow).ExecuteAsync(context, input is null ? default! : (TInput)input)
 3124        });
 3125        return builder;
 126    }
 127
 128    /// <summary>
 129    /// Uses an atomic process-local flow-state store. Intended for development, tests, and
 130    /// single-process apps; choose a DurableFlows provider package for multi-replica execution.
 131    /// </summary>
 132    public static AsyncResponseRegistrationBuilder WithInMemoryDurableFlows(
 133        this AsyncResponseRegistrationBuilder builder,
 134        Action<DurableFlowOptions>? configure = null)
 135    {
 3136        builder.Services.TryAddSingleton<InMemoryFlowStateStore>();
 3137        return builder.WithDurableFlows<InMemoryFlowStateStore>(configure);
 138    }
 139
 140    // The executor's public methods are lost-subscriber callback targets persisted by name
 141    // (RecoverAsync / FailAsync / ExecuteAsync); root them explicitly so the reflective dispatch
 142    // finds them in trimmed apps without any user action.
 143    [DynamicDependency(DynamicallyAccessedMemberTypes.PublicMethods, typeof(IDurableFlowExecutor))]
 144    [DynamicDependency(DynamicallyAccessedMemberTypes.PublicMethods, typeof(DurableFlowExecutor))]
 145    private static void AddDurableFlowEngine(IServiceCollection services)
 146    {
 3147        services.TryAddSingleton<IDurableFlowExecutor>(provider => new DurableFlowExecutor(
 3148            provider.GetRequiredService<IServiceScopeFactory>(),
 3149            provider.GetRequiredService<IAsyncResponseBuilder>(),
 3150            provider.GetRequiredService<IAsyncResponseSubscriber>(),
 3151            provider.GetService<IRecoverableAsyncResponseSubscriber>(),
 3152            provider.GetRequiredService<AsyncResponseContextPropagation>(),
 3153            provider.GetRequiredService<DurableFlowOptions>(),
 3154            provider.GetRequiredService<Microsoft.Extensions.Logging.ILogger<DurableFlowExecutor>>(),
 3155            provider.GetServices<DurableFlowRegistration>()));
 3156        services.TryAddSingleton<IDurableFlows>(provider => new DurableFlowService(
 3157            provider.GetRequiredService<IServiceScopeFactory>(),
 3158            provider.GetRequiredService<IAsyncResponseBuilder>(),
 3159            provider.GetRequiredService<AsyncResponseContextPropagation>(),
 3160            provider.GetRequiredService<DurableFlowOptions>(),
 3161            provider.GetRequiredService<Microsoft.Extensions.Logging.ILogger<DurableFlowService>>()));
 3162    }
 163
 164    /// <summary>
 165    /// Registers an application <see cref="IAsyncResponseContextPropagator"/> that carries ambient
 166    /// context (trace id, principal, tenant, …) across the serialization boundary into worker jobs
 167    /// and lost-subscriber recovery callbacks. In-process hops flow ambient state automatically via
 168    /// the captured <see cref="System.Threading.ExecutionContext"/>; propagators are only needed for
 169    /// context that must survive serialization (broker-backed workers, recovery after a redeploy).
 170    /// Register one per concern (e.g. a trace propagator and a principal propagator).
 171    /// </summary>
 172    public static AsyncResponseRegistrationBuilder WithContextPropagator<[DynamicallyAccessedMembers(DynamicallyAccessed
 173        where TPropagator : class, IAsyncResponseContextPropagator
 174    {
 3175        builder.Services.TryAddEnumerable(ServiceDescriptor.Singleton<IAsyncResponseContextPropagator, TPropagator>());
 3176        return builder;
 177    }
 178
 179    /// <summary>
 180    /// Registers the process-local response channel and recovery store. Waiters, subscriptions,
 181    /// and recovery state all live in memory and disappear when the process exits — the simplest
 182    /// setup, with no durable recovery. Pair with <see cref="WithInMemoryTransport"/> for a fully
 183    /// in-memory setup including background worker jobs.
 184    /// </summary>
 185    public static AsyncResponseRegistrationBuilder WithInMemoryChannel(
 186        this AsyncResponseRegistrationBuilder builder,
 187        Action<InMemoryAsyncResponseOptions>? configure = null)
 188    {
 2189        var services = builder.Services;
 2190        services.AddOptions();
 2191        if (configure is not null)
 192        {
 2193            services.Configure(configure);
 194        }
 195
 2196        services.TryAddSingleton<InMemoryRecoveryStateStore>();
 2197        services.TryAddSingleton<IRecoveryStateStore>(provider => provider.GetRequiredService<InMemoryRecoveryStateStore
 2198        services.TryAddSingleton<IRecoveryStateScanner>(provider => provider.GetRequiredService<InMemoryRecoveryStateSto
 199
 2200        services.TryAddSingleton<InMemoryAsyncResponseChannel>();
 2201        services.TryAddSingleton<IAsyncResponsePublisher>(provider => provider.GetRequiredService<InMemoryAsyncResponseC
 2202        services.TryAddSingleton<IRawAsyncResponsePublisher>(provider => provider.GetRequiredService<InMemoryAsyncRespon
 2203        services.TryAddSingleton<IAsyncResponseSubscriber>(provider => provider.GetRequiredService<InMemoryAsyncResponse
 2204        services.TryAddSingleton<IActiveSubscriberProbe>(provider => provider.GetRequiredService<InMemoryAsyncResponseCh
 205
 2206        services.AddSingleton(new AsyncResponseChannelMarker("InMemory"));
 2207        return builder;
 208    }
 209
 210    /// <summary>
 211    /// Registers the in-memory (in-process) worker transport and its background consumer. Jobs run
 212    /// in the current process and survive only as long as it does — suitable for development, tests,
 213    /// and single-node deployments. Chain exactly one transport after <see cref="AddAsyncResponse"/>;
 214    /// for distributed, durable execution use a full broker-backed transport package such as
 215    /// <c>WithGooglePubSubTransport</c> or <c>WithRabbitMqTransport</c>.
 216    /// </summary>
 217    public static AsyncResponseRegistrationBuilder WithInMemoryTransport(
 218        this AsyncResponseRegistrationBuilder builder,
 219        Action<InMemoryWorkerTransportOptions>? configure = null)
 220    {
 2221        var services = builder.Services;
 2222        services.AddOptions();
 2223        if (configure is not null)
 2224            services.Configure(configure);
 2225        services.TryAddSingleton<WorkerJobExecutor>();
 2226        services.TryAddSingleton<InMemoryWorkerTransport>();
 2227        services.TryAddSingleton<IWorkerTransport>(provider => provider.GetRequiredService<InMemoryWorkerTransport>());
 2228        services.AddHostedService<InMemoryWorkerHost>();
 2229        services.AddSingleton(new AsyncResponseTransportMarker("InMemory"));
 2230        return builder;
 231    }
 232}