Company Change Approaches on the Server


(Bart Elia) #1

I was abused on Twitter by @josecgomez this weekend on this topic so posting here. :wink:

The question is how to iterate through different companies in a server process. The Client way is to use SessionMod.SetCompany(). Stand up a new Service, Call SetCompany, life is good.

When on the server side though, you can change the state of your current instance and get confused easily. To do this correctly means either making REST Calls with the new Company as a header or standing up a ServiceRenderer in a suppressed transaction which can lead to interesting side effects - that one is not worth the fragility. So we came up with this functionality that works well. I honestly have not tried this in a BPM but don’t see why this would not work for these. We use this approach a LOT on the server - especially in administration features or processes that span companies.

The first thing to understand is that a Server Session is not a single instance but rather a .NET Stack of Session Instances. The ‘CallContext.Current.Session’ variable is just a pointer to the top of the Stack. In most cases, there is just a single Session instance in the CallContext stack. But when you need to iterate over Companies to process something the Session Stack get pushed and popped around. That’s where ‘TemporarySessions’ comes in.

First thing -
Security
When standing up a new Temporary Session, all the standard checks are performed on the session variable for permission. That means the ‘Parent Session’ must be able to change to the indicated company just as on the client or runtime security exceptions will occur.

Lifetime
The lifetime of a Temporary Sessions is only the span of one server method call (One CallContext to be more exact but one server method call in general use). If you start a temporary session with alternative session variables, those settings will be lost on the next server call. You need to change the Session on the client and make multiple method calls as has always been done to preserve new Session settings. NOTE: Only one TemporarySession at a time can be in effect. Trying to create a nested TemporarySession will throw a runtime exception.

Visibility
When starting a Temporary Session, it will appear as the session. The Session property in a ContextBound object or CallContext.Curent.Session will return the new temporary session. This means you can start the temporary session in a process and be confident that the temporary session variables will be used in any code downstream in some lib you were not even aware of being executed. The only way you can determine if the Session is the original parent or a temporary session is by referring to the ‘Session.IsTemporary’ property.

Creating
There are a couple of ways to create the Temporary Session based on your needs.

Fluent API
The simplest is to take advantage of the TemporarySessionCreator. This is a factory object that uses a fluent API so a series of ‘Set’ methods can be chained together:
CallContext.TemporarySessionCreator
.SetCompanyID(“Epic03”)
.Set(“SomeSessionVariable”, “SomeValue”)
.Create();
Note the mandatory ‘Create’ method that finishes the statement and starts the Temporary Session.

Disposable - PREFERRED!!!
The Temporary Session is disposable so can be used in a ‘using’:
using (CallContext.TemporarySessionCreator.SetCompanyID(companyID).Create()) {}

This allows for easy iteration of lists with proper disposal such as:

foreach (var companyID in companyList)
{
    using (CallContext.TemporarySessionCreator.SetCompanyID(companyID).Create())
    {
        //Your Code Here
        Debug.WriteLine("TemporarySession Current Company=" + Session.CompanyID);
    }
}

Usage
Pretty much anything inside of the ‘Create’ block can be executed and it looks like the Session is something other than what was used on the client - because it is. The ‘parent’ Session’ was pushed onto the stack and the new temp one is pointed at by everything. You need to be cautious if you try to do too much in the alternate session or you might loop around and get a second nesting - something forbidden due to the complexity of keep the recursion straight.

Support
To say this is advanced stuff goes without saying. Calling Support with this outside of SDK scenarios - you probably won’t find much help. It is pretty powerful for some scenarios where you don’t want to recurse from the client though so have it in the toolbox with other tools that you can injury your foot if mishandled.


(Jose C Gomez) #2

Thank you @Bart_Elia!! Wonderful information, I’ve moved to Expert’s Corner for safe keeping


(Jose C Gomez) #3

PS: I’ve added several drinks to my I.O.U to @Bart_Elia to be paid in full at Insights :wink:


(Haso Keric) #4

Bart awesome!


(Haso Keric) #5

Let me know if you try this in a BPM =) post a boilerplate code


How to switch plants in a BPM
(Haso Keric) #6

Is there anyway to temporary use a different UserID? We have a case where we would like to go to a Parent Company and toggle a field, but we don’t want the Users to have access to that Parent Company. I plan to use a “Service Account User” for the action.

Could I do something like:
CallContext.TemporarySessionCreator.SetCompanyID(CompanyID).SetPlantID(PlantID).SetUserID(UserID).SetEmployeeID(EmployeeID).Create()

2nd Question: What is the difference between creating a TemporarySession vs using Session.SetCompany(“ABC”)


(Mark Wonsil) #7

Haso,

I wonder if this wouldn’t be a good use of a REST call? You would use just a little time to log in via your Service Account User, make the update, confirm it, and exit quickly. It would be out-of-band of the current security context and keep things nice and clean.

Mark W.


(Haso Keric) #8

Definitely it would be, just for the time being looking for a solution until we enable REST and test it and so on. It involves Infrastructure Team to allow it and more.


(Bart Elia) #9

I can try to find out notes on why userid change was a no no. Or at least a HUGE impact on perf since you basically have to stand up a new Operation (e.g. Server Call). But in general, you need to make another server call so didn’t bother in the few scenarios we had - just use the alternate credentials from the client.

A Temp session lasts the life of the server call or shorter - ideally the span on what inside a for loop as you iterate different companies, plants, etc.
foreach(var company in companyList)
{
using (CallContext.TemporarySessionCreator.SetCompanyID(company).Create())
{
DoSomething(company);
}
}

