Overview

FileDataSource (FileManager/FileDataSource.cs) is the single [AddinAttribute]-registered data source for all flat-file and structured-file formats. It never parses bytes itself — every format is handled by an IFileFormatReader that it selects and delegates to at connection-open time.

What FileDataSource gives you
  • One classHandler = "FileDataSource" connection type covers all formats.
  • Format is chosen by DatasourceType on the connection — no code changes needed to switch from CSV to TSV.
  • Plugin formats (Parquet, ORC, …) are discovered at runtime and appear in the connection manager automatically.
  • At runtime you can even switch which reader handles a live connection without recreating it.

Folder Structure

FileManager/
├── FileDataSource.cs           ← IDataSource host, IFileDataSourceReaderHost
├── FileDataSource.Connection.cs
├── FileDataSource.Entity.cs
├── FileReaderFactory.cs        ← static DataSourceType → IFileFormatReader registry
├── Readers/
│   ├── IFileFormatReader.cs    ← reader contract
│   ├── CsvFileReader.cs
│   ├── TsvFileReader.cs
│   ├── JsonFileReader.cs
│   └── TextFileReader.cs
├── Registry/
│   ├── FileReaderRegistry.cs   ← assembly-level discovery
│   ├── FileReaderDescriptor.cs
│   └── FileReaderAttribute.cs
└── Attributes/
    └── FileReaderAttribute.cs  ← [FileReader(...)] decoration

FileDataSource

Registration

// Handles CSV, TSV, JSON and Text out of the box.
// Plugin readers extend this list without changing the registration.
[AddinAttribute(
    Category       = DatasourceCategory.FILE,
    DatasourceType = DataSourceType.CSV | DataSourceType.TSV
                   | DataSourceType.Json | DataSourceType.Text,
    FileType       = "csv,tsv,json,txt")]
public partial class FileDataSource : IDataSource, IFileDataSourceReaderHost { }

Add a connection and open it

dmeEditor.ConfigEditor.AddDataConnection(new ConnectionProperties
{
    ConnectionName = "SalesData",
    FilePath       = @"C:\data",
    FileName       = "sales.csv",
    DatabaseType   = DataSourceType.CSV,
    DriverName     = "FileDataSource"   // classHandler value
});

var fs = dmeEditor.GetDataSource("SalesData") as FileDataSource;
fs.Openconnection();
// Openconnection() sequence:
//   1. FileReaderFactory.RegisterDefaults()        — ensures built-ins are available
//   2. activeReader = FileReaderFactory.GetReader(DatasourceType)
//   3. activeReader.Configure(connectionProperties)
//   4. HasHeader / ParseMode applied from connection props

Standard IDataSource CRUD

// All calls delegate to the active IFileFormatReader internally
EntityStructure entity  = fs.GetEntityStructure("sales.csv", refresh: false);
object          rows    = fs.GetEntity("sales.csv", filters);
IErrorsInfo     ins     = fs.InsertEntity("sales.csv", newRow);
IErrorsInfo     upd     = fs.UpdateEntities("sales.csv", fs.ErrorObject, updatedRow);
IErrorsInfo     del     = fs.DeleteEntity("sales.csv", keyFilter);

fs.Closeconnection();
classHandler = "FileDataSource" is the routing key

Every ConnectionDriversConfig for a file-based format must set classHandler = "FileDataSource". BeepDM's driver resolver instantiates FileDataSource and the correct reader is then resolved from DatasourceType at connection-open time. FileReaderFactory.BuildDriverConfig sets this automatically.

Built-in Readers

Four readers ship with the core assembly and are registered automatically when FileReaderFactory.RegisterDefaults() is called (which happens inside every Openconnection()).

Reader classDataSourceTypeExtensionNotes
CsvFileReaderCSVcsvConfigurable delimiter & quote char
TsvFileReaderTSVtsvTab-separated values
JsonFileReaderJsonjsonJSON arrays / newline-delimited JSON
TextFileReaderTexttxtFixed-width or line-per-record text

