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);
}
}
}
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:
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’)
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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?