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:
// 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>();
...
}