2

I'm trying to figure out why my db context is being diposed of with the code below. I'm trying to update the status of the record throughout this processing stage, but I'm getting an exception when attempting to save the changes to the status field of htis entity.

It makes no difference if I use async or non-async methods on the .SaveChanges() or the .FirstOrDefault() calls.

This is registered to be run by an IHostedService

The IHostedService:

public class HostedDaemon : IHostedService
{
    private readonly ILogger<HostedDaemon> _logger;
    private Timer _queueTimer;
    private Timer _runTimer;
    private IServiceProvider Services { get; set; }

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

    public Task StartAsync(CancellationToken cancellationToken)
    {
        var queueAutoEvent = new AutoResetEvent(false);
        _logger.LogInformation("HostedDaemon Starting");
        _queueTimer = new Timer(QueueJobs, queueAutoEvent, TimeSpan.Zero,
            TimeSpan.FromSeconds(900));
        _runTimer = new Timer(RunQueuedJobs, queueAutoEvent, TimeSpan.Zero,
            TimeSpan.FromSeconds(10));
        return Task.CompletedTask;
    }
    public Task StopAsync(CancellationToken cancellationToken)
    {
        _logger.LogInformation("HostedDaemon Stopping");
        _queueTimer?.Change(Timeout.Infinite, 0);
        _runTimer?.Change(Timeout.Infinite, 0);
        return Task.CompletedTask;
    }
    public void Dispose()
    {
        _queueTimer?.Dispose();
        _runTimer?.Dispose();
    }

    private void QueueJobs(object stateInfo)
    {
        using (var scope = Services.CreateScope())
        {
            var autoEvent = (AutoResetEvent)stateInfo;
            var daemonProcessing = scope.ServiceProvider.GetRequiredService<IDaemonService>();
            //daemonProcessing.QueueJob();
            //daemonProcessing.RunJob();
            autoEvent.Set();
        }
    }
    private void RunQueuedJobs(object stateInfo)
    {
        using (var scope = Services.CreateScope())
        {
            var autoEvent = (AutoResetEvent)stateInfo;
            var daemonProcessing = scope.ServiceProvider.GetRequiredService<IDaemonService>();
            //daemonProcessing.QueueJob();
             daemonProcessing.RunJob(); 
            autoEvent.Set();
        }
    }
}

Background Service:

    public async Task RunJob()
    {
        try
        {
            _logger.LogInformation("Starting transaction processing");
            //TODO: Hook up SP
            var nextQueuedJob = _context.Transaction
                .Where(x => x.TransactionStatusId == (int) TransactionStatus.Queued).OrderBy(x => x.QueuedTime)
                .Include(x => x.BrokerageData).FirstOrDefault();

            if (nextQueuedJob != null)
            {
                nextQueuedJob.TransactionStatusId = (int)TransactionStatus.ProcessingData;
                _context.Transaction.Update(nextQueuedJob);
                //await _context.SaveChangesAsync();
                var brokerageData = nextQueuedJob.BrokerageData.FirstOrDefault();
                var order = JsonConvert.DeserializeObject<OrderShort>(brokerageData.BrokerageContent);

                var dynamicsSalesOrder = await CreateNewSalesOrderShort(order);
                var salesOrderLines = new List<SalesOrderLineShort>();
                foreach (var orderItem in order.Items)
                {
                    var unitPrice = (orderItem.SubTotal + orderItem.Tax)/orderItem.Quantity;
                    var salesOrderLine = new SalesOrderLineShort
                    {
                        ItemId = new Guid("42dd99ff-7133-41f7-88c5-81f922ef77dd"),
                        LineType = "Item",
                        Description = orderItem.ProductDescription,
                        UnitOfMeasureId = new Guid("cab0928b-e7af-4bd6-923f-3706b4761681"),
                        UnitPrice = unitPrice,
                        DiscountAmount = orderItem.CouponDiscountAmount,
                        Quantity = orderItem.Quantity,
                        TaxCode = "DEV"
                    };
                    salesOrderLines.Add(salesOrderLine);
                }
                nextQueuedJob.TransactionStatusId = (int)TransactionStatus.ProcessingComplete;
                await _context.SaveChangesAsync();

                nextQueuedJob.TransactionStatusId = (int)TransactionStatus.ReadyForTransfer;
                //TOOO: hook into reconciliation tables to commit an update 
                nextQueuedJob.TransactionStatusId = (int)TransactionStatus.TransferringData;

                foreach (var salesOrderLine in salesOrderLines)
                {

                    await _requestClient.PostRequest(
                        $"https://api.businesscentral.dynamics.com/v1.0/afff8eda-37c9-4c72-833b-a36dc6c8d5df/api/beta/companies/c7fdd45d-617d-47b3-adbe-7c0c69c21dd1/salesOrders/{dynamicsSalesOrder.Id}/salesOrderLines",
                        salesOrderLine);
                }

                nextQueuedJob.TransactionStatusId = (int)TransactionStatus.TransferComplete;

                nextQueuedJob.TransactionStatusId = (int)TransactionStatus.ReadyForValidation;
                nextQueuedJob.TransactionStatusId = (int)TransactionStatus.ValidatingOriginData;
                var query = new CrmQuery();
                query.UpdateOrderStatus(order.SalesOrderId, CrmQuery.OrderFIStatus.Completed,
                    order.FI_LastModified);
                nextQueuedJob.TransactionStatusId = (int)TransactionStatus.TransactionCompleted;
            }
        }
        catch (Exception e)
        {
            _logger.LogError($"exception occurred while running job.\n{e.InnerException}");
        }

    }

