Introduction
A reliable test environment needs realistic, consistent, and compliant data. Manual seeding or ad-hoc scripts produce brittle test sets: missing referential integrity, unrealistic distributions, PII leakage, and hard-to-reproduce failures. A Test Data Fabric is an automated, repeatable platform that generates relational test data across schemas while respecting constraints, business rules, and privacy.
This article explains how to design and implement a production-ready Test Data Fabric using .NET for backend orchestration, SQL Server / Postgres for target databases, and an Angular UI for configuration and preview. You’ll get architecture, data models, algorithms (graph-walk, dependency resolution, domain distribution), implementation patterns, sample code snippets, testing and governance, and operational tips.
Goals and Non-goals
Goals
Auto-generate referentially consistent relational data across multiple tables.
Respect foreign keys, unique constraints, check constraints, sequences and triggers.
Support domain-aware distributions (customer geography, order patterns, seasonality).
Support PII-safe synthetic data and tenant-aware masking.
Provide reproducible runs (seeded RNG) and selective refresh (table-level, row-level).
Offer UI for configuring datasets, sampling rates, and previewing samples.
Non-goals
Not a replacement for full production anonymization pipelines (but it can integrate with them).
Not a deep synthesis engine for complex unstructured data (NLG can be plugged in).
High-Level Architecture
┌──────────────────────────┐ ┌──────────────────────┐ ┌──────────────────┐
│ Angular Admin Console │ -> │ Orchestration API │ -> │ Target Databases │
│ (configure fabric, jobs) │ │ (.NET Core) │ │ (SQL Server/Postgres)
└──────────┬───────────────┘ └──┬───────────────────┘ └──────┬───────────┘
│ │ │
▼ │ ▼
┌──────────────────────────┐ │ ┌────────────────────┐
│ Template / Rules Store │ <------┘ │ Data Generators │
│ (JSON/YAML/DB) │ │ (Generators, Faker) │
└──────────────────────────┘ └────────────────────┘
Components
Admin UI (Angular): design templates, set volumes, preview, manage jobs, audit.
Orchestration API (.NET): job scheduling, dependency resolution, execution engine.
Generators: pluggable modules that create values per column (domains, distributions, references).
Target DB connector: applies data through efficient bulk APIs (BULK INSERT, COPY).
Audit + Replay store: store seeds, config, and generated manifests for reproducibility.
Key Concepts
Schema Graph
Model tables and FK edges as a directed acyclic graph (DAG). Generation order respects dependencies: parent tables first (or use multi-pass strategies for circular references).
Templates and Column Profiles
Each column has a profile: type, generator, domain constraints, uniqueness, nullable, distribution (zipf, normal, uniform), masking rules for PII.
Example column profile
{
"table":"Customer",
"column":"Email",
"generator":"EmailGenerator",
"uniqueness":"global",
"nullable":false,
"distribution": { "type":"categorical", "values":["gmail.com","yahoo.com","corp.com"], "weights":[0.5,0.3,0.2] }
}
Referential Integrity Strategies
Parent-first: generate parent rows, keep mapping of natural keys to generated surrogate keys, then emit child rows referencing parents.
Deferred constraints: in DBs that support deferred FK checking (Postgres), insert in any order and defer constraint validation at transaction end. Useful for circular FKs.
Two-pass generation: generate placeholders for children, then fill FK columns once parent keys are known.
Domain Awareness
Domain rules encode business invariants: e.g., Order.Total == SUM(OrderLine.Amount), Stock >= 0. The fabric must either generate consistent aggregates or run reconciliation jobs post-generation.
Reproducibility
Seeded PRNGs with recorded seed per job produce identical datasets for the same configuration. Persist seeds and job manifests for replay.
Workflow Diagram
1. User configures template in Angular UI and requests N customers, M orders.
2. UI sends template + job request to .NET Orchestration API.
3. Orchestration resolves schema graph, selects generators, computes volumes per table.
4. Generator modules produce rows in-memory or buffered to disk, mapping PKs and FKs.
5. Orchestration writes to temporary staging tables or directly bulk-loads target DB.
6. Post-checks run: constraints, business rules, uniqueness audits.
7. Job summary and sample data returned to UI for review.
Detailed Implementation Steps
1 — Schema Introspection
Read database schema (INFORMATION_SCHEMA or system catalog) to build table metadata:
columns, types, default values
foreign keys, unique constraints, check constraints
identity/sequence generation method
.NET snippet using Npgsql / SqlClient:
using var conn = new NpgsqlConnection(connStr);
await conn.OpenAsync();
var cmd = new NpgsqlCommand("SELECT table_name,column_name,data_type FROM information_schema.columns WHERE table_schema='public'", conn);
using var rdr = await cmd.ExecuteReaderAsync();
while (await rdr.ReadAsync()) {
// build metadata
}
2 — Build Dependency Graph
Construct graph with nodes as tables, edges from parent → child (FK direction). Detect cycles; for cycles use deferred constraints or placeholder PKs.
3 — Plan Volumes
User provides high-level volumes (e.g., 10k customers, average 5 orders/customer). Orchestration expands to table row counts using per-table multi-assignment:
Orders = Customers * avgOrders
OrderLines = Orders * avgLines
Allow variability (Poisson sampling) for realistic skew.
4 — Choose Generators
Generators are modular. Examples:
Primitive: random int, sequential, GUID, timestamp.
Semantic: PersonName, Email, PhoneNumber, Address (using Faker or custom datasets).
Domain: Product SKUs from product catalog distribution.
Currency/Financial: decimal with rounding and constraints.
Generators accept config: locale, distribution, nullRate, uniqueness key.
5 — Key Mapping & FK Assignment
While generating parent rows, store mapping from synthetic natural key to DB PK (or generate PK on the fly). For large volumes, avoid storing full mapping in memory: use streaming with deterministic generation or use bucketed mapping saved to local storage (e.g., RocksDB or Redis) to look up when generating children.

Join the conversation! Your thoughts help the community grow.