Any way to write to DB from a function?

Hi All,
I need to turn off the notify flag at the shiphead level once the tracking Number has been sent.

Here’s the code. I have no access to Db.Validate from the function. Do you have any ideas on what can be done? The function is set to read/write in the database.

var shipLists = (from x in this.Db.ShipHead where x.NotifyFlag == true && x.Company == "GRP"
    select x);

DataTable toSend = new DataTable("ToSend");
toSend.Columns.Add("OrderNum");
toSend.Columns.Add("TrackingNumber");
toSend.Columns.Add("Email Address");



if (shipLists != null) {
    foreach(var ship in shipLists) {
        ship.NotifyFlag = false;


        if (ship.TrackingNumber != "") {
            var shipdtl = (from y in this.Db.ShipDtl where y.PackNum == ship.PackNum select y).First();

            if (shipdtl != null) {
                using(var MyFile = new System.IO.StreamWriter(new System.IO.FileStream("c:\\temp\\TrackingInfo.txt", System.IO.FileMode.Append))) {
                    MyFile.WriteLine(DateTime.Now.ToString("yyyy/MM/dd HH:mm:ss:ff") + " ShipDtl : " + shipLists.Count().ToString());
                }
                var custNum = shipdtl.CustNum;
                var shipConNum = shipdtl.ShpConNum;
                var shipToNum = shipdtl.ShipToNum;

                using(var MyFile = new System.IO.StreamWriter(new System.IO.FileStream("c:\\temp\\TrackingInfo.txt", System.IO.FileMode.Append))) {
                    MyFile.WriteLine(DateTime.Now.ToString("yyyy/MM/dd HH:mm:ss:ff") + " ShipDtl : " + custNum.ToString() + " | " + shipConNum.ToString() + " | " + shipToNum.ToString());
                }

                var custCnt = (from cnt in this.Db.CustCnt where cnt.CustNum == custNum && cnt.ShipToNum == shipToNum && cnt.ConNum == shipConNum && cnt.Company == "GRP"
                    select cnt).FirstOrDefault();

                if (custCnt != null) {
                    string email = custCnt.EMailAddress;
                    if (email != "") {
                        toSend.Rows.Add(ship.OTSOrderNum, ship.TrackingNumber, email);
                        using(var MyFile = new System.IO.StreamWriter(new System.IO.FileStream("c:\\temp\\TrackingInfo.txt", System.IO.FileMode.Append))) {
                            MyFile.WriteLine(DateTime.Now.ToString("yyyy/MM/dd HH:mm:ss:ff") + "  Email : " + email);
                        }
                    }
                }
            }
        }

        using(var MyFile = new System.IO.StreamWriter(new System.IO.FileStream("c:\\temp\\TrackingInfo.txt", System.IO.FileMode.Append))) {
            MyFile.WriteLine(DateTime.Now.ToString("yyyy/MM/dd HH:mm:ss:ff") + " Total Email : " + toSend.Rows.Count.ToString());
        }
    }
    if (toSend.Rows.Count > 0) {
        var mailer = this.GetMailer(async :true);
        var email = new Ice.Mail.SmtpMail();

        foreach(DataRow currRow in toSend.Rows) {
            email.SetFrom("");
            email.SetTo("");
            email.SetReplyTo("");
            email.SetSubject(string.Format("Tracking Information for order : {0}", currRow["OrderNum"].ToString()));
            email.SetBody(string.Format("https://www.fedex.com/fedextrack/?trknbr={0}", currRow["TrackingNumber"].ToString()));
            email.IsBodyHtml = true;
            mailer.Send(email);
        }
    }

}
1 Like

Salut @alexbg89 et bienvenue…

Pretty sure you have to act on the database via methods in a function. Although it can be done in a BPM it seems to get riskier and more locked down with each upgrade.

I could be wrong but you’ll probably be calling ShipHead.Update(ref shipLists); or something similar.

Figuring out BO calls in a function is a killer but is probably the safest way - here’s one I did:

https://www.epiusers.help/t/my-first-efx-function-figured-out-the-data-context-problem-calling-a-bo/85656/4

1 Like

