Skip to content

PhenX/PhenX.EntityFrameworkCore.BulkInsert

Repository files navigation

PhenX.EntityFrameworkCore.BulkInsert

A high-performance, provider-agnostic bulk insert extension for Entity Framework Core 8+. Supports SQL Server, PostgreSQL, SQLite, MySQL and Oracle.

Its main purpose is to provide a fast way to perform simple bulk inserts in Entity Framework Core applications.

Why this library?

  • Performance: It is designed to be fast and memory efficient, making it suitable for high-performance applications.
  • Provider-agnostic: It works with multiple database providers (SQL Server, PostgreSQL, SQLite and MySQL), allowing you to use it in different environments without changing your code.
  • Simplicity: The API is simple and easy to use, making it accessible for developers of all skill levels.

For now, it does not support navigation properties, complex types, owned types, shadow properties, or inheritance, but they are in the roadmap.

Packages

Package Name Description NuGet Link
PhenX.EntityFrameworkCore.BulkInsert.SqlServer For SQL Server NuGet
PhenX.EntityFrameworkCore.BulkInsert.PostgreSql For PostgreSQL NuGet
PhenX.EntityFrameworkCore.BulkInsert.Sqlite For SQLite NuGet
PhenX.EntityFrameworkCore.BulkInsert.MySql For MySql NuGet
PhenX.EntityFrameworkCore.BulkInsert.Oracle For Oracle NuGet
PhenX.EntityFrameworkCore.BulkInsert Common library NuGet

Installation

Install the NuGet package for your database provider:

# For SQL Server
Install-Package PhenX.EntityFrameworkCore.BulkInsert.SqlServer

# For PostgreSQL
Install-Package PhenX.EntityFrameworkCore.BulkInsert.PostgreSql

# For SQLite
Install-Package PhenX.EntityFrameworkCore.BulkInsert.Sqlite

# For MySql
Install-Package PhenX.EntityFrameworkCore.BulkInsert.MySql

# For Oracle
Install-Package PhenX.EntityFrameworkCore.BulkInsert.Oracle

Usage

Register the bulk insert provider in your DbContextOptions:

services.AddDbContext<MyDbContext>(options =>
{
    options
        // .UseSqlServer(connectionString) // or UseNpgsql or UseSqlite, as appropriate

        .UseBulkInsertPostgreSql()
        // OR
        .UseBulkInsertSqlServer()
        // OR
        .UseBulkInsertSqlite()
        // OR
        .UseBulkInsertMySql()
        // OR
        .UseBulkInsertOracle()
        ;
});

Very basic usage

// Asynchronously
await dbContext.ExecuteBulkInsertAsync(entities);

// Or synchronously
dbContext.ExecuteBulkInsert(entities);

Bulk insert with options

// Common options
await dbContext.ExecuteBulkInsertAsync(entities, options =>
{
    options.BatchSize = 1000; // Set the batch size for the insert operation, the default value is different for each provider
});

// Provider specific options, when available, example for SQL Server
await dbContext.ExecuteBulkInsertAsync(entities, (SqlServerBulkInsertOptions o) => // <<< here specify the SQL Server options class
{
    options.EnableStreaming = true; // Enable streaming for SQL Server
});

// Provider specific options, supporting multiple providers
await dbContext.ExecuteBulkInsertAsync(entities, o =>
{
    o.MoveRows = true;

    if (o is SqlServerBulkInsertOptions sqlServerOptions)
    {
        sqlServerOptions.EnableStreaming = true;
    }
    else if (o is MySqlBulkInsertOptions mysqlOptions)
    {
        mysqlOptions.BatchSize = 1000;
    }
});

Returning inserted entities

await dbContext.ExecuteBulkInsertReturnEntitiesAsync(entities);

Conflict resolution / merge / upsert

Conflict resolution works by specifying columns that should be used to detect conflicts and the action to take when a conflict is detected (e.g., update existing rows), using the onConflict parameter.

  • The conflicting columns are specified with the Match property and must have a unique constraint in the database.
  • The action to take when a conflict is detected is specified with the Update property. If not specified, the default action is to do nothing (i.e., skip the conflicting rows).
  • You can also specify the condition for the update action using either the Where or the RawWhere property. If not specified, the update action will be applied to all conflicting rows.
await dbContext.ExecuteBulkInsertAsync(entities, onConflict: new OnConflictOptions<TestEntity>
{
    Match = e => new
    {
        e.Name,
        // ...other columns to match on
    },

    // Optional: specify the update action, if not specified, the default action is to do nothing
    // Excluded is the row being inserted which is in conflict, and Inserted is the row already in the database.
    Update = (inserted, excluded) => new TestEntity
    {
        Price = inserted.Price // Update the Price column with the new value
    },

    // Optional: specify the condition for the update action
    // Excluded is the row being inserted which is in conflict, and Inserted is the row already in the database.
    // Using raw SQL condition
    RawWhere = (insertedTable, excludedTable) => $"{excludedTable}.some_price > {insertedTable}.some_price",

    // OR using a lambda expression
    Where = (inserted, excluded) => excluded.Price > inserted.Price,
});

Roadmap

Benchmarks

Benchmark projects are available in the tests/PhenX.EntityFrameworkCore.BulkInsert.Benchmark directory. Run them to compare performance with raw bulk insert methods and other libraries (https://github.com/borisdj/EFCore.BulkExtensions and https://entityframework-extensions.net/bulk-extensions), using optimized configuration (local Docker is required).

Legend :

SQL Server results with 500 000 rows :

bench-sqlserver.png

PostgreSQL results with 500 000 rows :

bench-postgresql.png

SQLite results with 500 000 rows :

bench-sqlite.png

MySQL results with 500 000 rows :

bench-mysql.png

Oracle results with 500 000 rows :

bench-oracle.png

Contributing

Contributions are welcome! Please open issues or submit pull requests for bug fixes, features, or documentation improvements.

License

MIT License. See LICENSE for details.