Full-pipeline data import — validation, transformation, batching, quality rules, schema drift, staging, error store, run history, and dead-letter replay.
Overview
DataImportManager (namespace TheTechIdea.Beep.Editor.Importing) is a partial-class orchestrator
that moves data between any two IDataSource instances. It supports the full
ETL lifecycle: schema compatibility checks, field-level quality rules, multi-phase transformation pipelines,
intelligent batch processing, incremental sync, schema drift detection, staging tables, error quarantine,
dead-letter replay, and run-history persistence.
using var mgr = new DataImportManager(dmeEditor);
mgr.SourceDataSourceName = "ExternalCRM";
mgr.SourceEntityName = "Contacts";
mgr.DestDataSourceName = "MainDB";
mgr.DestEntityName = "Customers";
var progress = new Progress<IPassedArgs>(a => Console.WriteLine(a.Messege));
using var cts = new CancellationTokenSource();
var result = await mgr.RunImportAsync(progress, cts.Token, null, batchSize: 100);
Console.WriteLine(result.Flag); // Errors.Ok | Errors.Failed
ImportContext entry point (recommended)
var ctx = new ImportContext
{
Selection = new ImportSelectionContext
{
SourceDataSourceName = "ExternalCRM",
SourceEntityName = "Contacts",
DestinationDataSourceName = "MainDB",
DestinationEntityName = "Customers"
},
Options = new ImportExecutionOptions
{
BatchSize = 200,
RunMigrationPreflight = true,
OnBatchError = BatchErrorStrategy.Skip
}
};
using var mgr = new DataImportManager(dmeEditor);
var result = await mgr.RunImportAsync(ctx, progress, CancellationToken.None);
ImportContext
ImportContext is the canonical, serialisable entry-point for all import runs.
It replaces the older string-key parameter dictionary.
ImportContext
Property
Type
Description
RunId
string
Auto-generated GUID for this run (used for correlation in logs and error store)
Selection
ImportSelectionContext
Source + destination identifiers
Mapping
EntityDataMap?
Optional field-level mapping; null = use same field names
Options
ImportExecutionOptions
Execution knobs (batch size, preflight, retry, …)
ImportSelectionContext
Property
Default
Description
SourceDataSourceName
Registered name of the source IDataSource
SourceEntityName
Entity / table to read from
DestinationDataSourceName
Registered name of the destination IDataSource
DestinationEntityName
Entity / table to write to
CreateDestinationIfNotExists
true
Auto-create missing entity on destination
IsValid
computed
Returns true only when all four identifiers are non-empty
ImportExecutionOptions
Property
Default
Description
BatchSize
100
Records per write batch
OnBatchError
BatchErrorStrategy.Skip
What to do when a batch fails: Abort, Skip, or Retry
MaxRetries
3
Max retry attempts before applying OnBatchError
RunMigrationPreflight
false
Run schema-compatibility check before starting the import
AddMissingColumns
true
Automatically add source columns missing from destination
CreateSyncProfileDraft
false
Generate a DataSyncSchema draft after preflight but before executing
RunImportOnFinish
true
Execute the actual import after preflight / draft generation
BatchErrorStrategy enum
Value
Behaviour
Abort
Stop the entire import on first batch failure
Skip
Log the failure, quarantine the batch, continue
Retry
Retry up to MaxRetries times before quarantining
DataImportConfiguration
DataImportConfiguration is the expanded configuration object used when callers need
fine-grained control. It is accepted by RunImportAsync(config, …) and can be built from
an ImportContext via BuildConfigurationFromContext(ctx).
Core identifiers
Property
Type
Description
SourceEntityName
string
Source entity / table name
DestEntityName
string
Destination entity / table name
SourceDataSourceName
string
Registered source datasource name
DestDataSourceName
string
Registered destination datasource name
SourceData
IDataSource
Resolved source; auto-populated if name is set
DestData
IDataSource
Resolved destination; auto-populated if name is set
Preflight validation checks schema compatibility without writing any records. Enable it via ImportExecutionOptions.RunMigrationPreflight = true or call it directly.
// Direct call (independent of a full RunImportAsync)
var result = await mgr.RunMigrationPreflightAsync(config, log: msg => Console.WriteLine(msg));
if (result.Flag == Errors.Failed)
{
Console.WriteLine($"Preflight failed: {result.Message}");
return;
}
// Build sync-draft schema (no records imported)
var draft = await mgr.BuildSyncDraftAsync(config);
Preflight checks performed
Source datasource resolves and is connected.
Destination datasource resolves and is connected.
Source entity structure can be read.
Destination entity exists (or will be created if CreateDestinationIfNotExists=true).
When AddMissingColumns=false: all source fields must already exist on the destination.
Data Quality Rules
Add IDataQualityRule instances to DataImportConfiguration.QualityRules.
Each rule is evaluated per-record before writing. Failures trigger the rule's OnFailure action.
DataQualityAction enum
Value
Behaviour
Block
Reject the record — do not write, increment RecordsBlocked
Quarantine
Send the record to the error store for later replay, increment RecordsQuarantined
Warn
Write the record but log a warning, increment RecordsWarned
Built-in rules
Rule class
Constructor
What it checks
NotNullRule
(fieldName, onFailure)
Value must be non-null and non-empty
UniqueRule
(fieldName, onFailure)
All values within a batch must be distinct (case-insensitive)
RangeRule
(fieldName, min, max, onFailure)
Numeric / date value must fall within [min, max]
RegexRule
(fieldName, pattern, onFailure, options)
String value must match the regex pattern
config.QualityRules.Add(new NotNullRule("CustomerID", DataQualityAction.Block));
config.QualityRules.Add(new UniqueRule("CustomerID", DataQualityAction.Quarantine));
config.QualityRules.Add(new RangeRule("Age", min: 0, max: 150, DataQualityAction.Block));
config.QualityRules.Add(new RegexRule("Email", @"^[^@]+@[^@]+\.[^@]+$", DataQualityAction.Warn));
// Custom rule
public class PositivePriceRule : IDataQualityRule
{
public string FieldName => "Price";
public string RuleName => "positive_price";
public DataQualityAction OnFailure => DataQualityAction.Block;
public bool Evaluate(object? value, object record) =>
value != null && decimal.TryParse(value.ToString(), out var d) && d > 0;
public string FailureMessage(object? value) =>
$"Price must be positive (got '{value}').";
}
config.QualityRules.Add(new PositivePriceRule());
Schema Drift
The SchemaDriftPolicy controls what happens when the source entity's schema changes between runs.
SchemaComparator (static utility) compares two SchemaSnapshot instances and produces a SchemaDriftReport.
SchemaDriftPolicy enum
Value
Behaviour
Ignore
Proceed without checking for drift
AbortOnDrift
Abort the import if any added, removed, or altered fields are detected
AutoAddColumns
(Default) Automatically add new source columns to the destination
AutoDropColumns
Remove destination columns that no longer exist in source
config.DriftPolicy = SchemaDriftPolicy.AutoAddColumns;
// Manual schema comparison
var baseline = SchemaSnapshot.CaptureFrom(config.SourceData, config.SourceEntityName);
var current = SchemaSnapshot.CaptureFrom(config.SourceData, config.SourceEntityName);
var report = SchemaComparator.Compare(baseline, current);
Console.WriteLine($"Added: {report.AddedFields.Count}");
Console.WriteLine($"Removed: {report.RemovedFields.Count}");
Console.WriteLine($"Altered: {report.AlteredFields.Count}");
Staging
When StagingOptions.Enabled = true, raw records are written to a staging entity
(e.g.Customers_raw) first. The pipeline then normalizes and moves them to the
final destination. Useful for auditing, validation gates, and multi-step transformations.
Property
Default
Description
Enabled
false
Master switch; false = direct import (no staging)
StagingEntitySuffix
"_raw"
Appended to entity name to form staging entity name (Customers_raw)
DropStagingAfterNormalize
false
Drop staging entity after successful normalization
SkipNormalization
false
Useful for inspection-only runs — records stay in staging
// Attach error store to config
config.ErrorStore = new JsonFileImportErrorStore("./errors/");
var result = await mgr.RunImportAsync(config, progress, cts.Token);
// Later — replay records that previously failed
var contextKey = "ExternalCRM/Contacts/MainDB/Customers";
var replayResult = await mgr.ReplayFailedRecordsAsync(contextKey, progress, CancellationToken.None);
Console.WriteLine($"Replay: {replayResult.Flag}");
Data Profiling
DataProfiler (static utility) samples an entity and computes per-field statistics.
Results are returned as a DataProfile containing a list of FieldProfile entries.
Useful for understanding source data before configuring quality rules.
Assign an IImportRunHistoryStore to config.RunHistoryStore to persist run records.
Two implementations ship: DataSourceImportRunHistoryStore and JsonFileImportRunHistoryStore.
IImportRunHistoryStore API
Method
Description
SaveRunAsync(record, token)
Persist a completed run record
GetRunsAsync(contextKey, token)
Return all run records for the context key, newest first
GetLastSuccessfulRunAsync(contextKey, token)
Return the most recent Completed run record, or null
ClearAsync(contextKey, token)
Delete all run records for the context key
Incremental sync with watermarking
// Store and restore last watermark for incremental loads
var historyStore = new JsonFileImportRunHistoryStore("./history/");
config.RunHistoryStore = historyStore;
var last = await historyStore.GetLastSuccessfulRunAsync("CRM/Contacts/DB/Customers");
config.SyncMode = SyncMode.Incremental;
config.WatermarkColumn = "ModifiedDate";
config.LastWatermarkValue = last?.WatermarkValue; // null on first run = import all
var result = await mgr.RunImportAsync(config, progress, cts.Token);
Examples
Full pipeline with all features
using var mgr = new DataImportManager(dmeEditor);
var config = mgr.CreateImportConfiguration(
"ProductFeed", "ExternalCatalog",
"Products", "MainDB");
// Field subset
config.SelectedFields = new List<string> { "SKU", "Name", "Price", "Stock" };
// Source filter — only changed in last 7 days
config.SourceFilters.Add(new AppFilter
{
FieldName = "ModifiedDate",
Operator = ">=",
FilterValue = DateTime.Today.AddDays(-7).ToString("yyyy-MM-dd")
});
// Quality rules
config.QualityRules.Add(new NotNullRule("SKU", DataQualityAction.Block));
config.QualityRules.Add(new NotNullRule("Name", DataQualityAction.Quarantine));
config.QualityRules.Add(new RangeRule("Price", 0.01m, 999999m, DataQualityAction.Block));
// Error store — save quarantined records to JSON
config.ErrorStore = new JsonFileImportErrorStore("./errors/");
// Run history — enable incremental runs
config.RunHistoryStore = new JsonFileImportRunHistoryStore("./history/");
config.SyncMode = SyncMode.Incremental;
config.WatermarkColumn = "ModifiedDate";
// Staging — write raw records first
config.Staging = new StagingOptions { Enabled = true, StagingEntitySuffix = "_raw" };
// Schema drift
config.DriftPolicy = SchemaDriftPolicy.AutoAddColumns;
// Business logic transformation
config.CustomTransformation = record =>
{
if (record is Dictionary<string, object> d)
d["ImportedAt"] = DateTime.UtcNow;
return record;
};
// Execute
var progress = new Progress<IPassedArgs>(a => Console.WriteLine($"[{a.ParameterInt1}/{a.ParameterInt2}] {a.Messege}"));
var result = await mgr.RunImportAsync(config, progress, CancellationToken.None);
// Check results
var status = mgr.GetImportStatus();
Console.WriteLine($"Result: {result.Flag}");
Console.WriteLine($"Processed: {status.RecordsProcessed}");
Console.WriteLine($"Quarantined: {status.RecordsQuarantined}");
Console.WriteLine($"Throughput: {status.Metrics?.RecordsPerSecond:F1} rec/s");
Pause / Resume from UI
// Start import on background thread
_ = Task.Run(() => mgr.RunImportAsync(config, progress, cts.Token));
// Pause after 2 seconds (e.g., user clicks Pause button)
await Task.Delay(2000);
mgr.PauseImport();
var snap = mgr.GetImportStatus();
Console.WriteLine($"Paused at record {snap.RecordsProcessed}");
// Resume
await Task.Delay(3000);
mgr.ResumeImport();
Profile source data before import
var profile = await DataProfiler.ProfileAsync(dmeEditor, "ExternalCatalog", "ProductFeed", 5000);
// Log fields with >10% nulls — may need NotNullRule
foreach (var f in profile.Fields.Where(f => f.NullCount * 1.0 / profile.SampleSize > 0.1))
Console.WriteLine($"High null rate: {f.Name} ({f.NullCount}/{profile.SampleSize})");
In Sync/: persists watermark values for incremental data imports.
public interface IWatermarkStore
{
Task<object?> GetWatermarkAsync(string contextKey);
Task SaveWatermarkAsync(string contextKey, object value);
Task ClearAsync(string contextKey);
}
// Implementation
public class WatermarkStore : IWatermarkStore { /* backed by IDataSource or local store */ }
DataQualityEvaluator
In Quality/: orchestrates rule evaluation across a batch.
public class DataQualityEvaluator
{
public DataQualityEvaluator(List<IDataQualityRule> rules, IImportErrorStore? errorStore = null);
public QualityEvaluationResult Evaluate(object record);
// Returns: Passed {bool}, Action {DataQualityAction}, Failures {List<RuleResult>}
public class QualityEvaluationResult
{
public bool Passed { get; set; }
public DataQualityAction Action { get; set; }
public List<RuleResult> Failures { get; set; } = new();
}
public class RuleResult
{
public string RuleName { get; set; }
public string FieldName { get; set; }
public string Message { get; set; }
public DataQualityAction Action { get; set; }
}
}
LocalStoreFactory
In Factories/: creates local JSON-file backed error store, run history, and watermark stores.
public static class LocalStoreFactory
{
public static IImportErrorStore CreateErrorStore(string directoryPath);
public static IImportRunHistoryStore CreateRunHistoryStore(string directoryPath);
public static IWatermarkStore CreateWatermarkStore(string directoryPath);
}