Migration Guide
This article contains information and guidelines to migrate code written for the DLL4 interface from previous versions (< v5.0.0) to the latest version using multi threaded interface processes.
This migration is optional, but strongly recommended, backwards compatibility can be maintained by using Isolation : Dedicated
in the profile. Using this option will increase the memory usage by the process (due to the assemblies being loaded multiple times). Consider increasing the maximum memory setting to prevent frequent recycling of the DLL process. This option will be phased out in future versions.
Warning
Using Isolation : Dedicated
is not recommended for production environments as it can have a severe performance impact.
If DLL4 methods are inexplicably slower than expected, verify this setting on the profile.
Overview
Below is an overview of the changes to the DLL4 interface and the actions to take when migrating code.
- DLL4 method code is executed on the same process.
- The static
Interface
properties are no longer available. - Code should implement
IRequestProcessor
instead of defining a static function. - Interface scripts must now define the name of the class to instantiate and call instead of a function name.
- Change the profile to
Isolation: Default
once all code is migrated.
Example Migration
The sample below shows the code for 2 DLL methods: CreateEquipment and UpdateEquipment. A different class EquipmentServices
is used for reuseable functions. The tabs show the steps taken to migrate this code.
public static class EntryMethods
{
private static DateTime s_startedOn;
public static void CreateEquipment()
{
Interface.Log.Write("Creating equipment...", Ometa.Logging.Log.Category.Information);
s_startedOn = DateTime.Now;
EquipmentServices.CurrentEquipmentCategory = Interface.Method.GetFieldValueForExecution<string>("Type");
if (EquipmentServices.CurrentEquipmentCategory == "1")
return;
using (var bcClient = Interface.ConnectedClient.CreateNewClient())
{
var equipmentNumber = EquipmentServices.CreateEquipment(bcClient);
Interface.Log.PropertiesHelper.Add("Function Started", s_startedOn);
Interface.Log.Write("Equipment created.", Ometa.Logging.Log.Category.Information);
if (string.IsNullOrWhiteSpace(equipmentNumber))
throw new Exception("Equipment number was not returned from the equipment creation.");
Interface.ResultWriter.WriteBeginRecord();
Interface.ResultWriter.WriteFieldValue("Equipment", equipmentNumber);
Interface.ResultWriter.WriteEndRecord();
}
}
public static void UpdateEquipment()
{
Interface.Log.Write("Updating equipment...", Ometa.Logging.Log.Category.Information);
s_startedOn = DateTime.Now;
EquipmentServices.CurrentEquipmentCategory = Interface.Method.GetFieldValueForExecution<string>("Type");
var equipment = Interface.Method.GetFieldValueForExecution<string>("Equipment");
if (EquipmentServices.CurrentEquipmentCategory == "1")
return;
using (var bcClient = Interface.ConnectedClient.CreateNewClient())
{
var equipmentNumber = EquipmentServices.UpdateEquipment(bcClient, equipment);
Interface.Log.PropertiesHelper.Add("Function Started", s_startedOn);
Interface.Log.Write("Equipment updated.", Ometa.Logging.Log.Category.Information);
Interface.ResultWriter.WriteBeginRecord();
Interface.ResultWriter.WriteFieldValue("Equipment", equipmentNumber);
Interface.ResultWriter.WriteEndRecord();
}
}
}
public static class EquipmentServices
{
internal static string CurrentEquipmentCategory;
internal static string CreateEquipment(BCClient bcClient)
{
var equipmentNumber = string.Empty;
var createEquipmentMethod = bcClient.GetObject(Interface.Method.ParentObject.Name).GetMethod("Create Equipment");
if (createEquipmentMethod == null)
throw new Exception("The method 'Create Equipment' could not be found.");
createEquipmentMethod.SetFieldValueForExecution("EQTYP", CurrentEquipmentCategory);
foreach (var aDataExtension in createEquipmentMethod.DataExtensions)
createEquipmentMethod.SetDataExtensionProfileForExecution(aDataExtension.Id, "SAP PRD");
using (var resultSet = createEquipmentMethod.Execute("SAP Equipment", 0))
{
if (!string.IsNullOrWhiteSpace(resultSet.Error))
throw new Exception(resultSet.Error);
if (resultSet.ReadNextRecord())
equipmentNumber = resultSet.GetFieldValue<string>("Equipment");
}
return equipmentNumber;
}
internal static string UpdateEquipment(BCClient bcClient, string equipment)
{
var equipmentNumber = string.Empty;
var updateEquipment = bcClient.GetObject(Interface.Method.ParentObject.Name).GetMethod("Update Equipment");
if (updateEquipment == null)
throw new Exception("The method 'Update Equipment' could not be found.");
updateEquipment.SetFieldValueForExecution("Equipment", equipment);
updateEquipment.SetFieldValueForExecution("EQTYP", CurrentEquipmentCategory);
using (var resultSet = updateEquipment.Execute("OData v2 SAP", 0))
{
if (!string.IsNullOrWhiteSpace(resultSet.Error))
throw new Exception(resultSet.Error);
if (resultSet.ReadNextRecord())
equipmentNumber = resultSet.GetFieldValue<string>("Equipment");
}
return equipmentNumber;
}
}
Once all code in the DLL is migrated and deployed to the BCS_DLL 4
folder, don't forget to update the Isolation
on the DLL 4
profiles to Isolation : Default
. It's recommended to restart the BCSL service once completed. This will interrupt and cancel any running method.
Interface Scripts
The interface scripts must now define the name of the class to instantiate and call instead of a function name.
Interface script:
Equipment Sample.dll
CreateEquipment
- First line is the dll file name, including extension.
- Second line is the static entry method name, this could be left empty in which case the static
ProcessRequest
method was used if available.
namespace Equipment_Sample
{
public static class EntryMethods
{
public static void CreateEquipment()
{
Interface.Log.Write("Creating equipment...", Ometa.Logging.Log.Category.Information);
using (var bcClient = Interface.ConnectedClient.CreateNewClient())
{
var equipmentNumber = EquipmentServices.CreateEquipment(bcClient);
Interface.Log.Write("Equipment created.", Ometa.Logging.Log.Category.Information);
if (string.IsNullOrWhiteSpace(equipmentNumber))
throw new Exception("Equipment number was not returned from the equipment creation.");
Interface.ResultWriter.WriteBeginRecord();
Interface.ResultWriter.WriteFieldValue("Equipment", equipmentNumber);
Interface.ResultWriter.WriteEndRecord();
}
}
}
}
BuildingBlocks
For code that uses the Ometa BuildingBlocks through DLL4 method code; the namespace of the Ometa BuildingBlocks has moved from Ometa.Utilities.Client
to Ometa.Framework.BuildingBlocks
to enable multi threading on the interface process.
Note
Backwards compatibility can be maintained by using Isolation : Dedicated
in the profile. A runtime error will be thrown otherwise.
The only major change is that the BuildingBlocks no longer use static classes and functions. It is recommended to migrate the custom code following the previous chapter and use the Interface
instance member.
public static class Sample
{
public static void ReceiveMessage()
{
Ometa.Utilities.Client.SharePoint.Lists.GetList(web, "My List");
}
}
Coding with Data Extensions
Due to the fact that data extension have been moved to the core layers, existing code which targets data extensions will possibly break.
The following symbols have been removed:
- Method.DataExtensions
- Method.InputDataExtensions
- Method.OutputDataExtensions
- Method.AllDataExtensions
- Method.AddDataExtension(bool input)
- Method.RemoveDataExtension(DataExtension extension)
- Method.GetDataExtension(bool input, string dataExtensionObject, string dataExtensionMethod)
- Class BC.Extensions.DataExtension (CommLib)
- Class BC.Extensions.FieldRelation (CommLib)
var myObject = bcClient.GetObject("MyObject");
var myMethod = myObject.GetMethod("MyMethod");
// Usage of any of the following lines will break your code (compilation error):
var dataExtensions = myMethod.DataExtensions;
var inputDataExtensions = myMethod.InputDataExtensions;
var outputDataExtensions = myMethod.OutputDataExtensions;
var allDataExtensions = myMethod.AllDataExtensions;
var newDataExtension = myMethod.AddDataExtension(true);
var myDataExtension = myMethod.GetDataExtension(true, "MyExtensionObject", "MyExtensionMethod");
myMethod.RemoveDataExtension(myDataExtension);
DataExtension myDataExtensionInAVariable = null;
FieldRelation myFieldRelationInAVariable = null;
In most cases, the only code that should be fixed is the one which is responsible for setting data extension profiles on a method which is going to be executed.
The following example shows how to change your existing code.
var myObject = bcClient.GetObject("MyObject");
var myMethod = myObject.GetMethod("MyMethod");
// Your original code could look like this (and will now break compilation):
foreach (var aDataExtension in myMethod.DataExtensions)
{
myMethod.SetDataExtensionProfileForExecution(aDataExtension.Id, "myProfile");
}
// The above code can be replaced by:
foreach (var aDataExtensionSetting in myMethod.DataExtensionSettings)
{
myMethod.SetDataExtensionProfileForExecution(aDataExtensionSetting.Identifier, "myProfile");
}
The new DataExtensionSetting class is contained in our new Ometa.Framework.Client API (which will eventually replace the whole Ometa.CommLib and Ometa.Dcs libraries). The new property Method.DataExtensionSettings uses this class so existing code can be migrated with as little effort as possible.
If you, however, have code in place which also manages data extensions through the old Ometa.CommLib data extension class, let us know so we can help migrate your codebase.
Cancellation Support
Besides the fact that you now have a Task entry method called ProcessRequestAsync
, you'll also notice that this function comes with a parameter named cancellationToken
.
When a request is cancelled, this token will be set and you are responsible for taking this into account in your code. There are a variety of reasons why a request can be cancelled:
- a timeout is reached
- the user pressed F5 in the browser while the request was still running
- the user navigated away from the page where the request started
- the user closed its browser while the request was still running
- the request was cancelled in code
- the user cancelled the 'Test Method' window
Below is a short example how you can take cancellation into account.
namespace Orders_Sample
{
public static class EntryMethods
{
public static void GetOrders()
{
Interface.Log.Write("Getting orders...", Ometa.Logging.Log.Category.Information);
do
{
var orderName = Interface.Method.GetFieldValueFromExecution<string>("Order");
var orderNumber = OrderService.GetOrderNumberFromErp(orderName);
Interface.Log.Write("Order number fetched.", Ometa.Logging.Log.Category.Information);
Interface.ResultWriter.WriteBeginRecord();
Interface.ResultWriter.WriteFieldValue("Order Name", orderName);
Interface.ResultWriter.WriteFieldValue("Order Number", orderNumber);
Interface.ResultWriter.WriteEndRecord();
} while (Interface.Method.MoveToNextContextRecord());
}
}
}
There will always be scenarios where you cannot cancel the operation anymore or in a certain acceptable timeframe (e.g.: a long running update action to ERP), however try to support cancellation in the best possible way. A good practise is:
- identify your loops (foreach, for, while, ...) and add cancellation support there if these loops contain intensive work
- identify external code calls and investigate if they support cancellation. If so, forward the cancellation token to those calls
To learn more about cancellations, read this article.
Important
Not taking cancellation into account is considered bad coding practise and can lead to excessive and unnecessary use of system resources due to the fact that your code keeps running in the background.
If your code doesn't respond to a cancellation within a certain timeframe (x seconds), an error will also be written to BAM.