当 ASP.NET Core 应用程序关闭时,如何正确安全地处理在容器中注册的单例实例

Dun*_*mon 7 c# asp.net-core asp.net-core-2.0 asp.net-core-2.1

我正在寻找有关如何在我的 ASP.NET Core 2.0 应用程序关闭时正确安全地处理已注册的单例实例的指导。

根据以下文档,如果我注册一个单例实例(通过 IServiceCollection),容器将永远不会尝试创建一个实例(它也不会处理该实例),因此当应用程序关闭时,我只能自己处理这些实例.

https://docs.microsoft.com/en-us/aspnet/core/fundamentals/dependency-injection?view=aspnetcore-2.0(2.1有相同的指导)

我附上了一些伪代码来说明我想要实现的目标。

注意我必须维护对 IServiceCollection 的引用,因为提供给 OnShutDown 方法的 IServiceProvider 是一个简单的服务定位器,并且不能执行复杂的查询。

当应用程序关闭时,我想要一种通用的方法来确保所有的单例实例都被处理掉。我可以直接维护对所有这些单例实例的引用,但这不能很好地扩展。

我最初使用工厂方法来确保 DI 管理我的对象的生命周期,但是,工厂方法的执行发生在运行时处理请求的管道中,这意味着如果它抛出异常,则响应为 500 InternalServerError并记录了一个错误。通过直接创建对象,我正在努力获得更快的反馈,以便启动时的错误导致部署期间的自动回滚。这对我来说似乎并不合理,但同时我不会滥用 DI。

有没有人对我如何更优雅地实现这一目标有任何建议?

namespace MyApp
{
    public class Program
    {
        private static readonly CancellationTokenSource cts = new CancellationTokenSource();

        protected Program()
        {
        }

        public static int Main(string[] args)
        {
            Console.CancelKeyPress += OnExit;
            return RunHost(configuration).GetAwaiter().GetResult();
        }

        protected static void OnExit(object sender, ConsoleCancelEventArgs args)
        {
            cts.Cancel();
        }

        static async Task<int> RunHost()
        {
            await new WebHostBuilder()
                .UseStartup<Startup>()
                .Build()
                .RunAsync(cts.Token);
        }
    }

    public class Startup
    {
        public Startup()
        {
        }

        public void ConfigureServices(IServiceCollection services)
        {
            // This has been massively simplified, the actual objects I construct on the commercial app I work on are
            // lot more complicated to construct and span several lines of code.
            services.AddSingleton<IDisposableSingletonInstance>(new DisposableSingletonInstance());

            // See the OnShutdown method below
            this.serviceCollection = services;
        }

        public void Configure(IApplicationBuilder app)
        {
            var applicationLifetime = app.ApplicationServices.GetRequiredService<IApplicationLifetime>();
            applicationLifetime.ApplicationStopping.Register(this.OnShutdown, app.ApplicationServices);

            app.UseAuthentication();
            app.UseMvc();
        }

        private void OnShutdown(object state)
        {
            var serviceProvider = (IServiceProvider)state;

            var disposables = this.serviceCollection
                .Where(s => s.Lifetime == ServiceLifetime.Singleton &&
                            s.ImplementationInstance != null &&
                            s.ServiceType.GetInterfaces().Contains(typeof(IDisposable)))
                .Select(s => s.ImplementationInstance as IDisposable).ToList();

            foreach (var disposable in disposables)
            {
                disposable?.Dispose();
            }
        }
    }
}
Run Code Online (Sandbox Code Playgroud)

Chr*_*att 6

这不准确。单例在应用程序关闭时被处理,尽管它实际上并不是那么相关,因为当进程停止时,一切都会随之而来

一般的经验法则是,当使用 DI 时,您应该一直使用 DI,这意味着您几乎永远不会在任何地方自行处理。一切都与所有权有关。当你自己添加新东西时,你也有责任处理它。然而,当使用 DI 时,容器是新的东西,因此,容器并且只有容器应该处理这些东西。


Pan*_*vos 6

DI 的工作是处理它创建的任何 IDisposable 对象,无论是瞬态的、作用域的还是单例的。不要注册现有的单身人士,除非你打算事后清理它们。

在问题的代码中,没有理由注册DisposableSingletonInstance. 它应该注册为:

services.AddSingleton<IDisposableSingletonInstance,DisposableSingletonInstance>();
Run Code Online (Sandbox Code Playgroud)

当 IServiceCollection 被释放时,它将调用Dispose()由它创建的所有一次性实体。对于 Web 应用程序,这在RunAsync()结束时发生;

这同样适用于范围服务。但是在这种情况下,实例将在范围退出时被处理,例如当请求结束时。

ASP.NET 为每个请求创建一个范围。如果您希望在该请求结束时处理您的服务,您应该使用以下命令进行注册:

services.AddScoped<IDisposableSingletonInstance,DisposableSingletonInstance>();
Run Code Online (Sandbox Code Playgroud)

验证

对于最新的编辑:

通过直接创建对象,我正在努力获得更快的反馈,以便启动时的错误导致部署期间的自动回滚。

那是一个不同的问题。部署错误通常是由错误的配置值、无响应的数据库等引起的。

验证服务

一种非常快速和肮脏的检查方法是在所有启动步骤完成后实例化单例:

services.GetRequiredService<IDisposableSingletonInstance>();
Run Code Online (Sandbox Code Playgroud)

验证配置

验证配置更复杂,但不是那么棘手。可以在配置类上使用 Data Annotation 属性作为简单规则,并使用Validator类来验证它们。

另一种选择是IValidateable使用Validate必须由每个配置类实现的方法创建接口。这使得使用反射很容易发现。

这篇文章展示了如何在应用程序第一次启动时IValidator结合使用该接口IStartupFilter来验证所有配置对象

从文章:

public class SettingValidationStartupFilter : IStartupFilter  
{
    readonly IEnumerable<IValidatable> _validatableObjects;
    public SettingValidationStartupFilter(IEnumerable<IValidatable> validatableObjects)
    {
        _validatableObjects = validatableObjects;
    }

    public Action<IApplicationBuilder> Configure(Action<IApplicationBuilder> next)
    {
        foreach (var validatableObject in _validatableObjects)
        {
            validatableObject.Validate();
        }

        //don't alter the configuration
        return next;
    }
}
Run Code Online (Sandbox Code Playgroud)

构造函数IValidatable从 DI 提供程序获取所有实现的实例并调用Validate()它们