File: FakeHostingExtensions.cs
Web Access
Project: src\src\Libraries\Microsoft.Extensions.Hosting.Testing\Microsoft.Extensions.Hosting.Testing.csproj (Microsoft.Extensions.Hosting.Testing)
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
 
using System;
using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.Extensions.Compliance.Testing;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging.Testing;
using Microsoft.Shared.DiagnosticIds;
using Microsoft.Shared.Diagnostics;
 
namespace Microsoft.Extensions.Hosting;
 
/// <summary>
/// Extension methods supporting host unit testing scenarios.
/// </summary>
[Experimental(diagnosticId: DiagnosticIds.Experiments.Hosting, UrlFormat = DiagnosticIds.UrlFormat)]
public static class FakeHostingExtensions
{
    /// <summary>
    /// Starts and immediately stops the service.
    /// </summary>
    /// <param name="service">The tested service.</param>
    /// <param name="cancellationToken">Cancellation token. See <see cref="CancellationToken"/>.</param>
    /// <returns>A <see cref="Task"/> representing the asynchronous operation.</returns>
    public static async Task StartAndStopAsync(this IHostedService service, CancellationToken cancellationToken = default)
    {
        _ = Throw.IfNull(service);
 
        try
        {
            await service.StartAsync(cancellationToken).ConfigureAwait(false);
        }
        finally
        {
            await service.StopAsync(cancellationToken).ConfigureAwait(false);
        }
    }
 
    /// <summary>
    /// Gets the object that collects log records sent to the fake logger.
    /// </summary>
    /// <param name="host">An <see cref="IHost"/> instance.</param>
    /// <exception cref="InvalidOperationException">No collector exists in the provider.</exception>
    /// <returns>The collector that tracks records logged to fake loggers.</returns>
    public static FakeLogCollector GetFakeLogCollector(this IHost host)
    {
        _ = Throw.IfNull(host);
        return host.Services.GetFakeLogCollector();
    }
 
    /// <summary>
    /// Gets the object reporting all redactions performed.
    /// </summary>
    /// <param name="host">An <see cref="IHost"/> instance.</param>
    /// <exception cref="InvalidOperationException">No collector exists in the provider.</exception>
    /// <returns>The collector that tracks redactions performed on log messages.</returns>
    public static FakeRedactionCollector GetFakeRedactionCollector(this IHost host)
    {
        _ = Throw.IfNull(host);
        return host.Services.GetFakeRedactionCollector();
    }
 
    /// <summary>
    /// Adds an action invoked on each log message.
    /// </summary>
    /// <param name="builder">An <see cref="IHostBuilder"/> instance.</param>
    /// <param name="callback">The action to invoke on each log message.</param>
    /// <returns>The <see cref="IHostBuilder"/> instance.</returns>
    public static IHostBuilder AddFakeLoggingOutputSink(this IHostBuilder builder, Action<string> callback)
    {
        _ = Throw.IfNull(builder);
        _ = Throw.IfNull(callback);
 
        return builder.ConfigureServices(services => services.AddFakeLogging(logging =>
        {
            if (logging.OutputSink is null)
            {
                logging.OutputSink = callback;
            }
            else
            {
                var currentCallback = logging.OutputSink;
                logging.OutputSink = x =>
                {
                    currentCallback(x);
                    callback(x);
                };
            }
        }));
    }
 
    /// <summary>
    /// Exposes <see cref="IHostBuilder"/> for changes via a delegate.
    /// </summary>
    /// <param name="builder">An <see cref="IHostBuilder"/> instance.</param>
    /// <param name="configure">Configures the <see cref="IHostBuilder"/> instance.</param>
    /// <returns>The <see cref="IHostBuilder"/> instance.</returns>
    /// <remarks>Designed to ease host configuration in unit tests by defining common configuration methods.</remarks>
    [SuppressMessage("Minor Code Smell", "S3872:Parameter names should not duplicate the names of their methods",
        Justification = "We want to keep the parameter name for consistency.")]
    public static IHostBuilder Configure(this IHostBuilder builder, Action<IHostBuilder> configure)
    {
        _ = Throw.IfNull(builder);
        _ = Throw.IfNull(configure);
        configure(builder);
        return builder;
    }
 
    /// <summary>
    /// Adds configuration entries.
    /// </summary>
    /// <param name="builder">An <see cref="IHostBuilder"/> instance.</param>
    /// <param name="configurations">A list of key and value tuples that will be used as configuration entries.</param>
    /// <returns>The <see cref="IHostBuilder"/> instance.</returns>
    public static IHostBuilder ConfigureHostConfiguration(this IHostBuilder builder, params (string key, string value)[] configurations)
    {
        _ = Throw.IfNull(configurations);
 
        foreach ((var key, var value) in configurations)
        {
            _ = builder.ConfigureHostConfiguration(key, value);
        }
 
        return builder;
    }
 
    /// <summary>
    /// Adds a configuration value.
    /// </summary>
    /// <param name="builder">An <see cref="IHostBuilder"/> instance.</param>
    /// <param name="key">The configuration key.</param>
    /// <param name="value">The configuration value.</param>
    /// <returns>The <see cref="IHostBuilder"/> instance.</returns>
    public static IHostBuilder ConfigureHostConfiguration(this IHostBuilder builder, string key, string value)
    {
        _ = Throw.IfNull(builder);
        return builder.ConfigureHostConfiguration(configBuilder => ConfigureConfiguration(configBuilder, key, value));
    }
 
    /// <summary>
    /// Adds configuration entries.
    /// </summary>
    /// <param name="builder">An <see cref="IHostBuilder"/> instance.</param>
    /// <param name="configurations">A list of key and value tuples that will be used as configuration entries.</param>
    /// <returns>The <see cref="IHostBuilder"/> instance.</returns>
    public static IHostBuilder ConfigureAppConfiguration(this IHostBuilder builder, params (string key, string value)[] configurations)
    {
        _ = Throw.IfNull(configurations);
 
        foreach ((var key, var value) in configurations)
        {
            _ = builder.ConfigureAppConfiguration(key, value);
        }
 
        return builder;
    }
 
    /// <summary>
    /// Adds a configuration value.
    /// </summary>
    /// <param name="builder">An <see cref="IHostBuilder"/> instance.</param>
    /// <param name="key">The configuration key.</param>
    /// <param name="value">The configuration value.</param>
    /// <returns>The <see cref="IHostBuilder"/> instance.</returns>
    public static IHostBuilder ConfigureAppConfiguration(this IHostBuilder builder, string key, string value)
    {
        _ = Throw.IfNull(builder);
        return builder.ConfigureAppConfiguration((_, configBuilder) => ConfigureConfiguration(configBuilder, key, value));
    }
 
    private static void ConfigureConfiguration(IConfigurationBuilder builder, string key, string value)
    {
        if (builder.Sources.LastOrDefault() is FakeConfigurationSource source)
        {
            source.InitialData = source.InitialData!.Concat(new[] { new KeyValuePair<string, string?>(key, value) });
            return;
        }
 
        _ = builder.Add(new FakeConfigurationSource(new KeyValuePair<string, string?>(key, value)));
    }
}