├── .editorconfig
├── .github
├── FUNDING.yml
└── workflows
│ ├── dotnet-test.yml
│ └── release.yml
├── .gitignore
├── .idea
└── .idea.PhenX.EntityFrameworkCore.BulkInsert
│ └── .idea
│ ├── .gitignore
│ ├── .name
│ ├── encodings.xml
│ ├── indexLayout.xml
│ └── vcs.xml
├── CODEOWNERS
├── CONTRIBUTING.md
├── LICENSE
├── PhenX.EntityFrameworkCore.BulkInsert.sln
├── README.md
├── images
├── bench-mysql.png
├── bench-oracle.png
├── bench-postgresql.png
├── bench-sqlite.png
├── bench-sqlserver.png
└── icon.png
├── src
├── Directory.Build.props
├── PhenX.EntityFrameworkCore.BulkInsert.MySql
│ ├── MySqlBulkInsertOptions.cs
│ ├── MySqlBulkInsertProvider.cs
│ ├── MySqlDbContextOptionsExtensions.cs
│ ├── MySqlDialectBuilder.cs
│ ├── MySqlGeometryConverter.cs
│ └── PhenX.EntityFrameworkCore.BulkInsert.MySql.csproj
├── PhenX.EntityFrameworkCore.BulkInsert.Oracle
│ ├── OracleBulkInsertOptions.cs
│ ├── OracleBulkInsertProvider.cs
│ ├── OracleDbContextOptionsExtensions.cs
│ ├── OracleDialectBuilder.cs
│ └── PhenX.EntityFrameworkCore.BulkInsert.Oracle.csproj
├── PhenX.EntityFrameworkCore.BulkInsert.PostgreSql
│ ├── IPostgresTypeProvider.cs
│ ├── PhenX.EntityFrameworkCore.BulkInsert.PostgreSql.csproj
│ ├── PostgreSqlBulkInsertOptions.cs
│ ├── PostgreSqlBulkInsertProvider.cs
│ ├── PostgreSqlDbContextOptionsExtensions.cs
│ ├── PostgreSqlDialectBuilder.cs
│ └── PostgreSqlGeometryConverter.cs
├── PhenX.EntityFrameworkCore.BulkInsert.SqlServer
│ ├── PhenX.EntityFrameworkCore.BulkInsert.SqlServer.csproj
│ ├── SqlServerBulkInsertOptions.cs
│ ├── SqlServerBulkInsertProvider.cs
│ ├── SqlServerDbContextOptionsExtensions.cs
│ ├── SqlServerDialectBuilder.cs
│ └── SqlServerGeometryConverter.cs
├── PhenX.EntityFrameworkCore.BulkInsert.Sqlite
│ ├── PhenX.EntityFrameworkCore.BulkInsert.Sqlite.csproj
│ ├── SqliteBulkInsertProvider.cs
│ ├── SqliteDbContextOptionsExtensions.cs
│ └── SqliteDialectBuilder.cs
└── PhenX.EntityFrameworkCore.BulkInsert
│ ├── Abstractions
│ ├── IBulkInsertProvider.cs
│ └── IBulkValueConverter.cs
│ ├── BulkInsertOptionsExtension.cs
│ ├── BulkInsertProviderBase.cs
│ ├── BulkInsertProviderUntyped.cs
│ ├── Dialect
│ ├── RawSqlValue.cs
│ └── SqlDialectBuilder.cs
│ ├── EnumerableDataReader.cs
│ ├── Enums
│ └── ProviderType.cs
│ ├── Extensions
│ ├── InternalExtensions.cs
│ ├── PublicExtensions.DbContext.cs
│ ├── PublicExtensions.DbSet.cs
│ └── PublicExtensions.cs
│ ├── Helpers.cs
│ ├── Log.cs
│ ├── Metadata
│ ├── ColumnMetadata.cs
│ ├── ConnectionInfo.cs
│ ├── MetadataProvider.cs
│ ├── MetadataProviderExtension.cs
│ ├── PropertyAccessor.cs
│ └── TableMetadata.cs
│ ├── Options
│ ├── BulkInsertOptions.cs
│ └── OnConflictOptions.cs
│ ├── PhenX.EntityFrameworkCore.BulkInsert.csproj
│ └── Telemetry.cs
└── tests
├── Directory.Build.props
├── PhenX.EntityFrameworkCore.BulkInsert.Benchmark
├── GetValueComparator.IlGetter.cs
├── GetValueComparator.cs
├── LibComparator.RawInsert.cs
├── LibComparator.cs
├── PhenX.EntityFrameworkCore.BulkInsert.Benchmark.csproj
├── Program.cs
├── Providers
│ ├── LibComparatorMySql.cs
│ ├── LibComparatorOracle.cs
│ ├── LibComparatorPostgreSql.cs
│ ├── LibComparatorSqlServer.cs
│ └── LibComparatorSqlite.cs
├── TestDbContext.cs
└── TestEntity.cs
└── PhenX.EntityFrameworkCore.BulkInsert.Tests
├── DbContainer
├── TestDbContainer.cs
├── TestDbContainerMySql.cs
├── TestDbContainerOracle.cs
├── TestDbContainerPostgreSql.cs
├── TestDbContainerSqlServer.cs
└── TestDbContainerSqlite.cs
├── DbContext
├── Extensions.cs
├── NumericEnum.cs
├── OwnedObject.cs
├── StringEnum.cs
├── TestDbContext.cs
├── TestDbContextBase.cs
├── TestDbContextGeo.cs
├── TestEntity.cs
├── TestEntityBase.cs
├── TestEntityWithComplexType.cs
├── TestEntityWithConverters.cs
├── TestEntityWithGeo.cs
├── TestEntityWithGuidId.cs
├── TestEntityWithJson.cs
└── TestEntityWithSimpleTypes.cs
├── PhenX.EntityFrameworkCore.BulkInsert.Tests.csproj
├── TestHelpers.cs
└── Tests
├── Basic
├── BasicTestsBase.cs
├── BasicTestsMySql.cs
├── BasicTestsOracle.cs
├── BasicTestsPostgreSql.cs
├── BasicTestsSqlServer.cs
└── BasicTestsSqlite.cs
├── Geo
├── GeoTestsBase.cs
├── GeoTestsMySql.cs
├── GeoTestsPostgreSql.cs
└── GeoTestsSqlServer.cs
└── Merge
├── MergeTestsBase.cs
├── MergeTestsMySql.cs
├── MergeTestsPostgreSql.cs
├── MergeTestsSqlServer.cs
└── MergeTestsSqlite.cs
/.editorconfig:
--------------------------------------------------------------------------------
1 | root = true
2 |
3 | [*]
4 | end_of_line = CRLF
5 | indent_style = space
6 | insert_final_newline = true
7 | trim_trailing_whitespace = true
8 |
9 | [*.sql]
10 | insert_final_newline = false
11 |
12 | [*.cs]
13 | indent_size = 4
14 | charset = utf-8
15 | csharp_style_var_for_built_in_types = true:suggestion
16 | csharp_style_var_when_type_is_apparent = true:suggestion
17 | csharp_style_pattern_matching_over_is_with_cast_check = true:warning
18 | csharp_style_pattern_matching_over_as_with_null_check = true:suggestion
19 | csharp_prefer_braces = true:suggestion
20 | dotnet_sort_system_directives_first = true
21 | dotnet_separate_import_directive_groups = true
22 | csharp_new_line_before_open_brace = all
23 |
24 | # name all constant fields using PascalCase
25 | dotnet_naming_rule.constant_fields_should_be_pascal_case.severity = suggestion
26 | dotnet_naming_rule.constant_fields_should_be_pascal_case.symbols = constant_fields
27 | dotnet_naming_rule.constant_fields_should_be_pascal_case.style = constant_fields_style
28 |
29 | dotnet_naming_symbols.constant_fields.applicable_kinds = field
30 | dotnet_naming_symbols.constant_fields.required_modifiers = const
31 |
32 | dotnet_naming_style.constant_fields_style.capitalization = pascal_case
33 |
34 | # static fields should have s_ prefix
35 | dotnet_naming_rule.static_fields_fields_should_be_pascal_case.severity = suggestion
36 | dotnet_naming_rule.static_fields_fields_should_be_pascal_case.symbols = static_fields
37 | dotnet_naming_rule.static_fields_fields_should_be_pascal_case.style = static_fields_style
38 |
39 | dotnet_naming_symbols.static_fields.applicable_kinds = field
40 | dotnet_naming_symbols.static_fields.required_modifiers = static
41 |
42 | dotnet_naming_style.static_fields_style.capitalization = pascal_case
43 |
44 | # internal and private fields should be _camelCase
45 | dotnet_naming_rule.camel_case_for_private_internal_fields.severity = suggestion
46 | dotnet_naming_rule.camel_case_for_private_internal_fields.symbols = private_internal_fields
47 | dotnet_naming_rule.camel_case_for_private_internal_fields.style = camel_case_underscore_style
48 |
49 | dotnet_naming_symbols.private_internal_fields.applicable_kinds = field
50 | dotnet_naming_symbols.private_internal_fields.applicable_accessibilities = private, internal
51 |
52 | dotnet_naming_style.camel_case_underscore_style.required_prefix = _
53 | dotnet_naming_style.camel_case_underscore_style.capitalization = camel_case
54 |
55 | # Use language keywords instead of framework type names for type references
56 | dotnet_style_predefined_type_for_locals_parameters_members = true:suggestion
57 | dotnet_style_predefined_type_for_member_access = true:suggestion
58 |
59 | # IDE0090: Use 'new(...)'
60 | dotnet_diagnostic.IDE0090.severity = none
61 |
62 | # Resharper
63 | resharper_for_can_be_converted_to_foreach_highlighting = none
64 |
65 | [*.{csproj,proj,targets}]
66 | indent_size = 2
--------------------------------------------------------------------------------
/.github/FUNDING.yml:
--------------------------------------------------------------------------------
1 | # These are supported funding model platforms
2 |
3 | github: [PhenX]
4 | patreon: # Replace with a single Patreon username
5 | open_collective: # Replace with a single Open Collective username
6 | ko_fi: # Replace with a single Ko-fi username
7 | tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel
8 | community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry
9 | liberapay: # Replace with a single Liberapay username
10 | issuehunt: # Replace with a single IssueHunt username
11 | lfx_crowdfunding: # Replace with a single LFX Crowdfunding project-name e.g., cloud-foundry
12 | polar: # Replace with a single Polar username
13 | buy_me_a_coffee: # Replace with a single Buy Me a Coffee username
14 | thanks_dev: # Replace with a single thanks.dev username
15 | custom: # Replace with up to 4 custom sponsorship URLs e.g., ['link1', 'link2']
16 |
--------------------------------------------------------------------------------
/.github/workflows/dotnet-test.yml:
--------------------------------------------------------------------------------
1 | # This workflow will build a .NET project
2 | # For more information see: https://docs.github.com/en/actions/automating-builds-and-tests/building-and-testing-net
3 |
4 | name: dotnet test
5 |
6 | on:
7 | push:
8 | branches: [ "main" ]
9 | pull_request:
10 | branches: [ "main" ]
11 |
12 | jobs:
13 | build:
14 | runs-on: ubuntu-latest
15 |
16 | strategy:
17 | matrix:
18 | dotnet: [
19 | { tfm: net8.0, version: 8.0.x },
20 | { tfm: net9.0, version: 9.0.x },
21 | ]
22 |
23 | steps:
24 | - uses: actions/checkout@v4
25 |
26 | - name: Setup .NET 9.0 # Latest dotnet version supported
27 | uses: actions/setup-dotnet@v4
28 | with:
29 | dotnet-version: 9.0.x
30 |
31 | - name: Display dotnet version
32 | run: dotnet --version
33 |
34 | - name: Restore dependencies
35 | run: dotnet restore
36 |
37 | - name: Build
38 | run: dotnet build --no-restore --framework ${{ matrix.dotnet.tfm }}
39 |
40 | - name: Test
41 | run: dotnet test --no-build --verbosity normal --framework ${{ matrix.dotnet.tfm }}
42 |
--------------------------------------------------------------------------------
/.github/workflows/release.yml:
--------------------------------------------------------------------------------
1 | name: dotnet release
2 |
3 | on:
4 | push:
5 | tags:
6 | - "v[0-9]+.[0-9]+.[0-9]+"
7 | env:
8 | VERSION: 0.0.1
9 |
10 | jobs:
11 | build:
12 | runs-on: ubuntu-latest
13 | environment: nuget
14 | steps:
15 | - uses: actions/checkout@v4
16 |
17 | - name: Verify commit exists in origin/main
18 | run: |
19 | git fetch --no-tags --prune --depth=1 origin +refs/heads/*:refs/remotes/origin/*
20 | git branch --remote --contains | grep origin/main
21 |
22 | - name: Set Version Variable
23 | if: ${{ github.ref_type == 'tag' }}
24 | env:
25 | TAG: ${{ github.ref_name }}
26 | run: echo "VERSION=${TAG#v}" >> $GITHUB_ENV
27 |
28 | - name: Setup .NET 8.0
29 | uses: actions/setup-dotnet@v4
30 | with:
31 | dotnet-version: 8.0.x
32 |
33 | - name: Setup .NET 9.0
34 | uses: actions/setup-dotnet@v4
35 | with:
36 | dotnet-version: 9.0.x
37 |
38 | - name: Restore dependencies
39 | run: dotnet restore
40 |
41 | - name: Build
42 | run: dotnet build --configuration Release --no-restore /p:Version=$VERSION
43 |
44 | - name: Test
45 | run: dotnet test --configuration Release --no-restore --no-build --verbosity normal
46 |
47 | - name: Pack nuget packages
48 | run: dotnet pack --configuration Release --no-restore --no-build --output nupkgs /p:PackageVersion=$VERSION
49 |
50 | - name: Upload nuget package
51 | if: github.ref_type == 'tag' && startsWith(github.ref, 'refs/tags/v')
52 | run: dotnet nuget push nupkgs/*.nupkg -k ${{ secrets.NUGET_API_KEY }} -s https://api.nuget.org/v3/index.json
53 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | ### DotnetCore template
2 | # .NET Core build folders
3 | bin/
4 | obj/
5 |
6 | # Common node modules locations
7 | /node_modules
8 | /wwwroot/node_modules
9 |
10 | ### Rider template
11 | # Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio, WebStorm and Rider
12 | # Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839
13 |
14 | # User-specific stuff
15 | .idea/**/workspace.xml
16 | .idea/**/tasks.xml
17 | .idea/**/usage.statistics.xml
18 | .idea/**/dictionaries
19 | .idea/**/shelf
20 |
21 | # AWS User-specific
22 | .idea/**/aws.xml
23 |
24 | # Generated files
25 | .idea/**/contentModel.xml
26 |
27 | # Sensitive or high-churn files
28 | .idea/**/dataSources/
29 | .idea/**/dataSources.ids
30 | .idea/**/dataSources.local.xml
31 | .idea/**/sqlDataSources.xml
32 | .idea/**/dynamic.xml
33 | .idea/**/uiDesigner.xml
34 | .idea/**/dbnavigator.xml
35 |
36 | # Gradle
37 | .idea/**/gradle.xml
38 | .idea/**/libraries
39 |
40 | # Gradle and Maven with auto-import
41 | # When using Gradle or Maven with auto-import, you should exclude module files,
42 | # since they will be recreated, and may cause churn. Uncomment if using
43 | # auto-import.
44 | # .idea/artifacts
45 | # .idea/compiler.xml
46 | # .idea/jarRepositories.xml
47 | # .idea/modules.xml
48 | # .idea/*.iml
49 | # .idea/modules
50 | # *.iml
51 | # *.ipr
52 |
53 | # CMake
54 | cmake-build-*/
55 |
56 | # Mongo Explorer plugin
57 | .idea/**/mongoSettings.xml
58 |
59 | # File-based project format
60 | *.iws
61 |
62 | # IntelliJ
63 | out/
64 |
65 | # mpeltonen/sbt-idea plugin
66 | .idea_modules/
67 |
68 | # JIRA plugin
69 | atlassian-ide-plugin.xml
70 |
71 | # Cursive Clojure plugin
72 | .idea/replstate.xml
73 |
74 | # SonarLint plugin
75 | .idea/sonarlint/
76 |
77 | # Crashlytics plugin (for Android Studio and IntelliJ)
78 | com_crashlytics_export_strings.xml
79 | crashlytics.properties
80 | crashlytics-build.properties
81 | fabric.properties
82 |
83 | # Editor-based Rest Client
84 | .idea/httpRequests
85 |
86 | # Android studio 3.1+ serialized cache file
87 | .idea/caches/build_file_checksums.ser
88 |
89 | # Nuget assets
90 | /nupkgs
91 |
92 | # Visual Studio Files
93 | .vs
94 |
--------------------------------------------------------------------------------
/.idea/.idea.PhenX.EntityFrameworkCore.BulkInsert/.idea/.gitignore:
--------------------------------------------------------------------------------
1 | # Default ignored files
2 | /shelf/
3 | /workspace.xml
4 | # Rider ignored files
5 | /modules.xml
6 | /.idea.EntityFrameworkCore.ExecuteInsert.iml
7 | /contentModel.xml
8 | /projectSettingsUpdater.xml
9 | # Editor-based HTTP Client requests
10 | /httpRequests/
11 | # Datasource local storage ignored files
12 | /dataSources/
13 | /dataSources.local.xml
14 |
--------------------------------------------------------------------------------
/.idea/.idea.PhenX.EntityFrameworkCore.BulkInsert/.idea/.name:
--------------------------------------------------------------------------------
1 | PhenX.EntityFrameworkCore.BulkInsert
--------------------------------------------------------------------------------
/.idea/.idea.PhenX.EntityFrameworkCore.BulkInsert/.idea/encodings.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
--------------------------------------------------------------------------------
/.idea/.idea.PhenX.EntityFrameworkCore.BulkInsert/.idea/indexLayout.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | .github
6 | images
7 |
8 |
9 |
10 |
11 |
--------------------------------------------------------------------------------
/.idea/.idea.PhenX.EntityFrameworkCore.BulkInsert/.idea/vcs.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
--------------------------------------------------------------------------------
/CODEOWNERS:
--------------------------------------------------------------------------------
1 | # See https://docs.github.com/en/repositories/managing-your-repositorys-settings-and-features/customizing-your-repository/about-code-owners
2 | # This file designates code owners for this repository.
3 | # Each line is a file pattern followed by one or more owners.
4 |
5 | * @phenx
6 |
7 |
--------------------------------------------------------------------------------
/CONTRIBUTING.md:
--------------------------------------------------------------------------------
1 | # Contributing to Project Guidelines
2 |
3 | ### No guidelines here :D, hit us with your PR.
4 | **Working on your first Pull Request?** You can learn how from this *free* series [How to Contribute to an Open Source Project on GitHub](https://egghead.io/series/how-to-contribute-to-an-open-source-project-on-github)
5 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2025 phenx
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
23 |
--------------------------------------------------------------------------------
/PhenX.EntityFrameworkCore.BulkInsert.sln:
--------------------------------------------------------------------------------
1 |
2 | Microsoft Visual Studio Solution File, Format Version 12.00
3 | # Visual Studio Version 17
4 | VisualStudioVersion = 17.12.35707.178 d17.12
5 | MinimumVisualStudioVersion = 10.0.40219.1
6 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "PhenX.EntityFrameworkCore.BulkInsert", "src\PhenX.EntityFrameworkCore.BulkInsert\PhenX.EntityFrameworkCore.BulkInsert.csproj", "{56CA0AE2-6EAB-4394-9E06-132558551251}"
7 | EndProject
8 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "PhenX.EntityFrameworkCore.BulkInsert.PostgreSql", "src\PhenX.EntityFrameworkCore.BulkInsert.PostgreSql\PhenX.EntityFrameworkCore.BulkInsert.PostgreSql.csproj", "{F37308A8-1C3C-44D2-9440-670DF76A8C31}"
9 | EndProject
10 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "PhenX.EntityFrameworkCore.BulkInsert.SqlServer", "src\PhenX.EntityFrameworkCore.BulkInsert.SqlServer\PhenX.EntityFrameworkCore.BulkInsert.SqlServer.csproj", "{8098F37B-FA5E-4BDB-B64A-00FBDE2001C9}"
11 | EndProject
12 | Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "src", "src", "{CBEBA2A8-79E0-412E-93C1-C88F4473D78B}"
13 | ProjectSection(SolutionItems) = preProject
14 | src\Directory.Build.props = src\Directory.Build.props
15 | EndProjectSection
16 | EndProject
17 | Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "tests", "tests", "{F8A83782-311C-454D-8B97-B3FB86478BF4}"
18 | ProjectSection(SolutionItems) = preProject
19 | tests\Directory.Build.props = tests\Directory.Build.props
20 | EndProjectSection
21 | EndProject
22 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "PhenX.EntityFrameworkCore.BulkInsert.Tests", "tests\PhenX.EntityFrameworkCore.BulkInsert.Tests\PhenX.EntityFrameworkCore.BulkInsert.Tests.csproj", "{EDCCED5F-D456-45E2-81A6-1077977F042B}"
23 | EndProject
24 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "PhenX.EntityFrameworkCore.BulkInsert.Benchmark", "tests\PhenX.EntityFrameworkCore.BulkInsert.Benchmark\PhenX.EntityFrameworkCore.BulkInsert.Benchmark.csproj", "{E4EB1C53-575C-45F8-924A-93DC42E8ACCA}"
25 | EndProject
26 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "PhenX.EntityFrameworkCore.BulkInsert.Sqlite", "src\PhenX.EntityFrameworkCore.BulkInsert.Sqlite\PhenX.EntityFrameworkCore.BulkInsert.Sqlite.csproj", "{450E859C-411F-4D67-A0B4-4E02C3D30E14}"
27 | EndProject
28 | Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "root", "root", "{45366E91-4386-4DAC-9D09-226902EA6D9F}"
29 | ProjectSection(SolutionItems) = preProject
30 | .editorconfig = .editorconfig
31 | .gitignore = .gitignore
32 | LICENSE = LICENSE
33 | README.md = README.md
34 | EndProjectSection
35 | EndProject
36 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "PhenX.EntityFrameworkCore.BulkInsert.MySql", "src\PhenX.EntityFrameworkCore.BulkInsert.MySql\PhenX.EntityFrameworkCore.BulkInsert.MySql.csproj", "{17649766-EA68-4333-8DA8-47B014A8B2CC}"
37 | EndProject
38 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "PhenX.EntityFrameworkCore.BulkInsert.Oracle", "src\PhenX.EntityFrameworkCore.BulkInsert.Oracle\PhenX.EntityFrameworkCore.BulkInsert.Oracle.csproj", "{98CC5F0A-5739-4570-A384-A3A067D09755}"
39 | EndProject
40 | Global
41 | GlobalSection(SolutionConfigurationPlatforms) = preSolution
42 | Debug|Any CPU = Debug|Any CPU
43 | Release|Any CPU = Release|Any CPU
44 | EndGlobalSection
45 | GlobalSection(ProjectConfigurationPlatforms) = postSolution
46 | {56CA0AE2-6EAB-4394-9E06-132558551251}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
47 | {56CA0AE2-6EAB-4394-9E06-132558551251}.Debug|Any CPU.Build.0 = Debug|Any CPU
48 | {56CA0AE2-6EAB-4394-9E06-132558551251}.Release|Any CPU.ActiveCfg = Release|Any CPU
49 | {56CA0AE2-6EAB-4394-9E06-132558551251}.Release|Any CPU.Build.0 = Release|Any CPU
50 | {F37308A8-1C3C-44D2-9440-670DF76A8C31}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
51 | {F37308A8-1C3C-44D2-9440-670DF76A8C31}.Debug|Any CPU.Build.0 = Debug|Any CPU
52 | {F37308A8-1C3C-44D2-9440-670DF76A8C31}.Release|Any CPU.ActiveCfg = Release|Any CPU
53 | {F37308A8-1C3C-44D2-9440-670DF76A8C31}.Release|Any CPU.Build.0 = Release|Any CPU
54 | {8098F37B-FA5E-4BDB-B64A-00FBDE2001C9}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
55 | {8098F37B-FA5E-4BDB-B64A-00FBDE2001C9}.Debug|Any CPU.Build.0 = Debug|Any CPU
56 | {8098F37B-FA5E-4BDB-B64A-00FBDE2001C9}.Release|Any CPU.ActiveCfg = Release|Any CPU
57 | {8098F37B-FA5E-4BDB-B64A-00FBDE2001C9}.Release|Any CPU.Build.0 = Release|Any CPU
58 | {EDCCED5F-D456-45E2-81A6-1077977F042B}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
59 | {EDCCED5F-D456-45E2-81A6-1077977F042B}.Debug|Any CPU.Build.0 = Debug|Any CPU
60 | {EDCCED5F-D456-45E2-81A6-1077977F042B}.Release|Any CPU.ActiveCfg = Release|Any CPU
61 | {EDCCED5F-D456-45E2-81A6-1077977F042B}.Release|Any CPU.Build.0 = Release|Any CPU
62 | {E4EB1C53-575C-45F8-924A-93DC42E8ACCA}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
63 | {E4EB1C53-575C-45F8-924A-93DC42E8ACCA}.Debug|Any CPU.Build.0 = Debug|Any CPU
64 | {E4EB1C53-575C-45F8-924A-93DC42E8ACCA}.Release|Any CPU.ActiveCfg = Release|Any CPU
65 | {E4EB1C53-575C-45F8-924A-93DC42E8ACCA}.Release|Any CPU.Build.0 = Release|Any CPU
66 | {450E859C-411F-4D67-A0B4-4E02C3D30E14}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
67 | {450E859C-411F-4D67-A0B4-4E02C3D30E14}.Debug|Any CPU.Build.0 = Debug|Any CPU
68 | {450E859C-411F-4D67-A0B4-4E02C3D30E14}.Release|Any CPU.ActiveCfg = Release|Any CPU
69 | {450E859C-411F-4D67-A0B4-4E02C3D30E14}.Release|Any CPU.Build.0 = Release|Any CPU
70 | {17649766-EA68-4333-8DA8-47B014A8B2CC}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
71 | {17649766-EA68-4333-8DA8-47B014A8B2CC}.Debug|Any CPU.Build.0 = Debug|Any CPU
72 | {17649766-EA68-4333-8DA8-47B014A8B2CC}.Release|Any CPU.ActiveCfg = Release|Any CPU
73 | {17649766-EA68-4333-8DA8-47B014A8B2CC}.Release|Any CPU.Build.0 = Release|Any CPU
74 | {98CC5F0A-5739-4570-A384-A3A067D09755}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
75 | {98CC5F0A-5739-4570-A384-A3A067D09755}.Debug|Any CPU.Build.0 = Debug|Any CPU
76 | {98CC5F0A-5739-4570-A384-A3A067D09755}.Release|Any CPU.ActiveCfg = Release|Any CPU
77 | {98CC5F0A-5739-4570-A384-A3A067D09755}.Release|Any CPU.Build.0 = Release|Any CPU
78 | EndGlobalSection
79 | GlobalSection(SolutionProperties) = preSolution
80 | HideSolutionNode = FALSE
81 | EndGlobalSection
82 | GlobalSection(NestedProjects) = preSolution
83 | {56CA0AE2-6EAB-4394-9E06-132558551251} = {CBEBA2A8-79E0-412E-93C1-C88F4473D78B}
84 | {F37308A8-1C3C-44D2-9440-670DF76A8C31} = {CBEBA2A8-79E0-412E-93C1-C88F4473D78B}
85 | {8098F37B-FA5E-4BDB-B64A-00FBDE2001C9} = {CBEBA2A8-79E0-412E-93C1-C88F4473D78B}
86 | {EDCCED5F-D456-45E2-81A6-1077977F042B} = {F8A83782-311C-454D-8B97-B3FB86478BF4}
87 | {E4EB1C53-575C-45F8-924A-93DC42E8ACCA} = {F8A83782-311C-454D-8B97-B3FB86478BF4}
88 | {450E859C-411F-4D67-A0B4-4E02C3D30E14} = {CBEBA2A8-79E0-412E-93C1-C88F4473D78B}
89 | {17649766-EA68-4333-8DA8-47B014A8B2CC} = {CBEBA2A8-79E0-412E-93C1-C88F4473D78B}
90 | {98CC5F0A-5739-4570-A384-A3A067D09755} = {CBEBA2A8-79E0-412E-93C1-C88F4473D78B}
91 | EndGlobalSection
92 | EndGlobal
93 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # PhenX.EntityFrameworkCore.BulkInsert
2 |
3 | A high-performance, provider-agnostic bulk insert extension for Entity Framework Core 8+. Supports SQL Server, PostgreSQL, SQLite, MySQL and Oracle.
4 |
5 | Its main purpose is to provide a fast way to perform simple bulk inserts in Entity Framework Core applications.
6 |
7 | ## Why this library?
8 |
9 | - **Performance**: It is designed to be fast and memory efficient, making it suitable for high-performance applications.
10 | - **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.
11 | - **Simplicity**: The API is simple and easy to use, making it accessible for developers of all skill levels.
12 |
13 | For now, it does not support navigation properties, complex types, owned types, shadow properties, or inheritance,
14 | but they are in [the roadmap](#roadmap).
15 |
16 | ## Packages
17 |
18 | | Package Name | Description | NuGet Link |
19 | |---------------------------------------------------|----------------|--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
20 | | `PhenX.EntityFrameworkCore.BulkInsert.SqlServer` | For SQL Server | [](https://www.nuget.org/packages/PhenX.EntityFrameworkCore.BulkInsert.SqlServer) |
21 | | `PhenX.EntityFrameworkCore.BulkInsert.PostgreSql` | For PostgreSQL | [](https://www.nuget.org/packages/PhenX.EntityFrameworkCore.BulkInsert.PostgreSql) |
22 | | `PhenX.EntityFrameworkCore.BulkInsert.Sqlite` | For SQLite | [](https://www.nuget.org/packages/PhenX.EntityFrameworkCore.BulkInsert.Sqlite) |
23 | | `PhenX.EntityFrameworkCore.BulkInsert.MySql` | For MySql | [](https://www.nuget.org/packages/PhenX.EntityFrameworkCore.BulkInsert.MySql) |
24 | | `PhenX.EntityFrameworkCore.BulkInsert.Oracle` | For Oracle | [](https://www.nuget.org/packages/PhenX.EntityFrameworkCore.BulkInsert.Oracle) |
25 | | `PhenX.EntityFrameworkCore.BulkInsert` | Common library | [](https://www.nuget.org/packages/PhenX.EntityFrameworkCore.BulkInsert) |
26 |
27 | ## Installation
28 |
29 | Install the NuGet package for your database provider:
30 |
31 | ```shell
32 | # For SQL Server
33 | Install-Package PhenX.EntityFrameworkCore.BulkInsert.SqlServer
34 |
35 | # For PostgreSQL
36 | Install-Package PhenX.EntityFrameworkCore.BulkInsert.PostgreSql
37 |
38 | # For SQLite
39 | Install-Package PhenX.EntityFrameworkCore.BulkInsert.Sqlite
40 |
41 | # For MySql
42 | Install-Package PhenX.EntityFrameworkCore.BulkInsert.MySql
43 |
44 | # For Oracle
45 | Install-Package PhenX.EntityFrameworkCore.BulkInsert.Oracle
46 | ```
47 |
48 | ## Usage
49 |
50 | Register the bulk insert provider in your `DbContextOptions`:
51 |
52 | ```csharp
53 | services.AddDbContext(options =>
54 | {
55 | options
56 | // .UseSqlServer(connectionString) // or UseNpgsql or UseSqlite, as appropriate
57 |
58 | .UseBulkInsertPostgreSql()
59 | // OR
60 | .UseBulkInsertSqlServer()
61 | // OR
62 | .UseBulkInsertSqlite()
63 | // OR
64 | .UseBulkInsertMySql()
65 | // OR
66 | .UseBulkInsertOracle()
67 | ;
68 | });
69 | ```
70 |
71 | ### Very basic usage
72 |
73 | ```csharp
74 | // Asynchronously
75 | await dbContext.ExecuteBulkInsertAsync(entities);
76 |
77 | // Or synchronously
78 | dbContext.ExecuteBulkInsert(entities);
79 | ```
80 |
81 | ### Bulk insert with options
82 |
83 | ```csharp
84 | // Common options
85 | await dbContext.ExecuteBulkInsertAsync(entities, options =>
86 | {
87 | options.BatchSize = 1000; // Set the batch size for the insert operation, the default value is different for each provider
88 | });
89 |
90 | // Provider specific options, when available, example for SQL Server
91 | await dbContext.ExecuteBulkInsertAsync(entities, (SqlServerBulkInsertOptions o) => // <<< here specify the SQL Server options class
92 | {
93 | options.EnableStreaming = true; // Enable streaming for SQL Server
94 | });
95 |
96 | // Provider specific options, supporting multiple providers
97 | await dbContext.ExecuteBulkInsertAsync(entities, o =>
98 | {
99 | o.MoveRows = true;
100 |
101 | if (o is SqlServerBulkInsertOptions sqlServerOptions)
102 | {
103 | sqlServerOptions.EnableStreaming = true;
104 | }
105 | else if (o is MySqlBulkInsertOptions mysqlOptions)
106 | {
107 | mysqlOptions.BatchSize = 1000;
108 | }
109 | });
110 | ```
111 |
112 | ### Returning inserted entities
113 |
114 | ```csharp
115 | await dbContext.ExecuteBulkInsertReturnEntitiesAsync(entities);
116 | ```
117 |
118 | ### Conflict resolution / merge / upsert
119 |
120 | Conflict resolution works by specifying columns that should be used to detect conflicts and the action to take when
121 | a conflict is detected (e.g., update existing rows), using the `onConflict` parameter.
122 |
123 | * The conflicting columns are specified with the `Match` property and must have a unique constraint in the database.
124 | * 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).
125 | * 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.
126 |
127 | ```csharp
128 | await dbContext.ExecuteBulkInsertAsync(entities, onConflict: new OnConflictOptions
129 | {
130 | Match = e => new
131 | {
132 | e.Name,
133 | // ...other columns to match on
134 | },
135 |
136 | // Optional: specify the update action, if not specified, the default action is to do nothing
137 | // Excluded is the row being inserted which is in conflict, and Inserted is the row already in the database.
138 | Update = (inserted, excluded) => new TestEntity
139 | {
140 | Price = inserted.Price // Update the Price column with the new value
141 | },
142 |
143 | // Optional: specify the condition for the update action
144 | // Excluded is the row being inserted which is in conflict, and Inserted is the row already in the database.
145 | // Using raw SQL condition
146 | RawWhere = (insertedTable, excludedTable) => $"{excludedTable}.some_price > {insertedTable}.some_price",
147 |
148 | // OR using a lambda expression
149 | Where = (inserted, excluded) => excluded.Price > inserted.Price,
150 | });
151 | ```
152 |
153 | ## Roadmap
154 |
155 | - [ ] [Add support for navigation properties](https://github.com/PhenX/PhenX.EntityFrameworkCore.BulkInsert/issues/2)
156 | - [x] [Add support for complex types](https://github.com/PhenX/PhenX.EntityFrameworkCore.BulkInsert/issues/3)
157 | - [x] Add support for owned types
158 | - [ ] Add support for shadow properties
159 | - [ ] Add support for TPT (Table Per Type) inheritance
160 | - [ ] Add support for TPC (Table Per Concrete Type) inheritance
161 | - [ ] Add support for TPH (Table Per Hierarchy) inheritance
162 |
163 | ## Benchmarks
164 |
165 | Benchmark projects are available in the [`tests/PhenX.EntityFrameworkCore.BulkInsert.Benchmark`](tests/PhenX.EntityFrameworkCore.BulkInsert.Benchmark/LibComparator.cs) directory.
166 | Run them to compare performance with raw bulk insert methods and other libraries (https://github.com/borisdj/EFCore.BulkExtensions
167 | and https://entityframework-extensions.net/bulk-extensions), using optimized configuration (local Docker is required).
168 |
169 | Legend :
170 | * `PhenX_EntityFrameworkCore_BulkInsert`: this library
171 | * `RawInsert`: naive implementation without any library, using the native provider API (SqlBulkCopy for SQL Server, BeginBinaryImport for PostgreSQL, raw inserts for SQLite)
172 | * `Z_EntityFramework_Extensions_EFCore`: https://entityframework-extensions.net/bulk-extensions
173 | * `EFCore_BulkExtensions`: https://github.com/borisdj/EFCore.BulkExtensions
174 | * `Linq2Db`: https://github.com/linq2db/linq2db
175 |
176 | SQL Server results with 500 000 rows :
177 |
178 | 
179 |
180 | PostgreSQL results with 500 000 rows :
181 |
182 | 
183 |
184 | SQLite results with 500 000 rows :
185 |
186 | 
187 |
188 | MySQL results with 500 000 rows :
189 |
190 | 
191 |
192 | Oracle results with 500 000 rows :
193 |
194 | 
195 |
196 | ## Contributing
197 |
198 | Contributions are welcome! Please open issues or submit pull requests for bug fixes, features, or documentation improvements.
199 |
200 | ## License
201 |
202 | MIT License. See [LICENSE](LICENSE) for details.
203 |
--------------------------------------------------------------------------------
/images/bench-mysql.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/PhenX/PhenX.EntityFrameworkCore.BulkInsert/1592da100c41def88b3b951c6294991e94cb3680/images/bench-mysql.png
--------------------------------------------------------------------------------
/images/bench-oracle.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/PhenX/PhenX.EntityFrameworkCore.BulkInsert/1592da100c41def88b3b951c6294991e94cb3680/images/bench-oracle.png
--------------------------------------------------------------------------------
/images/bench-postgresql.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/PhenX/PhenX.EntityFrameworkCore.BulkInsert/1592da100c41def88b3b951c6294991e94cb3680/images/bench-postgresql.png
--------------------------------------------------------------------------------
/images/bench-sqlite.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/PhenX/PhenX.EntityFrameworkCore.BulkInsert/1592da100c41def88b3b951c6294991e94cb3680/images/bench-sqlite.png
--------------------------------------------------------------------------------
/images/bench-sqlserver.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/PhenX/PhenX.EntityFrameworkCore.BulkInsert/1592da100c41def88b3b951c6294991e94cb3680/images/bench-sqlserver.png
--------------------------------------------------------------------------------
/images/icon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/PhenX/PhenX.EntityFrameworkCore.BulkInsert/1592da100c41def88b3b951c6294991e94cb3680/images/icon.png
--------------------------------------------------------------------------------
/src/Directory.Build.props:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | net8.0;net9.0
5 | 12
6 | enable
7 | enable
8 | en
9 | true
10 |
11 | true
12 |
13 |
14 |
15 | Fabien Ménager
16 | Super fast bulk insertion for Entity Framework Core on SQL Server, PostgreSQL and SQLite
17 | Fabien Ménager © 2025
18 | https://github.com/PhenX/PhenX.EntityFrameworkCore.BulkInsert
19 | https://github.com/PhenX/PhenX.EntityFrameworkCore.BulkInsert
20 | git
21 | MIT
22 | sql sqlite postgresql entity-framework sqlbulkcopy efcore entity-framework-core sqlserver bulk-insert
23 | README.md
24 | icon.png
25 | https://github.com/PhenX/PhenX.EntityFrameworkCore.BulkInsert/releases
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 | true
35 | snupkg
36 |
37 |
38 |
39 | true
40 | true
41 | $(AllowedOutputExtensionsInPackageBuildOutputFolder);.pdb
42 |
43 |
44 |
45 | true
46 |
47 |
48 |
49 |
50 |
51 |
52 |
53 |
54 |
--------------------------------------------------------------------------------
/src/PhenX.EntityFrameworkCore.BulkInsert.MySql/MySqlBulkInsertOptions.cs:
--------------------------------------------------------------------------------
1 | using PhenX.EntityFrameworkCore.BulkInsert.Options;
2 |
3 | namespace PhenX.EntityFrameworkCore.BulkInsert.MySql;
4 |
5 | ///
6 | /// Options specific to MySQL bulk insert.
7 | ///
8 | public class MySqlBulkInsertOptions : BulkInsertOptions
9 | {
10 |
11 | }
12 |
--------------------------------------------------------------------------------
/src/PhenX.EntityFrameworkCore.BulkInsert.MySql/MySqlBulkInsertProvider.cs:
--------------------------------------------------------------------------------
1 | using JetBrains.Annotations;
2 |
3 | using Microsoft.EntityFrameworkCore;
4 | using Microsoft.EntityFrameworkCore.Storage;
5 | using Microsoft.Extensions.Logging;
6 |
7 | using MySqlConnector;
8 |
9 | using PhenX.EntityFrameworkCore.BulkInsert.Metadata;
10 | using PhenX.EntityFrameworkCore.BulkInsert.Options;
11 |
12 | namespace PhenX.EntityFrameworkCore.BulkInsert.MySql;
13 |
14 | [UsedImplicitly]
15 | internal class MySqlBulkInsertProvider(ILogger logger) : BulkInsertProviderBase(logger)
16 | {
17 | //language=sql
18 | ///
19 | protected override string AddTableCopyBulkInsertId => $"ALTER TABLE {{0}} ADD {BulkInsertId} INT AUTO_INCREMENT PRIMARY KEY;";
20 |
21 | ///
22 | protected override string GetTempTableName(string tableName) => $"#_temp_bulk_insert_{tableName}";
23 |
24 | ///
25 | protected override MySqlBulkInsertOptions CreateDefaultOptions() => new()
26 | {
27 | Converters = [MySqlGeometryConverter.Instance]
28 | };
29 |
30 | ///
31 | protected override IAsyncEnumerable BulkInsertReturnEntities(
32 | bool sync,
33 | DbContext context,
34 | TableMetadata tableInfo,
35 | IEnumerable entities,
36 | MySqlBulkInsertOptions options,
37 | OnConflictOptions? onConflict,
38 | CancellationToken ctk)
39 | {
40 | throw new NotSupportedException("Provider does not support returning entities.");
41 | }
42 |
43 | ///
44 | protected override async Task BulkInsert(
45 | bool sync,
46 | DbContext context,
47 | TableMetadata tableInfo,
48 | IEnumerable entities,
49 | string tableName,
50 | IReadOnlyList properties,
51 | MySqlBulkInsertOptions options,
52 | CancellationToken ctk
53 | )
54 | {
55 | var connection = (MySqlConnection)context.Database.GetDbConnection();
56 | var sqlTransaction = context.Database.CurrentTransaction!.GetDbTransaction()
57 | ?? throw new InvalidOperationException("No open transaction found.");
58 | if (sqlTransaction is not MySqlTransaction mySqlTransaction)
59 | {
60 | throw new InvalidOperationException($"Invalid transaction foud, got {sqlTransaction.GetType()}.");
61 | }
62 |
63 | var bulkCopy = new MySqlBulkCopy(connection, mySqlTransaction);
64 | bulkCopy.DestinationTableName = tableName;
65 | bulkCopy.BulkCopyTimeout = options.GetCopyTimeoutInSeconds();
66 |
67 | // Handle progress notifications
68 | if (options is { NotifyProgressAfter: not null, OnProgress: not null })
69 | {
70 | bulkCopy.NotifyAfter = options.NotifyProgressAfter.Value;
71 |
72 | bulkCopy.MySqlRowsCopied += (sender, e) =>
73 | {
74 | options.OnProgress(e.RowsCopied);
75 |
76 | if (ctk.IsCancellationRequested)
77 | {
78 | e.Abort = true;
79 | }
80 | };
81 | }
82 |
83 | // If no progress notification is set, we still need to handle cancellation.
84 | else
85 | {
86 | bulkCopy.MySqlRowsCopied += (sender, e) =>
87 | {
88 | if (ctk.IsCancellationRequested)
89 | {
90 | e.Abort = true;
91 | }
92 | };
93 | }
94 |
95 | var sourceOrdinal = 0;
96 | foreach (var prop in properties)
97 | {
98 | bulkCopy.ColumnMappings.Add(new MySqlBulkCopyColumnMapping(sourceOrdinal, prop.ColumnName));
99 | sourceOrdinal++;
100 | }
101 |
102 | var dataReader = new EnumerableDataReader(entities, properties, options);
103 |
104 | if (sync)
105 | {
106 | // ReSharper disable once MethodHasAsyncOverloadWithCancellation
107 | bulkCopy.WriteToServer(dataReader);
108 | }
109 | else
110 | {
111 | await bulkCopy.WriteToServerAsync(dataReader, ctk);
112 | }
113 | }
114 | }
115 |
--------------------------------------------------------------------------------
/src/PhenX.EntityFrameworkCore.BulkInsert.MySql/MySqlDbContextOptionsExtensions.cs:
--------------------------------------------------------------------------------
1 | using Microsoft.EntityFrameworkCore;
2 |
3 | using PhenX.EntityFrameworkCore.BulkInsert.Extensions;
4 |
5 | namespace PhenX.EntityFrameworkCore.BulkInsert.MySql;
6 |
7 | ///
8 | /// DbContext options extension for MySql.
9 | ///
10 | public static class MySqlDbContextOptionsExtensions
11 | {
12 | ///
13 | /// Configures the DbContext to use the MySql bulk insert provider.
14 | ///
15 | public static DbContextOptionsBuilder UseBulkInsertMySql(this DbContextOptionsBuilder optionsBuilder)
16 | {
17 | return optionsBuilder.UseProvider();
18 | }
19 | }
20 |
--------------------------------------------------------------------------------
/src/PhenX.EntityFrameworkCore.BulkInsert.MySql/MySqlDialectBuilder.cs:
--------------------------------------------------------------------------------
1 | using System.Text;
2 |
3 | using Microsoft.EntityFrameworkCore;
4 |
5 | using PhenX.EntityFrameworkCore.BulkInsert.Dialect;
6 | using PhenX.EntityFrameworkCore.BulkInsert.Metadata;
7 | using PhenX.EntityFrameworkCore.BulkInsert.Options;
8 |
9 | namespace PhenX.EntityFrameworkCore.BulkInsert.MySql;
10 |
11 | internal class MySqlServerDialectBuilder : SqlDialectBuilder
12 | {
13 | protected override string OpenDelimiter => "`";
14 |
15 | protected override string CloseDelimiter => "`";
16 |
17 | ///
18 | protected override bool SupportsMoveRows => false;
19 |
20 | ///
21 | protected override bool SupportsInsertIntoAlias => false;
22 |
23 | public override string CreateTableCopySql(string tempNameName, TableMetadata tableInfo, IReadOnlyList columns)
24 | {
25 | return $"CREATE TEMPORARY TABLE {tempNameName} SELECT * FROM {tableInfo.QuotedTableName} WHERE 1 = 0;";
26 | }
27 |
28 | protected override void AppendConflictCondition(
29 | StringBuilder sql,
30 | TableMetadata target,
31 | DbContext context,
32 | OnConflictOptions onConflictTyped)
33 | {
34 | throw new NotSupportedException("Conflict conditions are not supported in MYSQL");
35 | }
36 |
37 | protected override void AppendOnConflictUpdate(StringBuilder sql, IEnumerable updates)
38 | {
39 | sql.AppendLine("UPDATE");
40 |
41 | var i = 0;
42 | foreach (var update in updates)
43 | {
44 | if (i > 0)
45 | {
46 | sql.Append(", ");
47 | }
48 |
49 | sql.Append(update);
50 | i++;
51 | }
52 | }
53 |
54 | protected override void AppendOnConflictStatement(StringBuilder sql)
55 | {
56 | sql.Append("ON DUPLICATE KEY");
57 | }
58 |
59 | protected override void AppendDoNothing(StringBuilder sql, IEnumerable insertedColumns)
60 | {
61 | var columnName = insertedColumns.First().ColumnName;
62 |
63 | sql.Append($"UPDATE {Quote(columnName)} = {GetExcludedColumnName(columnName)}");
64 | }
65 |
66 | protected override string GetExcludedColumnName(string columnName)
67 | {
68 | return $"VALUES({Quote(columnName)})";
69 | }
70 | }
71 |
--------------------------------------------------------------------------------
/src/PhenX.EntityFrameworkCore.BulkInsert.MySql/MySqlGeometryConverter.cs:
--------------------------------------------------------------------------------
1 | using MySqlConnector;
2 |
3 | using NetTopologySuite.Geometries;
4 |
5 | using PhenX.EntityFrameworkCore.BulkInsert.Abstractions;
6 | using PhenX.EntityFrameworkCore.BulkInsert.Options;
7 |
8 | namespace PhenX.EntityFrameworkCore.BulkInsert.MySql;
9 |
10 | internal sealed class MySqlGeometryConverter : IBulkValueConverter
11 | {
12 | public static readonly MySqlGeometryConverter Instance = new();
13 |
14 | private MySqlGeometryConverter()
15 | {
16 | }
17 |
18 | public bool TryConvertValue(object source, BulkInsertOptions options, out object result)
19 | {
20 | if (source is Geometry geometry)
21 | {
22 | result = MySqlGeometry.FromWkb(options.SRID, geometry.ToBinary());
23 | return true;
24 | }
25 |
26 | result = source;
27 | return false;
28 | }
29 | }
30 |
--------------------------------------------------------------------------------
/src/PhenX.EntityFrameworkCore.BulkInsert.MySql/PhenX.EntityFrameworkCore.BulkInsert.MySql.csproj:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | $(NoWarn);NU5104
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
--------------------------------------------------------------------------------
/src/PhenX.EntityFrameworkCore.BulkInsert.Oracle/OracleBulkInsertOptions.cs:
--------------------------------------------------------------------------------
1 | using Oracle.ManagedDataAccess.Client;
2 |
3 | using PhenX.EntityFrameworkCore.BulkInsert.Options;
4 |
5 | namespace PhenX.EntityFrameworkCore.BulkInsert.Oracle;
6 |
7 | ///
8 | /// Options specific to Oracle bulk insert.
9 | ///
10 | public class OracleBulkInsertOptions : BulkInsertOptions
11 | {
12 | ///
13 | public OracleBulkCopyOptions CopyOptions { get; set; } = OracleBulkCopyOptions.Default;
14 | }
15 |
--------------------------------------------------------------------------------
/src/PhenX.EntityFrameworkCore.BulkInsert.Oracle/OracleBulkInsertProvider.cs:
--------------------------------------------------------------------------------
1 | using JetBrains.Annotations;
2 |
3 | using Microsoft.EntityFrameworkCore;
4 | using Microsoft.Extensions.Logging;
5 |
6 | using Oracle.ManagedDataAccess.Client;
7 |
8 | using PhenX.EntityFrameworkCore.BulkInsert.Metadata;
9 | using PhenX.EntityFrameworkCore.BulkInsert.Options;
10 |
11 | namespace PhenX.EntityFrameworkCore.BulkInsert.Oracle;
12 |
13 | [UsedImplicitly]
14 | internal class OracleBulkInsertProvider(ILogger? logger) : BulkInsertProviderBase(logger)
15 | {
16 | ///
17 | protected override string BulkInsertId => "ROWID";
18 |
19 | ///
20 | protected override string AddTableCopyBulkInsertId => ""; // No need to add an ID column in Oracle
21 |
22 | ///
23 | ///
24 | /// The temporary table name is generated with a GUID to ensure uniqueness, but limited to less than 30 characters,
25 | /// because Oracle prior 12.2 has a limit of 30 characters for identifiers.
26 | ///
27 | protected override string GetTempTableName(string tableName) => $"#temp_bulk_insert_{Guid.NewGuid().ToString("N")[..8]}";
28 |
29 | protected override OracleBulkInsertOptions CreateDefaultOptions() => new()
30 | {
31 | BatchSize = 50_000,
32 | };
33 |
34 | ///
35 | protected override IAsyncEnumerable BulkInsertReturnEntities(
36 | bool sync,
37 | DbContext context,
38 | TableMetadata tableInfo,
39 | IEnumerable entities,
40 | OracleBulkInsertOptions options,
41 | OnConflictOptions? onConflict,
42 | CancellationToken ctk)
43 | {
44 | throw new NotSupportedException("Provider does not support returning entities.");
45 | }
46 |
47 | ///
48 | protected override Task BulkInsert(
49 | bool sync,
50 | DbContext context,
51 | TableMetadata tableInfo,
52 | IEnumerable entities,
53 | string tableName,
54 | IReadOnlyList columns,
55 | OracleBulkInsertOptions options,
56 | CancellationToken ctk)
57 | {
58 | var connection = (OracleConnection) context.Database.GetDbConnection();
59 |
60 | using var bulkCopy = new OracleBulkCopy(connection, options.CopyOptions);
61 |
62 | bulkCopy.DestinationTableName = tableInfo.QuotedTableName;
63 | bulkCopy.BatchSize = options.BatchSize;
64 | bulkCopy.BulkCopyTimeout = options.GetCopyTimeoutInSeconds();
65 |
66 | // Handle progress notifications
67 | if (options is { NotifyProgressAfter: not null, OnProgress: not null })
68 | {
69 | bulkCopy.NotifyAfter = options.NotifyProgressAfter.Value;
70 |
71 | bulkCopy.OracleRowsCopied += (sender, e) =>
72 | {
73 | options.OnProgress(e.RowsCopied);
74 |
75 | if (ctk.IsCancellationRequested)
76 | {
77 | e.Abort = true;
78 | }
79 | };
80 | }
81 |
82 | // If no progress notification is set, we still need to handle cancellation.
83 | else
84 | {
85 | bulkCopy.OracleRowsCopied += (sender, e) =>
86 | {
87 | if (ctk.IsCancellationRequested)
88 | {
89 | e.Abort = true;
90 | }
91 | };
92 | }
93 |
94 | foreach (var column in columns)
95 | {
96 | bulkCopy.ColumnMappings.Add(column.PropertyName, column.QuotedColumName);
97 | }
98 |
99 | var dataReader = new EnumerableDataReader(entities, columns, options);
100 |
101 | bulkCopy.WriteToServer(dataReader);
102 |
103 | return Task.CompletedTask;
104 | }
105 |
106 | ///
107 | protected override async Task DropTempTableAsync(bool sync, DbContext dbContext, string tableName)
108 | {
109 | var commandText = $"""
110 | BEGIN
111 | EXECUTE IMMEDIATE 'DROP TABLE {tableName}';
112 | EXCEPTION
113 | WHEN OTHERS THEN
114 | IF SQLCODE != -942 THEN -- ORA-00942: table or view does not exist
115 | RAISE;
116 | END IF;
117 | END;
118 | """;
119 |
120 | await ExecuteAsync(sync, dbContext, commandText, CancellationToken.None);
121 | }
122 | }
123 |
--------------------------------------------------------------------------------
/src/PhenX.EntityFrameworkCore.BulkInsert.Oracle/OracleDbContextOptionsExtensions.cs:
--------------------------------------------------------------------------------
1 | using Microsoft.EntityFrameworkCore;
2 |
3 | using PhenX.EntityFrameworkCore.BulkInsert.Extensions;
4 |
5 | namespace PhenX.EntityFrameworkCore.BulkInsert.Oracle;
6 |
7 | ///
8 | /// DbContext options extension for Oracle.
9 | ///
10 | public static class OracleDbContextOptionsExtensions
11 | {
12 | ///
13 | /// Configures the DbContext to use the Oracle bulk insert provider.
14 | ///
15 | public static DbContextOptionsBuilder UseBulkInsertOracle(this DbContextOptionsBuilder optionsBuilder)
16 | {
17 | return optionsBuilder.UseProvider();
18 | }
19 | }
20 |
--------------------------------------------------------------------------------
/src/PhenX.EntityFrameworkCore.BulkInsert.Oracle/OracleDialectBuilder.cs:
--------------------------------------------------------------------------------
1 | using System.Text;
2 |
3 | using Microsoft.EntityFrameworkCore;
4 |
5 | using PhenX.EntityFrameworkCore.BulkInsert.Dialect;
6 | using PhenX.EntityFrameworkCore.BulkInsert.Metadata;
7 | using PhenX.EntityFrameworkCore.BulkInsert.Options;
8 |
9 | namespace PhenX.EntityFrameworkCore.BulkInsert.Oracle;
10 |
11 | internal class OracleDialectBuilder : SqlDialectBuilder
12 | {
13 | protected override string OpenDelimiter => "\"";
14 | protected override string CloseDelimiter => "\"";
15 | protected override string ConcatOperator => "||";
16 |
17 | protected override bool SupportsMoveRows => false;
18 |
19 | public override string CreateTableCopySql(string tempTableName, TableMetadata tableInfo, IReadOnlyList columns)
20 | {
21 | return CreateTableCopySqlBase(tempTableName, columns);
22 | }
23 |
24 | public override string BuildMoveDataSql(
25 | DbContext context,
26 | TableMetadata target,
27 | string source,
28 | IReadOnlyList insertedColumns,
29 | IReadOnlyList returnedColumns,
30 | BulkInsertOptions options,
31 | OnConflictOptions? onConflict = null)
32 | {
33 | var q = new StringBuilder();
34 |
35 | // Merge handling
36 | if (onConflict is OnConflictOptions onConflictTyped)
37 | {
38 | IEnumerable matchColumns;
39 | if (onConflictTyped.Match != null)
40 | {
41 | matchColumns = GetColumns(target, onConflictTyped.Match);
42 | }
43 | else if (target.PrimaryKey.Length > 0)
44 | {
45 | matchColumns = target.PrimaryKey.Select(x => x.QuotedColumName);
46 | }
47 | else
48 | {
49 | throw new InvalidOperationException("Table has no primary key that can be used for conflict detection.");
50 | }
51 |
52 | q.AppendLine($"MERGE INTO {target.QuotedTableName} AS {PseudoTableInserted}");
53 |
54 | q.Append("USING (SELECT ");
55 | q.AppendColumns(insertedColumns);
56 | q.Append($" FROM {source}) AS {PseudoTableExcluded} (");
57 | q.AppendColumns(insertedColumns);
58 | q.AppendLine(")");
59 |
60 | q.Append("ON ");
61 | q.AppendJoin(" AND ", matchColumns, (b, col) => b.Append($"{PseudoTableInserted}.{col} = {PseudoTableExcluded}.{col}"));
62 | q.AppendLine();
63 |
64 | if (onConflictTyped.Update != null)
65 | {
66 | var columns = target.GetColumns(false);
67 |
68 | q.AppendLine("WHEN MATCHED THEN UPDATE SET ");
69 | q.AppendJoin(", ", GetUpdates(context, target, columns, onConflictTyped.Update));
70 | q.AppendLine();
71 | }
72 |
73 | q.Append("WHEN NOT MATCHED THEN INSERT (");
74 | q.AppendColumns(insertedColumns);
75 | q.AppendLine(")");
76 |
77 | q.Append("VALUES (");
78 | q.AppendJoin(", ", insertedColumns, (b, col) => b.Append($"{PseudoTableExcluded}.{col.QuotedColumName}"));
79 | q.AppendLine(")");
80 |
81 | if (returnedColumns.Count != 0)
82 | {
83 | q.Append("OUTPUT ");
84 | q.AppendJoin(", ", returnedColumns, (b, col) => b.Append($"{PseudoTableInserted}.{col.QuotedColumName} AS {col.QuotedColumName}"));
85 | q.AppendLine();
86 | }
87 | }
88 |
89 | // No conflict handling
90 | else
91 | {
92 | q.Append($"INSERT INTO {target.QuotedTableName} (");
93 | q.AppendColumns(insertedColumns);
94 | q.AppendLine(")");
95 | q.Append("SELECT ");
96 | q.AppendColumns(insertedColumns);
97 | q.AppendLine();
98 | q.Append($"FROM {source}");
99 | q.AppendLine();
100 |
101 | if (returnedColumns.Count != 0)
102 | {
103 | q.Append("RETURNING ");
104 | q.AppendJoin(", ", returnedColumns, (b, col) => b.Append(col.QuotedColumName));
105 | q.Append(" INTO ");
106 | q.AppendJoin(", ", returnedColumns, (b, col) => b.Append($":{col.ColumnName}"));
107 | q.AppendLine();
108 | }
109 | }
110 |
111 | q.AppendLine(";");
112 |
113 | return q.ToString();
114 | }
115 | }
116 |
--------------------------------------------------------------------------------
/src/PhenX.EntityFrameworkCore.BulkInsert.Oracle/PhenX.EntityFrameworkCore.BulkInsert.Oracle.csproj:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
--------------------------------------------------------------------------------
/src/PhenX.EntityFrameworkCore.BulkInsert.PostgreSql/IPostgresTypeProvider.cs:
--------------------------------------------------------------------------------
1 | using Microsoft.EntityFrameworkCore.Metadata;
2 |
3 | using NpgsqlTypes;
4 |
5 | namespace PhenX.EntityFrameworkCore.BulkInsert.PostgreSql;
6 |
7 | ///
8 | /// Provides the type to write.
9 | ///
10 | public interface IPostgresTypeProvider
11 | {
12 | ///
13 | /// Gets the type of a value before written to the output.
14 | ///
15 | /// The source property.
16 | /// The result type.
17 | /// Indicates if an object should be written.
18 | bool TryGetType(IProperty property, out NpgsqlDbType result);
19 | }
20 |
--------------------------------------------------------------------------------
/src/PhenX.EntityFrameworkCore.BulkInsert.PostgreSql/PhenX.EntityFrameworkCore.BulkInsert.PostgreSql.csproj:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
--------------------------------------------------------------------------------
/src/PhenX.EntityFrameworkCore.BulkInsert.PostgreSql/PostgreSqlBulkInsertOptions.cs:
--------------------------------------------------------------------------------
1 | using PhenX.EntityFrameworkCore.BulkInsert.Options;
2 |
3 | namespace PhenX.EntityFrameworkCore.BulkInsert.PostgreSql;
4 |
5 | ///
6 | /// Options specific to SQL Server bulk insert.
7 | ///
8 | public class PostgreSqlBulkInsertOptions : BulkInsertOptions
9 | {
10 | ///
11 | /// A list of type providers.
12 | ///
13 | public List? TypeProviders { get; set; }
14 | }
15 |
--------------------------------------------------------------------------------
/src/PhenX.EntityFrameworkCore.BulkInsert.PostgreSql/PostgreSqlBulkInsertProvider.cs:
--------------------------------------------------------------------------------
1 | using System.Text;
2 |
3 | using JetBrains.Annotations;
4 |
5 | using Microsoft.EntityFrameworkCore;
6 | using Microsoft.Extensions.Logging;
7 |
8 | using Npgsql;
9 | using Npgsql.EntityFrameworkCore.PostgreSQL.Storage.Internal.Mapping;
10 |
11 | using NpgsqlTypes;
12 |
13 | using PhenX.EntityFrameworkCore.BulkInsert.Metadata;
14 |
15 | namespace PhenX.EntityFrameworkCore.BulkInsert.PostgreSql;
16 |
17 | [UsedImplicitly]
18 | internal class PostgreSqlBulkInsertProvider(ILogger? logger) : BulkInsertProviderBase(logger)
19 | {
20 | //language=sql
21 | ///
22 | protected override string AddTableCopyBulkInsertId => $"ALTER TABLE {{0}} ADD COLUMN {BulkInsertId} SERIAL PRIMARY KEY;";
23 |
24 | private static string GetBinaryImportCommand(IReadOnlyList properties, string tableName)
25 | {
26 | var sql = new StringBuilder();
27 | sql.Append($"COPY {tableName} (");
28 | sql.AppendColumns(properties);
29 | sql.Append(") FROM STDIN (FORMAT BINARY)");
30 | return sql.ToString();
31 | }
32 |
33 | ///
34 | protected override PostgreSqlBulkInsertOptions CreateDefaultOptions() => new()
35 | {
36 | Converters = [PostgreSqlGeometryConverter.Instance],
37 | };
38 |
39 | ///
40 | protected override async Task BulkInsert(
41 | bool sync,
42 | DbContext context,
43 | TableMetadata tableInfo,
44 | IEnumerable entities,
45 | string tableName,
46 | IReadOnlyList columns,
47 | PostgreSqlBulkInsertOptions options,
48 | CancellationToken ctk)
49 | {
50 | var connection = (NpgsqlConnection)context.Database.GetDbConnection();
51 | var command = GetBinaryImportCommand(columns, tableName);
52 |
53 | var writer = sync
54 | // ReSharper disable once MethodHasAsyncOverloadWithCancellation
55 | ? connection.BeginBinaryImport(command)
56 | : await connection.BeginBinaryImportAsync(command, ctk);
57 |
58 | // The type mapping can be null for obvious types like string.
59 | var columnTypes = columns.Select(c => GetPostgreSqlType(c, options)).ToArray();
60 |
61 | long rowsCopied = 0;
62 | foreach (var entity in entities)
63 | {
64 | if (sync)
65 | {
66 | // ReSharper disable once MethodHasAsyncOverloadWithCancellation
67 | writer.StartRow();
68 | }
69 | else
70 | {
71 | await writer.StartRowAsync(ctk);
72 | }
73 |
74 | for (var columnIndex = 0; columnIndex < columns.Count; columnIndex++)
75 | {
76 | var value = columns[columnIndex].GetValue(entity, options);
77 |
78 | // Get the actual type, so that the writer can do the conversation to the target type automatically.
79 | var type = columnTypes[columnIndex];
80 |
81 | if (sync)
82 | {
83 | if (type != null)
84 | {
85 | // ReSharper disable once MethodHasAsyncOverloadWithCancellation
86 | writer.Write(value, type.Value);
87 | }
88 | else
89 | {
90 | // ReSharper disable once MethodHasAsyncOverloadWithCancellation
91 | writer.Write(value);
92 | }
93 | }
94 | else
95 | {
96 | if (type != null)
97 | {
98 | await writer.WriteAsync(value, type.Value, ctk);
99 | }
100 | else
101 | {
102 | await writer.WriteAsync(value, ctk);
103 | }
104 | }
105 | }
106 |
107 | options.HandleOnProgress(ref rowsCopied);
108 | }
109 |
110 | if (sync)
111 | {
112 | // ReSharper disable once MethodHasAsyncOverloadWithCancellation
113 | writer.Complete();
114 | // ReSharper disable once MethodHasAsyncOverload
115 | writer.Dispose();
116 | }
117 | else
118 | {
119 | await writer.CompleteAsync(ctk);
120 | await writer.DisposeAsync();
121 | }
122 | }
123 |
124 | private static NpgsqlDbType? GetPostgreSqlType(ColumnMetadata column, PostgreSqlBulkInsertOptions options)
125 | {
126 | var typeProviders = options.TypeProviders;
127 | if (typeProviders is { Count: > 0 })
128 | {
129 | foreach (var typeProvider in typeProviders)
130 | {
131 | if (typeProvider.TryGetType(column.Property, out var type))
132 | {
133 | return type;
134 | }
135 | }
136 | }
137 |
138 | var mapping = column.Property.GetRelationalTypeMapping() as NpgsqlTypeMapping;
139 |
140 | return mapping?.NpgsqlDbType;
141 | }
142 | }
143 |
--------------------------------------------------------------------------------
/src/PhenX.EntityFrameworkCore.BulkInsert.PostgreSql/PostgreSqlDbContextOptionsExtensions.cs:
--------------------------------------------------------------------------------
1 | using Microsoft.EntityFrameworkCore;
2 |
3 | using PhenX.EntityFrameworkCore.BulkInsert.Extensions;
4 |
5 | namespace PhenX.EntityFrameworkCore.BulkInsert.PostgreSql;
6 |
7 | ///
8 | /// DbContext options extension for PostgreSQL.
9 | ///
10 | public static class PostgreSqlDbContextOptionsExtensions
11 | {
12 | ///
13 | /// Configures the DbContext to use the PostgreSQL bulk insert provider.
14 | ///
15 | public static DbContextOptionsBuilder UseBulkInsertPostgreSql(this DbContextOptionsBuilder optionsBuilder)
16 | {
17 | return optionsBuilder.UseProvider();
18 | }
19 | }
20 |
--------------------------------------------------------------------------------
/src/PhenX.EntityFrameworkCore.BulkInsert.PostgreSql/PostgreSqlDialectBuilder.cs:
--------------------------------------------------------------------------------
1 | using System.Text;
2 |
3 | using PhenX.EntityFrameworkCore.BulkInsert.Dialect;
4 | using PhenX.EntityFrameworkCore.BulkInsert.Metadata;
5 | using PhenX.EntityFrameworkCore.BulkInsert.Options;
6 |
7 | namespace PhenX.EntityFrameworkCore.BulkInsert.PostgreSql;
8 |
9 | internal class PostgreSqlDialectBuilder : SqlDialectBuilder
10 | {
11 | protected override string OpenDelimiter => "\"";
12 | protected override string CloseDelimiter => "\"";
13 |
14 | public override string CreateTableCopySql(string tempNameName, TableMetadata tableInfo, IReadOnlyList columns)
15 | {
16 | return $"CREATE TEMPORARY TABLE {tempNameName} AS TABLE {tableInfo.QuotedTableName} WITH NO DATA;";
17 | }
18 |
19 | protected override void AppendConflictMatch(StringBuilder sql, TableMetadata target, OnConflictOptions conflict)
20 | {
21 | if (conflict.Match != null)
22 | {
23 | base.AppendConflictMatch(sql, target, conflict);
24 | }
25 | else if (target.PrimaryKey.Length > 0)
26 | {
27 | sql.Append(' ');
28 | sql.AppendLine("(");
29 | sql.AppendColumns(target.PrimaryKey);
30 | sql.AppendLine(")");
31 | }
32 | else
33 | {
34 | throw new InvalidOperationException("Table has no primary key that can be used for conflict detection.");
35 | }
36 | }
37 | }
38 |
--------------------------------------------------------------------------------
/src/PhenX.EntityFrameworkCore.BulkInsert.PostgreSql/PostgreSqlGeometryConverter.cs:
--------------------------------------------------------------------------------
1 | using NetTopologySuite.Geometries;
2 |
3 | using PhenX.EntityFrameworkCore.BulkInsert.Abstractions;
4 | using PhenX.EntityFrameworkCore.BulkInsert.Options;
5 |
6 | namespace PhenX.EntityFrameworkCore.BulkInsert.PostgreSql;
7 |
8 | internal sealed class PostgreSqlGeometryConverter : IBulkValueConverter
9 | {
10 | public static readonly PostgreSqlGeometryConverter Instance = new();
11 |
12 | private PostgreSqlGeometryConverter()
13 | {
14 | }
15 |
16 | public bool TryConvertValue(object source, BulkInsertOptions options, out object result)
17 | {
18 | if (source is Geometry geometry)
19 | {
20 | if (geometry.SRID != options.SRID)
21 | {
22 | geometry = geometry.Copy();
23 | geometry.SRID = options.SRID;
24 | }
25 |
26 | result = geometry;
27 | return true;
28 | }
29 |
30 | result = source;
31 | return false;
32 | }
33 | }
34 |
--------------------------------------------------------------------------------
/src/PhenX.EntityFrameworkCore.BulkInsert.SqlServer/PhenX.EntityFrameworkCore.BulkInsert.SqlServer.csproj:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
--------------------------------------------------------------------------------
/src/PhenX.EntityFrameworkCore.BulkInsert.SqlServer/SqlServerBulkInsertOptions.cs:
--------------------------------------------------------------------------------
1 | using Microsoft.Data.SqlClient;
2 |
3 | using PhenX.EntityFrameworkCore.BulkInsert.Options;
4 |
5 | namespace PhenX.EntityFrameworkCore.BulkInsert.SqlServer;
6 |
7 | ///
8 | /// Options specific to SQL Server bulk insert.
9 | ///
10 | public class SqlServerBulkInsertOptions : BulkInsertOptions
11 | {
12 | ///
13 | public SqlBulkCopyOptions CopyOptions { get; set; } = SqlBulkCopyOptions.Default;
14 |
15 | ///
16 | public bool EnableStreaming { get; set; } = false;
17 |
18 | }
19 |
--------------------------------------------------------------------------------
/src/PhenX.EntityFrameworkCore.BulkInsert.SqlServer/SqlServerBulkInsertProvider.cs:
--------------------------------------------------------------------------------
1 | using JetBrains.Annotations;
2 |
3 | using Microsoft.Data.SqlClient;
4 | using Microsoft.EntityFrameworkCore;
5 | using Microsoft.EntityFrameworkCore.Storage;
6 | using Microsoft.Extensions.Logging;
7 |
8 | using PhenX.EntityFrameworkCore.BulkInsert.Metadata;
9 |
10 | namespace PhenX.EntityFrameworkCore.BulkInsert.SqlServer;
11 |
12 | [UsedImplicitly]
13 | internal class SqlServerBulkInsertProvider(ILogger? logger) : BulkInsertProviderBase(logger)
14 | {
15 | //language=sql
16 | ///
17 | protected override string AddTableCopyBulkInsertId => $"ALTER TABLE {{0}} ADD {BulkInsertId} INT IDENTITY PRIMARY KEY;";
18 |
19 | ///
20 | protected override string GetTempTableName(string tableName) => $"#_temp_bulk_insert_{tableName}";
21 |
22 | protected override SqlServerBulkInsertOptions CreateDefaultOptions() => new()
23 | {
24 | BatchSize = 50_000,
25 | Converters = [SqlServerGeometryConverter.Instance]
26 | };
27 |
28 | ///
29 | protected override async Task BulkInsert(
30 | bool sync,
31 | DbContext context,
32 | TableMetadata tableInfo,
33 | IEnumerable entities,
34 | string tableName,
35 | IReadOnlyList columns,
36 | SqlServerBulkInsertOptions options,
37 | CancellationToken ctk)
38 | {
39 | var connection = (SqlConnection) context.Database.GetDbConnection();
40 | var sqlTransaction = context.Database.CurrentTransaction!.GetDbTransaction() as SqlTransaction;
41 |
42 | using var bulkCopy = new SqlBulkCopy(connection, options.CopyOptions, sqlTransaction);
43 |
44 | bulkCopy.DestinationTableName = tableName;
45 | bulkCopy.BatchSize = options.BatchSize;
46 | bulkCopy.BulkCopyTimeout = options.GetCopyTimeoutInSeconds();
47 | bulkCopy.EnableStreaming = options.EnableStreaming;
48 |
49 | // Handle progress notifications
50 | if (options is { NotifyProgressAfter: not null, OnProgress: not null })
51 | {
52 | bulkCopy.NotifyAfter = options.NotifyProgressAfter.Value;
53 |
54 | bulkCopy.SqlRowsCopied += (sender, e) =>
55 | {
56 | options.OnProgress(e.RowsCopied);
57 |
58 | if (ctk.IsCancellationRequested)
59 | {
60 | e.Abort = true;
61 | }
62 | };
63 | }
64 |
65 | // If no progress notification is set, we still need to handle cancellation.
66 | else
67 | {
68 | bulkCopy.SqlRowsCopied += (sender, e) =>
69 | {
70 | if (ctk.IsCancellationRequested)
71 | {
72 | e.Abort = true;
73 | }
74 | };
75 | }
76 |
77 | foreach (var column in columns)
78 | {
79 | bulkCopy.ColumnMappings.Add(column.PropertyName, column.ColumnName);
80 | }
81 |
82 | var dataReader = new EnumerableDataReader(entities, columns, options);
83 |
84 | if (sync)
85 | {
86 | // ReSharper disable once MethodHasAsyncOverloadWithCancellation
87 | bulkCopy.WriteToServer(dataReader);
88 | }
89 | else
90 | {
91 | await bulkCopy.WriteToServerAsync(dataReader, ctk);
92 | }
93 | }
94 | }
95 |
--------------------------------------------------------------------------------
/src/PhenX.EntityFrameworkCore.BulkInsert.SqlServer/SqlServerDbContextOptionsExtensions.cs:
--------------------------------------------------------------------------------
1 | using Microsoft.EntityFrameworkCore;
2 |
3 | using PhenX.EntityFrameworkCore.BulkInsert.Extensions;
4 |
5 | namespace PhenX.EntityFrameworkCore.BulkInsert.SqlServer;
6 |
7 | ///
8 | /// DbContext options extension for SQL Server.
9 | ///
10 | public static class SqlServerDbContextOptionsExtensions
11 | {
12 | ///
13 | /// Configures the DbContext to use the SQL Server bulk insert provider.
14 | ///
15 | public static DbContextOptionsBuilder UseBulkInsertSqlServer(this DbContextOptionsBuilder optionsBuilder)
16 | {
17 | return optionsBuilder.UseProvider();
18 | }
19 | }
20 |
--------------------------------------------------------------------------------
/src/PhenX.EntityFrameworkCore.BulkInsert.SqlServer/SqlServerDialectBuilder.cs:
--------------------------------------------------------------------------------
1 | using System.Text;
2 |
3 | using Microsoft.EntityFrameworkCore;
4 |
5 | using PhenX.EntityFrameworkCore.BulkInsert.Dialect;
6 | using PhenX.EntityFrameworkCore.BulkInsert.Metadata;
7 | using PhenX.EntityFrameworkCore.BulkInsert.Options;
8 |
9 | namespace PhenX.EntityFrameworkCore.BulkInsert.SqlServer;
10 |
11 | internal class SqlServerDialectBuilder : SqlDialectBuilder
12 | {
13 | protected override string OpenDelimiter => "[";
14 | protected override string CloseDelimiter => "]";
15 | protected override string ConcatOperator => "+";
16 |
17 | protected override bool SupportsMoveRows => false;
18 |
19 | public override string CreateTableCopySql(string tempTableName, TableMetadata tableInfo, IReadOnlyList columns)
20 | {
21 | return CreateTableCopySqlBase(tempTableName, columns);
22 | }
23 |
24 | protected override string Trim(string lhs) => $"TRIM({lhs})";
25 |
26 | public override string BuildMoveDataSql(
27 | DbContext context,
28 | TableMetadata target,
29 | string source,
30 | IReadOnlyList insertedColumns,
31 | IReadOnlyList returnedColumns,
32 | BulkInsertOptions options,
33 | OnConflictOptions? onConflict = null)
34 | {
35 | var q = new StringBuilder();
36 |
37 | var identityInsert = options.CopyGeneratedColumns && insertedColumns.Any(x => x.IsGenerated);
38 | if (identityInsert)
39 | {
40 | q.AppendLine($"SET IDENTITY_INSERT {target.QuotedTableName} ON;");
41 | }
42 |
43 | // Merge handling
44 | if (onConflict is OnConflictOptions onConflictTyped)
45 | {
46 | IEnumerable matchColumns;
47 | if (onConflictTyped.Match != null)
48 | {
49 | matchColumns = GetColumns(target, onConflictTyped.Match);
50 | }
51 | else if (target.PrimaryKey.Length > 0)
52 | {
53 | matchColumns = target.PrimaryKey.Select(x => x.QuotedColumName);
54 | }
55 | else
56 | {
57 | throw new InvalidOperationException("Table has no primary key that can be used for conflict detection.");
58 | }
59 |
60 | q.AppendLine($"MERGE INTO {target.QuotedTableName} AS {PseudoTableInserted}");
61 |
62 | q.Append("USING (SELECT ");
63 | q.AppendColumns(insertedColumns);
64 | q.Append($" FROM {source}) AS {PseudoTableExcluded} (");
65 | q.AppendColumns(insertedColumns);
66 | q.AppendLine(")");
67 |
68 | q.Append("ON ");
69 | q.AppendJoin(" AND ", matchColumns, (b, col) => b.Append($"{PseudoTableInserted}.{col} = {PseudoTableExcluded}.{col}"));
70 | q.AppendLine();
71 |
72 | if (onConflictTyped.Update != null)
73 | {
74 | var columns = target.GetColumns(false);
75 |
76 | q.AppendLine("WHEN MATCHED ");
77 |
78 | if (onConflictTyped.RawWhere != null || onConflictTyped.Where != null)
79 | {
80 | if (onConflictTyped is { RawWhere: not null, Where: not null })
81 | {
82 | throw new ArgumentException("Cannot specify both RawWhere and Where in OnConflictOptions.");
83 | }
84 |
85 | q.Append("AND ");
86 | AppendConflictCondition(q, target, context, onConflictTyped);
87 | }
88 |
89 | q.AppendLine("THEN UPDATE SET ");
90 | q.AppendJoin(", ", GetUpdates(context, target, columns, onConflictTyped.Update));
91 | q.AppendLine();
92 | }
93 |
94 | q.Append("WHEN NOT MATCHED THEN INSERT (");
95 | q.AppendColumns(insertedColumns);
96 | q.AppendLine(")");
97 |
98 | q.Append("VALUES (");
99 | q.AppendJoin(", ", insertedColumns, (b, col) => b.Append($"{PseudoTableExcluded}.{col.QuotedColumName}"));
100 | q.AppendLine(")");
101 |
102 | if (returnedColumns.Count != 0)
103 | {
104 | q.Append("OUTPUT ");
105 | q.AppendJoin(", ", returnedColumns, (b, col) => b.Append($"{PseudoTableInserted}.{col.QuotedColumName} AS {col.QuotedColumName}"));
106 | q.AppendLine();
107 | }
108 | }
109 |
110 | // No conflict handling
111 | else
112 | {
113 | q.Append($"INSERT INTO {target.QuotedTableName} (");
114 | q.AppendColumns(insertedColumns);
115 | q.AppendLine(")");
116 |
117 | if (returnedColumns.Count != 0)
118 | {
119 | q.Append("OUTPUT ");
120 | q.AppendJoin(", ", returnedColumns, (b, col) => b.Append($"{PseudoTableInserted}.{col.QuotedColumName} AS {col.QuotedColumName}"));
121 | q.AppendLine();
122 | }
123 |
124 | q.Append("SELECT ");
125 | q.AppendColumns(insertedColumns);
126 | q.AppendLine();
127 | q.Append($"FROM {source}");
128 | q.AppendLine();
129 | }
130 |
131 | q.AppendLine(";");
132 |
133 | if (identityInsert)
134 | {
135 | q.AppendLine($"SET IDENTITY_INSERT {target.QuotedTableName} OFF;");
136 | }
137 |
138 | return q.ToString();
139 | }
140 | }
141 |
--------------------------------------------------------------------------------
/src/PhenX.EntityFrameworkCore.BulkInsert.SqlServer/SqlServerGeometryConverter.cs:
--------------------------------------------------------------------------------
1 | using System.Data.SqlTypes;
2 |
3 | using Microsoft.SqlServer.Types;
4 |
5 | using NetTopologySuite.Geometries;
6 |
7 | using PhenX.EntityFrameworkCore.BulkInsert.Abstractions;
8 | using PhenX.EntityFrameworkCore.BulkInsert.Options;
9 |
10 | namespace PhenX.EntityFrameworkCore.BulkInsert.SqlServer;
11 |
12 | internal sealed class SqlServerGeometryConverter : IBulkValueConverter
13 | {
14 | public static readonly SqlServerGeometryConverter Instance = new();
15 |
16 | private SqlServerGeometryConverter()
17 | {
18 | }
19 |
20 | public bool TryConvertValue(object source, BulkInsertOptions options, out object result)
21 | {
22 | if (source is Geometry geometry)
23 | {
24 | var reversed = Reverse(geometry);
25 | result = SqlGeometry.STGeomFromWKB(new SqlBytes(reversed.AsBinary()), options.SRID);
26 | return true;
27 | }
28 |
29 | result = source;
30 | return false;
31 | }
32 |
33 | private static Geometry Reverse(Geometry input)
34 | {
35 | switch (input)
36 | {
37 | case Point point:
38 | return Reverse(point);
39 |
40 | case LineString lineString:
41 | return Reverse(lineString);
42 |
43 | case Polygon polygon:
44 | return Reverse(polygon);
45 |
46 | case MultiPoint multiPoint:
47 | return Reverse(multiPoint);
48 |
49 | case MultiLineString multiLineString:
50 | return Reverse(multiLineString);
51 |
52 | case MultiPolygon mpoly:
53 | return Reverse(mpoly);
54 |
55 | case GeometryCollection gc:
56 | return Reverse(gc);
57 |
58 | default:
59 | throw new NotSupportedException($"Unsupported geometry type: {input.GeometryType}");
60 | }
61 | }
62 |
63 | private static Point Reverse(Point input)
64 | {
65 | return input.Factory.CreatePoint(Swap(input.Coordinate));
66 | }
67 |
68 | private static LineString Reverse(LineString input)
69 | {
70 | return input.Factory.CreateLineString(Swap(input.Coordinates));
71 | }
72 |
73 | private static MultiPoint Reverse(MultiPoint input)
74 | {
75 | return input.Factory.CreateMultiPoint(input.Geometries.OfType().Select(Reverse).ToArray());
76 | }
77 |
78 | private static MultiLineString Reverse(MultiLineString input)
79 | {
80 | return input.Factory.CreateMultiLineString(input.Geometries.OfType().Select(Reverse).ToArray());
81 | }
82 |
83 | private static MultiPolygon Reverse(MultiPolygon input)
84 | {
85 | return input.Factory.CreateMultiPolygon(input.Geometries.OfType().Select(Reverse).ToArray());
86 | }
87 |
88 | private static GeometryCollection Reverse(GeometryCollection input)
89 | {
90 | return input.Factory.CreateGeometryCollection(input.Geometries.Select(Reverse).ToArray());
91 | }
92 |
93 | private static Polygon Reverse(Polygon input)
94 | {
95 | var factory = input.Factory;
96 |
97 | return input.Factory.CreatePolygon(
98 | factory.CreateLinearRing(Swap(input.Shell.Coordinates)),
99 | input.Holes.Select(h => factory.CreateLinearRing(Swap(h.Coordinates))).ToArray());
100 | }
101 |
102 | private static Coordinate Swap(Coordinate c) => new Coordinate(c.Y, c.X);
103 |
104 | private static Coordinate[] Swap(Coordinate[] coords) => coords.Select(Swap).ToArray();
105 |
106 | }
107 |
--------------------------------------------------------------------------------
/src/PhenX.EntityFrameworkCore.BulkInsert.Sqlite/PhenX.EntityFrameworkCore.BulkInsert.Sqlite.csproj:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
--------------------------------------------------------------------------------
/src/PhenX.EntityFrameworkCore.BulkInsert.Sqlite/SqliteBulkInsertProvider.cs:
--------------------------------------------------------------------------------
1 | using System.Data.Common;
2 | using System.Text;
3 |
4 | using JetBrains.Annotations;
5 |
6 | using Microsoft.Data.Sqlite;
7 | using Microsoft.EntityFrameworkCore;
8 | using Microsoft.Extensions.Logging;
9 |
10 | using PhenX.EntityFrameworkCore.BulkInsert.Metadata;
11 | using PhenX.EntityFrameworkCore.BulkInsert.Options;
12 |
13 | namespace PhenX.EntityFrameworkCore.BulkInsert.Sqlite;
14 |
15 | [UsedImplicitly]
16 | internal class SqliteBulkInsertProvider(ILogger? logger) : BulkInsertProviderBase(logger)
17 | {
18 | private const int MaxParams = 1000;
19 |
20 | ///
21 | protected override string BulkInsertId => "rowid";
22 |
23 | ///
24 | protected override string AddTableCopyBulkInsertId => "--"; // No need to add an ID column in SQLite
25 |
26 | ///
27 | protected override string GetTempTableName(string tableName) => $"_temp_bulk_insert_{Guid.NewGuid():N}";
28 |
29 | ///
30 | protected override BulkInsertOptions CreateDefaultOptions() => new()
31 | {
32 | BatchSize = 5,
33 | };
34 |
35 | ///
36 | protected override Task AddBulkInsertIdColumn(
37 | bool sync,
38 | DbContext context,
39 | string tempTableName,
40 | CancellationToken cancellationToken
41 | ) where T : class => Task.CompletedTask;
42 |
43 | private static SqliteType GetSqliteType(ColumnMetadata column)
44 | {
45 | var storeType = column.Property.GetRelationalTypeMapping().StoreType;
46 |
47 | if (string.Equals(storeType, "INTEGER", StringComparison.OrdinalIgnoreCase))
48 | {
49 | return SqliteType.Integer;
50 | }
51 |
52 | if (string.Equals(storeType, "FLOAT", StringComparison.OrdinalIgnoreCase) || string.Equals(storeType, "REAL", StringComparison.OrdinalIgnoreCase))
53 | {
54 | return SqliteType.Real;
55 | }
56 |
57 | if (string.Equals(storeType, "TEXT", StringComparison.OrdinalIgnoreCase))
58 | {
59 | return SqliteType.Text;
60 | }
61 |
62 | if (string.Equals(storeType, "BLOB", StringComparison.OrdinalIgnoreCase))
63 | {
64 | return SqliteType.Blob;
65 | }
66 |
67 | throw new NotSupportedException($"Invalid store type '{storeType}' for property '{column.PropertyName}'");
68 | }
69 |
70 | private static DbCommand GetInsertCommand(
71 | DbContext context,
72 | string tableName,
73 | IReadOnlyList columns,
74 | SqliteType[] columnTypes,
75 | StringBuilder sb,
76 | int batchSize)
77 | {
78 | var command = context.Database.GetDbConnection().CreateCommand();
79 |
80 | sb.Clear();
81 | sb.AppendLine($"INSERT INTO {tableName} (");
82 | sb.AppendColumns(columns);
83 | sb.AppendLine(")");
84 | sb.AppendLine("VALUES");
85 |
86 | var p = 0;
87 | for (var i = 0; i < batchSize; i++)
88 | {
89 | if (i > 0)
90 | {
91 | sb.Append(',');
92 | }
93 |
94 | sb.Append('(');
95 |
96 | var columnIndex = 0;
97 | for (var index = 0; index < columns.Count; index++)
98 | {
99 | var parameterName = $"@p{p++}";
100 | command.Parameters.Add(new SqliteParameter(parameterName, columnTypes[columnIndex]));
101 |
102 | if (columnIndex > 0)
103 | {
104 | sb.Append(", ");
105 | }
106 |
107 | sb.Append(parameterName);
108 | columnIndex++;
109 | }
110 |
111 | sb.Append(')');
112 | sb.AppendLine();
113 | }
114 |
115 | command.CommandText = sb.ToString();
116 | command.Prepare();
117 |
118 | return command;
119 | }
120 |
121 | ///
122 | protected override Task DropTempTableAsync(bool sync, DbContext dbContext, string tableName)
123 | {
124 | return ExecuteAsync(sync, dbContext, $"DROP TABLE IF EXISTS {tableName}", default);
125 | }
126 |
127 | ///
128 | protected override async Task BulkInsert(
129 | bool sync,
130 | DbContext context,
131 | TableMetadata tableInfo,
132 | IEnumerable entities,
133 | string tableName,
134 | IReadOnlyList columns,
135 | BulkInsertOptions options,
136 | CancellationToken ctk
137 | ) where T : class
138 | {
139 | var batchSize = Math.Min(options.BatchSize, MaxParams / columns.Count);
140 |
141 | long rowsCopied = 0;
142 |
143 | // The StringBuilder can be reused between the batches.
144 | var sb = new StringBuilder();
145 |
146 | var columnList = tableInfo.GetColumns(options.CopyGeneratedColumns);
147 | var columnTypes = columnList.Select(GetSqliteType).ToArray();
148 |
149 | DbCommand? insertCommand = null;
150 | try
151 | {
152 | foreach (var chunk in entities.Chunk(batchSize))
153 | {
154 | // Full chunks
155 | if (chunk.Length == batchSize)
156 | {
157 | insertCommand ??=
158 | GetInsertCommand(
159 | context,
160 | tableName,
161 | columnList,
162 | columnTypes,
163 | sb,
164 | batchSize);
165 |
166 | FillValues(chunk, insertCommand.Parameters, columns, options);
167 | await ExecuteCommand(sync, insertCommand, ctk);
168 | }
169 | // Last chunk
170 | else
171 | {
172 | await using var partialInsertCommand =
173 | GetInsertCommand(
174 | context,
175 | tableName,
176 | columnList,
177 | columnTypes,
178 | sb,
179 | chunk.Length);
180 |
181 | FillValues(chunk, partialInsertCommand.Parameters, columns, options);
182 | await ExecuteCommand(sync, partialInsertCommand, ctk);
183 | }
184 |
185 | // Notify progress after each chunk
186 | for (var i = 0; i < chunk.Length; i++)
187 | {
188 | options.HandleOnProgress(ref rowsCopied);
189 | }
190 | }
191 | }
192 | finally
193 | {
194 | if (insertCommand != null)
195 | {
196 | await insertCommand.DisposeAsync();
197 | }
198 | }
199 | }
200 |
201 | private static async Task ExecuteCommand(bool sync, DbCommand insertCommand, CancellationToken ctk)
202 | {
203 | if (sync)
204 | {
205 | // ReSharper disable once MethodHasAsyncOverloadWithCancellation
206 | insertCommand.ExecuteNonQuery();
207 | }
208 | else
209 | {
210 | await insertCommand.ExecuteNonQueryAsync(ctk);
211 | }
212 | }
213 |
214 | private static void FillValues(
215 | T[] chunk,
216 | DbParameterCollection parameters,
217 | IReadOnlyList columns,
218 | BulkInsertOptions options) where T : class
219 | {
220 | var p = 0;
221 |
222 | for (var chunkIndex = 0; chunkIndex < chunk.Length; chunkIndex++)
223 | {
224 | var entity = chunk[chunkIndex];
225 |
226 | for (var columnIndex = 0; columnIndex < columns.Count; columnIndex++)
227 | {
228 | var column = columns[columnIndex];
229 | var value = column.GetValue(entity, options);
230 | parameters[p].Value = value;
231 | p++;
232 | }
233 | }
234 | }
235 | }
236 |
237 |
--------------------------------------------------------------------------------
/src/PhenX.EntityFrameworkCore.BulkInsert.Sqlite/SqliteDbContextOptionsExtensions.cs:
--------------------------------------------------------------------------------
1 | using Microsoft.EntityFrameworkCore;
2 |
3 | using PhenX.EntityFrameworkCore.BulkInsert.Extensions;
4 |
5 | namespace PhenX.EntityFrameworkCore.BulkInsert.Sqlite;
6 |
7 | ///
8 | /// DbContext options extension for SQLite.
9 | ///
10 | public static class SqliteDbContextOptionsExtensions
11 | {
12 | ///
13 | /// Configures the DbContext to use the SQLite bulk insert provider.
14 | ///
15 | public static DbContextOptionsBuilder UseBulkInsertSqlite(this DbContextOptionsBuilder optionsBuilder)
16 | {
17 | return optionsBuilder.UseProvider();
18 | }
19 | }
20 |
21 |
--------------------------------------------------------------------------------
/src/PhenX.EntityFrameworkCore.BulkInsert.Sqlite/SqliteDialectBuilder.cs:
--------------------------------------------------------------------------------
1 | using PhenX.EntityFrameworkCore.BulkInsert.Dialect;
2 | using PhenX.EntityFrameworkCore.BulkInsert.Metadata;
3 |
4 | namespace PhenX.EntityFrameworkCore.BulkInsert.Sqlite;
5 |
6 | internal class SqliteDialectBuilder : SqlDialectBuilder
7 | {
8 | protected override string OpenDelimiter => "\"";
9 | protected override string CloseDelimiter => "\"";
10 |
11 | protected override bool SupportsMoveRows => false;
12 |
13 | ///
14 | public override string CreateTableCopySql(string tempNameName, TableMetadata tableInfo, IReadOnlyList columns)
15 | {
16 | return $"CREATE TEMP TABLE {tempNameName} AS SELECT * FROM {tableInfo.QuotedTableName} WHERE 0;";
17 | }
18 |
19 | protected override string Trim(string lhs) => $"TRIM({lhs})";
20 | }
21 |
--------------------------------------------------------------------------------
/src/PhenX.EntityFrameworkCore.BulkInsert/Abstractions/IBulkInsertProvider.cs:
--------------------------------------------------------------------------------
1 | using Microsoft.EntityFrameworkCore;
2 |
3 | using PhenX.EntityFrameworkCore.BulkInsert.Dialect;
4 | using PhenX.EntityFrameworkCore.BulkInsert.Metadata;
5 | using PhenX.EntityFrameworkCore.BulkInsert.Options;
6 |
7 | namespace PhenX.EntityFrameworkCore.BulkInsert.Abstractions;
8 |
9 | ///
10 | /// Internal bulk insert provider interface.
11 | ///
12 | internal interface IBulkInsertProvider
13 | {
14 | ///
15 | /// Calls the provider to perform a bulk insert operation.
16 | ///
17 | internal IAsyncEnumerable BulkInsertReturnEntities(
18 | bool sync,
19 | DbContext context,
20 | TableMetadata tableInfo,
21 | IEnumerable entities,
22 | BulkInsertOptions options,
23 | OnConflictOptions? onConflict = null,
24 | CancellationToken ctk = default
25 | ) where T : class;
26 |
27 | ///
28 | /// Calls the provider to perform a bulk insert operation without returning the inserted entities.
29 | ///
30 | internal Task BulkInsert(
31 | bool sync,
32 | DbContext context,
33 | TableMetadata tableInfo,
34 | IEnumerable entities,
35 | BulkInsertOptions options,
36 | OnConflictOptions? onConflict = null,
37 | CancellationToken ctk = default
38 | ) where T : class;
39 |
40 | SqlDialectBuilder SqlDialect { get; }
41 |
42 | ///
43 | /// Make the default options for the provider, can be a subclass of .
44 | ///
45 | internal BulkInsertOptions CreateDefaultOptions();
46 | }
47 |
--------------------------------------------------------------------------------
/src/PhenX.EntityFrameworkCore.BulkInsert/Abstractions/IBulkValueConverter.cs:
--------------------------------------------------------------------------------
1 | using PhenX.EntityFrameworkCore.BulkInsert.Options;
2 |
3 | namespace PhenX.EntityFrameworkCore.BulkInsert.Abstractions;
4 |
5 | ///
6 | /// Provide an interface to control how objects are written.
7 | ///
8 | public interface IBulkValueConverter
9 | {
10 | ///
11 | /// Converts a value before written to the output.
12 | ///
13 | /// The source object.
14 | /// The result type.
15 | /// The options.
16 | /// Indicates if an object should be written.
17 | bool TryConvertValue(object source, BulkInsertOptions options, out object result);
18 | }
19 |
--------------------------------------------------------------------------------
/src/PhenX.EntityFrameworkCore.BulkInsert/BulkInsertOptionsExtension.cs:
--------------------------------------------------------------------------------
1 | using Microsoft.EntityFrameworkCore.Infrastructure;
2 | using Microsoft.Extensions.DependencyInjection;
3 | using Microsoft.Extensions.DependencyInjection.Extensions;
4 | using Microsoft.Extensions.Logging;
5 | using Microsoft.Extensions.Logging.Abstractions;
6 |
7 | using PhenX.EntityFrameworkCore.BulkInsert.Abstractions;
8 |
9 | namespace PhenX.EntityFrameworkCore.BulkInsert;
10 |
11 | internal class BulkInsertOptionsExtension : IDbContextOptionsExtension
12 | where TProvider : class, IBulkInsertProvider
13 | {
14 | public DbContextOptionsExtensionInfo Info
15 | => new BulkInsertOptionsExtensionInfo(this);
16 |
17 | public void ApplyServices(IServiceCollection services)
18 | {
19 | services.TryAddSingleton(typeof(ILogger<>), typeof(NullLogger<>));
20 | services.AddSingleton();
21 | }
22 |
23 | public void Validate(IDbContextOptions options)
24 | {
25 | }
26 |
27 | private class BulkInsertOptionsExtensionInfo(IDbContextOptionsExtension extension) : DbContextOptionsExtensionInfo(extension)
28 | {
29 | ///
30 | public override int GetServiceProviderHashCode() => 0;
31 |
32 | ///
33 | public override bool ShouldUseSameServiceProvider(DbContextOptionsExtensionInfo other) => true;
34 |
35 | ///
36 | public override bool IsDatabaseProvider => false;
37 |
38 | ///
39 | public override string LogFragment => "BulkInsertOptionsExtension";
40 |
41 | ///
42 | public override void PopulateDebugInfo(IDictionary debugInfo)
43 | {
44 | }
45 | }
46 | }
47 |
--------------------------------------------------------------------------------
/src/PhenX.EntityFrameworkCore.BulkInsert/BulkInsertProviderUntyped.cs:
--------------------------------------------------------------------------------
1 | using Microsoft.EntityFrameworkCore;
2 |
3 | using PhenX.EntityFrameworkCore.BulkInsert.Abstractions;
4 | using PhenX.EntityFrameworkCore.BulkInsert.Dialect;
5 | using PhenX.EntityFrameworkCore.BulkInsert.Metadata;
6 | using PhenX.EntityFrameworkCore.BulkInsert.Options;
7 |
8 | namespace PhenX.EntityFrameworkCore.BulkInsert;
9 |
10 | internal abstract class BulkInsertProviderUntyped : IBulkInsertProvider
11 | where TDialect : SqlDialectBuilder, new()
12 | where TOptions : BulkInsertOptions, new()
13 | {
14 | protected readonly TDialect SqlDialect = new();
15 |
16 | SqlDialectBuilder IBulkInsertProvider.SqlDialect => SqlDialect;
17 |
18 | BulkInsertOptions IBulkInsertProvider.CreateDefaultOptions() => CreateDefaultOptions();
19 |
20 | ///
21 | /// Create the default options for the provider, can be a subclass of .
22 | ///
23 | protected abstract TOptions CreateDefaultOptions();
24 |
25 | public IAsyncEnumerable BulkInsertReturnEntities(
26 | bool sync,
27 | DbContext context,
28 | TableMetadata tableInfo,
29 | IEnumerable entities,
30 | BulkInsertOptions options,
31 | OnConflictOptions? onConflict,
32 | CancellationToken ctk) where T : class
33 | {
34 | if (options is not TOptions providerOptions)
35 | {
36 | throw new InvalidOperationException($"Invalid options type: {options.GetType().Name}. Expected: {typeof(TOptions).Name}");
37 | }
38 |
39 | if (entities.TryGetNonEnumeratedCount(out var count) && count == 0)
40 | {
41 | throw new InvalidOperationException("No entities to insert.");
42 | }
43 |
44 | return BulkInsertReturnEntities(sync, context, tableInfo, entities, providerOptions, onConflict, ctk);
45 | }
46 |
47 | protected abstract IAsyncEnumerable BulkInsertReturnEntities(
48 | bool sync,
49 | DbContext context,
50 | TableMetadata tableInfo,
51 | IEnumerable entities,
52 | TOptions options,
53 | OnConflictOptions? onConflict,
54 | CancellationToken ctk) where T : class;
55 |
56 | public Task BulkInsert(
57 | bool sync,
58 | DbContext context,
59 | TableMetadata tableInfo,
60 | IEnumerable entities,
61 | BulkInsertOptions options,
62 | OnConflictOptions? onConflict,
63 | CancellationToken ctk) where T : class
64 | {
65 | if (options is not TOptions providerOptions)
66 | {
67 | throw new InvalidOperationException($"Invalid options type: {options.GetType().Name}. Expected: {typeof(TOptions).Name}");
68 | }
69 |
70 | if (entities.TryGetNonEnumeratedCount(out var count) && count == 0)
71 | {
72 | throw new InvalidOperationException("No entities to insert.");
73 | }
74 |
75 | return BulkInsert(sync, context, tableInfo, entities, providerOptions, onConflict, ctk);
76 | }
77 |
78 | protected abstract Task BulkInsert(
79 | bool sync,
80 | DbContext context,
81 | TableMetadata tableInfo,
82 | IEnumerable entities,
83 | TOptions options,
84 | OnConflictOptions? onConflict,
85 | CancellationToken ctk) where T : class;
86 | }
87 |
--------------------------------------------------------------------------------
/src/PhenX.EntityFrameworkCore.BulkInsert/Dialect/RawSqlValue.cs:
--------------------------------------------------------------------------------
1 | namespace PhenX.EntityFrameworkCore.BulkInsert.Dialect;
2 |
3 | ///
4 | /// Represents a raw SQL value.
5 | ///
6 | ///
7 | public class RawSqlValue(string sql)
8 | {
9 | ///
10 | /// The raw SQL value.
11 | ///
12 | public string Sql { get; } = sql;
13 | }
14 |
--------------------------------------------------------------------------------
/src/PhenX.EntityFrameworkCore.BulkInsert/EnumerableDataReader.cs:
--------------------------------------------------------------------------------
1 | using System.Data;
2 |
3 | using PhenX.EntityFrameworkCore.BulkInsert.Metadata;
4 | using PhenX.EntityFrameworkCore.BulkInsert.Options;
5 |
6 | namespace PhenX.EntityFrameworkCore.BulkInsert;
7 |
8 | internal sealed class EnumerableDataReader(
9 | IEnumerable rows,
10 | IReadOnlyList columns,
11 | BulkInsertOptions options) : IDataReader
12 | {
13 | private readonly IEnumerator _enumerator = rows.GetEnumerator();
14 | private readonly Dictionary _ordinalMap =
15 | columns
16 | .Select((c, i) => (Column: c, Index: i))
17 | .ToDictionary(
18 | p => p.Column.PropertyName,
19 | p => p.Index
20 | );
21 |
22 | public object GetValue(int i)
23 | {
24 | var current = _enumerator.Current;
25 | if (current == null)
26 | {
27 | return DBNull.Value;
28 | }
29 |
30 | return columns[i].GetValue(current, options)!;
31 | }
32 |
33 | public int GetValues(object[] values)
34 | {
35 | var current = _enumerator.Current;
36 | if (current == null)
37 | {
38 | return 0;
39 | }
40 |
41 | for (var i = 0; i < columns.Count; i++)
42 | {
43 | values[i] = columns[i].GetValue(current, options)!;
44 | }
45 |
46 | return columns.Count;
47 | }
48 |
49 | public bool Read() => _enumerator.MoveNext();
50 |
51 | public Type GetFieldType(int i) => columns[i].ClrType;
52 |
53 | public int GetOrdinal(string name) => _ordinalMap.GetValueOrDefault(name, -1);
54 |
55 | public int FieldCount => columns.Count;
56 |
57 | public int Depth => 0;
58 |
59 | public int RecordsAffected => 0;
60 |
61 | public bool IsClosed => false;
62 |
63 |
64 | public void Close()
65 | {
66 | }
67 |
68 | public void Dispose()
69 | {
70 | _enumerator.Dispose();
71 | }
72 |
73 | public DataTable GetSchemaTable() => throw new NotImplementedException();
74 |
75 | public bool NextResult() => throw new NotImplementedException();
76 |
77 | public bool IsDBNull(int i) => GetValue(i) is DBNull;
78 |
79 | public object this[int i] => throw new NotImplementedException();
80 |
81 | public object this[string name] => throw new NotImplementedException();
82 |
83 | public string GetString(int i) => throw new NotImplementedException();
84 |
85 | public bool GetBoolean(int i) => throw new NotImplementedException();
86 |
87 | public byte GetByte(int i) => throw new NotImplementedException();
88 |
89 | public long GetBytes(int i, long fieldOffset, byte[]? buffer, int bufferoffset, int length) => throw new NotImplementedException();
90 |
91 | public char GetChar(int i) => throw new NotImplementedException();
92 |
93 | public long GetChars(int i, long fieldoffset, char[]? buffer, int bufferoffset, int length) => throw new NotImplementedException();
94 |
95 | public IDataReader GetData(int i) => throw new NotImplementedException();
96 |
97 | public string GetDataTypeName(int i) => throw new NotImplementedException();
98 |
99 | public DateTime GetDateTime(int i) => throw new NotImplementedException();
100 |
101 | public decimal GetDecimal(int i) => throw new NotImplementedException();
102 |
103 | public double GetDouble(int i) => throw new NotImplementedException();
104 |
105 | public float GetFloat(int i) => throw new NotImplementedException();
106 |
107 | public Guid GetGuid(int i) => throw new NotImplementedException();
108 |
109 | public short GetInt16(int i) => throw new NotImplementedException();
110 |
111 | public int GetInt32(int i) => throw new NotImplementedException();
112 |
113 | public long GetInt64(int i) => throw new NotImplementedException();
114 |
115 | public string GetName(int i) => throw new NotImplementedException();
116 | }
117 |
--------------------------------------------------------------------------------
/src/PhenX.EntityFrameworkCore.BulkInsert/Enums/ProviderType.cs:
--------------------------------------------------------------------------------
1 | namespace PhenX.EntityFrameworkCore.BulkInsert.Enums;
2 |
3 | ///
4 | /// Enumeration of supported database providers.
5 | ///
6 | public enum ProviderType
7 | {
8 | ///
9 | /// SQL Server provider.
10 | ///
11 | SqlServer,
12 |
13 | ///
14 | /// PostgreSQL provider.
15 | ///
16 | PostgreSql,
17 |
18 | ///
19 | /// SQLite provider.
20 | ///
21 | Sqlite,
22 |
23 | ///
24 | /// MySQL provider.
25 | ///
26 | MySql,
27 |
28 | ///
29 | /// Oracle provider.
30 | ///
31 | Oracle,
32 | }
33 |
--------------------------------------------------------------------------------
/src/PhenX.EntityFrameworkCore.BulkInsert/Extensions/InternalExtensions.cs:
--------------------------------------------------------------------------------
1 | using System.Data;
2 |
3 | using Microsoft.EntityFrameworkCore;
4 | using Microsoft.EntityFrameworkCore.Infrastructure;
5 |
6 | using PhenX.EntityFrameworkCore.BulkInsert.Abstractions;
7 | using PhenX.EntityFrameworkCore.BulkInsert.Metadata;
8 |
9 | using PhenX.EntityFrameworkCore.BulkInsert.Enums;
10 |
11 | namespace PhenX.EntityFrameworkCore.BulkInsert.Extensions;
12 |
13 | internal static class InternalExtensions
14 | {
15 | internal static TableMetadata GetTableInfo(this DbContext context)
16 | {
17 | var provider = context.GetService();
18 |
19 | return provider.GetTableInfo(context);
20 | }
21 |
22 | internal static DbContextOptionsBuilder UseProvider(this DbContextOptionsBuilder optionsBuilder)
23 | where TProvider : class, IBulkInsertProvider
24 | {
25 | ((IDbContextOptionsBuilderInfrastructure)optionsBuilder).AddOrUpdateExtension(
26 | optionsBuilder.Options.FindExtension>() ?? new());
27 |
28 | ((IDbContextOptionsBuilderInfrastructure)optionsBuilder).AddOrUpdateExtension(
29 | optionsBuilder.Options.FindExtension() ?? new());
30 |
31 | return optionsBuilder;
32 | }
33 |
34 | internal static async Task GetConnection(
35 | this DbContext context, bool sync, CancellationToken ctk = default)
36 | {
37 | var connection = context.Database.GetDbConnection();
38 | var wasClosed = connection.State == ConnectionState.Closed;
39 |
40 | if (wasClosed)
41 | {
42 | if (sync)
43 | {
44 | // ReSharper disable once MethodHasAsyncOverloadWithCancellation
45 | connection.Open();
46 | }
47 | else
48 | {
49 | await connection.OpenAsync(ctk);
50 | }
51 | }
52 |
53 | var wasBegan = true;
54 | var transaction = context.Database.CurrentTransaction;
55 |
56 | if (transaction == null)
57 | {
58 | wasBegan = false;
59 |
60 | if (sync)
61 | {
62 | // ReSharper disable once MethodHasAsyncOverloadWithCancellation
63 | transaction = context.Database.BeginTransaction();
64 | }
65 | else
66 | {
67 | transaction = await context.Database.BeginTransactionAsync(ctk);
68 | }
69 | }
70 |
71 | return new ConnectionInfo(connection, wasClosed, transaction, wasBegan);
72 | }
73 |
74 | ///
75 | /// Tells if the current provider is the specified provider type.
76 | ///
77 | internal static bool IsProvider(this DbContext context, params ProviderType[] providerType)
78 | {
79 | if (context.Database.ProviderName == null)
80 | {
81 | throw new InvalidOperationException("Database provider name is null.");
82 | }
83 |
84 | return providerType.Any(p => context.Database.ProviderName.Contains(p.ToString(), StringComparison.OrdinalIgnoreCase));
85 | }
86 | }
87 |
--------------------------------------------------------------------------------
/src/PhenX.EntityFrameworkCore.BulkInsert/Extensions/PublicExtensions.DbSet.cs:
--------------------------------------------------------------------------------
1 | using Microsoft.EntityFrameworkCore;
2 |
3 | using PhenX.EntityFrameworkCore.BulkInsert.Options;
4 |
5 | namespace PhenX.EntityFrameworkCore.BulkInsert.Extensions;
6 |
7 | public static partial class PublicExtensions
8 | {
9 | ///
10 | /// Executes a bulk insert operation returning the inserted/updated entities, from the DbSet (synchronous variant), with provider specific options.
11 | ///
12 | public static List ExecuteBulkInsertReturnEntities(
13 | this DbSet dbSet,
14 | IEnumerable entities,
15 | Action configure,
16 | OnConflictOptions? onConflict = null
17 | )
18 | where T : class
19 | where TOptions : BulkInsertOptions
20 | {
21 | return ExecuteBulkInsertReturnEntitiesCoreAsync(dbSet, true, entities, configure, onConflict, CancellationToken.None)
22 | .GetAwaiter().GetResult();
23 | }
24 |
25 | ///
26 | /// Executes a bulk insert operation returning the inserted/updated entities, from the DbSet (synchronous variant), with common options.
27 | ///
28 | public static List ExecuteBulkInsertReturnEntities(
29 | this DbSet dbSet,
30 | IEnumerable entities,
31 | Action configure,
32 | OnConflictOptions? onConflict = null
33 | )
34 | where T : class
35 | {
36 | return ExecuteBulkInsertReturnEntities(dbSet, entities, configure, onConflict);
37 | }
38 |
39 | ///
40 | /// Executes a bulk insert operation returning the inserted/updated entities, from the DbSet (synchronous variant), without options.
41 | ///
42 | public static List ExecuteBulkInsertReturnEntities(
43 | this DbSet dbSet,
44 | IEnumerable entities,
45 | OnConflictOptions? onConflict = null
46 | )
47 | where T : class
48 | {
49 | return ExecuteBulkInsertReturnEntities(dbSet, entities, _ => { }, onConflict);
50 | }
51 |
52 | ///
53 | /// Executes a bulk insert operation returning the inserted/updated entities, from the DbSet, with provider specific options.
54 | ///
55 | public static Task> ExecuteBulkInsertReturnEntitiesAsync(
56 | this DbSet dbSet,
57 | IEnumerable entities,
58 | Action configure,
59 | OnConflictOptions? onConflict = null,
60 | CancellationToken cancellationToken = default
61 | )
62 | where T : class
63 | where TOptions : BulkInsertOptions
64 | {
65 | return ExecuteBulkInsertReturnEntitiesCoreAsync(dbSet, false, entities, configure, onConflict, cancellationToken);
66 | }
67 |
68 | ///
69 | /// Executes a bulk insert operation returning the inserted/updated entities, from the DbSet, with common options.
70 | ///
71 | public static Task> ExecuteBulkInsertReturnEntitiesAsync(
72 | this DbSet dbSet,
73 | IEnumerable entities,
74 | Action configure,
75 | OnConflictOptions? onConflict = null,
76 | CancellationToken cancellationToken = default
77 | )
78 | where T : class
79 | {
80 | return ExecuteBulkInsertReturnEntitiesAsync(dbSet, entities, configure, onConflict, cancellationToken);
81 | }
82 |
83 | ///
84 | /// Executes a bulk insert operation returning the inserted/updated entities, from the DbSet, without options.
85 | ///
86 | public static Task> ExecuteBulkInsertReturnEntitiesAsync(
87 | this DbSet dbSet,
88 | IEnumerable entities,
89 | OnConflictOptions? onConflict = null,
90 | CancellationToken cancellationToken = default
91 | )
92 | where T : class
93 | {
94 | return ExecuteBulkInsertReturnEntitiesAsync(dbSet, entities, _ => { }, onConflict, cancellationToken);
95 | }
96 |
97 | ///
98 | /// Executes a bulk insert operation returning the inserted/updated entities, from the DbSet, with provider specific options.
99 | ///
100 | public static IAsyncEnumerable ExecuteBulkInsertReturnEnumerableAsync(
101 | this DbSet dbSet,
102 | IEnumerable entities,
103 | Action configure,
104 | OnConflictOptions? onConflict = null,
105 | CancellationToken cancellationToken = default
106 | )
107 | where T : class
108 | where TOptions : BulkInsertOptions
109 | {
110 | var (provider, context, options) = InitProvider(dbSet, configure);
111 |
112 | return provider.BulkInsertReturnEntities(false, context, dbSet.GetDbContext().GetTableInfo(), entities,
113 | options, onConflict, cancellationToken);
114 | }
115 |
116 | ///
117 | /// Executes a bulk insert operation returning the inserted/updated entities, from the DbSet, with common options.
118 | ///
119 | public static IAsyncEnumerable ExecuteBulkInsertReturnEnumerableAsync(
120 | this DbSet dbSet,
121 | IEnumerable entities,
122 | Action configure,
123 | OnConflictOptions? onConflict = null,
124 | CancellationToken cancellationToken = default
125 | )
126 | where T : class
127 | {
128 | return ExecuteBulkInsertReturnEnumerableAsync(dbSet, entities, configure, onConflict, cancellationToken);
129 | }
130 |
131 | ///
132 | /// Executes a bulk insert operation returning the inserted/updated entities, from the DbSet, without options.
133 | ///
134 | public static IAsyncEnumerable ExecuteBulkInsertReturnEnumerableAsync(
135 | this DbSet dbSet,
136 | IEnumerable entities,
137 | OnConflictOptions? onConflict = null,
138 | CancellationToken cancellationToken = default
139 | )
140 | where T : class
141 | {
142 | return ExecuteBulkInsertReturnEnumerableAsync(dbSet, entities, _ => { }, onConflict, cancellationToken);
143 | }
144 |
145 | ///
146 | /// Executes a bulk insert operation without returning the inserted/updated entities, from the DbSet, with provider specific options.
147 | ///
148 | public static async Task ExecuteBulkInsertAsync(
149 | this DbSet dbSet,
150 | IEnumerable entities,
151 | Action configure,
152 | OnConflictOptions? onConflict = null,
153 | CancellationToken cancellationToken = default
154 | )
155 | where T : class
156 | where TOptions : BulkInsertOptions
157 | {
158 | var (provider, context, options) = InitProvider(dbSet, configure);
159 |
160 | await provider.BulkInsert(false, context, dbSet.GetDbContext().GetTableInfo(), entities, options, onConflict,
161 | cancellationToken);
162 | }
163 |
164 | ///
165 | /// Executes a bulk insert operation without returning the inserted/updated entities, from the DbSet, with common options.
166 | ///
167 | public static async Task ExecuteBulkInsertAsync(
168 | this DbSet dbSet,
169 | IEnumerable entities,
170 | Action configure,
171 | OnConflictOptions? onConflict = null,
172 | CancellationToken cancellationToken = default
173 | )
174 | where T : class
175 | {
176 | await ExecuteBulkInsertAsync(dbSet, entities, configure, onConflict, cancellationToken);
177 | }
178 |
179 | ///
180 | /// Executes a bulk insert operation without returning the inserted/updated entities, from the DbSet, without options.
181 | ///
182 | public static async Task ExecuteBulkInsertAsync(
183 | this DbSet dbSet,
184 | IEnumerable entities,
185 | OnConflictOptions? onConflict = null,
186 | CancellationToken cancellationToken = default
187 | )
188 | where T : class
189 | {
190 | await ExecuteBulkInsertAsync(dbSet, entities, _ => { }, onConflict, cancellationToken);
191 | }
192 |
193 | ///
194 | /// Executes a bulk insert operation without returning the inserted/updated entities, from the DbSet (synchronous variant), with provider specific options.
195 | ///
196 | public static void ExecuteBulkInsert(
197 | this DbSet dbSet,
198 | IEnumerable entities,
199 | Action configure,
200 | OnConflictOptions? onConflict = null
201 | )
202 | where T : class
203 | where TOptions : BulkInsertOptions
204 | {
205 | var (provider, context, options) = InitProvider(dbSet, configure);
206 |
207 | provider.BulkInsert(true, context, dbSet.GetDbContext().GetTableInfo(), entities, options, onConflict)
208 | .GetAwaiter().GetResult();
209 | }
210 |
211 | ///
212 | /// Executes a bulk insert operation without returning the inserted/updated entities, from the DbSet (synchronous variant), with common options.
213 | ///
214 | public static void ExecuteBulkInsert(
215 | this DbSet dbSet,
216 | IEnumerable entities,
217 | Action configure,
218 | OnConflictOptions? onConflict = null
219 | )
220 | where T : class
221 | {
222 | ExecuteBulkInsert(dbSet, entities, configure, onConflict);
223 | }
224 |
225 | ///
226 | /// Executes a bulk insert operation without returning the inserted/updated entities, from the DbSet (synchronous variant), without options.
227 | ///
228 | public static void ExecuteBulkInsert(
229 | this DbSet dbSet,
230 | IEnumerable entities,
231 | OnConflictOptions? onConflict = null
232 | )
233 | where T : class
234 | {
235 | ExecuteBulkInsert(dbSet, entities, _ => { }, onConflict);
236 | }
237 | }
238 |
--------------------------------------------------------------------------------
/src/PhenX.EntityFrameworkCore.BulkInsert/Extensions/PublicExtensions.cs:
--------------------------------------------------------------------------------
1 | using Microsoft.EntityFrameworkCore;
2 | using Microsoft.EntityFrameworkCore.Infrastructure;
3 |
4 | using PhenX.EntityFrameworkCore.BulkInsert.Abstractions;
5 | using PhenX.EntityFrameworkCore.BulkInsert.Options;
6 |
7 | namespace PhenX.EntityFrameworkCore.BulkInsert.Extensions;
8 |
9 | ///
10 | /// DbSet extensions for bulk insert operations.
11 | ///
12 | public static partial class PublicExtensions
13 | {
14 | private static async Task> ExecuteBulkInsertReturnEntitiesCoreAsync(
15 | this DbSet dbSet,
16 | bool sync,
17 | IEnumerable entities,
18 | Action configure,
19 | OnConflictOptions? onConflict,
20 | CancellationToken ctk
21 | )
22 | where TEntity : class
23 | where TOptions : BulkInsertOptions
24 | {
25 | var (provider, context, options) = InitProvider(dbSet, configure);
26 |
27 | var enumerable = provider.BulkInsertReturnEntities(sync, context, dbSet.GetDbContext().GetTableInfo(), entities, options, onConflict, ctk);
28 |
29 | var result = new List();
30 | await foreach (var item in enumerable.WithCancellation(ctk))
31 | {
32 | result.Add(item);
33 | }
34 |
35 | return result;
36 | }
37 |
38 | private static DbContext GetDbContext(this DbSet dbSet) where T : class
39 | {
40 | IInfrastructure infrastructure = dbSet;
41 | return (infrastructure.Instance.GetService(typeof(ICurrentDbContext)) as ICurrentDbContext)!.Context;
42 | }
43 |
44 | private static (IBulkInsertProvider, DbContext, TOptions) InitProvider(
45 | DbSet dbSet,
46 | Action? configure
47 | )
48 | where T : class where TOptions : BulkInsertOptions
49 | {
50 | var context = dbSet.GetDbContext();
51 | var provider = context.GetService();
52 | var options = provider.CreateDefaultOptions();
53 | if (options is not TOptions castedOptions)
54 | {
55 | throw new InvalidOperationException($"Options type mismatch. Expected {options.GetType().Name}, but got {typeof(TOptions).Name}.");
56 | }
57 |
58 | configure?.Invoke(castedOptions);
59 |
60 | return (provider, context, castedOptions);
61 | }
62 | }
63 |
--------------------------------------------------------------------------------
/src/PhenX.EntityFrameworkCore.BulkInsert/Helpers.cs:
--------------------------------------------------------------------------------
1 | using System.Text;
2 |
3 | using PhenX.EntityFrameworkCore.BulkInsert.Metadata;
4 |
5 | namespace PhenX.EntityFrameworkCore.BulkInsert;
6 |
7 | internal static class Helpers
8 | {
9 | public static StringBuilder AppendJoin(this StringBuilder sb, string separator, IEnumerable items, Action formatter)
10 | {
11 | var first = true;
12 | foreach (var item in items)
13 | {
14 | if (!first)
15 | {
16 | sb.Append(separator);
17 | }
18 |
19 | formatter(sb, item);
20 | first = false;
21 | }
22 |
23 | return sb;
24 | }
25 |
26 | public static StringBuilder AppendColumns(this StringBuilder sb, IReadOnlyList columns)
27 | {
28 | return sb.AppendJoin(", ", columns.Select(c => c.QuotedColumName));
29 | }
30 | }
31 |
--------------------------------------------------------------------------------
/src/PhenX.EntityFrameworkCore.BulkInsert/Log.cs:
--------------------------------------------------------------------------------
1 | using Microsoft.Extensions.Logging;
2 |
3 | namespace PhenX.EntityFrameworkCore.BulkInsert;
4 |
5 | internal static partial class Log
6 | {
7 | [LoggerMessage(
8 | EventId = 1000,
9 | Level = LogLevel.Trace,
10 | Message = "Using temporary table to return data")]
11 | public static partial void UsingTempTableToReturnData(ILogger logger);
12 |
13 | [LoggerMessage(
14 | EventId = 1001,
15 | Level = LogLevel.Trace,
16 | Message = "Using temporary table to resolve conflicts")]
17 | public static partial void UsingTempTableToResolveConflicts(ILogger logger);
18 |
19 | [LoggerMessage(
20 | EventId = 1002,
21 | Level = LogLevel.Trace,
22 | Message = "Insert to table directly")]
23 | public static partial void UsingDirectInsert(ILogger logger);
24 |
25 | [LoggerMessage(
26 | EventId = 1003,
27 | Level = LogLevel.Error,
28 | Message = "Failed to drop temporary table.")]
29 | public static partial void DropTemporaryTableFailed(ILogger logger, Exception exception);
30 | }
31 |
--------------------------------------------------------------------------------
/src/PhenX.EntityFrameworkCore.BulkInsert/Metadata/ColumnMetadata.cs:
--------------------------------------------------------------------------------
1 | using Microsoft.EntityFrameworkCore;
2 | using Microsoft.EntityFrameworkCore.Metadata;
3 |
4 | using PhenX.EntityFrameworkCore.BulkInsert.Dialect;
5 | using PhenX.EntityFrameworkCore.BulkInsert.Options;
6 |
7 | namespace PhenX.EntityFrameworkCore.BulkInsert.Metadata;
8 |
9 | internal sealed class ColumnMetadata
10 | {
11 | public ColumnMetadata(IProperty property, SqlDialectBuilder dialect, IComplexProperty? complexProperty = null)
12 | {
13 | StoreObjectIdentifier? ownerTable = complexProperty != null
14 | ? StoreObjectIdentifier.Table(complexProperty.DeclaringType.GetTableName()!, complexProperty.DeclaringType.GetSchema())
15 | : null;
16 |
17 | _getter = BuildGetter(property, complexProperty);
18 | Property = property;
19 | PropertyName = property.Name;
20 | ColumnName = ownerTable == null ? property.GetColumnName() : property.GetColumnName(ownerTable.Value)!;
21 | QuotedColumName = dialect.Quote(ColumnName);
22 | StoreDefinition = GetStoreDefinition(property);
23 | ClrType = property.ClrType;
24 | IsGenerated = property.ValueGenerated != ValueGenerated.Never;
25 | }
26 |
27 | private readonly Func