Best Practices for Making REST Calls from Classic Forms?

So my big, overarching project for 2023 is to move legacy business logic entirely out of classic forms and into (as much as practical) functions. Problem with that is that, of course, forms can’t directly access Functions. I have to go through Rest.

There’s many examples of that lying around here, and many thanks to those that have posted them. A lot of them, though, declare the ApiKey right in the form, which is iffy for a few reasons. I figured there had to be a better way, and came up with this after a half-day of messing around. It uses the “BasicBAQReport” Function I posted here.

Thoughts?

string apiKey, svcAcctName, svcAcctPw;

using (var dqa = new DynamicQueryAdapter(oTrans))
{
	dqa.BOConnect();
	//I originally tried calling the ApiKey BO, but normal users ran into security issues.
	//So I dumped it into a BAQ that did the same thing. 
	dqa.ExecuteByID("API_GetInternalApiKey");
	apiKey = Epicor.Security.Cryptography.Encryptor.DecryptToString(
						(string)dqa.QueryResults.Tables["Results"].Rows[0]["APIKey_EncryptedKey"]);
	//We use SSO. Unfortunately, that's completely broken on the E10 version of the EpiRestClient
	//So I built an internal-only service account.
	//Yeah, this literally just returns the ID of the account and a plaintext password.
	//It's better than baking it into the form, but I still feel gross.			
	dqa.ExecuteByID("API_GetInternalApiServiceAcct");
	svcAcctName = (string)dqa.QueryResults.Tables["Results"].Rows[0]["Calculated_UserID"];
	svcAcctPw = (string)dqa.QueryResults.Tables["Results"].Rows[0]["Calculated_Password"];
}

IRestClient client = new RestClientBuilder()
	.SetDefaultApiKey(apiKey)
	.UseSession((Ice.Core.Session)oTrans.CoreSession)
	.UseBasicAuthentication(svcAcctName, svcAcctPw)
	.Build();

RestContent content = new RestContent(new
{
	ReportName = "BlahBlah",
	BaqName = "BlahBlahBlah",
	ReportKey = (string)UDxx_Row.CurrentDataRow["Key1"],
	RenderFormat = "PDF",
	Action = "SSRSPREVIEW"
});
	
var response = client.Function.Post("Reporting", "BasicBaqReport", content, true);
2 Likes

Your version is the one where you can’t build it from the session and restbuilder is wonky correct?

I’m still on 10.2 (hence the E10 category for this post), and yes, the client library is broken. Everything works fine from BPM’s. It’s just the damn classic forms.

I used the built in rest client on 10.2.600 for a long time worked great. Did they break it in 700?

1 Like

Are you using SSO?

No, but that shouldn’t matter for any reason that I can think of. If you setup a non SSO appserver and try it with the same user does it work? @Olga is there any reason SSO would mess with the REST Client built into classic that you can think of?

It would seem that it is partially broken. I even have an open dev item with Epicor about how it isn’t working as intended, not that I expect them to patch something on 10.2.

Like, the whole thing with what I wrote is that it uses a non-SSO service account to authenticate the rest call.

I could probably whip up a function calling ubaq custom action so you wouldn’t need to make the rest call yourself at all if you are interested.

2 Likes

Im pretty sure its not the best practice, but the way I get around the lack of function calls in classic forms is to update a string field from a table I dont use with all the function input parameters as a ~-separated string. Then I can put the function call in a standard data-directive BPM. That way I can get the function parameters by just using string.split(‘~’).

@klincecum I can’t say I’m familiar with UBAQ Custom Actions and searching isn’t turning up much in the way of details. Do you have a link to guide?

@kve I don’t know if that’s brilliant or awful. Both? Maybe instead of writing to a UD table, I just, say, override the base method of UDXX.GetByID() or Update()? Still feels a little dirty, but the more I think about it, the less I hate about the concept. It’s relatively few lines of code, and post-Kinetic clean-up is simply deleting the BPM’s.

I feel a little proud that this isnt the first time I heard that :thinking:

