Creating Custom Data Sources
Learn how to extend the Beep Data Management Engine by implementing the IDataSource interface to create custom data source connectors
IDataSourcecontract:DataManagementModelsStandard/IDataSource.cs.- Driver metadata model:
ConnectionDriversConfigunder Models/Config. - Engine loading/orchestration:
DataManagementEngineStandard/Editor/DM/DMEEditor.csand assembly handling surfaces. - Migration orchestration after registration:
DataManagementEngineStandard/Editor/Migration/MigrationManager.cs.
Overview
The Beep Data Management Engine supports extensibility through the IDataSource interface, allowing developers to create custom data source connectors for any type of data store or service. This guide covers the complete process of implementing a custom data source.
- Support for any data source type (databases, APIs, files, cloud services)
- Seamless integration with existing Beep infrastructure
- Automatic connection management and error handling
- Consistent interface across all data sources
- Built-in support for ETL operations and data synchronization
When creating a new data source that doesn't exist in the Beep engine's built-in configurations, you must create a corresponding ConnectionDriversConfig to register your data source. This is covered in the next section and is essential for proper integration.
Creating Your ConnectionDriversConfig
Before implementing your IDataSource, you must understand and create a ConnectionDriversConfig. This configuration tells the Beep Data Management Engine how to identify, load, and instantiate your custom data source. Without this, your data source cannot be discovered or used by the engine.
Understanding ConnectionDriversConfig
The ConnectionDriversConfig class serves as the bridge between your custom data source implementation and the Beep engine. When a user requests a data source that doesn't exist in ConnectionHelper.GetAllConnectionConfigs(), the engine requires this configuration to:
- Identify: Your custom data source type and category
- Load: The appropriate driver assembly and class
- Map: Connection properties to your implementation
- Format: Connection strings with proper placeholders
- Display: UI metadata like icons and categories
ConnectionDriversConfig Structure
public class ConnectionDriversConfig : Entity
{
public string GuidID { get; set; } // Unique identifier
public string PackageName { get; set; } // Package/namespace name
public string DriverClass { get; set; } // Driver class name
public string version { get; set; } // Version information
public string dllname { get; set; } // DLL assembly name
public string classHandler { get; set; } // IDataSource implementation class
public string ConnectionString { get; set; } // Connection string template
public string iconname { get; set; } // Icon for UI
public DataSourceType DatasourceType { get; set; } // Data source type enum
public DatasourceCategory DatasourceCategory { get; set; } // Category enum
public bool ADOType { get; set; } // Whether it uses ADO.NET
public bool CreateLocal { get; set; } // Can create local databases
public bool InMemory { get; set; } // In-memory database
public string extensionstoHandle { get; set; } // File extensions (for file sources)
// Additional ADO.NET specific properties
public string AdapterType { get; set; } // DataAdapter type
public string CommandBuilderType { get; set; } // CommandBuilder type
public string DbConnectionType { get; set; } // Connection type
public string DbTransactionType { get; set; } // Transaction type
}
Creating Configuration Examples
Example 1: Custom REST API Data Source Config
public static ConnectionDriversConfig CreateCustomRestApiConfig()
{
return new ConnectionDriversConfig
{
// Unique identifier - always generate a new GUID
GuidID = Guid.NewGuid().ToString(),
// Package and assembly information
PackageName = "MyCustomDataSources",
DriverClass = "MyCustomDataSources.CustomRestApiDataSource",
version = "1.0.0.0",
dllname = "MyCustomDataSources.dll",
// Core handler class (your IDataSource implementation)
classHandler = "CustomRestApiDataSource",
// Connection string template with placeholders
ConnectionString = "BaseUrl={Url};ApiKey={Password};Timeout={Port}",
// UI and categorization
iconname = "api.svg",
DatasourceCategory = DatasourceCategory.WEBAPI,
DatasourceType = DataSourceType.WEBAPI, // Use appropriate enum value
// Behavior flags
ADOType = false, // Not using ADO.NET
CreateLocal = false, // Cannot create local instances
InMemory = false, // Not an in-memory database
Favourite = false, // Not marked as favorite by default
IsMissing = false // Driver is available
};
}
Example 2: Custom File-Based Data Source Config
public static ConnectionDriversConfig CreateCustomFileConfig()
{
return new ConnectionDriversConfig
{
GuidID = Guid.NewGuid().ToString(),
PackageName = "MyCustomDataSources",
DriverClass = "MyCustomDataSources.CustomFileDataSource",
version = "1.0.0.0",
dllname = "MyCustomDataSources.dll",
classHandler = "CustomFileDataSource",
// For file sources, connection string is usually the file path
ConnectionString = "{File}",
iconname = "file.svg",
DatasourceCategory = DatasourceCategory.FILE,
DatasourceType = DataSourceType.Text, // Or create custom enum value
ADOType = false,
CreateLocal = false,
InMemory = false,
Favourite = true,
IsMissing = false,
// Specify which file extensions this driver handles
extensionstoHandle = "myformat,dat,custom"
};
}
Example 3: Custom Database Data Source Config
public static ConnectionDriversConfig CreateCustomDatabaseConfig()
{
return new ConnectionDriversConfig
{
GuidID = Guid.NewGuid().ToString(),
PackageName = "MyCustomDatabase.Client",
DriverClass = "MyCustomDatabase.Client",
version = "2.1.0.0",
dllname = "MyCustomDatabase.Client.dll",
classHandler = "CustomDatabaseDataSource",
// ADO.NET style connection string
ConnectionString = "Server={Host};Port={Port};Database={Database};Username={UserID};Password={Password};",
// ADO.NET types (if using ADO.NET pattern)
AdapterType = "MyCustomDatabase.Client.CustomDataAdapter",
CommandBuilderType = "MyCustomDatabase.Client.CustomCommandBuilder",
DbConnectionType = "MyCustomDatabase.Client.CustomConnection",
DbTransactionType = "MyCustomDatabase.Client.CustomTransaction",
iconname = "customdb.svg",
DatasourceCategory = DatasourceCategory.RDBMS, // or NOSQL
DatasourceType = DataSourceType.CustomDB, // Define in enum
ADOType = true, // Uses ADO.NET pattern
CreateLocal = true, // Can create local databases
InMemory = false,
Favourite = true,
IsMissing = false
};
}
Registration Methods
There are several ways to register your ConnectionDriversConfig with the Beep engine:
Method 1: Direct Registration at Startup
// During application initialization
public void RegisterCustomDataSources(IDMEEditor dmeEditor)
{
// Create your custom config
var customConfig = CreateCustomRestApiConfig();
// Add to the data drivers classes collection
dmeEditor.ConfigEditor.DataDriversClasses.Add(customConfig);
// Also register the assembly class definition
var assemblyClassDef = new AssemblyClassDefinition
{
className = "CustomRestApiDataSource",
type = typeof(CustomRestApiDataSource),
PackageName = "MyCustomDataSources"
};
dmeEditor.ConfigEditor.DataSourcesClasses.Add(assemblyClassDef);
}
Method 2: Extend ConnectionHelper
// Create a helper extension class
public static class CustomConnectionHelper
{
public static List<ConnectionDriversConfig> GetCustomConnectionConfigs()
{
var configs = new List<ConnectionDriversConfig>();
// Add all your custom configurations
configs.Add(CreateCustomRestApiConfig());
configs.Add(CreateCustomFileConfig());
configs.Add(CreateCustomDatabaseConfig());
return configs;
}
public static void RegisterCustomConfigs(IDMEEditor dmeEditor)
{
var customConfigs = GetCustomConnectionConfigs();
foreach (var config in customConfigs)
{
// Check if not already registered
if (!dmeEditor.ConfigEditor.DataDriversClasses.Any(d => d.GuidID == config.GuidID))
{
dmeEditor.ConfigEditor.DataDriversClasses.Add(config);
}
}
}
}
Method 3: Plugin/Extension Pattern
// Create an extension method for IBeepService (if using DI container)
public static class BeepServiceExtensions
{
public static void AddCustomDataSources(this IBeepService beepService)
{
// Register configurations
var customConfigs = CustomConnectionHelper.GetCustomConnectionConfigs();
beepService.DMEEditor.ConfigEditor.DataDriversClasses.AddRange(customConfigs);
// Register assembly definitions
RegisterCustomAssemblyDefinitions(beepService.DMEEditor);
}
private static void RegisterCustomAssemblyDefinitions(IDMEEditor dmeEditor)
{
var definitions = new[]
{
new AssemblyClassDefinition
{
className = "CustomRestApiDataSource",
type = typeof(CustomRestApiDataSource),
PackageName = "MyCustomDataSources"
},
new AssemblyClassDefinition
{
className = "CustomFileDataSource",
type = typeof(CustomFileDataSource),
PackageName = "MyCustomDataSources"
}
// Add more as needed
};
foreach (var def in definitions)
{
if (!dmeEditor.ConfigEditor.DataSourcesClasses.Any(d => d.className == def.className))
{
dmeEditor.ConfigEditor.DataSourcesClasses.Add(def);
}
}
}
}
How Beep Resolves Your Data Source
Understanding how Beep finds and instantiates your data source helps ensure proper registration:
- Connection Request: User requests data source by name
- Find Connection Properties: Beep looks up connection in
ConfigEditor.DataConnections - Link to Driver:
Utilfunction.LinkConnection2Drivers()finds matchingConnectionDriversConfig - Find Assembly Class: Beep looks up
AssemblyClassDefinitionbyclassHandlername - Load Type:
assemblyHandler.GetType()loads the actual .NET type - Create Instance: Reflection creates instance using constructor
- Configure: Sets connection properties and driver config
Driver Resolution Logic (from DMEEditor)
// This is how Beep finds your driver configuration
public IDataSource CreateNewDataSourceConnection(ConnectionProperties cn, string datasourcename)
{
// 1. Link connection to driver config
ConnectionDriversConfig driversConfig = Utilfunction.LinkConnection2Drivers(cn);
// 2. Find assembly class definition
AssemblyClassDefinition ase = ConfigEditor.DataSourcesClasses
.FirstOrDefault(x => x.className.Equals(driversConfig.classHandler,
StringComparison.InvariantCultureIgnoreCase));
// 3. Load the actual type
Type adc = assemblyHandler.GetType(ase.type.AssemblyQualifiedName);
// 4. Create instance using reflection
ConstructorInfo ctor = adc.GetConstructors()
.FirstOrDefault(c => c.GetParameters().Length == 5);
ObjectActivator<IDataSource> createdActivator = GetActivator<IDataSource>(ctor);
IDataSource ds = createdActivator(cn.ConnectionName, Logger, this, cn.DatabaseType, ErrorObject);
// 5. Configure the data source
ds.Dataconnection.ConnectionProp = cn;
ds.Dataconnection.DataSourceDriver = driversConfig;
return ds;
}
Before proceeding to implement your IDataSource:
- ?
ConnectionDriversConfigcreated with unique GUID - ?
PackageNamematches what you'll use in connection properties - ?
classHandlermatches your class name exactly - ?
ConnectionStringtemplate has correct placeholders - ?
DatasourceTypeandDatasourceCategoryare appropriate - ? Config will be registered before creating data source instances
Understanding the IDataSource Interface
The IDataSource interface is located in the DataManagementModels project and defines the contract that all data sources must implement.
Core Interface Properties
public interface IDataSource : IDisposable
{
// Identity and Configuration
string GuidID { get; set; }
string DatasourceName { get; set; }
string Id { get; set; }
DataSourceType DatasourceType { get; set; }
DatasourceCategory Category { get; set; }
// Connection Management
IDataConnection Dataconnection { get; set; }
ConnectionState ConnectionStatus { get; set; }
// Data Structure
List<EntityStructure> Entities { get; set; }
List<string> EntitiesNames { get; set; }
// Services and Utilities
IDMEEditor DMEEditor { get; set; }
IDMLogger Logger { get; set; }
IErrorsInfo ErrorObject { get; set; }
// Query Configuration
string ColumnDelimiter { get; set; }
string ParameterDelimiter { get; set; }
// Events
event EventHandler<PassedArgs> PassEvent;
}
Essential Methods to Implement
// Connection Management
ConnectionState Openconnection();
ConnectionState Closeconnection();
// Entity Discovery and Management
List<string> GetEntitesList();
EntityStructure GetEntityStructure(string EntityName, bool refresh);
bool CheckEntityExist(string EntityName);
bool CreateEntityAs(EntityStructure entity);
// Data Operations
object GetEntity(string EntityName, List<AppFilter> filter);
Task<object> GetEntityAsync(string EntityName, List<AppFilter> Filter);
object RunQuery(string qrystr);
IErrorsInfo ExecuteSql(string sql);
// CRUD Operations
IErrorsInfo InsertEntity(string EntityName, object InsertedData);
IErrorsInfo UpdateEntity(string EntityName, object UploadDataRow);
IErrorsInfo DeleteEntity(string EntityName, object UploadDataRow);
// Transaction Management
IErrorsInfo BeginTransaction(PassedArgs args);
IErrorsInfo Commit(PassedArgs args);
IErrorsInfo EndTransaction(PassedArgs args);
Implementation Steps
Step 1: Create the Data Source Class
Create a new class that implements the IDataSource interface:
using System;
using System.Collections.Generic;
using System.Data;
using System.Threading.Tasks;
using TheTechIdea.Beep;
using TheTechIdea.Beep.DataBase;
using TheTechIdea.Beep.Editor;
using TheTechIdea.Beep.Logger;
using TheTechIdea.Beep.ConfigUtil;
using TheTechIdea.Beep.Addin;
namespace MyCustomDataSources
{
public class CustomDataSource : IDataSource
{
#region Properties
public string GuidID { get; set; } = Guid.NewGuid().ToString();
public string DatasourceName { get; set; }
public string Id { get; set; }
public DataSourceType DatasourceType { get; set; }
public DatasourceCategory Category { get; set; }
public IDataConnection Dataconnection { get; set; }
public ConnectionState ConnectionStatus { get; set; }
public List<EntityStructure> Entities { get; set; } = new List<EntityStructure>();
public List<string> EntitiesNames { get; set; } = new List<string>();
public IDMEEditor DMEEditor { get; set; }
public IDMLogger Logger { get; set; }
public IErrorsInfo ErrorObject { get; set; }
public string ColumnDelimiter { get; set; } = "'";
public string ParameterDelimiter { get; set; } = ":";
public event EventHandler<PassedArgs> PassEvent;
#endregion
#region Constructor
public CustomDataSource(string datasourcename, IDMLogger logger,
IDMEEditor dmeEditor, DataSourceType databasetype,
IErrorsInfo errorObject)
{
DatasourceName = datasourcename;
Logger = logger;
DMEEditor = dmeEditor;
DatasourceType = databasetype;
ErrorObject = errorObject;
Category = DatasourceCategory.NOSQL; // Set appropriate category
}
#endregion
#region Connection Management
public ConnectionState Openconnection()
{
try
{
ErrorObject.Flag = Errors.Ok;
// Initialize your connection here
// Example: Connect to your custom data source
if (Dataconnection?.ConnectionProp?.ConnectionString != null)
{
// Perform actual connection logic
// var connectionString = Dataconnection.ConnectionProp.ConnectionString;
// _customClient = new CustomClient(connectionString);
// _customClient.Connect();
ConnectionStatus = ConnectionState.Open;
Logger?.WriteLog($"Successfully opened connection to {DatasourceName}");
// Load entities after successful connection
LoadEntities();
}
else
{
ConnectionStatus = ConnectionState.Broken;
ErrorObject.Flag = Errors.Failed;
ErrorObject.Message = "Connection string is null or empty";
}
}
catch (Exception ex)
{
ConnectionStatus = ConnectionState.Broken;
ErrorObject.Flag = Errors.Failed;
ErrorObject.Ex = ex;
ErrorObject.Message = ex.Message;
Logger?.WriteLog($"Failed to open connection to {DatasourceName}: {ex.Message}");
}
return ConnectionStatus;
}
public ConnectionState Closeconnection()
{
try
{
if (ConnectionStatus == ConnectionState.Open)
{
// Close your custom connection
// _customClient?.Disconnect();
ConnectionStatus = ConnectionState.Closed;
Logger?.WriteLog($"Successfully closed connection to {DatasourceName}");
}
}
catch (Exception ex)
{
Logger?.WriteLog($"Error closing connection to {DatasourceName}: {ex.Message}");
}
return ConnectionStatus;
}
#endregion
#region Entity Discovery
public List<string> GetEntitesList()
{
try
{
if (ConnectionStatus != ConnectionState.Open)
return new List<string>();
// Discover entities from your data source
var entityNames = new List<string>();
// Example: Query your data source for available entities
// entityNames = _customClient.GetTableNames();
EntitiesNames = entityNames;
return entityNames;
}
catch (Exception ex)
{
Logger?.WriteLog($"Error getting entities list: {ex.Message}");
return new List<string>();
}
}
public EntityStructure GetEntityStructure(string EntityName, bool refresh)
{
try
{
if (refresh || !Entities.Any(e => e.EntityName.Equals(EntityName, StringComparison.OrdinalIgnoreCase)))
{
// Build entity structure from your data source
var entity = new EntityStructure
{
EntityName = EntityName,
DataSourceID = DatasourceName,
DatabaseType = DatasourceType,
Fields = new List<EntityField>()
};
// Example: Get column information from your data source
// var columns = _customClient.GetColumns(EntityName);
// foreach (var column in columns)
// {
// entity.Fields.Add(new EntityField
// {
// FieldName = column.Name,
// Fieldtype = column.DataType,
// Size1 = column.Size,
// AllowDBNull = column.IsNullable
// });
// }
// Update entities list
var existingEntity = Entities.FirstOrDefault(e =>
e.EntityName.Equals(EntityName, StringComparison.OrdinalIgnoreCase));
if (existingEntity != null)
Entities.Remove(existingEntity);
Entities.Add(entity);
return entity;
}
return Entities.FirstOrDefault(e =>
e.EntityName.Equals(EntityName, StringComparison.OrdinalIgnoreCase));
}
catch (Exception ex)
{
Logger?.WriteLog($"Error getting entity structure for {EntityName}: {ex.Message}");
return null;
}
}
public bool CheckEntityExist(string EntityName)
{
try
{
return GetEntitesList().Any(e =>
e.Equals(EntityName, StringComparison.OrdinalIgnoreCase));
}
catch (Exception ex)
{
Logger?.WriteLog($"Error checking entity existence for {EntityName}: {ex.Message}");
return false;
}
}
#endregion
#region Data Retrieval
public object GetEntity(string EntityName, List<AppFilter> filter)
{
try
{
if (ConnectionStatus != ConnectionState.Open)
{
ErrorObject.Flag = Errors.Failed;
ErrorObject.Message = "Connection is not open";
return null;
}
// Convert filters to your data source's query format
var queryFilters = ConvertFilters(filter);
// Execute query against your data source
// var result = _customClient.Query(EntityName, queryFilters);
// Convert result to DataTable or List<T>
// return ConvertToDataTable(result);
return new DataTable(); // Placeholder
}
catch (Exception ex)
{
ErrorObject.Flag = Errors.Failed;
ErrorObject.Ex = ex;
ErrorObject.Message = ex.Message;
Logger?.WriteLog($"Error getting entity {EntityName}: {ex.Message}");
return null;
}
}
public async Task<object> GetEntityAsync(string EntityName, List<AppFilter> Filter)
{
return await Task.Run(() => GetEntity(EntityName, Filter));
}
public object RunQuery(string qrystr)
{
try
{
if (ConnectionStatus != ConnectionState.Open)
{
ErrorObject.Flag = Errors.Failed;
ErrorObject.Message = "Connection is not open";
return null;
}
// Execute custom query
// var result = _customClient.ExecuteQuery(qrystr);
// return ConvertToDataTable(result);
return new DataTable(); // Placeholder
}
catch (Exception ex)
{
ErrorObject.Flag = Errors.Failed;
ErrorObject.Ex = ex;
ErrorObject.Message = ex.Message;
Logger?.WriteLog($"Error executing query: {ex.Message}");
return null;
}
}
#endregion
#region CRUD Operations
public IErrorsInfo InsertEntity(string EntityName, object InsertedData)
{
try
{
// Simple insert - in real implementation, you'd want to
// persist changes back to file
ErrorObject.Flag = Errors.Ok;
ErrorObject.Message = "Insert operation completed";
return ErrorObject;
}
catch (Exception ex)
{
ErrorObject.Flag = Errors.Failed;
ErrorObject.Ex = ex;
ErrorObject.Message = ex.Message;
return ErrorObject;
}
}
public IErrorsInfo UpdateEntity(string EntityName, object UploadDataRow)
{
try
{
ErrorObject.Flag = Errors.Ok;
ErrorObject.Message = "Update operation completed";
return ErrorObject;
}
catch (Exception ex)
{
ErrorObject.Flag = Errors.Failed;
ErrorObject.Ex = ex;
ErrorObject.Message = ex.Message;
return ErrorObject;
}
}
public IErrorsInfo DeleteEntity(string EntityName, object UploadDataRow)
{
try
{
ErrorObject.Flag = Errors.Ok;
ErrorObject.Message = "Delete operation completed";
return ErrorObject;
}
catch (Exception ex)
{
ErrorObject.Flag = Errors.Failed;
ErrorObject.Ex = ex;
ErrorObject.Message = ex.Message;
return ErrorObject;
}
}
public IErrorsInfo UpdateEntities(string EntityName, object UploadData, IProgress<PassedArgs> progress)
{
return UpdateEntity(EntityName, UploadData);
}
#endregion
#region Not Implemented Methods
public bool CreateEntityAs(EntityStructure entity) => false;
public List<ChildRelation> GetChildTablesList(string tablename, string SchemaName, string Filterparamters) => new List<ChildRelation>();
public List<RelationShipKeys> GetEntityforeignkeys(string entityname, string SchemaName) => new List<RelationShipKeys>();
public List<ETLScriptDet> GetCreateEntityScript(List<EntityStructure> entities = null) => new List<ETLScriptDet>();
public IErrorsInfo CreateEntities(List<EntityStructure> entities) => new ErrorsInfo { Flag = Errors.Ok, Message = "Not implemented" };
public IErrorsInfo RunScript(ETLScriptDet dDLScripts) => new ErrorsInfo { Flag = Errors.Ok, Message = "Not implemented" };
public IErrorsInfo ExecuteSql(string sql) => new ErrorsInfo { Flag = Errors.Ok, Message = "Not implemented" };
public IErrorsInfo BeginTransaction(PassedArgs args) => new ErrorsInfo { Flag = Errors.Ok, Message = "Not implemented" };
public IErrorsInfo Commit(PassedArgs args) => new ErrorsInfo { Flag = Errors.Ok, Message = "Not implemented" };
public IErrorsInfo EndTransaction(PassedArgs args) => new ErrorsInfo { Flag = Errors.Ok, Message = "Not implemented" };
#endregion
#region Dispose
public void Dispose()
{
_data?.Clear();
_data = null;
}
#endregion
}
}
Using the Simple Data Source
Here's how to register and use the simple text file data source:
// 1. Register the data source
var assemblyClassDef = new AssemblyClassDefinition
{
className = "SimpleTextFileDataSource",
type = typeof(SimpleTextFileDataSource),
PackageName = "SimpleDataSources"
};
dmeEditor.ConfigEditor.DataSourcesClasses.Add(assemblyClassDef);
// 2. Create connection properties
var connectionProps = new ConnectionProperties
{
ConnectionName = "MyTextFile",
ConnectionString = @"C:\Data\sample.txt", // Path to your text file
DatabaseType = DataSourceType.Text,
Category = DatasourceCategory.FILE,
DriverName = "SimpleTextFile"
};
dmeEditor.ConfigEditor.DataConnections.Add(connectionProps);
// 3. Use the data source
var dataSource = dmeEditor.GetDataSource("MyTextFile");
var state = dmeEditor.OpenDataSource("MyTextFile");
if (state == ConnectionState.Open)
{
var entities = dataSource.GetEntitesList();
Console.WriteLine($"Found entities: {string.Join(", ", entities)}");
foreach (var entityName in entities)
{
var data = dataSource.GetEntity(entityName, null);
Console.WriteLine($"Retrieved data for {entityName}");
if (data is DataTable dt)
{
Console.WriteLine($"Rows: {dt.Rows.Count}, Columns: {dt.Columns.Count}");
}
}
}
Sample Text File Format
Create a text file with pipe-separated values:
Name|Age|Email|Department
John Doe|30|john.doe@company.com|Engineering
Jane Smith|25|jane.smith@company.com|Marketing
Bob Johnson|35|bob.johnson@company.com|Sales
Alice Brown|28|alice.brown@company.com|Engineering