If you include the appropriate References in the Function Library you can use the Widgets to Insert into an appropriate DataSet Variable (ShipHead) with a Query using an expression in the field mapping to determine the changed value ("Use [MyQuery] to Insert into the [DataSet Variable] with the [configured mapping]). Then use the ‘Call BO Method’ widget to use the Variable Data Set with the CustShip.Update Method.
(off the top of my head… but I’ve done it this way a few times. Anymore I try to avoid writing code as much as possible do to the changes and how they are trying to keep BPM’s and Funcitons ‘upgrade safe’)

Hi @alexbg89

Is it On-prem or cloud environment?

On-Prem

I will try that. Just hoped there was something as straightforward as in BPM. Thanks for the help !

I agree with others that updating via service method calls is a better way.
But, your code updates only one field (NotifyFlag), so I think that the direct update is quite safe here.

  1. DB Access from Code option should be Read Write

  2. Table reference should be marked as updatable

  3. To save changes in the code you need to use Db.SaveChanges() method.
    image

4 Likes

Direct update of DB is always a dirty way (in BPM too). When you update standard fields (no UD) with EF, you bypass all existing business rules.

3 Likes

Thanks for your help, completely missed that Updatable checkbox.

I understand that it’s a dirty way, but sometimes you know the field you want to update doesn’t have other impacts. Most of the time I use the Methods but simple field updates are faster.

1 Like

Nice, @SAD , sorry to hijack this (I’ve recently been told I’m a puppy sticking my nose everywhere on this forum) but does the Db.SaveChanges() syntax follow the Db.Validate() idea where you can specify the var like Db.SaveChanges(row) ?

And if so, is it important? In BPMs, I had Db.Validate() that worked in 10.1.600 and 10.2.100 but broke and had to be changed to Db.Validate(varName) in 10.2.700. I’ve made a rule that we must use business methods now, but I’m still interested in how this works.

Nope, and never will. SaveChanges() is standard EF syntax. Db.Validate()/Db.Validate(row) is Epicor-specific and completely unusual for any normal EF developer things.

It is definitely not BPM-specific. BPM re-uses standard Epicor EF context. Most likely you have data conflicts in this case. It can be Data Directive on a table that you update, new or changed Epicor Data Trigger (Erp.Trigger.*.dll), changes in ICE FW.

1 Like

To be clear… EF was designed to be the Unit of Work. So, the normal approach is to make all required changes via DB context and then to call SaveChanges. In this case and under normal conditions, EF opens a connection, opens a transaction, applies all changes, and then commits the transaction and closes the connection.

Unfortunately, Epicor contexts (both IceContext and ErpContext) do not follow this approach :(.
There were the reasons for that - migration from Vantage/Epicor 9 with preserving their behaviors and inability to migrate old data triggers (and Data Directives too) to the SQL ones.

3 Likes

I found that you should absolutely never call Db.SaveChanges() in a BPM. Considering what’s happening under the hood, I would expect this to apply to EFs called from BPMs as well.

I’m not sure about the exact details because I no longer have access to an on-prem environment where I can dig into the generated code. But in a nutshell, the framework that calls BPMs creates an instance of a class called EpiTransaction or something like that. This is the thing that owns the DbContext and injects it into your BPM. After calling your BPM, it disposes the EpiTransaction which calls Db.SaveChanges(). So if you call Db.SaveChanges() in the BPM, it apparently races with the built-in save and causes database deadlock exceptions. I eliminated the deadlocks by replacing all uses of Db.SaveChanges() with Db.Validate() and later with BO method calls.

1 Like

Where did I mention BPM?

Moreover, how did you call SaveChanges in BPM? The method is overridden and marked as private (AFAIR) in Epicor context. So, the only way to call the method is by casting to ObjectContext.

1 Like

Interesting… I’m wrong. And it can be called in BPM. Straaaange

Update. Deja vu
The method is not marked as private, it just calls this.Validate()

    new public int SaveChanges()
    {
        return this.Validate();
    }

So, if Db.SaveChanges() call is replaced with direct Db.Validate() method call, then nothing can change.

However, there is yet another method - SaveChanges(SaveOptions options). And this method is implemented in a different way.

1 Like

This seems to tie in with what @SAD is saying about Db.SaveChanges() being overridden. IIRC, the problems I had with SaveChanges() causing deadlocks were in 10.2.100. In that version, changing BPMs to call Db.Validate() instead of Db.SaveChanges() caused an immediate observable change in behavior. So maybe the behavior of Validate() was changed at the same time SaveChanges() was overridden, sometime between 10.2.100 and 10.2.700.

1 Like

In 10.2.100, SaveChanges did nothing

    new public Int32 SaveChanges()
    {
        return 0;
    }

In general, it was marked as obsolete, but BPM does not show warnings…

Anyway, if you work with IceContext or ErpContext (or any CSF-specific context), you have to follow the Epicor way and use Validate()/Validate(row) methods. AFAIR our documentation never suggested using SaveChanges with these contexts.

2 Likes

Yes, the BPM migration guide says to call Validate. But we had an old BPM with an epicor.com email address in the attribution line that called SaveChanges, and my predecessor copied it everywhere and then wondered why it was deadlocking.

Incidentally, the code you shared does not override SaveChanges. Hiding works a bit differently. IIRC, which version of the method gets called depends on the declared type of the variable.

1 Like

It is very strange, even pre-release of E10.0 assumed Validate method.

The original method is accessible only if you downcast context to ObjectContext, as I wrote a few posts earlier. But it is an even more strange idea to do that.

Also, the method is overridden anyway. And yes, behavior depends on the method modifier (virtual vs. standard). Don’t mess up keywords and meaning.

1 Like

I find this very hard to believe. Our BPMs were frequently throwing exceptions with messages that described deadlock, and those exceptions stopped immediately when I removed calls to SaveChanges().

A hidden method is not overridden. A hidden virtual method is not overridden. Which version of the method gets called depends on the declared type of Db. The runtime type is ErpContext, but what is the declared type? And is it consistent in every method of every BO?