@jtownsend I am also working this year and probably next on moving to functions, but moved up to 11.1.200 and am planning on 11.2.300.x this year, so I get the latest new stuff, but stay classic.

Have you done a trial of move from 700 up to see what would be broken? For me it was very few custom screens that got broken, but maybe I was just lucky.

We’ll probably spin up a 2023.1 server later this summer, after it’s been through 10ish patches. I’m not really concerned about about things breaking. My customizations have sailed through a couple updates already without issue. I’m revisiting old customizations as part of business-side process improvement projects (needs have changed, etc.) and I don’t want to re-do them again when we move to the browser.

So I’m moving them out of the client, mostly to BPM, but in some cases I’ve got a button on the form that needs to do something (which is the example above).

1 Like

I know consultants who do it all the time, because it used to be an easy way to get stuff done.
I don’t hate it, but I don’t like it.

I have also used it :rofl:

Don’t mess with the base though, you’ll end up breaking something later when you need it.
Put appropriate trigger conditions so it doesn’t mess with normal functionality, and it’s a valid
way to get things done.

Just remember to document!

I know consultants who do it all the time, because it used to be an easy way to get stuff done.
I don’t hate it, but I don’t like it. I have also used it :rofl:

Having dabbled with it, I’m in the same boat. Clever, alluringly flexible, but by gods do I feel dirty doing it.

Don’t mess with the base though, you’ll end up breaking something later when you need it.

I mean, the alternative here is, as originally proposed, is a data directive. I’m just moving one step ahead of the line and overriding the Update call. It seems to be like that saves a number of steps. I’m not really sure what I could be breaking, other than eliminating the ability to write to UDT using the BO, but that’s intentional.

using (var bootlegFunctionCall = new UD10Adapter(oTrans))
{
	bootlegFunctionCall.BOConnect();
	//Pass key1 = Library key2 = Function
	bootlegFunctionCall.GetaNewUD10();
	bootlegFunctionCall.UD10Data.UD10[0].Key1 = "Reporting";
	bootlegFunctionCall.UD10Data.UD10[0].Key2 = "BasicBaqReport";
	//Use UDFields to pass function inputs
	bootlegFunctionCall.UD10Data.UD10[0].Character01 = "BlahBlah";
	bootlegFunctionCall.UD10Data.UD10[0].Character02 = "BlahBlahBlah";
	bootlegFunctionCall.UD10Data.UD10[0].Character03 = (string)UDxx_Row.CurrentDataRow["Key1"];
	bootlegFunctionCall.UD10Data.UD10[0].Character04 = "PDF";
	bootlegFunctionCall.UD10Data.UD10[0].Character05 = "SSRSPREVIEW";
	//Base overide executes Function with assigned parameters
	bootlegFunctionCall.Update();
	//You can even return a fair number of outputs.
	//This specific example is mocked up, but I confirmed that it worked in another function.
	MessageBox.Show(bootlegFunctionCall.UD10Data.UD10[0].Character06);
}
1 Like

All good then :beers:

UBAQ Custom Actions → The magic before functions existed.

No but I could probably make one, or find some relevant posts.

So…I ran into some issues with chaining multiple calls with the UD method. I’m leaning back towards my original idea since I can just remove the hacks from the RestClientBuilder once I’m on a fixed version in a few months.

You can use a UBaq to call a function… So in your code you can use the dynamic query and pass in the function library and function name, and parameters in a string that can be decoded.

Just another suggestion.

Not going to solve your whole “Get that stuff out of my UI code” issue but will go a long way.

1 Like

I wrote that thing and posted it here.

https://www.epiusers.help/t/calling-rest-yourself-from-classic-client-is-dead-long-live-functionrunner/104713

It’s a BAQ that runs functions for you and passes back your objects in the dataset.

You basically just add a small helper function to the classic client and call it like:

DataSet sampleDS = FunctionRunner(UD01Form, "FunctionRunnerBAQ", "FunctionRunnerTest", "TestFunc2", a, b, c);

Guid myEmptyGuid = JsonConvert.DeserializeObject<Guid>( sampleDS.Tables["Results"].Rows[3]["Calculated_ObjectJson"].ToString() );