Exception:

Message "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() on the context, or wrapping the
context in a using statement. If you are using dependency injection, you 
should let the dependency injection container take care of disposing context 
instances.\r\nObject name: 'FisEntities'."  string

Startup Configuration:

public void ConfigureServices(IServiceCollection services)
    {
        DynamicsAccountGuid = Configuration["Services:Dynamics:AccountGuid"];
        DynamicsCompany = Configuration["Services:Dynamics:Companies:Development"];
        DynamicsBasicAuth = Configuration["Services:Dynamics:BasicAuth"];
        ConnectionString = Configuration["ConnectionStrings:FIS_Local"];

        services.AddSingleton(Configuration);
        services.AddEntityFrameworkSqlServer()
            .AddDbContext<FisEntities>(options => options.UseSqlServer(ConnectionString));

        services.Configure<CookiePolicyOptions>(options =>
        {
            // This lambda determines whether user consent for non-essential cookies is needed for a given request.
            options.CheckConsentNeeded = context => true;
            options.MinimumSameSitePolicy = SameSiteMode.None;
        });

        services.AddHttpClient<IRequestClient, DynamicsClient>("dynamics", d =>
        {
            d.BaseAddress = new Uri($"https://api.businesscentral.dynamics.com/v1.0/{DynamicsAccountGuid}/api/beta/companies/{DynamicsCompany}");
            d.DefaultRequestHeaders.Add("Authorization", DynamicsBasicAuth);
        });

        services.AddTransient<IQueueClient, CrmClient>();
        services.AddHostedService<HostedDaemon>();
        services.AddScoped<IDaemonService, TransactionService>();
        services.AddMvc().SetCompatibilityVersion(CompatibilityVersion.Version_2_2);
    }
15
  • 1
    When getting an exception please remember to post the actual error message into the question. From the looks of what you have above nothing should be disposing of the context Commented Mar 15, 2019 at 9:36
  • I'm getting an exception- Why did not add the exception message to question? Please add the whole exception message to the question and the whole method including the method signature. Commented Mar 15, 2019 at 11:53
  • Are you by chance using some old/funky MySQL provider from Oracle for it? Commented Mar 15, 2019 at 15:31
  • @Gibbon it's a generic object disposed exception, but I've added the text from it regardless. Commented Mar 15, 2019 at 18:22
  • @TanvirArjel added, but it's generic Commented Mar 15, 2019 at 18:22

1 Answer 1

2

In your RunQueuedJobs method in HostedDaemon class, replace daemonProcessing.RunJob(); with daemonProcessing.RunJob().Wait(); as follows:

private void RunQueuedJobs(object stateInfo)
{
    using (var scope = Services.CreateScope())
    {
        var autoEvent = (AutoResetEvent)stateInfo;
        var daemonProcessing = scope.ServiceProvider.GetRequiredService<IDaemonService>();
        //daemonProcessing.QueueJob();
         daemonProcessing.RunJob().Wait(); // <-- Here it is
        autoEvent.Set();
    }
}

Now it should work properly.

Sign up to request clarification or add additional context in comments.

1 Comment

Thanks for the answer, that helped! Can you explain why exactly that is necessary?

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.