1

I have a singleton service which consists of a bot collection and methods to add/remove bots from the collection.

The problem occurs in BinanceBot.DoWork(...) when the web socket subscription calls _orderService.GetPreLastOrderAsync(dateTime) (it's commented below). The reason is that ApplicationDbContext is disposed.

The first GetPreLastOrderAsync call in BinanceBot.DoWork (outside web socket's subscription) works fine but the other one (inside the subscription) throws the exception that ApplicationDbContext is disposed.

Cannot access a disposed object. A common cause of this error is disposing a context that was resolved from dependency injection and then later trying to use the same context instance elsewhere in your application. This may occur if you are calling Dispose...

The code below is commented, have a look at DoWork method.

The question is how do I access anything EF Core related (in this case the orders) from web socket's subscription event since ApplicationDbContext is disposed at that point?

Image representing the problem:

enter image description here

// Startup.cs
services.AddSingleton<IBotManagementService, BotManagementService>();
services.AddScoped<IBotService, BotService>();
services.AddScoped<IOrderService, OrderService>();
services.AddScoped<INotificationService, NotificationService>();

// OrderService.cs
public class OrderService : IOrderService
{
    private readonly BinanceDbContext _context;

    public OrderService(BinanceDbContext context)
    {
        _context = context;
    }

    public async Task<Order> GetPreLastOrderAsync(DateTime startTime)
    {
        var asd = _context; // Disposed

        List<Order> orders = await _context.Orders
            .Where(e => e.PlacedAt > startTime)
            .Include(e => e.Bot)
            .ToListAsync();

        int ordersCount = orders.Count;

        if (ordersCount < 2)
        {
            return null;
        }

        return orders.SingleOrDefault(e => e.Id == ordersCount - 2);
    }
}

// The rest
public interface IBotManagementService
{
    Task AddBotAsync(string botName);
    void RemoveBot(string botName);
}

public class BotManagementService : IBotManagementService
{
    private readonly static ConcurrentDictionary<string, Tuple<Task, CancellationTokenSource>> _bots = new ConcurrentDictionary<string, Tuple<Task, CancellationTokenSource>>();

    public async Task AddBotAsync(string botName)
    {
        using var scope = _serviceProvider.CreateScope();
        var botService = scope.ServiceProvider.GetRequiredService<IBotService>();
        var notificationService = scope.ServiceProvider.GetRequiredService<INotificationService>();
        var orderService = scope.ServiceProvider.GetRequiredService<IOrderService>();

        var bot = await botService.GetByNameAsync(botName);

        CancellationTokenSource cts = new CancellationTokenSource();
        Task task = Task.Factory.StartNew(() => new BinanceBot(notificationService, orderService).DoWork(bot, cts.Token));
        _bots.TryAdd(botName, new Tuple<Task, CancellationTokenSource>(task, cts));
    }

    public void RemoveBot(string botName)
    {
        foreach (var bot in _bots)
        {
            if (bot.Key.Contains(botName))
            {
                CancellationTokenSource cts = bot.Value.Item2;
                cts.Cancel();

                _bots.TryRemove(botName, out _);
            }
        }
    }
}

public class BinanceBot : IDisposable
{
    private readonly IBinanceClient _client;
    private readonly IBinanceSocketClient _socketClient;
    private readonly INotificationService _notificationService;
    private readonly IOrderService _orderService;

    public BinanceBot(INotificationService notificationService, IOrderService orderService)
    {
        _client = new BinanceClient();
        _socketClient = new BinanceSocketClient();
        _notificationService = notificationService;
        _orderService = orderService;
    }

    private UpdateSubscription _subscription;

    public void DoWork(Bot bot, CancellationToken token)
    {
        var prelastOrder = _orderService.GetPreLastOrderAsync(DateTime.UtcNow).GetAwaiter().GetResult(); // this call works fine but the one below (in the subscription) throws the exception

        var subResult = _socketClient.SubscribeToKlineUpdates(bot.CryptoPair.Symbol, bot.TimeInterval.Interval, async data =>
        {
            var helper = new BinanceHelper(_notificationService, _orderService);

            ...

            var prelastOrder = await _orderService.GetPreLastOrderAsync(redisBot.StartTime); // Exception is thrown here but it actually appears in the method, because ApplicationDbContext is already disposed
        });

        if (subResult.Success)
        {
            _subscription = subResult.Data;
        }
    }

    private bool _disposed = false;

    public void Dispose()
    {
        Dispose(true);
        GC.SuppressFinalize(this);
    }

    protected virtual void Dispose(bool disposing)
    {
        if (_disposed)
            return;

        if (disposing)
        {
            _client.Dispose();
            _socketClient.Dispose();
        }

        _disposed = true;
    }
}

public class BinanceHelper : IDisposable
{
    private readonly IBinanceClient _client;
    private readonly INotificationService _notificationService;
    private readonly IOrderService _orderService;

    public BinanceHelper(INotificationService notificationService, IOrderService orderService)
    {
        _client = new BinanceClient();
        _notificationService = notificationService;
        _orderService = orderService;
    }

    public void TestMethod()
    {
        await _notificationService.SendNotificationAsync("Test"); // works fine

        Order order = new Order();
        ...
        await _orderService.CreateAsync(order);
    }
}

Solution:

To fix that, I passed IServiceProvider through the constructors of BinanceBot and BinanceHelper. That's basically instead of creating a scope at BotManagementService which is later disposing DbContext, I'm creating a new scope everytime the subscription pops.

var subResult = _socketClient.SubscribeToKlineUpdates(bot.CryptoPair.Symbol, bot.TimeInterval.Interval, async data =>
{
    using var scope = _serviceProvider.CreateScope();
    var notificationService = scope.ServiceProvider.GetRequiredService<INotificationService>();
    var orderService = scope.ServiceProvider.GetRequiredService<IOrderService>();

    ...
}
nop
  • 4,711
  • 6
  • 32
  • 93
  • where are you injecting the DbContext? it's usually caused by injecting context as a singleton. There are ways of adding scoped context to a singleton service. – zetawars Jan 16 '20 at 12:44
  • @zetawars, inside OrderService. I edited the code. – nop Jan 16 '20 at 12:50
  • Yes. you must be doing that in Startup.cs at someplace also? is it scoped or singleton? – zetawars Jan 16 '20 at 12:54
  • @zetawars, `services.AddDbContext(options => options.UseSqlServer(Configuration.GetConnectionString("DefaultConnection")));`, scoped. – nop Jan 16 '20 at 12:56
  • Please look at this question. I solved it using the answer from Spook https://stackoverflow.com/questions/36332239/use-dbcontext-in-asp-net-singleton-injected-class – zetawars Jan 16 '20 at 13:01
  • @zetawars, I'm already using `IServiceProvider` if you look at `BotManagementService`. – nop Jan 16 '20 at 13:09
  • Ohh! sorry about that. i thought maybe OrderService is a singleton class. – zetawars Jan 16 '20 at 13:13
  • how about try creating the OrderService object through constructor. and getting the DbContext scope inside the BotManagementService? – zetawars Jan 16 '20 at 13:15
  • @zetawars, thanks! I realized what I had to do: I called `_serviceProvider.CreateScope()` inside the web socket subscription, so now it will create a new scope everytime the event gets subscribed instead of one scope at BotManagementService which later not usable because it's getting disposed. Thanks again for reminding me to look at IServiceProvider. – nop Jan 16 '20 at 13:40
  • @nop rather than editing your solution into your question, post it as an answer then accept it once the time limit elapses (the time limit exists to prevent people from farming reputation by posting spam questions with spam answers that they accept). – Ian Kemp Jan 16 '20 at 13:53
  • @nop. np Glad that i could help :D – zetawars Jan 16 '20 at 14:07

1 Answers1

1

I have had similar problems my cure was to omit the await on the _context. call remove the async from the method definition add return Task.FromResult(orders);

this ensures the the _context is still called before it is disposed, you will still take advantage of asynchronous operations as the call to GetPreLastOrderAsync(DateTime startTime) will use await (probably).