Search code examples
c#asp.net-mvcasp.net-corediscord.netmediatr

Unable to Consume Scoped Service via MediatR


I have an ASP.NET Core application running .NET 5 and C# 9. This also runs a Discord bot in the background. My ConfigureServices() method in Startup.cs looks like this.

public void ConfigureServices(IServiceCollection services)
{
    services.AddControllersWithViews();
    
    var client = new DiscordSocketClient(new DiscordSocketConfig
    {
        AlwaysDownloadUsers = true,
        MessageCacheSize = 10000,
            
        GatewayIntents = GatewayIntents.Guilds | GatewayIntents.GuildMessages |
                         GatewayIntents.GuildMessageReactions | GatewayIntents.GuildPresences,
            
        LogLevel = LogSeverity.Info
    });
        
    var commandService = new CommandService(new CommandServiceConfig
    {
        LogLevel = LogSeverity.Debug,
        DefaultRunMode = RunMode.Sync,
        CaseSensitiveCommands = false,
        IgnoreExtraArgs = false,
    });
    
    services
        .AddMediatR(Assembly.GetEntryAssembly())
        .AddHostedService<StartupService>()
        .AddHostedService<DiscordListener>()
        .AddScoped<ITestService, TestService>()
        .AddSingleton(client)
        .AddSingleton(provider =>
        {
            commandService.AddModulesAsync(Assembly.GetEntryAssembly(), provider);
            return commandService;
        })
        .AddSingleton(Configuration);
}

As you can see, I have added ITestService and TestService as a scoped service.

public class TestService : ITestService
{
    public async Task<string> GetString()
    {
        await Task.Delay(1);
        return "hey";
    }
}

public interface ITestService
{
    Task<string> GetString();
}

I then inject this service into my command module.

public class TestModule : ModuleBase<SocketCommandContext>
{
    private readonly ITestService _testService;

    public TestModule(ITestService testService)
    {
        _testService = testService;
    }

    [Command("ping")]
    public async Task Ping()
    {
        var str = await _testService.GetString();
        await ReplyAsync(str);
    }
}

However, the application does not respond to the ping command. In fact, my handler for receiving messages is not hit at all (I have checked via breakpoint). This is the hosted services that listens for events and publishes the relevant MediatR notifications.

public partial class DiscordListener : IHostedService
{
    private readonly DiscordSocketClient _client;
    private readonly IServiceScopeFactory _serviceScopeFactory;

    public DiscordListener(
        DiscordSocketClient client,
        IServiceScopeFactory serviceScopeFactory)
    {
        _client = client;
        _serviceScopeFactory = serviceScopeFactory;
    }
    
    public Task StartAsync(CancellationToken cancellationToken)
    {
        _client.MessageReceived += MessageReceived;
        return Task.CompletedTask;
    }

    public Task StopAsync(CancellationToken cancellationToken)
    {
        _client.MessageReceived -= MessageReceived;
        return Task.CompletedTask;
    }

    // Creating our own scope here
    private async Task MessageReceived(SocketMessage message)
    {
        using var scope = _serviceScopeFactory.CreateScope();
        var mediator = scope.ServiceProvider.GetRequiredService<IMediator>();
        await mediator.Publish(new MessageReceivedNotification(message));
    }
}

And this is the notification handler that handles the notification.

public class CommandListener : INotificationHandler<MessageReceivedNotification>
{
    private readonly IConfiguration _configuration;
    private readonly DiscordSocketClient _client;
    private readonly CommandService _commandService;
    private readonly IServiceProvider _serviceProvider;

    public CommandListener(
        IConfiguration configuration,
        DiscordSocketClient client, 
        CommandService commandService, 
        IServiceProvider serviceProvider)
    {
        _configuration = configuration;
        _client = client;
        _commandService = commandService;
        _serviceProvider = serviceProvider;
    }
    
    public async Task Handle(MessageReceivedNotification notification, CancellationToken cancellationToken)
    {
        if (!(notification.Message is SocketUserMessage message)
            || !(message.Author is IGuildUser user)
            || user.IsBot)
        {
            return;
        }
        
        var argPos = 0;
        var prefix = _configuration["Prefix"];
        
        if (message.HasStringPrefix(prefix, ref argPos))
        {
            var context = new SocketCommandContext(_client, message);
            using var scope = _serviceProvider.CreateScope();
            await _commandService.ExecuteAsync(context, argPos, scope.ServiceProvider);
        }
    }
}

Just to clarify, the breakpoint at _client.MessageReceoved += ... is not hit. If I change the ITestService and TestService implementation to a Singleton, then the handler is hit and the command works as expected. Any idea on what I'm doing wrong?

Here is the GitHub repo to the project if you want to see the full code. It is not too large.


Solution

  • This a typical problem when mixing Singleton and scoped services. If you end up with situation a singleton is resolving a scoped service it is not allowed.

    From docs here https://learn.microsoft.com/en-us/aspnet/core/fundamentals/dependency-injection?view=aspnetcore-3.1

    Do not resolve a scoped service from a singleton. It may cause the service to have incorrect state when processing subsequent requests. It's fine to:

    Resolve a singleton service from a scoped or transient service. Resolve a scoped service from another scoped or transient service. By default, in the development environment, resolving a service from another service with a longer lifetime throws an exception. For more information, see Scope validation.

    Also more discussion on https://dotnetcoretutorials.com/2018/03/20/cannot-consume-scoped-service-from-singleton-a-lesson-in-asp-net-core-di-scopes/amp/