DLL Interface
A DLL file, or dynamic-link library file, is a library that contains code and data and that can be used by more than one program at the same time. The DLL4 interface is a custom wrapper that can be used to access such a DLL file, and as such can be used to retrieve data from any system. The interface allows you to set a DLL file as the interface script. A DLL file can for example be a C# Class Library.
Knowledge of the Ometa Business Connector and how to set it up is required before being able to understand and use this document.
Important
The DLL4 interface only supports DLLs written using the .NET Framework 4.8. It does NOT support .NET Core or .NET 5 and higher.
Articles
Create a Connection
Whenever you want to work with objects from the Ometa Framework, you need to create a connection.
It is important that the BCClient
is disposed after use (hence the using
statement).
/// <inheritdoc />
public Task ProcessRequestAsync(CancellationToken cancellationToken = default)
{
using (var bcClient = Interface.ConnectedClient.CreateNewClient())
{
// Do something with the bcClient.
// Try to keep it alive as short as possible.
// Within this using statement, a slot is taken from the BCM queue.
}
}
Important
NEVER create extra BCClient connections within another created BCClient connection. This can cause a deadlock in your DLL. In BAM a warning will be written if the Ometa framework detects connections within connections.
Use Execution Context
When your DLL is invoked via the Ometa Framework, a lot of execution context is send along that can be used in your DLL.
/// <inheritdoc />
public Task ProcessRequestAsync(CancellationToken cancellationToken = default)
{
/*
Gets the value of the field that is set for execution purposes in the current context record.
If the field has a conversion script, that script executed. If the conversion script fails, the raw value is returned.
Returns the default value of the field if no context value was found.
Returns the default of the requested type if neither the context value, neither the field was found.
Tries to convert the value to the requested type. Throws an exception if the value could not be converted to the given type.
Be advised that this function can and will cause data changes if things like precisions (ex: double to integer) cannot contain the current value.
*/
var textFieldValue = Interface.Method.GetFieldValueForExecution<string>("Text Field");
var intFieldValue = Interface.Method.GetFieldValueForExecution<int>("Integer Field");
var requestIpv4 = Interface.Method.GetFieldValueForExecution<string>("request.ipAddressV4");
var requestUserAgent = Interface.Method.GetFieldValueForExecution<string>("request.userAgent");
var userEmail = Interface.Method.GetFieldValueForExecution<string>("identity.user.email");
// Requesting the processed value of a field in the connection profile. This will throw an exception if the field doesn't exist.
var profileFieldValue = Interface.GetProfileVariable("Profile Field");
// Test if a profile field exists and can be converted to the specified type. If not, give it a default value.
if (!Interface.TryGetProfileVariable<int>("Profile Field", out var convertedProfileFieldValue))
convertedProfileFieldValue = 0;
}
Note
Read more about execution context here.
Execute Method
Use the following example to execute a method.
It is important that the ResultSet
is disposed after use (hence the using
statement).
/// <inheritdoc />
public Task ProcessRequestAsync(CancellationToken cancellationToken = default)
{
// Make sure you have a connected bcClient.
var myObject = bcClient.GetObject("My Object");
var myMethod = myObject.GetMethod("My Method");
// Set one or more execution values.
myMethod.SetFieldValueForExecution("My Integer Input Field", 123);
myMethod.SetFieldValueForExecution("My Text Input Field", "Text");
using (var resultSet = myMethod.Execute("My Profile", 0))
{
if (!string.IsNullOrWhiteSpace(resultSet.Error))
{
// Do something with the error.
}
else
{
while (resultSet.ReadNextRecord())
{
// Loop through the result set.
resultSet.GetFieldValue<string>("My Output Field");
}
}
}
}
Logging to BAM
Logging the flow of your DLL is crucial. In case of any issues, it is important that you can follow the flow of your code via BAM to hopefully discover where things went sideways.
By default, informational and higher levels are logged in the Ometa Framework. Consult the BAM configuration article to learn more.
/// <inheritdoc />
public Task ProcessRequestAsync(CancellationToken cancellationToken = default)
{
try
{
Interface.Log.Write("This is verbose logging");
Interface.Log.Write("This is informational logging", Log.Category.Information);
// Add properties to your next log line.
Interface.Log.PropertiesHelper.Add("Text Property", "Some value");
Interface.Log.PropertiesHelper.Add("Integer Property", 5);
Interface.Log.Write("This is informational logging with extra properties", Log.Category.Information);
// Code in your DLL.
} catch (Exception error) when (!(error is OperationCanceledException))
{
// The following line will extract the exception and log all its properties along with your custom error message unr the Error level.
Interface.Log.WriteException("Custom error message", error);
}
}
Warning
Do not exaggerate with the informational log lines to prevent flooding the BAM database with unimportant messages. The detailed log to follow your code flow is best kept on the verbose level.
Write Records
It is also possible to use your DLL to create a result set back to the Ometa Framework.
/// <inheritdoc />
public Task ProcessRequestAsync(CancellationToken cancellationToken = default)
{
for (var i = 1; i <= 5; i++)
{
Interface.ResultWriter.WriteBeginRecord();
Interface.ResultWriter.WriteFieldValue("Record Id", i);
Interface.ResultWriter.WriteFieldValue("Text Field", "Some text");
Interface.ResultWriter.WriteFieldValue("Integer Field", 5);
Interface.ResultWriter.WriteEndRecord();
}
}
Log and Throw Exception
/// <inheritdoc />
public Task ProcessRequestAsync(CancellationToken cancellationToken = default)
{
try
{
// Code in your DLL that causes an exception.
} catch (Exception error) when (!(error is OperationCanceledException))
{
// The following line will extract the exception and log all its properties along with your custom error message unr the Error level.
Interface.Log.WriteException("Custom error message", error);
// The following line will rethrow the error causing the method to fail and return the error as a result.
throw;
}
return Task.CompletedTask;
}
Use Cancellation Token
A cancellation token is provided by the Ometa Framework to the ProcessRequestAsync function of your DLL. Use this cancellation token to interrupt the execution when the token is canceled.
static HttpClient s_httpClient = new HttpClient();
/// <inheritdoc />
public async Task ProcessRequestAsync(CancellationToken cancellationToken = default)
{
// Check the cancellation token before and after time consuming code.
cancellationToken.ThrowIfCancellationRequested();
// Some time consuming code.
cancellationToken.ThrowIfCancellationRequested();
// Forward the cancellation token to functions supporting it.
await s_httpClient.GetAsync("https://www.ometa.net", cancellationToken);
}
Create SharePoint Client Context From Profile
/// <inheritdoc />
public async Task ProcessRequestAsync(CancellationToken cancellationToken = default)
{
// To fully support OBO flows, you first need to create a SharePointAuthentication instance from the request.
var spAuthentication = SharePointAuthentication.FromInterfaceRequest(Interface);
using (var clientContext = await spAuthentication.CreateAuthenticatedClientContextAsync("UrlOfSharePointSite"))
{
}
}
Context Records
Methods can be executed with context multiple times for example in synchronisations or list item functions. Handling these context records can be done either by the custom dll implementation, or by the DLL4 interface itself.
If the custom DLL hasn't moved context records after finishing, while the method still has more context records, the DLL 4 interface will invoke the custom DLL again with the next context record.
Processing Context Records In Custom DLLs
Looping through the context records available can be done using the snippet below.
/// <inheritdoc />
public async Task ProcessRequestAsync(CancellationToken cancellationToken = default)
{
do
{
var currentContextRecordFieldValue = Interface.Method.GetFieldValueForExecution<string>("An Input Field");
// Do something.
} while (Interface.Method.MoveToNextContextRecord());
}
If you wish for the custom DLL to fully stop processing other context records if an error occurred, be sure to move to the next context record so the DLL 4 interface is aware that the custom DLL is managing context records itself.
The snippet below shows how to stop the entire method execution on a CriticalException
, but continue on a Exception
.
/// <inheritdoc />
public async Task ProcessRequestAsync(CancellationToken cancellationToken = default)
{
do
{
try
{
// Do something.
}
catch (CriticalException ce)
{
// Example to stop the entire method.
Interface.ResultWriter.WriteError(ce.Message);
Interface.Method.MoveToNextContextRecord()
throw;
}
catch (Exception e)
{
// Example to continue processing the other context records.
Interface.ResultWriter.WriteContextRecordError(e.Message);
}
} while (Interface.Method.MoveToNextContextRecord());
}
Default Context Record Behavior In DLL 4 Interface
If you do not account for context records in your custom DLL, the following behavior is default:
- If there are no additional context records given for the execution, the dll is executed once.
- If there are additional context records, each context record will invoke the entry point (ProcessRequestAsync) of your dll sequentially.
- Keep in mind that your DLL class will only be instantiated once and therefor fields and properties keep their current values between context records.
- If errors occur during the execution of one of the context records, execution will continue for the other context records.
Memory Usage
The BCS_DLL 4 interface is a multithreaded process. As a result, your DLL will only be loaded once in the memory of the process. This has many advantages regarding performance and memory footprint but can also lead to issues if you are not careful.
Statics
If you use static fields or properties, keep in mind that they will always be kept in memory and will never be garbage collected. Therefor, make sure nothing is left in those fields and/or properties which should not be kept after your DLL has been invoked.
However, it is a completely valid scenario to use statics for caching between DLL invocations if this boosts the performance of your code.
Disposals
If you use classes in your DLL that needs to be disposed, make sure this is done before returning from your code. A simple way to do this is by making use of the using statement or by using a try finally block.
/// <inheritdoc />
public async Task ProcessRequestAsync(CancellationToken cancellationToken = default)
{
var anInstanceOfADisposableClass = new SomeDisposableClass();
try
{
// Do something with anInstanceOfADisposableClass.
using (var anotherInstanceOfADisposableClass = new SomeDisposableClass())
{
// Do something with anotherInstanceOfADisposableClass.
}
}
finally
{
anInstanceOfADisposableClass.Dispose();
}
}