Custom integration providers
Integration providers are providers which handles data flowing between DynamicWeb and an external source, e.g. an ERP system, a JSON-file, an OData-endpoint, etc. Read about our standard integration providers here.
Integration providers typically consist of:
- A provider-class which handles settings - connection strings, file names when reading or writing to files, etc.
- One or more reader-classes which handle reading data from the source
- One or more writer-classes which handle writing data received from the reader to e.g. a file or the DW database
In this article we will go through these components and explain how to create a custom integration provider.
Providers
An integration provider should inherit from the BaseProvider located in Dynamicweb.DataIntegration. This class contains a range of methods you must override to avoid facing NotImplementedExceptions down the line.
These can be categorized as:
- General BaseProvider overrides
- Source specific overrides
- Destination specific overrides
BaseProvider implementation
The BaseProvider-class handles standard functionality which all providers must be able to do, e.g. load settings, write settings, run an integration job, and handle start/stop when the provider runs.
The following BaseProvider methods must be overridden:
Initialize()
If there's anything that needs to be initialized for the job to run, it can be set in here, as this is run prior to the job being run.
public override bool Initialize()
{
// Initialization logic
}
RunJob(Job job)
Executes the data integration job set on a provider which functions as a destination provider. Does not execute if the provider is used as a source provider.
This method contains the core logic for running a data transfer process from a source to a destination based on the mappings and settings defined in the job.
It iterates over each mapping defined within the job, reads data from the source, processes it as needed (including any conditional filtering or transformations), and then writes it to the destination.
Any errors or exceptions encountered during the execution are logged. This method returns true if the job completes successfully or false if any errors occur.
public override bool RunJob(Job job)
{
ReplaceMappingConditionalsWithValuesFromRequest(job);
foreach (var mapping in job.Mappings)
{
// Create writer
// Use the reader to get all the data needed to be written
// Write the data
}
return true;
}
LoadSettings(Job job)
This is only used when the provider is set as a Source and is available so the data in the job can be adjusted and manipulated at runtime. It's run directly after initialize is called and before the actual job is run.
public override void LoadSettings(Job job)
{
// Manipulate anything necessary in job
}
Serialize()
Serializes the provider's configuration settings into a string representation in XML format. This is used to save the provider's settings for persistence, allowing them to be loaded back at a later time. The method encapsulates the current state of the provider's settings.
public override string Serialize()
{
var document = new XDocument(new XDeclaration("1.0", "utf-8", string.Empty));
var root = new XElement("Parameters");
document.Add(root);
root.Add(CreateParameterNode(GetType(), "Source ExampleParameter", SourceExampleParameter));
root.Add(CreateParameterNode(GetType(), "Destination ExampleParameter", DestinationExampleParameter));
return document.ToString();
}
Close()
Releases any resources or performs any clean-up necessary when the provider is no longer needed. This might include closing connections to data sources, releasing file handles, or any other clean-up to prevent memory leaks or resource contention. This method is called after a job completes or if the provider is disposed of.
public override void Close()
{
// Cleanup resources here
}
Source implementation
The ISource-interface allows the provider to be used as a source provider, which means it must be able to read data from an external system. This is done using the GetReader()-method, which returns a reader which should be implemented in the source provider as well.
The other methods are used to create, save, and handle the schema which contains information about the data structure the provider should read.
The following Source methods must be overridden:
- ISource.GetSchema()
- GetOriginalSourceSchema()
- OverwriteSourceSchemaToOriginal()
- ISource.SaveAsXml(XmlTextWriter textWriter)
- UpdateSourceSettings(ISource source)
- ValidateSourceSettings()
- GetReader(Mapping mapping)
ISource.GetSchema()
Retrieves the schema for the source. This includes the structure of the data such as tables, columns, data types. The schema is used by integration to understand the format and structure of the source data and how it maps to the destination. This method should return a schema that accurately represents the source data structure.
Schema ISource.GetSchema()
{
if (schema is null)
schema = GetOriginalSourceSchema();
return schema;
}
GetOriginalSourceSchema()
A way to create the schema for this provider. This could be doing some API calls based on some endpoint and constructing a schema based on the response, it could be hardcoding and creating a schema or generating it based on other means, such as reflection based on response models stored in the provider.
Schema ISource.GetOriginalSourceSchema()
{
var schema = new Schema();
var table = schema.AddTable("ExampleTable");
table.AddColumn(new Column("ExampleName", typeof(string), table, false, false));
return schema;
}
This will then create the following schema for the job file;
<Schema>
<table>
<isNew>False</isNew>
<tableName>ExampleTable</tableName>
<sqlSchema />
<columns>
<column type="Dynamicweb.DataIntegration.Integration.Column">
<name>ExampleName</name>
<type>System.String</type>
<isNew>False</isNew>
<isPrimaryKey>False</isPrimaryKey>
</column>
</columns>
</table>
</Schema>
OverwriteSourceSchemaToOriginal()
Can be used to run logic determining if a new source schema should be fetched/created. It's run when the schema comparer is run, so if any logic exists, it will be run. This does not need to be used, so it can overridden and left empty;
void ISource.OverwriteSourceSchemaToOriginal() { }
ISource.SaveAsXml(XmlTextWriter textWriter)
Serializes the source schema and any other necessary settings into XML format. This method is used to save the provider's configuration and schema information, making it possible to persist these details for later use or examination. The XmlTextWriter parameter is used to write the XML data, ensuring that the format is consistent and parsable.
void ISource.SaveAsXml(XmlTextWriter textWriter)
{
textWriter.WriteElementString("SourceExampleParameter", SourceExampleParameter);
(this as ISource).GetSchema().SaveAsXml(textWriter);
}
UpdateSourceSettings(ISource source)
Updates the source settings of the current provider instance to match the settings of the provided ISource instance. This method is used to synchronize settings between two source providers, ensuring they have consistent configurations.
void ISource.UpdateSourceSettings(ISource source)
{
var customProvider = (CustomProvider)source;
SourceExampleParameter = customProvider.SourceExampleParameter;
}
ValidateSourceSettings()
Can be used to validate the settings of the provider, if the existing validations on editors are not enough. The default implementation in the BaseProvider is;
public virtual string ValidateSourceSettings() => "No Validation Performed";
If the returned string is empty or it starts with "Warning: ", then it will pass, however if anything else is returned, it when a provider is saved, it will return an Invalid response error with the message
$"Source provider: { ValidateSourceSettings() }"
Therefore, if the existing editor validation is enough, this method should be overridden with an empty string.
string ISource.ValidateSourceSettings() => "";
GetReader(Mapping mapping)
Creates and returns an ISourceReader instance for reading data from the source based on the specified mapping. The ISourceReader is responsible for fetching data from the source table or entity defined in the mapping and translating it into a generic format that can be processed by the data integration framework. This method may need to consider the specifics of the source system, such as database connections or API endpoints, to properly instantiate the reader.
ISourceReader ISource.GetReader(Mapping mapping)
{
// Implementation logic to return a suitable ISourceReader
}
Destination implementation
The IDestination-interface allows the provider to be used as a destination provider, which means it handles data from a source provider and writes it to e.g. a file or the DW database.
This interface does not have a GetWriter()-method, it is instead handled by the RunJob()-method on the BaseProvider class. The RunJob()-method then used a writer to write data.
The following Destination methods must be overridden:
- IDestination.GetSchema()
- GetOriginalDestinationSchema()
- OverwriteDestinationSchemaToOriginal()
- IDestination.SaveAsXml(XmlTextWriter textWriter)
- UpdateDestinationSettings(IDestination destination)
- ValidateDestinationSettings()
IDestination.GetSchema()
Retrieves the schema for the destination similar to the source's GetSchema method. It provides the structure of the destination data storage, necessary for mapping and transferring data from the source.
Schema IDestination.GetSchema()
{
if (schema is null)
schema = GetOriginalDestinationSchema();
return schema;
}
GetOriginalDestinationSchema()
Similar to GetOriginalSourceSchema, but for the destination. It retrieves the schema of the destination data storage, providing details about its structure necessary for the data integration process. This can include information about tables, columns, and data types in the destination.
Schema IDestination.GetOriginalDestinationSchema()
{
var originalSchema = new Schema();
var table = schema.AddTable("ExampleTable");
table.AddColumn(new Column("ExampleName", typeof(string), table, false, false));
return originalSchema;
}
OverwriteDestinationSchemaToOriginal()
Allows updating the current destination schema with the most recent or original schema from the destination. It can be used to ensure that the schema used in the data integration process matches the actual structure of the destination data storage.
void IDestination.OverwriteDestinationSchemaToOriginal() { }
IDestination.SaveAsXml(XmlTextWriter textWriter)
Serializes the destination schema and settings into XML format for persistence. This method allows the configuration of the destination part of the provider to be saved in a standardized format for future reference or use.
void IDestination.SaveAsXml(XmlTextWriter textWriter)
{
textWriter.WriteElementString("DestinationExampleParameter", DestinationExampleParameter);
(this as IDestination).GetSchema().SaveAsXml(textWriter);
}
UpdateDestinationSettings(IDestination destination)
Updates the destination settings of the current provider instance to match the settings of the provided IDestination instance. This method ensures that the destination settings are synchronized between two providers or updated based on a specific configuration template.
void IDestination.UpdateDestinationSettings(IDestination destination)
{
var customProvider = (CustomProvider)destination;
DestinationExampleParameter = customProvider.DestinationExampleParameter;
}
ValidateDestinationSettings()
Validates the destination settings of the provider. Similar to ValidateSourceSettings, this method checks whether the configuration for the destination part of the provider is valid. It can be used to ensure that all required settings are provided and correct before running the integration job.
string IDestination.ValidateDestinationSettings() => "";
Example provider
This is a example that can be used to ensure all the relevant and important methods are implemented;
public class ExampleProvider : BaseProvider, ISource, IDestination
{
private Schema schema;
[AddInParameter("Source ExampleParameter"), AddInParameterEditor(typeof(TextParameterEditor), ""), AddInParameterGroup("Source")]
public string SourceExampleParameter { get; set; } = "";
[AddInParameter("Destination ExampleParameter"), AddInParameterEditor(typeof(TextParameterEditor), ""), AddInParameterGroup("Destination")]
public int DestinationExampleParameter { get; set; } = "";
public override bool SchemaIsEditable => false;
public CustomProvider() { }
public CustomProvider(XmlNode xmlNode)
{
foreach (XmlNode node in xmlNode.ChildNodes)
{
switch (node.Name)
{
case "Schema":
schema = new Schema(node);
break;
case "SourceExampleParameter":
if (node.HasChildNodes)
SourceExampleParameter = node.FirstChild.Value;
break;
case "DestinationExampleParameter":
if (node.HasChildNodes)
DestinationExampleParameter = node.FirstChild.Value;
break;
}
}
}
public override void Initialize() { }
public override bool RunJob(Job job)
{
ReplaceMappingConditionalsWithValuesFromRequest(job);
foreach (var mapping in job.Mappings)
{
// Create writer
using ISourceReader reader = mapping.Source.GetReader(mapping);
while (!reader.IsDone())
{
var currentRow = reader.GetNext();
ProcessInputRow(mapping, currentRow);
// Write the currentRow with your writer
}
}
return true;
}
public override void LoadSettings(Job job) { }
public override void Close() { }
public override string Serialize()
{
var document = new XDocument(new XDeclaration("1.0", "utf-8", string.Empty));
var root = new XElement("Parameters");
document.Add(root);
root.Add(CreateParameterNode(GetType(), "Source ExampleParameter", SourceExampleParameter));
root.Add(CreateParameterNode(GetType(), "Destination ExampleParameter", DestinationExampleParameter));
return document.ToString();
}
#region Source implementation
Schema ISource.GetSchema()
{
if (schema is null)
schema = GetOriginalSourceSchema();
return schema;
}
Schema ISource.GetOriginalSourceSchema()
{
var originalSchema = new Schema();
var table = schema.AddTable("ExampleTable");
table.AddColumn(new Column("ExampleName", typeof(string), table, false, false));
return originalSchema;
}
void ISource.OverwriteSourceSchemaToOriginal() { }
void ISource.SaveAsXml(XmlTextWriter textWriter)
{
textWriter.WriteElementString("SourceExampleParameter", SourceExampleParameter);
(this as ISource).GetSchema().SaveAsXml(textWriter);
}
void ISource.UpdateSourceSettings(ISource source)
{
var customProvider = (CustomProvider)source;
SourceExampleParameter = customProvider.SourceExampleParameter;
}
string ISource.ValidateSourceSettings() => "";
ISourceReader ISource.GetReader(Mapping mapping)
{
ISourceReader reader;
// Find the reader for this mapping and set whatever configuration is necessary
return reader;
}
#endregion
#region Destination implementation
Schema IDestination.GetSchema()
{
if (schema is null)
schema = GetOriginalDestinationSchema();
return schema;
}
Schema IDestination.GetOriginalDestinationSchema()
{
var originalSchema = new Schema();
var table = schema.AddTable("ExampleTable");
table.AddColumn(new Column("ExampleName", typeof(string), table, false, false));
return originalSchema;
}
void IDestination.OverwriteDestinationSchemaToOriginal() { }
void IDestination.SaveAsXml(XmlTextWriter textWriter)
{
textWriter.WriteElementString("DestinationExampleParameter", DestinationExampleParameter);
(this as IDestination).GetSchema().SaveAsXml(textWriter);
}
void IDestination.UpdateDestinationSettings(IDestination destination)
{
var customProvider = (CustomProvider)destination;
DestinationExampleParameter = customProvider.DestinationExampleParameter;
}
string IDestination.ValidateDestinationSettings() => "";
#endregion
}
Readers
Creating a reader is as simple as implementing the ISourceReader interface. It needs to implement a method that returns the next object that needs to be written, and then a method which determines if the reader is done with all the objects.
Here is an example implementation of a reader:
public sealed class ExampleSourceReader : ISourceReader
{
protected IEnumerator<Dictionary<string, object>> enumerator;
private Dictionary<string, object> nextItem;
public ExampleSourceReader(Mapping mapping)
{
enumerator = GetObjectsToWrite(mapping);
}
public Dictionary<string, object> GetNext() => nextItem;
public bool IsDone()
{
var moveNext = enumerator.MoveNext();
if (!moveNext)
return true;
nextItem = enumerator.Current;
return false;
}
public void Dispose()
{
enumerator.Dispose();
nextItem = null;
}
private IEnumerator<Dictionary<string, object>> GetObjectsToWrite(Mapping mapping)
{
// Your logic which fetches the data necessary from whereever it might be, reading files, creating a database connection, doing API requests
// Then you return all the data in an enumerator which will be handled afterwards
}
}
This is an example which stores an enumerator and then feeds each element to the writer one by one with the GetNext() method. After each write is done, the writer will check if the reader is done by calling IsDone(), where in this case we move the iterator to the next element, so it's ready to feed the next value. Different implementations are possible where instead of keeping an enumerator able to fetch all the data, it's paginated or fetched one chunk at a time to not keep too much of the data in memory.
If it should be possible to have different readers for different mappings, the reader itself can take care of that by fetching the correct data based on the mapping that gets sent, or multiple readers can be made and used depending on the scenario.
An example in one of DynamicWebs providers is a BaseSourceReader
which all the other readers implements, they each have a ModelName
determining which model they're able to read, so we then only pick the reader instance of the model we're about to read, making the structure of all the readers much more logical as each govern their own model.
Writers
A writer doesn't need to implement an interface, as the functionality of a writer is purely based off of doing what is needed to "write" the data in your scenario. Given that, an interface IDestinationWriter is still offered, which contains methods to help get you in the direction of implementing one.
Here is an example of a writer:
public class ExampleDestinaionWriter : IDestinationWriter
{
public Mapping Mapping { get; }
public CustomDestinationWriter(Mapping mapping)
{
Mapping = mapping;
}
public void Close()
{
// If any database connections etc needs to be closed
}
public void Write(Dictionary<string, object> row)
{
// All manipulation you might want done to the row
// This example loops through all the mappings to find the destination column names where the rows key is the source column name, gets those values and appends them if multiple to the same destination column is found.
if (Mapping?.DestinationTable?.Name is null)
return;
var columnMappings = Mapping.GetColumnMappings();
Dictionary<string, object> temporaryColumnMappings = [];
foreach (var columnMapping in columnMappings)
{
if (!columnMapping.Active)
continue;
if (temporaryColumnMappings.TryGetValue(columnMapping.DestinationColumn.Name, out var val))
{
if (columnMapping.DestinationColumn.Type != typeof(string))
continue;
if (row.TryGetValue(columnMapping.SourceColumn.Name, out var sourceColumnValue))
{
val += sourceColumnValue as string;
temporaryColumnMappings[columnMapping.DestinationColumn.Name] = val;
}
}
else if (row.TryGetValue(columnMapping.SourceColumn.Name, out var sourceColumnValue))
temporaryColumnMappings.Add(columnMapping.DestinationColumn.Name, columnMapping.ConvertInputValueToOutputValue(sourceColumnValue));
else
temporaryColumnMappings.Add(columnMapping.DestinationColumn.Name, columnMapping.ConvertInputValueToOutputValue(null));
}
row = temporaryColumnMappings;
if (row.Count == 0)
return;
// Write the actual data to where it needs to be, such as using APIs to post the data, sql to insert it to a database, a file manager to write it etc...
}
}
All the data received from the reader is what is received in the writer, the only manipulation that is done in-between is what is done in the provider itself, in the RunJob(Job job).
Job files
Once a job is saved, a file with all the information is stored in Files/Files/Integration/jobs/{activityFolder}
. At this point it also generates the schema for both the reader and the writer.
All the job settings are stored in a {jobName}.xml
file, and will look like the following;
<?xml version="1.0" encoding="utf-16"?>
<job>
<Name>Example job</Name>
<Description />
<CreateMappingAtRuntime>False</CreateMappingAtRuntime>
<source type="Dynamicweb.DataIntegration.Providers.ExampleProvider.ExampleProvider">
<ExampleParameter>ExampleValue</ExampleParameter>
<Schema>
<table>
<isNew>False</isNew>
<tableName>ExampleTable</tableName>
<sqlSchema>dbo</sqlSchema>
<columns>
<column type="Dynamicweb.DataIntegration.ProviderHelpers.SqlColumn"
columnType="Dynamicweb.DataIntegration.ProviderHelpers.SqlColumn">
<name>ExampleColumn</name>
<type>System.String</type>
<isNew>False</isNew>
<limit>50</limit>
<isIdentity>False</isIdentity>
<sqlDbType>NVarChar</sqlDbType>
<isPrimaryKey>False</isPrimaryKey>
</column>
</columns>
</table>
</Schema>
</source>
<destination type="Dynamicweb.DataIntegration.Providers.ExampleProvider.ExampleProvider">
<ExampleProperty>ExampleValue</ExampleProperty>
<Schema>
<table>
<isNew>True</isNew>
<tableName>ExampleTable</tableName>
<sqlSchema />
<columns>
<column type="Dynamicweb.DataIntegration.Integration.Column">
<name>ExampleColumn</name>
<type>System.String</type>
<isNew>True</isNew>
<isPrimaryKey>False</isPrimaryKey>
</column>
</columns>
</table>
</Schema>
</destination>
</job>
Schemas
With this information, an activity always knows the possible column mappings that can be created based on the table chosen. This can also be abstracted out to not only be seen as tables and columns, but instead as types and variables.
A schema from an API
If a new provider is to be created to support a specific systems API, it's important to take note of this possibility of abstraction. If we would create a provider for the Swagger Petstore APIs, the schema itself would be the information about this API, it could contain properties pointing to an endpoint that would be a qualifying endpoint to request data. Each table in the schema is then an endpoint, such as https://petstore.swagger.io/#/store/getOrderById, which has the response model;
{
"id": 0,
"petId": 0,
"quantity": 0,
"shipDate": "2024-03-15T09:27:16.146Z",
"status": "placed",
"complete": true
}
In this case, we can generate the table and columns to represent this object;
<table>
<isNew>True</isNew>
<tableName>Order</tableName>
<columns>
<column type="Dynamicweb.DataIntegration.Integration.Column">
<name>id</name>
<type>System.Int32</type>
<isNew>True</isNew>
<isPrimaryKey>True</isPrimaryKey>
</column>
<column type="Dynamicweb.DataIntegration.Integration.Column">
<name>petId</name>
<type>System.Int32</type>
<isNew>True</isNew>
<isPrimaryKey>False</isPrimaryKey>
</column>
<column type="Dynamicweb.DataIntegration.Integration.Column">
<name>quantity</name>
<type>System.String</type>
<isNew>True</isNew>
<isPrimaryKey>False</isPrimaryKey>
</column>
<column type="Dynamicweb.DataIntegration.Integration.Column">
<name>shipDate</name>
<type>System.DateTime</type>
<isNew>True</isNew>
<isPrimaryKey>False</isPrimaryKey>
</column>
<column type="Dynamicweb.DataIntegration.Integration.Column">
<name>status</name>
<type>System.String</type>
<isNew>True</isNew>
<isPrimaryKey>False</isPrimaryKey>
</column>
<column type="Dynamicweb.DataIntegration.Integration.Column">
<name>complete</name>
<type>System.Boolean</type>
<isNew>True</isNew>
<isPrimaryKey>False</isPrimaryKey>
</column>
</columns>
</table>
This will in turn let you select Order
as a TableMapping on either the reader or writer side depending on how this provider is used.
The data is then read by the reader in the provider and offered up to the writer in the form of a Dictionary<string, object>, so in the case of the json example model, it would be;
[
"id": 0,
"petId": 0,
"quantity": 0,
"shipDate": "2024-03-15T09:27:16.146Z",
"status": "placed",
"complete": true
]
And the writer will take this data and handle it the way the writer has defined it.
A schema from a database
Seeing a schema as a database setup is even simpler, as in this case it is and will always be a flat structure. A database has tables and a table has columns. This relates to DynamicWebs schema, table and column 1:1, so representing data fetched and inserted into a database with a Dictionary<string, object>.
An example database table called EcomCurrencies
with the following data;
CurrencyCode | CurrencyLanguageId | CurrencySymbol | CurrencyName | CurrencyRate | CurrencyIsDefault | CurrencyCultureInfo | CurrencyPayGatewayCode |
---|---|---|---|---|---|---|---|
EUR | LANG1 | € | Euro | 744 | 0 | de-DE | 978 |
DKK | LANG1 | kr. | Danish krone | 100 | 0 | da-DK | 208 |
GBP | LANG1 | £ | British Pound | 870 | 1 | en-GB | 826 |
BGN | LANG1 | лв. | Bulgarian Lev | 380 | 0 | bg-BG | 100 |
HRK | LANG1 | kn | Croatian Kuna | 99 | 0 | hr-HR | 191 |
CZK | LANG1 | Kč | Czech Koruna | 29 | 0 | cs-CZ | 203 |
PLN | LANG1 | zł | Polish Zloty | 163 | 0 | pl-PL | 985 |
RON | LANG1 | RON | Romanian Leu | 150 | 0 | ro-RO | 642 |
HUF | LANG1 | Ft | Hungarian Forint | 2 | 0 | hu-HU | 348 |
USD | LANG1 | $ | US Dollars | 656 | 0 | en-US | 840 |
Would have a schema as the following;
<table>
<isNew>False</isNew>
<tableName>EcomCurrencies</tableName>
<sqlSchema>dbo</sqlSchema>
<columns>
<column type="Dynamicweb.DataIntegration.ProviderHelpers.SqlColumn"
columnType="Dynamicweb.DataIntegration.ProviderHelpers.SqlColumn">
<name>CurrencyCode</name>
<type>System.String</type>
<isNew>False</isNew>
<limit>3</limit>
<isIdentity>False</isIdentity>
<sqlDbType>NVarChar</sqlDbType>
<isPrimaryKey>True</isPrimaryKey>
</column>
<column type="Dynamicweb.DataIntegration.ProviderHelpers.SqlColumn"
columnType="Dynamicweb.DataIntegration.ProviderHelpers.SqlColumn">
<name>CurrencyLanguageId</name>
<type>System.String</type>
<isNew>False</isNew>
<limit>50</limit>
<isIdentity>False</isIdentity>
<sqlDbType>NVarChar</sqlDbType>
<isPrimaryKey>True</isPrimaryKey>
</column>
<column type="Dynamicweb.DataIntegration.ProviderHelpers.SqlColumn"
columnType="Dynamicweb.DataIntegration.ProviderHelpers.SqlColumn">
<name>CurrencySymbol</name>
<type>System.String</type>
<isNew>False</isNew>
<limit>5</limit>
<isIdentity>False</isIdentity>
<sqlDbType>NVarChar</sqlDbType>
<isPrimaryKey>False</isPrimaryKey>
</column>
<column type="Dynamicweb.DataIntegration.ProviderHelpers.SqlColumn"
columnType="Dynamicweb.DataIntegration.ProviderHelpers.SqlColumn">
<name>CurrencyName</name>
<type>System.String</type>
<isNew>False</isNew>
<limit>50</limit>
<isIdentity>False</isIdentity>
<sqlDbType>NVarChar</sqlDbType>
<isPrimaryKey>False</isPrimaryKey>
</column>
<column type="Dynamicweb.DataIntegration.ProviderHelpers.SqlColumn"
columnType="Dynamicweb.DataIntegration.ProviderHelpers.SqlColumn">
<name>CurrencyRate</name>
<type>System.Double</type>
<isNew>False</isNew>
<limit>0</limit>
<isIdentity>False</isIdentity>
<sqlDbType>Float</sqlDbType>
<isPrimaryKey>False</isPrimaryKey>
</column>
<column type="Dynamicweb.DataIntegration.ProviderHelpers.SqlColumn"
columnType="Dynamicweb.DataIntegration.ProviderHelpers.SqlColumn">
<name>CurrencyIsDefault</name>
<type>System.Boolean</type>
<isNew>False</isNew>
<limit>0</limit>
<isIdentity>False</isIdentity>
<sqlDbType>Bit</sqlDbType>
<isPrimaryKey>False</isPrimaryKey>
</column>
<column type="Dynamicweb.DataIntegration.ProviderHelpers.SqlColumn"
columnType="Dynamicweb.DataIntegration.ProviderHelpers.SqlColumn">
<name>CurrencyCultureInfo</name>
<type>System.String</type>
<isNew>False</isNew>
<limit>50</limit>
<isIdentity>False</isIdentity>
<sqlDbType>NVarChar</sqlDbType>
<isPrimaryKey>False</isPrimaryKey>
</column>
<column type="Dynamicweb.DataIntegration.ProviderHelpers.SqlColumn"
columnType="Dynamicweb.DataIntegration.ProviderHelpers.SqlColumn">
<name>CurrencyPayGatewayCode</name>
<type>System.Int32</type>
<isNew>False</isNew>
<limit>0</limit>
<isIdentity>False</isIdentity>
<sqlDbType>SmallInt</sqlDbType>
<isPrimaryKey>False</isPrimaryKey>
</column>
</columns>
</table>
If this data is read by a reader, the data being sent to the writer should look something like this, as a Dictionary<string, object>;
[
"CurrencyCode": "EUR",
"CurrencyLanguageId": "LANG1",
"CurrencySymbol": "€",
"CurrencyName": "Euro",
"CurrencyRate": 744,
"CurrencyIsDefault": 0,
"CurrencyCultureInfo": "de-DE",
"CurrencyPayGatewayCode": 978
]