IFileFormatReader contract

public interface IFileFormatReader
{
    // Identity
    DataSourceType SupportedType { get; }
    string         GetDefaultExtension();

    // Configuration
    bool      HasHeader { get; set; }
    ParseMode ParseMode { get; set; }  // Lenient | Strict
    void      Configure(IConnectionProperties props);

    // Schema
    string[]        ReadHeaders(string filePath);
    EntityStructure GetEntityStructure(string filePath);

    // Read
    IEnumerable<string[]> ReadRows(string filePath);
    string InferFieldType(string current, string rawValue);

    // Write
    bool CreateFile  (string filePath, IReadOnlyList<string> headers);
    bool AppendRow   (string filePath, IReadOnlyList<string> headers, IReadOnlyList<string> values);
    bool RewriteFile (string filePath, IReadOnlyList<string> headers, IEnumerable<IReadOnlyList<string>> rows);

    // Diagnostics
    IReadOnlyList<RowDiagnostic> LastDiagnostics { get; }
    void ClearDiagnostics();
}

public enum ParseMode { Lenient, Strict }

public record RowDiagnostic(int RowIndex, DiagnosticSeverity Severity, string Message, string? RawLine = null);
public enum DiagnosticSeverity { Info, Warning, Error }

Every built-in and custom reader is decorated with [FileReaderAttribute] which carries display metadata used by FileReaderFactory.BuildDriverConfig:

// Attribute properties
DataSourceType FormatType       // routes to this reader
string         DisplayName      // shown in connection manager UI
string         DefaultExtension // e.g. "csv"
string         Description
string         Author
string         Version
string         IconPath         // defaults to "{ext}.svg" if empty

FileReaderFactory

FileReaderFactory is a static DataSourceType → IFileFormatReader dictionary. It is the single source of truth consulted by FileDataSource at connection-open time.

Core API

// Register a reader (replaces any existing entry for the same DataSourceType)
FileReaderFactory.Register(IFileFormatReader reader);

// Scan the core assembly for [FileReaderAttribute] classes — safe to call many times
FileReaderFactory.RegisterDefaults();

// Resolve; falls back to CSV reader for unknown types
IFileFormatReader r = FileReaderFactory.GetReader(DataSourceType.CSV);

// Introspection
bool supported = FileReaderFactory.IsSupported(DataSourceType.Json);
IEnumerable<DataSourceType> all = FileReaderFactory.SupportedTypes;

RegisterWithConfig — also add the format to the connection manager

// Registers in the factory AND upserts a ConnectionDriversConfig into
// configEditor.DataDriversClasses so the format appears in the connection manager.
// classHandler is always set to "FileDataSource".
// Idempotent: duplicate DataSourceType entries are skipped.
FileReaderFactory.RegisterWithConfig(reader, dmeEditor.ConfigEditor);

// Example — third-party Parquet reader registered at startup:
var parquet = new ThirdParty.ParquetFileReader();
FileReaderFactory.RegisterWithConfig(parquet, dmeEditor.ConfigEditor);

BuildDriverConfig — inspect the generated ConnectionDriversConfig

ConnectionDriversConfig cfg = FileReaderFactory.BuildDriverConfig(reader);
// cfg.classHandler       = "FileDataSource"
// cfg.DatasourceCategory = DatasourceCategory.FILE
// cfg.DatasourceType     = reader.SupportedType
// cfg.extensionstoHandle = attr.DefaultExtension ?? reader.GetDefaultExtension()
// cfg.GuidID             = deterministic MD5 GUID from reader type name
// cfg.NeedDrivers        = false

FileReaderRegistry & Plugin Readers

FileReaderRegistry bridges plugin assemblies (loaded by AssemblyHandler) into the factory. Call Discover() once at application startup after assemblies are loaded.

Discover — the startup call

