Runnable examples

Aplicación de consola en C# para ejecutar múltiples ejemplos

Cuando estás explorando algo nuevo es muy típico crear una aplicación de consola y comentar/descomentar código para ver en funcionamiento lo que estás probando. Pero, tarde o temprano, este flujo se vuelve tedioso, propenso a errores y termina siendo un galimatías

Por eso, y apoyándonos en el post Console application vs worker service vamos a crear una aplicación de consola que nos permita ejecutar múltiples ejemplos de forma sencilla.

La idea se inspirá principalmente en un evento al que acudí hace mucho tiempo y que me gustó mucho https://github.com/CarlosLanderas/CSharp-6-and-7-features

La idea es ofrecer al usuario un menú simple e instanciar dinámicamente una clase que implemente IRunnable.

Aunque en el post anterior se explica en detalle, habría que instalar los siguientes paquetes:

  • Microsoft.Extensions.Configuration.Json
  • Microsoft.Extensions.Configuration.UserSecrets
  • Microsoft.Extensions.DependencyInjection
  • Microsoft.Extensions.Logging.Console

Para el fichero appsettings.json:

<ItemGroup>
    <Content Include="appsettings.json">
        <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
    </Content>
</ItemGroup>

Y para los secretos de usuario lo dejo a tu elección, ya sea usando VS, Rider o dotnet user-secrets.

Finalmente, el código es el siguiente, ready for copy-paste:

using System.Reflection;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;

var configuration = new ConfigurationBuilder()
    .AddJsonFile("appsettings.json")
    .AddUserSecrets(Assembly.GetExecutingAssembly())
    .Build();

var services = new ServiceCollection();
services
    .AddTransient<IConfiguration>(_ => configuration)
    .AddLogging(configure => configure
        .AddConfiguration(configuration.GetSection("Logging"))
        .AddConsole())

RegisterExamples();
await RunExamplesAsync();

return;

async Task RunExamplesAsync()
{
    var examples = GetExamples();
    
    var i = 1;
    foreach (var example in examples)
    {
        Console.WriteLine($"{i++}. {example}");
    }
    Console.WriteLine($"{i}. Exit");
    Console.Write("Enter the number of the example to run: ");

    while (true)
    {
        var input = Console.ReadLine();
        if (int.TryParse(input, out var exampleIndex) && exampleIndex > 0 && exampleIndex <= examples.Length)
        {
            using var scope = services.BuildServiceProvider().CreateScope();
            await scope.ServiceProvider.GetRequiredKeyedService<IRunnable>(examples.ElementAt(exampleIndex - 1))
                .RunAsync();
        }
        else if (int.TryParse(input, out var exitIndex) && exitIndex == i)
        {
            break;
        }
        else
        {
            Console.WriteLine("Invalid input");
        }
    }
}

void RegisterExamples() =>
    Assembly.GetExecutingAssembly().GetTypes()
        .Where(t => typeof(IRunnable).IsAssignableFrom(t) && t is { IsInterface: false, IsAbstract: false })
        .Select(t => t)
        .ToList()
        .ForEach(t => { services.AddKeyedTransient(typeof(IRunnable), t.Name, t); });


string[] GetExamples()
{
    return services.Where(sd => sd.ServiceType == typeof(IRunnable))
        .Select(sd => (string)sd.ServiceKey!)
        .ToArray();
}

internal interface IRunnable
{
    Task RunAsync();
}

internal class Example1(ILogger<Example1> logger) : IRunnable
{
    public Task RunAsync()
    {
        logger.LogInformation("Example1");
        return Task.CompletedTask;
    }
}

internal class Example2 : IRunnable
{
    public Task RunAsync()
    {
        Console.WriteLine("Example2");
        return Task.CompletedTask;
    }
}

Un saludo!


Ver también