├── .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 | [![NuGet](https://img.shields.io/nuget/v/PhenX.EntityFrameworkCore.BulkInsert.SqlServer.svg)](https://www.nuget.org/packages/PhenX.EntityFrameworkCore.BulkInsert.SqlServer) | 21 | | `PhenX.EntityFrameworkCore.BulkInsert.PostgreSql` | For PostgreSQL | [![NuGet](https://img.shields.io/nuget/v/PhenX.EntityFrameworkCore.BulkInsert.PostgreSql.svg)](https://www.nuget.org/packages/PhenX.EntityFrameworkCore.BulkInsert.PostgreSql) | 22 | | `PhenX.EntityFrameworkCore.BulkInsert.Sqlite` | For SQLite | [![NuGet](https://img.shields.io/nuget/v/PhenX.EntityFrameworkCore.BulkInsert.Sqlite.svg)](https://www.nuget.org/packages/PhenX.EntityFrameworkCore.BulkInsert.Sqlite) | 23 | | `PhenX.EntityFrameworkCore.BulkInsert.MySql` | For MySql | [![NuGet](https://img.shields.io/nuget/v/PhenX.EntityFrameworkCore.BulkInsert.Sqlite.svg)](https://www.nuget.org/packages/PhenX.EntityFrameworkCore.BulkInsert.MySql) | 24 | | `PhenX.EntityFrameworkCore.BulkInsert.Oracle` | For Oracle | [![NuGet](https://img.shields.io/nuget/v/PhenX.EntityFrameworkCore.BulkInsert.Oracle.svg)](https://www.nuget.org/packages/PhenX.EntityFrameworkCore.BulkInsert.Oracle) | 25 | | `PhenX.EntityFrameworkCore.BulkInsert` | Common library | [![NuGet](https://img.shields.io/nuget/v/PhenX.EntityFrameworkCore.BulkInsert.svg)](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 | ![bench-sqlserver.png](https://raw.githubusercontent.com/PhenX/PhenX.EntityFrameworkCore.BulkInsert/refs/heads/main/images/bench-sqlserver.png) 179 | 180 | PostgreSQL results with 500 000 rows : 181 | 182 | ![bench-postgresql.png](https://raw.githubusercontent.com/PhenX/PhenX.EntityFrameworkCore.BulkInsert/refs/heads/main/images/bench-postgresql.png) 183 | 184 | SQLite results with 500 000 rows : 185 | 186 | ![bench-sqlite.png](https://raw.githubusercontent.com/PhenX/PhenX.EntityFrameworkCore.BulkInsert/refs/heads/main/images/bench-sqlite.png) 187 | 188 | MySQL results with 500 000 rows : 189 | 190 | ![bench-mysql.png](https://raw.githubusercontent.com/PhenX/PhenX.EntityFrameworkCore.BulkInsert/refs/heads/main/images/bench-mysql.png) 191 | 192 | Oracle results with 500 000 rows : 193 | 194 | ![bench-oracle.png](https://raw.githubusercontent.com/PhenX/PhenX.EntityFrameworkCore.BulkInsert/refs/heads/main/images/bench-oracle.png) 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 _getter; 28 | 29 | public IProperty Property { get; } 30 | 31 | public string PropertyName { get; } 32 | 33 | public string ColumnName { get; } 34 | 35 | public string QuotedColumName { get; } 36 | 37 | public string StoreDefinition { get; } 38 | 39 | public Type ClrType { get; } 40 | 41 | public bool IsGenerated { get; } 42 | 43 | public object GetValue(object entity, BulkInsertOptions options) 44 | { 45 | var result = _getter(entity); 46 | 47 | if (options.Converters != null && result != null) 48 | { 49 | foreach (var converter in options.Converters) 50 | { 51 | if (converter.TryConvertValue(result, options, out var temp)) 52 | { 53 | result = temp; 54 | break; 55 | } 56 | } 57 | } 58 | 59 | return result ?? DBNull.Value; 60 | } 61 | 62 | private static Func BuildGetter(IProperty property, IComplexProperty? complexProperty) 63 | { 64 | var valueConverter = 65 | property.GetValueConverter() ?? 66 | property.GetTypeMapping().Converter; 67 | 68 | var propInfo = property.PropertyInfo!; 69 | 70 | return PropertyAccessor.CreateGetter(propInfo, complexProperty, valueConverter?.ConvertToProviderExpression); 71 | } 72 | 73 | private static string GetStoreDefinition(IProperty property) 74 | { 75 | var typeMapping = property.GetRelationalTypeMapping(); 76 | 77 | var nullability = property.IsNullable ? "NULL" : "NOT NULL"; 78 | 79 | return $"{typeMapping.StoreType} {nullability}"; 80 | } 81 | 82 | public override string ToString() 83 | { 84 | return $"Name: {PropertyName}, Column: {ColumnName}"; 85 | } 86 | } 87 | -------------------------------------------------------------------------------- /src/PhenX.EntityFrameworkCore.BulkInsert/Metadata/ConnectionInfo.cs: -------------------------------------------------------------------------------- 1 | using System.Data.Common; 2 | 3 | using Microsoft.EntityFrameworkCore.Storage; 4 | 5 | namespace PhenX.EntityFrameworkCore.BulkInsert.Metadata; 6 | 7 | internal readonly record struct ConnectionInfo(DbConnection Connection, bool WasClosed, IDbContextTransaction Transaction, bool WasBegan) 8 | { 9 | public async Task Commit(bool sync, CancellationToken ctk) 10 | { 11 | if (!WasBegan) 12 | { 13 | if (sync) 14 | { 15 | // ReSharper disable once MethodHasAsyncOverloadWithCancellation 16 | Transaction.Commit(); 17 | } 18 | else 19 | { 20 | await Transaction.CommitAsync(ctk); 21 | } 22 | } 23 | } 24 | 25 | public async Task Close(bool sync, CancellationToken ctk) 26 | { 27 | if (!WasBegan) 28 | { 29 | if (sync) 30 | { 31 | // ReSharper disable once MethodHasAsyncOverloadWithCancellation 32 | Transaction.Dispose(); 33 | } 34 | else 35 | { 36 | await Transaction.DisposeAsync(); 37 | } 38 | } 39 | 40 | if (WasClosed) 41 | { 42 | if (sync) 43 | { 44 | // ReSharper disable once MethodHasAsyncOverload 45 | Connection.Close(); 46 | } 47 | else 48 | { 49 | await Connection.CloseAsync(); 50 | } 51 | } 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /src/PhenX.EntityFrameworkCore.BulkInsert/Metadata/MetadataProvider.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.EntityFrameworkCore; 2 | using Microsoft.EntityFrameworkCore.Infrastructure; 3 | 4 | using PhenX.EntityFrameworkCore.BulkInsert.Abstractions; 5 | 6 | namespace PhenX.EntityFrameworkCore.BulkInsert.Metadata; 7 | 8 | internal sealed class MetadataProvider 9 | { 10 | private Dictionary> _tablesPerContext = new(); 11 | 12 | public TableMetadata GetTableInfo(DbContext context) 13 | { 14 | var tables = GetTables(context); 15 | 16 | if (!tables.TryGetValue(typeof(T), out var table)) 17 | { 18 | throw new InvalidOperationException($"Cannot find metadata for type '{typeof(T)}'."); 19 | } 20 | 21 | return table; 22 | } 23 | 24 | private Dictionary GetTables(DbContext context) 25 | { 26 | lock (_tablesPerContext) 27 | { 28 | var type = context.GetType(); 29 | if (_tablesPerContext.TryGetValue(context.GetType(), out var tables)) 30 | { 31 | return tables; 32 | } 33 | 34 | var provider = context.GetService(); 35 | 36 | tables = 37 | context.Model.GetEntityTypes() 38 | .ToDictionary( 39 | x => x.ClrType, 40 | x => new TableMetadata(x, provider.SqlDialect)); 41 | 42 | _tablesPerContext[type] = tables; 43 | 44 | return tables; 45 | } 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /src/PhenX.EntityFrameworkCore.BulkInsert/Metadata/MetadataProviderExtension.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.EntityFrameworkCore.Infrastructure; 2 | using Microsoft.Extensions.DependencyInjection; 3 | 4 | namespace PhenX.EntityFrameworkCore.BulkInsert.Metadata; 5 | 6 | internal class MetadataProviderExtension : IDbContextOptionsExtension 7 | { 8 | public DbContextOptionsExtensionInfo Info 9 | => new MetadataProviderExtensionInfo(this); 10 | 11 | public void ApplyServices(IServiceCollection services) 12 | { 13 | services.AddSingleton(); 14 | } 15 | 16 | public void Validate(IDbContextOptions options) 17 | { 18 | } 19 | 20 | private class MetadataProviderExtensionInfo(IDbContextOptionsExtension extension) : DbContextOptionsExtensionInfo(extension) 21 | { 22 | 23 | /// 24 | public override int GetServiceProviderHashCode() => 0; 25 | 26 | /// 27 | public override bool ShouldUseSameServiceProvider(DbContextOptionsExtensionInfo other) => true; 28 | 29 | /// 30 | public override bool IsDatabaseProvider => false; 31 | 32 | /// 33 | public override string LogFragment => "MetadataProviderExtension"; 34 | 35 | /// 36 | public override void PopulateDebugInfo(IDictionary debugInfo) 37 | { 38 | } 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /src/PhenX.EntityFrameworkCore.BulkInsert/Metadata/PropertyAccessor.cs: -------------------------------------------------------------------------------- 1 | using System.Linq.Expressions; 2 | using System.Reflection; 3 | 4 | using Microsoft.EntityFrameworkCore.Metadata; 5 | 6 | namespace PhenX.EntityFrameworkCore.BulkInsert.Metadata; 7 | 8 | internal static class PropertyAccessor 9 | { 10 | public static Func CreateGetter( 11 | PropertyInfo propertyInfo, 12 | IComplexProperty? complexProperty = null, 13 | LambdaExpression? converter = null) 14 | { 15 | ArgumentNullException.ThrowIfNull(propertyInfo); 16 | 17 | // instance => { } 18 | var instanceParam = Expression.Parameter(typeof(object), "instance"); 19 | 20 | Expression body; 21 | 22 | if (complexProperty == null) 23 | { 24 | var propDeclaringType = propertyInfo.DeclaringType!; 25 | 26 | // Convert object to the declaring type 27 | var typedInstance = GetTypedInstance(propDeclaringType, instanceParam); 28 | 29 | // instance => ((TEntity)instance).Property 30 | body = Expression.Property(typedInstance, propertyInfo); 31 | } 32 | else 33 | { 34 | // Nested access: ((TEntity)instance).ComplexProp.Property 35 | var complexPropInfo = complexProperty.PropertyInfo!; 36 | var complexPropDeclaringType = complexPropInfo.DeclaringType!; 37 | 38 | var typedInstance = GetTypedInstance(complexPropDeclaringType, instanceParam); 39 | 40 | // instance => ((TEntity)instance).ComplexProp 41 | Expression complexAccess = Expression.Property(typedInstance, complexPropInfo); 42 | 43 | // instance => ((TEntity)instance).ComplexProp.Property 44 | body = Expression.Property(complexAccess, propertyInfo); 45 | } 46 | 47 | // If the converter is provided, we call it 48 | if (converter != null) 49 | { 50 | // Validate the converter input type matches property type 51 | var converterParamType = converter.Parameters[0].Type; 52 | if (!converterParamType.IsAssignableFrom(body.Type) && !body.Type.IsAssignableFrom(converterParamType)) 53 | { 54 | throw new ArgumentException($"Converter input must be assignable from property type ({body.Type} -> {converterParamType})"); 55 | } 56 | 57 | Expression converterInput = body; 58 | if (converterParamType != body.Type) 59 | { 60 | // instance => converter((TConverterType)body) 61 | converterInput = Expression.Convert(body, converterParamType); 62 | } 63 | 64 | // instance => converter(body) 65 | var invokeConverter = Expression.Invoke(converter, converterInput); 66 | 67 | if (body.Type.IsClass) 68 | { 69 | // instance => body == null ? null : converter(body) 70 | var nullCondition = Expression.Equal(body, Expression.Constant(null, body.Type)); 71 | var nullResult = Expression.Constant(null, invokeConverter.Type); 72 | 73 | body = Expression.Condition(nullCondition, nullResult, invokeConverter); 74 | } 75 | else 76 | { 77 | body = invokeConverter; 78 | } 79 | } 80 | 81 | var finalExpression = body.Type.IsValueType 82 | ? Expression.Convert(body, typeof(object)) 83 | : body; 84 | 85 | return Expression.Lambda>(finalExpression, instanceParam).Compile(); 86 | } 87 | 88 | private static UnaryExpression GetTypedInstance(Type propDeclaringType, ParameterExpression instanceParam) 89 | { 90 | return propDeclaringType.IsValueType 91 | ? Expression.Unbox(instanceParam, propDeclaringType) 92 | : Expression.Convert(instanceParam, propDeclaringType); 93 | } 94 | } 95 | -------------------------------------------------------------------------------- /src/PhenX.EntityFrameworkCore.BulkInsert/Metadata/TableMetadata.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.EntityFrameworkCore; 2 | using Microsoft.EntityFrameworkCore.Metadata; 3 | 4 | using PhenX.EntityFrameworkCore.BulkInsert.Dialect; 5 | 6 | namespace PhenX.EntityFrameworkCore.BulkInsert.Metadata; 7 | 8 | internal sealed class TableMetadata 9 | { 10 | private ColumnMetadata[]? _notGeneratedColumns; 11 | private ColumnMetadata[]? _primaryKeys; 12 | 13 | private readonly IEntityType _entityType; 14 | 15 | public string QuotedTableName { get; } 16 | 17 | public string TableName { get; } 18 | 19 | private ColumnMetadata[] Columns { get; } 20 | 21 | public TableMetadata(IEntityType entityType, SqlDialectBuilder dialect) 22 | { 23 | _entityType = entityType; 24 | TableName = entityType.GetTableName() ?? throw new InvalidOperationException("Cannot determine table name."); 25 | QuotedTableName = dialect.QuoteTableName(entityType.GetSchema(), TableName); 26 | Columns = GetColumns(entityType, dialect); 27 | } 28 | 29 | private static ColumnMetadata[] GetColumns(IEntityType entityType, SqlDialectBuilder dialect) 30 | { 31 | var properties = entityType.GetProperties() 32 | .Where(p => !p.IsShadowProperty()) 33 | .Select(x => new ColumnMetadata(x, dialect)); 34 | 35 | var complexProperties = entityType.GetComplexProperties() 36 | .SelectMany(cp => cp.ComplexType 37 | .GetProperties() 38 | .Where(p => !p.IsShadowProperty()) 39 | .Select(x => new ColumnMetadata(x, dialect, cp))); 40 | 41 | return properties.Concat(complexProperties).ToArray(); 42 | } 43 | 44 | public ColumnMetadata[] PrimaryKey => _primaryKeys ??= GetPrimaryKey(); 45 | 46 | public ColumnMetadata[] GetColumns(bool includeGenerated = true) 47 | { 48 | if (includeGenerated) 49 | { 50 | return Columns; 51 | } 52 | 53 | return _notGeneratedColumns ??= Columns.Where(x => !x.IsGenerated).ToArray(); 54 | } 55 | 56 | public string GetQuotedColumnName(string propertyName) 57 | { 58 | var property = Columns.FirstOrDefault(x => x.PropertyName == propertyName) 59 | ?? throw new InvalidOperationException($"Property {propertyName} not found in entity type {_entityType.Name}."); 60 | 61 | return property.QuotedColumName; 62 | } 63 | 64 | public string GetColumnName(string propertyName) 65 | { 66 | var property = Columns.FirstOrDefault(x => x.PropertyName == propertyName) 67 | ?? throw new InvalidOperationException($"Property {propertyName} not found in entity type {_entityType.Name}."); 68 | 69 | return property.ColumnName; 70 | } 71 | 72 | private ColumnMetadata[] GetPrimaryKey() 73 | { 74 | var primaryKey = _entityType.FindPrimaryKey()?.Properties ?? []; 75 | 76 | return Columns 77 | .Where(x => primaryKey.Any(y => x.PropertyName == y.Name)) 78 | .ToArray(); 79 | } 80 | } 81 | -------------------------------------------------------------------------------- /src/PhenX.EntityFrameworkCore.BulkInsert/Options/BulkInsertOptions.cs: -------------------------------------------------------------------------------- 1 | using PhenX.EntityFrameworkCore.BulkInsert.Abstractions; 2 | 3 | namespace PhenX.EntityFrameworkCore.BulkInsert.Options; 4 | 5 | /// 6 | /// Bulk insert general options. 7 | /// 8 | public class BulkInsertOptions 9 | { 10 | /// 11 | /// Progress callback delegate to notify about the number of rows copied. 12 | /// 13 | public delegate void ProgressCallback(long rowsCopied); 14 | 15 | /// 16 | /// Move rows between tables instead of inserting them. 17 | /// Only supported for PostgreSQL. 18 | /// 19 | public bool MoveRows { get; set; } 20 | 21 | /// 22 | /// Batch size for bulk insert. 23 | /// 24 | /// 25 | /// Default values 26 | /// 27 | /// 28 | /// PostgreSQL 29 | /// N/A 30 | /// 31 | /// 32 | /// SQL Server 33 | /// 50 000 34 | /// 35 | /// 36 | /// SQLite 37 | /// 5 38 | /// 39 | /// 40 | /// Oracle 41 | /// 50 000 42 | /// 43 | /// 44 | /// 45 | public int BatchSize { get; set; } 46 | 47 | /// 48 | /// Indicates if also generated columns should be copied. This is useful for upsert operations. 49 | /// 50 | public bool CopyGeneratedColumns { get; set; } 51 | 52 | /// 53 | /// The timeout to copy records. 54 | /// 55 | public TimeSpan CopyTimeout { get; set; } = TimeSpan.FromMinutes(10); 56 | 57 | /// 58 | /// The value converters. 59 | /// 60 | public List? Converters { get; set; } 61 | 62 | /// 63 | /// Sets the ID of the Spatial Reference System used by the Geometries to be inserted. 64 | /// 65 | public int SRID { get; set; } = 4326; 66 | 67 | /// 68 | /// Number of rows after which the progress callback is invoked. 69 | /// 70 | public int? NotifyProgressAfter { get; set; } 71 | 72 | /// 73 | /// Callback to notify about the progress of the bulk insert operation. 74 | /// 75 | public ProgressCallback? OnProgress { get; set; } 76 | 77 | internal int GetCopyTimeoutInSeconds() 78 | { 79 | return Math.Max(0, (int)CopyTimeout.TotalSeconds); 80 | } 81 | 82 | internal void HandleOnProgress(ref long rowsCopied) 83 | { 84 | rowsCopied++; 85 | 86 | if (OnProgress == null || NotifyProgressAfter == null || NotifyProgressAfter <= 0 || rowsCopied % NotifyProgressAfter != 0) 87 | { 88 | return; 89 | } 90 | 91 | OnProgress(rowsCopied); 92 | } 93 | } 94 | -------------------------------------------------------------------------------- /src/PhenX.EntityFrameworkCore.BulkInsert/Options/OnConflictOptions.cs: -------------------------------------------------------------------------------- 1 | using System.Linq.Expressions; 2 | 3 | namespace PhenX.EntityFrameworkCore.BulkInsert.Options; 4 | 5 | /// 6 | /// Conflict options for bulk insert. 7 | /// 8 | public abstract class OnConflictOptions 9 | { 10 | /// 11 | /// Raw SQL condition delegate to match on conflict. 12 | /// 13 | public delegate string RawWhereDelegate(string insertedTable, string excludedTable); 14 | 15 | /// 16 | /// Optional condition to apply on conflict, in raw SQL. 17 | /// The table names provided as parameters can be used to reference data : 18 | /// 19 | /// insertedTable: refers to the data already in the target table. 20 | /// excludedTable: refers to the new data, being in conflict. 21 | /// 22 | /// 23 | public RawWhereDelegate? RawWhere { get; set; } 24 | } 25 | 26 | /// 27 | /// Conflict options for bulk insert, for a specific entity type. 28 | /// 29 | /// 30 | public class OnConflictOptions : OnConflictOptions 31 | { 32 | /// 33 | /// Columns to match on conflict. 34 | /// 35 | /// Match = (inserted) => new { inserted.Id } // Match on the Id column 36 | /// 37 | /// 38 | public Expression>? Match { get; set; } 39 | 40 | /// 41 | /// Updates to apply on conflict. 42 | /// 43 | /// Update = (inserted, excluded) => new { inserted.Quantity = excluded.Quantity } // Update the Quantity column 44 | /// 45 | /// 46 | public Expression>? Update { get; set; } 47 | 48 | /// 49 | /// Condition to apply on conflict, with an expression. 50 | /// 51 | /// Where = (inserted, excluded) => inserted.Price > excluded.Price 52 | /// 53 | /// 54 | public Expression>? Where { get; set; } 55 | } 56 | -------------------------------------------------------------------------------- /src/PhenX.EntityFrameworkCore.BulkInsert/PhenX.EntityFrameworkCore.BulkInsert.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | -------------------------------------------------------------------------------- /src/PhenX.EntityFrameworkCore.BulkInsert/Telemetry.cs: -------------------------------------------------------------------------------- 1 | using System.Diagnostics; 2 | 3 | namespace PhenX.EntityFrameworkCore.BulkInsert; 4 | 5 | /// 6 | /// Utility class for telemetry. 7 | /// 8 | public static class Telemetry 9 | { 10 | /// 11 | /// The activity source. 12 | /// 13 | public static readonly ActivitySource ActivitySource = new("PhenX.EntityFrameworkCore.BulkInsert"); 14 | } 15 | -------------------------------------------------------------------------------- /tests/Directory.Build.props: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | net8.0;net9.0 5 | 12 6 | enable 7 | enable 8 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /tests/PhenX.EntityFrameworkCore.BulkInsert.Benchmark/GetValueComparator.IlGetter.cs: -------------------------------------------------------------------------------- 1 | using System.Reflection; 2 | using System.Reflection.Emit; 3 | 4 | namespace PhenX.EntityFrameworkCore.BulkInsert.Benchmark; 5 | 6 | public partial class GetValueComparator 7 | { 8 | public static Func CreateUntypedGetter(PropertyInfo propertyInfo, Type sourceType, Type valueType) 9 | { 10 | var method = 11 | typeof(GetValueComparator).GetMethod(nameof(CreateInternalUntypedGetter), BindingFlags.NonPublic | BindingFlags.Static)! 12 | .MakeGenericMethod(sourceType, valueType); 13 | 14 | return (Func)method.Invoke(null, [propertyInfo])!; 15 | } 16 | 17 | private static Func CreateInternalUntypedGetter(PropertyInfo propertyInfo) 18 | { 19 | var getter = CreateGetter(propertyInfo); 20 | 21 | return source => getter((TSource)source!); 22 | } 23 | 24 | public static Func CreateGetter(PropertyInfo propertyInfo) 25 | { 26 | if (!propertyInfo.CanRead) 27 | { 28 | return x => throw new NotSupportedException(); 29 | } 30 | 31 | var bakingField = 32 | propertyInfo.DeclaringType!.GetField($"<{propertyInfo.Name}>k__BackingField", 33 | BindingFlags.NonPublic | 34 | BindingFlags.Instance); 35 | 36 | var propertyGetMethod = propertyInfo.GetGetMethod()!; 37 | 38 | var getMethod = new DynamicMethod(propertyGetMethod.Name, typeof(TValue), [typeof(TSource)], true); 39 | var getGenerator = getMethod.GetILGenerator(); 40 | 41 | // Load this to stack. 42 | getGenerator.Emit(OpCodes.Ldarg_0); 43 | 44 | if (bakingField != null && !propertyGetMethod.IsVirtual) 45 | { 46 | // Get field directly. 47 | getGenerator.Emit(OpCodes.Ldfld, bakingField); 48 | } 49 | else if (propertyGetMethod.IsVirtual) 50 | { 51 | // Call the virtual property. 52 | getGenerator.Emit(OpCodes.Callvirt, propertyGetMethod); 53 | } 54 | else 55 | { 56 | // Call the non virtual property. 57 | getGenerator.Emit(OpCodes.Call, propertyGetMethod); 58 | } 59 | 60 | getGenerator.Emit(OpCodes.Ret); 61 | 62 | return getMethod.CreateDelegate>(); 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /tests/PhenX.EntityFrameworkCore.BulkInsert.Benchmark/GetValueComparator.cs: -------------------------------------------------------------------------------- 1 | using System.Linq.Expressions; 2 | using System.Reflection; 3 | 4 | using BenchmarkDotNet.Attributes; 5 | using BenchmarkDotNet.Engines; 6 | 7 | using PhenX.EntityFrameworkCore.BulkInsert.Metadata; 8 | 9 | namespace PhenX.EntityFrameworkCore.BulkInsert.Benchmark; 10 | 11 | [MemoryDiagnoser] 12 | [SimpleJob(RunStrategy.ColdStart, launchCount: 1, warmupCount: 0, iterationCount: 10)] 13 | public partial class GetValueComparator 14 | { 15 | [Params(1_000_000)] public int N; 16 | 17 | private IReadOnlyList data = []; 18 | 19 | [IterationSetup] 20 | public void IterationSetup() 21 | { 22 | data = Enumerable.Range(1, N).Select(i => new TestEntity 23 | { 24 | Name = $"Entity{i}", 25 | Price = (decimal)(i * 0.1), 26 | Identifier = Guid.NewGuid(), 27 | NumericEnumValue = (NumericEnum)(i % 2), 28 | }).ToList(); 29 | } 30 | 31 | private static readonly Dictionary>> Converters = new() 32 | { 33 | { nameof(TestEntity.NumericEnumValue), v => (int) (v ?? 1)}, 34 | }; 35 | 36 | private static readonly PropertyInfo[] PropertyInfos = typeof(TestEntity).GetProperties(); 37 | 38 | private static readonly Func[] PropertyInfoGetValueGetters = PropertyInfos 39 | .Select>(propertyInfo => 40 | { 41 | var converter = Converters.TryGetValue(propertyInfo.Name, out var expression) 42 | ? expression.Compile() 43 | : null; 44 | 45 | if (converter == null) 46 | { 47 | return propertyInfo.GetValue; 48 | } 49 | 50 | return entity => converter(propertyInfo.GetValue(entity)); 51 | }) 52 | .ToArray(); 53 | 54 | private static readonly Func[] PropertyInfoIlGetters = PropertyInfos 55 | .Select>(propertyInfo => 56 | { 57 | var converter = Converters.TryGetValue(propertyInfo.Name, out var expression) 58 | ? expression.Compile() 59 | : null; 60 | 61 | var getter = CreateUntypedGetter(propertyInfo, propertyInfo.DeclaringType!, propertyInfo.PropertyType); 62 | 63 | if (converter == null) 64 | { 65 | return getter; 66 | } 67 | 68 | return entity => converter(getter(entity)); 69 | }) 70 | .ToArray(); 71 | 72 | private static readonly Func[] PropertyAccessorGetters = PropertyInfos 73 | .Select>(propertyInfo => 74 | { 75 | var converter = Converters.TryGetValue(propertyInfo.Name, out var expression) 76 | ? expression 77 | : null; 78 | 79 | return PropertyAccessor.CreateGetter(propertyInfo, converter: converter); 80 | }) 81 | .ToArray(); 82 | 83 | [Benchmark(Baseline = true)] 84 | public void Native() 85 | { 86 | var enumConverter = Converters[nameof(TestEntity.NumericEnumValue)].Compile(); 87 | 88 | for (var i = 0; i < data.Count; i++) 89 | { 90 | var entity = data[i]; 91 | 92 | _ = entity.Id; 93 | _ = entity.Name; 94 | _ = entity.Price; 95 | _ = entity.Identifier; 96 | _ = enumConverter(entity.NumericEnumValue); 97 | _ = entity.CreatedAt; 98 | _ = entity.UpdatedAt; 99 | } 100 | } 101 | 102 | [Benchmark] 103 | public void PropertyInfo_GetValue() 104 | { 105 | for (var i = 0; i < data.Count; i++) 106 | { 107 | var entity = data[i]; 108 | 109 | for (var j = 0; j < PropertyInfoGetValueGetters.Length; j++) 110 | { 111 | var getter = PropertyInfoGetValueGetters[j]; 112 | 113 | _ = getter(entity); 114 | } 115 | } 116 | } 117 | 118 | [Benchmark] 119 | public void ExpressionTreeGetter() 120 | { 121 | for (var i = 0; i < data.Count; i++) 122 | { 123 | var entity = data[i]; 124 | 125 | for (var j = 0; j < PropertyAccessorGetters.Length; j++) 126 | { 127 | var getter = PropertyAccessorGetters[j]; 128 | 129 | _ = getter(entity); 130 | } 131 | } 132 | } 133 | 134 | [Benchmark] 135 | public void IlGetter() 136 | { 137 | for (var i = 0; i < data.Count; i++) 138 | { 139 | var entity = data[i]; 140 | 141 | for (var j = 0; j < PropertyInfoIlGetters.Length; j++) 142 | { 143 | var getter = PropertyInfoIlGetters[j]; 144 | 145 | _ = getter(entity); 146 | } 147 | } 148 | } 149 | } 150 | -------------------------------------------------------------------------------- /tests/PhenX.EntityFrameworkCore.BulkInsert.Benchmark/LibComparator.RawInsert.cs: -------------------------------------------------------------------------------- 1 | using System.Data; 2 | 3 | using Microsoft.Data.SqlClient; 4 | using Microsoft.Data.Sqlite; 5 | using Microsoft.EntityFrameworkCore; 6 | 7 | using MySqlConnector; 8 | 9 | using Npgsql; 10 | 11 | using Oracle.ManagedDataAccess.Client; 12 | 13 | namespace PhenX.EntityFrameworkCore.BulkInsert.Benchmark; 14 | 15 | public abstract partial class LibComparator 16 | { 17 | private void RawInsertPostgreSql() 18 | { 19 | using var connection = (NpgsqlConnection)DbContext.Database.GetDbConnection(); 20 | if (connection.State != ConnectionState.Open) 21 | { 22 | connection.Open(); 23 | } 24 | 25 | const string copyCommand = $""" 26 | COPY "{nameof(TestEntity)}" ( 27 | "Name", 28 | "Price", 29 | "Identifier", 30 | "CreatedAt", 31 | "UpdatedAt", 32 | "NumericEnumValue" 33 | ) FROM STDIN (FORMAT BINARY) 34 | """; 35 | 36 | using var writer = connection.BeginBinaryImport(copyCommand); 37 | foreach (var entity in data) 38 | { 39 | writer.StartRow(); 40 | writer.Write(entity.Name); 41 | writer.Write(entity.Price); 42 | writer.Write(entity.Identifier); 43 | writer.Write(entity.CreatedAt); 44 | writer.Write(entity.UpdatedAt); 45 | writer.Write((int)entity.NumericEnumValue); 46 | } 47 | 48 | writer.Complete(); 49 | } 50 | 51 | private void RawInsertSqlite() 52 | { 53 | var connection = (SqliteConnection)DbContext.Database.GetDbConnection(); 54 | if (connection.State != ConnectionState.Open) 55 | { 56 | connection.Open(); 57 | } 58 | 59 | using var transaction = connection.BeginTransaction(); 60 | using var command = connection.CreateCommand(); 61 | command.CommandText = $""" 62 | INSERT INTO "{nameof(TestEntity)}" ( 63 | "Name", 64 | "Price", 65 | "Identifier", 66 | "CreatedAt", 67 | "UpdatedAt", 68 | "NumericEnumValue" 69 | ) VALUES (@Name, @Price, @Identifier, @CreatedAt, @UpdatedAt, @NumericEnumValue) 70 | """; 71 | 72 | command.Parameters.Add(new SqliteParameter("@Name", DbType.String)); 73 | command.Parameters.Add(new SqliteParameter("@Price", DbType.Decimal)); 74 | command.Parameters.Add(new SqliteParameter("@Identifier", DbType.Guid)); 75 | command.Parameters.Add(new SqliteParameter("@CreatedAt", DbType.DateTime)); 76 | command.Parameters.Add(new SqliteParameter("@UpdatedAt", DbType.DateTime2)); 77 | command.Parameters.Add(new SqliteParameter("@NumericEnumValue", DbType.Int32)); 78 | 79 | foreach (var entity in data) 80 | { 81 | command.Parameters["@Name"].Value = entity.Name; 82 | command.Parameters["@Price"].Value = entity.Price; 83 | command.Parameters["@Identifier"].Value = entity.Identifier; 84 | command.Parameters["@CreatedAt"].Value = entity.CreatedAt; 85 | command.Parameters["@UpdatedAt"].Value = entity.UpdatedAt; 86 | command.Parameters["@NumericEnumValue"].Value = (int)entity.NumericEnumValue; 87 | 88 | command.ExecuteNonQuery(); 89 | } 90 | 91 | transaction.Commit(); 92 | } 93 | 94 | private void RawInsertSqlServer() 95 | { 96 | var connection = (SqlConnection)DbContext.Database.GetDbConnection(); 97 | if (connection.State != ConnectionState.Open) 98 | { 99 | connection.Open(); 100 | } 101 | 102 | using var bulkCopy = new SqlBulkCopy(connection); 103 | 104 | bulkCopy.DestinationTableName = nameof(TestEntity); 105 | bulkCopy.BatchSize = 50_000; 106 | bulkCopy.BulkCopyTimeout = 60; 107 | 108 | bulkCopy.ColumnMappings.Add("Name", "Name"); 109 | bulkCopy.ColumnMappings.Add("Price", "Price"); 110 | bulkCopy.ColumnMappings.Add("Identifier", "Identifier"); 111 | bulkCopy.ColumnMappings.Add("CreatedAt", "CreatedAt"); 112 | bulkCopy.ColumnMappings.Add("UpdatedAt", "UpdatedAt"); 113 | bulkCopy.ColumnMappings.Add("NumericEnumValue", "NumericEnumValue"); 114 | 115 | var dataTable = new DataTable(); 116 | dataTable.Columns.Add("Name", typeof(string)); 117 | dataTable.Columns.Add("Price", typeof(decimal)); 118 | dataTable.Columns.Add("Identifier", typeof(Guid)); 119 | dataTable.Columns.Add("CreatedAt", typeof(DateTime)); 120 | dataTable.Columns.Add("UpdatedAt", typeof(DateTimeOffset)); 121 | dataTable.Columns.Add("NumericEnumValue", typeof(int)); 122 | 123 | foreach (var entity in data) 124 | { 125 | var row = dataTable.NewRow(); 126 | row["Name"] = entity.Name; 127 | row["Price"] = entity.Price; 128 | row["Identifier"] = entity.Identifier; 129 | row["CreatedAt"] = entity.CreatedAt; 130 | row["UpdatedAt"] = entity.UpdatedAt; 131 | row["NumericEnumValue"] = (int)entity.NumericEnumValue; 132 | dataTable.Rows.Add(row); 133 | 134 | if (dataTable.Rows.Count >= 50_000) 135 | { 136 | bulkCopy.WriteToServer(dataTable); 137 | dataTable.Clear(); 138 | } 139 | } 140 | 141 | if (dataTable.Rows.Count > 0) 142 | { 143 | bulkCopy.WriteToServer(dataTable); 144 | } 145 | } 146 | 147 | private void RawInsertMySql() 148 | { 149 | var connection = (MySqlConnection)DbContext.Database.GetDbConnection(); 150 | if (connection.State != ConnectionState.Open) 151 | { 152 | connection.Open(); 153 | } 154 | 155 | var bulkCopy = new MySqlBulkCopy(connection); 156 | 157 | bulkCopy.DestinationTableName = nameof(TestEntity); 158 | bulkCopy.BulkCopyTimeout = 60; 159 | 160 | var i = 0; 161 | bulkCopy.ColumnMappings.Add(new MySqlBulkCopyColumnMapping(i++, "Name")); 162 | bulkCopy.ColumnMappings.Add(new MySqlBulkCopyColumnMapping(i++, "Price")); 163 | bulkCopy.ColumnMappings.Add(new MySqlBulkCopyColumnMapping(i++, "Identifier")); 164 | bulkCopy.ColumnMappings.Add(new MySqlBulkCopyColumnMapping(i++, "CreatedAt")); 165 | bulkCopy.ColumnMappings.Add(new MySqlBulkCopyColumnMapping(i++, "UpdatedAt")); 166 | bulkCopy.ColumnMappings.Add(new MySqlBulkCopyColumnMapping(i++, "NumericEnumValue")); 167 | 168 | var dataTable = new DataTable(); 169 | dataTable.Columns.Add("Name", typeof(string)); 170 | dataTable.Columns.Add("Price", typeof(decimal)); 171 | dataTable.Columns.Add("Identifier", typeof(Guid)); 172 | dataTable.Columns.Add("CreatedAt", typeof(DateTime)); 173 | dataTable.Columns.Add("UpdatedAt", typeof(DateTimeOffset)); 174 | dataTable.Columns.Add("NumericEnumValue", typeof(int)); 175 | 176 | foreach (var entity in data) 177 | { 178 | var row = dataTable.NewRow(); 179 | row["Name"] = entity.Name; 180 | row["Price"] = entity.Price; 181 | row["Identifier"] = entity.Identifier; 182 | row["CreatedAt"] = entity.CreatedAt; 183 | row["UpdatedAt"] = entity.UpdatedAt; 184 | row["NumericEnumValue"] = (int)entity.NumericEnumValue; 185 | dataTable.Rows.Add(row); 186 | 187 | if (dataTable.Rows.Count >= 50_000) 188 | { 189 | bulkCopy.WriteToServer(dataTable); 190 | dataTable.Clear(); 191 | } 192 | } 193 | 194 | if (dataTable.Rows.Count > 0) 195 | { 196 | bulkCopy.WriteToServer(dataTable); 197 | } 198 | } 199 | 200 | private void RawInsertOracle() 201 | { 202 | var connection = (OracleConnection)DbContext.Database.GetDbConnection(); 203 | if (connection.State != ConnectionState.Open) 204 | { 205 | connection.Open(); 206 | } 207 | 208 | using var bulkCopy = new OracleBulkCopy(connection); 209 | 210 | bulkCopy.DestinationTableName = "\"" + nameof(TestEntity) + "\""; 211 | bulkCopy.BatchSize = 50_000; 212 | bulkCopy.BulkCopyTimeout = 60; 213 | 214 | bulkCopy.ColumnMappings.Add("Name", "\"Name\""); 215 | bulkCopy.ColumnMappings.Add("Price", "\"Price\""); 216 | bulkCopy.ColumnMappings.Add("Identifier", "\"Identifier\""); 217 | bulkCopy.ColumnMappings.Add("CreatedAt", "\"CreatedAt\""); 218 | bulkCopy.ColumnMappings.Add("UpdatedAt", "\"UpdatedAt\""); 219 | bulkCopy.ColumnMappings.Add("NumericEnumValue", "\"NumericEnumValue\""); 220 | 221 | var dataTable = new DataTable(); 222 | dataTable.Columns.Add("Name", typeof(string)); 223 | dataTable.Columns.Add("Price", typeof(decimal)); 224 | dataTable.Columns.Add("Identifier", typeof(Guid)); 225 | dataTable.Columns.Add("CreatedAt", typeof(DateTime)); 226 | dataTable.Columns.Add("UpdatedAt", typeof(DateTimeOffset)); 227 | dataTable.Columns.Add("NumericEnumValue", typeof(int)); 228 | 229 | foreach (var entity in data) 230 | { 231 | var row = dataTable.NewRow(); 232 | row["Name"] = entity.Name; 233 | row["Price"] = entity.Price; 234 | row["Identifier"] = entity.Identifier; 235 | row["CreatedAt"] = entity.CreatedAt; 236 | row["UpdatedAt"] = entity.UpdatedAt; 237 | row["NumericEnumValue"] = (int)entity.NumericEnumValue; 238 | dataTable.Rows.Add(row); 239 | 240 | if (dataTable.Rows.Count >= 50_000) 241 | { 242 | bulkCopy.WriteToServer(dataTable); 243 | dataTable.Clear(); 244 | } 245 | } 246 | 247 | if (dataTable.Rows.Count > 0) 248 | { 249 | bulkCopy.WriteToServer(dataTable); 250 | } 251 | } 252 | } 253 | -------------------------------------------------------------------------------- /tests/PhenX.EntityFrameworkCore.BulkInsert.Benchmark/LibComparator.cs: -------------------------------------------------------------------------------- 1 | using BenchmarkDotNet.Attributes; 2 | using BenchmarkDotNet.Engines; 3 | 4 | using DotNet.Testcontainers.Containers; 5 | 6 | using EFCore.BulkExtensions; 7 | 8 | using LinqToDB.Data; 9 | using LinqToDB.EntityFrameworkCore; 10 | 11 | using PhenX.EntityFrameworkCore.BulkInsert.Extensions; 12 | 13 | namespace PhenX.EntityFrameworkCore.BulkInsert.Benchmark; 14 | 15 | [MemoryDiagnoser] 16 | [SimpleJob(RunStrategy.ColdStart, launchCount: 1, warmupCount: 0, iterationCount: 5)] 17 | public abstract partial class LibComparator 18 | { 19 | [Params(500_000/*, 1_000_000/*, 10_000_000*/)] 20 | public int N; 21 | 22 | private IList data = []; 23 | protected TestDbContext DbContext { get; set; } = null!; 24 | 25 | [IterationSetup] 26 | public void IterationSetup() 27 | { 28 | data = Enumerable.Range(1, N).Select(i => new TestEntity 29 | { 30 | Name = $"Entity{i}", 31 | Price = (decimal)(i * 0.1), 32 | Identifier = Guid.NewGuid(), 33 | NumericEnumValue = (NumericEnum)(i % 2), 34 | }).ToList(); 35 | 36 | ConfigureDbContext(); 37 | DbContext.Database.EnsureCreated(); 38 | } 39 | 40 | protected LibComparator() 41 | { 42 | DbContainer = GetDbContainer(); 43 | DbContainer?.StartAsync().GetAwaiter().GetResult(); 44 | LinqToDBForEFTools.Initialize(); 45 | } 46 | 47 | protected abstract void ConfigureDbContext(); 48 | 49 | protected virtual string GetConnectionString() 50 | { 51 | return DbContainer?.GetConnectionString() ?? string.Empty; 52 | } 53 | 54 | private IDatabaseContainer? DbContainer { get; } 55 | 56 | protected abstract IDatabaseContainer? GetDbContainer(); 57 | 58 | [Benchmark(Baseline = true)] 59 | public async Task PhenX_EntityFrameworkCore_BulkInsert() 60 | { 61 | await DbContext.ExecuteBulkInsertAsync(data); 62 | } 63 | // 64 | // [Benchmark] 65 | // public void PhenX_EntityFrameworkCore_BulkInsert_Sync() 66 | // { 67 | // DbContext.ExecuteBulkInsert(data); 68 | // } 69 | 70 | [Benchmark] 71 | public void RawInsert() 72 | { 73 | if (DbContext.Database.ProviderName!.Contains("SqlServer", StringComparison.InvariantCultureIgnoreCase)) 74 | { 75 | // Use SqlBulkCopy for SQL Server 76 | RawInsertSqlServer(); 77 | } 78 | else if (DbContext.Database.ProviderName!.Contains("Sqlite", StringComparison.InvariantCultureIgnoreCase)) 79 | { 80 | // Use raw sql insert statements for SQLite 81 | RawInsertSqlite(); 82 | } 83 | else if (DbContext.Database.ProviderName!.Contains("Npgsql", StringComparison.InvariantCultureIgnoreCase)) 84 | { 85 | // Use BeginBinaryImport for PostgreSQL 86 | RawInsertPostgreSql(); 87 | } 88 | else if (DbContext.Database.ProviderName!.Contains("MySql", StringComparison.InvariantCultureIgnoreCase)) 89 | { 90 | // Use MySqlBulkCopy for PostgreSQL 91 | RawInsertMySql(); 92 | } 93 | else if (DbContext.Database.ProviderName!.Contains("Oracle", StringComparison.InvariantCultureIgnoreCase)) 94 | { 95 | // Use OracleBulkCopy for Oracle 96 | RawInsertOracle(); 97 | } 98 | } 99 | 100 | [Benchmark] 101 | public async Task Linq2Db() 102 | { 103 | await DbContext.BulkCopyAsync(new BulkCopyOptions 104 | { 105 | BulkCopyType = BulkCopyType.ProviderSpecific, 106 | }, data); 107 | } 108 | 109 | [Benchmark] 110 | public async Task Z_EntityFramework_Extensions_EFCore() 111 | { 112 | await DbContext.BulkInsertOptimizedAsync(data, options => options.IncludeGraph = false); 113 | } 114 | 115 | // [Benchmark] 116 | // public void Z_EntityFramework_Extensions_EFCore_Sync() 117 | // { 118 | // DbContext.BulkInsertOptimized(data, options => options.IncludeGraph = false); 119 | // } 120 | 121 | [Benchmark] 122 | public async Task EFCore_BulkExtensions() 123 | { 124 | await DbContext.BulkInsertAsync(data, options => 125 | { 126 | options.IncludeGraph = false; 127 | options.PreserveInsertOrder = false; 128 | }); 129 | } 130 | 131 | // [Benchmark] 132 | // public void EFCore_BulkExtensions_Sync() 133 | // { 134 | // DbContext.BulkInsert(data, options => 135 | // { 136 | // options.IncludeGraph = false; 137 | // options.PreserveInsertOrder = false; 138 | // }); 139 | // } 140 | 141 | [Benchmark] 142 | public async Task EFCore_SaveChanges() 143 | { 144 | DbContext.AddRange(data); 145 | await DbContext.SaveChangesAsync(); 146 | } 147 | } 148 | -------------------------------------------------------------------------------- /tests/PhenX.EntityFrameworkCore.BulkInsert.Benchmark/PhenX.EntityFrameworkCore.BulkInsert.Benchmark.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | Exe 5 | net9.0 6 | false 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | -------------------------------------------------------------------------------- /tests/PhenX.EntityFrameworkCore.BulkInsert.Benchmark/Program.cs: -------------------------------------------------------------------------------- 1 | using BenchmarkDotNet.Configs; 2 | using BenchmarkDotNet.Running; 3 | 4 | using PhenX.EntityFrameworkCore.BulkInsert.Benchmark.Providers; 5 | 6 | namespace PhenX.EntityFrameworkCore.BulkInsert.Benchmark; 7 | 8 | public class Program 9 | { 10 | public static void Main(string[] args) 11 | { 12 | var config = ManualConfig 13 | .Create(DefaultConfig.Instance) 14 | .WithOptions(ConfigOptions.DisableOptimizationsValidator); 15 | 16 | // Micro benchmark for value getters 17 | // BenchmarkRunner.Run(config); 18 | 19 | // Library comparison benchmarks 20 | var comparators = new[] 21 | { 22 | typeof(LibComparatorMySql), 23 | typeof(LibComparatorPostgreSql), 24 | typeof(LibComparatorSqlite), 25 | typeof(LibComparatorSqlServer), 26 | typeof(LibComparatorOracle), 27 | }; 28 | 29 | BenchmarkRunner.Run(comparators, config); 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /tests/PhenX.EntityFrameworkCore.BulkInsert.Benchmark/Providers/LibComparatorMySql.cs: -------------------------------------------------------------------------------- 1 | using DotNet.Testcontainers.Containers; 2 | 3 | using Microsoft.EntityFrameworkCore; 4 | 5 | using PhenX.EntityFrameworkCore.BulkInsert.MySql; 6 | 7 | using Testcontainers.MySql; 8 | 9 | namespace PhenX.EntityFrameworkCore.BulkInsert.Benchmark.Providers; 10 | 11 | public class LibComparatorMySql : LibComparator 12 | { 13 | protected override void ConfigureDbContext() 14 | { 15 | var connectionString = GetConnectionString() + ";AllowLoadLocalInfile=true;"; 16 | 17 | DbContext = new TestDbContext(p => p 18 | .UseMySql(connectionString, ServerVersion.AutoDetect(connectionString)) 19 | .UseBulkInsertMySql() 20 | ); 21 | } 22 | 23 | protected override IDatabaseContainer? GetDbContainer() 24 | { 25 | return new MySqlBuilder() 26 | .WithCommand("--log-bin-trust-function-creators=1", "--local-infile=1") 27 | .Build(); 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /tests/PhenX.EntityFrameworkCore.BulkInsert.Benchmark/Providers/LibComparatorOracle.cs: -------------------------------------------------------------------------------- 1 | using DotNet.Testcontainers.Containers; 2 | 3 | using LinqToDB.EntityFrameworkCore; 4 | 5 | using Microsoft.EntityFrameworkCore; 6 | 7 | using PhenX.EntityFrameworkCore.BulkInsert.Oracle; 8 | 9 | using Testcontainers.Oracle; 10 | 11 | namespace PhenX.EntityFrameworkCore.BulkInsert.Benchmark.Providers; 12 | 13 | public class LibComparatorOracle : LibComparator 14 | { 15 | protected override void ConfigureDbContext() 16 | { 17 | var connectionString = GetConnectionString(); 18 | 19 | DbContext = new TestDbContext(p => p 20 | .UseOracle(connectionString) 21 | .UseBulkInsertOracle() 22 | .UseLinqToDB() 23 | ); 24 | } 25 | 26 | protected override IDatabaseContainer? GetDbContainer() 27 | { 28 | return new OracleBuilder() 29 | .WithImage("gvenzl/oracle-free:23-slim-faststart") 30 | .Build(); 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /tests/PhenX.EntityFrameworkCore.BulkInsert.Benchmark/Providers/LibComparatorPostgreSql.cs: -------------------------------------------------------------------------------- 1 | using DotNet.Testcontainers.Containers; 2 | 3 | using LinqToDB.EntityFrameworkCore; 4 | 5 | using Microsoft.EntityFrameworkCore; 6 | 7 | using PhenX.EntityFrameworkCore.BulkInsert.PostgreSql; 8 | 9 | using Testcontainers.PostgreSql; 10 | 11 | namespace PhenX.EntityFrameworkCore.BulkInsert.Benchmark.Providers; 12 | 13 | public class LibComparatorPostgreSql : LibComparator 14 | { 15 | protected override void ConfigureDbContext() 16 | { 17 | var connectionString = GetConnectionString() + ";Include Error Detail=true"; 18 | 19 | DbContext = new TestDbContext(p => p 20 | .UseNpgsql(connectionString) 21 | .UseBulkInsertPostgreSql() 22 | .UseLinqToDB() 23 | ); 24 | } 25 | 26 | protected override IDatabaseContainer? GetDbContainer() 27 | { 28 | return new PostgreSqlBuilder() 29 | .WithDatabase("testdb") 30 | .WithUsername("testuser") 31 | .WithPassword("testpassword") 32 | .Build(); 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /tests/PhenX.EntityFrameworkCore.BulkInsert.Benchmark/Providers/LibComparatorSqlServer.cs: -------------------------------------------------------------------------------- 1 | using DotNet.Testcontainers.Containers; 2 | 3 | using LinqToDB.EntityFrameworkCore; 4 | 5 | using Microsoft.EntityFrameworkCore; 6 | 7 | using PhenX.EntityFrameworkCore.BulkInsert.SqlServer; 8 | 9 | using Testcontainers.MsSql; 10 | 11 | namespace PhenX.EntityFrameworkCore.BulkInsert.Benchmark.Providers; 12 | 13 | public class LibComparatorSqlServer : LibComparator 14 | { 15 | protected override void ConfigureDbContext() 16 | { 17 | var connectionString = GetConnectionString(); 18 | 19 | DbContext = new TestDbContext(p => p 20 | .UseSqlServer(connectionString) 21 | .UseBulkInsertSqlServer() 22 | .UseLinqToDB() 23 | ); 24 | } 25 | 26 | protected override IDatabaseContainer? GetDbContainer() 27 | { 28 | return new MsSqlBuilder().Build(); 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /tests/PhenX.EntityFrameworkCore.BulkInsert.Benchmark/Providers/LibComparatorSqlite.cs: -------------------------------------------------------------------------------- 1 | using DotNet.Testcontainers.Containers; 2 | 3 | using LinqToDB.EntityFrameworkCore; 4 | 5 | using Microsoft.EntityFrameworkCore; 6 | 7 | using PhenX.EntityFrameworkCore.BulkInsert.Sqlite; 8 | 9 | namespace PhenX.EntityFrameworkCore.BulkInsert.Benchmark.Providers; 10 | 11 | public class LibComparatorSqlite : LibComparator 12 | { 13 | protected override void ConfigureDbContext() 14 | { 15 | var connectionString = GetConnectionString(); 16 | 17 | DbContext = new TestDbContext(p => p 18 | .UseSqlite(connectionString) 19 | .UseBulkInsertSqlite() 20 | .UseLinqToDB() 21 | ); 22 | } 23 | 24 | protected override string GetConnectionString() 25 | { 26 | return $"Data Source={Guid.NewGuid()}.db"; 27 | } 28 | 29 | protected override IDatabaseContainer? GetDbContainer() => null; 30 | } 31 | -------------------------------------------------------------------------------- /tests/PhenX.EntityFrameworkCore.BulkInsert.Benchmark/TestDbContext.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.EntityFrameworkCore; 2 | 3 | namespace PhenX.EntityFrameworkCore.BulkInsert.Benchmark; 4 | 5 | public class TestDbContext(Action configure) : DbContext 6 | { 7 | public Action Configure { get; } = configure; 8 | 9 | public DbSet TestEntities { get; set; } = null!; 10 | 11 | protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder) 12 | { 13 | Configure(optionsBuilder); 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /tests/PhenX.EntityFrameworkCore.BulkInsert.Benchmark/TestEntity.cs: -------------------------------------------------------------------------------- 1 | using System.ComponentModel.DataAnnotations.Schema; 2 | 3 | using Microsoft.EntityFrameworkCore; 4 | 5 | namespace PhenX.EntityFrameworkCore.BulkInsert.Benchmark; 6 | 7 | [PrimaryKey(nameof(Id))] 8 | [Table(nameof(TestEntity))] 9 | public class TestEntity 10 | { 11 | public int Id { get; set; } 12 | public string Name { get; set; } = string.Empty; 13 | public decimal Price { get; set; } 14 | public Guid Identifier { get; set; } 15 | 16 | public DateTime CreatedAt { get; set; } = DateTime.UtcNow; 17 | public DateTimeOffset UpdatedAt { get; set; } = DateTimeOffset.UtcNow; 18 | 19 | public NumericEnum NumericEnumValue { get; set; } 20 | } 21 | 22 | public enum NumericEnum 23 | { 24 | First = 1, 25 | Second = 2, 26 | } 27 | -------------------------------------------------------------------------------- /tests/PhenX.EntityFrameworkCore.BulkInsert.Tests/DbContainer/TestDbContainer.cs: -------------------------------------------------------------------------------- 1 | using System.Data.Common; 2 | 3 | using DotNet.Testcontainers.Containers; 4 | 5 | using Microsoft.EntityFrameworkCore; 6 | using Microsoft.Extensions.Logging.Abstractions; 7 | 8 | using PhenX.EntityFrameworkCore.BulkInsert.Tests.DbContext; 9 | 10 | using Xunit; 11 | 12 | namespace PhenX.EntityFrameworkCore.BulkInsert.Tests.DbContainer; 13 | 14 | public abstract class TestDbContainer : IAsyncLifetime 15 | { 16 | private readonly TimeSpan _waitTime = TimeSpan.FromSeconds(30); 17 | private readonly HashSet _connected = []; 18 | protected readonly IDatabaseContainer? DbContainer; 19 | 20 | protected TestDbContainer() 21 | { 22 | DbContainer = GetDbContainer(); 23 | } 24 | 25 | protected abstract IDatabaseContainer? GetDbContainer(); 26 | 27 | protected virtual string GetConnectionString(string databaseName) 28 | { 29 | if (DbContainer == null) 30 | { 31 | return string.Empty; 32 | } 33 | 34 | var builder = new DbConnectionStringBuilder() 35 | { 36 | ConnectionString = DbContainer.GetConnectionString() 37 | }; 38 | 39 | builder["database"] = databaseName; 40 | return builder.ToString(); 41 | } 42 | 43 | protected abstract void Configure(DbContextOptionsBuilder optionsBuilder, string databaseName); 44 | 45 | public async Task InitializeAsync() 46 | { 47 | if (DbContainer != null) 48 | { 49 | await DbContainer.StartAsync(); 50 | } 51 | } 52 | 53 | public async Task CreateContextAsync(string databaseName) 54 | where TDbContext : TestDbContextBase, new() 55 | { 56 | var dbContext = new TDbContext 57 | { 58 | ConfigureOptions = (builder) => 59 | { 60 | builder.UseLoggerFactory(NullLoggerFactory.Instance); 61 | Configure(builder, databaseName); 62 | } 63 | }; 64 | 65 | if (_connected.Add(databaseName)) 66 | { 67 | await EnsureConnectedAsync(dbContext, databaseName); 68 | } 69 | 70 | try 71 | { 72 | await dbContext.Database.EnsureCreatedAsync(); 73 | } 74 | catch 75 | { 76 | // Often fails with SQL server. 77 | } 78 | 79 | return dbContext; 80 | } 81 | 82 | protected virtual async Task EnsureConnectedAsync(TDbContext context, string databaseName) 83 | where TDbContext : TestDbContextBase 84 | { 85 | using var cts = new CancellationTokenSource(_waitTime); 86 | 87 | while (!await context.Database.CanConnectAsync(cts.Token)) 88 | { 89 | await Task.Delay(100, cts.Token); 90 | } 91 | } 92 | 93 | public async Task DisposeAsync() 94 | { 95 | if (DbContainer != null) 96 | { 97 | await DbContainer.DisposeAsync(); 98 | } 99 | } 100 | } 101 | -------------------------------------------------------------------------------- /tests/PhenX.EntityFrameworkCore.BulkInsert.Tests/DbContainer/TestDbContainerMySql.cs: -------------------------------------------------------------------------------- 1 | using DotNet.Testcontainers.Containers; 2 | 3 | using Microsoft.EntityFrameworkCore; 4 | 5 | using PhenX.EntityFrameworkCore.BulkInsert.MySql; 6 | 7 | using Testcontainers.MySql; 8 | 9 | using Xunit; 10 | 11 | namespace PhenX.EntityFrameworkCore.BulkInsert.Tests.DbContainer; 12 | 13 | [CollectionDefinition(Name)] 14 | public class TestDbContainerMySqlCollection : ICollectionFixture 15 | { 16 | public const string Name = "MySql"; 17 | } 18 | 19 | public class TestDbContainerMySql() : TestDbContainer 20 | { 21 | protected override IDatabaseContainer? GetDbContainer() 22 | { 23 | return new MySqlBuilder() 24 | .WithCommand("--log-bin-trust-function-creators=1", "--local-infile=1", "--innodb-print-all-deadlocks=ON") 25 | .WithReuse(true) 26 | .WithUsername("root") 27 | .WithPassword("root") 28 | .Build(); 29 | } 30 | 31 | protected override string GetConnectionString(string databaseName) 32 | { 33 | return $"{base.GetConnectionString(databaseName)};AllowLoadLocalInfile=true;"; 34 | } 35 | 36 | protected override void Configure(DbContextOptionsBuilder optionsBuilder, string databaseName) 37 | { 38 | var connectionString = GetConnectionString(databaseName); 39 | 40 | optionsBuilder 41 | .UseMySql(connectionString, ServerVersion.AutoDetect(connectionString), o => 42 | { 43 | o.UseNetTopologySuite(); 44 | }) 45 | .UseBulkInsertMySql(); 46 | } 47 | 48 | protected override async Task EnsureConnectedAsync(TDbContext context, string databaseName) 49 | { 50 | var container = (MySqlContainer)DbContainer!; 51 | 52 | await container.ExecScriptAsync($"CREATE DATABASE `{databaseName}`"); 53 | await base.EnsureConnectedAsync(context, databaseName); 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /tests/PhenX.EntityFrameworkCore.BulkInsert.Tests/DbContainer/TestDbContainerOracle.cs: -------------------------------------------------------------------------------- 1 | using DotNet.Testcontainers.Containers; 2 | 3 | using Microsoft.EntityFrameworkCore; 4 | 5 | using PhenX.EntityFrameworkCore.BulkInsert.Oracle; 6 | 7 | using Testcontainers.Oracle; 8 | 9 | using Xunit; 10 | 11 | namespace PhenX.EntityFrameworkCore.BulkInsert.Tests.DbContainer; 12 | 13 | [CollectionDefinition(Name)] 14 | public class TestDbContainerOracleCollection : ICollectionFixture 15 | { 16 | public const string Name = "Oracle"; 17 | } 18 | 19 | public class TestDbContainerOracle : TestDbContainer 20 | { 21 | protected override IDatabaseContainer? GetDbContainer() 22 | { 23 | return new OracleBuilder() 24 | .WithImage("gvenzl/oracle-free:23-slim-faststart") 25 | .WithReuse(true) 26 | .Build(); 27 | } 28 | 29 | protected override void Configure(DbContextOptionsBuilder optionsBuilder, string databaseName) 30 | { 31 | optionsBuilder 32 | .UseOracle(GetConnectionString(databaseName)) 33 | .UseBulkInsertOracle(); 34 | } 35 | 36 | protected override string GetConnectionString(string databaseName) 37 | { 38 | if (DbContainer == null) 39 | { 40 | return string.Empty; 41 | } 42 | 43 | var port = DbContainer.GetMappedPublicPort(1521); 44 | 45 | return $"Data Source=(DESCRIPTION = (ADDRESS_LIST = (ADDRESS = (PROTOCOL = TCP)(HOST = localhost)(PORT = {port})) ) (CONNECT_DATA = (SERVICE_NAME = FREEPDB1) ) );User ID=oracle;Password=oracle"; 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /tests/PhenX.EntityFrameworkCore.BulkInsert.Tests/DbContainer/TestDbContainerPostgreSql.cs: -------------------------------------------------------------------------------- 1 | using DotNet.Testcontainers.Containers; 2 | 3 | using Microsoft.EntityFrameworkCore; 4 | 5 | using PhenX.EntityFrameworkCore.BulkInsert.PostgreSql; 6 | 7 | using Testcontainers.PostgreSql; 8 | 9 | using Xunit; 10 | 11 | namespace PhenX.EntityFrameworkCore.BulkInsert.Tests.DbContainer; 12 | 13 | [CollectionDefinition(Name)] 14 | public class TestDbContainerPostgreSqlCollection : ICollectionFixture 15 | { 16 | public const string Name = "PostgreSql"; 17 | } 18 | 19 | public class TestDbContainerPostgreSql : TestDbContainer 20 | { 21 | protected override IDatabaseContainer? GetDbContainer() 22 | { 23 | return new PostgreSqlBuilder() 24 | .WithImage("postgis/postgis") // Geo GeoSpatial support. 25 | .WithReuse(true) 26 | .WithDatabase("testdb") 27 | .WithUsername("testuser") 28 | .WithPassword("testpassword") 29 | .Build(); 30 | } 31 | 32 | protected override void Configure(DbContextOptionsBuilder optionsBuilder, string databaseName) 33 | { 34 | optionsBuilder 35 | .UseNpgsql(GetConnectionString(databaseName), o => 36 | { 37 | o.UseNetTopologySuite(); 38 | }) 39 | .UseBulkInsertPostgreSql(); 40 | } 41 | 42 | protected override async Task EnsureConnectedAsync(TDbContext context, string databaseName) 43 | { 44 | var container = (PostgreSqlContainer)DbContainer!; 45 | 46 | await container.ExecScriptAsync($"CREATE DATABASE \"{databaseName}\""); 47 | await base.EnsureConnectedAsync(context, databaseName); 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /tests/PhenX.EntityFrameworkCore.BulkInsert.Tests/DbContainer/TestDbContainerSqlServer.cs: -------------------------------------------------------------------------------- 1 | using DotNet.Testcontainers.Containers; 2 | 3 | using Microsoft.EntityFrameworkCore; 4 | 5 | using PhenX.EntityFrameworkCore.BulkInsert.SqlServer; 6 | 7 | using Testcontainers.MsSql; 8 | 9 | using Xunit; 10 | 11 | namespace PhenX.EntityFrameworkCore.BulkInsert.Tests.DbContainer; 12 | 13 | [CollectionDefinition(Name)] 14 | public class TestDbContainerSqlServerCollection : ICollectionFixture 15 | { 16 | public const string Name = "SqlServer"; 17 | } 18 | 19 | public class TestDbContainerSqlServer : TestDbContainer 20 | { 21 | protected override IDatabaseContainer? GetDbContainer() 22 | { 23 | return new MsSqlBuilder() 24 | .WithImage("vibs2006/sql_server_fts") // Geo Geospatial support 25 | .WithReuse(true) 26 | .Build(); 27 | } 28 | 29 | protected override void Configure(DbContextOptionsBuilder optionsBuilder, string databaseName) 30 | { 31 | optionsBuilder 32 | .UseSqlServer(GetConnectionString(databaseName), o => 33 | { 34 | o.UseNetTopologySuite(); 35 | }) 36 | .UseBulkInsertSqlServer(); 37 | } 38 | 39 | protected override async Task EnsureConnectedAsync(TDbContext context, string databaseName) 40 | { 41 | var container = (MsSqlContainer)DbContainer!; 42 | 43 | await container.ExecScriptAsync($"CREATE DATABASE [{databaseName}]"); 44 | await base.EnsureConnectedAsync(context, databaseName); 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /tests/PhenX.EntityFrameworkCore.BulkInsert.Tests/DbContainer/TestDbContainerSqlite.cs: -------------------------------------------------------------------------------- 1 | using DotNet.Testcontainers.Containers; 2 | 3 | using Microsoft.EntityFrameworkCore; 4 | 5 | using PhenX.EntityFrameworkCore.BulkInsert.Sqlite; 6 | 7 | using Xunit; 8 | 9 | namespace PhenX.EntityFrameworkCore.BulkInsert.Tests.DbContainer; 10 | 11 | [CollectionDefinition(Name)] 12 | public class TestDbContainerSqliteCollection : ICollectionFixture 13 | { 14 | public const string Name = "Sqlite"; 15 | } 16 | 17 | public class TestDbContainerSqlite : TestDbContainer 18 | { 19 | protected override IDatabaseContainer? GetDbContainer() => null; 20 | 21 | protected override string GetConnectionString(string databaseName) 22 | { 23 | return $"Data Source={Guid.NewGuid()}.db"; 24 | } 25 | 26 | protected override void Configure(DbContextOptionsBuilder optionsBuilder, string databaseName) 27 | { 28 | optionsBuilder 29 | .UseSqlite(GetConnectionString(databaseName)) 30 | .UseBulkInsertSqlite(); 31 | } 32 | 33 | protected override Task EnsureConnectedAsync(TDbContext context, string databaseName) 34 | { 35 | return Task.CompletedTask; 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /tests/PhenX.EntityFrameworkCore.BulkInsert.Tests/DbContext/Extensions.cs: -------------------------------------------------------------------------------- 1 | using System.Text.Json; 2 | 3 | using Microsoft.EntityFrameworkCore; 4 | using Microsoft.EntityFrameworkCore.Metadata.Builders; 5 | using Microsoft.EntityFrameworkCore.Storage.ValueConversion; 6 | 7 | namespace PhenX.EntityFrameworkCore.BulkInsert.Tests.DbContext; 8 | 9 | public static class Extensions 10 | { 11 | public static PropertyBuilder AsJsonString(this PropertyBuilder propertyBuilder, string? columnType) 12 | where T : class? 13 | { 14 | var converter = new ValueConverter( 15 | v => JsonSerializer.Serialize(v, (JsonSerializerOptions?)null), 16 | v => JsonSerializer.Deserialize(v, (JsonSerializerOptions?)null)! 17 | ); 18 | 19 | propertyBuilder.HasConversion(converter).HasColumnType(columnType); 20 | return propertyBuilder; 21 | } 22 | 23 | } 24 | -------------------------------------------------------------------------------- /tests/PhenX.EntityFrameworkCore.BulkInsert.Tests/DbContext/NumericEnum.cs: -------------------------------------------------------------------------------- 1 | namespace PhenX.EntityFrameworkCore.BulkInsert.Tests.DbContext; 2 | 3 | public enum NumericEnum 4 | { 5 | First = 1, 6 | Second = 2, 7 | } 8 | -------------------------------------------------------------------------------- /tests/PhenX.EntityFrameworkCore.BulkInsert.Tests/DbContext/OwnedObject.cs: -------------------------------------------------------------------------------- 1 | namespace PhenX.EntityFrameworkCore.BulkInsert.Tests.DbContext; 2 | 3 | public class OwnedObject 4 | { 5 | public int Code { get; set; } 6 | 7 | public string Name { get; set; } = string.Empty; 8 | } 9 | -------------------------------------------------------------------------------- /tests/PhenX.EntityFrameworkCore.BulkInsert.Tests/DbContext/StringEnum.cs: -------------------------------------------------------------------------------- 1 | namespace PhenX.EntityFrameworkCore.BulkInsert.Tests.DbContext; 2 | 3 | public enum StringEnum 4 | { 5 | First, 6 | Second, 7 | } 8 | -------------------------------------------------------------------------------- /tests/PhenX.EntityFrameworkCore.BulkInsert.Tests/DbContext/TestDbContext.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.EntityFrameworkCore; 2 | using Microsoft.EntityFrameworkCore.Storage.ValueConversion; 3 | 4 | namespace PhenX.EntityFrameworkCore.BulkInsert.Tests.DbContext; 5 | 6 | public class TestDbContext : TestDbContextBase 7 | { 8 | public DbSet TestEntities { get; set; } = null!; 9 | public DbSet TestEntitiesWithSimpleTypes { get; set; } = null!; 10 | public DbSet TestEntitiesWithJson { get; set; } = null!; 11 | public DbSet TestEntitiesWithGuidId { get; set; } = null!; 12 | public DbSet TestEntitiesWithConverter { get; set; } = null!; 13 | public DbSet TestEntitiesWithComplexType { get; set; } = null!; 14 | 15 | protected override void OnModelCreating(ModelBuilder modelBuilder) 16 | { 17 | base.OnModelCreating(modelBuilder); 18 | 19 | modelBuilder.Entity(builder => 20 | { 21 | builder.Property(e => e.CreatedAt) 22 | .HasConversion(new DateTimeToBinaryConverter()); 23 | }); 24 | 25 | modelBuilder.Entity(builder => 26 | { 27 | builder.Property(e => e.Id) 28 | .ValueGeneratedNever(); 29 | }); 30 | 31 | modelBuilder.Entity(builder => 32 | { 33 | builder 34 | .ComplexProperty(e => e.OwnedComplexType) 35 | .IsRequired(); 36 | }); 37 | } 38 | } 39 | 40 | public class TestDbContextPostgreSql : TestDbContext 41 | { 42 | protected override void OnModelCreating(ModelBuilder modelBuilder) 43 | { 44 | base.OnModelCreating(modelBuilder); 45 | 46 | modelBuilder.Entity(b => 47 | { 48 | b.Property(x => x.JsonArray).AsJsonString("jsonb"); 49 | b.Property(x => x.JsonObject).AsJsonString("jsonb"); 50 | }); 51 | 52 | modelBuilder.Entity(b => 53 | { 54 | b.Property(x => x.StringEnumValue).HasColumnType("text"); 55 | }); 56 | } 57 | } 58 | 59 | public class TestDbContextMySql : TestDbContext 60 | { 61 | protected override void OnModelCreating(ModelBuilder modelBuilder) 62 | { 63 | base.OnModelCreating(modelBuilder); 64 | 65 | modelBuilder.Entity(b => 66 | { 67 | b.Property(x => x.JsonArray).AsJsonString("json"); 68 | b.Property(x => x.JsonObject).AsJsonString("json"); 69 | }); 70 | 71 | modelBuilder.Entity(b => 72 | { 73 | b.Property(x => x.StringEnumValue).HasColumnType("text"); 74 | }); 75 | } 76 | } 77 | 78 | public class TestDbContextSqlServer : TestDbContext 79 | { 80 | protected override void OnModelCreating(ModelBuilder modelBuilder) 81 | { 82 | base.OnModelCreating(modelBuilder); 83 | 84 | modelBuilder.Entity(b => 85 | { 86 | b.Property(x => x.JsonArray).AsJsonString(null); 87 | b.Property(x => x.JsonObject).AsJsonString(null); 88 | }); 89 | 90 | modelBuilder.Entity(b => 91 | { 92 | b.Property(x => x.StringEnumValue).HasColumnType("text"); 93 | }); 94 | } 95 | } 96 | 97 | public class TestDbContextSqlite : TestDbContext 98 | { 99 | protected override void OnModelCreating(ModelBuilder modelBuilder) 100 | { 101 | base.OnModelCreating(modelBuilder); 102 | 103 | modelBuilder.Entity(b => 104 | { 105 | b.Property(x => x.JsonArray).AsJsonString(null); 106 | b.Property(x => x.JsonObject).AsJsonString(null); 107 | }); 108 | 109 | modelBuilder.Entity(b => 110 | { 111 | b.Property(x => x.StringEnumValue).HasColumnType("text"); 112 | }); 113 | } 114 | } 115 | 116 | public class TestDbContextOracle : TestDbContext 117 | { 118 | protected override void OnModelCreating(ModelBuilder modelBuilder) 119 | { 120 | base.OnModelCreating(modelBuilder); 121 | 122 | modelBuilder.Entity(b => 123 | { 124 | b.Property(x => x.JsonArray).AsJsonString(null); 125 | b.Property(x => x.JsonObject).AsJsonString(null); 126 | }); 127 | 128 | modelBuilder.Entity(b => 129 | { 130 | b.Property(x => x.StringEnumValue).HasColumnType("nvarchar2(255)"); 131 | }); 132 | } 133 | } 134 | 135 | 136 | 137 | -------------------------------------------------------------------------------- /tests/PhenX.EntityFrameworkCore.BulkInsert.Tests/DbContext/TestDbContextBase.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.EntityFrameworkCore; 2 | 3 | namespace PhenX.EntityFrameworkCore.BulkInsert.Tests.DbContext; 4 | 5 | public abstract class TestDbContextBase : Microsoft.EntityFrameworkCore.DbContext 6 | { 7 | public Action ConfigureOptions { get; init; } = null!; 8 | 9 | protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder) => ConfigureOptions(optionsBuilder); 10 | } 11 | -------------------------------------------------------------------------------- /tests/PhenX.EntityFrameworkCore.BulkInsert.Tests/DbContext/TestDbContextGeo.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.EntityFrameworkCore; 2 | 3 | namespace PhenX.EntityFrameworkCore.BulkInsert.Tests.DbContext; 4 | 5 | public class TestDbContextGeo : TestDbContextBase 6 | { 7 | public DbSet TestEntitiesWithGeo { get; set; } = null!; 8 | } 9 | -------------------------------------------------------------------------------- /tests/PhenX.EntityFrameworkCore.BulkInsert.Tests/DbContext/TestEntity.cs: -------------------------------------------------------------------------------- 1 | using System.ComponentModel.DataAnnotations; 2 | using System.ComponentModel.DataAnnotations.Schema; 3 | 4 | using Microsoft.EntityFrameworkCore; 5 | 6 | namespace PhenX.EntityFrameworkCore.BulkInsert.Tests.DbContext; 7 | 8 | [PrimaryKey(nameof(Id))] 9 | [Index(nameof(Name), IsUnique = true)] 10 | [Table("test_entity")] 11 | public class TestEntity : TestEntityBase 12 | { 13 | public int Id { get; set; } 14 | 15 | [Column("name")] 16 | [MaxLength(100)] 17 | public string Name { get; set; } = string.Empty; 18 | 19 | [Column("some_price")] 20 | public decimal Price { get; set; } 21 | 22 | [Column("the_identifier")] 23 | public Guid Identifier { get; set; } 24 | 25 | [Column("nullable_identifier")] 26 | public Guid? NullableIdentifier { get; set; } 27 | 28 | [Column("string_enum_value")] 29 | public StringEnum StringEnumValue { get; set; } 30 | 31 | [Column("num_enum_value")] 32 | public NumericEnum NumericEnumValue { get; set; } 33 | } 34 | -------------------------------------------------------------------------------- /tests/PhenX.EntityFrameworkCore.BulkInsert.Tests/DbContext/TestEntityBase.cs: -------------------------------------------------------------------------------- 1 | using System.ComponentModel.DataAnnotations.Schema; 2 | 3 | namespace PhenX.EntityFrameworkCore.BulkInsert.Tests.DbContext; 4 | 5 | public abstract class TestEntityBase 6 | { 7 | [Column("test_run")] 8 | public Guid TestRun { get; set; } 9 | } 10 | -------------------------------------------------------------------------------- /tests/PhenX.EntityFrameworkCore.BulkInsert.Tests/DbContext/TestEntityWithComplexType.cs: -------------------------------------------------------------------------------- 1 | using System.ComponentModel.DataAnnotations; 2 | using System.ComponentModel.DataAnnotations.Schema; 3 | 4 | namespace PhenX.EntityFrameworkCore.BulkInsert.Tests.DbContext; 5 | 6 | [Table("test_entity_complex_type")] 7 | public class TestEntityWithComplexType : TestEntityBase 8 | { 9 | [Key] 10 | public int Id { get; set; } 11 | 12 | public OwnedObject OwnedComplexType { get; set; } = null!; 13 | } 14 | -------------------------------------------------------------------------------- /tests/PhenX.EntityFrameworkCore.BulkInsert.Tests/DbContext/TestEntityWithConverters.cs: -------------------------------------------------------------------------------- 1 | using System.ComponentModel.DataAnnotations; 2 | using System.ComponentModel.DataAnnotations.Schema; 3 | 4 | namespace PhenX.EntityFrameworkCore.BulkInsert.Tests.DbContext; 5 | 6 | [Table("test_entity_with_converters")] 7 | public class TestEntityWithConverters : TestEntityBase 8 | { 9 | public int Id { get; set; } 10 | 11 | [Column("name")] 12 | [MaxLength(100)] 13 | public string Name { get; set; } = string.Empty; 14 | 15 | [Column("created_at")] 16 | public DateTime CreatedAt { get; set; } 17 | 18 | [Column("uri")] 19 | public Uri? Uri { get; set; } 20 | } 21 | 22 | -------------------------------------------------------------------------------- /tests/PhenX.EntityFrameworkCore.BulkInsert.Tests/DbContext/TestEntityWithGeo.cs: -------------------------------------------------------------------------------- 1 | using System.ComponentModel.DataAnnotations; 2 | using System.ComponentModel.DataAnnotations.Schema; 3 | 4 | using NetTopologySuite.Geometries; 5 | 6 | namespace PhenX.EntityFrameworkCore.BulkInsert.Tests.DbContext; 7 | 8 | [Table("test_entity_geo")] 9 | public class TestEntityWithGeo : TestEntityBase 10 | { 11 | [Key] 12 | public int Id { get; set; } 13 | 14 | public Geometry GeoObject { get; set; } = null!; 15 | } 16 | -------------------------------------------------------------------------------- /tests/PhenX.EntityFrameworkCore.BulkInsert.Tests/DbContext/TestEntityWithGuidId.cs: -------------------------------------------------------------------------------- 1 | using System.ComponentModel.DataAnnotations; 2 | using System.ComponentModel.DataAnnotations.Schema; 3 | 4 | namespace PhenX.EntityFrameworkCore.BulkInsert.Tests.DbContext; 5 | 6 | [Table("test_entity_guids")] 7 | public class TestEntityWithGuidId : TestEntityBase 8 | { 9 | [Key] 10 | public Guid Id { get; set; } 11 | 12 | [Column("name")] 13 | [MaxLength(100)] 14 | public string Name { get; set; } = string.Empty; 15 | } 16 | -------------------------------------------------------------------------------- /tests/PhenX.EntityFrameworkCore.BulkInsert.Tests/DbContext/TestEntityWithJson.cs: -------------------------------------------------------------------------------- 1 | using System.ComponentModel.DataAnnotations; 2 | using System.ComponentModel.DataAnnotations.Schema; 3 | 4 | namespace PhenX.EntityFrameworkCore.BulkInsert.Tests.DbContext; 5 | 6 | [Table("test_entity_json")] 7 | public class TestEntityWithJson : TestEntityBase 8 | { 9 | [Key] 10 | public int Id { get; set; } 11 | 12 | public List JsonArray { get; set; } = []; 13 | 14 | public OwnedObject? JsonObject { get; set; } 15 | } 16 | -------------------------------------------------------------------------------- /tests/PhenX.EntityFrameworkCore.BulkInsert.Tests/DbContext/TestEntityWithSimpleTypes.cs: -------------------------------------------------------------------------------- 1 | using System.ComponentModel.DataAnnotations.Schema; 2 | 3 | using Microsoft.EntityFrameworkCore; 4 | 5 | namespace PhenX.EntityFrameworkCore.BulkInsert.Tests.DbContext; 6 | 7 | [PrimaryKey(nameof(Id))] 8 | [Table("test_entity_with_simple_types")] 9 | public class TestEntityWithSimpleTypes : TestEntityBase 10 | { 11 | public int Id { get; set; } 12 | 13 | public required string StringValue { get; set; } = string.Empty; 14 | 15 | public required bool BoolValue { get; set; } 16 | 17 | public required byte ByteValue { get; set; } 18 | public required byte[]? ByteArrayValue { get; set; } 19 | public required sbyte SByteValue { get; set; } 20 | public required char CharValue { get; set; } 21 | 22 | public required short ShortValue { get; set; } 23 | public required ushort UShortValue { get; set; } 24 | 25 | public required int IntValue { get; set; } 26 | public required uint UIntValue { get; set; } 27 | 28 | public required long LongValue { get; set; } 29 | public required ulong ULongValue { get; set; } 30 | 31 | public required float FloatValue { get; set; } 32 | public required double DoubleValue { get; set; } 33 | public required decimal DecimalValue { get; set; } 34 | 35 | public required DateTime DateTimeValue { get; set; } 36 | public required DateOnly DateOnlyValue { get; set; } 37 | public required TimeOnly TimeOnlyValue { get; set; } 38 | public required TimeSpan TimeSpanValue { get; set; } 39 | public required DateTimeOffset DateTimeOffsetValue { get; set; } 40 | 41 | public required Guid GuidValue { get; set; } 42 | } 43 | -------------------------------------------------------------------------------- /tests/PhenX.EntityFrameworkCore.BulkInsert.Tests/PhenX.EntityFrameworkCore.BulkInsert.Tests.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | net8.0;net9.0 5 | enable 6 | 7 | false 8 | true 9 | 10 | 11 | 12 | 13 | all 14 | runtime; build; native; contentfiles; analyzers; buildtransitive 15 | 16 | 17 | 18 | 19 | 20 | 21 | all 22 | runtime; build; native; contentfiles; analyzers; buildtransitive 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | -------------------------------------------------------------------------------- /tests/PhenX.EntityFrameworkCore.BulkInsert.Tests/TestHelpers.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.EntityFrameworkCore; 2 | 3 | using PhenX.EntityFrameworkCore.BulkInsert.Enums; 4 | using PhenX.EntityFrameworkCore.BulkInsert.Extensions; 5 | using PhenX.EntityFrameworkCore.BulkInsert.Options; 6 | using PhenX.EntityFrameworkCore.BulkInsert.Tests.DbContext; 7 | 8 | using Xunit; 9 | 10 | namespace PhenX.EntityFrameworkCore.BulkInsert.Tests; 11 | 12 | public enum InsertStrategy 13 | { 14 | Insert, 15 | InsertReturn, 16 | InsertAsync, 17 | InsertReturnAsync 18 | } 19 | 20 | public static class TestHelpers 21 | { 22 | public static async Task> InsertWithStrategyAsync( 23 | this TestDbContextBase dbContext, 24 | InsertStrategy strategy, 25 | List entities, 26 | Action? configure = null, 27 | OnConflictOptions? onConflict = null) 28 | where T : TestEntityBase 29 | { 30 | ProviderType[] returningNotSupported = [ 31 | ProviderType.MySql, 32 | ProviderType.Oracle, 33 | ]; 34 | 35 | Skip.If(strategy is InsertStrategy.InsertReturn or InsertStrategy.InsertReturnAsync && dbContext.IsProvider(returningNotSupported)); 36 | 37 | var runId = Guid.NewGuid(); 38 | if (entities.Any(x => x.TestRun == default)) 39 | { 40 | foreach (var entity in entities) 41 | { 42 | if (entity.TestRun == default) 43 | { 44 | entity.TestRun = runId; 45 | } 46 | } 47 | } 48 | else if (entities.Count > 0) 49 | { 50 | runId = entities[0].TestRun; 51 | } 52 | 53 | var actualConfigure = configure ?? (_ => { }); 54 | try 55 | { 56 | switch (strategy) 57 | { 58 | case InsertStrategy.InsertReturn: 59 | return dbContext.ExecuteBulkInsertReturnEntities(entities, actualConfigure, onConflict); 60 | case InsertStrategy.InsertReturnAsync: 61 | return await dbContext.ExecuteBulkInsertReturnEntitiesAsync(entities, actualConfigure, onConflict); 62 | case InsertStrategy.Insert: 63 | dbContext.ExecuteBulkInsert(entities, actualConfigure, onConflict); 64 | return dbContext.Set().Where(x => x.TestRun == runId).ToList(); 65 | case InsertStrategy.InsertAsync: 66 | await dbContext.ExecuteBulkInsertAsync(entities, actualConfigure, onConflict); 67 | return await dbContext.Set().Where(x => x.TestRun == runId).ToListAsync(); 68 | default: 69 | throw new NotImplementedException(); 70 | } 71 | } 72 | finally 73 | { 74 | dbContext.ChangeTracker.Clear(); 75 | } 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /tests/PhenX.EntityFrameworkCore.BulkInsert.Tests/Tests/Basic/BasicTestsMySql.cs: -------------------------------------------------------------------------------- 1 | using PhenX.EntityFrameworkCore.BulkInsert.Tests.DbContainer; 2 | using PhenX.EntityFrameworkCore.BulkInsert.Tests.DbContext; 3 | 4 | using Xunit; 5 | 6 | namespace PhenX.EntityFrameworkCore.BulkInsert.Tests.Tests.Basic; 7 | 8 | [Trait("Category", "MySql")] 9 | [Collection(TestDbContainerMySqlCollection.Name)] 10 | public class BasicTestsMySql(TestDbContainerMySql dbContainer) : BasicTestsBase(dbContainer) 11 | { 12 | } 13 | -------------------------------------------------------------------------------- /tests/PhenX.EntityFrameworkCore.BulkInsert.Tests/Tests/Basic/BasicTestsOracle.cs: -------------------------------------------------------------------------------- 1 | using PhenX.EntityFrameworkCore.BulkInsert.Tests.DbContainer; 2 | using PhenX.EntityFrameworkCore.BulkInsert.Tests.DbContext; 3 | 4 | using Xunit; 5 | 6 | namespace PhenX.EntityFrameworkCore.BulkInsert.Tests.Tests.Basic; 7 | 8 | [Trait("Category", "Oracle")] 9 | [Collection(TestDbContainerOracleCollection.Name)] 10 | public class BasicTestsOracle(TestDbContainerOracle dbContainer) : BasicTestsBase(dbContainer) 11 | { 12 | } 13 | -------------------------------------------------------------------------------- /tests/PhenX.EntityFrameworkCore.BulkInsert.Tests/Tests/Basic/BasicTestsPostgreSql.cs: -------------------------------------------------------------------------------- 1 | using PhenX.EntityFrameworkCore.BulkInsert.Tests.DbContainer; 2 | using PhenX.EntityFrameworkCore.BulkInsert.Tests.DbContext; 3 | 4 | using Xunit; 5 | 6 | namespace PhenX.EntityFrameworkCore.BulkInsert.Tests.Tests.Basic; 7 | 8 | [Trait("Category", "PostgreSql")] 9 | [Collection(TestDbContainerPostgreSqlCollection.Name)] 10 | public class BasicTestsPostgreSql(TestDbContainerPostgreSql dbContainer) : BasicTestsBase(dbContainer) 11 | { 12 | } 13 | -------------------------------------------------------------------------------- /tests/PhenX.EntityFrameworkCore.BulkInsert.Tests/Tests/Basic/BasicTestsSqlServer.cs: -------------------------------------------------------------------------------- 1 | using PhenX.EntityFrameworkCore.BulkInsert.Tests.DbContainer; 2 | using PhenX.EntityFrameworkCore.BulkInsert.Tests.DbContext; 3 | 4 | using Xunit; 5 | 6 | namespace PhenX.EntityFrameworkCore.BulkInsert.Tests.Tests.Basic; 7 | 8 | [Trait("Category", "SqlServer")] 9 | [Collection(TestDbContainerSqlServerCollection.Name)] 10 | public class BasicTestsSqlServer(TestDbContainerSqlServer dbContainer) : BasicTestsBase(dbContainer) 11 | { 12 | } 13 | -------------------------------------------------------------------------------- /tests/PhenX.EntityFrameworkCore.BulkInsert.Tests/Tests/Basic/BasicTestsSqlite.cs: -------------------------------------------------------------------------------- 1 | using PhenX.EntityFrameworkCore.BulkInsert.Tests.DbContainer; 2 | using PhenX.EntityFrameworkCore.BulkInsert.Tests.DbContext; 3 | 4 | using Xunit; 5 | 6 | namespace PhenX.EntityFrameworkCore.BulkInsert.Tests.Tests.Basic; 7 | 8 | [Trait("Category", "Sqlite")] 9 | [Collection(TestDbContainerSqliteCollection.Name)] 10 | public class BasicTestsSqlite(TestDbContainerSqlite dbContainer) : BasicTestsBase(dbContainer) 11 | { 12 | } 13 | -------------------------------------------------------------------------------- /tests/PhenX.EntityFrameworkCore.BulkInsert.Tests/Tests/Geo/GeoTestsBase.cs: -------------------------------------------------------------------------------- 1 | using FluentAssertions; 2 | 3 | using Microsoft.EntityFrameworkCore; 4 | 5 | using NetTopologySuite.Geometries; 6 | 7 | using PhenX.EntityFrameworkCore.BulkInsert.Tests.DbContainer; 8 | using PhenX.EntityFrameworkCore.BulkInsert.Tests.DbContext; 9 | 10 | using Xunit; 11 | 12 | namespace PhenX.EntityFrameworkCore.BulkInsert.Tests.Tests.Geo; 13 | 14 | public abstract class GeoTestsBase(TestDbContainer dbContainer) : IAsyncLifetime 15 | where TDbContext : TestDbContextGeo, new() 16 | { 17 | private readonly Guid _run = Guid.NewGuid(); 18 | private TDbContext _context = null!; 19 | 20 | public async Task InitializeAsync() 21 | { 22 | _context = await dbContainer.CreateContextAsync("geo"); 23 | } 24 | 25 | public Task DisposeAsync() 26 | { 27 | _context.Dispose(); 28 | return Task.CompletedTask; 29 | } 30 | 31 | [SkippableTheory] 32 | [CombinatorialData] 33 | public async Task InsertEntities_WithGeo(InsertStrategy strategy) 34 | { 35 | // Arrange 36 | var geo1 = new Point(1, 2) { SRID = 4326 }; 37 | var geo2 = new Point(3, 4) { SRID = 4326 }; 38 | 39 | var entities = new List 40 | { 41 | new TestEntityWithGeo { TestRun = _run, GeoObject = geo1 }, 42 | new TestEntityWithGeo { TestRun = _run, GeoObject = geo2 } 43 | }; 44 | 45 | // Act 46 | var insertedEntities = await _context.InsertWithStrategyAsync(strategy, entities); 47 | 48 | // Assert 49 | insertedEntities.Should().BeEquivalentTo(entities, 50 | o => o.RespectingRuntimeTypes().Excluding((TestEntityWithGeo e) => e.Id)); 51 | } 52 | 53 | [SkippableTheory] 54 | [CombinatorialData] 55 | public async Task InsertEntities_WithGeo_And_Default_SRID(InsertStrategy strategy) 56 | { 57 | // Arrange 58 | var geo1 = new Point(1, 2); 59 | var geo2 = new Point(3, 4); 60 | 61 | var entities = new List 62 | { 63 | new TestEntityWithGeo { TestRun = _run, GeoObject = geo1 }, 64 | new TestEntityWithGeo { TestRun = _run, GeoObject = geo2 } 65 | }; 66 | 67 | // Act 68 | var insertedEntities = await _context.InsertWithStrategyAsync(strategy, entities); 69 | 70 | geo1.SRID = 4326; 71 | geo2.SRID = 4326; 72 | 73 | // Assert 74 | insertedEntities.Should().BeEquivalentTo(entities, 75 | o => o.RespectingRuntimeTypes().Excluding((TestEntityWithGeo e) => e.Id)); 76 | } 77 | 78 | [SkippableTheory] 79 | [CombinatorialData] 80 | public async Task InsertEntities_WithGeo_And_Search(InsertStrategy strategy) 81 | { 82 | // Arrange 83 | var runId = Guid.NewGuid(); 84 | 85 | var geo1 = new Point(1, 2) { SRID = 4326 }; 86 | var geo2 = new Point(3, 4) { SRID = 4326 }; 87 | 88 | var entities = new List 89 | { 90 | new TestEntityWithGeo { TestRun = runId, GeoObject = geo1 }, 91 | new TestEntityWithGeo { TestRun = runId, GeoObject = geo2 } 92 | }; 93 | 94 | // Act 95 | await _context.InsertWithStrategyAsync(strategy, entities); 96 | 97 | var found = await _context.TestEntitiesWithGeo.Where(x => x.TestRun == runId && x.GeoObject.Distance(geo1) < 1).ToListAsync(); 98 | 99 | // Assert 100 | Assert.NotEmpty(found); 101 | } 102 | } 103 | -------------------------------------------------------------------------------- /tests/PhenX.EntityFrameworkCore.BulkInsert.Tests/Tests/Geo/GeoTestsMySql.cs: -------------------------------------------------------------------------------- 1 | using PhenX.EntityFrameworkCore.BulkInsert.Tests.DbContainer; 2 | using PhenX.EntityFrameworkCore.BulkInsert.Tests.DbContext; 3 | 4 | using Xunit; 5 | 6 | namespace PhenX.EntityFrameworkCore.BulkInsert.Tests.Tests.Geo; 7 | 8 | [Trait("Category", "MySql")] 9 | [Collection(TestDbContainerMySqlCollection.Name)] 10 | public class GeoTestsMySql(TestDbContainerMySql dbContainer) : GeoTestsBase(dbContainer) 11 | { 12 | } 13 | -------------------------------------------------------------------------------- /tests/PhenX.EntityFrameworkCore.BulkInsert.Tests/Tests/Geo/GeoTestsPostgreSql.cs: -------------------------------------------------------------------------------- 1 | using PhenX.EntityFrameworkCore.BulkInsert.Tests.DbContainer; 2 | using PhenX.EntityFrameworkCore.BulkInsert.Tests.DbContext; 3 | 4 | using Xunit; 5 | 6 | namespace PhenX.EntityFrameworkCore.BulkInsert.Tests.Tests.Geo; 7 | 8 | [Trait("Category", "PostgreSql")] 9 | [Collection(TestDbContainerPostgreSqlCollection.Name)] 10 | public class GeoTestsPostgreSql(TestDbContainerPostgreSql dbContainer) : GeoTestsBase(dbContainer) 11 | { 12 | } -------------------------------------------------------------------------------- /tests/PhenX.EntityFrameworkCore.BulkInsert.Tests/Tests/Geo/GeoTestsSqlServer.cs: -------------------------------------------------------------------------------- 1 | using PhenX.EntityFrameworkCore.BulkInsert.Tests.DbContainer; 2 | using PhenX.EntityFrameworkCore.BulkInsert.Tests.DbContext; 3 | 4 | using Xunit; 5 | 6 | namespace PhenX.EntityFrameworkCore.BulkInsert.Tests.Tests.Geo; 7 | 8 | [Trait("Category", "SqlServer")] 9 | [Collection(TestDbContainerSqlServerCollection.Name)] 10 | public class GeoTestsSqlServer(TestDbContainerSqlServer dbContainer) : GeoTestsBase(dbContainer) 11 | { 12 | } -------------------------------------------------------------------------------- /tests/PhenX.EntityFrameworkCore.BulkInsert.Tests/Tests/Merge/MergeTestsMySql.cs: -------------------------------------------------------------------------------- 1 | using PhenX.EntityFrameworkCore.BulkInsert.Tests.DbContainer; 2 | using PhenX.EntityFrameworkCore.BulkInsert.Tests.DbContext; 3 | 4 | using Xunit; 5 | 6 | namespace PhenX.EntityFrameworkCore.BulkInsert.Tests.Tests.Merge; 7 | 8 | [Trait("Category", "MySql")] 9 | [Collection(TestDbContainerMySqlCollection.Name)] 10 | public class MergeTestsMySql(TestDbContainerMySql dbContainer) : MergeTestsBase(dbContainer) 11 | { 12 | } 13 | -------------------------------------------------------------------------------- /tests/PhenX.EntityFrameworkCore.BulkInsert.Tests/Tests/Merge/MergeTestsPostgreSql.cs: -------------------------------------------------------------------------------- 1 | using PhenX.EntityFrameworkCore.BulkInsert.Tests.DbContainer; 2 | using PhenX.EntityFrameworkCore.BulkInsert.Tests.DbContext; 3 | 4 | using Xunit; 5 | 6 | namespace PhenX.EntityFrameworkCore.BulkInsert.Tests.Tests.Merge; 7 | 8 | [Trait("Category", "PostgreSql")] 9 | [Collection(TestDbContainerPostgreSqlCollection.Name)] 10 | public class MergeTestsPostgreSql(TestDbContainerPostgreSql dbContainer) : MergeTestsBase(dbContainer) 11 | { 12 | } 13 | -------------------------------------------------------------------------------- /tests/PhenX.EntityFrameworkCore.BulkInsert.Tests/Tests/Merge/MergeTestsSqlServer.cs: -------------------------------------------------------------------------------- 1 | using PhenX.EntityFrameworkCore.BulkInsert.Tests.DbContainer; 2 | using PhenX.EntityFrameworkCore.BulkInsert.Tests.DbContext; 3 | 4 | using Xunit; 5 | 6 | namespace PhenX.EntityFrameworkCore.BulkInsert.Tests.Tests.Merge; 7 | 8 | [Trait("Category", "SqlServer")] 9 | [Collection(TestDbContainerSqlServerCollection.Name)] 10 | public class MergeTestsSqlServer(TestDbContainerSqlServer dbContainer) : MergeTestsBase(dbContainer) 11 | { 12 | } 13 | -------------------------------------------------------------------------------- /tests/PhenX.EntityFrameworkCore.BulkInsert.Tests/Tests/Merge/MergeTestsSqlite.cs: -------------------------------------------------------------------------------- 1 | using PhenX.EntityFrameworkCore.BulkInsert.Tests.DbContainer; 2 | using PhenX.EntityFrameworkCore.BulkInsert.Tests.DbContext; 3 | 4 | using Xunit; 5 | 6 | namespace PhenX.EntityFrameworkCore.BulkInsert.Tests.Tests.Merge; 7 | 8 | [Trait("Category", "Sqlite")] 9 | [Collection(TestDbContainerSqliteCollection.Name)] 10 | public class MergeTestsSqlite(TestDbContainerSqlite dbContainer) : MergeTestsBase(dbContainer) 11 | { 12 | } 13 | --------------------------------------------------------------------------------