// Scans editor.assemblyHandler.ConfigEditor.FileReaderClasses,
// instantiates each reader, and calls RegisterWithConfig so the format
// also surfaces as a connection type in the connection manager.
var registry = new FileReaderRegistry(dmeEditor);
registry.Discover();   // call once after assemblies are loaded

Other registry API

IFileFormatReader              fresh   = registry.Create(DataSourceType.CSV);
IReadOnlyList<FileReaderDescriptor> all = registry.GetAll();
bool                           hasCsv  = registry.Contains(DataSourceType.CSV);

// Static — scan ONE assembly without ConfigEditor access (used by RegisterDefaults)
FileReaderRegistry.RegisterAttributedReadersFromAssembly(assembly);
Discover() vs RegisterDefaults()
  • RegisterDefaults() — core assembly only, no ConfigEditor, called automatically by Openconnection() as a safety net.
  • Discover() — all plugin assemblies, calls RegisterWithConfig, adds formats to the connection manager.

Feature: Reader Switching

FileDataSource also implements IFileDataSourceReaderHost, which lets you inspect all registered readers and switch the active reader on a live connection — without recreating the data-source object.

IFileDataSourceReaderHost interface

public interface IFileDataSourceReaderHost
{
    // Enumerate
    IReadOnlyList<FileReaderDescriptorInfo> GetAvailableReaderDescriptors(bool discover = false);
    IReadOnlyList<DataSourceType>           GetAvailableReaderTypes();
    FileReaderDescriptorInfo?               GetCurrentReaderDescriptor();
    object?                                 GetCurrentReader();

    // Switch — all overloads return false + reason on failure
    bool TrySwitchReader(DataSourceType        targetType,            bool reconfigure, out string? reason);
    bool TrySwitchReader(string                implementationTypeName, bool reconfigure, out string? reason);
    bool TrySwitchReader(Guid                  readerId,              bool reconfigure, out string? reason);
    bool TrySwitchReaderByExtension(string     extension,             bool reconfigure, out string? reason);

    void ResetReaderSelection();
}

How reader selection works during Openconnection()

// Priority order:
//   1. Override set by TrySwitchReader*() — if one was called since last open
//   2. DatasourceType default from ConnectionProperties

// After a successful switch, DatasourceType is updated automatically.
// Switch is blocked while an active transaction / ingestion is in progress.

Switch by DataSourceType

var host = dmeEditor.GetDataSource("SalesData") as IFileDataSourceReaderHost;

if (host.TrySwitchReader(DataSourceType.TSV, reconfigure: true, out var reason))
    (host as FileDataSource).Openconnection();
else
    Console.WriteLine(reason);

Switch by implementation name (deterministic when multiple readers share a type)

host.TrySwitchReader(
    "DuckDBDataSourceCore.FileReaders.DuckDbCsvFileReader",
    reconfigure: true,
    out var reason);

Switch by ReaderId

var csvReaders = host.GetAvailableReaderDescriptors(discover: true)
    .Where(r => r.FormatType == DataSourceType.CSV)
    .ToList();

// e.g. present list to user, then switch to selected
host.TrySwitchReader(csvReaders[1].ReaderId, reconfigure: true, out _);
Multiple readers for the same format

GetAvailableReaderDescriptors does not collapse by FormatType. When two CSV readers are registered, TrySwitchReader(DataSourceType.CSV, ...) picks whichever is currently mapped. Use ImplementationTypeName or ReaderId for deterministic selection.

Create a Custom Reader

Implement IFileFormatReader, decorate with [FileReader], load the assembly, and run registry.Discover(). The format then appears in the connection manager like any built-in.

1) Implement & decorate

using TheTechIdea.Beep.FileManager.Attributes;
using TheTechIdea.Beep.FileManager.Readers;

[FileReader(DataSourceType.CSV, "My Fast CSV Reader", "csv",
    Description = "High-performance CSV parser",
    Version     = "1.0.0")]
