Send custom properties to ODP
How to send custom properties to Optimizely Data Platform (ODP) from Optimizely Configured Commerce
Extend your Optimizely Configured Commerce data by sending custom properties to Optimizely Data Platform (ODP). This lets you enrich customer profiles, events, and transactions with additional attributes, delivered through real-time sends or scheduled integration jobs.
Configured Commerce supports two pipelines for pushing data into ODP:
- Real‑time transmission
- Batch integration jobs (for historical backfills)
This article explains how both pipelines work, where you can wire in custom property hooks, and how to implement them cleanly.
How sending data works
Real-time
- An entity change generates one or more change IDs.
- The system runs a base SQL query for those IDs, shaping them into the ODP payload.
- It calls your
IOdpDataCustomizationService<T>.GetDataCustomization(...)
to merge extra fields. - The system posts the enriched records to the ODP REST API in configurable batch sizes.
Integration jobs
- A scheduled job runs a full SQL extract of all target data.
- The system writes results as CSV and uploads them to an Amazon S3 bucket.
- ODP ingests the CSV file asynchronously.
You can inject custom properties in two ways without altering the base SQL, using one of the following:
- Static preload (Mode A)
- Dynamic factory (Mode B)
Export enrichment mechanisms (Bulk export)
Mode A: Static preload
- Entry point – Override
GetCustomizedDataQuery(filter)
in your postprocessor. - Trigger – Returns a non-null
IDictionary<Guid, IDictionary<string,string>>
. - Behavior – Platform merges your per-GUID dictionary into each CSV row. The system replaces missing keys with
DBNull.Value
. - Best for – Fast lookup on moderate-sized data (fits in memory).
protected override IDictionary<Guid, IDictionary<string,string>> GetCustomizedDataQuery(
CustomizationFilterParameter? filter)
Example
public class ProductOdpDataCustomizationService
: IOdpDataCustomizationService<Product>
{
private static readonly string[] PropertyNames = { "anyProperty1", "anyProperty2" };
private readonly IUnitOfWorkFactory factory;
public IDictionary<Guid, IDictionary<string,string>> GetDataCustomization(
CustomizationFilterParameter? filter)
{
var result = new Dictionary<Guid, IDictionary<string,string>>();
var uow = factory.GetUnitOfWork();
var props = uow.GetRepository<CustomProperty>()
.GetTableAsNoTracking()
.Where(cp => cp.ParentTable == "Product"
&& cp.ModifiedOn >= filter.From
&& PropertyNames.Contains(cp.Name))
.Select(cp => new { cp.ParentId, cp.Name, cp.Value });
foreach (var row in props)
{
if (!result.TryGetValue(row.ParentId, out var dict))
{
dict = new Dictionary<string,string>(StringComparer.OrdinalIgnoreCase);
result[row.ParentId] = dict;
}
dict[row.Name] = row.Value ?? string.Empty;
}
return result;
}
}
Mode B: Dynamic factory
Used only if GetCustomizedDataQuery
returns null
.
ImportantYou must create a copy of
JobPostprocessorBaseOdpExport
and the base postprocessor for your entity, such asJobPostprocessorProductOdpExport
, before using this method to extend Configure Commerce.
- Schema –
GetAdditionalColumnNames()
returns a list of extra column names. - Values –
GetAdditionalDataFactory()
returns aFunc<Guid, object[]>
invoked per row. - Behavior – Positionally maps array values to column names; short arrays will be trailing
DBNull.Value
. - Best for – Compute-heavy fields or very large datasets (minimal memory).
protected override IReadOnlyCollection<string> GetAdditionalColumnNames();
protected override Func<Guid, object[]> GetAdditionalDataFactory();
Example
public class JobPostprocessorCustomProductOdpExport
: JobPostprocessorProductOdpExport
{
private static readonly string[] Columns = { "anyProperty1", "anyProperty2" };
private readonly IUnitOfWork uow;
protected override IDictionary<Guid, IDictionary<string,string>> GetCustomizedDataQuery(...)
=> null;
protected override IReadOnlyCollection<string> GetAdditionalColumnNames()
=> Columns;
protected override Func<Guid, object[]> GetAdditionalDataFactory()
=> productId => {
var props = uow.GetRepository<CustomProperty>()
.GetTableAsNoTracking()
.Where(cp => cp.ParentTable=="Product"
&& cp.ParentId==productId
&& Columns.Contains(cp.Name))
.ToList();
var dict = props.ToDictionary(cp=>cp.Name, cp=>cp.Value ?? string.Empty,
StringComparer.OrdinalIgnoreCase);
return new object[]
{
dict.GetValueOrDefault("anyProperty1",""),
dict.GetValueOrDefault("anyProperty2","")
};
};
}
Comparison
Criterion | Static preload | Dynamic factory |
---|---|---|
Entry point | GetCustomizedDataQuery(filter) | GetAdditionalColumnNames + factory |
Memory footprint | Preloads all extra data | Computes per row |
Column uniformity | Missing replaced by DBNull | Fixed schema |
Best for | Fast lookup of known values | Expensive or huge data sets |
Developer extensibility
Define custom fields for both real-time and batch without touching base SQL.
public interface IOdpDataCustomizationService<T>
: IDependency, IExtension
where T : class, IBusinessObject
{
IDictionary<Guid, IDictionary<string,string>>? GetDataCustomization(
CustomizationFilterParameter? filter);
}
CustomizationFilterParameter
- IdFilter – Optional
ICollection<Guid>
to enrich only specific IDs - From/To – Inclusive timestamp bounds
Integration points
- Real-time –
OdpUpdatedEntityEventService
invokesGetDataCustomization
for changed IDs. - Batch export –
JobPostprocessorBaseOdpExport
invokes it before CSV generation .
Where customization hooks run
Bulk export flow
- Uses SQL to read base rows.
- Applies customization
.
- Static –
GetCustomizedDataQuery(filter)
- Dynamic –
GetAdditionalColumnNames()
+GetAdditionalDataFactory()
- Static –
- Writes enriched CSV then uploads to S3 and ODP ingestion .
Real-time API sync
- Detects entity changes then collects change IDs.
- Reruns base SQL snapshot for those IDs.
- Merges extra fields with
IOdpDataCustomizationService
. - Batches client‐side and
POST
to ODP API.
Updated 3 days ago