Prakrishta.Data.Bulk is a high‑performance bulk operations library for SQL Server, built for .NET 8. It provides blazing‑fast bulk insert, update, delete, and upsert operations using optimized strategies such as TVP‑based stored procedures, staging tables with MERGE, and raw SqlBulkCopy. Designed for ETL pipelines, data warehousing, and high‑volume ingestion workloads, Prakrishta.Data.Bulk consistently outperforms EFCore.BulkExtensions and approaches the performance of commercial libraries like Z.BulkOperations and Dapper Plus — without reflection, without EF Core overhead, and with predictable linear scaling.
$ dotnet add package Prakrishta.Data.BulkHigh performance, extensible bulk operations for .NET. Prakrishta.Data.Bulk is a provider agnostic, pipeline based bulk engine designed for speed, flexibility, and testability. It complements Prakrishta.Data by enabling large scale insert, update, and delete operations with minimal overhead.
| Feature | Prakrishta (Stored Proc) | Prakrishta (Staging) | EFCore.BulkExtensions | Raw SqlBulkCopy |
|---|---|---|---|---|
| Bulk Insert | ⭐ Fastest | ⭐ Very Fast | Fast | Fast |
| Bulk Update | ❌ | ⭐ Yes (MERGE) | Yes | ❌ |
| Bulk Delete | ❌ | ⭐ Yes (MERGE) | Yes | ❌ |
| Upsert | ❌ | ⭐ Yes (MERGE) | Yes | ❌ |
| TVP Support | ⭐ Yes | Yes | Yes | No |
| Reflection‑Free | ⭐ Yes | ⭐ Yes | ❌ No | Yes |
| EF Core Required | No | No | Yes | No |
dotnet add package Prakrishta.Data.Bulk
public sealed class SalesRecord
{
public int Id { get; set; }
public DateTime SaleDate { get; set; }
public decimal Amount { get; set; }
}
var builder = WebApplication.CreateBuilder(args);
// Register Bulk Engine
builder.Services.AddBulkEngine(opts =>
{
opts.DefaultStrategy = BulkStrategyKind.StoredProcedureTvp;
});
var app = builder.Build();
// Resolve engine
var bulk = app.Services.GetRequiredService<BulkEngine>();
// Sample data
var items = new List<SalesRecord>
{
new() { Id = 1, SaleDate = DateTime.UtcNow, Amount = 100 },
new() { Id = 2, SaleDate = DateTime.UtcNow, Amount = 200 }
};
// Insert
await bulk.InsertAsync(
items,
"dbo.Sales",
"dbo.SalesType",
"dbo.Sales_Insert");
// Partition Switch
await bulk.ReplacePartitionAsync(
items,
"dbo.FactSales",
opts => opts
.UseStagingTable("dbo.FactSales_Staging_7")
.ForPartition(7));
app.Run();
public static class ServiceCollectionExtensions
{
public static IServiceCollection AddBulkEngine(
this IServiceCollection services,
string connectionString,
Action<BulkOptions>? configure = null)
{
var options = new BulkOptions();
configure?.Invoke(options);
services.AddSingleton(options);
// Factories
services.AddSingleton<IBulkCopyFactory, SqlBulkCopyFactory>();
services.AddSingleton<IDbConnectionFactory, SqlConnectionFactory>();
// Register schema resolver
services.AddSingleton<ISchemaResolver>(sp =>
{
var factory = sp.GetRequiredService<IDbConnectionFactory>();
return new SchemaResolver(factory, connectionString);
});
// Strategy selector
services.AddSingleton<BulkStrategySelector>();
// Strategies
services.AddSingleton<IBulkStrategy>(sp =>
new StoredProcedureTvpStrategy(
sp.GetRequiredService<IDbConnectionFactory>()));
services.AddSingleton<IBulkStrategy>(sp =>
new StagingTableStrategy(
sp.GetRequiredService<BulkOptions>(),
sp.GetRequiredService<IBulkCopyFactory>(),
sp.GetRequiredService<IDbConnectionFactory>()));
services.AddSingleton<IBulkStrategy>(sp =>
new TruncateAndReloadStrategy(
sp.GetRequiredService<BulkOptions>(),
sp.GetRequiredService<IBulkCopyFactory>(),
sp.GetRequiredService<IDbConnectionFactory>()));
services.AddSingleton<IBulkStrategy>(sp =>
new PartitionSwitchStrategy(
sp.GetRequiredService<IBulkCopyFactory>(),
sp.GetRequiredService<IDbConnectionFactory>()));
// Strategy dictionary
services.AddSingleton<IDictionary<BulkStrategyKind, IBulkStrategy>>(sp =>
{
var strategies = sp.GetServices<IBulkStrategy>();
return strategies.ToDictionary(s => s.Kind, s => s);
});
// Pipeline
services.AddSingleton<IBulkPipeline, BulkPipelineEngine>();
// Register BulkEngine
services.AddSingleton<BulkEngine>(sp =>
{
var factory = sp.GetRequiredService<IDbConnectionFactory>();
var pipeline = sp.GetRequiredService<IBulkPipeline>();
return new BulkEngine(connectionString, factory, pipeline);
});
return services;
}
}
CREATE TYPE dbo.SalesType AS TABLE
(
Id INT NOT NULL,
SaleDate DATETIME2(7) NOT NULL,
Amount DECIMAL(18,2) NOT NULL
);
✔ Must match your C# entity ✔ Must match your staging table ✔ Must NOT include identity or constraints
IF OBJECT_ID('dbo.SalesRecord_Staging', 'U') IS NOT NULL
DROP TABLE dbo.SalesRecord_Staging;
CREATE TABLE dbo.SalesRecord_Staging
(
Id INT NOT NULL,
SaleDate DATETIME2(7) NOT NULL,
Amount DECIMAL(18,2) NOT NULL
);
CREATE CLUSTERED INDEX IX_SalesRecord_Staging_Id
ON dbo.SalesRecord_Staging (Id);
| Rows | Prakrishta (Stored Proc) | Prakrishta (Staging) | Raw Sql | EFCore.BulkExtensions | Result |
|---|---|---|---|---|---|
| 1,000 | 10.0 ms | 12.8 ms | 14.6 ms | 11.4 ms | EFCore slightly faster |
| 10,000 | 37.3 ms | 48.2 ms | 49.26 ms | 87.4 ms | Prakrishta ~2× faster |
| 50,000 | 188.0 ms | 195.0 ms | 203.2 ms | 395.0 ms | Prakrishta ~2× faster |
Key Findings
This chart visualizes the current benchmark results for 50,000 rows, the most meaningful scale for real‑world ETL and ingestion workloads.
Milliseconds (lower is better)
Prakrishta (Stored Proc) | ████████████████████████████ 188 ms
Prakrishta (Staging) | ██████████████████████████████ 195 ms
Raw SqlBulkCopy | ███████████████████████████████ 203 ms
EFCore.BulkExtensions | █████████████████████████████████████████████ 395 ms
Prakrishta (Stored Proc) | ████████████████ 37.3 ms
Prakrishta (Staging) | ████████████████████ 48.2 ms
Raw SqlBulkCopy | ████████████████████ 49.2 ms
EFCore.BulkExtensions | █████████████████████████████████ 87.4 ms
Prakrishta (Stored Proc) | ████████ 10.0 ms
EFCore.BulkExtensions | ████████ 11.4 ms
Prakrishta (Staging) | █████████ 12.8 ms
Raw SqlBulkCopy | ██████████ 14.6 ms
Prakrishta.Data.Bulk achieves industry‑grade performance because it:
This is why Prakrishta Data Bulk engine is:
Different workloads benefit from different bulk‑loading strategies. Prakrishta.Data.Bulk gives you three optimized paths — each designed for a specific class of problems.
Use when you want:
Ideal for:
Why choose it:
Fastest strategy at 1k, 10k, and 50k rows. Outperforms EFCore.BulkExtensions and even Raw SqlBulkCopy.
Use when you need:
Ideal for:
Why choose it:
Linear scaling, extremely stable, and 2× faster than EFCore.BulkExtensions at medium and large batch sizes.
Use when you want:
Ideal for:
Why choose it:
Great baseline — and your strategies outperform it at scale.
| Scenario | Best Strategy | why |
|---|---|---|
| Pure inserts | Stored Proc | Fastest end‑to‑end path |
| Inserts + updates | Staging | MERGE logic built‑in |
| Inserts + deletes | Staging | MERGE handles delete conditions |
| Large batch ingestion | Stored Proc / Staging | Both scale linearly |
| Small batch inserts | Stored Proc | Lowest overhead |
| EF Core replacement | Stored Proc / Staging | 2× faster at scale |
| Custom pipelines | Raw SqlBulkCopy | Maximum control |
The Bulk Engine supports strongly‑typed attributes that allow you to configure schema, table names, TVP names, stored procedures, and column mappings directly on your entity classes. This provides a clean, declarative alternative to fluent configuration and integrates seamlessly with automatic schema discovery. Attributes are optional — the engine continues to work with conventions and fluent overrides.
Attributes allow you to:
They also follow a clear precedence model:
Precedence Order (Highest → Lowest)
Column Rename
[BulkColumn("CustomerName")]
public string Name { get; set; }
Maps the property to a different column name.
Ignore Property
[BulkIgnore]
public string TempValue { get; set; }
Ignored during:
Explicit Key
[BulkKey]
public Guid CustomerId { get; set; }
Overrides the default "Id" convention.
Attributes provide defaults, but fluent API always wins:
[BulkSchema("sales")]
[BulkTable("Customer")]
public class Customer { ... }
await bulk
.For<Customer>()
.ToTable("custom.Customers") // overrides attribute
.InsertAsync(items);
Final table name: custom.Customers
If schema is not provided via:
Then the engine automatically discovers the schema from the database:
SELECT TABLE_SCHEMA
FROM INFORMATION_SCHEMA.TABLES
WHERE TABLE_NAME = 'Customer'
If multiple schemas contain the same table, the engine throws a clear error and instructs the user to specify a schema explicitly.
[BulkSchema("sales")]
[BulkTable("Customer")]
[BulkTvp("CustomerType")]
[BulkInsertProcedure("Customer_Insert")]
[BulkUpdateProcedure("Customer_Update")]
[BulkDeleteProcedure("Customer_Delete")]
public class Customer
{
[BulkKey]
public int CustomerId { get; set; }
[BulkColumn("CustomerName")]
public string Name { get; set; }
[BulkIgnore]
public string TempValue { get; set; }
}
Usage:
await bulk.For<Customer>().InsertAsync(customers);
Everything resolves automatically:
MIT License — free for commercial and open‑source use.