public sealed class MyFastCsvReader : IFileFormatReader
{
    public DataSourceType SupportedType => DataSourceType.CSV;
    public bool HasHeader   { get; set; } = true;
    public ParseMode ParseMode { get; set; } = ParseMode.Lenient;
    public IReadOnlyList<RowDiagnostic> LastDiagnostics { get; } = new List<RowDiagnostic>();

    public string   GetDefaultExtension() => "csv";
    public void     Configure(IConnectionProperties props) { /* delimiter / encoding */ }
    public void     ClearDiagnostics() { }
    public string[] ReadHeaders(string filePath)  => new[] { "Id", "Name" };
    public EntityStructure GetEntityStructure(string filePath) => null;
    public IEnumerable<string[]> ReadRows(string filePath) { yield break; }
    public string InferFieldType(string current, string rawValue) => typeof(string).FullName;
    public bool CreateFile(string fp, IReadOnlyList<string> h)  => true;
    public bool AppendRow (string fp, IReadOnlyList<string> h, IReadOnlyList<string> v) => true;
    public bool RewriteFile(string fp, IReadOnlyList<string> h, IEnumerable<IReadOnlyList<string>> r) => true;
}

2) Register at startup

// Option A — manual (no assembly scanning needed)
FileReaderFactory.RegisterWithConfig(new MyFastCsvReader(), dmeEditor.ConfigEditor);

// Option B — automatic via registry (preferred for plugin assemblies)
var registry = new FileReaderRegistry(dmeEditor);
registry.Discover();   // picks up any [FileReader]-decorated class in loaded assemblies

3) Use it — switch to the new reader on an existing connection

var host = dmeEditor.GetDataSource("SalesData") as IFileDataSourceReaderHost;

var descriptor = host.GetAvailableReaderDescriptors(discover: false)
    .First(r => r.ImplementationTypeName == typeof(MyFastCsvReader).FullName);

host.TrySwitchReader(descriptor.ReaderId, reconfigure: true, out var reason);
(host as FileDataSource).Openconnection();

Enterprise Contracts

Data Classification & Masking (Classification/)

public interface IDataClassificationEngine
{
    Task<IReadOnlyList<ClassificationHit>> ClassifyColumnAsync(
        string columnName, IEnumerable<string> sampleValues,
        int sampleSize = 500, CancellationToken ct = default);
}

public sealed record ClassificationHit(
    string PatternKey, string Category,
    double Confidence, int MatchedSamples, int TotalSamples);

public enum MaskingStrategy
    { Redact, PartialMask, Tokenize, FormatPreservingEncrypt, Synthesize, Generalize, None }

public sealed class ColumnMaskingPolicy
{
    public string PatternKey { get; init; }
    public MaskingStrategy Strategy { get; init; }
    public int PartialMaskRevealChars { get; init; } = 4;
    public string TokenizationKeyId { get; init; }
    public double GeneralizeRoundTo { get; init; }
}

public interface IDataMaskingEngine
    { string Mask(string value, string patternKey, ColumnMaskingPolicy policy); }

public interface IMaskingPolicyStore
    { ColumnMaskingPolicy GetPolicy(string key); void SetPolicy(string key, ColumnMaskingPolicy policy); }

Call ClassifyColumnAsync to detect PII patterns (SSN, credit cards, emails) in file columns, then apply ColumnMaskingPolicy to redact/mask/tokenize before export.

Governance (Governance/)

public interface IFileAccessPolicies
{
    bool CanRead(string fileName, string user, string role);
    bool CanWrite(string fileName, string user, string role);
}

public interface IFileAuditLogger
{
    Task LogReadAsync(string fileName, string user, int records);
    Task LogWriteAsync(string fileName, string user, int records);
}

Configure access policies per file and log all read/write operations for compliance.

Observability (Observability/)

