From 83f5a6764839709b832c47f1ddfc0629c06a799d Mon Sep 17 00:00:00 2001
From: Bart Koelman <10324372+bkoelman@users.noreply.github.com>
Date: Fri, 27 Oct 2023 03:52:01 +0200
Subject: [PATCH 1/7] Add example that produces SQL without Entity Framework
Core (#1361)
---
Directory.Build.props | 1 +
JsonApiDotNetCore.sln | 30 +
JsonApiDotNetCore.sln.DotSettings | 4 +
docs/getting-started/faq.md | 14 +-
.../AtomicOperations/AmbientTransaction.cs | 61 +
.../AmbientTransactionFactory.cs | 78 ++
.../Controllers/OperationsController.cs | 16 +
.../DapperExample/DapperExample.csproj | 19 +
.../DapperExample/Data/AppDbContext.cs | 81 ++
.../DapperExample/Data/RotatingList.cs | 35 +
src/Examples/DapperExample/Data/Seeder.cs | 94 ++
.../DapperExample/DatabaseProvider.cs | 11 +
.../Definitions/TodoItemDefinition.cs | 53 +
.../FromEntitiesNavigationResolver.cs | 46 +
.../DapperExample/Models/AccountRecovery.cs | 19 +
.../DapperExample/Models/LoginAccount.cs | 21 +
src/Examples/DapperExample/Models/Person.cs | 31 +
src/Examples/DapperExample/Models/RgbColor.cs | 55 +
src/Examples/DapperExample/Models/Tag.cs | 21 +
src/Examples/DapperExample/Models/TodoItem.cs | 36 +
.../DapperExample/Models/TodoItemPriority.cs | 11 +
src/Examples/DapperExample/Program.cs | 115 ++
.../DapperExample/Properties/AssemblyInfo.cs | 3 +
.../Properties/launchSettings.json | 30 +
.../CommandDefinitionExtensions.cs | 22 +
.../Repositories/DapperFacade.cs | 192 +++
.../Repositories/DapperRepository.cs | 582 ++++++++
.../Repositories/ResourceChangeDetector.cs | 219 +++
.../Repositories/ResultSetMapper.cs | 197 +++
.../Repositories/SqlCaptureStore.cs | 26 +
.../DeleteOneToOneStatementBuilder.cs | 37 +
.../DeleteResourceStatementBuilder.cs | 37 +
.../Builders/InsertStatementBuilder.cs | 55 +
.../TranslationToSql/Builders/SelectShape.cs | 22 +
.../Builders/SelectStatementBuilder.cs | 786 +++++++++++
.../Builders/SqlQueryBuilder.cs | 505 +++++++
.../Builders/StatementBuilder.cs | 33 +
.../UpdateClearOneToOneStatementBuilder.cs | 47 +
.../UpdateResourceStatementBuilder.cs | 55 +
.../DataModel/BaseDataModelService.cs | 175 +++
.../DataModel/FromEntitiesDataModelService.cs | 145 ++
.../DataModel/IDataModelService.cs | 24 +
.../DataModel/RelationshipForeignKey.cs | 69 +
.../Generators/ParameterGenerator.cs | 30 +
.../Generators/TableAliasGenerator.cs | 12 +
.../Generators/UniqueNameGenerator.cs | 26 +
.../TranslationToSql/ParameterFormatter.cs | 67 +
.../TranslationToSql/SqlCommand.cs | 23 +
.../TranslationToSql/SqlTreeNodeVisitor.cs | 151 ++
.../ColumnSelectorUsageCollector.cs | 163 +++
.../Transformations/ColumnVisitMode.cs | 14 +
.../Transformations/LogicalCombinator.cs | 58 +
.../StaleColumnReferenceRewriter.cs | 307 ++++
.../UnusedSelectorsRewriter.cs | 219 +++
.../TreeNodes/ColumnAssignmentNode.cs | 31 +
.../TreeNodes/ColumnInSelectNode.cs | 41 +
.../TreeNodes/ColumnInTableNode.cs | 22 +
.../TranslationToSql/TreeNodes/ColumnNode.cs | 33 +
.../TreeNodes/ColumnSelectorNode.cs | 31 +
.../TranslationToSql/TreeNodes/ColumnType.cs | 17 +
.../TreeNodes/ComparisonNode.cs | 31 +
.../TranslationToSql/TreeNodes/CountNode.cs | 28 +
.../TreeNodes/CountSelectorNode.cs | 22 +
.../TranslationToSql/TreeNodes/DeleteNode.cs | 28 +
.../TranslationToSql/TreeNodes/ExistsNode.cs | 28 +
.../TranslationToSql/TreeNodes/FilterNode.cs | 8 +
.../TranslationToSql/TreeNodes/FromNode.cs | 19 +
.../TranslationToSql/TreeNodes/InNode.cs | 28 +
.../TranslationToSql/TreeNodes/InsertNode.cs | 28 +
.../TranslationToSql/TreeNodes/JoinNode.cs | 31 +
.../TranslationToSql/TreeNodes/JoinType.cs | 7 +
.../TranslationToSql/TreeNodes/LikeNode.cs | 31 +
.../TranslationToSql/TreeNodes/LogicalNode.cs | 38 +
.../TranslationToSql/TreeNodes/NotNode.cs | 25 +
.../TreeNodes/NullConstantNode.cs | 18 +
.../TreeNodes/OneSelectorNode.cs | 22 +
.../TreeNodes/OrderByColumnNode.cs | 29 +
.../TreeNodes/OrderByCountNode.cs | 29 +
.../TranslationToSql/TreeNodes/OrderByNode.cs | 25 +
.../TreeNodes/OrderByTermNode.cs | 14 +
.../TreeNodes/ParameterNode.cs | 39 +
.../TranslationToSql/TreeNodes/SelectNode.cs | 70 +
.../TreeNodes/SelectorNode.cs | 14 +
.../TranslationToSql/TreeNodes/SqlTreeNode.cs | 18 +
.../TreeNodes/SqlValueNode.cs | 8 +
.../TreeNodes/TableAccessorNode.cs | 18 +
.../TranslationToSql/TreeNodes/TableNode.cs | 63 +
.../TreeNodes/TableSourceNode.cs | 38 +
.../TranslationToSql/TreeNodes/UpdateNode.cs | 31 +
.../TranslationToSql/TreeNodes/WhereNode.cs | 25 +
src/Examples/DapperExample/appsettings.json | 24 +
.../JsonApiDotNetCoreExample/Program.cs | 16 +-
.../Properties/AssemblyInfo.cs | 1 +
.../Resources/Annotations/HasManyAttribute.cs | 22 +
.../Properties/AssemblyInfo.cs | 1 +
.../Queries/FieldSelectors.cs | 4 +-
.../Queries/QueryLayerComposer.cs | 5 +
.../QueryLayerIncludeConverter.cs | 21 +-
.../QueryableBuilding/QueryableBuilder.cs | 2 +-
.../QueryableBuilding/SelectClauseBuilder.cs | 3 +-
test/DapperTests/DapperTests.csproj | 17 +
.../AtomicOperations/AtomicOperationsTests.cs | 522 +++++++
.../IntegrationTests/DapperTestContext.cs | 163 +++
.../QueryStrings/FilterTests.cs | 1244 +++++++++++++++++
.../QueryStrings/IncludeTests.cs | 234 ++++
.../QueryStrings/PaginationTests.cs | 52 +
.../QueryStrings/SortTests.cs | 410 ++++++
.../QueryStrings/SparseFieldSets.cs | 393 ++++++
.../AddToToManyRelationshipTests.cs | 93 ++
.../Relationships/FetchRelationshipTests.cs | 191 +++
.../RemoveFromToManyRelationshipTests.cs | 220 +++
.../ReplaceToManyRelationshipTests.cs | 402 ++++++
.../UpdateToOneRelationshipTests.cs | 1140 +++++++++++++++
.../Resources/CreateResourceTests.cs | 732 ++++++++++
.../Resources/DeleteResourceTests.cs | 123 ++
.../ReadWrite/Resources/FetchResourceTests.cs | 337 +++++
.../Resources/UpdateResourceTests.cs | 399 ++++++
.../Sql/SubQueryInJoinTests.cs | 602 ++++++++
.../IntegrationTests/SqlTextAdapter.cs | 44 +
.../IntegrationTests/TestFakers.cs | 61 +
.../UnitTests/LogicalCombinatorTests.cs | 49 +
.../DapperTests/UnitTests/LogicalNodeTests.cs | 22 +
.../UnitTests/ParameterNodeTests.cs | 45 +
.../UnitTests/RelationshipForeignKeyTests.cs | 54 +
.../UnitTests/SqlTreeNodeVisitorTests.cs | 45 +
.../PaginationWithTotalCountTests.cs | 2 +-
.../FieldChainPatternInheritanceMatchTests.cs | 2 +-
.../FieldChainPatternMatchTests.cs | 2 +-
.../ResourceGraph/HasManyAttributeTests.cs | 134 +-
.../CollectionExtensions.cs | 12 +
.../TestBuildingBlocks/XUnitLoggerProvider.cs | 49 +-
131 files changed, 14001 insertions(+), 64 deletions(-)
create mode 100644 src/Examples/DapperExample/AtomicOperations/AmbientTransaction.cs
create mode 100644 src/Examples/DapperExample/AtomicOperations/AmbientTransactionFactory.cs
create mode 100644 src/Examples/DapperExample/Controllers/OperationsController.cs
create mode 100644 src/Examples/DapperExample/DapperExample.csproj
create mode 100644 src/Examples/DapperExample/Data/AppDbContext.cs
create mode 100644 src/Examples/DapperExample/Data/RotatingList.cs
create mode 100644 src/Examples/DapperExample/Data/Seeder.cs
create mode 100644 src/Examples/DapperExample/DatabaseProvider.cs
create mode 100644 src/Examples/DapperExample/Definitions/TodoItemDefinition.cs
create mode 100644 src/Examples/DapperExample/FromEntitiesNavigationResolver.cs
create mode 100644 src/Examples/DapperExample/Models/AccountRecovery.cs
create mode 100644 src/Examples/DapperExample/Models/LoginAccount.cs
create mode 100644 src/Examples/DapperExample/Models/Person.cs
create mode 100644 src/Examples/DapperExample/Models/RgbColor.cs
create mode 100644 src/Examples/DapperExample/Models/Tag.cs
create mode 100644 src/Examples/DapperExample/Models/TodoItem.cs
create mode 100644 src/Examples/DapperExample/Models/TodoItemPriority.cs
create mode 100644 src/Examples/DapperExample/Program.cs
create mode 100644 src/Examples/DapperExample/Properties/AssemblyInfo.cs
create mode 100644 src/Examples/DapperExample/Properties/launchSettings.json
create mode 100644 src/Examples/DapperExample/Repositories/CommandDefinitionExtensions.cs
create mode 100644 src/Examples/DapperExample/Repositories/DapperFacade.cs
create mode 100644 src/Examples/DapperExample/Repositories/DapperRepository.cs
create mode 100644 src/Examples/DapperExample/Repositories/ResourceChangeDetector.cs
create mode 100644 src/Examples/DapperExample/Repositories/ResultSetMapper.cs
create mode 100644 src/Examples/DapperExample/Repositories/SqlCaptureStore.cs
create mode 100644 src/Examples/DapperExample/TranslationToSql/Builders/DeleteOneToOneStatementBuilder.cs
create mode 100644 src/Examples/DapperExample/TranslationToSql/Builders/DeleteResourceStatementBuilder.cs
create mode 100644 src/Examples/DapperExample/TranslationToSql/Builders/InsertStatementBuilder.cs
create mode 100644 src/Examples/DapperExample/TranslationToSql/Builders/SelectShape.cs
create mode 100644 src/Examples/DapperExample/TranslationToSql/Builders/SelectStatementBuilder.cs
create mode 100644 src/Examples/DapperExample/TranslationToSql/Builders/SqlQueryBuilder.cs
create mode 100644 src/Examples/DapperExample/TranslationToSql/Builders/StatementBuilder.cs
create mode 100644 src/Examples/DapperExample/TranslationToSql/Builders/UpdateClearOneToOneStatementBuilder.cs
create mode 100644 src/Examples/DapperExample/TranslationToSql/Builders/UpdateResourceStatementBuilder.cs
create mode 100644 src/Examples/DapperExample/TranslationToSql/DataModel/BaseDataModelService.cs
create mode 100644 src/Examples/DapperExample/TranslationToSql/DataModel/FromEntitiesDataModelService.cs
create mode 100644 src/Examples/DapperExample/TranslationToSql/DataModel/IDataModelService.cs
create mode 100644 src/Examples/DapperExample/TranslationToSql/DataModel/RelationshipForeignKey.cs
create mode 100644 src/Examples/DapperExample/TranslationToSql/Generators/ParameterGenerator.cs
create mode 100644 src/Examples/DapperExample/TranslationToSql/Generators/TableAliasGenerator.cs
create mode 100644 src/Examples/DapperExample/TranslationToSql/Generators/UniqueNameGenerator.cs
create mode 100644 src/Examples/DapperExample/TranslationToSql/ParameterFormatter.cs
create mode 100644 src/Examples/DapperExample/TranslationToSql/SqlCommand.cs
create mode 100644 src/Examples/DapperExample/TranslationToSql/SqlTreeNodeVisitor.cs
create mode 100644 src/Examples/DapperExample/TranslationToSql/Transformations/ColumnSelectorUsageCollector.cs
create mode 100644 src/Examples/DapperExample/TranslationToSql/Transformations/ColumnVisitMode.cs
create mode 100644 src/Examples/DapperExample/TranslationToSql/Transformations/LogicalCombinator.cs
create mode 100644 src/Examples/DapperExample/TranslationToSql/Transformations/StaleColumnReferenceRewriter.cs
create mode 100644 src/Examples/DapperExample/TranslationToSql/Transformations/UnusedSelectorsRewriter.cs
create mode 100644 src/Examples/DapperExample/TranslationToSql/TreeNodes/ColumnAssignmentNode.cs
create mode 100644 src/Examples/DapperExample/TranslationToSql/TreeNodes/ColumnInSelectNode.cs
create mode 100644 src/Examples/DapperExample/TranslationToSql/TreeNodes/ColumnInTableNode.cs
create mode 100644 src/Examples/DapperExample/TranslationToSql/TreeNodes/ColumnNode.cs
create mode 100644 src/Examples/DapperExample/TranslationToSql/TreeNodes/ColumnSelectorNode.cs
create mode 100644 src/Examples/DapperExample/TranslationToSql/TreeNodes/ColumnType.cs
create mode 100644 src/Examples/DapperExample/TranslationToSql/TreeNodes/ComparisonNode.cs
create mode 100644 src/Examples/DapperExample/TranslationToSql/TreeNodes/CountNode.cs
create mode 100644 src/Examples/DapperExample/TranslationToSql/TreeNodes/CountSelectorNode.cs
create mode 100644 src/Examples/DapperExample/TranslationToSql/TreeNodes/DeleteNode.cs
create mode 100644 src/Examples/DapperExample/TranslationToSql/TreeNodes/ExistsNode.cs
create mode 100644 src/Examples/DapperExample/TranslationToSql/TreeNodes/FilterNode.cs
create mode 100644 src/Examples/DapperExample/TranslationToSql/TreeNodes/FromNode.cs
create mode 100644 src/Examples/DapperExample/TranslationToSql/TreeNodes/InNode.cs
create mode 100644 src/Examples/DapperExample/TranslationToSql/TreeNodes/InsertNode.cs
create mode 100644 src/Examples/DapperExample/TranslationToSql/TreeNodes/JoinNode.cs
create mode 100644 src/Examples/DapperExample/TranslationToSql/TreeNodes/JoinType.cs
create mode 100644 src/Examples/DapperExample/TranslationToSql/TreeNodes/LikeNode.cs
create mode 100644 src/Examples/DapperExample/TranslationToSql/TreeNodes/LogicalNode.cs
create mode 100644 src/Examples/DapperExample/TranslationToSql/TreeNodes/NotNode.cs
create mode 100644 src/Examples/DapperExample/TranslationToSql/TreeNodes/NullConstantNode.cs
create mode 100644 src/Examples/DapperExample/TranslationToSql/TreeNodes/OneSelectorNode.cs
create mode 100644 src/Examples/DapperExample/TranslationToSql/TreeNodes/OrderByColumnNode.cs
create mode 100644 src/Examples/DapperExample/TranslationToSql/TreeNodes/OrderByCountNode.cs
create mode 100644 src/Examples/DapperExample/TranslationToSql/TreeNodes/OrderByNode.cs
create mode 100644 src/Examples/DapperExample/TranslationToSql/TreeNodes/OrderByTermNode.cs
create mode 100644 src/Examples/DapperExample/TranslationToSql/TreeNodes/ParameterNode.cs
create mode 100644 src/Examples/DapperExample/TranslationToSql/TreeNodes/SelectNode.cs
create mode 100644 src/Examples/DapperExample/TranslationToSql/TreeNodes/SelectorNode.cs
create mode 100644 src/Examples/DapperExample/TranslationToSql/TreeNodes/SqlTreeNode.cs
create mode 100644 src/Examples/DapperExample/TranslationToSql/TreeNodes/SqlValueNode.cs
create mode 100644 src/Examples/DapperExample/TranslationToSql/TreeNodes/TableAccessorNode.cs
create mode 100644 src/Examples/DapperExample/TranslationToSql/TreeNodes/TableNode.cs
create mode 100644 src/Examples/DapperExample/TranslationToSql/TreeNodes/TableSourceNode.cs
create mode 100644 src/Examples/DapperExample/TranslationToSql/TreeNodes/UpdateNode.cs
create mode 100644 src/Examples/DapperExample/TranslationToSql/TreeNodes/WhereNode.cs
create mode 100644 src/Examples/DapperExample/appsettings.json
rename src/{Examples/NoEntityFrameworkExample => JsonApiDotNetCore/Queries/QueryableBuilding}/QueryLayerIncludeConverter.cs (79%)
create mode 100644 test/DapperTests/DapperTests.csproj
create mode 100644 test/DapperTests/IntegrationTests/AtomicOperations/AtomicOperationsTests.cs
create mode 100644 test/DapperTests/IntegrationTests/DapperTestContext.cs
create mode 100644 test/DapperTests/IntegrationTests/QueryStrings/FilterTests.cs
create mode 100644 test/DapperTests/IntegrationTests/QueryStrings/IncludeTests.cs
create mode 100644 test/DapperTests/IntegrationTests/QueryStrings/PaginationTests.cs
create mode 100644 test/DapperTests/IntegrationTests/QueryStrings/SortTests.cs
create mode 100644 test/DapperTests/IntegrationTests/QueryStrings/SparseFieldSets.cs
create mode 100644 test/DapperTests/IntegrationTests/ReadWrite/Relationships/AddToToManyRelationshipTests.cs
create mode 100644 test/DapperTests/IntegrationTests/ReadWrite/Relationships/FetchRelationshipTests.cs
create mode 100644 test/DapperTests/IntegrationTests/ReadWrite/Relationships/RemoveFromToManyRelationshipTests.cs
create mode 100644 test/DapperTests/IntegrationTests/ReadWrite/Relationships/ReplaceToManyRelationshipTests.cs
create mode 100644 test/DapperTests/IntegrationTests/ReadWrite/Relationships/UpdateToOneRelationshipTests.cs
create mode 100644 test/DapperTests/IntegrationTests/ReadWrite/Resources/CreateResourceTests.cs
create mode 100644 test/DapperTests/IntegrationTests/ReadWrite/Resources/DeleteResourceTests.cs
create mode 100644 test/DapperTests/IntegrationTests/ReadWrite/Resources/FetchResourceTests.cs
create mode 100644 test/DapperTests/IntegrationTests/ReadWrite/Resources/UpdateResourceTests.cs
create mode 100644 test/DapperTests/IntegrationTests/Sql/SubQueryInJoinTests.cs
create mode 100644 test/DapperTests/IntegrationTests/SqlTextAdapter.cs
create mode 100644 test/DapperTests/IntegrationTests/TestFakers.cs
create mode 100644 test/DapperTests/UnitTests/LogicalCombinatorTests.cs
create mode 100644 test/DapperTests/UnitTests/LogicalNodeTests.cs
create mode 100644 test/DapperTests/UnitTests/ParameterNodeTests.cs
create mode 100644 test/DapperTests/UnitTests/RelationshipForeignKeyTests.cs
create mode 100644 test/DapperTests/UnitTests/SqlTreeNodeVisitorTests.cs
create mode 100644 test/TestBuildingBlocks/CollectionExtensions.cs
diff --git a/Directory.Build.props b/Directory.Build.props
index 90de08a271..14cee715e8 100644
--- a/Directory.Build.props
+++ b/Directory.Build.props
@@ -28,6 +28,7 @@
3.8.*
4.7.*
6.0.*
+ 2.1.*
2.1.*
7.0.*
6.12.*
diff --git a/JsonApiDotNetCore.sln b/JsonApiDotNetCore.sln
index e10df8567a..4f8bd6f8ef 100644
--- a/JsonApiDotNetCore.sln
+++ b/JsonApiDotNetCore.sln
@@ -56,6 +56,10 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "DatabasePerTenantExample",
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "AnnotationTests", "test\AnnotationTests\AnnotationTests.csproj", "{24B0C12F-38CD-4245-8785-87BEFAD55B00}"
EndProject
+Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "DapperExample", "src\Examples\DapperExample\DapperExample.csproj", "{C1774117-5073-4DF8-B5BE-BF7B538BD1C2}"
+EndProject
+Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "DapperTests", "test\DapperTests\DapperTests.csproj", "{80E322F5-5F5D-4670-A30F-02D33C2C7900}"
+EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
@@ -282,6 +286,30 @@ Global
{24B0C12F-38CD-4245-8785-87BEFAD55B00}.Release|x64.Build.0 = Release|Any CPU
{24B0C12F-38CD-4245-8785-87BEFAD55B00}.Release|x86.ActiveCfg = Release|Any CPU
{24B0C12F-38CD-4245-8785-87BEFAD55B00}.Release|x86.Build.0 = Release|Any CPU
+ {C1774117-5073-4DF8-B5BE-BF7B538BD1C2}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {C1774117-5073-4DF8-B5BE-BF7B538BD1C2}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {C1774117-5073-4DF8-B5BE-BF7B538BD1C2}.Debug|x64.ActiveCfg = Debug|Any CPU
+ {C1774117-5073-4DF8-B5BE-BF7B538BD1C2}.Debug|x64.Build.0 = Debug|Any CPU
+ {C1774117-5073-4DF8-B5BE-BF7B538BD1C2}.Debug|x86.ActiveCfg = Debug|Any CPU
+ {C1774117-5073-4DF8-B5BE-BF7B538BD1C2}.Debug|x86.Build.0 = Debug|Any CPU
+ {C1774117-5073-4DF8-B5BE-BF7B538BD1C2}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {C1774117-5073-4DF8-B5BE-BF7B538BD1C2}.Release|Any CPU.Build.0 = Release|Any CPU
+ {C1774117-5073-4DF8-B5BE-BF7B538BD1C2}.Release|x64.ActiveCfg = Release|Any CPU
+ {C1774117-5073-4DF8-B5BE-BF7B538BD1C2}.Release|x64.Build.0 = Release|Any CPU
+ {C1774117-5073-4DF8-B5BE-BF7B538BD1C2}.Release|x86.ActiveCfg = Release|Any CPU
+ {C1774117-5073-4DF8-B5BE-BF7B538BD1C2}.Release|x86.Build.0 = Release|Any CPU
+ {80E322F5-5F5D-4670-A30F-02D33C2C7900}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {80E322F5-5F5D-4670-A30F-02D33C2C7900}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {80E322F5-5F5D-4670-A30F-02D33C2C7900}.Debug|x64.ActiveCfg = Debug|Any CPU
+ {80E322F5-5F5D-4670-A30F-02D33C2C7900}.Debug|x64.Build.0 = Debug|Any CPU
+ {80E322F5-5F5D-4670-A30F-02D33C2C7900}.Debug|x86.ActiveCfg = Debug|Any CPU
+ {80E322F5-5F5D-4670-A30F-02D33C2C7900}.Debug|x86.Build.0 = Debug|Any CPU
+ {80E322F5-5F5D-4670-A30F-02D33C2C7900}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {80E322F5-5F5D-4670-A30F-02D33C2C7900}.Release|Any CPU.Build.0 = Release|Any CPU
+ {80E322F5-5F5D-4670-A30F-02D33C2C7900}.Release|x64.ActiveCfg = Release|Any CPU
+ {80E322F5-5F5D-4670-A30F-02D33C2C7900}.Release|x64.Build.0 = Release|Any CPU
+ {80E322F5-5F5D-4670-A30F-02D33C2C7900}.Release|x86.ActiveCfg = Release|Any CPU
+ {80E322F5-5F5D-4670-A30F-02D33C2C7900}.Release|x86.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
@@ -305,6 +333,8 @@ Global
{83FF097C-C8C6-477B-9FAB-DF99B84978B5} = {7A2B7ADD-ECB5-4D00-AA6A-D45BD11C97CF}
{60334658-BE51-43B3-9C4D-F2BBF56C89CE} = {026FBC6C-AF76-4568-9B87-EC73457899FD}
{24B0C12F-38CD-4245-8785-87BEFAD55B00} = {24B15015-62E5-42E1-9BA0-ECE6BE7AA15F}
+ {C1774117-5073-4DF8-B5BE-BF7B538BD1C2} = {026FBC6C-AF76-4568-9B87-EC73457899FD}
+ {80E322F5-5F5D-4670-A30F-02D33C2C7900} = {24B15015-62E5-42E1-9BA0-ECE6BE7AA15F}
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {A2421882-8F0A-4905-928F-B550B192F9A4}
diff --git a/JsonApiDotNetCore.sln.DotSettings b/JsonApiDotNetCore.sln.DotSettings
index 2602272e97..bf7a5182f0 100644
--- a/JsonApiDotNetCore.sln.DotSettings
+++ b/JsonApiDotNetCore.sln.DotSettings
@@ -659,8 +659,12 @@ $left$ = $right$;
True
True
True
+ True
True
+ True
True
+ True
+ True
True
True
True
diff --git a/docs/getting-started/faq.md b/docs/getting-started/faq.md
index 574ebaf92c..57f1258c24 100644
--- a/docs/getting-started/faq.md
+++ b/docs/getting-started/faq.md
@@ -145,12 +145,18 @@ Take a look at [JsonApiResourceService](https://github.com/json-api-dotnet/JsonA
You'll get a lot more out of the box if replacing at the repository level instead. You don't need to apply options or analyze query strings.
And most resource definition callbacks are handled.
-That's because the built-in resource service translates all JSON:API aspects of the request into a database-agnostic data structure called `QueryLayer`.
+That's because the built-in resource service translates all JSON:API query aspects of the request into a database-agnostic data structure called `QueryLayer`.
Now the hard part for you becomes reading that data structure and producing data access calls from that.
-If your data store provides a LINQ provider, you may reuse most of [QueryableBuilder](https://github.com/json-api-dotnet/JsonApiDotNetCore/blob/master/src/JsonApiDotNetCore/Queries/Internal/QueryableBuilding/QueryableBuilder.cs),
+If your data store provides a LINQ provider, you can probably reuse [QueryableBuilder](https://github.com/json-api-dotnet/JsonApiDotNetCore/blob/master/src/JsonApiDotNetCore/Queries/QueryableBuilding/QueryableBuilder.cs),
which drives the translation into [System.Linq.Expressions](https://learn.microsoft.com/en-us/dotnet/csharp/programming-guide/concepts/expression-trees/).
-Note however, that it also produces calls to `.Include("")`, which is an Entity Framework Core-specific extension method, so you'll likely need to prevent that from happening. There's an example [here](https://github.com/json-api-dotnet/JsonApiDotNetCore/blob/master/src/Examples/NoEntityFrameworkExample/Repositories/InMemoryResourceRepository.cs).
-We use a similar approach for accessing [MongoDB](https://github.com/json-api-dotnet/JsonApiDotNetCore.MongoDb/blob/674889e037334e3f376550178ce12d0842d7560c/src/JsonApiDotNetCore.MongoDb/Queries/Internal/QueryableBuilding/MongoQueryableBuilder.cs).
+Note however, that it also produces calls to `.Include("")`, which is an Entity Framework Core-specific extension method, so you'll need to
+[prevent that from happening](https://github.com/json-api-dotnet/JsonApiDotNetCore/blob/master/src/JsonApiDotNetCore/Queries/QueryableBuilding/QueryLayerIncludeConverter.cs).
+
+The example [here](https://github.com/json-api-dotnet/JsonApiDotNetCore/blob/master/src/Examples/NoEntityFrameworkExample/Repositories/InMemoryResourceRepository.cs) compiles and executes
+the LINQ query against an in-memory list of resources.
+For [MongoDB](https://github.com/json-api-dotnet/JsonApiDotNetCore.MongoDb/blob/master/src/JsonApiDotNetCore.MongoDb/Repositories/MongoRepository.cs), we use the MongoDB LINQ provider.
+If there's no LINQ provider available, the example [here](https://github.com/json-api-dotnet/JsonApiDotNetCore/blob/master/src/Examples/DapperExample/Repositories/DapperRepository.cs) may be of help,
+which produces SQL and uses [Dapper](https://github.com/DapperLib/Dapper) for data access.
> [!TIP]
> [ExpressionTreeVisualizer](https://github.com/zspitz/ExpressionTreeVisualizer) is very helpful in trying to debug LINQ expression trees!
diff --git a/src/Examples/DapperExample/AtomicOperations/AmbientTransaction.cs b/src/Examples/DapperExample/AtomicOperations/AmbientTransaction.cs
new file mode 100644
index 0000000000..c442861bc4
--- /dev/null
+++ b/src/Examples/DapperExample/AtomicOperations/AmbientTransaction.cs
@@ -0,0 +1,61 @@
+using System.Data.Common;
+using JsonApiDotNetCore;
+using JsonApiDotNetCore.AtomicOperations;
+
+namespace DapperExample.AtomicOperations;
+
+///
+/// Represents an ADO.NET transaction in a JSON:API atomic:operations request.
+///
+internal sealed class AmbientTransaction : IOperationsTransaction
+{
+ private readonly AmbientTransactionFactory _owner;
+
+ public DbTransaction Current { get; }
+
+ ///
+ public string TransactionId { get; }
+
+ public AmbientTransaction(AmbientTransactionFactory owner, DbTransaction current, Guid transactionId)
+ {
+ ArgumentGuard.NotNull(owner);
+ ArgumentGuard.NotNull(current);
+
+ _owner = owner;
+ Current = current;
+ TransactionId = transactionId.ToString();
+ }
+
+ ///
+ public Task BeforeProcessOperationAsync(CancellationToken cancellationToken)
+ {
+ return Task.CompletedTask;
+ }
+
+ ///
+ public Task AfterProcessOperationAsync(CancellationToken cancellationToken)
+ {
+ return Task.CompletedTask;
+ }
+
+ ///
+ public Task CommitAsync(CancellationToken cancellationToken)
+ {
+ return Current.CommitAsync(cancellationToken);
+ }
+
+ ///
+ public async ValueTask DisposeAsync()
+ {
+ DbConnection? connection = Current.Connection;
+
+ await Current.DisposeAsync();
+
+ if (connection != null)
+ {
+ await connection.DisposeAsync();
+ }
+
+ _owner.Detach(this);
+ }
+}
diff --git a/src/Examples/DapperExample/AtomicOperations/AmbientTransactionFactory.cs b/src/Examples/DapperExample/AtomicOperations/AmbientTransactionFactory.cs
new file mode 100644
index 0000000000..d10959b79a
--- /dev/null
+++ b/src/Examples/DapperExample/AtomicOperations/AmbientTransactionFactory.cs
@@ -0,0 +1,78 @@
+using System.Data.Common;
+using DapperExample.TranslationToSql.DataModel;
+using JsonApiDotNetCore;
+using JsonApiDotNetCore.AtomicOperations;
+using JsonApiDotNetCore.Configuration;
+
+namespace DapperExample.AtomicOperations;
+
+///
+/// Provides transaction support for JSON:API atomic:operation requests using ADO.NET.
+///
+public sealed class AmbientTransactionFactory : IOperationsTransactionFactory
+{
+ private readonly IJsonApiOptions _options;
+ private readonly IDataModelService _dataModelService;
+
+ internal AmbientTransaction? AmbientTransaction { get; private set; }
+
+ public AmbientTransactionFactory(IJsonApiOptions options, IDataModelService dataModelService)
+ {
+ ArgumentGuard.NotNull(options);
+ ArgumentGuard.NotNull(dataModelService);
+
+ _options = options;
+ _dataModelService = dataModelService;
+ }
+
+ internal async Task BeginTransactionAsync(CancellationToken cancellationToken)
+ {
+ var instance = (IOperationsTransactionFactory)this;
+
+ IOperationsTransaction transaction = await instance.BeginTransactionAsync(cancellationToken);
+ return (AmbientTransaction)transaction;
+ }
+
+ async Task IOperationsTransactionFactory.BeginTransactionAsync(CancellationToken cancellationToken)
+ {
+ if (AmbientTransaction != null)
+ {
+ throw new InvalidOperationException("Cannot start transaction because another transaction is already active.");
+ }
+
+ DbConnection dbConnection = _dataModelService.CreateConnection();
+
+ try
+ {
+ await dbConnection.OpenAsync(cancellationToken);
+
+ DbTransaction transaction = _options.TransactionIsolationLevel != null
+ ? await dbConnection.BeginTransactionAsync(_options.TransactionIsolationLevel.Value, cancellationToken)
+ : await dbConnection.BeginTransactionAsync(cancellationToken);
+
+ var transactionId = Guid.NewGuid();
+ AmbientTransaction = new AmbientTransaction(this, transaction, transactionId);
+
+ return AmbientTransaction;
+ }
+ catch (DbException)
+ {
+ await dbConnection.DisposeAsync();
+ throw;
+ }
+ }
+
+ internal void Detach(AmbientTransaction ambientTransaction)
+ {
+ ArgumentGuard.NotNull(ambientTransaction);
+
+ if (AmbientTransaction != null && AmbientTransaction == ambientTransaction)
+ {
+ AmbientTransaction = null;
+ }
+ else
+ {
+ throw new InvalidOperationException("Failed to detach ambient transaction.");
+ }
+ }
+}
diff --git a/src/Examples/DapperExample/Controllers/OperationsController.cs b/src/Examples/DapperExample/Controllers/OperationsController.cs
new file mode 100644
index 0000000000..979e6c9cd7
--- /dev/null
+++ b/src/Examples/DapperExample/Controllers/OperationsController.cs
@@ -0,0 +1,16 @@
+using JsonApiDotNetCore.AtomicOperations;
+using JsonApiDotNetCore.Configuration;
+using JsonApiDotNetCore.Controllers;
+using JsonApiDotNetCore.Middleware;
+using JsonApiDotNetCore.Resources;
+
+namespace DapperExample.Controllers;
+
+public sealed class OperationsController : JsonApiOperationsController
+{
+ public OperationsController(IJsonApiOptions options, IResourceGraph resourceGraph, ILoggerFactory loggerFactory, IOperationsProcessor processor,
+ IJsonApiRequest request, ITargetedFields targetedFields)
+ : base(options, resourceGraph, loggerFactory, processor, request, targetedFields)
+ {
+ }
+}
diff --git a/src/Examples/DapperExample/DapperExample.csproj b/src/Examples/DapperExample/DapperExample.csproj
new file mode 100644
index 0000000000..4445af8c1e
--- /dev/null
+++ b/src/Examples/DapperExample/DapperExample.csproj
@@ -0,0 +1,19 @@
+
+
+ $(TargetFrameworkName)
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/Examples/DapperExample/Data/AppDbContext.cs b/src/Examples/DapperExample/Data/AppDbContext.cs
new file mode 100644
index 0000000000..ee18bab08e
--- /dev/null
+++ b/src/Examples/DapperExample/Data/AppDbContext.cs
@@ -0,0 +1,81 @@
+using DapperExample.Models;
+using JetBrains.Annotations;
+using JsonApiDotNetCore;
+using Microsoft.EntityFrameworkCore;
+using Microsoft.EntityFrameworkCore.Metadata;
+
+// @formatter:wrap_chained_method_calls chop_always
+
+namespace DapperExample.Data;
+
+[UsedImplicitly(ImplicitUseTargetFlags.Members)]
+public sealed class AppDbContext : DbContext
+{
+ private readonly IConfiguration _configuration;
+
+ public DbSet TodoItems => Set();
+ public DbSet People => Set();
+ public DbSet LoginAccounts => Set();
+ public DbSet AccountRecoveries => Set();
+ public DbSet Tags => Set();
+ public DbSet RgbColors => Set();
+
+ public AppDbContext(DbContextOptions options, IConfiguration configuration)
+ : base(options)
+ {
+ ArgumentGuard.NotNull(configuration);
+
+ _configuration = configuration;
+ }
+
+ protected override void OnModelCreating(ModelBuilder builder)
+ {
+ builder.Entity()
+ .HasMany(person => person.AssignedTodoItems)
+ .WithOne(todoItem => todoItem.Assignee);
+
+ builder.Entity()
+ .HasMany(person => person.OwnedTodoItems)
+ .WithOne(todoItem => todoItem.Owner);
+
+ builder.Entity()
+ .HasOne(person => person.Account)
+ .WithOne(loginAccount => loginAccount.Person)
+ .HasForeignKey("AccountId");
+
+ builder.Entity()
+ .HasOne(loginAccount => loginAccount.Recovery)
+ .WithOne(accountRecovery => accountRecovery.Account)
+ .HasForeignKey("RecoveryId");
+
+ builder.Entity()
+ .HasOne(tag => tag.Color)
+ .WithOne(rgbColor => rgbColor.Tag)
+ .HasForeignKey("TagId");
+
+ var databaseProvider = _configuration.GetValue("DatabaseProvider");
+
+ if (databaseProvider != DatabaseProvider.SqlServer)
+ {
+ // In this example project, all cascades happen in the database, but SQL Server doesn't support that very well.
+ AdjustDeleteBehaviorForJsonApi(builder);
+ }
+ }
+
+ private static void AdjustDeleteBehaviorForJsonApi(ModelBuilder builder)
+ {
+ foreach (IMutableForeignKey foreignKey in builder.Model.GetEntityTypes()
+ .SelectMany(entityType => entityType.GetForeignKeys()))
+ {
+ if (foreignKey.DeleteBehavior == DeleteBehavior.ClientSetNull)
+ {
+ foreignKey.DeleteBehavior = DeleteBehavior.SetNull;
+ }
+
+ if (foreignKey.DeleteBehavior == DeleteBehavior.ClientCascade)
+ {
+ foreignKey.DeleteBehavior = DeleteBehavior.Cascade;
+ }
+ }
+ }
+}
diff --git a/src/Examples/DapperExample/Data/RotatingList.cs b/src/Examples/DapperExample/Data/RotatingList.cs
new file mode 100644
index 0000000000..3fa04762a3
--- /dev/null
+++ b/src/Examples/DapperExample/Data/RotatingList.cs
@@ -0,0 +1,35 @@
+namespace DapperExample.Data;
+
+internal abstract class RotatingList
+{
+ public static RotatingList Create(int count, Func createElement)
+ {
+ List elements = new();
+
+ for (int index = 0; index < count; index++)
+ {
+ T element = createElement(index);
+ elements.Add(element);
+ }
+
+ return new RotatingList(elements);
+ }
+}
+
+internal sealed class RotatingList
+{
+ private int _index = -1;
+
+ public IList Elements { get; }
+
+ public RotatingList(IList elements)
+ {
+ Elements = elements;
+ }
+
+ public T GetNext()
+ {
+ _index++;
+ return Elements[_index % Elements.Count];
+ }
+}
diff --git a/src/Examples/DapperExample/Data/Seeder.cs b/src/Examples/DapperExample/Data/Seeder.cs
new file mode 100644
index 0000000000..eb86eca7e8
--- /dev/null
+++ b/src/Examples/DapperExample/Data/Seeder.cs
@@ -0,0 +1,94 @@
+using DapperExample.Models;
+using JetBrains.Annotations;
+
+namespace DapperExample.Data;
+
+[UsedImplicitly(ImplicitUseKindFlags.InstantiatedNoFixedConstructorSignature)]
+internal sealed class Seeder
+{
+ public static async Task CreateSampleDataAsync(AppDbContext dbContext)
+ {
+ const int todoItemCount = 500;
+ const int personCount = 50;
+ const int accountRecoveryCount = 50;
+ const int loginAccountCount = 50;
+ const int tagCount = 25;
+ const int colorCount = 25;
+
+ RotatingList accountRecoveries = RotatingList.Create(accountRecoveryCount, index => new AccountRecovery
+ {
+ PhoneNumber = $"PhoneNumber{index + 1:D2}",
+ EmailAddress = $"EmailAddress{index + 1:D2}"
+ });
+
+ RotatingList loginAccounts = RotatingList.Create(loginAccountCount, index => new LoginAccount
+ {
+ UserName = $"UserName{index + 1:D2}",
+ Recovery = accountRecoveries.GetNext()
+ });
+
+ RotatingList people = RotatingList.Create(personCount, index =>
+ {
+ var person = new Person
+ {
+ FirstName = $"FirstName{index + 1:D2}",
+ LastName = $"LastName{index + 1:D2}"
+ };
+
+ if (index % 2 == 0)
+ {
+ person.Account = loginAccounts.GetNext();
+ }
+
+ return person;
+ });
+
+ RotatingList colors =
+ RotatingList.Create(colorCount, index => RgbColor.Create((byte)(index % 255), (byte)(index % 255), (byte)(index % 255)));
+
+ RotatingList tags = RotatingList.Create(tagCount, index =>
+ {
+ var tag = new Tag
+ {
+ Name = $"TagName{index + 1:D2}"
+ };
+
+ if (index % 2 == 0)
+ {
+ tag.Color = colors.GetNext();
+ }
+
+ return tag;
+ });
+
+ RotatingList priorities = RotatingList.Create(3, index => (TodoItemPriority)(index + 1));
+
+ RotatingList todoItems = RotatingList.Create(todoItemCount, index =>
+ {
+ var todoItem = new TodoItem
+ {
+ Description = $"TodoItem{index + 1:D3}",
+ Priority = priorities.GetNext(),
+ DurationInHours = index,
+ CreatedAt = DateTimeOffset.UtcNow,
+ Owner = people.GetNext(),
+ Tags = new HashSet
+ {
+ tags.GetNext(),
+ tags.GetNext(),
+ tags.GetNext()
+ }
+ };
+
+ if (index % 3 == 0)
+ {
+ todoItem.Assignee = people.GetNext();
+ }
+
+ return todoItem;
+ });
+
+ dbContext.TodoItems.AddRange(todoItems.Elements);
+ await dbContext.SaveChangesAsync();
+ }
+}
diff --git a/src/Examples/DapperExample/DatabaseProvider.cs b/src/Examples/DapperExample/DatabaseProvider.cs
new file mode 100644
index 0000000000..ea9c293c11
--- /dev/null
+++ b/src/Examples/DapperExample/DatabaseProvider.cs
@@ -0,0 +1,11 @@
+namespace DapperExample;
+
+///
+/// Lists the supported databases.
+///
+public enum DatabaseProvider
+{
+ PostgreSql,
+ MySql,
+ SqlServer
+}
diff --git a/src/Examples/DapperExample/Definitions/TodoItemDefinition.cs b/src/Examples/DapperExample/Definitions/TodoItemDefinition.cs
new file mode 100644
index 0000000000..77bf6f9548
--- /dev/null
+++ b/src/Examples/DapperExample/Definitions/TodoItemDefinition.cs
@@ -0,0 +1,53 @@
+using System.ComponentModel;
+using DapperExample.Models;
+using JetBrains.Annotations;
+using JsonApiDotNetCore;
+using JsonApiDotNetCore.Configuration;
+using JsonApiDotNetCore.Middleware;
+using JsonApiDotNetCore.Queries.Expressions;
+using JsonApiDotNetCore.Resources;
+using Microsoft.AspNetCore.Authentication;
+
+namespace DapperExample.Definitions;
+
+[UsedImplicitly(ImplicitUseKindFlags.InstantiatedNoFixedConstructorSignature)]
+public sealed class TodoItemDefinition : JsonApiResourceDefinition
+{
+ private readonly ISystemClock _systemClock;
+
+ public TodoItemDefinition(IResourceGraph resourceGraph, ISystemClock systemClock)
+ : base(resourceGraph)
+ {
+ ArgumentGuard.NotNull(systemClock);
+
+ _systemClock = systemClock;
+ }
+
+ public override SortExpression OnApplySort(SortExpression? existingSort)
+ {
+ return existingSort ?? GetDefaultSortOrder();
+ }
+
+ private SortExpression GetDefaultSortOrder()
+ {
+ return CreateSortExpressionFromLambda(new PropertySortOrder
+ {
+ (todoItem => todoItem.Priority, ListSortDirection.Ascending),
+ (todoItem => todoItem.LastModifiedAt, ListSortDirection.Descending)
+ });
+ }
+
+ public override Task OnWritingAsync(TodoItem resource, WriteOperationKind writeOperation, CancellationToken cancellationToken)
+ {
+ if (writeOperation == WriteOperationKind.CreateResource)
+ {
+ resource.CreatedAt = _systemClock.UtcNow;
+ }
+ else if (writeOperation == WriteOperationKind.UpdateResource)
+ {
+ resource.LastModifiedAt = _systemClock.UtcNow;
+ }
+
+ return Task.CompletedTask;
+ }
+}
diff --git a/src/Examples/DapperExample/FromEntitiesNavigationResolver.cs b/src/Examples/DapperExample/FromEntitiesNavigationResolver.cs
new file mode 100644
index 0000000000..8ab88473c1
--- /dev/null
+++ b/src/Examples/DapperExample/FromEntitiesNavigationResolver.cs
@@ -0,0 +1,46 @@
+using DapperExample.Data;
+using DapperExample.TranslationToSql.DataModel;
+using JsonApiDotNetCore;
+using JsonApiDotNetCore.Configuration;
+using JsonApiDotNetCore.Repositories;
+using Microsoft.EntityFrameworkCore;
+
+namespace DapperExample;
+
+///
+/// Resolves inverse navigations and initializes from an Entity Framework Core .
+///
+internal sealed class FromEntitiesNavigationResolver : IInverseNavigationResolver
+{
+ private readonly InverseNavigationResolver _defaultResolver;
+ private readonly FromEntitiesDataModelService _dataModelService;
+ private readonly DbContext _appDbContext;
+
+ public FromEntitiesNavigationResolver(IResourceGraph resourceGraph, FromEntitiesDataModelService dataModelService, AppDbContext appDbContext)
+ {
+ ArgumentGuard.NotNull(resourceGraph);
+ ArgumentGuard.NotNull(dataModelService);
+ ArgumentGuard.NotNull(appDbContext);
+
+ _defaultResolver = new InverseNavigationResolver(resourceGraph, new[]
+ {
+ new DbContextResolver(appDbContext)
+ });
+
+ _dataModelService = dataModelService;
+ _appDbContext = appDbContext;
+ }
+
+ public void Resolve()
+ {
+ // In order to produce SQL, some knowledge of the underlying database model is required.
+ // Because the database in this example project is created using Entity Framework Core, we derive that information from its model.
+ // Some alternative approaches to consider:
+ // - Query the database to obtain model information at startup.
+ // - Create a custom attribute that is put on [HasOne/HasMany] resource properties and scan for them at startup.
+ // - Hard-code the required information in the application.
+
+ _defaultResolver.Resolve();
+ _dataModelService.Initialize(_appDbContext);
+ }
+}
diff --git a/src/Examples/DapperExample/Models/AccountRecovery.cs b/src/Examples/DapperExample/Models/AccountRecovery.cs
new file mode 100644
index 0000000000..38410c203c
--- /dev/null
+++ b/src/Examples/DapperExample/Models/AccountRecovery.cs
@@ -0,0 +1,19 @@
+using JetBrains.Annotations;
+using JsonApiDotNetCore.Resources;
+using JsonApiDotNetCore.Resources.Annotations;
+
+namespace DapperExample.Models;
+
+[UsedImplicitly(ImplicitUseTargetFlags.Members)]
+[Resource]
+public sealed class AccountRecovery : Identifiable
+{
+ [Attr]
+ public string? PhoneNumber { get; set; }
+
+ [Attr]
+ public string? EmailAddress { get; set; }
+
+ [HasOne]
+ public LoginAccount Account { get; set; } = null!;
+}
diff --git a/src/Examples/DapperExample/Models/LoginAccount.cs b/src/Examples/DapperExample/Models/LoginAccount.cs
new file mode 100644
index 0000000000..149fc6c7f8
--- /dev/null
+++ b/src/Examples/DapperExample/Models/LoginAccount.cs
@@ -0,0 +1,21 @@
+using JetBrains.Annotations;
+using JsonApiDotNetCore.Resources;
+using JsonApiDotNetCore.Resources.Annotations;
+
+namespace DapperExample.Models;
+
+[UsedImplicitly(ImplicitUseTargetFlags.Members)]
+[Resource]
+public sealed class LoginAccount : Identifiable
+{
+ [Attr]
+ public string UserName { get; set; } = null!;
+
+ public DateTimeOffset? LastUsedAt { get; set; }
+
+ [HasOne]
+ public AccountRecovery Recovery { get; set; } = null!;
+
+ [HasOne]
+ public Person Person { get; set; } = null!;
+}
diff --git a/src/Examples/DapperExample/Models/Person.cs b/src/Examples/DapperExample/Models/Person.cs
new file mode 100644
index 0000000000..1eb4ecadee
--- /dev/null
+++ b/src/Examples/DapperExample/Models/Person.cs
@@ -0,0 +1,31 @@
+using System.ComponentModel.DataAnnotations.Schema;
+using JetBrains.Annotations;
+using JsonApiDotNetCore.Resources;
+using JsonApiDotNetCore.Resources.Annotations;
+
+namespace DapperExample.Models;
+
+[UsedImplicitly(ImplicitUseTargetFlags.Members)]
+[Resource]
+public sealed class Person : Identifiable
+{
+ [Attr]
+ public string? FirstName { get; set; }
+
+ [Attr]
+ public string LastName { get; set; } = null!;
+
+ // Mistakenly includes AllowFilter, so we can test for the error produced.
+ [Attr(Capabilities = AttrCapabilities.AllowView | AttrCapabilities.AllowFilter)]
+ [NotMapped]
+ public string DisplayName => FirstName != null ? $"{FirstName} {LastName}" : LastName;
+
+ [HasOne]
+ public LoginAccount? Account { get; set; }
+
+ [HasMany]
+ public ISet OwnedTodoItems { get; set; } = new HashSet();
+
+ [HasMany]
+ public ISet AssignedTodoItems { get; set; } = new HashSet();
+}
diff --git a/src/Examples/DapperExample/Models/RgbColor.cs b/src/Examples/DapperExample/Models/RgbColor.cs
new file mode 100644
index 0000000000..c29e1b1ba1
--- /dev/null
+++ b/src/Examples/DapperExample/Models/RgbColor.cs
@@ -0,0 +1,55 @@
+using System.ComponentModel.DataAnnotations.Schema;
+using System.Drawing;
+using JetBrains.Annotations;
+using JsonApiDotNetCore.Configuration;
+using JsonApiDotNetCore.Resources;
+using JsonApiDotNetCore.Resources.Annotations;
+
+namespace DapperExample.Models;
+
+[UsedImplicitly(ImplicitUseTargetFlags.Members)]
+[Resource(ClientIdGeneration = ClientIdGenerationMode.Required)]
+public sealed class RgbColor : Identifiable
+{
+ [DatabaseGenerated(DatabaseGeneratedOption.None)]
+ public override int? Id
+ {
+ get => base.Id;
+ set => base.Id = value;
+ }
+
+ [HasOne]
+ public Tag Tag { get; set; } = null!;
+
+ [Attr(Capabilities = AttrCapabilities.AllowView)]
+ [NotMapped]
+ public byte? Red => Id == null ? null : (byte)((Id & 0xFF_0000) >> 16);
+
+ [Attr(Capabilities = AttrCapabilities.AllowView)]
+ [NotMapped]
+ public byte? Green => Id == null ? null : (byte)((Id & 0x00_FF00) >> 8);
+
+ [Attr(Capabilities = AttrCapabilities.AllowView)]
+ [NotMapped]
+ public byte? Blue => Id == null ? null : (byte)(Id & 0x00_00FF);
+
+ public static RgbColor Create(byte red, byte green, byte blue)
+ {
+ Color color = Color.FromArgb(0xFF, red, green, blue);
+
+ return new RgbColor
+ {
+ Id = color.ToArgb() & 0x00FF_FFFF
+ };
+ }
+
+ protected override string? GetStringId(int? value)
+ {
+ return value?.ToString("X6");
+ }
+
+ protected override int? GetTypedId(string? value)
+ {
+ return value == null ? null : Convert.ToInt32(value, 16) & 0xFF_FFFF;
+ }
+}
diff --git a/src/Examples/DapperExample/Models/Tag.cs b/src/Examples/DapperExample/Models/Tag.cs
new file mode 100644
index 0000000000..cb49ff42fb
--- /dev/null
+++ b/src/Examples/DapperExample/Models/Tag.cs
@@ -0,0 +1,21 @@
+using System.ComponentModel.DataAnnotations;
+using JetBrains.Annotations;
+using JsonApiDotNetCore.Resources;
+using JsonApiDotNetCore.Resources.Annotations;
+
+namespace DapperExample.Models;
+
+[UsedImplicitly(ImplicitUseTargetFlags.Members)]
+[Resource]
+public sealed class Tag : Identifiable
+{
+ [Attr]
+ [MinLength(1)]
+ public string Name { get; set; } = null!;
+
+ [HasOne]
+ public RgbColor? Color { get; set; }
+
+ [HasOne]
+ public TodoItem? TodoItem { get; set; }
+}
diff --git a/src/Examples/DapperExample/Models/TodoItem.cs b/src/Examples/DapperExample/Models/TodoItem.cs
new file mode 100644
index 0000000000..d2f3916268
--- /dev/null
+++ b/src/Examples/DapperExample/Models/TodoItem.cs
@@ -0,0 +1,36 @@
+using System.ComponentModel.DataAnnotations;
+using JetBrains.Annotations;
+using JsonApiDotNetCore.Resources;
+using JsonApiDotNetCore.Resources.Annotations;
+
+namespace DapperExample.Models;
+
+[UsedImplicitly(ImplicitUseTargetFlags.Members)]
+[Resource]
+public sealed class TodoItem : Identifiable
+{
+ [Attr]
+ public string Description { get; set; } = null!;
+
+ [Attr]
+ [Required]
+ public TodoItemPriority? Priority { get; set; }
+
+ [Attr]
+ public long? DurationInHours { get; set; }
+
+ [Attr(Capabilities = AttrCapabilities.AllowFilter | AttrCapabilities.AllowSort | AttrCapabilities.AllowView)]
+ public DateTimeOffset CreatedAt { get; set; }
+
+ [Attr(PublicName = "modifiedAt", Capabilities = AttrCapabilities.AllowFilter | AttrCapabilities.AllowSort | AttrCapabilities.AllowView)]
+ public DateTimeOffset? LastModifiedAt { get; set; }
+
+ [HasOne]
+ public Person Owner { get; set; } = null!;
+
+ [HasOne]
+ public Person? Assignee { get; set; }
+
+ [HasMany]
+ public ISet Tags { get; set; } = new HashSet();
+}
diff --git a/src/Examples/DapperExample/Models/TodoItemPriority.cs b/src/Examples/DapperExample/Models/TodoItemPriority.cs
new file mode 100644
index 0000000000..ba10336ec3
--- /dev/null
+++ b/src/Examples/DapperExample/Models/TodoItemPriority.cs
@@ -0,0 +1,11 @@
+using JetBrains.Annotations;
+
+namespace DapperExample.Models;
+
+[UsedImplicitly(ImplicitUseTargetFlags.Members)]
+public enum TodoItemPriority
+{
+ High = 1,
+ Medium = 2,
+ Low = 3
+}
diff --git a/src/Examples/DapperExample/Program.cs b/src/Examples/DapperExample/Program.cs
new file mode 100644
index 0000000000..e19e45478f
--- /dev/null
+++ b/src/Examples/DapperExample/Program.cs
@@ -0,0 +1,115 @@
+using System.Diagnostics;
+using System.Text.Json.Serialization;
+using DapperExample;
+using DapperExample.AtomicOperations;
+using DapperExample.Data;
+using DapperExample.Models;
+using DapperExample.Repositories;
+using DapperExample.TranslationToSql.DataModel;
+using JsonApiDotNetCore.AtomicOperations;
+using JsonApiDotNetCore.Configuration;
+using JsonApiDotNetCore.Repositories;
+using Microsoft.AspNetCore.Authentication;
+using Microsoft.EntityFrameworkCore;
+using Microsoft.EntityFrameworkCore.Diagnostics;
+using Microsoft.Extensions.DependencyInjection.Extensions;
+
+WebApplicationBuilder builder = WebApplication.CreateBuilder(args);
+
+// Add services to the container.
+
+builder.Services.TryAddSingleton();
+
+DatabaseProvider databaseProvider = GetDatabaseProvider(builder.Configuration);
+string? connectionString = builder.Configuration.GetConnectionString($"DapperExample{databaseProvider}");
+
+switch (databaseProvider)
+{
+ case DatabaseProvider.PostgreSql:
+ {
+ builder.Services.AddNpgsql(connectionString, optionsAction: options => SetDbContextDebugOptions(options));
+ break;
+ }
+ case DatabaseProvider.MySql:
+ {
+ builder.Services.AddMySql(connectionString, ServerVersion.AutoDetect(connectionString),
+ optionsAction: options => SetDbContextDebugOptions(options));
+
+ break;
+ }
+ case DatabaseProvider.SqlServer:
+ {
+ builder.Services.AddSqlServer(connectionString, optionsAction: options => SetDbContextDebugOptions(options));
+ break;
+ }
+}
+
+builder.Services.AddScoped(typeof(IResourceRepository<,>), typeof(DapperRepository<,>));
+builder.Services.AddScoped(typeof(IResourceWriteRepository<,>), typeof(DapperRepository<,>));
+builder.Services.AddScoped(typeof(IResourceReadRepository<,>), typeof(DapperRepository<,>));
+
+builder.Services.AddJsonApi(options =>
+{
+ options.UseRelativeLinks = true;
+ options.IncludeTotalResourceCount = true;
+ options.DefaultPageSize = null;
+ options.SerializerOptions.Converters.Add(new JsonStringEnumConverter());
+
+#if DEBUG
+ options.IncludeExceptionStackTraceInErrors = true;
+ options.IncludeRequestBodyInErrors = true;
+ options.SerializerOptions.WriteIndented = true;
+#endif
+}, discovery => discovery.AddCurrentAssembly(), resourceGraphBuilder =>
+{
+ resourceGraphBuilder.Add();
+ resourceGraphBuilder.Add();
+ resourceGraphBuilder.Add();
+ resourceGraphBuilder.Add();
+ resourceGraphBuilder.Add();
+ resourceGraphBuilder.Add();
+});
+
+builder.Services.AddScoped();
+builder.Services.AddSingleton();
+builder.Services.AddSingleton(serviceProvider => serviceProvider.GetRequiredService());
+builder.Services.AddScoped();
+builder.Services.AddScoped(serviceProvider => serviceProvider.GetRequiredService());
+builder.Services.AddScoped();
+
+WebApplication app = builder.Build();
+
+// Configure the HTTP request pipeline.
+
+app.UseRouting();
+app.UseJsonApi();
+app.MapControllers();
+
+await CreateDatabaseAsync(app.Services);
+
+app.Run();
+
+static DatabaseProvider GetDatabaseProvider(IConfiguration configuration)
+{
+ return configuration.GetValue("DatabaseProvider");
+}
+
+[Conditional("DEBUG")]
+static void SetDbContextDebugOptions(DbContextOptionsBuilder options)
+{
+ options.EnableDetailedErrors();
+ options.EnableSensitiveDataLogging();
+ options.ConfigureWarnings(builder => builder.Ignore(CoreEventId.SensitiveDataLoggingEnabledWarning));
+}
+
+static async Task CreateDatabaseAsync(IServiceProvider serviceProvider)
+{
+ await using AsyncServiceScope scope = serviceProvider.CreateAsyncScope();
+
+ var dbContext = scope.ServiceProvider.GetRequiredService();
+
+ if (await dbContext.Database.EnsureCreatedAsync())
+ {
+ await Seeder.CreateSampleDataAsync(dbContext);
+ }
+}
diff --git a/src/Examples/DapperExample/Properties/AssemblyInfo.cs b/src/Examples/DapperExample/Properties/AssemblyInfo.cs
new file mode 100644
index 0000000000..acbcc24f88
--- /dev/null
+++ b/src/Examples/DapperExample/Properties/AssemblyInfo.cs
@@ -0,0 +1,3 @@
+using System.Runtime.CompilerServices;
+
+[assembly: InternalsVisibleTo("DapperTests")]
diff --git a/src/Examples/DapperExample/Properties/launchSettings.json b/src/Examples/DapperExample/Properties/launchSettings.json
new file mode 100644
index 0000000000..137620d860
--- /dev/null
+++ b/src/Examples/DapperExample/Properties/launchSettings.json
@@ -0,0 +1,30 @@
+{
+ "$schema": "http://json.schemastore.org/launchsettings.json",
+ "iisSettings": {
+ "windowsAuthentication": false,
+ "anonymousAuthentication": true,
+ "iisExpress": {
+ "applicationUrl": "http://localhost:14146",
+ "sslPort": 44346
+ }
+ },
+ "profiles": {
+ "IIS Express": {
+ "commandName": "IISExpress",
+ "launchBrowser": true,
+ "launchUrl": "todoItems?include=owner,assignee,tags&filter=equals(priority,'High')&fields[todoItems]=description,durationInHours",
+ "environmentVariables": {
+ "ASPNETCORE_ENVIRONMENT": "Development"
+ }
+ },
+ "Kestrel": {
+ "commandName": "Project",
+ "launchBrowser": true,
+ "launchUrl": "todoItems?include=owner,assignee,tags&filter=equals(priority,'High')&fields[todoItems]=description,durationInHours",
+ "applicationUrl": "https://localhost:44346;http://localhost:14146",
+ "environmentVariables": {
+ "ASPNETCORE_ENVIRONMENT": "Development"
+ }
+ }
+ }
+}
diff --git a/src/Examples/DapperExample/Repositories/CommandDefinitionExtensions.cs b/src/Examples/DapperExample/Repositories/CommandDefinitionExtensions.cs
new file mode 100644
index 0000000000..294e314eba
--- /dev/null
+++ b/src/Examples/DapperExample/Repositories/CommandDefinitionExtensions.cs
@@ -0,0 +1,22 @@
+using System.Data.Common;
+using Dapper;
+using DapperExample.AtomicOperations;
+
+namespace DapperExample.Repositories;
+
+internal static class CommandDefinitionExtensions
+{
+ // SQL Server and MySQL require any active DbTransaction to be explicitly associated to the DbConnection.
+
+ public static CommandDefinition Associate(this CommandDefinition command, DbTransaction transaction)
+ {
+ return new CommandDefinition(command.CommandText, command.Parameters, transaction, cancellationToken: command.CancellationToken);
+ }
+
+ public static CommandDefinition Associate(this CommandDefinition command, AmbientTransaction? transaction)
+ {
+ return transaction != null
+ ? new CommandDefinition(command.CommandText, command.Parameters, transaction.Current, cancellationToken: command.CancellationToken)
+ : command;
+ }
+}
diff --git a/src/Examples/DapperExample/Repositories/DapperFacade.cs b/src/Examples/DapperExample/Repositories/DapperFacade.cs
new file mode 100644
index 0000000000..d3247967f9
--- /dev/null
+++ b/src/Examples/DapperExample/Repositories/DapperFacade.cs
@@ -0,0 +1,192 @@
+using Dapper;
+using DapperExample.TranslationToSql.Builders;
+using DapperExample.TranslationToSql.DataModel;
+using DapperExample.TranslationToSql.TreeNodes;
+using JsonApiDotNetCore;
+using JsonApiDotNetCore.Configuration;
+using JsonApiDotNetCore.Resources.Annotations;
+
+namespace DapperExample.Repositories;
+
+///
+/// Constructs Dapper s from SQL trees and handles order of updates.
+///
+internal sealed class DapperFacade
+{
+ private readonly IDataModelService _dataModelService;
+
+ public DapperFacade(IDataModelService dataModelService)
+ {
+ ArgumentGuard.NotNull(dataModelService);
+
+ _dataModelService = dataModelService;
+ }
+
+ public CommandDefinition GetSqlCommand(SqlTreeNode node, CancellationToken cancellationToken)
+ {
+ ArgumentGuard.NotNull(node);
+
+ var queryBuilder = new SqlQueryBuilder(_dataModelService.DatabaseProvider);
+ string statement = queryBuilder.GetCommand(node);
+ IDictionary parameters = queryBuilder.Parameters;
+
+ return new CommandDefinition(statement, parameters, cancellationToken: cancellationToken);
+ }
+
+ public IReadOnlyCollection BuildSqlCommandsForOneToOneRelationshipsChangedToNotNull(ResourceChangeDetector changeDetector,
+ CancellationToken cancellationToken)
+ {
+ ArgumentGuard.NotNull(changeDetector);
+
+ List sqlCommands = new();
+
+ foreach ((HasOneAttribute relationship, (object? currentRightId, object newRightId)) in changeDetector.GetOneToOneRelationshipsChangedToNotNull())
+ {
+ // To prevent a unique constraint violation on the foreign key, first detach/delete the other row pointing to us, if any.
+ // See https://github.com/json-api-dotnet/JsonApiDotNetCore/issues/502.
+
+ RelationshipForeignKey foreignKey = _dataModelService.GetForeignKey(relationship);
+
+ ResourceType resourceType = foreignKey.IsAtLeftSide ? relationship.LeftType : relationship.RightType;
+ string whereColumnName = foreignKey.IsAtLeftSide ? foreignKey.ColumnName : TableSourceNode.IdColumnName;
+ object? whereValue = foreignKey.IsAtLeftSide ? newRightId : currentRightId;
+
+ if (whereValue == null)
+ {
+ // Creating new resource, so there can't be any existing FKs in other resources that are already pointing to us.
+ continue;
+ }
+
+ if (foreignKey.IsNullable)
+ {
+ var updateBuilder = new UpdateClearOneToOneStatementBuilder(_dataModelService);
+ UpdateNode updateNode = updateBuilder.Build(resourceType, foreignKey.ColumnName, whereColumnName, whereValue);
+ CommandDefinition sqlCommand = GetSqlCommand(updateNode, cancellationToken);
+ sqlCommands.Add(sqlCommand);
+ }
+ else
+ {
+ var deleteBuilder = new DeleteOneToOneStatementBuilder(_dataModelService);
+ DeleteNode deleteNode = deleteBuilder.Build(resourceType, whereColumnName, whereValue);
+ CommandDefinition sqlCommand = GetSqlCommand(deleteNode, cancellationToken);
+ sqlCommands.Add(sqlCommand);
+ }
+ }
+
+ return sqlCommands;
+ }
+
+ public IReadOnlyCollection BuildSqlCommandsForChangedRelationshipsHavingForeignKeyAtRightSide(ResourceChangeDetector changeDetector,
+ TId leftId, CancellationToken cancellationToken)
+ {
+ ArgumentGuard.NotNull(changeDetector);
+
+ List sqlCommands = new();
+
+ foreach ((HasOneAttribute hasOneRelationship, (object? currentRightId, object? newRightId)) in changeDetector
+ .GetChangedToOneRelationshipsWithForeignKeyAtRightSide())
+ {
+ RelationshipForeignKey foreignKey = _dataModelService.GetForeignKey(hasOneRelationship);
+
+ var columnsToUpdate = new Dictionary
+ {
+ [foreignKey.ColumnName] = newRightId == null ? null : leftId
+ };
+
+ var updateBuilder = new UpdateResourceStatementBuilder(_dataModelService);
+ UpdateNode updateNode = updateBuilder.Build(hasOneRelationship.RightType, columnsToUpdate, (newRightId ?? currentRightId)!);
+ CommandDefinition sqlCommand = GetSqlCommand(updateNode, cancellationToken);
+ sqlCommands.Add(sqlCommand);
+ }
+
+ foreach ((HasManyAttribute hasManyRelationship, (ISet