Search code examples
c#entity-frameworkasp.net-corehangfire

DbContext instance is used twice at the same time


I saw similar problems on the internet, but couldn't really fit any of them to my code.

I'm running two hangfire jobs and sometimes they have to run at the same time.

RecurringJob.AddOrUpdate(()=>abill.CheckUserPayment(),Cron.Minutely);
RecurringJob.AddOrUpdate("CalculateUserCharge",()=>abill.CalculateUserCharge(DateTime.Today.AddDays(-1)),Cron.Daily(12,37),TimeZoneInfo.Utc);

RecurringJobs are launched from Configure() method from startup and I pass AccountBilling abill as a parameter.

AccountBilling.cs (whole code is not present)

 public readonly EntityContext context;
    private TBCPaymentOptions _tbcPaymentOptions = null;
    public AccountBilling (EntityContext _context) {
        context = _context;
    }

    public AccountBilling (EntityContext _context, IOptions<TBCPaymentOptions> tbcPaymentOptions) {
        context = _context;
        this._tbcPaymentOptions = tbcPaymentOptions.Value;

    }

    public void Save () {
        try {
            context.SaveChanges ();

        } catch (Exception e) {
            Console.WriteLine (e);
            throw;
        }

    }

    public void CalculateUserCharge (DateTime date) {
        var latestJob = context.JobLogs.Include (c => c.JobStatus).OrderByDescending (c => c.StartDate).Where (c => c.JobId == (int) JobEnum.CloseDay).FirstOrDefault ();
        var jobLog = new JobLog ();
        jobLog.JobId = (int) JobEnum.CloseDay;
        jobLog.JobStatusID = (int) JobStatusEnum.Active;
        jobLog.StartDate = DateTime.Now;
        context.Add (jobLog);
        this.Save ();

        Console.WriteLine ("Starting...");

        if (latestJob != null && latestJob.JobStatusID == (int) JobStatusEnum.Active) {
            jobLog.JobStatusID = (int) JobStatusEnum.Canceled;
            jobLog.EndDate = DateTime.Now;

            context.Update (jobLog);
            context.SaveChanges ();

        } else {

            try {

                var result = new List<GetActiveUserPackagesForOpenBillingPeriodResult> ();

                using (var conn = new NpgsqlConnection (context.ConnectionString)) {
                    conn.Open ();

                    using (var cmd = new NpgsqlCommand ("\"GetActiveUserPackagesForOpenBillingPeriod\"", conn)) {
                        Console.WriteLine ("გავუშვი command");

                        cmd.CommandType = CommandType.StoredProcedure;
                        cmd.Parameters.AddWithValue ("somedate", DateTime.Today);

                        var reader = cmd.ExecuteReader ();

                        string x = DBNull.Value.Equals (reader) ? " " : reader.ToString ();

                        if (x != null) {
                            while (reader.Read ()) {
                                result.Add (
                                    new GetActiveUserPackagesForOpenBillingPeriodResult {
                                        Amount = (decimal) reader["Amount"],
                                            PackageID = (int) reader["PackageID"],
                                            UserID = (int) reader["UserID"],
                                            AccountID = (int) reader["AccountID"],
                                            UserPackageStartDate = (DateTime) reader["UserPackageStartDate"],
                                    }
                                );
                            }
                        }
                        conn.Close ();
                    }

                    var groupByResults = result.GroupBy (c => c.AccountID).Select (a => new {
                        accountId = a.Key,
                            lines = a.ToList ()
                    });

                    foreach (var group in groupByResults) {

                        var transactionHeader = new TransactionHeader () {

                            TransactionHeaderTypeID = (int) TransactionHeaderTypeEnum.Charge,
                            Date = date,
                            CorrectionDescription = null,
                            AccountID = group.accountId
                        };

                        foreach (var lineItem in group.lines) {
                            transactionHeader.TransactionLines.Add (new TransactionLine () {
                                UserID = lineItem.UserID,
                                    PackageID = lineItem.PackageID,
                                    Amount = this.CalculateUserChargeMethod (date, lineItem.Amount, lineItem.UserPackageStartDate)
                            });

                        }

                        transactionHeader.TotalAmount = transactionHeader.TransactionLines.Sum (c => c.Amount);

                        this.context.TransactionHeaders.Add (transactionHeader);

                        context.Add (transactionHeader);
                        this.Save ();

                    }
                    jobLog.EndDate = DateTime.Now;
                    jobLog.JobStatusID = (int) JobStatusEnum.Inactive;

                    context.Update (jobLog);
                    this.Save ();

                    ClosePeriodOnEndOfMonth (date, conn);
                }

            } catch (Exception ex) {
                jobLog.EndDate = DateTime.Now;
                jobLog.JobStatusID = (int) JobStatusEnum.Canceled;
                jobLog.Comment = ex.ToString ();
                context.Update (jobLog);
                this.Save ();

                //throw ex;
            }
        }

..........

public void CheckUserPayment () {

        var Cert = new X509Certificate2 ("cert.p12", _tbcPaymentOptions.TBCPayCertificatePassword, X509KeyStorageFlags.MachineKeySet);
        var time = DateTime.Now.AddMinutes (-5);

        var latestJob = context.JobLogs.Include (c => c.JobStatus).OrderByDescending (c => c.StartDate).Where (c => c.JobId == (int) JobEnum.CheckPayment).FirstOrDefault ();
        var jobLog = new JobLog ();
        jobLog.JobId = (int) JobEnum.CheckPayment;
        jobLog.JobStatusID = (int) JobStatusEnum.Active;
        jobLog.StartDate = DateTime.Now;
        context.Add (jobLog);
        this.Save ();

        if (latestJob != null && latestJob.JobStatusID == (int) JobStatusEnum.Active) {
            jobLog.JobStatusID = (int) JobStatusEnum.Canceled;
            jobLog.EndDate = DateTime.Now;
            context.Update (jobLog);
            this.Save ();

        } else {
            try {
                var processingPayments = context.Payments.Where (c => c.AcceptanceAct.AcceptanceActStatusID == (int) AcceptanceActStatusEnum.Processing && c.CreatedDate < time).ToList ();
                System.Console.WriteLine ("IN METHOD");
                foreach (var item in processingPayments) {
                    System.Console.WriteLine ("Job Started");
                    this.CheckUserPaymentMethod (item.BankPaymentCode, item, _tbcPaymentOptions.AppPlatformIP, _tbcPaymentOptions.MerchantURL, Cert);

                }
                Console.WriteLine ("Job Done");
                jobLog.JobStatusID = (int) JobStatusEnum.Inactive;
                jobLog.EndDate = DateTime.Now;
                context.Update (jobLog);
                this.Save ();

                //this.Save ();
            } catch (Exception ex) {
                jobLog.JobStatusID = (int) JobStatusEnum.Canceled;
                jobLog.EndDate = DateTime.Now;
                jobLog.Comment = ex.ToString ();
                context.Update (jobLog);
                this.Save ();
            }
        }

    }

Startup.cs (whole code is not present):

services.AddEntityFrameworkNpgsql ()
    .AddDbContext<EntityContext> (
      options => options.UseNpgsql (connectionString)

    );

Solution

  • The problem is that abill is effectively being used as if it were a singleton object. This is going to cause you problems, particularly with DbContext operations.

    You need to change that and allow Hangfire to leverage DI properly. First give your AccountBilling class an interface to use for injection:

    public class AccountBilling : IAccountBilling
    {
        // snip
    }
    

    Now add it to your container:

    services.AddScoped<IAccountBilling, AccountBilling>();
    

    Now in your startup code, instead of using the abill object, inject it properly:

    RecurringJob.AddOrUpdate<IAccountBilling>(a => a.CheckUserPayment(), Cron.Minutely);