创建一个没有后台服务的.NET Core控制台应用程序的“正确”方法

39
我正在构建一个简单的.NET Core控制台应用程序,它将从命令行读取基本选项,然后在没有用户交互的情况下执行并终止。我想利用DI,所以我使用了.NET Core通用主机。
我找到的所有构建控制台应用程序的示例都创建了一个实现IHostedService或扩展BackgroundService的类。然后,通过AddHostedService将该类添加到服务容器中,并通过StartAsync或ExecuteAsync开始应用程序的工作。但是,在所有这些示例中,它们都是在实现后台服务或其他运行循环或等待请求的应用程序,直到被操作系统关闭或收到某个请求终止为止。如果我只想要一个启动、执行操作,然后退出的应用程序怎么办?例如:
Program.cs:
namespace MyApp
{
    using System;
    using System.Threading.Tasks;
    using Microsoft.Extensions.DependencyInjection;
    using Microsoft.Extensions.Hosting;
    using Microsoft.Extensions.Logging;

    public static class Program
    {
        public static async Task Main(string[] args)
        {
            await CreateHostBuilder(args).RunConsoleAsync();
        }

        private static IHostBuilder CreateHostBuilder(string[] args) =>
            Host.CreateDefaultBuilder(args)
                .UseConsoleLifetime()
                .ConfigureLogging(builder => builder.SetMinimumLevel(LogLevel.Warning))
                .ConfigureServices((hostContext, services) =>
                {
                    services.Configure<MyServiceOptions>(hostContext.Configuration);
                    services.AddHostedService<MyService>();
                    services.AddSingleton(Console.Out);
                });
    }
}

MyServiceOptions.cs:

namespace MyApp
{
    public class MyServiceOptions
    {
        public int OpCode { get; set; }
        public int Operand { get; set; }
    }
}

我的服务.cs:

namespace MyApp
{
    using System.IO;
    using System.Threading;
    using System.Threading.Tasks;
    using Microsoft.Extensions.Hosting;
    using Microsoft.Extensions.Options;

    public class MyService : IHostedService
    {
        private readonly MyServiceOptions _options;
        private readonly TextWriter _outputWriter;

        public MyService(TextWriter outputWriter, IOptions<MyServiceOptions> options)
        {
            _options = options.Value;
            _outputWriter = outputWriter;
        }

        public async Task StartAsync(CancellationToken cancellationToken)
        {
            _outputWriter.WriteLine("Starting work");

            DoOperation(_options.OpCode, _options.Operand);

            _outputWriter.WriteLine("Work complete");
        }

        public async Task StopAsync(CancellationToken cancellationToken)
        {
            _outputWriter.WriteLine("StopAsync");
        }

        protected void DoOperation(int opCode, int operand)
        {
            _outputWriter.WriteLine("Doing {0} to {1}...", opCode, operand);

            // Do work that might take awhile
        }
    }
}
这段代码编译和运行都没有问题,会输出以下内容:
Starting work
Doing 1 to 2...
Work complete

然而,在此之后,应用程序将一直等待,直到我按下Ctrl+C。我知道在工作完成后可以强制关闭应用程序,但此时,我觉得我没有正确使用IHostedService。它似乎是为定期后台进程而设计的,而不是像这样简单的控制台应用程序。然而,在实际应用程序中,DoOperation可能需要20-30分钟,我希望在终止之前利用StopAsync方法进行清理。我也知道我可以自己创建服务容器等所有内容,但.NET Core通用主机已经做了很多我想要做的事情。它似乎是编写控制台应用程序的正确方式,但如果没有添加启动实际工作的托管服务,那么如何让应用程序真正执行任何操作呢?


如果你只需要一个 DI 容器,为什么不在标准控制台应用程序中创建一个 ServiceCollection 呢? - ProgrammingLlama
9
我不仅希望如此。我还想使用像日志记录、配置和主机生命周期管理等功能。我知道我可以自己配置和使用这些服务,但似乎.NET Core通用主机专门为此设计。 - Alan Shearer
2个回答

38

与其使用托管服务,我建议使用以下方法;

using (var host = CreateHostBuilder(args).Build())
{
    await host.StartAsync();
    var lifetime = host.Services.GetRequiredService<IHostApplicationLifetime>();

    // do work here / get your work service ...

    lifetime.StopApplication();
    await host.WaitForShutdownAsync();
}

我非常喜欢这种方法。它允许使用通用主机和由CreateDefaultBuilder配置的所有好处,同时仍然允许您启动实际的应用程序逻辑,而无需将类绑定到IHostedService或BackgroundService。 - Alan Shearer
2
取消令牌怎么样?主机应该为我们管理像Ctrl-C这样的事情,如果我们的主要服务是异步的,使用HostedService会更简单,因为它会自动接收令牌。 - Rast
1
该令牌是从lifetime.ApplicationStopping派生而来的,该对象可以轻松地从上述示例中访问。如果您喜欢托管服务,请使用它们。问题是如何避免使用托管服务的方法。 - Jeremy Lakeman

10
我知道我可以在工作完成后强制关闭应用程序,但此时我感觉我没有正确地使用 IHostedService。
我同意这似乎很奇怪。实际上,我总是在所有 IHostedService 实现的结尾停止应用程序。即使是长时间运行的服务器应用程序也是如此。如果托管服务停止(或失败),那么我明确地希望应用程序结束。
这种设计在 .NET Core 推出时似乎并不完善。设计的部分内容是为了让托管服务及其应用程序具有独立的生命周期,但由于它们无法重新启动,这在实践中并不有用。因此,它最终感觉像是一种不良设计,因为生命周期不能独立,但它们默认情况下是独立的。
我的所有托管服务最终都会将它们的生命周期与应用程序生命周期绑定
finally
{
  _hostApplicationLifetime.StopApplication();
}

你认为在一个简单的控制台应用程序中(只执行一次而不是托管服务),使用Host.StartAsyncIHostApplicationLifetime有什么好处吗? - Jan Suchotzki
2
有时候,即使只是运行并退出,如果我需要框架的足够 DI/config 等部分,我会使用完整的主机堆栈(工作服务模板)。这是一个判断调用。 - Stephen Cleary

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接