public interface IFileReadTelemetry
{
    Task RecordReadAsync(string fileName, string readerId,
        long bytes, long records, TimeSpan elapsed);
    Task RecordErrorAsync(string fileName, string readerId,
        string error, Exception ex);
}

public class FileReadMetrics
{
    public long TotalBytesRead { get; set; }
    public long TotalRecordsRead { get; set; }
    public int TotalErrors { get; set; }
    public TimeSpan AccumulatedReadTime { get; set; }
}

Plus built-in per-row diagnostics via IFileFormatReader.LastDiagnostics and ParseMode (Lenient vs Strict).

Resilience (Resilience/)

public interface IFileReadRetryPolicy
{
    Task<T> ExecuteAsync<T>(Func<Task<T>> operation,
        string operationName, CancellationToken ct);
}
// Built-in: exponential backoff, 3 retries, jitter

Schema Inference (Schema/ & Contracts/)

public interface ISchemaInferenceStrategy
{
    EntityStructure InferSchema(Stream stream, string fileName);
}

public interface IIngestionStateStore
{
    Task SaveStateAsync(string fileKey, object state);
    Task<T> LoadStateAsync<T>(string fileKey);
}

public class InMemoryIngestionStateStore : IIngestionStateStore { }

public static class TypeInferenceHelper
{
    public static Type InferColumnType(IEnumerable<string> sampleValues,
        string columnName);
}

public static class FileChecksumHelper
{
    public static string ComputeHash(string filePath,
        HashAlgorithm algorithm = HashAlgorithm.SHA256);
    public static bool VerifyHash(string filePath, string expectedHash);
}

Full end-to-end with a plugin Parquet reader

// After assemblies are loaded
var registry = new FileReaderRegistry(dmeEditor);
registry.Discover();  // Parquet reader found, RegisterWithConfig called

// Format is now visible in the connection manager
var cfg = dmeEditor.ConfigEditor.DataDriversClasses
    .First(d => d.DatasourceType == DataSourceType.Parquet);
// cfg.classHandler == "FileDataSource"

dmeEditor.ConfigEditor.AddDataConnection(new ConnectionProperties
{
    ConnectionName = "warehouse",
    DriverName     = cfg.PackageName,
    FilePath       = @"C:\data\warehouse.parquet",
    DatabaseType   = DataSourceType.Parquet
});

var ds = dmeEditor.GetDataSource("warehouse");
ds.Openconnection();  // FileDataSource routes to ParquetFileReader

Examples

Open a CSV file and read rows

dmeEditor.ConfigEditor.AddDataConnection(new ConnectionProperties
{
    ConnectionName = "sales",
    FilePath       = @"C:\data",
    FileName       = "sales.csv",
    DatabaseType   = DataSourceType.CSV,
    DriverName     = "FileDataSource"
});

var fs = dmeEditor.GetDataSource("sales") as FileDataSource;
fs.Openconnection();

var entity = fs.GetEntityStructure("sales.csv", false);
var rows   = (IEnumerable<object>)fs.GetEntity("sales.csv", null);

fs.Closeconnection();

Inspect all registered readers

var host    = fs as IFileDataSourceReaderHost;
var readers = host.GetAvailableReaderDescriptors(discover: true);

foreach (var r in readers)
    Console.WriteLine($"{r.DisplayName} | {r.FormatType} | {r.DefaultExtension}");

Switch format without recreating the connection

// Currently CSV — switch to TSV reader
if (host.TrySwitchReaderByExtension("tsv", reconfigure: true, out var reason))
    fs.Openconnection();
else
    Console.WriteLine(reason);

Insert and update rows

fs.InsertEntity("sales.csv", new { OrderId = "ORD-001", Amount = 150.00 });
fs.UpdateEntities("sales.csv", fs.ErrorObject, new { OrderId = "ORD-001", Amount = 175.00 });
fs.DeleteEntity("sales.csv", new AppFilter { FieldName = "OrderId", Operator = "=", FilterValue = "ORD-001" });
fs.Closeconnection();