Function Fails when Calling Another Function in same library if it requires transaction

I’m trying to call function B from function A but and found that as soon as I enable requires transaction on A or B the function throws the following error “Sorry! Something went wrong. Please contact your system administrator. Correlation Id: b90a31bb-27dc-475e-a099-5c6ca21fdff1”

Function A needs to update information but it was getting a bit too big so I tried breaking it up using the helper function B.

Function A will update OrderHed info while B only gets shipping info from OrderHed and OrderMsc.

Here is Function A (Requires Transaction)


// FUNCTION: RefreshApprovals
// Return values
hasSalesHold = false;
output = "";

var context = (Erp.ErpContext)Ice.Services.ContextFactory.CreateContext();
var erpContext = new Erp.Internal.Lib.CCredChk(context);

using(var txScope = Ice.IceContext.CreateDefaultTransactionScope())
{
  // UPDATE APPROVALS ON HEADER
  foreach(var oHead in (from h in erpContext.Db.OrderHed.With(LockHint.UpdLock) where
  h.Company == this.CompanyID && 
  h.OrderNum == orderNum
  select h))
  {
    bool interCompany = false;
    foreach(var cust in (from c in erpContext.Db.Customer where
    c.Company == this.CompanyID &&
    c.CustNum == oHead.CustNum
    select c))
    {
      interCompany = cust.ICCust;
    }
    if(interCompany)
    {
      oHead.OrderApproved_c = true;
    }
    
    var shippingInfo = this.ThisLib.GetShippingInfo(orderNum);
    string salesHolds = shippingInfo.output;
    hasSalesHold = (!shippingInfo.shippingApproved && ! interCompany);

    oHead.ApprovalInfo_c = output;
  }
  erpContext.Db.Validate();
  txScope.Complete();

Here is Function B (Does not require transaction)

// FUNCTION: GetShippingInfo
// Return Values
shippingApproved = false;
output = "";

foreach(var oMisc in (from o in Db.OrderMsc where
o.Company == this.CompanyID &&
o.OrderNum == orderNum &&
(o.MiscCode == "FRTC" ||
o.MiscCode == "FRT")
select o))
{ 
  shippingApproved |= oMisc.DocMiscAmt > 0.0M; // Check if they have a freight charge of more than zero
}
if(!shippingApproved) // If no freight charge do they have account info?
{
  foreach(var oHead in (from h in Db.OrderHed where
  h.Company == this.CompanyID && 
  h.OrderNum == orderNum
  select h))
  {
    shippingApproved |= oHead.PayFlag != "" && oHead.PayAccount != "" && oHead.PayBTAddress1 != "" && oHead.PayBTZip != "";
  }
}
if(!shippingApproved) output += "No shipping! Add freight or payors shipping account info (Manifest Detail > Billing)\n";

I found that if I remove every call to Db in function B then I don’t get an error. Is there a better way to break up my function into helper methods or should I just leave everything in function A?

1 Like

Leave it in A.

Use Func, Action Etc methods

            Action<int> showAgeAction = (age) =>
            {
                Console.WriteLine($"The person is {age} years old");
            };

            showAgeAction(10);

            Func<int, int, int> sumFunc = (number1, number2) =>
            {
                return number1 + number2;
            };

            int sum = sumFunc(12, 36);

            Console.WriteLine(sum);

Use Func, Action Etc methods

Don’t do that,
The issue here is that you are creating double transactions in your first function.

You are using Ice.Context.CreateDefaultTransactionScope

Which creates a transaction then you are checking create transaction on the function which wraps the whole thing in a transaction

You need to pick one either the default Tran scope or the transaction checkbox

I assumed it was because I was not passing the erpContext from A to B. I guess I’m unfamiliar with how to do that and I’m also not sure what you mean by “You need to pick one either the default Tran scope or the transaction checkbox”. Could you elaborate?

You can’t have Ice.CreateDefaultContext and a Transaction checkbox

3 Likes

Thanks for the responses I’m new to using functions to update values in Db. I’m putting together some simple functions to understand how context is handled when calling another function. I’ll post my findings here. In the mean time I was wondering do I need these lines in order to update values in the database

or could I just call Db.Validate without it? What is the purpose of those lines? I found that I couldn’t get my function to update without them in the past.

You do not need either of those

Just Db.Save you don’t need ERP Context Either.

In the Function Setup you need to Add your Tables

You need to select the updatable checkbox for the tables you want to update

And you should not be creating your own erp context in here

Getting to a computer in a bit I’ll expand

2 Likes

Here’s the Setup
Create Function
Add the Table you want to update (as well as any other table you want to reference)
Mark the table as Updatable

Then in the function just do the update / lookup etc. Db is automatically generated for you based on the selected tables.

var abc=(from a in Db.ABCCode where a.ABCCode1=="A" select a).FirstOrDefault();
if(abc)
{
  abc.CalcQty=10;
  Db.SaveChanges();
}

If you want the Protection of a Transaction Check the needs Transaction Box on the Function Settings

You should never instantiate your ERP Context Here, or establish your own Transaction Scope that creates double transactions and can cause all sorts of fun record locks and nightmares.

4 Likes

Awesome Thanks a ton! I’ll run some test functions to make sure I understand. That helps a bunch.

1 Like

I tested your advice out by making two simple functions. Function A (“Requires Transaction”) sets an order’s OrderComment field with the customers name and time stamp. It gets the customers name from Function B.

It worked and updated the order! Thank you again!

1 Like

@josecgomez I tried looping through my test function using a bpm and got the following error
The underlying provider failed on EnlistTransaction
this led me to your provided solution https://www.epiusers.help/t/method-directive-error-why-does-the-2nd-one-not-work/60209/7.

I updated my BPM that was calling the function to loop through a list of orders instead of the DB directly and now it runs without issue.

I wanted to clarify. Should I only loop through read only Dbs as a list and still loop through the write Dbs directly? For example, my function A (Requires transaction) would look like this

// FUNCTION A
foreach(var oHead in (from h in Db.OrderHed.With(LockHint.UpdLock) where
h.Company == this.CompanyID && 
h.OrderNum == inputOrderNum
select h))
{
  // CALL FUNCTION B
  var customerName = this.ThisLib.B(oHead.CustNum);
  oHead.OrderComment = customerName + " " + DateTime.Now.ToString("MM/dd/yyyy h:mm:ss tt");
}
Db.SaveChanges();

and my function B would look like this

// FUNCTION B
var customers = (from c in Db.Customer where
  c.Company == this.CompanyID && 
  c.CustNum == inputCustNum
  select c);
foreach(var customer in customers)
{
  outputName = customer.Name;
}

So you should NEVER ← here’s that word again take this as most of the time

You shouldn’t loop through an open linq query the way you are doing it

foreach(var oHead in (from h in Db.OrderHed.With(LockHint.UpdLock) where
h.Company == this.CompanyID && 
h.OrderNum == inputOrderNum
select h))

^ this can sometimes particularly when using transactions leave an open “cursor” ← not really but close enough and cause issues. Particularly if you are using transactions and jumping functions.

I recommend you make your linq query .ToList() so that you resolve that query and pull results, then you loop through the list and make your changes. Like this

var orderList = (from h in Db.OrderHed.With(LockHint.UpdLock) where
h.Company == this.CompanyID && 
h.OrderNum == inputOrderNum
select h).ToList();
foreach(var oHead in orderList)

Also it isn’t necessary to provide a LockHint.UpdLock (ever) SQL uses optimistic locking let SQL manage itself it is better for everyone.

3 Likes

I’m all for leaving out LockHint.UpdLock.
That being said is there ever a time to use any of these?
image

Bringing back a CLASSIC piece of advice, thanks Jose!

No it would have to be pretty extreme I would say almost never @Olga (thoughts?)

2 Likes

we set UpdLock when we want to get record and then update it.
But if you just read data - it is not necessary.

Less rows you select with Update lock and faster you commit the transaction - less possibility to have locks and deadlocks problems in your database.

1 Like

With that being said, if one is to do a direct update using the Db tables, should you always use UpdLock?

No let SQL manage locks.