Calling Session.SetCompany changes the default Company forever - or until the users clicks on a new Company, etc. If you think about ‘sagas’ in the transaction world you’ll get a hint of what I mean. Session is the breadcrumb trail of all those little context settings about current company, plant, site, workstation, currency, language, etc etc.
Temp does a ‘temporary’ hijacking of that saga to make a quick change in context of something else. Think Admin cleanup type functions where you are iterating over all the companies in a group.


(Haso Keric) #10

Works without a problem for me, thus far I haven’t discovered any issues.

BPM Sample Code (Toggling between 3 Child Companies in foreach loop, using it on 2 Methods, fast):

using (CallContext.Current.TemporarySessionCreator.SetCompanyID(childCompanyID).Create())
{
	using (var PartService = Ice.Assemblies.ServiceRenderer.GetService<Erp.Contracts.PartSvcContract>(Db))
	{
		PartTableset PartDataSet = new PartTableset();
		PartDataSet = PartService.GetByID(ipPartNum);
		PartService.GetNewPartRev(ref PartDataSet, ipPartNum, "");
		var ttPartRevRow = PartDataSet.PartRev.Where(r => r.Added()).FirstOrDefault();
		ttPartRevRow["PartNum"] = ParentPartRev.PartNum;
		ttPartRevRow["RevisionNum"] = ParentPartRev.RevisionNum;
		ttPartRevRow["RevDescription"] = ParentPartRev.RevDescription;
		ttPartRevRow["RevShortDesc"] = ParentPartRev.RevShortDesc;
		ttPartRevRow["EffectiveDate"] = ParentPartRev.EffectiveDate;
		ttPartRevRow["smDrawRev_c"] = ParentPartRev.smDrawRev_c;
		ttPartRevRow["smECOGroupId_c"] = ParentPartRev.smECOGroupID_c;
		ttPartRevRow["DrawNum"] = ParentPartRev.DrawNum;
		ttPartRevRow["Plant"] = DefaultSite;
		PartService.Update(ref PartDataSet);
	}
}

Several 100 lines of code later, another one.

using (CallContext.Current.TemporarySessionCreator.SetCompanyID(childCompanyID).Create())
{
	using (var EngWkBenchService = Ice.Assemblies.ServiceRenderer.GetService<Erp.Contracts.EngWorkBenchSvcContract>(Db))
	{
		EngWorkBenchTableset EngWorkBenchDataSet = new EngWorkBenchTableset();
		EngWkBenchService.GetNewECOGroup(ref EngWorkBenchDataSet);
		var ttECOGroupRow = EngWorkBenchDataSet.ECOGroup.Where(r => r.Added()).FirstOrDefault();
		ttECOGroupRow["GroupID"] = ECOGroupInParentCompany.GroupID;
		ttECOGroupRow["Description"] = ECOGroupInParentCompany.Description;
		ttECOGroupRow["EffectiveDate"] =ECOGroupInParentCompany.EffectiveDate;
		ttECOGroupRow["CreatedBy"] = ECOGroupInParentCompany.CreatedBy;
		ttECOGroupRow["smECOGroupID_c"] = ECOGroupInParentCompany.smECOGroupID_c;
		ttECOGroupRow["TaskSetID"] = ECOGroupInParentCompany.TaskSetID;
		ttECOGroupRow["WFGroupID"] = ECOGroupInParentCompany.WFGroupID;
		EngWkBenchService.Update(ref EngWorkBenchDataSet);
	}
}

(Haso Keric) #11

UPDATE: We have been using this now in Production for about 1 month. It is a Sub-Process to a bigger more complex Service Connect process. We processed an XML that took 61 hours to complete (BOMs, Parts, ECOs) the code was hammered for hours and hours. It worked flawless, fast and without any exceptions.


(Toni D) #12

Is it possible to use this code in form customization? So, how to get data from another company if we are using adapter from client customization?


(Jose C Gomez) #13

No, this code cannot be used in a customization


(Bart Elia) #14

No, this is for server side iterating sessions.

On the client side you just set a variable on the Session Object and it does the magic for you.


(Toni D) #15

Thanks @josecgomez and @Bart_Elia for your answers.

@Bart_Elia could you elaborate more in this “set a variable on the Session Object” in client side?


(Chris Conn) #16

Damn, it’s that easy?I assumed that variable was read only this whole time.


(Bart Elia) #17

LOL nope, client side Core.Session is magically (And not thread safe cough)


(Haso Keric) #18

@Bart_Elia Quick Question:

Let’s say I have a Cross-Company BAQ and the User does not have access to ABC Company. Is there a safe way to Impersonate the Adapter to execute as a different user.

This here works, a little slow - just poking around; but not sure what the consequences are and if there is a safer / recommended way to just execute this BAQ as a diff user (Service Account).

There is also a Session Clone Method so I could Clone this.oTrans.Session and pass in a cloned/modified one to the Adapter so the original this.oTrans is untouched, and use MyClonedSession.SetUser(“ServiceAccount”, “pw”);

The Dynamic BO also has some code that does:

queryRequest.Session["ImpersonatedUserName"] = this.Session.UserID; // Perhaps one can use this?

(Jose C Gomez) #19

What about doing an “Updatable” BAQ and in the GetList() post-processing go fetch the stuff you need using the DB. object. This doesn’t require a new session.


(Haso Keric) #20

Pain is that its a complex BAQ that has PIVOT, Joins, Sub-Queries etc… and it Crosses Companies for certain Data - its a mixture, but if the User doesn’t have access to ABC Company, the result is Empty.

Only other thing I can think of is use the DynamicQuery BO Instead and create a New Session in a using block.