Modern Roslyn source generator that turns .sql files into strongly-typed, DBMS-aware C# constants — with full IntelliSense and automatic generation on save.
Writing SQL as string literals inside C# is a painful experience:
- No Syntax Highlighting: SQL strings are just plain text to your editor.
- No Linting/Validation: SQL errors are only caught at runtime.
- Hard to Test: You can't easily run a string literal against a database without copying it.
- Messy Code: Large SQL queries clutter your C# logic.
SqlPartial solves this by letting you keep your SQL in dedicated .sql files. You get full editor support (highlighting, formatting, schema validation) while the generator seamlessly bridges them into your C# code as strongly-typed constants.
Each .sql file becomes a static readonly SqlStrings property prefixed with Sql on a partial class. At runtime, call Get("PostgreSql") to receive the provider-specific SQL, falling back to the fallback SQL automatically.
UserRepo.GetActive.ms.sql → SQL Server version
UserRepo.GetActive.pg.sql → PostgreSQL version
UserRepo.GetActive.sql → (Optional) Shared fallback
Generated output:
partial class UserRepo
{
private static readonly SqlStrings SqlGetActive = new SqlStrings(
// From UserRepo.GetActive.sql
fallback: @"SELECT * FROM Users WHERE IsActive = 1",
postgresql: @"SELECT * FROM Users WHERE IsActive = true",
sqlserver: @"SELECT * FROM Users WHERE IsActive = 1"
);
}Runtime usage:
// providerName comes from appsettings, e.g. "PostgreSql"
string sql = UserRepo.SqlGetActive.Get(providerName);
// For single-DBMS projects, use implicit conversion (returns fallback)
string sqlSimple = UserRepo.SqlGetActive;dotnet add package NkChinh.SqlPartial.GeneratorIf you use Gemini CLI or a similar agent-based environment, you can install the specialized skill to help you manage SQL files:
npx skills add nkchinh/sql-partial --skill sql-partialAdd to your .csproj. A fallback is always available — only declare additional providers for multi-DBMS support.
SqlPartial is DBMS-agnostic. You can define any provider by choosing an extension (matched at the end of the filename) and a Display Name (used in C# code and Get() calls). Multiple extensions can map to the same DBMS.
<PropertyGroup>
<!-- extension:DisplayName pairs, semicolon or comma separated -->
<SqlPartialProviders>.pg.sql:PostgreSql,.pgsql:PostgreSql,.ms.sql:SqlServer</SqlPartialProviders>
</PropertyGroup>| Extension | Display Name (C#) | DBMS Reference |
|---|---|---|
.pg.sql |
PostgreSql |
PostgreSQL |
.pgsql |
PostgreSql |
PostgreSQL |
.ms.sql |
SqlServer |
SQL Server |
.my.sql |
MySql |
MySQL |
.lt.sql |
Sqlite |
SQLite |
.ora.sql |
Oracle |
Oracle |
| ... | ... | Any other |
<ItemGroup>
<!-- Include all extensions you configured -->
<AdditionalFiles Include="**/*.sql;**/*.*.pgsql" Exclude="obj/**/*;bin/**/*">
<SourceItemType>SqlPartial</SourceItemType>
</AdditionalFiles>
</ItemGroup>The generator produces a partial class — you must declare the other half yourself:
namespace MyApp.Data
{
public partial class UserRepo { }
}The namespace is derived automatically from $(RootNamespace) + the relative directory of the .sql file.
ClassName.QueryName.sql Shared fallback (ANSI SQL recommended)
ClassName.QueryName.an.sql Same as above — explicit fallback variant
ClassName.QueryName.pg.sql PostgreSQL-specific
ClassName.QueryName.pgsql Also PostgreSQL (if configured)
ClassName.QueryName.ms.sql SQL Server-specific
- **ClassName** — must match the `partial class` name exactly.
- **QueryName** — becomes the property name on the class (prefixed with `Sql`).
- **Extension** — must match an extension declared in `SqlPartialProviders`, or use `.sql`/`.an.sql` for the fallback.
---
## Authoring Tips & Patterns
### 1. Parameter Documentation with Exclusion Blocks
Use `--#exclude` to provide test data and document parameter meanings. This block is stripped from C# but remains in your SQL file for IDE use.
**File: `ProductRepo.GetById.sql`** (Shared fallback SQL)
```sql
--#exclude
-- Parameters for local testing & documentation
DECLARE @Id INT = 1;
--/exclude
SELECT p.Id, p.Name, p.Price
FROM Products p
WHERE p.Id = @Id
### 2. Handling Multi-DBMS Transitions
If your original query was written for a specific DBMS (e.g., SQL Server), **rename it** when adding support for a second one. This prevents your "generic fallback" from containing incompatible syntax.
**File: `ProductRepo.Search.ms.sql`** (SQL Server version)
```sql
--#exclude
DECLARE @SearchText NVARCHAR(100) = 'Laptop';
--/exclude
SELECT p.Id, p.Name FROM Products p
WHERE p.Name LIKE '%' + @SearchText + '%'
ORDER BY p.CreatedDate DESC
OFFSET 0 ROWS FETCH NEXT 10 ROWS ONLY
File: ProductRepo.Search.pg.sql (PostgreSQL version)
--#exclude
DECLARE SearchText TEXT := 'Laptop';
--/exclude
SELECT p.Id, p.Name FROM Products p
WHERE p.Name ILIKE '%' || :SearchText || '%'
ORDER BY p.CreatedDate DESC
LIMIT 10Rule of thumb: The base
.sqlfile should only contain code that works across all your configured providers. If the syntax differs, use explicit extensions.
Lines beginning with -- are removed from the generated constant, so you can annotate freely:
-- Returns a single user by primary key
SELECT id, name, email FROM users WHERE id = @idBy default the SqlStrings struct is generated in each project. If you want to share the same struct:
Core project — normal setup, struct is generated here.
Consumer project — add this property to skip struct generation:
<PropertyGroup>
<SqlPartialStringsType>MyCompany.Core.SqlStrings</SqlPartialStringsType>
</PropertyGroup>By default the struct is placed in $(RootNamespace). To override:
<PropertyGroup>
<SqlPartialStringsNamespace>MyCompany.Data.Sql</SqlPartialStringsNamespace>
</PropertyGroup>| Property | Required | Default | Description |
|---|---|---|---|
SqlPartialProviders |
No | (none) | Semicolon-separated extension:DisplayName pairs |
SqlPartialStringsNamespace |
No | $(RootNamespace) |
Namespace for the generated SqlStrings struct |
SqlPartialStringsType |
No | (none) | Fully-qualified type to use instead of generating SqlStrings |
SqlPartialWarnOnUnrecognized |
No | false |
If true, emits SQLPG020 for unknown extensions |
| Code | Severity | Category | Meaning |
|---|---|---|---|
SQLPG001 |
Error | Config | Invalid SqlPartialProviders syntax. Format must be ext:Name. |
SQLPG002 |
Error | Tooling | Internal failure generating SqlStrings struct. |
SQLPG003 |
Error | Tooling | Internal failure generating partial class file. |
SQLPG010 |
Warning | Logic | Missing Fallback SQL & incomplete DBMS coverage. |
SQLPG011 |
Warning | Quality | SQL file is empty after cleaning comments/excludes. |
SQLPG012 |
Warning | Logic | Missing Fallback SQL in manual instantiation (new SqlStrings). |
SQLPG020 |
Warning | Usage | Unrecognized extension (Disabled by default). |
SqlPartial supports three main ways to author and consume SQL, all unified under the ISqlString interface.
Best for large, complex queries. You get full IDE support (syntax highlighting, formatting).
- Setup: Create
ClassName.QueryName.sql(or.pg.sql,.ms.sql, etc.) - Consumption: Use the generated static properties.
await QueryAsync(SqlGetActiveUsers);Best for simple one-liners where a separate file would be overkill.
- Consumption: Pass strings directly (via implicit conversion) or use the
SqlStringsconstructor for multiple providers.
// Simple fallback-only
await QueryAsync("SELECT * FROM Version");
// Multi-DBMS without a file (PostgreSQL, SQL Server, and SQLite)
await QueryAsync(new SqlStrings(
postgresql: "SELECT name FROM users LIMIT 10",
sqlserver: "SELECT TOP 10 name FROM users",
fallback: "SELECT name FROM users" // Fallback for SQLite and others
));Best for SQL that needs runtime calculations (table partitioning, dynamic filtering).
- Consumption: Use
SqlDynamicwith factories (Func<string>).
var dynamicQuery = new SqlDynamic(
postgresql: () => $"SELECT * FROM sales_{DateTime.Now:yyyy}",
sqlserver: () => $"SELECT * FROM sales_{DateTime.Now:yyyy}",
fallback: () => "SELECT * FROM sales"
);
await QueryAsync(dynamicQuery);To get the most out of SqlPartial, use ISqlString as a generic constraint in your data access layer. This allows your methods to accept SQL from any source (Auto or Manual) with zero allocation overhead for static cases.
public async Task<T> QueryAsync<TSql>(TSql sql) where TSql : struct, ISqlString
{
// 'dbProvider' is resolved at runtime from your configuration (e.g., "PostgreSql", "SqlServer")
string rawSql = sql.Get(dbProvider);
return await connection.QueryAsync<T>(rawSql);
}This project is licensed under the MIT License.