LinKit.Data
1.1.0
See the version list below for details.
dotnet add package LinKit.Data --version 1.1.0
NuGet\Install-Package LinKit.Data -Version 1.1.0
<PackageReference Include="LinKit.Data" Version="1.1.0" />
<PackageVersion Include="LinKit.Data" Version="1.1.0" />
<PackageReference Include="LinKit.Data" />
paket add LinKit.Data --version 1.1.0
#r "nuget: LinKit.Data, 1.1.0"
#:package LinKit.Data@1.1.0
#addin nuget:?package=LinKit.Data&version=1.1.0
#tool nuget:?package=LinKit.Data&version=1.1.0
LinKit.Data
LinKit.Data is a source-generated ADO.NET data access and schema synchronization library. Generated repository and reader-mapping paths are designed to reduce runtime discovery. LINQ closure extraction and other specific paths may still use runtime reflection/dynamic code; the repository AOT smoke app covers only a selected feature set.
💡 The Philosophy: Developer Does Only 2 Things
- Declare Domain Model / Entity: Add, remove, or modify properties and configuration attributes directly on C# classes.
- Execute Business Logic in CQRS Handlers: Inject source-generated
IRepository<TEntity, TId>and perform CRUD, raw SQL execution, or fluent queries with blazing-fast compiled ADO.NET speed.
LinKit does not require EF Core. It provides additive schema synchronization and versioned migrations for changes that need explicit data/schema operations.
🚀 Key Features
- Additive schema synchronization: At application startup, LinKit can create missing tables and add supported missing columns/indexes for SQLite, PostgreSQL, SQL Server, MySQL, and Oracle. It is not a general migration engine and does not safely infer renames, drops, or arbitrary existing-column changes.
- EF Core Equivalent Attributes: Configure lengths, precisions, scales, foreign keys, check constraints, comments, unicode, json columns, and optimistic concurrency.
- Compile-Time Code Generation: Repository implementations, DataReader mapping, and standard SQL operations are generated at compile time. Custom LINQ closure values are currently extracted from expression trees using runtime member access; this path is exercised under Native AOT but is not reflection-free.
- Native AOT JSON Serialization (
[JsonColumn]):- Compile-time Specific Context:
[JsonColumn(typeof(AppJsonContext))]can bind toAppJsonContext.Defaultfor the configured type. - Ambient Global Context:
options.UseJsonSerializerContext(AppJsonContext.Default)across repositories.
- Compile-time Specific Context:
- Explicit query parameter collections:
- Generated
QuerySqlAsync/ExecuteSqlAsyncpaths supportIEnumerable<KeyValuePair<string, object?>>,IEnumerable<(string Name, object? Value)>, andIEnumerable<DbParameter>without reflecting over anonymous parameter objects. - Anonymous parameter objects are not supported by generated raw-SQL repositories. Use one of the explicit parameter collection shapes above.
- Generated
- Full CLR & Database Type Coverage: Built-in code generation for
int,long,short,byte,sbyte,uint,ulong,ushort,bool,char,decimal,double,float,Guid,DateTime,DateTimeOffset,DateOnly,TimeOnly,TimeSpan,byte[],Enum,JsonColumn, and their Nullable equivalents. - Comprehensive Repository Abstractions:
- Standard CRUD:
GetByIdAsync,ListAsync,InsertAsync,UpdateAsync,DeleteAsync - Bulk Operations:
InsertRangeAsync,UpdateRangeAsync,DeleteRangeAsync - Pagination:
GetPagedAsync(pageNumber, pageSize)returningPagedResult<T> - Arbitrary SQL Execution:
QuerySqlAsync,QuerySingleSqlAsync,ExecuteSqlAsync,ExecuteScalarSqlAsync - Fluent Query Builder:
repo.Query().Where(...).OrderBy(...).Skip(...).Take(...).ToPagedListAsync(...)
- Standard CRUD:
- Unit of Work & Ambient Transactions: Repositories using the same scoped UoW can enlist commands in its current transaction.
Raw SQL and string-based fluent conditions are intentionally passed through to the database. Always bind untrusted values as parameters; never concatenate user input into SQL text. Prefer the generated LINQ query API where it supports the required expression.
The built-in soft-delete filter applies to generated repository and fluent/LINQ queries. Raw SQL and custom [SqlQuery] repository methods are application-controlled and must include the appropriate soft-delete predicate themselves. When combining multiple fluent .Where(...) calls, use unique parameter names across all calls (for example, @categoryId and @supplierId); the query builder does not rewrite SQL parameter names.
🏷️ Comprehensive Configuration Attributes
| Attribute | Scope | Description |
|---|---|---|
[Table("name", Schema = "...", Comment = "...")] |
Class | Maps entity to database table with optional schema and documentation comment. |
[PrimaryKey(AutoIncrement = true)] |
Property | Designates primary key with optional identity/auto-increment. |
[Column("col_name", CustomSqlType = "...")] |
Property | Overrides column name or explicit raw SQL type definition. |
[MaxLength(200)] |
Property | Defines max length for VARCHAR(n) / NVARCHAR(n) / VARBINARY(n). |
[FixedLength(10)] |
Property | Configures fixed character columns: CHAR(n) / NCHAR(n). |
[DecimalPrecision(18, 2)] |
Property | Configures precision and scale: DECIMAL(18, 2) / NUMERIC(18, 2). |
[DefaultValueSql("0")] |
Property | Sets raw SQL default value expression during column creation. |
[ForeignKey("other_table", TargetColumn = "Id", OnDelete = OnDeleteAction.Cascade)] |
Property | Configures foreign key constraint with referential delete actions. |
[CheckConstraint("Price >= 0", Name = "chk_price")] |
Class / Property | Enforces SQL check constraint in database DDL. |
[Comment("...")] |
Class / Property | Attaches documentation comment in database metadata. |
[Unicode(false)] |
Property | Sets string storage to ASCII/ANSI VARCHAR vs NVARCHAR. |
[JsonColumn] |
Property | Automatically serializes complex objects/lists to/from JSON/JSONB. |
[ConcurrencyCheck] |
Property | Marks column for optimistic concurrency tracking. |
[IgnoreColumn] |
Property | Excludes property from database mapping. |
[Indexed(IsUnique = true, IndexName = "...")] |
Property | Automatically builds single-column index on startup. |
[CompositeIndex("IX_Name_Age", nameof(Name), nameof(Age), IsUnique = true)] |
Class | Defines a multi-column index. |
[SoftDelete("DeletedAt")] |
Class | Enables soft-delete behavior using the specified timestamp column. Map that column as a nullable entity property or create it in a migration; the generator warns if it is missing. |
[MultiTenant("TenantId")] |
Class | Records tenant-column metadata only. It does not enforce tenant isolation; the generator emits a warning. Add explicit tenant predicates to every query. |
[DataRepository] |
Interface | Directs source generator to implement custom repository interfaces. |
[SqlQuery("SELECT ...")] |
Method | Declarative custom query method generation. |
📦 Installation
dotnet add package LinKit.Data
Add your database ADO.NET driver:
# For SQLite
dotnet add package Microsoft.Data.Sqlite
# For PostgreSQL
dotnet add package Npgsql
# For SQL Server
dotnet add package Microsoft.Data.SqlClient
# For MySQL
dotnet add package MySqlConnector
# For Oracle
dotnet add package Oracle.ManagedDataAccess.Core
🛠️ Usage Example
1. Declare Domain Entity
using LinKit.Data;
public enum ProductStatus { Draft = 0, Active = 1, Archived = 2 }
[Table("catalog_products", Comment = "Catalog inventory table")]
[CheckConstraint("Price >= 0", Name = "chk_product_price_positive")]
public class ProductEntity
{
[PrimaryKey]
public Guid Id { get; set; }
[Indexed]
[MaxLength(200)]
public string Name { get; set; } = string.Empty;
[DecimalPrecision(18, 2)]
public decimal Price { get; set; }
[DefaultValueSql("0")]
public int Stock { get; set; } = 0;
[MaxLength(50)]
public string? Sku { get; set; }
public bool IsFeatured { get; set; } = false;
public ProductStatus Status { get; set; } = ProductStatus.Active;
public DateOnly? ReleasedDate { get; set; }
public TimeOnly? DailyRestockTime { get; set; }
[JsonColumn]
public List<string>? Tags { get; set; }
public DateTimeOffset CreatedAt { get; set; } = DateTimeOffset.UtcNow;
}
2. Configure in Program.cs
using LinKit.Data;
using Microsoft.Data.Sqlite;
var builder = WebApplication.CreateBuilder(args);
// Register LinKit.Data with automatic schema synchronization
builder.Services.AddLinKitData(options =>
{
// SQLite:
options.UseConnection(DatabaseDialect.Sqlite, () => new SqliteConnection("Data Source=app.db"));
// Or PostgreSQL:
// options.UseConnection(DatabaseDialect.PostgreSql, () => new NpgsqlConnection(connStr));
// Or SQL Server:
// options.UseConnection(DatabaseDialect.SqlServer, () => new SqlConnection(connStr));
// Or MySQL:
// options.UseConnection(DatabaseDialect.MySql, () => new MySqlConnection(connStr));
// Or Oracle:
// options.UseConnection(DatabaseDialect.Oracle, () => new OracleConnection(connStr));
options.AutoSyncSchema = true; // Creates missing schema objects and adds supported missing columns.
});
3. Inject Repository in CQRS Handlers
Option A: LINQ Query Comprehension (from ... in ... select)
using LinKit.Core.Cqrs;
using LinKit.Data;
[CqrsHandler]
public class GetFeaturedProductsHandler(IRepository<ProductEntity, Guid> repository)
: IQueryHandler<GetFeaturedProductsQuery, List<ProductEntity>>
{
public async Task<List<ProductEntity>> HandleAsync(GetFeaturedProductsQuery query, CancellationToken ct)
{
// Supports only the LINQ expression subset implemented by LinKit.Data.
var q = from p in repository.AsQueryable()
where p.Price >= query.MinPrice && p.IsFeatured
orderby p.Price descending
select p;
return await q.ToListAsync(ct);
}
}
Option B: Fluent Query Builder
using LinKit.Core.Cqrs;
using LinKit.Data;
[CqrsHandler]
public class GetProductsPagedHandler(IRepository<ProductEntity, Guid> repository)
: IQueryHandler<GetProductsPagedQuery, PagedResult<ProductEntity>>
{
public async Task<PagedResult<ProductEntity>> HandleAsync(GetProductsPagedQuery query, CancellationToken ct)
{
return await repository.Query()
.Where("Price >= @minPrice", new[] { (Name: "minPrice", Value: (object?)query.MinPrice) })
.OrderBy("Price")
.ToPagedListAsync(query.Page, query.PageSize, ct);
}
}
4. Versioned Data & Schema Migrations ([DataMigration])
For master data seeding, backfills, or intentional schema alterations (rename column, drop column):
using LinKit.Data;
[DataMigration("20261021_01", Description = "Seed initial catalog products and categories")]
public class SeedCatalogInitialProductsMigration : IDataMigration
{
public async Task UpAsync(IMigrationContext context, CancellationToken ct = default)
{
// 1. Safe DDL operations across all 5 databases (if needed)
// await context.RenameColumnAsync("catalog_products", "OldSku", "Sku", ct);
// await context.DropColumnAsync("catalog_products", "ObsoleteColumn", ct);
// 2. Check existence & Seed initial data
if (!await context.ExistsAsync(
"catalog_products",
"\"Sku\" = @sku",
new Dictionary<string, object?> { ["sku"] = "SEED-001" },
ct))
{
var repo = context.GetRepository<ProductEntity, Guid>();
await repo.InsertAsync(new ProductEntity
{
Id = Guid.NewGuid(),
Name = "Master Seed Product",
Price = 149.99m,
Sku = "SEED-001",
Status = ProductStatus.Active,
CreatedAt = DateTimeOffset.UtcNow
}, ct);
}
}
}
- Compile-time Discovery: Migration registrations are generated by
LinKit.Data.Generator; the complete migration and provider path still requires application-level AOT validation. - Idempotent Execution: History tracked in
__linkit_migrationstable and executed once in exact version order inside a transaction.
📄 License
Licensed under the MIT License.
| Product | Versions Compatible and additional computed target framework versions. |
|---|---|
| .NET | net8.0 is compatible. net8.0-android was computed. net8.0-browser was computed. net8.0-ios was computed. net8.0-maccatalyst was computed. net8.0-macos was computed. net8.0-tvos was computed. net8.0-windows was computed. net9.0 is compatible. net9.0-android was computed. net9.0-browser was computed. net9.0-ios was computed. net9.0-maccatalyst was computed. net9.0-macos was computed. net9.0-tvos was computed. net9.0-windows was computed. net10.0 is compatible. net10.0-android was computed. net10.0-browser was computed. net10.0-ios was computed. net10.0-maccatalyst was computed. net10.0-macos was computed. net10.0-tvos was computed. net10.0-windows was computed. |
-
net10.0
- LinKit.Core (>= 2.5.0)
- Microsoft.Extensions.DependencyInjection.Abstractions (>= 8.0.0)
- Microsoft.Extensions.Hosting.Abstractions (>= 8.0.0)
- Microsoft.Extensions.Logging.Abstractions (>= 8.0.0)
-
net8.0
- LinKit.Core (>= 2.5.0)
- Microsoft.Extensions.DependencyInjection.Abstractions (>= 8.0.0)
- Microsoft.Extensions.Hosting.Abstractions (>= 8.0.0)
- Microsoft.Extensions.Logging.Abstractions (>= 8.0.0)
-
net9.0
- LinKit.Core (>= 2.5.0)
- Microsoft.Extensions.DependencyInjection.Abstractions (>= 8.0.0)
- Microsoft.Extensions.Hosting.Abstractions (>= 8.0.0)
- Microsoft.Extensions.Logging.Abstractions (>= 8.0.0)
NuGet packages (1)
Showing the top 1 NuGet packages that depend on LinKit.Data:
| Package | Downloads |
|---|---|
|
LinKit.Outbox.Data
Provider-neutral ADO.NET durable stores for LinKit Outbox and Inbox. |
GitHub repositories
This package is not used by any popular GitHub repositories.