Send custom properties to ODP
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.
🚧 Important You must create a copy of JobPostprocessorBaseOdpExport and the base postprocessor for your entity, such as JobPostprocessorProductOdpExport, before using this method to extend Configure Commerce.
- Schema – GetAdditionalColumnNames() returns a list of extra column names.
- Values – GetAdditionalDataFactory() returns a Func<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 invokes GetDataCustomization 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()
- 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.