在ASP.NET Core中注册HostedService的正确方法:AddHostedService vs AddSingleton。

72

如何在ASP.NET Core 2.1中正确注册自定义托管服务?例如,我有一个从BackgroundService派生的自定义托管服务,名为MyHostedService。应该如何注册它?

public IServiceProvider ConfigureServices(IServiceCollection services)
{           
    //...
    services.AddSingleton<IHostedService, MyHostedService>();
}

或者

public IServiceProvider ConfigureServices(IServiceCollection services)
{           
    //...
    services.AddHostedService<MyHostedService>();
}

?

这里我们可以看到第一个情况,但是这里有第二个情况。

这两种方法相等吗?


1
建议使用更近期的文档,推荐第二个选项。 - Nkosi
2
它们是否相等?第二个调用第一个,但作为瞬态而不是单例。 - Nkosi
3
不仅仅是单例与非单例的区别。托管服务通过它们的“StartAsync”、“StopAsync”方法获得运行时的特殊处理。使用作用域/瞬态对象可以通过使用作用域来实现。 - Panagiotis Kanavos
3个回答

64

更新

过去,HostedService是一个长期存在的瞬态,实际上充当单例。自.NET Core 3.1以来,它是一个真正的Singleton


使用AddHostedService

托管服务不仅仅是一个单例服务。运行时“知道”它,并且可以通过调用StartAsync启动或通过调用StopAsync()停止,例如应用程序池被回收时。运行时可以在 Web 应用程序本身终止之前等待托管服务完成。

文档所述,作用域作用域服务可以通过在托管服务的工作者方法中创建范围来消耗。 短暂服务同样适用。

为此,在托管服务的构造函数中注入IServicesProvider或IServiceScopeFactory,并用于创建范围。

参考文档,服务的构造函数和工作者方法可以像这样:

public IServiceProvider Services { get; }

public ConsumeScopedServiceHostedService(IServiceProvider services, 
    ILogger<ConsumeScopedServiceHostedService> logger)
{
    Services = services;
    _logger = logger;
}


private void DoWork()
{
    using (var scope = Services.CreateScope())
    {
        var scopedProcessingService = 
            scope.ServiceProvider
                .GetRequiredService<IScopedProcessingService>();

        scopedProcessingService.DoWork();
    }
}

这个相关的问题展示了如何在托管服务中使用短暂的 DbContext:

public class MyHostedService : IHostedService
{
    private readonly IServiceScopeFactory scopeFactory;

    public MyHostedService(IServiceScopeFactory scopeFactory)
    {
        this.scopeFactory = scopeFactory;
    }

    public void DoWork()
    {
        using (var scope = scopeFactory.CreateScope())
        {
            var dbContext = scope.ServiceProvider.GetRequiredService<MyDbContext>();
            …
        }
    }
    …
}

8
稍作修改:托管服务实际上不是一种单例服务。您可以针对某些具体类多次调用AddHostedService。运行时会启动多个服务实例。例如,这可与BackgroundService一起使用以设置工作进程池。 - kayjtea
1
@kayjtea 我并不是在暗示它是单例模式,我只是在回答问题。但现在看来托管服务确实是单例模式了。 - Panagiotis Kanavos
现在它是否能够正常工作?是否可以通过Factory和ActivatorUtilites.CreateInstance来实现? - xSx

33

更新

在.Net Core 2.2和3.1之间,AddHostedService的行为已经发生了变化,现在它添加的是Singleton而不是以前的Transient服务。 引用- 评论来自LeonG

public static class ServiceCollectionHostedServiceExtensions
{
    /// <summary>
    /// Add an <see cref="IHostedService"/> registration for the given type.
    /// </summary>
    /// <typeparam name="THostedService">An <see cref="IHostedService"/> to register.</typeparam>
    /// <param name="services">The <see cref="IServiceCollection"/> to register with.</param>
    /// <returns>The original <see cref="IServiceCollection"/>.</returns>
    public static IServiceCollection AddHostedService<[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicConstructors)] THostedService>(this IServiceCollection services)
        where THostedService : class, IHostedService
    {
        services.TryAddEnumerable(ServiceDescriptor.Singleton<IHostedService, THostedService>());

        return services;
    }

    /// <summary>
    /// Add an <see cref="IHostedService"/> registration for the given type.
    /// </summary>
    /// <typeparam name="THostedService">An <see cref="IHostedService"/> to register.</typeparam>
    /// <param name="services">The <see cref="IServiceCollection"/> to register with.</param>
    /// <param name="implementationFactory">A factory to create new instances of the service implementation.</param>
    /// <returns>The original <see cref="IServiceCollection"/>.</returns>
    public static IServiceCollection AddHostedService<THostedService>(this IServiceCollection services, Func<IServiceProvider, THostedService> implementationFactory)
        where THostedService : class, IHostedService
    {
        services.TryAddEnumerable(ServiceDescriptor.Singleton<IHostedService>(implementationFactory));

        return services;
    }
}

