DataViewDataSource
DataViewDataSource is a federated virtual data source that unifies entities
from multiple underlying datasources into a single logical view. It implements IDataSource,
IDataViewOperations, and IDisposable, and persists its definition as a
JSON file via DMDataView.
The view can operate in two execution modes: Cached (data is materialised into a
local in-memory engine and refreshed on a TTL) or DirectQuery (always fetched live
from the backing sources). Cross-source joins are expressed as
FederatedJoinDefinition records and are either detected automatically from
foreign-key metadata or defined manually.
Architecture
A DataViewDataSource instance owns one IDMDataView (concrete type
DMDataView). The view holds the list of EntityStructure objects (each
referring back to a real datasource via DataSourceID) and a list of
FederatedJoinDefinition records describing how entities are joined cross-source.
DataViewDataSource (IDataSource + IDataViewOperations)
├── IDMDataView (DMDataView) — persisted as JSON
│ ├── List<EntityStructure> — entities from 1..N datasources
│ ├── List<FederatedJoinDefinition> — cross-source joins
│ ├── ExecutionMode — Cached | DirectQuery
│ ├── CacheTTLSeconds — default 300 s
│ ├── LocalEngineConnectionName — in-memory engine name
│ └── ViewName / ViewID / VID
│
├── DataViewConnection (IDataConnection, IsLogical=true)
│ └── No physical file; OpenConnection() always returns Open
│
└── Partial classes
├── DataViewDataSource.EntityLifecycle.cs
├── DataViewDataSource.FieldManagement.cs
├── DataViewDataSource.Filters.cs
├── DataViewDataSource.JoinGraph.cs
├── DataViewDataSource.QueryExecution.cs
├── DataViewDataSource.Validation.cs
└── DataViewDataSource.ViewUtilities.cs
Execution flow — Cached mode
GetEntity()detects cross-source entities (multiple distinctDataSourceIDvalues).RefreshCacheIfExpired()checksCacheLastRefresh + CacheTTLSeconds.- If stale,
PrepareMergedQueryDB()materialises all entities into theLocalEngineConnectionNamein-memory store. - The local engine then satisfies the query — including joins — as a native SQL operation.
Execution flow — DirectQuery mode / single-source
- Single-source entities are delegated directly to the backing
IDataSource. - File, URL, and query view types are passed through unchanged.
Key types
| Type | Role |
|---|---|
DMDataView | Concrete IDMDataView — JSON-persisted view definition |
FederatedJoinDefinition | Describes a cross-source join: left/right entity+column+datasource, join type |
FederatedJoinType | Enum: Inner, Left, Right, Full |
FederationExecutionMode | Enum: Cached, DirectQuery |
ViewType | Enum: Table, File, Url, Query |
ViewSummary | Snapshot of entity/join counts, validation errors, cache status, broken/unconnected lists |
DataViewConnection
DataViewConnection implements IDataConnection for view-oriented sources.
Because views are logical constructs, IsLogical = true means
OpenConnection() immediately returns Open without touching the
filesystem.
| Property | Type | Notes |
|---|---|---|
InMemory | bool | When true the view is backed only by in-memory data. Default false. |
IsLogical | bool | When true, OpenConnection() always succeeds without filesystem I/O. Set to true by the constructor. |
ConnectionProp | IConnectionProperties | Standard connection properties (FileName, FilePath, DriverName, …). |
ConnectionStatus | ConnectionState | Tracks current state (Open / Closed). |
DbConn | IDbConnection | Unused for logical connections; reserved for future use. |
// DataViewConnection.OpenConnection()
public ConnectionState OpenConnection()
{
return OpenConn(); // fast-path when IsLogical=true → sets status=Open immediately
}
Core Properties
| Property | Type | Description |
|---|---|---|
DatasourceName | string | The view file name (e.g. MyView.json). |
DatasourceType | DataSourceType | Always DataSourceType.Text. |
Category | DatasourceCategory | Always DatasourceCategory.VIEWS. |
DataView | IDMDataView | The underlying view model. Entities and JoinDefinitions are exposed through it. |
Entities | List<EntityStructure> | Proxied from DataView.Entities. |
JoinDefinitions | List<FederatedJoinDefinition> | Cross-source join declarations. |
ExecutionMode | FederationExecutionMode | Cached (default) or DirectQuery. |
CacheEnabled | bool | Set to false to force always-live DirectQuery mode. Default true. |
CacheTTLSeconds | int | Seconds before cached data is considered stale. Default 300. |
CacheLastRefresh | DateTime | Timestamp of the latest successful materialise. |
LocalEngineConnectionName | string | Name of the registered in-memory engine used for federated SQL evaluation. |
ViewName | string | Human-readable view name. |
ViewID | int | Numeric identifier proxied from DataView.ViewID. |
VID | string | GUID vendor/view identifier. |
Viewtype | ViewType | Proxied from the DataView model (Table by default). |
Description | string | Optional human-readable description for UI display. |
FileLoaded | bool | True once LoadView() has populated the view from the JSON file. |
ColumnDelimiter | string | Default ''. |
ParameterDelimiter | string | Default :. |
Constructor
public DataViewDataSource(
string datasourcename,
IDMLogger logger,
IDMEEditor pDMEEditor,
DataSourceType pDatasourceType,
IErrorsInfo per);
The constructor performs the following steps:
- Assigns
Logger,ErrorObject,DMEEditor. - Sets
DatasourceType = DataSourceType.Text,Category = DatasourceCategory.VIEWS. - Creates a
DataViewConnectionwithIsLogical = true. - Resolves the file path from the configured
FolderFileTypes.DataViewfolder, or from the path embedded indatasourcename. - Looks up an existing
ConnectionPropertiesentry for the file name. If none exists, creates a new entry withDriverName = "DataViewReader"and saves the connections config. - Sets
DataViewFileto the full absolute path of the JSON view definition.
LoadView(), which is triggered by
GetEntitesList() and other data methods when DataView.Entities.Count <= 2.
Data Retrieval
| Method | Signature | Description |
|---|---|---|
GetEntitesList |
IEnumerable<string> GetEntitesList() |
Returns entity captions (or names if caption is empty). Triggers lazy load if needed. |
GetEntity |
IEnumerable<object> GetEntity(string EntityName, List<AppFilter> filter) |
Smart routing: if entities span multiple datasources (cross-source), materialises cache and delegates to the local federated engine. Otherwise delegates directly to the backing datasource. |
GetEntity (paged) |
PagedResult GetEntity(string EntityName, List<AppFilter> filter, int pageNumber, int pageSize) |
Paged read — delegates to the originating datasource. |
GetScalar |
double GetScalar(string query) |
Executes an aggregate query through the federated local engine (supports cross-source COUNT, SUM, AVG). |
GetScalarAsync |
Task<double> GetScalarAsync(string query) |
Async wrapper over GetScalar. |
Smart routing in GetEntity
// Cross-source path (entities have different DataSourceIDs at root level)
bool isCrossSource = DataView.Entities
.Where(e => e.ParentId == 0)
.Select(e => e.DataSourceID)
.Distinct(StringComparer.OrdinalIgnoreCase)
.Count() > 1;
if (isCrossSource && ent.Viewtype == ViewType.Table)
{
RefreshCacheIfExpired();
var db = PrepareMergedQueryDB();
return db?.GetEntity(EntityName, filter);
}
// Single-source fallback
IDataSource srcDs = GetDataSourceObject(EntityName);
return srcDs?.GetEntity(EntityName, filter);
Entity Lifecycle
Defined in DataViewDataSource.EntityLifecycle.cs. All mutating methods call
InvalidateCache() automatically.
| Method | Signature | Description |
|---|---|---|
MoveEntity |
IErrorsInfo MoveEntity(int entityId, int newParentId, bool updateJoins = true) |
Re-parents an entity. When updateJoins = true, removes auto-generated
joins between the old parent and the entity.
|
GetAutoJoinsToParent |
List<FederatedJoinDefinition> GetAutoJoinsToParent(int entityId) |
Returns the automatically-detected joins that link an entity to its current parent. |
RenameEntity |
IErrorsInfo RenameEntity(int entityId, string newName) |
Renames an entity and patches all FederatedJoinDefinition references
(LeftEntityName, RightEntityName) to match the new name.
Caption is also updated when it matched the old name.
|
DuplicateEntity |
EntityStructure DuplicateEntity(int entityId, string newName, int newParentId = 0) |
Deep-clones an EntityStructure via JSON round-trip. Assigns a new Id. |
ReorderEntities |
IErrorsInfo ReorderEntities(List<int> orderedEntityIds) |
Reorders the internal Entities list to match the supplied ID sequence.
Entities not in the list are appended at the end.
|
GetEntityStructure |
EntityStructure GetEntityStructure(string entityName) |
Case-insensitive lookup of an entity by name within the view. |
RenameEntity patches join references atomically within the same operation — all join definitions referencing the old name are updated before InvalidateCache() is called.
Field Management
Defined in DataViewDataSource.FieldManagement.cs.
| Method | Signature | Description |
|---|---|---|
GetEntityFields |
List<EntityField> GetEntityFields(string entityName) |
Returns the field list for a named entity. Returns empty list if entity not found. |
AddFieldToEntity |
IErrorsInfo AddFieldToEntity(string entityName, EntityField field) |
Adds a field to an entity. Fails gracefully (logged, not thrown) if entity not found or field already exists (case-insensitive duplicate check). |
RemoveFieldFromEntity |
IErrorsInfo RemoveFieldFromEntity(string entityName, string fieldName) |
Removes a field by name. Logs a warning if the field is not found. |
RefreshEntitySchema |
List<string> RefreshEntitySchema(string entityName) |
Calls DetectSchemaChanges first to surface drift, then pulls the live
schema from the backing datasource via GetEntityStructure(name, true)
and overwrites the stored field list.
|
// Add a computed/virtual field to a view entity
var field = new EntityField
{
FieldName = "FullName",
fieldtype = "System.String",
IsKey = false,
CustomField = true
};
var result = view.AddFieldToEntity("CUSTOMERS", field);
// Sync schema from live source after upstream changes
var changes = view.RefreshEntitySchema("ORDERS");
if (changes.Any())
Console.WriteLine("Schema drift: " + string.Join(", ", changes));
Filter Management
Defined in DataViewDataSource.Filters.cs. Filters are stored in
EntityStructure.CustomBuildQuery property (a WHERE-clause expression per entity).
Setting or clearing a filter automatically calls InvalidateCache().
| Method | Signature | Description |
|---|---|---|
SetEntityFilter |
IErrorsInfo SetEntityFilter(string entityName, string filterExpression) |
Sets a WHERE-clause expression for a specific entity. Invalidates cache. |
GetEntityFilter |
string GetEntityFilter(string entityName) |
Returns the current filter expression for a named entity, or null if none is set. |
ClearEntityFilter |
IErrorsInfo ClearEntityFilter(string entityName) |
Removes the filter for a specific entity (calls SetEntityFilter(name, null)). |
ClearAllFilters |
void ClearAllFilters() |
Resets CustomBuildQuery on every entity. Invalidates cache once. |
// Apply a WHERE filter to one entity in the view
view.SetEntityFilter("ORDERS", "Status = 'ACTIVE' AND Amount > 1000");
// Read it back
string expr = view.GetEntityFilter("ORDERS");
// Remove just that entity's filter
view.ClearEntityFilter("ORDERS");
// Wipe all filters in one call
view.ClearAllFilters();
Join Graph Intelligence
Defined in DataViewDataSource.JoinGraph.cs. Provides automatic FK-based join
detection, BFS path-finding, and a graph adjacency export.
| Method | Signature | Description |
|---|---|---|
AutoDetectJoins |
IErrorsInfo AutoDetectJoins() |
Iterates all entities, opens their backing datasource, reads
EntityStructure.Relations, and adds matching
FederatedJoinDefinition records for any FK relationships where both
entities are present in the view. Skips duplicates and manually-defined joins.
Calls InvalidateCache() on success.
|
FindJoinPath |
List<string> FindJoinPath(string fromEntityName, string toEntityName) |
BFS over the undirected join graph. Returns the shortest sequence of entity
names connecting from to to, or null if no
path exists.
|
GetJoinGraph |
Dictionary<string, List<string>> GetJoinGraph() |
Returns an adjacency list (undirected) keyed by uppercase entity name. All entities appear even if they have no edges (isolated nodes). |
FederatedJoinDefinition properties
| Property | Type | Description |
|---|---|---|
GuidID | string | Unique identifier for this join definition. |
LeftEntityName | string | Name of the left (source) entity. |
LeftColumn | string | Join column on the left entity. |
LeftDataSourceID | string | Registered datasource for the left entity. |
RightEntityName | string | Name of the right (target) entity. |
RightColumn | string | Join column on the right entity. |
RightDataSourceID | string | Registered datasource for the right entity. |
JoinType | FederatedJoinType | Inner, Left, Right, or Full. |
IsManuallyDefined | bool | When true, this join is exempt from auto-removal on entity moves. |
Description | string | Human-readable description (auto-populated for detected joins). |
// Auto-detect joins from FK metadata
view.AutoDetectJoins();
// Find shortest join path between two entities
var path = view.FindJoinPath("CUSTOMERS", "ORDERITEMS");
// → ["CUSTOMERS", "ORDERS", "ORDERITEMS"]
// Inspect the graph
var graph = view.GetJoinGraph();
foreach (var kvp in graph)
Console.WriteLine($"{kvp.Key} → [{string.Join(", ", kvp.Value)}]");
// Manually define a cross-source join
view.JoinDefinitions.Add(new FederatedJoinDefinition
{
GuidID = Guid.NewGuid().ToString(),
LeftEntityName = "CUSTOMERS",
LeftColumn = "CustomerID",
LeftDataSourceID = "SqliteDB",
RightEntityName = "CRMENTRIES",
RightColumn = "CustID",
RightDataSourceID = "CrmRestApi",
JoinType = FederatedJoinType.Left,
IsManuallyDefined = true
});
Query Execution & Cache
Defined in DataViewDataSource.QueryExecution.cs.
| Method | Signature | Description |
|---|---|---|
GetEntityPreview |
IEnumerable<object> GetEntityPreview(string entityName, int maxRows = 100) |
Fetches up to maxRows rows directly from the backing datasource for one entity. Useful for design-time preview without triggering federaton. |
GetViewPreview |
IEnumerable<object> GetViewPreview(int maxRows = 100) |
If a LocalEngineConnectionName is configured, queries the first entity via
the local engine. Otherwise falls back to GetEntityPreview for the first entity.
|
GetCacheStatus |
(bool IsExpired, int RowCount, DateTime LastRefresh, int SecondsTTLRemaining) GetCacheStatus() |
Returns a value tuple describing the current cache state without triggering a refresh. |
MaterializeAsync |
Task MaterializeAsync(CancellationToken ct = default) |
Forces an immediate cache invalidation and re-materialise on a background thread. |
GetExecutionPlan |
string GetExecutionPlan() |
Returns a human-readable pseudo-SQL description of how the view would be executed:
anchor entity, JOIN clauses built from JoinDefinitions, execution mode,
entity/join counts.
|
// Check cache state before deciding whether to force refresh
var (isExpired, _, lastRefresh, remaining) = view.GetCacheStatus();
Console.WriteLine($"Cache expires in {remaining}s (last: {lastRefresh})");
// Force async re-materialise (e.g. after a batch ETL)
await view.MaterializeAsync(cts.Token);
// Print the execution plan for debugging
Console.WriteLine(view.GetExecutionPlan());
// -- DataView: SalesView
// -- Execution Mode: Cached
// -- Entities: 3, Joins: 2
// SELECT * FROM [CUSTOMERS]
// INNER JOIN [ORDERS] ON [CUSTOMERS].[CustomerID] = [ORDERS].[CustomerID]
// INNER JOIN [ORDERITEMS] ON [ORDERS].[OrderID] = [ORDERITEMS].[OrderID]
Cache invalidation
InvalidateCache() is called automatically by any mutation that changes the
view structure: adding/removing entities, changing filters, moving entities, and modifying
joins. It resets CacheLastRefresh to DateTime.MinValue so that
the next GetEntity() call triggers a fresh materialise.
Validation & Health
Defined in DataViewDataSource.Validation.cs. All methods are non-destructive —
they open connections only to test reachability, never modify data.
| Method | Return | Description |
|---|---|---|
ValidateView |
List<string> |
Top-level check: runs ValidateEntities + ValidateJoins and returns all error messages. |
ValidateEntities |
List<string> |
For each entity: checks DataSourceID is non-empty, datasource is registered,
and connection can be opened.
|
GetBrokenEntities |
List<string> |
Returns names of entities whose backing datasource cannot be opened. |
GetUnconnectedEntities |
List<string> |
Returns entity names that are isolated in the join graph — no join edge to any other entity in the view. |
DetectSchemaChanges |
List<string> |
Compares the stored EntityStructure.Fields against the live schema from
the backing datasource. Returns a list of change descriptions
(added / removed / type-changed fields).
|
// Full view validation
var errors = view.ValidateView();
if (errors.Any())
{
foreach (var e in errors) Console.WriteLine("ERROR: " + e);
}
// Identify entities that can't be reached
var broken = view.GetBrokenEntities();
Console.WriteLine($"Broken: {string.Join(", ", broken)}");
// Find isolated entities (no joins to rest of the graph)
var lone = view.GetUnconnectedEntities();
Console.WriteLine($"Unconnected: {string.Join(", ", lone)}");
// Check for upstream schema drift on one entity
var drift = view.DetectSchemaChanges("PRODUCTS");
if (drift.Any())
Console.WriteLine("Schema changed: " + string.Join("; ", drift));
View Utilities
Defined in DataViewDataSource.ViewUtilities.cs.
| Method | Return | Description |
|---|---|---|
CloneView |
IDMDataView |
Deep-clones the entire view (JSON round-trip). The clone receives a new GuidID,
VID, and ViewName; all join definitions also get fresh GUIDs
to prevent collisions. Returns null on failure.
|
MergeView |
IErrorsInfo |
Merges another IDMDataView into the current one. Entities are merged
by EntityName (skips duplicates). Joins are merged by GuidID
(new GUID assigned on import). Invalidates cache.
|
GetAllDataSourceIDs |
List<string> |
Returns the distinct set of DataSourceID values referenced by all entities. |
GetViewSummary |
ViewSummary |
Returns a snapshot record with entity/join/datasource counts, validation errors,
broken and unconnected entity lists, cache state, and a computed IsValid flag.
|
ViewSummary properties
| Property | Type | Description |
|---|---|---|
EntityCount | int | Total entities in the view. |
JoinCount | int | Total join definitions. |
ManualJoinCount | int | Joins where IsManuallyDefined = true. |
DataSourceCount | int | Distinct datasource IDs referenced. |
IsValid | bool | True when Warnings.Count == 0. |
Warnings | List<string> | Validation error messages. |
BrokenEntities | List<string> | Entities whose datasource cannot be opened. |
UnconnectedEntities | List<string> | Isolated entities with no join edges. |
CacheLastRefresh | DateTime | Timestamp of the last materialise. |
// Get a full health snapshot
var summary = view.GetViewSummary();
Console.WriteLine($"Valid: {summary.IsValid}");
Console.WriteLine($"Entities: {summary.EntityCount}, Joins: {summary.JoinCount}");
Console.WriteLine($"Sources: {summary.DataSourceCount}");
Console.WriteLine($"Cache age: {(DateTime.UtcNow - summary.CacheLastRefresh).TotalSeconds:F0}s");
// Clone for dev/test environment
var devView = (DMDataView)view.CloneView("SalesView_DEV");
// Merge a partner team's view definition
var partnerView = JsonSerializer.Deserialize<DMDataView>(File.ReadAllText("partner.json"));
view.MergeView(partnerView);
Examples
Example 1 — Create a federated view from two sources
// 1. Resolve the DataViewDataSource from DMEEditor
var view = (DataViewDataSource)dme.GetDataSource("SalesView.json");
// 2. Load the view definition
view.GetEntitesList();
// 3. Verify entities and auto-detect joins from FK metadata
view.AssignDataSourceToEntities(); // ensure DataSourceIDs are set
view.AutoDetectJoins();
// 4. Show the execution plan
Console.WriteLine(view.GetExecutionPlan());
// 5. Execute a cross-source query
var orders = view.GetEntity("ORDERS", new List<AppFilter>
{
new AppFilter { FieldName = "Status", Operator = "=", FilterValue = "OPEN" }
});
Example 2 — Apply filters and check cache
// Narrow each participating entity before materialising
view.SetEntityFilter("ORDERS", "OrderDate > '2025-01-01'");
view.SetEntityFilter("CUSTOMERS", "Region = 'EMEA'");
// Cache was just invalidated. Force async materialise.
await view.MaterializeAsync();
// Check that the cache is now warm
var (expired, _, refreshed, ttlLeft) = view.GetCacheStatus();
Console.WriteLine($"Cache fresh — {ttlLeft}s remaining (refreshed {refreshed:u})");
// Query now routes through the local engine
var rows = view.GetEntity("ORDERS", null);
Console.WriteLine($"Rows: {rows?.Count()}");
Example 3 — View health check and schema sync
var summary = view.GetViewSummary();
if (!summary.IsValid)
{
Console.WriteLine("Validation errors:");
summary.Warnings.ForEach(w => Console.WriteLine(" " + w));
}
if (summary.BrokenEntities.Any())
{
Console.WriteLine("Broken entities (check datasource connections):");
summary.BrokenEntities.ForEach(b => Console.WriteLine(" " + b));
}
if (summary.UnconnectedEntities.Any())
{
Console.WriteLine("Isolated entities (no join path):");
summary.UnconnectedEntities.ForEach(u => Console.WriteLine(" " + u));
}
// Sync schema for any entity whose upstream definition may have changed
foreach (string entityName in view.EntitiesNames)
{
var drift = view.DetectSchemaChanges(entityName);
if (drift.Any())
{
Console.WriteLine($"Schema drift on {entityName} — refreshing...");
var changes = view.RefreshEntitySchema(entityName);
changes.ForEach(c => Console.WriteLine(" Applied: " + c));
}
}
Example 4 — Clone and merge views
// Create a test copy of the production view
var devView = view.CloneView("SalesView_Test");
// devView is a fully independent DMDataView — saving it will not affect the original
// Merge a partner view (e.g. Inventory entities) into the current view
var inventoryDef = JsonSerializer.Deserialize<DMDataView>(
File.ReadAllText("InventoryView.json"));
view.MergeView(inventoryDef);
// Add a manual cross-datasource join for the newly merged entities
view.JoinDefinitions.Add(new FederatedJoinDefinition
{
GuidID = Guid.NewGuid().ToString(),
LeftEntityName = "ORDERS",
LeftColumn = "ProductID",
LeftDataSourceID = "SalesDB",
RightEntityName = "PRODUCTS",
RightColumn = "ProductID",
RightDataSourceID = "InventoryDB",
JoinType = FederatedJoinType.Left,
IsManuallyDefined = true,
Description = "Sales ↔ Inventory product link"
});
Console.WriteLine($"View now has {view.JoinDefinitions.Count} joins across {view.GetAllDataSourceIDs().Count} sources.");