DataProvider
A .NET source generator that creates compile-time safe database extension methods from SQL queries. DataProvider eliminates runtime SQL errors by validating queries at compile time and generating strongly-typed C# code.
Features
- Compile-Time Safety - SQL queries are validated during compilation, catching errors before runtime
- Auto-Generated Extensions - Creates extension methods on
IDbConnectionandIDbTransaction - Schema Inspection - Automatically inspects database schema to generate appropriate types
- Result Type Pattern - All operations return
Result<T,E>types for explicit error handling - Multi-Database Support - Currently supports SQLite and SQL Server
- LQL Integration - Seamlessly works with Lambda Query Language files
How It Works
- Define SQL Queries - Place
.sqlor.lqlfiles in your project - Configure Generation - Set up
DataProvider.jsonconfiguration - Build Project - Source generators create extension methods during compilation
- Use Generated Code - Call type-safe methods with full IntelliSense support
Installation
SQLite
<PackageReference Include="DataProvider.SQLite" Version="*" />
SQL Server
<PackageReference Include="DataProvider.SqlServer" Version="*" />
Database Schema Setup (Migrations)
DataProvider requires a database with schema to exist before code generation runs. The schema allows the generator to introspect table structures and generate correct types.
Required Build Order
1. Export C# Schema to YAML (if schema defined in code)
2. Run Migration.Cli to create database from YAML
3. Run DataProvider code generation
Using Migration.Cli
Migration.Cli is the single canonical tool for creating databases from schema definitions. All projects that need a build-time database MUST use this tool.
dotnet run --project Migration/Migration.Cli/Migration.Cli.csproj -- \
--schema path/to/schema.yaml \
--output path/to/database.db \
--provider sqlite
MSBuild Integration
Configure your .csproj to run migrations before code generation:
<!-- Create database with schema before code generation -->
<Target Name="CreateDatabaseSchema" BeforeTargets="GenerateDataProvider">
<Exec Command='dotnet run --project "$(SolutionDir)Migration/Migration.Cli/Migration.Cli.csproj" -- --schema "$(MSBuildProjectDirectory)/schema.yaml" --output "$(MSBuildProjectDirectory)/build.db" --provider sqlite' />
</Target>
<!-- Generate C# from SQL using DataProvider.SQLite.Cli -->
<Target Name="GenerateDataProvider" BeforeTargets="BeforeCompile;CoreCompile">
<Exec Command='dotnet run --project "$(SolutionDir)DataProvider/DataProvider.SQLite.Cli/DataProvider.SQLite.Cli.csproj" -- --project-dir "$(MSBuildProjectDirectory)" --config "$(MSBuildProjectDirectory)/DataProvider.json" --out "$(MSBuildProjectDirectory)/Generated"' />
</Target>
YAML Schema Format
See this
Exporting C# Schemas to YAML
If your schema is defined in C# code using the Migration fluent API:
var schema = Schema.Define("my_schema")
.Table("Customer", t => t
.Column("Id", Text, c => c.PrimaryKey())
.Column("Name", Text, c => c.NotNull())
)
.Build();
// Export to YAML
SchemaYamlSerializer.ToYamlFile(schema, "schema.yaml");
Avoiding Circular Dependencies
CRITICAL: Projects that use DataProvider code generation MUST NOT have circular dependencies with the CLI tools.
The Problem: If your project references DataProvider.csproj AND runs DataProvider.SQLite.Cli as a build target, you create an infinite build loop:
YourProject → DataProvider → (build target) DataProvider.SQLite.Cli → DataProvider → ...
How to Fix:
- Remove the
ProjectReferenceto DataProvider.csproj from projects that run the CLI as a build target - Use raw YAML schemas checked into git - do NOT export C# schemas to YAML at build time
- Migration.Cli is safe - it does NOT depend on DataProvider, only on Migration projects
The Rule: YAML schema files are source of truth. Check them into git. Never generate them at build time. The C# → YAML export is a one-time developer action, not a build step.
Correct pattern:
<!-- SAFE: Migration.Cli has no DataProvider dependency -->
<Target Name="CreateDatabaseSchema" BeforeTargets="GenerateDataProvider">
<Exec Command='dotnet run --project Migration.Cli -- --schema schema.yaml --output build.db' />
</Target>
<!-- SAFE: DataProvider.SQLite.Cli runs AFTER schema exists, no circular ref -->
<Target Name="GenerateDataProvider" BeforeTargets="BeforeCompile">
<Exec Command='dotnet run --project DataProvider.SQLite.Cli -- ...' />
</Target>
Wrong pattern:
<!-- BROKEN: Project references DataProvider AND runs CLI that depends on DataProvider -->
<ProjectReference Include="DataProvider.csproj" /> <!-- REMOVE THIS -->
Forbidden Patterns
- NO raw SQL DDL files - Use Migration.Cli with YAML
- NO individual BuildDb projects - Use Migration.Cli (single tool)
- NO
schema.sqlfiles - YAML schemas only - NO code generation before schema creation - Migration MUST run first
- NO C# schema export at build time - Export once, commit YAML to git
Configuration
Create a DataProvider.json file in your project root:
{
"ConnectionString": "Data Source=mydatabase.db",
"Namespace": "MyApp.DataAccess",
"OutputDirectory": "Generated",
"Queries": [
{
"Name": "GetCustomers",
"SqlFile": "Queries/GetCustomers.sql"
},
{
"Name": "GetOrders",
"SqlFile": "Queries/GetOrders.lql"
}
]
}
Usage Examples
Simple Query
SQL file (GetCustomers.sql):
SELECT Id, Name, Email
FROM Customers
WHERE IsActive = @isActive
Generated C# usage:
using var connection = new SqliteConnection(connectionString);
var result = await connection.GetCustomersAsync(isActive: true);
if (result.IsSuccess)
{
foreach (var customer in result.Value)
{
Console.WriteLine($"{customer.Name}: {customer.Email}");
}
}
else
{
Console.WriteLine($"Error: {result.Error.Message}");
}
With LQL
LQL file (GetOrders.lql):
Order
|> join(Customer, on = Order.CustomerId = Customer.Id)
|> filter(fn(row) => row.Order.OrderDate >= @startDate)
|> select(Order.Id, Order.Total, Customer.Name)
This automatically generates:
var orders = await connection.GetOrdersAsync(
startDate: DateTime.Now.AddDays(-30)
);
Transaction Support
using var connection = new SqliteConnection(connectionString);
connection.Open();
using var transaction = connection.BeginTransaction();
var insertResult = await transaction.InsertCustomerAsync(
name: "John Doe",
email: "john@example.com"
);
if (insertResult.IsSuccess)
{
transaction.Commit();
}
else
{
transaction.Rollback();
}
Grouping Configuration
For complex result sets with joins, configure grouping in a .grouping.json file:
{
"PrimaryKey": "Id",
"GroupBy": ["Id"],
"Collections": {
"Addresses": {
"ForeignKey": "CustomerId",
"Properties": ["Street", "City", "State"]
}
}
}
Architecture
DataProvider follows functional programming principles:
- No Classes - Uses records and static extension methods
- No Exceptions - Returns
Result<T,E>types for all operations - Pure Functions - Static methods with no side effects
- Expression-Based - Prefers expressions over statements
Project Structure
DataProvider/
├── DataProvider/ # Core library and base types
├── DataProvider.SQLite/ # SQLite implementation
│ ├── Parsing/ # ANTLR grammar and parsers
│ └── SchemaInspection/ # Schema discovery
├── DataProvider.SqlServer/ # SQL Server implementation
│ └── SchemaInspection/
├── DataProvider.Example/ # Example usage
└── DataProvider.Tests/ # Unit tests
Testing
Run tests with:
dotnet test DataProvider.Tests/DataProvider.Tests.csproj
Performance
- Zero Runtime Overhead - All SQL parsing and validation happens at compile time
- Minimal Allocations - Uses value types and expressions where possible
- Async/Await - Full async support for all database operations
Logging
Generated methods support optional ILogger injection for observability. Pass an ILogger instance to any generated method:
var result = await connection.GetCustomersAsync(isActive: true, logger: _logger);
Logging includes query timing, parameter values (debug level), row counts, and structured error context. Zero overhead when logger is null.
Error Handling
All methods return Result<T,E> types:
var result = await connection.ExecuteQueryAsync();
var output = result switch
{
{ IsSuccess: true } => ProcessData(result.Value),
{ Error: SqlError error } => HandleError(error),
_ => "Unknown error"
};
Contributing
- Follow the functional programming style (no classes, no exceptions)
- Keep files under 450 lines
- All public members must have XML documentation
- Run
dotnet csharpier .before committing - Ensure all tests pass
License
MIT License
Author
MelbourneDeveloper - ChristianFindlay.com