参考ServiceCollectionHostedServiceExtensions


原始答案

它们相似但不完全相同

AddHostedServiceMicrosoft.Extensions.Hosting.Abstractions的一部分。

它属于Microsoft.Extensions.Hosting.Abstractions中的ServiceCollectionHostedServiceExtensions类。

using Microsoft.Extensions.Hosting;

namespace Microsoft.Extensions.DependencyInjection
{
    public static class ServiceCollectionHostedServiceExtensions
    {
        /// <summary>
        /// Add an <see cref="IHostedService"/> registration for the given type.
        /// </summary>
        /// <typeparam name="THostedService">An <see cref="IHostedService"/> to register.</typeparam>
        /// <param name="services">The <see cref="IServiceCollection"/> to register with.</param>
        /// <returns>The original <see cref="IServiceCollection"/>.</returns>
        public static IServiceCollection AddHostedService<THostedService>(this IServiceCollection services)
            where THostedService : class, IHostedService
            => services.AddTransient<IHostedService, THostedService>();
    }
}

请注意,它使用的是“瞬态”生命周期范围而不是“单例”。
内部框架将所有托管服务添加到另一个服务中(HostedServiceExecutor)
public HostedServiceExecutor(ILogger<HostedServiceExecutor> logger, 
    IEnumerable<IHostedService> services) //<<-- note services collection
{
    _logger = logger;
    _services = services;
}

在启动时,可以通过WebHost构造函数创建一个单例。

_applicationServiceCollection.AddSingleton<HostedServiceExecutor>();

1
这意味着,尽管服务在AddHostedService中被注册为scoped,但它的生命周期将与应用程序生命周期一样长,对吗? - Denis Babarykin
3
@DenisBabarykin 是的,那会很准确。 - Nkosi
@johnny5 不一定。短暂的生命周期服务仍将得到解决。只是由单例模式解决一次。 :) 短暂的生命周期服务在每次请求时都会被创建 - Nkosi
4
@johnny5 scoped是特殊的。 单例仍然可以调用瞬态生命周期的依赖项。 从技术上讲,瞬态依赖项本身变成了单例,因为该实例将位于单例中。 - Nkosi
6
在 .Net Core 2.2 和 3.1 之间,AddHostedService 方法的行为发生了变化,现在它添加的是 Singleton 服务而不是之前的 Transient 服务。 - LeonG
显示剩余5条评论

17

一个巨大的区别是,AddSingleton() 是懒加载的,而 AddHostedService() 是急切加载的。

使用 AddSingleton() 添加的服务将在第一次注入到类构造函数中时实例化。这对于大多数服务来说都很好,但如果它确实是一个后台服务,您可能希望它立即启动。

使用 AddHostedService() 添加的服务将立即实例化,即使没有其他类需要将其注入到其构造函数中。这通常适用于一直运行的后台服务。

另外,似乎不可以将使用 AddHostedService() 添加的服务注入到另一个类中。


你的最后一句话对我很关键。我发现无法将托管服务 DI 到另一个类中,这相当不方便! - Erik Westwig
1
@ErikWestwig 你只需要将它包装在一个范围内。在构造函数中包含 IServiceProvider services,然后调用 services.CreateScope - David Graham
1
@ErikWestwig 你只需要将它包裹在一个作用域中。在构造函数中包含 IServiceProvider services,然后调用 services.CreateScope - undefined
@DavidGraham,你能详细解释一下吗?我和Erik一样感到很沮丧。我想创建一个主服务来管理应用程序的生命周期(因此必须派生自IHostedService并添加AddHostedService),但也要对控制器可用(因此必须派生自IMasterService并添加AddSingleton)。 - stephen
@stephen,你可以尝试像使用AddHostedService一样添加服务,但同时创建一个接口,以便从作用域中获取。你的控制器可以访问IWorkerController接口。你可以根据需要创建该控制器接口来执行所需操作,如果将其注入到工作器和控制器中,它们都可以使用它。 - David Graham
@stephen 你觉得要不要像使用AddHostedService一样添加服务,但创建一个可以从作用域中获取的接口。你的控制器可以访问IWorkerController。你创建这个控制器接口来做你需要的事情,然后如果你把它注入到worker和控制器中,它们都可以使用它。 - undefined

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